Skip to content
10 changes: 9 additions & 1 deletion frontend/src/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1750,7 +1750,15 @@
"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"
"useDifferentRunner": "Use a different adapter",
"repo": {
"label": "Work in my repo",
"hint": "Runs in the folder you start it from, confined to that folder. Every pod this agent is in must be private and invite-only; if one is not, it will not read your folder. It uses its own scratch folder, or refuses to start if you ticked an option below. Without write, it reads only that folder and nothing else on this computer. With write, the commands it runs can read beyond the folder.",
"write": "Let it edit files and run commands (--allow write)",
"web": "Let it fetch web pages (--allow web)",
"folder": "Open your terminal in your project's folder (not your home folder) before you paste the command. Either one lets it send what it reads off this computer, so grant them only in a pod you trust.",
"saved": "Write and web are saved when the agent first starts, and unticking a box here does not remove them later. Stop the running listener first. Then change them with: commonly agent detach <name> --force, followed by a new run with the new choice."
Comment thread
lilyshen0722 marked this conversation as resolved.
Resolved
}
},
"mode": {
"label": "Where should it run?",
Expand Down
10 changes: 9 additions & 1 deletion frontend/src/i18n/locales/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -1809,7 +1809,15 @@
"unavailableClaude": "Claude Code 在 {{os}} 上暂时不能回复 @提及:Commonly 要把它放进沙箱里运行,而 Commonly 用的沙箱在 {{os}} 上还做不到。你可以用下面的命令把 Commonly 的工具接进自己的 Claude Code 会话,或者选择“在这里运行”,改用托管智能体。",
"unavailableCodex": "Codex CLI 在 {{os}} 上暂时不能回复 @提及。你可以选择“在这里运行”,改用托管智能体;也可以换到 Mac 机器上运行这条命令(Linux 尚未测试)。",
"toolCodex": "Codex CLI",
"useDifferentRunner": "换一种适配器"
"useDifferentRunner": "换一种适配器",
"repo": {
"label": "在我的仓库里工作",
"hint": "在你启动它的那个文件夹里运行,只能访问这个文件夹。这个智能体所在的每个 Pod 都必须是私密的邀请制 Pod;只要有一个不是,它就不会读取你的文件夹,而是改在自己的暂存文件夹里运行;如果你勾选了下面的选项,它会拒绝启动。不勾选写入时,它只读取这个文件夹,读不到这台电脑上的其他内容。勾选写入后,它运行的命令能读取文件夹以外的内容。",
"write": "允许它编辑文件、运行命令(--allow write)",
"web": "允许它抓取网页(--allow web)",
"folder": "粘贴命令之前,先在终端里打开你的项目文件夹(别在个人文件夹里)。无论选哪一项,它都能把读到的内容传出这台电脑,所以只在你信任的 Pod 里授予。",
"saved": "写入和联网权限在智能体第一次启动时保存,之后在这里取消勾选并不会移除它们。修改前请先停止正在运行的监听器。然后运行 commonly agent detach <name> --force,再用新的选择重新运行。"
}
},
"mode": {
"label": "在哪里运行?",
Expand Down
44 changes: 44 additions & 0 deletions frontend/src/v2/__tests__/V2AgentBYOListenSnippet.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -432,3 +432,47 @@ describe('BYO listener snippet', () => {
});
});
});

