From 7f5b2345bbc7b28b1fee67d00d614a9c50712c2b Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Mon, 26 Oct 2026 04:41:44 -0700 Subject: [PATCH 1/4] copy: Claude Code answers @mentions on macOS, and on Linux in invite-only pods (TASK-027) every pod it answers in is invite-only, and keeps a public one refused. The copy that said Mac only now says so: the README, the landing FAQ, three guide answers, Scout's platform list and card pairs, and the README benchmark row. The Bring your own page offers the Linux Claude Code listener only for a pod that passes the CLI's own private-pod test, a copy of podIsConfirmedPrivate: both visibility flags reported and false, and invite-only. Otherwise it says why and points to Who can join in pod settings. On Linux the reach note says the listener stops answering in a pod that opens up, runs from the project folder and needs bubblewrap. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01A9iDVr7rVKi8xSjpN14CU7 --- .../nativeAgents.scoutKnowledge.test.js | 22 +++++---- backend/config/native-agents/scout.ts | 18 ++++--- docs/design/readme-benchmark.md | 2 +- frontend/src/content/guides.json | 6 +-- frontend/src/content/landing-faq.json | 2 +- frontend/src/i18n/locales/en.json | 2 + frontend/src/i18n/locales/zh-CN.json | 2 + .../V2AgentBYOListenSnippet.test.tsx | 48 ++++++++++++++++--- frontend/src/v2/components/V2AgentBYO.tsx | 34 ++++++++++--- 9 files changed, 101 insertions(+), 35 deletions(-) diff --git a/backend/__tests__/unit/config/nativeAgents.scoutKnowledge.test.js b/backend/__tests__/unit/config/nativeAgents.scoutKnowledge.test.js index 64df7fc3d9..27b092babe 100644 --- a/backend/__tests__/unit/config/nativeAgents.scoutKnowledge.test.js +++ b/backend/__tests__/unit/config/nativeAgents.scoutKnowledge.test.js @@ -119,13 +119,16 @@ 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\)/); + expect(prompt).toMatch(/For Claude Code on Linux or WSL, 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 +156,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 +181,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..c946e8d6f3 100644 --- a/backend/config/native-agents/scout.ts +++ b/backend/config/native-agents/scout.ts @@ -166,15 +166,19 @@ 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 in invite-only pods; 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 +231,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/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 c6ed5f15f7..a6b16aefad 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; 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." }, { "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?", @@ -5723,7 +5723,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 86b1dd8e56..5d0ae12a63 100644 --- a/frontend/src/i18n/locales/en.json +++ b/frontend/src/i18n/locales/en.json @@ -1721,6 +1721,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 a pod stops being invite-only, {{name}} stops answering there. 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.", @@ -1737,6 +1738,7 @@ "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 9d56b08582..0c55a4fa5f 100644 --- a/frontend/src/i18n/locales/zh-CN.json +++ b/frontend/src/i18n/locales/zh-CN.json @@ -1780,6 +1780,7 @@ "listen": { "title": "在终端里运行这段命令", "note": "仅连接 MCP 不会唤醒 {{name}} 回复 @提及。请保持终端窗口开启。", + "reachClaudeLinux": "在 Linux 上,只有当 {{name}} 所在的每个 Pod 都是私密、仅限邀请且未列入「社区」的时候,它才会回复,而且只能读取你运行这段命令时所在的文件夹。请在项目文件夹里运行,不要在个人文件夹里运行,并且先装好 bubblewrap(bwrap)。如果某个 Pod 不再仅限邀请,{{name}} 就不会在那里回复了。想让它编辑文件、运行命令,就在最后一行加上 --allow write。它运行的命令能读取文件夹以外的内容。想让它抓取网页,就加上 --allow web。无论加哪一个,它都能把读到的内容传出这台电脑,所以只在你信任的 Pod 里授予这两项权限。", "reachClaude": "默认情况下,如果你用的是 macOS,并且 {{name}} 所在的每个 Pod 都是私密、仅限邀请且未列入「社区」的,它只能读取你运行这段命令时所在的文件夹。不满足这些条件,或者你在个人文件夹里运行这段命令,它都会在自己的临时文件夹里工作。想让它编辑文件、运行命令,就在最后一行加上 --allow write。它运行的命令能读取文件夹以外的内容。想让它抓取网页,就加上 --allow web。无论加哪一个,它都能把读到的内容传出这台电脑,所以只在你信任的 Pod 里授予这两项权限。", "reachCodex": "Codex 会在系统临时目录里用自己的暂存文件夹工作,不在你运行命令的这个文件夹。--allow 对 Codex 无效,所以最后一行不带 --allow。", "waiting": "正在等待 {{name}} 接入…命令一接通,这里就会变绿。", @@ -1795,6 +1796,7 @@ "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..bb7d1d45fd 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,38 @@ 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\)/); + }); + + // 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 }], + ])('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 bd7daad929..1ba38c997c 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 @@ -1605,7 +1623,9 @@ const V2AgentBYO: React.FC = () => { ? t('agentByo.listen.needs', { tool: listenAdapter === 'codex' ? t('agentByo.listen.toolCodex') : t('agentByo.listen.toolClaude'), }) - : t(listenAdapter === 'claude' ? 'agentByo.listen.unavailableClaude' : 'agentByo.listen.unavailableCodex', { + : 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'), })}{' '}