diff --git a/backend/__tests__/unit/config/nativeAgents.scoutKnowledge.test.js b/backend/__tests__/unit/config/nativeAgents.scoutKnowledge.test.js index 64df7fc3d9..8729c4f36f 100644 --- a/backend/__tests__/unit/config/nativeAgents.scoutKnowledge.test.js +++ b/backend/__tests__/unit/config/nativeAgents.scoutKnowledge.test.js @@ -119,13 +119,19 @@ describe('Scout onboarding knowledge', () => { expect(prompt).toMatch(/Do not claim any other tool connects/); }); - // Linux run 38982219348: a Claude Code listener cannot run on Linux, WSL or - // Windows (#2288 refuses it), so a connect card for it there is a dead end. - test('offers a connect card for Claude Code on a Mac only, and says what works elsewhere', () => { - expect(prompt).toMatch(/Claude Code on a Mac; Codex on a Mac \(Linux and WSL not yet tested\)/); + // TASK-027: a Claude Code listener runs on Linux or WSL only in invite-only + // pods, and not at all on native Windows, so a card for it there is a dead end. + // Codex stays Mac only until a Linux walk (#2783). + test('offers a connect card for Claude Code on a Mac, or on Linux in an invite-only pod, and says what works elsewhere', () => { + expect(prompt).toMatch(/Claude Code on a Mac, or on Linux \(WSL included\) in an invite-only pod; Codex on a Mac \(Linux and WSL not yet tested\)/); + // CodeRabbit on #2823: the CLI checks the whole seat, so one open pod stops + // replies in every pod, not just that one. + expect(prompt).toMatch(/For Claude Code on Linux or WSL, say it answers only while every pod it is in is invite-only: if any one of them opens up, it stops answering in all of them until that pod is invite-only again\./); + expect(prompt).not.toMatch(/say it answers only in invite-only pods/); expect(prompt).toMatch(/DeepSeek Harness \(dsh\) through its attach guide, walked on macOS with other OSs unverified/); expect(prompt).not.toMatch(/On Linux \(WSL included\), Codex only/); - expect(prompt).toMatch(/For Claude Code on Linux, WSL or Windows, do not propose a connect card/); + expect(prompt).toMatch(/For Claude Code on native Windows, do not propose a connect card/); + expect(prompt).not.toMatch(/For Claude Code on Linux, WSL or Windows, do not propose a connect card/); expect(prompt).toMatch(/commonly_offer_hosted_agent, and also say they can use the pod from their own Claude Code sessions/); expect(prompt).not.toMatch(/Windows is not verified yet/); }); @@ -153,9 +159,9 @@ describe('Scout onboarding knowledge', () => { // The prompt is the only guard before the card, so the pairs // it may go out for are listed, not inferred. - test('proposes the card only for Claude Code on macOS and Codex on macOS', () => { - expect(prompt).toMatch(/The card may go out for exactly these pairs: Claude Code on macOS; Codex on macOS\./); - expect(prompt).toMatch(/Any other OS, such as Linux, WSL, native Windows, ChromeOS, iPad or iPhone, Android or FreeBSD, gets a text answer and no card/); + test('proposes the card only for Claude Code on macOS, Linux or WSL and Codex on macOS', () => { + expect(prompt).toMatch(/The card may go out for exactly these pairs: Claude Code on macOS, Linux or WSL; Codex on macOS\./); + expect(prompt).toMatch(/Any other pair, such as Codex on Linux or WSL, or any tool on native Windows, ChromeOS, iPad or iPhone, Android or FreeBSD, gets a text answer and no card/); ['ChromeOS', 'iPad', 'Android'].forEach((os) => expect(prompt).toContain(os)); }); @@ -178,10 +184,11 @@ describe('Scout onboarding knowledge', () => { expect(prompt).toMatch(/For any other tool, or an OS where it does not run yet, never propose the card; answer in text\. OpenCode, Hermes and DeepSeek Harness have their own paths above: point them there\. Say plainly that it does not run there yet only for a tool with no path above, or an OS its path does not cover\./); // CodeRabbit on #2765: the no-card rule must not deny tools that do connect. expect(prompt).not.toMatch(/For any other tool, or an OS where it does not run yet, never propose the card; answer in text and say plainly that it does not run there yet/); - expect(prompt).toMatch(/For Claude Code on Linux, WSL or Windows, do not propose a connect card: offer "Run it here" with commonly_offer_hosted_agent, and also say they can use the pod from their own Claude Code sessions with the MCP command on the connect page \(on Windows the page gives it as two PowerShell lines\)/); + expect(prompt).toMatch(/For Claude Code on native Windows, do not propose a connect card: offer "Run it here" with commonly_offer_hosted_agent, and also say they can use the pod from their own Claude Code sessions with the MCP command on the connect page \(on Windows the page gives it as two PowerShell lines\)/); expect(prompt).toMatch(/For Codex on native Windows, do not propose a connect card either: offer "Run it here", or running Codex on a Mac\. Say plainly why/); const en = JSON.parse(fs.readFileSync(path.resolve(__dirname, '../../../../frontend/src/i18n/locales/en.json'), 'utf8')); expect(en.agentByo.listen.unavailableClaude).toMatch(/Connect Claude Code from your own sessions with the command below, or choose “Run it here”/); + expect(en.agentByo.listen.unavailableClaudeLinux).toMatch(/only in an invite-only pod.*connect Claude Code from your own sessions with the command below, or choose “Run it here”/); expect(en.agentByo.listen.unavailableCodex).toMatch(/Choose “Run it here” for a hosted agent, or run the command on a Mac\. Linux is not tested yet\./); }); diff --git a/backend/config/native-agents/scout.ts b/backend/config/native-agents/scout.ts index dc5f6b9bd7..735444d648 100644 --- a/backend/config/native-agents/scout.ts +++ b/backend/config/native-agents/scout.ts @@ -166,15 +166,21 @@ export const scoutApp = { + '--instance && commonly daemon register && commonly daemon install` registers their ' + 'machine once (on commonly.me the API URL is https://api.commonly.me); after that they add agents to it from this page with no ' + 'terminal.\n' - // Linux run 38982219348 (2026-10-16): the sandbox that keeps a connected - // agent to its own folder cannot start Claude Code on Linux or WSL, and - // the CLI refuses to try (#2288). Codex does not run in it. + // TASK-027: on Linux or WSL the CLI runs Claude Code only as a workspace + // seat, which needs every pod it answers in to be invite-only; a public + // Claude Code seat there is refused (Linux run 38982219348, #2288). The + // workspace made at signup and the pods Scout creates are invite-only. + '- Where their own agent can be connected as a teammate that answers ' - + '@mentions: Claude Code on a Mac; Codex on a Mac (Linux and WSL not yet tested); ' + + '@mentions: Claude Code on a Mac, or on Linux (WSL included) in an ' + + 'invite-only pod; Codex on a Mac (Linux and WSL not yet tested); ' + 'DeepSeek Harness (dsh) through its attach guide, walked on macOS with ' + 'other OSs unverified. On native Windows, Claude Code and Codex are not ' - + 'supported; Codex inside WSL counts as Linux. For ' - + 'Claude Code on Linux, WSL or Windows, do not propose a connect card: offer ' + + 'supported; Codex inside WSL counts as Linux. For Claude Code on Linux or ' + + 'WSL, say it answers only while every pod it is in is invite-only: if any ' + + 'one of them opens up, it stops answering in all of them until that pod is ' + + 'invite-only again. Their own workspace and the pods you create are ' + + 'invite-only. For ' + + 'Claude Code on native Windows, do not propose a connect card: offer ' + '"Run it here" with commonly_offer_hosted_agent, and also say they can use the pod ' + 'from their own Claude Code sessions with the MCP command on the connect ' + 'page (on Windows the page gives it as two PowerShell lines). For Codex on ' @@ -227,7 +233,7 @@ export const scoutApp = { // and adapter only). + '- You can SET UP their own agent for them, and the connect card is the ' + 'answer whenever it can work. The card may go out for exactly these pairs: ' - + 'Claude Code on macOS; Codex on macOS. Any other OS, such as Linux, WSL, ' + + 'Claude Code on macOS, Linux or WSL; Codex on macOS. Any other pair, such as Codex on Linux or WSL, or any tool on ' + 'native Windows, ChromeOS, iPad or iPhone, Android or FreeBSD, gets a text ' + 'answer and no card. The tool must be the command-line one they run in a ' + 'terminal: the Claude app (desktop or phone) and claude.ai are not Claude ' diff --git a/docs-site/bring-your-own/claude-code.mdx b/docs-site/bring-your-own/claude-code.mdx index 81c143b259..6a90507814 100644 --- a/docs-site/bring-your-own/claude-code.mdx +++ b/docs-site/bring-your-own/claude-code.mdx @@ -1,16 +1,16 @@ --- title: Connect Claude Code to a pod sidebarTitle: Claude Code -description: Run a Claude Code listener on your Mac that answers @mentions in a Commonly pod and uses Commonly tools. Needs Node.js 20 or later. +description: Run a Claude Code listener on your Mac, or on Linux in invite-only pods, that answers @mentions in a Commonly pod and uses Commonly tools. Needs Node.js 20 or later. --- -This listener runs Claude Code on your Mac and uses Commonly tools. In invite-only pods it can read the folder you start it in, and `--allow` can let it edit files or fetch web pages: see [what the listener can reach](/bring-your-own/work-in-your-repo#what-the-listener-can-reach). For a session that edits your repository, use [Work in your repo](/bring-your-own/work-in-your-repo). +This listener runs Claude Code on your Mac, or on Linux (WSL included) when every pod it answers in is invite-only, and uses Commonly tools. In invite-only pods it can read the folder you start it in, and `--allow` can let it edit files or fetch web pages: see [what the listener can reach](/bring-your-own/work-in-your-repo#what-the-listener-can-reach). For a session that edits your repository, use [Work in your repo](/bring-your-own/work-in-your-repo). -You need Node.js 20 or later and Claude Code signed in on that Mac. +You need Node.js 20 or later and Claude Code signed in on that computer. On Linux you also need bubblewrap (`bwrap`), and you run the command from your project folder, not your home folder. 1. Open [Bring your own agent](https://commonly.me/v2/agents/byo) and choose **Use my own agent**. 2. Name the agent and choose its pod. -3. Select **Get my connect command**. Under **Setting up on**, keep **macOS**. Claude Code is the default. Copy the listener command shown on the page into Terminal on the Mac with Claude Code. The token is shown once. +3. Select **Get my connect command**. Under **Setting up on**, keep **macOS**, or pick **Linux** for an invite-only pod. Claude Code is the default. Copy the listener command shown on the page into a terminal on that computer. The token is shown once. 4. Keep Terminal running. Wait for the page to show that the agent is listening, then @mention it in the pod. The Connect page shows this command with your key and agent name filled in. Copy it from the page: diff --git a/docs-site/bring-your-own/work-in-your-repo.mdx b/docs-site/bring-your-own/work-in-your-repo.mdx index 19f1ba23cf..638b363895 100644 --- a/docs-site/bring-your-own/work-in-your-repo.mdx +++ b/docs-site/bring-your-own/work-in-your-repo.mdx @@ -32,7 +32,7 @@ The [Claude Code listener](/bring-your-own/claude-code) checks the pods its key - Read-only git never contacts a remote. In a partial clone, an object that was never downloaded stays missing, and git reports it as missing. - Anyone who can @mention it in those pods can ask what the files there say, so run it from a folder you would show them. -Otherwise it reads only its own scratch folder, and the lines it prints when it starts say why. That happens when it runs anywhere but macOS, when a pod is publicly readable, listed in Community or open to join, or when you run it from your home directory, `/`, or a folder like `~/.ssh`. It checks its pods again before every turn. If one has become publicly readable, listed in Community or open to join, or the check fails, that turn runs in the scratch folder with a fresh session. It also gets none of the agent's saved memory and can't save any, because earlier turns may have stored what they read in your folder. +Otherwise, on a Mac, it reads only its own scratch folder, and the lines it prints when it starts say why. That happens when a pod is publicly readable, listed in Community or open to join, or when you run it from your home directory, `/`, or a folder like `~/.ssh`. It checks its pods again before every turn. If one has become publicly readable, listed in Community or open to join, or the check fails, that turn runs in the scratch folder with a fresh session. On Linux, including WSL, Claude Code has no scratch-folder fallback: in those cases it refuses to start, or stops answering until every pod is invite-only again. It also gets none of the agent's saved memory and can't save any, because earlier turns may have stored what they read in your folder. ### Give it more with `--allow` diff --git a/docs/design/readme-benchmark.md b/docs/design/readme-benchmark.md index 2f42047525..07bc424179 100644 --- a/docs/design/readme-benchmark.md +++ b/docs/design/readme-benchmark.md @@ -89,7 +89,7 @@ Every row cites its source: `backend/config/native-agents/scout.ts` pinned at `b | Claim | Walked status | |---|---| -| Your own Claude Code joins as a teammate that answers @mentions | **Mac only.** Not Linux, WSL or native Windows (sandbox, Linux run 38982219348). [scout.ts:140-148](https://github.com/Team-Commonly/commonly/blob/bf0bd593/backend/config/native-agents/scout.ts#L140-L148) | +| Your own Claude Code joins as a teammate that answers @mentions | **Mac, and Linux or WSL in invite-only pods** (TASK-027). Not native Windows (no sandbox). Earlier: Mac only (Linux run 38982219348). [scout.ts:140-148](https://github.com/Team-Commonly/commonly/blob/bf0bd593/backend/config/native-agents/scout.ts#L140-L148) | | Your own Codex joins as a teammate | Mac and Linux, WSL included. Not native Windows. [scout.ts:144-147](https://github.com/Team-Commonly/commonly/blob/bf0bd593/backend/config/native-agents/scout.ts#L144-L147) | | OpenCode (any provider, e.g. DeepSeek) | Via `docs/cli/opencode.md`, walked on a Mac only. [scout.ts:114-123](https://github.com/Team-Commonly/commonly/blob/bf0bd593/backend/config/native-agents/scout.ts#L114-L123) | | DeepSeek Harness (dsh) | Attached as your own agent in a **private pod**, via its attach guide. Walked on macOS; other OSs unverified. [scout.ts:145-146](https://github.com/Team-Commonly/commonly/blob/bf0bd593/backend/config/native-agents/scout.ts#L145-L146), [scout.ts:165-171](https://github.com/Team-Commonly/commonly/blob/bf0bd593/backend/config/native-agents/scout.ts#L165-L171) | diff --git a/frontend/src/content/guides.json b/frontend/src/content/guides.json index 66716c6609..05c4a893ac 100644 --- a/frontend/src/content/guides.json +++ b/frontend/src/content/guides.json @@ -135,7 +135,7 @@ }, { "question": "Can I use Claude Code and Codex in the same team?", - "answer": "Yes. Set each one up from the Bring your own agent page on your own machine (Claude Code on a Mac; Codex on a Mac; Linux is not tested yet). Both join the same pod with their own identities, answer @mentions while their listener runs, and keep their runtime-specific tools." + "answer": "Yes. Set each one up from the Bring your own agent page on your own machine (Claude Code on a Mac, or on Linux, including WSL, in an invite-only pod; Codex on a Mac, not yet tested on Linux). Both join the same pod with their own identities, answer @mentions while their listener runs, and keep their runtime-specific tools." }, { "question": "How do agents share context?", @@ -792,7 +792,7 @@ }, { "question": "Will an MCP-connected agent respond automatically when mentioned?", - "answer": "An MCP-connected agent is reactive: it acts when you invoke it in its host tool. To have @mentions answered while you are away, run the listener command the Bring your own agent page gives you, commonly agent run . While it runs, Claude Code (Mac only) or Codex (Mac only; Linux is not tested yet) answers on your machine. Start with MCP when you use Claude Code or Codex interactively." + "answer": "An MCP-connected agent is reactive: it acts when you invoke it in its host tool. To have @mentions answered while you are away, run the listener command the Bring your own agent page gives you, commonly agent run . While it runs, Claude Code (on a Mac, or on Linux, including WSL, in an invite-only pod) or Codex (Mac only; Linux is not tested yet) answers on your machine. Start with MCP when you use Claude Code or Codex interactively." }, { "question": "Can this replace GitHub, tests, or source control?", @@ -5724,7 +5724,7 @@ }, "intro": [ "An AI agent CLI wrapper turns a local command-line agent into a participant in a shared workspace: it polls collaboration events and drives the local CLI to respond. It does not move the agent into the workspace, make a laptop a hosted runtime, or give the agent permissions its runtime and connected systems did not already have.", - "Commonly (commonly.me), the shared workspace where humans and AI agents work together, offers a CLI path for the case where a team wants a local agent to stay present in a pod while the operator is away. The listener runs Claude Code (Mac only) or Codex (Mac only; Linux is not tested yet), set up from the Bring your own agent page, or OpenCode, attached from the command line with commonly agent attach opencode. The agent still runs on the machine that starts it. Commonly supplies the pod identity, collaboration surface, runtime events, memory, and task record; the local CLI supplies the reasoning and any host-level capabilities its operator has configured.", + "Commonly (commonly.me), the shared workspace where humans and AI agents work together, offers a CLI path for the case where a team wants a local agent to stay present in a pod while the operator is away. The listener runs Claude Code (on a Mac, or on Linux, including WSL, in an invite-only pod) or Codex (Mac only; Linux is not tested yet), set up from the Bring your own agent page, or OpenCode, attached from the command line with commonly agent attach opencode. The agent still runs on the machine that starts it. Commonly supplies the pod identity, collaboration surface, runtime events, memory, and task record; the local CLI supplies the reasoning and any host-level capabilities its operator has configured.", "That split is the useful part of the design, and the part worth being explicit about. A local agent can collaborate continuously without a public inbound endpoint, but a wrapper is not a security boundary, a repository permission system, or a substitute for review. This guide explains how to attach and run a CLI agent, where its authority begins and ends, how heartbeats fit into the operating model, and how to test one small workflow before handing the agent a larger role." ], "sections": [ diff --git a/frontend/src/content/landing-faq.json b/frontend/src/content/landing-faq.json index 9141130b77..1553eb8faa 100644 --- a/frontend/src/content/landing-faq.json +++ b/frontend/src/content/landing-faq.json @@ -37,7 +37,7 @@ }, { "question": "how do I connect Claude Code to my team's chat so it answers @mentions", - "answer": "In a Commonly pod, choose \"Use my own agent\", then \"Get my connect command\", and run the lines it shows on your Mac. Your own Claude Code then answers @mentions in the pod. On Linux or Windows, the same page connects your Claude Code session over MCP." + "answer": "In a Commonly pod, choose \"Use my own agent\", then \"Get my connect command\", and run the lines it shows on your Mac, or on Linux in an invite-only pod. Your own Claude Code then answers @mentions in the pod. Otherwise, as on Windows, the same page connects your Claude Code session over MCP." }, { "question": "self-hosted platform for multi-agent collaboration with humans", diff --git a/frontend/src/i18n/locales/en.json b/frontend/src/i18n/locales/en.json index 25680c0b90..eed6311661 100644 --- a/frontend/src/i18n/locales/en.json +++ b/frontend/src/i18n/locales/en.json @@ -1670,6 +1670,7 @@ "footnote": { "cliRoute": "Prefer the CLI? Choose “{{byo}}”; after install, the next step shows the listener command that answers @mentions.", "cliRouteClaudeUnavailable": "Prefer the CLI? Choose “{{byo}}”; after install, connect Claude Code to Commonly from your own sessions. To answer @mentions, run the listener on a supported computer or choose “{{hosted}}”.", + "cliRouteClaudeLinuxUnavailable": "Prefer the CLI? Choose “{{byo}}”; after install, connect Claude Code to Commonly from your own sessions. On Linux, Claude Code answers @mentions only when every pod it’s in is invite-only; otherwise choose “{{hosted}}”.", "cliRouteCodexUnavailable": "Prefer the CLI? Choose “{{byo}}”; after install, the next step explains how to run the listener on a Mac computer. To answer @mentions from {{os}}, choose “{{hosted}}”.", "notSure": "Not sure which path?", "mcpVsCli": "MCP vs CLI vs SDK", @@ -1724,6 +1725,7 @@ "listen": { "title": "Run this in Terminal", "note": "MCP alone won't wake {{name}} for @mentions. Keep this Terminal open.", + "reachClaudeLinux": "On Linux, {{name}} answers only while every pod it's in is private, invite-only and not listed in Community, and it reads only the folder you run this from. Run it from your project's folder, not your home folder, with bubblewrap (bwrap) installed. If any of its pods stops being invite-only, {{name}} stops answering until that pod is invite-only again. To let it edit files and run commands, add --allow write to the last line. Those commands can read beyond the folder. To let it fetch web pages, add --allow web. Either one lets it send what it reads off this computer, so grant them only in a pod you trust.", "reachClaude": "By default, on macOS, when every pod it's in is private, invite-only and not listed in Community, {{name}} reads only the folder you run this from. Otherwise, or when you run it from your home folder, it works in its own scratch folder. To let it edit files and run commands, add --allow write to the last line. Those commands can read beyond the folder. To let it fetch web pages, add --allow web. Either one lets it send what it reads off this computer, so grant them only in a pod you trust.", "reachCodex": "Codex works in its own scratch folder under your temp directory, not the folder you run this from. --allow does not apply to Codex, so the last line has no --allow.", "waiting": "Waiting for {{name}} to check in… this turns green the moment the command connects.", @@ -1734,12 +1736,14 @@ "npmPermission": "If npm stops with EACCES (permission denied), don’t add sudo. Reinstall Node with a version manager such as nvm, then run the four lines again.", "ctaWarning": "{{name}} isn’t listening yet: it won’t answer mentions until the command above is running.", "needs": "Needs Node 20+ with npm and {{tool}} signed in. Run all four lines in one window: the exports last only there.", + "needsLinuxFolder": "On Linux, open the window in your project folder first, with bubblewrap (bwrap) installed: run from your home folder, it won’t start.", "osLabel": "Setting up on:", "osLinux": "Linux", "osMac": "Mac", "osWindows": "Windows", "toolClaude": "Claude Code", "toolCodex": "the Codex CLI", + "unavailableClaudeLinux": "On Linux, Claude Code answers @mentions only in an invite-only pod, and this one isn’t. Its owner can switch it to Invite-only under “Who can join” in pod settings. Or connect Claude Code from your own sessions with the command below, or choose “Run it here” for a hosted agent.", "unavailableClaude": "Claude Code can’t answer @mentions from {{os}} yet: Commonly runs it in a sandbox, and on {{os}} that sandbox can’t run it. Connect Claude Code from your own sessions with the command below, or choose “Run it here” for a hosted agent.", "unavailableCodex": "The Codex CLI can’t answer @mentions from {{os}} yet. Choose “Run it here” for a hosted agent, or run the command on a Mac. Linux is not tested yet.", "useDifferentRunner": "Use a different adapter" diff --git a/frontend/src/i18n/locales/zh-CN.json b/frontend/src/i18n/locales/zh-CN.json index 3ffafea633..3d8253f3ef 100644 --- a/frontend/src/i18n/locales/zh-CN.json +++ b/frontend/src/i18n/locales/zh-CN.json @@ -1729,6 +1729,7 @@ "footnote": { "cliRoute": "更喜欢用 CLI?选择「{{byo}}」;安装后下一步会显示回复 @提及的监听命令。", "cliRouteClaudeUnavailable": "更喜欢用 CLI?选择「{{byo}}」;安装后可以在自己的 Claude Code 会话中连接 Commonly。要回复 @提及,请在支持的电脑上运行监听命令,或选择「{{hosted}}」。", + "cliRouteClaudeLinuxUnavailable": "更喜欢用 CLI?选择「{{byo}}」;安装后可以在自己的 Claude Code 会话中连接 Commonly。在 Linux 上,只有当它所在的每个 Pod 都仅限邀请时,Claude Code 才会回复 @提及;否则请选择「{{hosted}}」。", "cliRouteCodexUnavailable": "更喜欢用 CLI?选择「{{byo}}」;安装后下一步会说明如何在 Mac 电脑上运行监听命令。要从 {{os}} 回复 @提及,请选择「{{hosted}}」。", "notSure": "不确定用哪条路径?", "mcpVsCli": "MCP、CLI 与 SDK 对比", @@ -1783,6 +1784,7 @@ "listen": { "title": "在终端里运行这段命令", "note": "仅连接 MCP 不会唤醒 {{name}} 回复 @提及。请保持终端窗口开启。", + "reachClaudeLinux": "在 Linux 上,只有当 {{name}} 所在的每个 Pod 都是私密、仅限邀请且未列入「社区」的时候,它才会回复,而且只能读取你运行这段命令时所在的文件夹。请在项目文件夹里运行(别在个人文件夹里),并先装好 bubblewrap(bwrap)。只要有一个 Pod 不再仅限邀请,{{name}} 就会停止回复,直到那个 Pod 恢复为仅限邀请。想让它编辑文件、运行命令,就在最后一行加上 --allow write。它运行的命令能读取文件夹以外的内容。想让它抓取网页,就加上 --allow web。无论加哪一个,它都能把读到的内容传出这台电脑,所以只在你信任的 Pod 里授予这两项权限。", "reachClaude": "默认情况下,如果你用的是 macOS,并且 {{name}} 所在的每个 Pod 都是私密、仅限邀请且未列入「社区」的,它只能读取你运行这段命令时所在的文件夹。不满足这些条件,或者你在个人文件夹里运行这段命令,它都会在自己的临时文件夹里工作。想让它编辑文件、运行命令,就在最后一行加上 --allow write。它运行的命令能读取文件夹以外的内容。想让它抓取网页,就加上 --allow web。无论加哪一个,它都能把读到的内容传出这台电脑,所以只在你信任的 Pod 里授予这两项权限。", "reachCodex": "Codex 会在系统临时目录里用自己的暂存文件夹工作,不在你运行命令的这个文件夹。--allow 对 Codex 无效,所以最后一行不带 --allow。", "waiting": "正在等待 {{name}} 接入…命令一接通,这里就会变绿。", @@ -1793,11 +1795,13 @@ "npmPermission": "如果 npm 报 EACCES(权限被拒绝),不要加 sudo。请用版本管理工具(如 nvm)重新安装 Node,然后重新运行这四行。", "ctaWarning": "{{name}} 尚未监听:上面的命令运行起来之前,它不会回复 @提及。", "needs": "需要 Node 20+(含 npm)和已登录的 {{tool}}。四行都要在同一个窗口里运行,换了窗口 export 就失效。", + "needsLinuxFolder": "在 Linux 上,请先在项目文件夹里打开窗口,并装好 bubblewrap(bwrap):在个人文件夹里运行,它不会启动。", "osLabel": "你的系统:", "osLinux": "Linux", "osMac": "Mac", "osWindows": "Windows", "toolClaude": "Claude Code", + "unavailableClaudeLinux": "在 Linux 上,Claude Code 只在仅限邀请的 Pod 里回复 @提及,而这个 Pod 不是。Pod 的所有者可以在 Pod 设置的「谁可以加入」里改成「仅限邀请」。你也可以用下面的命令把 Commonly 的工具接进自己的 Claude Code 会话,或者选择「在这里运行」,改用托管智能体。", "unavailableClaude": "Claude Code 在 {{os}} 上暂时不能回复 @提及:Commonly 要把它放进沙箱里运行,而 Commonly 用的沙箱在 {{os}} 上还做不到。你可以用下面的命令把 Commonly 的工具接进自己的 Claude Code 会话,或者选择“在这里运行”,改用托管智能体。", "unavailableCodex": "Codex CLI 在 {{os}} 上暂时不能回复 @提及。你可以选择“在这里运行”,改用托管智能体;也可以换到 Mac 机器上运行这条命令(Linux 尚未测试)。", "toolCodex": "Codex CLI", diff --git a/frontend/src/v2/__tests__/V2AgentBYOListenSnippet.test.tsx b/frontend/src/v2/__tests__/V2AgentBYOListenSnippet.test.tsx index 9733f652b8..39e0126864 100644 --- a/frontend/src/v2/__tests__/V2AgentBYOListenSnippet.test.tsx +++ b/frontend/src/v2/__tests__/V2AgentBYOListenSnippet.test.tsx @@ -47,9 +47,9 @@ const authValue = { updateProfile: jest.fn(), }; -const issueToken = async (runner?: 'codex') => { +const issueToken = async (runner?: 'codex', pod = {}) => { axios.get.mockImplementation((url) => { - if (url === '/api/pods') return Promise.resolve({ data: [{ _id: 'p1', name: 'Workspace', type: 'chat' }] }); + if (url === '/api/pods') return Promise.resolve({ data: [{ _id: 'p1', name: 'Workspace', type: 'chat', ...pod }] }); if (url.startsWith('/api/registry/agents/next-free-name?')) return Promise.resolve({ data: { name: new URLSearchParams(url.split('?')[1]).get('base') } }); return Promise.resolve({ data: {} }); }); @@ -256,10 +256,11 @@ describe('BYO listener snippet', () => { expect(lines.filter((l) => l.trimStart().startsWith('#'))).toHaveLength(0); }); - // Where the listener runs: Mac with either CLI, Linux with Codex. Claude Code - // under Linux's sandbox could not start (ubuntu run 38982219348) and native - // Windows has none; there the page says so and offers MCP and hosted. The - // Windows MCP command is PowerShell lines through cmd /c (windows-latest). + // Where the listener runs: Mac with either CLI, Linux with Codex, and Linux + // with Claude Code in an invite-only pod (TASK-027: the CLI refuses a public + // Claude Code seat under bubblewrap). Native Windows has no sandbox; there + // the page says so and offers MCP and hosted. The Windows MCP command is + // PowerShell lines through cmd /c (windows-latest). describe('where the listener runs', () => { const realUserAgent = window.navigator.userAgent; const setUserAgent = (value) => Object.defineProperty(window.navigator, 'userAgent', { value, configurable: true }); @@ -319,7 +320,8 @@ describe('BYO listener snippet', () => { expect(screen.getByTestId('byo-setup-os-linux')).toHaveAttribute('aria-pressed', 'true'); expect(listener()).toBeUndefined(); expect(screen.queryByTestId('byo-listen-reach')).toBeNull(); - expect(screen.getByTestId('byo-listen-unavailable')).toHaveTextContent(/from Linux yet/); + expect(screen.getByTestId('byo-listen-unavailable')) + .toHaveTextContent(/On Linux, Claude Code answers @mentions only in an invite-only pod, and this one isn’t/); expect(screen.queryByTestId('byo-listen-npm-hint')).toBeNull(); expect(mcpBox()).toMatch(/-- npx -y @commonlyai\/mcp$/); expect(screen.getByTestId('byo-mcp-claude')).not.toHaveTextContent(/command above|Optional/); @@ -334,6 +336,80 @@ describe('BYO listener snippet', () => { expect(screen.queryByTestId('byo-listen-reach-codex')).toBeNull(); }); + const PRIVATE_POD = { joinPolicy: 'invite-only', publicRead: false, communityListed: false }; + + test('Linux with Claude Code in an invite-only pod: the listener command and the Linux reach note', async () => { + setUserAgent(LINUX); + await issueToken(undefined, PRIVATE_POD); + expect(screen.getByTestId('byo-setup-os-linux')).toHaveAttribute('aria-pressed', 'true'); + expect((listener()?.textContent || '').split('\n')[3]).toMatch(/ --adapter claude$/); + expect(screen.queryByTestId('byo-listen-unavailable')).toBeNull(); + expect(screen.getByTestId('byo-listen-reach')).toHaveTextContent(/^On Linux, .* answers only while every pod it's in is private, invite-only/); + expect(screen.getByTestId('byo-listen-reach')).toHaveTextContent(/bubblewrap \(bwrap\)/); + }); + + test('Linux with Claude Code: the folder and bubblewrap requirement sits beside the command', async () => { + setUserAgent(LINUX); + await issueToken(undefined, PRIVATE_POD); + expect(screen.getByTestId('byo-listen-needs')) + .toHaveTextContent(/On Linux, open the window in your project folder first, with bubblewrap \(bwrap\) installed: run from your home folder, it won’t start\./); + }); + + test('Mac with Claude Code: no Linux folder note', async () => { + setUserAgent(MAC); + await issueToken(undefined, PRIVATE_POD); + expect(screen.getByTestId('byo-listen-needs')).not.toHaveTextContent(/On Linux/); + }); + + // #2783: Codex stays Mac only, even in a pod the Claude Code check accepts. + test('Linux with Codex in an invite-only pod: still no listener command', async () => { + setUserAgent(LINUX); + await issueToken('codex', PRIVATE_POD); + expect(listener()).toBeUndefined(); + expect(screen.getByTestId('byo-listen-unavailable')).toHaveTextContent(/The Codex CLI can’t answer @mentions from Linux yet/); + }); + + test('Linux with Claude Code in a pod that is not private: the footnote names the pod rule, not the computer', async () => { + setUserAgent(LINUX); + axios.get.mockImplementation((url) => { + if (url === '/api/pods') return Promise.resolve({ data: [{ _id: 'p1', name: 'Workspace', type: 'chat', joinPolicy: 'open', publicRead: false, communityListed: false }] }); + if (url.startsWith('/api/registry/agents/next-free-name?')) return Promise.resolve({ data: { name: new URLSearchParams(url.split('?')[1]).get('base') } }); + return Promise.resolve({ data: {} }); + }); + render( + + + + + , + ); + await waitFor(() => expect(screen.getByText('Workspace (chat)')).toBeInTheDocument()); + expect(screen.getByText(/On Linux, Claude Code answers @mentions only when every pod it’s in is invite-only/)).toBeInTheDocument(); + expect(screen.queryByText(/run the listener on a supported computer/)).toBeNull(); + }); + + // The CLI treats a pod that does not report a flag as public, so the page + // must not offer a listener the CLI would refuse. + test.each([ + ['open to join', { ...PRIVATE_POD, joinPolicy: 'open' }], + ['listed in Community', { ...PRIVATE_POD, communityListed: true }], + ['publicRead not reported', { joinPolicy: 'invite-only', communityListed: false }], + ['communityListed not reported', { joinPolicy: 'invite-only', publicRead: false }], + // The CLI treats a missing joinPolicy as open. + ['joinPolicy not reported', { publicRead: false, communityListed: false }], + ])('Linux with Claude Code in a pod %s: no listener command', async (_label, pod) => { + setUserAgent(LINUX); + await issueToken(undefined, pod); + expect(listener()).toBeUndefined(); + expect(screen.getByTestId('byo-listen-unavailable')).toHaveTextContent(/under “Who can join” in pod settings/); + }); + + test('Mac with Claude Code keeps the macOS reach note', async () => { + setUserAgent(MAC); + await issueToken(undefined, PRIVATE_POD); + expect(screen.getByTestId('byo-listen-reach')).toHaveTextContent(/^By default, on macOS/); + }); + test('Codex on Windows says so, and offers hosted or another machine', async () => { setUserAgent(WINDOWS); await issueToken('codex'); diff --git a/frontend/src/v2/components/V2AgentBYO.tsx b/frontend/src/v2/components/V2AgentBYO.tsx index 99c33c7c53..664232d88b 100644 --- a/frontend/src/v2/components/V2AgentBYO.tsx +++ b/frontend/src/v2/components/V2AgentBYO.tsx @@ -75,9 +75,11 @@ const CLAUDE_FILE_NAME = 'CLAUDE.md'; type MachineAdapter = '' | 'claude' | 'codex'; // Where the listener runs (cli 0.1.106 stops elsewhere, with directions): -// Commonly runs it in a sandbox. Seatbelt runs Claude Code and Codex on a Mac; -// on Linux (WSL included) the bubblewrap namespace cannot start Claude Code -// yet (ubuntu run 38982219348), and native Windows has no sandbox for either. +// Commonly runs it in a sandbox. Seatbelt runs Claude Code and Codex on a Mac. +// On Linux (WSL included) bubblewrap runs Codex, and Claude Code only as a +// workspace seat, which the CLI allows only while every pod it answers in is +// invite-only (TASK-027); a public Claude Code seat there is refused. Native +// Windows has no sandbox for either. // PowerShell also has no `export`, so a Windows visitor gets PowerShell lines // for the MCP connection instead (windows-latest, 2026-10-16). type SetupOs = 'mac' | 'linux' | 'windows'; @@ -103,7 +105,19 @@ const SETUP_OS_OPTIONS: Array<{ os: SetupOs; labelKey: string }> = [ // which PowerShell cannot run, and Codex on native Windows is unmeasured. // Not drift: the page only offers what it has seen work. Codex on Linux is // not walked yet (Lily, 10-26: "macOS; Linux not yet tested"), so no listener. -const listenerRunsOn = (os: SetupOs): boolean => os === 'mac'; +// Claude Code on Linux runs only in a pod that passes the CLI's own test +// (workspace-trust.js podIsConfirmedPrivate): both flags reported and false, +// and invite-only. A pod that omits a flag is not private. +const podIsConfirmedPrivate = (pod?: V2Pod): boolean => ( + typeof pod?.publicRead === 'boolean' + && typeof pod?.communityListed === 'boolean' + && !pod.publicRead + && !pod.communityListed + && pod.joinPolicy === 'invite-only' +); +const listenerRunsOn = (os: SetupOs, adapter: 'claude' | 'codex', pod?: V2Pod): boolean => ( + os === 'mac' || (os === 'linux' && adapter === 'claude' && podIsConfirmedPrivate(pod)) +); type ModelOption = { value: string; labelKey: string }; const CLAUDE_MODEL_OPTIONS: ModelOption[] = [ { value: 'opus', labelKey: 'agentByo.form.modelOpus' }, @@ -200,7 +214,11 @@ const V2AgentBYO: React.FC = () => { // Where the person sets up, defaulted from this browser. They can switch: // the machine they browse on is not always the one they set up. const [setupOs, setSetupOs] = useState(browserOs); - const listenerAvailable = listenerRunsOn(setupOs); + const listenerAvailable = listenerRunsOn( + setupOs, + listenAdapter, + pods.find((p) => p._id === (issued?.podId || podId)), + ); // Live listening verification (#887 class, second half). #888 made the // listening step legible; this makes it CHECKABLE — the page polls the // kernel until THIS agent's runtime token authenticates for the first time @@ -1436,7 +1454,8 @@ const V2AgentBYO: React.FC = () => { {addComputerBlock}

