@vellumai/assistant 0.4.35 → 0.4.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (239) hide show
  1. package/AGENTS.md +1 -1
  2. package/ARCHITECTURE.md +44 -49
  3. package/README.md +32 -20
  4. package/docs/architecture/keychain-broker.md +186 -0
  5. package/docs/architecture/security.md +110 -116
  6. package/docs/runbook-trusted-contacts.md +2 -2
  7. package/docs/skills.md +25 -25
  8. package/package.json +4 -1
  9. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +11 -2
  10. package/src/__tests__/actor-token-service.test.ts +1 -0
  11. package/src/__tests__/amazon-cdp-integration.test.ts +74 -0
  12. package/src/__tests__/assistant-feature-flags-integration.test.ts +38 -9
  13. package/src/__tests__/assistant-id-boundary-guard.test.ts +29 -0
  14. package/src/__tests__/browser-fill-credential.test.ts +1 -1
  15. package/src/__tests__/bundle-scanner.test.ts +1 -1
  16. package/src/__tests__/channel-guardian.test.ts +102 -102
  17. package/src/__tests__/channel-invite-transport.test.ts +155 -256
  18. package/src/__tests__/channel-readiness-routes.test.ts +336 -0
  19. package/src/__tests__/checker.test.ts +6 -6
  20. package/src/__tests__/chrome-cdp.test.ts +350 -0
  21. package/src/__tests__/computer-use-session-lifecycle.test.ts +3 -3
  22. package/src/__tests__/computer-use-session-working-dir.test.ts +86 -52
  23. package/src/__tests__/computer-use-skill-lifecycle-cleanup.test.ts +1 -1
  24. package/src/__tests__/config-loader-migration.test.ts +85 -0
  25. package/src/__tests__/conversation-pairing.test.ts +370 -5
  26. package/src/__tests__/credential-broker-browser-fill.test.ts +1 -10
  27. package/src/__tests__/credential-broker-server-use.test.ts +1 -10
  28. package/src/__tests__/credential-security-e2e.test.ts +7 -1
  29. package/src/__tests__/credential-security-invariants.test.ts +14 -20
  30. package/src/__tests__/credential-vault-unit.test.ts +1 -11
  31. package/src/__tests__/credential-vault.test.ts +5 -19
  32. package/src/__tests__/credentials-cli.test.ts +806 -0
  33. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +23 -4
  34. package/src/__tests__/email-invite-adapter.test.ts +78 -0
  35. package/src/__tests__/email-service-config-fallback.test.ts +102 -0
  36. package/src/__tests__/encrypted-store.test.ts +6 -6
  37. package/src/__tests__/ephemeral-permissions.test.ts +3 -3
  38. package/src/__tests__/gateway-only-enforcement.test.ts +5 -1
  39. package/src/__tests__/guardian-actions-endpoint.test.ts +70 -12
  40. package/src/__tests__/guardian-outbound-http.test.ts +53 -47
  41. package/src/__tests__/handle-user-message-secret-resume.test.ts +23 -0
  42. package/src/__tests__/handlers-add-trust-rule-metadata.test.ts +32 -23
  43. package/src/__tests__/handlers-telegram-config.test.ts +8 -2
  44. package/src/__tests__/handlers-twitter-config.test.ts +2 -2
  45. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +108 -7
  46. package/src/__tests__/ingress-reconcile.test.ts +6 -0
  47. package/src/__tests__/intent-routing.test.ts +23 -4
  48. package/src/__tests__/invite-routes-http.test.ts +12 -0
  49. package/src/__tests__/ipc-snapshot.test.ts +8 -2
  50. package/src/__tests__/keychain-broker-client.test.ts +543 -0
  51. package/src/__tests__/llm-usage-store.test.ts +344 -0
  52. package/src/__tests__/mcp-client-auth.test.ts +2 -2
  53. package/src/__tests__/media-reuse-story.e2e.test.ts +1 -1
  54. package/src/__tests__/migration-transport.test.ts +49 -0
  55. package/src/__tests__/notification-broadcaster.test.ts +205 -5
  56. package/src/__tests__/notification-deep-link.test.ts +365 -1
  57. package/src/__tests__/oauth-connect-handler.test.ts +2 -2
  58. package/src/__tests__/onboarding-starter-tasks.test.ts +17 -4
  59. package/src/__tests__/proxy-approval-callback.test.ts +1 -1
  60. package/src/__tests__/recording-handler.test.ts +1 -1
  61. package/src/__tests__/recording-intent-handler.test.ts +6 -1
  62. package/src/__tests__/recording-state-machine.test.ts +1 -1
  63. package/src/__tests__/relay-server.test.ts +9 -1
  64. package/src/__tests__/ride-shotgun-handler.test.ts +499 -0
  65. package/src/__tests__/runtime-attachment-metadata.test.ts +160 -1
  66. package/src/__tests__/script-proxy-injection-runtime.test.ts +299 -2
  67. package/src/__tests__/script-proxy-profile-template-fallback.test.ts +1 -1
  68. package/src/__tests__/secret-onetime-send.test.ts +8 -2
  69. package/src/__tests__/secure-keys.test.ts +175 -216
  70. package/src/__tests__/session-confirmation-signals.test.ts +1 -1
  71. package/src/__tests__/session-messaging-secret-redirect.test.ts +1 -1
  72. package/src/__tests__/session-queue.test.ts +2 -1
  73. package/src/__tests__/session-tool-setup-app-refresh.test.ts +2 -2
  74. package/src/__tests__/skill-feature-flags-integration.test.ts +29 -4
  75. package/src/__tests__/skill-feature-flags.test.ts +12 -9
  76. package/src/__tests__/skill-load-feature-flag.test.ts +26 -5
  77. package/src/__tests__/skill-projection.benchmark.test.ts +0 -1
  78. package/src/__tests__/skills.test.ts +34 -4
  79. package/src/__tests__/slack-channel-config.test.ts +2 -2
  80. package/src/__tests__/system-prompt.test.ts +26 -4
  81. package/src/__tests__/telegram-bot-username-resolution.test.ts +212 -0
  82. package/src/__tests__/telegram-invite-adapter.test.ts +164 -0
  83. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +1 -1
  84. package/src/__tests__/tool-permission-simulate-handler.test.ts +8 -2
  85. package/src/__tests__/trusted-contact-approval-notifier.test.ts +9 -1
  86. package/src/__tests__/twitter-auth-handler.test.ts +2 -2
  87. package/src/__tests__/twitter-oauth-client.test.ts +1 -1
  88. package/src/__tests__/usage-routes.test.ts +339 -0
  89. package/src/__tests__/whatsapp-invite-adapter.test.ts +94 -0
  90. package/src/agent/loop.ts +3 -0
  91. package/src/amazon/checkout.ts +0 -1
  92. package/src/approvals/guardian-request-resolvers.ts +9 -1
  93. package/src/bundler/app-bundler.ts +28 -12
  94. package/src/bundler/bundle-scanner.ts +1 -1
  95. package/src/bundler/bundle-signer.ts +3 -3
  96. package/src/bundler/manifest.ts +1 -1
  97. package/src/bundler/signature-verifier.ts +3 -3
  98. package/src/channels/config.ts +1 -1
  99. package/src/cli/AGENTS.md +63 -0
  100. package/src/cli/__tests__/notifications.test.ts +470 -0
  101. package/src/cli/amazon.ts +344 -167
  102. package/src/cli/audit.ts +85 -0
  103. package/src/cli/autonomy.ts +369 -0
  104. package/src/cli/channels.ts +51 -0
  105. package/src/cli/completions.ts +208 -0
  106. package/src/cli/config.ts +220 -0
  107. package/src/cli/contacts.ts +471 -0
  108. package/src/cli/credentials.ts +564 -0
  109. package/src/cli/default-action.ts +14 -0
  110. package/src/cli/dev.ts +131 -0
  111. package/src/cli/doctor.ts +398 -0
  112. package/src/cli/email.ts +491 -0
  113. package/src/cli/influencer.ts +72 -0
  114. package/src/cli/integrations.ts +248 -57
  115. package/src/cli/keys.ts +114 -0
  116. package/src/cli/map.ts +46 -54
  117. package/src/cli/mcp.ts +111 -3
  118. package/src/cli/{config-commands.ts → memory.ts} +133 -242
  119. package/src/cli/notifications.ts +407 -0
  120. package/src/cli/program.ts +65 -0
  121. package/src/cli/reference.ts +48 -0
  122. package/src/cli/sequence.ts +154 -0
  123. package/src/cli/sessions.ts +262 -0
  124. package/src/cli/trust.ts +177 -0
  125. package/src/cli/twitter.ts +323 -106
  126. package/src/config/__tests__/build-cli-reference-section.test.ts +49 -0
  127. package/src/config/bundled-skills/amazon/SKILL.md +2 -2
  128. package/src/config/bundled-skills/app-builder/TOOLS.json +26 -0
  129. package/src/config/bundled-skills/app-builder/tools/app-generate-icon.ts +13 -0
  130. package/src/config/bundled-skills/contacts/SKILL.md +178 -10
  131. package/src/config/bundled-skills/doordash/doordash-cli.ts +23 -168
  132. package/src/config/bundled-skills/google-oauth-setup/SKILL.md +135 -34
  133. package/src/config/bundled-skills/messaging/tools/shared.ts +4 -1
  134. package/src/config/bundled-skills/twilio-setup/SKILL.md +70 -17
  135. package/src/config/bundled-tool-registry.ts +2 -0
  136. package/src/config/core-schema.ts +7 -0
  137. package/src/config/feature-flag-registry.json +16 -0
  138. package/src/config/loader.ts +26 -0
  139. package/src/config/schema.ts +4 -0
  140. package/src/config/skill-state.ts +0 -13
  141. package/src/config/system-prompt.ts +27 -0
  142. package/src/contacts/contact-store.ts +25 -0
  143. package/src/daemon/computer-use-session.ts +1 -1
  144. package/src/daemon/handlers/apps.ts +1 -0
  145. package/src/daemon/handlers/config-channels.ts +3 -3
  146. package/src/daemon/handlers/config-dispatch.ts +29 -0
  147. package/src/daemon/handlers/config-inbox.ts +4 -3
  148. package/src/daemon/handlers/config.ts +3 -43
  149. package/src/daemon/handlers/contacts.ts +34 -0
  150. package/src/daemon/handlers/index.ts +17 -3
  151. package/src/daemon/handlers/session-user-message.ts +7 -0
  152. package/src/daemon/handlers/sessions.ts +21 -2
  153. package/src/daemon/handlers/shared.ts +17 -0
  154. package/src/daemon/ipc-contract/apps.ts +2 -0
  155. package/src/daemon/ipc-contract/computer-use.ts +9 -0
  156. package/src/daemon/ipc-contract/contacts.ts +3 -3
  157. package/src/daemon/ipc-contract/inbox.ts +2 -0
  158. package/src/daemon/ipc-contract/messages.ts +4 -0
  159. package/src/daemon/ipc-contract/sessions.ts +8 -0
  160. package/src/daemon/ipc-contract-inventory.json +1 -0
  161. package/src/daemon/lifecycle.ts +0 -5
  162. package/src/daemon/ride-shotgun-handler.ts +139 -25
  163. package/src/daemon/session-agent-loop-handlers.ts +100 -0
  164. package/src/daemon/session-agent-loop.ts +72 -0
  165. package/src/daemon/session-tool-setup.ts +7 -0
  166. package/src/daemon/session.ts +23 -1
  167. package/src/daemon/tool-side-effects.ts +39 -1
  168. package/src/email/service.ts +59 -2
  169. package/src/index.ts +2 -60
  170. package/src/mcp/mcp-oauth-provider.ts +90 -8
  171. package/src/media/app-icon-generator.ts +86 -0
  172. package/src/memory/db-init.ts +11 -0
  173. package/src/memory/llm-usage-store.ts +186 -0
  174. package/src/memory/migrations/137-usage-dashboard-indexes.ts +26 -0
  175. package/src/memory/migrations/139-drop-usage-composite-indexes.ts +30 -0
  176. package/src/memory/migrations/index.ts +2 -0
  177. package/src/memory/schema-migration.ts +1 -0
  178. package/src/memory/shared-app-links-store.ts +1 -1
  179. package/src/messaging/registry.ts +27 -0
  180. package/src/notifications/README.md +79 -70
  181. package/src/notifications/broadcaster.ts +2 -1
  182. package/src/notifications/conversation-pairing.ts +147 -13
  183. package/src/notifications/copy-composer.ts +7 -3
  184. package/src/notifications/destination-resolver.ts +14 -1
  185. package/src/notifications/emit-signal.ts +3 -2
  186. package/src/notifications/signal.ts +105 -1
  187. package/src/notifications/types.ts +16 -0
  188. package/src/permissions/checker.ts +29 -3
  189. package/src/permissions/prompter.ts +11 -3
  190. package/src/runtime/access-request-helper.ts +2 -1
  191. package/src/runtime/auth/route-policy.ts +7 -1
  192. package/src/runtime/channel-invite-transport.ts +40 -63
  193. package/src/runtime/channel-invite-transports/email.ts +13 -39
  194. package/src/runtime/channel-invite-transports/slack.ts +5 -34
  195. package/src/runtime/channel-invite-transports/sms.ts +8 -29
  196. package/src/runtime/channel-invite-transports/telegram.ts +69 -28
  197. package/src/runtime/channel-invite-transports/voice.ts +0 -7
  198. package/src/runtime/channel-invite-transports/whatsapp.ts +43 -0
  199. package/src/runtime/channel-readiness-service.ts +202 -45
  200. package/src/runtime/confirmation-request-guardian-bridge.ts +2 -1
  201. package/src/runtime/guardian-outbound-actions.ts +8 -5
  202. package/src/runtime/http-server.ts +2 -0
  203. package/src/runtime/invite-instruction-generator.ts +178 -0
  204. package/src/runtime/invite-service.ts +22 -25
  205. package/src/runtime/migrations/migration-transport.ts +13 -0
  206. package/src/runtime/routes/app-routes.ts +1 -1
  207. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +8 -7
  208. package/src/runtime/routes/channel-readiness-routes.ts +30 -11
  209. package/src/runtime/routes/contact-routes.ts +54 -26
  210. package/src/runtime/routes/inbound-stages/bootstrap-intercept.ts +1 -1
  211. package/src/runtime/routes/inbound-stages/escalation-intercept.ts +2 -1
  212. package/src/runtime/routes/inbound-stages/verification-intercept.ts +2 -1
  213. package/src/runtime/routes/integration-routes.ts +1 -1
  214. package/src/runtime/routes/invite-routes.ts +1 -1
  215. package/src/runtime/routes/secret-routes.ts +31 -7
  216. package/src/runtime/routes/twilio-routes.ts +32 -1
  217. package/src/runtime/routes/usage-routes.ts +114 -0
  218. package/src/runtime/tool-grant-request-helper.ts +2 -1
  219. package/src/security/encrypted-store.ts +9 -5
  220. package/src/security/keychain-broker-client.ts +393 -0
  221. package/src/security/secure-keys.ts +106 -321
  222. package/src/tools/apps/executors.ts +73 -0
  223. package/src/tools/browser/auto-navigate.ts +15 -6
  224. package/src/tools/browser/chrome-cdp.ts +211 -0
  225. package/src/tools/browser/network-recorder.test.ts +83 -0
  226. package/src/tools/browser/network-recorder.ts +8 -7
  227. package/src/tools/browser/x-auto-navigate.ts +12 -6
  228. package/src/tools/credentials/policy-types.ts +24 -0
  229. package/src/tools/credentials/vault.ts +22 -27
  230. package/src/tools/network/script-proxy/session-manager.ts +47 -3
  231. package/src/tools/permission-checker.ts +1 -0
  232. package/src/tools/types.ts +2 -0
  233. package/src/tools/ui-surface/definitions.ts +1 -2
  234. package/src/tools/watch/watch-state.ts +2 -0
  235. package/src/__tests__/key-migration.test.ts +0 -240
  236. package/src/__tests__/keychain.test.ts +0 -286
  237. package/src/cli/core-commands.ts +0 -899
  238. package/src/security/keychain-to-encrypted-migration.ts +0 -66
  239. package/src/security/keychain.ts +0 -490
