@vellumai/assistant 0.12.0 → 0.12.1-staging.1
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 +5 -1
- package/ARCHITECTURE.md +11 -12
- package/docs/architecture/turn-actor.md +15 -8
- package/openapi.yaml +292 -2
- package/package.json +1 -1
- package/scripts/smoke-desktop-window-theme.ts +186 -0
- package/src/__tests__/always-loaded-tools-guard.test.ts +18 -10
- package/src/__tests__/app-compiler.test.ts +6 -0
- package/src/__tests__/app-control-tool-schemas.test.ts +5 -2
- package/src/__tests__/app-dir-path-guard.test.ts +1 -3
- package/src/__tests__/computer-use-skill-proxy-bridge.test.ts +5 -2
- package/src/__tests__/computer-use-tools.test.ts +17 -5
- package/src/__tests__/config-schema-cmd.test.ts +2 -2
- package/src/__tests__/config-schema.test.ts +28 -29
- package/src/__tests__/config-watcher.test.ts +23 -8
- package/src/__tests__/connection-model-compat.test.ts +2 -2
- package/src/__tests__/conversation-key-store-disk-view.test.ts +1 -1
- package/src/__tests__/conversation-pairing.test.ts +26 -0
- package/src/__tests__/conversation-process-app-control-preactivation.test.ts +4 -10
- package/src/__tests__/conversation-queue.test.ts +48 -207
- package/src/__tests__/conversation-retry-discard.test.ts +54 -1
- package/src/__tests__/conversation-skill-tools.test.ts +27 -0
- package/src/__tests__/conversation-surfaces-app-control.test.ts +8 -3
- package/src/__tests__/conversation-surfaces-task-progress.test.ts +67 -0
- package/src/__tests__/conversation-title-service.test.ts +14 -10
- package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +3 -5
- package/src/__tests__/conversation-tool-setup-attribution.test.ts +51 -0
- package/src/__tests__/credential-record-retire.test.ts +171 -1
- package/src/__tests__/credential-routes.test.ts +212 -3
- package/src/__tests__/credential-security-invariants.test.ts +2 -1
- package/src/__tests__/cu-unified-flow.test.ts +8 -2
- package/src/__tests__/drain-kick-guard.test.ts +0 -2
- package/src/__tests__/drain-requeue-on-contention.test.ts +0 -6
- package/src/__tests__/evict-conversations-for-reload.test.ts +1 -1
- package/src/__tests__/git-porcelain.test.ts +47 -0
- package/src/__tests__/helpers/set-workspace-mcp.ts +11 -0
- package/src/__tests__/host-bash-proxy.test.ts +26 -45
- package/src/__tests__/host-browser-proxy.test.ts +6 -17
- package/src/__tests__/host-file-proxy-targeted.test.ts +23 -0
- package/src/__tests__/host-file-proxy.test.ts +11 -23
- package/src/__tests__/host-transfer-proxy-targeted.test.ts +30 -9
- package/src/__tests__/llm-catalog-parity.test.ts +25 -0
- package/src/__tests__/mcp-auth-routes.test.ts +6 -9
- package/src/__tests__/mcp-config-secret-boundary.test.ts +78 -136
- package/src/__tests__/mcp-health-check.test.ts +8 -11
- package/src/__tests__/mcp-list-plugin-servers.test.ts +8 -10
- package/src/__tests__/message-deleted-hook-dispatch.test.ts +119 -0
- package/src/__tests__/messages-after-tiebreaker.test.ts +91 -0
- package/src/__tests__/messages-read-boundary-guard.test.ts +1 -1
- package/src/__tests__/messaging-send-tool.test.ts +355 -548
- package/src/__tests__/model-intents.test.ts +3 -0
- package/src/__tests__/outlook-messaging-provider.test.ts +6 -6
- package/src/__tests__/platform-client-verify-credential.test.ts +2 -0
- package/src/__tests__/plugin-import-boundary-guard.test.ts +13 -0
- package/src/__tests__/resolve-app.test.ts +135 -0
- package/src/__tests__/script-proxy-certs.test.ts +35 -0
- package/src/__tests__/skill-feature-flags-integration.test.ts +1 -1
- package/src/__tests__/skill-feature-flags.test.ts +2 -2
- package/src/__tests__/source-fingerprint.test.ts +184 -0
- package/src/__tests__/subagent-tool-gate-mode.test.ts +19 -3
- package/src/__tests__/subagent-tools.test.ts +3 -2
- package/src/__tests__/telephony-tts-guard.test.ts +38 -3
- package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +4 -0
- package/src/__tests__/watch-retro-tool-availability.test.ts +8 -17
- package/src/__tests__/workspace-git-service.test.ts +26 -0
- package/src/__tests__/workspace-migration-154-repair-retired-codex-gpt-5-4-model-ids.test.ts +336 -0
- package/src/acp/__tests__/agent-process.test.ts +258 -4
- package/src/acp/__tests__/auth-required.test.ts +96 -0
- package/src/acp/__tests__/client-handler.test.ts +90 -2
- package/src/acp/__tests__/helpers/acp-model-option.ts +51 -0
- package/src/acp/__tests__/session-manager-persistence.test.ts +5 -1
- package/src/acp/__tests__/session-manager-resume.test.ts +270 -6
- package/src/acp/__tests__/session-manager-usage.test.ts +4 -2
- package/src/acp/__tests__/session-manager.test.ts +895 -4
- package/src/acp/agent-process.test.ts +4 -1
- package/src/acp/agent-process.ts +94 -9
- package/src/acp/auth-required.ts +50 -1
- package/src/acp/auto-install.test.ts +9 -7
- package/src/acp/client-handler.ts +16 -3
- package/src/acp/model-config.test.ts +166 -0
- package/src/acp/model-config.ts +138 -0
- package/src/acp/resolve-agent.test.ts +220 -6
- package/src/acp/resolve-agent.ts +88 -23
- package/src/acp/session-manager.test.ts +196 -3
- package/src/acp/session-manager.ts +441 -18
- package/src/acp/types.ts +50 -5
- package/src/api/events/acp-session-model-update.test.ts +88 -0
- package/src/api/events/acp-session-model-update.ts +46 -0
- package/src/api/events/app-files-changed.ts +4 -3
- package/src/api/index.ts +6 -0
- package/src/apps/app-store.ts +1 -1
- package/src/apps/resolve-app.ts +81 -0
- package/src/apps/source-fingerprint.ts +266 -0
- package/src/bundler/app-compiler.ts +15 -0
- package/src/calls/__tests__/barge-in-guard.test.ts +81 -0
- package/src/{__tests__ → calls/__tests__}/call-controller.test.ts +503 -82
- package/src/{__tests__ → calls/__tests__}/media-stream-audio-transcode.test.ts +1 -1
- package/src/{__tests__ → calls/__tests__}/media-stream-output.test.ts +37 -8
- package/src/{__tests__ → calls/__tests__}/media-stream-parser.test.ts +1 -1
- package/src/{__tests__ → calls/__tests__}/media-stream-server-integration.test.ts +150 -57
- package/src/{__tests__ → calls/__tests__}/media-stream-stt-session.test.ts +5 -5
- package/src/{__tests__ → calls/__tests__}/media-turn-detector.test.ts +1 -1
- package/src/{live-voice → calls}/__tests__/progress-narration.test.ts +4 -4
- package/src/{live-voice → calls}/__tests__/progress-phrases.test.ts +1 -1
- package/src/calls/__tests__/voice-control-protocol.test.ts +58 -25
- package/src/calls/__tests__/voice-leg-coordinator.test.ts +229 -0
- package/src/calls/__tests__/voice-progress-cadence.test.ts +274 -0
- package/src/{__tests__/voice-session-bridge.test.ts → calls/__tests__/voice-session-bridge-turn-flow.test.ts} +20 -12
- package/src/calls/__tests__/voice-session-bridge.test.ts +37 -1
- package/src/calls/__tests__/voice-triage-escalate.test.ts +105 -0
- package/src/calls/barge-in-guard.ts +118 -0
- package/src/calls/call-constants.ts +24 -4
- package/src/calls/call-controller.ts +499 -199
- package/src/calls/call-transport.ts +16 -0
- package/src/calls/media-stream-output.ts +32 -0
- package/src/calls/media-stream-server.ts +82 -30
- package/src/calls/media-stream-stt-session.ts +15 -0
- package/src/{live-voice → calls}/progress-narration.ts +2 -2
- package/src/calls/resolve-call-tts-provider.ts +33 -2
- package/src/calls/tts-call-strategy.ts +9 -5
- package/src/calls/types.ts +2 -0
- package/src/calls/voice-control-protocol.ts +53 -21
- package/src/calls/voice-leg-coordinator.ts +189 -0
- package/src/calls/voice-progress-cadence.ts +423 -0
- package/src/calls/voice-session-bridge.ts +59 -46
- package/src/calls/voice-triage-escalate.ts +175 -56
- package/src/cli/commands/__tests__/apps.test.ts +245 -0
- package/src/cli/commands/__tests__/roadmap.help.test.ts +19 -0
- package/src/cli/commands/apps.help.ts +67 -13
- package/src/cli/commands/apps.ts +183 -0
- package/src/cli/commands/channels/__tests__/send.test.ts +218 -0
- package/src/cli/commands/channels/index.help.ts +70 -2
- package/src/cli/commands/channels/index.ts +16 -3
- package/src/cli/commands/channels/send.ts +128 -0
- package/src/cli/commands/credentials.help.ts +6 -2
- package/src/cli/commands/credentials.ts +2 -2
- package/src/cli/commands/mcp.help.ts +8 -8
- package/src/cli/commands/mcp.ts +1 -1
- package/src/cli/commands/roadmap.help.ts +5 -1
- package/src/cli/commands/roadmap.ts +1 -1
- package/src/cli/lib/bundled-marketplace.json +103 -90
- package/src/config/__tests__/default-profile-catalog.test.ts +22 -0
- package/src/config/__tests__/memory-retrospective-schema.test.ts +15 -1
- package/src/config/__tests__/platform-identity.test.ts +117 -0
- package/src/config/__tests__/platform-rehydration.test.ts +93 -0
- package/src/config/acp-agent-write.ts +43 -0
- package/src/config/acp-defaults.test.ts +13 -3
- package/src/config/acp-defaults.ts +23 -6
- package/src/config/acp-schema.test.ts +81 -0
- package/src/config/acp-schema.ts +30 -2
- package/src/config/bundled-skills/acp/SKILL.md +22 -14
- package/src/config/bundled-skills/acp/TOOLS.json +5 -1
- package/src/config/bundled-skills/app-builder/SKILL.md +12 -9
- package/src/config/bundled-skills/app-builder/references/CUSTOM_ROUTES.md +1 -1
- package/src/config/bundled-skills/messaging/TOOLS.json +3 -3
- package/src/config/bundled-skills/messaging/tools/messaging-send.ts +67 -139
- package/src/config/bundled-skills/phone-calls/references/TRANSCRIPTS.md +1 -1
- package/src/config/bundled-skills/screen-annotation/SKILL.md +8 -1
- package/src/config/bundled-skills/screen-annotation/tools/screen-annotation.test.ts +4 -1
- package/src/config/bundled-skills/skill-management/TOOLS.json +2 -2
- package/src/config/default-profile-catalog.ts +1 -1
- package/src/config/env.ts +2 -2
- package/src/config/feature-flag-registry.json +8 -12
- package/src/config/memory-v3-gate.test.ts +31 -0
- package/src/config/memory-v3-gate.ts +20 -16
- package/src/config/platform-identity.ts +144 -0
- package/src/config/platform-rehydration.ts +75 -21
- package/src/config/schema.ts +2 -2
- package/src/config/schemas/__tests__/live-voice.test.ts +8 -158
- package/src/config/schemas/__tests__/voice.test.ts +148 -0
- package/src/config/schemas/calls.ts +1 -28
- package/src/config/schemas/live-voice.ts +1 -173
- package/src/config/schemas/mcp.ts +1 -1
- package/src/config/schemas/memory-retrospective.ts +10 -0
- package/src/config/schemas/skills.ts +0 -14
- package/src/config/schemas/voice.ts +150 -0
- package/src/credential-execution/process-manager.test.ts +18 -19
- package/src/daemon/__tests__/conversation-tool-setup.test.ts +66 -121
- package/src/daemon/__tests__/plugin-mcp-reconcile.test.ts +4 -1
- package/src/daemon/__tests__/skill-memory-refresh-mtime.test.ts +133 -0
- package/src/daemon/app-change-notify.ts +45 -0
- package/src/daemon/config-watcher.ts +13 -194
- package/src/daemon/conversation-process.ts +35 -64
- package/src/daemon/conversation-skill-tools.ts +13 -31
- package/src/daemon/conversation-store.ts +1 -1
- package/src/daemon/conversation-surfaces.ts +10 -8
- package/src/daemon/conversation-tool-setup.ts +37 -43
- package/src/daemon/conversation.ts +0 -3
- package/src/daemon/host-bash-proxy.ts +8 -12
- package/src/daemon/host-browser-proxy.ts +15 -1
- package/src/daemon/host-file-proxy.ts +8 -10
- package/src/daemon/host-transfer-proxy.ts +14 -18
- package/src/daemon/lifecycle.ts +0 -5
- package/src/daemon/mcp-reload-service.ts +5 -5
- package/src/daemon/message-types/sync.ts +1 -0
- package/src/daemon/providers-setup.ts +1 -1
- package/src/daemon/shutdown-handlers.ts +1 -9
- package/src/daemon/skill-memory-refresh.ts +64 -0
- package/src/daemon/tool-side-effects.ts +7 -18
- package/src/desktop/__tests__/fake-desktop.ts +119 -0
- package/src/desktop/desktop-chrome-policy.test.ts +71 -0
- package/src/desktop/desktop-chrome-policy.ts +42 -0
- package/src/desktop/desktop-dependencies.test.ts +102 -0
- package/src/desktop/desktop-dependencies.ts +306 -0
- package/src/desktop/desktop-feature.test.ts +18 -0
- package/src/desktop/desktop-feature.ts +19 -0
- package/src/desktop/desktop-panel-config.test.ts +135 -0
- package/src/desktop/desktop-panel-config.ts +149 -0
- package/src/desktop/desktop-session-manager.test.ts +627 -0
- package/src/desktop/desktop-session-manager.ts +814 -0
- package/src/desktop/desktop-stream-bridge.test.ts +395 -0
- package/src/desktop/desktop-stream-bridge.ts +214 -0
- package/src/desktop/desktop-wallpaper.test.ts +191 -0
- package/src/desktop/desktop-wallpaper.ts +113 -0
- package/src/desktop/desktop-window-theme.ts +170 -0
- package/src/hooks/types.ts +33 -0
- package/src/i18n/AGENTS.md +22 -0
- package/src/i18n/__tests__/i18n.test.ts +108 -0
- package/src/i18n/index.ts +16 -0
- package/src/i18n/locales.ts +93 -0
- package/src/i18n/messages.ts +61 -0
- package/src/i18n/title.ts +25 -0
- package/src/inbound/platform-callback-registration.ts +4 -5
- package/src/live-voice/__tests__/live-voice-metrics.test.ts +0 -3
- package/src/live-voice/__tests__/live-voice-progress.test.ts +15 -15
- package/src/live-voice/__tests__/live-voice-vad.test.ts +2 -2
- package/src/live-voice/__tests__/runtime-websocket-shell.test.ts +13 -69
- package/src/live-voice/live-voice-session.ts +213 -693
- package/src/live-voice/protocol.ts +0 -2
- package/src/mcp/__tests__/effective-config.test.ts +2 -2
- package/src/mcp/__tests__/workspace-mcp-config.test.ts +135 -0
- package/src/mcp/effective-config.ts +3 -3
- package/src/mcp/mcp-header-store.ts +29 -45
- package/src/mcp/spec-schema.ts +111 -0
- package/src/mcp/startup.ts +8 -4
- package/src/mcp/workspace-mcp-config.ts +188 -0
- package/src/messaging/provider.ts +10 -3
- package/src/messaging/providers/__tests__/proactive-address.test.ts +95 -0
- package/src/messaging/providers/callback-routing.ts +18 -0
- package/src/messaging/providers/channel-transport.ts +56 -0
- package/src/messaging/providers/discord/transport.ts +17 -0
- package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +6 -17
- package/src/messaging/providers/slack/adapter.ts +7 -44
- package/src/messaging/providers/slack/transport.ts +16 -0
- package/src/messaging/providers/telegram-bot/adapter.ts +1 -48
- package/src/messaging/providers/telegram-bot/client.ts +0 -26
- package/src/messaging/providers/telegram-bot/transport.ts +20 -0
- package/src/messaging/providers/whatsapp/adapter.ts +1 -48
- package/src/messaging/providers/whatsapp/transport.ts +18 -0
- package/src/monitoring/__tests__/workspace-git-heartbeat.test.ts +8 -0
- package/src/notifications/__tests__/assistant-reply-producer.test.ts +55 -0
- package/src/notifications/__tests__/home-feed-side-effect.test.ts +153 -3
- package/src/notifications/assistant-reply-producer.ts +8 -3
- package/src/notifications/conversation-pairing.ts +36 -16
- package/src/notifications/delivered-post-record.ts +11 -1
- package/src/notifications/home-feed-side-effect.ts +55 -7
- package/src/outbound-proxy/certs.ts +3 -5
- package/src/permissions/checker.test.ts +11 -6
- package/src/persistence/compaction-log-store-clickhouse.ts +3 -3
- package/src/persistence/conversation-crud.ts +71 -63
- package/src/persistence/conversation-lineage.ts +12 -4
- package/src/persistence/conversation-title-placeholders.ts +23 -0
- package/src/persistence/conversation-title-service.ts +9 -26
- package/src/persistence/llm-request-log-sink-clickhouse.ts +3 -3
- package/src/persistence/llm-request-log-source-clickhouse.ts +3 -3
- package/src/persistence/message-cursor.ts +78 -0
- package/src/persistence/message-reads.ts +30 -0
- package/src/persistence/migrations/157-invite-contact-id.ts +18 -6
- package/src/persistence/migrations/__tests__/157-invite-contact-id.test.ts +149 -0
- package/src/persistence/schema/memory-core.ts +5 -0
- package/src/platform/client.test.ts +2 -0
- package/src/platform/client.ts +2 -10
- package/src/plugin-api/constants.ts +2 -0
- package/src/plugin-api/index.ts +1 -0
- package/src/plugin-api/types.ts +2 -0
- package/src/plugins/defaults/index.ts +2 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +46 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-cursor-backfill.test.ts +118 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-cursor-preserve.test.ts +83 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-enqueue.test.ts +9 -5
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +20 -10
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-state.test.ts +219 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-sweep.test.ts +31 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-trigger-check.test.ts +63 -0
- package/src/plugins/defaults/memory/__tests__/shutdown-hook.test.ts +4 -0
- package/src/plugins/defaults/memory/graph/capability-seed.ts +4 -6
- package/src/plugins/defaults/memory/hooks/init.ts +10 -0
- package/src/plugins/defaults/memory/hooks/message-deleted.ts +21 -0
- package/src/plugins/defaults/memory/hooks/shutdown.ts +2 -0
- package/src/plugins/defaults/memory/jobs-worker.ts +13 -0
- package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +34 -60
- package/src/plugins/defaults/memory/memory-retrospective-cursor-backfill.ts +126 -0
- package/src/plugins/defaults/memory/memory-retrospective-cursor-preserve.ts +79 -0
- package/src/plugins/defaults/memory/memory-retrospective-cursor.ts +28 -0
- package/src/plugins/defaults/memory/memory-retrospective-enqueue.ts +3 -5
- package/src/plugins/defaults/memory/memory-retrospective-job.ts +12 -3
- package/src/plugins/defaults/memory/memory-retrospective-state.ts +312 -71
- package/src/plugins/defaults/memory/memory-retrospective-sweep.ts +2 -1
- package/src/plugins/defaults/memory/memory-retrospective-trigger-check.ts +5 -3
- package/src/plugins/defaults/memory/skill-md-mtime-poll.ts +9 -0
- package/src/plugins/defaults/memory/startup.ts +6 -1
- package/src/plugins/defaults/memory/substrate/__tests__/skill-content.test.ts +7 -9
- package/src/plugins/defaults/memory/substrate/skill-content.ts +2 -2
- package/src/plugins/defaults/memory/v3/__tests__/pool-select.test.ts +89 -0
- package/src/plugins/defaults/memory/worker.ts +6 -0
- package/src/plugins/mcp-servers.ts +15 -75
- package/src/providers/__tests__/connection-model-compat.test.ts +3 -3
- package/src/providers/gemini/client.ts +9 -23
- package/src/providers/gemini-thought-signature.test.ts +63 -0
- package/src/providers/gemini-thought-signature.ts +44 -0
- package/src/providers/model-catalog.ts +34 -0
- package/src/providers/model-intents.ts +10 -5
- package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +146 -18
- package/src/providers/openai/__tests__/chat-completions-provider-thought-signature.test.ts +613 -0
- package/src/providers/openai/__tests__/google-thought-signature.test.ts +235 -0
- package/src/providers/openai/__tests__/tool-choice-mapping.test.ts +83 -1
- package/src/providers/openai/chat-completions-provider.ts +192 -78
- package/src/providers/openai/codex-models.ts +0 -4
- package/src/providers/openai/google-thought-signature.ts +320 -0
- package/src/runtime/AGENTS.md +2 -0
- package/src/runtime/__tests__/channel-send.test.ts +443 -0
- package/src/runtime/__tests__/desktop-stream-upgrade.test.ts +89 -0
- package/src/runtime/__tests__/live-voice-upgrade.test.ts +58 -0
- package/src/runtime/__tests__/runtime-ws-test-utils.ts +72 -0
- package/src/runtime/__tests__/watch-stream-upgrade.test.ts +59 -0
- package/src/runtime/auth/same-actor.ts +38 -3
- package/src/runtime/channel-send.ts +316 -0
- package/src/runtime/http-server.ts +160 -151
- package/src/runtime/routes/__tests__/acp-routes.test.ts +159 -4
- package/src/runtime/routes/__tests__/apps-refresh-route.test.ts +205 -0
- package/src/runtime/routes/__tests__/channel-send-routes.test.ts +127 -0
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +108 -1
- package/src/runtime/routes/__tests__/dictation-cleanup-guard.test.ts +107 -0
- package/src/runtime/routes/__tests__/mcp-add-default-risk.test.ts +16 -12
- package/src/runtime/routes/acp-routes.test.ts +15 -0
- package/src/runtime/routes/acp-routes.ts +72 -9
- package/src/runtime/routes/app-management-routes.ts +73 -0
- package/src/runtime/routes/app-routes.ts +1 -1
- package/src/runtime/routes/channel-send-routes.ts +85 -0
- package/src/runtime/routes/conversation-list-routes.ts +14 -3
- package/src/runtime/routes/conversation-query-routes.ts +40 -0
- package/src/runtime/routes/credential-routes.ts +19 -10
- package/src/runtime/routes/desktop-setup-routes.test.ts +74 -0
- package/src/runtime/routes/desktop-setup-routes.ts +35 -0
- package/src/runtime/routes/diagnostics-routes.ts +55 -3
- package/src/runtime/routes/index.ts +4 -0
- package/src/runtime/routes/log-export-routes.ts +2 -0
- package/src/runtime/routes/mcp-auth-routes.ts +39 -47
- package/src/runtime/routes/migration-routes.ts +8 -21
- package/src/runtime/routes/platform-routes.ts +18 -15
- package/src/runtime/services/__tests__/conversation-serializer.test.ts +28 -0
- package/src/runtime/services/conversation-serializer.ts +16 -3
- package/src/runtime/sync/resource-sync-events.ts +2 -1
- package/src/schedule/worker.ts +1 -1
- package/src/security/__tests__/ces-http-record-backend.test.ts +137 -0
- package/src/security/ces-http-record-backend.ts +120 -0
- package/src/security/ces-rpc-record-backend.ts +7 -0
- package/src/skills/__tests__/workspace-skill-md-signature.test.ts +60 -0
- package/src/skills/workspace-skill-md-signature.ts +50 -0
- package/src/subagent/consult-context.ts +12 -25
- package/src/tools/acp/list-agents.test.ts +3 -1
- package/src/tools/acp/spawn.test.ts +146 -11
- package/src/tools/acp/spawn.ts +21 -3
- package/src/tools/acp/status.test.ts +94 -0
- package/src/tools/acp/status.ts +40 -3
- package/src/tools/app-control/skill-proxy-bridge.ts +2 -1
- package/src/tools/capability-offer.test.ts +22 -2
- package/src/tools/capability-offer.ts +16 -1
- package/src/tools/computer-use/definitions.ts +4 -2
- package/src/tools/computer-use/skill-proxy-bridge.ts +2 -1
- package/src/tools/credentials/metadata-store.ts +100 -0
- package/src/tools/host-filesystem/edit.test.ts +2 -1
- package/src/tools/host-filesystem/edit.ts +2 -2
- package/src/tools/host-filesystem/read.test.ts +2 -1
- package/src/tools/host-filesystem/read.ts +2 -2
- package/src/tools/host-filesystem/transfer.test.ts +2 -1
- package/src/tools/host-filesystem/transfer.ts +2 -2
- package/src/tools/host-filesystem/write.test.ts +2 -1
- package/src/tools/host-filesystem/write.ts +2 -2
- package/src/tools/host-terminal/host-shell.test.ts +16 -0
- package/src/tools/host-terminal/host-shell.ts +4 -20
- package/src/tools/policy-context.ts +6 -7
- package/src/tools/subagent/spawn.ts +5 -8
- package/src/tools/terminal/__tests__/safe-env.test.ts +6 -6
- package/src/tools/terminal/safe-env.ts +23 -3
- package/src/tools/watch/watch-retro-report.ts +0 -8
- package/src/util/__tests__/language-subtag.test.ts +23 -1
- package/src/util/ca-bundle.ts +13 -0
- package/src/util/language-subtag.ts +16 -0
- package/src/util/platform.ts +8 -0
- package/src/workspace/git-porcelain.ts +66 -0
- package/src/workspace/git-service.ts +5 -54
- package/src/workspace/migrations/154-repair-retired-codex-gpt-5-4-model-ids.ts +258 -0
- package/src/workspace/migrations/155-move-front-model-config-to-voice.ts +78 -0
- package/src/workspace/migrations/156-extract-workspace-mcp-json.ts +214 -0
- package/src/workspace/migrations/__tests__/155-move-front-model-config-to-voice.test.ts +102 -0
- package/src/workspace/migrations/__tests__/156-extract-workspace-mcp-json.test.ts +199 -0
- package/src/workspace/migrations/registry.ts +6 -0
- package/src/__tests__/app-source-watcher.test.ts +0 -218
- package/src/__tests__/config-watcher-skill-reseed.test.ts +0 -359
- package/src/__tests__/speaker-identification.test.ts +0 -56
- package/src/calls/speaker-identification.ts +0 -233
- package/src/daemon/app-source-watcher.ts +0 -218
- package/src/messaging/providers/whatsapp/client.ts +0 -29
- package/src/util/fs-watcher-error.ts +0 -36
- /package/src/{live-voice → calls}/progress-phrases.ts +0 -0
|
@@ -104,6 +104,7 @@ mock.module("../runtime/sync/resource-sync-events.js", () => ({
|
|
|
104
104
|
publishConversationTitleChanged: mockPublishConversationTitleChanged,
|
|
105
105
|
}));
|
|
106
106
|
|
|
107
|
+
import { MESSAGE_KEYS } from "../i18n/index.js";
|
|
107
108
|
import {
|
|
108
109
|
AUTO_TITLE_DETERMINISTIC,
|
|
109
110
|
AUTO_TITLE_LLM,
|
|
@@ -309,10 +310,10 @@ describe("conversation-title-service", () => {
|
|
|
309
310
|
userMessage: "so about that t-shirt...",
|
|
310
311
|
});
|
|
311
312
|
|
|
312
|
-
expect(result.title).toBe(
|
|
313
|
+
expect(result.title).toBe(MESSAGE_KEYS.CONVERSATION_TITLE_UNTITLED);
|
|
313
314
|
expect(mockUpdateConversationTitle).toHaveBeenCalledWith(
|
|
314
315
|
"conv-1",
|
|
315
|
-
|
|
316
|
+
MESSAGE_KEYS.CONVERSATION_TITLE_UNTITLED,
|
|
316
317
|
AUTO_TITLE_DETERMINISTIC,
|
|
317
318
|
);
|
|
318
319
|
});
|
|
@@ -333,7 +334,7 @@ describe("conversation-title-service", () => {
|
|
|
333
334
|
userMessage: "something",
|
|
334
335
|
});
|
|
335
336
|
|
|
336
|
-
expect(result.title).toBe(
|
|
337
|
+
expect(result.title).toBe(MESSAGE_KEYS.CONVERSATION_TITLE_UNTITLED);
|
|
337
338
|
});
|
|
338
339
|
|
|
339
340
|
// The core bug this PR fixes: weak title models emit their reasoning or
|
|
@@ -361,10 +362,10 @@ describe("conversation-title-service", () => {
|
|
|
361
362
|
userMessage: "hey baby",
|
|
362
363
|
});
|
|
363
364
|
|
|
364
|
-
expect(result.title).toBe(
|
|
365
|
+
expect(result.title).toBe(MESSAGE_KEYS.CONVERSATION_TITLE_UNTITLED);
|
|
365
366
|
expect(mockUpdateConversationTitle).toHaveBeenCalledWith(
|
|
366
367
|
"conv-1",
|
|
367
|
-
|
|
368
|
+
MESSAGE_KEYS.CONVERSATION_TITLE_UNTITLED,
|
|
368
369
|
AUTO_TITLE_DETERMINISTIC,
|
|
369
370
|
);
|
|
370
371
|
});
|
|
@@ -423,7 +424,7 @@ describe("conversation-title-service", () => {
|
|
|
423
424
|
userMessage: "x",
|
|
424
425
|
});
|
|
425
426
|
|
|
426
|
-
expect(result.title).toBe(
|
|
427
|
+
expect(result.title).toBe(MESSAGE_KEYS.CONVERSATION_TITLE_UNTITLED);
|
|
427
428
|
});
|
|
428
429
|
|
|
429
430
|
test("regeneration skips LLM call when recent messages have no extractable text", async () => {
|
|
@@ -515,17 +516,17 @@ describe("conversation-title-service", () => {
|
|
|
515
516
|
});
|
|
516
517
|
|
|
517
518
|
expect(provider.sendMessage).not.toHaveBeenCalled();
|
|
518
|
-
expect(result).toEqual({ title:
|
|
519
|
+
expect(result).toEqual({ title: MESSAGE_KEYS.CONVERSATION_TITLE_UNTITLED, updated: true });
|
|
519
520
|
expect(mockUpdateConversationTitle).toHaveBeenCalledWith(
|
|
520
521
|
"conv-1",
|
|
521
|
-
|
|
522
|
+
MESSAGE_KEYS.CONVERSATION_TITLE_UNTITLED,
|
|
522
523
|
AUTO_TITLE_DETERMINISTIC,
|
|
523
524
|
);
|
|
524
525
|
});
|
|
525
526
|
|
|
526
527
|
test("keeps a background job's deterministic title when the model declines", async () => {
|
|
527
528
|
// The model answered with prose the normalizer rejects, so there is no
|
|
528
|
-
// generated title. Settling for
|
|
529
|
+
// generated title. Settling for the untitled fallback would name the
|
|
529
530
|
// job's work less well than the bootstrap title already does.
|
|
530
531
|
mockGetConversation.mockImplementation(() => ({
|
|
531
532
|
title: "Memory consolidation",
|
|
@@ -584,6 +585,9 @@ describe("conversation-title-service", () => {
|
|
|
584
585
|
expect(content).not.toContain("Generate a very short title");
|
|
585
586
|
expect(content).not.toContain("do NOT respond");
|
|
586
587
|
expect(options.systemPrompt).toContain("Do NOT respond");
|
|
588
|
+
expect(options.systemPrompt).toContain(
|
|
589
|
+
"same language as the conversation content",
|
|
590
|
+
);
|
|
587
591
|
});
|
|
588
592
|
|
|
589
593
|
test("queueGenerateConversationTitle serializes concurrent calls", async () => {
|
|
@@ -661,7 +665,7 @@ describe("conversation-title-service", () => {
|
|
|
661
665
|
).find((c) => c[0] === "conv-1");
|
|
662
666
|
expect(firstUpdate).toEqual([
|
|
663
667
|
"conv-1",
|
|
664
|
-
|
|
668
|
+
MESSAGE_KEYS.CONVERSATION_TITLE_UNTITLED,
|
|
665
669
|
AUTO_TITLE_DETERMINISTIC,
|
|
666
670
|
]);
|
|
667
671
|
const secondUpdate = (
|
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
* app_id from the executor's typed `resolvedAppId` side channel — and that
|
|
8
8
|
* non-hooked tools (app_file_edit, app_file_write) do not trigger side effects.
|
|
9
9
|
*
|
|
10
|
-
* File-change detection for file_write/file_edit is
|
|
11
|
-
*
|
|
10
|
+
* File-change detection for file_write/file_edit is not hooked: compile with
|
|
11
|
+
* `app_refresh` or `assistant apps refresh` after a batch of edits.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
@@ -50,9 +50,7 @@ mock.module("../tools/browser/browser-screencast.js", () => ({
|
|
|
50
50
|
registerConversationSender: mock(() => {}),
|
|
51
51
|
}));
|
|
52
52
|
|
|
53
|
-
// Stub app-store functions used by other modules (e.g.
|
|
54
|
-
// conversation-surfaces) so tool-side-effects' hooks can run without touching
|
|
55
|
-
// the real app store during tests.
|
|
53
|
+
// Stub app-store functions used by other modules (e.g. conversation-surfaces)
|
|
56
54
|
mock.module("../apps/app-store.js", () => ({
|
|
57
55
|
getApp: mock(() => null),
|
|
58
56
|
getAppDirPath: mock(() => "/tmp/test-apps/dummy"),
|
|
@@ -354,6 +354,57 @@ describe("createToolExecutor isInteractive threading", () => {
|
|
|
354
354
|
});
|
|
355
355
|
});
|
|
356
356
|
|
|
357
|
+
describe("createToolExecutor unattended host-tool gate", () => {
|
|
358
|
+
test("rejects a direct host tool in a background turn before dispatch", async () => {
|
|
359
|
+
const { executor, calls } = makeCapturingExecutor();
|
|
360
|
+
const toolFn = makeToolFn(
|
|
361
|
+
executor,
|
|
362
|
+
makeCtx({ currentTurnIsNonInteractive: true, transportInterface: "web" }),
|
|
363
|
+
);
|
|
364
|
+
|
|
365
|
+
const result = await toolFn("host_bash", { command: "pwd" });
|
|
366
|
+
|
|
367
|
+
expect(result).toMatchObject({ isError: true });
|
|
368
|
+
expect(result.content).toContain("requires an interactive user turn");
|
|
369
|
+
expect(calls).toHaveLength(0);
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
test("rejects a skill-dispatched host tool in a background turn before dispatch", async () => {
|
|
373
|
+
const { executor, calls } = makeCapturingExecutor();
|
|
374
|
+
const toolFn = makeToolFn(
|
|
375
|
+
executor,
|
|
376
|
+
makeCtx({ currentTurnIsNonInteractive: true, transportInterface: "web" }),
|
|
377
|
+
);
|
|
378
|
+
|
|
379
|
+
const result = await toolFn("skill_execute", {
|
|
380
|
+
tool: "host_file_read",
|
|
381
|
+
input: { path: "/tmp/a" },
|
|
382
|
+
activity: "testing",
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
expect(result).toMatchObject({ isError: true });
|
|
386
|
+
expect(result.content).toContain("requires an interactive user turn");
|
|
387
|
+
expect(calls).toHaveLength(0);
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
test("dispatches a host tool during an interactive turn", async () => {
|
|
391
|
+
const { executor, calls } = makeCapturingExecutor();
|
|
392
|
+
const toolFn = makeToolFn(
|
|
393
|
+
executor,
|
|
394
|
+
makeCtx({
|
|
395
|
+
currentTurnIsNonInteractive: false,
|
|
396
|
+
transportInterface: "web",
|
|
397
|
+
}),
|
|
398
|
+
);
|
|
399
|
+
|
|
400
|
+
const result = await toolFn("host_bash", { command: "pwd" });
|
|
401
|
+
|
|
402
|
+
expect(result).toMatchObject({ content: "ok", isError: false });
|
|
403
|
+
expect(calls).toHaveLength(1);
|
|
404
|
+
expect(calls[0].name).toBe("host_bash");
|
|
405
|
+
});
|
|
406
|
+
});
|
|
407
|
+
|
|
357
408
|
describe("createToolExecutor channel-permission coordinate threading", () => {
|
|
358
409
|
beforeEach(() => {
|
|
359
410
|
mockBindingExternalChatId = null;
|
|
@@ -8,8 +8,10 @@ import type { CredentialRecord } from "@vellumai/service-contracts/credential-rp
|
|
|
8
8
|
import type { CredentialRecordBackend } from "../security/ces-rpc-record-backend.js";
|
|
9
9
|
import {
|
|
10
10
|
_ensureCesRecordsLoaded,
|
|
11
|
+
_setHttpRecordBackendForTests,
|
|
11
12
|
_setMetadataPath,
|
|
12
13
|
getCredentialMetadata,
|
|
14
|
+
listCredentialRecordsLive,
|
|
13
15
|
setCredentialRecordBackend,
|
|
14
16
|
upsertCredentialMetadata,
|
|
15
17
|
} from "../tools/credentials/metadata-store.js";
|
|
@@ -35,7 +37,9 @@ function leftoverContents(): string {
|
|
|
35
37
|
return readFileSync(leftoverPath(), "utf-8");
|
|
36
38
|
}
|
|
37
39
|
|
|
38
|
-
function makeBackend(
|
|
40
|
+
function makeBackend(
|
|
41
|
+
name?: string,
|
|
42
|
+
): CredentialRecordBackend & {
|
|
39
43
|
store: Map<string, CredentialRecord>;
|
|
40
44
|
bulkSetCalls: number;
|
|
41
45
|
listCalls: number;
|
|
@@ -46,6 +50,7 @@ function makeBackend(): CredentialRecordBackend & {
|
|
|
46
50
|
bulkSetCalls: number;
|
|
47
51
|
listCalls: number;
|
|
48
52
|
} = {
|
|
53
|
+
name,
|
|
49
54
|
store,
|
|
50
55
|
bulkSetCalls: 0,
|
|
51
56
|
listCalls: 0,
|
|
@@ -83,6 +88,7 @@ function makeBackend(): CredentialRecordBackend & {
|
|
|
83
88
|
describe("CES credential record cache", () => {
|
|
84
89
|
beforeEach(() => {
|
|
85
90
|
setCredentialRecordBackend(undefined);
|
|
91
|
+
_setHttpRecordBackendForTests(null);
|
|
86
92
|
_setMetadataPath(null);
|
|
87
93
|
const leftover = leftoverPath();
|
|
88
94
|
if (existsSync(leftover)) {
|
|
@@ -92,6 +98,7 @@ describe("CES credential record cache", () => {
|
|
|
92
98
|
|
|
93
99
|
afterEach(() => {
|
|
94
100
|
setCredentialRecordBackend(undefined);
|
|
101
|
+
_setHttpRecordBackendForTests(null);
|
|
95
102
|
_setMetadataPath(null);
|
|
96
103
|
const leftover = leftoverPath();
|
|
97
104
|
if (existsSync(leftover)) {
|
|
@@ -218,6 +225,169 @@ describe("CES credential record cache", () => {
|
|
|
218
225
|
await _ensureCesRecordsLoaded();
|
|
219
226
|
});
|
|
220
227
|
|
|
228
|
+
test("live catalog list returns CES records without the in-process cache", async () => {
|
|
229
|
+
const backend = makeBackend();
|
|
230
|
+
const record: CredentialRecord = {
|
|
231
|
+
credentialId: "cred-live",
|
|
232
|
+
service: "github",
|
|
233
|
+
field: "token",
|
|
234
|
+
allowedTools: ["bash"],
|
|
235
|
+
allowedDomains: [],
|
|
236
|
+
createdAt: 1,
|
|
237
|
+
updatedAt: 2,
|
|
238
|
+
};
|
|
239
|
+
backend.store.set(credentialKey("github", "token"), record);
|
|
240
|
+
setCredentialRecordBackend(backend);
|
|
241
|
+
|
|
242
|
+
const live = await listCredentialRecordsLive();
|
|
243
|
+
expect(live.unreachable).toBe(false);
|
|
244
|
+
expect(live.records).toHaveLength(1);
|
|
245
|
+
expect(live.records[0]?.credentialId).toBe("cred-live");
|
|
246
|
+
expect(getCredentialMetadata("github", "token")).toBeUndefined();
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
test("live catalog list reports unreachable when CES list fails", async () => {
|
|
250
|
+
const backend = makeBackend();
|
|
251
|
+
backend.list = async () => null;
|
|
252
|
+
setCredentialRecordBackend(backend);
|
|
253
|
+
|
|
254
|
+
const live = await listCredentialRecordsLive();
|
|
255
|
+
expect(live.unreachable).toBe(true);
|
|
256
|
+
expect(live.records).toEqual([]);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
test("live catalog list reports unreachable when no record backend is attached", async () => {
|
|
260
|
+
const live = await listCredentialRecordsLive();
|
|
261
|
+
expect(live.unreachable).toBe(true);
|
|
262
|
+
expect(live.records).toEqual([]);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
test("live catalog list fails over to CES HTTP when CES RPC list fails", async () => {
|
|
266
|
+
const rpc = makeBackend("ces-rpc");
|
|
267
|
+
rpc.list = async () => null;
|
|
268
|
+
setCredentialRecordBackend(rpc);
|
|
269
|
+
|
|
270
|
+
const http = makeBackend("ces-http");
|
|
271
|
+
const record: CredentialRecord = {
|
|
272
|
+
credentialId: "cred-http",
|
|
273
|
+
service: "github",
|
|
274
|
+
field: "token",
|
|
275
|
+
allowedTools: ["bash"],
|
|
276
|
+
allowedDomains: [],
|
|
277
|
+
createdAt: 1,
|
|
278
|
+
updatedAt: 2,
|
|
279
|
+
};
|
|
280
|
+
http.store.set(credentialKey("github", "token"), record);
|
|
281
|
+
_setHttpRecordBackendForTests(http);
|
|
282
|
+
|
|
283
|
+
const live = await listCredentialRecordsLive();
|
|
284
|
+
expect(live.unreachable).toBe(false);
|
|
285
|
+
expect(live.records).toHaveLength(1);
|
|
286
|
+
expect(live.records[0]?.credentialId).toBe("cred-http");
|
|
287
|
+
expect(http.listCalls).toBe(1);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
test("live catalog list fails over to CES HTTP when CES RPC is unavailable", async () => {
|
|
291
|
+
const rpc = makeBackend("ces-rpc");
|
|
292
|
+
rpc.isAvailable = () => false;
|
|
293
|
+
setCredentialRecordBackend(rpc);
|
|
294
|
+
|
|
295
|
+
const http = makeBackend("ces-http");
|
|
296
|
+
const record: CredentialRecord = {
|
|
297
|
+
credentialId: "cred-http-down-rpc",
|
|
298
|
+
service: "github",
|
|
299
|
+
field: "token",
|
|
300
|
+
allowedTools: ["bash"],
|
|
301
|
+
allowedDomains: [],
|
|
302
|
+
createdAt: 1,
|
|
303
|
+
updatedAt: 2,
|
|
304
|
+
};
|
|
305
|
+
http.store.set(credentialKey("github", "token"), record);
|
|
306
|
+
_setHttpRecordBackendForTests(http);
|
|
307
|
+
|
|
308
|
+
const live = await listCredentialRecordsLive();
|
|
309
|
+
expect(live.unreachable).toBe(false);
|
|
310
|
+
expect(live.records[0]?.credentialId).toBe("cred-http-down-rpc");
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
test("live catalog list uses CES HTTP when no record backend is attached", async () => {
|
|
314
|
+
const http = makeBackend("ces-http");
|
|
315
|
+
const record: CredentialRecord = {
|
|
316
|
+
credentialId: "cred-http-only",
|
|
317
|
+
service: "github",
|
|
318
|
+
field: "token",
|
|
319
|
+
allowedTools: ["bash"],
|
|
320
|
+
allowedDomains: [],
|
|
321
|
+
createdAt: 1,
|
|
322
|
+
updatedAt: 2,
|
|
323
|
+
};
|
|
324
|
+
http.store.set(credentialKey("github", "token"), record);
|
|
325
|
+
_setHttpRecordBackendForTests(http);
|
|
326
|
+
|
|
327
|
+
const live = await listCredentialRecordsLive();
|
|
328
|
+
expect(live.unreachable).toBe(false);
|
|
329
|
+
expect(live.records[0]?.credentialId).toBe("cred-http-only");
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
test("live catalog list does not fail over when CES RPC returns an empty catalog", async () => {
|
|
333
|
+
const rpc = makeBackend("ces-rpc");
|
|
334
|
+
setCredentialRecordBackend(rpc);
|
|
335
|
+
|
|
336
|
+
const http = makeBackend("ces-http");
|
|
337
|
+
http.store.set(credentialKey("github", "token"), {
|
|
338
|
+
credentialId: "cred-http-ignored",
|
|
339
|
+
service: "github",
|
|
340
|
+
field: "token",
|
|
341
|
+
allowedTools: ["bash"],
|
|
342
|
+
allowedDomains: [],
|
|
343
|
+
createdAt: 1,
|
|
344
|
+
updatedAt: 2,
|
|
345
|
+
});
|
|
346
|
+
_setHttpRecordBackendForTests(http);
|
|
347
|
+
|
|
348
|
+
const live = await listCredentialRecordsLive();
|
|
349
|
+
expect(live.unreachable).toBe(false);
|
|
350
|
+
expect(live.records).toEqual([]);
|
|
351
|
+
expect(http.listCalls).toBe(0);
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
test("live catalog list does not fail over for a non-RPC record backend", async () => {
|
|
355
|
+
const backend = makeBackend();
|
|
356
|
+
backend.list = async () => null;
|
|
357
|
+
setCredentialRecordBackend(backend);
|
|
358
|
+
|
|
359
|
+
const http = makeBackend("ces-http");
|
|
360
|
+
http.store.set(credentialKey("github", "token"), {
|
|
361
|
+
credentialId: "cred-http-ignored",
|
|
362
|
+
service: "github",
|
|
363
|
+
field: "token",
|
|
364
|
+
allowedTools: ["bash"],
|
|
365
|
+
allowedDomains: [],
|
|
366
|
+
createdAt: 1,
|
|
367
|
+
updatedAt: 2,
|
|
368
|
+
});
|
|
369
|
+
_setHttpRecordBackendForTests(http);
|
|
370
|
+
|
|
371
|
+
const live = await listCredentialRecordsLive();
|
|
372
|
+
expect(live.unreachable).toBe(true);
|
|
373
|
+
expect(live.records).toEqual([]);
|
|
374
|
+
expect(http.listCalls).toBe(0);
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
test("live catalog list reports unreachable when CES RPC and HTTP both fail", async () => {
|
|
378
|
+
const rpc = makeBackend("ces-rpc");
|
|
379
|
+
rpc.list = async () => null;
|
|
380
|
+
setCredentialRecordBackend(rpc);
|
|
381
|
+
|
|
382
|
+
const http = makeBackend("ces-http");
|
|
383
|
+
http.list = async () => null;
|
|
384
|
+
_setHttpRecordBackendForTests(http);
|
|
385
|
+
|
|
386
|
+
const live = await listCredentialRecordsLive();
|
|
387
|
+
expect(live.unreachable).toBe(true);
|
|
388
|
+
expect(live.records).toEqual([]);
|
|
389
|
+
});
|
|
390
|
+
|
|
221
391
|
test("upsert write-throughs to CES without updating leftover metadata.json", async () => {
|
|
222
392
|
const leftoverRecord: CredentialRecord = {
|
|
223
393
|
credentialId: "cred-leftover",
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
BadRequestError,
|
|
5
|
+
ForbiddenError,
|
|
6
|
+
InternalError,
|
|
7
|
+
} from "../runtime/routes/errors.js";
|
|
4
8
|
import type { CredentialMetadata } from "../tools/credentials/metadata-store.js";
|
|
5
9
|
|
|
6
10
|
// ---------------------------------------------------------------------------
|
|
@@ -15,6 +19,8 @@ let disconnectedProviders: string[];
|
|
|
15
19
|
let credentialIdCounter: number;
|
|
16
20
|
let scrubbedValues: string[];
|
|
17
21
|
let scrubRejects: boolean;
|
|
22
|
+
let liveRecordsUnreachable: boolean;
|
|
23
|
+
let secretGetUnreachable: boolean;
|
|
18
24
|
|
|
19
25
|
function metaKey(service: string, field: string): string {
|
|
20
26
|
return `${service}:${field}`;
|
|
@@ -49,7 +55,7 @@ mock.module("../security/secure-keys.js", () => ({
|
|
|
49
55
|
getSecureKeyAsync: mock(async (key: string) => secureStore.get(key)),
|
|
50
56
|
getSecureKeyResultAsync: mock(async (key: string) => ({
|
|
51
57
|
value: secureStore.get(key),
|
|
52
|
-
unreachable:
|
|
58
|
+
unreachable: secretGetUnreachable,
|
|
53
59
|
})),
|
|
54
60
|
deleteSecureKeyAsync: mock(async (key: string) =>
|
|
55
61
|
secureStore.delete(key) ? "deleted" : "not-found",
|
|
@@ -60,7 +66,10 @@ mock.module("../security/secure-keys.js", () => ({
|
|
|
60
66
|
|
|
61
67
|
mock.module("../tools/credentials/metadata-store.js", () => ({
|
|
62
68
|
assertMetadataWritable: () => {},
|
|
63
|
-
|
|
69
|
+
listCredentialRecordsLive: mock(async () => ({
|
|
70
|
+
records: Array.from(metadataStore.values()),
|
|
71
|
+
unreachable: liveRecordsUnreachable,
|
|
72
|
+
})),
|
|
64
73
|
getCredentialMetadata: mock((service: string, field: string) =>
|
|
65
74
|
metadataStore.get(metaKey(service, field)),
|
|
66
75
|
),
|
|
@@ -201,6 +210,8 @@ describe("credentials routes", () => {
|
|
|
201
210
|
credentialIdCounter = 0;
|
|
202
211
|
scrubbedValues = [];
|
|
203
212
|
scrubRejects = false;
|
|
213
|
+
liveRecordsUnreachable = false;
|
|
214
|
+
secretGetUnreachable = false;
|
|
204
215
|
_resetRevealSuccessRegistryForTest();
|
|
205
216
|
resetForChatMintRegistryForTest();
|
|
206
217
|
chatCredentialRevealFlag = false;
|
|
@@ -799,6 +810,204 @@ describe("credentials routes", () => {
|
|
|
799
810
|
expect(result.credentials).toHaveLength(1);
|
|
800
811
|
expect(result.credentials[0].service).toBe("github");
|
|
801
812
|
});
|
|
813
|
+
|
|
814
|
+
test("fails immediately when the credential vault is unreachable", async () => {
|
|
815
|
+
liveRecordsUnreachable = true;
|
|
816
|
+
await setRoute!.handler({
|
|
817
|
+
body: { service: "vercel", field: "api_token", value: SECRET_VALUE },
|
|
818
|
+
});
|
|
819
|
+
|
|
820
|
+
await expect(listRoute!.handler({ body: {} })).rejects.toThrow(
|
|
821
|
+
InternalError,
|
|
822
|
+
);
|
|
823
|
+
await expect(listRoute!.handler({ body: {} })).rejects.toThrow(
|
|
824
|
+
/Credential store is unreachable/,
|
|
825
|
+
);
|
|
826
|
+
});
|
|
827
|
+
|
|
828
|
+
test("fails immediately when a secret fetch reports the vault unreachable", async () => {
|
|
829
|
+
await setRoute!.handler({
|
|
830
|
+
body: { service: "vercel", field: "api_token", value: SECRET_VALUE },
|
|
831
|
+
});
|
|
832
|
+
secretGetUnreachable = true;
|
|
833
|
+
|
|
834
|
+
await expect(listRoute!.handler({ body: {} })).rejects.toThrow(
|
|
835
|
+
InternalError,
|
|
836
|
+
);
|
|
837
|
+
});
|
|
838
|
+
});
|
|
839
|
+
|
|
840
|
+
describe("platform-managed credentials", () => {
|
|
841
|
+
const PLATFORM_KEY = "vk-platform-provisioned-key";
|
|
842
|
+
const PLATFORM_BASE_URL = "https://platform.example";
|
|
843
|
+
|
|
844
|
+
/**
|
|
845
|
+
* The platform provisions the pod's own credentials by POSTing them to
|
|
846
|
+
* `/v1/secrets` as `credential` entries, which land in the same secure
|
|
847
|
+
* store and metadata table as a user's own credentials.
|
|
848
|
+
*/
|
|
849
|
+
function seedPlatformCredential(field: string, value: string): string {
|
|
850
|
+
const key = `vellum:${field}`;
|
|
851
|
+
secureStore.set(key, value);
|
|
852
|
+
const now = Date.now();
|
|
853
|
+
metadataStore.set(key, {
|
|
854
|
+
credentialId: `cred-${++credentialIdCounter}`,
|
|
855
|
+
service: "vellum",
|
|
856
|
+
field,
|
|
857
|
+
allowedTools: [],
|
|
858
|
+
allowedDomains: [],
|
|
859
|
+
createdAt: now,
|
|
860
|
+
updatedAt: now,
|
|
861
|
+
});
|
|
862
|
+
return metadataStore.get(key)!.credentialId;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
test("the listing omits platform credentials and keeps the user's own", async () => {
|
|
866
|
+
// GIVEN the platform's provisioned credentials alongside a user's
|
|
867
|
+
seedPlatformCredential("assistant_api_key", PLATFORM_KEY);
|
|
868
|
+
seedPlatformCredential("platform_base_url", PLATFORM_BASE_URL);
|
|
869
|
+
await setRoute!.handler({
|
|
870
|
+
body: { service: "vercel", field: "api_token", value: SECRET_VALUE },
|
|
871
|
+
});
|
|
872
|
+
|
|
873
|
+
// WHEN the credentials are listed
|
|
874
|
+
const result = (await listRoute!.handler({ body: {} })) as ListResponse;
|
|
875
|
+
|
|
876
|
+
// THEN only the user's credential is a row, and no platform value leaks
|
|
877
|
+
expect(result.credentials.map((c) => `${c.service}:${c.field}`)).toEqual([
|
|
878
|
+
"vercel:api_token",
|
|
879
|
+
]);
|
|
880
|
+
expect(JSON.stringify(result)).not.toContain(PLATFORM_KEY);
|
|
881
|
+
});
|
|
882
|
+
|
|
883
|
+
test("reveal refuses the platform API key for every principal", async () => {
|
|
884
|
+
/**
|
|
885
|
+
* Both farming paths land on this handler: the assistant's own tool
|
|
886
|
+
* shell arrives as `local`, a Settings click as `user`.
|
|
887
|
+
*/
|
|
888
|
+
seedPlatformCredential("assistant_api_key", PLATFORM_KEY);
|
|
889
|
+
|
|
890
|
+
for (const principal of ["local", "user", "svc_gateway"]) {
|
|
891
|
+
await expect(
|
|
892
|
+
revealRoute!.handler({
|
|
893
|
+
body: { service: "vellum", field: "assistant_api_key" },
|
|
894
|
+
headers: { "x-vellum-principal-type": principal },
|
|
895
|
+
}),
|
|
896
|
+
).rejects.toThrow(ForbiddenError);
|
|
897
|
+
}
|
|
898
|
+
});
|
|
899
|
+
|
|
900
|
+
test("reveal refuses a platform credential looked up by UUID", async () => {
|
|
901
|
+
const credentialId = seedPlatformCredential(
|
|
902
|
+
"assistant_api_key",
|
|
903
|
+
PLATFORM_KEY,
|
|
904
|
+
);
|
|
905
|
+
|
|
906
|
+
await expect(
|
|
907
|
+
revealRoute!.handler({
|
|
908
|
+
body: { id: credentialId },
|
|
909
|
+
headers: { "x-vellum-principal-type": "local" },
|
|
910
|
+
}),
|
|
911
|
+
).rejects.toThrow(/owned by the Vellum platform/);
|
|
912
|
+
});
|
|
913
|
+
|
|
914
|
+
test("inspect refuses platform credentials", async () => {
|
|
915
|
+
seedPlatformCredential("assistant_api_key", PLATFORM_KEY);
|
|
916
|
+
seedPlatformCredential("platform_base_url", PLATFORM_BASE_URL);
|
|
917
|
+
|
|
918
|
+
for (const field of ["assistant_api_key", "platform_base_url"]) {
|
|
919
|
+
await expect(
|
|
920
|
+
inspectRoute!.handler({
|
|
921
|
+
body: { service: "vellum", field },
|
|
922
|
+
}),
|
|
923
|
+
).rejects.toThrow(ForbiddenError);
|
|
924
|
+
}
|
|
925
|
+
});
|
|
926
|
+
|
|
927
|
+
test("set refuses platform credentials", async () => {
|
|
928
|
+
/**
|
|
929
|
+
* A write is a read by another name: repointing `platform_base_url`
|
|
930
|
+
* sends the next platform call, bearing the API key, to a host the
|
|
931
|
+
* writer chose. The platform's own provisioning does not come through
|
|
932
|
+
* this route; it writes over `POST /v1/secrets`.
|
|
933
|
+
*/
|
|
934
|
+
for (const field of ["assistant_api_key", "platform_base_url"]) {
|
|
935
|
+
await expect(
|
|
936
|
+
setRoute!.handler({
|
|
937
|
+
body: { service: "vellum", field, value: "https://attacker.test" },
|
|
938
|
+
}),
|
|
939
|
+
).rejects.toThrow(ForbiddenError);
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
expect(secureStore.has("vellum:assistant_api_key")).toBe(false);
|
|
943
|
+
expect(secureStore.has("vellum:platform_base_url")).toBe(false);
|
|
944
|
+
});
|
|
945
|
+
|
|
946
|
+
test("set does not overwrite an already-provisioned platform credential", async () => {
|
|
947
|
+
seedPlatformCredential("platform_base_url", PLATFORM_BASE_URL);
|
|
948
|
+
|
|
949
|
+
await expect(
|
|
950
|
+
setRoute!.handler({
|
|
951
|
+
body: {
|
|
952
|
+
service: "vellum",
|
|
953
|
+
field: "platform_base_url",
|
|
954
|
+
value: "https://attacker.test",
|
|
955
|
+
},
|
|
956
|
+
}),
|
|
957
|
+
).rejects.toThrow(/owned by the Vellum platform/);
|
|
958
|
+
|
|
959
|
+
expect(secureStore.get("vellum:platform_base_url")).toBe(
|
|
960
|
+
PLATFORM_BASE_URL,
|
|
961
|
+
);
|
|
962
|
+
});
|
|
963
|
+
|
|
964
|
+
test("delete refuses platform credentials", async () => {
|
|
965
|
+
seedPlatformCredential("assistant_api_key", PLATFORM_KEY);
|
|
966
|
+
seedPlatformCredential("platform_base_url", PLATFORM_BASE_URL);
|
|
967
|
+
|
|
968
|
+
for (const field of ["assistant_api_key", "platform_base_url"]) {
|
|
969
|
+
await expect(
|
|
970
|
+
deleteRoute!.handler({ body: { service: "vellum", field } }),
|
|
971
|
+
).rejects.toThrow(ForbiddenError);
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
expect(secureStore.get("vellum:assistant_api_key")).toBe(PLATFORM_KEY);
|
|
975
|
+
expect(secureStore.get("vellum:platform_base_url")).toBe(
|
|
976
|
+
PLATFORM_BASE_URL,
|
|
977
|
+
);
|
|
978
|
+
});
|
|
979
|
+
|
|
980
|
+
test("a user credential is still writable and deletable", async () => {
|
|
981
|
+
const stored = (await setRoute!.handler({
|
|
982
|
+
body: { service: "vercel", field: "api_token", value: SECRET_VALUE },
|
|
983
|
+
})) as SetResponse;
|
|
984
|
+
expect(stored.service).toBe("vercel");
|
|
985
|
+
expect(secureStore.get("vercel:api_token")).toBe(SECRET_VALUE);
|
|
986
|
+
|
|
987
|
+
const deleted = (await deleteRoute!.handler({
|
|
988
|
+
body: { service: "vercel", field: "api_token" },
|
|
989
|
+
})) as DeleteResponse;
|
|
990
|
+
expect(deleted.field).toBe("api_token");
|
|
991
|
+
expect(secureStore.has("vercel:api_token")).toBe(false);
|
|
992
|
+
});
|
|
993
|
+
|
|
994
|
+
test("a user credential still inspects and reveals", async () => {
|
|
995
|
+
seedPlatformCredential("assistant_api_key", PLATFORM_KEY);
|
|
996
|
+
await setRoute!.handler({
|
|
997
|
+
body: { service: "vercel", field: "api_token", value: SECRET_VALUE },
|
|
998
|
+
});
|
|
999
|
+
|
|
1000
|
+
const inspected = (await inspectRoute!.handler({
|
|
1001
|
+
body: { service: "vercel", field: "api_token" },
|
|
1002
|
+
})) as { scrubbedValue: string };
|
|
1003
|
+
expect(inspected.scrubbedValue).toBe("supe****");
|
|
1004
|
+
|
|
1005
|
+
const revealed = (await revealRoute!.handler({
|
|
1006
|
+
body: { service: "vercel", field: "api_token" },
|
|
1007
|
+
headers: { "x-vellum-principal-type": "user" },
|
|
1008
|
+
})) as { value: string };
|
|
1009
|
+
expect(revealed.value).toBe(SECRET_VALUE);
|
|
1010
|
+
});
|
|
802
1011
|
});
|
|
803
1012
|
|
|
804
1013
|
describe("platform-managed credentials", () => {
|
|
@@ -211,7 +211,8 @@ describe("Invariant 2: no generic plaintext secret read API", () => {
|
|
|
211
211
|
"persistence/llm-request-log-source-clickhouse.ts", // ClickHouse read source — lazy lookup of clickhouse:url + clickhouse:password + vellum:platform_assistant_id for self-scoped mirror reads
|
|
212
212
|
"persistence/llm-request-log-sink-clickhouse.ts", // ClickHouse write sink — lazy lookup of clickhouse:url + clickhouse:password + vellum:platform_assistant_id for self-scoped log writes
|
|
213
213
|
"persistence/compaction-log-store-clickhouse.ts", // ClickHouse compaction log writer — lazy lookup of clickhouse:url + clickhouse:password + vellum:platform_assistant_id for self-scoped event writes
|
|
214
|
-
"config/platform-
|
|
214
|
+
"config/platform-identity.ts", // in-memory platform ids, then vault copies of vellum:platform_*
|
|
215
|
+
"config/platform-rehydration.ts", // startup rehydration of platform base URL from the credential store and ids from platform validate
|
|
215
216
|
"workspace/migrations/006-services-config.ts", // services config migration reads provider API keys
|
|
216
217
|
"workspace/migrations/018-rekey-compound-credential-keys.ts", // re-key compound credential storage keys
|
|
217
218
|
"daemon/conversation-process.ts", // masked provider key display
|
|
@@ -224,7 +224,10 @@ describe("surfaceProxyResolver — CU tool routing", () => {
|
|
|
224
224
|
);
|
|
225
225
|
|
|
226
226
|
expect(result.isError).toBe(true);
|
|
227
|
-
expect(result.content).toContain(
|
|
227
|
+
expect(result.content).toContain(
|
|
228
|
+
"The Vellum desktop app is needed to view or control your screen",
|
|
229
|
+
);
|
|
230
|
+
expect(result.content).toContain("https://www.vellum.ai/downloads");
|
|
228
231
|
});
|
|
229
232
|
|
|
230
233
|
test("returns error when proxy exists but client not connected", async () => {
|
|
@@ -237,7 +240,10 @@ describe("surfaceProxyResolver — CU tool routing", () => {
|
|
|
237
240
|
});
|
|
238
241
|
|
|
239
242
|
expect(result.isError).toBe(true);
|
|
240
|
-
expect(result.content).toContain(
|
|
243
|
+
expect(result.content).toContain(
|
|
244
|
+
"The Vellum desktop app is needed to view or control your screen",
|
|
245
|
+
);
|
|
246
|
+
expect(result.content).toContain("https://www.vellum.ai/downloads");
|
|
241
247
|
proxyObj.dispose();
|
|
242
248
|
});
|
|
243
249
|
|
|
@@ -79,8 +79,6 @@ function makeFakeConversation(
|
|
|
79
79
|
getTurnInterfaceContext: () => null,
|
|
80
80
|
setTurnInterfaceContext: () => {},
|
|
81
81
|
setTransportHints: () => {},
|
|
82
|
-
setTrustContext: () => {},
|
|
83
|
-
ensureActorScopedHistory: async () => {},
|
|
84
82
|
emitActivityState: () => {
|
|
85
83
|
if (activityFailures > 0) {
|
|
86
84
|
activityFailures -= 1;
|