@vellumai/assistant 0.11.4 → 0.11.5-staging.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +2 -2
- package/ARCHITECTURE.md +29 -4
- package/README.md +1 -1
- package/docs/architecture/memory.md +17 -5
- package/docs/architecture/security.md +96 -152
- package/docs/guardian-request-flow.md +13 -2
- 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/__tests__/url-normalization.test.ts +135 -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/url-normalization.ts +107 -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/__tests__/url-normalization.test.ts +135 -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/url-normalization.ts +107 -0
- package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +166 -1
- package/node_modules/@vellumai/gateway-client/src/http-delivery.ts +7 -14
- package/node_modules/@vellumai/gateway-client/src/index.ts +2 -0
- package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +29 -31
- package/node_modules/@vellumai/service-contracts/package.json +1 -0
- package/node_modules/@vellumai/service-contracts/src/__tests__/url-normalization.test.ts +135 -0
- package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/service-contracts/src/url-normalization.ts +107 -0
- package/openapi.yaml +193 -3
- package/package.json +1 -1
- package/src/__tests__/anthropic-provider.test.ts +10 -3
- package/src/__tests__/approval-interception-trust-gates.test.ts +40 -0
- package/src/__tests__/background-workers-disk-pressure.test.ts +1 -1
- package/src/__tests__/channel-approval-routes.test.ts +4 -0
- package/src/__tests__/channel-inbound-disk-pressure.test.ts +5 -6
- package/src/__tests__/channel-reply-delivery.test.ts +26 -13
- package/src/__tests__/checker.test.ts +20 -314
- package/src/__tests__/cli-memory-v2-reembed-skills.test.ts +6 -2
- package/src/__tests__/client-os-metadata-persistence.test.ts +16 -0
- package/src/__tests__/compactor-retained-image-validation.test.ts +142 -0
- package/src/__tests__/config-loader-backfill.test.ts +19 -10
- package/src/__tests__/container-cpu-sampler.test.ts +153 -0
- package/src/__tests__/context-search-memory-v2-source.test.ts +0 -1
- package/src/__tests__/conversation-attachments.test.ts +0 -1
- package/src/__tests__/conversation-error.test.ts +17 -0
- package/src/__tests__/conversation-lifecycle.test.ts +20 -26
- package/src/__tests__/conversation-pairing.test.ts +186 -0
- package/src/__tests__/conversation-runtime-assembly.test.ts +21 -2
- package/src/__tests__/credential-broker-server-use.test.ts +24 -5
- package/src/__tests__/credential-routes.test.ts +22 -3
- package/src/__tests__/credential-security-invariants.test.ts +2 -1
- package/src/__tests__/daemon-credential-client.test.ts +88 -0
- package/src/__tests__/default-plugin-names-guard.test.ts +12 -1
- package/src/__tests__/dm-backfill.test.ts +63 -0
- package/src/__tests__/dynamic-skill-background-guard.test.ts +0 -2
- package/src/__tests__/edit-propagation.test.ts +107 -4
- package/src/__tests__/events-client-registration.test.ts +28 -0
- package/src/__tests__/file-write-tool.test.ts +4 -2
- package/src/__tests__/guardian-prompt-notice-privacy.test.ts +133 -0
- package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +143 -97
- package/src/__tests__/helpers/gateway-classify-mock.ts +27 -6
- package/src/__tests__/host-proxy-interface.test.ts +11 -1
- package/src/__tests__/host-shell-tool.test.ts +23 -4
- package/src/__tests__/image-conversion.test.ts +143 -1
- package/src/__tests__/inline-skill-load-permissions.test.ts +47 -36
- package/src/__tests__/input-repairs.test.ts +207 -0
- package/src/__tests__/live-workspace-guard.test.ts +75 -0
- package/src/__tests__/managed-profile-guard.test.ts +4 -2
- package/src/__tests__/mcp-abort-signal.test.ts +1 -1
- package/src/__tests__/mcp-client-auth.test.ts +1 -1
- package/src/__tests__/mcp-tool-annotations-risk.test.ts +1 -1
- package/src/__tests__/media-resolve-image-validation.test.ts +310 -0
- package/src/__tests__/mtime-cache.test.ts +2 -0
- package/src/__tests__/notification-vellum-adapter.test.ts +124 -1
- package/src/__tests__/openai-provider.test.ts +22 -0
- package/src/__tests__/personal-memory-auth-bypass.test.ts +22 -6
- package/src/__tests__/platform-bash-auto-approve.test.ts +0 -4
- package/src/__tests__/platform.test.ts +16 -1
- package/src/__tests__/plugin-api-resolve-credential.test.ts +22 -13
- package/src/__tests__/plugin-api-shim.test.ts +5 -0
- package/src/__tests__/plugin-api-store-credential.test.ts +268 -0
- package/src/__tests__/plugin-config-data-migration.test.ts +8 -1
- package/src/__tests__/plugin-disabled-state.test.ts +2 -0
- package/src/__tests__/plugin-effective-enabled-set.test.ts +55 -3
- package/src/__tests__/plugin-execution-context.test.ts +73 -0
- package/src/__tests__/plugin-import-boundary-guard.test.ts +0 -1
- package/src/__tests__/plugin-import-boundary-reverse-guard.test.ts +0 -1
- package/src/__tests__/reaction-persistence.test.ts +200 -7
- package/src/__tests__/require-fresh-approval.test.ts +0 -4
- package/src/__tests__/resource-pressure-guard.test.ts +428 -0
- package/src/__tests__/resource-pressure-routes.test.ts +113 -0
- package/src/__tests__/risk-classification-boundary-guard.test.ts +115 -0
- package/src/__tests__/run-conversation-turn-persistence.test.ts +27 -1
- package/src/__tests__/secret-routes-platform-proxy.test.ts +7 -0
- package/src/__tests__/skill-tool-factory.test.ts +161 -0
- package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +1 -1
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +132 -5
- package/src/__tests__/tool-executor.test.ts +63 -38
- package/src/__tests__/tool-policy.test.ts +97 -1
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +7 -4
- package/src/__tests__/user-plugin-loader.test.ts +2 -0
- package/src/__tests__/validate-input.test.ts +243 -7
- package/src/__tests__/verification-control-plane-policy.test.ts +0 -2
- package/src/__tests__/voice-scoped-grant-consumer.test.ts +2 -0
- package/src/__tests__/voice-session-bridge.test.ts +42 -0
- package/src/acp/__tests__/acp-claude-oauth.test.ts +97 -29
- package/src/acp/__tests__/prepare-agent-env.test.ts +376 -153
- package/src/acp/acp-claude-oauth.ts +38 -21
- package/src/acp/prepare-agent-env.ts +146 -55
- package/src/agent/loop-tool-dedup.test.ts +161 -0
- package/src/agent/loop.ts +68 -22
- package/src/api/constants/app-tools.ts +34 -0
- package/src/api/events/resource-pressure-status-changed.ts +53 -0
- package/src/api/index.ts +19 -0
- package/src/api/responses/resource-pressure-status.ts +25 -0
- package/src/approvals/guardian-channel-delivery.ts +70 -6
- package/src/approvals/guardian-request-resolvers.ts +32 -66
- package/src/calls/__tests__/voice-session-bridge.test.ts +114 -0
- package/src/calls/voice-session-bridge.ts +60 -6
- package/src/channels/__tests__/message-audience.test.ts +49 -0
- package/src/channels/__tests__/types.test.ts +17 -3
- package/src/channels/message-audience.ts +40 -0
- package/src/channels/types.ts +16 -12
- package/src/cli/__tests__/catalog-search-help.test.ts +50 -0
- package/src/cli/commands/__tests__/channel-verification-sessions.test.ts +31 -0
- package/src/cli/commands/__tests__/conversations-search.test.ts +289 -0
- package/src/cli/commands/__tests__/conversations-slack.test.ts +1 -0
- package/src/cli/commands/__tests__/inference-providers.test.ts +68 -0
- package/src/cli/commands/__tests__/keys.test.ts +89 -8
- package/src/cli/commands/__tests__/plugins.test.ts +57 -1
- package/src/cli/commands/channel-verification-sessions.help.ts +11 -9
- package/src/cli/commands/channel-verification-sessions.ts +11 -21
- package/src/cli/commands/conversations.help.ts +34 -0
- package/src/cli/commands/conversations.ts +125 -0
- package/src/cli/commands/inference-providers.ts +9 -4
- package/src/cli/commands/inference.help.ts +9 -4
- package/src/cli/commands/keys.help.ts +13 -1
- package/src/cli/commands/keys.ts +26 -15
- package/src/cli/commands/memory/__tests__/memory-v2.test.ts +57 -7
- package/src/cli/commands/memory/index.help.ts +41 -27
- package/src/cli/commands/memory/index.ts +2 -0
- package/src/cli/commands/memory/memory-v2.ts +58 -54
- package/src/cli/commands/memory/memory-validate.ts +18 -0
- package/src/cli/commands/monitoring.ts +1 -0
- package/src/cli/commands/plugins.help.ts +28 -31
- package/src/cli/commands/plugins.ts +10 -0
- package/src/cli/commands/skills.help.ts +13 -16
- package/src/cli/commands/trust.ts +3 -14
- package/src/cli/lib/__tests__/install-from-github.test.ts +38 -0
- package/src/cli/lib/__tests__/merge-plugin-tree.test.ts +34 -0
- package/src/cli/lib/__tests__/plugin-surfaces.test.ts +32 -1
- package/src/cli/lib/__tests__/toggle-plugin.test.ts +2 -1
- package/src/cli/lib/__tests__/upgrade-plugin.test.ts +64 -0
- package/src/cli/lib/bundled-marketplace.json +13 -0
- package/src/cli/lib/daemon-credential-client.ts +25 -3
- package/src/cli/lib/install-from-github.ts +35 -24
- package/src/cli/lib/merge-plugin-tree.ts +22 -4
- package/src/cli/lib/plugin-surfaces.ts +27 -0
- package/src/config/__tests__/balanced-model-experiment.test.ts +7 -7
- package/src/config/__tests__/default-profile-catalog.test.ts +3 -3
- package/src/config/default-profile-catalog.ts +5 -6
- package/src/config/feature-flag-registry.json +32 -32
- package/src/config/webhook-routing.ts +8 -0
- package/src/context/compactor.ts +31 -2
- package/src/daemon/__tests__/provider-rejection-log-fields.test.ts +272 -0
- package/src/daemon/conversation-agent-loop-handlers.ts +4 -1
- package/src/daemon/conversation-error.ts +13 -0
- package/src/daemon/conversation-messaging.ts +22 -2
- package/src/daemon/conversation-process.ts +11 -0
- package/src/daemon/conversation-store.ts +10 -0
- package/src/daemon/conversation-surfaces.ts +21 -8
- package/src/daemon/conversation.ts +12 -10
- package/src/daemon/lifecycle.ts +6 -2
- package/src/daemon/message-types/conversations.ts +5 -7
- package/src/daemon/provider-rejection-log-fields.ts +124 -0
- package/src/daemon/resource-pressure-guard-lifecycle.ts +66 -0
- package/src/daemon/resource-pressure-guard.ts +387 -0
- package/src/daemon/shutdown-handlers.ts +2 -0
- package/src/daemon/startup-error.ts +16 -0
- package/src/daemon/trust-context.ts +14 -10
- package/src/daemon/unsendable-image-notice.ts +76 -0
- package/src/hooks/registry.ts +65 -51
- package/src/ipc/__tests__/socket-path.test.ts +15 -7
- package/src/ipc/gateway-client.test.ts +1 -1
- package/src/ipc/gateway-client.ts +14 -21
- package/src/ipc/socket-cleanup.ts +2 -11
- package/src/mcp/__tests__/mcp-auth-orchestrator.test.ts +1 -1
- package/src/messaging/provider-message-metadata.ts +128 -0
- package/src/messaging/providers/__tests__/transport-dispatch.test.ts +116 -68
- package/src/messaging/providers/channel-transport.ts +90 -13
- package/src/messaging/providers/discord/send.ts +16 -0
- package/src/messaging/providers/discord/transport.ts +11 -1
- package/src/messaging/providers/index.ts +109 -16
- package/src/messaging/providers/slack/message-metadata.ts +45 -0
- package/src/messaging/providers/slack/render-transcript.ts +1 -4
- package/src/messaging/providers/slack/send.test.ts +6 -9
- package/src/messaging/providers/slack/send.ts +40 -28
- package/src/messaging/providers/slack/transport.ts +22 -26
- package/src/messaging/providers/telegram-bot/send.test.ts +2 -8
- package/src/messaging/providers/telegram-bot/transport.ts +3 -6
- package/src/messaging/read-provider-metadata.test.ts +157 -0
- package/src/messaging/read-provider-metadata.ts +51 -0
- package/src/notifications/__tests__/assistant-reply-producer.test.ts +94 -0
- package/src/notifications/__tests__/edit-notification.test.ts +174 -2
- package/src/notifications/__tests__/home-feed-side-effect.test.ts +536 -4
- package/src/notifications/adapters/macos.ts +55 -0
- package/src/notifications/adapters/slack.ts +10 -5
- package/src/notifications/assistant-reply-producer.ts +37 -4
- package/src/notifications/conversation-pairing.ts +111 -21
- package/src/notifications/edit-notification.ts +38 -8
- package/src/notifications/emit-signal.ts +12 -14
- package/src/notifications/home-feed-side-effect.ts +218 -15
- package/src/notifications/types.ts +5 -0
- package/src/permissions/AGENTS.md +16 -0
- package/src/permissions/checker.test.ts +75 -122
- package/src/permissions/checker.ts +44 -560
- package/src/permissions/confirmation-guardian-request.test.ts +28 -3
- package/src/permissions/confirmation-guardian-request.ts +5 -1
- package/src/persistence/conversation-crud.ts +23 -20
- package/src/persistence/conversation-queries.ts +14 -1
- package/src/persistence/conversation-types.ts +22 -0
- package/src/persistence/delivery-crud.ts +122 -15
- package/src/plugin-api/__tests__/import-graph-partial-mock.test.ts +67 -0
- package/src/plugin-api/conversation-turn.ts +27 -0
- package/src/plugin-api/credential-scope.test.ts +15 -0
- package/src/plugin-api/credential-scope.ts +14 -0
- package/src/plugin-api/index.ts +19 -1
- package/src/plugin-api/resolve-credential.ts +15 -10
- package/src/plugin-api/store-credential.ts +148 -0
- package/src/plugin-api/system-card.ts +36 -0
- package/src/plugin-api/vision-support.test.ts +82 -0
- package/src/plugin-api/vision-support.ts +14 -2
- package/src/plugins/defaults/image-fallback/__tests__/image-fallback.test.ts +361 -110
- package/src/plugins/defaults/image-fallback/__tests__/vision-recovery.test.ts +4 -0
- package/src/plugins/defaults/image-fallback/hooks/user-prompt-submit.ts +58 -0
- package/src/plugins/defaults/image-fallback/src/caption-blocks.ts +64 -23
- package/src/plugins/defaults/image-recovery/detect.ts +24 -1
- package/src/plugins/defaults/main.ts +15 -0
- package/src/plugins/defaults/memory/AGENTS.md +11 -3
- package/src/plugins/defaults/memory/__tests__/memory-tier-boundary-guard.test.ts +0 -1
- package/src/plugins/defaults/memory/graph-topology/pending-buffer.ts +9 -12
- package/src/plugins/defaults/memory/memory-retrospective-prompt.ts +1 -1
- package/src/plugins/defaults/memory/src/memory-v2-routes.ts +12 -10
- package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +114 -0
- package/src/plugins/defaults/memory/substrate/__tests__/consolidation-prompt-flag-gating-guard.test.ts +8 -0
- package/src/plugins/defaults/memory/substrate/__tests__/edge-index.test.ts +0 -30
- package/src/plugins/defaults/memory/substrate/__tests__/ingest.test.ts +73 -1
- package/src/plugins/defaults/memory/substrate/__tests__/page-index.test.ts +37 -0
- package/src/plugins/defaults/memory/substrate/__tests__/page-links.test.ts +208 -0
- package/src/plugins/defaults/memory/substrate/__tests__/prompts-consolidation.test.ts +158 -1
- package/src/plugins/defaults/memory/substrate/__tests__/static-context.test.ts +1 -61
- package/src/plugins/defaults/memory/substrate/consolidation-job.ts +68 -6
- package/src/plugins/defaults/memory/substrate/edge-index.ts +0 -31
- package/src/plugins/defaults/memory/substrate/ingest.ts +59 -0
- package/src/plugins/defaults/memory/substrate/page-index.ts +35 -8
- package/src/plugins/defaults/memory/substrate/page-links.ts +133 -0
- package/src/plugins/defaults/memory/substrate/page-store.ts +10 -0
- package/src/plugins/defaults/memory/substrate/prompts/consolidation.ts +124 -34
- package/src/plugins/defaults/memory/substrate/static-context.ts +0 -29
- package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +3 -1
- package/src/plugins/defaults/memory/v3/card.ts +9 -9
- package/src/plugins/defaults/memory/v3/core-set.test.ts +7 -0
- package/src/plugins/defaults/memory/v3/core-set.ts +5 -1
- package/src/plugins/defaults/memory/v3/edge.ts +13 -57
- package/src/plugins/mtime-cache.ts +1 -1
- package/src/plugins/pipeline.ts +14 -7
- package/src/plugins/plugin-execution-context.ts +35 -11
- package/src/plugins/plugin-tree-walk.ts +63 -4
- package/src/providers/anthropic/__tests__/pause-turn-continuation.test.ts +170 -0
- package/src/providers/anthropic/client.ts +332 -241
- package/src/providers/connection-resolution.ts +2 -1
- package/src/providers/content-blocks.ts +22 -0
- package/src/providers/gemini/client.ts +5 -2
- package/src/providers/inference/__tests__/adapter-factory-litellm.test.ts +47 -1
- package/src/providers/inference/__tests__/adapter-factory-ollama.test.ts +83 -0
- package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +98 -1
- package/src/providers/inference/__tests__/base-url-route-validation.test.ts +38 -0
- package/src/providers/inference/__tests__/base-url-security.test.ts +10 -0
- package/src/providers/inference/__tests__/connections-ollama.test.ts +90 -0
- package/src/providers/inference/__tests__/missing-credential-guard.test.ts +117 -0
- package/src/providers/inference/adapter-factory.ts +33 -2
- package/src/providers/inference/auth.ts +13 -2
- package/src/providers/inference/credential-usage.ts +37 -0
- package/src/providers/inference/missing-credential-guard.ts +110 -0
- package/src/providers/inference/resolve-auth.ts +7 -7
- package/src/providers/media-resolve.ts +176 -15
- package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +576 -46
- package/src/providers/openai/__tests__/tool-choice-mapping.test.ts +125 -2
- package/src/providers/openai/chat-completions-provider.ts +322 -42
- package/src/routes/route-host-protocol.ts +7 -0
- package/src/routes/worker.ts +31 -10
- package/src/runtime/AGENTS.md +35 -0
- package/src/runtime/__tests__/runtime-http-port-collision.test.ts +131 -0
- package/src/runtime/__tests__/web-presence.test.ts +234 -0
- package/src/runtime/assistant-event-hub.ts +38 -0
- package/src/runtime/channel-reply-delivery.ts +54 -31
- package/src/runtime/channel-retry-sweep.ts +6 -4
- package/src/runtime/effective-capabilities.test.ts +19 -0
- package/src/runtime/effective-capabilities.ts +25 -0
- package/src/runtime/guardian-reply-router.ts +6 -2
- package/src/runtime/http-errors.ts +1 -0
- package/src/runtime/http-server.ts +20 -5
- package/src/runtime/routes/__tests__/client-routes.test.ts +150 -0
- package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +133 -0
- package/src/runtime/routes/__tests__/credential-delete-in-use.test.ts +155 -0
- package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +52 -1
- package/src/runtime/routes/__tests__/monitoring-routes.test.ts +1 -0
- package/src/runtime/routes/__tests__/pending-interactions-route.test.ts +175 -0
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +88 -35
- package/src/runtime/routes/__tests__/surface-action-routes.test.ts +55 -6
- package/src/runtime/routes/__tests__/system-card-turn-termination.test.ts +107 -0
- package/src/runtime/routes/__tests__/user-route-dispatcher-host.test.ts +52 -1
- package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +75 -4
- package/src/runtime/routes/approval-routes.ts +37 -4
- package/src/runtime/routes/approval-strategies/guardian-text-engine-strategy.ts +16 -22
- package/src/runtime/routes/canned-message-complete.ts +68 -29
- package/src/runtime/routes/client-routes.ts +113 -0
- package/src/runtime/routes/conversation-list-routes.ts +22 -7
- package/src/runtime/routes/conversation-management-routes.ts +15 -9
- package/src/runtime/routes/conversation-routes.ts +36 -22
- package/src/runtime/routes/credential-in-use.ts +85 -0
- package/src/runtime/routes/credential-routes.ts +55 -85
- package/src/runtime/routes/guardian-approval-interception.ts +22 -22
- package/src/runtime/routes/guardian-approval-reply-helpers.ts +14 -12
- package/src/runtime/routes/identity-routes.ts +5 -121
- package/src/runtime/routes/inbound-message-handler.ts +71 -27
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +16 -17
- package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +77 -50
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +61 -97
- package/src/runtime/routes/inbound-stages/edit-intercept.ts +101 -77
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +17 -8
- package/src/runtime/routes/inbound-stages/reaction-intercept.test.ts +91 -7
- package/src/runtime/routes/inbound-stages/reaction-intercept.ts +72 -70
- package/src/runtime/routes/index.ts +2 -0
- package/src/runtime/routes/inference-provider-connection-routes.ts +9 -7
- package/src/runtime/routes/monitoring-routes.ts +9 -1
- package/src/runtime/routes/plugins-routes.ts +17 -5
- package/src/runtime/routes/resource-pressure-routes.ts +22 -0
- package/src/runtime/routes/secret-routes.ts +39 -8
- package/src/runtime/routes/settings-routes.ts +6 -6
- package/src/runtime/routes/surface-action-routes.ts +20 -19
- package/src/runtime/routes/user-route-dispatcher.ts +31 -2
- package/src/runtime/routes/user-route-resolution.ts +18 -1
- package/src/runtime/slack-reply-session.test.ts +23 -13
- package/src/runtime/slack-reply-session.ts +38 -55
- package/src/runtime/web-presence.ts +90 -0
- package/src/skills/validate-input.ts +177 -40
- package/src/subagent/__tests__/consult-context-gating.test.ts +6 -10
- package/src/subagent/consult-context.ts +7 -23
- package/src/tools/credentials/broker.ts +24 -79
- package/src/tools/credentials/ref-parse.ts +35 -0
- package/src/tools/credentials/resolve.ts +4 -10
- package/src/tools/credentials/store.ts +168 -0
- package/src/tools/credentials/tool-policy.ts +67 -0
- package/src/tools/executor.ts +53 -15
- package/src/tools/network/__tests__/web-search.test.ts +41 -1
- package/src/tools/network/url-safety.ts +9 -16
- package/src/tools/network/web-search.ts +21 -3
- package/src/tools/permission-checker.ts +43 -52
- package/src/tools/schema-transforms.ts +40 -5
- package/src/tools/shared/input-repairs.ts +160 -0
- package/src/tools/skills/skill-tool-factory.ts +18 -4
- package/src/tools/subagent/spawn.ts +0 -1
- package/src/tools/tool-approval-handler.ts +16 -10
- package/src/tools/tool-types.ts +26 -13
- package/src/tools/types.ts +6 -6
- package/src/util/__tests__/cgroup-memory.test.ts +3 -0
- package/src/util/cgroup-memory.ts +3 -0
- package/src/util/container-cpu-sampler.ts +250 -0
- package/src/util/image-conversion.ts +178 -14
- package/src/util/platform.ts +93 -10
- package/src/permissions/ipc-risk-types.ts +0 -143
- package/src/permissions/risk-types.ts +0 -76
|
@@ -1,128 +1,125 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Tests for `prepareAgentEnv
|
|
2
|
+
* Tests for `prepareAgentEnv`, the shared helper that injects required env
|
|
3
3
|
* vars onto an `AcpAgentConfig` and preflights that they're set.
|
|
4
4
|
*
|
|
5
5
|
* The route-level test in `runtime/routes/acp-routes.test.ts` covers the same
|
|
6
6
|
* behavior through the HTTP handler; these tests pin the helper in isolation
|
|
7
7
|
* so the contract is clear and a future refactor can't silently break it.
|
|
8
8
|
*
|
|
9
|
-
* Credential reads go through the credential broker (`serverUse`),
|
|
10
|
-
*
|
|
9
|
+
* Credential reads go through the credential broker (`serverUse`), which is
|
|
10
|
+
* exercised for real here: the metadata store and the encrypted secure-key
|
|
11
|
+
* store are pointed at a temp dir, so the tool and domain policy these tests
|
|
12
|
+
* observe is the same policy production evaluates.
|
|
13
|
+
*
|
|
14
|
+
* The module logger is the one exception. `mock.module` does not hoist above
|
|
15
|
+
* static imports, so the module under test is imported dynamically AFTER the
|
|
16
|
+
* mock is installed: that makes `prepare-agent-env.js` the only module holding
|
|
17
|
+
* the spy logger, so every warn the spy records came from `prepareAgentEnv`.
|
|
11
18
|
*/
|
|
12
19
|
|
|
13
|
-
import {
|
|
20
|
+
import { randomBytes } from "node:crypto";
|
|
21
|
+
import { mkdirSync, rmSync } from "node:fs";
|
|
22
|
+
import { tmpdir } from "node:os";
|
|
23
|
+
import { join } from "node:path";
|
|
24
|
+
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
14
25
|
|
|
26
|
+
import { setStorePathForTesting } from "../../__tests__/encrypted-store-test-helpers.js";
|
|
27
|
+
import { createMockLoggerModule } from "../../__tests__/helpers/mock-logger.js";
|
|
15
28
|
import { FailedDependencyError } from "../../runtime/routes/errors.js";
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
field,
|
|
60
|
-
allowedTools: metadataStore.get(key)!.allowedTools,
|
|
61
|
-
allowedDomains: [],
|
|
62
|
-
createdAt: 0,
|
|
63
|
-
updatedAt: 0,
|
|
64
|
-
};
|
|
65
|
-
},
|
|
66
|
-
}));
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Simulates the credential broker's serverUse method. The vault stores
|
|
70
|
-
* plaintext values keyed by `service/field`; the broker enforces tool
|
|
71
|
-
* policy via the metadata store before passing the value to the callback.
|
|
72
|
-
*/
|
|
73
|
-
const vaultStore = new Map<string, string>();
|
|
74
|
-
|
|
75
|
-
mock.module("../../tools/credentials/broker.js", () => ({
|
|
76
|
-
credentialBroker: {
|
|
77
|
-
serverUse: async <T>(request: {
|
|
78
|
-
service: string;
|
|
79
|
-
field: string;
|
|
80
|
-
toolName: string;
|
|
81
|
-
execute: (value: string) => Promise<T>;
|
|
82
|
-
}) => {
|
|
83
|
-
const key = `${request.service}/${request.field}`;
|
|
84
|
-
const meta = metadataStore.get(key);
|
|
85
|
-
if (!meta) {
|
|
86
|
-
return { success: false, reason: `No credential found for ${key}` };
|
|
87
|
-
}
|
|
88
|
-
if (!meta.allowedTools.includes(request.toolName)) {
|
|
89
|
-
return {
|
|
90
|
-
success: false,
|
|
91
|
-
reason: `Tool "${request.toolName}" not allowed`,
|
|
92
|
-
};
|
|
93
|
-
}
|
|
94
|
-
const value = vaultStore.get(key);
|
|
95
|
-
if (!value) {
|
|
96
|
-
return {
|
|
97
|
-
success: false,
|
|
98
|
-
reason: `No stored value for ${key}`,
|
|
99
|
-
};
|
|
100
|
-
}
|
|
101
|
-
const result = await request.execute(value);
|
|
102
|
-
return { success: true, result };
|
|
103
|
-
},
|
|
104
|
-
},
|
|
105
|
-
}));
|
|
106
|
-
|
|
107
|
-
const { prepareAgentEnv, ACP_CLAUDE_OAUTH_MISSING_CODE, grantAcpSpawnPolicy } =
|
|
108
|
-
await import("../prepare-agent-env.js");
|
|
29
|
+
import { credentialKey } from "../../security/credential-key.js";
|
|
30
|
+
import {
|
|
31
|
+
_resetBackend,
|
|
32
|
+
setSecureKeyAsync,
|
|
33
|
+
} from "../../security/secure-keys.js";
|
|
34
|
+
import { credentialBroker } from "../../tools/credentials/broker.js";
|
|
35
|
+
import {
|
|
36
|
+
_setMetadataPath,
|
|
37
|
+
deleteCredentialMetadata,
|
|
38
|
+
getCredentialMetadata,
|
|
39
|
+
upsertCredentialMetadata,
|
|
40
|
+
} from "../../tools/credentials/metadata-store.js";
|
|
41
|
+
import { ACP_OAUTH_TOKEN_FIELD, ACP_SERVICE } from "../acp-credentials.js";
|
|
42
|
+
|
|
43
|
+
const mockLogWarn = mock((_fields: unknown, _msg: string) => {});
|
|
44
|
+
|
|
45
|
+
mock.module("../../util/logger.js", () =>
|
|
46
|
+
createMockLoggerModule({
|
|
47
|
+
getLogger: () => ({
|
|
48
|
+
warn: mockLogWarn,
|
|
49
|
+
info: () => {},
|
|
50
|
+
error: () => {},
|
|
51
|
+
debug: () => {},
|
|
52
|
+
trace: () => {},
|
|
53
|
+
fatal: () => {},
|
|
54
|
+
}),
|
|
55
|
+
}),
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
const {
|
|
59
|
+
ACP_CLAUDE_OAUTH_MISSING_CODE,
|
|
60
|
+
acpSpawnCredentialDenialReason,
|
|
61
|
+
ensureAcpCredentialPolicy,
|
|
62
|
+
prepareAgentEnv,
|
|
63
|
+
repairAcpSpawnPolicy,
|
|
64
|
+
} = await import("../prepare-agent-env.js");
|
|
65
|
+
|
|
66
|
+
const ACP_SPAWN_TOOL = "acp_spawn";
|
|
67
|
+
|
|
68
|
+
const TEST_DIR = join(
|
|
69
|
+
tmpdir(),
|
|
70
|
+
`vellum-prepare-agent-env-test-${randomBytes(4).toString("hex")}`,
|
|
71
|
+
);
|
|
109
72
|
|
|
110
73
|
beforeEach(() => {
|
|
111
|
-
|
|
112
|
-
|
|
74
|
+
mkdirSync(TEST_DIR, { recursive: true });
|
|
75
|
+
setStorePathForTesting(join(TEST_DIR, "keys.enc"));
|
|
76
|
+
_resetBackend();
|
|
77
|
+
_setMetadataPath(join(TEST_DIR, "metadata.json"));
|
|
78
|
+
mockLogWarn.mockClear();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
afterEach(() => {
|
|
82
|
+
_setMetadataPath(null);
|
|
83
|
+
setStorePathForTesting(null);
|
|
84
|
+
_resetBackend();
|
|
85
|
+
rmSync(TEST_DIR, { recursive: true, force: true });
|
|
113
86
|
});
|
|
114
87
|
|
|
115
88
|
// ---------------------------------------------------------------------------
|
|
116
|
-
//
|
|
89
|
+
// Helpers to seed the vault + metadata (simulates `assistant credentials set`).
|
|
117
90
|
// ---------------------------------------------------------------------------
|
|
118
91
|
|
|
119
|
-
function
|
|
120
|
-
|
|
92
|
+
async function seedVaultValue(field: string, value: string): Promise<void> {
|
|
93
|
+
await setSecureKeyAsync(credentialKey(ACP_SERVICE, field), value);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function seedVaultToken(token: string): Promise<void> {
|
|
97
|
+
return seedVaultValue(ACP_OAUTH_TOKEN_FIELD, token);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function oauthMetadata() {
|
|
101
|
+
return getCredentialMetadata(ACP_SERVICE, ACP_OAUTH_TOKEN_FIELD);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Structured fields of the single injection-miss warn the module emitted.
|
|
106
|
+
* Exactly one line per failed spawn: it is the operator's only record of WHY
|
|
107
|
+
* the token was missing, and a duplicate would double-count in log searches.
|
|
108
|
+
*/
|
|
109
|
+
function soleInjectionMissWarnFields(): Record<string, unknown> {
|
|
110
|
+
expect(mockLogWarn).toHaveBeenCalledTimes(1);
|
|
111
|
+
const [fields, message] = mockLogWarn.mock.calls[0] as [
|
|
112
|
+
Record<string, unknown>,
|
|
113
|
+
string,
|
|
114
|
+
];
|
|
115
|
+
expect(message).toBe("Claude OAuth token not injected for acp_spawn");
|
|
116
|
+
expect(fields.field).toBe(ACP_OAUTH_TOKEN_FIELD);
|
|
117
|
+
return fields;
|
|
121
118
|
}
|
|
122
119
|
|
|
123
120
|
describe("prepareAgentEnv — claude-agent-acp gating", () => {
|
|
124
121
|
test("injects CLAUDE_CODE_OAUTH_TOKEN from the vault via the broker when agent.env has no override", async () => {
|
|
125
|
-
seedVaultToken("vault-AAA");
|
|
122
|
+
await seedVaultToken("vault-AAA");
|
|
126
123
|
|
|
127
124
|
const prepared = await prepareAgentEnv({
|
|
128
125
|
command: "claude-agent-acp",
|
|
@@ -133,39 +130,35 @@ describe("prepareAgentEnv — claude-agent-acp gating", () => {
|
|
|
133
130
|
});
|
|
134
131
|
|
|
135
132
|
test("auto-registers metadata with acp_spawn in allowedTools when none exists", async () => {
|
|
136
|
-
seedVaultToken("vault-auto-meta");
|
|
133
|
+
await seedVaultToken("vault-auto-meta");
|
|
137
134
|
|
|
138
135
|
await prepareAgentEnv({ command: "claude-agent-acp", args: [] });
|
|
139
136
|
|
|
140
|
-
|
|
141
|
-
expect(meta).toBeDefined();
|
|
142
|
-
expect(meta!.allowedTools).toContain("acp_spawn");
|
|
137
|
+
expect(oauthMetadata()?.allowedTools).toContain(ACP_SPAWN_TOOL);
|
|
143
138
|
});
|
|
144
139
|
|
|
145
140
|
test("adds acp_spawn to metadata with empty allowedTools (default provisioning path)", async () => {
|
|
146
|
-
|
|
141
|
+
upsertCredentialMetadata(ACP_SERVICE, ACP_OAUTH_TOKEN_FIELD, {
|
|
147
142
|
allowedTools: [],
|
|
148
143
|
});
|
|
149
|
-
seedVaultToken("vault-augment");
|
|
144
|
+
await seedVaultToken("vault-augment");
|
|
150
145
|
|
|
151
146
|
await prepareAgentEnv({ command: "claude-agent-acp", args: [] });
|
|
152
147
|
|
|
153
|
-
|
|
154
|
-
expect(meta!.allowedTools).toContain("acp_spawn");
|
|
148
|
+
expect(oauthMetadata()?.allowedTools).toContain(ACP_SPAWN_TOOL);
|
|
155
149
|
});
|
|
156
150
|
|
|
157
151
|
test("respects explicit tool policy that excludes acp_spawn", async () => {
|
|
158
|
-
|
|
152
|
+
upsertCredentialMetadata(ACP_SERVICE, ACP_OAUTH_TOKEN_FIELD, {
|
|
159
153
|
allowedTools: ["other_tool"],
|
|
160
154
|
});
|
|
161
|
-
seedVaultToken("vault-restricted");
|
|
155
|
+
await seedVaultToken("vault-restricted");
|
|
162
156
|
|
|
163
157
|
await expect(
|
|
164
158
|
prepareAgentEnv({ command: "claude-agent-acp", args: [] }),
|
|
165
159
|
).rejects.toThrow("CLAUDE_CODE_OAUTH_TOKEN");
|
|
166
160
|
|
|
167
|
-
|
|
168
|
-
expect(meta!.allowedTools).toEqual(["other_tool"]);
|
|
161
|
+
expect(oauthMetadata()?.allowedTools).toEqual(["other_tool"]);
|
|
169
162
|
});
|
|
170
163
|
|
|
171
164
|
test("accepts CLAUDE_CODE_OAUTH_TOKEN from agent.env (config.json override) with no vault entry", async () => {
|
|
@@ -179,7 +172,7 @@ describe("prepareAgentEnv — claude-agent-acp gating", () => {
|
|
|
179
172
|
});
|
|
180
173
|
|
|
181
174
|
test("agent.env override wins over the vault entry (precedence pin)", async () => {
|
|
182
|
-
seedVaultToken("vault-CCC");
|
|
175
|
+
await seedVaultToken("vault-CCC");
|
|
183
176
|
|
|
184
177
|
const prepared = await prepareAgentEnv({
|
|
185
178
|
command: "claude-agent-acp",
|
|
@@ -191,7 +184,7 @@ describe("prepareAgentEnv — claude-agent-acp gating", () => {
|
|
|
191
184
|
});
|
|
192
185
|
|
|
193
186
|
test("preserves unrelated env vars on agent.env when injecting from the vault", async () => {
|
|
194
|
-
seedVaultToken("vault-EEE");
|
|
187
|
+
await seedVaultToken("vault-EEE");
|
|
195
188
|
|
|
196
189
|
const prepared = await prepareAgentEnv({
|
|
197
190
|
command: "claude-agent-acp",
|
|
@@ -244,10 +237,70 @@ describe("prepareAgentEnv — claude-agent-acp gating", () => {
|
|
|
244
237
|
// card actually renders above the model's reply, so "below" is always wrong.
|
|
245
238
|
expect(message).toContain('never say "below"');
|
|
246
239
|
expect(message).toContain('"above"');
|
|
240
|
+
// Pins the seam between the opening and the shared guidance: an absent
|
|
241
|
+
// value keeps the "which is not set" wording verbatim.
|
|
242
|
+
expect(message).toContain(
|
|
243
|
+
'which is not set. The app shows the user an inline "Connect Claude Code" card.',
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
// The operator-facing half: the broker's own reason, classified.
|
|
247
|
+
const fields = soleInjectionMissWarnFields();
|
|
248
|
+
expect(fields.missReason).toContain("no stored value");
|
|
249
|
+
expect(fields.policyBlocked).toBe(false);
|
|
250
|
+
expect(fields.apiKeyShaped).toBe(false);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
test("a policy-denied read logs the broker reason and reports the policy block", async () => {
|
|
254
|
+
// Invariant: a policy-denied read must be reported as a policy block, not
|
|
255
|
+
// as an absent value. The two states have different repair stories, and
|
|
256
|
+
// the message must never assert that a value is stored (policy is checked
|
|
257
|
+
// before the value, so metadata alone can trigger this branch).
|
|
258
|
+
upsertCredentialMetadata(ACP_SERVICE, ACP_OAUTH_TOKEN_FIELD, {
|
|
259
|
+
allowedTools: ["bash"],
|
|
260
|
+
});
|
|
261
|
+
await seedVaultToken("sk-ant-oat01-policy-blocked");
|
|
262
|
+
|
|
263
|
+
let caught: unknown;
|
|
264
|
+
try {
|
|
265
|
+
await prepareAgentEnv({ command: "claude-agent-acp", args: [] });
|
|
266
|
+
} catch (err) {
|
|
267
|
+
caught = err;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
expect(caught).toBeInstanceOf(FailedDependencyError);
|
|
271
|
+
// Same wire contract: old clients keep rendering the same Connect card.
|
|
272
|
+
expect((caught as FailedDependencyError).details).toEqual({
|
|
273
|
+
code: ACP_CLAUDE_OAUTH_MISSING_CODE,
|
|
274
|
+
});
|
|
275
|
+
const message = (caught as Error).message;
|
|
276
|
+
expect(message).toContain("cannot read the Claude OAuth token");
|
|
277
|
+
expect(message).toContain("blocks the acp_spawn read");
|
|
278
|
+
expect(message).toContain("CLAUDE_CODE_OAUTH_TOKEN");
|
|
279
|
+
expect(message).toContain("signs in again and repairs that policy");
|
|
280
|
+
// Policy is checked before the value, so the message must never assert
|
|
281
|
+
// that a value is actually stored.
|
|
282
|
+
expect(message).not.toContain("has a stored");
|
|
283
|
+
expect(message).not.toContain("which is not set");
|
|
284
|
+
// The guidance steering the model away from CLI/token-paste workarounds is
|
|
285
|
+
// shared with the missing-value variant, so it must survive here too.
|
|
286
|
+
expect(message).toContain("ONE short sentence");
|
|
287
|
+
expect(message).toContain("Do NOT");
|
|
288
|
+
expect(message).toContain("claude setup-token");
|
|
289
|
+
expect(message).toContain("do NOT retry the spawn yourself");
|
|
290
|
+
expect(message).toContain("assistant credentials prompt");
|
|
291
|
+
|
|
292
|
+
const fields = soleInjectionMissWarnFields();
|
|
293
|
+
expect(fields.missReason).toContain(
|
|
294
|
+
'Tool "acp_spawn" is not allowed to use credential acp/claude_oauth_token',
|
|
295
|
+
);
|
|
296
|
+
expect(fields.policyBlocked).toBe(true);
|
|
297
|
+
expect(fields.apiKeyShaped).toBe(false);
|
|
298
|
+
// The denial reason is a policy verdict, never the credential itself.
|
|
299
|
+
expect(JSON.stringify(fields)).not.toContain("sk-ant-oat01-policy-blocked");
|
|
247
300
|
});
|
|
248
301
|
|
|
249
302
|
test("does NOT attach the marker when a token is present (happy path unchanged)", async () => {
|
|
250
|
-
seedVaultToken("vault-marker-absent");
|
|
303
|
+
await seedVaultToken("vault-marker-absent");
|
|
251
304
|
|
|
252
305
|
const prepared = await prepareAgentEnv({
|
|
253
306
|
command: "claude-agent-acp",
|
|
@@ -262,7 +315,7 @@ describe("prepareAgentEnv — claude-agent-acp gating", () => {
|
|
|
262
315
|
// format guard existed must not spawn with a doomed credential (a 401 with
|
|
263
316
|
// no repair). The presence check alone would pass, so the injected value is
|
|
264
317
|
// classified and an API key is treated as missing → the Connect card fires.
|
|
265
|
-
seedVaultToken("sk-ant-api03-legacy-bad-value");
|
|
318
|
+
await seedVaultToken("sk-ant-api03-legacy-bad-value");
|
|
266
319
|
|
|
267
320
|
let caught: unknown;
|
|
268
321
|
try {
|
|
@@ -275,6 +328,20 @@ describe("prepareAgentEnv — claude-agent-acp gating", () => {
|
|
|
275
328
|
expect((caught as FailedDependencyError).details).toEqual({
|
|
276
329
|
code: ACP_CLAUDE_OAUTH_MISSING_CODE,
|
|
277
330
|
});
|
|
331
|
+
// Connect genuinely repairs this by storing a fresh OAuth token, so the
|
|
332
|
+
// message stays the missing-token one rather than the policy-blocked one.
|
|
333
|
+
expect((caught as Error).message).toContain(
|
|
334
|
+
'which is not set. The app shows the user an inline "Connect Claude Code" card.',
|
|
335
|
+
);
|
|
336
|
+
|
|
337
|
+
// The log is where the api-key shape is recorded; the value never is.
|
|
338
|
+
const fields = soleInjectionMissWarnFields();
|
|
339
|
+
expect(fields.apiKeyShaped).toBe(true);
|
|
340
|
+
expect(fields.policyBlocked).toBe(false);
|
|
341
|
+
expect(fields.missReason).toBeUndefined();
|
|
342
|
+
expect(JSON.stringify(fields)).not.toContain(
|
|
343
|
+
"sk-ant-api03-legacy-bad-value",
|
|
344
|
+
);
|
|
278
345
|
});
|
|
279
346
|
|
|
280
347
|
test("routes an API key supplied via agent.env (config override) to the missing-token path", async () => {
|
|
@@ -304,7 +371,7 @@ describe("prepareAgentEnv — claude-agent-acp gating", () => {
|
|
|
304
371
|
// be used and the Connect card would re-fire forever. The bad override is
|
|
305
372
|
// dropped BEFORE the read, so the vault OAuth token is picked up and spawn
|
|
306
373
|
// proceeds instead of looping.
|
|
307
|
-
seedVaultToken("sk-ant-oat01-freshly-connected");
|
|
374
|
+
await seedVaultToken("sk-ant-oat01-freshly-connected");
|
|
308
375
|
|
|
309
376
|
const prepared = await prepareAgentEnv({
|
|
310
377
|
command: "claude-agent-acp",
|
|
@@ -318,7 +385,7 @@ describe("prepareAgentEnv — claude-agent-acp gating", () => {
|
|
|
318
385
|
});
|
|
319
386
|
|
|
320
387
|
test("keeps a valid OAuth token (sk-ant-oat…) usable (not misclassified)", async () => {
|
|
321
|
-
seedVaultToken("sk-ant-oat01-good-token");
|
|
388
|
+
await seedVaultToken("sk-ant-oat01-good-token");
|
|
322
389
|
|
|
323
390
|
const prepared = await prepareAgentEnv({
|
|
324
391
|
command: "claude-agent-acp",
|
|
@@ -331,7 +398,7 @@ describe("prepareAgentEnv — claude-agent-acp gating", () => {
|
|
|
331
398
|
});
|
|
332
399
|
|
|
333
400
|
test("gates on the resolved command BASENAME (alias to /custom/path/claude-agent-acp still gets the token)", async () => {
|
|
334
|
-
seedVaultToken("vault-FFF");
|
|
401
|
+
await seedVaultToken("vault-FFF");
|
|
335
402
|
|
|
336
403
|
const prepared = await prepareAgentEnv({
|
|
337
404
|
command: "/opt/bin/claude-agent-acp",
|
|
@@ -342,7 +409,7 @@ describe("prepareAgentEnv — claude-agent-acp gating", () => {
|
|
|
342
409
|
});
|
|
343
410
|
|
|
344
411
|
test("does NOT mutate the caller's agentConfig", async () => {
|
|
345
|
-
seedVaultToken("vault-GGG");
|
|
412
|
+
await seedVaultToken("vault-GGG");
|
|
346
413
|
const original = {
|
|
347
414
|
command: "claude-agent-acp",
|
|
348
415
|
args: [],
|
|
@@ -360,16 +427,16 @@ describe("prepareAgentEnv — claude-agent-acp gating", () => {
|
|
|
360
427
|
});
|
|
361
428
|
|
|
362
429
|
describe("prepareAgentEnv - codex-acp gating", () => {
|
|
363
|
-
function seedVaultOpenaiKey(key: string): void {
|
|
364
|
-
|
|
430
|
+
function seedVaultOpenaiKey(key: string): Promise<void> {
|
|
431
|
+
return seedVaultValue("openai_api_key", key);
|
|
365
432
|
}
|
|
366
433
|
|
|
367
|
-
function seedVaultCodexKey(key: string): void {
|
|
368
|
-
|
|
434
|
+
function seedVaultCodexKey(key: string): Promise<void> {
|
|
435
|
+
return seedVaultValue("codex_api_key", key);
|
|
369
436
|
}
|
|
370
437
|
|
|
371
438
|
test("injects OPENAI_API_KEY from the vault via the broker when agent.env has no override", async () => {
|
|
372
|
-
seedVaultOpenaiKey("vault-fake-openai-AAA");
|
|
439
|
+
await seedVaultOpenaiKey("vault-fake-openai-AAA");
|
|
373
440
|
|
|
374
441
|
const prepared = await prepareAgentEnv({
|
|
375
442
|
command: "codex-acp",
|
|
@@ -382,7 +449,7 @@ describe("prepareAgentEnv - codex-acp gating", () => {
|
|
|
382
449
|
test("agent.env override wins over the vault entry and skips the broker (precedence pin)", async () => {
|
|
383
450
|
// Seed a vault value but no metadata: if the override path consulted the
|
|
384
451
|
// broker anyway, ensureAcpCredentialPolicy would create metadata here.
|
|
385
|
-
seedVaultOpenaiKey("vault-fake-openai-BBB");
|
|
452
|
+
await seedVaultOpenaiKey("vault-fake-openai-BBB");
|
|
386
453
|
|
|
387
454
|
const prepared = await prepareAgentEnv({
|
|
388
455
|
command: "codex-acp",
|
|
@@ -391,7 +458,9 @@ describe("prepareAgentEnv - codex-acp gating", () => {
|
|
|
391
458
|
});
|
|
392
459
|
|
|
393
460
|
expect(prepared.env?.OPENAI_API_KEY).toBe("config-fake-openai-CCC");
|
|
394
|
-
expect(
|
|
461
|
+
expect(
|
|
462
|
+
getCredentialMetadata(ACP_SERVICE, "openai_api_key"),
|
|
463
|
+
).toBeUndefined();
|
|
395
464
|
});
|
|
396
465
|
|
|
397
466
|
test("a vault miss for both fields does NOT throw and spawns with env unchanged (keys are optional)", async () => {
|
|
@@ -407,7 +476,7 @@ describe("prepareAgentEnv - codex-acp gating", () => {
|
|
|
407
476
|
});
|
|
408
477
|
|
|
409
478
|
test("injects CODEX_API_KEY independently of OPENAI_API_KEY", async () => {
|
|
410
|
-
seedVaultCodexKey("vault-fake-codex-DDD");
|
|
479
|
+
await seedVaultCodexKey("vault-fake-codex-DDD");
|
|
411
480
|
|
|
412
481
|
const prepared = await prepareAgentEnv({
|
|
413
482
|
command: "codex-acp",
|
|
@@ -419,8 +488,8 @@ describe("prepareAgentEnv - codex-acp gating", () => {
|
|
|
419
488
|
});
|
|
420
489
|
|
|
421
490
|
test("injects both keys when both vault fields are present", async () => {
|
|
422
|
-
seedVaultOpenaiKey("vault-fake-openai-EEE");
|
|
423
|
-
seedVaultCodexKey("vault-fake-codex-FFF");
|
|
491
|
+
await seedVaultOpenaiKey("vault-fake-openai-EEE");
|
|
492
|
+
await seedVaultCodexKey("vault-fake-codex-FFF");
|
|
424
493
|
|
|
425
494
|
const prepared = await prepareAgentEnv({
|
|
426
495
|
command: "codex-acp",
|
|
@@ -432,7 +501,7 @@ describe("prepareAgentEnv - codex-acp gating", () => {
|
|
|
432
501
|
});
|
|
433
502
|
|
|
434
503
|
test("gates on the resolved command BASENAME (custom agent id with full path still gets injection)", async () => {
|
|
435
|
-
seedVaultOpenaiKey("vault-fake-openai-GGG");
|
|
504
|
+
await seedVaultOpenaiKey("vault-fake-openai-GGG");
|
|
436
505
|
|
|
437
506
|
const prepared = await prepareAgentEnv({
|
|
438
507
|
command: "/data/.bun/bin/codex-acp",
|
|
@@ -445,7 +514,7 @@ describe("prepareAgentEnv - codex-acp gating", () => {
|
|
|
445
514
|
|
|
446
515
|
describe("prepareAgentEnv — non-claude commands", () => {
|
|
447
516
|
test("returns the config unchanged for an unrecognized command basename", async () => {
|
|
448
|
-
seedVaultToken("vault-HHH");
|
|
517
|
+
await seedVaultToken("vault-HHH");
|
|
449
518
|
|
|
450
519
|
const prepared = await prepareAgentEnv({
|
|
451
520
|
command: "some-future-adapter",
|
|
@@ -457,38 +526,192 @@ describe("prepareAgentEnv — non-claude commands", () => {
|
|
|
457
526
|
});
|
|
458
527
|
});
|
|
459
528
|
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
529
|
+
/**
|
|
530
|
+
* The Connect flow's repair, which `storeAcpClaudeToken` performs after writing
|
|
531
|
+
* the token. Every metadata shape the broker can deny on is covered here, so
|
|
532
|
+
* `acp-claude-oauth.test.ts` only has to pin the end-to-end wiring.
|
|
533
|
+
*/
|
|
534
|
+
describe("repairAcpSpawnPolicy - the Connect repair", () => {
|
|
535
|
+
test("creates a record with acp_spawn, the usage description, and no domain restriction when none exists", () => {
|
|
536
|
+
repairAcpSpawnPolicy(ACP_OAUTH_TOKEN_FIELD, "desc");
|
|
537
|
+
|
|
538
|
+
expect(oauthMetadata()?.allowedTools).toEqual([ACP_SPAWN_TOOL]);
|
|
539
|
+
expect(oauthMetadata()?.allowedDomains).toEqual([]);
|
|
540
|
+
expect(oauthMetadata()?.usageDescription).toBe("desc");
|
|
466
541
|
});
|
|
467
542
|
|
|
468
|
-
test("
|
|
469
|
-
|
|
543
|
+
test("adds acp_spawn to an empty allowedTools (default provisioning path)", () => {
|
|
544
|
+
upsertCredentialMetadata(ACP_SERVICE, ACP_OAUTH_TOKEN_FIELD, {
|
|
545
|
+
allowedTools: [],
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
repairAcpSpawnPolicy(ACP_OAUTH_TOKEN_FIELD, "desc");
|
|
549
|
+
|
|
550
|
+
expect(oauthMetadata()?.allowedTools).toEqual([ACP_SPAWN_TOOL]);
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
test("unions acp_spawn into an explicit policy that omitted it", () => {
|
|
554
|
+
upsertCredentialMetadata(ACP_SERVICE, ACP_OAUTH_TOKEN_FIELD, {
|
|
470
555
|
allowedTools: ["other_tool"],
|
|
471
556
|
});
|
|
472
557
|
|
|
473
|
-
|
|
558
|
+
repairAcpSpawnPolicy(ACP_OAUTH_TOKEN_FIELD, "desc");
|
|
474
559
|
|
|
475
|
-
// Unlike ensureAcpCredentialPolicy (which preserves),
|
|
476
|
-
expect(
|
|
560
|
+
// Unlike ensureAcpCredentialPolicy (which preserves), the repair widens.
|
|
561
|
+
expect(oauthMetadata()?.allowedTools).toEqual([
|
|
477
562
|
"other_tool",
|
|
478
|
-
|
|
563
|
+
ACP_SPAWN_TOOL,
|
|
479
564
|
]);
|
|
480
565
|
});
|
|
481
566
|
|
|
482
|
-
test("
|
|
483
|
-
|
|
484
|
-
allowedTools: [
|
|
567
|
+
test("clears a domain restriction the broker refuses server-side, keeping the tools", () => {
|
|
568
|
+
upsertCredentialMetadata(ACP_SERVICE, ACP_OAUTH_TOKEN_FIELD, {
|
|
569
|
+
allowedTools: [ACP_SPAWN_TOOL],
|
|
570
|
+
allowedDomains: ["api.anthropic.com"],
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
repairAcpSpawnPolicy(ACP_OAUTH_TOKEN_FIELD, "desc");
|
|
574
|
+
|
|
575
|
+
expect(oauthMetadata()?.allowedDomains).toEqual([]);
|
|
576
|
+
expect(oauthMetadata()?.allowedTools).toEqual([ACP_SPAWN_TOOL]);
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
test("repairs both halves of a denied policy in one pass", () => {
|
|
580
|
+
upsertCredentialMetadata(ACP_SERVICE, ACP_OAUTH_TOKEN_FIELD, {
|
|
581
|
+
allowedTools: ["other_tool"],
|
|
582
|
+
allowedDomains: ["api.anthropic.com"],
|
|
485
583
|
});
|
|
486
584
|
|
|
487
|
-
|
|
585
|
+
repairAcpSpawnPolicy(ACP_OAUTH_TOKEN_FIELD, "desc");
|
|
488
586
|
|
|
489
|
-
expect(
|
|
490
|
-
"acp_spawn",
|
|
587
|
+
expect(oauthMetadata()?.allowedTools).toEqual([
|
|
491
588
|
"other_tool",
|
|
589
|
+
ACP_SPAWN_TOOL,
|
|
492
590
|
]);
|
|
591
|
+
expect(oauthMetadata()?.allowedDomains).toEqual([]);
|
|
493
592
|
});
|
|
593
|
+
|
|
594
|
+
test("writes nothing when the stored policy already satisfies both halves", async () => {
|
|
595
|
+
upsertCredentialMetadata(ACP_SERVICE, ACP_OAUTH_TOKEN_FIELD, {
|
|
596
|
+
allowedTools: [ACP_SPAWN_TOOL, "other_tool"],
|
|
597
|
+
});
|
|
598
|
+
const before = oauthMetadata();
|
|
599
|
+
// Every upsert stamps `updatedAt` with the current millisecond, so waiting
|
|
600
|
+
// here makes an unconditional write show up as a changed record.
|
|
601
|
+
await Bun.sleep(5);
|
|
602
|
+
|
|
603
|
+
repairAcpSpawnPolicy(ACP_OAUTH_TOKEN_FIELD, "desc");
|
|
604
|
+
|
|
605
|
+
expect(oauthMetadata()).toEqual(before);
|
|
606
|
+
});
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
/**
|
|
610
|
+
* Drift guard for the Connect Claude status predicate.
|
|
611
|
+
*
|
|
612
|
+
* `acpSpawnCredentialDenialReason` answers "would the spawn's broker read of
|
|
613
|
+
* this credential succeed?" without performing it, because the status route is
|
|
614
|
+
* a side-effect-free GET. Every state below is run twice: once through the
|
|
615
|
+
* predicate, and once through the real spawn sequence
|
|
616
|
+
* (`ensureAcpCredentialPolicy` then `credentialBroker.serverUse`). The two must
|
|
617
|
+
* agree down to the denial string; this suite fails whenever they diverge.
|
|
618
|
+
*
|
|
619
|
+
* The predicate reads only the persisted value via `getSecureKeyAsync` and
|
|
620
|
+
* never calls `serverUse`, so it can never consume a one-time transient
|
|
621
|
+
* credential the way a real broker read would.
|
|
622
|
+
*/
|
|
623
|
+
describe("acpSpawnCredentialDenialReason parity with the real spawn read", () => {
|
|
624
|
+
const STATES: { name: string; seed: () => void }[] = [
|
|
625
|
+
{ name: "no metadata at all", seed: () => {} },
|
|
626
|
+
{
|
|
627
|
+
name: "empty allowedTools",
|
|
628
|
+
seed: () => {
|
|
629
|
+
upsertCredentialMetadata(ACP_SERVICE, ACP_OAUTH_TOKEN_FIELD, {
|
|
630
|
+
allowedTools: [],
|
|
631
|
+
});
|
|
632
|
+
},
|
|
633
|
+
},
|
|
634
|
+
{
|
|
635
|
+
name: "allowedTools listing acp_spawn",
|
|
636
|
+
seed: () => {
|
|
637
|
+
upsertCredentialMetadata(ACP_SERVICE, ACP_OAUTH_TOKEN_FIELD, {
|
|
638
|
+
allowedTools: [ACP_SPAWN_TOOL],
|
|
639
|
+
});
|
|
640
|
+
},
|
|
641
|
+
},
|
|
642
|
+
{
|
|
643
|
+
name: "allowedTools listing another tool",
|
|
644
|
+
seed: () => {
|
|
645
|
+
upsertCredentialMetadata(ACP_SERVICE, ACP_OAUTH_TOKEN_FIELD, {
|
|
646
|
+
allowedTools: ["bash"],
|
|
647
|
+
});
|
|
648
|
+
},
|
|
649
|
+
},
|
|
650
|
+
{
|
|
651
|
+
// The alias map in tool-policy.ts has no entry canonicalizing to
|
|
652
|
+
// acp_spawn, so this covers alias resolution generically: a legacy alias
|
|
653
|
+
// that canonicalizes to some OTHER capability must still deny.
|
|
654
|
+
name: "allowedTools listing a legacy alias for another capability",
|
|
655
|
+
seed: () => {
|
|
656
|
+
upsertCredentialMetadata(ACP_SERVICE, ACP_OAUTH_TOKEN_FIELD, {
|
|
657
|
+
allowedTools: ["browser_fill_credential"],
|
|
658
|
+
});
|
|
659
|
+
},
|
|
660
|
+
},
|
|
661
|
+
{
|
|
662
|
+
name: "acp_spawn allowed but domain-restricted",
|
|
663
|
+
seed: () => {
|
|
664
|
+
upsertCredentialMetadata(ACP_SERVICE, ACP_OAUTH_TOKEN_FIELD, {
|
|
665
|
+
allowedTools: [ACP_SPAWN_TOOL],
|
|
666
|
+
allowedDomains: ["api.anthropic.com"],
|
|
667
|
+
});
|
|
668
|
+
},
|
|
669
|
+
},
|
|
670
|
+
{
|
|
671
|
+
// The ensure-repair grants acp_spawn but must not drop the domain policy,
|
|
672
|
+
// so the projection has to preserve the rest of the record.
|
|
673
|
+
name: "empty allowedTools and domain-restricted",
|
|
674
|
+
seed: () => {
|
|
675
|
+
upsertCredentialMetadata(ACP_SERVICE, ACP_OAUTH_TOKEN_FIELD, {
|
|
676
|
+
allowedTools: [],
|
|
677
|
+
allowedDomains: ["api.anthropic.com"],
|
|
678
|
+
});
|
|
679
|
+
},
|
|
680
|
+
},
|
|
681
|
+
{
|
|
682
|
+
name: "acp_spawn allowed with an explicitly empty allowedDomains",
|
|
683
|
+
seed: () => {
|
|
684
|
+
upsertCredentialMetadata(ACP_SERVICE, ACP_OAUTH_TOKEN_FIELD, {
|
|
685
|
+
allowedTools: [ACP_SPAWN_TOOL],
|
|
686
|
+
allowedDomains: [],
|
|
687
|
+
});
|
|
688
|
+
},
|
|
689
|
+
},
|
|
690
|
+
];
|
|
691
|
+
|
|
692
|
+
for (const state of STATES) {
|
|
693
|
+
test(`predicts the spawn read for: ${state.name}`, async () => {
|
|
694
|
+
await seedVaultToken("sk-ant-oat01-parity");
|
|
695
|
+
|
|
696
|
+
state.seed();
|
|
697
|
+
const beforePrediction = oauthMetadata();
|
|
698
|
+
const predicted = acpSpawnCredentialDenialReason(ACP_OAUTH_TOKEN_FIELD);
|
|
699
|
+
// The status route is a GET: predicting must not touch the store.
|
|
700
|
+
expect(oauthMetadata()).toEqual(beforePrediction);
|
|
701
|
+
|
|
702
|
+
// ensureAcpCredentialPolicy mutates the store, so the actual run starts
|
|
703
|
+
// from a fresh copy of the same state rather than the predicted one.
|
|
704
|
+
deleteCredentialMetadata(ACP_SERVICE, ACP_OAUTH_TOKEN_FIELD);
|
|
705
|
+
state.seed();
|
|
706
|
+
ensureAcpCredentialPolicy(ACP_OAUTH_TOKEN_FIELD, "desc");
|
|
707
|
+
const result = await credentialBroker.serverUse<void>({
|
|
708
|
+
service: ACP_SERVICE,
|
|
709
|
+
field: ACP_OAUTH_TOKEN_FIELD,
|
|
710
|
+
toolName: ACP_SPAWN_TOOL,
|
|
711
|
+
execute: async () => {},
|
|
712
|
+
});
|
|
713
|
+
|
|
714
|
+
expect(predicted).toBe(result.success ? undefined : result.reason);
|
|
715
|
+
});
|
|
716
|
+
}
|
|
494
717
|
});
|