From d89d8531b90ff0391ab8ed5c93e2a41f417aaba2 Mon Sep 17 00:00:00 2001 From: Thomas Kosiewski Date: Wed, 4 Nov 2026 00:15:35 +0000 Subject: [PATCH 1/4] =?UTF-8?q?=F0=9F=A4=96=20fix:=20turn=20off=20the=20ag?= =?UTF-8?q?ent-browser=20stream=20before=20the=20sandbox=20recipe=20sends?= =?UTF-8?q?=20the=20token?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit agent-browser starts a credential-less WebSocket stream per session. The recipe now opens about:blank, turns the stream off, checks that it is off, and only then sends the token URL. The skill states the Chrome DevTools port limit. AGENTS.md says not to open credential URLs directly. --- .xum/skills/dev-server-sandbox/SKILL.md | 21 ++++++++++++++++--- docs/AGENTS.md | 2 ++ .../builtInSkillContent.generated.ts | 2 ++ 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/.xum/skills/dev-server-sandbox/SKILL.md b/.xum/skills/dev-server-sandbox/SKILL.md index a17cd93f0ff..852e950cd32 100644 --- a/.xum/skills/dev-server-sandbox/SKILL.md +++ b/.xum/skills/dev-server-sandbox/SKILL.md @@ -72,20 +72,35 @@ printf 'Authorization: Bearer %s\n' "$TOKEN" | curl -s -X POST -H @- "http://127 Command lines are readable by every local account. So node gets only the lockfile path, and the header goes to `curl -H @-` on stdin from the `printf` builtin. Never pass the token as an argument. -Open the app in a browser once with the token in the URL. Node builds the URL and pipes it to `agent-browser batch` on stdin, so the token is in no command line. The batch prints the URL it opened, with the token, so the recipe keeps that output in a variable: it prints nothing when the open works, and prints it with the token removed when it fails: +Open the app in a browser once with the token in the URL. Three things keep the token away from other local accounts, and the recipe does all of them: + +- The token goes to the browser on stdin: node builds the URL and pipes it to `agent-browser batch`, so the token is in no command line. +- `agent-browser` starts a WebSocket stream on a loopback port for every session. It takes mouse and keyboard input with no credential, so any local account could drive the authenticated page. The recipe opens `about:blank`, turns the stream off (this also closes clients that are already connected) and checks that it is off before the token goes in. +- The batch prints the URL it opened, with the token. The recipe keeps that output in a variable: it prints nothing when the open works, and prints it with the token removed when it fails. ```bash -out=$(node -e ' +# One named session, so that later agent-browser commands reach the same page. +export AGENT_BROWSER_SESSION=${AGENT_BROWSER_SESSION:-xum-sandbox} +if agent-browser open about:blank >/dev/null && + { agent-browser stream disable >/dev/null 2>&1; agent-browser --json stream status | grep -q '"enabled":false'; }; then + out=$(node -e ' const [lock, port] = process.argv.slice(1); const token = JSON.parse(require("fs").readFileSync(lock, "utf8")).token; const url = `http://localhost:${port}/?token=${encodeURIComponent(token)}`; process.stdout.write(JSON.stringify([["open", url]])); ' "$XUM_ROOT/server.lock" "$VITE_PORT" | agent-browser batch --bail 2>&1) || - { sed 's/token=[^&[:space:]]*/token=/g' <<<"$out" >&2; false; } + { sed 's/token=[^&[:space:]]*/token=/g' <<<"$out" >&2; false; } +else + echo "agent-browser: could not turn its stream off, so the token was not sent" >&2; false +fi ``` The app keeps the token and strips it from the address bar, so later `agent-browser` commands carry no token. Without it, the app asks for the token. Do not use `agent-browser open ""`: it puts the token in the command line. Do not pipe the batch straight to the terminal: its output repeats the URL with the token. +The stream is on again in every new `agent-browser` daemon: after `agent-browser close`, or after the daemon idles out, run the recipe again before you open the app. Check with `agent-browser stream status`. + +Limit: Chrome itself listens on a loopback DevTools port (`--remote-debugging-port=0`, set by `agent-browser`, which cannot start Chrome without it). That port has no credential either, so while the authenticated page is open in `agent-browser`, any local account can drive it, read its storage and open a terminal in the sandbox. Turning the stream off removes only one of the two open ports. On a host with other accounts, call the backend with `curl` or `xum api` as above and do not open the authenticated page. + - `XUM_ROOT= xum api ...` reads the token from the lockfile. - The Vite proxy adds no credentials, so a request to the Vite port needs the token too. - Keep `XUM_ROOT` outside the checkout. Vite serves the checkout to anyone who can reach its port, and its `public/` folder without any file checks. The sandbox root is under the system temp dir. diff --git a/docs/AGENTS.md b/docs/AGENTS.md index de0dbebd495..eeaae2f687d 100644 --- a/docs/AGENTS.md +++ b/docs/AGENTS.md @@ -77,6 +77,8 @@ Core workflow: 3. `agent-browser click @e1` / `fill @e2 "text"` - Interact using refs 4. Re-snapshot after page changes +Never `open` a URL that carries a token or another credential: it is readable in the command line, and every `agent-browser` session has a loopback WebSocket stream and Chrome has a DevTools port, with no credential on either. To open an authenticated dev-server sandbox, use the recipe in the `dev-server-sandbox` skill. + ## PR Workflow - When checking PR readiness, audit **all** PR reviews, review comments, and issue comments from every reviewer/bot (including `coder-agents-review`), not just Codex; address or explicitly resolve them before declaring readiness. diff --git a/src/node/services/agentSkills/builtInSkillContent.generated.ts b/src/node/services/agentSkills/builtInSkillContent.generated.ts index 5dbc4c4b6a0..d65986dbfb6 100644 --- a/src/node/services/agentSkills/builtInSkillContent.generated.ts +++ b/src/node/services/agentSkills/builtInSkillContent.generated.ts @@ -2134,6 +2134,8 @@ export const BUILTIN_SKILL_FILES: Record> = { '3. `agent-browser click @e1` / `fill @e2 "text"` - Interact using refs', "4. Re-snapshot after page changes", "", + "Never `open` a URL that carries a token or another credential: it is readable in the command line, and every `agent-browser` session has a loopback WebSocket stream and Chrome has a DevTools port, with no credential on either. To open an authenticated dev-server sandbox, use the recipe in the `dev-server-sandbox` skill.", + "", "## PR Workflow", "", "- When checking PR readiness, audit **all** PR reviews, review comments, and issue comments from every reviewer/bot (including `coder-agents-review`), not just Codex; address or explicitly resolve them before declaring readiness.", From b97205814114e7b3ad41faeb564ee87b2bb8244d Mon Sep 17 00:00:00 2001 From: Thomas Kosiewski Date: Wed, 4 Nov 2026 00:31:46 +0000 Subject: [PATCH 2/4] =?UTF-8?q?=F0=9F=A4=96=20docs:=20say=20that=20attachi?= =?UTF-8?q?ng=20the=20session=20in=20the=20Browser=20tab=20turns=20the=20s?= =?UTF-8?q?tream=20on?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .xum/skills/dev-server-sandbox/SKILL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.xum/skills/dev-server-sandbox/SKILL.md b/.xum/skills/dev-server-sandbox/SKILL.md index 852e950cd32..d88181b67b0 100644 --- a/.xum/skills/dev-server-sandbox/SKILL.md +++ b/.xum/skills/dev-server-sandbox/SKILL.md @@ -97,7 +97,7 @@ fi The app keeps the token and strips it from the address bar, so later `agent-browser` commands carry no token. Without it, the app asks for the token. Do not use `agent-browser open ""`: it puts the token in the command line. Do not pipe the batch straight to the terminal: its output repeats the URL with the token. -The stream is on again in every new `agent-browser` daemon: after `agent-browser close`, or after the daemon idles out, run the recipe again before you open the app. Check with `agent-browser stream status`. +The stream is on again in every new `agent-browser` daemon: after `agent-browser close`, or after the daemon idles out, run the recipe again before you open the app. Check with `agent-browser stream status`. A Xum app that attaches the session in its Browser tab also turns the stream on (`ensureSessionAttachable`): do not attach this session. Limit: Chrome itself listens on a loopback DevTools port (`--remote-debugging-port=0`, set by `agent-browser`, which cannot start Chrome without it). That port has no credential either, so while the authenticated page is open in `agent-browser`, any local account can drive it, read its storage and open a terminal in the sandbox. Turning the stream off removes only one of the two open ports. On a host with other accounts, call the backend with `curl` or `xum api` as above and do not open the authenticated page. From c2adfcf1bd96905e8403bb70487de17b91d46c1e Mon Sep 17 00:00:00 2001 From: Thomas Kosiewski Date: Wed, 4 Nov 2026 00:51:20 +0000 Subject: [PATCH 3/4] =?UTF-8?q?=F0=9F=A4=96=20fix:=20name=20the=20sandbox?= =?UTF-8?q?=20session=20on=20every=20command=20and=20guard=20against=20re-?= =?UTF-8?q?attachment?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Codex review on #7910. The bash tool drops AGENT_BROWSER_SESSION between calls, so the recipe passes --session on every command and the text says to do the same later. The Xum Browser tab attaches a session by itself and turns its stream on, so the text says to close the tab and the recipe checks the stream again after the page loads. The limit names the Windows daemon port. --- .xum/skills/dev-server-sandbox/SKILL.md | 27 +++++++++++++++---------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/.xum/skills/dev-server-sandbox/SKILL.md b/.xum/skills/dev-server-sandbox/SKILL.md index d88181b67b0..3cc7a8e4e88 100644 --- a/.xum/skills/dev-server-sandbox/SKILL.md +++ b/.xum/skills/dev-server-sandbox/SKILL.md @@ -72,34 +72,39 @@ printf 'Authorization: Bearer %s\n' "$TOKEN" | curl -s -X POST -H @- "http://127 Command lines are readable by every local account. So node gets only the lockfile path, and the header goes to `curl -H @-` on stdin from the `printf` builtin. Never pass the token as an argument. -Open the app in a browser once with the token in the URL. Three things keep the token away from other local accounts, and the recipe does all of them: +Open the app in a browser once with the token in the URL. Close the Xum Browser tab of this workspace first: it attaches to the first `agent-browser` session it finds and turns that session's stream on (`ensureSessionAttachable`). Three things keep the token away from other local accounts, and the recipe does all of them: - The token goes to the browser on stdin: node builds the URL and pipes it to `agent-browser batch`, so the token is in no command line. -- `agent-browser` starts a WebSocket stream on a loopback port for every session. It takes mouse and keyboard input with no credential, so any local account could drive the authenticated page. The recipe opens `about:blank`, turns the stream off (this also closes clients that are already connected) and checks that it is off before the token goes in. +- `agent-browser` starts a WebSocket stream on a loopback port for every session. It takes mouse and keyboard input with no credential, so any local account could drive the authenticated page. The recipe opens `about:blank`, turns the stream off (this also closes clients that are already connected) and checks that it is off before the token goes in. It checks again after the page loads, and closes the session if the stream came back. - The batch prints the URL it opened, with the token. The recipe keeps that output in a variable: it prints nothing when the open works, and prints it with the token removed when it fails. ```bash -# One named session, so that later agent-browser commands reach the same page. -export AGENT_BROWSER_SESSION=${AGENT_BROWSER_SESSION:-xum-sandbox} -if agent-browser open about:blank >/dev/null && - { agent-browser stream disable >/dev/null 2>&1; agent-browser --json stream status | grep -q '"enabled":false'; }; then - out=$(node -e ' +S=xum-sandbox # the session name goes on every agent-browser command, here and later +if agent-browser --session "$S" open about:blank >/dev/null && + { agent-browser --session "$S" stream disable >/dev/null 2>&1; agent-browser --session "$S" --json stream status | grep -q '"enabled":false'; }; then + if out=$(node -e ' const [lock, port] = process.argv.slice(1); const token = JSON.parse(require("fs").readFileSync(lock, "utf8")).token; const url = `http://localhost:${port}/?token=${encodeURIComponent(token)}`; process.stdout.write(JSON.stringify([["open", url]])); -' "$XUM_ROOT/server.lock" "$VITE_PORT" | agent-browser batch --bail 2>&1) || - { sed 's/token=[^&[:space:]]*/token=/g' <<<"$out" >&2; false; } +' "$XUM_ROOT/server.lock" "$VITE_PORT" | agent-browser --session "$S" batch --bail 2>&1); then + agent-browser --session "$S" --json stream status | grep -q '"enabled":false' || + { agent-browser --session "$S" close >/dev/null 2>&1; echo "agent-browser: its stream came back on, so the session was closed" >&2; false; } + else + sed 's/token=[^&[:space:]]*/token=/g' <<<"$out" >&2; false + fi else echo "agent-browser: could not turn its stream off, so the token was not sent" >&2; false fi ``` +Pass `--session xum-sandbox` on every later command (`agent-browser --session xum-sandbox snapshot -i`, `... close`). The bash tool does not keep `AGENT_BROWSER_SESSION` between calls: Xum removes it from the environment of the commands it starts, so a command without `--session` reaches the default session, not this page. + The app keeps the token and strips it from the address bar, so later `agent-browser` commands carry no token. Without it, the app asks for the token. Do not use `agent-browser open ""`: it puts the token in the command line. Do not pipe the batch straight to the terminal: its output repeats the URL with the token. -The stream is on again in every new `agent-browser` daemon: after `agent-browser close`, or after the daemon idles out, run the recipe again before you open the app. Check with `agent-browser stream status`. A Xum app that attaches the session in its Browser tab also turns the stream on (`ensureSessionAttachable`): do not attach this session. +The stream is on again in every new `agent-browser` daemon: after `agent-browser --session xum-sandbox close`, or after the daemon idles out, run the recipe again before you open the app. Check with `agent-browser --session xum-sandbox stream status`. -Limit: Chrome itself listens on a loopback DevTools port (`--remote-debugging-port=0`, set by `agent-browser`, which cannot start Chrome without it). That port has no credential either, so while the authenticated page is open in `agent-browser`, any local account can drive it, read its storage and open a terminal in the sandbox. Turning the stream off removes only one of the two open ports. On a host with other accounts, call the backend with `curl` or `xum api` as above and do not open the authenticated page. +Limit: Chrome itself listens on a loopback DevTools port (`--remote-debugging-port=0`, set by `agent-browser`, which cannot start Chrome without it). That port has no credential either, so while the authenticated page is open in `agent-browser`, any local account can drive it, read its storage and open a terminal in the sandbox. On Windows the `agent-browser` daemon also takes commands, without a credential, on a loopback TCP port that follows from the session name. Turning the stream off removes only one of the open ports. On a host with other accounts, call the backend with `curl` or `xum api` as above and do not open the authenticated page. - `XUM_ROOT= xum api ...` reads the token from the lockfile. - The Vite proxy adds no credentials, so a request to the Vite port needs the token too. From 25b6a35ca0357563d55bfed18f1597e2cf36ae17 Mon Sep 17 00:00:00 2001 From: Thomas Kosiewski Date: Wed, 4 Nov 2026 00:59:57 +0000 Subject: [PATCH 4/4] =?UTF-8?q?=F0=9F=A4=96=20fix:=20report=20when=20the?= =?UTF-8?q?=20emergency=20close=20of=20the=20sandbox=20session=20fails?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Codex review on #7910. A failing close no longer prints that the session was closed. --- .xum/skills/dev-server-sandbox/SKILL.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.xum/skills/dev-server-sandbox/SKILL.md b/.xum/skills/dev-server-sandbox/SKILL.md index 3cc7a8e4e88..e626a60a539 100644 --- a/.xum/skills/dev-server-sandbox/SKILL.md +++ b/.xum/skills/dev-server-sandbox/SKILL.md @@ -88,8 +88,12 @@ const token = JSON.parse(require("fs").readFileSync(lock, "utf8")).token; const url = `http://localhost:${port}/?token=${encodeURIComponent(token)}`; process.stdout.write(JSON.stringify([["open", url]])); ' "$XUM_ROOT/server.lock" "$VITE_PORT" | agent-browser --session "$S" batch --bail 2>&1); then - agent-browser --session "$S" --json stream status | grep -q '"enabled":false' || - { agent-browser --session "$S" close >/dev/null 2>&1; echo "agent-browser: its stream came back on, so the session was closed" >&2; false; } + agent-browser --session "$S" --json stream status | grep -q '"enabled":false' || { + agent-browser --session "$S" close >/dev/null 2>&1 && + echo "agent-browser: its stream came back on, so the session was closed" >&2 || + echo "agent-browser: its stream is on and the session could not be closed: close it yourself" >&2 + false + } else sed 's/token=[^&[:space:]]*/token=/g' <<<"$out" >&2; false fi