{t(listenerAvailable ? 'agentByo.footnote.cliRoute' - : listenAdapter === 'claude' ? 'agentByo.footnote.cliRouteClaudeUnavailable' + : listenAdapter === 'claude' + ? (setupOs === 'linux' ? 'agentByo.footnote.cliRouteClaudeLinuxUnavailable' : 'agentByo.footnote.cliRouteClaudeUnavailable') : 'agentByo.footnote.cliRouteCodexUnavailable', { byo: t('agentByo.mode.byo'), hosted: t('agentByo.mode.hosted'), @@ -1589,10 +1608,12 @@ const V2AgentBYO: React.FC = () => {

{listenerAvailable - ? t('agentByo.listen.needs', { + ? `${t('agentByo.listen.needs', { tool: listenAdapter === 'codex' ? t('agentByo.listen.toolCodex') : t('agentByo.listen.toolClaude'), - }) - : t(listenAdapter === 'claude' ? 'agentByo.listen.unavailableClaude' : 'agentByo.listen.unavailableCodex', { + })}${setupOs === 'linux' ? ` ${t('agentByo.listen.needsLinuxFolder')}` : ''}` + : t(listenAdapter === 'claude' + ? (setupOs === 'linux' ? 'agentByo.listen.unavailableClaudeLinux' : 'agentByo.listen.unavailableClaude') + : 'agentByo.listen.unavailableCodex', { os: t(SETUP_OS_OPTIONS.find((o) => o.os === setupOs)?.labelKey || 'agentByo.listen.osMac'), })}{' '}