package/AGENTS.md CHANGED
@@ -2,4 +2,4 @@
2
2
 
3
3
  For error handling conventions (throw vs result objects vs null), see [docs/error-handling.md](docs/error-handling.md).
4
4
 
5
- Subdirectory-scoped rules live in local AGENTS.md files: `src/runtime/`, `src/approvals/`, `src/notifications/`.
5
+ Subdirectory-scoped rules live in local AGENTS.md files: `src/cli/`, `src/runtime/`, `src/approvals/`, `src/notifications/`.
package/ARCHITECTURE.md CHANGED
@@ -340,7 +340,7 @@ The WhatsApp channel enables inbound and outbound messaging via the Meta WhatsAp
340
340
 
341
341
  1. **Webhook verification**: Meta sends a `GET` with `hub.mode=subscribe`, `hub.verify_token`, and `hub.challenge`. The gateway compares `hub.verify_token` against `WHATSAPP_WEBHOOK_VERIFY_TOKEN` and echoes `hub.challenge` as plain text.
342
342
  2. On `POST`, the gateway verifies the `X-Hub-Signature-256` header (HMAC-SHA256 of the raw request body using `WHATSAPP_APP_SECRET`) when the app secret is configured. Fail-closed: requests are rejected when the secret is set but the signature fails.
