# Nimbus — Claude Code Context
## Project Overview
Nimbus is a **local-first AI agent framework**: a headless Bun Gateway that maintains a private SQLite index of the user's data across ~90 cloud services (Google / Microsoft / GitHub / GitLab / Slack / Jira / Notion + observability, CI-CD, security-quality, feature-flags, GitOps, data-BI, deploy, finance, and support tools — full roster: `CONNECTOR_VAULT_SECRET_KEYS` in `packages/gateway/src/connectors/connector-secrets-manifest.ts`), optional `[[filesystem.roots]]` indexing, and the local filesystem via first-party MCP (Model Context Protocol) connectors, and executes multi-step agentic workflows on the user's behalf. Clients (CLI, Tauri 2.0 desktop) talk to the Gateway only over JSON-RPC 2.0 IPC.
**Runtime:** Bun v1.2+ / TypeScript 7.x strict (two pins stay on TypeScript 6: `packages/docs`, because `astro check` refuses TypeScript 7.0, and the root `typescript-compiler-api` alias, which `scripts/cleanup/` and `scripts/structure-audit/check-connector-hosts.ts` import because 7 ships its compiler API only under unstable subpaths, and which `audit:boundaries` hands to dependency-cruiser because that accepts only `typescript` below 7) · **Linter:** Biome · **License:** AGPL-3.0 (GNU Affero GPL; gateway/cli) + MIT (sdk)
**Status:** Phases 1–6 ✅ (Phase 6, Team, closed 2026-07-19). From Phase 6 onward the build order is the Phase 7+ Sequencing Spine overlay (S1 → S5), not the phase numbers. **Spine S1 (Local Brain) ✅** 2026-08-20; **Spine S2 (Local Compute Fleet) ✅** 2026-10-10, then the pre-S3 close-out. **Spine S3 (Open Surface) ✅** 2026-11-04 (opened 2026-10-11): the MCP capture sink (I28, 3 of 3) ✅ 2026-10-15; the extension runtime (I43, 4 of 4) ✅ 2026-10-17; role packs (I47) ✅ 2026-10-26; and Phase 9.5, the marketplace registry, in seven projects — registry protocol and client (I44) ✅ 2026-10-18, publishing ✅ 2026-10-23, the Readwise unbundling pilot (I46, the baked `nimbus-official` registry) ✅, and the trust layer (project 3, I48) ✅ 2026-10-27 in 5 PRs (revoked or taken-down code is refused at install, update and spawn until the owner re-approves; publishers rotate and revoke keys through a registry; the verified-publisher badge; registry maintainer blocks and `nimbus extension report`; `nimbus trust maintain`; registry-verified build provenance; and the Nimbus trust feed `nimbus-agent/nimbus-trust` baked into the release with a verified snapshot). Dependencies and starter packs (project 4) ✅ 2026-10-29 in 5 PRs (a registry install fetches its dependencies from the registry each declaration names, in one approved plan; connector requirements with their status and a post-install walk; `kind: "starter-pack"` packs that never run; updates that add dependencies list them in their approval and install them all or nothing). `nimbus eval` (project 5) ✅ 2026-10-30 in 5 PRs (the `evals/*.eval.json` format; a sandboxed, gateway-free runner with no network and no credentials, invariant I49; `extension.eval` for an installed extension behind one owner approval; registry CI that scores each version and signs the score into the index; and `nimbus eval --installed`, which evaluates every enabled installed extension behind one approval and compares each score with the one its source registry signed for the same version, though no live registry publishes scores yet). The quality layer (project 6) ✅ 2026-11-03 in 12 PRs (permission transparency and evidence lines; registry-signed scores and a baseline; liveness checks published under a separate key (I51); golden suites recorded with `nimbus eval --record`; local network observation and `nimbus extension audit` on all three OSes with warnings; owner-approved narrower network grants (I52)). Author onboarding (project 7) ✅ 2026-11-04 in 4 PRs (optional manifest `funding` and `license` with `nimbus extension sponsor`; `nimbus extension clone`, the gateway's `registry.cloneSource`; `nimbus scaffold extension --from-openapi`; and the docs site's Authors section, ). That completed all seven Phase 9.5 projects and closed S3. **S4 (Autonomous Agent) is the active slot** (opened by the owner 2026-11-04): Phase 9.7 Wave 1 ✅ 2026-11-05 (Project A, the model's connector-write door, A1–A5b) — A1 (one connector-write row table and a generated manifest, #1883) ✅ 2026-11-04; A2 (the typed consent preview of a connector write) ✅ 2026-11-04; A3 (the per-write coverage proof: every connector write prompts, is ledgered before dispatch and is refused over federation) ✅ 2026-11-04; A4 (the owner's opt-in: `nimbus connector writes enable|disable|status`, the V83 table, off by default for every connector) ✅ 2026-11-04; A5a (plumbing for the model door: `origin: "model"` actions are never delegated, the preview names the credential, a guarded raw per-server listing, share replay refuses write ids, `status` counts offerable writes) ✅ 2026-11-05; A5b (the door itself: on the local owner's `cli`/`ui` non-demo turn the model is offered a tool per enabled, offerable write, every call an `origin: "model"` action through an executor built without delegation, so the owner approves the typed preview and sees `Requested by: the model`; I53, static D39) ✅ 2026-11-05. Project B (Waves 2–4) has started: B0a (the Wave-2 framework: each wave-2 service is listed on its own mesh slot with its own generation behind a per-service, no-network credential check; the V84 `approved_tool_ids` column, so the model is offered only the writes the owner approved by name and an earlier opt-in offers nothing until re-enabled; the door refuses a post to a ChatOps channel or DM and a write to the tribal knowledge base; the MCP act door shows the typed preview with the proposer in an unverified block); B0b (the before-state reader, spec § 5.4: a row may declare a `before` reader, and on the door's executor only the prompt then shows the target's current state beside the new values, read through the same gate and ledgered, refused rather than prompted if it would need approval, dispatchable only as a read of the same connector, capped at 5 s and shown as `unavailable ()` on any failure; static D40); B1 (messaging and comments) ✅ 2026-11-05: the model is offered its first 12 writes (Slack post and DM, Teams channel and chat posts, a Discord post as the bot, a GitHub issue or pull-request comment, GitLab issue and merge-request notes, and Jira, Linear, Notion and Confluence comments), each behind the owner's typed preview, seven of them with a before-state that names the target; an opt-in from before B1 offers none of them until the owner re-runs `nimbus connector writes enable`; the model door AND the MCP act door refuse a post to a channel ChatOps listens on, a broadcast mention (Slack ``-style, GitHub team, GitLab `@all`, Teams ``) and a GitLab quick action (`ERR_CAPTURE_ACTION_REFUSED` on the act door); the comment and Discord writes became MCP-proposable (34 proposable types); B2a ✅ 2026-11-05: a before-state reader may take a second read of the same connector (each its own gated, ledgered read, one 5 s budget, all or nothing) and carry a guard that refuses a write inside the executor's gate before any prompt, fail closed; B2 (issues) ✅ 2026-11-05: the model is offered 25 writes (B1's 12 plus 13 issue writes: create on GitHub, GitLab, Jira and Linear; Jira and Linear update; the Jira transition; GitHub/GitLab labels add, assignees add and state set), each with a typed preview, and nine with a before-state (the Jira transition reads twice and shows From, Transition and To); labels and assignees are add-only (pinned against the installed connectors); a GitLab scoped label (`key::value`) is refused on both doors; a Linear `assigneeId` is allowed on the model door only while the read proves the issue unassigned (the B2a guard) and is always refused on the act door (`actDoorRefusal`); the broadcast and quick-action refusals cover issue titles, bodies and descriptions; seven issue writes became MCP-proposable (41 proposable types at B2); B3 (drafts and pages) ✅ 2026-11-05: the model is offered 34 writes (B2's 25 plus 9: a Gmail, iCloud or Outlook draft, which names every recipient on its own line and sends nothing; Notion page create, page update and block append; Confluence page create, update and append), each with a typed preview; five more rows have a before-state, and a page update or append shows a CAPPED excerpt of the current content (at most 1,000 characters per value, at most five Notion blocks); the KB refusal covers every page row on both doors, and an update or append of a page whose parent is the KB destination is refused on the model door by a before-state guard reading a frozen KB-config snapshot (for Confluence, since connectors 0.5.0, a page with the KB parent among its `ancestors`; a read without well-formed `ancestors` still refuses every page in the KB space), while the act door refuses a service's page update and append types whenever a KB destination is configured for that service; the Outlook draft and the Confluence append became MCP-proposable (43 proposable types, 117 denied, 1 `not_yet_offered`: `jira.issue.transition`). B4a (calendar and Drive) ✅ 2026-11-05: the model is offered 37 writes (B3's 34 plus 3: an Outlook or iCloud calendar event create, whose preview names every attendee in full and shows the time zone, and a Google Drive file create, whose before-state reads the target folder: its name, whether it is a folder, whether it is shared); both doors refuse an Outlook or iCloud event whose time does not show its zone (Outlook `YYYY-MM-DDTHH:MM[:SS]` with `timeZone`, iCloud `YYYYMMDDTHHMMSSZ`) and an iCloud event field or attendee with a line break or control character (a hidden-invitee form), and any of the three whose optional field is set to `null` (the preview would show the unset default the connector never applies); proposability is unchanged (43 / 117 / 1); B4p ✅ 2026-11-05: the gateway pins `@nimbus-dev/connectors` 0.5.0, whose `confluence_page_get` returns the page's `ancestors`, so the Confluence KB-child guard refuses only the KB parent itself and a page with it among its ancestors, in any space, and still refuses every page in the KB space when the read has no `ancestors` or any entry has a null or malformed id. B4b (Obsidian) ✅ 2026-11-05: the model is offered 38 writes (B4a's 37 plus the Obsidian daily-note append, whose before-state shows the end of the note it will write), so no Wave-2 row is left untemplated; approving that append in `nimbus connector writes enable obsidian` is also the one thing that lets the Obsidian connector's sandbox WRITE, and only ONE FILE per vault: today's daily note (UTC), which the gateway creates on the host refusing links, never `.obsidian`, `.git`, another note or a folder (owner ruling: a whole-vault grant could plant an Obsidian plugin); the prompt names the note pattern per vault; enable, disable and the date change restart the connector; on Windows every Obsidian spawn first revokes the profile's earlier write ACEs, read from a ledger kept under the config directory (fail closed), and the sandbox helper serialises every DACL rewrite behind one lock; Obsidian indexing no longer follows or reads through a link. B3r (remote content) ✅ 2026-11-06: the model door and the MCP act door (at submit and at accept) refuse a draft, page, note or file body that would load remote content when opened (an external image, embed, frame or stylesheet: an Outlook HTML draft, Confluence storage, Notion blocks and properties, the Obsidian append, and a Drive file whose type is HTML, XHTML, SVG or other markup or whose name ends in a markup extension, or Markdown for `.md`), by a structural scan that never strips or rewrites (anything that does not parse is refused); plain links stay allowed; B3w: the same rule covers a sent Outlook HTML mail (act door; the model is not offered sends), Teams HTML posts, and GitHub, GitLab and Linear Markdown, where every Markdown image is refused; that completes Project B (Wave 2). C0 (2026-11-06; owner rulings OR-W34-1 and OR-W34-3, from the Waves 3-4 design round) moved the ten templated Tableau, Looker, Power BI, Monte Carlo and Bigeye refreshes and acknowledge/resolve rows to wave 3, so the model is offered 48, and the merges, branch delete, tag create and calendar deletes to wave 4; no mail send is ever offered (`MODEL_NEVER_OFFERED_TOOL_IDS`). C1 (2026-11-06) pins `@nimbus-dev/connectors` 0.6.0: its 11 new writes (GitHub and GitLab branch, pull request and merge request creates and updates, the LaunchDarkly toggle and rollout, the Flagsmith feature state, the two Jira Service Management alert writes) are classified as wave 3, ask the owner like any write and are refused over federation (5 are recoverable, 6 are not; at C1 none was offered or proposable, C2 below changed the six VCS ones); and the eight existing wave-3 file, pull request and incident writes (Drive move, rename and trash, the OneDrive move, the GitHub pull request close, PagerDuty acknowledge, resolve and escalate) got a typed preview and a before-state, so the model was offered 56 at C1 (171 HITL types and 113 write rows; 43 proposable, 128 denied, 12 `not_yet_offered`). C2 (2026-11-06) templated the six VCS writes (branch, pull request and merge request create and update; the update previews show the current title, state and base, a base or target change reads `RETARGETS`, and a GitLab title change that drops or adds a draft prefix reads `MARKS READY` or `MARKS AS DRAFT`) and made the four creates MCP-proposable behind a broadcast mention, forge-Markdown remote content and GitLab quick-action refusal on both doors, so the model was offered 62 at C2 (templated 70, 35 with a before-state; 171 types: 47 proposable, 124 denied, 8 `not_yet_offered`); the two updates stay model-door-only, since the connector overwrites the old text and the act door has no before-state to show. C3 (2026-11-06) templated the last five wave-3 writes, so the model is offered 67 (templated 75, 40 with a before-state, 8 rows with a guard): the LaunchDarkly toggle and percentage rollout and the Flagsmith feature-state set (the environment comes first in each preview; a rollout lists every variation's percentage now and after, by name, and is refused unless it names every variation of the flag) and the Jira Service Management Operations alert acknowledge and close (the preview says Atlassian runs the action asynchronously and that Nimbus does not confirm it took effect). All five stay `not_yet_offered` on the MCP act door (it shows no before-state, and variation ids and alert ids are opaque), so 47 proposable, 124 denied and 8 `not_yet_offered` are unchanged. `nimbus connector auth jira --cloud-id ` (env `NIMBUS_JIRA_CLOUD_ID`, Vault key `jira.cloud_id`) stores the Atlassian cloud id the alert tools need: optional on an `*.atlassian.net` site, required for Data Center or a proxied site, and a re-auth that changes the site without a new id deletes it, as does `nimbus connector remove jira` (re-auth asks for the e-mail, token and site again, and a set `NIMBUS_JIRA_CLOUD_ID` counts as a given id). PagerDuty writes need the e-mail of a PagerDuty user as the `From` header (`nimbus connector auth pagerduty --token --username `). Wave 3 (Project C) is complete; Wave 4 (Project D) has started: D0a ✅ 2026-11-06 (type-to-confirm: every Wave 4 write, on every door, is approved only by the owner typing its confirm text, `/`, which the executor compares exactly; the model is offered a Wave 4 row only with a `confirm` target; `--yes` and scripted consent deny a Wave 4 request; ChatOps, which cannot collect text, refuses it with a fixed reply; I53 amended, I20 gains the typed-text rule, static D41). The `@nimbus-dev/client` and VS Code extension `typed?` follow-up is deferred (see `docs/roadmap.md`). D0c ✅ 2026-11-07 (typed confirmation over ChatOps: a Wave 4 write asked in Slack or Teams posts a card with a form that collects the confirm text, a Slack modal opened by the card's Confirm button or a Teams Adaptive Card `Input.Text`; only the resource owner's exact text approves, a channel text `approve` never does, and the modal open is a ledgered `chatops.confirmModal` post; I20, I23, I29 and static D17 extended, no new invariant, no migration; the Slack app needs Interactivity enabled and the `views:write` bot scope). GitHub Enterprise Server (a carried-in item) ✅ 2026-11-07: one optional Vault key, `github.api_base`, connects the GitHub connector and GitHub Actions to ONE self-hosted instance at a time (`nimbus connector auth github --api-base https://ghe.example.com`, or a `gh` login on that host through `nimbus connector detect`): https only, GHE.com (data residency) refused, loopback, unspecified and link-local hosts refused, RFC 1918 hosts allowed, DNS rebinding not prevented by the literal check; the gateway's own GitHub fetches, the fetch-host boundary, `resolve`'s `fetchable`, Git remote parsing and the repo-context query follow it, a stored base that no longer validates fails closed (no spawn, no request), a write's consent preview names the instance of the credential that will run (`Instance:`; a team entry may carry its own optional `github.api_base`, and its PAT goes only to that host; the line is a fresh Vault read at prompt time on both the model door and the MCP act door, and every PAT reader reads base, PAT, base again and makes no request if the base changed mid-read), and no invariant was added or retired (I15, I29 and I53 gained stated bounds in `docs/SECURITY-INVARIANTS.md`). D1 ✅ 2026-11-07 (Project D, the first Wave 4 writes for the model; pins `@nimbus-dev/connectors` 0.8.0): the model is offered 94 writes, the 67 of C3 plus 27 Wave 4 writes, each behind a before-state read and the type-to-confirm text (GitHub and GitLab merge, GitHub branch delete and tag create, the Jenkins, GitHub Actions, CircleCI and GitLab pipeline writes, the AWS Lambda invoke, EC2 start and stop and ECS service update, the Argo CD sync and rollback, the Flux reconciles, the MLflow promote and stage transition, the Snowflake tag and comment set, and the OneDrive, Outlook and iCloud deletes). A Wave 4 row is offered only after `nimbus connector writes enable --wave 4` and the owner's approval of it by name, so an earlier opt-in offers none. A Wave 4 model write FAILS CLOSED: whenever the current state of its target cannot be read (an unlisted reader, a timeout, a failed or policy-refused read, a selection that shows nothing, a failed second read), or the row has no reader or no preview, it is refused before any prompt and the model gets `{ refused }`. Thirteen rows now carry a before-state guard (the GitLab merge `sha`, the GitHub merge `sha` the model must send equal to the pull request head, the Argo CD rollback id, the GitHub tag `sha` prefix and the CircleCI branch-and-tag refusal join the earlier eight). The seven mail sends and `bitbucket_pr_merge` (no head-commit precondition) are never offered. Not offered, with reasons: `azure_app_service_restart`, `azure_aks_node_pool_scale` and `gcp_cloud_run_deploy` (owner ruling 2026-11-07: the `az` and `gcloud` reads are code-execution class, since on Windows a model-supplied value could reach a `.cmd` wrapper's argv and a before-read runs before approval, so they cannot back a before-read); the four Kubernetes and GKE rows wait for D3's sandbox grants; the four `iac_*` rows shipped at D2 and the two commit writes at D1c (both below). The confirm text now constrains a field only by the literal text that follows it (a slashed branch or an image reference is accepted; a value holding its following separator is refused) and `=` is allowed in values. Seven more services are listed for the door (Bitbucket, Jenkins, CircleCI, GitHub Actions, AWS, Azure, GCP), each with a credential check. Counts at D1: 115 write rows (wave 2 x38, wave 3 x36, wave 4 x41), 98 templated, 68 with a before-state, 173 HITL types; proposability is unchanged (47 proposable, 126 denied, 8 `not_yet_offered`). Stated bounds: a merge's preview-to-dispatch window for GitLab when the model sends no `sha`; the Lambda reader lists the first page and matches the exact name; the 5 s before-read cap may expire on a CLI cold start (the write is then refused); live reply shapes are not verified against live accounts. No new invariant or schema change. D1c ✅ 2026-11-08 (Project D, the two multi-file commits; no connectors release): the model is offered 96 writes, D1's 94 plus `github_file_commit` and `gitlab_commit_create`, the first two BOUND rows (I54). The gateway runs `github_commit_prepare` / `gitlab_commit_prepare` itself, shows the branch head it read, injects `expectedHeadSha` (the model is never shown that field, and a caller-supplied one is refused), and renders the unified diff of the whole change itself from the model's own file text and the head's text, in full or the commit is refused (at most 20 files, 32 KiB of new text, 32 KiB of old text and 64 KiB of diff; the diff escapes every Unicode control character except LF, every format, line-separator, paragraph-separator, surrogate and default-ignorable character and the braille blank U+2800, and doubles a backslash, so no text can pass for another); nothing the connector says about the change is shown. The owner approves by typing `/commit @`. On the model door ONLY, a commit touching a CI, workflow, hook, dev container, `CODEOWNERS`, `.gitattributes` or `.gitmodules` path, or whose new text looks like a credential or holds a bidirectional control character, is refused before any read or prompt (the owner's own doors are not restricted). ChatOps cannot approve a commit (no typed channel; refused after one ledgered prepare read), the MCP act door still denies both (its prompt would carry no diff), and federation refuses the two prepare tools like the writes (`isConnectorWriteOrPrepareToolId`). The audit row carries `boundValuesSha256`, `diffSha256` and `filesSha256`; neither it nor the egress ledger holds file text. Counts at D1c: 115 write rows (unchanged), 100 templated, 68 with a before-state, 2 bound, 173 HITL types; proposability unchanged (47 proposable, 126 denied, 8 `not_yet_offered`). Stated bounds: the BEFORE side of a diff is the connector's claim (a minus line can be wrong, a plus line cannot, and the write re-verifies the head; a byte-order mark is invisible); GitLab's head check is not atomic with its commit; a build file the path list omits (a `Makefile`, `package.json` scripts) also runs code; commits to a default or protected branch are not refused (the platform decides); live reply shapes are not verified. No new invariant or schema change. D2 ✅ 2026-11-08 (Project D, the plan-bound infrastructure applies; pins `@nimbus-dev/connectors` 0.9.0; V86; I2, I15, I50 and I54 text extended, static D42(b)/(h) extended and D43 new; no new invariant): the model is offered 98 writes, D1c's 96 plus `iac_terraform_apply` and `iac_cloudformation_deploy`, the first rows that run infrastructure tooling, and only inside directories the owner approved (`nimbus connector writes enable iac --wave 4 --workdir --cloud aws,azure,gcp [--region ] [--net ]`, stored in V85 `connector_write_iac_grant`; `status` lists them and `disable iac` drops them; a directory that is not the exact canonical spelling of an approved one, or that is a link, an alias, a root or a Nimbus folder, is refused). A bound IaC write runs five ordered phases on one executor: `preflightPhase` (the typed confirm must render, the channel must be able to collect it, the directory and a granted cloud must be approved, all before any child exists), `preparePhase` (a prepare child makes the plan or the change set), settle (the gateway moves the plan file into its own directory, hashes it itself and has `terraform show -json` render it in a child with no network and no credential; a CloudFormation change set is cross-checked against the caller's own input), consent (the owner types `iac/apply ` or `iac/deploy ` after the whole plan or the whole template, led by the counts of deletes, replaces, creates and updates and closed by the plan hash; a plan too large to show is refused, never cut) and dispatch (the apply receives the settled plan path and hash, never the caller's). `iac_terraform_destroy` is an owner-typed action only and is never offered to the model (OR-D2-2); `iac_pulumi_up` is refused on every door before any prompt (saved update plans are experimental); the MCP act door denies every iac row (it has no plan to show); IAM capabilities on a CloudFormation deploy are refused to the model. The gateway alone discards a CloudFormation change set that a refused or declined prepare left unused (an audited, ledgered, no-prompt write of its own, static D43; never after a dispatch). Each Terraform or CloudFormation call runs in its own short-lived connector child under one of five per-run profiles (`plan`, `show`, `apply`, `cfn`, and D3a's `kube`, which runs every Kubernetes call; built only in `connectors/lazy-mesh/iac-runner.ts`) with the granted clouds' exact hosts and credential (none for `show`); on Linux an iac profile answers `pidfd_send_signal` with ENOSYS instead of killing the process, because Go's runtime probes it (owner ruling OR-D2-5; set only by the runner, static D43(d)); on Windows every run has its own AppContainer profile id, whose grants are released on exit and revoked from a persisted ledger before the next start. Whole-branch review fixes (2026-11-08): the write dispatcher keeps the bound-write capabilities (`createConnectorWriteDispatcher` had dropped them, so every iac write was refused on all three doors; a census test now requires every `ConnectorDispatcher` implementer to forward them or be listed); the model's write source reads the V85 workdir grants itself (the Terraform apply was never offered in production); the dispatcher's iac race is the runner's budget plus its escalation grace plus a margin, so the runner's "may be partly changed" text always reaches the caller; the enable prompt's host line follows the sandbox's live posture; and the settle step rescans for a `.git` the plan child planted, before anything is shown (nothing in the owner's directory is ever deleted, so a refused or declined run can leave files the plan wrote). Stated bounds: on macOS (ports only), on Windows (all or nothing) and on Linux without the sandbox helper (all or nothing) a `local-exec` provisioner or a malicious provider in an approved directory could send the credential to any host (on port 443 on macOS, on any port where the network is all or nothing) (owner ruling OR-D2-1); the per-cloud host lists come from documented endpoints and are UNVERIFIED until a credentialed probe; providers and modules are never downloaded (the owner runs `terraform init`); macOS is first exercised by the CI sandbox probe; the Windows dev-tree round-trip test skips (a sandboxed Bun connector cannot start from a dev tree there; only the compiled gateway can). CI installs a SHA-pinned Terraform. Counts: 117 write rows (wave 2 x38, wave 3 x36, wave 4 x43), 103 templated, 68 with a before-state, 5 bound, 7 prepare ids, 175 HITL types, 98 model-offerable, 12 never offered; proposability: 47 proposable, 128 denied, 8 `not_yet_offered`. D3b ✅ 2026-11-10 (Project D, the deploy triggers and mobile release; pins `@nimbus-dev/connectors` 0.10.0; no schema change and no new invariant or static rule): the model is offered 105 writes, D2's 98 plus seven Wave 4 writes, each behind a typed preview, a before-state read and the type-to-confirm text: the Vercel redeploy and promote, the Netlify build, the Bitrise and Codemagic builds, the TestFlight distribute and the Firebase App Distribution distribute. Six services are newly listed for the door (Vercel, Netlify, Bitrise, Codemagic, TestFlight, Firebase; each in the phase-3 bundle, checked by its spawner's own credential test), and each write is offered only after `nimbus connector writes enable --wave 4` and the owner's approval of it by name, so an earlier opt-in offers none. Production deploys ARE offered (owner ruling OR-D3-1: a Vercel production redeploy, a Vercel promote and a Netlify production publish), behind the before-state and the typed text. On the model door only, the gate refuses before any prompt: a Netlify build whose branch equals the site's production branch (a before-state guard that reads `build_settings.repo_branch` and also refuses when it cannot be read; the model passes `production: true` to publish production, which types its own `publish production` text) and a Netlify build with neither or both of branch and production; a Firebase distribute to tester emails (OR-D3-2: groups only, since an address that is not a tester yet is created and emailed an invitation that cannot be unsent); and a TestFlight distribute to a beta group whose public link is on, that is not one of the app's, or whose link state cannot be read (OR-D3-3: the preview always shows each target group's name, kind and public-link state). The owner's own doors are not restricted. The two latest-build readers (`bitrise_build_latest`, `codemagic_build_latest`) end in `_latest`, which is not a read verb, so they are named by id in `EXTRA_READ_TOOL_IDS` (bare and namespaced, with a sync test that fails on any other `_latest` tool in the installed package). The MCP act door denies all seven (classes `deployment`, `pipeline` and the new `mobile_release`). Counts at D3b: 124 write rows (wave 2 x38, wave 3 x36, wave 4 x50), 110 templated, 75 with a before-state, 5 bound, 7 prepare ids, 182 HITL types, 105 model-offerable, 12 never offered; proposability: 47 proposable, 135 denied, 8 `not_yet_offered`. Stated bounds: a TestFlight group's public link can be switched on between the guard's read and the dispatch (the connector-side check that closes it, and the row's `allowPublicLink` field, come with connectors 0.11.0); a Vercel redeploy cannot show the deployment it replaces (the request names no project); the previews of `vercel_get`, `netlify_get` and `firebase_get` follow the vendors' documented shapes and are not verified against a live account; an owner-door Netlify build has no before-state, so a branch that is the production branch reads as a branch build there; a branch long enough to push the confirm text past 200 characters is refused as too long to type. D3a ✅ 2026-11-10 (Project D, Kubernetes; pins `@nimbus-dev/connectors` 0.11.0; V86 `connector_write_k8s_grant`; no new invariant or static rule number): the model is offered 108 writes, D3b's 105 plus three: `k8s_manifest_apply`, `k8s_rollout_restart` and `k8s_deployment_scale`, each behind a typed preview and the type-to-confirm text, and each only while the owner approved a (context, namespace) pair and approved the write by name. The apply is the SIXTH bound row (I54): `k8s_manifest_plan` makes a server-side dry run, reads the live objects and diffs them inside the connector (no `diff` program on any OS, OR-D3-9), and the GATEWAY settles the identity itself (`engine/k8s-settle.ts`, pure, reached only through the capability's `settle`, static D42(h)): it computes the plan hash over the context, the namespace and the exact manifest (`nimbus-k8s-manifest-v2`) and the diff hash over the diff text it shows (`nimbus-k8s-diff-v1`), requires its own parse of the manifest (`js-yaml`, core schema; anchors, aliases, tags and flow collections refused before the parser runs; a duplicate key throws) to AGREE with the connector's scan and with `kubectl`'s own object list, and binds BOTH hashes; the connector's apply re-runs the plan and refuses `the cluster changed since the plan was approved` when its diff no longer matches (OR-D3-8; the residual window is the milliseconds between that re-read and the apply). Secrets are refused on every door (OR-D3-5); the model door also refuses RBAC kinds and privileged or host-access pod settings (OR-D3-6). The owner approves pairs with `nimbus connector writes enable kubernetes --wave 4 --grant @` (repeatable; `kube-system`, `kube-public` and `kube-node-lease` are never approvable; each context is pinned to its API server `host:port` and credential form in V86, and the prompt shows both). Every kubernetes call (the nine tools, the reads a before-state needs included) runs in its own short-lived child under a fifth per-run profile, `kube` (`connectors/lazy-mesh/iac-runner.ts`, `wrapIacServerSpec`, static D43(d)): the network is the ONE approved API server; the only credential is a single-context kubeconfig the gateway builds (certificate files inlined; `exec`, `auth-provider`, `proxy-url`, basic auth, impersonation, `tls-server-name`, insecure TLS, non-https and IPv6 servers refused; cluster and context `extensions` are accepted and dropped, OR-D3-13), so exec-plugin clusters (typical EKS, GKE, AKS) are not supported (OR-D3-4); the ONLY writable path is the call's scratch directory (pinned by a test); the pair is re-checked against V86 for every child; one bound apply runs per (context, namespace) at a time. The typed text is derived by the gateway from its own parse: one object `kubernetes/apply //`, several `kubernetes/apply / objects `; the restart types `kubernetes/restart //` and the scale `kubernetes/scale // to `. A restart of any resource other than a deployment has no reader, hence no before-state, and is refused on every door. ChatOps cannot collect the typed text and is refused before any child; the MCP act door denies all three (class `k8s`). The audit row and the egress ledger hold a manifest's size and SHA-256, never its text. Counts at D3a: 124 write rows (wave 2 x38, wave 3 x36, wave 4 x50), 113 templated, 77 with a before-state, 6 bound, 7 prepare ids, 182 HITL types, 108 model-offerable, 11 never offered; proposability: 47 proposable, 135 denied, 8 `not_yet_offered`. Stated bounds: a REAL apply against a real cluster is UNVERIFIED (the CI stub API server cannot exercise client-side validation, which needs the real OpenAPI document); on Windows the AppContainer cannot reach an API server on this machine (loopback isolation covers local-NIC addresses too), so Docker Desktop, kind and minikube clusters cannot take a sandboxed call there, and the enable prompt says so (OR-D3-12; no loopback exemption); on macOS the host limit is by port and on Windows all or nothing, so the residual there is a compromised `kubectl`, not a manifest; a plan contacts the cluster before the prompt (read-only except admission webhooks); the single-object typed text proves which object, not what changes (the manifest and the diff are above it); `k8s_pod_delete` is never offered to the model and `gcp_gke_workload_restart` stays un-offered (its reader is code-execution class, OR-D1-1). **Phase 9.7 ✅ 2026-11-10** (all four waves; owner rulings OR-97-1 and OR-97-2): the Azure App Service restart, the AKS node-pool scale, the Cloud Run deploy and the GKE workload restart (whose only before-state readers are `az` and `gcloud` reads, code-execution class) and the Kubernetes pod delete joined `MODEL_NEVER_OFFERED_REASONS` as a stated exclusion, so of the 124 write rows 108 are model-offerable and the other 16 are never offered, each with a recorded reason (all but two stay on the owner's own doors: the Pulumi apply is refused on every door, and the change-set discard is reserved for the gateway's own cleanup); the standing-approval and taint integration, and the acceptance criterion that a Wave 4 write is never auto-fired by a standing rule, moved to Phase 10. Phase 10 SA1 ✅ 2026-11-11 (standing approvals, the groundwork PR; migration V87; no new invariant, I4 text extended, I55 reserved): every approval now records where its answer came from (`answerSource`: `human`, `auto` for `--yes`, `script`, `unknown` for an older client, `delegate`, `remote` for ChatOps; client-reported, and a reply naming any other source is a denial), the gate decides through an `ApprovalOutcome` (`approved` / `rejected` / `standing_approved`), and `audit_log` and `egress_ledger` accept a fourth `hitl_status`, `standing_approved`, which no code produces yet (a source pin fails if one does before I55 lands). Phase 10 SA2 ✅ 2026-11-11 (deterministic workflow steps; pins `@nimbus-dev/connectors` 0.12.0; no schema change, no new invariant, static D40(b) amended and D39(e) extended): a saved workflow may hold `query` and `write` steps beside its `run` prompts. A `query` is a closed declared form (service, type, bound predicates, a plain select column, a required limit of at most 500) run as one bound SELECT over the local index; a `write` names one wave 2 or 3 connector write and takes each input as a constant or an id an earlier query selected, never a `run` step's output. Both are validated at `workflow.save` and again at every run, and a workflow with no `run` step needs no model. Every write action runs through `buildWorkflowWriteExecutor` (consent bound to the run's client, no delegation, the before-state runner), so the owner is asked on every one and sees the typed preview and the thread's current state; the action carries a gateway-set `provenance` (workflow id, SHA-256 of the steps, step index, run id and a trigger the IPC dispatcher derives from the connection's declared client kind: `cli` gives `owner_cli`, `ui` gives `owner_ui`, anything else none; a `trigger` or `provenance` param and a `paramsOverride` naming a write or query step are refused with -32602) that nothing reads yet (the audit row records it without the field sources). The Gmail thread archive (`gmail_thread_archive`, `gmail.thread.archive`, wave 2, a before-state that reads the thread) is the 125th row: offered to the model after the owner approves it by name, `not_yet_offered` on the MCP act door. The Gmail catalog scopes gain `gmail.modify` and only the Gmail child gets `GOOGLE_OAUTH_SCOPES`, so a connection made before this must run `nimbus connector auth gmail` again, and `nimbus connector writes status` and the enable prompt say so. Counts at SA2: 125 write rows (wave 2 x39, wave 3 x36, wave 4 x50), 114 templated, 78 with a before-state, 6 bound, 183 HITL types, 109 model-offerable, 16 never offered; proposability: 47 proposable, 136 denied, 9 `not_yet_offered`. Stated bounds: the declared query's selection residual and index staleness; the trigger trusts the client's declared kind on the local socket; existing Gmail connections must re-authorise; the CLI's `workflow run` did not declare its kind until SA3, so its runs carried no provenance (every write still prompted). Phase 10 SA3 ✅ 2026-11-11 (standing approval rules; migration V88; new invariant I55, static D44; I2, I4, I20 and I29 text extended): the owner approves a recurring workflow write ONCE. `nimbus approve add --workflow --step