@vellumai/assistant 0.10.9 → 0.10.10-dev.202607162206.d08e98e
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 +1 -1
- package/Dockerfile +8 -0
- package/docs/activation-funnel-telemetry.md +13 -7
- 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__/redacted-credential.test.ts +200 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/redacted-credential.ts +226 -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__/redacted-credential.test.ts +200 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/redacted-credential.ts +226 -0
- package/node_modules/@vellumai/service-contracts/package.json +1 -0
- package/node_modules/@vellumai/service-contracts/src/__tests__/redacted-credential.test.ts +200 -0
- package/node_modules/@vellumai/service-contracts/src/redacted-credential.ts +226 -0
- package/openapi.yaml +530 -107
- package/package.json +1 -1
- package/scripts/generate-openapi.ts +8 -0
- package/src/__tests__/activation-early-marking.test.ts +6 -5
- package/src/__tests__/agent-loop-override-profile.test.ts +22 -25
- package/src/__tests__/agent-wake-override-profile.test.ts +21 -48
- package/src/__tests__/app-builder-tool-scripts.test.ts +0 -1
- package/src/__tests__/app-bundler.test.ts +4 -12
- package/src/__tests__/app-executors.test.ts +2 -49
- package/src/__tests__/app-routes-csp.test.ts +114 -146
- package/src/__tests__/auth-fallback-events-store.test.ts +8 -1
- package/src/__tests__/build-persisted-content.test.ts +96 -0
- package/src/__tests__/bundle-scanner.test.ts +27 -1
- package/src/__tests__/call-controller.test.ts +291 -0
- package/src/__tests__/call-site-routing-connection-auto-resolve.test.ts +165 -0
- package/src/__tests__/chat-credential-redaction.test.ts +1395 -0
- package/src/__tests__/chat-reveal-guard-priming.test.ts +791 -0
- package/src/__tests__/compaction.benchmark.test.ts +2 -1
- package/src/__tests__/compactor-image-manifest-trust.test.ts +50 -0
- package/src/__tests__/config-loader-backfill.test.ts +9 -4
- package/src/__tests__/config-schema-cmd.test.ts +10 -11
- package/src/__tests__/config-schema.test.ts +190 -257
- package/src/__tests__/conversation-agent-loop-fatal-cleanup.test.ts +208 -0
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +5 -19
- package/src/__tests__/conversation-agent-loop.test.ts +22 -9
- package/src/__tests__/conversation-error.test.ts +31 -0
- package/src/__tests__/conversation-load-history-repair.test.ts +110 -0
- package/src/__tests__/conversation-process-callsite.test.ts +12 -19
- package/src/__tests__/conversation-routes-slash-commands.test.ts +7 -21
- package/src/__tests__/conversation-summarize-route.test.ts +36 -44
- package/src/__tests__/conversation-surfaces-action-delivery.test.ts +1 -0
- package/src/__tests__/conversation-surfaces-activation-emit.test.ts +7 -7
- package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +0 -1
- package/src/__tests__/conversation-tool-setup-attribution.test.ts +0 -1
- package/src/__tests__/conversation-usage.test.ts +4 -12
- package/src/__tests__/credential-routes.test.ts +250 -0
- package/src/__tests__/credential-security-invariants.test.ts +3 -0
- package/src/__tests__/db-migration-rollback.test.ts +22 -0
- package/src/__tests__/empty-response-hook.test.ts +186 -1
- package/src/__tests__/external-plugin-loader.test.ts +11 -5
- package/src/__tests__/heartbeat-service.test.ts +0 -28
- package/src/__tests__/host-shell-tool.test.ts +2 -0
- package/src/__tests__/inactive-tool-error-messages.test.ts +2 -2
- package/src/__tests__/inference-no-mode-boot-e2e.test.ts +52 -8
- package/src/__tests__/internal-telemetry-routes.test.ts +23 -4
- package/src/__tests__/invite-routes-http.test.ts +12 -16
- package/src/__tests__/list-all-apps.test.ts +0 -4
- package/src/__tests__/llm-context-resolution.test.ts +32 -56
- package/src/__tests__/llm-request-log-turn-query.test.ts +109 -0
- package/src/__tests__/llm-resolver-override-or-default.test.ts +3 -52
- package/src/__tests__/llm-resolver.test.ts +342 -602
- package/src/__tests__/llm-schema.test.ts +79 -37
- package/src/__tests__/max-tokens-continue-hook.test.ts +19 -0
- package/src/__tests__/media-stream-output.test.ts +259 -3
- package/src/__tests__/media-stream-server-integration.test.ts +22 -1
- package/src/__tests__/media-stream-stt-session.test.ts +47 -0
- package/src/__tests__/memory-jobs-worker-cleanup-cadence.test.ts +33 -0
- package/src/__tests__/memory-recall-log-store.test.ts +47 -13
- package/src/__tests__/mock-gateway-ipc.ts +46 -1
- package/src/__tests__/mtime-cache.test.ts +61 -0
- package/src/__tests__/navigate-settings-tab.test.ts +2 -0
- package/src/__tests__/normalize-onboarding.test.ts +33 -0
- package/src/__tests__/onboarding-persona-write.test.ts +26 -0
- package/src/__tests__/plugin-api-resolve-credential.test.ts +140 -0
- package/src/__tests__/plugin-app-serve-routes.test.ts +166 -14
- package/src/__tests__/plugin-import-boundary-reverse-guard.test.ts +2 -0
- package/src/__tests__/post-turn-tool-result-truncation.test.ts +38 -0
- package/src/__tests__/provider-commit-message-generator.test.ts +27 -20
- package/src/__tests__/provider-connections-backfill.test.ts +138 -0
- package/src/__tests__/provider-platform-proxy-integration.test.ts +10 -31
- package/src/__tests__/provider-registry-ollama.test.ts +8 -18
- package/src/__tests__/provider-send-message-override-profile.test.ts +23 -23
- package/src/__tests__/provider-usage-tracking.test.ts +9 -19
- package/src/__tests__/prune-old-conversations-job.test.ts +12 -0
- package/src/__tests__/published-app-updater.test.ts +22 -16
- package/src/__tests__/registry.test.ts +5 -16
- package/src/__tests__/retry-openrouter-only-normalization.test.ts +22 -23
- package/src/__tests__/retry-thinking-adaptive-only.test.ts +43 -48
- package/src/__tests__/retry-thinking-tool-choice.test.ts +57 -66
- package/src/__tests__/retry-verbosity-normalization.test.ts +24 -23
- package/src/__tests__/reveal-success-registry.test.ts +123 -0
- package/src/__tests__/run-conversation-turn-persistence.test.ts +130 -0
- package/src/__tests__/secret-fixtures.ts +9 -0
- package/src/__tests__/server-history-render.test.ts +28 -0
- package/src/__tests__/skills.test.ts +9 -4
- package/src/__tests__/slack-share-routes.test.ts +0 -1
- package/src/__tests__/stt-stream-session.test.ts +6 -5
- package/src/__tests__/subagent-call-site-routing.test.ts +69 -95
- package/src/__tests__/subagent-disposal.test.ts +2 -0
- package/src/__tests__/subagent-fork-notifications.test.ts +2 -0
- package/src/__tests__/subagent-fork-spawn.test.ts +2 -0
- package/src/__tests__/subagent-manager-notify.test.ts +2 -0
- package/src/__tests__/subagent-role-registry.test.ts +37 -0
- package/src/__tests__/subagent-spawn-and-await.test.ts +1 -0
- package/src/__tests__/subagent-terminal-message.test.ts +50 -0
- package/src/__tests__/subagent-tool-gate-mode.test.ts +78 -5
- package/src/__tests__/surface-completion-nudge-hook.test.ts +19 -0
- package/src/__tests__/telemetry-routes.test.ts +99 -19
- package/src/__tests__/tool-audit.test.ts +34 -4
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +1 -1
- package/src/__tests__/tool-profiler.test.ts +72 -1
- package/src/__tests__/tool-result-spool.test.ts +49 -4
- package/src/__tests__/tool-side-effects-slack-dm.test.ts +0 -1
- package/src/__tests__/ui-channel-variants.test.ts +108 -0
- package/src/__tests__/ui-shape-teaching.test.ts +255 -0
- package/src/__tests__/usage-attribution.test.ts +18 -41
- package/src/__tests__/user-plugin-loader.test.ts +4 -4
- package/src/__tests__/voice-config-update.test.ts +46 -0
- package/src/__tests__/voice-session-bridge.test.ts +216 -84
- package/src/__tests__/workspace-migration-131-drop-web-fetch-mode.test.ts +120 -0
- package/src/agent/loop.ts +4 -3
- package/src/api/events/open-conversation.test.ts +64 -0
- package/src/api/events/open-conversation.ts +33 -0
- package/src/api/index.ts +6 -0
- package/src/api/responses/conversation-message.ts +5 -0
- package/src/apps/app-store.ts +25 -35
- package/src/bundler/app-bundler.ts +34 -48
- package/src/bundler/app-compiler.ts +39 -4
- package/src/bundler/bundle-scanner.ts +13 -0
- package/src/bundler/manifest.ts +1 -1
- package/src/calls/__tests__/voice-session-bridge.test.ts +47 -0
- package/src/calls/call-constants.ts +5 -0
- package/src/calls/call-controller.ts +100 -32
- package/src/calls/call-transport.ts +9 -0
- package/src/calls/media-stream-output.ts +107 -4
- package/src/calls/media-stream-server.ts +29 -9
- package/src/calls/media-stream-stt-session.ts +7 -1
- package/src/calls/media-turn-detector.ts +11 -1
- package/src/calls/voice-session-bridge.ts +84 -63
- package/src/cli/commands/__tests__/inference-providers.test.ts +270 -33
- package/src/cli/commands/__tests__/notifications.test.ts +24 -3
- package/src/cli/commands/config.help.ts +6 -6
- package/src/cli/commands/credentials.help.ts +13 -0
- package/src/cli/commands/credentials.ts +6 -1
- package/src/cli/commands/email.help.ts +7 -0
- package/src/cli/commands/email.ts +35 -1
- package/src/cli/commands/inference-providers.ts +168 -107
- package/src/cli/commands/inference.help.ts +118 -46
- package/src/cli/commands/memory/index.help.ts +23 -0
- package/src/cli/commands/memory/nodes.ts +146 -0
- package/src/cli/commands/notifications.help.ts +10 -10
- package/src/cli/commands/oauth/connect-surface-guidance.test.ts +40 -0
- package/src/cli/commands/oauth/connect-surface-guidance.ts +54 -0
- package/src/cli/commands/oauth/connect.test.ts +126 -0
- package/src/cli/commands/oauth/connect.ts +29 -6
- package/src/cli/commands/oauth/index.help.ts +7 -1
- package/src/cli/commands/oauth/status.test.ts +69 -3
- package/src/cli/commands/oauth/status.ts +50 -12
- package/src/cli/commands/plugins.help.ts +13 -2
- package/src/cli/commands/plugins.ts +61 -7
- package/src/cli/commands/telemetry.help.ts +13 -0
- package/src/cli/commands/telemetry.ts +45 -5
- package/src/cli/lib/__tests__/plugin-catalog-local.test.ts +8 -2
- package/src/cli/lib/__tests__/upgrade-plugin.test.ts +213 -9
- package/src/cli/lib/bundled-marketplace.json +93 -36
- package/src/cli/lib/inspect-plugin.ts +5 -1
- package/src/cli/lib/install-from-github.ts +68 -4
- package/src/cli/lib/plugin-catalog-local.ts +6 -2
- package/src/cli/lib/plugin-fingerprint.ts +3 -3
- package/src/cli/lib/upgrade-plugin.ts +236 -35
- package/src/config/__tests__/default-profile-catalog.test.ts +8 -12
- package/src/config/__tests__/plugin-resident-skill-discovery.test.ts +137 -0
- package/src/config/__tests__/profile-materialization.test.ts +1 -88
- package/src/config/bundled-skills/AGENTS.md +3 -30
- package/src/config/bundled-skills/app-builder/SKILL.md +5 -3
- package/src/config/bundled-skills/app-builder/TOOLS.json +23 -0
- package/src/config/bundled-skills/app-builder/tools/app-open.ts +32 -0
- package/src/config/bundled-skills/messaging/tools/messaging-send.ts +1 -1
- package/src/config/bundled-skills/phone-calls/references/CONFIG.md +7 -7
- package/src/config/bundled-skills/schedule/SKILL.md +1 -1
- package/src/config/bundled-skills/schedule/TOOLS.json +1 -1
- package/src/config/bundled-skills/settings/TOOLS.json +3 -1
- package/src/config/bundled-skills/settings/tools/navigate-settings-tab.ts +2 -0
- package/src/config/bundled-skills/settings/tools/voice-config-update.ts +18 -10
- package/src/config/bundled-skills/subagent/SKILL.md +2 -0
- package/src/config/bundled-skills/subagent/TOOLS.json +1 -1
- package/src/config/call-site-defaults.ts +3 -3
- package/src/config/feature-flag-registry.json +24 -39
- package/src/config/llm-resolver.ts +88 -475
- package/src/config/profile-materialization.ts +11 -11
- package/src/config/schema.ts +93 -127
- package/src/config/schemas/__tests__/live-voice.test.ts +24 -6
- package/src/config/schemas/__tests__/stt.test.ts +31 -3
- package/src/config/schemas/live-voice.ts +10 -4
- package/src/config/schemas/llm.ts +52 -15
- package/src/config/schemas/memory-lifecycle.ts +1 -1
- package/src/config/schemas/memory-retrospective.ts +8 -0
- package/src/config/schemas/memory-v2.ts +2 -2
- package/src/config/schemas/memory-v3.ts +1 -1
- package/src/config/schemas/services.ts +6 -3
- package/src/config/schemas/stt.ts +38 -21
- package/src/config/schemas/tts.ts +13 -18
- package/src/config/skills.ts +31 -21
- package/src/context/compactor.ts +44 -7
- package/src/context/post-turn-tool-result-truncation.ts +4 -2
- package/src/context/tool-result-spool.ts +26 -5
- package/src/conversations/__tests__/message-consolidation.test.ts +48 -0
- package/src/conversations/message-consolidation.ts +22 -2
- package/src/daemon/__tests__/conversation-tool-setup.test.ts +5 -12
- package/src/daemon/app-source-watcher.ts +17 -23
- package/src/daemon/chat-credential-redaction.ts +1365 -0
- package/src/daemon/conversation-agent-loop-handlers.ts +627 -34
- package/src/daemon/conversation-agent-loop.ts +71 -31
- package/src/daemon/conversation-error.ts +36 -14
- package/src/daemon/conversation-process.ts +22 -0
- package/src/daemon/conversation-store.ts +35 -0
- package/src/daemon/conversation-surfaces.ts +25 -25
- package/src/daemon/conversation-tool-setup.ts +33 -7
- package/src/daemon/conversation.ts +55 -5
- package/src/daemon/handlers/shared.ts +33 -2
- package/src/daemon/lifecycle.ts +4 -4
- package/src/daemon/message-types/conversations.ts +6 -17
- package/src/daemon/providers-setup.ts +8 -0
- package/src/daemon/tool-setup-types.ts +7 -1
- package/src/daemon/wake-conversation-ops.ts +10 -1
- package/src/hooks/types.ts +9 -0
- package/src/ipc/__tests__/email-ipc.test.ts +90 -0
- package/src/ipc/gateway-client.test.ts +59 -0
- package/src/ipc/gateway-client.ts +70 -27
- package/src/live-voice/__tests__/live-voice-events.test.ts +14 -2
- package/src/live-voice/__tests__/live-voice-integration.test.ts +116 -2
- package/src/live-voice/__tests__/live-voice-vad.test.ts +804 -13
- package/src/live-voice/__tests__/protocol.test.ts +122 -0
- package/src/live-voice/live-voice-session.ts +443 -40
- package/src/live-voice/protocol.ts +143 -1
- package/src/monitoring/__tests__/plugin-source-watch.test.ts +3 -1
- package/src/monitoring/plugin-source-watch.ts +3 -62
- package/src/notifications/README.md +1 -1
- package/src/permissions/checker.ts +8 -4
- package/src/persistence/__tests__/db-init-migrations-ok.test.ts +26 -0
- package/src/persistence/conversation-crud.ts +104 -2
- package/src/persistence/db-init.ts +14 -3
- package/src/persistence/job-handlers/cleanup.ts +15 -7
- package/src/persistence/llm-request-log-store.ts +88 -52
- package/src/persistence/migrations/298-move-memory-jobs-to-memory-db.ts +7 -31
- package/src/persistence/migrations/305-drop-contact-acl-columns.ts +3 -2
- package/src/persistence/migrations/326-move-injection-events-to-memory-db.ts +8 -34
- package/src/persistence/migrations/336-move-memory-v2-activation-logs-to-memory-db.ts +90 -0
- package/src/persistence/migrations/337-move-memory-recall-logs-to-memory-db.ts +114 -0
- package/src/persistence/migrations/338-move-memory-v3-selections-to-memory-db.ts +84 -0
- package/src/persistence/migrations/339-move-activation-sessions-to-memory-db.ts +52 -0
- package/src/persistence/migrations/__tests__/run-migrations.test.ts +155 -0
- package/src/persistence/migrations/helpers/relocation.ts +44 -1
- package/src/persistence/migrations/run-migrations.ts +25 -1
- package/src/persistence/schema/infrastructure.ts +9 -0
- package/src/persistence/schema/memory-core.ts +3 -0
- package/src/persistence/schema/memory-injection.ts +2 -0
- package/src/persistence/steps.ts +36 -0
- package/src/platform/client.test.ts +1 -44
- package/src/platform/client.ts +10 -20
- package/src/platform/consent-cache.test.ts +89 -23
- package/src/platform/consent-cache.ts +71 -33
- package/src/plugin-api/constants.ts +12 -0
- package/src/plugin-api/conversation-turn.ts +37 -14
- package/src/plugin-api/index.ts +11 -1
- package/src/plugin-api/resolve-credential.ts +75 -0
- package/src/plugin-api/vision-support.test.ts +8 -19
- package/src/plugin-api/vision-support.ts +26 -17
- package/src/plugins/collect-source-versions.ts +77 -0
- package/src/plugins/defaults/compaction/compact.ts +6 -0
- package/src/plugins/defaults/compaction/window-manager.ts +9 -0
- package/src/plugins/defaults/empty-response/hooks/post-model-call.ts +18 -24
- package/src/plugins/defaults/empty-response/hooks/user-prompt-submit.ts +38 -0
- package/src/plugins/defaults/empty-response/refusal-quarantine.ts +99 -0
- package/src/plugins/defaults/image-fallback/__tests__/caption-cache-persistence.test.ts +7 -3
- package/src/plugins/defaults/index.ts +8 -1
- package/src/plugins/defaults/max-tokens-continue/hooks/post-model-call.ts +4 -1
- package/src/plugins/defaults/memory/__tests__/activation-session-store.test.ts +48 -6
- package/src/plugins/defaults/memory/__tests__/db-memory-attach.test.ts +28 -22
- package/src/plugins/defaults/memory/__tests__/memory-log-stores-degraded.test.ts +148 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +80 -8
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-prompt.test.ts +202 -0
- package/src/plugins/defaults/memory/__tests__/memory-v2-activation-log-store.test.ts +64 -25
- package/src/plugins/defaults/memory/__tests__/memory-v2-concept-frequency.test.ts +24 -11
- package/src/plugins/defaults/memory/__tests__/prompt-override.test.ts +70 -6
- package/src/plugins/defaults/memory/__tests__/table-relocation.test.ts +278 -0
- package/src/plugins/defaults/memory/activation-session-store.ts +27 -20
- package/src/plugins/defaults/memory/context-search/sources/memory-v2.ts +2 -9
- package/src/plugins/defaults/memory/context-search/sources/workspace.ts +1 -8
- package/src/plugins/defaults/memory/graph/retriever.test.ts +6 -6
- package/src/plugins/defaults/memory/graph/store.ts +114 -0
- package/src/plugins/defaults/memory/graph/tool-handlers.ts +36 -1
- package/src/plugins/defaults/memory/graph/tools.ts +47 -13
- package/src/plugins/defaults/memory/graph-topology/build-memory-graph.ts +16 -35
- package/src/plugins/defaults/memory/jobs-worker.ts +10 -1
- package/src/plugins/defaults/memory/memory-db.ts +3 -2
- package/src/plugins/defaults/memory/memory-recall-log-store.ts +129 -66
- package/src/plugins/defaults/memory/memory-retrospective-constants.ts +8 -0
- package/src/plugins/defaults/memory/memory-retrospective-job.ts +11 -122
- package/src/plugins/defaults/memory/memory-retrospective-prompt.ts +216 -0
- package/src/plugins/defaults/memory/memory-v2-activation-log-store.ts +84 -46
- package/src/plugins/defaults/memory/memory-v2-concept-frequency.ts +42 -32
- package/src/plugins/defaults/memory/path-containment.ts +21 -0
- package/src/plugins/defaults/memory/prompt-override.ts +51 -13
- package/src/plugins/defaults/memory/tools.test.ts +34 -0
- package/src/plugins/defaults/memory/tools.ts +12 -2
- package/src/plugins/defaults/memory/v2/__tests__/harness-compare.test.ts +19 -15
- package/src/plugins/defaults/memory/v2/__tests__/harness-oracle.test.ts +24 -19
- package/src/plugins/defaults/memory/v2/__tests__/harness-replay-input.test.ts +19 -15
- package/src/plugins/defaults/memory/v2/__tests__/injection.test.ts +22 -2
- package/src/plugins/defaults/memory/v2/__tests__/prompts-consolidation.test.ts +5 -3
- package/src/plugins/defaults/memory/v2/harness/oracle.ts +59 -41
- package/src/plugins/defaults/memory/v2/harness/replay-input.ts +29 -25
- package/src/plugins/defaults/memory/v2/migration.ts +46 -16
- package/src/plugins/defaults/memory/v2/prompts/consolidation.ts +4 -0
- package/src/plugins/defaults/memory/v3/__tests__/carry-integration.test.ts +17 -15
- package/src/plugins/defaults/memory/v3/__tests__/gate.test.ts +24 -22
- package/src/plugins/defaults/memory/v3/__tests__/injection.test.ts +9 -5
- package/src/plugins/defaults/memory/v3/__tests__/orchestrate.test.ts +277 -10
- package/src/plugins/defaults/memory/v3/__tests__/selection-log-store.test.ts +57 -5
- package/src/plugins/defaults/memory/v3/__tests__/shadow-integration.test.ts +9 -7
- package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +38 -7
- package/src/plugins/defaults/memory/v3/hot-set.test.ts +36 -18
- package/src/plugins/defaults/memory/v3/hot-set.ts +12 -14
- package/src/plugins/defaults/memory/v3/learned-edges.test.ts +47 -27
- package/src/plugins/defaults/memory/v3/learned-edges.ts +12 -14
- package/src/plugins/defaults/memory/v3/orchestrate.ts +139 -22
- package/src/plugins/defaults/memory/v3/prune.test.ts +10 -3
- package/src/plugins/defaults/memory/v3/prune.ts +17 -11
- package/src/plugins/defaults/memory/v3/selection-log-store.ts +23 -11
- package/src/plugins/defaults/memory/v3/shadow-plugin.ts +70 -53
- package/src/plugins/defaults/surface-completion-nudge/hooks/post-model-call.ts +9 -6
- package/src/plugins/external-plugin-loader.ts +15 -15
- package/src/plugins/mtime-cache.ts +47 -0
- package/src/plugins/pipeline.ts +9 -1
- package/src/plugins/plugin-execution-context.ts +44 -0
- package/src/plugins/plugin-tree-walk.ts +31 -24
- package/src/plugins/source-fingerprint.ts +3 -4
- package/src/prompts/normalize-onboarding.ts +12 -0
- package/src/prompts/persona-resolver.ts +8 -0
- package/src/prompts/templates/BOOTSTRAP-ACTIVATION-RAIL.md +1 -1
- package/src/providers/__tests__/dispatch-connection-routing.test.ts +6 -9
- package/src/providers/__tests__/registry-native-web-search.test.ts +4 -10
- package/src/providers/__tests__/retry-callsite.test.ts +235 -215
- package/src/providers/__tests__/satellite-connection-routing.test.ts +8 -16
- package/src/providers/atlascloud/client.ts +10 -49
- package/src/providers/baseten/client.ts +43 -0
- package/src/providers/call-site-routing.ts +26 -13
- package/src/providers/connection-resolution.ts +9 -11
- package/src/providers/fetch-provider-catalog.ts +4 -2
- package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +29 -4
- package/src/providers/inference/__tests__/connection-availability-keyless.test.ts +78 -0
- package/src/providers/inference/adapter-factory.ts +27 -6
- package/src/providers/inference/auth.ts +29 -1
- package/src/providers/inference/backfill.ts +61 -10
- package/src/providers/inference/connection-availability.ts +3 -2
- package/src/providers/inference/resolve-auth.ts +9 -1
- package/src/providers/model-catalog.ts +37 -0
- package/src/providers/openai/__tests__/api-error-normalization.test.ts +24 -2
- package/src/providers/openai/api-key-validation.ts +70 -0
- package/src/providers/retry.ts +2 -0
- package/src/providers/types.ts +11 -4
- package/src/providers/vellum-model-routing.test.ts +26 -0
- package/src/providers/vellum-model-routing.ts +30 -0
- package/src/providers/voice-error-copy.ts +47 -0
- package/src/runtime/agent-wake.ts +40 -24
- package/src/runtime/for-chat-mint-registry.ts +118 -0
- package/src/runtime/reveal-nonce.ts +49 -0
- package/src/runtime/reveal-success-registry.ts +306 -0
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +88 -3
- package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +161 -30
- package/src/runtime/routes/__tests__/migration-vellum-metadata-reconcile.test.ts +7 -0
- package/src/runtime/routes/__tests__/schedule-worker-routes.test.ts +15 -0
- package/src/runtime/routes/app-management-routes.ts +152 -45
- package/src/runtime/routes/app-routes.ts +16 -77
- package/src/runtime/routes/canned-message-complete.ts +16 -16
- package/src/runtime/routes/conversation-management-routes.ts +17 -14
- package/src/runtime/routes/conversation-query-routes.ts +54 -6
- package/src/runtime/routes/conversation-routes.ts +48 -2
- package/src/runtime/routes/credential-routes.ts +116 -3
- package/src/runtime/routes/email-routes.ts +17 -1
- package/src/runtime/routes/inbound-stages/transcribe-audio.test.ts +33 -5
- package/src/runtime/routes/inbound-stages/transcribe-audio.ts +6 -5
- package/src/runtime/routes/inference-provider-connection-routes.ts +77 -28
- package/src/runtime/routes/inference-send-routes.ts +1 -1
- package/src/runtime/routes/internal-telemetry-routes.ts +7 -6
- package/src/runtime/routes/plugins-routes.ts +37 -6
- package/src/runtime/routes/publish-routes.ts +15 -18
- package/src/runtime/routes/schedule-worker-routes.ts +9 -0
- package/src/runtime/routes/secret-routes.ts +10 -0
- package/src/runtime/routes/telemetry-routes.ts +149 -45
- package/src/schedule/__tests__/schedule-timezone.test.ts +101 -0
- package/src/schedule/__tests__/worker-watchdog.test.ts +209 -0
- package/src/schedule/schedule-store.ts +12 -1
- package/src/schedule/schedule-timezone.ts +63 -0
- package/src/schedule/scheduler.ts +100 -6
- package/src/schedule/worker-control.ts +19 -0
- package/src/security/auth-fallback-events-store.ts +7 -6
- package/src/security/secret-scanner.ts +26 -1
- package/src/services/published-app-updater.ts +6 -11
- package/src/stt/stt-stream-session.ts +36 -16
- package/src/subagent/manager.ts +34 -8
- package/src/telemetry/AGENTS.md +30 -1
- package/src/telemetry/__tests__/config-setting-snapshot.test.ts +18 -0
- package/src/telemetry/__tests__/outbox-test-harness.ts +5 -3
- package/src/telemetry/config-setting-snapshot.ts +44 -10
- package/src/telemetry/telemetry-event-sources.test.ts +124 -30
- package/src/telemetry/telemetry-event-sources.ts +137 -83
- package/src/telemetry/telemetry-events-outbox.test.ts +46 -1
- package/src/telemetry/telemetry-events-outbox.ts +60 -9
- package/src/telemetry/telemetry-wire-source.json +1 -1
- package/src/telemetry/telemetry-wire-validation.ts +39 -2
- package/src/telemetry/telemetry-wire.generated.ts +8 -0
- package/src/telemetry/tool-audit.ts +15 -9
- package/src/telemetry/tool-executed-events-store.test.ts +1 -1
- package/src/telemetry/turn-events-store.ts +20 -0
- package/src/telemetry/types.ts +45 -12
- package/src/telemetry/usage-telemetry-reporter.test.ts +294 -25
- package/src/telemetry/usage-telemetry-reporter.ts +117 -18
- package/src/telemetry/watchdog-direct-emit.test.ts +11 -3
- package/src/telemetry/watchdog-direct-emit.ts +11 -6
- package/src/tools/apps/executors.ts +11 -36
- package/src/tools/executor.ts +14 -1
- package/src/tools/host-terminal/host-shell.ts +6 -2
- package/src/tools/network/__tests__/web-fetch-firecrawl.test.ts +1 -1
- package/src/tools/network/__tests__/web-fetch-metadata.test.ts +25 -0
- package/src/tools/network/web-fetch.ts +6 -2
- package/src/tools/skills/sandbox-runner.ts +5 -2
- package/src/tools/subagent/spawn.ts +7 -11
- package/src/tools/terminal/shell.ts +5 -2
- package/src/tools/tool-manifest.ts +0 -2
- package/src/tools/tool-profiler.ts +37 -6
- package/src/tools/ui-surface/channel-variants.ts +101 -0
- package/src/tools/ui-surface/definitions.ts +23 -67
- package/src/tools/ui-surface/surface-shape-docs.ts +229 -0
- package/src/tts/__tests__/provider-adapters.test.ts +18 -0
- package/src/tts/provider-catalog.ts +2 -4
- package/src/tts/providers/deepgram-provider.ts +10 -1
- package/src/types/onboarding-context.ts +8 -0
- package/src/usage/attribution.ts +18 -112
- package/src/{config/bundled-skills/messaging/tools/gmail-mime-helpers.ts → util/mime-type.ts} +4 -1
- package/src/util/provider-error-patterns.ts +7 -1
- package/src/util/worker-process.ts +105 -0
- package/src/watcher/__tests__/telemetry.test.ts +17 -4
- package/src/watcher/telemetry.ts +5 -5
- package/src/workspace/migrations/131-drop-web-fetch-mode.ts +61 -0
- package/src/workspace/migrations/registry.ts +2 -0
- package/src/workspace/provider-commit-message-generator.ts +6 -4
- package/src/__tests__/app-open-proxy.test.ts +0 -67
- package/src/onboarding/onboarding-research-events-store.test.ts +0 -230
- package/src/onboarding/onboarding-research-events-store.ts +0 -104
- package/src/runtime/routes/assets/vellum-design-system.css +0 -2236
- package/src/tools/apps/definitions.ts +0 -73
- package/src/tools/apps/open-proxy.ts +0 -43
|
@@ -0,0 +1,791 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regression tests for the live reveal-guard priming barrier (LUM-2768).
|
|
3
|
+
*
|
|
4
|
+
* The race: promoting a proven `credentials reveal` starts priming
|
|
5
|
+
* `liveRevealGuardEntries` from the resolved candidates. Resolution is
|
|
6
|
+
* asynchronous (a Promise settled on a microtask), so if the next provider
|
|
7
|
+
* stream's text deltas reached `drainSentinelGuardedText` before priming
|
|
8
|
+
* settled they would guard against an EMPTY entry list and an echoed
|
|
9
|
+
* plaintext would cross SSE raw (the persisted row still redacts, but the
|
|
10
|
+
* live transcript flashed the secret). The dispatcher therefore awaits any
|
|
11
|
+
* in-flight priming before guarding a `text_delta` and before the
|
|
12
|
+
* end-of-message guard flush.
|
|
13
|
+
*
|
|
14
|
+
* The candidate value is the plaintext the reveal ROUTE
|
|
15
|
+
* served (captured on the success record), so resolution no longer re-reads
|
|
16
|
+
* the vault for a proven ref — the store mock below stays untouched on the
|
|
17
|
+
* proven path and is only exercised by the deliberate store-fallback case.
|
|
18
|
+
* The barrier still holds across the priming microtask, which the
|
|
19
|
+
* concurrent-dispatch test asserts.
|
|
20
|
+
*/
|
|
21
|
+
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
22
|
+
|
|
23
|
+
// ── Store mock: reads stay pending until the test releases them ──────────
|
|
24
|
+
// Only reached on the store-fallback path (a ref with no proven value); the
|
|
25
|
+
// proven path redacts the route-served bytes without touching the store.
|
|
26
|
+
|
|
27
|
+
type StoreRelease = (value: string | null) => void;
|
|
28
|
+
const pendingStoreReads: StoreRelease[] = [];
|
|
29
|
+
|
|
30
|
+
mock.module("../security/secure-keys.js", () => ({
|
|
31
|
+
getSecureKeyAsync: (_key: string) =>
|
|
32
|
+
new Promise<string | null>((resolve) => {
|
|
33
|
+
pendingStoreReads.push(resolve);
|
|
34
|
+
}),
|
|
35
|
+
}));
|
|
36
|
+
|
|
37
|
+
// Feature flag on regardless of config contents.
|
|
38
|
+
mock.module("../config/assistant-feature-flags.js", () => ({
|
|
39
|
+
isAssistantFeatureFlagEnabled: (flag: string) =>
|
|
40
|
+
flag === "chat-credential-reveal",
|
|
41
|
+
}));
|
|
42
|
+
|
|
43
|
+
mock.module("../config/loader.js", () => ({
|
|
44
|
+
getConfig: () => ({}),
|
|
45
|
+
}));
|
|
46
|
+
|
|
47
|
+
// ── Persistence mocks (same shape as tool-preview-lifecycle.test.ts) ─────
|
|
48
|
+
|
|
49
|
+
mock.module("../persistence/conversation-crud.js", () => ({
|
|
50
|
+
setConversationProcessingStartedAt: () => {},
|
|
51
|
+
isConversationProcessing: () => false,
|
|
52
|
+
getConversation: () => null,
|
|
53
|
+
getMessageById: () => null,
|
|
54
|
+
updateMessageContent: () => {},
|
|
55
|
+
markMessageContentInflight: () => {},
|
|
56
|
+
finalizeMessageContent: () => {},
|
|
57
|
+
provenanceFromTrustContext: () => ({}),
|
|
58
|
+
reserveMessage: () => "reserved-message-id",
|
|
59
|
+
recordConversationPersistedSeq: () => {},
|
|
60
|
+
getConversationPersistedSeq: () => null,
|
|
61
|
+
}));
|
|
62
|
+
|
|
63
|
+
mock.module("../persistence/conversation-disk-view.js", () => ({
|
|
64
|
+
syncMessageToDisk: () => {},
|
|
65
|
+
}));
|
|
66
|
+
|
|
67
|
+
mock.module("../persistence/llm-request-log-store.js", () => ({
|
|
68
|
+
recordRequestLog: () => {},
|
|
69
|
+
backfillMessageIdOnLogs: () => {},
|
|
70
|
+
}));
|
|
71
|
+
|
|
72
|
+
mock.module("../plugins/defaults/memory/memory-recall-log-store.js", () => ({
|
|
73
|
+
backfillMemoryRecallLogMessageId: () => {},
|
|
74
|
+
}));
|
|
75
|
+
|
|
76
|
+
mock.module(
|
|
77
|
+
"../plugins/defaults/memory/memory-v2-activation-log-store.js",
|
|
78
|
+
() => ({
|
|
79
|
+
backfillMemoryV2ActivationMessageId: () => {},
|
|
80
|
+
}),
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
// ── Imports (after mocks) ────────────────────────────────────────────────
|
|
84
|
+
|
|
85
|
+
import type { AgentEvent } from "../agent/loop.js";
|
|
86
|
+
import type { EventHandlerDeps } from "../daemon/conversation-agent-loop-handlers.js";
|
|
87
|
+
import {
|
|
88
|
+
createEventHandlerState,
|
|
89
|
+
dispatchAgentEvent,
|
|
90
|
+
} from "../daemon/conversation-agent-loop-handlers.js";
|
|
91
|
+
import type { ServerMessage } from "../daemon/message-protocol.js";
|
|
92
|
+
import { _resetStreamStateForTesting } from "../runtime/assistant-stream-state.js";
|
|
93
|
+
import {
|
|
94
|
+
recordForChatMint,
|
|
95
|
+
resetForChatMintRegistryForTest,
|
|
96
|
+
} from "../runtime/for-chat-mint-registry.js";
|
|
97
|
+
import {
|
|
98
|
+
_resetRevealNoncesForTest,
|
|
99
|
+
conversationRevealNonce,
|
|
100
|
+
} from "../runtime/reveal-nonce.js";
|
|
101
|
+
import {
|
|
102
|
+
_resetRevealSuccessRegistryForTest,
|
|
103
|
+
recordRevealSuccess,
|
|
104
|
+
} from "../runtime/reveal-success-registry.js";
|
|
105
|
+
import {
|
|
106
|
+
SYNTHETIC_OPAQUE_CREDENTIAL,
|
|
107
|
+
SYNTHETIC_OPENAI_PROJECT_KEY,
|
|
108
|
+
} from "./secret-fixtures.js";
|
|
109
|
+
|
|
110
|
+
function toolResults(events: ServerMessage[]): string[] {
|
|
111
|
+
return events
|
|
112
|
+
.filter(
|
|
113
|
+
(e): e is Extract<ServerMessage, { type: "tool_result" }> =>
|
|
114
|
+
(e as { type?: string }).type === "tool_result",
|
|
115
|
+
)
|
|
116
|
+
.map((e) => (typeof e.result === "string" ? e.result : ""));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function createMockDeps(collected: ServerMessage[]): EventHandlerDeps {
|
|
120
|
+
return {
|
|
121
|
+
ctx: {
|
|
122
|
+
conversationId: "test-conversation",
|
|
123
|
+
provider: { name: "anthropic" },
|
|
124
|
+
streamThinking: false,
|
|
125
|
+
emitActivityState: () => {},
|
|
126
|
+
markWorkspaceTopLevelDirty: () => {},
|
|
127
|
+
currentTurnSurfaces: [],
|
|
128
|
+
} as unknown as EventHandlerDeps["ctx"],
|
|
129
|
+
onEvent: (msg: ServerMessage) => {
|
|
130
|
+
collected.push(msg);
|
|
131
|
+
},
|
|
132
|
+
reqId: "test-req-id",
|
|
133
|
+
isFirstMessage: false,
|
|
134
|
+
shouldGenerateTitle: false,
|
|
135
|
+
rlog: new Proxy({} as Record<string, unknown>, {
|
|
136
|
+
get: () => () => {},
|
|
137
|
+
}) as unknown as EventHandlerDeps["rlog"],
|
|
138
|
+
turnChannelContext: {
|
|
139
|
+
userMessageChannel: "vellum",
|
|
140
|
+
assistantMessageChannel: "vellum",
|
|
141
|
+
} as EventHandlerDeps["turnChannelContext"],
|
|
142
|
+
turnInterfaceContext: {
|
|
143
|
+
userMessageInterface: "macos",
|
|
144
|
+
assistantMessageInterface: "macos",
|
|
145
|
+
} as EventHandlerDeps["turnInterfaceContext"],
|
|
146
|
+
} as EventHandlerDeps;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function streamedText(events: ServerMessage[]): string {
|
|
150
|
+
return events
|
|
151
|
+
.filter(
|
|
152
|
+
(e): e is Extract<ServerMessage, { type: "assistant_text_delta" }> =>
|
|
153
|
+
(e as { type?: string }).type === "assistant_text_delta",
|
|
154
|
+
)
|
|
155
|
+
.map((e) => e.text)
|
|
156
|
+
.join("");
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const REVEAL_TOOL_USE = {
|
|
160
|
+
type: "tool_use",
|
|
161
|
+
id: "toolu_reveal",
|
|
162
|
+
name: "bash",
|
|
163
|
+
input: {
|
|
164
|
+
command: "assistant credentials reveal --service openai --field api_key",
|
|
165
|
+
},
|
|
166
|
+
} as Extract<AgentEvent, { type: "tool_use" }>;
|
|
167
|
+
|
|
168
|
+
const REVEAL_TOOL_RESULT = {
|
|
169
|
+
type: "tool_result",
|
|
170
|
+
toolUseId: "toolu_reveal",
|
|
171
|
+
content: "(revealed value printed to stdout)",
|
|
172
|
+
isError: false,
|
|
173
|
+
} as Extract<AgentEvent, { type: "tool_result" }>;
|
|
174
|
+
|
|
175
|
+
beforeEach(() => {
|
|
176
|
+
_resetStreamStateForTesting();
|
|
177
|
+
_resetRevealSuccessRegistryForTest();
|
|
178
|
+
resetForChatMintRegistryForTest();
|
|
179
|
+
_resetRevealNoncesForTest();
|
|
180
|
+
pendingStoreReads.length = 0;
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
describe("live reveal guard priming barrier", () => {
|
|
184
|
+
test("a text delta dispatched while priming is pending waits for the guard and emits the sentinel", async () => {
|
|
185
|
+
const events: ServerMessage[] = [];
|
|
186
|
+
const state = createEventHandlerState();
|
|
187
|
+
const deps = createMockDeps(events);
|
|
188
|
+
|
|
189
|
+
// tool_use only STAGES the reveal — no store read may happen while the
|
|
190
|
+
// command is merely proposed (it can still be denied or cancelled).
|
|
191
|
+
await dispatchAgentEvent(state, deps, REVEAL_TOOL_USE);
|
|
192
|
+
expect(pendingStoreReads.length).toBe(0);
|
|
193
|
+
|
|
194
|
+
// The reveal route records its success (with the served plaintext) while
|
|
195
|
+
// the tool runs; the result promotes the proven refs and starts priming.
|
|
196
|
+
// The result dispatch is deliberately NOT awaited and the echo delta is
|
|
197
|
+
// dispatched in the same tick — priming settles on a microtask, so the
|
|
198
|
+
// barrier must hold the delta until then. The proven value means no store
|
|
199
|
+
// read happens: redaction uses the route-served bytes directly.
|
|
200
|
+
recordRevealSuccess(
|
|
201
|
+
"openai",
|
|
202
|
+
"api_key",
|
|
203
|
+
SYNTHETIC_OPENAI_PROJECT_KEY,
|
|
204
|
+
conversationRevealNonce("test-conversation"),
|
|
205
|
+
);
|
|
206
|
+
const resultDispatch = dispatchAgentEvent(state, deps, REVEAL_TOOL_RESULT);
|
|
207
|
+
const delta = dispatchAgentEvent(state, deps, {
|
|
208
|
+
type: "text_delta",
|
|
209
|
+
text: `Your key is ${SYNTHETIC_OPENAI_PROJECT_KEY} — rotate it.`,
|
|
210
|
+
} as Extract<AgentEvent, { type: "text_delta" }>);
|
|
211
|
+
|
|
212
|
+
// The store is never touched on the proven path.
|
|
213
|
+
expect(pendingStoreReads.length).toBe(0);
|
|
214
|
+
|
|
215
|
+
await resultDispatch;
|
|
216
|
+
await delta;
|
|
217
|
+
|
|
218
|
+
const streamed = streamedText(events);
|
|
219
|
+
expect(streamed).toContain(
|
|
220
|
+
"\u3014redacted:OpenAI Project Key:openai:api_key\u3015",
|
|
221
|
+
);
|
|
222
|
+
expect(streamed).not.toContain(SYNTHETIC_OPENAI_PROJECT_KEY);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test("a plaintext echo cannot beat the guard onto the wire", async () => {
|
|
226
|
+
const events: ServerMessage[] = [];
|
|
227
|
+
const state = createEventHandlerState();
|
|
228
|
+
const deps = createMockDeps(events);
|
|
229
|
+
|
|
230
|
+
await dispatchAgentEvent(state, deps, REVEAL_TOOL_USE);
|
|
231
|
+
recordRevealSuccess(
|
|
232
|
+
"openai",
|
|
233
|
+
"api_key",
|
|
234
|
+
SYNTHETIC_OPENAI_PROJECT_KEY,
|
|
235
|
+
conversationRevealNonce("test-conversation"),
|
|
236
|
+
);
|
|
237
|
+
// Await the result dispatch fully (promotion + priming settle), then the
|
|
238
|
+
// echo delta — the steady-state ordering. The sentinel must still appear.
|
|
239
|
+
await dispatchAgentEvent(state, deps, REVEAL_TOOL_RESULT);
|
|
240
|
+
await dispatchAgentEvent(state, deps, {
|
|
241
|
+
type: "text_delta",
|
|
242
|
+
text: `key: ${SYNTHETIC_OPENAI_PROJECT_KEY}`,
|
|
243
|
+
} as Extract<AgentEvent, { type: "text_delta" }>);
|
|
244
|
+
|
|
245
|
+
const streamed = streamedText(events);
|
|
246
|
+
expect(streamed).toContain(
|
|
247
|
+
"\u3014redacted:OpenAI Project Key:openai:api_key\u3015",
|
|
248
|
+
);
|
|
249
|
+
expect(streamed).not.toContain(SYNTHETIC_OPENAI_PROJECT_KEY);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
test("a denied or failed reveal never reads the store and stays unrevealable", async () => {
|
|
253
|
+
const events: ServerMessage[] = [];
|
|
254
|
+
const state = createEventHandlerState();
|
|
255
|
+
const deps = createMockDeps(events);
|
|
256
|
+
|
|
257
|
+
// `tool_use` is emitted before execution, so approval
|
|
258
|
+
// denial / cancellation / the route's untrusted-shell block can still
|
|
259
|
+
// stop the command. A blocked reveal never reaches the route, so no
|
|
260
|
+
// success is recorded and the staged refs must be dropped without ever
|
|
261
|
+
// touching the store — resolving at propose time would read plaintext
|
|
262
|
+
// for a reveal that never ran, bypassing the route's own policy gates.
|
|
263
|
+
await dispatchAgentEvent(state, deps, REVEAL_TOOL_USE);
|
|
264
|
+
await dispatchAgentEvent(state, deps, {
|
|
265
|
+
type: "tool_result",
|
|
266
|
+
toolUseId: "toolu_reveal",
|
|
267
|
+
content: "Command was denied by the user",
|
|
268
|
+
isError: true,
|
|
269
|
+
} as Extract<AgentEvent, { type: "tool_result" }>);
|
|
270
|
+
|
|
271
|
+
expect(pendingStoreReads.length).toBe(0);
|
|
272
|
+
|
|
273
|
+
// The refs are gone for the rest of the turn, not merely deferred.
|
|
274
|
+
await dispatchAgentEvent(state, deps, {
|
|
275
|
+
type: "text_delta",
|
|
276
|
+
text: "understood, not revealing anything",
|
|
277
|
+
} as Extract<AgentEvent, { type: "text_delta" }>);
|
|
278
|
+
expect(pendingStoreReads.length).toBe(0);
|
|
279
|
+
expect(streamedText(events)).toContain("not revealing anything");
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
test("a successful shell command is not proof — no route success, no store read", async () => {
|
|
283
|
+
const state = createEventHandlerState();
|
|
284
|
+
const deps = createMockDeps([]);
|
|
285
|
+
|
|
286
|
+
// `reveal … || true` (or an echo of the command text)
|
|
287
|
+
// yields a SUCCESSFUL tool result even though the reveal route failed
|
|
288
|
+
// or never ran. Without the route's own success record, the staged
|
|
289
|
+
// refs must not promote.
|
|
290
|
+
await dispatchAgentEvent(state, deps, REVEAL_TOOL_USE);
|
|
291
|
+
await dispatchAgentEvent(state, deps, REVEAL_TOOL_RESULT);
|
|
292
|
+
expect(pendingStoreReads.length).toBe(0);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
test("a route success for a different identity does not promote the staged refs", async () => {
|
|
296
|
+
const state = createEventHandlerState();
|
|
297
|
+
const deps = createMockDeps([]);
|
|
298
|
+
|
|
299
|
+
await dispatchAgentEvent(state, deps, REVEAL_TOOL_USE);
|
|
300
|
+
recordRevealSuccess(
|
|
301
|
+
"linear",
|
|
302
|
+
"api_key",
|
|
303
|
+
"sk-linear-other",
|
|
304
|
+
conversationRevealNonce("test-conversation"),
|
|
305
|
+
);
|
|
306
|
+
await dispatchAgentEvent(state, deps, REVEAL_TOOL_RESULT);
|
|
307
|
+
expect(pendingStoreReads.length).toBe(0);
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
test("a proven reveal primes even when the enclosing command exits non-zero", async () => {
|
|
311
|
+
const events: ServerMessage[] = [];
|
|
312
|
+
const state = createEventHandlerState();
|
|
313
|
+
const deps = createMockDeps(events);
|
|
314
|
+
|
|
315
|
+
// Compound command: the reveal route succeeded (secret is in the
|
|
316
|
+
// model's context) but a later segment failed the tool overall. The
|
|
317
|
+
// guard entry is protective here — the plaintext can still be echoed,
|
|
318
|
+
// and the reveal's own stdout is redacted on the live tool_result.
|
|
319
|
+
await dispatchAgentEvent(state, deps, REVEAL_TOOL_USE);
|
|
320
|
+
recordRevealSuccess(
|
|
321
|
+
"openai",
|
|
322
|
+
"api_key",
|
|
323
|
+
SYNTHETIC_OPENAI_PROJECT_KEY,
|
|
324
|
+
conversationRevealNonce("test-conversation"),
|
|
325
|
+
);
|
|
326
|
+
await dispatchAgentEvent(state, deps, {
|
|
327
|
+
type: "tool_result",
|
|
328
|
+
toolUseId: "toolu_reveal",
|
|
329
|
+
content: `${SYNTHETIC_OPENAI_PROJECT_KEY}\ncommand not found: bogus-follow-up`,
|
|
330
|
+
isError: true,
|
|
331
|
+
} as Extract<AgentEvent, { type: "tool_result" }>);
|
|
332
|
+
// No store read on the proven path.
|
|
333
|
+
expect(pendingStoreReads.length).toBe(0);
|
|
334
|
+
// A subsequent echo of the plaintext still swaps to the sentinel.
|
|
335
|
+
await dispatchAgentEvent(state, deps, {
|
|
336
|
+
type: "text_delta",
|
|
337
|
+
text: `again: ${SYNTHETIC_OPENAI_PROJECT_KEY}`,
|
|
338
|
+
} as Extract<AgentEvent, { type: "text_delta" }>);
|
|
339
|
+
expect(streamedText(events)).not.toContain(SYNTHETIC_OPENAI_PROJECT_KEY);
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
test("steady-state deltas with no priming in flight emit synchronously", async () => {
|
|
343
|
+
const events: ServerMessage[] = [];
|
|
344
|
+
const state = createEventHandlerState();
|
|
345
|
+
const deps = createMockDeps(events);
|
|
346
|
+
|
|
347
|
+
await dispatchAgentEvent(state, deps, {
|
|
348
|
+
type: "text_delta",
|
|
349
|
+
text: "hello",
|
|
350
|
+
} as Extract<AgentEvent, { type: "text_delta" }>);
|
|
351
|
+
|
|
352
|
+
expect(pendingStoreReads.length).toBe(0);
|
|
353
|
+
expect(streamedText(events)).toBe("hello");
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
test("a rotate-and-re-reveal in one window guards both served values", async () => {
|
|
357
|
+
const events: ServerMessage[] = [];
|
|
358
|
+
const state = createEventHandlerState();
|
|
359
|
+
const deps = createMockDeps(events);
|
|
360
|
+
|
|
361
|
+
// reveal (v1) → `credentials set` → reveal (v2) while the tool runs: the
|
|
362
|
+
// route records BOTH plaintexts, both hit the tool's stdout and the
|
|
363
|
+
// model's context, and neither is scanner-classifiable. Each must
|
|
364
|
+
// become a candidate — keeping only the latest would stream the earlier
|
|
365
|
+
// value raw on a later echo.
|
|
366
|
+
await dispatchAgentEvent(state, deps, REVEAL_TOOL_USE);
|
|
367
|
+
recordRevealSuccess(
|
|
368
|
+
"openai",
|
|
369
|
+
"api_key",
|
|
370
|
+
"hunter2-rotated-alpha",
|
|
371
|
+
conversationRevealNonce("test-conversation"),
|
|
372
|
+
);
|
|
373
|
+
recordRevealSuccess(
|
|
374
|
+
"openai",
|
|
375
|
+
"api_key",
|
|
376
|
+
"hunter2-rotated-beta",
|
|
377
|
+
conversationRevealNonce("test-conversation"),
|
|
378
|
+
);
|
|
379
|
+
await dispatchAgentEvent(state, deps, REVEAL_TOOL_RESULT);
|
|
380
|
+
expect(pendingStoreReads.length).toBe(0);
|
|
381
|
+
|
|
382
|
+
await dispatchAgentEvent(state, deps, {
|
|
383
|
+
type: "text_delta",
|
|
384
|
+
text: "old: hunter2-rotated-alpha new: hunter2-rotated-beta end",
|
|
385
|
+
} as Extract<AgentEvent, { type: "text_delta" }>);
|
|
386
|
+
|
|
387
|
+
const streamed = streamedText(events);
|
|
388
|
+
expect(streamed).not.toContain("hunter2-rotated-alpha");
|
|
389
|
+
expect(streamed).not.toContain("hunter2-rotated-beta");
|
|
390
|
+
expect(streamed).toContain(
|
|
391
|
+
"\u3014redacted:Credential:openai:api_key\u3015",
|
|
392
|
+
);
|
|
393
|
+
});
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
describe("reveal stdout redaction on the live tool_result", () => {
|
|
397
|
+
test("an opaque revealed value in the reveal's own stdout is redacted before emit", async () => {
|
|
398
|
+
const events: ServerMessage[] = [];
|
|
399
|
+
const state = createEventHandlerState();
|
|
400
|
+
const deps = createMockDeps(events);
|
|
401
|
+
|
|
402
|
+
// The reveal prints an opaque/manual token the scanner cannot classify.
|
|
403
|
+
// Without redaction here the live tool card would show it raw until a
|
|
404
|
+
// history refetch; the emitted tool_result must already hide it.
|
|
405
|
+
await dispatchAgentEvent(state, deps, REVEAL_TOOL_USE);
|
|
406
|
+
recordRevealSuccess(
|
|
407
|
+
"openai",
|
|
408
|
+
"api_key",
|
|
409
|
+
SYNTHETIC_OPAQUE_CREDENTIAL,
|
|
410
|
+
conversationRevealNonce("test-conversation"),
|
|
411
|
+
);
|
|
412
|
+
await dispatchAgentEvent(state, deps, {
|
|
413
|
+
type: "tool_result",
|
|
414
|
+
toolUseId: "toolu_reveal",
|
|
415
|
+
content: `value: ${SYNTHETIC_OPAQUE_CREDENTIAL}`,
|
|
416
|
+
isError: false,
|
|
417
|
+
} as Extract<AgentEvent, { type: "tool_result" }>);
|
|
418
|
+
|
|
419
|
+
const results = toolResults(events);
|
|
420
|
+
expect(results.length).toBe(1);
|
|
421
|
+
expect(results[0]).not.toContain(SYNTHETIC_OPAQUE_CREDENTIAL);
|
|
422
|
+
expect(results[0]).toContain("<redacted");
|
|
423
|
+
expect(pendingStoreReads.length).toBe(0);
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
test("buffers the RAW tool result so persist redacts exactly once", async () => {
|
|
427
|
+
const events: ServerMessage[] = [];
|
|
428
|
+
const state = createEventHandlerState();
|
|
429
|
+
const deps = createMockDeps(events);
|
|
430
|
+
|
|
431
|
+
// The live emit is redacted, but the pending buffer must keep the raw
|
|
432
|
+
// bytes: every persist path redacts via buildToolResultBlocks, and
|
|
433
|
+
// buffering already-redacted content would redact twice — a candidate
|
|
434
|
+
// value overlapping the marker's own text would corrupt the persisted
|
|
435
|
+
// marker on the second pass.
|
|
436
|
+
await dispatchAgentEvent(state, deps, REVEAL_TOOL_USE);
|
|
437
|
+
recordRevealSuccess(
|
|
438
|
+
"openai",
|
|
439
|
+
"api_key",
|
|
440
|
+
SYNTHETIC_OPAQUE_CREDENTIAL,
|
|
441
|
+
conversationRevealNonce("test-conversation"),
|
|
442
|
+
);
|
|
443
|
+
await dispatchAgentEvent(state, deps, {
|
|
444
|
+
type: "tool_result",
|
|
445
|
+
toolUseId: "toolu_reveal",
|
|
446
|
+
content: `value: ${SYNTHETIC_OPAQUE_CREDENTIAL}`,
|
|
447
|
+
isError: false,
|
|
448
|
+
} as Extract<AgentEvent, { type: "tool_result" }>);
|
|
449
|
+
|
|
450
|
+
expect(toolResults(events)[0]).not.toContain(SYNTHETIC_OPAQUE_CREDENTIAL);
|
|
451
|
+
expect(state.pendingToolResults.get("toolu_reveal")?.content).toBe(
|
|
452
|
+
`value: ${SYNTHETIC_OPAQUE_CREDENTIAL}`,
|
|
453
|
+
);
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
test("a tool_result that never revealed anything is forwarded unchanged", async () => {
|
|
457
|
+
const events: ServerMessage[] = [];
|
|
458
|
+
const state = createEventHandlerState();
|
|
459
|
+
const deps = createMockDeps(events);
|
|
460
|
+
|
|
461
|
+
// No reveal staged/proven — the fast path must not alter ordinary output.
|
|
462
|
+
await dispatchAgentEvent(state, deps, {
|
|
463
|
+
type: "tool_use",
|
|
464
|
+
id: "toolu_ls",
|
|
465
|
+
name: "bash",
|
|
466
|
+
input: { command: "ls -la" },
|
|
467
|
+
} as Extract<AgentEvent, { type: "tool_use" }>);
|
|
468
|
+
await dispatchAgentEvent(state, deps, {
|
|
469
|
+
type: "tool_result",
|
|
470
|
+
toolUseId: "toolu_ls",
|
|
471
|
+
content: "total 0\ndrwxr-xr-x 2 user staff",
|
|
472
|
+
isError: false,
|
|
473
|
+
} as Extract<AgentEvent, { type: "tool_result" }>);
|
|
474
|
+
|
|
475
|
+
const results = toolResults(events);
|
|
476
|
+
expect(results[0]).toBe("total 0\ndrwxr-xr-x 2 user staff");
|
|
477
|
+
expect(pendingStoreReads.length).toBe(0);
|
|
478
|
+
});
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
function outputChunks(events: ServerMessage[]): string[] {
|
|
482
|
+
return events
|
|
483
|
+
.filter(
|
|
484
|
+
(e): e is Extract<ServerMessage, { type: "tool_output_chunk" }> =>
|
|
485
|
+
(e as { type?: string }).type === "tool_output_chunk",
|
|
486
|
+
)
|
|
487
|
+
.map((e) => e.chunk);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
describe("live tool output chunk redaction", () => {
|
|
491
|
+
test("reveal stdout chunks are redacted before forwarding", async () => {
|
|
492
|
+
const events: ServerMessage[] = [];
|
|
493
|
+
const state = createEventHandlerState();
|
|
494
|
+
const deps = createMockDeps(events);
|
|
495
|
+
|
|
496
|
+
// The chunk streams WHILE the tool runs — before any tool_result — and
|
|
497
|
+
// the drawer renders it live. The route recorded its success before the
|
|
498
|
+
// CLI could print, so the guard has the proven value synchronously.
|
|
499
|
+
await dispatchAgentEvent(state, deps, REVEAL_TOOL_USE);
|
|
500
|
+
recordRevealSuccess(
|
|
501
|
+
"openai",
|
|
502
|
+
"api_key",
|
|
503
|
+
SYNTHETIC_OPAQUE_CREDENTIAL,
|
|
504
|
+
conversationRevealNonce("test-conversation"),
|
|
505
|
+
);
|
|
506
|
+
await dispatchAgentEvent(state, deps, {
|
|
507
|
+
type: "tool_output_chunk",
|
|
508
|
+
toolUseId: "toolu_reveal",
|
|
509
|
+
chunk: `value: ${SYNTHETIC_OPAQUE_CREDENTIAL}\n`,
|
|
510
|
+
} as Extract<AgentEvent, { type: "tool_output_chunk" }>);
|
|
511
|
+
|
|
512
|
+
const streamedChunks = outputChunks(events).join("");
|
|
513
|
+
expect(streamedChunks).not.toContain(SYNTHETIC_OPAQUE_CREDENTIAL);
|
|
514
|
+
expect(streamedChunks).toContain('<redacted type="Credential" />');
|
|
515
|
+
// Synchronous path — the guard never touches the store.
|
|
516
|
+
expect(pendingStoreReads.length).toBe(0);
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
test("a value split across chunks is held back, then flushed redacted", async () => {
|
|
520
|
+
const events: ServerMessage[] = [];
|
|
521
|
+
const state = createEventHandlerState();
|
|
522
|
+
const deps = createMockDeps(events);
|
|
523
|
+
|
|
524
|
+
await dispatchAgentEvent(state, deps, REVEAL_TOOL_USE);
|
|
525
|
+
recordRevealSuccess(
|
|
526
|
+
"openai",
|
|
527
|
+
"api_key",
|
|
528
|
+
SYNTHETIC_OPAQUE_CREDENTIAL,
|
|
529
|
+
conversationRevealNonce("test-conversation"),
|
|
530
|
+
);
|
|
531
|
+
const head = SYNTHETIC_OPAQUE_CREDENTIAL.slice(0, 8);
|
|
532
|
+
const tail = SYNTHETIC_OPAQUE_CREDENTIAL.slice(8);
|
|
533
|
+
await dispatchAgentEvent(state, deps, {
|
|
534
|
+
type: "tool_output_chunk",
|
|
535
|
+
toolUseId: "toolu_reveal",
|
|
536
|
+
chunk: `out: ${head}`,
|
|
537
|
+
} as Extract<AgentEvent, { type: "tool_output_chunk" }>);
|
|
538
|
+
// The partial prefix is held — nothing containing it is emitted yet.
|
|
539
|
+
expect(outputChunks(events).join("")).toBe("out: ");
|
|
540
|
+
await dispatchAgentEvent(state, deps, {
|
|
541
|
+
type: "tool_output_chunk",
|
|
542
|
+
toolUseId: "toolu_reveal",
|
|
543
|
+
chunk: `${tail} done`,
|
|
544
|
+
} as Extract<AgentEvent, { type: "tool_output_chunk" }>);
|
|
545
|
+
|
|
546
|
+
const streamedChunks = outputChunks(events).join("");
|
|
547
|
+
expect(streamedChunks).toBe('out: <redacted type="Credential" /> done');
|
|
548
|
+
expect(pendingStoreReads.length).toBe(0);
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
test("a held remainder is DISCARDED at tool_result, never emitted raw", async () => {
|
|
552
|
+
const events: ServerMessage[] = [];
|
|
553
|
+
const state = createEventHandlerState();
|
|
554
|
+
const deps = createMockDeps(events);
|
|
555
|
+
|
|
556
|
+
await dispatchAgentEvent(state, deps, REVEAL_TOOL_USE);
|
|
557
|
+
recordRevealSuccess(
|
|
558
|
+
"openai",
|
|
559
|
+
"api_key",
|
|
560
|
+
SYNTHETIC_OPAQUE_CREDENTIAL,
|
|
561
|
+
conversationRevealNonce("test-conversation"),
|
|
562
|
+
);
|
|
563
|
+
// The stream ends mid-value: the tail is held when the result arrives.
|
|
564
|
+
const head = SYNTHETIC_OPAQUE_CREDENTIAL.slice(0, 8);
|
|
565
|
+
await dispatchAgentEvent(state, deps, {
|
|
566
|
+
type: "tool_output_chunk",
|
|
567
|
+
toolUseId: "toolu_reveal",
|
|
568
|
+
chunk: `out: ${head}`,
|
|
569
|
+
} as Extract<AgentEvent, { type: "tool_output_chunk" }>);
|
|
570
|
+
await dispatchAgentEvent(state, deps, {
|
|
571
|
+
type: "tool_result",
|
|
572
|
+
toolUseId: "toolu_reveal",
|
|
573
|
+
content: `out: ${SYNTHETIC_OPAQUE_CREDENTIAL}`,
|
|
574
|
+
isError: false,
|
|
575
|
+
} as Extract<AgentEvent, { type: "tool_result" }>);
|
|
576
|
+
|
|
577
|
+
// The held bytes are a raw credential prefix that complete-value
|
|
578
|
+
// redaction cannot mask — they must never reach the wire. The redacted
|
|
579
|
+
// final result supersedes the streamed view, so nothing is lost.
|
|
580
|
+
const streamedChunks = outputChunks(events).join("");
|
|
581
|
+
expect(streamedChunks).toBe("out: ");
|
|
582
|
+
expect(streamedChunks).not.toContain(head);
|
|
583
|
+
expect(state.toolOutputGuardBuffers.size).toBe(0);
|
|
584
|
+
const results = toolResults(events);
|
|
585
|
+
expect(results[0]).toBe('out: <redacted type="Credential" />');
|
|
586
|
+
expect(results[0]).not.toContain(SYNTHETIC_OPAQUE_CREDENTIAL);
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
test("chunks from a tool with no reveal candidates pass through verbatim", async () => {
|
|
590
|
+
const events: ServerMessage[] = [];
|
|
591
|
+
const state = createEventHandlerState();
|
|
592
|
+
const deps = createMockDeps(events);
|
|
593
|
+
|
|
594
|
+
await dispatchAgentEvent(state, deps, {
|
|
595
|
+
type: "tool_use",
|
|
596
|
+
id: "toolu_ls",
|
|
597
|
+
name: "bash",
|
|
598
|
+
input: { command: "ls -la" },
|
|
599
|
+
} as Extract<AgentEvent, { type: "tool_use" }>);
|
|
600
|
+
await dispatchAgentEvent(state, deps, {
|
|
601
|
+
type: "tool_output_chunk",
|
|
602
|
+
toolUseId: "toolu_ls",
|
|
603
|
+
chunk: "total 0\n",
|
|
604
|
+
} as Extract<AgentEvent, { type: "tool_output_chunk" }>);
|
|
605
|
+
|
|
606
|
+
expect(outputChunks(events)).toEqual(["total 0\n"]);
|
|
607
|
+
expect(pendingStoreReads.length).toBe(0);
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
test("a later tool echoing an already-promoted value is redacted too", async () => {
|
|
611
|
+
const events: ServerMessage[] = [];
|
|
612
|
+
const state = createEventHandlerState();
|
|
613
|
+
const deps = createMockDeps(events);
|
|
614
|
+
|
|
615
|
+
// Tool 1: the reveal, promoted at its result.
|
|
616
|
+
await dispatchAgentEvent(state, deps, REVEAL_TOOL_USE);
|
|
617
|
+
recordRevealSuccess(
|
|
618
|
+
"openai",
|
|
619
|
+
"api_key",
|
|
620
|
+
SYNTHETIC_OPAQUE_CREDENTIAL,
|
|
621
|
+
conversationRevealNonce("test-conversation"),
|
|
622
|
+
);
|
|
623
|
+
await dispatchAgentEvent(state, deps, REVEAL_TOOL_RESULT);
|
|
624
|
+
// Tool 2: `echo <value>` — no reveal staged, but the promoted turn
|
|
625
|
+
// candidates must still guard its live stdout.
|
|
626
|
+
await dispatchAgentEvent(state, deps, {
|
|
627
|
+
type: "tool_use",
|
|
628
|
+
id: "toolu_echo",
|
|
629
|
+
name: "bash",
|
|
630
|
+
input: { command: "echo it back" },
|
|
631
|
+
} as Extract<AgentEvent, { type: "tool_use" }>);
|
|
632
|
+
await dispatchAgentEvent(state, deps, {
|
|
633
|
+
type: "tool_output_chunk",
|
|
634
|
+
toolUseId: "toolu_echo",
|
|
635
|
+
chunk: `${SYNTHETIC_OPAQUE_CREDENTIAL}\n`,
|
|
636
|
+
} as Extract<AgentEvent, { type: "tool_output_chunk" }>);
|
|
637
|
+
|
|
638
|
+
const streamedChunks = outputChunks(events).join("");
|
|
639
|
+
expect(streamedChunks).not.toContain(SYNTHETIC_OPAQUE_CREDENTIAL);
|
|
640
|
+
expect(streamedChunks).toContain('<redacted type="Credential" />');
|
|
641
|
+
});
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
describe("for-chat re-mint authority (two legs: staged AND executed)", () => {
|
|
645
|
+
const FOR_CHAT_SENTINEL = "\u3014redacted:Credential:openai:api_key\u3015";
|
|
646
|
+
|
|
647
|
+
const FOR_CHAT_TOOL_USE = {
|
|
648
|
+
type: "tool_use",
|
|
649
|
+
id: "toolu_forchat",
|
|
650
|
+
name: "bash",
|
|
651
|
+
input: {
|
|
652
|
+
command:
|
|
653
|
+
"assistant credentials reveal --for-chat --service openai --field api_key",
|
|
654
|
+
},
|
|
655
|
+
} as Extract<AgentEvent, { type: "tool_use" }>;
|
|
656
|
+
|
|
657
|
+
const FOR_CHAT_TOOL_RESULT = {
|
|
658
|
+
type: "tool_result",
|
|
659
|
+
toolUseId: "toolu_forchat",
|
|
660
|
+
content: FOR_CHAT_SENTINEL,
|
|
661
|
+
isError: false,
|
|
662
|
+
} as Extract<AgentEvent, { type: "tool_result" }>;
|
|
663
|
+
|
|
664
|
+
test("staged by this run + route-minted: the echoed sentinel re-mints on the live wire", async () => {
|
|
665
|
+
const events: ServerMessage[] = [];
|
|
666
|
+
const state = createEventHandlerState();
|
|
667
|
+
const deps = createMockDeps(events);
|
|
668
|
+
|
|
669
|
+
await dispatchAgentEvent(state, deps, FOR_CHAT_TOOL_USE);
|
|
670
|
+
// The route records the mint while the tool executes (no plaintext
|
|
671
|
+
// proof — the for-chat channel never returns the secret), stamped with
|
|
672
|
+
// the nonce this conversation's tool shell forwarded.
|
|
673
|
+
recordForChatMint({
|
|
674
|
+
service: "openai",
|
|
675
|
+
field: "api_key",
|
|
676
|
+
sentinel: FOR_CHAT_SENTINEL,
|
|
677
|
+
nonce: conversationRevealNonce("test-conversation"),
|
|
678
|
+
});
|
|
679
|
+
await dispatchAgentEvent(state, deps, FOR_CHAT_TOOL_RESULT);
|
|
680
|
+
|
|
681
|
+
await dispatchAgentEvent(state, deps, {
|
|
682
|
+
type: "text_delta",
|
|
683
|
+
text: `Here it is: ${FOR_CHAT_SENTINEL} — click to reveal.`,
|
|
684
|
+
} as Extract<AgentEvent, { type: "text_delta" }>);
|
|
685
|
+
|
|
686
|
+
expect(streamedText(events)).toContain(FOR_CHAT_SENTINEL);
|
|
687
|
+
expect(streamedText(events)).not.toContain("\u2060");
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
test("a concurrent turn's mint — staged here via a quote, executed elsewhere — neutralizes", async () => {
|
|
691
|
+
// The concurrent-turn attack: this run STAGES the identity (a quoted
|
|
692
|
+
// reveal command parses identically to a real one) while an
|
|
693
|
+
// overlapping conversation executes a real `--for-chat` reveal for the
|
|
694
|
+
// same identity. That mint passes the watermark and staging checks but
|
|
695
|
+
// carries the OTHER conversation's nonce, so it grants nothing here
|
|
696
|
+
// and the echoed sentinel neutralizes like any forgery.
|
|
697
|
+
const events: ServerMessage[] = [];
|
|
698
|
+
const state = createEventHandlerState();
|
|
699
|
+
const deps = createMockDeps(events);
|
|
700
|
+
|
|
701
|
+
await dispatchAgentEvent(state, deps, FOR_CHAT_TOOL_USE);
|
|
702
|
+
recordForChatMint({
|
|
703
|
+
service: "openai",
|
|
704
|
+
field: "api_key",
|
|
705
|
+
sentinel: FOR_CHAT_SENTINEL,
|
|
706
|
+
nonce: conversationRevealNonce("some-other-conversation"),
|
|
707
|
+
});
|
|
708
|
+
await dispatchAgentEvent(state, deps, FOR_CHAT_TOOL_RESULT);
|
|
709
|
+
|
|
710
|
+
await dispatchAgentEvent(state, deps, {
|
|
711
|
+
type: "text_delta",
|
|
712
|
+
text: `forged: ${FOR_CHAT_SENTINEL}!`,
|
|
713
|
+
} as Extract<AgentEvent, { type: "text_delta" }>);
|
|
714
|
+
|
|
715
|
+
expect(streamedText(events)).not.toContain(FOR_CHAT_SENTINEL);
|
|
716
|
+
expect(streamedText(events)).toContain("\u3014\u2060redacted:");
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
test("a concurrent conversation's later same-credential mint does not clobber this run's own", async () => {
|
|
720
|
+
// Both conversations legitimately reveal the SAME credential; the
|
|
721
|
+
// other conversation's mint lands later. This run's own mint must
|
|
722
|
+
// still re-mint its echoed sentinel — an identity-only dedupe in the
|
|
723
|
+
// registry would have dropped it in favor of the foreign-nonce record.
|
|
724
|
+
const events: ServerMessage[] = [];
|
|
725
|
+
const state = createEventHandlerState();
|
|
726
|
+
const deps = createMockDeps(events);
|
|
727
|
+
|
|
728
|
+
await dispatchAgentEvent(state, deps, FOR_CHAT_TOOL_USE);
|
|
729
|
+
recordForChatMint({
|
|
730
|
+
service: "openai",
|
|
731
|
+
field: "api_key",
|
|
732
|
+
sentinel: FOR_CHAT_SENTINEL,
|
|
733
|
+
nonce: conversationRevealNonce("test-conversation"),
|
|
734
|
+
});
|
|
735
|
+
recordForChatMint({
|
|
736
|
+
service: "openai",
|
|
737
|
+
field: "api_key",
|
|
738
|
+
sentinel: FOR_CHAT_SENTINEL,
|
|
739
|
+
nonce: conversationRevealNonce("some-other-conversation"),
|
|
740
|
+
});
|
|
741
|
+
await dispatchAgentEvent(state, deps, FOR_CHAT_TOOL_RESULT);
|
|
742
|
+
|
|
743
|
+
await dispatchAgentEvent(state, deps, {
|
|
744
|
+
type: "text_delta",
|
|
745
|
+
text: `Here it is: ${FOR_CHAT_SENTINEL}`,
|
|
746
|
+
} as Extract<AgentEvent, { type: "text_delta" }>);
|
|
747
|
+
|
|
748
|
+
expect(streamedText(events)).toContain(FOR_CHAT_SENTINEL);
|
|
749
|
+
expect(streamedText(events)).not.toContain("\u2060");
|
|
750
|
+
});
|
|
751
|
+
|
|
752
|
+
test("a mint alone — identity never staged by this run — neutralizes", async () => {
|
|
753
|
+
const events: ServerMessage[] = [];
|
|
754
|
+
const state = createEventHandlerState();
|
|
755
|
+
const deps = createMockDeps(events);
|
|
756
|
+
|
|
757
|
+
recordForChatMint({
|
|
758
|
+
service: "openai",
|
|
759
|
+
field: "api_key",
|
|
760
|
+
sentinel: FOR_CHAT_SENTINEL,
|
|
761
|
+
nonce: conversationRevealNonce("test-conversation"),
|
|
762
|
+
});
|
|
763
|
+
|
|
764
|
+
await dispatchAgentEvent(state, deps, {
|
|
765
|
+
type: "text_delta",
|
|
766
|
+
text: `forged: ${FOR_CHAT_SENTINEL}!`,
|
|
767
|
+
} as Extract<AgentEvent, { type: "text_delta" }>);
|
|
768
|
+
|
|
769
|
+
expect(streamedText(events)).not.toContain(FOR_CHAT_SENTINEL);
|
|
770
|
+
expect(streamedText(events)).toContain("\u3014\u2060redacted:");
|
|
771
|
+
});
|
|
772
|
+
|
|
773
|
+
test("staging alone — a quoted command that never executed — neutralizes", async () => {
|
|
774
|
+
const events: ServerMessage[] = [];
|
|
775
|
+
const state = createEventHandlerState();
|
|
776
|
+
const deps = createMockDeps(events);
|
|
777
|
+
|
|
778
|
+
// Staged (the parse cannot tell a quoted invocation from a real one)…
|
|
779
|
+
await dispatchAgentEvent(state, deps, FOR_CHAT_TOOL_USE);
|
|
780
|
+
await dispatchAgentEvent(state, deps, FOR_CHAT_TOOL_RESULT);
|
|
781
|
+
// …but the route never ran, so no mint exists.
|
|
782
|
+
|
|
783
|
+
await dispatchAgentEvent(state, deps, {
|
|
784
|
+
type: "text_delta",
|
|
785
|
+
text: `quoted: ${FOR_CHAT_SENTINEL}`,
|
|
786
|
+
} as Extract<AgentEvent, { type: "text_delta" }>);
|
|
787
|
+
|
|
788
|
+
expect(streamedText(events)).not.toContain(FOR_CHAT_SENTINEL);
|
|
789
|
+
expect(streamedText(events)).toContain("\u3014\u2060redacted:");
|
|
790
|
+
});
|
|
791
|
+
});
|