Skip to content

Commit 2229e88

Browse files
mchwangclaude
andcommitted
Address second Copilot review: container lock-down, symlinks, cmd wording
- Design doc: lock down agent containers (read-only root, tmpfs scratch, non-root, no capabilities, no host mounts, resource limits) with a start-up self-test; /work is the only lasting writable place. T1 verify steps extended. Fix the last "three checks" to four. - plan-format.md: reject symlink components and .git targets in path and renamed_from; after each run, reject undeclared symlinks, symlinks that leave the repo, and any .git change; run codeboost's git with hooks off. - Both schemas, the guide, and the prompt describe cmd as one program with literal arguments run without a shell. Example P2 command no longer contains a quoted pipe. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 270c7c9 commit 2229e88

6 files changed

Lines changed: 24 additions & 8 deletions

File tree

‎docs/designs/codeboost-plan-indexed-review.md‎

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -212,11 +212,24 @@ It ignores this task's own PR, any draft PRs it opened earlier, and its own comm
212212
- Every invocation records which comments it was given.
213213

214214
**The container (the real safety boundary).** Changed by the engineering review, R1 (answer D2: B). Every invocation, of every phase, runs inside a Docker or Podman container. The agent tool itself runs inside it. The container holds only:
215-
- the task's folder, including its own `.git` (O6), mounted as `/work`. This is the only folder the agent can write to;
215+
- the task's folder, including its own `.git` (O6), mounted as `/work`. This is the only project folder the agent can write to;
216216
- the agent's own sign-in. For Codex, that is its `auth.json` from `CODEX_HOME`, mounted read-only. For Claude, it is a long-lived token made with `claude setup-token`, passed as an environment variable. (On macOS, Claude keeps its normal sign-in in the keychain, which a container cannot read.)
217217

218218
Nothing else from your computer is inside. So `~/.ssh`, `~/.config/gh`, `~/.npmrc`, `~/.aws`, `~/.docker`, and your git credential helper simply are not there. The container's `HOME` is its own empty folder.
219219

220+
**How the container is locked down.** Docker and Podman leave a container's own files writable by default. So codeboost starts every agent container with these settings, and a start-up self-test fails the run if any is missing:
221+
222+
| Setting | Why |
223+
|---|---|
224+
| Read-only root filesystem (`--read-only`) | The agent cannot change the tools in the image, such as `git`, `go`, or the agent CLI itself. |
225+
| Writable scratch only as size-limited in-memory folders (`--tmpfs`): `/tmp` and the empty `HOME`. Both are emptied when the container ends. | Tools that need scratch space still work, and nothing written there outlives the invocation or reaches the task's code. |
226+
| Build caches point inside the scratch folders (for example `GOCACHE`, `npm_config_cache`). | Caches do not need a writable root. |
227+
| Runs as a non-root user; all Linux capabilities dropped (`--cap-drop=ALL`); `--security-opt=no-new-privileges` | The agent cannot become root or use privileged system calls. |
228+
| No `--privileged`, no `--device`, no host network, no Docker socket, and no host mounts other than `/work` and the read-only sign-in file | Nothing on the host is reachable through the container. |
229+
| Limits on processes, memory, and CPU (`--pids-limit`, `--memory`, `--cpus`) | A runaway agent cannot slow down your computer. |
230+
231+
So the only place an agent can change something that lasts is `/work`, and codeboost checks every change there against the declared files before it commits (see "Checks after import" and "After each run" in `docs/plan-format.md`).
232+
220233
**Network (engineering review, R2, answer D3: A).** The container sits on an internal network. A small proxy lets it reach only the agent vendor's API hosts, and the host list is pinned with each image version. As a second layer, codeboost turns off the agent's own ways to reach the web: for Claude, `--disallowedTools WebFetch,WebSearch` and `--strict-mcp-config` with no servers; for Codex, web search is turned off. So a hostile issue cannot make the agent send your code to an outside server.
221234