343
- 3. **Normalization**: Only `type=text` messages from `messages` change fields are forwarded. Delivery receipts, read receipts, and non-text message types (image, audio, video, document, sticker) are silently acknowledged with `{ ok: true }`.
343
+ 3. **Normalization**: Text and media messages (image, audio, video, document, sticker) from `messages` change fields are forwarded. Delivery receipts, read receipts, and unsupported message types (contacts, location) are silently acknowledged with `{ ok: true }`. Media attachments are downloaded from the WhatsApp Cloud API, uploaded to the runtime attachment store, and their IDs are passed alongside the message content.
344
344
  4. **`/new` command**: When the message body is `/new` (case-insensitive), the gateway resolves routing, resets the conversation, and sends a confirmation message without forwarding to the runtime.
345
345
  5. The payload is normalized into a `GatewayInboundEvent` with `sourceChannel: "whatsapp"` and `conversationExternalId` set to the sender's WhatsApp phone number (E.164).
346
346
  6. WhatsApp message IDs are deduplicated via `StringDedupCache` (24-hour TTL).
@@ -363,7 +363,7 @@ The WhatsApp channel enables inbound and outbound messaging via the Meta WhatsAp
363
363
 
364
364
  These can be set via environment variables or stored in the credential vault (keychain / encrypted store) under the `whatsapp` service prefix.
365
365
 
366
- **Limitations (v1)**: Text-only non-text message types are acknowledged but not forwarded; rich approval UI (inline buttons) is not supported.
366
+ **Limitations (v1)**: Rich approval UI (inline buttons) is not supported. Contacts and location message types are acknowledged but not forwarded.
367
367
 
368
368
  **Channel Readiness**: The channel readiness HTTP endpoints (`GET /v1/channels/readiness`, `POST /v1/channels/readiness/refresh`) backed by `ChannelReadinessService` in `src/runtime/channel-readiness-service.ts` provide a unified readiness subsystem for all channels. Each channel registers a `ChannelProbe` that runs synchronous local checks (credential presence, phone number, ingress config) and optional async remote checks with a 5-minute TTL cache. Built-in probes: SMS (Twilio credentials, phone number, ingress; remote checks query Twilio toll-free verification status for toll-free numbers) and Telegram (bot token, webhook secret, ingress). The GET endpoint returns cached snapshots; the refresh endpoint invalidates the cache first. Unknown channels return `unsupported_channel`. Route handlers live in `src/runtime/routes/channel-readiness-routes.ts`.
369
369
 
@@ -441,13 +441,13 @@ External users who are not the guardian can gain access to the assistant through
441
441
 
442
442
  **HTTP API (for management):**
443
443
 
444
- | Endpoint | Method | Description |
445
- | --------------------------- | ------ | ---------------------------------------------------------------- |
446
- | `/v1/contacts` | GET | List contacts (filterable by role, search by query/channel/etc.) |
447
- | `/v1/contacts` | POST | Create or update a contact |
448
- | `/v1/contacts/:id` | GET | Get a contact by ID |
449
- | `/v1/contacts/merge` | POST | Merge two contacts |
450
- | `/v1/contacts/channels/:id` | PATCH | Update a contact channel's status/policy |
444
+ | Endpoint | Method | Description |
445
+ | ---------------------------------------- | ------ | ---------------------------------------------------------------- |
446
+ | `/v1/contacts` | GET | List contacts (filterable by role, search by query/channel/etc.) |
447
+ | `/v1/contacts` | POST | Create or update a contact |
448
+ | `/v1/contacts/:id` | GET | Get a contact by ID |
449
+ | `/v1/contacts/merge` | POST | Merge two contacts |
450
+ | `/v1/contact-channels/:contactChannelId` | PATCH | Update a contact channel's status/policy |
451
451
 
452
452
  **Key source files:**
453
453
 
