docs: row 91 brief, host git treats the clone as hostile; decision 126 (#1585)
Darkwing found the brief had the host run unconfined git in a clone the session writes. The brief now requires a clean git environment, hook and fsmonitor overrides, an lstat walk of .git, a config-key allowlist and a session-live check, and adds bundle.mjs and a harness test to the files. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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-<row>/`
|
||||
|
||||
### 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. `<workspace>/<id>` 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 <f> --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.<name>.remote` and `branch.<name>.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
|
||||
|
||||
Reference in New Issue
Block a user