@vellumai/assistant 0.4.35 → 0.4.37
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.
- package/AGENTS.md +1 -1
- package/ARCHITECTURE.md +44 -49
- package/README.md +32 -20
- package/docs/architecture/keychain-broker.md +186 -0
- package/docs/architecture/security.md +110 -116
- package/docs/runbook-trusted-contacts.md +2 -2
- package/docs/skills.md +25 -25
- package/package.json +5 -2
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +11 -2
- package/src/__tests__/actor-token-service.test.ts +1 -0
- package/src/__tests__/amazon-cdp-integration.test.ts +74 -0
- package/src/__tests__/assistant-feature-flags-integration.test.ts +38 -9
- package/src/__tests__/assistant-id-boundary-guard.test.ts +29 -0
- package/src/__tests__/browser-fill-credential.test.ts +1 -1
- package/src/__tests__/bundle-scanner.test.ts +1 -1
- package/src/__tests__/channel-guardian.test.ts +102 -102
- package/src/__tests__/channel-invite-transport.test.ts +155 -256
- package/src/__tests__/channel-readiness-routes.test.ts +336 -0
- package/src/__tests__/checker.test.ts +6 -6
- package/src/__tests__/chrome-cdp.test.ts +350 -0
- package/src/__tests__/computer-use-session-lifecycle.test.ts +3 -3
- package/src/__tests__/computer-use-session-working-dir.test.ts +86 -52
- package/src/__tests__/computer-use-skill-lifecycle-cleanup.test.ts +1 -1
- package/src/__tests__/config-loader-migration.test.ts +85 -0
- package/src/__tests__/conversation-pairing.test.ts +370 -5
- package/src/__tests__/credential-broker-browser-fill.test.ts +1 -10
- package/src/__tests__/credential-broker-server-use.test.ts +1 -10
- package/src/__tests__/credential-security-e2e.test.ts +7 -1
- package/src/__tests__/credential-security-invariants.test.ts +14 -20
- package/src/__tests__/credential-vault-unit.test.ts +1 -11
- package/src/__tests__/credential-vault.test.ts +5 -19
- package/src/__tests__/credentials-cli.test.ts +814 -0
- package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +23 -4
- package/src/__tests__/email-invite-adapter.test.ts +78 -0
- package/src/__tests__/email-service-config-fallback.test.ts +102 -0
- package/src/__tests__/encrypted-store.test.ts +6 -6
- package/src/__tests__/ephemeral-permissions.test.ts +3 -3
- package/src/__tests__/gateway-only-enforcement.test.ts +5 -1
- package/src/__tests__/guardian-actions-endpoint.test.ts +70 -12
- package/src/__tests__/guardian-outbound-http.test.ts +53 -47
- package/src/__tests__/handle-user-message-secret-resume.test.ts +23 -0
- package/src/__tests__/handlers-add-trust-rule-metadata.test.ts +32 -23
- package/src/__tests__/handlers-telegram-config.test.ts +8 -2
- package/src/__tests__/handlers-twitter-config.test.ts +2 -2
- package/src/__tests__/handlers-user-message-approval-consumption.test.ts +108 -7
- package/src/__tests__/ingress-reconcile.test.ts +6 -0
- package/src/__tests__/intent-routing.test.ts +23 -4
- package/src/__tests__/invite-routes-http.test.ts +12 -0
- package/src/__tests__/ipc-snapshot.test.ts +8 -2
- package/src/__tests__/keychain-broker-client.test.ts +543 -0
- package/src/__tests__/llm-usage-store.test.ts +344 -0
- package/src/__tests__/mcp-client-auth.test.ts +2 -2
- package/src/__tests__/media-reuse-story.e2e.test.ts +1 -1
- package/src/__tests__/migration-transport.test.ts +49 -0
- package/src/__tests__/notification-broadcaster.test.ts +205 -5
- package/src/__tests__/notification-deep-link.test.ts +365 -1
- package/src/__tests__/oauth-connect-handler.test.ts +2 -2
- package/src/__tests__/onboarding-starter-tasks.test.ts +17 -4
- package/src/__tests__/proxy-approval-callback.test.ts +1 -1
- package/src/__tests__/recording-handler.test.ts +1 -1
- package/src/__tests__/recording-intent-handler.test.ts +6 -1
- package/src/__tests__/recording-state-machine.test.ts +1 -1
- package/src/__tests__/relay-server.test.ts +9 -1
- package/src/__tests__/ride-shotgun-handler.test.ts +499 -0
- package/src/__tests__/runtime-attachment-metadata.test.ts +160 -1
- package/src/__tests__/script-proxy-injection-runtime.test.ts +299 -2
- package/src/__tests__/script-proxy-profile-template-fallback.test.ts +1 -1
- package/src/__tests__/secret-onetime-send.test.ts +8 -2
- package/src/__tests__/secure-keys.test.ts +175 -216
- package/src/__tests__/session-confirmation-signals.test.ts +1 -1
- package/src/__tests__/session-messaging-secret-redirect.test.ts +1 -1
- package/src/__tests__/session-queue.test.ts +2 -1
- package/src/__tests__/session-tool-setup-app-refresh.test.ts +2 -2
- package/src/__tests__/skill-feature-flags-integration.test.ts +29 -4
- package/src/__tests__/skill-feature-flags.test.ts +12 -9
- package/src/__tests__/skill-load-feature-flag.test.ts +26 -5
- package/src/__tests__/skill-projection.benchmark.test.ts +0 -1
- package/src/__tests__/skills.test.ts +34 -4
- package/src/__tests__/slack-channel-config.test.ts +2 -2
- package/src/__tests__/system-prompt.test.ts +26 -4
- package/src/__tests__/telegram-bot-username-resolution.test.ts +212 -0
- package/src/__tests__/telegram-invite-adapter.test.ts +164 -0
- package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +1 -1
- package/src/__tests__/tool-permission-simulate-handler.test.ts +8 -2
- package/src/__tests__/trusted-contact-approval-notifier.test.ts +9 -1
- package/src/__tests__/twitter-auth-handler.test.ts +2 -2
- package/src/__tests__/twitter-oauth-client.test.ts +1 -1
- package/src/__tests__/usage-routes.test.ts +339 -0
- package/src/__tests__/whatsapp-invite-adapter.test.ts +94 -0
- package/src/agent/loop.ts +3 -0
- package/src/amazon/checkout.ts +0 -1
- package/src/approvals/guardian-request-resolvers.ts +9 -1
- package/src/bundler/app-bundler.ts +28 -12
- package/src/bundler/bundle-scanner.ts +1 -1
- package/src/bundler/bundle-signer.ts +3 -3
- package/src/bundler/manifest.ts +1 -1
- package/src/bundler/signature-verifier.ts +3 -3
- package/src/channels/config.ts +1 -1
- package/src/cli/AGENTS.md +63 -0
- package/src/cli/__tests__/notifications.test.ts +470 -0
- package/src/cli/amazon.ts +344 -167
- package/src/cli/audit.ts +85 -0
- package/src/cli/autonomy.ts +369 -0
- package/src/cli/channels.ts +51 -0
- package/src/cli/completions.ts +208 -0
- package/src/cli/config.ts +220 -0
- package/src/cli/contacts.ts +471 -0
- package/src/cli/credentials.ts +564 -0
- package/src/cli/default-action.ts +14 -0
- package/src/cli/dev.ts +131 -0
- package/src/cli/doctor.ts +398 -0
- package/src/cli/email.ts +494 -0
- package/src/cli/influencer.ts +72 -0
- package/src/cli/integrations.ts +248 -57
- package/src/cli/keys.ts +114 -0
- package/src/cli/map.ts +46 -54
- package/src/cli/mcp.ts +111 -3
- package/src/cli/{config-commands.ts → memory.ts} +134 -245
- package/src/cli/notifications.ts +407 -0
- package/src/cli/program.ts +65 -0
- package/src/cli/reference.ts +48 -0
- package/src/cli/sequence.ts +154 -0
- package/src/cli/sessions.ts +262 -0
- package/src/cli/trust.ts +175 -0
- package/src/cli/twitter.ts +323 -106
- package/src/config/__tests__/build-cli-reference-section.test.ts +49 -0
- package/src/config/bundled-skills/amazon/SKILL.md +2 -2
- package/src/config/bundled-skills/app-builder/TOOLS.json +26 -0
- package/src/config/bundled-skills/app-builder/tools/app-generate-icon.ts +13 -0
- package/src/config/bundled-skills/contacts/SKILL.md +178 -10
- package/src/config/bundled-skills/doordash/doordash-cli.ts +23 -168
- package/src/config/bundled-skills/google-oauth-setup/SKILL.md +135 -34
- package/src/config/bundled-skills/messaging/tools/shared.ts +4 -1
- package/src/config/bundled-skills/twilio-setup/SKILL.md +70 -17
- package/src/config/bundled-tool-registry.ts +2 -0
- package/src/config/core-schema.ts +7 -0
- package/src/config/feature-flag-registry.json +16 -0
- package/src/config/loader.ts +26 -0
- package/src/config/schema.ts +4 -0
- package/src/config/skill-state.ts +0 -13
- package/src/config/system-prompt.ts +27 -0
- package/src/contacts/contact-store.ts +25 -0
- package/src/daemon/computer-use-session.ts +1 -1
- package/src/daemon/handlers/apps.ts +1 -0
- package/src/daemon/handlers/config-channels.ts +3 -3
- package/src/daemon/handlers/config-dispatch.ts +29 -0
- package/src/daemon/handlers/config-inbox.ts +4 -3
- package/src/daemon/handlers/config.ts +3 -43
- package/src/daemon/handlers/contacts.ts +34 -0
- package/src/daemon/handlers/index.ts +17 -3
- package/src/daemon/handlers/session-user-message.ts +7 -0
- package/src/daemon/handlers/sessions.ts +21 -2
- package/src/daemon/handlers/shared.ts +17 -0
- package/src/daemon/ipc-contract/apps.ts +2 -0
- package/src/daemon/ipc-contract/computer-use.ts +9 -0
- package/src/daemon/ipc-contract/contacts.ts +3 -3
- package/src/daemon/ipc-contract/inbox.ts +2 -0
- package/src/daemon/ipc-contract/messages.ts +4 -0
- package/src/daemon/ipc-contract/sessions.ts +8 -0
- package/src/daemon/ipc-contract-inventory.json +1 -0
- package/src/daemon/lifecycle.ts +0 -5
- package/src/daemon/ride-shotgun-handler.ts +139 -25
- package/src/daemon/session-agent-loop-handlers.ts +100 -0
- package/src/daemon/session-agent-loop.ts +72 -0
- package/src/daemon/session-tool-setup.ts +7 -0
- package/src/daemon/session.ts +23 -1
- package/src/daemon/tool-side-effects.ts +39 -1
- package/src/email/service.ts +59 -2
- package/src/index.ts +2 -60
- package/src/mcp/mcp-oauth-provider.ts +90 -8
- package/src/media/app-icon-generator.ts +86 -0
- package/src/memory/db-init.ts +11 -0
- package/src/memory/llm-usage-store.ts +186 -0
- package/src/memory/migrations/137-usage-dashboard-indexes.ts +26 -0
- package/src/memory/migrations/139-drop-usage-composite-indexes.ts +30 -0
- package/src/memory/migrations/index.ts +2 -0
- package/src/memory/schema-migration.ts +1 -0
- package/src/memory/shared-app-links-store.ts +1 -1
- package/src/messaging/registry.ts +27 -0
- package/src/notifications/README.md +79 -70
- package/src/notifications/broadcaster.ts +2 -1
- package/src/notifications/conversation-pairing.ts +147 -13
- package/src/notifications/copy-composer.ts +7 -3
- package/src/notifications/destination-resolver.ts +14 -1
- package/src/notifications/emit-signal.ts +3 -2
- package/src/notifications/signal.ts +105 -1
- package/src/notifications/types.ts +16 -0
- package/src/permissions/checker.ts +29 -3
- package/src/permissions/prompter.ts +11 -3
- package/src/runtime/access-request-helper.ts +2 -1
- package/src/runtime/auth/route-policy.ts +7 -1
- package/src/runtime/channel-invite-transport.ts +40 -63
- package/src/runtime/channel-invite-transports/email.ts +13 -39
- package/src/runtime/channel-invite-transports/slack.ts +5 -34
- package/src/runtime/channel-invite-transports/sms.ts +8 -29
- package/src/runtime/channel-invite-transports/telegram.ts +69 -28
- package/src/runtime/channel-invite-transports/voice.ts +0 -7
- package/src/runtime/channel-invite-transports/whatsapp.ts +43 -0
- package/src/runtime/channel-readiness-service.ts +202 -45
- package/src/runtime/confirmation-request-guardian-bridge.ts +2 -1
- package/src/runtime/guardian-outbound-actions.ts +8 -5
- package/src/runtime/http-server.ts +2 -0
- package/src/runtime/invite-instruction-generator.ts +178 -0
- package/src/runtime/invite-service.ts +22 -25
- package/src/runtime/migrations/migration-transport.ts +13 -0
- package/src/runtime/routes/app-routes.ts +1 -1
- package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +8 -7
- package/src/runtime/routes/channel-readiness-routes.ts +30 -11
- package/src/runtime/routes/contact-routes.ts +54 -26
- package/src/runtime/routes/inbound-stages/bootstrap-intercept.ts +1 -1
- package/src/runtime/routes/inbound-stages/escalation-intercept.ts +2 -1
- package/src/runtime/routes/inbound-stages/verification-intercept.ts +2 -1
- package/src/runtime/routes/integration-routes.ts +1 -1
- package/src/runtime/routes/invite-routes.ts +1 -1
- package/src/runtime/routes/secret-routes.ts +31 -7
- package/src/runtime/routes/twilio-routes.ts +32 -1
- package/src/runtime/routes/usage-routes.ts +114 -0
- package/src/runtime/tool-grant-request-helper.ts +2 -1
- package/src/security/encrypted-store.ts +9 -5
- package/src/security/keychain-broker-client.ts +393 -0
- package/src/security/secure-keys.ts +106 -321
- package/src/tools/apps/executors.ts +73 -0
- package/src/tools/browser/auto-navigate.ts +15 -6
- package/src/tools/browser/chrome-cdp.ts +211 -0
- package/src/tools/browser/network-recorder.test.ts +83 -0
- package/src/tools/browser/network-recorder.ts +8 -7
- package/src/tools/browser/x-auto-navigate.ts +12 -6
- package/src/tools/credentials/policy-types.ts +24 -0
- package/src/tools/credentials/vault.ts +22 -27
- package/src/tools/network/script-proxy/session-manager.ts +47 -3
- package/src/tools/permission-checker.ts +1 -0
- package/src/tools/types.ts +2 -0
- package/src/tools/ui-surface/definitions.ts +1 -2
- package/src/tools/watch/watch-state.ts +2 -0
- package/src/__tests__/key-migration.test.ts +0 -240
- package/src/__tests__/keychain.test.ts +0 -286
- package/src/cli/core-commands.ts +0 -899
- package/src/security/keychain-to-encrypted-migration.ts +0 -66
- 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**:
|
|
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)**:
|
|
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
|
|
445
|
-
|
|
|
446
|
-
| `/v1/contacts`
|
|
447
|
-
| `/v1/contacts`
|
|
448
|
-
| `/v1/contacts/:id`
|
|
449
|
-
| `/v1/contacts/merge`
|
|
450
|
-
| `/v1/
|
|
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 `
|
|
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
|
-
- `
|
|
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 `
|
|
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()`, `
|
|
697
|
-
| `src/config/skill-state.ts` | `
|
|
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
|
|
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
|
|
1581
|
+
### Permission Modes: Workspace and Strict
|
|
1587
1582
|
|
|
1588
|
-
The `permissions.mode` config option (`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 |
|
|
1591
|
-
| -------------------------------------------------- | --------------------------------------------- | --------------------------------------------- |
|
|
1592
|
-
| Workspace-scoped ops with no matching rule | Auto-allowed | Prompted |
|
|
1593
|
-
| Non-workspace low-risk tools with no matching rule | Auto-allowed | Prompted |
|
|
1594
|
-
| Medium-risk tools with no matching rule | Prompted | Prompted |
|
|
1595
|
-
| High-risk tools with no matching rule | Prompted | Prompted |
|
|
1596
|
-
| `skill_load` with no matching rule | Prompted | Prompted |
|
|
1597
|
-
| `skill_load` with system default rule | 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) |
|
|
1599
|
-
| Skill-origin tools with no matching rule | Prompted | Prompted |
|
|
1600
|
-
| Allow rules for non-high-risk tools | Auto-allowed | Auto-allowed |
|
|
1601
|
-
| Allow rules with `allowHighRisk: true` | Auto-allowed (even high risk) | Auto-allowed (even high risk) |
|
|
1602
|
-
| Deny rules | 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
|
-
|
|
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.
|
|
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
|
|
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`
|
|
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;
|
|
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
|
|
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
|
|
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
|
|
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/
|
|
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.
|