@vellumai/assistant 0.11.7 → 0.11.8-staging.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +10 -10
- package/Dockerfile +1 -0
- package/docs/architecture/security.md +1 -1
- package/docs/error-handling.md +1 -2
- package/docs/guardian-request-flow.md +51 -5
- package/docs/trusted-contact-access.md +13 -12
- package/knip.json +1 -0
- package/node_modules/@vellumai/avatar-catalog/package.json +19 -0
- package/{src/avatar/character-components.ts → node_modules/@vellumai/avatar-catalog/src/catalog.ts} +12 -44
- package/node_modules/@vellumai/avatar-catalog/src/colors.ts +19 -0
- package/node_modules/@vellumai/avatar-catalog/src/index.ts +25 -0
- package/node_modules/@vellumai/avatar-catalog/src/types.ts +47 -0
- package/node_modules/@vellumai/avatar-catalog/tsconfig.json +20 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +1 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/channels.ts +7 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/no-response.ts +84 -0
- package/node_modules/@vellumai/credential-storage/src/static-credentials.ts +43 -12
- package/node_modules/@vellumai/environments/package.json +2 -1
- package/node_modules/@vellumai/environments/src/__tests__/test-path-guard.test.ts +89 -0
- package/node_modules/@vellumai/environments/src/test-path-guard.ts +113 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +1 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/channels.ts +7 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/no-response.ts +84 -0
- package/node_modules/@vellumai/gateway-client/src/__tests__/guardian-request-contract.test.ts +8 -18
- package/node_modules/@vellumai/gateway-client/src/__tests__/inbound-event-kind.test.ts +46 -0
- package/node_modules/@vellumai/gateway-client/src/__tests__/invite-contract.test.ts +1 -0
- package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +46 -0
- package/node_modules/@vellumai/gateway-client/src/guardian-request-contract.ts +62 -21
- package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +14 -0
- package/node_modules/@vellumai/gateway-client/src/inbound-event-kind.ts +53 -0
- package/node_modules/@vellumai/gateway-client/src/index.ts +11 -5
- package/node_modules/@vellumai/gateway-client/src/invite-contract.ts +1 -0
- package/node_modules/@vellumai/service-contracts/package.json +1 -0
- package/node_modules/@vellumai/service-contracts/src/channels.ts +7 -0
- package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/service-contracts/src/no-response.ts +84 -0
- package/openapi.yaml +481 -9
- package/package.json +3 -1
- package/scripts/sync-llm-catalog.ts +3 -0
- package/src/__tests__/agent-loop-callsite-precedence.test.ts +16 -0
- package/src/__tests__/agent-loop-compacted-history-transform.test.ts +304 -0
- package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +3 -0
- package/src/__tests__/agent-wake-override-profile.test.ts +3 -0
- package/src/__tests__/anthropic-provider.test.ts +56 -0
- package/src/__tests__/approval-cascade.test.ts +0 -13
- package/src/__tests__/attachments-store.test.ts +117 -6
- package/src/__tests__/attachments.test.ts +122 -0
- package/src/__tests__/bundled-asset.test.ts +22 -1
- package/src/__tests__/channel-approval.test.ts +14 -7
- package/src/__tests__/channel-approvals.test.ts +3 -3
- package/src/__tests__/channel-delivery-store.test.ts +58 -0
- package/src/__tests__/channel-inbound-disk-pressure.test.ts +1 -0
- package/src/__tests__/channel-invite-transport.test.ts +1 -0
- package/src/__tests__/channel-readiness-slack-remote.test.ts +3 -3
- package/src/__tests__/compaction-events.test.ts +0 -12
- package/src/__tests__/compactor-call-site-logging.test.ts +7 -1
- package/src/__tests__/compactor-retained-image-validation.test.ts +15 -0
- package/src/__tests__/config-schema.test.ts +35 -0
- package/src/__tests__/contacts-write.test.ts +141 -3
- package/src/__tests__/conversation-abort-tool-results.test.ts +0 -13
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +0 -12
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +0 -13
- package/src/__tests__/conversation-agent-loop.test.ts +95 -13
- package/src/__tests__/conversation-confirmation-signals.test.ts +0 -13
- package/src/__tests__/conversation-load-history-repair.test.ts +418 -0
- package/src/__tests__/conversation-media-retry.test.ts +33 -2
- package/src/__tests__/conversation-pairing.test.ts +54 -0
- package/src/__tests__/conversation-process-callsite.test.ts +0 -12
- package/src/__tests__/conversation-provider-retry-repair.test.ts +0 -13
- package/src/__tests__/conversation-queue.test.ts +4 -13
- package/src/__tests__/conversation-retry-route.test.ts +5 -0
- package/src/__tests__/conversation-routes-disk-view.test.ts +1 -1
- package/src/__tests__/conversation-routes-enabled-plugins.test.ts +1 -1
- package/src/__tests__/conversation-routes-hidden-queue.test.ts +1 -1
- package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -1
- package/src/__tests__/conversation-sight-frames.test.ts +615 -0
- package/src/__tests__/conversation-slash-queue.test.ts +0 -13
- package/src/__tests__/conversation-slash-unknown.test.ts +0 -13
- package/src/__tests__/conversation-speed-override.test.ts +0 -12
- package/src/__tests__/conversation-store-ephemeral.test.ts +0 -12
- package/src/__tests__/conversation-summarize-route.test.ts +5 -0
- package/src/__tests__/conversation-summarize-up-to.test.ts +102 -0
- package/src/__tests__/conversation-wait-for-idle.test.ts +0 -12
- package/src/__tests__/conversation-workspace-cache-state.test.ts +0 -5
- package/src/__tests__/conversation-workspace-injection.test.ts +0 -17
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +0 -17
- package/src/__tests__/credential-health-service.test.ts +45 -0
- package/src/__tests__/credential-record-retire.test.ts +256 -0
- package/src/__tests__/credential-record-write-through.test.ts +8 -4
- package/src/__tests__/db-conversation-fork-lineage-migration.test.ts +0 -4
- package/src/__tests__/db-conversation-inference-profile-migration.test.ts +0 -4
- package/src/__tests__/db-llm-request-log-provider-migration.test.ts +0 -8
- package/src/__tests__/delete-propagation.test.ts +121 -3
- package/src/__tests__/evict-conversations-for-reload.test.ts +46 -6
- package/src/__tests__/file-ops-service.test.ts +8 -3
- package/src/__tests__/gemini-provider.test.ts +64 -0
- package/src/__tests__/guardian-action-service-scope.test.ts +118 -0
- package/src/__tests__/guardian-card-withdrawal.test.ts +299 -5
- package/src/__tests__/guardian-expiry-notifier.test.ts +117 -13
- package/src/__tests__/guardian-gateway-sim.test.ts +119 -0
- package/src/__tests__/guardian-gateway-sim.ts +56 -20
- package/src/__tests__/guardian-reply-router-answer-mode.test.ts +6 -0
- package/src/__tests__/guardian-request-status-sync.test.ts +162 -0
- package/src/__tests__/guardian-wait-controller.test.ts +6 -0
- package/src/__tests__/headless-browser-mode.test.ts +27 -0
- package/src/__tests__/helpers/mock-conversation.ts +2 -0
- package/src/__tests__/list-messages-system-card.test.ts +70 -0
- package/src/__tests__/llm-catalog-parity.test.ts +2 -0
- package/src/__tests__/managed-profile-guard.test.ts +2 -2
- package/src/__tests__/media-resolve-image-validation.test.ts +106 -0
- package/src/__tests__/media-stream-server-integration.test.ts +6 -0
- package/src/__tests__/non-member-access-request.test.ts +12 -0
- package/src/__tests__/notification-discord-adapter.test.ts +76 -4
- package/src/__tests__/oauth-commands-routes.test.ts +21 -0
- package/src/__tests__/openai-provider.test.ts +63 -0
- package/src/__tests__/outbound-slack-persistence.test.ts +114 -2
- package/src/__tests__/persist-unsendable-image-downscale.test.ts +63 -1
- package/src/__tests__/persist-unsendable-image.test.ts +19 -0
- package/src/__tests__/plugin-app-serve-routes.test.ts +2 -2
- package/src/__tests__/plugin-import-boundary-guard.test.ts +1 -0
- package/src/__tests__/process-message-background-slack.test.ts +1 -1
- package/src/__tests__/process-message-display-content.test.ts +1 -1
- package/src/__tests__/processing-flag-persist-failure.test.ts +0 -5
- package/src/__tests__/prompt-cache-cross-turn-stability.test.ts +0 -17
- package/src/__tests__/qdrant-manager.test.ts +6 -0
- package/src/__tests__/reaction-persistence.test.ts +58 -62
- package/src/__tests__/retry-thinking-adaptive-only.test.ts +67 -0
- package/src/__tests__/server-history-render.test.ts +22 -0
- package/src/__tests__/sight-frame-retention.test.ts +362 -0
- package/src/__tests__/slack-reaction-approvals.test.ts +44 -24
- package/src/__tests__/slack-reaction-guardian-approval.test.ts +29 -1
- package/src/__tests__/subagent-fork-notifications.test.ts +2 -0
- package/src/__tests__/subagent-manager-notify.test.ts +2 -0
- package/src/__tests__/subagent-notify-parent.test.ts +2 -0
- package/src/__tests__/subagent-notify-stale-rebuild.test.ts +85 -0
- package/src/__tests__/subagent-tool-gate-mode.test.ts +4 -4
- package/src/__tests__/terminal-tools.test.ts +9 -9
- package/src/__tests__/tool-result-spool.test.ts +5 -8
- package/src/__tests__/usage-attribution.test.ts +26 -0
- package/src/acp/__tests__/acp-auth-marker-store.test.ts +191 -0
- package/src/acp/__tests__/acp-claude-oauth.test.ts +262 -0
- package/src/acp/__tests__/acp-connect-card-state.test.ts +17 -0
- package/src/acp/__tests__/helpers/acp-history-db.ts +10 -3
- package/src/acp/acp-auth-marker-store.ts +196 -0
- package/src/acp/acp-claude-oauth.ts +154 -4
- package/src/acp/acp-connect-card-state.ts +49 -8
- package/src/acp/agent-process.ts +1 -0
- package/src/acp/auto-install.ts +1 -0
- package/src/acp/client-handler.ts +1 -0
- package/src/acp/prepare-agent-env.ts +79 -1
- package/src/acp/resolve-agent.ts +12 -0
- package/src/acp/session-manager.test.ts +183 -0
- package/src/acp/session-manager.ts +110 -6
- package/src/acp/types.ts +17 -0
- package/src/agent/attachments.ts +7 -2
- package/src/agent/loop.ts +59 -1
- package/src/api/events/contact-form-closed.ts +32 -0
- package/src/api/events/contact-record-request.ts +63 -0
- package/src/api/events/contact-request.ts +10 -3
- package/src/api/index.ts +20 -0
- package/src/api/responses/conversation-message.ts +20 -0
- package/src/api/responses/home.ts +101 -0
- package/src/approvals/AGENTS.md +1 -1
- package/src/approvals/guardian-card-withdrawal.ts +127 -30
- package/src/approvals/guardian-decision-primitive.ts +9 -0
- package/src/approvals/guardian-expiry-notifier.ts +22 -16
- package/src/approvals/guardian-request-status-sync.ts +98 -0
- package/src/avatar/svg-compositor.ts +1 -1
- package/src/avatar/traits-png-sync.ts +1 -1
- package/src/bundler/app-compiler.ts +1 -0
- package/src/bundler/compiler-tools.ts +1 -0
- package/src/bundler/package-resolver.ts +1 -0
- package/src/calls/__tests__/voice-session-bridge.test.ts +183 -3
- package/src/calls/guardian-action-sweep.ts +6 -1
- package/src/calls/voice-session-bridge.ts +46 -1
- package/src/channels/__tests__/gateway-guardian-requests.test.ts +13 -18
- package/src/channels/gateway-guardian-requests.ts +37 -18
- package/src/cli/__tests__/contacts-record-prompt-cli.test.ts +329 -0
- package/src/cli/commands/__tests__/browser.test.ts +65 -1
- package/src/cli/commands/browser.ts +30 -10
- package/src/cli/commands/contacts.help.ts +173 -11
- package/src/cli/commands/contacts.ts +343 -4
- package/src/cli/commands/db/refresh.ts +1 -0
- package/src/cli/commands/oauth/index.help.ts +6 -3
- package/src/cli/commands/oauth/request.test.ts +37 -0
- package/src/cli/commands/oauth/request.ts +7 -6
- package/src/cli/lib/bundled-marketplace.json +1 -1
- package/src/cli/lib/install-from-github.ts +20 -19
- package/src/cli/lib/install-plugin-dependencies.ts +3 -5
- package/src/cli/lib/merge-plugin-tree.ts +1 -0
- package/src/cli/lib/publish-plugin.ts +4 -1
- package/src/config/__tests__/balanced-model-experiment.test.ts +63 -31
- package/src/config/__tests__/default-profile-catalog.test.ts +2 -2
- package/src/config/balanced-model-experiment.ts +15 -9
- package/src/config/bundled-skills/contacts/SKILL.md +61 -18
- package/src/config/default-profile-catalog.ts +12 -11
- package/src/config/feature-flag-registry.json +22 -13
- package/src/config/schema.ts +2 -0
- package/src/config/schemas/__tests__/live-voice.test.ts +13 -0
- package/src/config/schemas/live-voice.ts +7 -0
- package/src/config/schemas/sight.ts +45 -0
- package/src/config/skills.ts +2 -1
- package/src/contacts/__tests__/mirror-reconciler.test.ts +352 -0
- package/src/contacts/contact-store.ts +47 -4
- package/src/contacts/contacts-write.ts +5 -2
- package/src/contacts/mirror-reconciler.ts +210 -0
- package/src/contacts/mirror-tombstones.ts +42 -0
- package/src/contacts/notify-contacts-changed.ts +11 -2
- package/src/context/compactor.ts +47 -19
- package/src/context/tool-result-spool.ts +21 -14
- package/src/conversations/message-consolidation.ts +2 -2
- package/src/credential-health/credential-health-service.ts +37 -6
- package/src/daemon/conversation-agent-loop-handlers.ts +19 -2
- package/src/daemon/conversation-agent-loop.ts +100 -1
- package/src/daemon/conversation-media-retry.ts +11 -6
- package/src/daemon/conversation-messaging.ts +293 -30
- package/src/daemon/conversation-runtime-assembly.ts +67 -0
- package/src/daemon/conversation-sight-frames.ts +187 -0
- package/src/daemon/conversation-store.ts +18 -28
- package/src/daemon/conversation-surfaces.ts +12 -2
- package/src/daemon/conversation-tool-setup.ts +39 -10
- package/src/daemon/conversation.ts +159 -16
- package/src/daemon/daemon-control.ts +6 -17
- package/src/daemon/handlers/config-model.test.ts +1 -0
- package/src/daemon/handlers/conversations.ts +10 -11
- package/src/daemon/install-assistant-command.ts +1 -0
- package/src/daemon/lifecycle.ts +10 -13
- package/src/daemon/message-types/sync.ts +4 -0
- package/src/daemon/process-message.ts +2 -17
- package/src/daemon/reaction-history-render.test.ts +162 -0
- package/src/daemon/reaction-history-render.ts +115 -0
- package/src/daemon/reaction-record.test.ts +162 -0
- package/src/daemon/reaction-record.ts +83 -0
- package/src/daemon/shutdown-handlers.ts +0 -2
- package/src/daemon/summarize-boundary.ts +12 -0
- package/src/daemon/video-thumbnail.ts +1 -1
- package/src/home/feed-types.ts +4 -0
- package/src/home/feed-writer.ts +25 -0
- package/src/live-voice/__tests__/live-voice-attach-frame.test.ts +679 -0
- package/src/live-voice/__tests__/live-voice-attach-image.test.ts +8 -5
- package/src/live-voice/__tests__/live-voice-photo.test.ts +637 -1
- package/src/live-voice/__tests__/live-voice-sight-frame-inline.test.ts +787 -0
- package/src/live-voice/__tests__/live-voice-sight-frame.test.ts +347 -0
- package/src/live-voice/__tests__/live-voice-vad.test.ts +109 -0
- package/src/live-voice/live-voice-photo.ts +420 -65
- package/src/live-voice/live-voice-session.ts +311 -27
- package/src/live-voice/protocol.ts +147 -11
- package/src/messaging/provider-message-metadata.ts +7 -0
- package/src/messaging/providers/__tests__/transport-dispatch.test.ts +113 -1
- package/src/messaging/providers/channel-transport.ts +37 -0
- package/src/messaging/providers/discord/api.ts +4 -4
- package/src/messaging/providers/discord/send.test.ts +177 -2
- package/src/messaging/providers/discord/send.ts +197 -10
- package/src/messaging/providers/discord/transport.ts +10 -0
- package/src/messaging/providers/index.ts +42 -0
- package/src/messaging/providers/slack/__tests__/adapter-mention-rendering.test.ts +129 -3
- package/src/messaging/providers/slack/__tests__/auth.test.ts +35 -0
- package/src/messaging/providers/slack/__tests__/message-content.test.ts +152 -0
- package/src/messaging/providers/slack/adapter.ts +112 -27
- package/src/messaging/providers/slack/api.test.ts +10 -0
- package/src/messaging/providers/slack/auth.ts +32 -6
- package/src/messaging/providers/slack/client.ts +0 -15
- package/src/messaging/providers/slack/message-content.ts +137 -0
- package/src/messaging/providers/slack/send.test.ts +43 -2
- package/src/messaging/providers/slack/send.ts +18 -5
- package/src/messaging/providers/slack/transport.ts +10 -0
- package/src/messaging/providers/slack/types.ts +21 -2
- package/src/messaging/providers/telegram-bot/adapter.ts +5 -1
- package/src/messaging/providers/telegram-bot/send.test.ts +49 -2
- package/src/messaging/providers/telegram-bot/send.ts +41 -1
- package/src/messaging/providers/telegram-bot/transport.ts +10 -0
- package/src/messaging/providers/telegram-bot/withdraw.ts +13 -5
- package/src/messaging/reaction-envelopes.ts +79 -0
- package/src/monitoring/control.ts +2 -2
- package/src/monitoring/page-cache.ts +5 -1
- package/src/notifications/AGENTS.md +1 -1
- package/src/notifications/__tests__/guardian-feed-projection.test.ts +360 -0
- package/src/notifications/__tests__/home-feed-side-effect.test.ts +8 -0
- package/src/notifications/adapters/discord.ts +62 -9
- package/src/notifications/conversation-pairing.ts +31 -4
- package/src/notifications/emit-signal.ts +8 -5
- package/src/notifications/guardian-delivery-recorder.ts +47 -6
- package/src/notifications/guardian-feed-projection.ts +492 -0
- package/src/notifications/home-feed-side-effect.ts +59 -11
- package/src/oauth/byo-connection.test.ts +22 -0
- package/src/oauth/byo-connection.ts +11 -5
- package/src/oauth/connection.ts +3 -2
- package/src/oauth/platform-connection.test.ts +35 -0
- package/src/oauth/platform-connection.ts +18 -13
- package/src/oauth/scope-utils.ts +48 -0
- package/src/oauth/seed-providers.ts +12 -0
- package/src/outbound-proxy/certs.ts +5 -4
- package/src/permissions/question-guardian-request.test.ts +7 -1
- package/src/persistence/attachments-store.ts +134 -31
- package/src/persistence/conversation-crud.ts +137 -4
- package/src/persistence/conversation-types.ts +109 -0
- package/src/persistence/db-connection.ts +3 -21
- package/src/persistence/delivery-crud.ts +42 -12
- package/src/persistence/embeddings/__tests__/embedding-local-lifecycle.test.ts +15 -2
- package/src/persistence/embeddings/embedding-local.ts +7 -112
- package/src/persistence/embeddings/qdrant-manager.ts +4 -1
- package/src/persistence/llm-request-log-store.ts +1 -1
- package/src/persistence/migrations/370-acp-session-history-auth-error-code.ts +36 -0
- package/src/persistence/migrations/371-acp-session-history-auth-error-credential.ts +40 -0
- package/src/persistence/migrations/372-create-acp-refused-credentials.ts +34 -0
- package/src/persistence/migrations/373-acp-auth-marker-index.ts +27 -0
- package/src/persistence/migrations/__tests__/370-acp-session-history-auth-error-code.test.ts +65 -0
- package/src/persistence/migrations/__tests__/371-acp-session-history-auth-error-credential.test.ts +68 -0
- package/src/persistence/migrations/__tests__/372-create-acp-refused-credentials.test.ts +64 -0
- package/src/persistence/migrations/__tests__/373-acp-auth-marker-index.test.ts +82 -0
- package/src/persistence/schema/acp.ts +25 -0
- package/src/persistence/steps.ts +8 -0
- package/src/platform/platform-patch-queue.test.ts +43 -41
- package/src/plugin-api/index.ts +12 -0
- package/src/plugins/defaults/compaction/window-manager.ts +2 -0
- package/src/plugins/defaults/image-recovery/recover.ts +12 -1
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +166 -1
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-provider-path.test.ts +3 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-wake-chain.test.ts +3 -0
- package/src/plugins/defaults/memory/jobs-worker.ts +1 -2
- package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +53 -9
- package/src/plugins/defaults/memory/src/__tests__/memory-worker-routes.test.ts +4 -7
- package/src/plugins/defaults/memory/src/memory-worker-routes.ts +1 -1
- package/src/plugins/defaults/memory/v1/job-handlers/index-maintenance.ts +15 -1
- package/src/plugins/defaults/memory/worker-control.ts +1 -2
- package/src/providers/content-block-schema.ts +1 -0
- package/src/providers/fireworks/client.ts +9 -1
- package/src/providers/gemini/client.ts +22 -15
- package/src/providers/media-resolve.ts +24 -10
- package/src/providers/model-catalog.ts +91 -0
- package/src/providers/openai/chat-completions-provider.ts +44 -5
- package/src/providers/retry.ts +16 -11
- package/src/providers/types.ts +56 -0
- package/src/routes/__tests__/control.test.ts +4 -3
- package/src/routes/control.ts +1 -1
- package/src/routes/route-host-client.ts +1 -1
- package/src/runtime/AGENTS.md +30 -12
- package/src/runtime/__tests__/agent-wake.test.ts +71 -6
- package/src/runtime/__tests__/guardian-form-registry.test.ts +242 -0
- package/src/runtime/access-request-helper.ts +15 -7
- package/src/runtime/agent-wake.ts +16 -4
- package/src/runtime/channel-approval-types.ts +8 -5
- package/src/runtime/channel-invite-transport.ts +2 -0
- package/src/runtime/channel-invite-transports/discord.ts +27 -0
- package/src/runtime/channel-readiness-service.ts +4 -4
- package/src/runtime/channel-reply-delivery.ts +72 -28
- package/src/runtime/guardian-action-service.ts +9 -5
- package/src/runtime/guardian-form-registry.ts +256 -0
- package/src/runtime/guardian-reply-router.ts +33 -14
- package/src/runtime/invite-instruction-generator.ts +14 -2
- package/src/runtime/migrations/vbundle-metadata-merge.ts +4 -7
- package/src/runtime/no-response.ts +13 -74
- package/src/runtime/routes/__tests__/acp-routes.test.ts +527 -0
- package/src/runtime/routes/__tests__/app-management-shared-apps.test.ts +74 -0
- package/src/runtime/routes/__tests__/contact-form-kinds.test.ts +22 -0
- package/src/runtime/routes/__tests__/contact-record-prompt-route.test.ts +303 -0
- package/src/runtime/routes/__tests__/monitoring-routes.test.ts +3 -4
- package/src/runtime/routes/__tests__/pending-interactions-route.test.ts +6 -0
- package/src/runtime/routes/__tests__/route-host-worker-routes.test.ts +3 -4
- package/src/runtime/routes/__tests__/schedule-worker-routes.test.ts +5 -6
- package/src/runtime/routes/acp-routes-list.test.ts +58 -0
- package/src/runtime/routes/acp-routes.ts +253 -47
- package/src/runtime/routes/app-management-routes.ts +80 -47
- package/src/runtime/routes/approval-routes.ts +9 -0
- package/src/runtime/routes/archive-utils.ts +6 -1
- package/src/runtime/routes/avatar-routes.ts +1 -1
- package/src/runtime/routes/channel-route-shared.ts +18 -20
- package/src/runtime/routes/contact-prompt-routes.ts +271 -105
- package/src/runtime/routes/conversation-routes.ts +41 -32
- package/src/runtime/routes/credential-prompt-routes.ts +10 -1
- package/src/runtime/routes/debug-bash-routes.ts +1 -0
- package/src/runtime/routes/guardian-expiry-sweep.ts +89 -31
- package/src/runtime/routes/guardian-form-routes.ts +115 -0
- package/src/runtime/routes/inbound-message-handler.ts +82 -29
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +16 -3
- package/src/runtime/routes/inbound-stages/edit-intercept.ts +6 -0
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.test.ts +6 -0
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +6 -1
- package/src/runtime/routes/inbound-stages/reaction-intercept.test.ts +50 -17
- package/src/runtime/routes/inbound-stages/reaction-intercept.ts +97 -94
- package/src/runtime/routes/index.ts +2 -0
- package/src/runtime/routes/integrations/slack/channel.ts +39 -18
- package/src/runtime/routes/migration-routes.ts +1 -3
- package/src/runtime/routes/monitoring-routes.ts +0 -3
- package/src/runtime/routes/oauth-commands-routes.ts +18 -5
- package/src/runtime/routes/route-host-worker-routes.ts +1 -1
- package/src/runtime/routes/schedule-worker-routes.ts +1 -1
- package/src/runtime/tool-grant-request-helper.ts +4 -0
- package/src/schedule/run-script.ts +1 -0
- package/src/schedule/scheduler.ts +1 -1
- package/src/schedule/worker-control.ts +2 -2
- package/src/security/oauth2.ts +2 -11
- package/src/security/secure-keys.ts +55 -8
- package/src/skills/catalog-install.ts +7 -4
- package/src/skills/clawhub.ts +1 -0
- package/src/skills/inline-command-runner.ts +1 -0
- package/src/stt/__tests__/room-noise-floor.test.ts +108 -0
- package/src/stt/room-noise-floor.ts +140 -0
- package/src/subagent/notify.ts +49 -3
- package/src/tools/__tests__/tool-input-schemas.test.ts +2 -0
- package/src/tools/acp/spawn.ts +2 -2
- package/src/tools/browser/__tests__/browser-status.test.ts +9 -0
- package/src/tools/browser/browser-execution.ts +10 -43
- package/src/tools/browser/browser-manager.ts +1 -0
- package/src/tools/browser/browser-status-constants.ts +6 -5
- package/src/tools/browser/runtime-check.ts +2 -0
- package/src/tools/channel/react-to-message.test.ts +193 -0
- package/src/tools/channel/react-to-message.ts +159 -0
- package/src/tools/credentials/metadata-store.ts +115 -19
- package/src/tools/credentials/store.ts +0 -2
- package/src/tools/filesystem/read.ts +2 -2
- package/src/tools/host-filesystem/read.ts +2 -2
- package/src/tools/host-terminal/host-shell.test.ts +24 -0
- package/src/tools/host-terminal/host-shell.ts +11 -38
- package/src/tools/shared/filesystem/file-ops-service.ts +10 -7
- package/src/tools/shared/shell-output.test.ts +104 -0
- package/src/tools/shared/shell-output.ts +112 -39
- package/src/tools/skills/sandbox-runner.ts +1 -0
- package/src/tools/system/request-permission.test.ts +34 -0
- package/src/tools/system/request-permission.ts +29 -5
- package/src/tools/terminal/shell.test.ts +63 -0
- package/src/tools/terminal/shell.ts +12 -44
- package/src/tools/tool-input-schemas.ts +2 -0
- package/src/tools/tool-manifest.ts +2 -0
- package/src/usage/attribution.ts +12 -4
- package/src/util/__tests__/worker-ownership.test.ts +77 -0
- package/src/util/__tests__/worker-process-command.test.ts +246 -1
- package/src/util/bun-runtime.ts +44 -13
- package/src/util/bundled-asset.ts +9 -1
- package/src/util/disk-usage.ts +1 -0
- package/src/util/file-use.ts +4 -1
- package/src/util/guardian-form-timeouts.ts +50 -0
- package/src/util/image-conversion.ts +1 -0
- package/src/util/oauth-request-body.test.ts +59 -0
- package/src/util/oauth-request-body.ts +56 -5
- package/src/util/platform.ts +69 -73
- package/src/util/process-table.ts +16 -5
- package/src/util/read-stdin.ts +5 -0
- package/src/util/spawn.ts +1 -0
- package/src/util/sqlite3-runtime.ts +25 -5
- package/src/util/worker-ownership.ts +193 -0
- package/src/util/worker-process.ts +375 -37
- package/src/workspace/git-service.ts +4 -12
- package/src/workspace/migrations/010-app-dir-rename.ts +1 -0
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
setSecureKeyAsync,
|
|
19
19
|
} from "../security/secure-keys.js";
|
|
20
20
|
import { getLogger } from "../util/logger.js";
|
|
21
|
+
import { claudeTokenDigest } from "./acp-auth-marker-store.js";
|
|
21
22
|
import {
|
|
22
23
|
ACP_OAUTH_TOKEN_FIELD,
|
|
23
24
|
ACP_SERVICE,
|
|
@@ -111,6 +112,31 @@ export function parseManualClaudeCode(input: string): {
|
|
|
111
112
|
};
|
|
112
113
|
}
|
|
113
114
|
|
|
115
|
+
/**
|
|
116
|
+
* Digest of the Claude OAuth token secure storage holds right now, or
|
|
117
|
+
* `undefined` when it holds none.
|
|
118
|
+
*
|
|
119
|
+
* The credential a marker is judged against. A configured
|
|
120
|
+
* `CLAUDE_CODE_OAUTH_TOKEN` that Claude has already refused stands down at the
|
|
121
|
+
* next spawn, so once a token carries a marker the vault value is what a spawn
|
|
122
|
+
* goes on to resolve, and comparing against it answers whether the marker
|
|
123
|
+
* still describes the credential in use.
|
|
124
|
+
*
|
|
125
|
+
* Read on demand rather than cached. The vault is the authority on what it
|
|
126
|
+
* holds, and a copy of that answer in this process is a second one that has to
|
|
127
|
+
* be kept in step with every write, including writes this process did not
|
|
128
|
+
* make.
|
|
129
|
+
*
|
|
130
|
+
* Lives here rather than beside the comparison it feeds because this module is
|
|
131
|
+
* already the one authorised to read this vault field (see the importer
|
|
132
|
+
* allowlist in `credential-security-invariants`). The digest is all that
|
|
133
|
+
* leaves; the token itself never does.
|
|
134
|
+
*/
|
|
135
|
+
export async function storedClaudeTokenDigest(): Promise<string | undefined> {
|
|
136
|
+
const token = await usableStoredClaudeToken();
|
|
137
|
+
return token ? claudeTokenDigest(token) : undefined;
|
|
138
|
+
}
|
|
139
|
+
|
|
114
140
|
/**
|
|
115
141
|
* Store a captured Claude OAuth token in the `acp/claude_oauth_token` vault
|
|
116
142
|
* field and provision the policy the broker applies at spawn time: grant the
|
|
@@ -133,6 +159,66 @@ export async function storeAcpClaudeToken(token: string): Promise<void> {
|
|
|
133
159
|
ACP_OAUTH_TOKEN_FIELD,
|
|
134
160
|
ACP_CLAUDE_OAUTH_USAGE_DESCRIPTION,
|
|
135
161
|
);
|
|
162
|
+
// Again, because the write above already ran this and the policy it repairs
|
|
163
|
+
// is what decides the answer. A token whose `acp_spawn` read was denied is
|
|
164
|
+
// not usable at the moment `setSecureKeyAsync` asks, so that pass declines
|
|
165
|
+
// to retire anything, and the repair on the line above is what makes it
|
|
166
|
+
// usable. Running it a second time is safe: taking the registry is
|
|
167
|
+
// idempotent, and the invalidation is a refetch trigger.
|
|
168
|
+
//
|
|
169
|
+
// Never allowed to fail or delay the store. The token is written either way,
|
|
170
|
+
// and these are notifications: losing one costs a client a stale card until
|
|
171
|
+
// its next snapshot, while waiting on one holds the user at "signing in"
|
|
172
|
+
// while a scan finishes.
|
|
173
|
+
void notifyAcpConnectRetired().catch((err: unknown) => {
|
|
174
|
+
log.warn(
|
|
175
|
+
{ err },
|
|
176
|
+
"ACP Connect card notification failed after a token store",
|
|
177
|
+
);
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Tell the rest of the system that a usable Claude token now exists.
|
|
183
|
+
*
|
|
184
|
+
* Two things only clients and the credential prompt can act on. The card
|
|
185
|
+
* registry is in-memory and about a card on screen rather than about the
|
|
186
|
+
* credential, so no marker answers for it, and left standing the prompt keeps
|
|
187
|
+
* redirecting at a card the new token just made stale. The invalidation tells
|
|
188
|
+
* other clients to re-read the snapshot, which they otherwise would not until
|
|
189
|
+
* navigation or reconnect.
|
|
190
|
+
*
|
|
191
|
+
* Neither retires anything by itself. A card is retired by its marker no
|
|
192
|
+
* longer matching the credential a spawn would resolve, which is decided when
|
|
193
|
+
* the marker is read, so a failure here costs freshness rather than
|
|
194
|
+
* correctness.
|
|
195
|
+
*
|
|
196
|
+
* Does nothing for a value a spawn could not use. A bulk restore can land an
|
|
197
|
+
* api-key-shaped token, or one the policy blocks, and the marker comparison
|
|
198
|
+
* keeps the card up for exactly that reason.
|
|
199
|
+
*/
|
|
200
|
+
export async function notifyAcpConnectRetired(): Promise<void> {
|
|
201
|
+
if (!(await hasAcpClaudeToken())) {
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
// Dropped per conversation, and only where the failure it stands for has
|
|
205
|
+
// actually stopped being shown. A writer can store the very token Claude
|
|
206
|
+
// rejected, which passes the usability check above and changes nothing about
|
|
207
|
+
// the failure: the snapshot goes on serving that marker, so forgetting the
|
|
208
|
+
// entry would leave the credential prompt opening a second prompt beside a
|
|
209
|
+
// card that is still there. Asking the markers is the same question the
|
|
210
|
+
// snapshot answers, rather than a proxy for it.
|
|
211
|
+
const { conversationsWithRaisedAcpConnectCard, dropAcpConnectCardRaised } =
|
|
212
|
+
await import("./acp-connect-card-state.js");
|
|
213
|
+
for (const conversationId of conversationsWithRaisedAcpConnectCard()) {
|
|
214
|
+
if (!(await acpConnectCardStillWarranted(conversationId))) {
|
|
215
|
+
dropAcpConnectCardRaised(conversationId);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
const { publishSyncInvalidation } =
|
|
219
|
+
await import("../runtime/sync/sync-publisher.js");
|
|
220
|
+
const { SYNC_TAGS } = await import("../daemon/message-types/sync.js");
|
|
221
|
+
await publishSyncInvalidation([SYNC_TAGS.acpAuthRecovery]);
|
|
136
222
|
}
|
|
137
223
|
|
|
138
224
|
/**
|
|
@@ -157,14 +243,28 @@ export async function storeAcpClaudeToken(token: string): Promise<void> {
|
|
|
157
243
|
* knows nothing about Anthropic token formats.
|
|
158
244
|
*/
|
|
159
245
|
export async function hasAcpClaudeToken(): Promise<boolean> {
|
|
246
|
+
return (await usableStoredClaudeToken()) !== undefined;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* The stored Claude token, but only when a spawn would actually get it.
|
|
251
|
+
*
|
|
252
|
+
* One definition of usable for both callers. "Is the account connected" and
|
|
253
|
+
* "which credential would a spawn resolve" are the same question asked for
|
|
254
|
+
* different reasons, and answering them separately is how they drift: a value
|
|
255
|
+
* this rejects still has a digest, so a marker judged against that digest is
|
|
256
|
+
* withheld while the next spawn cannot authenticate, leaving the user with no
|
|
257
|
+
* card and no working token.
|
|
258
|
+
*/
|
|
259
|
+
async function usableStoredClaudeToken(): Promise<string | undefined> {
|
|
160
260
|
const token = await getSecureKeyAsync(
|
|
161
261
|
credentialKey(ACP_SERVICE, ACP_OAUTH_TOKEN_FIELD),
|
|
162
262
|
);
|
|
163
263
|
if (token == null || token.length === 0) {
|
|
164
|
-
return
|
|
264
|
+
return undefined;
|
|
165
265
|
}
|
|
166
266
|
if (classifyAnthropicToken(token) === "api_key") {
|
|
167
|
-
return
|
|
267
|
+
return undefined;
|
|
168
268
|
}
|
|
169
269
|
const denialReason = acpSpawnCredentialDenialReason(ACP_OAUTH_TOKEN_FIELD);
|
|
170
270
|
if (denialReason !== undefined) {
|
|
@@ -172,7 +272,57 @@ export async function hasAcpClaudeToken(): Promise<boolean> {
|
|
|
172
272
|
{ field: ACP_OAUTH_TOKEN_FIELD, reason: denialReason },
|
|
173
273
|
"Connect Claude status: token present but spawn read would be denied",
|
|
174
274
|
);
|
|
175
|
-
return
|
|
275
|
+
return undefined;
|
|
276
|
+
}
|
|
277
|
+
return token;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Whether a conversation's raised Connect card is still something to point at.
|
|
282
|
+
*
|
|
283
|
+
* Two ways a card earns its place, matching the two ways one is raised. A
|
|
284
|
+
* mid-run rejection leaves a marker, and the card stands while that marker
|
|
285
|
+
* still names the credential a spawn would resolve. A pre-spawn failure leaves
|
|
286
|
+
* no marker at all, because there was no session to record one, and its card
|
|
287
|
+
* stands while there is still no usable token to spawn with.
|
|
288
|
+
*
|
|
289
|
+
* Asked rather than remembered, so it does not matter what made the situation
|
|
290
|
+
* change. A vault write, a `config.json` edit to
|
|
291
|
+
* `acp.agents.<id>.env.CLAUDE_CODE_OAUTH_TOKEN`, or a policy repair all move
|
|
292
|
+
* the answer, and only one of those runs through a credential write. Hanging
|
|
293
|
+
* this off the write would leave the other two stale.
|
|
294
|
+
*/
|
|
295
|
+
export async function acpConnectCardStillWarranted(
|
|
296
|
+
conversationId: string,
|
|
297
|
+
): Promise<boolean> {
|
|
298
|
+
const { conversationHasCurrentAcpMarker } =
|
|
299
|
+
await import("./acp-auth-marker-store.js");
|
|
300
|
+
if (await conversationHasCurrentAcpMarker(conversationId)) {
|
|
301
|
+
return true;
|
|
302
|
+
}
|
|
303
|
+
// No marker, so this is the pre-spawn card: the failure was that a spawn
|
|
304
|
+
// found nothing to authenticate with, and it stands while that is still
|
|
305
|
+
// true. Asked through the spawn's own resolution rather than of the vault,
|
|
306
|
+
// because a user can repair it by setting the agent's configured token, and
|
|
307
|
+
// a vault-only answer would call that card warranted forever.
|
|
308
|
+
const { raisedAcpConnectCardAgent } =
|
|
309
|
+
await import("./acp-connect-card-state.js");
|
|
310
|
+
const agentId = raisedAcpConnectCardAgent(conversationId);
|
|
311
|
+
if (agentId === undefined) {
|
|
312
|
+
return !(await hasAcpClaudeToken());
|
|
313
|
+
}
|
|
314
|
+
const { resolvedClaudeCredentialDigest } =
|
|
315
|
+
await import("./prepare-agent-env.js");
|
|
316
|
+
const resolved = await resolvedClaudeCredentialDigest(agentId);
|
|
317
|
+
if (resolved === undefined) {
|
|
318
|
+
return true;
|
|
176
319
|
}
|
|
177
|
-
|
|
320
|
+
// Resolving something is not the same as having a repair. A configured
|
|
321
|
+
// token Claude already refused stands down only once the vault offers an
|
|
322
|
+
// alternative, so with no replacement stored the resolver still reports it
|
|
323
|
+
// and the next spawn rejects it exactly as this one did. Treating that as
|
|
324
|
+
// fixed drops the card and lets a second prompt open beside it.
|
|
325
|
+
const { claudeCredentialRefused } =
|
|
326
|
+
await import("./acp-auth-marker-store.js");
|
|
327
|
+
return claudeCredentialRefused(resolved);
|
|
178
328
|
}
|
|
@@ -11,23 +11,40 @@
|
|
|
11
11
|
* leaving auth unset.
|
|
12
12
|
*
|
|
13
13
|
* In-memory and ephemeral by design: a daemon restart clears it, which fails
|
|
14
|
-
* safe
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
14
|
+
* safe. The redirect simply doesn't fire and the secure prompt is shown.
|
|
15
|
+
*
|
|
16
|
+
* Cleared for real on a successful token write (`storeAcpClaudeToken`), which
|
|
17
|
+
* is the moment the cards these entries stand for stop being the right place
|
|
18
|
+
* to send anyone. Left standing, the redirect keeps pointing the model at a
|
|
19
|
+
* card for auth that already works, and the persisted history markers the
|
|
20
|
+
* entries name keep re-raising it on every reload.
|
|
19
21
|
*/
|
|
20
|
-
const conversationsWithAcpConnectCard = new
|
|
22
|
+
const conversationsWithAcpConnectCard = new Map<string, string | undefined>();
|
|
21
23
|
|
|
22
|
-
/**
|
|
24
|
+
/**
|
|
25
|
+
* Record that a Connect Claude card was raised for this conversation.
|
|
26
|
+
*
|
|
27
|
+
* The agent is kept alongside it because deciding later whether the card still
|
|
28
|
+
* stands means asking what a spawn of that agent would resolve, and precedence
|
|
29
|
+
* is per agent: a configured `CLAUDE_CODE_OAUTH_TOKEN` on one alias is not the
|
|
30
|
+
* vault and is not another alias.
|
|
31
|
+
*/
|
|
23
32
|
export function markAcpConnectCardRaised(
|
|
24
33
|
conversationId: string | undefined,
|
|
34
|
+
agentId?: string,
|
|
25
35
|
): void {
|
|
26
36
|
if (conversationId) {
|
|
27
|
-
conversationsWithAcpConnectCard.
|
|
37
|
+
conversationsWithAcpConnectCard.set(conversationId, agentId);
|
|
28
38
|
}
|
|
29
39
|
}
|
|
30
40
|
|
|
41
|
+
/** The agent whose spawn raised this conversation's card, when it was known. */
|
|
42
|
+
export function raisedAcpConnectCardAgent(
|
|
43
|
+
conversationId: string,
|
|
44
|
+
): string | undefined {
|
|
45
|
+
return conversationsWithAcpConnectCard.get(conversationId);
|
|
46
|
+
}
|
|
47
|
+
|
|
31
48
|
/** Whether a Connect Claude card has been raised for this conversation. */
|
|
32
49
|
export function hasAcpConnectCardRaised(
|
|
33
50
|
conversationId: string | undefined,
|
|
@@ -37,3 +54,27 @@ export function hasAcpConnectCardRaised(
|
|
|
37
54
|
conversationsWithAcpConnectCard.has(conversationId)
|
|
38
55
|
);
|
|
39
56
|
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Hand back every conversation that raised a card and forget them all.
|
|
60
|
+
*
|
|
61
|
+
* Take-and-clear rather than a read plus a separate reset: the caller is
|
|
62
|
+
* retiring these conversations' persisted markers, and an entry left behind
|
|
63
|
+
* after that would redirect the secure-prompt fallback at a card nothing can
|
|
64
|
+
* re-raise.
|
|
65
|
+
*/
|
|
66
|
+
export function takeConversationsWithAcpConnectCard(): string[] {
|
|
67
|
+
const conversationIds = [...conversationsWithAcpConnectCard.keys()];
|
|
68
|
+
conversationsWithAcpConnectCard.clear();
|
|
69
|
+
return conversationIds;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Forget one conversation's raised card. */
|
|
73
|
+
export function dropAcpConnectCardRaised(conversationId: string): void {
|
|
74
|
+
conversationsWithAcpConnectCard.delete(conversationId);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Every conversation currently believed to have a card up. */
|
|
78
|
+
export function conversationsWithRaisedAcpConnectCard(): string[] {
|
|
79
|
+
return [...conversationsWithAcpConnectCard.keys()];
|
|
80
|
+
}
|
package/src/acp/agent-process.ts
CHANGED
package/src/acp/auto-install.ts
CHANGED
|
@@ -31,11 +31,16 @@ import {
|
|
|
31
31
|
} from "../tools/credentials/metadata-store.js";
|
|
32
32
|
import { serverUseDenialReason } from "../tools/credentials/tool-policy.js";
|
|
33
33
|
import { getLogger } from "../util/logger.js";
|
|
34
|
+
import {
|
|
35
|
+
claudeTokenDigest,
|
|
36
|
+
claudeTokenRefusedByClaude,
|
|
37
|
+
} from "./acp-auth-marker-store.js";
|
|
34
38
|
import {
|
|
35
39
|
ACP_OAUTH_TOKEN_FIELD,
|
|
36
40
|
ACP_SERVICE,
|
|
37
41
|
classifyAnthropicToken,
|
|
38
42
|
} from "./acp-credentials.js";
|
|
43
|
+
import { lookupAcpAgentConfig } from "./resolve-agent.js";
|
|
39
44
|
import type { AcpAgentConfig } from "./types.js";
|
|
40
45
|
|
|
41
46
|
const log = getLogger("acp:prepare-agent-env");
|
|
@@ -240,6 +245,61 @@ async function injectOptionalCredential(
|
|
|
240
245
|
}
|
|
241
246
|
}
|
|
242
247
|
|
|
248
|
+
/**
|
|
249
|
+
* Whether a configured `CLAUDE_CODE_OAUTH_TOKEN` should stand down.
|
|
250
|
+
*
|
|
251
|
+
* True once Claude has refused that exact value and the vault offers a
|
|
252
|
+
* different one. Only then: with nothing to fall back to, dropping it would
|
|
253
|
+
* trade a token that fails for no token at all, and the missing-token branch
|
|
254
|
+
* would report the wrong reason.
|
|
255
|
+
*
|
|
256
|
+
* `acp-claude-oauth` is imported at call time because it reaches back into
|
|
257
|
+
* this module for the spawn policy helpers, and it is the module authorised to
|
|
258
|
+
* read this vault field, so the dependency runs one way at load and the other
|
|
259
|
+
* here.
|
|
260
|
+
*/
|
|
261
|
+
async function configuredClaudeTokenStandsDown(
|
|
262
|
+
configured: string | undefined,
|
|
263
|
+
): Promise<boolean> {
|
|
264
|
+
if (!configured || !claudeTokenRefusedByClaude(configured)) {
|
|
265
|
+
return false;
|
|
266
|
+
}
|
|
267
|
+
const { storedClaudeTokenDigest } = await import("./acp-claude-oauth.js");
|
|
268
|
+
const stored = await storedClaudeTokenDigest();
|
|
269
|
+
return stored !== undefined && stored !== claudeTokenDigest(configured);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Digest of the Claude token a spawn of `agentId` would resolve now, or
|
|
274
|
+
* `undefined` when it would find none.
|
|
275
|
+
*
|
|
276
|
+
* The credential a marker is judged against, and the same precedence
|
|
277
|
+
* `prepareAgentEnv` applies rather than a second derivation of it. A
|
|
278
|
+
* vault-only answer is wrong in both directions: a user who repairs auth by
|
|
279
|
+
* setting `acp.agents.<id>.env.CLAUDE_CODE_OAUTH_TOKEN` would keep seeing a
|
|
280
|
+
* card for a failure the next spawn will not repeat, and one whose configured
|
|
281
|
+
* token is the broken one would see none.
|
|
282
|
+
*
|
|
283
|
+
* Deliberately does not check that the agent's binary exists. A marker
|
|
284
|
+
* outlives the run that wrote it, and an uninstalled adapter says nothing
|
|
285
|
+
* about whether its credential was replaced.
|
|
286
|
+
*/
|
|
287
|
+
export async function resolvedClaudeCredentialDigest(
|
|
288
|
+
agentId: string,
|
|
289
|
+
): Promise<string | undefined> {
|
|
290
|
+
const configured =
|
|
291
|
+
lookupAcpAgentConfig(agentId)?.env?.CLAUDE_CODE_OAUTH_TOKEN;
|
|
292
|
+
const usable =
|
|
293
|
+
configured && classifyAnthropicToken(configured) !== "api_key"
|
|
294
|
+
? configured
|
|
295
|
+
: undefined;
|
|
296
|
+
if (usable && !(await configuredClaudeTokenStandsDown(usable))) {
|
|
297
|
+
return claudeTokenDigest(usable);
|
|
298
|
+
}
|
|
299
|
+
const { storedClaudeTokenDigest } = await import("./acp-claude-oauth.js");
|
|
300
|
+
return storedClaudeTokenDigest();
|
|
301
|
+
}
|
|
302
|
+
|
|
243
303
|
/**
|
|
244
304
|
* Returns a NEW config with any required credentials merged into `env`.
|
|
245
305
|
* Does NOT mutate the input. Throws `FailedDependencyError` if a required
|
|
@@ -281,6 +341,7 @@ export async function prepareAgentEnv(
|
|
|
281
341
|
// limitations on the optional `AcpAgentConfig.env` field.
|
|
282
342
|
const env: Record<string, string> = { ...(agentConfig.env ?? {}) };
|
|
283
343
|
const adapterCommand = basename(agentConfig.command);
|
|
344
|
+
let credentialDigest: string | undefined;
|
|
284
345
|
|
|
285
346
|
if (adapterCommand === "claude-agent-acp") {
|
|
286
347
|
// A config `env` override or a legacy vault entry can hold an Anthropic API
|
|
@@ -302,6 +363,12 @@ export async function prepareAgentEnv(
|
|
|
302
363
|
};
|
|
303
364
|
|
|
304
365
|
dropApiKeyOauthToken();
|
|
366
|
+
// A configured token Claude has already refused stands down in favour of
|
|
367
|
+
// the vault. Config otherwise wins, so honouring it here would resolve the
|
|
368
|
+
// same revoked value and raise the card again on every retry.
|
|
369
|
+
if (await configuredClaudeTokenStandsDown(env.CLAUDE_CODE_OAUTH_TOKEN)) {
|
|
370
|
+
delete env.CLAUDE_CODE_OAUTH_TOKEN;
|
|
371
|
+
}
|
|
305
372
|
let missReason: string | undefined;
|
|
306
373
|
if (!env.CLAUDE_CODE_OAUTH_TOKEN) {
|
|
307
374
|
missReason = await injectCredential(
|
|
@@ -318,6 +385,13 @@ export async function prepareAgentEnv(
|
|
|
318
385
|
env.CLAUDE_CODE_OAUTH_TOKEN !== undefined &&
|
|
319
386
|
classifyAnthropicToken(env.CLAUDE_CODE_OAUTH_TOKEN) === "api_key";
|
|
320
387
|
dropApiKeyOauthToken();
|
|
388
|
+
// Identity of the token this spawn will actually run with, whichever
|
|
389
|
+
// source survived the resolution above. Recorded on the history row if
|
|
390
|
+
// Claude refuses it, which is what lets the marker be compared against the
|
|
391
|
+
// credential a later spawn would resolve instead of relying on a sweep.
|
|
392
|
+
credentialDigest = env.CLAUDE_CODE_OAUTH_TOKEN
|
|
393
|
+
? claudeTokenDigest(env.CLAUDE_CODE_OAUTH_TOKEN)
|
|
394
|
+
: undefined;
|
|
321
395
|
if (!env.CLAUDE_CODE_OAUTH_TOKEN) {
|
|
322
396
|
// The operator's record of WHY the spawn has no token. `missReason` is
|
|
323
397
|
// the broker's own reason string and the rest are policy verdicts, so no
|
|
@@ -388,5 +462,9 @@ export async function prepareAgentEnv(
|
|
|
388
462
|
]);
|
|
389
463
|
}
|
|
390
464
|
|
|
391
|
-
return {
|
|
465
|
+
return {
|
|
466
|
+
...agentConfig,
|
|
467
|
+
env,
|
|
468
|
+
credentialDigest,
|
|
469
|
+
};
|
|
392
470
|
}
|
package/src/acp/resolve-agent.ts
CHANGED
|
@@ -153,6 +153,18 @@ function normalizeAgentId(id: string): string {
|
|
|
153
153
|
* id. The alias is consulted ONLY after both direct lookups miss, so a user
|
|
154
154
|
* config entry literally keyed "claude code" always wins over the alias.
|
|
155
155
|
*/
|
|
156
|
+
/**
|
|
157
|
+
* The configured agent for an id, without checking that its binary exists.
|
|
158
|
+
*
|
|
159
|
+
* `resolveAcpAgent` answers "can this be run", which a caller that only wants
|
|
160
|
+
* to know which credential the agent would use does not need and cannot always
|
|
161
|
+
* get: a marker outlives the run that wrote it, and the binary may since have
|
|
162
|
+
* gone.
|
|
163
|
+
*/
|
|
164
|
+
export function lookupAcpAgentConfig(id: string): AcpAgentConfig | undefined {
|
|
165
|
+
return lookupAgent(getConfig().acp.agents, id)?.agent;
|
|
166
|
+
}
|
|
167
|
+
|
|
156
168
|
function lookupAgent(
|
|
157
169
|
userAgents: Record<string, AcpAgentConfig>,
|
|
158
170
|
id: string,
|
|
@@ -5,7 +5,39 @@ import {
|
|
|
5
5
|
deleteConversation,
|
|
6
6
|
setConversation,
|
|
7
7
|
} from "../daemon/conversation-registry.js";
|
|
8
|
+
import { claudeTokenDigest } from "./acp-auth-marker-store.js";
|
|
8
9
|
import { hasAcpConnectCardRaised } from "./acp-connect-card-state.js";
|
|
10
|
+
|
|
11
|
+
// The credential a spawn of this agent would resolve now. The failure path
|
|
12
|
+
// asks the same question the read path does before emitting a live event, so
|
|
13
|
+
// a card is not raised into connected clients for auth that was already
|
|
14
|
+
// repaired. Spreading the real module keeps every other export intact.
|
|
15
|
+
// Spy on the refusal record. Spread the real module so every other export
|
|
16
|
+
// still resolves for the graph session-manager pulls in.
|
|
17
|
+
const refusedDigests: Array<string | undefined> = [];
|
|
18
|
+
const realMarkerStoreModule = await import("./acp-auth-marker-store.js");
|
|
19
|
+
mock.module("./acp-auth-marker-store.js", () => ({
|
|
20
|
+
...realMarkerStoreModule,
|
|
21
|
+
noteClaudeTokenRefused: (digest: string | undefined) => {
|
|
22
|
+
refusedDigests.push(digest);
|
|
23
|
+
},
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
let fakeResolvedCredential: string | undefined;
|
|
27
|
+
// Held open by a test that needs to land a teardown inside the window this
|
|
28
|
+
// read suspends the failure handler for.
|
|
29
|
+
let resolveGate: Promise<void> | undefined;
|
|
30
|
+
const realPrepareAgentEnv = await import("./prepare-agent-env.js");
|
|
31
|
+
mock.module("./prepare-agent-env.js", () => ({
|
|
32
|
+
...realPrepareAgentEnv,
|
|
33
|
+
resolvedClaudeCredentialDigest: async () => {
|
|
34
|
+
if (resolveGate) {
|
|
35
|
+
await resolveGate;
|
|
36
|
+
}
|
|
37
|
+
return fakeResolvedCredential;
|
|
38
|
+
},
|
|
39
|
+
}));
|
|
40
|
+
|
|
9
41
|
import { VellumAcpClientHandler } from "./client-handler.js";
|
|
10
42
|
import { AcpSessionManager } from "./session-manager.js";
|
|
11
43
|
|
|
@@ -331,6 +363,7 @@ describe("AcpSessionManager auth-required recovery surface", () => {
|
|
|
331
363
|
command: string;
|
|
332
364
|
parentToolUseId?: string;
|
|
333
365
|
cancelled?: boolean;
|
|
366
|
+
credentialDigest?: string;
|
|
334
367
|
}) {
|
|
335
368
|
const manager = new AcpSessionManager(1);
|
|
336
369
|
const parentId = `parent-${opts.id}`;
|
|
@@ -350,6 +383,10 @@ describe("AcpSessionManager auth-required recovery surface", () => {
|
|
|
350
383
|
if (opts.cancelled) {
|
|
351
384
|
entry.state.status = "cancelled";
|
|
352
385
|
}
|
|
386
|
+
if (opts.credentialDigest !== undefined) {
|
|
387
|
+
(entry as { credentialDigest?: string }).credentialDigest =
|
|
388
|
+
opts.credentialDigest;
|
|
389
|
+
}
|
|
353
390
|
|
|
354
391
|
await fire(manager, opts.id, entry).catch(() => {});
|
|
355
392
|
if (!opts.cancelled) {
|
|
@@ -366,9 +403,65 @@ describe("AcpSessionManager auth-required recovery surface", () => {
|
|
|
366
403
|
parentId,
|
|
367
404
|
authEvent: events.find((e) => e.type === "acp_auth_required"),
|
|
368
405
|
persistedContent: firstPersist?.[0].content,
|
|
406
|
+
persistedAuthErrorCredential: (
|
|
407
|
+
entry.state as { authErrorCredential?: string }
|
|
408
|
+
).authErrorCredential,
|
|
369
409
|
};
|
|
370
410
|
}
|
|
371
411
|
|
|
412
|
+
test("no live event when the credential was already replaced", async () => {
|
|
413
|
+
// A replacement token and its invalidation can both land before an older
|
|
414
|
+
// run reports its rejection. The card the live event raises deliberately
|
|
415
|
+
// skips connected-state self-healing, so nothing would retire it until the
|
|
416
|
+
// next snapshot, which may be a navigation away.
|
|
417
|
+
fakeResolvedCredential = claudeTokenDigest("sk-ant-oat-replacement");
|
|
418
|
+
const r = await driveAuthFailure({
|
|
419
|
+
id: "sess-auth-replaced",
|
|
420
|
+
command: "claude-agent-acp",
|
|
421
|
+
parentToolUseId: "tool-anchor-replaced",
|
|
422
|
+
credentialDigest: claudeTokenDigest("sk-ant-oat-refused"),
|
|
423
|
+
});
|
|
424
|
+
fakeResolvedCredential = undefined;
|
|
425
|
+
|
|
426
|
+
expect(r.authEvent).toBeUndefined();
|
|
427
|
+
expect(hasAcpConnectCardRaised(r.parentId)).toBe(false);
|
|
428
|
+
// The guidance follows the same predicate: pointing the model at a card
|
|
429
|
+
// that was never raised sends it to an affordance that does not exist.
|
|
430
|
+
expect(r.persistedContent).not.toContain("Connect Claude Code");
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
test("still raises when the run holds the credential a spawn would resolve", async () => {
|
|
434
|
+
const digest = claudeTokenDigest("sk-ant-oat-still-current");
|
|
435
|
+
fakeResolvedCredential = digest;
|
|
436
|
+
const r = await driveAuthFailure({
|
|
437
|
+
id: "sess-auth-current",
|
|
438
|
+
command: "claude-agent-acp",
|
|
439
|
+
parentToolUseId: "tool-anchor-current",
|
|
440
|
+
credentialDigest: digest,
|
|
441
|
+
});
|
|
442
|
+
fakeResolvedCredential = undefined;
|
|
443
|
+
|
|
444
|
+
expect(r.authEvent).toBeDefined();
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
test("the marker names the credential the run was refused on", async () => {
|
|
448
|
+
// The failure path does not judge whether the rejection still matters. It
|
|
449
|
+
// records what was refused, and the read path compares that against the
|
|
450
|
+
// credential a spawn would resolve. Deciding here would mean guessing
|
|
451
|
+
// about writes that have not finished, and guessing toward suppression
|
|
452
|
+
// spends the rejection that would have raised the card.
|
|
453
|
+
const digest = claudeTokenDigest("sk-ant-oat-refused");
|
|
454
|
+
const r = await driveAuthFailure({
|
|
455
|
+
id: "sess-auth-credential",
|
|
456
|
+
command: "claude-agent-acp",
|
|
457
|
+
parentToolUseId: "tool-anchor-credential",
|
|
458
|
+
credentialDigest: digest,
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
expect(r.authEvent).toBeDefined();
|
|
462
|
+
expect(r.persistedAuthErrorCredential).toBe(digest);
|
|
463
|
+
});
|
|
464
|
+
|
|
372
465
|
test("claude failure with an anchor raises the full surface: event, registry mark, guidance", async () => {
|
|
373
466
|
const r = await driveAuthFailure({
|
|
374
467
|
id: "sess-auth-anchor",
|
|
@@ -429,3 +522,93 @@ describe("AcpSessionManager auth-required recovery surface", () => {
|
|
|
429
522
|
expect(hasAcpConnectCardRaised(r.parentId)).toBe(false);
|
|
430
523
|
});
|
|
431
524
|
});
|
|
525
|
+
|
|
526
|
+
describe("AcpSessionManager: teardown during the credential read", () => {
|
|
527
|
+
test("a session torn down mid-read is left alone", async () => {
|
|
528
|
+
// The credential read suspends the failure handler. A cancel landing in
|
|
529
|
+
// that window runs to completion first: it persists the cancelled row,
|
|
530
|
+
// drops the event buffer and removes the session. Carrying on would emit a
|
|
531
|
+
// card for a run the user stopped, and persist a second time over a buffer
|
|
532
|
+
// that is already gone, replacing the stored event log with an empty one.
|
|
533
|
+
const revoked = () =>
|
|
534
|
+
Promise.reject(
|
|
535
|
+
new Error(
|
|
536
|
+
"Internal error: Failed to authenticate. API Error: 401 OAuth access token has been revoked.",
|
|
537
|
+
),
|
|
538
|
+
);
|
|
539
|
+
const manager = new AcpSessionManager(1);
|
|
540
|
+
const parentId = "parent-torn-down";
|
|
541
|
+
const { conversation } = mockConversation();
|
|
542
|
+
setConversation(parentId, conversation);
|
|
543
|
+
registered.push(parentId);
|
|
544
|
+
|
|
545
|
+
const entry = injectSession(
|
|
546
|
+
manager,
|
|
547
|
+
"sess-torn-down",
|
|
548
|
+
parentId,
|
|
549
|
+
fakeProcess(revoked),
|
|
550
|
+
);
|
|
551
|
+
entry.command = "claude-agent-acp";
|
|
552
|
+
(entry as { parentToolUseId?: string }).parentToolUseId = "tool-torn";
|
|
553
|
+
|
|
554
|
+
let openGate = () => {};
|
|
555
|
+
resolveGate = new Promise<void>((resolve) => {
|
|
556
|
+
openGate = resolve;
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
const running = fire(manager, "sess-torn-down", entry).catch(() => {});
|
|
560
|
+
// Let the handler reach the suspended read, then take the session out from
|
|
561
|
+
// under it exactly as a cancel would.
|
|
562
|
+
await new Promise((r) => setTimeout(r, 5));
|
|
563
|
+
(manager as unknown as { sessions: Map<string, unknown> }).sessions.delete(
|
|
564
|
+
"sess-torn-down",
|
|
565
|
+
);
|
|
566
|
+
openGate();
|
|
567
|
+
await running;
|
|
568
|
+
resolveGate = undefined;
|
|
569
|
+
|
|
570
|
+
const events = (
|
|
571
|
+
entry.sendToVellum as ReturnType<typeof mock>
|
|
572
|
+
).mock.calls.map((c) => c[0] as { type: string });
|
|
573
|
+
expect(events.find((e) => e.type === "acp_auth_required")).toBeUndefined();
|
|
574
|
+
});
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
describe("AcpSessionManager.spawn: a credential Claude refused at startup", () => {
|
|
578
|
+
test("records the refusal, so the HTTP route gets it too", async () => {
|
|
579
|
+
// The record lives here rather than at the tool boundary because
|
|
580
|
+
// `POST /v1/acp/spawn` reaches this method directly. Without it a
|
|
581
|
+
// configured CLAUDE_CODE_OAUTH_TOKEN that Claude refused keeps winning
|
|
582
|
+
// over whatever Connect stores, and every later spawn repeats the failure.
|
|
583
|
+
const { AcpAgentProcess: Process } = await import("./agent-process.js");
|
|
584
|
+
const { AcpAuthRequiredError: AuthError } =
|
|
585
|
+
await import("./auth-required.js");
|
|
586
|
+
const originalInitialize = Process.prototype.initialize;
|
|
587
|
+
Process.prototype.initialize = async () => {
|
|
588
|
+
throw new AuthError("claude", "Authentication required");
|
|
589
|
+
};
|
|
590
|
+
refusedDigests.length = 0;
|
|
591
|
+
|
|
592
|
+
try {
|
|
593
|
+
const manager = new AcpSessionManager(5);
|
|
594
|
+
await manager
|
|
595
|
+
.spawn(
|
|
596
|
+
"claude",
|
|
597
|
+
{
|
|
598
|
+
command: "echo",
|
|
599
|
+
args: ["hi"],
|
|
600
|
+
credentialDigest: "digest-refused-at-startup",
|
|
601
|
+
},
|
|
602
|
+
"do something",
|
|
603
|
+
"/tmp",
|
|
604
|
+
"conv-startup-auth",
|
|
605
|
+
() => {},
|
|
606
|
+
)
|
|
607
|
+
.catch(() => {});
|
|
608
|
+
} finally {
|
|
609
|
+
Process.prototype.initialize = originalInitialize;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
expect(refusedDigests).toContain("digest-refused-at-startup");
|
|
613
|
+
});
|
|
614
|
+
});
|