@vellumai/assistant 0.9.1-staging.1 → 0.10.0-staging.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/activation-funnel-telemetry.md +24 -18
- package/node_modules/@vellumai/gateway-client/src/admission-policy-contract.ts +97 -0
- package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +10 -0
- package/node_modules/@vellumai/gateway-client/src/index.ts +15 -0
- package/openapi.yaml +852 -15
- package/package.json +1 -1
- package/src/__tests__/access-request-card-view.test.ts +98 -0
- package/src/__tests__/access-request-seed-content-blocks.test.ts +2 -4
- package/src/__tests__/actor-trust-resolver-address-fallback.test.ts +59 -7
- package/src/__tests__/agent-loop-compaction-strip.test.ts +17 -16
- package/src/__tests__/agent-loop-mutable-latest-user-message.test.ts +16 -13
- package/src/__tests__/app-compiler.test.ts +15 -1
- package/src/__tests__/auth-fallback-events-store.test.ts +6 -14
- package/src/__tests__/call-pointer-messages.test.ts +28 -0
- package/src/__tests__/cancel-clears-processing.test.ts +89 -0
- package/src/__tests__/channel-approval-routes.test.ts +0 -4
- package/src/__tests__/channel-inbound-disk-pressure.test.ts +5 -15
- package/src/__tests__/cli-memory-v2-reembed-skills.test.ts +3 -4
- package/src/__tests__/compactor-image-manifest-trust.test.ts +21 -1
- package/src/__tests__/compactor-summary-call-truncation.test.ts +223 -0
- package/src/__tests__/config-loader-backfill.test.ts +174 -30
- package/src/__tests__/config-schema.test.ts +35 -0
- package/src/__tests__/confirmation-request-guardian-bridge.test.ts +2 -2
- package/src/__tests__/contact-store-user-file.test.ts +0 -6
- package/src/__tests__/contacts-tools.test.ts +29 -0
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +1 -0
- package/src/__tests__/conversation-agent-loop.test.ts +58 -0
- package/src/__tests__/conversation-attention-telegram.test.ts +0 -1
- package/src/__tests__/conversation-lifecycle.test.ts +7 -9
- package/src/__tests__/conversation-load-history-repair.test.ts +101 -0
- package/src/__tests__/conversation-routes-guardian-reply.test.ts +15 -12
- package/src/__tests__/conversation-surfaces-activation-emit.test.ts +6 -3
- package/src/__tests__/conversation-title-service.test.ts +62 -0
- package/src/__tests__/credential-execution-shell-lockdown.test.ts +18 -11
- package/src/__tests__/credential-prompt-route.test.ts +1 -0
- package/src/__tests__/credential-security-invariants.test.ts +2 -0
- package/src/__tests__/disk-pressure-policy.test.ts +12 -0
- package/src/__tests__/disk-usage.test.ts +65 -0
- package/src/__tests__/dynamic-page-surface.test.ts +51 -0
- package/src/__tests__/gateway-flag-listener.test.ts +110 -1
- package/src/__tests__/guardian-binding-drift-heal.test.ts +1 -1
- package/src/__tests__/guardian-card-withdrawal.test.ts +403 -0
- package/src/__tests__/guardian-decision-primitive-canonical.test.ts +5 -3
- package/src/__tests__/guardian-grant-minting.test.ts +3 -35
- package/src/__tests__/guardian-routing-invariants.test.ts +64 -26
- package/src/__tests__/guardian-routing-state.test.ts +0 -1
- package/src/__tests__/headless-browser-mode.test.ts +10 -0
- package/src/__tests__/headless-browser-navigate.test.ts +8 -3
- package/src/__tests__/helpers/create-guardian-binding.ts +0 -1
- package/src/__tests__/host-browser-proxy.test.ts +87 -0
- package/src/__tests__/injector-v3-suppression.test.ts +27 -20
- package/src/__tests__/internal-telemetry-routes.test.ts +6 -14
- package/src/__tests__/invite-redemption-service.test.ts +0 -3
- package/src/__tests__/llm-catalog-parity.test.ts +30 -1
- package/src/__tests__/llm-resolver.test.ts +21 -0
- package/src/__tests__/llm-schema.test.ts +1 -0
- package/src/__tests__/managed-profile-guard.test.ts +163 -4
- package/src/__tests__/mcp-health-check.test.ts +6 -7
- package/src/__tests__/media-stream-server-integration.test.ts +317 -13
- package/src/__tests__/path-policy.test.ts +34 -0
- package/src/__tests__/persona-resolver.test.ts +38 -0
- package/src/__tests__/plugin-api-provider.test.ts +24 -0
- package/src/__tests__/plugin-tool-contribution.test.ts +6 -3
- package/src/__tests__/post-compaction-reinjection-idempotency.test.ts +214 -0
- package/src/__tests__/provider-send-message-override-profile.test.ts +76 -0
- package/src/__tests__/reaction-persistence.test.ts +150 -29
- package/src/__tests__/relay-server.test.ts +285 -0
- package/src/__tests__/runtime-attachment-metadata.test.ts +0 -1
- package/src/__tests__/scheduler-reuse-conversation.test.ts +8 -5
- package/src/__tests__/skill-execute-input.test.ts +5 -0
- package/src/__tests__/skills.test.ts +51 -0
- package/src/__tests__/slack-notification-approval-card.test.ts +176 -0
- package/src/__tests__/slack-reaction-canonical-approval.test.ts +285 -0
- package/src/__tests__/subagent-tools.test.ts +150 -0
- package/src/__tests__/task-progress-nudge-hook.test.ts +1 -1
- package/src/__tests__/title-generate-hook.test.ts +100 -3
- package/src/__tests__/tool-approval-seed-content-blocks.test.ts +1 -1
- package/src/__tests__/tool-audit-listener.test.ts +7 -7
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +6 -3
- package/src/__tests__/trusted-contact-approval-notifier.test.ts +4 -2
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +220 -3
- package/src/__tests__/trusted-contact-verification.test.ts +2 -4
- package/src/__tests__/twilio-routes.test.ts +81 -1
- package/src/__tests__/voice-invite-redemption.test.ts +0 -1
- package/src/__tests__/weak-open-model.test.ts +30 -0
- package/src/__tests__/workspace-migration-105-enable-memory-v3-live-for-new-workspaces.test.ts +149 -0
- package/src/__tests__/workspace-migration-108-drop-balanced-economy-profile.test.ts +285 -0
- package/src/__tests__/workspace-migration-add-send-diagnostics.test.ts +1 -1
- package/src/__tests__/workspace-migration-drop-collect-usage-data.test.ts +118 -0
- package/src/__tests__/workspace-migration-drop-send-diagnostics.test.ts +118 -0
- package/src/a2a/__tests__/e2e-a2a-channel.test.ts +0 -4
- package/src/agent/loop.ts +33 -33
- package/src/api/events/tool-result.ts +6 -0
- package/src/api/events/workflow-completed.ts +53 -0
- package/src/api/events/workflow-leaf-finished.ts +38 -0
- package/src/api/events/workflow-leaf-started.ts +35 -0
- package/src/api/events/workflow-progress.ts +32 -0
- package/src/api/events/workflow-started.ts +31 -0
- package/src/api/index.ts +40 -0
- package/src/api/responses/conversation-message.ts +26 -0
- package/src/api/responses/home.ts +26 -0
- package/src/api/responses/workflow-journal.ts +53 -0
- package/src/approvals/guardian-card-withdrawal.ts +145 -0
- package/src/approvals/guardian-decision-primitive.ts +26 -3
- package/src/approvals/guardian-request-resolvers.ts +181 -78
- package/src/calls/__tests__/channel-admission-reader.test.ts +132 -0
- package/src/calls/__tests__/relay-setup-router.test.ts +350 -0
- package/src/calls/call-pointer-messages.ts +10 -4
- package/src/calls/channel-admission-reader.ts +104 -0
- package/src/calls/guardian-dispatch.ts +17 -45
- package/src/calls/media-stream-server.ts +84 -2
- package/src/calls/relay-server.ts +66 -0
- package/src/calls/relay-setup-router.ts +82 -1
- package/src/calls/twilio-routes.ts +17 -8
- package/src/cli/commands/clients.ts +3 -0
- package/src/cli/commands/{__tests__ → memory/__tests__}/memory-v2-compare-render.test.ts +1 -1
- package/src/cli/commands/{__tests__ → memory/__tests__}/memory-v2.test.ts +8 -7
- package/src/cli/commands/{__tests__ → memory/__tests__}/memory-v3.test.ts +5 -4
- package/src/cli/commands/memory/index.ts +30 -0
- package/src/cli/commands/{memory-v2-compare-render.ts → memory/memory-v2-compare-render.ts} +1 -1
- package/src/cli/commands/{memory-v2.ts → memory/memory-v2.ts} +6 -15
- package/src/cli/commands/{memory-v3.ts → memory/memory-v3.ts} +97 -11
- package/src/cli/commands/oauth/status.test.ts +36 -0
- package/src/cli/commands/oauth/status.ts +23 -3
- package/src/cli/commands/plugins.ts +57 -5
- package/src/cli/lib/__tests__/inspect-plugin.test.ts +54 -0
- package/src/cli/lib/__tests__/merge-plugin-tree.test.ts +134 -4
- package/src/cli/lib/__tests__/plugin-surfaces.test.ts +111 -0
- package/src/cli/lib/__tests__/upgrade-plugin.test.ts +53 -11
- package/src/cli/lib/inspect-plugin.ts +12 -1
- package/src/cli/lib/merge-plugin-tree.ts +149 -49
- package/src/cli/lib/plugin-surfaces.ts +104 -0
- package/src/cli/lib/upgrade-plugin.ts +64 -36
- package/src/cli/program.ts +2 -4
- package/src/config/__tests__/sync-gated-profiles.test.ts +368 -0
- package/src/config/assistant-feature-flags.ts +22 -7
- package/src/config/bundled-skills/contacts/tools/contact-search.ts +0 -1
- package/src/config/bundled-skills/messaging/SKILL.md +6 -4
- package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +9 -8
- package/src/config/bundled-skills/workflows/SKILL.md +14 -7
- package/src/config/call-site-defaults.ts +3 -0
- package/src/config/feature-flag-registry.json +49 -18
- package/src/config/llm-resolver.ts +3 -0
- package/src/config/memory-v3-gate.ts +11 -0
- package/src/config/schema.ts +8 -6
- package/src/config/schemas/__tests__/memory-v3.test.ts +1 -0
- package/src/config/schemas/call-site-catalog.ts +7 -0
- package/src/config/schemas/channels.ts +11 -0
- package/src/config/schemas/llm.ts +31 -0
- package/src/config/schemas/memory-lifecycle.ts +3 -7
- package/src/config/schemas/memory-v3.ts +6 -0
- package/src/config/schemas/services.ts +18 -0
- package/src/config/seed-inference-profiles.ts +94 -34
- package/src/config/skills.ts +21 -0
- package/src/config/sync-gated-profiles.ts +220 -0
- package/src/contacts/contact-store.ts +2 -10
- package/src/contacts/contacts-write.ts +1 -2
- package/src/contacts/types.ts +0 -1
- package/src/context/compactor.ts +86 -52
- package/src/context/strip-injections.ts +58 -10
- package/src/context/token-estimator.ts +1 -1
- package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +3 -2
- package/src/daemon/conversation-agent-loop-handlers.ts +2 -0
- package/src/daemon/conversation-agent-loop.ts +100 -19
- package/src/daemon/conversation-history.ts +1 -1
- package/src/daemon/conversation-lifecycle.ts +3 -5
- package/src/daemon/conversation-process.ts +13 -5
- package/src/daemon/conversation-runtime-assembly.ts +13 -15
- package/src/daemon/conversation-surfaces.ts +26 -0
- package/src/daemon/conversation-tool-setup.ts +16 -11
- package/src/daemon/conversation.ts +64 -14
- package/src/daemon/disk-pressure-policy.ts +5 -3
- package/src/daemon/handlers/__tests__/config-a2a-complete.test.ts +0 -1
- package/src/daemon/handlers/__tests__/config-a2a-redeem.test.ts +0 -1
- package/src/daemon/handlers/config-a2a.ts +0 -2
- package/src/daemon/handlers/config-channels.ts +5 -10
- package/src/daemon/handlers/config-slack-channel.ts +20 -0
- package/src/daemon/handlers/conversations.ts +107 -0
- package/src/daemon/host-browser-proxy.ts +41 -0
- package/src/daemon/lifecycle.ts +55 -20
- package/src/daemon/message-provenance.ts +2 -0
- package/src/daemon/message-types/contacts.ts +0 -1
- package/src/daemon/message-types/web-activity.ts +7 -1
- package/src/daemon/message-types/workflows.ts +83 -1
- package/src/daemon/tool-setup-types.ts +4 -0
- package/src/daemon/trust-context.ts +1 -1
- package/src/events/tool-audit-listener.ts +2 -2
- package/src/home/feed-source-enrichment.test.ts +151 -0
- package/src/home/feed-source-enrichment.ts +176 -0
- package/src/instrument.ts +18 -6
- package/src/ipc/__tests__/binary-result-ipc.test.ts +81 -0
- package/src/ipc/__tests__/clients-list-ipc.test.ts +20 -0
- package/src/ipc/assistant-server.ts +37 -4
- package/src/ipc/gateway-flag-listener.ts +18 -2
- package/src/memory/__tests__/auto-analysis-enqueue.test.ts +5 -16
- package/src/memory/__tests__/jobs-store-enqueue-gate.test.ts +7 -11
- package/src/memory/__tests__/memory-retrospective-enqueue.test.ts +37 -7
- package/src/memory/__tests__/memory-retrospective-job.test.ts +34 -0
- package/src/memory/__tests__/onboarding-events-store.test.ts +7 -7
- package/src/memory/auth-fallback-events-store.ts +2 -2
- package/src/memory/auto-analysis-enqueue.ts +3 -5
- package/src/memory/canonical-guardian-store.ts +39 -1
- package/src/memory/conversation-crud.ts +9 -4
- package/src/memory/conversation-key-store.ts +17 -2
- package/src/memory/conversation-title-service.ts +64 -7
- package/src/memory/db-init.ts +10 -0
- package/src/memory/embedding-backend.ts +15 -1
- package/src/memory/jobs-worker.ts +2 -1
- package/src/memory/lifecycle-events-store.ts +2 -2
- package/src/memory/memory-retrospective-enqueue.ts +31 -6
- package/src/memory/memory-retrospective-job.ts +9 -0
- package/src/memory/migrations/129-contact-channels-access-fields.ts +18 -9
- package/src/memory/migrations/131-drop-legacy-member-guardian-tables.ts +14 -2
- package/src/memory/migrations/289-contact-channels-unique-ext-user.ts +10 -0
- package/src/memory/migrations/291-contact-channels-renormalize-addresses.ts +10 -0
- package/src/memory/migrations/292-schedule-default-no-reuse-conversation.test.ts +67 -0
- package/src/memory/migrations/292-schedule-default-no-reuse-conversation.ts +25 -0
- package/src/memory/migrations/293-workflow-journal-leaf-tokens.ts +32 -0
- package/src/memory/migrations/294-drop-external-user-id.ts +31 -0
- package/src/memory/migrations/295-drop-approval-prompt-ts-tracker.ts +20 -0
- package/src/memory/migrations/296-rewrite-balanced-economy-profile-pins.test.ts +110 -0
- package/src/memory/migrations/296-rewrite-balanced-economy-profile-pins.ts +68 -0
- package/src/memory/migrations/__tests__/131-drop-legacy-member-guardian-tables.test.ts +154 -0
- package/src/memory/migrations/__tests__/289-contact-channels-unique-ext-user.test.ts +31 -0
- package/src/memory/migrations/__tests__/291-contact-channels-renormalize-addresses.test.ts +30 -0
- package/src/memory/migrations/index.ts +5 -0
- package/src/memory/onboarding-events-store.ts +3 -3
- package/src/memory/schema/contacts.ts +0 -1
- package/src/memory/skill-loaded-events-store.test.ts +7 -15
- package/src/memory/skill-loaded-events-store.ts +2 -2
- package/src/memory/tool-executed-events-store.test.ts +7 -7
- package/src/memory/turn-trace-store.test.ts +736 -0
- package/src/memory/turn-trace-store.ts +364 -0
- package/src/memory/v2/__tests__/consolidation-job.test.ts +8 -0
- package/src/memory/v2/__tests__/skill-content.test.ts +30 -0
- package/src/memory/v2/consolidation-job.ts +2 -2
- package/src/memory/v2/skill-content.ts +25 -7
- package/src/memory/v2/skill-store.ts +7 -1
- package/src/memory/v3-eval/__tests__/eval-packets.test.ts +248 -0
- package/src/memory/v3-eval/eval-packets.ts +546 -0
- package/src/messaging/providers/slack/api.ts +31 -0
- package/src/messaging/providers/slack/send.test.ts +114 -2
- package/src/messaging/providers/slack/send.ts +30 -7
- package/src/messaging/providers/slack/withdraw.test.ts +200 -0
- package/src/messaging/providers/slack/withdraw.ts +161 -0
- package/src/notifications/AGENTS.md +2 -0
- package/src/notifications/access-request-copy.ts +72 -59
- package/src/notifications/adapters/slack.ts +55 -73
- package/src/notifications/approval-card-data.ts +333 -0
- package/src/notifications/broadcaster.ts +6 -2
- package/src/notifications/canonical-delivery-recorder.ts +139 -0
- package/src/notifications/copy-composer.ts +3 -3
- package/src/notifications/decision-engine.ts +4 -2
- package/src/notifications/destination-resolver.ts +4 -6
- package/src/notifications/guardian-question-mode.ts +10 -0
- package/src/notifications/home-feed-side-effect.ts +3 -13
- package/src/notifications/notification-utils.ts +2 -1
- package/src/notifications/signal.ts +79 -43
- package/src/notifications/types.ts +98 -128
- package/src/permissions/checker.test.ts +51 -0
- package/src/permissions/checker.ts +185 -26
- package/src/permissions/ipc-risk-types.ts +24 -0
- package/src/permissions/question-prompter.test.ts +27 -0
- package/src/permissions/question-prompter.ts +4 -0
- package/src/platform/client.test.ts +119 -0
- package/src/platform/client.ts +66 -0
- package/src/platform/consent-cache.test.ts +267 -0
- package/src/platform/consent-cache.ts +174 -0
- package/src/plugin-api/index.ts +27 -0
- package/src/plugins/defaults/advisor/__tests__/advisor-gate.test.ts +56 -0
- package/src/plugins/defaults/advisor/__tests__/advisor-state-store.test.ts +43 -0
- package/src/plugins/defaults/advisor/__tests__/agent-loop-integration.test.ts +137 -0
- package/src/plugins/defaults/advisor/__tests__/consult.test.ts +153 -0
- package/src/plugins/defaults/advisor/__tests__/hooks.test.ts +138 -0
- package/src/plugins/defaults/advisor/__tests__/transcript.test.ts +147 -0
- package/src/plugins/defaults/advisor/advisor-gate.ts +29 -0
- package/src/plugins/defaults/advisor/advisor-state-store.ts +94 -0
- package/src/plugins/defaults/advisor/config.ts +21 -0
- package/src/plugins/defaults/advisor/consult.ts +93 -0
- package/src/plugins/defaults/advisor/hooks/post-model-call.ts +34 -0
- package/src/plugins/defaults/advisor/hooks/pre-model-call.ts +30 -0
- package/src/plugins/defaults/advisor/hooks/user-prompt-submit.ts +19 -0
- package/src/plugins/defaults/advisor/package.json +14 -0
- package/src/plugins/defaults/advisor/steering.ts +67 -0
- package/src/plugins/defaults/advisor/tools/advisor.ts +65 -0
- package/src/plugins/defaults/advisor/transcript.ts +76 -0
- package/src/plugins/defaults/index.ts +35 -0
- package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +22 -9
- package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit.ts +2 -2
- package/src/plugins/defaults/memory-retrieval/tail-reinjection-strip.ts +64 -0
- package/src/plugins/defaults/memory-retrieval/unified-turn-context.ts +29 -21
- package/src/plugins/defaults/memory-v3-shadow/__tests__/carry-integration.test.ts +1 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/injection.test.ts +1 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/maintain-job.test.ts +75 -7
- package/src/plugins/defaults/memory-v3-shadow/__tests__/orchestrate.test.ts +31 -4
- package/src/plugins/defaults/memory-v3-shadow/__tests__/selection-log-store.test.ts +77 -2
- package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +1 -0
- package/src/plugins/defaults/memory-v3-shadow/injector.ts +7 -10
- package/src/plugins/defaults/memory-v3-shadow/maintain-job.ts +37 -4
- package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +32 -20
- package/src/plugins/defaults/memory-v3-shadow/selection-log-store.ts +56 -3
- package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +23 -2
- package/src/plugins/defaults/task-progress-nudge/hooks/post-tool-use.ts +2 -12
- package/src/plugins/defaults/title-generate/hooks/stop.ts +56 -21
- package/src/prompts/persona-resolver.ts +12 -2
- package/src/prompts/templates/system-sections.ts +7 -2
- package/src/providers/__tests__/provider-env-vars.test.ts +6 -0
- package/src/providers/__tests__/provider-secret-catalog.test.ts +1 -0
- package/src/providers/__tests__/retry-callsite.test.ts +176 -0
- package/src/providers/atlascloud/client.ts +85 -0
- package/src/providers/fetch-provider-catalog.ts +85 -0
- package/src/providers/inference/adapter-factory.ts +3 -0
- package/src/providers/model-catalog.ts +58 -0
- package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +33 -0
- package/src/providers/openai/chat-completions-provider.ts +7 -0
- package/src/providers/openai/responses-provider.ts +10 -0
- package/src/providers/provider-send-message.ts +11 -3
- package/src/providers/retry.ts +53 -12
- package/src/providers/search-provider-catalog.ts +10 -0
- package/src/providers/weak-open-model.ts +22 -0
- package/src/runtime/__tests__/agent-wake.test.ts +181 -0
- package/src/runtime/__tests__/client-health.test.ts +44 -0
- package/src/runtime/access-request-helper.ts +21 -53
- package/src/runtime/actor-trust-resolver.ts +49 -21
- package/src/runtime/agent-wake.ts +52 -0
- package/src/runtime/assistant-event-hub.ts +18 -4
- package/src/runtime/auth/__tests__/route-policy.test.ts +12 -0
- package/src/runtime/auth/require-bound-guardian.ts +1 -4
- package/src/runtime/capabilities.test.ts +120 -0
- package/src/runtime/capabilities.ts +197 -0
- package/src/runtime/channel-approval-types.ts +5 -1
- package/src/runtime/channel-retry-sweep.ts +1 -0
- package/src/runtime/channel-verification-service.ts +1 -2
- package/src/runtime/client-health.ts +26 -0
- package/src/runtime/confirmation-request-guardian-bridge.ts +38 -29
- package/src/runtime/effective-capabilities.test.ts +128 -0
- package/src/runtime/effective-capabilities.ts +84 -0
- package/src/runtime/guardian-reply-router.ts +106 -21
- package/src/runtime/invite-redemption-service.ts +6 -22
- package/src/runtime/migrations/__tests__/vbundle-builder-fd-leak.test.ts +123 -0
- package/src/runtime/migrations/vbundle-builder.ts +49 -20
- package/src/runtime/pending-interactions.ts +15 -0
- package/src/runtime/routes/__tests__/client-routes.test.ts +13 -0
- package/src/runtime/routes/__tests__/conversation-management-routes.test.ts +67 -0
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +35 -13
- package/src/runtime/routes/browser-tabs-routes.ts +9 -0
- package/src/runtime/routes/canonical-guardian-expiry-sweep.ts +17 -8
- package/src/runtime/routes/client-routes.ts +10 -0
- package/src/runtime/routes/contact-routes.ts +31 -8
- package/src/runtime/routes/conversation-management-routes.ts +80 -1
- package/src/runtime/routes/conversation-query-routes.ts +68 -22
- package/src/runtime/routes/conversation-routes.ts +37 -12
- package/src/runtime/routes/events-routes.ts +1 -3
- package/src/runtime/routes/guardian-approval-interception.ts +14 -73
- package/src/runtime/routes/guardian-approval-prompt.ts +22 -4
- package/src/runtime/routes/home-feed-routes.ts +8 -3
- package/src/runtime/routes/inbound-message-handler.ts +214 -228
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +88 -6
- package/src/runtime/routes/inbound-stages/admission-policy.test.ts +154 -0
- package/src/runtime/routes/inbound-stages/admission-policy.ts +140 -0
- package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +3 -3
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +11 -6
- package/src/runtime/routes/inbound-stages/escalation-intercept.ts +1 -2
- package/src/runtime/routes/inbound-stages/guardian-activation-intercept.ts +1 -2
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.test.ts +7 -7
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +47 -28
- package/src/runtime/routes/inbound-stages/reaction-intercept.ts +358 -0
- package/src/runtime/routes/index.ts +2 -0
- package/src/runtime/routes/integrations/slack/__tests__/channel.test.ts +8 -0
- package/src/runtime/routes/integrations/slack/channel.ts +36 -0
- package/src/runtime/routes/internal-telemetry-routes.ts +1 -1
- package/src/runtime/routes/mcp-auth-routes.ts +233 -41
- package/src/runtime/routes/memory-eval-routes.ts +87 -0
- package/src/runtime/routes/notification-routes.ts +122 -133
- package/src/runtime/routes/platform-routes.ts +2 -2
- package/src/runtime/routes/plugins-routes.ts +40 -7
- package/src/runtime/routes/secret-routes.ts +10 -0
- package/src/runtime/routes/surface-action-routes.ts +2 -1
- package/src/runtime/routes/tool-call-question-enrichment.test.ts +146 -0
- package/src/runtime/routes/tool-call-question-enrichment.ts +66 -0
- package/src/runtime/routes/workflow-routes.test.ts +225 -1
- package/src/runtime/routes/workflow-routes.ts +131 -1
- package/src/runtime/tool-grant-request-helper.ts +18 -16
- package/src/runtime/trust-context-resolver.ts +8 -5
- package/src/schedule/schedule-store.ts +1 -1
- package/src/schedule/scheduler-types.ts +5 -1
- package/src/security/__tests__/provider-key-env-fallback.test.ts +6 -0
- package/src/security/secret-patterns.ts +3 -0
- package/src/subagent/manager.ts +11 -4
- package/src/telemetry/trace-collection-policy.test.ts +28 -0
- package/src/telemetry/trace-collection-policy.ts +30 -0
- package/src/telemetry/types.ts +89 -0
- package/src/telemetry/usage-telemetry-reporter.test.ts +586 -36
- package/src/telemetry/usage-telemetry-reporter.ts +148 -41
- package/src/tools/browser/__tests__/browser-execution-acquire.test.ts +31 -0
- package/src/tools/browser/browser-execution.ts +29 -18
- package/src/tools/document/document-tool.ts +2 -3
- package/src/tools/executor.ts +5 -3
- package/src/tools/host-terminal/host-shell.ts +5 -4
- package/src/tools/memory/register.ts +2 -2
- package/src/tools/network/__tests__/web-fetch-firecrawl.test.ts +360 -0
- package/src/tools/network/__tests__/web-search.test.ts +143 -0
- package/src/tools/network/web-fetch.ts +372 -1
- package/src/tools/network/web-search.ts +213 -10
- package/src/tools/permission-checker.ts +3 -2
- package/src/tools/registry.ts +20 -0
- package/src/tools/schedule/create.ts +4 -3
- package/src/tools/schedule/update.ts +2 -1
- package/src/tools/shared/filesystem/path-policy.ts +39 -13
- package/src/tools/skills/execute.ts +1 -2
- package/src/tools/subagent/spawn.ts +37 -13
- package/src/tools/terminal/shell.ts +10 -4
- package/src/tools/tool-approval-handler.ts +17 -10
- package/src/tools/types.ts +9 -0
- package/src/tools/ui-surface/definitions.ts +25 -2
- package/src/tools/verification-control-plane-policy.ts +3 -1
- package/src/tools/workflows/run-workflow.ts +1 -0
- package/src/util/disk-usage.ts +78 -23
- package/src/util/platform.ts +8 -1
- package/src/watcher/telemetry.ts +2 -2
- package/src/workflows/engine.test.ts +175 -1
- package/src/workflows/engine.ts +82 -0
- package/src/workflows/journal-store.test.ts +70 -0
- package/src/workflows/journal-store.ts +18 -3
- package/src/workflows/run-manager.test.ts +171 -3
- package/src/workflows/run-manager.ts +64 -0
- package/src/workspace/migrations/105-enable-memory-v3-live-for-new-workspaces.ts +63 -0
- package/src/workspace/migrations/106-drop-collect-usage-data.ts +47 -0
- package/src/workspace/migrations/107-drop-send-diagnostics.ts +47 -0
- package/src/workspace/migrations/108-drop-balanced-economy-profile.ts +129 -0
- package/src/workspace/migrations/registry.ts +8 -0
- package/src/notifications/tool-approval-copy.ts +0 -142
- package/src/runtime/routes/approval-prompt-ts-tracker.ts +0 -78
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-surface withdrawal of guardian approval cards.
|
|
3
|
+
*
|
|
4
|
+
* A single guardian request is projected onto every surface it was delivered
|
|
5
|
+
* to — the in-app Vellum card, a Slack message, etc. When the request reaches a
|
|
6
|
+
* terminal status its cards must stop offering live actions on *all* surfaces,
|
|
7
|
+
* not just the one the guardian acted on. This is the withdrawal counterpart to
|
|
8
|
+
* the unified card *rendering* dispatcher (`notifications/approval-card-data.ts`):
|
|
9
|
+
* rendering projects `pending` onto each surface, withdrawal projects the
|
|
10
|
+
* terminal status back onto each surface.
|
|
11
|
+
*
|
|
12
|
+
* Withdrawal preserves the card's information and removes only the live
|
|
13
|
+
* affordances (it does not delete the message/card) so each surface keeps an
|
|
14
|
+
* audit trail of what was decided.
|
|
15
|
+
*
|
|
16
|
+
* It is driven off `canonical_guardian_deliveries` — the per-request registry of
|
|
17
|
+
* where each card was sent — so it stays correct as surfaces are added and is
|
|
18
|
+
* agnostic to which surface originated the decision.
|
|
19
|
+
*
|
|
20
|
+
* Best-effort by contract: the request is already resolved (CAS committed)
|
|
21
|
+
* before this runs, so a failed edit must never surface as a decision failure.
|
|
22
|
+
* Every surface is attempted independently; one failure never blocks the rest.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { completeSurfaceAndNotify } from "../daemon/conversation-surfaces.js";
|
|
26
|
+
import {
|
|
27
|
+
type CanonicalGuardianDelivery,
|
|
28
|
+
type CanonicalGuardianRequest,
|
|
29
|
+
type CanonicalRequestStatus,
|
|
30
|
+
listCanonicalGuardianDeliveries,
|
|
31
|
+
} from "../memory/canonical-guardian-store.js";
|
|
32
|
+
import { withdrawSlackApprovalCard } from "../messaging/providers/slack/withdraw.js";
|
|
33
|
+
import { approvalCardSurfaceId } from "../notifications/approval-card-data.js";
|
|
34
|
+
import { getLogger } from "../util/logger.js";
|
|
35
|
+
|
|
36
|
+
const log = getLogger("guardian-card-withdrawal");
|
|
37
|
+
|
|
38
|
+
/** Completion-summary label shown on an in-app card for a resolved request. */
|
|
39
|
+
const SURFACE_STATUS_LABELS: Partial<Record<CanonicalRequestStatus, string>> = {
|
|
40
|
+
approved: "Approved",
|
|
41
|
+
denied: "Denied",
|
|
42
|
+
expired: "Expired",
|
|
43
|
+
cancelled: "Cancelled",
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export interface WithdrawGuardianCardsParams {
|
|
47
|
+
/** The request, already transitioned to its terminal status. */
|
|
48
|
+
request: CanonicalGuardianRequest;
|
|
49
|
+
/** Terminal status to reflect on each card. */
|
|
50
|
+
status: CanonicalRequestStatus;
|
|
51
|
+
/**
|
|
52
|
+
* Channel the decision originated on, when applicable.
|
|
53
|
+
*
|
|
54
|
+
* The acting in-app client completes its own card optimistically, so the
|
|
55
|
+
* in-app card is skipped when the decision originated in-app; it is withdrawn
|
|
56
|
+
* here only when the decision came from another surface. Omit (e.g. the expiry
|
|
57
|
+
* sweep) to withdraw every surface.
|
|
58
|
+
*/
|
|
59
|
+
originChannel?: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Withdraw a resolved request's approval cards across all delivery surfaces.
|
|
64
|
+
* Never throws.
|
|
65
|
+
*/
|
|
66
|
+
export async function withdrawGuardianRequestCards(
|
|
67
|
+
params: WithdrawGuardianCardsParams,
|
|
68
|
+
): Promise<void> {
|
|
69
|
+
const { request, status, originChannel } = params;
|
|
70
|
+
|
|
71
|
+
let deliveries: CanonicalGuardianDelivery[];
|
|
72
|
+
try {
|
|
73
|
+
deliveries = listCanonicalGuardianDeliveries(request.id);
|
|
74
|
+
} catch (err) {
|
|
75
|
+
log.warn(
|
|
76
|
+
{ err, requestId: request.id },
|
|
77
|
+
"Failed to list deliveries for card withdrawal",
|
|
78
|
+
);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
for (const delivery of deliveries) {
|
|
83
|
+
try {
|
|
84
|
+
if (delivery.destinationChannel === "vellum") {
|
|
85
|
+
withdrawVellumCard(request, delivery, status, originChannel);
|
|
86
|
+
} else if (delivery.destinationChannel === "slack") {
|
|
87
|
+
await withdrawSlackCard(request, delivery, status);
|
|
88
|
+
}
|
|
89
|
+
// Telegram/WhatsApp direct delivery can't edit a message in place (it
|
|
90
|
+
// would post a new one), so their stale clicks are left to the existing
|
|
91
|
+
// "already resolved" reply until in-place edit support lands.
|
|
92
|
+
} catch (err) {
|
|
93
|
+
log.warn(
|
|
94
|
+
{
|
|
95
|
+
err,
|
|
96
|
+
requestId: request.id,
|
|
97
|
+
channel: delivery.destinationChannel,
|
|
98
|
+
},
|
|
99
|
+
"Failed to withdraw guardian card on surface (non-fatal)",
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Withdraw the in-app approval card so it stops offering live actions while
|
|
107
|
+
* keeping its content. Skipped when the decision originated in-app — the acting
|
|
108
|
+
* client already completed the card itself.
|
|
109
|
+
*/
|
|
110
|
+
function withdrawVellumCard(
|
|
111
|
+
request: CanonicalGuardianRequest,
|
|
112
|
+
delivery: CanonicalGuardianDelivery,
|
|
113
|
+
status: CanonicalRequestStatus,
|
|
114
|
+
originChannel: string | undefined,
|
|
115
|
+
): void {
|
|
116
|
+
if (originChannel === "vellum") return;
|
|
117
|
+
if (!delivery.destinationConversationId) return;
|
|
118
|
+
const surfaceId = approvalCardSurfaceId(request.kind, request.id);
|
|
119
|
+
if (!surfaceId) return;
|
|
120
|
+
completeSurfaceAndNotify(
|
|
121
|
+
delivery.destinationConversationId,
|
|
122
|
+
surfaceId,
|
|
123
|
+
SURFACE_STATUS_LABELS[status] ?? "Resolved",
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Edit the Slack message in place to its resolved state — original card content
|
|
129
|
+
* preserved, action buttons removed, an outcome/decider/time line appended.
|
|
130
|
+
* No-ops when the channel-native message id was not captured at delivery time.
|
|
131
|
+
*/
|
|
132
|
+
async function withdrawSlackCard(
|
|
133
|
+
request: CanonicalGuardianRequest,
|
|
134
|
+
delivery: CanonicalGuardianDelivery,
|
|
135
|
+
status: CanonicalRequestStatus,
|
|
136
|
+
): Promise<void> {
|
|
137
|
+
if (!delivery.destinationChatId || !delivery.destinationMessageId) return;
|
|
138
|
+
await withdrawSlackApprovalCard({
|
|
139
|
+
channel: delivery.destinationChatId,
|
|
140
|
+
messageTs: delivery.destinationMessageId,
|
|
141
|
+
status,
|
|
142
|
+
decidedByExternalUserId: request.decidedByExternalUserId ?? undefined,
|
|
143
|
+
decidedAtMs: request.updatedAt,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
@@ -48,6 +48,7 @@ import type { ApplyGuardianDecisionResult } from "../runtime/guardian-decision-t
|
|
|
48
48
|
import { computeToolApprovalDigest } from "../security/tool-approval-digest.js";
|
|
49
49
|
import { getLogger } from "../util/logger.js";
|
|
50
50
|
import { mintGrantFromDecision } from "./approval-primitive.js";
|
|
51
|
+
import { withdrawGuardianRequestCards } from "./guardian-card-withdrawal.js";
|
|
51
52
|
import {
|
|
52
53
|
type ActorContext,
|
|
53
54
|
type ChannelDeliveryContext,
|
|
@@ -354,6 +355,7 @@ export type CanonicalDecisionResult =
|
|
|
354
355
|
| "not_found"
|
|
355
356
|
| "already_resolved"
|
|
356
357
|
| "identity_mismatch"
|
|
358
|
+
| "request_misconfigured"
|
|
357
359
|
| "invalid_action"
|
|
358
360
|
| "expired";
|
|
359
361
|
detail?: string;
|
|
@@ -426,18 +428,22 @@ export async function applyCanonicalGuardianDecision(
|
|
|
426
428
|
// authorization gate — principal identity must always match.
|
|
427
429
|
|
|
428
430
|
if (!request.guardianPrincipalId) {
|
|
429
|
-
|
|
431
|
+
// A decisionable request with no bound principal can never be authorized
|
|
432
|
+
// by anyone — it is stuck. This is a data-integrity fault (creation guards
|
|
433
|
+
// against it for decisionable kinds), not an authorization denial, so it
|
|
434
|
+
// must NOT be reported to the actor as "you don't have permission".
|
|
435
|
+
log.error(
|
|
430
436
|
{
|
|
431
437
|
event: "canonical_decision_missing_request_principal",
|
|
432
438
|
requestId,
|
|
433
439
|
kind: request.kind,
|
|
434
440
|
sourceType: request.sourceType,
|
|
435
441
|
},
|
|
436
|
-
"Canonical request missing guardianPrincipalId;
|
|
442
|
+
"Canonical request missing guardianPrincipalId; request is undecidable",
|
|
437
443
|
);
|
|
438
444
|
return {
|
|
439
445
|
applied: false,
|
|
440
|
-
reason: "
|
|
446
|
+
reason: "request_misconfigured",
|
|
441
447
|
detail: "request missing guardianPrincipalId",
|
|
442
448
|
};
|
|
443
449
|
}
|
|
@@ -571,6 +577,23 @@ export async function applyCanonicalGuardianDecision(
|
|
|
571
577
|
grantMinted = grantResult.minted;
|
|
572
578
|
}
|
|
573
579
|
|
|
580
|
+
// 6. Project the terminal status onto the request's approval cards on every
|
|
581
|
+
// surface it was delivered to (in-app, Slack, ...). Fire-and-forget: the
|
|
582
|
+
// decision is already committed via CAS and withdrawal is a best-effort
|
|
583
|
+
// cosmetic projection, so awaiting its Slack round-trips would only add
|
|
584
|
+
// latency to the decision response that interactive callers wait on. The
|
|
585
|
+
// projector never throws; the `.catch` is a defensive backstop.
|
|
586
|
+
void withdrawGuardianRequestCards({
|
|
587
|
+
request: resolved,
|
|
588
|
+
status: targetStatus,
|
|
589
|
+
originChannel: actorContext.channel,
|
|
590
|
+
}).catch((err) => {
|
|
591
|
+
log.warn(
|
|
592
|
+
{ err, requestId },
|
|
593
|
+
"Cross-surface card withdrawal failed (non-fatal)",
|
|
594
|
+
);
|
|
595
|
+
});
|
|
596
|
+
|
|
574
597
|
log.info(
|
|
575
598
|
{
|
|
576
599
|
event: "canonical_decision_applied",
|
|
@@ -55,6 +55,87 @@ function shouldUseEphemeral(sourceChannel: string, chatId: string): boolean {
|
|
|
55
55
|
return sourceChannel === "slack" && chatId.startsWith("C");
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
/**
|
|
59
|
+
* Deliver the verification code straight to the requester's Slack DM so the
|
|
60
|
+
* guardian is never an out-of-band courier for the secret.
|
|
61
|
+
*
|
|
62
|
+
* Slack is the only channel with a guaranteed private path to the requester:
|
|
63
|
+
* posting to their user ID (`U…`) opens a 1:1 DM. The `threadTs` query param is
|
|
64
|
+
* dropped because it points at the guardian's channel thread and would raise
|
|
65
|
+
* `thread_not_found` in the DM. The code is sent as a durable (non-ephemeral)
|
|
66
|
+
* message the requester can refer back to when verifying.
|
|
67
|
+
*
|
|
68
|
+
* The verification session is identity-bound to the requester, so delivering
|
|
69
|
+
* the code to them directly does not widen who can consume it — it only removes
|
|
70
|
+
* the guardian relay step.
|
|
71
|
+
*
|
|
72
|
+
* Returns whether the code was delivered.
|
|
73
|
+
*/
|
|
74
|
+
async function deliverVerificationCodeToSlackRequester(params: {
|
|
75
|
+
replyCallbackUrl: string;
|
|
76
|
+
requesterExternalUserId: string;
|
|
77
|
+
verificationCode: string;
|
|
78
|
+
assistantId: string;
|
|
79
|
+
}): Promise<boolean> {
|
|
80
|
+
let callbackUrl = params.replyCallbackUrl;
|
|
81
|
+
try {
|
|
82
|
+
const url = new URL(params.replyCallbackUrl);
|
|
83
|
+
url.searchParams.delete("threadTs");
|
|
84
|
+
callbackUrl = url.toString();
|
|
85
|
+
} catch {
|
|
86
|
+
// Relative path (e.g. the desktop "/deliver/slack" target) — use as-is;
|
|
87
|
+
// it carries no threadTs to strip.
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
await deliverChannelReply(callbackUrl, {
|
|
92
|
+
chatId: params.requesterExternalUserId,
|
|
93
|
+
text:
|
|
94
|
+
"Great news — your access request was approved! " +
|
|
95
|
+
`Your verification code is: \`${params.verificationCode}\`. ` +
|
|
96
|
+
"Reply with it here to complete verification. The code expires in 10 minutes.",
|
|
97
|
+
assistantId: params.assistantId,
|
|
98
|
+
});
|
|
99
|
+
return true;
|
|
100
|
+
} catch (err) {
|
|
101
|
+
log.error(
|
|
102
|
+
{ err, requesterExternalUserId: params.requesterExternalUserId },
|
|
103
|
+
"Failed to auto-deliver verification code to Slack requester",
|
|
104
|
+
);
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Build a requester-facing channel notice (approval/denial/courier text).
|
|
111
|
+
*
|
|
112
|
+
* Posts to the originating chat. On a Slack shared channel it goes out as an
|
|
113
|
+
* ephemeral message visible only to the requester — and because
|
|
114
|
+
* `chat.postEphemeral` needs a channel ID, `chatId` stays the channel
|
|
115
|
+
* (`requesterChatId`), never the requester's `U…` user ID.
|
|
116
|
+
*/
|
|
117
|
+
function buildRequesterChannelNotice(params: {
|
|
118
|
+
channel: string;
|
|
119
|
+
requesterChatId: string;
|
|
120
|
+
requesterExternalUserId: string;
|
|
121
|
+
text: string;
|
|
122
|
+
assistantId: string;
|
|
123
|
+
}): Parameters<typeof deliverChannelReply>[1] {
|
|
124
|
+
const payload: Parameters<typeof deliverChannelReply>[1] = {
|
|
125
|
+
chatId: params.requesterChatId,
|
|
126
|
+
text: params.text,
|
|
127
|
+
assistantId: params.assistantId,
|
|
128
|
+
};
|
|
129
|
+
if (
|
|
130
|
+
shouldUseEphemeral(params.channel, params.requesterChatId) &&
|
|
131
|
+
params.requesterExternalUserId
|
|
132
|
+
) {
|
|
133
|
+
payload.ephemeral = true;
|
|
134
|
+
payload.user = params.requesterExternalUserId;
|
|
135
|
+
}
|
|
136
|
+
return payload;
|
|
137
|
+
}
|
|
138
|
+
|
|
58
139
|
// ---------------------------------------------------------------------------
|
|
59
140
|
// Types
|
|
60
141
|
// ---------------------------------------------------------------------------
|
|
@@ -363,8 +444,6 @@ const accessRequestResolver: GuardianRequestResolver = {
|
|
|
363
444
|
const requesterExternalUserId = request.requesterExternalUserId ?? "";
|
|
364
445
|
const requesterChatId =
|
|
365
446
|
request.requesterChatId ?? request.requesterExternalUserId ?? "";
|
|
366
|
-
const requesterLabel =
|
|
367
|
-
requesterExternalUserId || requesterChatId || "the requester";
|
|
368
447
|
const decidedByExternalUserId = ctx.actor.actorExternalUserId ?? "";
|
|
369
448
|
const assistantId = DAEMON_INTERNAL_ASSISTANT_ID;
|
|
370
449
|
const desktopDeliverUrl = resolveDeliverCallbackUrlForChannel(channel);
|
|
@@ -379,6 +458,13 @@ const accessRequestResolver: GuardianRequestResolver = {
|
|
|
379
458
|
const requesterDisplayName =
|
|
380
459
|
requesterContactResult?.contact.displayName ?? null;
|
|
381
460
|
|
|
461
|
+
// Guardian-facing label prefers the contact display name over the raw ID.
|
|
462
|
+
const requesterLabel =
|
|
463
|
+
requesterDisplayName ||
|
|
464
|
+
requesterExternalUserId ||
|
|
465
|
+
requesterChatId ||
|
|
466
|
+
"the requester";
|
|
467
|
+
|
|
382
468
|
const decidedByContactResult = decidedByExternalUserId
|
|
383
469
|
? findContactChannel({
|
|
384
470
|
channelType: channel,
|
|
@@ -397,22 +483,15 @@ const accessRequestResolver: GuardianRequestResolver = {
|
|
|
397
483
|
// Deliver denial notification and lifecycle signals when channel context is available
|
|
398
484
|
if (channelDeliveryContext) {
|
|
399
485
|
try {
|
|
400
|
-
const denialPayload: Parameters<typeof deliverChannelReply>[1] = {
|
|
401
|
-
chatId: requesterChatId,
|
|
402
|
-
text: "Your access request has been denied.",
|
|
403
|
-
assistantId,
|
|
404
|
-
};
|
|
405
|
-
// On Slack shared channels, deliver as ephemeral so only the requester sees the denial
|
|
406
|
-
if (
|
|
407
|
-
shouldUseEphemeral(channel, requesterChatId) &&
|
|
408
|
-
requesterExternalUserId
|
|
409
|
-
) {
|
|
410
|
-
denialPayload.ephemeral = true;
|
|
411
|
-
denialPayload.user = requesterExternalUserId;
|
|
412
|
-
}
|
|
413
486
|
await deliverChannelReply(
|
|
414
487
|
channelDeliveryContext.replyCallbackUrl,
|
|
415
|
-
|
|
488
|
+
buildRequesterChannelNotice({
|
|
489
|
+
channel,
|
|
490
|
+
requesterChatId,
|
|
491
|
+
requesterExternalUserId,
|
|
492
|
+
text: "Your access request has been denied.",
|
|
493
|
+
assistantId,
|
|
494
|
+
}),
|
|
416
495
|
);
|
|
417
496
|
} catch (err) {
|
|
418
497
|
log.error(
|
|
@@ -563,7 +642,7 @@ const accessRequestResolver: GuardianRequestResolver = {
|
|
|
563
642
|
|
|
564
643
|
// Deliver verification code to guardian
|
|
565
644
|
const codeText =
|
|
566
|
-
`You approved access for ${
|
|
645
|
+
`You approved access for ${requesterLabel}. ` +
|
|
567
646
|
`Give them this verification code: \`${session.secret}\`. ` +
|
|
568
647
|
`The code expires in 10 minutes.`;
|
|
569
648
|
try {
|
|
@@ -629,12 +708,8 @@ const accessRequestResolver: GuardianRequestResolver = {
|
|
|
629
708
|
}
|
|
630
709
|
}
|
|
631
710
|
|
|
632
|
-
//
|
|
633
|
-
//
|
|
634
|
-
const requesterTargetChatId =
|
|
635
|
-
channel === "slack" && requesterExternalUserId
|
|
636
|
-
? requesterExternalUserId
|
|
637
|
-
: requesterChatId;
|
|
711
|
+
// Strip threadTs from the requester reply URL — it belongs to the
|
|
712
|
+
// guardian's channel thread and would cause thread_not_found in a DM.
|
|
638
713
|
let requesterCallbackUrl = channelDeliveryContext.replyCallbackUrl;
|
|
639
714
|
if (channel === "slack" && requesterExternalUserId) {
|
|
640
715
|
try {
|
|
@@ -647,47 +722,58 @@ const accessRequestResolver: GuardianRequestResolver = {
|
|
|
647
722
|
}
|
|
648
723
|
|
|
649
724
|
if (codeDelivered) {
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
}
|
|
666
|
-
await deliverChannelReply(requesterCallbackUrl, approvalPayload);
|
|
725
|
+
// On Slack, deliver the code straight to the requester's DM so the
|
|
726
|
+
// guardian doesn't have to relay it. Other channels (and a failed Slack
|
|
727
|
+
// delivery) fall back to the courier notice — there is no guaranteed
|
|
728
|
+
// private path to the requester elsewhere (e.g. group chats).
|
|
729
|
+
const requesterCodeDelivered =
|
|
730
|
+
channel === "slack" && requesterExternalUserId
|
|
731
|
+
? await deliverVerificationCodeToSlackRequester({
|
|
732
|
+
replyCallbackUrl: channelDeliveryContext.replyCallbackUrl,
|
|
733
|
+
requesterExternalUserId,
|
|
734
|
+
verificationCode: session.secret,
|
|
735
|
+
assistantId,
|
|
736
|
+
})
|
|
737
|
+
: false;
|
|
738
|
+
|
|
739
|
+
if (requesterCodeDelivered) {
|
|
667
740
|
requesterNotified = true;
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
741
|
+
} else {
|
|
742
|
+
try {
|
|
743
|
+
await deliverChannelReply(
|
|
744
|
+
requesterCallbackUrl,
|
|
745
|
+
buildRequesterChannelNotice({
|
|
746
|
+
channel,
|
|
747
|
+
requesterChatId,
|
|
748
|
+
requesterExternalUserId,
|
|
749
|
+
text:
|
|
750
|
+
"Your access request has been approved! " +
|
|
751
|
+
"Please enter the 6-digit verification code you receive from the guardian.",
|
|
752
|
+
assistantId,
|
|
753
|
+
}),
|
|
754
|
+
);
|
|
755
|
+
requesterNotified = true;
|
|
756
|
+
} catch (err) {
|
|
757
|
+
log.error(
|
|
758
|
+
{ err, requesterChatId },
|
|
759
|
+
"Failed to notify requester of access request approval",
|
|
760
|
+
);
|
|
761
|
+
}
|
|
673
762
|
}
|
|
674
763
|
} else {
|
|
675
764
|
try {
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
failurePayload.user = requesterExternalUserId;
|
|
689
|
-
}
|
|
690
|
-
await deliverChannelReply(requesterCallbackUrl, failurePayload);
|
|
765
|
+
await deliverChannelReply(
|
|
766
|
+
requesterCallbackUrl,
|
|
767
|
+
buildRequesterChannelNotice({
|
|
768
|
+
channel,
|
|
769
|
+
requesterChatId,
|
|
770
|
+
requesterExternalUserId,
|
|
771
|
+
text:
|
|
772
|
+
"Your access request was approved, but we were unable to " +
|
|
773
|
+
"deliver the verification code. Please try again later.",
|
|
774
|
+
assistantId,
|
|
775
|
+
}),
|
|
776
|
+
);
|
|
691
777
|
} catch (err) {
|
|
692
778
|
log.error(
|
|
693
779
|
{ err, requesterChatId },
|
|
@@ -721,26 +807,43 @@ const accessRequestResolver: GuardianRequestResolver = {
|
|
|
721
807
|
});
|
|
722
808
|
}
|
|
723
809
|
} else if (desktopDeliverUrl && requesterChatId) {
|
|
724
|
-
//
|
|
725
|
-
//
|
|
726
|
-
|
|
810
|
+
// Guardian decided off-channel (e.g. desktop) but the requester is on a
|
|
811
|
+
// deliverable channel. On Slack, DM the code directly (parity with the
|
|
812
|
+
// on-channel path); otherwise fall back to the courier notice.
|
|
813
|
+
const requesterCodeDelivered =
|
|
727
814
|
channel === "slack" && requesterExternalUserId
|
|
728
|
-
?
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
});
|
|
815
|
+
? await deliverVerificationCodeToSlackRequester({
|
|
816
|
+
replyCallbackUrl: desktopDeliverUrl,
|
|
817
|
+
requesterExternalUserId,
|
|
818
|
+
verificationCode: session.secret,
|
|
819
|
+
assistantId,
|
|
820
|
+
})
|
|
821
|
+
: false;
|
|
822
|
+
|
|
823
|
+
if (requesterCodeDelivered) {
|
|
738
824
|
requesterNotified = true;
|
|
739
|
-
}
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
825
|
+
} else {
|
|
826
|
+
// For Slack, route to DM via requesterExternalUserId (user ID) instead
|
|
827
|
+
// of requesterChatId (channel ID) to avoid posting in public channels.
|
|
828
|
+
const targetChatId =
|
|
829
|
+
channel === "slack" && requesterExternalUserId
|
|
830
|
+
? requesterExternalUserId
|
|
831
|
+
: requesterChatId;
|
|
832
|
+
try {
|
|
833
|
+
await deliverChannelReply(desktopDeliverUrl, {
|
|
834
|
+
chatId: targetChatId,
|
|
835
|
+
text:
|
|
836
|
+
"Your access request has been approved! " +
|
|
837
|
+
"Please enter the 6-digit verification code you receive from the guardian.",
|
|
838
|
+
assistantId,
|
|
839
|
+
});
|
|
840
|
+
requesterNotified = true;
|
|
841
|
+
} catch (err) {
|
|
842
|
+
log.error(
|
|
843
|
+
{ err, requesterChatId },
|
|
844
|
+
"Failed to notify requester of access request approval (desktop decision path)",
|
|
845
|
+
);
|
|
846
|
+
}
|
|
744
847
|
}
|
|
745
848
|
}
|
|
746
849
|
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the gateway-backed channel admission policy reader.
|
|
3
|
+
*
|
|
4
|
+
* The reader fails open (returns null = admit) so a gateway IPC failure can
|
|
5
|
+
* never block a live call. These tests pin that contract plus the TTL cache.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
9
|
+
|
|
10
|
+
// ── Controllable IPC mock ────────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
type IpcHandler = (params?: Record<string, unknown>) => unknown;
|
|
13
|
+
|
|
14
|
+
const ipcHandlers = new Map<string, IpcHandler>();
|
|
15
|
+
const ipcCallLog: Array<{
|
|
16
|
+
method: string;
|
|
17
|
+
params?: Record<string, unknown>;
|
|
18
|
+
timeoutMs?: number;
|
|
19
|
+
}> = [];
|
|
20
|
+
|
|
21
|
+
mock.module("../../ipc/gateway-client.js", () => ({
|
|
22
|
+
ipcCall: async (
|
|
23
|
+
method: string,
|
|
24
|
+
params?: Record<string, unknown>,
|
|
25
|
+
timeoutMs?: number,
|
|
26
|
+
) => {
|
|
27
|
+
ipcCallLog.push({ method, params, timeoutMs });
|
|
28
|
+
const handler = ipcHandlers.get(method);
|
|
29
|
+
return handler ? handler(params) : undefined;
|
|
30
|
+
},
|
|
31
|
+
ipcCallPersistent: async () => undefined,
|
|
32
|
+
resetPersistentClient: () => {},
|
|
33
|
+
}));
|
|
34
|
+
|
|
35
|
+
import {
|
|
36
|
+
_clearCacheForTesting,
|
|
37
|
+
getChannelAdmissionPolicy,
|
|
38
|
+
} from "../channel-admission-reader.js";
|
|
39
|
+
|
|
40
|
+
const METHOD = "get_channel_admission_policy";
|
|
41
|
+
|
|
42
|
+
function countCalls(method: string): number {
|
|
43
|
+
return ipcCallLog.filter((c) => c.method === method).length;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
describe("getChannelAdmissionPolicy", () => {
|
|
47
|
+
beforeEach(() => {
|
|
48
|
+
_clearCacheForTesting();
|
|
49
|
+
ipcHandlers.clear();
|
|
50
|
+
ipcCallLog.length = 0;
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("returns the gateway-resolved policy and caches it within the TTL", async () => {
|
|
54
|
+
ipcHandlers.set(METHOD, () => ({ policy: "guardian_only" }));
|
|
55
|
+
|
|
56
|
+
expect(await getChannelAdmissionPolicy("telegram")).toBe("guardian_only");
|
|
57
|
+
// Second call within TTL is served from cache — no extra IPC round-trip.
|
|
58
|
+
expect(await getChannelAdmissionPolicy("telegram")).toBe("guardian_only");
|
|
59
|
+
expect(countCalls(METHOD)).toBe(1);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("bounds the IPC read with a short timeout so it fails open promptly", async () => {
|
|
63
|
+
ipcHandlers.set(METHOD, () => ({ policy: "guardian_only" }));
|
|
64
|
+
|
|
65
|
+
await getChannelAdmissionPolicy("telegram");
|
|
66
|
+
|
|
67
|
+
const call = ipcCallLog.find((c) => c.method === METHOD);
|
|
68
|
+
expect(call?.timeoutMs).toBe(1_000);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("returns null when IPC transport fails (undefined)", async () => {
|
|
72
|
+
ipcHandlers.set(METHOD, () => undefined);
|
|
73
|
+
expect(await getChannelAdmissionPolicy("telegram")).toBeNull();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("returns null when the gateway explicitly resolves no policy", async () => {
|
|
77
|
+
ipcHandlers.set(METHOD, () => ({ policy: null }));
|
|
78
|
+
expect(await getChannelAdmissionPolicy("telegram")).toBeNull();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("returns null when the IPC call throws", async () => {
|
|
82
|
+
ipcHandlers.set(METHOD, () => {
|
|
83
|
+
throw new Error("socket exploded");
|
|
84
|
+
});
|
|
85
|
+
expect(await getChannelAdmissionPolicy("telegram")).toBeNull();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("returns null for an invalid policy string", async () => {
|
|
89
|
+
ipcHandlers.set(METHOD, () => ({ policy: "definitely-not-a-policy" }));
|
|
90
|
+
expect(await getChannelAdmissionPolicy("telegram")).toBeNull();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("caches the explicit no-enforcement (null) answer within the TTL", async () => {
|
|
94
|
+
// The gateway successfully said "no enforcement" — a real, cacheable answer.
|
|
95
|
+
ipcHandlers.set(METHOD, () => ({ policy: null }));
|
|
96
|
+
expect(await getChannelAdmissionPolicy("telegram")).toBeNull();
|
|
97
|
+
expect(await getChannelAdmissionPolicy("telegram")).toBeNull();
|
|
98
|
+
expect(countCalls(METHOD)).toBe(1);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("does NOT cache a transport failure (undefined) — re-consults the gateway", async () => {
|
|
102
|
+
// First setup: gateway hiccup → fail open to null, NOT cached.
|
|
103
|
+
ipcHandlers.set(METHOD, () => undefined);
|
|
104
|
+
expect(await getChannelAdmissionPolicy("telegram")).toBeNull();
|
|
105
|
+
|
|
106
|
+
// Gateway recovers with a restrictive policy: the next setup must re-attempt
|
|
107
|
+
// the IPC (not serve a stale fail-open null) and honor the now-recovered floor.
|
|
108
|
+
ipcHandlers.set(METHOD, () => ({ policy: "guardian_only" }));
|
|
109
|
+
expect(await getChannelAdmissionPolicy("telegram")).toBe("guardian_only");
|
|
110
|
+
expect(countCalls(METHOD)).toBe(2);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("does NOT cache a thrown IPC error — re-consults the gateway", async () => {
|
|
114
|
+
ipcHandlers.set(METHOD, () => {
|
|
115
|
+
throw new Error("socket exploded");
|
|
116
|
+
});
|
|
117
|
+
expect(await getChannelAdmissionPolicy("telegram")).toBeNull();
|
|
118
|
+
|
|
119
|
+
ipcHandlers.set(METHOD, () => ({ policy: "no_one" }));
|
|
120
|
+
expect(await getChannelAdmissionPolicy("telegram")).toBe("no_one");
|
|
121
|
+
expect(countCalls(METHOD)).toBe(2);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("does NOT cache a malformed shape — re-consults the gateway", async () => {
|
|
125
|
+
ipcHandlers.set(METHOD, () => ({ policy: "definitely-not-a-policy" }));
|
|
126
|
+
expect(await getChannelAdmissionPolicy("telegram")).toBeNull();
|
|
127
|
+
|
|
128
|
+
ipcHandlers.set(METHOD, () => ({ policy: "guardian_only" }));
|
|
129
|
+
expect(await getChannelAdmissionPolicy("telegram")).toBe("guardian_only");
|
|
130
|
+
expect(countCalls(METHOD)).toBe(2);
|
|
131
|
+
});
|
|
132
|
+
});
|