diff --git a/.gitignore b/.gitignore index 5358193..994b362 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /target /scratch +/CLAUDE.local.md diff --git a/README.md b/README.md index 93efa9a..d6b0b0a 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,16 @@ relaxed if you deliberately mark a host eligible in `hosts.json` *and* confirm once per session (a grant that is never persisted). See [REVIEWING.md](REVIEWING.md) invariant **G7**. +**Escalate vs. confine.** sudo-proxy's security model is the inverse of an agent +harness's. A harness (Claude Code, Codex CLI, …) gates with *software* — +permission rules plus an optional classifier — and runs the agent at *your own* +privilege, narrowing it to a safe subset. sudo-proxy inverts both: a *human* +approves every command (no classifier, no auto-approve) and execution is +*escalated* beyond what the agent has. The two are complementary — the harness +bounds the agent to your privileges; sudo-proxy is how it reaches past them, with +a human as the escalation authority. See +[docs/architecture.md](docs/architecture.md#security-model-vs-agent-harnesses). + For how this relates to mcp-firewall, sandboxing, polkit, doas, and other neighboring tools, see [docs/comparison.md](docs/comparison.md). diff --git a/backlog.md b/backlog.md index c8e9831..7e69f2a 100644 --- a/backlog.md +++ b/backlog.md @@ -164,10 +164,11 @@ on top:** requests `3a7cb1f9`, `e6aeb649`, `942cd83f`, `b1f6294a`). Dune actions are synchronous; `concurrent` still joins. We need launch-and-detach returning the pid as a structured field (not a shell `$!`). -- **`||` fallback.** `progn` ≈ `;`/`&&` (stops on failure); there is no - run-on-failure branch for the log's `pgrep … || echo none`. Add it, or fold it +- ~~**`||` fallback.**~~ *Landed:* the `alt` op (dual of `seq`) runs steps until + one succeeds, covering `pgrep … || echo none`. A first-class op, not a fold into `with-accepted-exit-codes`. -- **Append redirection (`>>`).** `with-stdout-to` truncates; no append mode. +- ~~**Append redirection (`>>`).**~~ *Landed:* `redirect` takes `mode:"append"` + alongside `"truncate"` (and `stream:"both"` for `2>&1`). - **Runtime substitution** (`$(cat /proc/…)`, `$!`, `$?`). Dune resolves `%{…}` at rule-eval, not runtime — so it has no runtime `$(…)` either, which *confirms* our plan: promote the common cases to first-class ops (a read-file op @@ -187,6 +188,49 @@ shell`) and a policy mode rejects it — making `no-opaque-blob` a special case daemon-side *reject-unresolvable-stages*, enforced structurally and for every client rather than by regex in one user's hook. +## Make the shell escape hatch avoidable (plan-model design goal + remaining levers) + +**The design goal.** The `Plan` type's *raison d'être* is to make its `Shell` +constructor (the opaque escape hatch, formerly `System`) unnecessary: every +structured op exists so a given shell feature need not be expressed by shelling +out. The real invariant is **no shell on the privileged path**; `Shell` is the +one node that breaks displayed-equals-executed — it resolves only the shell +binary, so its inner commands are invisible at the prompt. The goal is therefore +not to *delete* `Shell` (the shell tail — runtime `$(…)`, `$!`, globbing, +word-splitting, profile functions — is open-ended) but to **shrink-and-flag**: +drive real usage into structured ops, and keep every remaining shell use loud, +countable, and policy-rejectable. Read each `Shell` node as measurable debt — a +prompt to ask "what op would have avoided this?" That loop already produced `alt` +(for `||`) and `seq` (for `;`). See +[docs/architecture.md](docs/architecture.md) ("Security model vs. agent +harnesses") for the framing. + +**Remaining levers** (rough payoff order): + +1. **Close the `exec sh -c` loophole** (highest payoff). The goal is quietly + defeated today not by `Shell` but by its dishonest sibling: a plain `Exec` + leaf (or pipe stage) `["sh","-c","