@@ -497,7 +497,7 @@ A complementary access-granting flow where the guardian proactively creates a sh
497
497
  3. The skill calls the ingress HTTP API to create an invite token, then calls the Telegram transport adapter to build a deep link: `https://t.me/<bot>?start=iv_<token>`.
498
498
  4. Guardian shares the link with the invitee out-of-band.
499
499
  5. Invitee clicks the link, opening Telegram which sends `/start iv_<token>` to the bot.
500
- 6. The gateway forwards the message to `/channels/inbound`. The inbound handler calls `getTransport('telegram').extractInboundToken()` to parse the `iv_` token.
500
+ 6. The gateway forwards the message to `/channels/inbound`. The inbound handler calls `getInviteAdapterRegistry().get('telegram').extractInboundToken()` to parse the `iv_` token.
501
501
  7. The token is redeemed via `invite-redemption-service.ts`, which validates, activates the contact, and returns a `redeemed` outcome.
502
502
  8. A deterministic welcome message is delivered to the invitee (bypasses the LLM pipeline).
503
503
 
@@ -672,8 +672,7 @@ The assistant feature-flag resolver (`src/config/assistant-feature-flags.ts`) is
672
672
  **Public API:**
673
673
 
674
674
  - `isAssistantFeatureFlagEnabled(key, config)` — full resolver with the canonical key
675
- - `isAssistantSkillEnabled(skillId, config)` — convenience wrapper that constructs `feature_flags.<skillId>.enabled` and delegates
676
- - `isSkillFeatureEnabled(skillId, config)` — deprecated legacy wrapper in `config/skill-state.ts`
675
+ - `skillFlagKey(skillId)` — derives the canonical flag key for a skill, respecting overrides (in `config/skill-state.ts`)
677
676
 
678
677
  **Skill-gating guarantee:** For skills that are explicitly mapped to declared assistant flags, when the flag is OFF the skill is unavailable everywhere — it cannot appear in client UIs, model context, or runtime tool execution. This is enforced at five independent points:
679
678
 
@@ -685,24 +684,24 @@ The assistant feature-flag resolver (`src/config/assistant-feature-flags.ts`) is
685
684
  | **4. Runtime tool projection** | `projectSkillTools()` in `daemon/session-skill-tools.ts` | Even if a skill was previously active in a session (has `<loaded_skill>` markers in history), the per-turn projection drops it when the flag is OFF. Already-registered tools are unregistered. |
686
685
  | **5. Included child skills** | `executeSkillLoad()` in `tools/skills/load.ts` | When a parent skill includes children via the `includes` directive, each child is independently checked against its feature flag. Flagged-off children are silently excluded from the loaded skill content. |
687
686
 
688
- All five enforcement points use `isAssistantSkillEnabled()` from `config/assistant-feature-flags.ts` for consistency.
687
+ All five enforcement points use `isAssistantFeatureFlagEnabled(skillFlagKey(skillId), config)` for consistency.
689
688
 
690
689
  **Migration path:** The legacy `skills.<id>.enabled` key format is no longer supported. All code must use the canonical `feature_flags.<id>.enabled` format. Guard tests enforce canonical key usage and declaration coverage for literal key references in the unified registry.
691
690
 
692
691
  **Key source files:**
693
692
 
694
- | File | Purpose |
695
- | ----------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
696
- | `src/config/assistant-feature-flags.ts` | Canonical resolver: `isAssistantFeatureFlagEnabled()`, `isAssistantSkillEnabled()`, `getAssistantFeatureFlagDefaults()`, registry loader |
697
- | `src/config/skill-state.ts` | `isSkillFeatureEnabled()` (deprecated wrapper) delegates to canonical resolver; `resolveSkillStates()` — enforcement point 1 |
698
- | `src/config/system-prompt.ts` | `appendSkillsCatalog()` — enforcement point 2 |
699
- | `src/tools/skills/load.ts` | `executeSkillLoad()` — enforcement points 3 and 5 |
700
- | `src/daemon/session-skill-tools.ts` | `projectSkillTools()` — enforcement point 4 |
701
- | `src/config/schema.ts` | `featureFlags` and `assistantFeatureFlagValues` field definitions in `AssistantConfig` (Zod schema) |
702
- | `src/config/types.ts` | Type definitions for `FeatureFlags` (legacy) and `AssistantFeatureFlagValues` (canonical) |
703
- | `src/daemon/handlers/skills.ts` | `handleSkillsList()` — uses `resolveSkillStates()` for IPC client responses |
704
- | `meta/feature-flags/feature-flag-registry.json` | Unified feature flag registry (repo root) — all declared flags with scope, label, default values, and descriptions |
705
- | `src/config/feature-flag-registry.json` | Bundled copy of the unified registry for compiled binary resolution |
693
+ | File | Purpose |
694
+ | ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
695
+ | `src/config/assistant-feature-flags.ts` | Canonical resolver: `isAssistantFeatureFlagEnabled()`, `getAssistantFeatureFlagDefaults()`, registry loader |
696
+ | `src/config/skill-state.ts` | `skillFlagKey()` derives canonical flag key for skills; `resolveSkillStates()` — enforcement point 1 |
697
+ | `src/config/system-prompt.ts` | `appendSkillsCatalog()` — enforcement point 2 |
698
+ | `src/tools/skills/load.ts` | `executeSkillLoad()` — enforcement points 3 and 5 |
699
+ | `src/daemon/session-skill-tools.ts` | `projectSkillTools()` — enforcement point 4 |
700
+ | `src/config/schema.ts` | `featureFlags` and `assistantFeatureFlagValues` field definitions in `AssistantConfig` (Zod schema) |
701
+ | `src/config/types.ts` | Type definitions for `FeatureFlags` (legacy) and `AssistantFeatureFlagValues` (canonical) |
702
+ | `src/daemon/handlers/skills.ts` | `handleSkillsList()` — uses `resolveSkillStates()` for IPC client responses |
703
+ | `meta/feature-flags/feature-flag-registry.json` | Unified feature flag registry (repo root) — all declared flags with scope, label, default values, and descriptions |
704
+ | `src/config/feature-flag-registry.json` | Bundled copy of the unified registry for compiled binary resolution |
706
705
 
707
706
  ---
708
707
 
@@ -1549,7 +1548,7 @@ graph TB
1549
1548
 
1550
1549
  ## Permission and Trust Security Model
1551
1550
 
1552
- The permission system controls which tool actions the agent can execute without explicit user approval. It supports three operating modes (`workspace`, `strict`, and `legacy`), execution-target-scoped trust rules, and risk-based escalation to provide defense-in-depth against unintended or malicious tool execution.
1551
+ The permission system controls which tool actions the agent can execute without explicit user approval. It supports two operating modes (`workspace` and `strict`), execution-target-scoped trust rules, and risk-based escalation to provide defense-in-depth against unintended or malicious tool execution.
1553
1552
 
1554
1553
  ### Permission Evaluation Flow
1555
1554
 
@@ -1577,35 +1576,31 @@ graph TB
1577
1576
  RISK_FALLBACK_WS -->|"Low"| AUTO_WS_LOW["decision: allow<br/>Low risk auto-allow"]
