diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9716344b..097fe5f8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -226,7 +226,10 @@ Adapters should stay thin. A new or changed adapter should: 4. update the provider matrix and adapter-control-plane docs See [workflows/adapter-porting.md](workflows/adapter-porting.md) for the -porting checklist. +porting checklist and +[docs/extending-adapters.md](docs/extending-adapters.md) for worked examples +(adding a credential type, extending an adapter) annotated with the invariants +each step touches. ## Package naming diff --git a/adapters/README.md b/adapters/README.md index 0ace75b8..ad0f1924 100644 --- a/adapters/README.md +++ b/adapters/README.md @@ -3,16 +3,37 @@ Each adapter maps the shared Workcell runtime into one provider's native control plane. -Current adapters: +Current adapters (each README covers auth methods, managed control-plane files, +and behavior): -- `codex/` -- `claude/` -- `copilot/` -- `gemini/` +- [`codex/`](codex/README.md) +- [`claude/`](claude/README.md) +- [`copilot/`](copilot/README.md) +- [`gemini/`](gemini/README.md) Planned fail-closed scaffolds: -- `antigravity/` +- [`antigravity/`](antigravity/README.md) + +## Common adapter contract + +Every supported adapter follows the same shape: + +- one shared runtime VM-plus-container boundary; the adapter is thin and its + provider config is defense in depth, not the boundary +- a session-local provider home rebuilt each launch from immutable baselines + under `adapters//`, explicit injection-policy inputs, and masked + workspace imports (`runtime/container/home-control-plane.sh`) +- explicit credential keys only — no host home, keychain, socket, or ambient CLI + auth passthrough (see [`../docs/injection-policy.md`](../docs/injection-policy.md) + and [`../docs/invariants.md`](../docs/invariants.md)) +- provider-native unsafe-flag rejection in the wrapper + (`runtime/container/provider-policy.sh`), enforced in **every** mode — the + wrapper re-checks arguments (`WORKCELL_WRAPPER_CONTEXT=1`) so even `breakglass` + cannot pass them; breakglass raises the sandbox floor, not the unsafe-flag policy + +The cross-adapter mapping tables live in +[`../docs/adapter-control-planes.md`](../docs/adapter-control-planes.md). Adapter rules: @@ -30,3 +51,9 @@ under `adapters//`. There is no longer a per-provider Go sub-package; see `internal/adapters/adapters.go` for the public API that the injection, policy, and runtime paths consume. A provider directory without a registry row is only a fail-closed scaffold. + +For step-by-step worked examples — adding a credential type and extending or +adding an adapter, each annotated with the invariants and threat-model items it +touches — see [`../docs/extending-adapters.md`](../docs/extending-adapters.md). +The porting checklist is in +[`../workflows/adapter-porting.md`](../workflows/adapter-porting.md). diff --git a/adapters/antigravity/README.md b/adapters/antigravity/README.md index 397bc84c..46e345ae 100644 --- a/adapters/antigravity/README.md +++ b/adapters/antigravity/README.md @@ -1,18 +1,49 @@ # Google Antigravity CLI Adapter This adapter directory is a fail-closed planning scaffold. Workcell recognizes -`antigravity` as a planned Google Antigravity CLI provider id, but it does not -install, prepare, authenticate, or launch Antigravity yet. +`antigravity` as a planned Google Antigravity CLI provider id +(`internal/providerid/providerid.go`), but it does not install, prepare, +authenticate, or launch Antigravity yet. It is not in the supported-provider +registry (`internal/adapters/data.go`) and `providerid.IsValid("antigravity")` +returns false. -Support promotion requires the same review unit to add: +## Auth methods + +None today. A future path must first pin official install and auth provenance, +then stage only reviewed Google auth material into session-local provider state. +Host Google account caches, browser profiles, keychains, host homes, and +provider caches are not acceptable implicit safe-path inputs +(`docs/injection-policy.md`, `docs/invariants.md` §1). No Antigravity credential +keys exist in current releases; they must not appear in operator policy until +the matching adapter, validation, and docs land. + +## Managed control-plane files + +None today. Before support is claimed the adapter must own session-local +provider home/cache/settings state under `/state/agent-home` and explicitly map +or block Antigravity's subagent, plugin, MCP, sandbox, permission, hook, and +instruction surfaces (`docs/adapter-control-planes.md`). + +## Adapter behavior + +`workcell --agent antigravity` exits before runtime preparation with a +"planned Workcell provider adapter, but it is not supported yet" diagnostic and +a non-zero status (`scripts/workcell`). Support promotion requires the same +review unit to add: - an official pinned Antigravity CLI install path with provenance evidence - explicit Antigravity home and cache-state handling under `/state/agent-home` - a staged Google auth handoff that does not mount host Google account state, browser profiles, keychains, host homes, or provider caches - provider-native unsafe-flag rejection in the Workcell wrapper + (`runtime/container/provider-policy.sh`) - deterministic dry-run and scenario coverage - a live provider certification run before any supported Tier 1 matrix claim -Until those gates land, `workcell --agent antigravity` exits before runtime -preparation with an unsupported-provider diagnostic. +## See also + +- [../README.md](../README.md) — adapter index and common contract +- [../../docs/adapter-control-planes.md](../../docs/adapter-control-planes.md) +- [../../docs/invariants.md](../../docs/invariants.md) +- [../../docs/extending-adapters.md](../../docs/extending-adapters.md) — worked + contributor examples (including adding a new adapter) diff --git a/adapters/claude/README.md b/adapters/claude/README.md index 9f8de404..33050662 100644 --- a/adapters/claude/README.md +++ b/adapters/claude/README.md @@ -1,21 +1,85 @@ # Claude Adapter The Claude adapter maps the shared Workcell runtime into Claude Code's native -files and guardrails. +files and guardrails. Hooks and settings are defense in depth; the runtime +VM-plus-container boundary stays primary. -Managed surfaces: +## Auth methods -- `~/.claude/settings.json` -- rendered `~/.claude/CLAUDE.md` -- `~/.mcp.json` -- Claude auth compatibility files when injected -- a reviewed `PreToolUse` Bash hook as defense in depth +- `claude_auth` credential key — a direct staged auth file mirrored into the + Claude auth locations under `~/.claude/`, `~/.claude.json`, and + `~/.config/claude-code/` (`internal/adapters/data.go`, + `runtime/container/home-control-plane.sh`). +- `claude_api_key` credential key — wired through an `apiKeyHelper` script at + `~/.claude/workcell/api-key-helper.sh` that reads the mounted key file, so a + second plaintext key copy is not seeded into the session + (`runtime/container/home-control-plane.sh`). +- `claude_mcp` credential key — reviewed Claude MCP config seeded to `~/.mcp.json`. +- `claude-macos-keychain` resolver — a fail-closed scaffold: it records the + intended host-side auth source in policy but Workcell still aborts launch + until a supported export path exists + (`internal/authresolve/resolve_credential_sources.go`, + `docs/injection-policy.md`). +- Shared GitHub CLI (`github_hosts`, `github_config`) and SSH inputs apply + (`sharedCredentialsEnabled: true` in `internal/adapters/data.go`). -Key points: +See [../../docs/injection-policy.md](../../docs/injection-policy.md). -- hooks are guardrails, not the security boundary -- the default MCP template is empty -- `claude_api_key`, `claude_auth`, and `claude_mcp` are the supported - credential/injection paths -- GUI or IDE use is lower assurance unless it is only a client to the same - bounded runtime +## Managed control-plane files + +Repo baselines under `adapters/claude/`: + +- `.claude/settings.json`: reviewed session settings seeded to + `~/.claude/settings.json` by `workcell_render_claude_settings` — this is the + file to edit for session-home settings changes. +- `managed-settings.json`: the separate enterprise managed-policy file, installed + and verified at `/etc/claude-code/managed-settings.json` (it is **not** the + session-home `~/.claude/settings.json`). +- `CLAUDE.md`: managed baseline rendered into `~/.claude/CLAUDE.md`. +- `mcp-template.json`: MCP template seeded to `~/.mcp.json`. The default template + is empty — no live MCP servers ship in the baseline. +- `hooks/guard-bash.sh`: the reviewed `PreToolUse` Bash hook. + +In-container reserved session targets include `~/.claude`, +`~/.claude/settings.json`, `~/.claude/CLAUDE.md`, the auth mirrors +(`~/.claude/.credentials.json`, `~/.claude.json`, +`~/.config/claude-code/auth.json`), the API-key helper dir `~/.claude/workcell`, +and `~/.mcp.json` (`ReservedTargets` in `internal/adapters/data.go`). + +## Adapter behavior + +- Each launch rebuilds the provider home from the immutable baseline plus + explicit injection inputs; repo-local `.claude/` and `CLAUDE.md` are masked and + imported only as reviewed layers + (`runtime/container/home-control-plane.sh`, `docs/invariants.md` §3). +- The reviewed `PreToolUse` Bash hook blocks common trust-widening shell + patterns. It is defense in depth: it does not replace the runtime boundary and + does not cover non-Bash Claude tools + (`docs/adapter-control-planes.md#claude-hook-coverage`). +- Autonomy is set host-side via `workcell --agent-autonomy` (mapped to + `--permission-mode`); the wrapper does not honor provider-native overrides. +- Unsafe-argument policy (`reject_unsafe_claude_args` in + `runtime/container/provider-policy.sh`): the wrapper blocks + `--dangerously-skip-permissions`, `--add-dir`, `--allowedTools`, + `--mcp-config`, `--plugin-dir`, `--settings`, `--setting-sources`, + `--system-prompt`, `--append-system-prompt`, in-session `--permission-mode` + overrides, and the `install`/`update` lifecycle commands. These are rejected in + **every** mode including `breakglass`: `provider-wrapper.sh` re-checks arguments + (it sets `WORKCELL_WRAPPER_CONTEXT=1`, which makes the breakglass exemption + inapplicable), so `container-smoke.sh` confirms breakglass overrides still fail. + Breakglass raises the container sandbox floor, not the provider unsafe-flag + policy. +- The wrapper scrubs provider env such as `CLAUDE_CONFIG_DIR`, `GH_TOKEN`, + `GITHUB_TOKEN`, and the OpenTelemetry variables before launch + (`runtime/container/provider-wrapper.sh`). + +GUI or IDE use is lower assurance unless it is only a client to the same bounded +runtime. + +## See also + +- [../README.md](../README.md) — adapter index and common contract +- [../../docs/adapter-control-planes.md](../../docs/adapter-control-planes.md) +- [../../docs/invariants.md](../../docs/invariants.md) +- [../../docs/extending-adapters.md](../../docs/extending-adapters.md) — worked + contributor examples diff --git a/adapters/codex/README.md b/adapters/codex/README.md index f0bf5d38..8cde590f 100644 --- a/adapters/codex/README.md +++ b/adapters/codex/README.md @@ -1,38 +1,80 @@ # Codex Adapter The Codex adapter maps the shared Workcell boundary into Codex-native controls. -Adapter layout (paths relative to `adapters/codex/`): +It re-seeds a session-local Codex home from reviewed baselines on every launch; +the runtime VM-plus-container boundary stays primary and Codex config is defense +in depth, not the boundary. -- `.codex/config.toml`: managed Codex base configuration template seeded into - the session-local Codex home as `~/.codex/config.toml` +## Auth methods + +- `codex_auth` credential key — a direct staged `auth.json` seeded to + `~/.codex/auth.json` (`internal/adapters/data.go`). +- `codex-home-auth-file` resolver — a reviewed host-side Codex auth-file reuse + path; still host-side preprocessing only, not Keychain passthrough + (`internal/authresolve/resolve_credential_sources.go`). +- Shared GitHub CLI (`github_hosts`, `github_config`) and SSH inputs apply + because Codex opts into shared credentials + (`sharedCredentialsEnabled: true` in `internal/adapters/data.go`). + +See [../../docs/injection-policy.md](../../docs/injection-policy.md) and +[../../docs/provider-bootstrap-matrix.md](../../docs/provider-bootstrap-matrix.md). + +## Managed control-plane files + +Repo baselines under `adapters/codex/` (paths relative to this directory): + +- `.codex/config.toml`: managed base configuration seeded into the session-local + Codex home as `~/.codex/config.toml`. The base sets no `sandbox_mode`, so every + sandbox decision is profile-scoped. - `.codex/{strict,development,build,breakglass}.config.toml`: Codex 0.134+ - profile-v2 layer files. The launcher selects one with `--profile `, - and Codex layers it on top of the base `config.toml`. Each layer carries the - per-profile `sandbox_mode`/`approval_policy`; the base config sets no sandbox - mode so every sandbox decision is profile-scoped. -- `.codex/AGENTS.md`: managed agent guidance seeded into the session-local - Codex home -- `.codex/rules/default.rules`: managed Codex execpolicy ruleset seeded - into the session-local Codex home. Workcell currently ships one - ruleset; the directory shape exists so future overlays can drop in - alongside `default.rules`. -- `.codex/agents/`: managed sub-agent guidance -- `managed_config.toml`: workcell-side managed-mode TOML consumed by the - launcher -- `requirements.toml`: workcell-side adapter requirements contract -- `mcp/config.toml`: MCP server config - -Workcell re-seeds this state into the session-local Codex home on launch. -Repo-local control files stay masked on the safe path and are imported only as -reviewed layers. - -Codex's own Linux `workspace-write` sandbox is pinned off on the managed -Workcell path. Workcell already provides the outer VM-plus-container boundary, -and the current Codex Linux sandbox depends on unprivileged user namespaces -that are unavailable inside the Tier 1 container. - -Final branch publication stays on the host through `workcell publish-pr`, not -from inside the container session. - -Codex CLI is Tier 1 when it runs fully inside the bounded runtime. App support -is only valid once it becomes a client of the same bounded executor. + profile-v2 layer files. The launcher selects one with `--profile `; + Codex layers it on top of the base config. Each layer carries the per-profile + `sandbox_mode`/`approval_policy`. +- `.codex/AGENTS.md`: managed agent guidance. +- `.codex/rules/default.rules`: managed Codex execpolicy ruleset. One ruleset + ships today; the directory shape lets future overlays drop in alongside it. +- `.codex/agents/`: managed sub-agent guidance. +- `managed_config.toml`: workcell-side managed-mode TOML consumed by the launcher. +- `requirements.toml`: workcell-side adapter requirements contract. +- `mcp/config.toml`: MCP server config (no live MCP defaults ship in the baseline). + +In-container reserved session targets: `~/.codex/{config.toml,auth.json,` +`AGENTS.md,managed_config.toml,requirements.toml,agents,rules,mcp}` +(`ReservedTargets` in `internal/adapters/data.go`). + +## Adapter behavior + +- The provider wrapper sets `CODEX_HOME=~/.codex`, re-seeds the baselines into the + session-local Codex home, and imports repo-local control-plane files only as + masked, reviewed layers (`runtime/container/provider-wrapper.sh`, + `runtime/container/home-control-plane.sh`). +- Codex's own Linux `workspace-write` sandbox is pinned off on the managed path: + the current Codex Linux sandbox needs unprivileged user namespaces that are + unavailable inside the Tier 1 container. +- `~/.codex/rules/` is read-only by default; it becomes a session-local writable + copy only in explicit lower-assurance cases (see + [../../docs/adapter-control-planes.md](../../docs/adapter-control-planes.md#codex-rules-mutability)). +- Unsafe-argument policy (`reject_unsafe_codex_args` in + `runtime/container/provider-policy.sh`): the wrapper blocks + `--dangerously-bypass-approvals-and-sandbox`, `--full-auto`, `-a`/ + `--ask-for-approval`, `--add-dir`, `--search`, `--remote`, `--enable`/ + `--disable`, `--cd`, `--sandbox danger-full-access`, reserved `--config` + overrides, off-mode `--profile` values, and `app`/`app-server`/`cloud`/`mcp`/ + `sandbox` subcommands outside the managed GUI path. These are rejected in + **every** mode including `breakglass`: `provider-wrapper.sh` re-checks arguments + (`WORKCELL_WRAPPER_CONTEXT=1`), so `container-smoke.sh` confirms breakglass + overrides still fail. Breakglass raises the sandbox floor, not the unsafe-flag + policy. +- Final branch publication stays on the host through `workcell publish-pr`, not + from inside the container session. + +Codex CLI is Tier 1 when it runs fully inside the bounded runtime. App support is +only valid once it becomes a client of the same bounded executor. + +## See also + +- [../README.md](../README.md) — adapter index and common contract +- [../../docs/adapter-control-planes.md](../../docs/adapter-control-planes.md) +- [../../docs/invariants.md](../../docs/invariants.md) +- [../../docs/extending-adapters.md](../../docs/extending-adapters.md) — worked + contributor examples diff --git a/adapters/copilot/README.md b/adapters/copilot/README.md index bc9fe08a..c5e3df7b 100644 --- a/adapters/copilot/README.md +++ b/adapters/copilot/README.md @@ -1,24 +1,73 @@ # GitHub Copilot CLI Adapter -This adapter owns the Workcell-managed GitHub Copilot CLI baseline. - -The safe path uses a pinned Copilot CLI binary, a session-local `COPILOT_HOME`, -a session-local `COPILOT_CACHE_HOME`, and one explicit credential key: -`copilot_github_token`. For auth-required launches, Workcell converts that -staged token into a host-mounted token handoff outside mounted provider state; -the runtime entrypoint moves it into a transient handoff file, unlinks the -mounted file, and re-execs without the token in its environment, then the -provider wrapper unlinks that file and exports the value as -`COPILOT_GITHUB_TOKEN` only for the managed -Copilot child process. The original staged token file is removed from direct -runtime mounts; the temporary handoff mount is outside provider state and is not -copied into `COPILOT_HOME`. - -The adapter intentionally does not use host GitHub CLI state, host Copilot -provider state (`~/.copilot`, `~/.config/github-copilot`, -`~/.cache/github-copilot`), host keychains, `GH_TOKEN`, or `GITHUB_TOKEN` as -readiness or auth inputs. Copilot custom instructions are disabled; plugin, -MCP, custom-agent, hook, skill, dynamic-retrieval, and remote-session expansion -overrides stay -blocked on the default path until each one has a separate Workcell review unit -and validation evidence. +This adapter owns the Workcell-managed GitHub Copilot CLI baseline. It runs a +pinned Copilot CLI binary against session-local provider state; the runtime +VM-plus-container boundary stays primary. + +## Auth methods + +- `copilot_github_token` credential key — the only Copilot auth input + (`internal/adapters/data.go`; Copilot sets `sharedCredentialsEnabled: false`, + so shared GitHub CLI state is deliberately excluded). +- For auth-required launches the staged token is not seeded into provider state. + Instead it flows through a scrubbing handoff + (`runtime/container/entrypoint.sh`, `runtime/container/provider-wrapper.sh`): + the entrypoint moves the token into a transient runtime handoff file, unlinks + the mounted file, and re-execs without the token in its environment while + staying PID 1 (no Docker `--init`) so `/proc/1/environ` is scrubbed; the + provider wrapper then unlinks the handoff file and exports the value as + `COPILOT_GITHUB_TOKEN` only for the managed Copilot child process. The original + staged token file and its direct-mount copy are removed from direct runtime + mounts and are not copied into `COPILOT_HOME`. +- The adapter does not use host GitHub CLI state, host Copilot provider state + (`~/.copilot`, `~/.config/github-copilot`, `~/.cache/github-copilot`), host + keychains, `GH_TOKEN`, `GITHUB_TOKEN`, or ambient `gh auth token` as auth or + readiness inputs (`docs/invariants.md` §1). + +See [../../docs/injection-policy.md](../../docs/injection-policy.md). + +## Managed control-plane files + +Copilot ships no repo baseline files under `adapters/copilot/`; its control +plane is entirely session-local. The adapter owns a session-local `COPILOT_HOME` +and `COPILOT_CACHE_HOME`, the token handoff, logs, and the cache/config +directories. In-container reserved session targets: `~/.copilot`, +`~/.copilot/AGENTS.md`, `~/.copilot/logs`, `~/.cache/github-copilot`, and +`~/.config/github-copilot` (`ReservedTargets` in `internal/adapters/data.go`). + +## Adapter behavior + +- The wrapper sets `COPILOT_HOME=~/.copilot` and + `COPILOT_CACHE_HOME=~/.cache/github-copilot` and launches with custom + instructions disabled (`runtime/container/provider-wrapper.sh`). +- Autonomy maps host-side through the reviewed wrapper; shell access is withheld + by omission from `--available-tools` + (`docs/adapter-control-planes.md#autonomy-mapping`). +- Repo-local Copilot control-plane files (`.github/copilot-instructions.md`, + `.github/instructions/**`, `.github/mcp.json`, `.github/copilot/settings*.json`, + repo-local skill/hook dirs) are masked on the safe path (`docs/invariants.md` + §3). +- Unsafe-argument policy (`reject_unsafe_copilot_args` in + `runtime/container/provider-policy.sh`): the wrapper blocks the + `init`/`login`/`mcp`/`plugin`/`skill`/`update` lifecycle and control-plane + subcommands and a broad set of trust-widening flags (MCP toolset/config, + `--allow-all*`, `--allow-tool`/`--allow-url`, `--add-dir`, `--dynamic-retrieval`, + `--no-custom-instructions`, `--remote`/`--share*`/`--worktree`, `--yolo`, + bundled short options, and more). These are rejected in **every** mode + including `breakglass`: `provider-wrapper.sh` re-checks arguments + (`WORKCELL_WRAPPER_CONTEXT=1`), so `container-smoke.sh` confirms breakglass + overrides still fail. Breakglass raises the sandbox floor, not the unsafe-flag + policy. +- Plugin, MCP, custom-agent, hook, skill, dynamic-retrieval, and remote-session + expansion each stay blocked on the default path until a separate Workcell + review unit and validation evidence land. +- Any Copilot telemetry, OpenTelemetry, or content-capture enablement is a + lower-assurance, acknowledged path, not a default (`docs/invariants.md` §6). + +## See also + +- [../README.md](../README.md) — adapter index and common contract +- [../../docs/adapter-control-planes.md](../../docs/adapter-control-planes.md) +- [../../docs/invariants.md](../../docs/invariants.md) +- [../../docs/extending-adapters.md](../../docs/extending-adapters.md) — worked + contributor examples diff --git a/adapters/gemini/README.md b/adapters/gemini/README.md index 192373fa..843c5c0c 100644 --- a/adapters/gemini/README.md +++ b/adapters/gemini/README.md @@ -1,23 +1,69 @@ # Gemini Adapter -The Gemini adapter seeds Gemini CLI's session-local home from reviewed -baselines and explicit injection inputs. - -Managed surfaces: - -- `~/.gemini/settings.json` -- rendered `~/.gemini/GEMINI.md` -- `~/.gemini/.env` -- `~/.gemini/oauth_creds.json` -- `~/.gemini/projects.json` -- `~/.gemini/trustedFolders.json` - -Key points: - -- Workcell's external VM-plus-container boundary is primary; Gemini's own - sandbox is optional and not the Tier 1 boundary here -- repo-local `.gemini/` content stays masked on the safe path -- `gemini_env`, `gemini_oauth`, `gemini_projects`, and `gcloud_adc` cover the - supported long-lived auth and project-state paths for upstream-served Gemini - auth modes; see `docs/provider-matrix.md` for the Gemini retirement caveat -- `breakglass` restores Gemini's own folder-trust prompt behavior +The Gemini adapter seeds Gemini CLI's session-local home from reviewed baselines +and explicit injection inputs. Workcell's external VM-plus-container boundary is +primary; Gemini's own sandbox is optional and not the Tier 1 boundary here. + +## Auth methods + +- `gemini_env` credential key — API key, GCA, or Vertex configuration seeded to + `~/.gemini/.env` (`internal/adapters/data.go`). +- `gemini_oauth` credential key — cached Gemini OAuth state seeded to + `~/.gemini/oauth_creds.json`. +- `gemini_projects` credential key — persisted project registry seeded to + `~/.gemini/projects.json`; a supplemental input, not a standalone auth mode. +- `gcloud_adc` credential key — Vertex application-default credentials seeded to + `~/.config/gcloud/application_default_credentials.json`; supplemental to + `gemini_env` Vertex config, not a standalone auth mode. +- Shared GitHub CLI (`github_hosts`, `github_config`) and SSH inputs apply + (`sharedCredentialsEnabled: true` in `internal/adapters/data.go`). +- Google OAuth/ADC needs extra egress: `accounts.google.com:443`, + `oauth2.googleapis.com:443`, `sts.googleapis.com:443` + (`GeminiGoogleAuthEndpoints` in `internal/adapters/data.go`). + +See [../../docs/injection-policy.md](../../docs/injection-policy.md) and the +Gemini retirement caveat in +[../../docs/provider-matrix.md](../../docs/provider-matrix.md). + +## Managed control-plane files + +Repo baselines under `adapters/gemini/`: + +- `.gemini/settings.json`: managed settings seeded to `~/.gemini/settings.json`. +- `GEMINI.md`: managed baseline rendered into `~/.gemini/GEMINI.md`. + +Additional in-container session targets: `~/.gemini/.env`, +`~/.gemini/oauth_creds.json`, `~/.gemini/projects.json`, +`~/.gemini/trustedFolders.json`, and the gcloud ADC path +(`ReservedTargets` in `internal/adapters/data.go`). + +## Adapter behavior + +- Each launch rebuilds the provider home from the baseline plus explicit + injection inputs; repo-local `.gemini/` content is masked on the safe path + (`runtime/container/home-control-plane.sh`, `docs/invariants.md` §3). +- Workcell seeds `~/.gemini/trustedFolders.json` for `/workspace` so masked + ephemeral sessions do not force a restart-based trust prompt; `breakglass` + restores Gemini's own folder-trust flow + (`docs/adapter-control-planes.md#gemini-folder-trust`). +- The wrapper sanitizes Gemini's own-sandbox env + (`GEMINI_SANDBOX*`) before launch (`sanitize_gemini_sandbox_env` in + `runtime/container/provider-wrapper.sh`). +- Autonomy is set host-side via `workcell --agent-autonomy` (mapped to + `--approval-mode`); provider-native overrides are not honored. +- Unsafe-argument policy (`reject_unsafe_gemini_args` in + `runtime/container/provider-policy.sh`): the wrapper blocks + `--*dangerously*`, `--*bypass*permission*`, `--sandbox`, `--add-dir`, + `-y`/`--yolo`, and in-session `--approval-mode` overrides. These are rejected in + **every** mode including `breakglass`: `provider-wrapper.sh` re-checks arguments + (`WORKCELL_WRAPPER_CONTEXT=1`), so `container-smoke.sh` confirms breakglass + overrides still fail. Breakglass raises the sandbox floor, not the unsafe-flag + policy. + +## See also + +- [../README.md](../README.md) — adapter index and common contract +- [../../docs/adapter-control-planes.md](../../docs/adapter-control-planes.md) +- [../../docs/invariants.md](../../docs/invariants.md) +- [../../docs/extending-adapters.md](../../docs/extending-adapters.md) — worked + contributor examples diff --git a/docs/extending-adapters.md b/docs/extending-adapters.md new file mode 100644 index 00000000..47c9ff95 --- /dev/null +++ b/docs/extending-adapters.md @@ -0,0 +1,217 @@ +# Extending Adapters: Worked Contributor Examples + +Two step-by-step examples for the most common adapter-contributor tasks. Each +step names the file it touches and the security invariant or threat-model item +it must satisfy, so a change either preserves the boundary or fails a gate. + +Read [invariants.md](invariants.md) and [threat-model.md](threat-model.md) +first. For the higher-level porting checklist see +[../workflows/adapter-porting.md](../workflows/adapter-porting.md). For the +per-adapter reference see [adapter-control-planes.md](adapter-control-planes.md) +and each adapter's `README.md` under [`../adapters/`](../adapters/README.md). + +The per-adapter registry is data, not code: adding a credential type or a +provider is a table edit in `internal/adapters/data.go` plus the runtime seeding +and docs, not a new Go package (`internal/adapters/adapters.go`). + +## Example 1: Add a credential type + +Goal: add a new provider-native credential key (for an existing adapter) that an +operator can stage through injection policy. This walkthrough uses a +hypothetical `gemini_settings` key for the Gemini adapter as a concrete shape; +substitute your real key and target. + +### Steps (add a credential type) + +1. **Register the key and its mount path.** In `internal/adapters/data.go`, add + the key to the provider's `credentialKeys` and add a + `credentialContainerPaths[key]` entry under + `/opt/workcell/host-inputs/credentials/`. Add the in-container destination to + that provider's `reservedTargets`. + - Invariant [§2 writes stay inside the intended workspace](invariants.md#2-writes-stay-inside-the-intended-workspace): + the reserved target makes the destination Workcell-owned so a `[[copies]]` + entry cannot clobber it (see the "no writes into Workcell-managed + control-plane paths" limit in + [injection-policy.md](injection-policy.md#deliberate-limits)). + - Guardrail: `TestScopedCredentialKeysHaveContainerPaths` and + `TestCredentialContainerPathsRootedAtHostInputs` in + `internal/adapters/adapters_test.go` fail if the mount path is missing or + not rooted at the host-inputs credentials dir. + +2. **(Optional) Register a host resolver.** Only if the key is resolver-backed, + add it to `allowedResolvers` in + `internal/authresolve/resolve_credential_sources.go`, and add the matching + `resolveCredential` and `ProbeResolverReadiness` cases in + `resolve_provider_credentials.go` — the `allowedResolvers` entry only makes + policy validation accept the name; without the resolution/probe cases, launch + returns `unsupported credential resolver`. Resolvers materialize to + ordinary files under the per-launch injection bundle host-side; they never + pass Keychain or host-agent access into the runtime. + - Invariant [§1 host secrets stay outside the default trust boundary](invariants.md#1-host-secrets-stay-outside-the-default-trust-boundary): + resolver-backed auth is host-side preprocessing only + ([injection-policy.md](injection-policy.md#provider-auth-maturity)). + +3. **Confirm rendering picks up the key.** Rendering is table-driven off + `adapters.CredentialContainerPaths()` in + `internal/injection/render_credentials.go`, so a registered key flows without + editing the renderer. It validates that direct credential sources live + outside the mounted workspace and are owner-only. + - Invariant [§1](invariants.md#1-host-secrets-stay-outside-the-default-trust-boundary): + a source under the workspace is rejected so the original secret path is not + also exposed through the workspace mount + ([injection-policy.md](injection-policy.md#how-it-works)). + +4. **Seed the credential into the provider home.** In + `runtime/container/home-control-plane.sh`, copy the mounted credential into + its provider-home target with + `workcell_copy_manifest_credential_file "${HOME}/"` (the Gemini + block near the other `gemini_*` seeds is the pattern). Validate the file shape + if the provider requires it. + - If the credential is optional or mode-filtered, guard the copy so an absent + credential does not abort the whole seed path: `home-control-plane.sh` runs + under `set -e` and `workcell_copy_manifest_credential_file` returns non-zero + when the key has no mount path, so follow the existing seeds' `|| true` or + `if ! ...` pattern rather than a bare call. + - Invariant [§1](invariants.md#1-host-secrets-stay-outside-the-default-trust-boundary): + the credential is copied from a read-only mount into session-local state and + the direct mount is not persisted back into the baseline. + +5. **Document the key.** Add a row to the credential-keys table and the + provider-auth-maturity table in [injection-policy.md](injection-policy.md), + update [adapter-control-planes.md](adapter-control-planes.md) if the managed + file set changes, and add the key to the adapter's `README.md` auth section. + - Also update the operator status surface: `print_injection_status` in + `scripts/workcell` prints a hard-coded `supported_credential_keys=...` list + when no policy is loaded, so a new key is accepted by the table-driven Go + paths but missing from `workcell --auth-status` guidance until that list is + updated too. + - Threat-model note: an undocumented credential path is exactly the "trust + widened without review" case CONTRIBUTING flags — keep the docs in the same + change. + +6. **Test.** Extend `internal/adapters/adapters_test.go` and the injection render + tests, then run `go test ./internal/adapters/... ./internal/injection/...`. + +> **The registration spans layers — grep to find them all.** A credential key is +> also referenced by the host auth-management surfaces: `workcell auth set` +> staging destinations (`canonicalCredentialDestinations`, declared in +> `internal/authpolicy/manage.go` and consumed by `staging.go`) and +> `--auth-status` ordering/summaries +> (`statusOrder`, `bootstrapSummaryForCredential` in `internal/authpolicy/`), +> plus the hard-coded `supported_credential_keys` list in `scripts/workcell`. +> These sites are intentionally spread across the validation, resolution, +> staging, and status layers, so grep the tree for an existing key (for example +> `codex_auth`) and mirror every hit — a key that lands only in the registry +> renders from hand-written TOML but fails `workcell auth set` or reports +> `provider_auth_mode=none`. + +### Checklist touched (add a credential type) + +- [§1 host secrets stay outside the boundary](invariants.md#1-host-secrets-stay-outside-the-default-trust-boundary) + (steps 2, 3, 4) +- [§2 writes stay inside the intended workspace](invariants.md#2-writes-stay-inside-the-intended-workspace) + (step 1: reserved target ownership) +- Injection-policy [deliberate limits](injection-policy.md#deliberate-limits): + no arbitrary env-var secret injection, no `[[copies]]` into managed paths + (steps 1, 3) + +## Example 2: Extend an adapter (or add a new one) + +Goal: change an adapter's provider-native surface — for instance add a blocked +unsafe flag, or promote a planned adapter such as `antigravity`. + +### Steps (extend an adapter) + +1. **Lock the shared boundary first.** Do not add host mounts, sockets, or + ambient credential passthrough. Confirm the VM profile, inner container, + narrow mount set, and network posture are unchanged + ([../workflows/adapter-porting.md](../workflows/adapter-porting.md), step 1). + - Invariant [§1](invariants.md#1-host-secrets-stay-outside-the-default-trust-boundary) + and [§2](invariants.md#2-writes-stay-inside-the-intended-workspace). + +2. **Register the provider (new adapter only).** Add the id to + `internal/providerid/providerid.go` and the supported set, and add a row to + `providers` in `internal/adapters/data.go` with its credential keys, container + paths, and reserved targets. A directory without a registry row stays a + fail-closed scaffold (see the `antigravity` README). + - Also wire the host launcher and runtime dispatch: `scripts/workcell` + validates the agent name against a fixed set (which also backs `--help`, + `workcell why`, and `--auth-status`), and `runtime/container/entrypoint.sh`, + the Rust core-launcher, and the Dockerfile core-wrapper symlinks map default + launches and managed-wrapper routing for the same fixed + `codex|claude|copilot|gemini` set. Until all of these accept the id, + `workcell --agent ` fails before or during runtime prep (rejected as an + `Unsupported agent/ui combination`, or not routed through the managed + provider wrapper), so the registry edits alone are not enough. + - Exec-guard the binary (security): a provider that installs a direct runtime + binary must also be added to `PROTECTED_RUNTIME_PATHS` + (`runtime/container/rust/src/lib.rs`) and the development-mode blocklist + (`development_wrapper_protected_runtime_match` and the provider + command-name case in `runtime/container/development-wrapper.sh`). Otherwise + `/usr/local/libexec/workcell/real/` can be executed directly, bypassing + the managed wrapper and its unsafe-argument rejection. + - Guardrail: `internal/providerid/providerid_test.go` asserts a planned + provider stays out of the supported set until support lands. + +3. **Add or extend unsafe-argument rejection.** Edit the provider's + `reject_unsafe__args` in `runtime/container/provider-policy.sh` (and wire + a new provider into `validate_command_args` and + `runtime/container/provider-wrapper.sh`). Block provider-native flags and + subcommands that widen trust. The only code-level exemption is + `provider_policy_allows_breakglass`, but it is false inside + `provider-wrapper.sh` (which always sets `WORKCELL_WRAPPER_CONTEXT=1`), so the + wrapper re-check rejects these flags even in a `breakglass` session — + `container-smoke.sh` asserts breakglass overrides still fail. + - Invariant [§3 repo policy must not silently widen trust](invariants.md#3-repo-policy-must-not-silently-widen-trust) + and [§5 destructive or trust-widening actions need defense in depth](invariants.md#5-destructive-or-trust-widening-actions-need-defense-in-depth). + +4. **Seed the control plane.** Rebuild the provider home in + `runtime/container/home-control-plane.sh` from the immutable baseline under + `adapters//`, explicit injection inputs, and masked workspace imports. + Mask any repo-local provider control-plane files the provider reads. + - Wire the manifest: `runtime/container/control-plane-manifest.json` (generated + from `internal/metadatautil/core.go`) is the inventory of adapter baseline + files; `workcell_verify_control_plane_prefix` only checks the entries it + already lists, so it will not by itself catch a baseline file you forgot to + register. The required gate is `scripts/verify-control-plane-manifest.sh`, + which regenerates the manifest and diffs it against the committed file — so + add the new baseline files to the manifest source, regenerate, and run that + check, or an unlisted baseline file slips through. + - Invariant [§3](invariants.md#3-repo-policy-must-not-silently-widen-trust): + repo content must not retake the control plane. + +5. **Map autonomy and label lower-assurance paths.** Map + `workcell --agent-autonomy` to the provider's native flags in the wrapper and + record the mapping in + [adapter-control-planes.md](adapter-control-planes.md#autonomy-mapping). + Scrub provider telemetry env by default; any opt-in is lower assurance. + - Invariant [§4 network posture is explicit](invariants.md#4-network-posture-is-explicit) + and [§6 lower-assurance paths are labeled](invariants.md#6-lower-assurance-paths-are-labeled). + +6. **Ship validation with the change.** Add or extend deterministic tests + (`internal/adapters/adapters_test.go`, provider-policy coverage) and a + scenario/smoke path. No adapter change is complete without matching invariant + coverage ([../workflows/adapter-porting.md](../workflows/adapter-porting.md), + step 6). + - Promotion gate: before `provider-matrix.md` claims a supported Tier 1, run + the live provider certification (`provider-e2e`) — deterministic tests and a + smoke path are necessary but not sufficient. A supported-tier claim without + that live-certification evidence is exactly what the release gate forbids. + +7. **Update the docs in the same change.** Update + [provider-matrix.md](provider-matrix.md), + [adapter-control-planes.md](adapter-control-planes.md), and the adapter's + `README.md`. + +### Checklist touched (extend an adapter) + +- [§1](invariants.md#1-host-secrets-stay-outside-the-default-trust-boundary), + [§2](invariants.md#2-writes-stay-inside-the-intended-workspace) (step 1: no new + host exposure) +- [§3 no silent trust widening](invariants.md#3-repo-policy-must-not-silently-widen-trust) + (steps 3, 4) +- [§4 explicit network posture](invariants.md#4-network-posture-is-explicit) and + [§6 labeled lower-assurance paths](invariants.md#6-lower-assurance-paths-are-labeled) + (step 5) +- [§5 defense in depth](invariants.md#5-destructive-or-trust-widening-actions-need-defense-in-depth) + (step 3: wrapper-side rejection is defense in depth, not the boundary) diff --git a/runtime/container/control-plane-manifest.json b/runtime/container/control-plane-manifest.json index c9fd8648..75ca983d 100644 --- a/runtime/container/control-plane-manifest.json +++ b/runtime/container/control-plane-manifest.json @@ -148,7 +148,7 @@ "kind": "adapter-baseline", "repo_path": "adapters/copilot/README.md", "runtime_path": "/opt/workcell/adapters/copilot/README.md", - "sha256": "251e21f07123df83faf3939dabbf5835f2ee652850e45cbebf41929ad184c6b6" + "sha256": "8152b7c1490787bed3b99b5dd20c072b6cf34e482ea99f3fb968a599c19c9aaa" }, { "kind": "adapter-baseline",