From a4bcb7a5dd224d4781e8bf0172089442cc055827 Mon Sep 17 00:00:00 2001 From: Artem Matskevych <52060443+Reederey87@users.noreply.github.com> Date: Tue, 7 Jul 2026 16:25:30 -0400 Subject: [PATCH] docs(audits): Pass-7 adversarial audit of the #77-#140 wave + spec truth-up Seventh-pass verification-driven audit (9 dimension reviewers -> per-finding adversarial verification; the 3 P1s cross-verified by a second model family). 22 candidates -> 21 confirmed (P1=3, P2=11, P3=7), 1 refuted. Adds AUDIT_RECOMMENDATIONS_2026-07-07_PASS7.md + the Pass-7 ledger rows, and fixes the four confirmed doc-staleness findings (P7-DOC-01/02/03/05): ARCHITECTURE.md, spec/00, and docs/quickstart.md are re-synced to the shipped OS sandbox + `service` installers. 17 of 21 findings remain open for the Phase-B implementation wave. Co-Authored-By: Claude Opus 4.8 (1M context) --- ARCHITECTURE.md | 12 +- .../AUDIT_RECOMMENDATIONS_2026-07-07_PASS7.md | 441 ++++++++++++++++++ docs/audits/README.md | 29 +- docs/quickstart.md | 12 +- spec/00_START_HERE.md | 2 +- spec/18_WORK_LOG.md | 17 + 6 files changed, 502 insertions(+), 11 deletions(-) create mode 100644 docs/audits/AUDIT_RECOMMENDATIONS_2026-07-07_PASS7.md diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 664daa7..e695aa0 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -137,8 +137,9 @@ built on a moved base. Runs are logged to a `0600` file and tracked in a queryab The wrapper's command/file policy is **guardrails, not a sandbox**. On macOS an OS-enforced Seatbelt sandbox now wraps the child (`--sandbox auto|off|require`): writes are confined to the worktree, credential paths (`~/.ssh`, `~/.aws`, …) are denied, and network is blocked for -read-only policies. Linux OS-level confinement (bubblewrap/landlock/seccomp) is the next slice; -until it lands the Linux wrapper is advisory and says so at run start. +read-only policies. Linux OS-level confinement is shipped too — bubblewrap with a Landlock +fallback plus a seccomp syscall denylist, chosen at run start — so `agent run` is kernel-confined +on both macOS and Linux (containerization remains the one deferred slice). **Depth:** [`spec/10_AGENT_WORKSPACES_AND_POLICIES.md`](spec/10_AGENT_WORKSPACES_AND_POLICIES.md). @@ -146,9 +147,10 @@ until it lands the Linux wrapper is advisory and says so at run start. DevStrap is honest about its edges. Not yet built, by design: -- **The local daemon** (`devstrapd`), its socket API, an FSEvents-specific Mac watcher, and - LaunchAgent/systemd installers — every CLI command works correctly without a daemon; local - reconciliation is the explicit `devstrap scan` plus the portable `run-loop`. +- **The local daemon** (`devstrapd`), its socket API, and an FSEvents-specific Mac watcher — + every CLI command works correctly without a daemon; local reconciliation is the explicit + `devstrap scan` plus the portable `run-loop`. (LaunchAgent/systemd *installers* are shipped as + `devstrap service install|uninstall|status`, which wrap `run-loop` — they are not the daemon.) - **StrapFS** — the optional lazy virtual filesystem, deferred until the product loop is proven. - **A bespoke HTTP/SSE relay** and a hosted control plane for production device enrollment — the git/folder/R2 carriers cover the transport today. diff --git a/docs/audits/AUDIT_RECOMMENDATIONS_2026-07-07_PASS7.md b/docs/audits/AUDIT_RECOMMENDATIONS_2026-07-07_PASS7.md new file mode 100644 index 0000000..f1cfb13 --- /dev/null +++ b/docs/audits/AUDIT_RECOMMENDATIONS_2026-07-07_PASS7.md @@ -0,0 +1,441 @@ +# DevStrap — Design & Implementation Audit (Seventh Pass) + +_Date: 2026-07-07 · Trunk audited: `d667530` (`docs: unattended-operation wave close-out (PRs #136-#139) (#140)`)_ + +## How this relates to the prior audits + +This is the **seventh** design & implementation pass. Pass 6 (2026-07-01, trunk `8c739b8`) closed +43/43. Since then **~40 PRs (#77–#140) landed without an adversarial audit checkpoint**. The ledger +(`docs/audits/README.md`) itself tracked the shipped waves and is current through #140 — but the new +code had never been audited, and two design specs drifted behind it (`spec/00` and `ARCHITECTURE.md` +still called the OS sandbox and the LaunchAgent/systemd installers unbuilt; see `P7-DOC-01/02/03`, +fixed in this PR). This pass **adversarially audits the new #77–#140 code** — the densest cluster of +fast-written, security-critical work the project has shipped in one stretch — and re-syncs those specs: + +1. **The OS-enforced agent sandbox** (`P4-GIT-03`, PRs #107–#129) — macOS Seatbelt, Linux + bubblewrap + Landlock + seccomp, read-confinement, `sandbox.violation` telemetry. This is new + kernel-boundary code and it carries the densest defect cluster in this pass. +2. **The AD-1 zero-infrastructure carrier hub** (PRs #96–#106) — a private-git-repo and a + local/cloud-drive-folder backend behind the pluggable `Hub` interface, now the quickstart default. +3. **The supply-chain + distribution pipeline** (PRs #105–#120) — cosign keyless signing, SLSA v1 + provenance, per-archive SBOMs, the `curl|sh` installer + Homebrew cask, dormant notarization. +4. **The multi-device completeness wave** (PRs #131–#139) — `ENV-SYNC-01` cross-device env-profile + exchange, `TRUST-01` synced device-revocation propagation, self-healing WCK rotation, and + `devstrap service install|uninstall|status`. + +This pass does **not** restate still-open PASS4/PASS5 recommendations; those remain tracked in the +ledger (mapped in **Appendix A**). It concentrates where a fresh pass adds the most value: the surfaces +above, which no prior pass has seen. + +**ID scheme.** Every finding is prefixed `P7-` so its ID is globally unique, per the ledger convention +(`P5-PROC-01`). Dimension codes: `SANDBOX` (OS agent sandbox), `HUB` (carrier hub), `SEC` +(key/trust lifecycle), `SYNC` (sync engine), `SUPPLY`/`QUAL` (supply-chain/CI), `SVC` (service manager), +`CLI` (CLI/UX), `DATA` (data model), `DOC` (specs/docs/process). + +## Methodology + +Findings were produced by a verification-driven multi-agent workflow against the `d667530` worktree, +mirroring the Pass-6 method: **nine dimension reviewers**, each told exactly which PASS4/5/6 IDs are +*shipped vs open* (so they hunt **new** issues on the #77–#140 surfaces), then **every candidate finding +was independently adversarially verified** by a separate agent that re-opened the cited code, reproduced +the failure from source (several by building a real git worktree or reading the exact deny-set), and tried +to refute it. **The three P1 candidates were additionally cross-verified by a second model family +(gpt-5.5 via Codex)** — the project's dual-review norm applied to the audit itself. Verifier corrections +are folded into every finding: **22 candidates → 21 confirmed/plausible, 1 refuted** (P1=3, P2=11, P3=7). +Two candidates were downgraded at verification (`P7-SUPPLY-01` P1→P3, `P7-SANDBOX-02` scope-narrowed) +and one was upgraded (`P7-SANDBOX-01` P2→P1). External best-practice anchors (five exa-backed research +topics on agent-sandbox escapes, SLSA/cosign verification, git-carrier concurrency, and E2E-encrypted +git services) are cited inline and collected at the end. + +**Severity:** P1 = correctness/security/data-loss or major; P2 = significant; P3 = minor/polish/DX. +**Effort:** S ≈ <½ day, M ≈ 1–3 days, L ≈ ~1 week, XL ≈ multi-week. + +## Executive summary + +The #77–#140 wave is real, disciplined progress — every obvious hardening surface is already closed +(the carrier already fetch-resets-and-retries on non-fast-forward, `--force-with-lease`es compaction, and +carries an adversarial test; the Seatbelt profile already denies `~/.ssh`/`~/.aws`/`.netrc`; the cosign +verify recipe already pins `--certificate-identity` + `--certificate-oidc-issuer`; release permissions are +per-job-scoped). So this pass found **no gaping holes — the value is in the subtle gaps a fast wave leaves +behind**, and there are three that matter. + +Risk clusters in four themes: + +1. **The default-on sandbox quietly breaks the core agent loop.** `P7-SANDBOX-01` (P1): the write + allow-list grants only the worktree dir and tmp dir, but a DevStrap agent worktree is a git *linked* + worktree whose index/HEAD/objects/refs live in the **parent clone's `.git`** — outside both. Under the + default `guarded` policy with `--sandbox auto` (the common Mac dev host), any `git add`/`git commit` + the agent runs is EPERM'd by the kernel, so `agent pr` has no commits to push. All three backends share + the gap, and the only e2e canary commits into a *fresh nested* repo, so it never exercises the linked-worktree + path and the bug is untested. This silently defeats the "agent commits → `agent pr` pushes" promise. + +2. **Two multi-device data paths lose or leak data at the edges.** `P7-DATA-01` (P1): the revoke-triggered + env-blob rewrap is the **one** env-profile writer that skips the source-coordinate compare-and-swap that + `events.go` and `snapshot_import.go` both apply, so a concurrent newer `env.profile.updated` can be + clobbered by stale re-encrypted content. `P7-SEC-01` (P1): a device that recovers via a hub snapshot + never learns of a `device.revoked` compacted below the retention floor — the snapshot carries no trust + state — so it keeps the revoked device `approved` and its next WCK rotation **re-grants the fresh epoch + to the revoked device**, defeating `TRUST-01` forward-secrecy-on-revoke for the stale-approved recovering + replica. `P7-SYNC-02` (P2): env profiles are encrypted only to devices approved *at capture time*, with no + rewrap-on-approve, so a later-enrolled device gets the pointer and blob but can never decrypt it. + +3. **The new carrier + service surfaces have small correctness edges.** Folder-carrier objects are written + truncate-then-stream with no temp+rename (`P7-HUB-01`), exposing torn reads to cloud-drive replication; + the shared stale-lock breaker is a non-atomic Stat-then-Remove that can double-grant the carrier lock + (`P7-HUB-02`); `devstrap service` bakes an unvalidated/Cellar-resolved exec path into the unit + (`P7-CLI-01`/`P7-SVC-01`) and a single global `DefaultLabel` silently overwrites a second workspace's + service (`P7-SVC-02`); a transient blob GET permanently strands a referenced blob because the pointer + event still consumes the cursor (`P7-SYNC-01`). + +4. **The specs and gates drifted the same wave they documented.** `spec/00`'s "Not implemented yet" block + and `ARCHITECTURE.md` still call the OS sandbox and LaunchAgent/systemd installers unbuilt after they + shipped (`P7-DOC-01/02/03`); the convergence property generator never grew + the new `env.profile.updated`/`draft.snapshot.created` event types (`P7-SYNC-03`); and the credential + deny-set the OS sandbox enforces drifted from the advisory scanner's set (`.snowflake` missing, `P7-SANDBOX-02`). + +The near-term imperative: **grant the linked-worktree git-common-dir in all three sandbox backends** +(`P7-SANDBOX-01`) so the default agent loop works; **add the CAS guard to the revoke rewrap and rewrap-on-approve +for env profiles** (`P7-DATA-01`, `P7-SYNC-02`); and **carry device-trust state in the snapshot** (`P7-SEC-01`) +so revocation survives compaction. Then close the carrier/service edges and re-sync the specs. + +## Findings at a glance + +| Dimension | P1 | P2 | P3 | Total | +|---|---|---|---|---| +| OS Agent Sandbox | 1 | 1 | 1 | 3 | +| Carrier Hub | 0 | 2 | 0 | 2 | +| Key & Trust Lifecycle | 1 | 0 | 1 | 2 | +| Sync Engine | 0 | 3 | 0 | 3 | +| Service Manager | 0 | 2 | 0 | 2 | +| CLI, UX & Data | 1 | 1 | 2 | 4 | +| Supply-Chain & CI | 0 | 0 | 1 | 1 | +| Specs, Docs & Process | 0 | 2 | 2 | 4 | +| **Total** | **3** | **11** | **7** | **21** | + +> One further candidate (`P7-DOC-04`) was **refuted** at verification: spec/00's "remaining core-engine +> candidate" phrasing for `P4-GIT-03` is accurate — spec/14 itself marks it `[~]` partial (only +> containerization remains), so there is no contradiction. + +## Prioritized roadmap + +| # | Sev | ID | Recommendation | Dim | Effort | +|---|---|---|---|---|---| +| 1 | P1 | P7-SANDBOX-01 | Grant the linked worktree's git-common-dir write access in all three sandbox backends (+ an e2e that commits in a real linked worktree) | Sandbox | M | +| 2 | P1 | P7-DATA-01 | Add the `EnvProfileSourceCoords`/`envCoordLess` CAS guard to the revoke rewrap writer | Data | M | +| 3 | P1 | P7-SEC-01 | Carry device-trust/revocation state in the snapshot so revocation survives compaction | Sec | L | +| 4 | P2 | P7-SYNC-02 | Rewrap env blobs on device approve, not only on revoke | Sync | M | +| 5 | P2 | P7-SYNC-01 | Retry stranded referenced blobs on later syncs; don't consume the pointer's cursor when its blob is missing | Sync | M | +| 6 | P2 | P7-HUB-01 | Write folder-carrier objects temp+rename (atomic) | Hub | S | +| 7 | P2 | P7-HUB-02 | Make the stale-lock breaker atomic (rename/O_EXCL), not Stat-then-Remove | Hub | S | +| 8 | P2 | P7-CLI-01 | Validate `--exec-path` exists+executable before writing the unit | Svc/CLI | S | +| 9 | P2 | P7-SVC-01 | Don't `EvalSymlinks` the exec path into the Homebrew Cellar; keep the stable install path | Svc | M | +| 10 | P2 | P7-SVC-02 | Derive the service label from `--home`/workspace id; refuse to overwrite a different workspace's unit | Svc | M | +| 11 | P2 | P7-SANDBOX-02 | Add `.snowflake` (and `.config/gcloud`/`.azure`) to the OS-sandbox credential deny-set; unify with the advisory scanner set (`AGEN-05`) | Sandbox | S | +| 12 | P2 | P7-SYNC-03 | Fold `env.profile.updated`/`draft.snapshot.created` into the convergence property/model-check generator | Sync | M | +| 13 | P2 | P7-DOC-02 | ARCHITECTURE.md: Linux sandbox shipped, not "next slice" *(fixed in this PR)* | Doc | S | +| 14 | P2 | P7-DOC-03 | spec/00 "Not implemented yet": OS sandbox shipped *(fixed in this PR)* | Doc | S | +| 15 | P3 | P7-SUPPLY-01 | Have `install.sh` cosign-verify when `cosign` is present, or point curl\|sh users at the signed path | Supply | S | +| 16 | P3 | P7-SEC-02 | Owed-rotation retry: warn-and-continue on a transient epoch read, don't fail the sync | Sec | S | +| 17 | P3 | P7-SANDBOX-03 | Exclude credential files inside allowed `$HOME` build caches (`~/.cargo/credentials.toml`, …) under read-confine | Sandbox | S | +| 18 | P3 | P7-CLI-02 | `service install`/`uninstall` honor `--json` like `service status` | CLI | S | +| 19 | P3 | P7-CLI-03 | Classify a bad `--label` as `exitUsage(10)`, not `exitGeneric(1)` | CLI | S | +| 20 | P3 | P7-DOC-01 | ARCHITECTURE.md: `service` installers shipped, not "not built" *(fixed in this PR)* | Doc | S | +| 21 | P3 | P7-DOC-05 | quickstart.md: document the Linux sandbox + `service install` *(fixed in this PR)* | Doc | S | + +--- + +## P1 findings + +### P7-SANDBOX-01 (P1, M) — Sandbox write-allow omits the linked worktree's shared git dir → `git commit` EPERMs inside the default sandbox +`internal/platform/sandbox_profile.go:53-59` (+ `sandbox_bwrap_args.go:68-71`, `sandbox_landlock.go:117-121`) + +**Defect.** All three sandbox backends re-allow writes to exactly `spec.WorktreeDir` and `spec.TmpDir` +and deny everything else. But `createFreshWorktree` (`internal/cli/worktree.go:139`) creates a git +**linked** worktree via `git worktree add` against the `~/Code` clone, so the worktree's index/HEAD/logs +live at `/.git/worktrees//…` and its objects/refs at `/.git/objects` + `.git/refs` +— none of which is under `WorktreeDir` or `TmpDir`. `SandboxSpec` (`sandbox.go:15-55`) has no field for the +git-common-dir, and `agentSandboxSpec` (`agent.go:66-77`) never grants the parent clone's `.git`. + +**Failure scenario (verified, reproduced against a real linked worktree).** Under the default `guarded` +policy with `--sandbox auto` (`agent.go:382-383`) — the common Mac dev host — an agent that runs +`git add`/`git commit` inside its worktree has the write EPERM'd/EROFS'd by the kernel (Seatbelt +`(deny file-write*)`; bwrap `--ro-bind / /`; Landlock RWDirs = only the two dirs). The commit fails, so +`pushAgentBranch` (`agent.go:949`) does a bare `git push` of an unchanged branch — the "agent commits → +`agent pr` pushes" loop produces nothing, and the failure surfaces as a confusing EPERM buried in agent +output, not a loud error. The only sandbox+git e2e canary (`sandbox_landlock_e2e_test.go:82`) does +`git init -q sc && … commit` — a fresh *nested* repo, which never touches a shared parent `.git`, so the +linked-worktree path is untested. + +**Verifier ruling.** CONFIRMED; **upgraded P2→P1** (opus verifier independently reproduced the write +targets with a real `git worktree add`+`commit`; confirmed all three backends and the missing spec field; +traced the caller chain). This is the default configuration and it silently breaks the product's core loop. + +**Fix.** Resolve the worktree's `git rev-parse --git-common-dir` (and the per-worktree +`.git/worktrees/` dir) and add them to the write allow-list in all three backends; plumb a +`GitCommonDir` onto `SandboxSpec`. Add an e2e that commits inside a real linked worktree under each +sandbox. *Anchor:* the Codex/Claude-Code sandboxes carve `.git` **read-only within writable roots** and +still allow the index/objects writes commits need — the inverse of the DevStrap gap. + +### P7-DATA-01 (P1, M) — Revoke-triggered env-blob rewrap overwrites a concurrently-updated env profile (no CAS guard) +`internal/cli/blob_gc.go:93-96, 248-260` + +**Defect.** `rewrapEnvBlob` reads the affected env profiles once (`EnvProfilesForBlobRef`, line 94), then +in a *later, separate* transaction unconditionally re-emits and upserts them via `emitSupersedingEnvProfile` +→ `tx.UpsertEnvProfileTx` (lines 248-260) — with **no** check of the profile's current source-event +coordinates. Every other env-profile writer in the ENV-SYNC-01 wave guards with a compare-and-swap: +`internal/sync/events.go:897-903` and `internal/sync/snapshot_import.go:142-148` both call +`tx.EnvProfileSourceCoords` + `envCoordLess` and return early if the local coordinates dominate. The +revoke rewrap is the one writer that skips it. + +**Failure scenario (verified by both model families).** The profiles are read before any transaction; +between that read and the rewrap write, a concurrent `sync` applies a newer `env.profile.updated` (each in +its own `WithTx`). The rewrap then overwrites name/provider/mode and **deletes+reinserts all bindings** +(`store.go:1755-1797`) from the stale snapshot plus the new blob ref, clobbering the newer profile with +stale content. `MaxOpenConns(1)`+`_txlock=immediate` serialize individual transactions but do **not** make +the read and the later write one atomic read-modify-write. + +**Verifier ruling.** CONFIRMED by opus (P2, race-window-bounded) and independently by gpt-5.5 (P1, +data-loss class, with line-level evidence). Recorded **P1**: silent overwrite of a newer env profile is +data-loss, and it is the lone writer missing the guard the wave established. + +**Fix.** Wrap the read+rewrap in one transaction (or re-read inside the tx) and apply the same +`EnvProfileSourceCoords`/`envCoordLess` guard the other two writers use before upserting. + +### P7-SEC-01 (P1, L) — Synced device revocations are lost on snapshot recovery → revoked device re-admitted to go-forward keys +`internal/sync/snapshot_build.go:20-107`, `snapshot_import.go:39-75` + +**Defect.** `TRUST-01` (#132) propagates `device.revoked` through the event log only +(`events.go:916-947` → `ApplyRemoteDeviceTrustTx`). `BuildSnapshot` captures workspace id, producer, HLC, +floor, namespace entries, tombstones, and anchors — **no device/trust field** (the wire `Snapshot` struct, +`snapshot.go:81-99`, has none). `ImportSnapshot` never touches the `devices` table. Recovery pulls only +from `floor-1` (`snapshot_recovery.go:88-105`), and `CompactEventsBelow` deletes events below the floor +(`hub.go:124-127`), so a `device.revoked` compacted below the floor is unreachable, and the signed +retention manifest carries no trust state either. + +**Failure scenario (verified by both families).** A replica C that already has device B marked `approved` +locally recovers via a snapshot (or joins after `hub compact`) whose floor is above B's `device.revoked`. +C never learns B was revoked, keeps B `approved`, and its next WCK `Rotate` grants the **fresh epoch** to +B (`ApprovedRecipients`, `keyring.go:358-408`) — defeating forward-secrecy-on-revoke. gpt-5.5 refined the +scope: the confirmed failure is the **stale-approved recovering replica**; a fresh empty-registry joiner is +not affected (nothing synthesizes an `approved` row for it). + +**Verifier ruling.** CONFIRMED P1 by both families, with the scope refinement above. + +**Fix.** Include the device trust table (at minimum the revoked/lost set, with signing-key ids) in the +snapshot, verified against the producing device's identity on import (mirror the fail-closed producer check +the snapshot already applies), so a compacted revocation is reconstructed on recovery before the next rotation. + +--- + +## P2 findings + +### P7-SYNC-02 (P2, M) — Env profile is undecryptable by any device approved *after* capture (no rewrap-on-approve) +`internal/cli/env.go:206-227` + +`captureEnvProfile` encrypts the env bundle to the age-recipient set of devices approved **at capture time** +(`envRecipients` = local device + `ListDevices` filtered to `approved`). Only *revoke* rewraps existing env +blobs; there is no rewrap when a **new** device is later approved. So a device enrolled after capture syncs +the pointer (`env.profile.updated`) and the blob, but is not a recipient and can never decrypt it — the +symmetric hole to the draft plane, which the wave otherwise closed. CONFIRMED P2. **Fix:** on +`devices approve`, rewrap the workspace's env blobs to the enlarged recipient set (the revoke rewrap path is +the model; add the approve trigger). + +### P7-SYNC-01 (P2, M) — Transient blob GET permanently strands a referenced env/draft blob +`internal/cli/sync.go:543-579` + +`pullBlobsByRef` fetches blobs only for the current pull batch; on a `hub.GetBlob` error it does +`missing++; continue`, but the pointer event (`env.profile.updated`/`draft.snapshot.created`) is still +**consumed and advances the per-device Seq cursor**. A transient GET failure (network blip, eventual-consistency +lag on R2/carrier) is therefore never retried on later syncs — the blob is stranded and the profile/draft +stays unmaterializable with no self-heal. CONFIRMED P2. **Fix:** track referenced-but-missing blobs (a +pending set, like the undecryptable/skipped-event records) and re-attempt on subsequent syncs; don't treat +the pointer as fully consumed until its blob is present. + +### P7-HUB-01 (P2, S) — Folder-carrier writes object payloads in-place (no temp+rename) +`internal/hub/gitcarrier.go:802-822` (`fsObjectStore.PutObject`/`PutObjectIfMatch`) + +`PutObject` does a truncate-then-stream `MkdirAll`+write directly into the store root. For `FolderHub`, that +root is the shared Dropbox/iCloud/Google-Drive folder, so a peer's sync daemon can replicate and another +device can read a **partially-written** object. CONFIRMED P2. **Fix:** write to a sibling temp file and +`os.Rename` into place (atomic on POSIX); this also fixes the conditional-put read-back to see whole objects. + +### P7-HUB-02 (P2, S) — Stale-lock breaker can double-grant the carrier lock +`internal/hub/folder.go:398-447` (`fsLock.acquire`) + +On `EEXIST`, the breaker does a non-atomic `os.Stat`-then-`os.Remove` of the lock path when it looks stale. +Two waiting processes can both observe staleness, both `Remove`, and both re-create → both believe they hold +the single-writer lock, defeating the discipline the git and folder carriers depend on. The in-process +`l.mu` only serializes goroutines within one process. CONFIRMED P2. **Fix:** break the lock by `Rename`-ing +the stale file to a unique name and only proceeding if *your* rename won (compare-and-swap), or recreate via +`O_CREATE|O_EXCL` and treat `EEXIST` as "someone else won." + +### P7-SVC-01 (P2, M) — `resolveServiceExecPath` resolves through the Homebrew Cellar symlink → unstable versioned path in the unit +`internal/cli/service.go:213-225` + +It calls `filepath.EvalSymlinks(os.Executable())` before baking the result into `ServiceSpec.ExecPath`. For a +Homebrew install that resolves the stable `/opt/homebrew/bin/devstrap` symlink down to the version-pinned +`Cellar/devstrap//bin/devstrap` — so the next `brew upgrade` leaves the plist/unit pointing at a +deleted path, the opposite of the "stable location" the function's own error text asks for. CONFIRMED P2. +**Fix:** bake the *unresolved* stable path (or the caller-provided `--exec-path`) into the unit; only +`EvalSymlinks` to *validate* existence, not to rewrite. + +### P7-SVC-02 (P2, M) — A single global `DefaultLabel` silently overwrites a second workspace's service +`internal/platform/service_darwin.go:25`, `service_linux.go:25` + +`DefaultLabel()` returns one hardcoded string with no dependency on `--home`/`--root`/workspace id, and +`service.go` falls back to it whenever `--label` is omitted. Installing a service for a second `--home` +overwrites the first workspace's plist/unit (same label) with no persisted record of which home a label was +installed for. CONFIRMED P2. **Fix:** derive the default label from the workspace id (or `--home` hash); +refuse to overwrite a unit whose recorded home differs, unless `--force`. + +### P7-CLI-01 (P2, S) — `service install` bakes an unvalidated `--exec-path` into the unit +`internal/cli/service.go:206-226` + +`resolveServiceExecPath` accepts any absolute `--exec-path` verbatim with no `os.Stat`/executable check +(the implicit default is validated only incidentally by `EvalSymlinks` requiring existence), and neither +`service_darwin.go` nor `service_systemd.go` checks `ExecPath` existence — so a typo silently installs a +service that always fails to start. CONFIRMED P2. **Fix:** stat + executable-bit check the exec path before +writing the unit; fail with a usage error naming the bad path. + +### P7-SANDBOX-02 (P2, S) — OS-sandbox credential deny-set drifted from the advisory scanner set +`internal/platform/sandbox_profile.go:7` + +`sensitiveHomeDirs` (the sole anchor for the OS-enforced credential deny across all three backends) is +`{.ssh, .aws, .gnupg, .config/gh, .kube, .docker}` — it omits `~/.snowflake`, which the *advisory* layer +explicitly protects (`agent.go:622`, cited "AGEN-05: match the scan detector's set so the deny list and +scanner cannot drift"). So under `guarded`, an agent that spawns a subprocess to sidestep the argv-substring +advisory check can read `~/.snowflake/config.toml` despite the sandbox being active — the exact cross-layer +drift `AGEN-05` warns against, now between advisory and the kernel boundary. CONFIRMED P2 (verifier +narrowed: `.config/gcloud`/`.azure` are protected by *no* layer, so they are a pre-existing gap to add, not +"drift"). **Fix:** add `.snowflake` (and, while here, `.config/gcloud`/`.azure`) to `sensitiveHomeDirs`; +unify the OS deny-set and the advisory scanner set behind one source per `AGEN-05`. + +### P7-SYNC-03 (P2, M) — New applied event types absent from the convergence property/model-check generator +`internal/sync/property_helpers_test.go:72` + +The `P4-QUAL-02` property/model-check harness folds `genEventSet` over `Decide`, which handles only +`project.added/updated/deleted`. The new inline-applied `env.profile.updated` and `draft.snapshot.created` +LWW/idempotency logic (exactly the fast-written new code) is exercised only by hand-written example tests — +an unverified convergence gap. CONFIRMED P2. **Fix:** extend the generator to emit the new event types and +assert their convergence + duplicate-delivery idempotency, closing the property coverage the wave should +have grown with it. + +### P7-DOC-02 (P2, S) — ARCHITECTURE.md says the Linux OS sandbox is still "the next slice" *(fixed in this PR)* +`ARCHITECTURE.md:140-141` + +Reads "Linux OS-level confinement (bubblewrap/landlock/seccomp) is the next slice; until it lands the Linux +wrapper is advisory and says so at run start." — but bubblewrap + Landlock + seccomp shipped +2026-07-05 (PRs #121–#129). CONFIRMED P2 (a security capability materially understated). Fixed in this PR. + +### P7-DOC-03 (P2, S) — spec/00's "Not implemented yet" block still lists OS-enforced agent sandboxing *(fixed in this PR)* +`spec/00_START_HERE.md:161` + +"- OS-enforced agent sandboxing, project-env allowlists, and non-generic engine adapters;" sits under +"Not implemented yet (genuinely unbuilt…)". The OS-enforced sandbox shipped (`P4-GIT-03`, +Seatbelt+bwrap+Landlock+seccomp); only containerization remains (`[~]` in spec/14). CONFIRMED P2. Fixed in +this PR by moving the sandbox out of the unbuilt block and qualifying it "shipped except containerization." + +--- + +## P3 findings + +### P7-SUPPLY-01 (P3, S) — The recommended `curl|sh` installer never checks the cosign signature it advertises +`scripts/install.sh:65-90` + +`install.sh` (the headline one-line install path) verifies the archive's sha256 against `checksums.txt` +downloaded from the **same, unauthenticated** release — it never invokes `cosign verify-blob` on +`checksums.txt.sigstore.json`. So an attacker who can serve/replace release assets slips a consistent +`(archive, checksums.txt)` pair past this path; the keyless signature protects nothing here. **Downgraded +P1→P3** by both families: README scopes `install.sh` to "verifies against checksums.txt" and the signed-verify +recipe is a cleanly-separated manual section, so it's a defense-in-depth gap on an optional convenience path, +not a broken promise. **Fix:** when `cosign` is on `PATH`, have `install.sh` verify the bundle at the pinned +identity; otherwise print a one-line pointer to the manual signed-verify path. *Anchors:* SLSA v1.1 +verifying-artifacts; goreleaser example-supply-chain verify recipe. + +### P7-SEC-02 (P3, S) — Owed-rotation retry turns a transient epoch-read error into a fatal sync +`internal/cli/sync.go:356-371` + +In the self-healing owed-rotation retry (#137), a transient error reading the current epoch after a failed +`Rotate` is treated as fatal even in the `pending` path, aborting the whole sync cycle and blocking the +queued `device.revoked` from propagating — contradicting #137's stated intent that an early retry failure +should warn and let the cycle continue. CONFIRMED P3. **Fix:** warn-and-continue on the transient read in +the `pending` path; only fail when the rotation is genuinely required this cycle. + +### P7-SANDBOX-03 (P3, S) — Read-confinement re-exposes credential files inside allowed `$HOME` build caches +`internal/platform/sandbox_read_confine.go:14-16` + +`readConfineHomeCaches` whitelists whole `$HOME` cache dirs (`.cargo`, `.npm`, `.local`, …) for reading, but +some contain credential files not in the deny anchor set (e.g. `~/.cargo/credentials.toml`, +`~/.npmrc` is a file-level deny but `~/.config/*/credentials` under an allowed dir is not), so even the +strictest read-confine mode leaks them. CONFIRMED P3. **Fix:** intersect the cache allow-list with the +credential deny-set (deny-wins), or whitelist specific cache subdirs rather than whole dotdirs. + +### P7-CLI-02 (P3, S) — `service install`/`uninstall` ignore `--json`, unlike `service status` +`internal/cli/service.go:87-96, 130-134` + +`install`/`uninstall` always emit human text via `opts.progressf`; only `status` branches on `--json`. +CONFIRMED P3 (the open `P5-CLI-01`/#112 render-seam gap, now widened by the new command). **Fix:** route +`install`/`uninstall` results through the render seam with a typed JSON payload. + +### P7-CLI-03 (P3, S) — A bad `--label` is misclassified `exitGeneric(1)` instead of `exitUsage(10)` +`internal/cli/service.go:85, 129, 171` + +When `Install`/`Uninstall`/`Status` fail `validateServiceLabel` (e.g. `/` or leading `.` in `--label`), the +error is a bare `return err` (not `platform.ErrUnsupported` and not wrapped in an `appError`), so +`ExitCodeWithWriter` falls through to `exitGeneric(1)` — even though every other flag-validation failure in +the file is deliberately wrapped with a specific non-generic code and `root.go` documents `exitUsage(10)` +for bad-flag errors. CONFIRMED P3. **Fix:** wrap the label-validation error in `appError{exitUsage}`. + +### P7-DOC-01 (P3, S) — ARCHITECTURE.md claims LaunchAgent/systemd installers are "deliberately not built" *(fixed in this PR)* +`ARCHITECTURE.md:148-152` + +The "What is deliberately not built" section lists "LaunchAgent/systemd installers" alongside the daemon — +but `P4-PROD-04` shipped them as `devstrap service install|uninstall|status` (#139). CONFIRMED P3. Fixed in +this PR by moving the installers out of the "not built" list (the resident *daemon* stays correctly listed). + +### P7-DOC-05 (P3, S) — quickstart.md's sandbox section documents only macOS Seatbelt *(fixed in this PR)* +`docs/quickstart.md:127-129` + +Documents only the macOS Seatbelt sandbox, omitting the shipped Linux bubblewrap/Landlock/seccomp stack and +the shipped `devstrap service install` for unattended operation. CONFIRMED P3. Fixed in this PR. + +--- + +## Appendix A — PASS4/PASS5 open items carried forward (not re-audited here) + +These remain the live backlog; see `docs/audits/README.md` for status. After the #77–#140 wave the genuinely +open, buildable remainder is small: `P4-HUB-14` (hub op/byte counters — none exist in `internal/hub`), +`P4-HUB-15` (cost controls/quotas/rate-limiting), `P4-HUB-16` (R2 at-rest versioning/Object-Lock + +backup/replication runbook), `P4-QUAL-04` (Windows build — CI is macOS+Linux only), `P4-SYNC-03` +(`epochFloorMS`>0), `P4-SYNC-05` (signed per-device head), `P4-SYNC-07` (`MaxOpenConns(1)`), `P4-GIT-07` +(persisted materialize-failure record), `P4-SEC-08` (hosted-mode scoped creds — premature until hosted mode), +`P5-CLI-01`/#112 (render seam), `P4-QUAL-07` `contextcheck`/#113, and #111 (clone flag mutual-exclusivity). +Apple notarization is blocked on Developer-ID enrollment; the daemon/HTTP-SSE relay/StrapFS planes stay gated. + +## Appendix B — ledger & spec truth-up state (#95–#140) + +The **ledger** (`docs/audits/README.md`) was already reconciled through #140 by the shipping PRs +themselves (per convention #3): `P4-GIT-03` (OS sandbox, #107–#129), `P4-GIT-04` (worktree GC, #138), +`P4-PROD-04` (`service`, #139), `P4-PROD-05` (distribution, #105/#108), `P4-SEC-05` (narrowed to +notarization-only — cosign/SLSA/SBOM live-verified on v0.1.1, #115/#117/#119), `P4-QUAL-05` +(SBOM+provenance, #115/#117), `P4-SYNC-06` (ack wave), and the `reconcileSamePath` HLC-monotonic winner +(#95) are all in *Recently shipped*; `TRUST-01`/`ENV-SYNC-01`/the unattended-operation wave are noted in +the open-backlog blockquote. This pass adds only the **Pass-7 index row + open table** and a dated note. + +What genuinely **drifted** (and is fixed in this PR) was the design-spec prose, not the ledger: +`spec/00_START_HERE.md` and `ARCHITECTURE.md` still described the OS sandbox and the LaunchAgent/systemd +installers as unbuilt (`P7-DOC-01/02/03`), and `docs/quickstart.md` documented only the macOS sandbox +(`P7-DOC-05`). + +## External best-practice anchors (exa) + +- **Agent sandbox escapes & pitfalls** — "A deep dive on agent sandboxes" (pierce.dev); "Inside the Codex + Sandbox: Platform-Specific Implementation" (danielvaughan) — the zsh-fork bypass class, Seatbelt custom-policy + holes, "workspace root ≠ security boundary", Ubuntu-24.04 AppArmor userns degradation; `cdxgen/safer-exec`. + Informs `P7-SANDBOX-01/02/03`. +- **Supply-chain verification** — SLSA v1.1 "Verifying artifacts"; `slsa-framework/slsa-verifier`; + `goreleaser/example-supply-chain`; cosign keyless issue #2659. Informs `P7-SUPPLY-01`. +- **Git carrier concurrency & E2E-encrypted git** — "High Performance Git" ch.18 (ref locking under write + pressure); the single-committer pattern; git push atomicity (SO); "End-to-End Encrypted Git Services" + (ACM CCS 2025 — confidentiality + repository unforgeability vs a malicious server). Confirmed the carrier's + existing fetch-reset-retry + `--force-with-lease` design is sound; informs `P7-HUB-01/02`. diff --git a/docs/audits/README.md b/docs/audits/README.md index 9844eff..0e9af76 100644 --- a/docs/audits/README.md +++ b/docs/audits/README.md @@ -13,7 +13,8 @@ This directory holds DevStrap's chronological design & implementation audits. ** | 3 | 2026-06-28 | [`AUDIT_RECOMMENDATIONS_2026-06-28.md`](AUDIT_RECOMMENDATIONS_2026-06-28.md) | Cloud-sync architecture: `EAGER-*`, `DRAFT-*`, `HUB-01..08`, `XP-*`, `SCALE-*` | workstreams | `EAGER-*`/`DRAFT-*`/`HUB-01..08`/`XP-*` shipped (PR #16); `SCALE-*` documented-not-built | | 4 | 2026-06-28 | [`AUDIT_RECOMMENDATIONS_2026-06-28_PASS4.md`](AUDIT_RECOMMENDATIONS_2026-06-28_PASS4.md) | Fourth pass: audit of the now-shipped cloud-sync system | 44 (P1=17, P2=23, P3=4) | ~22 shipped/partial across PRs #20/#23/#25 (incl. P4-SEC-02, P4-SEC-07 foundation, P4-SEC-05 partial); remainder open — see below | | 5 | 2026-06-29 | [`AUDIT_RECOMMENDATIONS_2026-06-29_PASS5.md`](AUDIT_RECOMMENDATIONS_2026-06-29_PASS5.md) | Fifth pass: adversarial review of the PASS4 batch + under-examined dimensions + new features | 36 (P1=1, P2=12, P3=23) | 33 shipped (`P5-SYNC-01` 2026-07-03); 3 open — see below | -| 6 | 2026-07-01 | [`AUDIT_RECOMMENDATIONS_2026-07-01_PASS6.md`](AUDIT_RECOMMENDATIONS_2026-07-01_PASS6.md) | Sixth pass: adversarial audit of the PR #24/#25 batch (live R2 hub + envelope-encryption foundation) + under-examined dimensions | 43 (P1=5, P2=25, P3=13) | Open — see below | +| 6 | 2026-07-01 | [`AUDIT_RECOMMENDATIONS_2026-07-01_PASS6.md`](AUDIT_RECOMMENDATIONS_2026-07-01_PASS6.md) | Sixth pass: adversarial audit of the PR #24/#25 batch (live R2 hub + envelope-encryption foundation) + under-examined dimensions | 43 (P1=5, P2=25, P3=13) | Closed (43/43) | +| 7 | 2026-07-07 | [`AUDIT_RECOMMENDATIONS_2026-07-07_PASS7.md`](AUDIT_RECOMMENDATIONS_2026-07-07_PASS7.md) | Seventh pass: adversarial audit of the #77–#140 wave (OS agent sandbox, AD-1 carrier hub, supply-chain/distribution, multi-device env/trust/service) + ledger truth-up | 21 (P1=3, P2=11, P3=7) | 17 open of 21 — see below (4 doc truth-ups fixed in the landing PR) | ## Conventions (going forward) @@ -27,7 +28,7 @@ This directory holds DevStrap's chronological design & implementation audits. ** Currently-actionable findings, pass-scoped. Earlier passes (1–3) are largely implemented or superseded (see `spec/18_WORK_LOG.md` for the shipped history); the open backlog is concentrated in passes 4, 5, and 6. -> **2026-07-01 — Pass 6 landed.** The sixth pass ([`AUDIT_RECOMMENDATIONS_2026-07-01_PASS6.md`](AUDIT_RECOMMENDATIONS_2026-07-01_PASS6.md)) audited trunk `8c739b8` (PR #25) via a verification-driven nine-dimension workflow: 43 findings (P1=5, P2=25, P3=13), each adversarially verified against the code and checked for novelty against this backlog. Headlines: `P6-SEC-01`/`P6-SYNC-01` (the envelope layer still trusts the hub — unverified grant ingestion + whole-batch abort on one bad event), `P6-HUB-01`/`P6-DATA-01` (the now-live hub GC deletes live draft blobs), and `P6-GIT-01` (a universal 2-minute git timeout silently breaks eager materialization of large repos). This pass also **reconciled the ledger** below per convention #3: `P4-SEC-02` moved to *Recently shipped*, `P4-SEC-05` corrected to *partial* (its own finding is `P6-DOC-02`). **2026-07-02 update:** the hub-trust workstream (`P6-SYNC-01`, `P6-SEC-01`, `P6-SEC-02`; PRs #30–#34), `P6-DATA-01` (PR #35), `P6-HUB-01` (PR #36), and `P6-GIT-01` are shipped and moved to *Recently shipped* — **all five Pass 6 P1s are closed** (the AD-2 hardening-freeze P1 wave is complete). **2026-07-03 update:** `P6-DATA-02` is shipped; the open backlog is now the remaining P2/P3 set below. **2026-07-04 — snapshot-exchange wave (PRs #65–#76):** the event log is now BOUNDED — `P4-SYNC-02`/`P4-HUB-11` (compaction + full-state snapshot exchange, both P1s open since Pass 4), `P4-HUB-12`, `P4-SYNC-06` (narrowed), and Pass-6's `P6-HUB-04` shipped, alongside a parallel quick-win sub-wave closing seven Pass-6 P2/P3s (`P6-GIT-02`, `P6-GIT-03`, `P6-CLI-01`, `P6-XP-01`, `P6-QUAL-04`, `P6-QUAL-05`, `P6-DOC-04`; PRs #66–#72). Pass-6 drops 19→11 open. Every code PR carried dual review (coordinator line-by-line + Codex gpt-5.5, several with an additional opus pass); review findings fixed pre-merge include a fail-open hollow-manifest parse (P1), a CAS self-race, a byte-for-byte read-back gate before compaction deletes, a gc stale-list race, and an owner-aware sweep-lock release. **2026-07-03 (evening) — sync-convergence wave (PRs #59–#63):** `P5-SYNC-01` (the last AD-2 hardening-freeze critical — the freeze is COMPLETE), `P6-SYNC-02`, `P6-DATA-03/05/06`, `P6-XP-04`, and `P6-QUAL-01/02` shipped and moved to *Recently shipped*; Pass-6 drops 26→19 open. **2026-07-03 update:** `P6-GIT-02` shipped; Pass-6 drops 19→18 open. A separate 2026-07-03 **pairing wave** (`init --join --workspace-id` adoption, the founder-pinning ceremony, doctor mismatch detection, and the two-device runbook; PRs #48–#50 + the docs PR) closed the cross-device workspace-ID pairing remainder of `P4-SEC-07` and the joiner half of `P4-SEC-04` — both are Pass-4 findings tracked in the Pass 4 table, so the **Pass-6 count is unchanged** (they narrow, not close, and never counted toward the 43). **2026-07-04 update:** `P6-HUB-03` shipped (`fix/p6-hub-03`); Pass-6 drops 6→5 open. `P6-CLI-03` shipped (`fix/p6-cli-03`); 5→4 open. `P6-CLI-04` shipped (`fix/p6-cli-04`); 4→3 open. `P6-DOC-01` test-hardening residual shipped (`fix/p6-doc-01-test`); 3→2 open. `P6-XP-06` shipped (`fix/p6-xp-06`); 2→1 open. `P6-GIT-06` shipped (`fix/p6-git-06`); 1→0 open — **Pass 6 is fully closed (43/43).** **2026-07-05 — next wave chosen:** multi-device completeness — synced env-bundle exchange (`ENV-SYNC-01`) + synced device-trust propagation (`TRUST-01`); direction and backlog rows live in `spec/14_MVP_ROADMAP_AND_BACKLOG.md`. **2026-07-06 — unattended-operation wave COMPLETE (PRs #136–#139):** issues #133/#134 closed, `P4-GIT-04` and `P4-PROD-04` shipped and moved to *Recently shipped*; see the spec/14 DIRECTION bullet. +> **2026-07-01 — Pass 6 landed.** The sixth pass ([`AUDIT_RECOMMENDATIONS_2026-07-01_PASS6.md`](AUDIT_RECOMMENDATIONS_2026-07-01_PASS6.md)) audited trunk `8c739b8` (PR #25) via a verification-driven nine-dimension workflow: 43 findings (P1=5, P2=25, P3=13), each adversarially verified against the code and checked for novelty against this backlog. Headlines: `P6-SEC-01`/`P6-SYNC-01` (the envelope layer still trusts the hub — unverified grant ingestion + whole-batch abort on one bad event), `P6-HUB-01`/`P6-DATA-01` (the now-live hub GC deletes live draft blobs), and `P6-GIT-01` (a universal 2-minute git timeout silently breaks eager materialization of large repos). This pass also **reconciled the ledger** below per convention #3: `P4-SEC-02` moved to *Recently shipped*, `P4-SEC-05` corrected to *partial* (its own finding is `P6-DOC-02`). **2026-07-02 update:** the hub-trust workstream (`P6-SYNC-01`, `P6-SEC-01`, `P6-SEC-02`; PRs #30–#34), `P6-DATA-01` (PR #35), `P6-HUB-01` (PR #36), and `P6-GIT-01` are shipped and moved to *Recently shipped* — **all five Pass 6 P1s are closed** (the AD-2 hardening-freeze P1 wave is complete). **2026-07-03 update:** `P6-DATA-02` is shipped; the open backlog is now the remaining P2/P3 set below. **2026-07-04 — snapshot-exchange wave (PRs #65–#76):** the event log is now BOUNDED — `P4-SYNC-02`/`P4-HUB-11` (compaction + full-state snapshot exchange, both P1s open since Pass 4), `P4-HUB-12`, `P4-SYNC-06` (narrowed), and Pass-6's `P6-HUB-04` shipped, alongside a parallel quick-win sub-wave closing seven Pass-6 P2/P3s (`P6-GIT-02`, `P6-GIT-03`, `P6-CLI-01`, `P6-XP-01`, `P6-QUAL-04`, `P6-QUAL-05`, `P6-DOC-04`; PRs #66–#72). Pass-6 drops 19→11 open. Every code PR carried dual review (coordinator line-by-line + Codex gpt-5.5, several with an additional opus pass); review findings fixed pre-merge include a fail-open hollow-manifest parse (P1), a CAS self-race, a byte-for-byte read-back gate before compaction deletes, a gc stale-list race, and an owner-aware sweep-lock release. **2026-07-03 (evening) — sync-convergence wave (PRs #59–#63):** `P5-SYNC-01` (the last AD-2 hardening-freeze critical — the freeze is COMPLETE), `P6-SYNC-02`, `P6-DATA-03/05/06`, `P6-XP-04`, and `P6-QUAL-01/02` shipped and moved to *Recently shipped*; Pass-6 drops 26→19 open. **2026-07-03 update:** `P6-GIT-02` shipped; Pass-6 drops 19→18 open. A separate 2026-07-03 **pairing wave** (`init --join --workspace-id` adoption, the founder-pinning ceremony, doctor mismatch detection, and the two-device runbook; PRs #48–#50 + the docs PR) closed the cross-device workspace-ID pairing remainder of `P4-SEC-07` and the joiner half of `P4-SEC-04` — both are Pass-4 findings tracked in the Pass 4 table, so the **Pass-6 count is unchanged** (they narrow, not close, and never counted toward the 43). **2026-07-04 update:** `P6-HUB-03` shipped (`fix/p6-hub-03`); Pass-6 drops 6→5 open. `P6-CLI-03` shipped (`fix/p6-cli-03`); 5→4 open. `P6-CLI-04` shipped (`fix/p6-cli-04`); 4→3 open. `P6-DOC-01` test-hardening residual shipped (`fix/p6-doc-01-test`); 3→2 open. `P6-XP-06` shipped (`fix/p6-xp-06`); 2→1 open. `P6-GIT-06` shipped (`fix/p6-git-06`); 1→0 open — **Pass 6 is fully closed (43/43).** **2026-07-05 — next wave chosen:** multi-device completeness — synced env-bundle exchange (`ENV-SYNC-01`) + synced device-trust propagation (`TRUST-01`); direction and backlog rows live in `spec/14_MVP_ROADMAP_AND_BACKLOG.md`. **2026-07-06 — unattended-operation wave COMPLETE (PRs #136–#139):** issues #133/#134 closed, `P4-GIT-04` and `P4-PROD-04` shipped and moved to *Recently shipped*; see the spec/14 DIRECTION bullet. **2026-07-07 — Pass 7 landed** ([`AUDIT_RECOMMENDATIONS_2026-07-07_PASS7.md`](AUDIT_RECOMMENDATIONS_2026-07-07_PASS7.md)): the first adversarial audit of the #77–#140 wave (OS agent sandbox, AD-1 carrier hub, supply-chain/distribution, multi-device env/trust/service), verification-driven nine-dimension workflow with the three P1s cross-verified by a second model family. 22 candidates → **21 confirmed** (P1=3, P2=11, P3=7), 1 refuted. Headlines: `P7-SANDBOX-01` (the default-on sandbox omits the linked worktree's shared `.git`, so `agent` `git commit` EPERMs — the core loop silently breaks), `P7-DATA-01`/`P7-SEC-01` (revoke rewrap skips the env-profile CAS guard; device revocation is lost on snapshot recovery), and `P7-SYNC-02` (no rewrap-on-approve leaves env blobs undecryptable to later-enrolled devices). The four doc truth-ups (`P7-DOC-01/02/03/05` — `spec/00`/`ARCHITECTURE.md`/`docs/quickstart.md` still called the shipped OS sandbox and `service` installers unbuilt) are fixed in the landing PR; **17 open of 21**. @@ -97,6 +98,30 @@ Currently-actionable findings, pass-scoped. Earlier passes (1–3) are largely i | P6-DATA-04 | P2 | `fix/p6-data-04` (2026-07-04) | `devstrap db backup --full ` now captures a recoverable workspace: `state.db` (`VACUUM INTO`), the `blobs/.age` files `AllBlobRefs` reports, the key material (`keys/` under file custody; a custody-aware keychain **escrow** — device age + Ed25519 signing identities + every held WCK epoch from `HeldKeys` + hub S3 creds via `HybridStore.ExportForBackup`, a hard error if any required key is unreadable), AND `config.yaml` (the hub pointer + custom root). New `devstrap db restore ` refuses a non-empty state dir without `--force`, validates the staged `state.db` (`quick_check`+`foreign_key_check`) before promoting, and swaps ONLY the captured targets in place (temp+rename+rollback) so un-captured Home data (`quarantine/`, `logs/`) survives; a zip-slip guard (`sanitizeBackupEntry`) rejects abs/`..`/symlink/out-of-layout entries. New doctor **dangling-blob-refs** check + a keychain-custody restore warning; the `doctor` "restore from a db backup" remedy now points at `--full`. Round-trip test proves `env capture → backup --full → wipe → restore → env hydrate` recovers the identical secret AND config `hub`/`root` survive; blocking review finding (config.yaml omission / whole-Home wipe) fixed. spec/12 disaster-recovery runbook. | | P4-GIT-04 | P2 | `feat/p4-git-04-worktree-gc` (2026-07-06) | `worktree cleanup --merged` now also reaps squash/rebase-merged worktrees: new `git.Runner.IsSquashMerged` — offline content-equivalence via the current-tree merge probe (`git merge-tree --write-tree `, git ≥ 2.38: merged iff the simulated merge's tree equals the CURRENT base tree). The first-draft `git cherry`/patch-id chain was replaced in dual review — historical patch-id equivalence matches merged-then-REVERTED changes and would reap unmerged work (both reviewers reproduced it). Conservative rule: conflicting merge / old git / any error = not merged. Documented accepted limitation: a coincidentally-identical unrelated commit reads as merged (tripwire-pinned); every reap prints the deleted branch's tip SHA as the recovery breadcrumb. Ancestry stays the first check; misses consult content-equivalence, labeled `merged (squash)`; base fetched under the repo lock, deduped per project/ref; reaped branches get `git branch -D` (warn-only). Forge-API (`gh pr list`) cross-checks are an explicit non-goal — cleanup works offline. Pinned by the real-git `TestIsSquashMerged*` matrix (incl. revert-on-base) + CLI e2e `TestWorktreeCleanupReapsSquashMergedWorktree`. | +### Pass 7 (2026-07-07) — 17 open of 21 (P1=3, P2=9, P3=5; 4 doc truth-ups fixed in the landing PR, 1 refuted) + +> Full detail: [`AUDIT_RECOMMENDATIONS_2026-07-07_PASS7.md`](AUDIT_RECOMMENDATIONS_2026-07-07_PASS7.md). The four fixed-in-landing-PR doc findings (`P7-DOC-01/02/03/05` — `ARCHITECTURE.md`/`spec/00`/`docs/quickstart.md` re-synced to the shipped OS sandbox + `service` installers) and the one refuted candidate (`P7-DOC-04`) are **not** in this table, so 21 − 4 = 17 rows. + +| ID | Sev | Finding | Effort | +|---|---|---|---| +| P7-SANDBOX-01 | P1 | Default-on sandbox omits the linked worktree's shared git-common-dir (`/.git`), so agent `git add`/`commit` EPERMs and `agent pr` has nothing to push; grant it in all three backends + a real-linked-worktree e2e | M | +| P7-DATA-01 | P1 | Revoke-triggered env-blob rewrap is the one env-profile writer that skips the `EnvProfileSourceCoords`/`envCoordLess` CAS guard → clobbers a concurrently-updated profile with stale content | M | +| P7-SEC-01 | P1 | Snapshot carries no device-trust state, so a `device.revoked` compacted below the floor is lost on recovery → the stale-approved recovering replica re-grants the fresh WCK epoch to the revoked device | L | +| P7-SYNC-02 | P2 | Env blobs are encrypted only to devices approved at capture time; no rewrap-on-approve, so a later-enrolled device can never decrypt the profile it syncs | M | +| P7-SYNC-01 | P2 | A transient `GetBlob` failure strands a referenced env/draft blob forever — the pointer event still consumes the per-device Seq cursor and is never retried | M | +| P7-HUB-01 | P2 | Folder-carrier writes objects in-place (truncate-then-stream, no temp+rename), exposing torn reads to cloud-drive replication | S | +| P7-HUB-02 | P2 | The shared stale-lock breaker is a non-atomic Stat-then-Remove that can double-grant the carrier lock (single-writer violation) | S | +| P7-CLI-01 | P2 | `service install` bakes an unvalidated `--exec-path` into the unit (no stat/executable check) → silently installs a service that always fails | S | +| P7-SVC-01 | P2 | `resolveServiceExecPath` `EvalSymlinks` through the Homebrew Cellar, baking a version-pinned path a `brew upgrade` deletes | M | +| P7-SVC-02 | P2 | A single global `DefaultLabel` (no `--home`/workspace dependency) silently overwrites a second workspace's installed service | M | +| P7-SANDBOX-02 | P2 | OS-sandbox credential deny-set omits `~/.snowflake` (protected by the advisory layer) — cross-layer drift the `AGEN-05` unification was meant to prevent; add `.snowflake`/`.config/gcloud`/`.azure` | S | +| P7-SYNC-03 | P2 | The convergence property/model-check generator never grew the new `env.profile.updated`/`draft.snapshot.created` event types — unverified convergence for the fast-written new code | M | +| P7-SUPPLY-01 | P3 | The recommended `curl\|sh` `install.sh` never cosign-verifies (checksum-only against the same unauthenticated release); verify when `cosign` is present or point users at the signed path | S | +| P7-SEC-02 | P3 | The owed-rotation retry (#137) treats a transient epoch-read error as fatal in the `pending` path, blocking `device.revoked` propagation — should warn-and-continue | S | +| P7-SANDBOX-03 | P3 | Read-confinement whitelists whole `$HOME` build caches, re-exposing credential files inside them (`~/.cargo/credentials.toml`, …); intersect with the deny-set | S | +| P7-CLI-02 | P3 | `service install`/`uninstall` ignore `--json` (unlike `service status`); route through the render seam | S | +| P7-CLI-03 | P3 | A bad `--label` is misclassified `exitGeneric(1)` instead of `exitUsage(10)`; wrap in `appError{exitUsage}` | S | + ### Pass 6 (2026-07-01) — 0 open of 43 — **PASS CLOSED**; **all five P1s shipped**; **quick-win, sync-convergence, and snapshot-exchange waves complete** > The header count equals the rows in the table below (CodeRabbit, PR #39): 43 findings − 43 shipped **P6** rows in *Recently shipped* (the seven non-P6 rows there — `P4-SEC-02`, the `P4-SEC-07` foundation/pairing/rotation rows, `P4-SEC-04`, `P5-SYNC-01`, and `P5-ARCH-01` — are Pass-4/Pass-5 findings and do **not** count toward Pass 6's 43; the 43 include the 3 fully-applied doc fixes `P6-DOC-02`/`P6-DOC-03`/`P6-DOC-04`) = 0. `P6-SYNC-01`, `P6-SEC-01`, `P6-SEC-02` (PRs #30–#34), `P6-DATA-01` (PR #35), `P6-HUB-01` (PR #36), `P6-GIT-01` (PR #37), `P6-GIT-02` (PR #66), `P6-SYNC-03` (PR #38), `P6-DATA-02` (PR #39), `P6-CLI-02` (PR #40), `P6-GIT-05`, `P6-SYNC-04` (PR #44), `P6-QUAL-03` (`fix/p6-qual-03`), `P6-SEC-03` (`fix/never-granted-epoch-grace`), the sync-convergence wave's `P6-SYNC-02` (PR #63), `P6-DATA-03`/`P6-DATA-05`/`P6-DATA-06` (PR #61), `P6-XP-04` (PR #62), `P6-QUAL-01`/`P6-QUAL-02` (PR #60), `P6-XP-01` (PR #67), `P6-QUAL-05` (PR #68), `P6-GIT-03` (PR #69), `P6-QUAL-04` (PR #70), `P6-DOC-04` (PR #71), `P6-CLI-01` (PR #72), `P6-HUB-04` (PR #75), `P6-HUB-03` (`fix/p6-hub-03`), `P6-CLI-03` (`fix/p6-cli-03`), `P6-CLI-04` (`fix/p6-cli-04`), `P6-DOC-01` (`fix/p6-doc-01-test`), `P6-XP-06` (`fix/p6-xp-06`), `P6-GIT-06` (`fix/p6-git-06`), `P6-XP-05` (`fix/p6-xp-05`), `P6-XP-02` (`fix/p6-xp-02`), `P6-GIT-04` (`fix/p6-git-04`), `P6-XP-03` (`fix/p6-xp-03`), and `P6-DATA-04` (`fix/p6-data-04`) moved to *Recently shipped* above per convention #3. diff --git a/docs/quickstart.md b/docs/quickstart.md index 29fa6a4..4b6b7cd 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -124,9 +124,15 @@ devstrap agent run work/acme/api --engine generic --task "run tests" -- npm test devstrap agent pr --dry-run ``` -On macOS, `agent run` wraps the child in an OS-enforced Seatbelt sandbox by default -(`--sandbox auto|off|require`). The wrapper's command/file policy is guardrails, not a full -sandbox — see [`../spec/10_AGENT_WORKSPACES_AND_POLICIES.md`](../spec/10_AGENT_WORKSPACES_AND_POLICIES.md). +`agent run` wraps the child in an OS-enforced sandbox by default (`--sandbox auto|off|require`): +macOS uses Seatbelt, and Linux uses bubblewrap with a Landlock fallback plus a seccomp syscall +denylist. Writes are confined to the worktree and credential paths are denied; network is blocked +for read-only policies. The wrapper's command/file policy is guardrails on top of that kernel +confinement — see [`../spec/10_AGENT_WORKSPACES_AND_POLICIES.md`](../spec/10_AGENT_WORKSPACES_AND_POLICIES.md). + +For unattended operation, `devstrap service install` registers a LaunchAgent (macOS) or systemd +user unit (Linux) that runs `run-loop` in the background; `devstrap service status|uninstall` +manage it. ## Where to next diff --git a/spec/00_START_HERE.md b/spec/00_START_HERE.md index 120503c..a21c145 100644 --- a/spec/00_START_HERE.md +++ b/spec/00_START_HERE.md @@ -158,7 +158,7 @@ Not implemented yet (genuinely unbuilt — features that are partly shipped are - the bespoke **HTTP/SSE relay** (full-state snapshot exchange is now SHIPPED — sealed snapshots, signed retention manifest, `hub compact`, fail-closed import; the live R2/S3 backend is shipped: the `aws-sdk-go-v2` S3 adapter is wired behind the `hubFromOptions` `r2://` seam, with the `Hub` interface, R2 keying/retry/conditional-put logic, blob GC, retention floor, and content-hash verification all shipped and unit-tested, and the same conformance contract proven against MinIO via an env-gated integration test); - production **remote device registration** (out-of-band fingerprint confirmation, the local trust plane, synced encrypted env/draft bundle exchange, device-revoke rewrap, and synced `device.revoked`/`device.lost` trust propagation — TRUST-01 — are shipped); - daemon, local socket API, FSEvents-specific Mac watcher (the LaunchAgent/systemd installers are now shipped as `devstrap service`, wrapping `run-loop`); -- OS-enforced agent sandboxing, project-env allowlists, and non-generic engine adapters; +- project-env allowlists and non-generic engine adapters (OS-enforced agent sandboxing is now shipped — macOS Seatbelt + Linux bubblewrap/Landlock/seccomp, `P4-GIT-03`, PRs #107–#129; only the containerized sandbox slice remains, tracked `[~]` in `14_MVP_ROADMAP_AND_BACKLOG.md`); - cross-machine working-state sync — git-state validation plane (`repo.gitstate.observed`) and WIP refs (`refs/devstrap/wip/*`); the encrypted draft-bundle layer (Layer C) is shipped; - forge hardening beyond the shipped PR/MR routing — `agent pr` detects GitHub/GitLab/Gitea/Bitbucket/Azure and routes through `gh`/`glab`/`tea` (or a compare-URL fallback) and resolves SSH host aliases via `ssh -G` (`P5-CLI-04`); `doctor` now probes the matching forge CLI per adopted remote (`FORGE-04`, shipped — `checkForgeCLIs` in `internal/cli/doctor.go`), leaving broader hermetic test coverage (`FORGE-05`) as the remaining gap. diff --git a/spec/18_WORK_LOG.md b/spec/18_WORK_LOG.md index c0fc2f3..5008d31 100644 --- a/spec/18_WORK_LOG.md +++ b/spec/18_WORK_LOG.md @@ -31,6 +31,23 @@ Follow-ups: Entries are newest-first: each code-modifying cycle prepends ONE dated entry at the top. +## 2026-07-07 — docs(audits): Pass-7 adversarial audit of the #77–#140 wave + spec truth-up + +Changed: +- `docs/audits/AUDIT_RECOMMENDATIONS_2026-07-07_PASS7.md`: new seventh-pass audit of the OS agent sandbox (`P4-GIT-03`, #107–#129), the AD-1 carrier hub (#96–#106), the supply-chain/distribution pipeline (#105–#120), and the multi-device env/trust/service wave (#131–#139). Verification-driven nine-dimension workflow (9 dimension reviewers → per-finding adversarial verification), with the three P1 candidates additionally cross-verified by a second model family (gpt-5.5 via Codex, the project's dual-review norm applied to the audit). **22 candidates → 21 confirmed** (P1=3, P2=11, P3=7), 1 refuted (`P7-DOC-04` — spec/00's "remaining core-engine candidate" phrasing is consistent with spec/14's `[~]` partial marker). Two severity corrections at verification: `P7-SANDBOX-01` P2→P1 (default-on sandbox breaks the core agent commit loop), `P7-SUPPLY-01` P1→P3 (defense-in-depth on an optional installer path, not a broken promise). Five exa best-practice anchors cited (agent-sandbox escapes, SLSA/cosign verification, git-carrier concurrency, E2E-encrypted git). +- `docs/audits/README.md`: Pass-7 index row; open-backlog blockquote 2026-07-07 note; new "Pass 7 (2026-07-07) — 17 open of 21" section with the open table (17 rows = 21 − 4 doc truth-ups fixed here; header count == row count). The ledger was already reconciled through #140 by the shipping PRs; Pass 7 adds only the Pass-7 rows. +- Doc truth-up (the four confirmed `P7-DOC-*` findings, fixed in this PR): `ARCHITECTURE.md` (Linux OS sandbox is shipped not "next slice"; `service` installers are shipped, only the resident daemon is unbuilt), `spec/00_START_HERE.md` (OS-enforced agent sandbox moved out of "Not implemented yet"; only containerization remains), `docs/quickstart.md` (document the Linux bwrap/Landlock/seccomp sandbox and `devstrap service install`). + +Findings (17 open, for the next implementation wave): P1 — `P7-SANDBOX-01` (grant the linked worktree's git-common-dir in all three sandbox backends), `P7-DATA-01` (add the CAS guard to the revoke env-blob rewrap), `P7-SEC-01` (carry device-trust state in the snapshot so revocation survives compaction). P2 — `P7-SYNC-02` (rewrap-on-approve), `P7-SYNC-01` (retry stranded blobs), `P7-HUB-01`/`P7-HUB-02` (folder-carrier atomic write + atomic stale-lock break), `P7-CLI-01`/`P7-SVC-01`/`P7-SVC-02` (service exec-path validation, Cellar-symlink path, per-workspace label), `P7-SANDBOX-02` (`.snowflake` deny-set drift), `P7-SYNC-03` (property-gen coverage). P3 — `P7-SUPPLY-01`, `P7-SEC-02`, `P7-SANDBOX-03`, `P7-CLI-02`, `P7-CLI-03`. + +Validated: +- `go run ./cmd/spec-drift --base origin/main --head HEAD` (docs+spec change set mapped, work-log present). +- `go test ./internal/cli/... -run 'TestEveryCommandIsDocumented|TestMigrationsDocumented'` (no code/command surface changed). +- Docs-only PR: single reviewer per the PR-cycle convention (no Codex code review needed). + +Follow-ups: +- Implement the 17 open Pass-7 findings per severity in per-finding PRs (Phase B). P1s first: `P7-SANDBOX-01` unblocks the sandboxed agent-commit loop; `P7-DATA-01`/`P7-SEC-01`/`P7-SYNC-02` close the multi-device env/trust data-integrity edges. + ## 2026-07-06 — docs: unattended-operation wave close-out (PRs #136–#139) Changed: