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
|
- 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
|
been ready for a full working session while row 63 is still under
|
||||||
review. Then Sage looks at it again.
|
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/src/checkout.mjs`, new
|
||||||
- `packages/seat/README.md`, `packages/cli/README.md`, the workspace
|
- `packages/seat/README.md`, `packages/cli/README.md`, the workspace
|
||||||
paragraphs
|
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>/`
|
- `agents/darkwing/work/queue-<row>/`
|
||||||
|
|
||||||
### What ships
|
### What ships
|
||||||
@@ -251,6 +255,47 @@ Reviewer: Filbert.
|
|||||||
launch steps. No partial clone is left behind.
|
launch steps. No partial clone is left behind.
|
||||||
- The bundle's session text names the clone's path, so the session
|
- The bundle's session text names the clone's path, so the session
|
||||||
knows where the project is.
|
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
|
- 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
|
to the root; a dirty clone and one on another branch are kept and
|
||||||
logged; an unreadable root refuses `checkout-failed` with nothing
|
logged; an unreadable root refuses `checkout-failed` with nothing
|
||||||
|
|||||||
Reference in New Issue
Block a user