1578
1577
  RISK_FALLBACK_WS -->|"Medium"| PROMPT_WS_MED["decision: prompt"]
1579
1578
  RISK_FALLBACK_WS -->|"High"| PROMPT_WS_HIGH["decision: prompt"]
1580
- NO_MATCH -->|"legacy mode"| RISK_FALLBACK{"Risk level?"}
1581
- RISK_FALLBACK -->|"Low"| AUTO_LOW["decision: allow<br/>Low risk auto-allow"]
1582
- RISK_FALLBACK -->|"Medium"| PROMPT_MED["decision: prompt"]
1583
- RISK_FALLBACK -->|"High"| PROMPT_HIGH2["decision: prompt"]
1584
1579
  ```
1585
1580
 
1586
- ### Permission Modes: Workspace, Strict, and Legacy
1581
+ ### Permission Modes: Workspace and Strict
1587
1582
 
1588
- The `permissions.mode` config option (`workspace`, `strict`, or `legacy`) controls the default behavior when no trust rule matches a tool invocation. The default is `workspace`.
1583
+ The `permissions.mode` config option (`workspace` or `strict`) controls the default behavior when no trust rule matches a tool invocation. The default is `workspace`.
1589
1584
 
1590
- | Behavior | Workspace mode (default) | Strict mode | Legacy mode (deprecated) |
1591
- | -------------------------------------------------- | --------------------------------------------- | --------------------------------------------- | --------------------------------------------- |
1592
- | Workspace-scoped ops with no matching rule | Auto-allowed | Prompted | Auto-allowed (low risk) |
1593
- | Non-workspace low-risk tools with no matching rule | Auto-allowed | Prompted | Auto-allowed |
1594
- | Medium-risk tools with no matching rule | Prompted | Prompted | Prompted |
1595
- | High-risk tools with no matching rule | Prompted | Prompted | Prompted |
1596
- | `skill_load` with no matching rule | Prompted | Prompted | Auto-allowed (low risk) |
1597
- | `skill_load` with system default rule | Auto-allowed (`skill_load:*` at priority 100) | Auto-allowed (`skill_load:*` at priority 100) | Auto-allowed (`skill_load:*` at priority 100) |
1598
- | `browser_*` skill tools with system default rules | Auto-allowed (priority 100 allow rules) | Auto-allowed (priority 100 allow rules) | Auto-allowed (priority 100 allow rules) |
1599
- | Skill-origin tools with no matching rule | Prompted | Prompted | Prompted |
1600
- | Allow rules for non-high-risk tools | Auto-allowed | Auto-allowed | Auto-allowed |
1601
- | Allow rules with `allowHighRisk: true` | Auto-allowed (even high risk) | Auto-allowed (even high risk) | Auto-allowed (even high risk) |
1602
- | Deny rules | Blocked | Blocked | Blocked |
1585
+ | Behavior | Workspace mode (default) | Strict mode |
1586
+ | -------------------------------------------------- | --------------------------------------------- | --------------------------------------------- |
1587
+ | Workspace-scoped ops with no matching rule | Auto-allowed | Prompted |
1588
+ | Non-workspace low-risk tools with no matching rule | Auto-allowed | Prompted |
1589
+ | Medium-risk tools with no matching rule | Prompted | Prompted |
1590
+ | High-risk tools with no matching rule | Prompted | Prompted |
1591
+ | `skill_load` with no matching rule | Prompted | Prompted |
1592
+ | `skill_load` with system default rule | Auto-allowed (`skill_load:*` at priority 100) | Auto-allowed (`skill_load:*` at priority 100) |
1593
+ | `browser_*` skill tools with system default rules | Auto-allowed (priority 100 allow rules) | Auto-allowed (priority 100 allow rules) |
1594
+ | Skill-origin tools with no matching rule | Prompted | Prompted |
1595
+ | Allow rules for non-high-risk tools | Auto-allowed | Auto-allowed |
1596
+ | Allow rules with `allowHighRisk: true` | Auto-allowed (even high risk) | Auto-allowed (even high risk) |
1597
+ | Deny rules | Blocked | Blocked |
1603
1598
 
1604
1599
  **Workspace mode** (default) auto-allows operations scoped to the workspace (file reads/writes/edits within the workspace directory, sandboxed bash) without prompting. Host operations, network requests, and operations outside the workspace still follow the normal approval flow. Explicit deny and ask rules override auto-allow.
1605
1600
 
1606
1601
  **Strict mode** is designed for security-conscious deployments where every tool action must have an explicit matching rule in the trust store. It eliminates implicit auto-allow for any risk level, ensuring the user has consciously approved each class of tool usage.
1607
1602
 
1608
- **Legacy mode** (deprecated) auto-allows all low-risk tools regardless of scope. It is deprecated and will be removed in a future release. A one-time runtime warning is emitted when legacy mode is active. Users should migrate to `workspace` (default) or `strict`.
1603
+ > **Migration note:** Existing config files with `permissions.mode = "legacy"` are automatically migrated to `workspace` during config loading. The `legacy` value is not a supported steady-state mode.
1609
1604
 
1610
1605
  ### Trust Rules (v3 Schema)
1611
1606
 
@@ -1649,7 +1644,7 @@ The `skill_load` tool generates version-aware command candidates for rule matchi
1649
1644
  2. `skill_load:<skill-id>` — matches any-version rules
1650
1645
  3. `skill_load:<raw-selector>` — matches the raw user-provided selector
1651
1646
 
1652
- In strict mode, `skill_load` without a matching rule is always prompted. In legacy mode, it is auto-allowed as a Low-risk tool. The allowlist options presented to the user include both version-specific and any-version patterns. Note: the system default allow rule `skill_load:*` (priority 100) now globally allows all skill loads in both modes (see "System Default Allow Rules" below).
1647
+ In strict mode, `skill_load` without a matching rule is always prompted. The allowlist options presented to the user include both version-specific and any-version patterns. Note: the system default allow rule `skill_load:*` (priority 100) now globally allows all skill loads in both modes (see "System Default Allow Rules" below).
1653
1648
 
1654
1649
  ### Starter Approval Bundle
1655
1650
 
@@ -1684,7 +1679,7 @@ In addition to the opt-in starter bundle, the permission system seeds unconditio
1684
1679
  | `default:allow-browser_extract-global` | `browser_extract` | `browser_extract:*` | (same) |
1685
1680
  | `default:allow-browser_fill_credential-global` | `browser_fill_credential` | `browser_fill_credential:*` | (same) |
1686
1681
 
1687
- These rules are emitted by `getDefaultRuleTemplates()` in `assistant/src/permissions/defaults.ts`. Because they use priority 100 (equal to user rules), they take effect in both strict and legacy modes. The `skill_load` rule means skill activation never prompts; the `browser_*` rules mean the browser skill's tools behave identically to the old core `headless-browser` tool from a permission standpoint.
1682
+ These rules are emitted by `getDefaultRuleTemplates()` in `assistant/src/permissions/defaults.ts`. Because they use priority 100 (equal to user rules), they take effect in both workspace and strict modes. The `skill_load` rule means skill activation never prompts; the `browser_*` rules mean the browser skill's tools behave identically to the old core `headless-browser` tool from a permission standpoint.
1688
1683
 
1689
1684
  ### Shell Command Identity and Allowlist Options
1690
1685
 
@@ -1732,7 +1727,7 @@ File tool candidates include canonical (symlink-resolved) absolute paths via `no
1732
1727
  | `assistant/src/permissions/defaults.ts` | Default rule templates (system ask rules for host tools, CU, etc.) |
