feat: vendor permission system source

This commit is contained in:
云服务部-叶林立
2026-08-19 14:35:19 +08:00
parent 198584daf8
commit 410c50a3e5
809 changed files with 157793 additions and 139 deletions
@@ -0,0 +1,244 @@
---
issue: 639
issue_title: "pi-permission-system: decide the permission policy model — capabilities, config shape, prior art (ADR 0009)"
---
# ADR 0009 — permission policy model: capabilities, config shape, prior art
## Release Recommendation
**Release:** ship independently
This is a documentation-only decision record: it touches `docs/decisions/` and `docs/architecture/`, both release-please `exclude-paths`, so it cuts no physical release on its own — the same posture as the ADR 0007 plan ([#591]).
The decisions it records are implemented later (starting with [#609]'s re-plan), and those changes release on their own merits.
## Problem Statement
Issue [#609] (third-party, filed by `hcrosse`) asks for output redirects to be governed separately from the command: an allowed bash command should not implicitly carry the right to write files through `>`/`>>`.
Planning it exposed a general gap: access direction/capability is not a first-class fact anywhere in the model — the cross-cutting `path` surface is direction-blind, and bash path tokens have no read/write identity at all.
The operator widened the question deliberately: rather than bolt on one key, decide the permission policy model itself, with nothing locked down going in — including the current config format.
The deliverable is ADR 0009, settled interactively during the build session.
The [#581] lesson applies in full: the deliberation is the deliverable; the ADR must record decisions actually made with the operator, not transcribe the sketches produced during this planning conversation.
The operator's decision criteria, stated verbatim in planning: clarity; simplicity ("straightforward, avoiding complex calculus of interactions between rules, and ambiguity"); designed for our users first.
## Goals
- Author `docs/decisions/0009-permission-policy-model.md` deciding the future shape of the permission policy model, with every decision settled interactively during the `/build-plan` session.
- Survey prior art with citations before deciding — agent tools (OpenCode, Claude Code, Codex CLI) and capability systems (Deno permissions, Node's permission model, Linux Landlock, macOS Seatbelt, WASI preopens, systemd sandboxing) — extracting each system's policy axis, naming, composition semantics, default stance, and unknown-handling.
- State the threat model explicitly in the ADR: a cooperative-but-fallible agent; attention routing, not containment; an OS sandbox as the eventual enforcement seam this design should be able to hand classifications to.
- Evaluate the full option space (documented in Design Overview) against the operator's criteria, recording rejected alternatives with reasons.
- Decide the staging: what [#609]'s implementation builds first, unblocking its re-plan.
## Non-Goals
- Implementing anything — no `src/`, `test/`, `schemas/`, `config/`, or `README.md` change; current behavior is untouched.
Issue [#609] stays open and is re-planned after the ADR lands.
- Executing any config migration or renaming, even if the ADR decides one — implementation issues carry that work.
- Filing speculative follow-up issues for stages beyond [#609] (e.g. read-side wiring, a command-effects knowledge base, net-egress effects) — the ADR's staging section names them; filing happens at the next `/plan-improvements` pass or during [#609]'s re-plan, whichever the ADR directs.
- Redesigning the live-authority layer (the ADR 0007 authorizer chain) — this ADR is about the deterministic policy model the chain consults, not about who holds live authority.
## Background
### The current model, mapped
Every gated action has three independent facts: an actor (which tool/channel), an object (which path, command, server:tool, skill name), and a capability (read fs, write fs, execute, cross the cwd boundary).
Today's flat permission map expresses:
| Key today | Axis expressed | Notes |
| ------------------------------------- | --------------------------------- | ------------------------------------------------ |
| `read`/`grep`/`find`/`ls` | actor (implies capability: read) | path-matched |
| `write`/`edit` | actor (implies capability: write) | two keys, one capability; users must set both |
| `bash` | actor (capability: exec) | command patterns; decomposition + wrapper floors |
| `mcp`, `skill`, `special`, tool names | actor | |
| `path` | object, capability-blind | any file access, any direction, any actor |
| `external_directory` | boundary | outside-cwd guard, orthogonal to the above |
| `*` | universal fallback | |
Composition is most-restrictive-wins across surfaces, last-match-wins within a surface.
OpenCode's model (the fork's origin) is actor-keyed with two guards; its only nod to direction is collapsing `edit`/`write`/`patch` into one `edit` key, and its bash gating is command-pattern only.
### The engine seams that price the options
- `AccessIntent` (`tool` | `access-path`) carries a free-form `surface` string; the resolver and manager do not care what the keys mean.
New surface keys are additive.
- Every policy channel speaks flat `(surface, pattern)` pairs: session-approval rules, the forwarded-intent wire ([#596]), the cross-extension `PermissionsService` queries, and per-agent frontmatter.
A nested config shape would be flattened internally regardless; a format remodel churns all four channels.
- Bash path tokens are gated on `path` + `external_directory` only.
Redirect targets are collected by `collectRedirectTokens` but then shape-filtered like any other token, so a bare in-cwd target (`> out.txt`) is not even a rule candidate today, and `BashProgram.commands()` deliberately strips redirects from command text — the exact [#609] gap.
### The evidence: capability keeps escaping pattern matching
The fork's history is a catalog of the same event — a capability escaping command-pattern rules through a syntactic synonym:
| Issue | Escape channel |
| -------------- | -------------------------------------------------------------- |
| [#301] | whole-string matching bypassed by chained commands |
| [#306] | command/process substitution executes unseen |
| [#393] | `cd` folding; unknown bases must stay literal-only |
| [#452] | unparseable commands must fail closed |
| [#481] | `bash -c`/`eval` opacity; env-var prefixes |
| [#490], [#575] | indirection wrappers (`sudo`, `env`, `xargs`, `find -exec`, …) |
| [#509] | bare tokens invisible to shape classifiers |
| [#533] | platform reinterprets the same token |
| [#574] | other extensions re-expose bash under new names |
| [#609] | output redirects ride the command's allow |
The structural parts age well (`AccessPath` alias matching [#418], the boundary gate, fail-closed floors, determinism); the command-spelling parts are a treadmill.
This evidence motivates the reframe the ADR must weigh: policy keyed by effects, with command patterns as evidence and refinement.
### Leanings from the planning conversation — explicitly not decisions
The operator unlocked everything ("none of the decisions we've explored today are locked-down"), so these are inputs to the deliberation, not settled outcomes:
- Naming leaning: `path_read`/`path_write` (path-family) over `fs.read`/`fs.write` — though the `fs` prefix is the cross-ecosystem convention (Node's `--allow-fs-read`/`--allow-fs-write`, Landlock `ACCESS_FS_*`, Seatbelt `file-read*`/`file-write*`), Deno-style bare `read`/`write` collides with our existing tool keys.
- Nesting (`path: { read: …, write: …, "*": … }`) was analyzed and found grammatically ambiguous: `path: { "read": "allow" }` is already a valid pattern rule matching a file literally named `read`, and the map-valued discriminator collides with `denyWithReason` object values.
The analysis rides into the ADR; nesting remains evaluable as sugar over flat keys.
- An earlier gate (under the narrower flooring framing) chose a shipped default of `ask` for output-redirect writes (breaking, `feat!:`) — recorded as a leaning toward least-privilege defaults, explicitly reopened with everything else.
- The effect-centered sketch (effects primary; structural proof + a command-effects knowledge base + honest unknowns; `net` as a future effect; effect-level session approvals; sandbox-handoff seam) is one candidate among several, not the presumed target.
### Standing constraints
- Config files are the source of truth; no policy baked into code; a declared config field not read at runtime is a maintenance trap.
- Determinism: same policy + same input → same decision (no `cygpath`, no environment reads in matching).
- Least privilege and fail-closed stay non-negotiable; the composition semantics that deliver them are what the ADR may redesign.
- The architecture doc inline-copies the `rule.ts` types; this ADR changes no types, but any implementation that does must update that listing.
- ADR markdown follows the `markdown-conventions` skill (one-sentence-per-line, reference-style issue links, MD053 discipline).
## Design Overview
This plan deliberately does not settle the design.
It defines the decision framework the build session executes.
### Decision criteria
1. Clarity — a user can predict what a config does by reading it.
2. Simplicity — straightforward; avoid a complex calculus of interactions between rules.
This criterion cuts at the multi-surface most-restrictive lattice itself, so composition semantics are on the table, not just key names.
3. No ambiguity — no config text with two plausible readings (the nesting analysis is the cautionary example).
4. User-first — ergonomics of authoring, reading prompts, and approving sessions outrank internal elegance.
5. Retained unless deliberately revisited: determinism, least privilege, fail-closed unknowns.
### Option space to evaluate
- **O1 — status quo + targeted `path_write` key.**
Smallest change; fixes [#609]; direction stays a bolt-on.
- **O2 — capability family as flat keys.**
`path_read`/`path_write` join `path` + `external_directory` as a capability/boundary layer; actor keys become refinement; most-restrictive lattice retained.
- **O3 — nested facets under `path`.**
Grouping sugar; carries the documented grammar ambiguity; internally flattens to O2.
- **O4 — effect-centered model.**
Effects primary (`fs read/write`, `exec`, later `net`), classified by structural proof, a curated command-effects knowledge base (consolidating `PATTERN_FIRST_COMMANDS`, the wrapper sets, `SAFE_SYSTEM_PATHS`), and an explicit unknown category the user must deliberately allow; flat keys; actor keys as refinement.
- **O5 — full config remodel.**
A v2 format with capability domains primary and tools demoted; prices in a dual-format loader window and churn across all four flat-pair channels.
- **O6 — single ordered rule list.**
A firewall-style model: one ordered list of typed rules (match on effect/path/command/tool → action), first- or last-match wins; trades the lattice's cross-surface calculus for explicit ordering.
Included because criterion 2 questions the lattice; the ADR must evaluate whether ordering is simpler or merely different.
### Prior-art survey scope
For each system, extract: policy axis (actor/capability/effect), key naming, composition and conflict semantics, default stance, unknown-handling, and prompt/escalation UX.
| System | Why it matters |
| --------------------- | -------------------------------------------------------------------------------- |
| OpenCode | the fork's origin; actor-keyed baseline |
| Claude Code | agent-adjacent; `Tool(specifier)` allow/ask/deny lists + sandbox modes |
| Codex CLI | agent-adjacent; approval modes paired with an OS sandbox |
| Deno | cleanest capability flags (`--allow-read/--allow-write/--allow-net/--allow-run`) |
| Node permission model | literally `--allow-fs-read`/`--allow-fs-write` |
| Linux Landlock | kernel fs capability rights (`ACCESS_FS_*`) |
| macOS Seatbelt | operation-named profile rules (`file-read*`, `process-exec`) |
| WASI | capability handles; preopened dirs; `path_open` rights |
| systemd | path-scoped mode lists (`ReadOnlyPaths=`, `ReadWritePaths=`) |
### Open parameters the build session settles interactively
1. Policy axis: actor-keyed (status quo), +capability layer, or effect-primary.
2. Composition semantics: retain the most-restrictive lattice, or simplify (O6-style ordering, or a narrower layer set).
3. Key naming and shape: `path_read`/`path_write` vs `fs`-prefixed vs nesting sugar.
4. Unknown-effects stance: inert-when-absent vs an explicit unknown category in policy.
5. Defaults and breaking posture for redirect writes (leaning: shipped `ask` default, `feat!:` — reopened).
6. Whether the command-effects knowledge base becomes a named, curated asset (and when).
7. Staging: what [#609] implements first; which follow-ups the ADR names and where they get filed.
The gate protocol during `/build-plan`: complete the survey first, present findings and option evaluations, then run `ask_user` gates per parameter cluster before authoring a word of the ADR — decisions precede prose.
## Module-Level Changes
Documentation only.
- **New:** `packages/pi-permission-system/docs/decisions/0009-permission-policy-model.md` — the ADR: threat model, decision criteria, current-model inventory, prior-art survey with citations, options considered with rejected alternatives and reasons, the settled decisions (parameters 17), staging, and consequences.
The 0009 slot is next (00010008 taken); the file slug may sharpen at build time to reflect the settled decision, keeping the 0009 number.
- **Changed:** `packages/pi-permission-system/docs/architecture/architecture.md` — link ADR 0009 from the design-principles section and reconcile any prose the settled decision contradicts (candidates: the most-restrictive-wins principle statement, the `path`-surface description, any aspirational prose about policy evolution).
Grep the whole file for stale framing after the decision is known — the [#581] failure mode was un-reconciled prose surviving an internally consistent ADR.
- **Not edited:** `docs/architecture/history/*`, `docs/plans/*`, `docs/retro/*` — frozen point-in-time records; `README.md`, `docs/configuration.md`, `schemas/`, `config/` — they describe current behavior, which this ADR does not change; `.pi/skills/package-pi-permission-system/SKILL.md` — it documents current behavior and constraints, all still true.
## Test Impact Analysis
Not applicable — the deliverable is a decision record with no code.
Tests the settled design enables (recorded for [#609]'s re-plan to inherit): redirect-operator classification (output vs input vs fd-duplication), unconditional collection of output-redirect targets, capability-surface resolution and composition, and — if the ADR adopts them — knowledge-base row lookups and unknown-category resolution.
## Invariants at risk
- **The [#581] transcription failure.**
This planning conversation produced sketches and leanings; the ADR must not launder them into settled status.
Mitigation is structural: the Build Order places the survey and the `ask_user` decision gates before ADR authoring, and the plan marks every leaning as reopened.
- **Cross-doc consistency.**
If the decision revises composition semantics or the `path` family, the architecture doc's principle statements must be reconciled in the same change, verified by a whole-file grep for the superseded framing (`most-restrictive`, `path` family descriptions, aspirational policy prose).
- **Current-behavior docs stay true.**
`README.md` and `docs/configuration.md` describe shipped behavior; the ADR decides future direction and must not cause edits that make current-behavior docs describe unshipped design.
## Build Order
Documentation-only, so `/build-plan` (no red→green cycles).
Numbered `docs:` commits, each leaving the docs internally consistent.
1. **Survey prior art.**
Research the nine systems in scope (web sources with citations; `librarian`/`fetch_content` for primary docs), producing per-system extractions of policy axis, naming, composition, defaults, unknown-handling, and prompt UX.
No commit — this is input to the deliberation.
2. **Deliberate and settle.**
Present the survey findings and the O1O6 evaluations against the criteria; run `ask_user` gates covering the seven open parameters (clustered: axis+composition; naming+shape; unknowns+defaults; knowledge base+staging).
No commit — decisions precede prose.
3. **Author ADR 0009.**
Write `docs/decisions/0009-permission-policy-model.md` recording the threat model, criteria, survey, options with rejected alternatives, the settled decisions, staging for [#609], and consequences.
Verify with `pnpm exec rumdl check` on the new file.
Commit: `docs(pi-permission-system): record ADR 0009 deciding the permission policy model (#639)`.
4. **Reconcile the architecture doc.**
Link ADR 0009 and reconcile any contradicted prose in `docs/architecture/architecture.md` in one commit; run the whole-file grep from *Invariants at risk*; verify any touched Mermaid diagrams still render.
Commit: `docs(pi-permission-system): reconcile architecture with ADR 0009 (#639)`.
## Risks and Mitigations
- **Risk: transcription instead of deliberation (the [#581] revert).**
Mitigated: survey-then-gates-then-prose ordering in the Build Order; leanings explicitly marked reopened in Background.
- **Risk: a shallow survey that just confirms the effect-model sketch.**
Mitigated: the per-system extraction template forces comparable facts (axis, composition, defaults, unknowns) rather than cherry-picked naming; O6 is in the option space specifically because it challenges the sketch's lattice.
- **Risk: the ADR over-commits implementation detail.**
Mitigated: the ADR settles model, semantics, naming, defaults, and staging; schemas, migration mechanics, and knowledge-base contents belong to the implementation issues.
- **Risk: scope creep into code.**
Mitigated: Non-Goals fences this to `docs/`; [#609] and successors implement.
- **Risk: a breaking-default decision ships without migration discipline.**
Mitigated: if the settled decision is breaking, the ADR records the posture, and the implementing issue's plan carries the `feat!:`/`BREAKING CHANGE:` footer and a verified migration note — not this ADR.
## Open Questions
- The seven open parameters in Design Overview — deliberately open; they are the ADR's subject.
- Whether nesting sugar over flat keys is ever worth its grammar cost — evaluable in the ADR, decidable later without model change if deferred.
- Where the ADR-named follow-ups get filed (during [#609]'s re-plan vs the next `/plan-improvements` pass) — settled by the ADR's staging section.
[#301]: https://github.com/gotgenes/pi-packages/issues/301
[#306]: https://github.com/gotgenes/pi-packages/issues/306
[#393]: https://github.com/gotgenes/pi-packages/issues/393
[#418]: https://github.com/gotgenes/pi-packages/issues/418
[#452]: https://github.com/gotgenes/pi-packages/issues/452
[#481]: https://github.com/gotgenes/pi-packages/issues/481
[#490]: https://github.com/gotgenes/pi-packages/issues/490
[#509]: https://github.com/gotgenes/pi-packages/issues/509
[#533]: https://github.com/gotgenes/pi-packages/issues/533
[#574]: https://github.com/gotgenes/pi-packages/issues/574
[#575]: https://github.com/gotgenes/pi-packages/issues/575
[#581]: https://github.com/gotgenes/pi-packages/issues/581
[#591]: https://github.com/gotgenes/pi-packages/issues/591
[#596]: https://github.com/gotgenes/pi-packages/issues/596
[#609]: https://github.com/gotgenes/pi-packages/issues/609