// TASK-705: "Work in my repo" only ever adds a folder step and the two existing
// opt-ins to the same command. It has no path that skips the sandbox.
describe('BYO listener: Work in my repo', () => {
const PRIVATE_POD = { joinPolicy: 'invite-only', publicRead: false, communityListed: false };
const MAC_UA = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)';
const runLine = () => Array.from(document.querySelectorAll('.v2-byo__pre'))
.map((el) => el.textContent || '')
.find((txt) => /commonly agent run /.test(txt));

beforeEach(() => {
Object.defineProperty(window.navigator, 'userAgent', { value: MAC_UA, configurable: true });
});

test('off by default: the command is unchanged and carries no --allow', async () => {
await issueToken(undefined, PRIVATE_POD);
expect(screen.getByTestId('byo-repo-toggle')).not.toBeChecked();
expect(runLine()).not.toMatch(/--allow|cd /);
});

test('choosing it adds no cd step (the note says to run from the project folder), and write/web add only their own --allow', async () => {
await issueToken(undefined, PRIVATE_POD);
fireEvent.click(screen.getByTestId('byo-repo-toggle'));
expect(runLine()).toMatch(/commonly agent run \S+ --adapter claude$/);
fireEvent.click(screen.getByTestId('byo-repo-write'));
expect(runLine()).toMatch(/--adapter claude --allow write$/);
fireEvent.click(screen.getByTestId('byo-repo-web'));
expect(runLine()).toMatch(/--adapter claude --allow write,web$/);
expect(runLine()).not.toMatch(/dangerously|skip-permissions|cd /);
expect(screen.getByTestId('byo-repo-saved-note')).toHaveTextContent(/detach/);
});

test('not offered in a pod that is not confirmed private', async () => {
await issueToken(undefined, { joinPolicy: 'open', publicRead: false, communityListed: false });
expect(screen.queryByTestId('byo-repo-choice')).toBeNull();
expect(runLine()).not.toMatch(/--allow/);
});

test('not offered for Codex, where --allow does not apply', async () => {
await issueToken('codex', PRIVATE_POD);
expect(screen.queryByTestId('byo-repo-choice')).toBeNull();
expect(runLine()).not.toMatch(/--allow/);
});
});
61 changes: 60 additions & 1 deletion frontend/src/v2/components/V2AgentBYO.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,27 @@ const V2AgentBYO: React.FC = () => {
listenAdapter,
pods.find((p) => p._id === (issued?.podId || podId)),
);
// TASK-705: "Work in my repo". The CLI already confines a seat to the folder it
// starts in when every pod is confirmed private (workspace trust: Seatbelt on
// a Mac, bwrap on Linux). This choice makes that explicit and carries the two
// opt-ins. There is no unconfined path: write and web only ever ride on
// `--allow`, and the CLI refuses them for a folder it will not confine.
const [workInRepo, setWorkInRepo] = useState(false);
const [allowWrite, setAllowWrite] = useState(false);
const [allowWeb, setAllowWeb] = useState(false);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
// Each seat is its own opt-in: starting another install clears all three.
useEffect(() => {
setWorkInRepo(false); setAllowWrite(false); setAllowWeb(false);
}, [issued?.token]);
const repoPod = pods.find((p) => p._id === (issued?.podId || podId));
const repoChoiceAvailable = listenAdapter === 'claude'
&& setupOs !== 'windows'
&& podIsConfirmedPrivate(repoPod);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
const repoActive = workInRepo && repoChoiceAvailable;
const allowFlag = repoActive
? [allowWrite && 'write', allowWeb && 'web'].filter(Boolean).join(',')
: '';
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved

// 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
Expand Down Expand Up @@ -1061,7 +1082,7 @@ const V2AgentBYO: React.FC = () => {
// EINVALIDTAGNAME "#" and installs nothing (ux-lead on #2104). The command
// names itself; the body above the box says where to run it.
const listenSnippet = issued
? `${CLI_INSTALL_COMMAND}\nexport COMMONLY_API_URL=${apiUrl}\nexport COMMONLY_AGENT_TOKEN=${issued.token}\ncommonly agent run ${issued.agentName} --adapter ${listenAdapter}`
? `${CLI_INSTALL_COMMAND}\nexport COMMONLY_API_URL=${apiUrl}\nexport COMMONLY_AGENT_TOKEN=${issued.token}\ncommonly agent run ${issued.agentName} --adapter ${listenAdapter}${allowFlag ? ` --allow ${allowFlag}` : ''}`
: '';

const cursorSnippet = issued
Expand Down Expand Up @@ -1684,6 +1705,44 @@ const V2AgentBYO: React.FC = () => {
{t(setupOs === 'linux' ? 'agentByo.listen.reachClaudeLinux' : 'agentByo.listen.reachClaude', { name: issued.agentName })}
</p>
)}
{listenerAvailable && repoChoiceAvailable && (
<div className="v2-byo__listen-note" data-testid="byo-repo-choice">
<label>
<input
type="checkbox"
data-testid="byo-repo-toggle"
checked={workInRepo}
onChange={(e) => setWorkInRepo(e.target.checked)}
/>
{' '}{t('agentByo.listen.repo.label')}
</label>
<p>{t('agentByo.listen.repo.hint')}</p>
{workInRepo && (
<>
<label>
<input
type="checkbox"
data-testid="byo-repo-write"
checked={allowWrite}
onChange={(e) => setAllowWrite(e.target.checked)}
/>
{' '}{t('agentByo.listen.repo.write')}
</label>
<label>
<input
type="checkbox"
data-testid="byo-repo-web"
checked={allowWeb}
onChange={(e) => setAllowWeb(e.target.checked)}
/>
{' '}{t('agentByo.listen.repo.web')}
</label>
<p data-testid="byo-repo-folder-note">{t('agentByo.listen.repo.folder')}</p>
<p data-testid="byo-repo-saved-note">{t('agentByo.listen.repo.saved')}</p>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
</>
)}
</div>
)}
{listenerAvailable && listenAdapter === 'codex' && (
<p className="v2-byo__listen-note" data-testid="byo-listen-reach-codex">
{t('agentByo.listen.reachCodex')}
Expand Down
Loading