1733
1728
  | `assistant/src/skills/version-hash.ts` | `computeSkillVersionHash()` — deterministic SHA-256 of skill source files |
1734
1729
  | `assistant/src/skills/path-classifier.ts` | `isSkillSourcePath()`, `normalizeFilePath()`, skill root detection |
1735
- | `assistant/src/config/schema.ts` | `PermissionsConfigSchema` — `permissions.mode` (`workspace` / `strict` / `legacy`) |
1730
+ | `assistant/src/config/schema.ts` | `PermissionsConfigSchema` — `permissions.mode` (`workspace` / `strict`) |
1736
1731
  | `assistant/src/tools/executor.ts` | `ToolExecutor` — orchestrates risk classification, permission check, and execution |
1737
1732
  | `assistant/src/daemon/handlers/config.ts` | `handleToolPermissionSimulate()` — dry-run simulation handler |
1738
1733
 
package/README.md CHANGED
@@ -365,29 +365,33 @@ These endpoints share the same business logic as the IPC-based verification flow
365
365
 
366
366
  ## Channel Readiness
367
367
 
368
- Channel readiness is exposed via HTTP control-plane endpoints that provide a unified way to check whether a channel (SMS, Telegram, etc.) is fully configured and operational. Local checks (credential presence, phone number assignment, ingress config) run synchronously; optional remote checks (API reachability) run asynchronously with a 5-minute TTL cache.
368
+ Channel readiness is exposed via HTTP control-plane endpoints that provide a unified way to check whether a channel (SMS, Telegram, etc.) is fully configured and operational. Local checks (credential presence, phone number assignment, ingress config) run synchronously; remote checks (API reachability) run by default and are cached with a 5-minute TTL. Remote checks can be disabled by passing `includeRemote=false`.
369
369
 
370
370
  ### Channel Readiness HTTP Endpoints
371
371
 
372
- | Method | Path | Description |
373
- | ------ | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
374
- | GET | `/v1/channels/readiness` | Returns readiness snapshots for the specified channel (query param `channel`, optional) or all channels. Local checks always run; remote checks run only when `includeRemote=true` and cache is stale. |
375
- | POST | `/v1/channels/readiness/refresh` | Invalidates the cache for the specified channel (or all channels), then returns fresh snapshots. Body: `{ channel?: ChannelId, includeRemote?: boolean }` |
372
+ | Method | Path | Description |
373
+ | ------ | -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
374
+ | GET | `/v1/channels/readiness` | Returns readiness snapshots for the specified channel (query param `channel`, optional) or all channels. Local checks always run; remote checks run by default (`includeRemote=true`) and use a cached result when fresh. Pass `includeRemote=false` to skip remote checks. |
375
+ | POST | `/v1/channels/readiness/refresh` | Invalidates the cache for the specified channel (or all channels), then returns fresh snapshots. Body: `{ channel?: ChannelId, includeRemote?: boolean }`. `includeRemote` defaults to `true`. |
376
376
 
377
377
  All endpoints are bearer-authenticated. Skills and clients should call the gateway URL (default `http://localhost:7830`) rather than the runtime port directly, as the gateway proxies all `/v1/channels/readiness*` routes.
378
378
 
379
379
  ### Built-in Channel Probes
380
380
 
381
381
  - **SMS**: Checks Twilio credentials, phone number assignment, and public ingress URL.
382
+ - **Voice**: Checks Twilio credentials, phone number assignment, and public ingress URL.
382
383
  - **Telegram**: Checks bot token, webhook secret, and public ingress URL.
384
+ - **Email**: Checks AgentMail API key, invite policy, public ingress URL, and verifies an inbox address is available (remote check).
385
+ - **WhatsApp**: Checks Meta WhatsApp Business API credentials (phoneNumberId, accessToken, appSecret, webhookVerifyToken), display phone number (`whatsapp.phoneNumber`), invite policy, and public ingress URL.
386
+ - **Slack**: Checks bot token and app token.
383
387
 
384
388
  ### Key modules
385
389
 
386
- | File | Purpose |
387
- | ------------------------------------------------ | ---------------------------------------------------------------------------------------------------- |
388
- | `src/runtime/channel-readiness-types.ts` | Shared types: `ChannelId`, `ReadinessCheckResult`, `ChannelReadinessSnapshot`, `ChannelProbe` |
389
- | `src/runtime/channel-readiness-service.ts` | Service class with probe registration, cached readiness evaluation, and built-in SMS/Telegram probes |
390
- | `src/runtime/routes/channel-readiness-routes.ts` | HTTP route handlers for `/v1/channels/readiness` and `/v1/channels/readiness/refresh` |
390
+ | File | Purpose |
391
+ | ------------------------------------------------ | ----------------------------------------------------------------------------------------------- |
392
+ | `src/runtime/channel-readiness-types.ts` | Shared types: `ChannelId`, `ReadinessCheckResult`, `ChannelReadinessSnapshot`, `ChannelProbe` |
393
+ | `src/runtime/channel-readiness-service.ts` | Service class with probe registration, cached readiness evaluation, and built-in channel probes |
394
+ | `src/runtime/routes/channel-readiness-routes.ts` | HTTP route handlers for `/v1/channels/readiness` and `/v1/channels/readiness/refresh` |
391
395
 
392
396
  ## Ingress Membership + Escalation
393
397
 
@@ -415,24 +419,16 @@ The `iv_` prefix distinguishes invite tokens from `gv_` (guardian verification)
415
419
  The invite redemption system uses a three-layer architecture:
416
420
 
417
421
  - **Core redemption engine** (`invite-redemption-service.ts`) — Channel-agnostic business logic that validates tokens, enforces expiry/use-count/channel-match constraints, handles member reactivation, and returns a discriminated-union `InviteRedemptionOutcome`. Deterministic reply templates (`invite-redemption-templates.ts`) map each outcome to a user-facing message without passing through the LLM.
418
- - **Channel transport adapters** (`channel-invite-transport.ts` + `channel-invite-transports/`) — A registry of per-channel adapters that know how to build shareable deep links (`buildShareableInvite`) and extract inbound tokens (`extractInboundToken`). Currently only the Telegram adapter is implemented.
422
+ - **Channel transport adapters** (`channel-invite-transport.ts` + `channel-invite-transports/`) — A registry of per-channel adapters that know how to build shareable links (`buildShareLink`) and extract inbound tokens (`extractInboundToken`). Adapters are implemented for Telegram, SMS, Voice, Email, WhatsApp, and Slack.
419
423
  - **Conversational orchestration** (`guardian-invite-intent.ts`) — Pattern-based intent detection that intercepts guardian invite management requests (create, list, revoke) in the session pipeline and forces immediate entry into the `contacts` skill, bypassing the normal agent loop.
420
424
 