222235
If Docker or Podman is missing or not running, codeboost runs no agents and tells you what to install. When the container image is built, codeboost pins the `claude` and `codex` versions it tested.
@@ -1552,7 +1565,7 @@ Leave this finding open. ✅ No work now. ✅ Listed as an open decision. ❌ Bu
15521565
15531566
State: approved
15541567
Actual answer: A) Apply this change (answer to D18, 2026-09-22)
1555-
Accepted scope: build step 2 is a read-only review screen (rows, segments, three checks, approvals, per-item conversation; no merging). The merge gate, pre-merge sequence, and `gh` merge become build step 4, right after the go/no-go check. Later steps renumber: running agents 5, planning 6, queue 7, issue list 8. Design sections amended: Build order and the go/no-go check.
1568+
Accepted scope: build step 2 is a read-only review screen (rows, segments, four checks, approvals, per-item conversation; no merging). The merge gate, pre-merge sequence, and `gh` merge become build step 4, right after the go/no-go check. Later steps renumber: running agents 5, planning 6, queue 7, issue list 8. Design sections amended: Build order and the go/no-go check.
15561569
History: an earlier draft of this record bundled the experiment design; it was split into O8 and O9 before being asked.
15571570
15581571
### O9: Writing the go/no-go experiment down before running it
@@ -1862,7 +1875,7 @@ Built from this review's findings. Each task comes from a specific decision abov
18621875
- [ ] **T1 (P1, human: ~3 days / CC: ~1 hour)** — agents — Build the pinned agent container that mounts only `/work` (with its own `.git`) and the agent's sign-in
18631876
- Surfaced by: R1 (D2: B), O6 (D16: A)
18641877
- Files: agents/container/, git/clone
1865-
- Verify: real-Docker test shows only `/work` and sign-in; `git status` works inside; the main repo path is absent; changing a task-clone object in disposable repos leaves the source object unchanged
1878+
- Verify: real-Docker test shows only `/work` and sign-in; writing to `/`, `/usr/bin`, or the image's tools fails; `/tmp` and `HOME` are empty in each new container; the process is not root and has no capabilities; the start-up self-test refuses a container missing any lock-down setting; `git status` works inside; the main repo path is absent; changing a task-clone object in disposable repos leaves the source object unchanged
18661879
- [ ] **T2 (P1, human: ~1 day / CC: ~30 min)** — agents — Add the vendor-only egress proxy and turn off web and MCP tools
18671880
- Surfaced by: R2 (D3: A)
18681881
- Files: agents/network/, agents/claude, agents/codex

‎docs/plan-format.md‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@
6161

6262
| Field | Type | Rule |
6363
|---|---|---|
64-
| `type` | `cmd` or `check` | `cmd` runs in the agent's container and passes when it exits with 0. `check` is a statement the review agent judges. |
64+
| `type` | `cmd` or `check` | `cmd` is one program and its literal arguments, such as `go test ./... -run TestRetry`. codeboost runs it in the agent's container without a shell, so pipes, redirects, `;`, `&&`, `$( )`, and variables are not allowed. It passes when it exits with 0. `check` is a statement the review agent judges. |
6565
| `text` | text | The command, or the statement. |
6666

6767
**Every field is always present.** A field with nothing to say is `null` or `[]`, never left out. This is what lets the same schema work with both agents' strict answer modes.
@@ -100,6 +100,7 @@ The schema checks the shape. codeboost then checks the meaning. A **failure** bl
100100
| Item IDs are unique. | Failure |
101101
| Every `depends_on` ID exists, comes earlier in the list, and there is no loop. | Failure |
102102
| A path has no `..` part and stays inside the repo. | Failure |
103+
| No part of `path` or `renamed_from` is a symlink in the projected state, and neither points into `.git`. codeboost checks each folder and the file itself without following links. A plan that needs to change a symlink must declare the link itself; its target is never edited through it. | Failure |
103104
| File operations are valid in the projected repo state immediately before the item runs (see below). `edit` and `delete` need an existing path; `add` needs an unused path; `rename` needs an existing `renamed_from` and an unused destination `path`. | Failure |
104105
| `renamed_from` is set only for kind `rename`. | Failure |
105106
| The same path is not declared twice in one item. | Failure |
@@ -108,6 +109,8 @@ The schema checks the shape. codeboost then checks the meaning. A **failure** bl
108109
| A `cmd` changes a dependency or a script codeboost runs. | The task stops in "needs approval" when it runs, as for any such change |
109110
| `questions` is not empty. | The plan shows the questions at the top; answer them or approve anyway |
110111

