@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
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
setPlatformAssistantId,
|
|
5
|
+
setPlatformOrganizationId,
|
|
6
|
+
setPlatformUserId,
|
|
7
|
+
} from "../env.js";
|
|
8
|
+
import {
|
|
9
|
+
fetchPlatformIdentityIds,
|
|
10
|
+
PLATFORM_IDENTITY_VALIDATE_PATH,
|
|
11
|
+
resolvePlatformAssistantId,
|
|
12
|
+
} from "../platform-identity.js";
|
|
13
|
+
|
|
14
|
+
const BASE_URL = "https://platform.vellum.ai";
|
|
15
|
+
const ASSISTANT_ID = "11111111-2222-4333-8444-555555555555";
|
|
16
|
+
const ORG_ID = "22222222-3333-4444-8555-666666666666";
|
|
17
|
+
const USER_ID = "33333333-4444-4555-8666-777777777777";
|
|
18
|
+
|
|
19
|
+
describe("fetchPlatformIdentityIds", () => {
|
|
20
|
+
const originalFetch = globalThis.fetch;
|
|
21
|
+
const fetchCalls: Array<{ url: string; init?: RequestInit }> = [];
|
|
22
|
+
let fetchImpl: (
|
|
23
|
+
input: RequestInfo | URL,
|
|
24
|
+
init?: RequestInit,
|
|
25
|
+
) => Promise<Response> = async () =>
|
|
26
|
+
new Response("not found", { status: 404 });
|
|
27
|
+
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
fetchCalls.length = 0;
|
|
30
|
+
fetchImpl = async () => new Response("not found", { status: 404 });
|
|
31
|
+
globalThis.fetch = (async (
|
|
32
|
+
input: RequestInfo | URL,
|
|
33
|
+
init?: RequestInit,
|
|
34
|
+
) => {
|
|
35
|
+
const url = typeof input === "string" ? input : input.toString();
|
|
36
|
+
fetchCalls.push({ url, init });
|
|
37
|
+
return fetchImpl(input, init);
|
|
38
|
+
}) as unknown as typeof fetch;
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
afterEach(() => {
|
|
42
|
+
globalThis.fetch = originalFetch;
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("returns the three ids from a successful validate response", async () => {
|
|
46
|
+
fetchImpl = async () =>
|
|
47
|
+
new Response(
|
|
48
|
+
JSON.stringify({
|
|
49
|
+
assistant_id: ASSISTANT_ID,
|
|
50
|
+
organization_id: ORG_ID,
|
|
51
|
+
user_id: USER_ID,
|
|
52
|
+
name: "Example Assistant",
|
|
53
|
+
}),
|
|
54
|
+
{ status: 200 },
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
await expect(
|
|
58
|
+
fetchPlatformIdentityIds(BASE_URL, "assistant-key"),
|
|
59
|
+
).resolves.toEqual({
|
|
60
|
+
assistantId: ASSISTANT_ID,
|
|
61
|
+
organizationId: ORG_ID,
|
|
62
|
+
userId: USER_ID,
|
|
63
|
+
});
|
|
64
|
+
expect(fetchCalls[0]?.url).toBe(
|
|
65
|
+
`${BASE_URL}${PLATFORM_IDENTITY_VALIDATE_PATH}`,
|
|
66
|
+
);
|
|
67
|
+
expect(fetchCalls[0]?.init?.method).toBe("POST");
|
|
68
|
+
const headers = new Headers(fetchCalls[0]?.init?.headers);
|
|
69
|
+
expect(headers.get("Authorization")).toBe("Api-Key assistant-key");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("strips a trailing slash from the base URL", async () => {
|
|
73
|
+
fetchImpl = async () =>
|
|
74
|
+
new Response(
|
|
75
|
+
JSON.stringify({
|
|
76
|
+
assistant_id: ASSISTANT_ID,
|
|
77
|
+
organization_id: ORG_ID,
|
|
78
|
+
user_id: USER_ID,
|
|
79
|
+
}),
|
|
80
|
+
{ status: 200 },
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
await fetchPlatformIdentityIds(`${BASE_URL}/`, "assistant-key");
|
|
84
|
+
expect(fetchCalls[0]?.url).toBe(
|
|
85
|
+
`${BASE_URL}${PLATFORM_IDENTITY_VALIDATE_PATH}`,
|
|
86
|
+
);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("returns null on 401 without throwing", async () => {
|
|
90
|
+
fetchImpl = async () => new Response("no", { status: 401 });
|
|
91
|
+
await expect(
|
|
92
|
+
fetchPlatformIdentityIds(BASE_URL, "assistant-key"),
|
|
93
|
+
).resolves.toBeNull();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("returns null on a network error without throwing", async () => {
|
|
97
|
+
fetchImpl = async () => {
|
|
98
|
+
throw new Error("network down");
|
|
99
|
+
};
|
|
100
|
+
await expect(
|
|
101
|
+
fetchPlatformIdentityIds(BASE_URL, "assistant-key"),
|
|
102
|
+
).resolves.toBeNull();
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
describe("resolvePlatformAssistantId", () => {
|
|
107
|
+
afterEach(() => {
|
|
108
|
+
setPlatformAssistantId(undefined);
|
|
109
|
+
setPlatformOrganizationId(undefined);
|
|
110
|
+
setPlatformUserId(undefined);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("returns the in-memory override", async () => {
|
|
114
|
+
setPlatformAssistantId(ASSISTANT_ID);
|
|
115
|
+
await expect(resolvePlatformAssistantId()).resolves.toBe(ASSISTANT_ID);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
@@ -39,15 +39,34 @@ describe("rehydratePlatformCredentials", () => {
|
|
|
39
39
|
const originalPlatformUrlEnv = process.env.VELLUM_PLATFORM_URL;
|
|
40
40
|
const originalOrgEnv = process.env.PLATFORM_ORGANIZATION_ID;
|
|
41
41
|
const originalUserEnv = process.env.PLATFORM_USER_ID;
|
|
42
|
+
const originalAssistantApiKeyEnv = process.env.ASSISTANT_API_KEY;
|
|
43
|
+
const originalFetch = globalThis.fetch;
|
|
44
|
+
const fetchCalls: Array<{ url: string; init?: RequestInit }> = [];
|
|
45
|
+
let fetchImpl: (
|
|
46
|
+
input: RequestInfo | URL,
|
|
47
|
+
init?: RequestInit,
|
|
48
|
+
) => Promise<Response> = async () =>
|
|
49
|
+
new Response("not found", { status: 404 });
|
|
42
50
|
|
|
43
51
|
beforeEach(() => {
|
|
44
52
|
mockSecureKeys = {};
|
|
45
53
|
throwForKey = undefined;
|
|
54
|
+
fetchCalls.length = 0;
|
|
55
|
+
fetchImpl = async () => new Response("not found", { status: 404 });
|
|
56
|
+
globalThis.fetch = (async (
|
|
57
|
+
input: RequestInfo | URL,
|
|
58
|
+
init?: RequestInit,
|
|
59
|
+
) => {
|
|
60
|
+
const url = typeof input === "string" ? input : input.toString();
|
|
61
|
+
fetchCalls.push({ url, init });
|
|
62
|
+
return fetchImpl(input, init);
|
|
63
|
+
}) as unknown as typeof fetch;
|
|
46
64
|
// Env vars take precedence over the rehydrated overrides for some fields;
|
|
47
65
|
// clear them so the credential-store values are what we observe.
|
|
48
66
|
delete process.env.VELLUM_PLATFORM_URL;
|
|
49
67
|
delete process.env.PLATFORM_ORGANIZATION_ID;
|
|
50
68
|
delete process.env.PLATFORM_USER_ID;
|
|
69
|
+
delete process.env.ASSISTANT_API_KEY;
|
|
51
70
|
// Reset in-memory overrides between tests.
|
|
52
71
|
setPlatformBaseUrl(undefined);
|
|
53
72
|
setPlatformAssistantId(undefined);
|
|
@@ -56,6 +75,7 @@ describe("rehydratePlatformCredentials", () => {
|
|
|
56
75
|
});
|
|
57
76
|
|
|
58
77
|
afterEach(() => {
|
|
78
|
+
globalThis.fetch = originalFetch;
|
|
59
79
|
if (originalPlatformUrlEnv === undefined) {
|
|
60
80
|
delete process.env.VELLUM_PLATFORM_URL;
|
|
61
81
|
} else {
|
|
@@ -71,6 +91,11 @@ describe("rehydratePlatformCredentials", () => {
|
|
|
71
91
|
} else {
|
|
72
92
|
process.env.PLATFORM_USER_ID = originalUserEnv;
|
|
73
93
|
}
|
|
94
|
+
if (originalAssistantApiKeyEnv === undefined) {
|
|
95
|
+
delete process.env.ASSISTANT_API_KEY;
|
|
96
|
+
} else {
|
|
97
|
+
process.env.ASSISTANT_API_KEY = originalAssistantApiKeyEnv;
|
|
98
|
+
}
|
|
74
99
|
setPlatformBaseUrl(undefined);
|
|
75
100
|
setPlatformAssistantId(undefined);
|
|
76
101
|
setPlatformOrganizationId(undefined);
|
|
@@ -122,4 +147,72 @@ describe("rehydratePlatformCredentials", () => {
|
|
|
122
147
|
|
|
123
148
|
expect(getPlatformAssistantId()).toBe(ASSISTANT_ID);
|
|
124
149
|
});
|
|
150
|
+
|
|
151
|
+
test("trades the assistant API key for ids via platform validate", async () => {
|
|
152
|
+
mockSecureKeys[credentialKey("vellum", "platform_base_url")] = PROD_URL;
|
|
153
|
+
mockSecureKeys[credentialKey("vellum", "assistant_api_key")] =
|
|
154
|
+
"assistant-key";
|
|
155
|
+
fetchImpl = async () =>
|
|
156
|
+
new Response(
|
|
157
|
+
JSON.stringify({
|
|
158
|
+
assistant_id: ASSISTANT_ID,
|
|
159
|
+
organization_id: ORG_ID,
|
|
160
|
+
user_id: USER_ID,
|
|
161
|
+
}),
|
|
162
|
+
{ status: 200 },
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
await rehydratePlatformCredentials();
|
|
166
|
+
|
|
167
|
+
expect(fetchCalls).toHaveLength(1);
|
|
168
|
+
expect(fetchCalls[0]?.url).toBe(
|
|
169
|
+
`${PROD_URL}/v1/internal/assistants/validate/`,
|
|
170
|
+
);
|
|
171
|
+
const headers = new Headers(fetchCalls[0]?.init?.headers);
|
|
172
|
+
expect(headers.get("Authorization")).toBe("Api-Key assistant-key");
|
|
173
|
+
expect(getPlatformAssistantId()).toBe(ASSISTANT_ID);
|
|
174
|
+
expect(getPlatformOrganizationId()).toBe(ORG_ID);
|
|
175
|
+
expect(getPlatformUserId()).toBe(USER_ID);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test("validate ids win over vault leftovers", async () => {
|
|
179
|
+
mockSecureKeys[credentialKey("vellum", "platform_base_url")] = PROD_URL;
|
|
180
|
+
mockSecureKeys[credentialKey("vellum", "assistant_api_key")] =
|
|
181
|
+
"assistant-key";
|
|
182
|
+
mockSecureKeys[credentialKey("vellum", "platform_assistant_id")] =
|
|
183
|
+
"asst-from-vault";
|
|
184
|
+
fetchImpl = async () =>
|
|
185
|
+
new Response(
|
|
186
|
+
JSON.stringify({
|
|
187
|
+
assistant_id: ASSISTANT_ID,
|
|
188
|
+
organization_id: ORG_ID,
|
|
189
|
+
user_id: USER_ID,
|
|
190
|
+
}),
|
|
191
|
+
{ status: 200 },
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
await rehydratePlatformCredentials();
|
|
195
|
+
|
|
196
|
+
expect(getPlatformAssistantId()).toBe(ASSISTANT_ID);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test("falls back to the credential store when validate is unreachable", async () => {
|
|
200
|
+
mockSecureKeys[credentialKey("vellum", "platform_base_url")] = PROD_URL;
|
|
201
|
+
mockSecureKeys[credentialKey("vellum", "assistant_api_key")] =
|
|
202
|
+
"assistant-key";
|
|
203
|
+
mockSecureKeys[credentialKey("vellum", "platform_assistant_id")] =
|
|
204
|
+
ASSISTANT_ID;
|
|
205
|
+
mockSecureKeys[credentialKey("vellum", "platform_organization_id")] =
|
|
206
|
+
ORG_ID;
|
|
207
|
+
mockSecureKeys[credentialKey("vellum", "platform_user_id")] = USER_ID;
|
|
208
|
+
fetchImpl = async () => {
|
|
209
|
+
throw new Error("network down");
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
await rehydratePlatformCredentials();
|
|
213
|
+
|
|
214
|
+
expect(getPlatformAssistantId()).toBe(ASSISTANT_ID);
|
|
215
|
+
expect(getPlatformOrganizationId()).toBe(ORG_ID);
|
|
216
|
+
expect(getPlatformUserId()).toBe(USER_ID);
|
|
217
|
+
});
|
|
125
218
|
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Raw-config helper shared by the config PATCH and SET routes, the paths that
|
|
3
|
+
* write a coding agent's entry.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { isPlainObject } from "../util/object.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Clearing a leaf of a coding agent's entry is a write of `null` - a PATCH of
|
|
10
|
+
* `{ acp: { agents: { <id>: { model: null } } } }` or a
|
|
11
|
+
* `config set acp.agents.<id>.model null`. The deep-merge assigns that literal
|
|
12
|
+
* `null` because the stored value is a scalar, and a SET writes it verbatim by
|
|
13
|
+
* design. Both `command` and `model` are optional strings in `AcpConfigSchema`,
|
|
14
|
+
* so a persisted `null` makes every later `loadConfig()` warn and take its
|
|
15
|
+
* salvage path, dropping the whole `acp` section with the agents defined in it.
|
|
16
|
+
* Clearing means removing the key, so drop it here on every write path.
|
|
17
|
+
*
|
|
18
|
+
* A cleared `command` then reads as an omitted one: a bundled id inherits the
|
|
19
|
+
* profile's command, and an id that has no profile fails the agents refinement
|
|
20
|
+
* with a message naming the id, which tells the user more than zod's "expected
|
|
21
|
+
* string, received null".
|
|
22
|
+
*/
|
|
23
|
+
export function scrubNulledAcpAgentLeaves(raw: Record<string, unknown>): void {
|
|
24
|
+
const acp = raw.acp;
|
|
25
|
+
if (!isPlainObject(acp)) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
const agents = acp.agents;
|
|
29
|
+
if (!isPlainObject(agents)) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
for (const entry of Object.values(agents)) {
|
|
33
|
+
if (!isPlainObject(entry)) {
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if (entry.command === null) {
|
|
37
|
+
delete entry.command;
|
|
38
|
+
}
|
|
39
|
+
if (entry.model === null) {
|
|
40
|
+
delete entry.model;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -13,15 +13,17 @@ describe("DEFAULT_ACP_AGENT_PROFILES", () => {
|
|
|
13
13
|
]);
|
|
14
14
|
});
|
|
15
15
|
|
|
16
|
-
test("claude profile uses the @agentclientprotocol adapter binary", () => {
|
|
16
|
+
test("claude profile uses the @agentclientprotocol adapter binary on opus", () => {
|
|
17
17
|
expect(DEFAULT_ACP_AGENT_PROFILES.claude).toEqual({
|
|
18
18
|
command: "claude-agent-acp",
|
|
19
19
|
args: [],
|
|
20
20
|
description: "Claude Code (via @agentclientprotocol/claude-agent-acp)",
|
|
21
|
+
model: "opus",
|
|
21
22
|
});
|
|
22
23
|
});
|
|
23
24
|
|
|
24
25
|
test("codex profile uses the @agentclientprotocol adapter binary", () => {
|
|
26
|
+
// No model: Codex aliases are its own, so the adapter's default stands.
|
|
25
27
|
expect(DEFAULT_ACP_AGENT_PROFILES.codex).toEqual({
|
|
26
28
|
command: "codex-acp",
|
|
27
29
|
args: [],
|
|
@@ -44,11 +46,19 @@ describe("DEFAULT_ACP_AGENT_PROFILES", () => {
|
|
|
44
46
|
describe("DEFAULT_AGENT_NPM_PACKAGES", () => {
|
|
45
47
|
test("is keyed by command name with the canonical npm package", () => {
|
|
46
48
|
expect(DEFAULT_AGENT_NPM_PACKAGES).toEqual({
|
|
47
|
-
"claude-agent-acp": "@agentclientprotocol/claude-agent-acp",
|
|
48
|
-
"codex-acp": "@agentclientprotocol/codex-acp",
|
|
49
|
+
"claude-agent-acp": "@agentclientprotocol/claude-agent-acp@0.75.1",
|
|
50
|
+
"codex-acp": "@agentclientprotocol/codex-acp@1.10.0",
|
|
49
51
|
});
|
|
50
52
|
});
|
|
51
53
|
|
|
54
|
+
test("every package spec pins an exact version", () => {
|
|
55
|
+
// A bare name would let a clean install resolve whatever npm calls
|
|
56
|
+
// latest, which is what the repo's pinning rule exists to prevent.
|
|
57
|
+
for (const spec of Object.values(DEFAULT_AGENT_NPM_PACKAGES)) {
|
|
58
|
+
expect(spec).toMatch(/^@?[^@]+@\d+\.\d+\.\d+$/);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
52
62
|
test("every default profile's command has a matching npm package", () => {
|
|
53
63
|
for (const profile of Object.values(DEFAULT_ACP_AGENT_PROFILES)) {
|
|
54
64
|
expect(DEFAULT_AGENT_NPM_PACKAGES[profile.command]).toBeDefined();
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import type { AcpAgentConfig } from "./acp-schema.js";
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* A bundled profile always names its command; a user entry for the same id
|
|
5
|
+
* may omit `command` and inherit this one.
|
|
6
|
+
*/
|
|
7
|
+
export type AcpAgentProfile = AcpAgentConfig & { command: string };
|
|
8
|
+
|
|
3
9
|
// Shared frozen empty args array — these defaults are read by every ACP spawn,
|
|
4
10
|
// so a single accidental `args.push(...)` from one caller would corrupt every
|
|
5
11
|
// subsequent read. Cast through `unknown` because `AcpAgentConfig.args` is
|
|
@@ -13,17 +19,22 @@ const FROZEN_EMPTY_ARGS = Object.freeze([] as string[]) as unknown as string[];
|
|
|
13
19
|
* falls back to this map so common agents like `claude` and `codex` Just Work
|
|
14
20
|
* without requiring per-user config.
|
|
15
21
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
22
|
+
* A profile's `model` is the model a session of that agent starts on when the
|
|
23
|
+
* ask names none, and is the rung `acp.agents.<id>.model` fills: a user config
|
|
24
|
+
* entry that names a model overrides it, one that omits it inherits it.
|
|
25
|
+
*
|
|
26
|
+
* Keyed by agent id. Deeply frozen: the outer object, each profile, and the
|
|
27
|
+
* `args` arrays, so mutation throws in strict mode rather than silently
|
|
18
28
|
* corrupting the shared defaults.
|
|
19
29
|
*/
|
|
20
30
|
export const DEFAULT_ACP_AGENT_PROFILES: Readonly<
|
|
21
|
-
Record<string,
|
|
31
|
+
Record<string, AcpAgentProfile>
|
|
22
32
|
> = Object.freeze({
|
|
23
33
|
claude: Object.freeze({
|
|
24
34
|
command: "claude-agent-acp",
|
|
25
35
|
args: FROZEN_EMPTY_ARGS,
|
|
26
36
|
description: "Claude Code (via @agentclientprotocol/claude-agent-acp)",
|
|
37
|
+
model: "opus",
|
|
27
38
|
}),
|
|
28
39
|
codex: Object.freeze({
|
|
29
40
|
command: "codex-acp",
|
|
@@ -33,15 +44,21 @@ export const DEFAULT_ACP_AGENT_PROFILES: Readonly<
|
|
|
33
44
|
});
|
|
34
45
|
|
|
35
46
|
/**
|
|
36
|
-
* Single source of truth for adapter binary → npm package
|
|
47
|
+
* Single source of truth for adapter binary → npm package spec. Automatic
|
|
37
48
|
* installation and the resolver's install hints use this map, so a new
|
|
38
49
|
* adapter only needs one entry here.
|
|
39
50
|
*
|
|
40
51
|
* Keyed by command name (not agent id) so the mapping follows the binary
|
|
41
52
|
* regardless of how a user's config aliases an agent.
|
|
53
|
+
*
|
|
54
|
+
* Values carry an exact version, so a clean install lands the adapter version
|
|
55
|
+
* the daemon was tested against instead of whatever npm calls latest today.
|
|
56
|
+
* An adapter already on PATH is left alone: installation runs only when
|
|
57
|
+
* preflight found no binary at all, so this never moves a version the user
|
|
58
|
+
* installed themselves.
|
|
42
59
|
*/
|
|
43
60
|
export const DEFAULT_AGENT_NPM_PACKAGES: Readonly<Record<string, string>> =
|
|
44
61
|
Object.freeze({
|
|
45
|
-
"claude-agent-acp": "@agentclientprotocol/claude-agent-acp",
|
|
46
|
-
"codex-acp": "@agentclientprotocol/codex-acp",
|
|
62
|
+
"claude-agent-acp": "@agentclientprotocol/claude-agent-acp@0.75.1",
|
|
63
|
+
"codex-acp": "@agentclientprotocol/codex-acp@1.10.0",
|
|
47
64
|
});
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Covers the ACP config leaf that carries model selection, the per-agent
|
|
3
|
+
* `model`. It is optional with no `.default()`, so an absent value must stay
|
|
4
|
+
* absent after parsing: the bundled profile supplies the model a session
|
|
5
|
+
* starts on, and a key materialized as `undefined` would shadow it.
|
|
6
|
+
*
|
|
7
|
+
* `command` is optional in the same shape, so an entry for a bundled id can
|
|
8
|
+
* carry a model and nothing else; the record's own check keeps a command
|
|
9
|
+
* required for every id that has no bundled profile to inherit one from.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { describe, expect, test } from "bun:test";
|
|
13
|
+
|
|
14
|
+
import { AcpAgentConfigSchema, AcpConfigSchema } from "./acp-schema.js";
|
|
15
|
+
|
|
16
|
+
describe("AcpConfigSchema", () => {
|
|
17
|
+
test("populates defaults for an empty config", () => {
|
|
18
|
+
expect(AcpConfigSchema.parse({})).toEqual({
|
|
19
|
+
maxConcurrentSessions: 4,
|
|
20
|
+
agents: {},
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe("AcpConfigSchema agents", () => {
|
|
26
|
+
test("a bundled id's entry may omit command and name only a model", () => {
|
|
27
|
+
const parsed = AcpConfigSchema.parse({
|
|
28
|
+
agents: { claude: { model: "sonnet" } },
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
expect(parsed.agents).toEqual({ claude: { args: [], model: "sonnet" } });
|
|
32
|
+
expect("command" in parsed.agents.claude!).toBe(false);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("an id with no bundled profile needs a command", () => {
|
|
36
|
+
const result = AcpConfigSchema.safeParse({
|
|
37
|
+
agents: { mine: { model: "sonnet" } },
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
expect(result.success).toBe(false);
|
|
41
|
+
if (result.success) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
expect(result.error.issues.map((issue) => issue.path)).toEqual([
|
|
45
|
+
["agents", "mine", "command"],
|
|
46
|
+
]);
|
|
47
|
+
expect(result.error.issues[0]?.message).toContain("acp.agents.mine");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("an id naming an Object.prototype member inherits no command", () => {
|
|
51
|
+
expect(
|
|
52
|
+
AcpConfigSchema.safeParse({ agents: { constructor: { model: "x" } } })
|
|
53
|
+
.success,
|
|
54
|
+
).toBe(false);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe("AcpAgentConfigSchema", () => {
|
|
59
|
+
test("round-trips a per-agent model override", () => {
|
|
60
|
+
expect(
|
|
61
|
+
AcpAgentConfigSchema.parse({
|
|
62
|
+
command: "claude-agent-acp",
|
|
63
|
+
model: "opus",
|
|
64
|
+
}),
|
|
65
|
+
).toEqual({ command: "claude-agent-acp", args: [], model: "opus" });
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("leaves model unset when omitted, so a bundled profile's shows through", () => {
|
|
69
|
+
const parsed = AcpAgentConfigSchema.parse({ command: "claude-agent-acp" });
|
|
70
|
+
|
|
71
|
+
expect(parsed.model).toBeUndefined();
|
|
72
|
+
expect("model" in parsed).toBe(false);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("rejects a non-string model", () => {
|
|
76
|
+
expect(
|
|
77
|
+
AcpAgentConfigSchema.safeParse({ command: "claude-agent-acp", model: 3 })
|
|
78
|
+
.success,
|
|
79
|
+
).toBe(false);
|
|
80
|
+
});
|
|
81
|
+
});
|
package/src/config/acp-schema.ts
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
import { DEFAULT_ACP_AGENT_PROFILES } from "./acp-defaults.js";
|
|
4
|
+
|
|
5
|
+
export const AcpAgentConfigSchema = z
|
|
4
6
|
.object({
|
|
5
|
-
command: z
|
|
7
|
+
command: z
|
|
8
|
+
.string()
|
|
9
|
+
.optional()
|
|
10
|
+
.describe(
|
|
11
|
+
"Command to spawn the ACP agent process. Required unless the id has a bundled profile, whose command is inherited when this is unset",
|
|
12
|
+
),
|
|
6
13
|
args: z
|
|
7
14
|
.array(z.string())
|
|
8
15
|
.default([])
|
|
@@ -15,6 +22,12 @@ const AcpAgentConfigSchema = z
|
|
|
15
22
|
.record(z.string(), z.string())
|
|
16
23
|
.optional()
|
|
17
24
|
.describe("Environment variables set for the agent process"),
|
|
25
|
+
model: z
|
|
26
|
+
.string()
|
|
27
|
+
.optional()
|
|
28
|
+
.describe(
|
|
29
|
+
"Model sessions of this agent start on when the ask names none (an adapter-reported alias such as 'opus' or 'sonnet' for Claude, not an Assistant catalog id). Overrides the bundled profile's value; unset inherits it.",
|
|
30
|
+
),
|
|
18
31
|
})
|
|
19
32
|
.describe("Configuration for an individual ACP agent");
|
|
20
33
|
|
|
@@ -30,6 +43,21 @@ export const AcpConfigSchema = z
|
|
|
30
43
|
),
|
|
31
44
|
agents: z
|
|
32
45
|
.record(z.string(), AcpAgentConfigSchema)
|
|
46
|
+
.superRefine((agents, ctx) => {
|
|
47
|
+
for (const [id, agent] of Object.entries(agents)) {
|
|
48
|
+
if (
|
|
49
|
+
agent.command !== undefined ||
|
|
50
|
+
Object.hasOwn(DEFAULT_ACP_AGENT_PROFILES, id)
|
|
51
|
+
) {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
ctx.addIssue({
|
|
55
|
+
code: "custom",
|
|
56
|
+
path: [id, "command"],
|
|
57
|
+
message: `acp.agents.${id} needs a command: only the bundled ids (${Object.keys(DEFAULT_ACP_AGENT_PROFILES).join(", ")}) inherit one`,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
})
|
|
33
61
|
.default({})
|
|
34
62
|
.describe("Map of agent names to their configurations"),
|
|
35
63
|
})
|
|
@@ -23,6 +23,18 @@ Use `acp_spawn` to delegate a coding task to an external agent. The agent runs a
|
|
|
23
23
|
|
|
24
24
|
Users can refer to agents by natural names: "claude code", "codex cli", and "openai codex" all resolve to the canonical `claude` and `codex` ids (unless the user's config defines an agent literally keyed by that name, which always wins).
|
|
25
25
|
|
|
26
|
+
## Choosing a model
|
|
27
|
+
|
|
28
|
+
Claude runs on Opus unless the user names a model; every other agent starts on its own default.
|
|
29
|
+
|
|
30
|
+
When the user does name one, pass it as `model` on `acp_spawn`. Model names are the agent's own vocabulary, not Assistant model ids: an alias such as `default`, `sonnet`, `opus`, `haiku`, `fable`, or `opusplan` for Claude, or a full model id. Pass what the user said and let the agent resolve it.
|
|
31
|
+
|
|
32
|
+
The spawn result reports `requestedModel` and `effectiveModel`. Treat `effectiveModel` as authoritative for the top-level ACP session. Never infer that model from the task text or from a nested subagent. When `requestedModel` is non-null and differs from `effectiveModel`, state which model the session is actually running on and relay `modelWarning` when present.
|
|
33
|
+
|
|
34
|
+
If the agent refuses the model, or advertises no model selector, the spawn result says so: relay it in one sentence and carry on, because the session is live on the agent's own model.
|
|
35
|
+
|
|
36
|
+
A session runs on the model it started on, so a different model means a new `acp_spawn`. A standing default per agent lives in the config at `acp.agents.<id>.model`.
|
|
37
|
+
|
|
26
38
|
## When the user names Claude Code or Codex
|
|
27
39
|
|
|
28
40
|
If they name Claude Code or Codex without asking you to run it here (for example they say that tool will do the work), offer once, in one short sentence, that you can connect and run it in this conversation. Then continue with whatever they were doing.
|
|
@@ -35,21 +47,23 @@ If they name Claude Code or Codex without asking you to run it here (for example
|
|
|
35
47
|
|
|
36
48
|
ACP is always available - default profiles for `claude` and `codex` ship out-of-box, so no config edit is needed to start. First-time setup is just making the adapter binary available, then spawning:
|
|
37
49
|
|
|
38
|
-
1. Install the adapter binary if it's missing. This happens automatically: when `acp_spawn` finds the agent's binary missing from PATH, the assistant installs
|
|
50
|
+
1. Install the adapter binary if it's missing. This happens automatically: when `acp_spawn` finds the agent's binary missing from PATH, the assistant installs the pinned version via a sandboxed bun global install and proceeds in the same call (see "Automatic adapter availability" below).
|
|
39
51
|
|
|
40
52
|
2. Call `acp_spawn`. Do NOT run `vellum sleep && vellum wake` - that kills the conversation.
|
|
41
53
|
|
|
42
54
|
## Automatic adapter availability
|
|
43
55
|
|
|
44
|
-
When
|
|
56
|
+
When an agent's binary is missing from PATH, the assistant installs the adapter version it was built against via a sandboxed bun global install and then runs the real installed binary. The install runs in a fresh empty temporary directory (never the task's project directory), with known secrets stripped from the installer environment and the registry pinned to the public npm registry, so a malicious project directory cannot hijack package resolution or capture a token.
|
|
57
|
+
|
|
58
|
+
An adapter already on PATH is left alone, whoever installed it: the install runs only when preflight found no binary at all.
|
|
45
59
|
|
|
46
60
|
Only the allowlisted out-of-box packages are ever installed this way (`@agentclientprotocol/claude-agent-acp`, `@agentclientprotocol/codex-acp`); user-configured agents with custom commands are never installed automatically.
|
|
47
61
|
|
|
48
|
-
Manual installation is fallback guidance for unusual setups: bun unavailable, restricted global installs, or an auto-install failure (the failure reason is surfaced in the tool result).
|
|
62
|
+
Manual installation is fallback guidance for unusual setups: bun unavailable, restricted global installs, or an auto-install failure (the failure reason is surfaced in the tool result). Install the pinned version below rather than `@latest`, so the adapter matches what the assistant was built against.
|
|
49
63
|
|
|
50
64
|
```bash
|
|
51
|
-
bun add -g @agentclientprotocol/claude-agent-acp # claude
|
|
52
|
-
bun add -g @agentclientprotocol/codex-acp # codex
|
|
65
|
+
bun add -g @agentclientprotocol/claude-agent-acp@0.75.1 # claude
|
|
66
|
+
bun add -g @agentclientprotocol/codex-acp@1.10.0 # codex
|
|
53
67
|
```
|
|
54
68
|
|
|
55
69
|
## Claude setup
|
|
@@ -84,20 +98,14 @@ Do NOT put API keys (or any secret) in the workspace config file - secrets never
|
|
|
84
98
|
|
|
85
99
|
- Two agents are supported out-of-box: `claude` (via the `claude-agent-acp` adapter) and `codex` (via the `codex-acp` adapter).
|
|
86
100
|
- NEVER use `claude`, `claude -p`, `claude --acp`, or the bare `codex` CLI as the ACP `command`. Claude and Codex only speak the protocol through their dedicated `*-acp` adapters.
|
|
87
|
-
- Default profiles for both ship out-of-box. Users only need an `agents.<id>` entry in config if they want to override the defaults (e.g. point to a custom binary path or pass extra args/env). An
|
|
101
|
+
- Default profiles for both ship out-of-box. Users only need an `agents.<id>` entry in config if they want to override the defaults (e.g. point to a custom binary path or pass extra args/env). An entry that still runs the bundled adapter, whether it omits `command` or names the same binary by name or full path, inherits the `command`, `description` and `model` it leaves out, so a single-field change such as `acp.agents.claude.model` is all it takes. An entry that points the id at a different binary stands on its own, so it must spell out everything it needs, `command` included.
|
|
88
102
|
- NEVER change an existing ACP config to use a different command. If the config already has `claude-agent-acp` or `codex-acp`, leave it alone.
|
|
89
103
|
|
|
90
104
|
## Updating an adapter
|
|
91
105
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
```bash
|
|
95
|
-
bun add -g @agentclientprotocol/claude-agent-acp@latest
|
|
96
|
-
# or
|
|
97
|
-
bun add -g @agentclientprotocol/codex-acp@latest
|
|
98
|
-
```
|
|
106
|
+
Adapter upgrades ship with Assistant releases: the pinned version is what a missing adapter is installed at. An adapter already on PATH is never replaced, so a user who upgrades one themselves keeps that version.
|
|
99
107
|
|
|
100
|
-
Codex uses the adapter's bundled dependency by default, within the version range declared by the adapter. If `CODEX_PATH` selects a separate CLI, update that installation separately.
|
|
108
|
+
Codex uses the adapter's bundled dependency by default, within the version range declared by the adapter. If `CODEX_PATH` selects a separate CLI, update that installation separately.
|
|
101
109
|
|
|
102
110
|
## When to use acp_steer vs acp_spawn
|
|
103
111
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"tools": [
|
|
4
4
|
{
|
|
5
5
|
"name": "acp_spawn",
|
|
6
|
-
"description": "Spawn an external coding agent (e.g. Claude Code, Codex) via ACP to work on a task. Default profiles ship for `claude` (`claude-agent-acp`) and `codex` (`codex-acp`); the assistant resolves the agent id to the right binary. The agent runs as a subprocess and streams results back. Use this when you want to delegate a coding task to an external agent that has its own tools, file editing, and terminal access. If a default agent's binary is missing, the assistant installs it once via a sandboxed bun global install and proceeds in the same call; if that fails (e.g. bun unavailable), an actionable install hint is returned - do NOT alter `agents.<id>.command` to swap binaries.",
|
|
6
|
+
"description": "Spawn an external coding agent (e.g. Claude Code, Codex) via ACP to work on a task. Default profiles ship for `claude` (`claude-agent-acp`) and `codex` (`codex-acp`); the assistant resolves the agent id to the right binary. The result reports `requestedModel` and the adapter-reported `effectiveModel`; treat `effectiveModel` as authoritative for the top-level ACP session. The agent runs as a subprocess and streams results back. Use this when you want to delegate a coding task to an external agent that has its own tools, file editing, and terminal access. If a default agent's binary is missing, the assistant installs it once via a sandboxed bun global install and proceeds in the same call; if that fails (e.g. bun unavailable), an actionable install hint is returned - do NOT alter `agents.<id>.command` to swap binaries.",
|
|
7
7
|
"category": "orchestration",
|
|
8
8
|
"risk": "high",
|
|
9
9
|
"input_schema": {
|
|
@@ -20,6 +20,10 @@
|
|
|
20
20
|
"cwd": {
|
|
21
21
|
"type": "string",
|
|
22
22
|
"description": "Working directory for the agent. This determines where the agent runs and where its session is stored. Set this to the project/repo root the user wants the agent to work in. Defaults to current conversation's working directory."
|
|
23
|
+
},
|
|
24
|
+
"model": {
|
|
25
|
+
"type": "string",
|
|
26
|
+
"description": "Model the agent should run on, passed ONLY when the user named one (e.g. 'opus' for Claude). Left off, a Claude session runs on Opus and every other agent on its own default. If the agent refuses the value, or advertises no model selector, the result says so and the session runs on its own model."
|
|
23
27
|
}
|
|
24
28
|
},
|
|
25
29
|
"required": ["task"]
|