421
- #### Deferred Channel Support
422
-
423
- The transport adapter registry is architecturally extensible to additional channels. The following are not yet implemented:
424
-
425
- - **SMS** — Requires a deep-link strategy compatible with SMS (e.g., a short URL that redirects to an SMS reply flow or web-based redemption page). The core redemption engine is channel-agnostic and ready.
426
- - **Slack** — Requires DM-safe ingress (Socket Mode currently handles channel messages but DM-initiated invite flows need additional routing). The adapter would build Slack deep links or slash-command payloads.
427
- - **Voice** — Requires DTMF or speech-based token capture during an inbound call. The adapter would need to integrate with the voice relay state machine for token entry.
428
-
429
425
  Redemption auto-creates a **member** record with an access policy:
430
426
 
431
427
  - **`allow`** — Messages are processed normally through the agent pipeline.
432
428
  - **`deny`** — Messages are rejected with a refusal notice.
433
429
  - **`escalate`** — Messages are held for guardian (owner) approval before processing.
434
430
 
435
- Non-members (senders with no invite redemption) are denied by default. Contacts can be listed, updated, revoked, or blocked via the HTTP API (`/v1/contacts` and `/v1/contacts/channels`).
431
+ Non-members (senders with no invite redemption) are denied by default. Contacts can be listed, updated, revoked, or blocked via the HTTP API (`/v1/contacts` and `/v1/contact-channels`).
436
432
 
437
433
  ### Escalation Flow
438
434
 
@@ -491,6 +487,22 @@ docker run --rm -p 3001:3001 \
491
487
 
492
488
  The image exposes port `3001` and bundles the `vellum` CLI binary.
493
489
 
490
+ ## Ride Shotgun
491
+
492
+ Ride Shotgun is a background screen-watching feature that observes user workflows. It has two modes:
493
+
494
+ - **Observe mode** — captures periodic screenshots and generates a workflow summary via the LLM.
495
+ - **Learn mode** — records browser network traffic alongside screenshots to capture API patterns. The assistant owns CDP browser lifecycle: `ride-shotgun-handler.ts` calls `ensureChromeWithCdp()` to launch or connect to Chrome with remote debugging, so clients do not need to pre-launch Chrome with `--remote-debugging-port`.
496
+
497
+ Key modules:
498
+
499
+ | File | Purpose |
500
+ | --------------------------------------- | ------------------------------------------------------- |
501
+ | `src/daemon/ride-shotgun-handler.ts` | Session orchestration, CDP bootstrap, network recording |
502
+ | `src/tools/browser/chrome-cdp.ts` | Reusable Chrome CDP launcher (`ensureChromeWithCdp`) |
503
+ | `src/tools/browser/network-recorder.ts` | CDP-based network traffic capture |
504
+ | `src/tools/browser/recording-store.ts` | Session recording persistence |
505
+
494
506
  ## Troubleshooting
495
507
 
496
508
  ### Guardian and gateway-origin issues
