@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
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* testable while keeping shared mutable state bundled in EventHandlerState.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import { SENTINEL_REDACTION_VERSION } from "@vellumai/service-contracts/redacted-credential";
|
|
9
10
|
import type pino from "pino";
|
|
10
11
|
import { v4 as uuid } from "uuid";
|
|
11
12
|
|
|
@@ -15,6 +16,7 @@ import type {
|
|
|
15
16
|
TurnChannelContext,
|
|
16
17
|
TurnInterfaceContext,
|
|
17
18
|
} from "../channels/types.js";
|
|
19
|
+
import { isAssistantFeatureFlagEnabled } from "../config/assistant-feature-flags.js";
|
|
18
20
|
import { getConfig } from "../config/loader.js";
|
|
19
21
|
import { recordEstimate } from "../context/estimator-calibration.js";
|
|
20
22
|
import { stripInjectionsForCompaction } from "../context/strip-injections.js";
|
|
@@ -61,7 +63,18 @@ import type {
|
|
|
61
63
|
Message,
|
|
62
64
|
} from "../providers/types.js";
|
|
63
65
|
import { getCurrentSeq } from "../runtime/assistant-stream-state.js";
|
|
64
|
-
import {
|
|
66
|
+
import type { ForChatMint } from "../runtime/for-chat-mint-registry.js";
|
|
67
|
+
import {
|
|
68
|
+
currentForChatMintWatermark,
|
|
69
|
+
forChatMintsSince,
|
|
70
|
+
} from "../runtime/for-chat-mint-registry.js";
|
|
71
|
+
import { conversationRevealNonce } from "../runtime/reveal-nonce.js";
|
|
72
|
+
import {
|
|
73
|
+
closeRevealProofWindow,
|
|
74
|
+
currentRevealSuccessWatermark,
|
|
75
|
+
openRevealProofWindow,
|
|
76
|
+
} from "../runtime/reveal-success-registry.js";
|
|
77
|
+
import { credentialKey } from "../security/credential-key.js";
|
|
65
78
|
import { extractDomain } from "../tools/network/domain-normalize.js";
|
|
66
79
|
import {
|
|
67
80
|
classifyWebSearchFailure,
|
|
@@ -81,6 +94,26 @@ import {
|
|
|
81
94
|
cleanAssistantContent,
|
|
82
95
|
drainDirectiveDisplayBuffer,
|
|
83
96
|
} from "./assistant-attachments.js";
|
|
97
|
+
import type {
|
|
98
|
+
LiveRevealGuardEntry,
|
|
99
|
+
ResolvedRevealCandidate,
|
|
100
|
+
RevealCandidateRef,
|
|
101
|
+
} from "./chat-credential-redaction.js";
|
|
102
|
+
import {
|
|
103
|
+
buildLiveRevealGuardEntries,
|
|
104
|
+
collectRevealRefsFromCommand,
|
|
105
|
+
drainCandidateGuardedChunk,
|
|
106
|
+
drainSentinelGuardedText,
|
|
107
|
+
filterRefsByRevealProof,
|
|
108
|
+
neutralizeAndSwapLiveRevealValues,
|
|
109
|
+
redactCandidateValuesLegacy,
|
|
110
|
+
redactSecretsForChat,
|
|
111
|
+
remintAuthoritiesFromCandidates,
|
|
112
|
+
resolveProvenRevealCandidates,
|
|
113
|
+
resolveRefIdentities,
|
|
114
|
+
resolveRevealCandidates,
|
|
115
|
+
type SentinelRemintAuthority,
|
|
116
|
+
} from "./chat-credential-redaction.js";
|
|
84
117
|
import type { Conversation } from "./conversation.js";
|
|
85
118
|
import type { AssistantSurface } from "./conversation-agent-loop.js";
|
|
86
119
|
import {
|
|
@@ -351,6 +384,111 @@ export interface EventHandlerState {
|
|
|
351
384
|
* every produced assistant row is indexed.
|
|
352
385
|
*/
|
|
353
386
|
readonly deferredFinalizeEffects: Array<() => Promise<void>>;
|
|
387
|
+
/**
|
|
388
|
+
* Credential refs parsed from `credentials reveal` invocations in this
|
|
389
|
+
* turn's shell-style tool commands (see `chat-credential-redaction.ts`).
|
|
390
|
+
* Staged per tool_use in {@link pendingRevealRefsByToolUse} and promoted
|
|
391
|
+
* here only when that tool's result arrives successfully; consumed at the
|
|
392
|
+
* persist seams to scope the candidate fetch for redaction-sentinel
|
|
393
|
+
* enrichment. Only ever read when the `chat-credential-reveal` feature
|
|
394
|
+
* flag is on.
|
|
395
|
+
*/
|
|
396
|
+
readonly revealCandidateRefs: RevealCandidateRef[];
|
|
397
|
+
/**
|
|
398
|
+
* Reveal refs parsed at `tool_use` time, staged until the reveal route
|
|
399
|
+
* itself proves it executed. `tool_use` is emitted BEFORE tool
|
|
400
|
+
* execution — approval denial, cancellation, or the route's
|
|
401
|
+
* untrusted-shell block can still stop the command — and candidate
|
|
402
|
+
* resolution reads plaintext straight from the store, so resolving at
|
|
403
|
+
* propose time would fetch secrets for a reveal that never ran,
|
|
404
|
+
* side-stepping the reveal route's own policy gates. The enclosing
|
|
405
|
+
* tool's success is not proof either (`reveal … || true`, or an echo of
|
|
406
|
+
* the command text), so each staging captures a reveal-success-registry
|
|
407
|
+
* watermark and `handleToolResult` promotes only the refs whose
|
|
408
|
+
* identity the route actually served after it (see
|
|
409
|
+
* `filterRefsByRevealProof`); the rest are dropped.
|
|
410
|
+
*/
|
|
411
|
+
readonly pendingRevealRefsByToolUse: Map<
|
|
412
|
+
string,
|
|
413
|
+
/** `proofWindowToken` arms registry recording for this staging's
|
|
414
|
+
* lifetime (see `openRevealProofWindow`) and is closed at result. */
|
|
415
|
+
{ refs: RevealCandidateRef[]; watermark: number; proofWindowToken: number }
|
|
416
|
+
>;
|
|
417
|
+
/**
|
|
418
|
+
* Tool stdout held back from live `tool_output_chunk` emission because
|
|
419
|
+
* it ends in a partial occurrence of a reveal candidate's plaintext
|
|
420
|
+
* (keyed by toolUseId). Re-prepended to the tool's next chunk and
|
|
421
|
+
* flushed, redacted, when its tool_result arrives — nothing can complete
|
|
422
|
+
* the partial after that. Only ever populated while proven reveal
|
|
423
|
+
* candidates exist (see `handleToolOutputChunk`).
|
|
424
|
+
*/
|
|
425
|
+
readonly toolOutputGuardBuffers: Map<string, string>;
|
|
426
|
+
/**
|
|
427
|
+
* Live text the sentinel stream guard held back from
|
|
428
|
+
* `assistant_text_delta` emission — a trailing partial `〔redacted:`
|
|
429
|
+
* trigger or reveal-candidate plaintext prefix that a later chunk may
|
|
430
|
+
* complete. Re-prepended to the next delta and flushed
|
|
431
|
+
* (neutralized + candidate-swapped) at message_complete. See
|
|
432
|
+
* `drainSentinelGuardedText`.
|
|
433
|
+
*/
|
|
434
|
+
pendingSentinelGuardBuffer: string;
|
|
435
|
+
/**
|
|
436
|
+
* Precomputed live-swap entries for this turn's reveal candidates: when
|
|
437
|
+
* the model echoes a candidate's plaintext, the stream guard replaces it
|
|
438
|
+
* with its enriched sentinel before emission so the secret never reaches
|
|
439
|
+
* the wire. Primed asynchronously when `handleToolResult` confirms a
|
|
440
|
+
* staged reveal invocation actually succeeded — before the model's next
|
|
441
|
+
* text can echo the tool's stdout. Empty when the
|
|
442
|
+
* `chat-credential-reveal` flag is off.
|
|
443
|
+
*/
|
|
444
|
+
liveRevealGuardEntries: readonly LiveRevealGuardEntry[];
|
|
445
|
+
/**
|
|
446
|
+
* Re-mint authorities derived from this turn's proven reveal candidates
|
|
447
|
+
* (see `remintAuthoritiesFromCandidates`), primed together with
|
|
448
|
+
* {@link liveRevealGuardEntries} behind the same dispatcher barrier so
|
|
449
|
+
* the live guard can consult them synchronously. Combined at each guard
|
|
450
|
+
* site with the `--for-chat` mints the reveal route recorded this run.
|
|
451
|
+
*/
|
|
452
|
+
candidateRemintAuthorities: readonly SentinelRemintAuthority[];
|
|
453
|
+
/**
|
|
454
|
+
* `--for-chat` mint-registry watermark captured when this run's state
|
|
455
|
+
* was created. `turnForChatMints` returns only mints the reveal route
|
|
456
|
+
* recorded after this point — the guard's authority for re-minting a
|
|
457
|
+
* sentinel identity is an actually-executed reveal, never a parse of
|
|
458
|
+
* the requested command (which would let a quoted/commented-out
|
|
459
|
+
* invocation allowlist a forgery).
|
|
460
|
+
*/
|
|
461
|
+
readonly forChatMintWatermark: number;
|
|
462
|
+
/**
|
|
463
|
+
* `credentialKey`-formatted identities of every reveal invocation THIS run
|
|
464
|
+
* staged from its own `tool_use` commands (`credentialKey` format). The
|
|
465
|
+
* conversation-scoping leg of re-mint authority: registry mints are
|
|
466
|
+
* global (a mint's request body carries no trustworthy conversation
|
|
467
|
+
* identity — see the registry module doc), so `turnForChatMints` accepts
|
|
468
|
+
* only mints whose identity this run itself requested. Staging parses
|
|
469
|
+
* the command the daemon actually dispatched — trusted context a
|
|
470
|
+
* subprocess env override can never rewrite.
|
|
471
|
+
*/
|
|
472
|
+
readonly stagedRevealIdentities: Set<string>;
|
|
473
|
+
/**
|
|
474
|
+
* In-flight priming of {@link liveRevealGuardEntries}. The dispatcher
|
|
475
|
+
* awaits this before processing a `text_delta` (and before the
|
|
476
|
+
* end-of-message guard flush) so a fast reveal echo can never race the
|
|
477
|
+
* guard: without the barrier, a quick tool return plus a slow store read
|
|
478
|
+
* would let `drainSentinelGuardedText` run with an empty entry list and
|
|
479
|
+
* send the plaintext over SSE even though the persisted row redacts it.
|
|
480
|
+
* Cleared once settled — steady-state deltas await nothing.
|
|
481
|
+
*/
|
|
482
|
+
liveRevealGuardPriming: Promise<void> | undefined;
|
|
483
|
+
/**
|
|
484
|
+
* Memoized resolution of {@link revealCandidateRefs} so a turn with many
|
|
485
|
+
* persist flushes fetches each candidate's plaintext once. Invalidated by
|
|
486
|
+
* ref-count when a later tool call adds more reveal invocations mid-turn.
|
|
487
|
+
*/
|
|
488
|
+
revealCandidateCache?: {
|
|
489
|
+
refCount: number;
|
|
490
|
+
candidates: Promise<ResolvedRevealCandidate[]>;
|
|
491
|
+
};
|
|
354
492
|
}
|
|
355
493
|
|
|
356
494
|
/** Immutable context shared across event handlers within a single agent loop run. */
|
|
@@ -441,16 +579,211 @@ export function createEventHandlerState(): EventHandlerState {
|
|
|
441
579
|
compactionStartMessages: new Map(),
|
|
442
580
|
latencyCursor: 0,
|
|
443
581
|
deferredFinalizeEffects: [],
|
|
582
|
+
revealCandidateRefs: [],
|
|
583
|
+
pendingRevealRefsByToolUse: new Map(),
|
|
584
|
+
toolOutputGuardBuffers: new Map(),
|
|
585
|
+
revealCandidateCache: undefined,
|
|
586
|
+
pendingSentinelGuardBuffer: "",
|
|
587
|
+
liveRevealGuardEntries: [],
|
|
588
|
+
candidateRemintAuthorities: [],
|
|
589
|
+
forChatMintWatermark: currentForChatMintWatermark(),
|
|
590
|
+
stagedRevealIdentities: new Set(),
|
|
591
|
+
liveRevealGuardPriming: undefined,
|
|
444
592
|
};
|
|
445
593
|
}
|
|
446
594
|
|
|
595
|
+
/**
|
|
596
|
+
* Resolve this turn's reveal candidates for chat sentinel redaction.
|
|
597
|
+
*
|
|
598
|
+
* Returns `undefined` when the `chat-credential-reveal` flag is off — the
|
|
599
|
+
* persist seams then use the legacy `<redacted type="…" />` marker for
|
|
600
|
+
* SCANNER matches, byte-identical to today (the candidate-aware
|
|
601
|
+
* exact-match fallback still applies via
|
|
602
|
+
* {@link resolvedRevealCandidatesForState}, which is deliberately not
|
|
603
|
+
* flag-gated: keeping a proven revealed plaintext out of persisted rows
|
|
604
|
+
* is independent of which marker format the client renders). When the
|
|
605
|
+
* flag is on, returns the resolved candidates (possibly empty: sentinel
|
|
606
|
+
* mode with nothing revealable). Resolution is memoized on the ref count
|
|
607
|
+
* so plaintexts are fetched at most once per new batch of reveal
|
|
608
|
+
* invocations.
|
|
609
|
+
*/
|
|
610
|
+
async function chatRevealCandidates(
|
|
611
|
+
state: EventHandlerState,
|
|
612
|
+
): Promise<readonly ResolvedRevealCandidate[] | undefined> {
|
|
613
|
+
if (!isAssistantFeatureFlagEnabled("chat-credential-reveal", getConfig())) {
|
|
614
|
+
return undefined;
|
|
615
|
+
}
|
|
616
|
+
return resolvedRevealCandidatesForState(state);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/**
|
|
620
|
+
* Flag-independent candidate resolution for the legacy-marker fallbacks:
|
|
621
|
+
* a route-proven reveal plaintext must be kept out of persisted rows in
|
|
622
|
+
* BOTH modes, so the fallback surfaces (tool results always; assistant
|
|
623
|
+
* text when the sentinel flag is off) resolve candidates here directly.
|
|
624
|
+
* Refs only exist when the reveal route actually served the identity
|
|
625
|
+
* (see `filterRefsByRevealProof`), so with the flag off this performs no
|
|
626
|
+
* store reads until a genuine reveal has already happened. Shares the
|
|
627
|
+
* per-state memoization with {@link chatRevealCandidates}.
|
|
628
|
+
*/
|
|
629
|
+
async function resolvedRevealCandidatesForState(
|
|
630
|
+
state: EventHandlerState,
|
|
631
|
+
): Promise<readonly ResolvedRevealCandidate[]> {
|
|
632
|
+
const refCount = state.revealCandidateRefs.length;
|
|
633
|
+
if (refCount === 0) {
|
|
634
|
+
return [];
|
|
635
|
+
}
|
|
636
|
+
if (
|
|
637
|
+
state.revealCandidateCache === undefined ||
|
|
638
|
+
state.revealCandidateCache.refCount !== refCount
|
|
639
|
+
) {
|
|
640
|
+
state.revealCandidateCache = {
|
|
641
|
+
refCount,
|
|
642
|
+
candidates: resolveRevealCandidates([...state.revealCandidateRefs]),
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
return state.revealCandidateCache.candidates;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
/**
|
|
649
|
+
* Kick off (or refresh) the live stream guard's swap entries from the
|
|
650
|
+
* memoized candidate resolution. Called from `handleToolResult` once a
|
|
651
|
+
* staged reveal invocation is confirmed successful — never at propose
|
|
652
|
+
* time, since candidate resolution reads plaintext straight from the
|
|
653
|
+
* store and the tool may yet be denied or cancelled. The store read is
|
|
654
|
+
* asynchronous while the dispatcher moves on, so a slow
|
|
655
|
+
* `getSecureKeyAsync` could let the next text deltas reach the guard
|
|
656
|
+
* before entries exist. The pending promise is therefore recorded on
|
|
657
|
+
* state and awaited by the dispatcher at the delta boundary (see
|
|
658
|
+
* `dispatchAgentEvent`), turning the race into a barrier. If resolution
|
|
659
|
+
* fails, the guard stays on its previous entries and the persist seams
|
|
660
|
+
* still redact; the stream guard remains a wire-level layer, persistence
|
|
661
|
+
* the redaction boundary.
|
|
662
|
+
*/
|
|
663
|
+
function primeLiveRevealGuard(state: EventHandlerState): void {
|
|
664
|
+
const priming = chatRevealCandidates(state)
|
|
665
|
+
.then((candidates) => {
|
|
666
|
+
if (candidates !== undefined && candidates.length > 0) {
|
|
667
|
+
state.liveRevealGuardEntries = buildLiveRevealGuardEntries(candidates);
|
|
668
|
+
state.candidateRemintAuthorities =
|
|
669
|
+
remintAuthoritiesFromCandidates(candidates);
|
|
670
|
+
}
|
|
671
|
+
})
|
|
672
|
+
.catch((err: unknown) => {
|
|
673
|
+
log.debug(
|
|
674
|
+
{ err },
|
|
675
|
+
"live reveal guard priming failed; stream swap stays inactive",
|
|
676
|
+
);
|
|
677
|
+
})
|
|
678
|
+
.finally(() => {
|
|
679
|
+
// Only clear our own registration — a later reveal in the same turn
|
|
680
|
+
// may have replaced the pending promise with a fresh one.
|
|
681
|
+
if (state.liveRevealGuardPriming === priming) {
|
|
682
|
+
state.liveRevealGuardPriming = undefined;
|
|
683
|
+
}
|
|
684
|
+
});
|
|
685
|
+
state.liveRevealGuardPriming = priming;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
/**
|
|
689
|
+
* Barrier for the live reveal guard: resolves once any in-flight priming
|
|
690
|
+
* has settled. Awaited before text deltas are guarded and before the
|
|
691
|
+
* end-of-message guard flush, so echoed plaintext can never beat the
|
|
692
|
+
* guard entries onto the wire. No-op (no await, no microtask churn) when
|
|
693
|
+
* nothing is being primed.
|
|
694
|
+
*/
|
|
695
|
+
async function awaitLiveRevealGuardReady(
|
|
696
|
+
state: EventHandlerState,
|
|
697
|
+
): Promise<void> {
|
|
698
|
+
// Loop: priming that settles can be superseded by a newer registration
|
|
699
|
+
// (two reveals back-to-back) before this await resumes.
|
|
700
|
+
while (state.liveRevealGuardPriming !== undefined) {
|
|
701
|
+
await state.liveRevealGuardPriming;
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
/**
|
|
706
|
+
* `--for-chat` mints authorized for THIS run: recorded by the reveal route
|
|
707
|
+
* after this run's watermark AND matching an identity this run itself
|
|
708
|
+
* staged from its own `tool_use` commands. Both legs are load-bearing —
|
|
709
|
+
* the registry proves a reveal EXECUTED (a quoted/commented-out invocation
|
|
710
|
+
* never reaches the route), while the staging set scopes authority to the
|
|
711
|
+
* conversation that actually requested it (registry records carry no
|
|
712
|
+
* trustworthy conversation identity, so without this leg one
|
|
713
|
+
* conversation's approved reveal could authorize a concurrent
|
|
714
|
+
* conversation's forged sentinel). Read fresh at each guard site rather
|
|
715
|
+
* than cached: the route records a mint while the reveal tool is still
|
|
716
|
+
* executing, so by the time the model echoes the sentinel back the
|
|
717
|
+
* registry is already current — no priming race like the async candidate
|
|
718
|
+
* fetch has.
|
|
719
|
+
*/
|
|
720
|
+
function turnForChatMints(
|
|
721
|
+
state: EventHandlerState,
|
|
722
|
+
deps: EventHandlerDeps,
|
|
723
|
+
): ForChatMint[] {
|
|
724
|
+
if (state.stagedRevealIdentities.size === 0) {
|
|
725
|
+
return [];
|
|
726
|
+
}
|
|
727
|
+
const nonce = conversationRevealNonce(deps.ctx.conversationId);
|
|
728
|
+
return forChatMintsSince(state.forChatMintWatermark).filter(
|
|
729
|
+
(mint) =>
|
|
730
|
+
mint.nonce === nonce &&
|
|
731
|
+
state.stagedRevealIdentities.has(credentialKey(mint.service, mint.field)),
|
|
732
|
+
);
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
/**
|
|
736
|
+
* Combined re-mint authorities for the LIVE emit sites (synchronous):
|
|
737
|
+
* route-recorded `--for-chat` mints plus the candidate-derived authorities
|
|
738
|
+
* primed behind the dispatcher barrier alongside the swap entries.
|
|
739
|
+
*/
|
|
740
|
+
function liveRemintAuthorities(
|
|
741
|
+
state: EventHandlerState,
|
|
742
|
+
deps: EventHandlerDeps,
|
|
743
|
+
): SentinelRemintAuthority[] {
|
|
744
|
+
return [
|
|
745
|
+
...turnForChatMints(state, deps),
|
|
746
|
+
...state.candidateRemintAuthorities,
|
|
747
|
+
];
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
/**
|
|
751
|
+
* Combined re-mint authorities for the PERSIST sites, deriving the
|
|
752
|
+
* candidate half from the resolved candidate set the call site already
|
|
753
|
+
* fetched — persistence must not depend on whether live priming ran
|
|
754
|
+
* (a flush can outlive the stream), so it never reads the primed state.
|
|
755
|
+
*/
|
|
756
|
+
function persistRemintAuthorities(
|
|
757
|
+
state: EventHandlerState,
|
|
758
|
+
deps: EventHandlerDeps,
|
|
759
|
+
candidates: readonly ResolvedRevealCandidate[] | undefined,
|
|
760
|
+
): SentinelRemintAuthority[] {
|
|
761
|
+
return [
|
|
762
|
+
...turnForChatMints(state, deps),
|
|
763
|
+
...(candidates !== undefined && candidates.length > 0
|
|
764
|
+
? remintAuthoritiesFromCandidates(candidates)
|
|
765
|
+
: []),
|
|
766
|
+
];
|
|
767
|
+
}
|
|
768
|
+
|
|
447
769
|
// ── Partial-persistence helpers ──────────────────────────────────────
|
|
448
770
|
|
|
449
|
-
/**
|
|
771
|
+
/**
|
|
772
|
+
* Canonical persisted-content build: clean → append surfaces → redact.
|
|
773
|
+
*
|
|
774
|
+
* `revealCandidates` (defined only when the sentinel flag is on) selects
|
|
775
|
+
* sentinel-mode redaction; `legacyFallbackCandidates` feeds the
|
|
776
|
+
* flag-independent exact-match fallback in legacy mode, so a route-proven
|
|
777
|
+
* reveal plaintext the scanner cannot classify never persists raw
|
|
778
|
+
* regardless of which marker format is active.
|
|
779
|
+
*/
|
|
450
780
|
export function buildPersistedAssistantContent(
|
|
451
781
|
rawBlocks: readonly ContentBlock[],
|
|
452
782
|
surfaces: readonly AssistantSurface[],
|
|
453
783
|
activityByToolUseId?: ReadonlyMap<string, ToolActivityMetadata>,
|
|
784
|
+
revealCandidates?: readonly ResolvedRevealCandidate[],
|
|
785
|
+
legacyFallbackCandidates: readonly ResolvedRevealCandidate[] = [],
|
|
786
|
+
forChatMints: readonly SentinelRemintAuthority[] = [],
|
|
454
787
|
): ContentBlock[] {
|
|
455
788
|
const { cleanedContent } = cleanAssistantContent(rawBlocks);
|
|
456
789
|
const cleaned = cleanedContent as ContentBlock[];
|
|
@@ -471,7 +804,21 @@ export function buildPersistedAssistantContent(
|
|
|
471
804
|
return withSurfaces.map((block) => {
|
|
472
805
|
if (block.type === "text") {
|
|
473
806
|
const tb = block as Extract<ContentBlock, { type: "text" }>;
|
|
474
|
-
|
|
807
|
+
// Sentinel mode (chat-credential-reveal flag on) persists redactions
|
|
808
|
+
// the client can render as chips; legacy mode keeps the marker
|
|
809
|
+
// byte-identical to today. Detection is the same scanner either way.
|
|
810
|
+
// Both modes neutralize forged sentinel-shaped strings first so
|
|
811
|
+
// arbitrary content can never manufacture a reveal chip — only
|
|
812
|
+
// redactor-inserted sentinels survive persistence. The
|
|
813
|
+
// `_redactionVersion` rider (internal, same convention as `_startedAt`)
|
|
814
|
+
// marks the block as neutralization-aware; `renderHistoryContent`
|
|
815
|
+
// neutralizes unmarked (pre-feature) blocks at read so forged sentinels
|
|
816
|
+
// in old history rows can never chip-ify either.
|
|
817
|
+
const text =
|
|
818
|
+
revealCandidates !== undefined
|
|
819
|
+
? redactSecretsForChat(tb.text, revealCandidates, forChatMints)
|
|
820
|
+
: redactCandidateValuesLegacy(tb.text, legacyFallbackCandidates);
|
|
821
|
+
return { ...tb, text, _redactionVersion: SENTINEL_REDACTION_VERSION };
|
|
475
822
|
}
|
|
476
823
|
// Native server tools (Anthropic web_search) resolve mid-stream — their
|
|
477
824
|
// `server_tool_complete` fires before `message_complete` — so the captured
|
|
@@ -584,6 +931,13 @@ function resetPartialPersistAccumulator(state: EventHandlerState): void {
|
|
|
584
931
|
state.currentThinkingTimestamps = [];
|
|
585
932
|
state.lastPersistedContentSeq = undefined;
|
|
586
933
|
state.pendingPartialFlushPromise = undefined;
|
|
934
|
+
// If a previous LLM call (e.g. a retried/replaced stream) held back
|
|
935
|
+
// sentinel-guarded text via `drainSentinelGuardedText`, the stale
|
|
936
|
+
// bytes would be prepended to the retry's first text_delta, potentially
|
|
937
|
+
// emitting a raw credential prefix if it no longer matches a candidate.
|
|
938
|
+
// Clear alongside the other per-row accumulators so the new LLM call
|
|
939
|
+
// starts from an empty buffer.
|
|
940
|
+
state.pendingSentinelGuardBuffer = "";
|
|
587
941
|
}
|
|
588
942
|
|
|
589
943
|
/**
|
|
@@ -672,10 +1026,14 @@ async function flushAccumulatedContent(
|
|
|
672
1026
|
return;
|
|
673
1027
|
}
|
|
674
1028
|
|
|
1029
|
+
const revealCandidates = await chatRevealCandidates(state);
|
|
675
1030
|
const built = buildPersistedAssistantContent(
|
|
676
1031
|
state.currentMessageContent,
|
|
677
1032
|
[],
|
|
678
1033
|
state.toolActivityMetadata,
|
|
1034
|
+
revealCandidates,
|
|
1035
|
+
await resolvedRevealCandidatesForState(state),
|
|
1036
|
+
persistRemintAuthorities(state, deps, revealCandidates),
|
|
679
1037
|
);
|
|
680
1038
|
// Pair the seq with the exact content snapshot taken above: deltas that
|
|
681
1039
|
// arrive while the write is in flight bump `lastPersistedContentSeq`
|
|
@@ -969,24 +1327,43 @@ function handleTextDelta(
|
|
|
969
1327
|
statusText: "Thinking",
|
|
970
1328
|
});
|
|
971
1329
|
}
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
1330
|
+
// Live stream guard: neutralize forged sentinels (a genuine sentinel is
|
|
1331
|
+
// created at persist time, never in raw model output) and swap a reveal
|
|
1332
|
+
// candidate's echoed plaintext for its enriched sentinel so the secret
|
|
1333
|
+
// never flashes in the live transcript or crosses the wire. A trigger or
|
|
1334
|
+
// candidate prefix split across chunks is held back in
|
|
1335
|
+
// `pendingSentinelGuardBuffer` and flushed at message end.
|
|
1336
|
+
const guarded = drainSentinelGuardedText(
|
|
1337
|
+
state.pendingSentinelGuardBuffer + drained.emitText,
|
|
1338
|
+
state.liveRevealGuardEntries,
|
|
1339
|
+
liveRemintAuthorities(state, deps),
|
|
1340
|
+
);
|
|
1341
|
+
state.pendingSentinelGuardBuffer = guarded.bufferedRemainder;
|
|
1342
|
+
if (guarded.emitText.length > 0) {
|
|
1343
|
+
deps.onEvent({
|
|
1344
|
+
type: "assistant_text_delta",
|
|
1345
|
+
text: guarded.emitText,
|
|
1346
|
+
conversationId: deps.ctx.conversationId,
|
|
1347
|
+
messageId: state.lastAssistantMessageId,
|
|
1348
|
+
});
|
|
1349
|
+
// Mirror the RAW consumed bytes (not the emitted swap) into
|
|
1350
|
+
// currentMessageContent: the partial flush re-redacts them through
|
|
1351
|
+
// `redactSecretsForChat`, which derives the same enriched sentinel
|
|
1352
|
+
// from the plaintext — whereas a mirrored, already-swapped sentinel
|
|
1353
|
+
// would be indistinguishable from a forgery there and get
|
|
1354
|
+
// neutralized. Buffered bytes (partial triggers / candidate
|
|
1355
|
+
// prefixes) stay excluded until a later chunk emits them.
|
|
1356
|
+
appendTextToCurrentMessage(state, guarded.consumedRaw);
|
|
1357
|
+
// The hub stamps `seq` synchronously on the delta emitted above, so
|
|
1358
|
+
// `getCurrentSeq()` here is that delta's seq -- the position the
|
|
1359
|
+
// mirrored content now reflects. A partial flush snapshots this to
|
|
1360
|
+
// record how far the durable rows track the live stream.
|
|
1361
|
+
state.lastPersistedContentSeq = getCurrentSeq();
|
|
1362
|
+
schedulePartialFlush(state, deps);
|
|
1363
|
+
}
|
|
978
1364
|
if (deps.shouldGenerateTitle) {
|
|
979
1365
|
state.firstAssistantText += drained.emitText;
|
|
980
1366
|
}
|
|
981
|
-
// Mirror the drained delta into state.currentMessageContent so partial
|
|
982
|
-
// flushes mid-turn see the same content the user is watching live.
|
|
983
|
-
appendTextToCurrentMessage(state, drained.emitText);
|
|
984
|
-
// The hub stamps `seq` synchronously on the delta emitted above, so
|
|
985
|
-
// `getCurrentSeq()` here is that delta's seq -- the position the
|
|
986
|
-
// mirrored content now reflects. A partial flush snapshots this to
|
|
987
|
-
// record how far the durable rows track the live stream.
|
|
988
|
-
state.lastPersistedContentSeq = getCurrentSeq();
|
|
989
|
-
schedulePartialFlush(state, deps);
|
|
990
1367
|
}
|
|
991
1368
|
}
|
|
992
1369
|
|
|
@@ -1042,6 +1419,46 @@ export function handleToolUse(
|
|
|
1042
1419
|
if (event.name === "app_create" || event.name === "app_refresh") {
|
|
1043
1420
|
state.appBuildToolUsedThisRun = true;
|
|
1044
1421
|
}
|
|
1422
|
+
// Record `credentials reveal` invocations so the persist seams can enrich
|
|
1423
|
+
// redaction sentinels with a proven vault identity (chat-credential-reveal).
|
|
1424
|
+
// Tool-name agnostic on purpose: any shell-style tool (bash, host_bash)
|
|
1425
|
+
// carries the command in `input.command`. Pure string parse — no store
|
|
1426
|
+
// access happens here: `tool_use` precedes execution, and the refs are
|
|
1427
|
+
// only STAGED until `handleToolResult` sees the reveal actually succeed
|
|
1428
|
+
// (see `pendingRevealRefsByToolUse` — priming at propose time would read
|
|
1429
|
+
// plaintext for a command that approval/cancellation may still block).
|
|
1430
|
+
const command = (event.input as { command?: unknown } | undefined)?.command;
|
|
1431
|
+
if (typeof command === "string" && command.length > 0) {
|
|
1432
|
+
// Id-form refs resolve to service/field NOW (metadata-only lookup): the
|
|
1433
|
+
// same compound command may remove the credential after revealing it,
|
|
1434
|
+
// and by result time the id would no longer resolve — dropping the
|
|
1435
|
+
// proof for a value the tool already printed.
|
|
1436
|
+
const refs = resolveRefIdentities(collectRevealRefsFromCommand(command));
|
|
1437
|
+
if (refs.length > 0) {
|
|
1438
|
+
state.pendingRevealRefsByToolUse.set(event.id, {
|
|
1439
|
+
refs,
|
|
1440
|
+
// Captured before execution: only reveal-route successes recorded
|
|
1441
|
+
// AFTER this point can prove these refs at result time.
|
|
1442
|
+
watermark: currentRevealSuccessWatermark(),
|
|
1443
|
+
// Arms registry recording for this staging's lifetime — the route
|
|
1444
|
+
// retains plaintext only while some tool's proof is pending.
|
|
1445
|
+
proofWindowToken: openRevealProofWindow(),
|
|
1446
|
+
});
|
|
1447
|
+
// The conversation-scoping leg of `--for-chat` re-mint authority:
|
|
1448
|
+
// record which identities THIS run's own commands named. Retained for
|
|
1449
|
+
// the whole run (unlike the staging entry, which handleToolResult
|
|
1450
|
+
// consumes) — the model echoes the sentinel back only after the tool
|
|
1451
|
+
// completes. Parse-only, so by itself this authorizes nothing; a
|
|
1452
|
+
// registry mint (an executed reveal) must also exist.
|
|
1453
|
+
for (const ref of refs) {
|
|
1454
|
+
if (ref.service !== undefined && ref.field !== undefined) {
|
|
1455
|
+
state.stagedRevealIdentities.add(
|
|
1456
|
+
credentialKey(ref.service, ref.field),
|
|
1457
|
+
);
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1045
1462
|
const startedAt = Date.now();
|
|
1046
1463
|
state.toolCallTimestamps.set(event.id, { startedAt });
|
|
1047
1464
|
state.currentToolUseId = event.id;
|
|
@@ -1182,15 +1599,59 @@ function handleToolOutputChunk(
|
|
|
1182
1599
|
subToolIsError: structured.subToolIsError,
|
|
1183
1600
|
subToolId: structured.subToolId,
|
|
1184
1601
|
});
|
|
1185
|
-
|
|
1186
|
-
deps.onEvent({
|
|
1187
|
-
type: "tool_output_chunk",
|
|
1188
|
-
chunk: event.chunk,
|
|
1189
|
-
conversationId: deps.ctx.conversationId,
|
|
1190
|
-
toolUseId: event.toolUseId,
|
|
1191
|
-
messageId: state.lastAssistantMessageId,
|
|
1192
|
-
});
|
|
1602
|
+
return;
|
|
1193
1603
|
}
|
|
1604
|
+
|
|
1605
|
+
// Redact revealed plaintext from the LIVE stdout stream. The final
|
|
1606
|
+
// tool_result is redacted at its seam, but these chunks reach the client
|
|
1607
|
+
// first and the web drawer renders them until the result replaces them —
|
|
1608
|
+
// without this guard a `credentials reveal` value flashes raw for the
|
|
1609
|
+
// whole tool run. By the time printed bytes arrive here the route has
|
|
1610
|
+
// already recorded any success (the record precedes the CLI receiving
|
|
1611
|
+
// the plaintext), so proven candidates are available SYNCHRONOUSLY from
|
|
1612
|
+
// the registry — no vault read, and the reveal-free path stays untouched.
|
|
1613
|
+
// Covers both this tool's own staged reveals and values already promoted
|
|
1614
|
+
// by an earlier tool in the turn (a later `echo <value>` streams too). A
|
|
1615
|
+
// trailing partial occurrence is held back for the next chunk; the
|
|
1616
|
+
// tool_result seam flushes the remainder. Structured control frames
|
|
1617
|
+
// above are forwarded untouched — they are parsed subtool events, not
|
|
1618
|
+
// reveal stdout, and rewriting their raw JSON could corrupt them.
|
|
1619
|
+
let chunk = event.chunk;
|
|
1620
|
+
const staged = state.pendingRevealRefsByToolUse.get(event.toolUseId);
|
|
1621
|
+
const guardRefs =
|
|
1622
|
+
staged === undefined
|
|
1623
|
+
? state.revealCandidateRefs
|
|
1624
|
+
: [
|
|
1625
|
+
...state.revealCandidateRefs,
|
|
1626
|
+
...filterRefsByRevealProof(
|
|
1627
|
+
staged.refs,
|
|
1628
|
+
staged.watermark,
|
|
1629
|
+
conversationRevealNonce(deps.ctx.conversationId),
|
|
1630
|
+
),
|
|
1631
|
+
];
|
|
1632
|
+
if (guardRefs.length > 0) {
|
|
1633
|
+
const candidates = resolveProvenRevealCandidates(guardRefs);
|
|
1634
|
+
if (candidates.length > 0) {
|
|
1635
|
+
const held = state.toolOutputGuardBuffers.get(event.toolUseId) ?? "";
|
|
1636
|
+
const drained = drainCandidateGuardedChunk(held + chunk, candidates);
|
|
1637
|
+
state.toolOutputGuardBuffers.set(
|
|
1638
|
+
event.toolUseId,
|
|
1639
|
+
drained.bufferedRemainder,
|
|
1640
|
+
);
|
|
1641
|
+
if (drained.emitText.length === 0) {
|
|
1642
|
+
return;
|
|
1643
|
+
}
|
|
1644
|
+
chunk = drained.emitText;
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
deps.onEvent({
|
|
1649
|
+
type: "tool_output_chunk",
|
|
1650
|
+
chunk,
|
|
1651
|
+
conversationId: deps.ctx.conversationId,
|
|
1652
|
+
toolUseId: event.toolUseId,
|
|
1653
|
+
messageId: state.lastAssistantMessageId,
|
|
1654
|
+
});
|
|
1194
1655
|
}
|
|
1195
1656
|
|
|
1196
1657
|
export function handleInputJsonDelta(
|
|
@@ -1222,17 +1683,30 @@ export function handleInputJsonDelta(
|
|
|
1222
1683
|
*/
|
|
1223
1684
|
function buildToolResultBlocks(
|
|
1224
1685
|
pending: ReadonlyMap<string, PendingToolResult>,
|
|
1686
|
+
revealCandidates: readonly ResolvedRevealCandidate[] = [],
|
|
1225
1687
|
) {
|
|
1688
|
+
// Tool results keep the legacy `<redacted type/>` marker (NOT sentinels):
|
|
1689
|
+
// history maps tool_result content to `toolCall.result`, which the tool
|
|
1690
|
+
// detail panel renders via CodeBlock — a path with no markdown/chip
|
|
1691
|
+
// support, where a sentinel would show as an inert glyph string. Convert
|
|
1692
|
+
// here only once that surface can render chips. Forged sentinel-shaped
|
|
1693
|
+
// strings in tool output are still neutralized so they can never reach a
|
|
1694
|
+
// chip-enabled surface via quoting. Proven reveal-candidate plaintexts
|
|
1695
|
+
// the scanner cannot classify (opaque manual tokens in the reveal's own
|
|
1696
|
+
// stdout) get a candidate-aware legacy-marker fallback — the tool detail
|
|
1697
|
+
// panel and history must not retain a value every other surface redacts.
|
|
1698
|
+
const redact = (text: string): string =>
|
|
1699
|
+
redactCandidateValuesLegacy(text, revealCandidates);
|
|
1226
1700
|
return Array.from(pending.entries()).map(([toolUseId, result]) => ({
|
|
1227
1701
|
type: "tool_result",
|
|
1228
1702
|
tool_use_id: toolUseId,
|
|
1229
|
-
content:
|
|
1703
|
+
content: redact(result.content),
|
|
1230
1704
|
is_error: result.isError,
|
|
1231
1705
|
...(result.contentBlocks
|
|
1232
1706
|
? {
|
|
1233
1707
|
contentBlocks: result.contentBlocks.map((block) =>
|
|
1234
1708
|
block.type === "text"
|
|
1235
|
-
? { ...block, text:
|
|
1709
|
+
? { ...block, text: redact(block.text) }
|
|
1236
1710
|
: block,
|
|
1237
1711
|
),
|
|
1238
1712
|
}
|
|
@@ -1316,6 +1790,7 @@ async function persistPendingToolResultRow(
|
|
|
1316
1790
|
// the in-flight delta file; the finalize seam folds the row inline.
|
|
1317
1791
|
const batchBlocks = buildToolResultBlocks(
|
|
1318
1792
|
state.pendingToolResults,
|
|
1793
|
+
await resolvedRevealCandidatesForState(state),
|
|
1319
1794
|
) as ContentBlock[];
|
|
1320
1795
|
const writer = state.inflightWriters.get(rowId);
|
|
1321
1796
|
const persisted = writer
|
|
@@ -1365,7 +1840,10 @@ export async function finalizePendingToolResultRow(
|
|
|
1365
1840
|
// for workspace references so the blob stays in the attachment store, out of
|
|
1366
1841
|
// this row and the lexical index. Runs once, here at finalize (on-arrival
|
|
1367
1842
|
// writes keep base64 for durability); the send boundary re-inflates the refs.
|
|
1368
|
-
const blocks = buildToolResultBlocks(
|
|
1843
|
+
const blocks = buildToolResultBlocks(
|
|
1844
|
+
state.pendingToolResults,
|
|
1845
|
+
await resolvedRevealCandidatesForState(state),
|
|
1846
|
+
);
|
|
1369
1847
|
const contentJson = JSON.stringify(
|
|
1370
1848
|
conv != null
|
|
1371
1849
|
? referenceMediaBlocksForPersist(
|
|
@@ -1454,6 +1932,76 @@ export async function handleToolResult(
|
|
|
1454
1932
|
deps: EventHandlerDeps,
|
|
1455
1933
|
event: Extract<AgentEvent, { type: "tool_result" }>,
|
|
1456
1934
|
): Promise<void> {
|
|
1935
|
+
// Promote staged reveal refs now that the tool has finished: a ref is
|
|
1936
|
+
// promoted only if the reveal ROUTE recorded a success for its identity
|
|
1937
|
+
// after the staging watermark — the enclosing tool's exit status proves
|
|
1938
|
+
// nothing (`reveal … || true` succeeds when the route failed; an echo of
|
|
1939
|
+
// the command text never calls the route; conversely, a compound command
|
|
1940
|
+
// can print the secret and then exit non-zero, in which case the model
|
|
1941
|
+
// HAS the plaintext and the guard entry is protective). Only then may
|
|
1942
|
+
// candidate resolution read the plaintext from the store. Synchronous —
|
|
1943
|
+
// the dispatcher awaits `tool_result` before any later `text_delta`, so
|
|
1944
|
+
// the priming promise is registered before the barrier can be consulted.
|
|
1945
|
+
const stagedReveal = state.pendingRevealRefsByToolUse.get(event.toolUseId);
|
|
1946
|
+
if (stagedReveal !== undefined) {
|
|
1947
|
+
state.pendingRevealRefsByToolUse.delete(event.toolUseId);
|
|
1948
|
+
const provenRefs = filterRefsByRevealProof(
|
|
1949
|
+
stagedReveal.refs,
|
|
1950
|
+
stagedReveal.watermark,
|
|
1951
|
+
conversationRevealNonce(deps.ctx.conversationId),
|
|
1952
|
+
);
|
|
1953
|
+
// The proof is consumed (proven values now ride the refs), so this
|
|
1954
|
+
// staging no longer needs the registry to record.
|
|
1955
|
+
closeRevealProofWindow(stagedReveal.proofWindowToken);
|
|
1956
|
+
if (provenRefs.length > 0) {
|
|
1957
|
+
state.revealCandidateRefs.push(...provenRefs);
|
|
1958
|
+
primeLiveRevealGuard(state);
|
|
1959
|
+
}
|
|
1960
|
+
}
|
|
1961
|
+
|
|
1962
|
+
// Redact THIS tool's own stdout before it leaves the daemon. The reveal
|
|
1963
|
+
// command that just proved a candidate printed the plaintext into its own
|
|
1964
|
+
// `event.content`; priming the assistant-text guard only protects a later
|
|
1965
|
+
// echo, not this result. The persist path already redacts via
|
|
1966
|
+
// `buildToolResultBlocks`, but the LIVE `tool_result` SSE below forwards
|
|
1967
|
+
// `event.content` verbatim and the web reducer stores `event.result`
|
|
1968
|
+
// directly — so an opaque/manual value the scanner cannot classify would
|
|
1969
|
+
// flash in the tool card until a history refetch. Apply the same
|
|
1970
|
+
// candidate-aware legacy redaction the persisted tool-result row uses, to
|
|
1971
|
+
// both the buffered content and the emitted result, so wire and storage
|
|
1972
|
+
// agree from the first frame.
|
|
1973
|
+
//
|
|
1974
|
+
// Guard the resolution behind the ref count: candidate resolution is async
|
|
1975
|
+
// (it reads the vault), and awaiting it here would push every side effect
|
|
1976
|
+
// below — the cancellation emit, the pending-result buffering, the
|
|
1977
|
+
// activity/risk metadata capture, `annotatePersistedAssistantMessage`, and
|
|
1978
|
+
// the live `tool_result` emit — onto a later microtask. Callers that drive
|
|
1979
|
+
// this handler synchronously (the dispatcher, and the metadata/preview
|
|
1980
|
+
// tests) rely on those effects landing before the returned promise's first
|
|
1981
|
+
// suspension, so a reveal-free tool result must stay fully synchronous and
|
|
1982
|
+
// pass its content through untouched — exactly as before this guard shipped.
|
|
1983
|
+
// The refs are non-empty only after the reveal route actually served an
|
|
1984
|
+
// identity this turn, which is precisely when redaction must fire.
|
|
1985
|
+
let redactedContent = event.content;
|
|
1986
|
+
// DISCARD (never emit) stdout the live chunk guard held back for this
|
|
1987
|
+
// tool. The buffer was held precisely because it contains or ends in a
|
|
1988
|
+
// PARTIAL occurrence of a candidate's plaintext, and complete-value
|
|
1989
|
+
// redaction cannot mask a partial — flushing it would put up to
|
|
1990
|
+
// value-length-minus-one raw credential bytes on the wire. Nothing is
|
|
1991
|
+
// lost: the redacted `event.content` emitted below carries the full
|
|
1992
|
+
// output and supersedes the streamed view immediately.
|
|
1993
|
+
state.toolOutputGuardBuffers.delete(event.toolUseId);
|
|
1994
|
+
if (state.revealCandidateRefs.length > 0) {
|
|
1995
|
+
const revealCandidatesForResult =
|
|
1996
|
+
await resolvedRevealCandidatesForState(state);
|
|
1997
|
+
if (revealCandidatesForResult.length > 0) {
|
|
1998
|
+
redactedContent = redactCandidateValuesLegacy(
|
|
1999
|
+
event.content,
|
|
2000
|
+
revealCandidatesForResult,
|
|
2001
|
+
);
|
|
2002
|
+
}
|
|
2003
|
+
}
|
|
2004
|
+
|
|
1457
2005
|
// A synthesized cancellation (the tool never executed) is captured for
|
|
1458
2006
|
// persistence and forwarded to the client like any result, but skips every
|
|
1459
2007
|
// side effect that assumes the tool ran. A real result already captured or
|
|
@@ -1465,6 +2013,10 @@ export async function handleToolResult(
|
|
|
1465
2013
|
) {
|
|
1466
2014
|
return;
|
|
1467
2015
|
}
|
|
2016
|
+
// Buffer the RAW content: every persist path redacts exactly once via
|
|
2017
|
+
// `buildToolResultBlocks`, and buffering already-redacted bytes would
|
|
2018
|
+
// redact twice — a candidate value overlapping the marker's own text
|
|
2019
|
+
// would corrupt the persisted marker on the second pass.
|
|
1468
2020
|
state.pendingToolResults.set(event.toolUseId, {
|
|
1469
2021
|
content: event.content,
|
|
1470
2022
|
isError: event.isError,
|
|
@@ -1473,7 +2025,7 @@ export async function handleToolResult(
|
|
|
1473
2025
|
deps.onEvent({
|
|
1474
2026
|
type: "tool_result",
|
|
1475
2027
|
toolName: "",
|
|
1476
|
-
result:
|
|
2028
|
+
result: redactedContent,
|
|
1477
2029
|
isError: event.isError,
|
|
1478
2030
|
conversationId: deps.ctx.conversationId,
|
|
1479
2031
|
messageId: state.lastAssistantMessageId,
|
|
@@ -1507,6 +2059,12 @@ export async function handleToolResult(
|
|
|
1507
2059
|
// Perform state mutations before deps.onEvent() so that if onEvent throws
|
|
1508
2060
|
// (e.g. SSE disconnection) and the error is suppressed by dispatchAgentEvent,
|
|
1509
2061
|
// critical state like pendingToolResults and currentToolUseId is still updated.
|
|
2062
|
+
// Buffer the RAW content: every persist path redacts exactly once via
|
|
2063
|
+
// `buildToolResultBlocks` (with the fullest candidate set at flush time),
|
|
2064
|
+
// so wire and storage still agree. Buffering the already-redacted bytes
|
|
2065
|
+
// would redact twice — a candidate value that overlaps the marker's own
|
|
2066
|
+
// text (e.g. a manual value `redacted`) would match inside the
|
|
2067
|
+
// first-pass marker and corrupt the persisted row.
|
|
1510
2068
|
state.pendingToolResults.set(event.toolUseId, {
|
|
1511
2069
|
content: event.content,
|
|
1512
2070
|
isError: event.isError,
|
|
@@ -1614,10 +2172,12 @@ export async function handleToolResult(
|
|
|
1614
2172
|
}
|
|
1615
2173
|
|
|
1616
2174
|
// Send to client last so state is consistent even if onEvent throws.
|
|
2175
|
+
// `result` carries the reveal-redacted stdout (see above) so the live tool
|
|
2176
|
+
// card never shows a revealed plaintext the persisted row hides.
|
|
1617
2177
|
deps.onEvent({
|
|
1618
2178
|
type: "tool_result",
|
|
1619
2179
|
toolName: "",
|
|
1620
|
-
result:
|
|
2180
|
+
result: redactedContent,
|
|
1621
2181
|
isError: event.isError,
|
|
1622
2182
|
diff: event.diff,
|
|
1623
2183
|
status: event.status,
|
|
@@ -2053,18 +2613,42 @@ export async function handleMessageComplete(
|
|
|
2053
2613
|
state.pendingPartialFlushPromise = undefined;
|
|
2054
2614
|
}
|
|
2055
2615
|
|
|
2056
|
-
// Flush any remaining directive display buffer
|
|
2057
|
-
|
|
2616
|
+
// Flush any remaining directive display buffer, prepending live text the
|
|
2617
|
+
// sentinel guard held back (a split trigger or candidate-prefix tail).
|
|
2618
|
+
// The concatenation is candidate-swapped and gap-neutralized as a whole:
|
|
2619
|
+
// at end-of-message nothing can complete a partial trigger (a completed
|
|
2620
|
+
// one here would be a forged sentinel), and a reveal-candidate plaintext
|
|
2621
|
+
// that completes across the two buffers still swaps to its sentinel.
|
|
2622
|
+
const trailingLiveText =
|
|
2623
|
+
state.pendingSentinelGuardBuffer + state.pendingDirectiveDisplayBuffer;
|
|
2624
|
+
if (trailingLiveText.length > 0) {
|
|
2625
|
+
// Same barrier as the text-delta path: the flush below swaps against
|
|
2626
|
+
// `liveRevealGuardEntries`, so an in-flight priming must settle first.
|
|
2627
|
+
await awaitLiveRevealGuardReady(state);
|
|
2058
2628
|
deps.onEvent({
|
|
2059
2629
|
type: "assistant_text_delta",
|
|
2060
|
-
text:
|
|
2630
|
+
text: neutralizeAndSwapLiveRevealValues(
|
|
2631
|
+
trailingLiveText,
|
|
2632
|
+
state.liveRevealGuardEntries,
|
|
2633
|
+
liveRemintAuthorities(state, deps),
|
|
2634
|
+
),
|
|
2061
2635
|
conversationId: deps.ctx.conversationId,
|
|
2062
2636
|
messageId: state.lastAssistantMessageId,
|
|
2063
2637
|
});
|
|
2638
|
+
// The hub stamps `seq` synchronously on the delta emitted above, so
|
|
2639
|
+
// `getCurrentSeq()` is that delta's position — advance the persisted-seq
|
|
2640
|
+
// mirror exactly like the normal text-delta path. The finalize below
|
|
2641
|
+
// records this value; without the advance it would record the PREVIOUS
|
|
2642
|
+
// emitted chunk's seq, so a `/messages` snapshot could contain this tail
|
|
2643
|
+
// while advertising a seq before the delta that carried it — and a
|
|
2644
|
+
// reconnecting client applying `seq > snapshot.seq` would append the
|
|
2645
|
+
// tail a second time.
|
|
2646
|
+
state.lastPersistedContentSeq = getCurrentSeq();
|
|
2064
2647
|
if (deps.shouldGenerateTitle) {
|
|
2065
2648
|
state.firstAssistantText += state.pendingDirectiveDisplayBuffer;
|
|
2066
2649
|
}
|
|
2067
2650
|
state.pendingDirectiveDisplayBuffer = "";
|
|
2651
|
+
state.pendingSentinelGuardBuffer = "";
|
|
2068
2652
|
}
|
|
2069
2653
|
|
|
2070
2654
|
// Finalize the grouped tool-result row. Each result was persisted into this
|
|
@@ -2111,11 +2695,15 @@ export async function handleMessageComplete(
|
|
|
2111
2695
|
// redacted) via the shared helper. The partial-persist flush uses
|
|
2112
2696
|
// the same helper with `surfaces=[]` so a mid-turn snapshot lands in
|
|
2113
2697
|
// the same shape as the finalize.
|
|
2698
|
+
const finalRevealCandidates = await chatRevealCandidates(state);
|
|
2114
2699
|
const contentForPersistence = stampThinkingTiming(
|
|
2115
2700
|
buildPersistedAssistantContent(
|
|
2116
2701
|
event.message.content as ContentBlock[],
|
|
2117
2702
|
deps.ctx.currentTurnSurfaces,
|
|
2118
2703
|
state.toolActivityMetadata,
|
|
2704
|
+
finalRevealCandidates,
|
|
2705
|
+
await resolvedRevealCandidatesForState(state),
|
|
2706
|
+
persistRemintAuthorities(state, deps, finalRevealCandidates),
|
|
2119
2707
|
),
|
|
2120
2708
|
state.currentThinkingTimestamps,
|
|
2121
2709
|
);
|
|
@@ -2431,6 +3019,11 @@ export async function dispatchAgentEvent(
|
|
|
2431
3019
|
await handleLlmCallStarted(state, deps);
|
|
2432
3020
|
break;
|
|
2433
3021
|
case "text_delta":
|
|
3022
|
+
// Reveal-guard barrier: if a `credentials reveal` tool_use just
|
|
3023
|
+
// started priming the live guard, resolve it before this delta is
|
|
3024
|
+
// guarded — otherwise a fast reveal echo could cross SSE with an
|
|
3025
|
+
// empty entry list. Steady state awaits nothing.
|
|
3026
|
+
await awaitLiveRevealGuardReady(state);
|
|
2434
3027
|
handleTextDelta(state, deps, event);
|
|
2435
3028
|
break;
|
|
2436
3029
|
case "thinking_delta":
|