Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,14 @@ These landed after phase 10, and all are documented in `design/orchestration.md`

**`@path` is the stale check** — the notes half of the codewiki citation rule, since a note about code that no longer exists is read as current, which is worse than the comment it replaced: that one at least died with the file. At *write* time it is a warning and never a block, because a note about a file this branch has not created yet is the note most worth having; `notes add` otherwise writes under the same verify-and-roll-back contract as `scc patch`. The ninth validator also reports the failure this file cannot tolerate quietly: a hand-written line that missed the grammar, which no query will ever return.

- **`scc launch --jail`, the sandbox.** An agent needs filesystem access to do its job, and the same access lets it run `rm -rf`, read `~/.aws`, or ship a key somewhere — by accident, on a poisoned instruction in a file it read, or through a dependency it installed. `autonomy: auto` sharpens that rather than softening it: nobody is watching the step where noticing was still possible. `--jail` starts the agent inside [ai-jail](https://github.com/akitaonrails/ai-jail) — bubblewrap on Linux, `sandbox-exec` on macOS — wrapping *outermost*, Headroom included.

**It is the one integration that refuses.** Headroom, CodeGraph and RTK all degrade to starting the agent bare, because each is an enhancement. A sandbox is the property the user asked for by name, so a missing binary, a declined install, an unattended run, or an unsupported platform all end in **nothing starting** — and the refusal happens before the graph is built or the entry file is touched, so it leaves the workspace as it found it. Somebody who typed `--jail` and watched an agent start believes they are contained, and a false belief about containment is worse than a known absence of it: they would not have run the thing at all.

**scc decides two flags and no more.** `--network` and `--agent-state` are function rather than policy — an agent with neither cannot reach its model or authenticate, so `ai-jail claude` bare is a jail that starts nothing. Everything else (lockdown, denied paths, extra mounts, Docker, the browser) is policy, and policy lives in ai-jail's own `~/.ai-jail` / `./.ai-jail`, which it reads by itself and scc never writes; `--jail-arg` is the per-run escape hatch and comes last so it wins. Even those two are read off `ai-jail --help` rather than compiled in — the lesson `internal/headroom` already paid for — and a build that advertises neither gets **no substitute**, only a warning, because a sandbox opened by a guess is the failure this whole feature exists to prevent.

**Windows has no backend and is unlikely to get one**: the sandbox stands on Linux namespaces and Apple's sandbox interface. WSL2 is the answer there, and it is a real one rather than a workaround — scc inside WSL2 is scc on Linux. The idea and the tool are [Fábio Akita's](https://akitaonrails.com/2026/01/10/ai-agents-garantindo-a-protecao-do-seu-sistema/); scc integrates the binary rather than the article's original shell script, because that script has since become a maintained Rust tool with a second platform backend.

- **The spec records where it is being built: `scc spec track` and `scc spec sync`.** A branch was the one part of this methodology that left no trace in the artifacts — the spec said which boxes were ticked, git said a branch had been unmerged for three weeks, and nothing joined the two, so *which of these actually shipped* was answerable only by somebody holding both halves. Under `autonomy: auto` that is nobody. Three keys on `requirements.md` beside the kickoff answers — `branch:`, `pr:`, `delivery: in-progress|in-review|merged|abandoned` — with the vocabulary closed for the reason a task's flags are, and graded by the validator only when present, so every spec written before this keeps passing.

**`track` records what the caller knows; `sync` derives what git knows.** `--here` takes the branch from the checkout, `--pr <n>` the pull request, and `scc spec sync` walks every spec, asks git and (when installed) `gh`, and writes the answer back under the same verify-and-roll-back contract as `scc patch`. `scc spec list` shows the record beside the phases. **Neither guesses**: a deleted branch with no PR to ask about is reported undetermined and left alone, because merged and abandoned are indistinguishable once the ref is gone.
Expand Down Expand Up @@ -164,7 +172,7 @@ cmd/scc/main.go os.Exit(cli.Run(os.Args[1:]))
plain files on disk: <harness>/ · specs/ · plans/ · docs/ · CLAUDE.md|AGENTS.md
```

Four packages sit off to the side of that tree — `rtk`, `headroom`, `codegraph`, `git` — reached only from `internal/cli`. They are the third-party integrations, and they are the only code that starts another process. `git` is the one scc never installs and never writes with: every call in it is a query.
Five packages sit off to the side of that tree — `rtk`, `headroom`, `codegraph`, `git`, `jail` — reached only from `internal/cli`. They are the third-party integrations, and they are the only code that starts another process. `git` is the one scc never installs and never writes with: every call in it is a query.

`internal/cli/cli.go` is the whole dispatcher: `Run(args)` switches on `args[0]` and hands off to `run<Resource>` in a file named for that resource. Each handler owns its own `flag.FlagSet`. Adding a subcommand means adding a case there plus one file — nothing is registered dynamically, so the command set is readable in one place.

Expand All @@ -186,11 +194,12 @@ Four packages sit off to the side of that tree — `rtk`, `headroom`, `codegraph
| `internal/validate` | The nine validators, one file each, sharing `mdscan` and `finding`. The exception is `stack_manifests.go`: the seven dependency-file readers age on their own schedule, so they sit beside the rule rather than inside it. |
| `internal/rtk` | RTK's marker pair (`rtk.Markers`, spliced by `internal/mdblock`), the foreign-block detection that names Headroom's copy, and finding or `cargo install`ing the binary. |
| `internal/headroom` | Headroom's agent-slug table, the `wrap` argument vector, the MCP opt-out discovered from `wrap <agent> --help`, and finding or installing the binary (uv, then pip — never npm, which ships the SDK and no CLI). The slugs live here rather than on `paths.Harness` because they are Headroom's vocabulary, not scc's layout. |
| `internal/jail` | [ai-jail](https://github.com/akitaonrails/ai-jail)'s argument vector, its platform backends, the two flags scc asks for (discovered from `--help`), and finding or `cargo install`ing the binary. Composes a command line and reimplements nothing: a sandbox is security-critical kernel interface work, and a half-copy of one has the confidence of containment without the containment. |
| `internal/git` | `git` and `gh`, read-only: does this branch still exist, has it landed, and what did the forge do with the pull request. Two binaries in one package because the question has two halves and no caller should have to route between them — and nothing here installs or writes, so running it over every spec in a workspace is safe by construction. |
| `internal/codegraph` | CodeGraph's argument vectors (`init`/`sync`/`index`/`status`/`query`/`explore`), the `.codegraph/` presence test, `codegraph.Markers` for the usage block `scc launch` splices, and finding or `npm install -g`ing the binary. Composes command lines and reads nothing inside the graph — the database is CodeGraph's schema on CodeGraph's schedule. |
| `internal/cli` | The dispatcher and every command handler. |

`internal/rtk`, `internal/headroom`, `internal/codegraph`, and `internal/git` are the only packages that shell out to another program. Keep that boundary there rather than in a command handler: a third party's binary name, install command, and argument vocabulary all age on that third party's schedule, and one package per integration is what keeps a version bump from touching the dispatcher. Headroom's renamed MCP flag is the worked example — the fix stayed inside `internal/headroom`, and nothing else in the tree knows the flag exists.
`internal/rtk`, `internal/headroom`, `internal/codegraph`, `internal/git`, and `internal/jail` are the only packages that shell out to another program. Keep that boundary there rather than in a command handler: a third party's binary name, install command, and argument vocabulary all age on that third party's schedule, and one package per integration is what keeps a version bump from touching the dispatcher. Headroom's renamed MCP flag is the worked example — the fix stayed inside `internal/headroom`, and nothing else in the tree knows the flag exists.

`go.mod` is stdlib-only. Keep it that way unless a dependency earns its place — the binary is distributed to six platforms and every dep is a supply-chain surface.

Expand Down
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ Installed globally (`npm i -g @protonspy/scc`) the same commands are just `scc i
| `skill validate` | Conformance to the published [Agent Skills](https://agentskills.io/specification) spec. |
| `validate` | Every applicable validator, one exit code, one JSON document. |
| `rtk` | Wires in [RTK](https://github.com/rtk-ai/rtk): installs it if missing, then splices its usage block into the entry file. |
| `launch` | Starts the harness with the workspace's symbol graph and RTK block current — and, with `--jail`, inside a sandbox. |

### RTK, optionally

Expand All @@ -57,6 +58,33 @@ the markers is untouched either way.
Opt-in on purpose: it tells the agent to prefix every command with a binary the
machine may not have. `--no-install` writes the block and never touches cargo.

### A sandbox, optionally

An agent needs filesystem access to do its job, and the same access lets it run
`rm -rf`, read `~/.aws`, or ship a key somewhere — by accident, on a poisoned
instruction in a file it read, or through a dependency it installed. `scc launch
--jail` starts it inside [ai-jail](https://github.com/akitaonrails/ai-jail), which
sandboxes with bubblewrap on Linux and `sandbox-exec` on macOS:

```bash
npx @protonspy/scc launch claude --jail # the agent, contained
npx @protonspy/scc launch claude --jail --jail-arg --lockdown
```

**It refuses rather than degrading.** Every other integration here starts the agent
anyway when its binary is missing, because every other one is an enhancement. A
sandbox is the property you asked for by name: if ai-jail is not installed, or the
platform has no backend (Windows — use WSL2), nothing starts and it says why. An
agent that started unjailed would hand you the confidence of containment without the
containment.

scc passes exactly the two flags that let an agent run at all — a network to reach
its model and the credential state to authenticate — and reads even those off
`ai-jail --help` rather than hardcoding them. Everything else is policy and belongs
in ai-jail's own `~/.ai-jail` / `./.ai-jail`, which scc never writes.

The idea, and the tool, are [Fábio Akita's](https://akitaonrails.com/2026/01/10/ai-agents-garantindo-a-protecao-do-seu-sistema/).

### Three harnesses, one methodology

The same rules, review agents, and skills — the knowledge base's authors, plus
Expand Down
53 changes: 53 additions & 0 deletions design/orchestration.md
Original file line number Diff line number Diff line change
Expand Up @@ -986,6 +986,59 @@ stays reproducible from the file, and nobody gets asked twice.
- **A checkout left dirty or off `main`** is what this shape can leave behind. Say what
is still uncommitted rather than starting the next unit of work on top of it.

### The sandbox — the one integration that refuses

An agent needs filesystem access to do its job. The same access lets it run `rm -rf`,
read `~/.aws`, or ship a private key somewhere — by accident, on an instruction
planted in a file it was asked to read, or through a dependency it installed. §2's
`auto` sharpens that rather than softening it: autonomy means nobody is watching the
step where noticing was still possible, which is exactly the argument for putting a
boundary around the whole session instead of around each decision inside it.

`scc launch --jail` starts the agent inside [ai-jail], which sandboxes with
bubblewrap on Linux and `sandbox-exec` on macOS. It wraps *outermost* — Headroom
included — so the jail contains the session and everything the session starts.

**It refuses rather than degrading, and that is the whole design.** Headroom,
CodeGraph and RTK all end in the agent starting anyway when their binary is missing,
because each of the three is an enhancement and a launcher that failed over a
compression proxy would be putting its own preference above the thing the user asked
for. A sandbox inverts that. It *is* the thing the user asked for, by name, and the
failure mode is not a worse session — it is somebody who typed `--jail`, watched an
agent start, and believes they are contained. A false belief about containment is
worse than a known absence of it, because with the known absence they would not have
run the thing at all. So a missing binary, a declined install, an unattended run, or
a platform with no backend all end in nothing starting, and the refusal is decided
before the graph is built or the entry file is touched: a launch that cannot be
jailed leaves the workspace exactly as it found it.

**scc decides two flags and no more.** ai-jail defaults network and credential state
to off, which is the right default for a sandbox and the wrong one for a launcher: an
agent with neither cannot reach the model it is or authenticate as anyone, so
`ai-jail claude` bare is a jail that starts nothing. Those two are *function*.
Everything else — lockdown, denied paths, extra mounts, Docker, the browser — is
*policy*, and policy belongs in ai-jail's own `~/.ai-jail` and `./.ai-jail`, which it
reads by itself and scc never writes. A launcher that quietly loosened somebody's
sandbox policy would be the worst kind of helpful. `--jail-arg` is the per-run escape
hatch, and it comes last on the command line so a flag the user typed wins over the
two scc supplies.

Even those two are read off `ai-jail --help` rather than compiled in — the lesson
§6's Headroom integration already paid for — and a build advertising neither gets
**no substitute**, only a warning. Guessing at a replacement spelling is precisely how
a sandbox ends up opened by the tool that was trying to help.

**scc integrates the binary and reimplements nothing.** ai-jail began as the shell
script in [Akita's article][akita], and copying that script into Go was the obvious
move and the wrong one: the thing does namespaces, Landlock and seccomp, it has since
grown a second platform backend, and a half-copy of a sandbox is worse than none for
the same reason the refusal above exists. Windows has no backend and is unlikely to
get one, since there is nothing there for bubblewrap or `sandbox-exec` to stand on;
WSL2 is the answer, and it is a real one rather than a workaround.

[ai-jail]: https://github.com/akitaonrails/ai-jail
[akita]: https://akitaonrails.com/2026/01/10/ai-agents-garantindo-a-protecao-do-seu-sistema/

## 10 · The three spec artifacts

§1 routes work into a spec at `specs/<feature>/`; this is what those three files
Expand Down
2 changes: 1 addition & 1 deletion internal/cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ Commands:
init Scaffold a workspace: rules, agents, skills, commands, layout, manifest
update Bring the managed files onto this build's templates, after showing the plan
rtk Install RTK if missing and put its usage block in the entry file
launch Start a harness in this workspace, with its symbol graph and RTK block current
launch Start a harness here, with its symbol graph and RTK block current; --jail sandboxes it
graph The workspace's symbol graph — build | sync | status | query | explore
spec Create and inspect specs — new | list | show | delete | validate
plan Create and inspect plans — new | list | delete | validate
Expand Down
Loading
Loading