diff --git a/docs/plans/2026-09-26_lead-decisions.md b/docs/plans/2026-09-26_lead-decisions.md index cc17c15f2..c62c69d09 100644 --- a/docs/plans/2026-09-26_lead-decisions.md +++ b/docs/plans/2026-09-26_lead-decisions.md @@ -2816,3 +2816,25 @@ which stay with him. Each item names who decided it and what happened. - If row 63 needs a round 3, the order still holds unless row 90 has been ready for a full working session while row 63 is still under review. Then Sage looks at it again. +126. **Row 91's host git treats the clone as hostile (2026-10-12).** + Darkwing found that the brief had the host run git, outside the + sandbox, in a clone the session can write. A session could set + `core.fsmonitor` or a hook and make the host's next `git status` or + merge run a command, or plant a symlink or `gitdir:` file so the + fast-forward writes somewhere else. That undoes row 92. + - The brief now requires a clean git environment, `-c` overrides for + hooks, fsmonitor and protocols, an `lstat` check of the clone with + no symlinks, special files or extra hard links under `.git`, and a + config-key allowlist read with `git config --file --list`. A clone + that fails is kept with a reason and no git runs in it. Darkwing + proposed these; Sage added the `.git` walk, the config size cap and + the session-live check, because a session that can still write the + clone could change it between check and use. + - The git step gets no bwrap sandbox. One would have to bind the + root's `.git`, whose remote URL may hold a credential. Filbert + reviews the allowlist as the row's main risk. + - `packages/harness/src/bundle.mjs` (the `prompt()` line naming the + clone, and its doc comment) and a harness test join the files + owned. The harness suite was already in the gate. + - The brief is re-pinned to the edited section, with the old section + byte-identical at 51085ddd and HEAD before the edit. diff --git a/docs/plans/2026-10-12_pm-task-flow.md b/docs/plans/2026-10-12_pm-task-flow.md index e8a1150e3..844832c33 100644 --- a/docs/plans/2026-10-12_pm-task-flow.md +++ b/docs/plans/2026-10-12_pm-task-flow.md @@ -229,7 +229,11 @@ Reviewer: Filbert. - `packages/seat/src/checkout.mjs`, new - `packages/seat/README.md`, `packages/cli/README.md`, the workspace paragraphs -- tests under `packages/seat/tests/` and `packages/cli/tests/` +- `packages/harness/src/bundle.mjs`, the line in `prompt()` that names + the clone's path and the doc comment on the session argument (added + by decision 126) +- tests under `packages/seat/tests/`, `packages/cli/tests/` and + `packages/harness/tests/` - `agents/darkwing/work/queue-/` ### What ships @@ -251,6 +255,47 @@ Reviewer: Filbert. launch steps. No partial clone is left behind. - The bundle's session text names the clone's path, so the session knows where the project is. +- The clone is written by the session, so the host treats it as hostile + before it runs git there (decision 126). Without these guards, a + session could make the host run a command outside the sandbox, which + would undo row 92. + - Every git call runs with `GIT_CONFIG_NOSYSTEM=1`, + `GIT_CONFIG_GLOBAL=/dev/null`, an empty host-made `HOME` and + `GIT_TERMINAL_PROMPT=0`, and with `-c core.hooksPath=/dev/null + -c core.fsmonitor=false -c protocol.allow=never + -c protocol.file.allow=always`. + - Before any git runs in an existing clone, the host checks it with + `lstat` and never follows a link. `/` and its `.git` + are real directories. Nothing under `.git` is a symlink or a special + file, and no regular file there has more than one link. + `.git/commondir` and `objects/info/alternates` are absent. + - The host reads `.git/config`, a regular file under a size cap, with + `git config --file --list`, which follows no include and runs + nothing. Every key is on an allowlist: the keys `git clone` writes, + with `remote.origin.url` equal to the root and the standard fetch + line, plus `branch..remote` and `branch..merge`. + - A clone that fails a check is kept, `checkout-kept` with reason + `not-a-clone` or `config-changed`, and no git runs in it. + - The checks and the git calls run only while no session holds that + workspace, so a session can't change the clone between check and + use. If the launcher already refuses a second live session in one + workspace, the packet cites that refusal and a test shows it. + Otherwise the clone is kept with reason `session-live`. + - A new clone goes to a temporary sibling directory and is renamed + into place, so a failure removes only that directory. + - A root with no branch checked out refuses `checkout-failed` for a + missing clone and keeps an existing one. + - The git step gets no bwrap sandbox of its own. One would have to + bind the root's `.git`, whose remote URL may carry a credential. + The allowlist closes the execution paths instead, and Filbert + reviews it as the row's main risk. +- Tests: one case for each way a session could make the host run + something or write outside the clone: `core.fsmonitor`, + `core.hooksPath`, a hook file, `include.path`, a symlinked clone + directory, a `gitdir:` file, a symlink under `.git` (`logs`, `refs`, + `objects`), alternates and `commondir`. Each case keeps the clone and + shows the command never ran, because the marker file it would write + is absent. The root is unchanged. - Tests: a first launch clones; a second fast-forwards after a commit to the root; a dirty clone and one on another branch are kept and logged; an unreadable root refuses `checkout-failed` with nothing