@vellumai/assistant 0.8.10 → 0.8.11-staging.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bun.lock +62 -1
- package/docs/workspace-tools.md +196 -0
- package/examples/plugins/echo/README.md +3 -3
- package/knip.json +1 -0
- package/openapi.yaml +460 -128
- package/package.json +2 -1
- package/scripts/build-plugin-api.ts +299 -0
- package/src/__tests__/agent-loop-callsite-precedence.test.ts +7 -0
- package/src/__tests__/agent-loop-compaction-events.test.ts +197 -0
- package/src/__tests__/agent-loop-exit-reason.test.ts +93 -96
- package/src/__tests__/agent-loop-mutable-latest-user-message.test.ts +2 -0
- package/src/__tests__/agent-loop-output-hooks.test.ts +274 -1
- package/src/__tests__/agent-loop-override-profile.test.ts +3 -0
- package/src/__tests__/agent-loop-provider-error-recording.test.ts +4 -0
- package/src/__tests__/agent-loop-thinking.test.ts +4 -0
- package/src/__tests__/agent-loop.test.ts +578 -5
- package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +0 -1
- package/src/__tests__/approval-cascade.test.ts +1 -0
- package/src/__tests__/background-workers-disk-pressure.test.ts +0 -2
- package/src/__tests__/btw-routes.test.ts +0 -1
- package/src/__tests__/build-persisted-content.test.ts +75 -1
- package/src/__tests__/catalog-install-normalize.test.ts +141 -0
- package/src/__tests__/ces-startup-timeout.test.ts +60 -0
- package/src/__tests__/compaction-events.test.ts +1 -0
- package/src/__tests__/config-managed-gemini-defaults.test.ts +2 -46
- package/src/__tests__/context-overflow-reducer.test.ts +264 -124
- package/src/__tests__/context-window-manager-overflow-rung.test.ts +351 -0
- package/src/__tests__/conversation-abort-tool-results.test.ts +1 -1
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +13 -5
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +284 -455
- package/src/__tests__/conversation-agent-loop.test.ts +131 -551
- package/src/__tests__/conversation-app-control-instantiation.test.ts +13 -0
- package/src/__tests__/conversation-confirmation-signals.test.ts +1 -0
- package/src/__tests__/conversation-fork-crud.test.ts +259 -0
- package/src/__tests__/conversation-history-web-search.test.ts +1 -1
- package/src/__tests__/conversation-lifecycle.test.ts +257 -1
- package/src/__tests__/conversation-process-callsite.test.ts +1 -0
- package/src/__tests__/conversation-provider-retry-repair.test.ts +38 -377
- package/src/__tests__/conversation-queue.test.ts +1 -39
- package/src/__tests__/conversation-runtime-assembly.test.ts +119 -8
- package/src/__tests__/conversation-skill-tools.test.ts +491 -5
- package/src/__tests__/conversation-slash-queue.test.ts +1 -1
- package/src/__tests__/conversation-slash-unknown.test.ts +1 -0
- package/src/__tests__/conversation-speed-override.test.ts +1 -0
- package/src/__tests__/conversation-store.test.ts +74 -0
- package/src/__tests__/conversation-surfaces-app-control.test.ts +4 -1
- package/src/__tests__/conversation-tool-setup-attribution.test.ts +323 -0
- package/src/__tests__/conversation-tool-setup-tools-disabled.test.ts +34 -0
- package/src/__tests__/conversation-workspace-cache-state.test.ts +1 -0
- package/src/__tests__/conversation-workspace-injection.test.ts +1 -1
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +1 -0
- package/src/__tests__/corrected-target.test.ts +93 -0
- package/src/__tests__/credential-execution-feature-gates.test.ts +3 -5
- package/src/__tests__/credential-execution-tools.test.ts +23 -11
- package/src/__tests__/credential-security-invariants.test.ts +6 -1
- package/src/__tests__/db-schedule-syntax-migration.test.ts +80 -0
- package/src/__tests__/device-id.test.ts +70 -1
- package/src/__tests__/embedding-managed-proxy-selection.test.ts +6 -40
- package/src/__tests__/empty-response-hook.test.ts +242 -66
- package/src/__tests__/external-plugin-loader.test.ts +0 -31
- package/src/__tests__/get-skill-detail-audit.test.ts +43 -1
- package/src/__tests__/guardian-routing-invariants.test.ts +91 -0
- package/src/__tests__/history-repair-hook.test.ts +228 -3
- package/src/__tests__/host-app-control-proxy.test.ts +45 -0
- package/src/__tests__/host-browser-proxy.test.ts +254 -9
- package/src/__tests__/identity-routes.test.ts +1 -0
- package/src/__tests__/image-recovery-hook.test.ts +387 -0
- package/src/__tests__/injector-chain.test.ts +5 -4
- package/src/__tests__/injector-v3-suppression.test.ts +373 -47
- package/src/__tests__/intent-routing.test.ts +7 -0
- package/src/__tests__/memory-retrieval-hook.test.ts +117 -15
- package/src/__tests__/notification-decision-strategy.test.ts +3 -3
- package/src/__tests__/oauth-store.test.ts +0 -85
- package/src/__tests__/{context-overflow-policy.test.ts → overflow-policy.test.ts} +1 -1
- package/src/__tests__/parallel-tool.benchmark.test.ts +4 -0
- package/src/__tests__/persist-unsendable-image-downscale.test.ts +29 -9
- package/src/__tests__/persist-unsendable-image.test.ts +4 -4
- package/src/__tests__/persistence-secret-redaction.test.ts +78 -0
- package/src/__tests__/plugin-bootstrap.test.ts +82 -73
- package/src/__tests__/plugin-tool-contribution.test.ts +7 -4
- package/src/__tests__/plugin-types.test.ts +0 -8
- package/src/__tests__/provider-catalog-visibility.test.ts +1 -9
- package/src/__tests__/prune-old-conversations-job.test.ts +99 -0
- package/src/__tests__/registry.test.ts +240 -1
- package/src/__tests__/require-fresh-approval.test.ts +3 -0
- package/src/__tests__/schedule-routes.test.ts +116 -1
- package/src/__tests__/schedule-store.test.ts +28 -0
- package/src/__tests__/schedule-tools.test.ts +94 -1
- package/src/__tests__/server-history-render.test.ts +39 -0
- package/src/__tests__/skill-projection-feature-flag.test.ts +13 -0
- package/src/__tests__/skill-projection.benchmark.test.ts +25 -7
- package/src/__tests__/skills.test.ts +202 -0
- package/src/__tests__/slim-skill-category.test.ts +195 -0
- package/src/__tests__/strip-memory-injections.test.ts +33 -39
- package/src/__tests__/test-support/tool-invocation-seed.ts +79 -0
- package/src/__tests__/title-generate-hook.test.ts +9 -7
- package/src/__tests__/tool-audit-listener.test.ts +264 -1
- package/src/__tests__/tool-error-hook.test.ts +4 -3
- package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +1 -0
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +273 -0
- package/src/__tests__/tool-result-truncate-hook.test.ts +1 -0
- package/src/__tests__/tool-start-timestamp.test.ts +218 -0
- package/src/__tests__/tools-get-route.test.ts +202 -0
- package/src/__tests__/workspace-tool-loader.test.ts +319 -0
- package/src/__tests__/workspace-tools-watcher-flag.test.ts +70 -0
- package/src/agent/loop.ts +569 -319
- package/src/api/events/tool-result.ts +9 -0
- package/src/api/events/tool-use-start.ts +7 -0
- package/src/api/index.ts +10 -0
- package/src/api/responses/conversation-message.ts +135 -27
- package/src/api/responses/memory-v3-selection-log.ts +4 -4
- package/src/approvals/guardian-request-resolvers.ts +26 -0
- package/src/browser-session/backends/host-bridge.ts +29 -0
- package/src/browser-session/index.ts +1 -0
- package/src/browser-session/types.ts +5 -1
- package/src/cli/commands/__tests__/schedules.test.ts +62 -4
- package/src/cli/commands/__tests__/skills.test.ts +53 -0
- package/src/cli/commands/channel-verification-sessions.ts +6 -6
- package/src/cli/commands/inference-providers.ts +0 -8
- package/src/cli/commands/plugins.ts +2 -2
- package/src/cli/commands/schedules.ts +27 -4
- package/src/cli/commands/skills.ts +187 -146
- package/src/cli/commands/tools.ts +106 -0
- package/src/cli/lib/__tests__/install-from-github.test.ts +256 -328
- package/src/cli/lib/__tests__/plugin-catalog-cache.test.ts +6 -2
- package/src/cli/lib/__tests__/plugin-details.test.ts +10 -16
- package/src/cli/lib/__tests__/plugin-marketplace.test.ts +2 -2
- package/src/cli/lib/__tests__/search-plugins.test.ts +145 -240
- package/src/cli/lib/install-from-github.ts +187 -117
- package/src/cli/lib/plugin-catalog-cache.ts +9 -9
- package/src/cli/lib/plugin-details.ts +38 -68
- package/src/cli/lib/plugin-marketplace.ts +42 -14
- package/src/cli/lib/search-plugins.ts +29 -129
- package/src/cli/program.ts +2 -0
- package/src/config/bundled-skills/acp/SKILL.md +1 -0
- package/src/config/bundled-skills/app-builder/SKILL.md +1 -0
- package/src/config/bundled-skills/app-control/SKILL.md +1 -0
- package/src/config/bundled-skills/computer-use/SKILL.md +1 -0
- package/src/config/bundled-skills/contacts/SKILL.md +1 -0
- package/src/config/bundled-skills/document-editor/SKILL.md +1 -0
- package/src/config/bundled-skills/followups/SKILL.md +1 -0
- package/src/config/bundled-skills/image-studio/SKILL.md +1 -0
- package/src/config/bundled-skills/media-processing/SKILL.md +1 -0
- package/src/config/bundled-skills/messaging/SKILL.md +1 -0
- package/src/config/bundled-skills/phone-calls/SKILL.md +1 -0
- package/src/config/bundled-skills/playbooks/SKILL.md +1 -0
- package/src/config/bundled-skills/schedule/SKILL.md +1 -0
- package/src/config/bundled-skills/schedule/TOOLS.json +11 -3
- package/src/config/bundled-skills/sequences/SKILL.md +1 -0
- package/src/config/bundled-skills/settings/SKILL.md +1 -0
- package/src/config/bundled-skills/skill-management/SKILL.md +101 -1
- package/src/config/bundled-skills/subagent/SKILL.md +1 -0
- package/src/config/bundled-skills/transcribe/SKILL.md +1 -0
- package/src/config/env-registry.ts +23 -0
- package/src/config/feature-flag-registry.json +17 -81
- package/src/config/loader.ts +5 -22
- package/src/config/schema.ts +2 -0
- package/src/config/schemas/__tests__/compaction-logs.test.ts +56 -0
- package/src/config/schemas/__tests__/memory-v2.test.ts +0 -1
- package/src/config/schemas/__tests__/memory-v3.test.ts +61 -1
- package/src/config/schemas/compaction-logs.ts +79 -0
- package/src/config/schemas/memory-v2.ts +0 -8
- package/src/config/schemas/memory-v3.ts +104 -33
- package/src/config/seed-inference-profiles.ts +1 -1
- package/src/config/skills.ts +117 -47
- package/src/context/compactor.ts +11 -0
- package/src/context/strip-injections.ts +38 -4
- package/src/credential-execution/feature-gates.ts +0 -21
- package/src/credential-execution/startup-timeout.ts +32 -4
- package/src/daemon/__tests__/conversation-tool-setup-exclude.test.ts +18 -0
- package/src/daemon/conversation-agent-loop-handlers.ts +140 -91
- package/src/daemon/conversation-agent-loop.ts +123 -658
- package/src/daemon/conversation-error.ts +6 -33
- package/src/daemon/conversation-lifecycle.ts +1 -1
- package/src/daemon/conversation-runtime-assembly.ts +183 -22
- package/src/daemon/conversation-skill-tools.ts +137 -8
- package/src/daemon/conversation-slash.ts +0 -14
- package/src/daemon/conversation-store.ts +2 -19
- package/src/daemon/conversation-tool-setup.ts +87 -1
- package/src/daemon/conversation.ts +119 -50
- package/src/daemon/external-plugins-bootstrap.ts +36 -95
- package/src/daemon/handlers/config-channels.ts +11 -2
- package/src/daemon/handlers/shared.ts +9 -1
- package/src/daemon/handlers/skills.ts +10 -4
- package/src/daemon/host-app-control-proxy.ts +72 -57
- package/src/daemon/host-browser-proxy.ts +117 -22
- package/src/daemon/lifecycle.ts +34 -5
- package/src/daemon/message-protocol.ts +0 -7
- package/src/daemon/message-types/schedules.ts +1 -0
- package/src/daemon/message-types/skills.ts +17 -0
- package/src/daemon/providers-setup.ts +3 -0
- package/src/daemon/server.ts +3 -3
- package/src/daemon/tool-setup-types.ts +9 -3
- package/src/daemon/trust-context.ts +23 -0
- package/src/daemon/workspace-tools-watcher.ts +324 -0
- package/src/events/tool-audit-listener.ts +78 -16
- package/src/events/tool-metrics-listener.ts +2 -5
- package/src/memory/__tests__/compaction-log-writer-clickhouse.test.ts +227 -0
- package/src/memory/__tests__/conversation-queries.test.ts +176 -0
- package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +20 -32
- package/src/memory/compaction-log-writer-clickhouse.ts +418 -0
- package/src/memory/conversation-crud.ts +134 -3
- package/src/memory/conversation-queries.ts +64 -7
- package/src/memory/db-init.ts +14 -0
- package/src/memory/embedding-backend.test.ts +130 -1
- package/src/memory/embedding-backend.ts +79 -106
- package/src/memory/embedding-gemini.ts +5 -0
- package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +12 -0
- package/src/memory/graph/__tests__/handle-remember-v2.test.ts +19 -0
- package/src/memory/graph/conversation-graph-memory.ts +36 -25
- package/src/memory/graph/tool-handlers.ts +3 -0
- package/src/memory/job-handlers/cleanup.ts +3 -1
- package/src/memory/jobs-store.ts +0 -28
- package/src/memory/jobs-worker.ts +10 -22
- package/src/memory/memory-marker.ts +29 -0
- package/src/memory/memory-retrospective-startup-cleanup.ts +1 -1
- package/src/memory/migrations/268-add-memory-v3-selections.ts +6 -0
- package/src/memory/migrations/270-schedule-description.ts +36 -0
- package/src/memory/migrations/275-tool-invocations-add-skill-id.test.ts +81 -0
- package/src/memory/migrations/275-tool-invocations-add-skill-id.ts +20 -0
- package/src/memory/migrations/276-tool-invocations-created-at-id-index.test.ts +68 -0
- package/src/memory/migrations/276-tool-invocations-created-at-id-index.ts +20 -0
- package/src/memory/migrations/277-add-memory-v3-ever-injected.ts +29 -0
- package/src/memory/migrations/278-tool-invocations-telemetry-columns.test.ts +96 -0
- package/src/memory/migrations/278-tool-invocations-telemetry-columns.ts +39 -0
- package/src/memory/migrations/279-create-skill-loaded-events.test.ts +84 -0
- package/src/memory/migrations/279-create-skill-loaded-events.ts +26 -0
- package/src/memory/migrations/280-conversations-surfaced-at.test.ts +88 -0
- package/src/memory/migrations/280-conversations-surfaced-at.ts +24 -0
- package/src/memory/migrations/index.ts +10 -0
- package/src/memory/migrations/registry.ts +8 -0
- package/src/memory/schema/conversations.ts +16 -0
- package/src/memory/schema/infrastructure.ts +26 -0
- package/src/memory/skill-loaded-events-store.test.ts +160 -0
- package/src/memory/skill-loaded-events-store.ts +95 -0
- package/src/memory/tool-executed-events-store.test.ts +219 -0
- package/src/memory/tool-executed-events-store.ts +102 -0
- package/src/memory/tool-usage-store.ts +15 -3
- package/src/memory/v2/__tests__/consolidation-job.test.ts +117 -12
- package/src/memory/v2/__tests__/consolidation-prompt-flag-gating-guard.test.ts +189 -0
- package/src/memory/v2/__tests__/injected-block-slugs.test.ts +90 -0
- package/src/memory/v2/__tests__/page-store.test.ts +33 -0
- package/src/memory/v2/__tests__/prompts-consolidation.test.ts +88 -15
- package/src/memory/v2/activation-store.ts +50 -1
- package/src/memory/v2/consolidation-job.ts +113 -29
- package/src/memory/v2/injected-block-slugs.ts +79 -0
- package/src/memory/v2/injection.ts +6 -1
- package/src/memory/v2/prompts/consolidation.ts +414 -13
- package/src/memory/v2/router.ts +2 -28
- package/src/memory/v2/static-context.ts +1 -1
- package/src/memory/v2/types.ts +16 -0
- package/src/notifications/__tests__/copy-composer.test.ts +244 -0
- package/src/notifications/access-request-copy.ts +298 -0
- package/src/notifications/adapters/slack.ts +3 -3
- package/src/notifications/adapters/telegram.ts +2 -1
- package/src/notifications/copy-composer.ts +49 -267
- package/src/notifications/decision-engine.ts +16 -35
- package/src/notifications/home-feed-side-effect.ts +1 -6
- package/src/oauth/oauth-store.ts +0 -9
- package/src/permissions/checker.test.ts +83 -1
- package/src/permissions/checker.ts +25 -2
- package/src/permissions/gateway-threshold-reader.test.ts +182 -0
- package/src/permissions/gateway-threshold-reader.ts +80 -0
- package/src/platform/client.ts +1 -3
- package/src/platform/feature-gate.ts +3 -12
- package/src/plugin-api/constants.ts +4 -2
- package/src/plugin-api/index.ts +63 -11
- package/src/plugin-api/types.ts +236 -71
- package/src/plugins/defaults/compaction/compact.ts +66 -2
- package/src/plugins/defaults/compaction/context-overflow-reducer.ts +240 -32
- package/src/plugins/defaults/compaction/corrected-target.ts +53 -0
- package/src/{daemon/context-overflow-policy.ts → plugins/defaults/compaction/overflow-policy.ts} +1 -1
- package/src/plugins/defaults/compaction/window-manager.ts +303 -1
- package/src/plugins/defaults/empty-response/hooks/post-model-call.ts +173 -0
- package/src/plugins/defaults/empty-response/hooks/stop.ts +11 -115
- package/src/plugins/defaults/empty-response/nudge-state-store.ts +46 -0
- package/src/plugins/defaults/history-repair/hooks/post-model-call.ts +50 -0
- package/src/plugins/defaults/history-repair/hooks/stop.ts +22 -0
- package/src/plugins/defaults/history-repair/repair-state-store.ts +51 -0
- package/src/plugins/defaults/history-repair/terminal.ts +39 -2
- package/src/plugins/defaults/image-recovery/detect.ts +25 -0
- package/src/plugins/defaults/image-recovery/hooks/post-model-call.ts +73 -0
- package/src/plugins/defaults/image-recovery/hooks/stop.ts +22 -0
- package/src/plugins/defaults/image-recovery/image-recovery-state-store.ts +48 -0
- package/src/plugins/defaults/image-recovery/package.json +14 -0
- package/src/{daemon/persist-unsendable-image.ts → plugins/defaults/image-recovery/recover.ts} +67 -14
- package/src/plugins/defaults/index.ts +71 -5
- package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +76 -112
- package/src/plugins/defaults/memory-retrieval/hooks/{user-prompt-submit-temp.ts → user-prompt-submit.ts} +83 -74
- package/src/plugins/defaults/memory-retrieval/injector-chain.ts +14 -8
- package/src/plugins/defaults/memory-retrieval/injectors.ts +2 -18
- package/src/plugins/defaults/memory-retrieval/package.json +14 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/carry-integration.test.ts +1157 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/injection.test.ts +683 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/live-integration.test.ts +161 -140
- package/src/plugins/defaults/memory-v3-shadow/__tests__/maintain-job.test.ts +160 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/orchestrate.test.ts +335 -316
- package/src/plugins/defaults/memory-v3-shadow/__tests__/pool-select.test.ts +145 -53
- package/src/plugins/defaults/memory-v3-shadow/__tests__/render-injection.test.ts +38 -1
- package/src/plugins/defaults/memory-v3-shadow/__tests__/selection-log-store.test.ts +18 -8
- package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-integration.test.ts +112 -71
- package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +192 -51
- package/src/plugins/defaults/memory-v3-shadow/__tests__/types.test.ts +4 -16
- package/src/plugins/defaults/memory-v3-shadow/card.test.ts +173 -0
- package/src/plugins/defaults/memory-v3-shadow/card.ts +116 -0
- package/src/plugins/defaults/memory-v3-shadow/core-set.test.ts +104 -0
- package/src/plugins/defaults/memory-v3-shadow/core-set.ts +59 -0
- package/src/plugins/defaults/memory-v3-shadow/ever-injected-store.test.ts +305 -0
- package/src/plugins/defaults/memory-v3-shadow/ever-injected-store.ts +278 -0
- package/src/plugins/defaults/memory-v3-shadow/hot-set.test.ts +138 -0
- package/src/plugins/defaults/memory-v3-shadow/hot-set.ts +85 -0
- package/src/plugins/defaults/memory-v3-shadow/injector.ts +331 -24
- package/src/plugins/defaults/memory-v3-shadow/maintain-job.ts +119 -13
- package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +169 -114
- package/src/plugins/defaults/memory-v3-shadow/page-content.ts +47 -16
- package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +144 -66
- package/src/plugins/defaults/memory-v3-shadow/prune.test.ts +758 -0
- package/src/plugins/defaults/memory-v3-shadow/prune.ts +471 -0
- package/src/plugins/defaults/memory-v3-shadow/render-injection.ts +68 -16
- package/src/plugins/defaults/memory-v3-shadow/selection-log-store.ts +19 -12
- package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +96 -43
- package/src/plugins/defaults/memory-v3-shadow/types.ts +34 -17
- package/src/plugins/defaults/title-generate/hooks/stop.ts +9 -11
- package/src/plugins/pipeline.ts +8 -5
- package/src/plugins/types.ts +5 -51
- package/src/providers/cache-control.ts +26 -0
- package/src/providers/inference/__tests__/base-url-route-validation.test.ts +1 -2
- package/src/providers/model-catalog.ts +13 -1
- package/src/providers/openai/__tests__/tool-choice-mapping.test.ts +147 -0
- package/src/providers/openai/chat-completions-provider.ts +46 -0
- package/src/providers/openai/responses-provider.ts +45 -0
- package/src/providers/registry.ts +0 -8
- package/src/runtime/__tests__/agent-wake.test.ts +0 -1
- package/src/runtime/agent-wake.ts +13 -0
- package/src/runtime/routes/__tests__/acp-routes.test.ts +151 -0
- package/src/runtime/routes/__tests__/consolidation-routes.test.ts +12 -50
- package/src/runtime/routes/__tests__/conversation-surface-routes.test.ts +322 -0
- package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +0 -62
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +31 -11
- package/src/runtime/routes/acp-routes.test.ts +106 -0
- package/src/runtime/routes/acp-routes.ts +248 -2
- package/src/runtime/routes/browser-tabs-routes.ts +1 -1
- package/src/runtime/routes/channel-verification-routes.ts +14 -5
- package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +0 -14
- package/src/runtime/routes/consolidation-routes.ts +6 -82
- package/src/runtime/routes/conversation-list-routes.ts +6 -0
- package/src/runtime/routes/conversation-management-routes.ts +71 -0
- package/src/runtime/routes/identity-routes.ts +8 -0
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +33 -34
- package/src/runtime/routes/inference-provider-connection-routes.ts +0 -45
- package/src/runtime/routes/plugins-routes.ts +34 -45
- package/src/runtime/routes/schedule-routes.ts +43 -5
- package/src/runtime/routes/settings-routes.ts +140 -15
- package/src/runtime/routes/skills-routes.ts +18 -6
- package/src/runtime/services/__tests__/conversation-serializer.test.ts +140 -0
- package/src/runtime/services/conversation-serializer.ts +38 -1
- package/src/runtime/verification-outbound-actions.ts +147 -2
- package/src/runtime/verification-templates.ts +29 -3
- package/src/schedule/schedule-store.ts +19 -0
- package/src/skills/catalog-install.ts +77 -13
- package/src/tasks/task-scheduler.ts +1 -0
- package/src/telemetry/types.ts +66 -1
- package/src/telemetry/usage-telemetry-reporter.test.ts +542 -13
- package/src/telemetry/usage-telemetry-reporter.ts +213 -20
- package/src/tools/browser/__tests__/browser-execution-acquire.test.ts +49 -2
- package/src/tools/browser/__tests__/browser-status.test.ts +29 -5
- package/src/tools/browser/browser-execution.ts +27 -9
- package/src/tools/browser/cdp-client/__tests__/factory.test.ts +380 -4
- package/src/tools/browser/cdp-client/__tests__/host-bridge-cdp-client.test.ts +107 -0
- package/src/tools/browser/cdp-client/__tests__/types.test.ts +6 -1
- package/src/tools/browser/cdp-client/factory.ts +217 -17
- package/src/tools/browser/cdp-client/host-bridge-cdp-client.ts +67 -0
- package/src/tools/browser/cdp-client/types.ts +22 -2
- package/src/tools/credential-execution/make-authenticated-request.ts +2 -1
- package/src/tools/credential-execution/manage-secure-command-tool.ts +169 -164
- package/src/tools/credential-execution/run-authenticated-command.ts +2 -1
- package/src/tools/executor.ts +39 -7
- package/src/tools/registry.ts +387 -5
- package/src/tools/schedule/create.ts +16 -0
- package/src/tools/schedule/list.ts +12 -4
- package/src/tools/schedule/update.ts +12 -0
- package/src/tools/skills/load.ts +11 -6
- package/src/tools/terminal/safe-env.ts +2 -0
- package/src/tools/types.ts +65 -9
- package/src/tools/workspace-tools/loader.ts +673 -0
- package/src/usage/attribution.ts +28 -0
- package/src/util/device-id.ts +17 -3
- package/src/util/platform.ts +16 -0
- package/tsconfig.plugin-api.json +13 -0
- package/src/__tests__/plugin-external-api.test.ts +0 -68
- package/src/__tests__/plugin-skill-contribution.test.ts +0 -355
- package/src/daemon/message-types/browser.ts +0 -10
- package/src/notifications/__tests__/emit-signal-home-feed.test.ts +0 -187
- package/src/plugins/defaults/memory-v3-shadow/__tests__/fixtures/eval-turns.json +0 -36
- package/src/plugins/defaults/memory-v3-shadow/__tests__/fixtures/live-turns.json +0 -37
- package/src/plugins/defaults/memory-v3-shadow/__tests__/working-set-eviction.test.ts +0 -106
- package/src/plugins/defaults/memory-v3-shadow/__tests__/working-set-skeleton.test.ts +0 -44
- package/src/plugins/defaults/memory-v3-shadow/working-set.ts +0 -91
- package/src/plugins/external-api.ts +0 -114
- package/src/plugins/plugin-skill-contributions.ts +0 -292
|
@@ -9,6 +9,10 @@
|
|
|
9
9
|
* values from the context payload.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
+
import {
|
|
13
|
+
buildAccessRequestContractText,
|
|
14
|
+
sanitizeIdentityField,
|
|
15
|
+
} from "./access-request-copy.js";
|
|
12
16
|
import {
|
|
13
17
|
buildGuardianRequestCodeInstruction,
|
|
14
18
|
resolveGuardianQuestionInstructionMode,
|
|
@@ -32,285 +36,39 @@ export function nonEmpty(value: string | undefined): string | undefined {
|
|
|
32
36
|
return trimmed.length > 0 ? trimmed : undefined;
|
|
33
37
|
}
|
|
34
38
|
|
|
35
|
-
// ── Access-request copy contract ─────────────────────────────────────────────
|
|
36
|
-
//
|
|
37
|
-
// Deterministic helpers for building guardian-facing access-request copy.
|
|
38
|
-
// These are used both by the fallback template and the decision-engine
|
|
39
|
-
// post-generation enforcement to ensure required directives always appear.
|
|
40
|
-
|
|
41
|
-
const IDENTITY_FIELD_MAX_LENGTH = 120;
|
|
42
|
-
|
|
43
39
|
/**
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
* - Clamps to IDENTITY_FIELD_MAX_LENGTH characters.
|
|
48
|
-
* - Wraps in quotes to neutralize instruction-like payload text.
|
|
40
|
+
* Safely read a string property from an unknown-typed payload object.
|
|
41
|
+
* Returns `undefined` when the payload is falsy, not an object, or the
|
|
42
|
+
* key does not hold a string value.
|
|
49
43
|
*/
|
|
50
|
-
export function
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
stripped.length > IDENTITY_FIELD_MAX_LENGTH
|
|
54
|
-
? stripped.slice(0, IDENTITY_FIELD_MAX_LENGTH) + "…"
|
|
55
|
-
: stripped;
|
|
56
|
-
return clamped;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export function buildAccessRequestIdentityLine(
|
|
60
|
-
payload: Record<string, unknown>,
|
|
61
|
-
): string {
|
|
62
|
-
const requester = sanitizeIdentityField(
|
|
63
|
-
str(payload.senderIdentifier, "Someone"),
|
|
64
|
-
);
|
|
65
|
-
const sourceChannel =
|
|
66
|
-
typeof payload.sourceChannel === "string"
|
|
67
|
-
? payload.sourceChannel
|
|
68
|
-
: undefined;
|
|
69
|
-
const callerName = nonEmpty(
|
|
70
|
-
typeof payload.actorDisplayName === "string"
|
|
71
|
-
? payload.actorDisplayName
|
|
72
|
-
: undefined,
|
|
73
|
-
);
|
|
74
|
-
const actorUsername = nonEmpty(
|
|
75
|
-
typeof payload.actorUsername === "string"
|
|
76
|
-
? payload.actorUsername
|
|
77
|
-
: undefined,
|
|
78
|
-
);
|
|
79
|
-
const actorExternalId = nonEmpty(
|
|
80
|
-
typeof payload.actorExternalId === "string"
|
|
81
|
-
? payload.actorExternalId
|
|
82
|
-
: undefined,
|
|
83
|
-
);
|
|
84
|
-
|
|
85
|
-
if (sourceChannel === "phone" && callerName) {
|
|
86
|
-
const safeName = sanitizeIdentityField(callerName);
|
|
87
|
-
const safeId = sanitizeIdentityField(
|
|
88
|
-
str(payload.actorExternalId, requester),
|
|
89
|
-
);
|
|
90
|
-
return `${safeName} (${safeId}) is calling and requesting access to the assistant.`;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// For non-voice, include extra context when available.
|
|
94
|
-
// Sanitize before comparing to avoid deduplication failures when identity
|
|
95
|
-
// fields contain control characters that are stripped from `requester`.
|
|
96
|
-
const sanitizedUsername = actorUsername
|
|
97
|
-
? sanitizeIdentityField(actorUsername)
|
|
98
|
-
: undefined;
|
|
99
|
-
const sanitizedExternalId = actorExternalId
|
|
100
|
-
? sanitizeIdentityField(actorExternalId)
|
|
101
|
-
: undefined;
|
|
102
|
-
// When the requester is a raw Slack user ID (e.g. the fallback path in
|
|
103
|
-
// access-request-helper sets senderIdentifier to the raw actorExternalId),
|
|
104
|
-
// format it as a Slack mention so it renders as a clickable display name.
|
|
105
|
-
const formattedRequester =
|
|
106
|
-
sourceChannel === "slack" && /^U[A-Z0-9]+$/i.test(requester)
|
|
107
|
-
? `<@${requester}>`
|
|
108
|
-
: requester;
|
|
109
|
-
const parts = [formattedRequester];
|
|
110
|
-
if (sanitizedUsername && sanitizedUsername !== requester) {
|
|
111
|
-
parts.push(`@${sanitizedUsername}`);
|
|
112
|
-
}
|
|
113
|
-
if (
|
|
114
|
-
sanitizedExternalId &&
|
|
115
|
-
sanitizedExternalId !== requester &&
|
|
116
|
-
sanitizedExternalId !== sanitizedUsername
|
|
117
|
-
) {
|
|
118
|
-
// For Slack, use the <@U...> mention format so Slack auto-renders
|
|
119
|
-
// the user ID as a clickable display name.
|
|
120
|
-
const formattedId =
|
|
121
|
-
sourceChannel === "slack" && /^U[A-Z0-9]+$/i.test(sanitizedExternalId)
|
|
122
|
-
? `<@${sanitizedExternalId}>`
|
|
123
|
-
: `[${sanitizedExternalId}]`;
|
|
124
|
-
parts.push(formattedId);
|
|
125
|
-
}
|
|
126
|
-
if (sourceChannel) {
|
|
127
|
-
parts.push(`via ${sourceChannel}`);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
return `${parts.join(" ")} is requesting access to the assistant.`;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
export const MESSAGE_PREVIEW_MAX_LENGTH = 200;
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* Sanitize an untrusted message preview for inclusion in notification copy.
|
|
137
|
-
*
|
|
138
|
-
* Like {@link sanitizeIdentityField} but uses the higher
|
|
139
|
-
* MESSAGE_PREVIEW_MAX_LENGTH limit (200 chars) instead of the identity
|
|
140
|
-
* field limit (120 chars).
|
|
141
|
-
*/
|
|
142
|
-
export function sanitizeMessagePreview(value: string): string {
|
|
143
|
-
const stripped = value.replace(/[\x00-\x1f\x7f-\x9f\r\n]+/g, " ").trim();
|
|
144
|
-
const clamped =
|
|
145
|
-
stripped.length > MESSAGE_PREVIEW_MAX_LENGTH
|
|
146
|
-
? stripped.slice(0, MESSAGE_PREVIEW_MAX_LENGTH) + "…"
|
|
147
|
-
: stripped;
|
|
148
|
-
return clamped;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
/**
|
|
152
|
-
* Build a quoted preview of the requester's original message for inclusion
|
|
153
|
-
* in guardian-facing access-request copy. Sanitizes and truncates to keep
|
|
154
|
-
* the notification concise.
|
|
155
|
-
*
|
|
156
|
-
* Returns `undefined` when no usable preview is available.
|
|
157
|
-
*/
|
|
158
|
-
function buildAccessRequestMessagePreview(
|
|
159
|
-
payload: Record<string, unknown>,
|
|
44
|
+
export function readPayloadString(
|
|
45
|
+
payload: unknown,
|
|
46
|
+
key: string,
|
|
160
47
|
): string | undefined {
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
: undefined;
|
|
165
|
-
if (!raw) return undefined;
|
|
166
|
-
|
|
167
|
-
const sanitized = sanitizeMessagePreview(raw);
|
|
168
|
-
if (sanitized.length === 0) return undefined;
|
|
169
|
-
|
|
170
|
-
return `> Their message: "${sanitized}"`;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
export function buildAccessRequestInviteDirective(): string {
|
|
174
|
-
return 'Reply "open invite flow" to start Trusted Contacts invite flow.';
|
|
48
|
+
if (!payload || typeof payload !== "object") return undefined;
|
|
49
|
+
const value = (payload as Record<string, unknown>)[key];
|
|
50
|
+
return typeof value === "string" ? value : undefined;
|
|
175
51
|
}
|
|
176
52
|
|
|
177
53
|
/**
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
* so contractions like "Don\u2019t" are matched by the `n't` lookbehind.
|
|
182
|
-
* - Collapses runs of whitespace into a single space so "Do not reply"
|
|
183
|
-
* is matched by the single-space negative lookbehind.
|
|
184
|
-
* - Trims leading/trailing whitespace.
|
|
54
|
+
* Derive a short notification title from a message body. Trims to the
|
|
55
|
+
* first sentence terminator when present, then caps the result at
|
|
56
|
+
* 60 characters with an ellipsis.
|
|
185
57
|
*/
|
|
186
|
-
export function
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
.
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
/**
|
|
194
|
-
* Check whether a text contains the required access-request instruction elements:
|
|
195
|
-
* 1. Approve directive: Reply "CODE approve"
|
|
196
|
-
* 2. Reject directive: Reply "CODE reject"
|
|
197
|
-
* 3. Invite directive: Reply "open invite flow"
|
|
198
|
-
*
|
|
199
|
-
* Each directive is matched independently using negative lookbehind to reject
|
|
200
|
-
* matches preceded by negation words ("not", "n't", "never"). This prevents
|
|
201
|
-
* contradictory copy like `Do not reply "CODE reject"` from satisfying the
|
|
202
|
-
* check even when a positive approve directive exists nearby.
|
|
203
|
-
*
|
|
204
|
-
* The text is normalized before matching to handle smart apostrophes and
|
|
205
|
-
* multiple whitespace characters that would otherwise bypass negation detection.
|
|
206
|
-
*/
|
|
207
|
-
export function hasAccessRequestInstructions(
|
|
208
|
-
text: string | undefined,
|
|
209
|
-
requestCode: string,
|
|
210
|
-
): boolean {
|
|
211
|
-
if (typeof text !== "string") return false;
|
|
212
|
-
const normalized = normalizeForDirectiveMatching(text);
|
|
213
|
-
const escapedCode = requestCode.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
214
|
-
// Each directive must follow "reply" without a preceding negation word.
|
|
215
|
-
// Negative lookbehinds reject "do not reply", "don't reply", "never reply".
|
|
216
|
-
const approveRe = new RegExp(
|
|
217
|
-
`(?<!not\\s)(?<!n't\\s)(?<!never\\s)reply\\b[^.!?\\n]*?"${escapedCode}\\s+approve"`,
|
|
218
|
-
"i",
|
|
219
|
-
);
|
|
220
|
-
const rejectRe = new RegExp(
|
|
221
|
-
`(?<!not\\s)(?<!n't\\s)(?<!never\\s)reply\\b[^.!?\\n]*?"${escapedCode}\\s+reject"`,
|
|
222
|
-
"i",
|
|
223
|
-
);
|
|
224
|
-
const inviteRe =
|
|
225
|
-
/(?<!not\s)(?<!n't\s)(?<!never\s)reply\b[^.!?\n]*?"open invite flow"/i;
|
|
226
|
-
|
|
227
|
-
return (
|
|
228
|
-
approveRe.test(normalized) &&
|
|
229
|
-
rejectRe.test(normalized) &&
|
|
230
|
-
inviteRe.test(normalized)
|
|
231
|
-
);
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
/**
|
|
235
|
-
* Check whether text contains the invite-flow directive ("open invite flow")
|
|
236
|
-
* using the same normalized negative-lookbehind pattern as the full check.
|
|
237
|
-
* This is used for enforcement when requestCode is absent but the invite
|
|
238
|
-
* directive should still be present.
|
|
239
|
-
*/
|
|
240
|
-
export function hasInviteFlowDirective(text: string | undefined): boolean {
|
|
241
|
-
if (typeof text !== "string") return false;
|
|
242
|
-
const normalized = normalizeForDirectiveMatching(text);
|
|
243
|
-
const inviteRe =
|
|
244
|
-
/(?<!not\s)(?<!n't\s)(?<!never\s)reply\b[^.!?\n]*?"open invite flow"/i;
|
|
245
|
-
return inviteRe.test(normalized);
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
/**
|
|
249
|
-
* Build the deterministic access-request contract text from payload fields.
|
|
250
|
-
* This is the canonical baseline that enforcement can append when generated
|
|
251
|
-
* copy is missing required elements.
|
|
252
|
-
*
|
|
253
|
-
* Channel-agnostic by design: this function reads from the generic
|
|
254
|
-
* `contextPayload` and works identically regardless of which channel
|
|
255
|
-
* (Slack, Telegram, desktop, etc.) the notification is delivered to.
|
|
256
|
-
* When `guardianResolutionSource` is present and not `"source-channel-contact"`,
|
|
257
|
-
* the guardian was resolved via fallback (e.g. vellum anchor) rather than
|
|
258
|
-
* a verified same-channel contact — downstream copy or routing can use
|
|
259
|
-
* this to append verification CTAs like "Was this you?".
|
|
260
|
-
*/
|
|
261
|
-
export function buildAccessRequestContractText(
|
|
262
|
-
payload: Record<string, unknown>,
|
|
263
|
-
): string {
|
|
264
|
-
const requestCode = nonEmpty(
|
|
265
|
-
typeof payload.requestCode === "string" ? payload.requestCode : undefined,
|
|
266
|
-
);
|
|
267
|
-
const previousMemberStatus =
|
|
268
|
-
typeof payload.previousMemberStatus === "string"
|
|
269
|
-
? payload.previousMemberStatus
|
|
270
|
-
: undefined;
|
|
271
|
-
|
|
272
|
-
const guardianResolutionSource =
|
|
273
|
-
typeof payload.guardianResolutionSource === "string"
|
|
274
|
-
? payload.guardianResolutionSource
|
|
275
|
-
: undefined;
|
|
276
|
-
const sourceChannel =
|
|
277
|
-
typeof payload.sourceChannel === "string"
|
|
278
|
-
? payload.sourceChannel
|
|
279
|
-
: undefined;
|
|
280
|
-
|
|
281
|
-
const lines: string[] = [];
|
|
282
|
-
lines.push(buildAccessRequestIdentityLine(payload));
|
|
283
|
-
const preview = buildAccessRequestMessagePreview(payload);
|
|
284
|
-
if (preview) {
|
|
285
|
-
lines.push(preview);
|
|
286
|
-
}
|
|
287
|
-
if (previousMemberStatus === "revoked") {
|
|
288
|
-
lines.push("Note: this user was previously revoked.");
|
|
289
|
-
}
|
|
290
|
-
if (requestCode) {
|
|
291
|
-
const code = requestCode.toUpperCase();
|
|
292
|
-
lines.push(
|
|
293
|
-
`Reply "${code} approve" to grant access or "${code} reject" to deny.`,
|
|
294
|
-
);
|
|
295
|
-
}
|
|
296
|
-
lines.push(buildAccessRequestInviteDirective());
|
|
297
|
-
if (
|
|
298
|
-
(guardianResolutionSource === "vellum-anchor" ||
|
|
299
|
-
guardianResolutionSource === "none") &&
|
|
300
|
-
sourceChannel
|
|
301
|
-
) {
|
|
302
|
-
lines.push(
|
|
303
|
-
`Note: You haven't verified your identity on ${sourceChannel} yet. If this was you trying to message your assistant, say "help me verify as guardian on ${sourceChannel}" to set up direct access.`,
|
|
304
|
-
);
|
|
305
|
-
}
|
|
306
|
-
return lines.join("\n");
|
|
58
|
+
export function deriveTitle(body: string): string {
|
|
59
|
+
const firstSentenceEnd = body.search(/[.!?](\s|$)/);
|
|
60
|
+
const candidate =
|
|
61
|
+
firstSentenceEnd > 0 ? body.slice(0, firstSentenceEnd + 1) : body;
|
|
62
|
+
return candidate.length > 60
|
|
63
|
+
? candidate.slice(0, 60).trim() + "\u2026"
|
|
64
|
+
: candidate.trim();
|
|
307
65
|
}
|
|
308
66
|
|
|
309
67
|
// Templates keyed by dot-separated sourceEventName strings matching producers.
|
|
310
68
|
const TEMPLATES: Partial<Record<NotificationSourceEventName, CopyTemplate>> = {
|
|
311
69
|
"schedule.notify": (payload) => ({
|
|
312
|
-
title: "Reminder",
|
|
313
|
-
body: str(payload.message,
|
|
70
|
+
title: str(payload.label, "Reminder"),
|
|
71
|
+
body: str(payload.message, "A reminder has fired"),
|
|
314
72
|
}),
|
|
315
73
|
|
|
316
74
|
"guardian.question": (payload) => {
|
|
@@ -553,6 +311,30 @@ export function composeFallbackCopy(
|
|
|
553
311
|
signal: NotificationSignal,
|
|
554
312
|
channels: NotificationChannel[],
|
|
555
313
|
): Partial<Record<NotificationChannel, RenderedChannelCopy>> {
|
|
314
|
+
// Honor user-supplied content when present. The assistant_tool
|
|
315
|
+
// pass-through handles the happy path (sourceChannel === "assistant_tool");
|
|
316
|
+
// this catches the same payload fields when the LLM fallback fires for
|
|
317
|
+
// any source channel.
|
|
318
|
+
const msg = nonEmpty(
|
|
319
|
+
readPayloadString(signal.contextPayload, "requestedMessage"),
|
|
320
|
+
);
|
|
321
|
+
if (msg) {
|
|
322
|
+
const title =
|
|
323
|
+
nonEmpty(readPayloadString(signal.contextPayload, "requestedTitle")) ??
|
|
324
|
+
deriveTitle(msg);
|
|
325
|
+
const baseCopy: RenderedChannelCopy = {
|
|
326
|
+
title,
|
|
327
|
+
body: msg,
|
|
328
|
+
conversationSeedMessage: msg,
|
|
329
|
+
};
|
|
330
|
+
const result: Partial<Record<NotificationChannel, RenderedChannelCopy>> =
|
|
331
|
+
{};
|
|
332
|
+
for (const ch of channels) {
|
|
333
|
+
result[ch] = applyChannelDefaults(ch, baseCopy);
|
|
334
|
+
}
|
|
335
|
+
return result;
|
|
336
|
+
}
|
|
337
|
+
|
|
556
338
|
const template =
|
|
557
339
|
TEMPLATES[signal.sourceEventName as NotificationSourceEventName];
|
|
558
340
|
|
|
@@ -23,17 +23,22 @@ import {
|
|
|
23
23
|
import type { Provider } from "../providers/types.js";
|
|
24
24
|
import { getLogger } from "../util/logger.js";
|
|
25
25
|
import { truncate } from "../util/truncate.js";
|
|
26
|
+
import {
|
|
27
|
+
buildAccessRequestContractText,
|
|
28
|
+
buildAccessRequestInviteDirective,
|
|
29
|
+
hasAccessRequestInstructions,
|
|
30
|
+
hasInviteFlowDirective,
|
|
31
|
+
} from "./access-request-copy.js";
|
|
26
32
|
import {
|
|
27
33
|
buildConversationCandidates,
|
|
28
34
|
type ConversationCandidateSet,
|
|
29
35
|
serializeCandidatesForPrompt,
|
|
30
36
|
} from "./conversation-candidates.js";
|
|
31
37
|
import {
|
|
32
|
-
buildAccessRequestContractText,
|
|
33
|
-
buildAccessRequestInviteDirective,
|
|
34
38
|
composeFallbackCopy,
|
|
35
|
-
|
|
36
|
-
|
|
39
|
+
deriveTitle,
|
|
40
|
+
nonEmpty,
|
|
41
|
+
readPayloadString,
|
|
37
42
|
} from "./copy-composer.js";
|
|
38
43
|
import { createDecision } from "./decisions-store.js";
|
|
39
44
|
import {
|
|
@@ -56,21 +61,6 @@ const log = getLogger("notification-decision-engine");
|
|
|
56
61
|
const DECISION_TIMEOUT_MS = 15_000;
|
|
57
62
|
const PROMPT_VERSION = "v4";
|
|
58
63
|
|
|
59
|
-
/**
|
|
60
|
-
* Derive a short notification title from a message body. Used when an
|
|
61
|
-
* assistant_tool-sourced signal supplies `requestedMessage` without an
|
|
62
|
-
* explicit `requestedTitle`: trims to the first sentence terminator when
|
|
63
|
-
* present, then caps the result at 60 characters with an ellipsis.
|
|
64
|
-
*/
|
|
65
|
-
function deriveTitle(body: string): string {
|
|
66
|
-
const firstSentenceEnd = body.search(/[.!?](\s|$)/);
|
|
67
|
-
const candidate =
|
|
68
|
-
firstSentenceEnd > 0 ? body.slice(0, firstSentenceEnd + 1) : body;
|
|
69
|
-
return candidate.length > 60
|
|
70
|
-
? candidate.slice(0, 60).trim() + "…"
|
|
71
|
-
: candidate.trim();
|
|
72
|
-
}
|
|
73
|
-
|
|
74
64
|
/**
|
|
75
65
|
* Maximum character budget for identity context injected into the notification
|
|
76
66
|
* decision prompt. We truncate to prevent oversized prompts when SOUL.md /
|
|
@@ -768,24 +758,15 @@ export async function evaluateSignal(
|
|
|
768
758
|
// classifier entirely. The producer has already done the routing and
|
|
769
759
|
// copy decisions — we just enforce the standard post-decision guards
|
|
770
760
|
// and persist the result.
|
|
771
|
-
|
|
772
|
-
signal.
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
typeof (signal.contextPayload as Record<string, unknown>)
|
|
776
|
-
.requestedMessage === "string" &&
|
|
777
|
-
(
|
|
778
|
-
(signal.contextPayload as Record<string, unknown>)
|
|
779
|
-
.requestedMessage as string
|
|
780
|
-
).trim().length > 0
|
|
781
|
-
) {
|
|
761
|
+
const requestedBody = nonEmpty(
|
|
762
|
+
readPayloadString(signal.contextPayload, "requestedMessage"),
|
|
763
|
+
);
|
|
764
|
+
if (signal.sourceChannel === "assistant_tool" && requestedBody) {
|
|
782
765
|
const payload = signal.contextPayload as Record<string, unknown>;
|
|
783
|
-
const body =
|
|
766
|
+
const body = requestedBody;
|
|
784
767
|
const title =
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
? (payload.requestedTitle as string).trim()
|
|
788
|
-
: deriveTitle(body);
|
|
768
|
+
nonEmpty(readPayloadString(signal.contextPayload, "requestedTitle")) ??
|
|
769
|
+
deriveTitle(body);
|
|
789
770
|
const isUrgent =
|
|
790
771
|
signal.attentionHints.urgency === "critical" ||
|
|
791
772
|
signal.attentionHints.urgency === "high";
|
|
@@ -22,6 +22,7 @@ import { getConversation } from "../memory/conversation-crud.js";
|
|
|
22
22
|
import { isBackgroundConversationType } from "../memory/conversation-types.js";
|
|
23
23
|
import { getLogger } from "../util/logger.js";
|
|
24
24
|
import { isConversationSeedSane } from "./conversation-seed-composer.js";
|
|
25
|
+
import { readPayloadString } from "./copy-composer.js";
|
|
25
26
|
import type { NotificationSignal } from "./signal.js";
|
|
26
27
|
import type { NotificationDecision, RenderedChannelCopy } from "./types.js";
|
|
27
28
|
|
|
@@ -231,12 +232,6 @@ function resolveHomeFeedMirror(
|
|
|
231
232
|
return { mirror: false };
|
|
232
233
|
}
|
|
233
234
|
|
|
234
|
-
function readPayloadString(payload: unknown, key: string): string | undefined {
|
|
235
|
-
if (!payload || typeof payload !== "object") return undefined;
|
|
236
|
-
const value = (payload as Record<string, unknown>)[key];
|
|
237
|
-
return typeof value === "string" ? value : undefined;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
235
|
/**
|
|
241
236
|
* Routing-intent enforcement can prune `selectedChannels` without also
|
|
242
237
|
* pruning `renderedCopy`, so iterating `renderedCopy` directly risks
|
package/src/oauth/oauth-store.ts
CHANGED
|
@@ -856,15 +856,6 @@ export function getConnectionByProvider(
|
|
|
856
856
|
return getActiveConnection(provider, { clientId });
|
|
857
857
|
}
|
|
858
858
|
|
|
859
|
-
/** @deprecated Use {@link getActiveConnection} instead. */
|
|
860
|
-
export function getConnectionByProviderAndAccount(
|
|
861
|
-
provider: string,
|
|
862
|
-
accountInfo?: string,
|
|
863
|
-
clientId?: string,
|
|
864
|
-
): OAuthConnectionRow | undefined {
|
|
865
|
-
return getActiveConnection(provider, { clientId, account: accountInfo });
|
|
866
|
-
}
|
|
867
|
-
|
|
868
859
|
/**
|
|
869
860
|
* Get ALL active connections for a provider (supports multi-account).
|
|
870
861
|
*/
|
|
@@ -76,8 +76,13 @@ mock.module("../util/platform.js", () => ({
|
|
|
76
76
|
}));
|
|
77
77
|
|
|
78
78
|
// Mock gateway threshold reader — return "low" by default (conversation context default).
|
|
79
|
+
// `mockRefreshedThreshold` simulates the cache-bypassing refresh performed
|
|
80
|
+
// before a prompt is surfaced; `null` means "refresh failed, keep decision".
|
|
81
|
+
let mockCachedThreshold = "low";
|
|
82
|
+
let mockRefreshedThreshold: string | null = null;
|
|
79
83
|
mock.module("./gateway-threshold-reader.js", () => ({
|
|
80
|
-
getAutoApproveThreshold: async () =>
|
|
84
|
+
getAutoApproveThreshold: async () => mockCachedThreshold,
|
|
85
|
+
refreshAutoApproveThreshold: async () => mockRefreshedThreshold,
|
|
81
86
|
_clearGlobalCacheForTesting: () => {},
|
|
82
87
|
}));
|
|
83
88
|
|
|
@@ -136,6 +141,8 @@ describe("Permission Checker (gateway IPC)", () => {
|
|
|
136
141
|
testConfig.skills = { load: { extraDirs: [] } };
|
|
137
142
|
mockIsContainerized = false;
|
|
138
143
|
mockIpcClassifyRiskResult = undefined;
|
|
144
|
+
mockCachedThreshold = "low";
|
|
145
|
+
mockRefreshedThreshold = null;
|
|
139
146
|
});
|
|
140
147
|
|
|
141
148
|
// ── classifyRisk ──────────────────────────────────────────────────────────
|
|
@@ -473,6 +480,81 @@ describe("Permission Checker (gateway IPC)", () => {
|
|
|
473
480
|
),
|
|
474
481
|
).rejects.toThrow(/Gateway IPC classify_risk failed/);
|
|
475
482
|
});
|
|
483
|
+
|
|
484
|
+
// ── Stale-threshold refresh before prompting ────────────────────────────
|
|
485
|
+
// The threshold reader caches values (5s conversation / 30s global TTL)
|
|
486
|
+
// and no write path invalidates them, so a user who just switched to
|
|
487
|
+
// Full access could still be prompted from the stale snapshot. check()
|
|
488
|
+
// must re-read the threshold fresh before surfacing a prompt.
|
|
489
|
+
|
|
490
|
+
test("re-evaluates with refreshed threshold instead of prompting (stale cache after Full access)", async () => {
|
|
491
|
+
mockIpcClassifyRiskResult = {
|
|
492
|
+
risk: "high",
|
|
493
|
+
reason: "Recursive force delete",
|
|
494
|
+
matchType: "registry",
|
|
495
|
+
scopeOptions: [],
|
|
496
|
+
};
|
|
497
|
+
mockCachedThreshold = "low"; // stale cached value
|
|
498
|
+
mockRefreshedThreshold = "high"; // user just selected Full access
|
|
499
|
+
const result = await check(
|
|
500
|
+
"bash",
|
|
501
|
+
{ command: "rm -rf /tmp/stale-cache-test" },
|
|
502
|
+
"/home/user/project",
|
|
503
|
+
);
|
|
504
|
+
expect(result.decision).toBe("allow");
|
|
505
|
+
expect(result.reason).toContain("within auto-approve threshold");
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
test("keeps the prompt when the threshold refresh fails", async () => {
|
|
509
|
+
mockIpcClassifyRiskResult = {
|
|
510
|
+
risk: "high",
|
|
511
|
+
reason: "Recursive force delete",
|
|
512
|
+
matchType: "registry",
|
|
513
|
+
scopeOptions: [],
|
|
514
|
+
};
|
|
515
|
+
mockCachedThreshold = "low";
|
|
516
|
+
mockRefreshedThreshold = null; // gateway unreachable during refresh
|
|
517
|
+
const result = await check(
|
|
518
|
+
"bash",
|
|
519
|
+
{ command: "rm -rf /tmp/refresh-failed-test" },
|
|
520
|
+
"/home/user/project",
|
|
521
|
+
);
|
|
522
|
+
expect(result.decision).toBe("prompt");
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
test("keeps the prompt when the refreshed threshold matches the cached one", async () => {
|
|
526
|
+
mockIpcClassifyRiskResult = {
|
|
527
|
+
risk: "high",
|
|
528
|
+
reason: "Recursive force delete",
|
|
529
|
+
matchType: "registry",
|
|
530
|
+
scopeOptions: [],
|
|
531
|
+
};
|
|
532
|
+
mockCachedThreshold = "low";
|
|
533
|
+
mockRefreshedThreshold = "low"; // no change — still below the risk
|
|
534
|
+
const result = await check(
|
|
535
|
+
"bash",
|
|
536
|
+
{ command: "rm -rf /tmp/refresh-unchanged-test" },
|
|
537
|
+
"/home/user/project",
|
|
538
|
+
);
|
|
539
|
+
expect(result.decision).toBe("prompt");
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
test("keeps the prompt when the refreshed threshold is stricter", async () => {
|
|
543
|
+
mockIpcClassifyRiskResult = {
|
|
544
|
+
risk: "medium",
|
|
545
|
+
reason: "Network request (default)",
|
|
546
|
+
matchType: "registry",
|
|
547
|
+
scopeOptions: [],
|
|
548
|
+
};
|
|
549
|
+
mockCachedThreshold = "low";
|
|
550
|
+
mockRefreshedThreshold = "none"; // user tightened to Strict mid-flight
|
|
551
|
+
const result = await check(
|
|
552
|
+
"network_request",
|
|
553
|
+
{ url: "https://api.example.com/refresh-stricter-test" },
|
|
554
|
+
"/home/user/project",
|
|
555
|
+
);
|
|
556
|
+
expect(result.decision).toBe("prompt");
|
|
557
|
+
});
|
|
476
558
|
});
|
|
477
559
|
|
|
478
560
|
// ── generateAllowlistOptions ──────────────────────────────────────────────
|
|
@@ -28,7 +28,10 @@ import {
|
|
|
28
28
|
type ApprovalContext,
|
|
29
29
|
DefaultApprovalPolicy,
|
|
30
30
|
} from "./approval-policy.js";
|
|
31
|
-
import {
|
|
31
|
+
import {
|
|
32
|
+
getAutoApproveThreshold,
|
|
33
|
+
refreshAutoApproveThreshold,
|
|
34
|
+
} from "./gateway-threshold-reader.js";
|
|
32
35
|
import type { RiskAssessment } from "./risk-types.js";
|
|
33
36
|
import {
|
|
34
37
|
type AllowlistOption,
|
|
@@ -555,7 +558,27 @@ export async function check(
|
|
|
555
558
|
};
|
|
556
559
|
|
|
557
560
|
// Delegate the allow/prompt/deny decision to the approval policy
|
|
558
|
-
|
|
561
|
+
let approvalDecision = defaultApprovalPolicy.evaluate(approvalContext);
|
|
562
|
+
|
|
563
|
+
// A "prompt" computed from a cached threshold may contradict a setting
|
|
564
|
+
// the user just changed: the reader caches thresholds (5s conversation /
|
|
565
|
+
// 30s global TTL) and no threshold write path invalidates this process's
|
|
566
|
+
// caches. Re-read the threshold fresh before interrupting the user; when
|
|
567
|
+
// the current value differs, re-evaluate so e.g. Full access never
|
|
568
|
+
// prompts. A failed refresh returns null and keeps the prompt — fail
|
|
569
|
+
// toward asking, never toward silent approval.
|
|
570
|
+
if (approvalDecision.decision === "prompt") {
|
|
571
|
+
const freshThreshold = await refreshAutoApproveThreshold(
|
|
572
|
+
policyContext?.conversationId,
|
|
573
|
+
policyContext?.executionContext,
|
|
574
|
+
);
|
|
575
|
+
if (freshThreshold !== null && freshThreshold !== threshold) {
|
|
576
|
+
approvalDecision = defaultApprovalPolicy.evaluate({
|
|
577
|
+
...approvalContext,
|
|
578
|
+
autoApproveUpTo: freshThreshold,
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
}
|
|
559
582
|
|
|
560
583
|
// Enrich the reason with the classifier's explanation when available.
|
|
561
584
|
// For risk-based fallback decisions (prompt/deny from High/Medium risk),
|