diff --git a/.xum/skills/dev-server-sandbox/SKILL.md b/.xum/skills/dev-server-sandbox/SKILL.md index a17cd93f0ff..e626a60a539 100644 --- a/.xum/skills/dev-server-sandbox/SKILL.md +++ b/.xum/skills/dev-server-sandbox/SKILL.md @@ -72,20 +72,44 @@ 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. 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. 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 -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 || + 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 +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 --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. 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. - 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.",