From 279980aa71868d13af99e4166c5ddbe120e8479c Mon Sep 17 00:00:00 2001 From: itz4blitz <60281870+itz4blitz@users.noreply.github.com> Date: Wed, 19 Aug 2026 12:45:13 -0400 Subject: [PATCH] =?UTF-8?q?plan:=20plugin=20architecture=20(D41=E2=80=93D4?= =?UTF-8?q?6)=20=E2=80=94=20extension=20seams=20for=20credential=20plane,?= =?UTF-8?q?=20sandboxing,=20harness=20admission,=20approval=20policy?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Core stays small and security-reviewable; deployment-specific concerns become capability-scoped plugins. First-party flagship plugins: 1Password CredentialProvider (service-account scoped, feeds the D23 session cache), container SandboxProvider (implements the D25 independent-enforcement rule), ACP HarnessAdapters (claude-code/codex/opencode/zcode admission per D17), and the ApprovalPolicy pack over the D12 4-way enum. - docs/DECISIONS.md: D41–D46 appended (house format), summary table gains plan/21 row - plan/21-plugin-architecture.md: new plan doc — trait seams, manifest & capabilities, out-of-process plugin host, threat-model mapping (credential readability + abuse-in-session), milestone mapping on the D40 spine, testing bars, open questions for adversarial review --- docs/DECISIONS.md | 31 ++++++++ plan/21-plugin-architecture.md | 132 +++++++++++++++++++++++++++++++++ 2 files changed, 163 insertions(+) create mode 100644 plan/21-plugin-architecture.md diff --git a/docs/DECISIONS.md b/docs/DECISIONS.md index 176f2a4..f93a222 100644 --- a/docs/DECISIONS.md +++ b/docs/DECISIONS.md @@ -163,6 +163,36 @@ ## D40. Roadmap dependency spine — Phase 4 depends on Phase 1 (LOCKED) - **Decision:** plan/19's dependency-spine diagram is corrected: Phase 4 (mobile+remote) depends on Phase 1 (wire contract), not Phase 3. Phase 3 and Phase 4 can run in parallel (both depend on Phase 1). +## D41. Extension mechanism — plugins, not PRs (LOCKED) +- **Decision:** Everything deployment-specific — credential vaults, session sandboxes, harness adapters, approval policies, future panes — is a **plugin** behind stable, capability-scoped extension seams (`multiplexer-plugin-api`). Users extend Multiplexer by writing plugins, NOT by forking or PR-ing core. +- **Rationale:** Keeps the security-critical core small and reviewable while the ecosystem grows without our bottleneck. A vault integration (1Password), a sandbox backend, or a new harness (claude-code, codex, opencode, zcode) each become a plugin instead of core surface area. This is also a moat: the extension seam is a product. +- **Trade-off accepted:** API stability discipline and versioning burden on the seams (D20 discipline applies). Third-party plugins are out-of-process against a versioned sidecar protocol in v1 (no in-process WASM yet). +- **Action:** plan/21 defines the seams, manifest, lifecycle, and testing bar. + +## D42. Plugin capability model — declared, least-privilege, enforced (LOCKED) +- **Decision:** Every plugin ships a manifest declaring `kind` (credential | sandbox | harness | approval | pane) and **capabilities** (e.g., `credential-read = ["1pass://automation-vault/*"]`, an egress network allow-list — deny by default). The plugin host enforces capabilities; plugins get **only** the API handles their capabilities grant — no ambient authority. +- **Rationale:** A malicious or compromised plugin must be bounded to its declared slice. This is the blast-radius guarantee that makes third-party plugins acceptable at all. +- **Trade-off accepted:** capability enforcement code is itself security-critical core (mutation-gated per D21) and the install-time consent UX must be honest about what each capability grants. + +## D43. Credential plane — CredentialProvider plugin; 1Password is first-party flagship (LOCKED) +- **Decision:** A `CredentialProvider` plugin trait bridges external vaults into the **existing session-cache model (D23)**. The first-party `plugin-1password` authenticates as a **1Password service account scoped to a single automation vault** and resolves references into the server-side session cache at session start — values are then injected, task-scoped, into the session sandbox. **No live user-session `op` reads (D23 unchanged); no credentials ever exist client-side.** +- **Rationale:** Extends D23 rather than amending it: the core SecretStore stays keychain + session cache; the plugin is the external-vault bridge with its own narrowly-scoped auth. Service-account scoping means a compromised agent (or agent host) can reach exactly the automation vault and nothing else — human vaults are structurally invisible. +- **Trade-off accepted:** operators must maintain an automation vault + service account; the plugin documents this as the canonical deployment. + +## D44. Session isolation — SandboxProvider plugin; containers first (LOCKED) +- **Decision:** Each agent session executes inside a sandbox provisioned by a `SandboxProvider` plugin. The first-party default is container-based isolation on the server host: per-session container, workspace bind-mount scoped to the session worktree, no host home exposure, per-session egress policy, teardown shreds injected secrets. The **independent-enforcement rule of D25 is implemented at this layer** — confinement is enforced by the sandbox, never trusted from a client. +- **Rationale:** Sessions must be isolated from each other, from the server host, and from sibling agents' state (`~/.claude` et al.). The plugin seam lets deployments choose OS-user, container, or microVM backends without core changes. +- **Trade-off accepted:** container runtime is a server-host requirement; the plugin must degrade loudly (refuse to run unsandboxed) rather than silently. + +## D45. Harness admission — HarnessAdapter plugin (LOCKED) +- **Decision:** Grok stays the core in-process adapter (D10). **All external harnesses (Claude Code, Codex, OpenCode, ZCode, …) are admitted as `HarnessAdapter` plugins** riding the generic ACP machinery (D17). Adding a harness is a plugin install, never a core PR. +- **Rationale:** The process boundary at ACP is a security feature here, not the introspection limitation plan/01 frames: external runtimes are closed and untrusted-by-default, so admission through an adapter + sandbox (D44) is the correct trust posture. +- **Trade-off accepted:** external-harness sessions get wire-level introspection only (no in-process internals) — acceptable; full internals remain a Grok-only differentiator. + +## D46. Approval policy — pluggable on the D12 4-way enum (LOCKED) +- **Decision:** The approval decision path is an **`ApprovalPolicy` plugin chain** over the D12 enum: local prompt, mobile push, and declarative policies ("reads auto-allow; writes/egress always gate") compose in order, first non-defer wins. Because this gate is a security boundary (extending D12/D25), policy plugins require explicit user consent at install and are held to core testing bars (D21/D33). +- **Rationale:** The approval gate is where a hijacked session's *use* of its access is stopped; making the policy pluggable lets users tune strictness per deployment without forking, while keeping enforcement in core. + --- ## Summary of doc-level fixes required (mapped to decisions) @@ -190,3 +220,4 @@ | plan/18 | Azure Trusted Signing + budget; no live-swap; monetization | D29, D39, D30 | | plan/19 | Crate names; MVP=Phases 1-4; effort estimate; dependency spine; track-upstream task | D13, D8, D40, D31 | | plan/20 | Orca baseline match-all; embedding hypothesis; Windows contingency; monetization; risk register updates | D36, D10, D35, D30 | +| plan/21 | NEW — plugin architecture: seams, manifest/capabilities, first-party plugins, threat-model mapping, milestones | D41, D42, D43, D44, D45, D46 | diff --git a/plan/21-plugin-architecture.md b/plan/21-plugin-architecture.md new file mode 100644 index 0000000..a693431 --- /dev/null +++ b/plan/21-plugin-architecture.md @@ -0,0 +1,132 @@ +# Plan 21 — Plugin Architecture (extension points, credential plane, sandboxing, harness admission) + +**Status:** Authored for adversarial review. +**Anchored to:** D13, D17, D12, D23, D25, and the new D41–D46. +**One-line thesis:** Core Multiplexer stays small and security-reviewable; everything deployment-specific — credential vaults, session sandboxes, harness adapters, approval policies — is a plugin with declared, least-privilege capabilities. Users extend Multiplexer by writing plugins, not PRs. + +--- + +## 1. Purpose + +The server-centric runtime (D13) makes `multiplexer-server` the natural trust boundary for agent execution. That is only true if the server itself is minimal and if the things that vary per deployment (which vault, which sandbox, which harness, which approval policy) live behind stable, capability-scoped extension seams. This doc defines those seams. + +The first-party plugins that ship alongside core are the proof of the model: + +1. **`plugin-1password`** — CredentialProvider bridging a 1Password service account scoped to a single automation vault into the session-cache model (D23). +2. **`plugin-container-sandbox`** — SandboxProvider running each agent session in an isolated container on the server host. +3. **`plugin-acp-harness`** — HarnessAdapter admitting Claude Code / Codex / OpenCode / ZCode via the generic ACP machinery (D17). +4. **`plugin-approval-pack`** — ApprovalPolicy implementations over the D12 4-way enum (local prompt, mobile push, declarative policy). + +## 2. Non-goals + +- No in-core knowledge of any specific vault vendor, container runtime, or harness beyond Grok (in-process, D10). +- No plugin UI in the MVP beyond pane registration hooks; the editor/pane system consumes plugins, it does not host arbitrary web content. +- No dynamic marketplace/signing story in MVP; plugins are local installs from a manifest + directory (marketplace is a Phase 5+ concern, D30 adjacent). + +## 3. Extension points (trait seams) + +All plugin traits live in `multiplexer-wire`-adjacent `multiplexer-plugin-api` so client codegen never depends on plugin internals. Traits are async, object-safe, and versioned by the plugin API semver (§7). + +### 3.1 `CredentialProvider` (D43) + +```rust +#[async_trait] +pub trait CredentialProvider: Send + Sync { + /// List addressable references this provider can resolve, e.g. + /// "1pass://automation-vault/forgejo-agent/token". + async fn inventory(&self) -> Result, PluginError>; + /// Resolve a reference into the SERVER-SIDE session cache only. + /// Providers never hand secrets to plugins or clients. + async fn resolve(&self, r: &CredentialRef, cache: &SessionCache) + -> Result; +} +``` + +Resolution happens once at session start; values materialize only as `CachedSecretId` handles inside the server-side session cache (D23). Per-session credential injection into a sandbox (§3.2) uses those handles — a session sees only the references its task config names. **No live user-session `op` reads, ever (D23).** External vault auth (e.g., a 1Password service-account token) is itself a keychain secret on the server host. + +### 3.2 `SandboxProvider` (D44) + +```rust +#[async_trait] +pub trait SandboxProvider: Send + Sync { + /// Provision an isolated execution environment for one session. + async fn create(&self, spec: SandboxSpec) -> Result; + /// Inject only the CachedSecretIds the task config declares (env/files, + /// inside the sandbox, never on the host). + async fn inject(&self, h: &SandboxHandle, secrets: &[CachedSecretId]) -> Result<(), PluginError>; + async fn exec(&self, h: &SandboxHandle, cmd: ExecSpec) -> Result; + async fn destroy(&self, h: &SandboxHandle) -> Result<(), PluginError>; +} +``` + +The default `plugin-container-sandbox` uses per-session containers with: workspace bind-mount scoped to the session worktree, no host home exposure, egress policy per session config, and teardown that shreds injected secrets. The remote-agent independent-enforcement rule (D25) is implemented HERE: confinement is enforced by the sandbox, not trusted from the client. + +### 3.3 `HarnessAdapter` (D45) + +```rust +#[async_trait] +pub trait HarnessAdapter: Send + Sync { + fn id(&self) -> HarnessId; // "grok" | "claude-code" | "codex" | … + async fn start(&self, s: &SandboxHandle, cfg: HarnessConfig) + -> Result; // canonical events (D16) + async fn decide(&self, pending: &PermissionRequested) -> FourWay; // D12 plumbing only +} +``` + +Grok in-process remains a core adapter (D10). All external harnesses enter as HarnessAdapter plugins riding the generic ACP machinery (D17). Adding a harness is a plugin, never a core PR. + +### 3.4 `ApprovalPolicy` (D46) + +```rust +#[async_trait] +pub trait ApprovalPolicy: Send + Sync { + /// Map a pending PermissionRequested to a D12 decision. Policies may + /// escalate to a human (local prompt / mobile push) or apply declarative + /// rules ("reads auto-allow; writes and network egress always gate"). + async fn decide(&self, pending: &PermissionRequested, ctx: &SessionCtx) + -> Result; +} +``` + +Policies compose (ordered chain, first non-`defer` wins). The approval gate is a security boundary extending D12/D25: policy plugins require explicit user consent at install and are mutation-gated like core (D21). + +## 4. Manifest & capabilities (D42) + +```toml +# plugin.toml +[plugin] +id = "1password" +api = "1" # plugin API semver major +kind = "credential" # credential | sandbox | harness | approval | pane + +[capabilities] +credential-read = ["1pass://automation-vault/*"] +network = ["https://my.1password.com"] # egress allow-list, deny by default +``` + +- Capabilities are **declared, user-approved at install, and enforced** by the plugin host. No ambient authority: a plugin receives only the API handles its capabilities grant. +- Plugins run **out-of-process** against a versioned JSON-RPC sidecar of the same style as the wire (D20 discipline): the host can crash-isolate, resource-limit, and kill them. (WASM-in-process is a future optimization, not v1.) +- Unsigned plugins warn loudly; signed distribution is Phase 5+. + +## 5. Threat-model mapping + +- **Credential readability ("agent reads the vault"):** secrets exist only server-side in the session cache; sessions receive injected, task-scoped secrets inside a sandbox; clients never see any of it. +- **Credential abuse-in-session ("hijacked session USES its access"):** scoped per-agent tokens + sandbox egress policy + ApprovalPolicy gates on destructive operations. +- **Plugin compromise:** capabilities + process isolation bound a malicious plugin to its declared slice; the credential provider is the highest-value target and therefore gets the strictest review bar (see §7 testing). + +## 6. Milestone mapping (per the D40 spine) + +- Phase 1 additions: `multiplexer-plugin-api` crate, manifest + host, capability enforcement (unit + property + mutation per D21/D33). +- Phase 4 pull-forward (parallel-safe per D40): container SandboxProvider + remote relay enforcement wiring (D25). +- First-party plugins land with their phases: 1Password + approval-pack early (they harden the MVP), ACP harness adapters with multi-harness admission, pane plugins with Phase 2. + +## 7. Compatibility & testing + +- Plugin API semver: `api = "1"`; breaking bumps are additive-major with a deprecation window. Trait sketches above are normative for names, not signatures. +- Every plugin API boundary is fuzzed and mutation-gated; `plugin-1password` and the sandbox host additionally require integration tests against a real service-account vault and a real container runtime respectively. No mock-only security tests. + +## 8. Open questions (for adversarial review) + +1. Pane plugins (D42 `pane` kind): render via GPUI hooks only, or webview panes with a hardened origin model? Recommend GPUI-only for MVP. +2. Should `SandboxProvider` also own per-session egress network policy config, or does that live in session config consumed by the provider? Recommend the latter. +3. Credential rotation TTLs: enforce minimums at the API level or leave to providers? Recommend providers, with a lint warning.