112+
**After each run, codeboost checks the result too.** A plan check alone cannot stop an agent from creating a new symlink and writing through it. So after each invocation, before committing, codeboost: rejects any change outside the declared files (as today, out of scope); rejects a new or changed symlink that the item did not declare, and any symlink whose target leaves the repo or enters `.git`; and rejects any change to `.git` itself (config, hooks, refs, objects other than new ones). codeboost's own git commands on the task folder run with hooks turned off (`core.hooksPath=/dev/null`), so a planted hook never runs.
113+
111114
**Projected file state.** Start with the paths at the plan's base commit, then walk items in their listed execution order. Check an item's file operations against the state before that item; after it passes, apply its declared additions, deletions, and renames to the projected state before checking the next item. No repo files change during validation. A path may participate in only one operation per item, counting both the source and destination of a rename. If an item uses a path created or renamed by an earlier item, it must depend on that item, directly or through other dependencies.
112115

113116
For example, P1 may add `src/new.go`, then P2 with `depends_on: [P1]` may edit it. Likewise, P1 may rename `src/old.go` to `src/new.go`, then P2 may edit the new path. Editing a missing path, adding an existing path, or renaming onto an occupied path blocks approval. Recompute the projected state from the base commit after each plan edit.

‎prompts/plan-author.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ Write revision {{revision}} of the plan for issue {{issue_number}}. Follow these
5858
1. **One concern per item.** Split unrelated changes into separate items. Keep tests for a change in the same item, or in a test item that depends on it. Put docs changes in their own item.
5959
2. **Declare every file.** List every file the item will add, edit, rename, or delete. The carrying-out agent may touch only declared files. If you are not sure a file needs to change, declare it and say why in `change`.
6060
3. **Say what changes, file by file.** In each file's `change`, name the functions and behavior that change. Do not give line numbers.
61-
4. **Make it checkable.** Give every item at least one acceptance entry. Prefer a `cmd` built from the allowed commands, so codeboost can run it. Add a `check` for behavior a command cannot show.
61+
4. **Make it checkable.** Give every item at least one acceptance entry. Prefer a `cmd` built from the allowed commands, so codeboost can run it. A `cmd` is one program and its literal arguments, run without a shell: no pipes, redirects, `;`, `&&`, `$( )`, or variables. Add a `check` for behavior a command cannot show.
6262
5. **Order and dependencies.** List items in the order they should run. `depends_on` may name only earlier items.
6363
6. **Paths.** Paths start at the repo root, use forward slashes, and never contain `..`. For a new file use kind `add`; for a move use `rename` with `renamed_from`.
6464
7. **Ask, don't guess.** When the issue leaves a real choice open, make the most reasonable plan and put the open choice in `questions`.

‎schema/examples/plan-412-r3.yaml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ items:
4848
change: Add TestJitterStaysInBounds with a fixed seed.
4949
acceptance:
5050
- type: cmd
51-
text: go test ./src/retry/... -run 'Jitter|Retry' -count=3
51+
text: go test ./src/retry/... -count=3
5252
- type: check
5353
text: Delays stay within 75% to 125% of the base delay.
5454
depends_on: [P1]

‎schema/plan-edit.schema.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -275,7 +275,7 @@
275275
],
276276
"properties": {
277277
"type": {
278-
"description": "cmd: a shell command codeboost runs in the container; it passes when it exits 0. check: a statement the review agent judges against the code.",
278+
"description": "cmd: one program and its literal arguments, for example go test ./... -run TestRetry. codeboost splits it into arguments and runs it in the container without a shell, so pipes, redirects, ;, &&, $(), backticks, and variables are not allowed; the program must be on the repo's allowed command list. It passes when it exits 0. check: a statement the review agent judges against the code.",
279279
"type": "string",
280280
"enum": [
281281
"cmd",

‎schema/plan.schema.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@
167167
],
168168
"properties": {
169169
"type": {
170-
"description": "cmd: a shell command codeboost runs in the container; it passes when it exits 0. check: a statement the review agent judges against the code.",
170+
"description": "cmd: one program and its literal arguments, for example go test ./... -run TestRetry. codeboost splits it into arguments and runs it in the container without a shell, so pipes, redirects, ;, &&, $(), backticks, and variables are not allowed; the program must be on the repo's allowed command list. It passes when it exits 0. check: a statement the review agent judges against the code.",
171171
"type": "string",
172172
"enum": [
173173
"cmd",

0 commit comments

Comments
 (0)