@vellumai/assistant 0.11.3 → 0.11.4-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/ARCHITECTURE.md +11 -6
- package/docs/architecture/memory.md +11 -0
- package/docs/architecture/turn-actor.md +70 -0
- package/docs/flux-turn-detection-spike.md +243 -0
- package/docs/stt-provider-onboarding.md +3 -1
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/channels.ts +11 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/channels.ts +11 -0
- package/node_modules/@vellumai/gateway-client/src/admission-policy-contract.ts +34 -0
- package/node_modules/@vellumai/gateway-client/src/index.ts +2 -0
- package/node_modules/@vellumai/service-contracts/src/channels.ts +11 -0
- package/openapi.yaml +139 -37
- package/package.json +1 -1
- package/scripts/voice-ttft-spike.ts +3 -3
- package/src/__tests__/app-compiler.test.ts +38 -3
- package/src/__tests__/attachments-store.test.ts +21 -12
- package/src/__tests__/byok-default-profile-ensure.test.ts +17 -0
- package/src/__tests__/call-setup-flow-name-capture.test.ts +0 -1
- package/src/__tests__/call-site-routing-provider.test.ts +1 -1
- package/src/__tests__/channel-availability-routes.test.ts +14 -1
- package/src/__tests__/channel-capabilities-dedupe.test.ts +214 -0
- package/src/__tests__/channel-delivery-store.test.ts +14 -14
- package/src/__tests__/config-loader-backfill.test.ts +3 -3
- package/src/__tests__/config-schema.test.ts +25 -10
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +8 -11
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +8 -11
- package/src/__tests__/conversation-agent-loop.test.ts +28 -20
- package/src/__tests__/conversation-attention-store.test.ts +63 -0
- package/src/__tests__/conversation-delete-schedule-cleanup.test.ts +0 -4
- package/src/__tests__/conversation-fork-crud.test.ts +69 -0
- package/src/__tests__/conversation-fork-referential.test.ts +67 -0
- package/src/__tests__/conversation-fork-retrospective.test.ts +24 -0
- package/src/__tests__/conversation-notifiers-provenance.test.ts +59 -0
- package/src/__tests__/conversation-queue.test.ts +177 -4
- package/src/__tests__/conversation-runtime-assembly.test.ts +134 -102
- package/src/__tests__/conversation-runtime-workspace.test.ts +14 -10
- package/src/__tests__/credential-prompt-route.test.ts +7 -10
- package/src/__tests__/custom-profile-ensure.test.ts +5 -1
- package/src/__tests__/discord-access-request-privacy.test.ts +5 -1
- package/src/__tests__/discord-requester-notice-privacy.test.ts +3 -3
- package/src/__tests__/document-append-idempotency.test.ts +233 -0
- package/src/__tests__/edit-propagation.test.ts +0 -7
- package/src/__tests__/helpers/mock-actor-context.ts +49 -0
- package/src/__tests__/helpers/mock-conversation.ts +13 -1
- package/src/__tests__/injector-chain.test.ts +63 -41
- package/src/__tests__/injector-disk-pressure.test.ts +11 -23
- package/src/__tests__/llm-context-resolution.test.ts +73 -1
- package/src/__tests__/llm-schema.test.ts +5 -2
- package/src/__tests__/mcp-list-plugin-servers.test.ts +250 -0
- package/src/__tests__/memory-retrieval-hook.test.ts +6 -5
- package/src/__tests__/messages-read-boundary-guard.test.ts +134 -0
- package/src/__tests__/mtime-cache.test.ts +1 -1
- package/src/__tests__/non-member-access-request.test.ts +0 -20
- package/src/__tests__/outbound-slack-persistence.test.ts +40 -1
- package/src/__tests__/plugin-import-boundary-guard.test.ts +5 -0
- package/src/__tests__/plugin-secret-pattern-contribution.test.ts +1 -1
- package/src/__tests__/post-compaction-reinjection-idempotency.test.ts +14 -7
- package/src/__tests__/provider-commit-message-generator.test.ts +20 -0
- package/src/__tests__/run-conversation-turn-persistence.test.ts +434 -105
- package/src/__tests__/scoped-approval-grants.test.ts +11 -6
- package/src/__tests__/secret-ingress-channel.test.ts +0 -1
- package/src/__tests__/skills.test.ts +32 -0
- package/src/__tests__/slack-edit-ordering-characterization.test.ts +0 -1
- package/src/__tests__/subagent-call-site-routing.test.ts +31 -19
- package/src/__tests__/subagent-spawn-and-await.test.ts +14 -10
- package/src/__tests__/turn-events-store.test.ts +43 -0
- package/src/__tests__/ui-shape-teaching.test.ts +33 -0
- package/src/__tests__/ui-voice-picker-surface.test.ts +128 -0
- package/src/__tests__/user-plugin-loader.test.ts +1 -1
- package/src/__tests__/visible-app-context.test.ts +16 -9
- package/src/__tests__/voice-config-update.test.ts +40 -0
- package/src/__tests__/worker-entrypoint-guards.test.ts +54 -0
- package/src/__tests__/worker-plugin-surface.test.ts +77 -0
- package/src/__tests__/workspace-migration-142-consolidate-voice-front-door.test.ts +158 -0
- package/src/__tests__/workspace-migration-143-repair-deprecated-codex-model-id.test.ts +133 -0
- package/src/__tests__/workspace-migration-144-convert-stranded-subscription-openai-profiles.test.ts +316 -0
- package/src/__tests__/workspace-migration-145-collapse-profile-bindings-to-entries.test.ts +325 -0
- package/src/__tests__/workspace-migration-146-repair-retired-fireworks-deepseek-flash-model-id.test.ts +235 -0
- package/src/acp/__tests__/acp-claude-oauth.test.ts +10 -2
- package/src/acp/__tests__/auth-required.test.ts +161 -0
- package/src/acp/acp-claude-oauth.ts +19 -2
- package/src/acp/agent-process.test.ts +100 -0
- package/src/acp/agent-process.ts +29 -26
- package/src/acp/auth-required.ts +102 -0
- package/src/acp/session-manager.test.ts +119 -0
- package/src/acp/session-manager.ts +68 -2
- package/src/api/events/acp-auth-required.ts +55 -0
- package/src/api/index.ts +7 -0
- package/src/api/surfaces.ts +7 -3
- package/src/apps/app-store.ts +3 -0
- package/src/bundler/package-resolver.ts +2 -30
- package/src/calls/__tests__/voice-session-bridge.test.ts +173 -1
- package/src/calls/__tests__/voice-triage-escalate.test.ts +94 -0
- package/src/calls/call-controller.ts +19 -3
- package/src/calls/call-setup-flow.ts +0 -1
- package/src/calls/media-stream-stt-session.ts +15 -0
- package/src/calls/voice-session-bridge.ts +71 -16
- package/src/calls/voice-triage-escalate.ts +104 -2
- package/src/channels/__tests__/plugin-channel-declarations.test.ts +161 -0
- package/src/channels/config.ts +13 -0
- package/src/channels/plugin-channel-declarations.ts +108 -0
- package/src/channels/types.ts +30 -0
- package/src/cli/AGENTS.md +5 -2
- package/src/cli/commands/credentials.help.ts +2 -2
- package/src/cli/commands/inference-providers.ts +1 -1
- package/src/cli/commands/mcp.help.ts +13 -4
- package/src/cli/commands/mcp.ts +9 -0
- package/src/cli/commands/memory/__tests__/memory-v3.test.ts +128 -5
- package/src/cli/commands/memory/index.help.ts +43 -1
- package/src/cli/commands/memory/memory-v3.ts +64 -0
- package/src/cli/commands/stt.help.ts +27 -2
- package/src/cli/lib/__tests__/upgrade-plugin.test.ts +39 -0
- package/src/cli/lib/bundled-marketplace.json +13 -0
- package/src/cli/lib/upgrade-plugin.ts +42 -0
- package/src/config/__tests__/default-profile-catalog.test.ts +34 -2
- package/src/config/__tests__/default-provider.test.ts +6 -1
- package/src/config/__tests__/profile-materialization.test.ts +75 -19
- package/src/config/bundled-skills/acp/SKILL.md +6 -7
- package/src/config/bundled-skills/document-editor/SKILL.md +2 -2
- package/src/config/bundled-skills/document-editor/TOOLS.json +2 -2
- package/src/config/bundled-skills/media-processing/services/preprocess.ts +14 -4
- package/src/config/bundled-skills/settings/TOOLS.json +3 -3
- package/src/config/bundled-skills/settings/tools/navigate-settings-tab.test.ts +65 -0
- package/src/config/bundled-skills/settings/tools/navigate-settings-tab.ts +7 -1
- package/src/config/bundled-skills/settings/tools/shared.ts +16 -0
- package/src/config/bundled-skills/settings/tools/voice-config-update.ts +19 -1
- package/src/config/bundled-skills/transcribe/tools/transcribe-media.test.ts +22 -1
- package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +9 -2
- package/src/config/call-site-defaults.ts +4 -5
- package/src/config/default-profile-catalog.ts +83 -12
- package/src/config/default-profile-names.ts +4 -1
- package/src/config/default-provider-resolution.ts +4 -0
- package/src/config/llm-context-resolution.ts +11 -3
- package/src/config/llm-resolver.ts +28 -1
- package/src/config/profile-materialization.ts +70 -22
- package/src/config/schemas/__tests__/live-voice.test.ts +107 -4
- package/src/config/schemas/call-site-catalog.ts +4 -4
- package/src/config/schemas/live-voice.ts +57 -23
- package/src/config/schemas/llm.ts +59 -32
- package/src/config/schemas/mcp.ts +23 -0
- package/src/config/schemas/plugin-updates.ts +6 -2
- package/src/config/schemas/stt.ts +1 -0
- package/src/context/outbound-sanitize.ts +96 -1
- package/src/daemon/__tests__/plugin-mcp-reconcile.test.ts +82 -0
- package/src/daemon/conversation-agent-loop-handlers.ts +15 -10
- package/src/daemon/conversation-agent-loop.ts +17 -6
- package/src/daemon/conversation-messaging.ts +5 -1
- package/src/daemon/conversation-notifiers.ts +9 -1
- package/src/daemon/conversation-process.ts +36 -6
- package/src/daemon/conversation-runtime-assembly.ts +3 -4
- package/src/daemon/conversation-surfaces.ts +27 -5
- package/src/daemon/conversation-tool-setup.ts +1 -2
- package/src/daemon/conversation.ts +48 -0
- package/src/daemon/interactive-turn-sender.ts +59 -0
- package/src/daemon/mcp-reload-service.ts +36 -6
- package/src/daemon/process-message.ts +24 -24
- package/src/daemon/providers-setup.ts +6 -3
- package/src/daemon/trust-context-types.ts +29 -0
- package/src/daemon/wake-conversation-ops.ts +3 -2
- package/src/documents/document-store.ts +138 -5
- package/src/hooks/hook-loader.ts +3 -3
- package/src/hooks/registry.ts +50 -6
- package/src/inbound/__tests__/oauth-callback-url.test.ts +83 -0
- package/src/inbound/oauth-callback-url.ts +61 -0
- package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +1 -104
- package/src/live-voice/__tests__/live-voice-events.test.ts +7 -8
- package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +932 -0
- package/src/live-voice/__tests__/live-voice-metrics.test.ts +115 -8
- package/src/live-voice/__tests__/live-voice-photo.test.ts +100 -0
- package/src/live-voice/__tests__/live-voice-progress.test.ts +60 -194
- package/src/live-voice/__tests__/live-voice-stt.test.ts +14 -0
- package/src/live-voice/__tests__/live-voice-triage-escalate.test.ts +29 -0
- package/src/live-voice/__tests__/live-voice-tts-session.test.ts +0 -483
- package/src/live-voice/__tests__/live-voice-vad.test.ts +0 -16
- package/src/live-voice/__tests__/progress-narration.test.ts +214 -0
- package/src/live-voice/live-voice-archive.ts +2 -0
- package/src/live-voice/live-voice-metrics.ts +57 -32
- package/src/live-voice/live-voice-photo.ts +1 -2
- package/src/live-voice/live-voice-session.ts +535 -314
- package/src/live-voice/progress-narration.ts +277 -0
- package/src/live-voice/protocol.ts +21 -1
- package/src/mcp/__tests__/effective-config.test.ts +238 -0
- package/src/mcp/__tests__/mcp-auth-orchestrator.test.ts +0 -1
- package/src/mcp/__tests__/mcp-oauth-client-registration.test.ts +200 -0
- package/src/mcp/__tests__/mcp-oauth-provider.test.ts +9 -9
- package/src/mcp/__tests__/plugin-server-credential-isolation.test.ts +95 -0
- package/src/mcp/client.ts +16 -11
- package/src/mcp/effective-config.ts +113 -0
- package/src/mcp/manager.ts +11 -6
- package/src/mcp/mcp-auth-orchestrator.ts +13 -22
- package/src/mcp/mcp-oauth-provider.ts +205 -240
- package/src/monitoring/__tests__/plugin-auto-update.test.ts +166 -3
- package/src/monitoring/plugin-auto-update.ts +128 -24
- package/src/notifications/signal.ts +1 -0
- package/src/permissions/confirmation-guardian-request.test.ts +15 -11
- package/src/permissions/confirmation-guardian-request.ts +2 -2
- package/src/permissions/question-guardian-request.test.ts +14 -6
- package/src/permissions/question-guardian-request.ts +1 -2
- package/src/persistence/attachments-store.ts +8 -1
- package/src/persistence/bookmark-crud.ts +3 -7
- package/src/persistence/conversation-attention-store.ts +16 -45
- package/src/persistence/conversation-crud.ts +33 -4
- package/src/persistence/conversation-lineage.ts +9 -0
- package/src/persistence/conversation-queries.ts +108 -41
- package/src/persistence/delivery-crud.ts +38 -29
- package/src/persistence/external-conversation-store.ts +32 -4
- package/src/persistence/llm-request-log-store.ts +4 -10
- package/src/persistence/llm-usage-store.ts +8 -3
- package/src/persistence/message-reads.test.ts +197 -0
- package/src/persistence/message-reads.ts +211 -0
- package/src/persistence/migrations/366-chatgpt-subscription-row-identity.test.ts +120 -0
- package/src/persistence/migrations/366-chatgpt-subscription-row-identity.ts +62 -0
- package/src/persistence/real-user-turn-filter.ts +27 -3
- package/src/persistence/steps.ts +9 -0
- package/src/plugin-api/__tests__/oauth-callback-url-export.test.ts +29 -0
- package/src/plugin-api/conversation-turn.ts +168 -5
- package/src/plugin-api/index.ts +21 -5
- package/src/plugin-api/vision-support.test.ts +1 -1
- package/src/plugins/__tests__/mcp-servers.test.ts +371 -0
- package/src/plugins/defaults/memory/AGENTS.md +4 -0
- package/src/plugins/defaults/memory/__tests__/buffer-format.test.ts +204 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +72 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +4 -1
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-provider-path.test.ts +4 -1
- package/src/plugins/defaults/memory/buffer-format.ts +165 -0
- package/src/plugins/defaults/memory/context-search/sources/conversations.ts +6 -0
- package/src/plugins/defaults/memory/graph/image-ref-utils.ts +3 -0
- package/src/plugins/defaults/memory/graph/tool-handlers.ts +1 -30
- package/src/plugins/defaults/memory/graph-topology/pending-buffer.test.ts +34 -0
- package/src/plugins/defaults/memory/graph-topology/pending-buffer.ts +8 -12
- package/src/plugins/defaults/memory/hooks/post-compact.ts +1 -4
- package/src/plugins/defaults/memory/indexer.ts +3 -1
- package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +19 -7
- package/src/plugins/defaults/memory/src/__tests__/memory-v3-gate-stats.test.ts +281 -0
- package/src/plugins/defaults/memory/src/memory-v3-routes.ts +207 -0
- package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +33 -0
- package/src/plugins/defaults/memory/substrate/__tests__/static-context.test.ts +199 -2
- package/src/plugins/defaults/memory/substrate/consolidation-job.ts +25 -18
- package/src/plugins/defaults/memory/substrate/skill-content.ts +8 -1
- package/src/plugins/defaults/memory/substrate/static-context.ts +160 -4
- package/src/plugins/defaults/memory/substrate/sweep-job.ts +2 -4
- package/src/plugins/defaults/memory/v1/graph/extraction.ts +3 -1
- package/src/plugins/defaults/memory/v3/prune.ts +2 -0
- package/src/plugins/defaults/memory/v3/selection-log-store.ts +2 -0
- package/src/plugins/defaults/memory/worker.ts +6 -3
- package/src/plugins/external-plugin-loader.ts +47 -0
- package/src/plugins/mcp-servers.ts +361 -0
- package/src/plugins/mtime-cache.ts +23 -49
- package/src/plugins/worker-plugin-surface.ts +33 -0
- package/src/providers/__tests__/connection-model-compat.test.ts +1 -1
- package/src/providers/__tests__/dispatch-connection-routing.test.ts +214 -2
- package/src/providers/__tests__/preflight-resolved-config.test.ts +57 -0
- package/src/providers/__tests__/retry-callsite.test.ts +5 -2
- package/src/providers/call-site-routing.ts +30 -3
- package/src/providers/connection-resolution.ts +194 -11
- package/src/providers/inference/auth.ts +6 -0
- package/src/providers/inference/connection-availability.ts +24 -2
- package/src/providers/inference/connections.ts +2 -0
- package/src/providers/model-catalog.ts +3 -3
- package/src/providers/model-intents.ts +28 -8
- package/src/providers/openai/chat-completions-provider.ts +5 -6
- package/src/providers/openai/codex-models.ts +2 -1
- package/src/providers/provider-send-message.ts +32 -3
- package/src/providers/speech-to-text/__tests__/deepgram-flux-frames.test.ts +433 -0
- package/src/providers/speech-to-text/__tests__/deepgram-flux-realtime.test.ts +620 -0
- package/src/providers/speech-to-text/__tests__/provider-catalog.test.ts +34 -0
- package/src/providers/speech-to-text/__tests__/resolve.test.ts +285 -6
- package/src/providers/speech-to-text/deepgram-flux-frames.ts +395 -0
- package/src/providers/speech-to-text/deepgram-flux-realtime.ts +719 -0
- package/src/providers/speech-to-text/provider-catalog.ts +99 -8
- package/src/providers/speech-to-text/resolve.ts +25 -2
- package/src/routes/worker.ts +17 -5
- package/src/runtime/access-request-helper.ts +9 -12
- package/src/runtime/agent-wake.ts +3 -3
- package/src/runtime/pre-first-message-gate.ts +4 -0
- package/src/runtime/routes/__tests__/acp-claude-auth-routes.test.ts +12 -4
- package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +219 -1
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +52 -0
- package/src/runtime/routes/__tests__/default-provider-routes.test.ts +61 -0
- package/src/runtime/routes/__tests__/inference-profiles-routes.test.ts +44 -0
- package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +102 -1
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +44 -0
- package/src/runtime/routes/__tests__/stt-routes.test.ts +25 -0
- package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +62 -1
- package/src/runtime/routes/channel-availability-routes.ts +32 -14
- package/src/runtime/routes/channel-route-shared.ts +0 -6
- package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +6 -6
- package/src/runtime/routes/conversation-list-routes.ts +112 -1
- package/src/runtime/routes/conversation-query-routes.ts +40 -27
- package/src/runtime/routes/credential-prompt-routes.ts +4 -7
- package/src/runtime/routes/default-provider-routes.ts +15 -0
- package/src/runtime/routes/inbound-message-handler.ts +17 -41
- package/src/runtime/routes/inbound-stages/acl-enforcement.test.ts +0 -1
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +0 -9
- package/src/runtime/routes/inbound-stages/admission-policy.ts +1 -17
- package/src/runtime/routes/inbound-stages/bootstrap-intercept.test.ts +0 -1
- package/src/runtime/routes/inbound-stages/bootstrap-intercept.ts +2 -3
- package/src/runtime/routes/inbound-stages/edit-intercept.ts +1 -3
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.test.ts +0 -1
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +3 -4
- package/src/runtime/routes/inbound-stages/reaction-intercept.test.ts +0 -1
- package/src/runtime/routes/inbound-stages/reaction-intercept.ts +11 -20
- package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +2 -3
- package/src/runtime/routes/inference-profiles-routes.ts +20 -11
- package/src/runtime/routes/inference-provider-connection-routes.ts +77 -15
- package/src/runtime/routes/log-export-routes.ts +3 -0
- package/src/runtime/routes/mcp-auth-routes.ts +148 -57
- package/src/runtime/routes/plugins-routes.ts +21 -3
- package/src/runtime/routes/stt-routes.ts +31 -25
- package/src/runtime/routes/surface-conversation-resolver.ts +3 -0
- package/src/runtime/routes/user-route-dispatcher.ts +39 -14
- package/src/runtime/routes/user-route-import.ts +108 -0
- package/src/schedule/worker.ts +6 -0
- package/src/security/oauth2.ts +6 -22
- package/src/stt/__tests__/daemon-batch-transcriber.test.ts +22 -0
- package/src/stt/__tests__/types.test.ts +94 -0
- package/src/stt/daemon-batch-transcriber.ts +10 -0
- package/src/stt/stt-stream-session.ts +8 -4
- package/src/stt/types.ts +103 -0
- package/src/subagent/manager.ts +1 -3
- package/src/subagent/types.ts +7 -6
- package/src/tools/acp/spawn.test.ts +97 -0
- package/src/tools/acp/spawn.ts +32 -0
- package/src/tools/document/document-tool.ts +12 -3
- package/src/tools/registry.ts +2 -1
- package/src/tools/ui-surface/surface-shape-docs.ts +11 -0
- package/src/tools/workflows/run-workflow.ts +1 -2
- package/src/tts/__tests__/reasoning-tag-filter.test.ts +78 -0
- package/src/tts/reasoning-tag-filter.ts +89 -0
- package/src/util/think-tag-stream.ts +95 -0
- package/src/workspace/byok-default-profile-ensure.ts +76 -24
- package/src/workspace/custom-profile-ensure.ts +4 -24
- package/src/workspace/migrations/142-consolidate-voice-front-door.ts +70 -0
- package/src/workspace/migrations/143-repair-deprecated-codex-model-id.ts +134 -0
- package/src/workspace/migrations/144-convert-stranded-subscription-openai-profiles.ts +265 -0
- package/src/workspace/migrations/145-collapse-profile-bindings-to-entries.ts +328 -0
- package/src/workspace/migrations/146-repair-retired-fireworks-deepseek-flash-model-id.ts +195 -0
- package/src/workspace/migrations/__tests__/141-stt-english-default-to-multilingual.test.ts +0 -10
- package/src/workspace/migrations/registry.ts +10 -0
- package/src/workspace/provider-commit-message-generator.ts +7 -5
- package/src/live-voice/__tests__/front-decision.test.ts +0 -645
- package/src/live-voice/front-decision.ts +0 -476
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pins the two shapes a Claude auth failure travels in, plus the
|
|
3
|
+
* wire-contract literals. Losing either shape silently degrades an actionable
|
|
4
|
+
* "reconnect Claude" failure into an opaque one.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, expect, test } from "bun:test";
|
|
8
|
+
|
|
9
|
+
import { AcpAuthRequiredEventSchema } from "../../api/events/acp-auth-required.js";
|
|
10
|
+
import { AcpSessionErrorEventSchema } from "../../api/events/acp-session-error.js";
|
|
11
|
+
import {
|
|
12
|
+
ACP_CLAUDE_AUTH_REQUIRED_CODE,
|
|
13
|
+
AcpAuthRequiredError,
|
|
14
|
+
AUTH_REQUIRED_CODE,
|
|
15
|
+
CLAUDE_ACP_COMMAND,
|
|
16
|
+
isAcpAuthRequired,
|
|
17
|
+
isClaudeAuthFailureMessage,
|
|
18
|
+
} from "../auth-required.js";
|
|
19
|
+
|
|
20
|
+
describe("isAcpAuthRequired", () => {
|
|
21
|
+
test("recognizes the raw JSON-RPC rejection from the agent", () => {
|
|
22
|
+
// What the adapter actually sends: a plain object off the wire, not an
|
|
23
|
+
// instance of any class we control.
|
|
24
|
+
expect(isAcpAuthRequired({ code: AUTH_REQUIRED_CODE })).toBe(true);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("recognizes our own error after the retry path gives up", () => {
|
|
28
|
+
expect(isAcpAuthRequired(new AcpAuthRequiredError("claude", "nope"))).toBe(
|
|
29
|
+
true,
|
|
30
|
+
);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("does not fire on other failures", () => {
|
|
34
|
+
expect(isAcpAuthRequired(new Error("Internal error"))).toBe(false);
|
|
35
|
+
expect(isAcpAuthRequired({ code: -32601 })).toBe(false);
|
|
36
|
+
expect(isAcpAuthRequired(null)).toBe(false);
|
|
37
|
+
expect(isAcpAuthRequired(undefined)).toBe(false);
|
|
38
|
+
expect(isAcpAuthRequired("auth_required")).toBe(false);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("uses the ACP-specified code", () => {
|
|
42
|
+
// Matches the SDK's RequestError.authRequired(); drifting from it would
|
|
43
|
+
// silently stop classifying every auth failure.
|
|
44
|
+
expect(AUTH_REQUIRED_CODE).toBe(-32000);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe("isClaudeAuthFailureMessage", () => {
|
|
49
|
+
test("matches the live rejected-credential failure verbatim", () => {
|
|
50
|
+
// Captured from a real claude-agent-acp run against a revoked token: the
|
|
51
|
+
// adapter relays the CLI's failure as a -32603 internal error carrying
|
|
52
|
+
// this text, NOT as the structured auth_required rejection. This message
|
|
53
|
+
// is the reported bug, so it must classify.
|
|
54
|
+
expect(
|
|
55
|
+
isClaudeAuthFailureMessage(
|
|
56
|
+
"Internal error: Failed to authenticate. API Error: 401 OAuth access token has been revoked.",
|
|
57
|
+
),
|
|
58
|
+
).toBe(true);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("is insensitive to the server's variable suffix", () => {
|
|
62
|
+
// The CLI-authored prefix is the stable part; the API's suffix varies.
|
|
63
|
+
expect(
|
|
64
|
+
isClaudeAuthFailureMessage(
|
|
65
|
+
"Failed to authenticate. API Error: 401 OAuth access token has expired. Re-authenticate to continue.",
|
|
66
|
+
),
|
|
67
|
+
).toBe(true);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("matches the CLI's other auth phrasings", () => {
|
|
71
|
+
expect(
|
|
72
|
+
isClaudeAuthFailureMessage(
|
|
73
|
+
"Session expired. Please run /login to sign in again.",
|
|
74
|
+
),
|
|
75
|
+
).toBe(true);
|
|
76
|
+
expect(isClaudeAuthFailureMessage("Not logged in")).toBe(true);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("does not fire on ordinary failures", () => {
|
|
80
|
+
expect(isClaudeAuthFailureMessage("Internal error")).toBe(false);
|
|
81
|
+
expect(isClaudeAuthFailureMessage("ECONNRESET: connection reset")).toBe(
|
|
82
|
+
false,
|
|
83
|
+
);
|
|
84
|
+
expect(
|
|
85
|
+
isClaudeAuthFailureMessage("Command failed: login.sh: not found"),
|
|
86
|
+
).toBe(false);
|
|
87
|
+
expect(isClaudeAuthFailureMessage(undefined)).toBe(false);
|
|
88
|
+
expect(isClaudeAuthFailureMessage("")).toBe(false);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe("AcpAuthRequiredError", () => {
|
|
93
|
+
test("is an Error that carries the agent id and its message", () => {
|
|
94
|
+
const err = new AcpAuthRequiredError("claude", "needs a reconnect");
|
|
95
|
+
expect(err).toBeInstanceOf(Error);
|
|
96
|
+
expect(err.name).toBe("AcpAuthRequiredError");
|
|
97
|
+
expect(err.agentId).toBe("claude");
|
|
98
|
+
expect(err.message).toBe("needs a reconnect");
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe("event shape is additive-safe for older clients", () => {
|
|
103
|
+
test("acp_session_error rejects unknown keys, which is why the signal is not a field on it", () => {
|
|
104
|
+
// Clients drop a whole event over an unknown key (strict schemas plus a
|
|
105
|
+
// safeParse fallback to `unknown`), so new signals must ride new event
|
|
106
|
+
// types; this pins the strictness that forces that.
|
|
107
|
+
const result = AcpSessionErrorEventSchema.safeParse({
|
|
108
|
+
type: "acp_session_error",
|
|
109
|
+
acpSessionId: "acp-1",
|
|
110
|
+
error: "boom",
|
|
111
|
+
someFutureField: "x",
|
|
112
|
+
});
|
|
113
|
+
expect(result.success).toBe(false);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("acp_session_error still parses in its unchanged shape", () => {
|
|
117
|
+
expect(
|
|
118
|
+
AcpSessionErrorEventSchema.safeParse({
|
|
119
|
+
type: "acp_session_error",
|
|
120
|
+
acpSessionId: "acp-1",
|
|
121
|
+
error: "boom",
|
|
122
|
+
}).success,
|
|
123
|
+
).toBe(true);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("acp_auth_required carries the code and an optional anchor", () => {
|
|
127
|
+
const parsed = AcpAuthRequiredEventSchema.parse({
|
|
128
|
+
type: "acp_auth_required",
|
|
129
|
+
acpSessionId: "acp-1",
|
|
130
|
+
authCode: ACP_CLAUDE_AUTH_REQUIRED_CODE,
|
|
131
|
+
agent: "claude",
|
|
132
|
+
parentToolUseId: "tool-1",
|
|
133
|
+
});
|
|
134
|
+
expect(parsed.authCode).toBe(ACP_CLAUDE_AUTH_REQUIRED_CODE);
|
|
135
|
+
expect(parsed.parentToolUseId).toBe("tool-1");
|
|
136
|
+
|
|
137
|
+
// The anchor is optional: a run not started by a tool call has none.
|
|
138
|
+
expect(
|
|
139
|
+
AcpAuthRequiredEventSchema.safeParse({
|
|
140
|
+
type: "acp_auth_required",
|
|
141
|
+
acpSessionId: "acp-1",
|
|
142
|
+
authCode: ACP_CLAUDE_AUTH_REQUIRED_CODE,
|
|
143
|
+
agent: "claude",
|
|
144
|
+
}).success,
|
|
145
|
+
).toBe(true);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
describe("wire contract", () => {
|
|
150
|
+
test("the client marker matches the web literal", () => {
|
|
151
|
+
// Paired with ACP_CLAUDE_AUTH_REQUIRED_CODE in
|
|
152
|
+
// clients/web/src/domains/chat/utils/acp-connect.ts. A silent rename on
|
|
153
|
+
// either side turns the Connect card off with nothing failing.
|
|
154
|
+
expect(ACP_CLAUDE_AUTH_REQUIRED_CODE).toBe("acp_claude_auth_required");
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test("the adapter gate matches the resolved command basename", () => {
|
|
158
|
+
// Compared against SessionEntry.command, which is already a basename.
|
|
159
|
+
expect(CLAUDE_ACP_COMMAND).toBe("claude-agent-acp");
|
|
160
|
+
});
|
|
161
|
+
});
|
|
@@ -33,7 +33,14 @@ import {
|
|
|
33
33
|
* `CLAUDE_CODE_OAUTH_TOKEN` requires.
|
|
34
34
|
*/
|
|
35
35
|
export const CLAUDE_OAUTH_CONFIG: OAuth2Config = {
|
|
36
|
-
|
|
36
|
+
// The claude.ai-account authorize endpoint, matching the Claude Code CLI's
|
|
37
|
+
// own CLAUDE_AI_AUTHORIZE_URL. Shared by both flows: the loopback path
|
|
38
|
+
// builds its URL from this in `prepareOAuth2Flow`, the manual path in
|
|
39
|
+
// `buildClaudeAuthorizeUrl`. Only the claude.com/cai host completes a
|
|
40
|
+
// grant; the legacy claude.ai host still renders a working-looking consent
|
|
41
|
+
// screen but rejects every authorize POST, so a wrong host here looks
|
|
42
|
+
// functional until the final click of a real sign-in.
|
|
43
|
+
authorizeUrl: "https://claude.com/cai/oauth/authorize",
|
|
37
44
|
tokenExchangeUrl: "https://platform.claude.com/v1/oauth/token",
|
|
38
45
|
clientId: "9d1c250a-e61b-44d9-88ed-5944d1962f5e",
|
|
39
46
|
scopes: ["user:inference"],
|
|
@@ -53,12 +60,22 @@ export const CLAUDE_OAUTH_CONFIG: OAuth2Config = {
|
|
|
53
60
|
export const CLAUDE_MANUAL_REDIRECT_URI =
|
|
54
61
|
"https://platform.claude.com/oauth/code/callback";
|
|
55
62
|
|
|
56
|
-
/**
|
|
63
|
+
/**
|
|
64
|
+
* Build the Claude authorize URL for the MANUAL (paste) PKCE flow.
|
|
65
|
+
*
|
|
66
|
+
* `code=true` tells Claude to render the `code#state` string on the callback
|
|
67
|
+
* page for the user to copy, which is the whole mechanism of the manual flow;
|
|
68
|
+
* Claude rejects a manual-redirect grant without it. The loopback flow
|
|
69
|
+
* performs a real redirect instead and must NOT send it; that URL is built
|
|
70
|
+
* separately in `prepareOAuth2Flow`, which is why this builder can include
|
|
71
|
+
* the param unconditionally.
|
|
72
|
+
*/
|
|
57
73
|
export function buildClaudeAuthorizeUrl(
|
|
58
74
|
redirectUri: string,
|
|
59
75
|
pkce: { codeChallenge: string; state: string },
|
|
60
76
|
): string {
|
|
61
77
|
const params = new URLSearchParams({
|
|
78
|
+
code: "true",
|
|
62
79
|
response_type: "code",
|
|
63
80
|
client_id: CLAUDE_OAUTH_CONFIG.clientId,
|
|
64
81
|
redirect_uri: redirectUri,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
2
|
|
|
3
3
|
import { AcpAgentProcess } from "./agent-process.js";
|
|
4
|
+
import { AcpAuthRequiredError, isAcpAuthRequired } from "./auth-required.js";
|
|
4
5
|
import type { AcpAgentConfig } from "./types.js";
|
|
5
6
|
|
|
6
7
|
const config: AcpAgentConfig = { command: "noop", args: [] };
|
|
@@ -114,3 +115,102 @@ describe("AcpAgentProcess.markStderr / stderrSince", () => {
|
|
|
114
115
|
expect(proc.stderrSince(mark)).toBe("fresh failure");
|
|
115
116
|
});
|
|
116
117
|
});
|
|
118
|
+
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
// createSession: structured auth_required handling through the real retry path
|
|
121
|
+
// ---------------------------------------------------------------------------
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Wires a process with a fake protocol connection and advertised auth methods,
|
|
125
|
+
* following the same no-child-process injection style as `feed` above. The
|
|
126
|
+
* retry logic under test (`withAuthRetry`, method selection, the typed throw)
|
|
127
|
+
* runs for real.
|
|
128
|
+
*/
|
|
129
|
+
function wireProcess(opts: {
|
|
130
|
+
authMethods: unknown[];
|
|
131
|
+
newSession: () => Promise<unknown>;
|
|
132
|
+
env?: Record<string, string>;
|
|
133
|
+
}): { proc: AcpAgentProcess; authenticateCalls: () => number } {
|
|
134
|
+
const proc = new AcpAgentProcess("test-agent", config, clientFactory);
|
|
135
|
+
let authed = 0;
|
|
136
|
+
(proc as unknown as { connection: unknown }).connection = {
|
|
137
|
+
newSession: opts.newSession,
|
|
138
|
+
authenticate: async () => {
|
|
139
|
+
authed += 1;
|
|
140
|
+
return {};
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
(proc as unknown as { initializeResponse: unknown }).initializeResponse = {
|
|
144
|
+
authMethods: opts.authMethods,
|
|
145
|
+
};
|
|
146
|
+
(proc as unknown as { spawnedEnv: unknown }).spawnedEnv = opts.env ?? {};
|
|
147
|
+
return { proc, authenticateCalls: () => authed };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const TERMINAL_ONLY_METHODS = [
|
|
151
|
+
{ id: "claude-ai-login", name: "Claude Subscription", type: "terminal" },
|
|
152
|
+
];
|
|
153
|
+
|
|
154
|
+
describe("AcpAgentProcess.createSession auth_required handling", () => {
|
|
155
|
+
test("a structured auth_required rejection with only terminal methods becomes AcpAuthRequiredError", async () => {
|
|
156
|
+
// The wire shape the adapter actually sends: a plain JSON-RPC error
|
|
157
|
+
// object, not an Error subclass. With no satisfiable env_var method the
|
|
158
|
+
// retry path must surface the typed error the session manager and the
|
|
159
|
+
// spawn tool classify on; losing the type here silently degrades the
|
|
160
|
+
// whole recovery surface back into a generic failure.
|
|
161
|
+
const { proc } = wireProcess({
|
|
162
|
+
authMethods: TERMINAL_ONLY_METHODS,
|
|
163
|
+
newSession: () =>
|
|
164
|
+
Promise.reject({ code: -32000, message: "Authentication required" }),
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
const err: unknown = await proc.createSession("/tmp").then(
|
|
168
|
+
() => null,
|
|
169
|
+
(e: unknown) => e,
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
expect(err).toBeInstanceOf(AcpAuthRequiredError);
|
|
173
|
+
expect((err as AcpAuthRequiredError).agentId).toBe("test-agent");
|
|
174
|
+
expect(isAcpAuthRequired(err)).toBe(true);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test("a satisfiable env_var method authenticates and retries instead of throwing", async () => {
|
|
178
|
+
let calls = 0;
|
|
179
|
+
const { proc, authenticateCalls } = wireProcess({
|
|
180
|
+
authMethods: [
|
|
181
|
+
{
|
|
182
|
+
id: "api-key",
|
|
183
|
+
name: "Use SOME_KEY",
|
|
184
|
+
type: "env_var",
|
|
185
|
+
vars: [{ name: "SOME_KEY" }],
|
|
186
|
+
},
|
|
187
|
+
],
|
|
188
|
+
env: { SOME_KEY: "value" },
|
|
189
|
+
newSession: () => {
|
|
190
|
+
calls += 1;
|
|
191
|
+
return calls === 1
|
|
192
|
+
? Promise.reject({ code: -32000, message: "Authentication required" })
|
|
193
|
+
: Promise.resolve({ sessionId: "session-1" });
|
|
194
|
+
},
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
await expect(proc.createSession("/tmp")).resolves.toBe("session-1");
|
|
198
|
+
expect(authenticateCalls()).toBe(1);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test("a non-auth rejection passes through untyped", async () => {
|
|
202
|
+
const { proc } = wireProcess({
|
|
203
|
+
authMethods: TERMINAL_ONLY_METHODS,
|
|
204
|
+
newSession: () =>
|
|
205
|
+
Promise.reject({ code: -32603, message: "Internal error" }),
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
const err: unknown = await proc.createSession("/tmp").then(
|
|
209
|
+
() => null,
|
|
210
|
+
(e: unknown) => e,
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
expect(err).not.toBeInstanceOf(AcpAuthRequiredError);
|
|
214
|
+
expect(isAcpAuthRequired(err)).toBe(false);
|
|
215
|
+
});
|
|
216
|
+
});
|
package/src/acp/agent-process.ts
CHANGED
|
@@ -20,16 +20,11 @@ import type {
|
|
|
20
20
|
import * as acp from "@agentclientprotocol/sdk";
|
|
21
21
|
|
|
22
22
|
import { getLogger } from "../util/logger.js";
|
|
23
|
+
import { AcpAuthRequiredError, isAcpAuthRequired } from "./auth-required.js";
|
|
23
24
|
import type { AcpAgentConfig } from "./types.js";
|
|
24
25
|
|
|
25
26
|
const log = getLogger("acp");
|
|
26
27
|
|
|
27
|
-
/**
|
|
28
|
-
* JSON-RPC error code agents use to signal that authentication is required
|
|
29
|
-
* (matches the SDK's RequestError.authRequired()).
|
|
30
|
-
*/
|
|
31
|
-
const AUTH_REQUIRED_CODE = -32000;
|
|
32
|
-
|
|
33
28
|
/**
|
|
34
29
|
* Rough byte cap for retained stderr. Oldest lines are evicted once the sum of
|
|
35
30
|
* retained line lengths exceeds this, so the stderr ring (read via stderrSince)
|
|
@@ -37,19 +32,6 @@ const AUTH_REQUIRED_CODE = -32000;
|
|
|
37
32
|
*/
|
|
38
33
|
const STDERR_RETENTION_BYTES = 4096;
|
|
39
34
|
|
|
40
|
-
/**
|
|
41
|
-
* Detects the ACP auth-required error. Checks the `code` property rather than
|
|
42
|
-
* `instanceof acp.RequestError` so plain JSON-RPC error objects are also
|
|
43
|
-
* recognized.
|
|
44
|
-
*/
|
|
45
|
-
function isAuthRequiredError(err: unknown): boolean {
|
|
46
|
-
return (
|
|
47
|
-
typeof err === "object" &&
|
|
48
|
-
err !== null &&
|
|
49
|
-
(err as { code?: unknown }).code === AUTH_REQUIRED_CODE
|
|
50
|
-
);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
35
|
function isEnvVarMethod(
|
|
54
36
|
method: AuthMethod,
|
|
55
37
|
): method is AuthMethodEnvVar & { type: "env_var" } {
|
|
@@ -296,7 +278,7 @@ export class AcpAgentProcess {
|
|
|
296
278
|
try {
|
|
297
279
|
return await op();
|
|
298
280
|
} catch (err) {
|
|
299
|
-
if (!
|
|
281
|
+
if (!isAcpAuthRequired(err)) {
|
|
300
282
|
throw err;
|
|
301
283
|
}
|
|
302
284
|
|
|
@@ -306,12 +288,23 @@ export class AcpAgentProcess {
|
|
|
306
288
|
|
|
307
289
|
const method = this.selectEnvVarAuthMethod();
|
|
308
290
|
if (!method) {
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
291
|
+
// Typed so the session manager can tell an auth failure from a crash
|
|
292
|
+
// and surface a re-authentication path. The remediation text is split
|
|
293
|
+
// by what the agent advertises: env-var and `credentials prompt`
|
|
294
|
+
// advice only helps when an env_var method exists to satisfy, and for
|
|
295
|
+
// terminal-login-only adapters it sends the user chasing a CLI
|
|
296
|
+
// workaround for a credential the app repairs on its own.
|
|
297
|
+
throw new AcpAuthRequiredError(
|
|
298
|
+
this.agentId,
|
|
299
|
+
this.hasEnvVarAuthMethod()
|
|
300
|
+
? `ACP agent "${this.agentId}" requires authentication. ` +
|
|
301
|
+
`Advertised methods: ${this.describeAuthMethods()}. ` +
|
|
302
|
+
"Set the required env var under acp.agents.<id>.env in config.json, " +
|
|
303
|
+
"or collect it securely via 'assistant credentials prompt --service acp --field <field> --label \"<label>\"'."
|
|
304
|
+
: `ACP agent "${this.agentId}" requires authentication and its ` +
|
|
305
|
+
"stored credential was not accepted. Advertised methods: " +
|
|
306
|
+
`${this.describeAuthMethods()}. None can be completed headlessly, ` +
|
|
307
|
+
"so the credential has to be reconnected.",
|
|
315
308
|
);
|
|
316
309
|
}
|
|
317
310
|
|
|
@@ -325,6 +318,16 @@ export class AcpAgentProcess {
|
|
|
325
318
|
}
|
|
326
319
|
}
|
|
327
320
|
|
|
321
|
+
/**
|
|
322
|
+
* Whether the agent advertises any `env_var` auth method at all, satisfiable
|
|
323
|
+
* or not. Distinguishes "we could not satisfy the env var it wants" from
|
|
324
|
+
* "env vars are not how this agent authenticates", which are different
|
|
325
|
+
* problems with different fixes.
|
|
326
|
+
*/
|
|
327
|
+
private hasEnvVarAuthMethod(): boolean {
|
|
328
|
+
return this.authMethods.some(isEnvVarMethod);
|
|
329
|
+
}
|
|
330
|
+
|
|
328
331
|
/**
|
|
329
332
|
* Renders the agent's advertised auth methods for error messages, e.g.
|
|
330
333
|
* `"Login with ChatGPT" (chatgpt), "Use OPENAI_API_KEY" (env var OPENAI_API_KEY)`.
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detection of Claude authentication failures on ACP runs.
|
|
3
|
+
*
|
|
4
|
+
* These failures reach the daemon in two shapes, and both are needed:
|
|
5
|
+
*
|
|
6
|
+
* 1. No credentials at all: the adapter raises a structured ACP
|
|
7
|
+
* `auth_required` rejection (JSON-RPC -32000). {@link isAcpAuthRequired}.
|
|
8
|
+
* 2. Credentials present but rejected (the expired/revoked case): the CLI
|
|
9
|
+
* surfaces the API 401 as an error message it authors, and the adapter
|
|
10
|
+
* relays it as a generic -32603 internal error, NOT as `auth_required`.
|
|
11
|
+
* {@link isClaudeAuthFailureMessage}.
|
|
12
|
+
*
|
|
13
|
+
* A positive detection becomes {@link ACP_CLAUDE_AUTH_REQUIRED_CODE} on the
|
|
14
|
+
* `acp_auth_required` event, which the client turns into the inline
|
|
15
|
+
* "Connect Claude Code" card.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
export { ACP_CLAUDE_AUTH_REQUIRED_CODE } from "../api/events/acp-auth-required.js";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* JSON-RPC error code agents use to signal that authentication is required
|
|
22
|
+
* (matches the ACP SDK's `RequestError.authRequired()`).
|
|
23
|
+
*/
|
|
24
|
+
export const AUTH_REQUIRED_CODE = -32000;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* An agent rejected an operation with `auth_required` and the daemon has no
|
|
28
|
+
* way to satisfy it. A distinct type so the classification survives to the
|
|
29
|
+
* session manager, where the failure becomes an event the UI can act on.
|
|
30
|
+
*/
|
|
31
|
+
export class AcpAuthRequiredError extends Error {
|
|
32
|
+
constructor(
|
|
33
|
+
readonly agentId: string,
|
|
34
|
+
message: string,
|
|
35
|
+
) {
|
|
36
|
+
super(message);
|
|
37
|
+
this.name = "AcpAuthRequiredError";
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Detects the structured auth-required signal in either shape it travels: the
|
|
43
|
+
* raw JSON-RPC rejection from the agent, or our own
|
|
44
|
+
* {@link AcpAuthRequiredError}. Checks the `code` property rather than an
|
|
45
|
+
* `instanceof` so plain JSON-RPC error objects are recognized too.
|
|
46
|
+
*/
|
|
47
|
+
export function isAcpAuthRequired(err: unknown): boolean {
|
|
48
|
+
if (err instanceof AcpAuthRequiredError) {
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
return (
|
|
52
|
+
typeof err === "object" &&
|
|
53
|
+
err !== null &&
|
|
54
|
+
(err as { code?: unknown }).code === AUTH_REQUIRED_CODE
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The Claude CLI's own auth-failure phrasings. Unanchored (no ^/$) because
|
|
60
|
+
* the messages arrive with adapter framing ("Internal error: ...") or as a
|
|
61
|
+
* line `deriveFailureError` picked out of stderr.
|
|
62
|
+
*/
|
|
63
|
+
const CLAUDE_AUTH_FAILURE_PATTERNS: readonly RegExp[] = [
|
|
64
|
+
/Failed to authenticate\b/,
|
|
65
|
+
/Please run \/login/,
|
|
66
|
+
/\bNot logged in\b/,
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Whether a failure message is a Claude authentication failure. Callers gate
|
|
71
|
+
* on the adapter (`claude-agent-acp`) BEFORE consulting this: the patterns
|
|
72
|
+
* are Claude-specific, and the marker built on them promises a repair only
|
|
73
|
+
* the Connect Claude flow can perform.
|
|
74
|
+
*/
|
|
75
|
+
export function isClaudeAuthFailureMessage(
|
|
76
|
+
message: string | undefined,
|
|
77
|
+
): boolean {
|
|
78
|
+
return (
|
|
79
|
+
message != null &&
|
|
80
|
+
CLAUDE_AUTH_FAILURE_PATTERNS.some((pattern) => pattern.test(message))
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** The adapter whose auth failures the Connect Claude flow can repair. */
|
|
85
|
+
export const CLAUDE_ACP_COMMAND = "claude-agent-acp";
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Model-facing text accompanying a Claude auth failure wherever the inline
|
|
89
|
+
* Connect card is raised (the tool result for a pre-spawn rejection, the
|
|
90
|
+
* parent notification for a post-spawn one), so the model points at the card
|
|
91
|
+
* instead of inventing a remedy (CLI commands, pasted tokens, or a card that
|
|
92
|
+
* is not there). Bans placement words because card position is a UI detail
|
|
93
|
+
* the model cannot see.
|
|
94
|
+
*/
|
|
95
|
+
export const ACP_AUTH_RECOVERY_GUIDANCE =
|
|
96
|
+
"The Claude Code connection needs to be re-authorized. The app shows the " +
|
|
97
|
+
'user an inline "Connect Claude Code" card. Reply with ONE short sentence: ' +
|
|
98
|
+
"ask them to click Connect to sign in again, and tell them you'll continue " +
|
|
99
|
+
"automatically once they are connected. Do NOT say where the card is; never " +
|
|
100
|
+
'say "below", "above", "at the bottom", or "here". Do NOT tell them to run ' +
|
|
101
|
+
"`claude setup-token`, paste a token in chat, run credential CLI commands, " +
|
|
102
|
+
"or re-run the agent yourself; the card and auto-continue handle it.";
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
deleteConversation,
|
|
6
6
|
setConversation,
|
|
7
7
|
} from "../daemon/conversation-registry.js";
|
|
8
|
+
import { hasAcpConnectCardRaised } from "./acp-connect-card-state.js";
|
|
8
9
|
import { VellumAcpClientHandler } from "./client-handler.js";
|
|
9
10
|
import { AcpSessionManager } from "./session-manager.js";
|
|
10
11
|
|
|
@@ -310,3 +311,121 @@ describe("AcpSessionManager parent notification", () => {
|
|
|
310
311
|
expect((manager.getStatus() as unknown[]).length).toBe(1);
|
|
311
312
|
});
|
|
312
313
|
});
|
|
314
|
+
|
|
315
|
+
// ---------------------------------------------------------------------------
|
|
316
|
+
// Auth-required recovery surface
|
|
317
|
+
// ---------------------------------------------------------------------------
|
|
318
|
+
|
|
319
|
+
describe("AcpSessionManager auth-required recovery surface", () => {
|
|
320
|
+
/** The live-captured shape of a rejected-credential failure. */
|
|
321
|
+
const authFailure = () =>
|
|
322
|
+
Promise.reject(
|
|
323
|
+
new Error(
|
|
324
|
+
"Internal error: Failed to authenticate. API Error: 401 OAuth access token has been revoked.",
|
|
325
|
+
),
|
|
326
|
+
);
|
|
327
|
+
|
|
328
|
+
/** Drives an auth-shaped prompt failure through firePromptInBackground. */
|
|
329
|
+
async function driveAuthFailure(opts: {
|
|
330
|
+
id: string;
|
|
331
|
+
command: string;
|
|
332
|
+
parentToolUseId?: string;
|
|
333
|
+
cancelled?: boolean;
|
|
334
|
+
}) {
|
|
335
|
+
const manager = new AcpSessionManager(1);
|
|
336
|
+
const parentId = `parent-${opts.id}`;
|
|
337
|
+
const { conversation, persistUserMessage, loopRan } = mockConversation();
|
|
338
|
+
setConversation(parentId, conversation);
|
|
339
|
+
registered.push(parentId);
|
|
340
|
+
|
|
341
|
+
const entry = injectSession(
|
|
342
|
+
manager,
|
|
343
|
+
opts.id,
|
|
344
|
+
parentId,
|
|
345
|
+
fakeProcess(authFailure),
|
|
346
|
+
);
|
|
347
|
+
entry.command = opts.command;
|
|
348
|
+
(entry as { parentToolUseId?: string }).parentToolUseId =
|
|
349
|
+
opts.parentToolUseId;
|
|
350
|
+
if (opts.cancelled) {
|
|
351
|
+
entry.state.status = "cancelled";
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
await fire(manager, opts.id, entry).catch(() => {});
|
|
355
|
+
if (!opts.cancelled) {
|
|
356
|
+
await loopRan;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const events = (
|
|
360
|
+
entry.sendToVellum as ReturnType<typeof mock>
|
|
361
|
+
).mock.calls.map((c) => c[0] as { type: string } & Record<string, unknown>);
|
|
362
|
+
const firstPersist = persistUserMessage.mock.calls[0] as unknown as
|
|
363
|
+
| [{ content: string }]
|
|
364
|
+
| undefined;
|
|
365
|
+
return {
|
|
366
|
+
parentId,
|
|
367
|
+
authEvent: events.find((e) => e.type === "acp_auth_required"),
|
|
368
|
+
persistedContent: firstPersist?.[0].content,
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
test("claude failure with an anchor raises the full surface: event, registry mark, guidance", async () => {
|
|
373
|
+
const r = await driveAuthFailure({
|
|
374
|
+
id: "sess-auth-anchor",
|
|
375
|
+
command: "claude-agent-acp",
|
|
376
|
+
parentToolUseId: "tool-anchor-1",
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
expect(r.authEvent).toMatchObject({
|
|
380
|
+
acpSessionId: "sess-auth-anchor",
|
|
381
|
+
authCode: "acp_claude_auth_required",
|
|
382
|
+
agent: "claude",
|
|
383
|
+
parentToolUseId: "tool-anchor-1",
|
|
384
|
+
});
|
|
385
|
+
// The credential-prompt route consults this registry to redirect a
|
|
386
|
+
// redundant secure prompt at the card instead of stacking a second one.
|
|
387
|
+
expect(hasAcpConnectCardRaised(r.parentId)).toBe(true);
|
|
388
|
+
expect(r.persistedContent).toContain("Connect Claude Code");
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
test("claude failure without an anchor keeps the plain failure: no event, no mark, no guidance", async () => {
|
|
392
|
+
// No spawning tool call means no transcript row to render the card under;
|
|
393
|
+
// guidance would point the model at a card that cannot appear.
|
|
394
|
+
const r = await driveAuthFailure({
|
|
395
|
+
id: "sess-auth-noanchor",
|
|
396
|
+
command: "claude-agent-acp",
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
expect(r.authEvent).toBeUndefined();
|
|
400
|
+
expect(hasAcpConnectCardRaised(r.parentId)).toBe(false);
|
|
401
|
+
expect(r.persistedContent).not.toContain("Connect Claude Code");
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
test("a run the user already stopped raises nothing: no event, no registry mark, no parent turn", async () => {
|
|
405
|
+
// The client never renders a card for a cancelled run, and the
|
|
406
|
+
// prompt-dedup registry is never cleared, so marking it here would
|
|
407
|
+
// suppress the secure token prompt for the daemon's lifetime against a
|
|
408
|
+
// card that does not exist.
|
|
409
|
+
const r = await driveAuthFailure({
|
|
410
|
+
id: "sess-auth-cancelled",
|
|
411
|
+
command: "claude-agent-acp",
|
|
412
|
+
parentToolUseId: "tool-anchor-3",
|
|
413
|
+
cancelled: true,
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
expect(r.authEvent).toBeUndefined();
|
|
417
|
+
expect(hasAcpConnectCardRaised(r.parentId)).toBe(false);
|
|
418
|
+
expect(r.persistedContent).toBeUndefined();
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
test("a non-claude adapter never raises the surface, even on an auth-shaped failure", async () => {
|
|
422
|
+
const r = await driveAuthFailure({
|
|
423
|
+
id: "sess-auth-codex",
|
|
424
|
+
command: "codex-acp",
|
|
425
|
+
parentToolUseId: "tool-anchor-2",
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
expect(r.authEvent).toBeUndefined();
|
|
429
|
+
expect(hasAcpConnectCardRaised(r.parentId)).toBe(false);
|
|
430
|
+
});
|
|
431
|
+
});
|