@vellumai/assistant 0.12.1 → 0.12.2-staging.2
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 +4 -0
- package/ARCHITECTURE.md +20 -21
- package/docs/architecture/turn-actor.md +15 -8
- package/docs/guardian-request-flow.md +16 -0
- package/node_modules/@vellumai/slack-text/src/index.ts +13 -8
- package/node_modules/@vellumai/slack-text/src/label-resolution-entities.test.ts +95 -0
- package/openapi.yaml +360 -51
- package/package.json +3 -1
- package/scripts/smoke-desktop-window-theme.ts +186 -0
- package/src/__tests__/always-loaded-tools-guard.test.ts +18 -10
- package/src/__tests__/anthropic-provider.test.ts +0 -63
- 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__/assistant-event-hub-machine-name.test.ts +6 -7
- package/src/__tests__/assistant-event-hub.test.ts +44 -0
- package/src/__tests__/computer-use-skill-manifest-regression.test.ts +1 -1
- package/src/__tests__/computer-use-skill-proxy-bridge.test.ts +5 -2
- package/src/__tests__/computer-use-tools.test.ts +19 -7
- 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-runtime-assembly.test.ts +28 -0
- 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-point-at-budget.test.ts +1 -0
- package/src/__tests__/conversation-surfaces-point-at-capability.test.ts +1 -0
- 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 +250 -3
- package/src/__tests__/credential-security-invariants.test.ts +5 -3
- package/src/__tests__/cu-unified-flow.test.ts +14 -4
- 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__/heartbeat-service.test.ts +56 -40
- 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-cu-proxy.test.ts +596 -0
- 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__/list-messages-camera-frame.test.ts +121 -0
- 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__/oauth-commands-routes.test.ts +168 -0
- package/src/__tests__/oauth-provider-profiles.test.ts +18 -0
- package/src/__tests__/oauth-provider-serializer.test.ts +22 -0
- package/src/__tests__/oauth-providers-routes.test.ts +1 -0
- package/src/__tests__/openai-provider.test.ts +1 -0
- 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 +14 -0
- package/src/__tests__/qdrant-manager.test.ts +132 -0
- package/src/__tests__/resolve-app.test.ts +135 -0
- package/src/__tests__/scaffold-managed-skill-tool.test.ts +229 -6
- package/src/__tests__/script-proxy-certs.test.ts +35 -0
- package/src/__tests__/secret-routes-acp-guard.test.ts +59 -1
- 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__/skills.test.ts +9 -4
- package/src/__tests__/source-fingerprint.test.ts +184 -0
- package/src/__tests__/subagent-tool-gate-mode.test.ts +70 -3
- package/src/__tests__/subagent-tools.test.ts +3 -2
- package/src/__tests__/telephony-tts-guard.test.ts +38 -3
- package/src/__tests__/test-support/computer-use-skill-harness.ts +3 -2
- package/src/__tests__/ui-channel-variants.test.ts +1 -56
- package/src/__tests__/unicode.test.ts +13 -0
- 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__/acp-claude-oauth.test.ts +257 -6
- package/src/acp/__tests__/acp-credentials.test.ts +13 -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__/claude-token-refresh.test.ts +257 -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__/prepare-agent-env.test.ts +60 -1
- 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/acp-claude-oauth.ts +328 -14
- package/src/acp/acp-credentials.ts +19 -0
- package/src/acp/agent-process.test.ts +4 -1
- package/src/acp/agent-process.ts +96 -10
- package/src/acp/auth-required.ts +50 -1
- package/src/acp/auto-install.test.ts +9 -7
- package/src/acp/claude-token-refresh.ts +150 -0
- 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/prepare-agent-env.ts +21 -6
- 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/agent/loop.ts +1 -0
- 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/events/user-message-echo.ts +7 -1
- package/src/api/index.ts +6 -0
- package/src/api/responses/conversation-message.ts +5 -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/avatar/identity-avatar.ts +7 -2
- 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 +120 -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 +56 -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 +155 -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 +92 -72
- package/src/calls/voice-triage-escalate.ts +178 -59
- 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/oauth/status.ts +5 -0
- 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/computer-use/SKILL.md +29 -2
- package/src/config/bundled-skills/computer-use/TOOLS.json +99 -0
- package/src/config/bundled-skills/computer-use/tools/computer-use-sequence.ts +12 -0
- package/src/config/bundled-skills/media-processing/services/preprocess.ts +2 -1
- package/src/config/bundled-skills/media-processing/services/processing-pipeline.ts +2 -1
- package/src/config/bundled-skills/media-processing/tools/generate-clip.ts +2 -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/playbooks/tools/playbook-create.ts +2 -1
- package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +2 -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 +6 -2
- package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +5 -2
- package/src/config/default-profile-catalog.ts +1 -1
- package/src/config/env.ts +2 -2
- package/src/config/feature-flag-registry.json +33 -13
- package/src/config/loader.ts +2 -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 +18 -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/credential-health/credential-health-service.ts +11 -17
- 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-error.ts +3 -2
- package/src/daemon/conversation-media-retry.ts +2 -1
- package/src/daemon/conversation-process.ts +35 -64
- package/src/daemon/conversation-runtime-assembly.ts +20 -7
- package/src/daemon/conversation-skill-tools.ts +13 -31
- package/src/daemon/conversation-store.ts +1 -1
- package/src/daemon/conversation-surfaces.ts +21 -10
- package/src/daemon/conversation-tool-setup.ts +45 -59
- package/src/daemon/conversation.ts +0 -3
- package/src/daemon/dictation-profile-store.ts +2 -1
- package/src/daemon/handlers/shared.ts +2 -1
- package/src/daemon/handlers/skills.ts +6 -3
- package/src/daemon/host-bash-proxy.ts +8 -12
- package/src/daemon/host-browser-proxy.ts +15 -1
- package/src/daemon/host-cu-proxy.ts +224 -19
- package/src/daemon/host-file-proxy.ts +8 -10
- package/src/daemon/host-proxy-preactivation.ts +2 -1
- 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/reaction-history-render.ts +2 -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 +122 -0
- package/src/desktop/desktop-chrome-policy.test.ts +79 -0
- package/src/desktop/desktop-chrome-policy.ts +47 -0
- package/src/desktop/desktop-chrome-session.ts +37 -0
- package/src/desktop/desktop-dependencies.test.ts +102 -0
- package/src/desktop/desktop-dependencies.ts +308 -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 +730 -0
- package/src/desktop/desktop-session-manager.ts +850 -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-drag.ts +177 -0
- package/src/desktop/desktop-window-manager-config.test.ts +230 -0
- package/src/desktop/desktop-window-manager-config.ts +238 -0
- package/src/desktop/desktop-window-theme.ts +170 -0
- package/src/heartbeat/heartbeat-service.ts +21 -3
- 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-agent-turn.test.ts +121 -0
- package/src/live-voice/__tests__/live-voice-events.test.ts +8 -7
- package/src/live-voice/__tests__/live-voice-metrics.test.ts +0 -3
- package/src/live-voice/__tests__/live-voice-progress.test.ts +94 -15
- package/src/live-voice/__tests__/live-voice-sight-frame-echo.test.ts +238 -0
- package/src/live-voice/__tests__/live-voice-vad.test.ts +2 -2
- package/src/live-voice/__tests__/protocol.test.ts +39 -0
- package/src/live-voice/__tests__/runtime-websocket-shell.test.ts +13 -69
- package/src/live-voice/__tests__/session-controls.test.ts +79 -0
- package/src/live-voice/live-voice-photo.ts +25 -6
- package/src/live-voice/live-voice-session.ts +297 -699
- package/src/live-voice/protocol.ts +60 -2
- package/src/live-voice/session-controls.ts +111 -0
- 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/AGENTS.md +2 -0
- package/src/oauth/__tests__/seed-providers-managed.test.ts +149 -0
- package/src/oauth/byo-connection.test.ts +105 -0
- package/src/oauth/byo-connection.ts +36 -6
- package/src/oauth/connection-resolver.test.ts +100 -0
- package/src/oauth/connection-resolver.ts +120 -3
- package/src/oauth/provider-serializer.ts +9 -0
- package/src/oauth/scope-utils.ts +24 -0
- package/src/oauth/seed-providers.ts +314 -27
- 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-queries.ts +2 -2
- package/src/persistence/conversation-title-placeholders.ts +23 -0
- package/src/persistence/conversation-title-service.ts +9 -26
- package/src/persistence/embeddings/qdrant-manager.ts +163 -18
- 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-prompt.test.ts +5 -0
- 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/context-search/agent-runner.ts +3 -2
- package/src/plugins/defaults/memory/context-search/format.ts +3 -2
- package/src/plugins/defaults/memory/context-search/sources/memory-v2.ts +6 -3
- package/src/plugins/defaults/memory/context-search/sources/workspace.ts +3 -2
- package/src/plugins/defaults/memory/graph/capability-seed.ts +6 -7
- 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/host-utils.ts +10 -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-prompt.ts +1 -1
- 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 +28 -2
- package/src/plugins/defaults/memory/substrate/__tests__/skill-content.test.ts +7 -9
- package/src/plugins/defaults/memory/substrate/page-index.ts +6 -1
- package/src/plugins/defaults/memory/substrate/skill-content.ts +2 -2
- package/src/plugins/defaults/memory/v1/graph/consolidation.ts +4 -2
- package/src/plugins/defaults/memory/v1/graph/extraction.ts +4 -2
- package/src/plugins/defaults/memory/v1/graph/retriever.ts +4 -1
- package/src/plugins/defaults/memory/v2/reranker.ts +6 -1
- package/src/plugins/defaults/memory/v3/__tests__/card.test.ts +24 -0
- package/src/plugins/defaults/memory/v3/__tests__/pool-select.test.ts +125 -0
- package/src/plugins/defaults/memory/v3/__tests__/sections.test.ts +26 -0
- package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +30 -0
- package/src/plugins/defaults/memory/v3/card.ts +3 -2
- package/src/plugins/defaults/memory/v3/pool-select.ts +5 -3
- package/src/plugins/defaults/memory/v3/sections.ts +9 -3
- package/src/plugins/defaults/memory/v3/shadow-plugin.ts +5 -2
- 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/__tests__/retry-callsite.test.ts +41 -0
- package/src/providers/__tests__/retry-outbound-sanitize.test.ts +176 -0
- package/src/providers/anthropic/client.ts +0 -47
- 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/__tests__/endpoint-probe.test.ts +42 -3
- package/src/providers/inference/auth.ts +2 -3
- package/src/providers/inference/connection-availability.ts +27 -8
- package/src/providers/inference/endpoint-probe.ts +10 -1
- 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-output-token-limit.test.ts +141 -0
- 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 +212 -79
- package/src/providers/openai/codex-models.ts +0 -4
- package/src/providers/openai/google-thought-signature.ts +320 -0
- package/src/providers/opencode/client.test.ts +73 -4
- package/src/providers/opencode/client.ts +60 -3
- package/src/providers/openrouter/client.ts +20 -0
- package/src/providers/outbound-request-sanitize.ts +96 -0
- package/src/providers/retry.ts +30 -19
- package/src/providers/unparseable-tool-args.ts +5 -1
- package/src/runtime/AGENTS.md +3 -1
- 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__/host-observe.test.ts +40 -1
- 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/agent-wake.ts +2 -1
- package/src/runtime/assistant-event-hub.ts +21 -3
- package/src/runtime/auth/same-actor.ts +38 -3
- package/src/runtime/channel-send.ts +316 -0
- package/src/runtime/host-observe.ts +11 -1
- package/src/runtime/http-server.ts +160 -151
- package/src/runtime/routes/__tests__/acp-claude-auth-routes.test.ts +16 -5
- package/src/runtime/routes/__tests__/acp-routes.test.ts +159 -4
- package/src/runtime/routes/__tests__/apps-refresh-route.test.ts +275 -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-claude-auth-routes.ts +11 -3
- 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 +90 -2
- 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/conversation-routes.ts +16 -3
- package/src/runtime/routes/credential-routes.ts +20 -14
- 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 +61 -4
- package/src/runtime/routes/events-routes.ts +4 -0
- package/src/runtime/routes/host-cu-routes.ts +6 -0
- 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/oauth-commands-routes.ts +50 -0
- package/src/runtime/routes/oauth-providers.ts +7 -0
- package/src/runtime/routes/platform-routes.ts +18 -15
- package/src/runtime/routes/workspace-routes.test.ts +55 -26
- package/src/runtime/routes/workspace-routes.ts +46 -63
- package/src/runtime/routes/workspace-tree-walk.test.ts +244 -0
- package/src/runtime/routes/workspace-tree-walk.ts +240 -0
- package/src/runtime/routes/workspace-utils.ts +17 -0
- 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/run-script.ts +2 -1
- package/src/schedule/schedule-store.ts +9 -2
- 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/security/untrusted-content.ts +3 -1
- 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/__tests__/side-effect-abort-guard.test.ts +1 -0
- 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/browser/browser-execution.ts +1 -1
- package/src/tools/browser/cdp-client/accessibility-snapshot.ts +9 -3
- package/src/tools/browser/cdp-client/cdp-inspect/discovery.ts +2 -1
- package/src/tools/browser/cdp-client/extension-cdp-client.ts +3 -2
- package/src/tools/browser/network-recorder.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 +121 -2
- package/src/tools/computer-use/skill-proxy-bridge.ts +2 -1
- package/src/tools/credentials/metadata-store.ts +100 -0
- package/src/tools/filesystem/search.ts +2 -1
- 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/network/web-search.ts +6 -1
- package/src/tools/policy-context.ts +6 -7
- package/src/tools/skills/scaffold-managed.ts +61 -3
- package/src/tools/subagent/spawn.ts +5 -8
- package/src/tools/terminal/__tests__/sanitized-bash.test.ts +57 -0
- package/src/tools/terminal/sanitized-bash.ts +105 -0
- package/src/tools/ui-surface/channel-variants.ts +10 -59
- package/src/tools/watch/watch-retro-report.ts +0 -8
- package/src/types/host-capabilities.ts +5 -3
- 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/util/unicode.ts +15 -1
- package/src/watch/watch-retro.ts +6 -8
- package/src/watch/watch-timeline.ts +2 -2
- package/src/workflows/run-manager.ts +2 -1
- package/src/workspace/git-porcelain.ts +66 -0
- package/src/workspace/git-service.ts +6 -55
- 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
|
@@ -8,18 +8,29 @@
|
|
|
8
8
|
* builds one against the manual redirect page and parses the `code#state`
|
|
9
9
|
* string the user copies back. Both converge on `storeAcpClaudeToken`, which
|
|
10
10
|
* writes the `acp/claude_oauth_token` vault field the ACP broker reads at
|
|
11
|
-
* spawn time and
|
|
11
|
+
* spawn time, persists any refresh token and expiry the exchange returned,
|
|
12
|
+
* and provisions the `acp_spawn` read policy.
|
|
13
|
+
*
|
|
14
|
+
* Refresh, expiry, and the bound-access digest are stored without credential
|
|
15
|
+
* metadata so the broker cannot hand them to a spawned agent. The policy for
|
|
16
|
+
* when to spend the refresh token lives in `claude-token-refresh.ts`.
|
|
12
17
|
*/
|
|
13
18
|
|
|
19
|
+
import { computeExpiresAt, isTokenExpired } from "@vellumai/credential-storage";
|
|
20
|
+
|
|
14
21
|
import { credentialKey } from "../security/credential-key.js";
|
|
15
22
|
import type { OAuth2Config } from "../security/oauth2.js";
|
|
16
23
|
import {
|
|
24
|
+
deleteSecureKeyAsync,
|
|
17
25
|
getSecureKeyAsync,
|
|
18
26
|
setSecureKeyAsync,
|
|
19
27
|
} from "../security/secure-keys.js";
|
|
20
28
|
import { getLogger } from "../util/logger.js";
|
|
21
29
|
import { claudeTokenDigest } from "./acp-auth-marker-store.js";
|
|
22
30
|
import {
|
|
31
|
+
ACP_OAUTH_ACCESS_DIGEST_FIELD,
|
|
32
|
+
ACP_OAUTH_EXPIRES_AT_FIELD,
|
|
33
|
+
ACP_OAUTH_REFRESH_TOKEN_FIELD,
|
|
23
34
|
ACP_OAUTH_TOKEN_FIELD,
|
|
24
35
|
ACP_SERVICE,
|
|
25
36
|
classifyAnthropicToken,
|
|
@@ -32,6 +43,22 @@ import {
|
|
|
32
43
|
|
|
33
44
|
const log = getLogger("acp:claude-oauth");
|
|
34
45
|
|
|
46
|
+
/**
|
|
47
|
+
* Serializes every Claude OAuth token-set write (Connect, renewal persist,
|
|
48
|
+
* companion clear) so a compare-and-write cannot interleave with another
|
|
49
|
+
* writer. The network refresh itself stays outside this queue.
|
|
50
|
+
*/
|
|
51
|
+
let tokenWriteQueue: Promise<unknown> = Promise.resolve();
|
|
52
|
+
|
|
53
|
+
function withAcpClaudeTokenWrite<T>(fn: () => Promise<T>): Promise<T> {
|
|
54
|
+
const run = tokenWriteQueue.then(fn, fn);
|
|
55
|
+
tokenWriteQueue = run.then(
|
|
56
|
+
() => undefined,
|
|
57
|
+
() => undefined,
|
|
58
|
+
);
|
|
59
|
+
return run;
|
|
60
|
+
}
|
|
61
|
+
|
|
35
62
|
/**
|
|
36
63
|
* Verified Claude Code public OAuth client. PKCE-only (no client secret);
|
|
37
64
|
* the single `user:inference` scope is what the ACP adapter's
|
|
@@ -137,20 +164,103 @@ export async function storedClaudeTokenDigest(): Promise<string | undefined> {
|
|
|
137
164
|
return token ? claudeTokenDigest(token) : undefined;
|
|
138
165
|
}
|
|
139
166
|
|
|
167
|
+
/** Tokens as returned by an authorization-code exchange or a refresh. */
|
|
168
|
+
export interface AcpClaudeTokens {
|
|
169
|
+
accessToken: string;
|
|
170
|
+
refreshToken?: string;
|
|
171
|
+
/** Lifetime in seconds, as the provider reports it. */
|
|
172
|
+
expiresIn?: number;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function vaultKey(field: string): string {
|
|
176
|
+
return credentialKey(ACP_SERVICE, field);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Write a companion field, or delete it when there is no value.
|
|
181
|
+
*
|
|
182
|
+
* Both outcomes are checked. The store signals failure by return value rather
|
|
183
|
+
* than by throwing (`setSecureKeyAsync` returns false, `deleteSecureKeyAsync`
|
|
184
|
+
* returns `"error"` on timeout), so ignoring them would let a backend hiccup
|
|
185
|
+
* report success while leaving the token-set fields out of sync: an access
|
|
186
|
+
* token with no way to renew it, or a new access token still paired with a
|
|
187
|
+
* previous connect's refresh token.
|
|
188
|
+
*/
|
|
189
|
+
async function writeOrClear(
|
|
190
|
+
field: string,
|
|
191
|
+
value: string | undefined,
|
|
192
|
+
): Promise<void> {
|
|
193
|
+
if (value) {
|
|
194
|
+
const stored = await setSecureKeyAsync(vaultKey(field), value);
|
|
195
|
+
if (!stored) {
|
|
196
|
+
throw new Error(
|
|
197
|
+
`Failed to store Claude OAuth ${field} in secure storage.`,
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
const result = await deleteSecureKeyAsync(vaultKey(field));
|
|
203
|
+
if (result === "error") {
|
|
204
|
+
throw new Error(
|
|
205
|
+
`Failed to clear Claude OAuth ${field} from secure storage.`,
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Write a Claude OAuth token set to the `acp/claude_oauth_*` vault fields.
|
|
212
|
+
* Throws when the backing store rejects any of the writes.
|
|
213
|
+
*
|
|
214
|
+
* The refresh token and expiry are written as a set with the access token:
|
|
215
|
+
* when the provider returns neither, any previously stored values are cleared
|
|
216
|
+
* rather than left behind. A stale refresh token paired with a newly connected
|
|
217
|
+
* access token would otherwise renew into the credential from a previous
|
|
218
|
+
* connect.
|
|
219
|
+
*
|
|
220
|
+
* Only the access-token field gets credential metadata. `serverUse` refuses
|
|
221
|
+
* any field without metadata, so the refresh token and expiry stay
|
|
222
|
+
* unreachable through the broker and cannot be injected into a spawned
|
|
223
|
+
* agent's env.
|
|
224
|
+
*/
|
|
225
|
+
async function writeConnectedTokenSet(tokens: AcpClaudeTokens): Promise<void> {
|
|
226
|
+
await withAcpClaudeTokenWrite(async () => {
|
|
227
|
+
const stored = await setSecureKeyAsync(
|
|
228
|
+
vaultKey(ACP_OAUTH_TOKEN_FIELD),
|
|
229
|
+
tokens.accessToken,
|
|
230
|
+
);
|
|
231
|
+
if (!stored) {
|
|
232
|
+
throw new Error("Failed to store Claude OAuth token in secure storage.");
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
await writeOrClear(ACP_OAUTH_REFRESH_TOKEN_FIELD, tokens.refreshToken);
|
|
236
|
+
const expiresAt = computeExpiresAt(tokens.expiresIn);
|
|
237
|
+
await writeOrClear(
|
|
238
|
+
ACP_OAUTH_EXPIRES_AT_FIELD,
|
|
239
|
+
expiresAt == null ? undefined : String(expiresAt),
|
|
240
|
+
);
|
|
241
|
+
const hasRenewal = Boolean(tokens.refreshToken) || expiresAt != null;
|
|
242
|
+
await writeOrClear(
|
|
243
|
+
ACP_OAUTH_ACCESS_DIGEST_FIELD,
|
|
244
|
+
hasRenewal ? claudeTokenDigest(tokens.accessToken) : undefined,
|
|
245
|
+
);
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
|
|
140
249
|
/**
|
|
141
|
-
* Store a captured Claude OAuth token in the `acp/
|
|
142
|
-
*
|
|
250
|
+
* Store a captured Claude OAuth token set in the `acp/claude_oauth_*` vault
|
|
251
|
+
* fields and provision the policy the broker applies at spawn time: grant the
|
|
143
252
|
* `acp_spawn` read and lift any domain restriction. Throws when the backing
|
|
144
253
|
* store rejects the write.
|
|
254
|
+
*
|
|
255
|
+
* A string argument is treated as an access-token-only write and clears any
|
|
256
|
+
* companion refresh or expiry fields so they cannot describe a previous token.
|
|
145
257
|
*/
|
|
146
|
-
export async function storeAcpClaudeToken(
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
throw new Error("Failed to store Claude OAuth token in secure storage.");
|
|
153
|
-
}
|
|
258
|
+
export async function storeAcpClaudeToken(
|
|
259
|
+
tokens: string | AcpClaudeTokens,
|
|
260
|
+
): Promise<void> {
|
|
261
|
+
const tokenSet: AcpClaudeTokens =
|
|
262
|
+
typeof tokens === "string" ? { accessToken: tokens } : tokens;
|
|
263
|
+
await writeConnectedTokenSet(tokenSet);
|
|
154
264
|
// Repair rather than merely ensure the policy: an explicit Connect is a
|
|
155
265
|
// deliberate opt-in to ACP, so this widens a credential the broker would
|
|
156
266
|
// otherwise keep denying the spawn read on, which would dead-loop the Connect
|
|
@@ -241,6 +351,11 @@ export async function notifyAcpConnectRetired(): Promise<void> {
|
|
|
241
351
|
* spawn-time broker read applies, so "connected" means precisely "the spawn
|
|
242
352
|
* would get this token". The token-shape guard stays here instead: the broker
|
|
243
353
|
* knows nothing about Anthropic token formats.
|
|
354
|
+
*
|
|
355
|
+
* An expired token with no refresh token is the same shape of problem, and is
|
|
356
|
+
* likewise NOT connected: the card has to stay up so the user can reconnect.
|
|
357
|
+
* An expired token that still has a refresh token IS connected, because
|
|
358
|
+
* `ensureFreshAcpClaudeToken` renews it on the next spawn.
|
|
244
359
|
*/
|
|
245
360
|
export async function hasAcpClaudeToken(): Promise<boolean> {
|
|
246
361
|
return (await usableStoredClaudeToken()) !== undefined;
|
|
@@ -257,9 +372,7 @@ export async function hasAcpClaudeToken(): Promise<boolean> {
|
|
|
257
372
|
* card and no working token.
|
|
258
373
|
*/
|
|
259
374
|
async function usableStoredClaudeToken(): Promise<string | undefined> {
|
|
260
|
-
const token = await getSecureKeyAsync(
|
|
261
|
-
credentialKey(ACP_SERVICE, ACP_OAUTH_TOKEN_FIELD),
|
|
262
|
-
);
|
|
375
|
+
const token = await getSecureKeyAsync(vaultKey(ACP_OAUTH_TOKEN_FIELD));
|
|
263
376
|
if (token == null || token.length === 0) {
|
|
264
377
|
return undefined;
|
|
265
378
|
}
|
|
@@ -274,6 +387,15 @@ async function usableStoredClaudeToken(): Promise<string | undefined> {
|
|
|
274
387
|
);
|
|
275
388
|
return undefined;
|
|
276
389
|
}
|
|
390
|
+
if (await isAcpClaudeTokenExpiring()) {
|
|
391
|
+
const boundDigest = await readBoundAccessDigest();
|
|
392
|
+
if (boundDigest && boundDigest !== claudeTokenDigest(token)) {
|
|
393
|
+
return token;
|
|
394
|
+
}
|
|
395
|
+
if (!(await hasAcpClaudeRefreshToken())) {
|
|
396
|
+
return undefined;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
277
399
|
return token;
|
|
278
400
|
}
|
|
279
401
|
|
|
@@ -326,3 +448,195 @@ export async function acpConnectCardStillWarranted(
|
|
|
326
448
|
await import("./acp-auth-marker-store.js");
|
|
327
449
|
return claudeCredentialRefused(resolved);
|
|
328
450
|
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Persist a token set obtained by renewing an existing credential, leaving
|
|
454
|
+
* the read policy exactly as it was.
|
|
455
|
+
*
|
|
456
|
+
* A renewal happens on a passive spawn with no user in the loop, so it must
|
|
457
|
+
* not widen what the credential is allowed to do. Granting `acp_spawn` here
|
|
458
|
+
* would let a background refresh restore a permission a user or admin had
|
|
459
|
+
* removed from `allowedTools`.
|
|
460
|
+
*
|
|
461
|
+
* A rotated refresh token is written when the provider returns one. When the
|
|
462
|
+
* response omits a refresh token, the stored refresh token is kept so a
|
|
463
|
+
* non-rotating grant does not lose its renewal material. A missing or
|
|
464
|
+
* non-positive `expiresIn` clears the recorded expiry so a stale past expiry
|
|
465
|
+
* cannot condemn the new access token.
|
|
466
|
+
*
|
|
467
|
+
* `expectedRefreshToken` is the refresh token this request spent. If the
|
|
468
|
+
* vault no longer holds that value, the access-token field is gone, or the
|
|
469
|
+
* stored access token is not the one this refresh material was written
|
|
470
|
+
* with, the persist is skipped so a newer token set is not overwritten and
|
|
471
|
+
* a deleted or pasted credential is not replaced. Returns whether the write
|
|
472
|
+
* happened.
|
|
473
|
+
*/
|
|
474
|
+
export async function persistRefreshedAcpClaudeTokens(
|
|
475
|
+
tokens: AcpClaudeTokens,
|
|
476
|
+
expectedRefreshToken: string,
|
|
477
|
+
): Promise<boolean> {
|
|
478
|
+
if (!tokens.accessToken) {
|
|
479
|
+
throw new Error("Refreshed Claude OAuth response had no access token.");
|
|
480
|
+
}
|
|
481
|
+
return withAcpClaudeTokenWrite(async () => {
|
|
482
|
+
const current = await readAcpClaudeRefreshToken();
|
|
483
|
+
if (current !== expectedRefreshToken) {
|
|
484
|
+
log.info(
|
|
485
|
+
"Skipping Claude OAuth refresh persist because the stored refresh token changed while the request was in flight",
|
|
486
|
+
);
|
|
487
|
+
return false;
|
|
488
|
+
}
|
|
489
|
+
const access = await getSecureKeyAsync(vaultKey(ACP_OAUTH_TOKEN_FIELD));
|
|
490
|
+
if (access == null || access.length === 0) {
|
|
491
|
+
log.info(
|
|
492
|
+
"Skipping Claude OAuth refresh persist because the access token is no longer stored",
|
|
493
|
+
);
|
|
494
|
+
return false;
|
|
495
|
+
}
|
|
496
|
+
if (!(await renewalIsBoundTo(access))) {
|
|
497
|
+
log.info(
|
|
498
|
+
"Skipping Claude OAuth refresh persist because the stored access token is not the one this refresh material was written with",
|
|
499
|
+
);
|
|
500
|
+
return false;
|
|
501
|
+
}
|
|
502
|
+
const stored = await setSecureKeyAsync(
|
|
503
|
+
vaultKey(ACP_OAUTH_TOKEN_FIELD),
|
|
504
|
+
tokens.accessToken,
|
|
505
|
+
);
|
|
506
|
+
if (!stored) {
|
|
507
|
+
throw new Error("Failed to store Claude OAuth token in secure storage.");
|
|
508
|
+
}
|
|
509
|
+
if (tokens.refreshToken) {
|
|
510
|
+
await writeOrClear(ACP_OAUTH_REFRESH_TOKEN_FIELD, tokens.refreshToken);
|
|
511
|
+
}
|
|
512
|
+
const expiresAt = computeExpiresAt(tokens.expiresIn);
|
|
513
|
+
await writeOrClear(
|
|
514
|
+
ACP_OAUTH_EXPIRES_AT_FIELD,
|
|
515
|
+
expiresAt == null ? undefined : String(expiresAt),
|
|
516
|
+
);
|
|
517
|
+
await writeOrClear(
|
|
518
|
+
ACP_OAUTH_ACCESS_DIGEST_FIELD,
|
|
519
|
+
claudeTokenDigest(tokens.accessToken),
|
|
520
|
+
);
|
|
521
|
+
return true;
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* The stored absolute expiry in epoch milliseconds, or null when unknown.
|
|
527
|
+
*
|
|
528
|
+
* Unknown is the norm for tokens connected before expiry was recorded, and is
|
|
529
|
+
* treated as "assume usable": we cannot tell fresh from expired, and guessing
|
|
530
|
+
* expired would refresh or discard a perfectly good token.
|
|
531
|
+
*/
|
|
532
|
+
async function readExpiresAt(): Promise<number | null> {
|
|
533
|
+
const raw = await getSecureKeyAsync(vaultKey(ACP_OAUTH_EXPIRES_AT_FIELD));
|
|
534
|
+
if (!raw) {
|
|
535
|
+
return null;
|
|
536
|
+
}
|
|
537
|
+
const parsed = Number(raw);
|
|
538
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* Whether the stored access token is past its recorded expiry, or close
|
|
543
|
+
* enough that it should be renewed before use. False when no expiry was
|
|
544
|
+
* recorded.
|
|
545
|
+
*/
|
|
546
|
+
export async function isAcpClaudeTokenExpiring(): Promise<boolean> {
|
|
547
|
+
return isTokenExpired(await readExpiresAt());
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/** The stored refresh token, or null when none was captured. */
|
|
551
|
+
export async function readAcpClaudeRefreshToken(): Promise<string | null> {
|
|
552
|
+
const token = await getSecureKeyAsync(vaultKey(ACP_OAUTH_REFRESH_TOKEN_FIELD));
|
|
553
|
+
return token != null && token.length > 0 ? token : null;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/** Whether renewal material is on hand, without revealing it. */
|
|
557
|
+
export async function hasAcpClaudeRefreshToken(): Promise<boolean> {
|
|
558
|
+
return (await readAcpClaudeRefreshToken()) != null;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* Whether the access-token vault field is present. Presence only: not
|
|
563
|
+
* whether the value is usable, expired, or readable by the spawn broker.
|
|
564
|
+
*
|
|
565
|
+
* Renewal spends a refresh token to replace this field. If the field is
|
|
566
|
+
* gone, there is no credential to renew.
|
|
567
|
+
*/
|
|
568
|
+
export async function hasStoredAcpClaudeAccessToken(): Promise<boolean> {
|
|
569
|
+
const token = await getSecureKeyAsync(vaultKey(ACP_OAUTH_TOKEN_FIELD));
|
|
570
|
+
return token != null && token.length > 0;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
async function readBoundAccessDigest(): Promise<string | null> {
|
|
574
|
+
const digest = await getSecureKeyAsync(
|
|
575
|
+
vaultKey(ACP_OAUTH_ACCESS_DIGEST_FIELD),
|
|
576
|
+
);
|
|
577
|
+
return digest != null && digest.length > 0 ? digest : null;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
async function renewalIsBoundTo(accessToken: string): Promise<boolean> {
|
|
581
|
+
const digest = await readBoundAccessDigest();
|
|
582
|
+
return digest != null && digest === claudeTokenDigest(accessToken);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
async function clearRenewalFields(): Promise<void> {
|
|
586
|
+
await writeOrClear(ACP_OAUTH_REFRESH_TOKEN_FIELD, undefined);
|
|
587
|
+
await writeOrClear(ACP_OAUTH_EXPIRES_AT_FIELD, undefined);
|
|
588
|
+
await writeOrClear(ACP_OAUTH_ACCESS_DIGEST_FIELD, undefined);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* Drop the refresh token, keeping the recorded expiry and the access token.
|
|
593
|
+
* Called when the provider rejects the refresh token.
|
|
594
|
+
*
|
|
595
|
+
* The expiry has to survive. `hasAcpClaudeToken()` reads "not connected" from
|
|
596
|
+
* the combination of an expired token and no refresh token. Clearing the
|
|
597
|
+
* expiry as well would make `readExpiresAt()` return null, which
|
|
598
|
+
* {@link isAcpClaudeTokenExpiring} treats as "assume usable", and the dead
|
|
599
|
+
* credential would report itself connected.
|
|
600
|
+
*/
|
|
601
|
+
export async function clearAcpClaudeRefreshToken(
|
|
602
|
+
expectedRefreshToken?: string,
|
|
603
|
+
): Promise<void> {
|
|
604
|
+
await withAcpClaudeTokenWrite(async () => {
|
|
605
|
+
if (expectedRefreshToken !== undefined) {
|
|
606
|
+
const current = await readAcpClaudeRefreshToken();
|
|
607
|
+
if (current !== expectedRefreshToken) {
|
|
608
|
+
log.info(
|
|
609
|
+
"Skipping Claude OAuth refresh-token clear because the stored refresh token changed while the request was in flight",
|
|
610
|
+
);
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
await writeOrClear(ACP_OAUTH_REFRESH_TOKEN_FIELD, undefined);
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* Drop refresh, expiry, and the bound-access digest when they do not describe
|
|
620
|
+
* the access token currently stored.
|
|
621
|
+
*
|
|
622
|
+
* Connect and persist write those fields as a set with the access token. A
|
|
623
|
+
* later paste or CLI write replaces only the access-token field. Spending the
|
|
624
|
+
* leftover refresh token would overwrite that replacement.
|
|
625
|
+
*/
|
|
626
|
+
export async function forgetAcpClaudeRenewalStateIfUnbound(): Promise<void> {
|
|
627
|
+
await withAcpClaudeTokenWrite(async () => {
|
|
628
|
+
const access = await getSecureKeyAsync(vaultKey(ACP_OAUTH_TOKEN_FIELD));
|
|
629
|
+
const refresh = await readAcpClaudeRefreshToken();
|
|
630
|
+
const expiresAt = await readExpiresAt();
|
|
631
|
+
const digest = await readBoundAccessDigest();
|
|
632
|
+
const hasRenewal = refresh != null || expiresAt != null || digest != null;
|
|
633
|
+
if (!hasRenewal) {
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
if (access && digest && digest === claudeTokenDigest(access)) {
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
await clearRenewalFields();
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
|
|
@@ -14,6 +14,25 @@
|
|
|
14
14
|
export const ACP_SERVICE = "acp";
|
|
15
15
|
export const ACP_OAUTH_TOKEN_FIELD = "claude_oauth_token";
|
|
16
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Refresh and expiry for {@link ACP_OAUTH_TOKEN_FIELD}, written by the Connect
|
|
19
|
+
* Claude exchange that stored that access token. Expiry is epoch milliseconds,
|
|
20
|
+
* matching `computeExpiresAt` and `isTokenExpired` in
|
|
21
|
+
* `@vellumai/credential-storage`.
|
|
22
|
+
*
|
|
23
|
+
* {@link ACP_OAUTH_ACCESS_DIGEST_FIELD} is a digest of the access token those
|
|
24
|
+
* fields were written with, so a later paste of a different access token cannot
|
|
25
|
+
* spend leftover refresh material.
|
|
26
|
+
*
|
|
27
|
+
* These fields are given no credential metadata. `credentialBroker.serverUse`
|
|
28
|
+
* refuses any field without metadata, so they stay unreachable through the
|
|
29
|
+
* broker and cannot be injected into a spawned agent's env. Only the access
|
|
30
|
+
* token crosses into the child process.
|
|
31
|
+
*/
|
|
32
|
+
export const ACP_OAUTH_REFRESH_TOKEN_FIELD = "claude_oauth_refresh_token";
|
|
33
|
+
export const ACP_OAUTH_EXPIRES_AT_FIELD = "claude_oauth_expires_at";
|
|
34
|
+
export const ACP_OAUTH_ACCESS_DIGEST_FIELD = "claude_oauth_access_digest";
|
|
35
|
+
|
|
17
36
|
/**
|
|
18
37
|
* True for the ACP vault field the "Connect Claude" flow owns
|
|
19
38
|
* (`acp/claude_oauth_token`). Used to route this credential to the inline
|
|
@@ -194,7 +194,10 @@ describe("AcpAgentProcess.createSession auth_required handling", () => {
|
|
|
194
194
|
},
|
|
195
195
|
});
|
|
196
196
|
|
|
197
|
-
await expect(proc.createSession("/tmp")).resolves.
|
|
197
|
+
await expect(proc.createSession("/tmp")).resolves.toEqual({
|
|
198
|
+
sessionId: "session-1",
|
|
199
|
+
configOptions: [],
|
|
200
|
+
});
|
|
198
201
|
expect(authenticateCalls()).toBe(1);
|
|
199
202
|
});
|
|
200
203
|
|
package/src/acp/agent-process.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { type ChildProcess, spawn } from "node:child_process";
|
|
9
|
+
import { basename } from "node:path";
|
|
9
10
|
import { Readable, Writable } from "node:stream";
|
|
10
11
|
|
|
11
12
|
import type {
|
|
@@ -14,14 +15,25 @@ import type {
|
|
|
14
15
|
AuthMethodEnvVar,
|
|
15
16
|
Client,
|
|
16
17
|
InitializeResponse,
|
|
18
|
+
LoadSessionResponse,
|
|
17
19
|
NewSessionResponse,
|
|
18
20
|
PromptResponse,
|
|
21
|
+
ResumeSessionResponse,
|
|
22
|
+
SessionConfigOption,
|
|
23
|
+
SetSessionConfigOptionRequest,
|
|
19
24
|
} from "@agentclientprotocol/sdk";
|
|
20
25
|
import * as acp from "@agentclientprotocol/sdk";
|
|
21
26
|
|
|
22
27
|
import { getLogger } from "../util/logger.js";
|
|
23
|
-
import {
|
|
24
|
-
import
|
|
28
|
+
import { safeStringSlice } from "../util/unicode.js";
|
|
29
|
+
import {
|
|
30
|
+
AcpAuthRequiredError,
|
|
31
|
+
CLAUDE_ACP_COMMAND,
|
|
32
|
+
isAcpAuthRequired,
|
|
33
|
+
isClaudeAuthFailureMessage,
|
|
34
|
+
requestErrorReason,
|
|
35
|
+
} from "./auth-required.js";
|
|
36
|
+
import { type AcpAgentConfig, AcpConfigOptionRefusedError } from "./types.js";
|
|
25
37
|
|
|
26
38
|
const log = getLogger("acp");
|
|
27
39
|
|
|
@@ -32,6 +44,16 @@ const log = getLogger("acp");
|
|
|
32
44
|
*/
|
|
33
45
|
const STDERR_RETENTION_BYTES = 4096;
|
|
34
46
|
|
|
47
|
+
/**
|
|
48
|
+
* Normalizes the SDK's optional-and-nullable config-option field to an array,
|
|
49
|
+
* so every session call hands its caller the same shape.
|
|
50
|
+
*/
|
|
51
|
+
function normalizeConfigOptions(
|
|
52
|
+
configOptions: SessionConfigOption[] | null | undefined,
|
|
53
|
+
): SessionConfigOption[] {
|
|
54
|
+
return configOptions ?? [];
|
|
55
|
+
}
|
|
56
|
+
|
|
35
57
|
function isEnvVarMethod(
|
|
36
58
|
method: AuthMethod,
|
|
37
59
|
): method is AuthMethodEnvVar & { type: "env_var" } {
|
|
@@ -142,7 +164,7 @@ export class AcpAgentProcess {
|
|
|
142
164
|
// deriveFailureError reads from the end, so dropping the head is lossless.
|
|
143
165
|
const line =
|
|
144
166
|
text.length > STDERR_RETENTION_BYTES
|
|
145
|
-
? text.
|
|
167
|
+
? safeStringSlice(text, text.length - STDERR_RETENTION_BYTES)
|
|
146
168
|
: text;
|
|
147
169
|
this.stderrSeq += 1;
|
|
148
170
|
this.stderrRing.push({ seq: this.stderrSeq, text: line });
|
|
@@ -355,16 +377,21 @@ export class AcpAgentProcess {
|
|
|
355
377
|
|
|
356
378
|
/**
|
|
357
379
|
* Creates a new ACP session in the specified working directory.
|
|
358
|
-
* Returns the session ID.
|
|
380
|
+
* Returns the session ID and the config options the agent reported.
|
|
359
381
|
*/
|
|
360
|
-
async createSession(
|
|
382
|
+
async createSession(
|
|
383
|
+
cwd: string,
|
|
384
|
+
): Promise<{ sessionId: string; configOptions: SessionConfigOption[] }> {
|
|
361
385
|
log.info({ agentId: this.agentId, cwd }, "Creating ACP session");
|
|
362
386
|
|
|
363
387
|
const result: NewSessionResponse = await this.withAuthRetry(() =>
|
|
364
388
|
this.requireConnection().newSession({ cwd, mcpServers: [] }),
|
|
365
389
|
);
|
|
366
390
|
|
|
367
|
-
return
|
|
391
|
+
return {
|
|
392
|
+
sessionId: result.sessionId,
|
|
393
|
+
configOptions: normalizeConfigOptions(result.configOptions),
|
|
394
|
+
};
|
|
368
395
|
}
|
|
369
396
|
|
|
370
397
|
/**
|
|
@@ -375,12 +402,17 @@ export class AcpAgentProcess {
|
|
|
375
402
|
* callers should suppress forwarding of those replayed updates (see
|
|
376
403
|
* VellumAcpClientHandler.beginReplaySuppression).
|
|
377
404
|
*/
|
|
378
|
-
async loadSession(
|
|
405
|
+
async loadSession(
|
|
406
|
+
sessionId: string,
|
|
407
|
+
cwd: string,
|
|
408
|
+
): Promise<{ configOptions: SessionConfigOption[] }> {
|
|
379
409
|
log.info({ agentId: this.agentId, sessionId, cwd }, "Loading ACP session");
|
|
380
410
|
|
|
381
|
-
await this.withAuthRetry(() =>
|
|
411
|
+
const result: LoadSessionResponse = await this.withAuthRetry(() =>
|
|
382
412
|
this.requireConnection().loadSession({ sessionId, cwd, mcpServers: [] }),
|
|
383
413
|
);
|
|
414
|
+
|
|
415
|
+
return { configOptions: normalizeConfigOptions(result.configOptions) };
|
|
384
416
|
}
|
|
385
417
|
|
|
386
418
|
/**
|
|
@@ -390,16 +422,70 @@ export class AcpAgentProcess {
|
|
|
390
422
|
* preferred when the agent advertises the capability
|
|
391
423
|
* (see supportsSessionResume).
|
|
392
424
|
*/
|
|
393
|
-
async resumeSession(
|
|
425
|
+
async resumeSession(
|
|
426
|
+
sessionId: string,
|
|
427
|
+
cwd: string,
|
|
428
|
+
): Promise<{ configOptions: SessionConfigOption[] }> {
|
|
394
429
|
log.info({ agentId: this.agentId, sessionId, cwd }, "Resuming ACP session");
|
|
395
430
|
|
|
396
|
-
await this.withAuthRetry(() =>
|
|
431
|
+
const result: ResumeSessionResponse = await this.withAuthRetry(() =>
|
|
397
432
|
this.requireConnection().resumeSession({
|
|
398
433
|
sessionId,
|
|
399
434
|
cwd,
|
|
400
435
|
mcpServers: [],
|
|
401
436
|
}),
|
|
402
437
|
);
|
|
438
|
+
|
|
439
|
+
return { configOptions: normalizeConfigOptions(result.configOptions) };
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Sets one session config option (e.g. the model selector) via
|
|
444
|
+
* `session/set_config_option`. The agent answers with the full refreshed
|
|
445
|
+
* option set, which is returned to the caller.
|
|
446
|
+
*
|
|
447
|
+
* A boolean value sends the `type: "boolean"` request variant; a string
|
|
448
|
+
* sends the value-id variant.
|
|
449
|
+
*/
|
|
450
|
+
async setConfigOption(
|
|
451
|
+
sessionId: string,
|
|
452
|
+
configId: string,
|
|
453
|
+
value: string | boolean,
|
|
454
|
+
): Promise<SessionConfigOption[]> {
|
|
455
|
+
log.info(
|
|
456
|
+
{ agentId: this.agentId, sessionId, configId, value },
|
|
457
|
+
"Setting ACP session config option",
|
|
458
|
+
);
|
|
459
|
+
|
|
460
|
+
const request: SetSessionConfigOptionRequest =
|
|
461
|
+
typeof value === "boolean"
|
|
462
|
+
? { sessionId, configId, type: "boolean", value }
|
|
463
|
+
: { sessionId, configId, value };
|
|
464
|
+
|
|
465
|
+
const response = await this.withAuthRetry(async () => {
|
|
466
|
+
try {
|
|
467
|
+
return await this.requireConnection().setSessionConfigOption(request);
|
|
468
|
+
} catch (err) {
|
|
469
|
+
// Wrapped here, at the request itself, so `withAuthRetry` still sees
|
|
470
|
+
// an auth_required answer and a caller cannot confuse the adapter's
|
|
471
|
+
// refusal with a transport or authentication failure. Claude's
|
|
472
|
+
// expired-token failure travels as a generic error whose reason is
|
|
473
|
+
// the only signal, so under the Claude adapter it is left for the
|
|
474
|
+
// caller to classify.
|
|
475
|
+
if (err instanceof acp.RequestError && !isAcpAuthRequired(err)) {
|
|
476
|
+
const reason = requestErrorReason(err);
|
|
477
|
+
const claudeAuthFailure =
|
|
478
|
+
basename(this.config.command) === CLAUDE_ACP_COMMAND &&
|
|
479
|
+
isClaudeAuthFailureMessage(reason);
|
|
480
|
+
if (!claudeAuthFailure) {
|
|
481
|
+
throw new AcpConfigOptionRefusedError(reason);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
throw err;
|
|
485
|
+
}
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
return normalizeConfigOptions(response.configOptions);
|
|
403
489
|
}
|
|
404
490
|
|
|
405
491
|
/**
|
package/src/acp/auth-required.ts
CHANGED
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
* 2. Credentials present but rejected (the expired/revoked case): the CLI
|
|
9
9
|
* surfaces the API 401 as an error message it authors, and the adapter
|
|
10
10
|
* relays it as a generic -32603 internal error, NOT as `auth_required`.
|
|
11
|
-
* {@link isClaudeAuthFailureMessage}
|
|
11
|
+
* {@link isClaudeAuthFailureMessage}, over the text
|
|
12
|
+
* {@link requestErrorReason} decodes out of the rejection.
|
|
12
13
|
*
|
|
13
14
|
* A positive detection becomes {@link ACP_CLAUDE_AUTH_REQUIRED_CODE} on the
|
|
14
15
|
* `acp_auth_required` event, which the client turns into the inline
|
|
@@ -81,6 +82,54 @@ export function isClaudeAuthFailureMessage(
|
|
|
81
82
|
);
|
|
82
83
|
}
|
|
83
84
|
|
|
85
|
+
/** Messages the SDK's `RequestError` factories build when given no text. */
|
|
86
|
+
const GENERIC_RPC_MESSAGES: ReadonlySet<string> = new Set([
|
|
87
|
+
"Parse error",
|
|
88
|
+
"Invalid request",
|
|
89
|
+
"Invalid params",
|
|
90
|
+
"Internal error",
|
|
91
|
+
"Request cancelled",
|
|
92
|
+
"Authentication required",
|
|
93
|
+
"Resource not found",
|
|
94
|
+
]);
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* The sentence behind a JSON-RPC rejection. An adapter that throws a plain
|
|
98
|
+
* Error reaches the client as a bare "Internal error" whose real text the
|
|
99
|
+
* agent-side SDK moved into `data.details`. An adapter that raises
|
|
100
|
+
* `RequestError.internalError(data, text)` keeps its words in the message and
|
|
101
|
+
* only supplemental context in `data`, so a specific message wins. Behind a
|
|
102
|
+
* generic message such as "Invalid params", `data.details` or the serialized
|
|
103
|
+
* payload supplies the reason instead. Duck-typed like
|
|
104
|
+
* {@link isAcpAuthRequired}:
|
|
105
|
+
* `message` is read as a property, so a rejection that arrives as a plain
|
|
106
|
+
* object off the wire decodes the same as an `Error` instance.
|
|
107
|
+
*/
|
|
108
|
+
export function requestErrorReason(err: unknown): string {
|
|
109
|
+
if (typeof err !== "object" || err === null) {
|
|
110
|
+
return String(err);
|
|
111
|
+
}
|
|
112
|
+
const { data, message } = err as { data?: unknown; message?: unknown };
|
|
113
|
+
const own = typeof message === "string" ? message : String(err);
|
|
114
|
+
if (data == null) {
|
|
115
|
+
return own;
|
|
116
|
+
}
|
|
117
|
+
if (
|
|
118
|
+
typeof message === "string" &&
|
|
119
|
+
message.length > 0 &&
|
|
120
|
+
!GENERIC_RPC_MESSAGES.has(message)
|
|
121
|
+
) {
|
|
122
|
+
return message;
|
|
123
|
+
}
|
|
124
|
+
const details = (data as { details?: unknown }).details;
|
|
125
|
+
if (typeof details === "string" && details.length > 0) {
|
|
126
|
+
return details;
|
|
127
|
+
}
|
|
128
|
+
// JSON.stringify answers undefined for a value it cannot represent.
|
|
129
|
+
const serialized: string | undefined = JSON.stringify(data);
|
|
130
|
+
return serialized ?? own;
|
|
131
|
+
}
|
|
132
|
+
|
|
84
133
|
/** The adapter whose auth failures the Connect Claude flow can repair. */
|
|
85
134
|
export const CLAUDE_ACP_COMMAND = "claude-agent-acp";
|
|
86
135
|
|