@vellumai/assistant 0.12.0 → 0.12.1-dev.202609141913.5e16607
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/docs/guardian-request-flow.md +16 -0
- package/openapi.yaml +292 -50
- 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 +4 -2
- package/src/__tests__/cu-unified-flow.test.ts +8 -2
- package/src/__tests__/document-sync-tags.test.ts +25 -0
- 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__/notification-vellum-adapter.test.ts +85 -2
- package/src/__tests__/outlook-messaging-provider.test.ts +6 -6
- package/src/__tests__/platform-callback-registration.test.ts +8 -16
- 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__/service-account-token.test.ts +247 -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/events/notification-intent.ts +2 -0
- 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__/bash.test.ts +53 -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/bash.help.ts +8 -13
- package/src/cli/commands/bash.ts +7 -25
- 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 +255 -0
- package/src/config/__tests__/platform-rehydration.test.ts +99 -9
- 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/SKILL.md +3 -3
- 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/schedule/SKILL.md +3 -4
- 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 +24 -12
- package/src/config/loader.ts +1 -0
- 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 +201 -0
- package/src/config/platform-rehydration.ts +11 -27
- 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/services.ts +8 -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/orphan-reaper.ts +1 -1
- 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/home/__tests__/feed-writer.test.ts +22 -0
- package/src/home/feed-writer.ts +11 -2
- 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__/guardian-feed-projection.test.ts +158 -2
- package/src/notifications/__tests__/home-feed-side-effect.test.ts +153 -3
- package/src/notifications/__tests__/schedule-result-producer.test.ts +99 -3
- package/src/notifications/adapters/macos.ts +3 -0
- 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/guardian-feed-projection.ts +125 -5
- package/src/notifications/home-feed-side-effect.ts +55 -7
- package/src/notifications/schedule-result-producer.ts +66 -22
- package/src/oauth/__tests__/seed-providers-managed.test.ts +54 -0
- package/src/oauth/byo-connection.test.ts +45 -0
- package/src/oauth/byo-connection.ts +36 -6
- package/src/oauth/connection-resolver.test.ts +73 -0
- package/src/oauth/connection-resolver.ts +95 -2
- package/src/oauth/seed-providers.ts +165 -0
- 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 +4 -2
- 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/inference/auth.ts +2 -3
- package/src/providers/inference/connection-availability.ts +27 -8
- package/src/providers/inference/resolve-auth.ts +22 -4
- package/src/providers/inference/service-account-token.ts +248 -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__/default-provider-routes.test.ts +2 -2
- 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 -2
- 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 +6 -20
- 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/__tests__/sanitized-bash.test.ts +57 -0
- package/src/tools/terminal/safe-env.ts +23 -3
- package/src/tools/terminal/sanitized-bash.ts +105 -0
- 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/runtime/routes/debug-bash-routes.ts +0 -173
- package/src/util/fs-watcher-error.ts +0 -36
- /package/src/{live-voice → calls}/progress-phrases.ts +0 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { afterEach, describe, expect, spyOn, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import type { CredentialRecord } from "@vellumai/service-contracts/credential-rpc";
|
|
4
|
+
|
|
5
|
+
import * as envRegistry from "../../config/env-registry.js";
|
|
6
|
+
import {
|
|
7
|
+
CesHttpRecordBackend,
|
|
8
|
+
createCesHttpRecordBackendIfConfigured,
|
|
9
|
+
} from "../ces-http-record-backend.js";
|
|
10
|
+
|
|
11
|
+
const ORIGINAL_URL = process.env.CES_CREDENTIAL_URL;
|
|
12
|
+
const ORIGINAL_TOKEN = process.env.CES_SERVICE_TOKEN;
|
|
13
|
+
|
|
14
|
+
function restoreCesHttpEnv(): void {
|
|
15
|
+
if (ORIGINAL_URL === undefined) {
|
|
16
|
+
delete process.env.CES_CREDENTIAL_URL;
|
|
17
|
+
} else {
|
|
18
|
+
process.env.CES_CREDENTIAL_URL = ORIGINAL_URL;
|
|
19
|
+
}
|
|
20
|
+
if (ORIGINAL_TOKEN === undefined) {
|
|
21
|
+
delete process.env.CES_SERVICE_TOKEN;
|
|
22
|
+
} else {
|
|
23
|
+
process.env.CES_SERVICE_TOKEN = ORIGINAL_TOKEN;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function fakeHttpClient(options: {
|
|
28
|
+
listRecords: () => Promise<{
|
|
29
|
+
records: Array<{ account: string; record: CredentialRecord }>;
|
|
30
|
+
unreachable: boolean;
|
|
31
|
+
}>;
|
|
32
|
+
}) {
|
|
33
|
+
return {
|
|
34
|
+
getRecord: async () => ({ record: undefined, unreachable: false }),
|
|
35
|
+
setRecord: async () => false,
|
|
36
|
+
deleteRecord: async () => "error" as const,
|
|
37
|
+
listRecords: options.listRecords,
|
|
38
|
+
bulkSetRecords: async (records: Array<{ account: string }>) =>
|
|
39
|
+
records.map((entry) => ({ account: entry.account, ok: false })),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
describe("CES HTTP credential record backend", () => {
|
|
44
|
+
afterEach(() => {
|
|
45
|
+
restoreCesHttpEnv();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("factory returns null when the assistant is not containerized", () => {
|
|
49
|
+
const spy = spyOn(envRegistry, "getIsContainerized").mockReturnValue(false);
|
|
50
|
+
process.env.CES_CREDENTIAL_URL = "http://ces-container:8090";
|
|
51
|
+
process.env.CES_SERVICE_TOKEN = "token-123";
|
|
52
|
+
try {
|
|
53
|
+
expect(createCesHttpRecordBackendIfConfigured()).toBeNull();
|
|
54
|
+
} finally {
|
|
55
|
+
spy.mockRestore();
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("factory returns null when CES_CREDENTIAL_URL is unset", () => {
|
|
60
|
+
const spy = spyOn(envRegistry, "getIsContainerized").mockReturnValue(true);
|
|
61
|
+
delete process.env.CES_CREDENTIAL_URL;
|
|
62
|
+
process.env.CES_SERVICE_TOKEN = "token-123";
|
|
63
|
+
try {
|
|
64
|
+
expect(createCesHttpRecordBackendIfConfigured()).toBeNull();
|
|
65
|
+
} finally {
|
|
66
|
+
spy.mockRestore();
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("factory returns null when CES_SERVICE_TOKEN is unset", () => {
|
|
71
|
+
const spy = spyOn(envRegistry, "getIsContainerized").mockReturnValue(true);
|
|
72
|
+
process.env.CES_CREDENTIAL_URL = "http://ces-container:8090";
|
|
73
|
+
delete process.env.CES_SERVICE_TOKEN;
|
|
74
|
+
try {
|
|
75
|
+
expect(createCesHttpRecordBackendIfConfigured()).toBeNull();
|
|
76
|
+
} finally {
|
|
77
|
+
spy.mockRestore();
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("factory returns a backend when containerized with CES HTTP env", () => {
|
|
82
|
+
const spy = spyOn(envRegistry, "getIsContainerized").mockReturnValue(true);
|
|
83
|
+
process.env.CES_CREDENTIAL_URL = "http://ces-container:8090";
|
|
84
|
+
process.env.CES_SERVICE_TOKEN = "token-123";
|
|
85
|
+
try {
|
|
86
|
+
const backend = createCesHttpRecordBackendIfConfigured();
|
|
87
|
+
expect(backend?.name).toBe("ces-http");
|
|
88
|
+
expect(backend?.isAvailable()).toBe(true);
|
|
89
|
+
} finally {
|
|
90
|
+
spy.mockRestore();
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("list returns records when HTTP reports a reachable catalog", async () => {
|
|
95
|
+
const record: CredentialRecord = {
|
|
96
|
+
credentialId: "cred-http",
|
|
97
|
+
service: "github",
|
|
98
|
+
field: "token",
|
|
99
|
+
allowedTools: ["bash"],
|
|
100
|
+
allowedDomains: [],
|
|
101
|
+
createdAt: 1,
|
|
102
|
+
updatedAt: 2,
|
|
103
|
+
};
|
|
104
|
+
const backend = new CesHttpRecordBackend(
|
|
105
|
+
fakeHttpClient({
|
|
106
|
+
listRecords: async () => ({
|
|
107
|
+
records: [{ account: "github:token", record }],
|
|
108
|
+
unreachable: false,
|
|
109
|
+
}),
|
|
110
|
+
}),
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
const listed = await backend.list();
|
|
114
|
+
expect(listed).toEqual([{ account: "github:token", record }]);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("list returns an empty catalog when HTTP is reachable and empty", async () => {
|
|
118
|
+
const backend = new CesHttpRecordBackend(
|
|
119
|
+
fakeHttpClient({
|
|
120
|
+
listRecords: async () => ({ records: [], unreachable: false }),
|
|
121
|
+
}),
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
const listed = await backend.list();
|
|
125
|
+
expect(listed).toEqual([]);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("list returns null when HTTP reports the vault unreachable", async () => {
|
|
129
|
+
const backend = new CesHttpRecordBackend(
|
|
130
|
+
fakeHttpClient({
|
|
131
|
+
listRecords: async () => ({ records: [], unreachable: true }),
|
|
132
|
+
}),
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
expect(await backend.list()).toBeNull();
|
|
136
|
+
});
|
|
137
|
+
});
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CES HTTP backend for non-secret credential records (identity + policy).
|
|
3
|
+
*
|
|
4
|
+
* Used when the CES RPC socket is down but the CES HTTP sidecar is
|
|
5
|
+
* reachable (containerized deployments with CES_CREDENTIAL_URL).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
type CesHttpCredentialClient,
|
|
10
|
+
createCesHttpCredentialClient,
|
|
11
|
+
} from "@vellumai/ces-client/http-credentials";
|
|
12
|
+
import type { CredentialRecord } from "@vellumai/service-contracts/credential-rpc";
|
|
13
|
+
|
|
14
|
+
import { getIsContainerized } from "../config/env-registry.js";
|
|
15
|
+
import { getLogger } from "../util/logger.js";
|
|
16
|
+
import type { CredentialRecordBackend } from "./ces-rpc-record-backend.js";
|
|
17
|
+
|
|
18
|
+
const log = getLogger("ces-http-record-backend");
|
|
19
|
+
|
|
20
|
+
type RecordHttpClient = Pick<
|
|
21
|
+
CesHttpCredentialClient,
|
|
22
|
+
| "getRecord"
|
|
23
|
+
| "setRecord"
|
|
24
|
+
| "deleteRecord"
|
|
25
|
+
| "listRecords"
|
|
26
|
+
| "bulkSetRecords"
|
|
27
|
+
>;
|
|
28
|
+
|
|
29
|
+
function resolveEnvHttpClient(): RecordHttpClient | undefined {
|
|
30
|
+
const baseUrl = process.env.CES_CREDENTIAL_URL?.trim();
|
|
31
|
+
const serviceToken = process.env.CES_SERVICE_TOKEN?.trim();
|
|
32
|
+
if (!baseUrl || !serviceToken) {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
return createCesHttpCredentialClient({ baseUrl, serviceToken }, log);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export class CesHttpRecordBackend implements CredentialRecordBackend {
|
|
39
|
+
readonly name = "ces-http";
|
|
40
|
+
|
|
41
|
+
constructor(private readonly client?: RecordHttpClient) {}
|
|
42
|
+
|
|
43
|
+
private resolveClient(): RecordHttpClient | undefined {
|
|
44
|
+
return this.client ?? resolveEnvHttpClient();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
isAvailable(): boolean {
|
|
48
|
+
return this.resolveClient() !== undefined;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async get(account: string): Promise<CredentialRecord | undefined> {
|
|
52
|
+
const client = this.resolveClient();
|
|
53
|
+
if (!client) {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
const result = await client.getRecord(account);
|
|
57
|
+
return result.record;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async set(account: string, record: CredentialRecord): Promise<boolean> {
|
|
61
|
+
const client = this.resolveClient();
|
|
62
|
+
if (!client) {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
return client.setRecord(account, record);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async delete(
|
|
69
|
+
account: string,
|
|
70
|
+
): Promise<"deleted" | "not-found" | "error"> {
|
|
71
|
+
const client = this.resolveClient();
|
|
72
|
+
if (!client) {
|
|
73
|
+
return "error";
|
|
74
|
+
}
|
|
75
|
+
return client.deleteRecord(account);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async list(): Promise<Array<{
|
|
79
|
+
account: string;
|
|
80
|
+
record: CredentialRecord;
|
|
81
|
+
}> | null> {
|
|
82
|
+
const client = this.resolveClient();
|
|
83
|
+
if (!client) {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
const result = await client.listRecords();
|
|
87
|
+
if (result.unreachable) {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
return result.records ?? [];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async bulkSet(
|
|
94
|
+
records: Array<{ account: string; record: CredentialRecord }>,
|
|
95
|
+
): Promise<Array<{ account: string; ok: boolean }>> {
|
|
96
|
+
const client = this.resolveClient();
|
|
97
|
+
if (!client) {
|
|
98
|
+
return records.map((entry) => ({ account: entry.account, ok: false }));
|
|
99
|
+
}
|
|
100
|
+
return client.bulkSetRecords(records);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* CES HTTP record backend when the assistant is containerized and the
|
|
106
|
+
* CES HTTP sidecar URL is configured. Returns null otherwise.
|
|
107
|
+
*/
|
|
108
|
+
export function createCesHttpRecordBackendIfConfigured(): CredentialRecordBackend | null {
|
|
109
|
+
if (!getIsContainerized()) {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
if (!process.env.CES_CREDENTIAL_URL?.trim()) {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
const backend = new CesHttpRecordBackend();
|
|
116
|
+
if (!backend.isAvailable()) {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
return backend;
|
|
120
|
+
}
|
|
@@ -11,6 +11,11 @@ import { getLogger } from "../util/logger.js";
|
|
|
11
11
|
const log = getLogger("ces-rpc-record-backend");
|
|
12
12
|
|
|
13
13
|
export interface CredentialRecordBackend {
|
|
14
|
+
/**
|
|
15
|
+
* Transport id. `"ces-rpc"` is the attached CES socket backend; live
|
|
16
|
+
* catalog listing fails over to CES HTTP when that transport is down.
|
|
17
|
+
*/
|
|
18
|
+
readonly name?: string;
|
|
14
19
|
isAvailable(): boolean;
|
|
15
20
|
get(account: string): Promise<CredentialRecord | undefined>;
|
|
16
21
|
set(account: string, record: CredentialRecord): Promise<boolean>;
|
|
@@ -29,6 +34,8 @@ export interface CredentialRecordBackend {
|
|
|
29
34
|
}
|
|
30
35
|
|
|
31
36
|
export class CesRpcRecordBackend implements CredentialRecordBackend {
|
|
37
|
+
readonly name = "ces-rpc";
|
|
38
|
+
|
|
32
39
|
constructor(private readonly client: CesClient) {}
|
|
33
40
|
|
|
34
41
|
isAvailable(): boolean {
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { mkdirSync, mkdtempSync, rmSync, utimesSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
5
|
+
|
|
6
|
+
import { readWorkspaceSkillMdSignature } from "../workspace-skill-md-signature.js";
|
|
7
|
+
|
|
8
|
+
const ROOT = mkdtempSync(join(tmpdir(), "skill-md-signature-"));
|
|
9
|
+
|
|
10
|
+
afterEach(() => {
|
|
11
|
+
rmSync(ROOT, { recursive: true, force: true });
|
|
12
|
+
mkdirSync(ROOT, { recursive: true });
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
function writeSkill(id: string, body = "body"): string {
|
|
16
|
+
const dir = join(ROOT, id);
|
|
17
|
+
mkdirSync(dir, { recursive: true });
|
|
18
|
+
const skillMd = join(dir, "SKILL.md");
|
|
19
|
+
writeFileSync(skillMd, `---\nname: ${id}\ndescription: ${id}\n---\n${body}\n`);
|
|
20
|
+
return skillMd;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe("readWorkspaceSkillMdSignature", () => {
|
|
24
|
+
test("empty or missing skills dir is the empty signature", () => {
|
|
25
|
+
expect(readWorkspaceSkillMdSignature(join(ROOT, "missing"))).toBe("");
|
|
26
|
+
mkdirSync(ROOT, { recursive: true });
|
|
27
|
+
expect(readWorkspaceSkillMdSignature(ROOT)).toBe("");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("includes SKILL.md mtimes and ignores TOOLS.json", () => {
|
|
31
|
+
const skillMd = writeSkill("alpha");
|
|
32
|
+
const first = readWorkspaceSkillMdSignature(ROOT);
|
|
33
|
+
expect(first).toMatch(/^alpha:\d+$/);
|
|
34
|
+
|
|
35
|
+
writeFileSync(join(ROOT, "alpha", "TOOLS.json"), '{"version":1,"tools":[]}');
|
|
36
|
+
expect(readWorkspaceSkillMdSignature(ROOT)).toBe(first);
|
|
37
|
+
|
|
38
|
+
const later = new Date(Date.now() + 5_000);
|
|
39
|
+
utimesSync(skillMd, later, later);
|
|
40
|
+
expect(readWorkspaceSkillMdSignature(ROOT)).not.toBe(first);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("add and remove skill directories change the signature", () => {
|
|
44
|
+
writeSkill("alpha");
|
|
45
|
+
const one = readWorkspaceSkillMdSignature(ROOT);
|
|
46
|
+
writeSkill("bravo");
|
|
47
|
+
const two = readWorkspaceSkillMdSignature(ROOT);
|
|
48
|
+
expect(two).not.toBe(one);
|
|
49
|
+
expect(two.split("\n")).toHaveLength(2);
|
|
50
|
+
|
|
51
|
+
rmSync(join(ROOT, "bravo"), { recursive: true, force: true });
|
|
52
|
+
expect(readWorkspaceSkillMdSignature(ROOT)).toBe(one);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("directories without SKILL.md are omitted", () => {
|
|
56
|
+
mkdirSync(join(ROOT, "not-a-skill"), { recursive: true });
|
|
57
|
+
writeFileSync(join(ROOT, "README.md"), "nope");
|
|
58
|
+
expect(readWorkspaceSkillMdSignature(ROOT)).toBe("");
|
|
59
|
+
});
|
|
60
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
import { getWorkspaceSkillsDir } from "../util/platform.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Cheap change detector for workspace skill capability cards: the sorted set
|
|
8
|
+
* of immediate skill directories that contain a top-level `SKILL.md`, each
|
|
9
|
+
* paired with that file's mtime. Capability cards are frontmatter fields from
|
|
10
|
+
* `SKILL.md`. Body files, `TOOLS.json`, scripts, and `references/` are not part
|
|
11
|
+
* of this signature.
|
|
12
|
+
*
|
|
13
|
+
* Listing the skills directory (not only statting known paths) is what makes
|
|
14
|
+
* install and delete visible: a new or removed skill directory changes the
|
|
15
|
+
* signature even when no previously known `SKILL.md` mtime moved.
|
|
16
|
+
*/
|
|
17
|
+
export function readWorkspaceSkillMdSignature(
|
|
18
|
+
skillsDir: string = getWorkspaceSkillsDir(),
|
|
19
|
+
): string {
|
|
20
|
+
if (!existsSync(skillsDir)) {
|
|
21
|
+
return "";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
let entries;
|
|
25
|
+
try {
|
|
26
|
+
entries = readdirSync(skillsDir, { withFileTypes: true });
|
|
27
|
+
} catch {
|
|
28
|
+
return "";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const parts: string[] = [];
|
|
32
|
+
for (const entry of entries) {
|
|
33
|
+
if (!entry.isDirectory() && !entry.isSymbolicLink()) {
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
const skillMdPath = join(skillsDir, entry.name, "SKILL.md");
|
|
37
|
+
try {
|
|
38
|
+
const st = statSync(skillMdPath);
|
|
39
|
+
if (!st.isFile()) {
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
parts.push(`${entry.name}:${Math.trunc(st.mtimeMs)}`);
|
|
43
|
+
} catch {
|
|
44
|
+
// No readable SKILL.md: not a catalog skill.
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
parts.sort();
|
|
49
|
+
return parts.join("\n");
|
|
50
|
+
}
|
|
@@ -33,7 +33,6 @@
|
|
|
33
33
|
import { readdir } from "node:fs/promises";
|
|
34
34
|
import { join } from "node:path";
|
|
35
35
|
|
|
36
|
-
import type { SkillSummary } from "../config/skills.js";
|
|
37
36
|
import type { TrustContext } from "../daemon/trust-context-types.js";
|
|
38
37
|
import type { TrustClass } from "../runtime/actor-trust-resolver.js";
|
|
39
38
|
import { truncate as truncateText } from "../util/truncate.js";
|
|
@@ -56,15 +55,6 @@ export interface AdvisorContextSources {
|
|
|
56
55
|
* from the catalog section, mirroring the `skill_load` gate.
|
|
57
56
|
*/
|
|
58
57
|
enabledPluginSet?: ReadonlySet<string> | null;
|
|
59
|
-
/**
|
|
60
|
-
* Pre-resolved skill catalog, typically the parent conversation's warm
|
|
61
|
-
* `skillProjectionCache.catalog`. Passing it keeps the synchronous on-disk
|
|
62
|
-
* catalog scan out of the consult path (and matches the catalog view the
|
|
63
|
-
* parent turn's tool projection used). When absent, the section falls back
|
|
64
|
-
* to a fresh `loadSkillCatalog()` scan, the same call every agent turn's
|
|
65
|
-
* projection already makes.
|
|
66
|
-
*/
|
|
67
|
-
skillCatalog?: readonly SkillSummary[];
|
|
68
58
|
}
|
|
69
59
|
|
|
70
60
|
/** Cap a block so the assembled context never balloons the consult prompt. */
|
|
@@ -114,7 +104,6 @@ async function buildToolsSection(
|
|
|
114
104
|
*/
|
|
115
105
|
async function buildSkillsSection(
|
|
116
106
|
enabledPluginSet: ReadonlySet<string> | null | undefined,
|
|
117
|
-
preResolvedCatalog: readonly SkillSummary[] | undefined,
|
|
118
107
|
): Promise<string | null> {
|
|
119
108
|
try {
|
|
120
109
|
const [
|
|
@@ -130,19 +119,17 @@ async function buildSkillsSection(
|
|
|
130
119
|
]);
|
|
131
120
|
const config = getConfig();
|
|
132
121
|
const pluginScope = enabledPluginSet ?? null;
|
|
133
|
-
const catalog =
|
|
134
|
-
(
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
},
|
|
145
|
-
);
|
|
122
|
+
const catalog = loadSkillCatalog().filter((skill) => {
|
|
123
|
+
if (
|
|
124
|
+
pluginScope !== null &&
|
|
125
|
+
skill.owner?.kind === "plugin" &&
|
|
126
|
+
!pluginScope.has(skill.owner.id)
|
|
127
|
+
) {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
const flagKey = skillFlagKey(skill);
|
|
131
|
+
return !flagKey || isAssistantFeatureFlagEnabled(flagKey, config);
|
|
132
|
+
});
|
|
146
133
|
if (catalog.length === 0) {
|
|
147
134
|
return null;
|
|
148
135
|
}
|
|
@@ -384,7 +371,7 @@ export async function buildAdvisorContext(
|
|
|
384
371
|
const sections = await Promise.all(
|
|
385
372
|
[
|
|
386
373
|
buildToolsSection(sources.allowedToolNames),
|
|
387
|
-
buildSkillsSection(sources.enabledPluginSet
|
|
374
|
+
buildSkillsSection(sources.enabledPluginSet),
|
|
388
375
|
buildWorkspaceSection(sources),
|
|
389
376
|
].map((section) => withSectionTimeout(section, sectionTimeoutMs)),
|
|
390
377
|
);
|
|
@@ -90,7 +90,9 @@ describe("executeAcpListAgents", () => {
|
|
|
90
90
|
const codex = parsed.agents.find((a: { id: string }) => a.id === "codex");
|
|
91
91
|
expect(codex.available).toBe(false);
|
|
92
92
|
expect(codex.unavailableReason).toBe("'codex-acp' is not on PATH");
|
|
93
|
-
expect(codex.setupHint).toBe(
|
|
93
|
+
expect(codex.setupHint).toBe(
|
|
94
|
+
"bun add -g @agentclientprotocol/codex-acp@1.10.0",
|
|
95
|
+
);
|
|
94
96
|
|
|
95
97
|
const claude = parsed.agents.find((a: { id: string }) => a.id === "claude");
|
|
96
98
|
expect(claude.available).toBe(true);
|
|
@@ -124,11 +124,22 @@ const spawnMock = mock(
|
|
|
124
124
|
_cwd: string,
|
|
125
125
|
_parentConversationId: string,
|
|
126
126
|
_sendToVellum: (msg: unknown) => void,
|
|
127
|
-
|
|
128
|
-
)
|
|
129
|
-
acpSessionId:
|
|
130
|
-
protocolSessionId:
|
|
131
|
-
|
|
127
|
+
_options?: { parentToolUseId?: string; model?: string },
|
|
128
|
+
): Promise<{
|
|
129
|
+
acpSessionId: string;
|
|
130
|
+
protocolSessionId: string;
|
|
131
|
+
requestedModel?: string;
|
|
132
|
+
effectiveModel?: string;
|
|
133
|
+
modelWarning?: string;
|
|
134
|
+
}> => {
|
|
135
|
+
const requestedModel = _options?.model?.trim() || undefined;
|
|
136
|
+
return {
|
|
137
|
+
acpSessionId: "acp-session-test",
|
|
138
|
+
protocolSessionId: "proto-session-test",
|
|
139
|
+
requestedModel,
|
|
140
|
+
effectiveModel: "opus",
|
|
141
|
+
};
|
|
142
|
+
},
|
|
132
143
|
);
|
|
133
144
|
|
|
134
145
|
// Spread the real module's exports so transitive importers that pull other
|
|
@@ -201,6 +212,8 @@ describe("executeAcpSpawn - happy path", () => {
|
|
|
201
212
|
const payload = JSON.parse(result.content);
|
|
202
213
|
expect(payload.acpSessionId).toBe("acp-session-test");
|
|
203
214
|
expect(payload.status).toBe("running");
|
|
215
|
+
expect(payload.requestedModel).toBeNull();
|
|
216
|
+
expect(payload.effectiveModel).toBe("opus");
|
|
204
217
|
// The real adapter binary is spawned (no `bun x` wrapper).
|
|
205
218
|
const agentConfigArg = spawnMock.mock.calls[0][1] as { command: string };
|
|
206
219
|
expect(agentConfigArg.command).toBe("claude-agent-acp");
|
|
@@ -214,8 +227,10 @@ describe("executeAcpSpawn - happy path", () => {
|
|
|
214
227
|
|
|
215
228
|
expect(result.isError).toBe(false);
|
|
216
229
|
expect(spawnMock).toHaveBeenCalledTimes(1);
|
|
217
|
-
//
|
|
218
|
-
expect(spawnMock.mock.calls[0][6]).
|
|
230
|
+
// The options bag is the 7th positional arg to spawn().
|
|
231
|
+
expect(spawnMock.mock.calls[0][6]).toEqual({
|
|
232
|
+
parentToolUseId: "toolu_abc123",
|
|
233
|
+
});
|
|
219
234
|
});
|
|
220
235
|
|
|
221
236
|
test("default-profile fallback when user config is empty", async () => {
|
|
@@ -272,7 +287,7 @@ describe("executeAcpSpawn — input validation", () => {
|
|
|
272
287
|
expect(result.isError).toBe(true);
|
|
273
288
|
expect(result.content).toContain("claude-agent-acp is not on PATH");
|
|
274
289
|
expect(result.content).toContain(
|
|
275
|
-
"bun add -g @agentclientprotocol/claude-agent-acp",
|
|
290
|
+
"bun add -g @agentclientprotocol/claude-agent-acp@0.75.1",
|
|
276
291
|
);
|
|
277
292
|
expect(execFileMock).not.toHaveBeenCalled();
|
|
278
293
|
expect(spawnMock).not.toHaveBeenCalled();
|
|
@@ -309,7 +324,7 @@ describe("executeAcpSpawn: sandboxed bun auto-install on missing binary", () =>
|
|
|
309
324
|
expect(spawnMock).toHaveBeenCalledTimes(1);
|
|
310
325
|
const payload = JSON.parse(result.content);
|
|
311
326
|
expect(payload.message).toContain(
|
|
312
|
-
"Installed @agentclientprotocol/claude-agent-acp automatically.",
|
|
327
|
+
"Installed @agentclientprotocol/claude-agent-acp@0.75.1 automatically.",
|
|
313
328
|
);
|
|
314
329
|
// The real binary was spawned with cwd = the project dir and token
|
|
315
330
|
// injected (trusted-binary config, no resolution at spawn).
|
|
@@ -330,7 +345,7 @@ describe("executeAcpSpawn: sandboxed bun auto-install on missing binary", () =>
|
|
|
330
345
|
expect(args).toEqual([
|
|
331
346
|
"add",
|
|
332
347
|
"--global",
|
|
333
|
-
"@agentclientprotocol/claude-agent-acp",
|
|
348
|
+
"@agentclientprotocol/claude-agent-acp@0.75.1",
|
|
334
349
|
]);
|
|
335
350
|
});
|
|
336
351
|
|
|
@@ -419,7 +434,7 @@ describe("executeAcpSpawn: sandboxed bun auto-install on missing binary", () =>
|
|
|
419
434
|
expect(result.isError).toBe(true);
|
|
420
435
|
expect(result.content).toContain("claude-agent-acp is not on PATH");
|
|
421
436
|
expect(result.content).toContain(
|
|
422
|
-
"bun add -g @agentclientprotocol/claude-agent-acp",
|
|
437
|
+
"bun add -g @agentclientprotocol/claude-agent-acp@0.75.1",
|
|
423
438
|
);
|
|
424
439
|
expect(result.content).toContain("auto-install failed");
|
|
425
440
|
expect(result.content).toContain("EACCES");
|
|
@@ -458,6 +473,126 @@ describe("executeAcpSpawn — per-agent resume hint", () => {
|
|
|
458
473
|
});
|
|
459
474
|
});
|
|
460
475
|
|
|
476
|
+
describe("executeAcpSpawn - model selection", () => {
|
|
477
|
+
test("threads the requested model into manager.spawn's options", async () => {
|
|
478
|
+
const result = await executeAcpSpawn(
|
|
479
|
+
{ agent: "claude", task: "do something", model: "opus" },
|
|
480
|
+
{ ...makeContext(), toolUseId: "toolu_model" },
|
|
481
|
+
);
|
|
482
|
+
|
|
483
|
+
expect(result.isError).toBe(false);
|
|
484
|
+
expect(spawnMock.mock.calls[0][6]).toEqual({
|
|
485
|
+
parentToolUseId: "toolu_model",
|
|
486
|
+
model: "opus",
|
|
487
|
+
});
|
|
488
|
+
const payload = JSON.parse(result.content);
|
|
489
|
+
expect(payload.requestedModel).toBe("opus");
|
|
490
|
+
expect(payload.effectiveModel).toBe("opus");
|
|
491
|
+
expect(payload.message).toContain(
|
|
492
|
+
'The top-level ACP session reports "opus" as its effective model.',
|
|
493
|
+
);
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
test("reports the manager-normalized requested model", async () => {
|
|
497
|
+
const result = await executeAcpSpawn(
|
|
498
|
+
{ agent: "claude", task: "do something", model: " opus " },
|
|
499
|
+
makeContext(),
|
|
500
|
+
);
|
|
501
|
+
|
|
502
|
+
expect(spawnMock.mock.calls[0][6]).toEqual({
|
|
503
|
+
parentToolUseId: undefined,
|
|
504
|
+
model: " opus ",
|
|
505
|
+
});
|
|
506
|
+
expect(JSON.parse(result.content).requestedModel).toBe("opus");
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
test("a null model is treated as omitted", async () => {
|
|
510
|
+
const result = await executeAcpSpawn(
|
|
511
|
+
{ agent: "claude", task: "do something", model: null },
|
|
512
|
+
makeContext(),
|
|
513
|
+
);
|
|
514
|
+
|
|
515
|
+
expect(result.isError).toBe(false);
|
|
516
|
+
const options = spawnMock.mock.calls[0][6] as { model?: string };
|
|
517
|
+
expect(options.model).toBeUndefined();
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
test("a modelWarning from the spawn is relayed in the result message", async () => {
|
|
521
|
+
spawnMock.mockImplementationOnce(async () => ({
|
|
522
|
+
acpSessionId: "acp-session-test",
|
|
523
|
+
protocolSessionId: "proto-session-test",
|
|
524
|
+
requestedModel: "nope",
|
|
525
|
+
effectiveModel: "opus",
|
|
526
|
+
modelWarning: "Unknown model: nope",
|
|
527
|
+
}));
|
|
528
|
+
|
|
529
|
+
const result = await executeAcpSpawn(
|
|
530
|
+
{ agent: "claude", task: "do something", model: "nope" },
|
|
531
|
+
makeContext(),
|
|
532
|
+
);
|
|
533
|
+
|
|
534
|
+
expect(result.isError).toBe(false);
|
|
535
|
+
const payload = JSON.parse(result.content);
|
|
536
|
+
expect(payload.message).toContain(
|
|
537
|
+
"The requested model was not applied: Unknown model: nope",
|
|
538
|
+
);
|
|
539
|
+
expect(payload.requestedModel).toBe("nope");
|
|
540
|
+
expect(payload.effectiveModel).toBe("opus");
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
test("the note relays a no-selector warning without calling it a refusal", async () => {
|
|
544
|
+
spawnMock.mockImplementationOnce(async () => ({
|
|
545
|
+
acpSessionId: "acp-session-test",
|
|
546
|
+
protocolSessionId: "proto-session-test",
|
|
547
|
+
requestedModel: "opus",
|
|
548
|
+
modelWarning:
|
|
549
|
+
'Agent "claude" does not support model selection, so the session is running on the agent\'s own model.',
|
|
550
|
+
}));
|
|
551
|
+
|
|
552
|
+
const result = await executeAcpSpawn(
|
|
553
|
+
{ agent: "claude", task: "do something", model: "opus" },
|
|
554
|
+
makeContext(),
|
|
555
|
+
);
|
|
556
|
+
|
|
557
|
+
const payload = JSON.parse(result.content);
|
|
558
|
+
expect(payload.message).toContain("does not support model selection");
|
|
559
|
+
expect(payload.message).not.toContain("refused");
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
test("no model note when the spawn reports no warning", async () => {
|
|
563
|
+
const result = await executeAcpSpawn(
|
|
564
|
+
{ agent: "claude", task: "do something", model: "opus" },
|
|
565
|
+
makeContext(),
|
|
566
|
+
);
|
|
567
|
+
|
|
568
|
+
expect(result.isError).toBe(false);
|
|
569
|
+
const payload = JSON.parse(result.content);
|
|
570
|
+
expect(payload.message).not.toContain(
|
|
571
|
+
"The requested model was not applied",
|
|
572
|
+
);
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
test("reports when the adapter does not identify the effective model", async () => {
|
|
576
|
+
spawnMock.mockImplementationOnce(async () => ({
|
|
577
|
+
acpSessionId: "acp-session-test",
|
|
578
|
+
protocolSessionId: "proto-session-test",
|
|
579
|
+
requestedModel: "sonnet",
|
|
580
|
+
}));
|
|
581
|
+
|
|
582
|
+
const result = await executeAcpSpawn(
|
|
583
|
+
{ agent: "claude", task: "do something", model: "sonnet" },
|
|
584
|
+
makeContext(),
|
|
585
|
+
);
|
|
586
|
+
|
|
587
|
+
const payload = JSON.parse(result.content);
|
|
588
|
+
expect(payload.requestedModel).toBe("sonnet");
|
|
589
|
+
expect(payload.effectiveModel).toBeNull();
|
|
590
|
+
expect(payload.message).toContain(
|
|
591
|
+
"The top-level ACP session did not report an effective model.",
|
|
592
|
+
);
|
|
593
|
+
});
|
|
594
|
+
});
|
|
595
|
+
|
|
461
596
|
// ---------------------------------------------------------------------------
|
|
462
597
|
// executeAcpSpawn — CLAUDE_CODE_OAUTH_TOKEN env injection + preflight
|
|
463
598
|
//
|