@@ -0,0 +1,186 @@
1
+ # macOS Keychain Broker Architecture
2
+
3
+ **Status:** Accepted
4
+ **Last Updated:** 2026-03-05
5
+ **Owners:** macOS client + assistant runtime
6
+
7
+ ## Decision
8
+
9
+ Embed the keychain broker in the macOS app process rather than running a standalone daemon. The app exposes SecItem keychain operations over a Unix domain socket to the assistant runtime and gateway. Debug builds skip the broker entirely (`#if !DEBUG` guard) so developers never see keychain authorization prompts during rebuilds.
10
+
11
+ ## Problem Statement
12
+
13
+ We want macOS to use Keychain as the primary secret store, but direct keychain access from the daemon process causes repeated authorization prompts. The prompts are especially problematic during development with ad-hoc signed builds, where every rebuild changes the signing identity and triggers a new keychain prompt.
14
+
15
+ Prior state:
16
+
17
+ - macOS defaulted to encrypted file storage to avoid prompt fatigue: `assistant/src/security/secure-keys.ts`.
18
+ - Historical comments documented prompt issues with ad-hoc signing: `clients/shared/App/SigningIdentityManager.swift`.
19
+ - The `keychain.ts` module (now deleted) called `/usr/bin/security` CLI, which was unreliable and prompt-heavy.
20
+
21
+ ## Architecture
22
+
23
+ ```mermaid
24
+ graph LR
25
+ subgraph "macOS App Process"
26
+ SERVER["KeychainBrokerServer<br/>(NWListener on UDS)"]
27
+ SERVICE["KeychainBrokerService<br/>(SecItem* wrapper)"]
28
+ SERVER --> SERVICE
29
+ SERVICE --> KC["macOS Keychain"]
30
+ end
31
+
32
+ RUNTIME["Assistant Runtime (Bun)"] -->|"UDS JSON"| SERVER
33
+ GATEWAY["Gateway (Bun)"] -->|"UDS JSON<br/>(async)"| SERVER
34
+
35
+ RUNTIME -.->|"fallback<br/>(sync + broker unavailable)"| ENC["Encrypted Store<br/>(~/.vellum/protected/keys.enc)"]
36
+ GATEWAY -.->|"fallback<br/>(broker unavailable)"| ENC
37
+ ```
38
+
39
+ ### Key properties
40
+
41
+ - **No separate process lifecycle.** The broker starts when the app starts and stops when the app stops. No launchd plist, no health checks, no orphan cleanup.
42
+ - **No auth bootstrap problem.** The app writes the broker auth token to disk before launching the daemon, so the daemon always has a valid token at startup.
43
+ - **No keychain prompts on signed builds.** Items are stored with `kSecAttrAccessibleAfterFirstUnlock` under the `vellum-assistant` service name. A stable code-signing identity means macOS grants access without prompting after the first unlock.
44
+ - **No keychain interaction on debug builds.** The entire `KeychainBrokerServer` is compiled out with `#if !DEBUG`, so development builds use the encrypted file store exclusively.
45
+ - **Encrypted file store as permanent fallback.** CLI-only, headless, and development environments always have the encrypted store (`~/.vellum/protected/keys.enc`) available. Async callers that use the broker also write through to the encrypted store so sync callers see a consistent view.
46
+
47
+ ## Components
48
+
49
+ ### Swift side (macOS app)
50
+
51
+ | File | Role |
52
+ | --------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
53
+ | `clients/macos/vellum-assistant/Security/KeychainBrokerServer.swift` | UDS server using `NWListener`. Accepts newline-delimited JSON requests, validates auth token, dispatches to `KeychainBrokerService`. Compiled only for `!DEBUG` builds. |
54
+ | `clients/macos/vellum-assistant/Security/KeychainBrokerService.swift` | Thin `SecItem*` wrapper scoped to the `vellum-assistant` service. Provides `get`, `set`, `delete`, `list`. Uses `kSecAttrAccessibleAfterFirstUnlock`. Compiled only for `macOS`. |
55
+
56
+ ### TypeScript side (runtime + gateway)
57
+
58
+ | File | Role |
59
+ | -------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
60
+ | `assistant/src/security/keychain-broker-client.ts` | Async UDS client for the runtime. Persistent socket connection, request/response correlation, auth token caching with auto-refresh on `UNAUTHORIZED`. Falls back gracefully (returns safe defaults, never throws). |
61
+ | `assistant/src/security/secure-keys.ts` | Unified API surface. Sync variants use encrypted store only. Async variants (`getSecureKeyAsync`, `setSecureKeyAsync`, `deleteSecureKeyAsync`) try broker first. **Reads** fall back to the encrypted store when the broker is unavailable or key is not found. **Writes** return `false` on broker failure (no encrypted-store fallback). **Deletes** return `"deleted"`, `"not-found"`, or `"error"` to let callers distinguish idempotent no-ops from real failures. |
62
+ | `gateway/src/credential-reader.ts` | Read-only credential reader. Tries broker via native async UDS connection (`node:net`), falls back to encrypted store. All public credential read functions are async. |
63
+
64
+ ## IPC Contract
65
+
66
+ ### Transport
67
+
68
+ - Unix domain socket: `~/.vellum/keychain-broker.sock`
69
+ - Socket path is passed to daemon/gateway via `VELLUM_KEYCHAIN_BROKER_SOCKET` environment variable
70
+ - Newline-delimited JSON (`\n` as message boundary)
71
+
72
+ ### Request envelope
73
+
74
+ ```json
75
+ {
76
+ "v": 1,
77
+ "id": "uuid",
78
+ "token": "hex-auth-token",
79
+ "method": "key.get",
80
+ "params": { "account": "anthropic" }
81
+ }
82
+ ```
83
+
84
+ The `v` field is a protocol version number (currently `1`). The server rejects requests with unsupported versions.
85
+
86
+ ### Response envelope
87
+
88
+ Success:
89
+
90
+ ```json
91
+ {
92
+ "id": "uuid",
93
+ "ok": true,
94
+ "result": { "found": true, "value": "sk-..." }
95
+ }
96
+ ```
97
+
98
+ Error:
99
+
100
+ ```json
101
+ {
102
+ "id": "uuid",
103
+ "ok": false,
104
+ "error": { "code": "UNAUTHORIZED", "message": "Invalid auth token" }
105
+ }
106
+ ```
107
+
108
+ ### Methods
109
+
110
+ | Method | Params | Result |
111
+ | ------------- | -------------------- | ------------------------ |
112
+ | `broker.ping` | none | `{ pong: true }` |
113
+ | `key.get` | `{ account }` | `{ found, value? }` |
114
+ | `key.set` | `{ account, value }` | `{ stored: true }` |
115
+ | `key.delete` | `{ account }` | `{ deleted: true }` |
116
+ | `key.list` | none | `{ accounts: string[] }` |
117
+
118
+ ### Error codes
119
+
120
+ - `UNAUTHORIZED` — invalid or missing auth token
121
+ - `INVALID_REQUEST` — malformed request, missing params, or unsupported protocol version
122
+ - `KEYCHAIN_ERROR` — SecItem operation failed
123
+
124
+ ## Security Model
125
+
126
+ ### Authentication
127
+
128
+ 1. On app launch, the broker generates 32 random bytes via `SecRandomCopyBytes` and hex-encodes them as the auth token.
129
+ 2. The token is written to `~/.vellum/protected/keychain-broker.token` with `0600` permissions. The parent directory is `0700`.
130
+ 3. Every request must include the token. Requests with missing or invalid tokens are rejected before method dispatch.
131
+ 4. If the app restarts (new token), the TS client detects `UNAUTHORIZED`, re-reads the token from disk, and retries once.
132
+
133
+ ### Process boundary
134
+
135
+ - The UDS itself restricts access to local processes.
136
+ - The token file's `0600` permissions restrict readers to the same user.
137
+ - Together: only same-user local processes that can read the token file can authenticate.
138
+
139
+ ### Keychain access control
140
+
141
+ - All items use `kSecAttrAccessibleAfterFirstUnlock` — secrets survive screen lock without re-prompting.
142
+ - All items are scoped to the `vellum-assistant` service name via `kSecAttrService`.
143
+ - On signed release builds, the stable code-signing identity means macOS trusts the app to access its own keychain items without prompting.
144
+
145
+ ### Threat model
146
+
147
+ | Threat | Mitigation |
148
+ | ------------------------------------------- | ------------------------------------------------------------------------------------------ |
149
+ | Other-user process reads secrets | UDS + token file `0600` restrict to same user |
150
+ | Malicious local process impersonates broker | Client reads token from a known file path; attacker would need same-user file write access |
151
+ | Stale socket from unclean exit | Server calls `unlink()` on the socket path before binding |
152
+ | App restart invalidates cached token | Client detects `UNAUTHORIZED`, re-reads token, retries once |
153
+
154
+ ### Future: XPC transport
155
+
156
+ XPC provides stronger caller identity guarantees via audit tokens and code requirement checks. This would replace the file-based auth token with kernel-enforced caller verification, preventing same-user impersonation. No timeline — the UDS + token model is sufficient for the current threat profile.
157
+
158
+ ## Developer Experience
159
+
160
+ - **Debug builds:** The `#if !DEBUG` guard compiles out the entire `KeychainBrokerServer`. The `VELLUM_KEYCHAIN_BROKER_SOCKET` env var is not set, so clients see the broker as unavailable and use the encrypted store. Developers never encounter keychain prompts during the edit-build-run cycle.
161
+ - **Release builds:** The broker starts automatically with the app. The daemon discovers it via the socket env var and token file. No configuration needed.
162
+ - **CLI-only / headless:** No macOS app means no broker socket. All storage uses the encrypted file store. This is the expected path for CI, servers, and non-macOS platforms.
163
+
164
+ ## Callsite Policy
165
+
166
+ ### Runtime request handlers (secret-routes, etc.)
167
+
168
+ All runtime HTTP handlers that write or delete secrets **must** use the async APIs (`setSecureKeyAsync`, `deleteSecureKeyAsync`). These are the primary entry points for macOS app flows and must go through the broker to reach keychain.
169
+
170
+ ### CLI commands (keys, credentials)
171
+
172
+ CLI commands may use sync APIs (`setSecureKey`, `deleteSecureKey`, `getSecureKey`) since they run outside the macOS app process and the broker may not be available. The sync path uses the encrypted store directly, which is correct for headless/CLI environments.
173
+
174
+ ### Gateway (credential-reader)
175
+
176
+ The gateway reads credentials via async `readCredential()` which tries the broker first (native async UDS), falling back to the encrypted store. The gateway never writes credentials — that responsibility belongs to the assistant runtime.
177
+
178
+ ### Startup / initialization code
179
+
180
+ Sync APIs are acceptable for startup paths (e.g. provider initialization, config loading) where async is impractical or the broker may not yet be available.
181
+
182
+ ## Migration
183
+
184
+ Existing encrypted store keys remain accessible — the encrypted store is always consulted as a **read** fallback when the broker does not have a key. Successful writes from async code paths go to both the broker (keychain) and the encrypted store, keeping both in sync. If a broker write or delete fails, the operation returns `false` without falling back to the encrypted store alone, preventing stale divergence. Callers must inspect the boolean return value and handle failures (typically by logging a warning). There is no one-time migration step required.
185
+
186
+ The old `keychain.ts` module (which called `/usr/bin/security` CLI directly) has been deleted. The old keychain-to-encrypted migration code has been removed. All keychain access now flows exclusively through the broker.