@vellumai/assistant 0.11.7 → 0.11.8-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/ARCHITECTURE.md +10 -10
- package/Dockerfile +1 -0
- package/docs/architecture/security.md +1 -1
- package/docs/error-handling.md +1 -2
- package/docs/guardian-request-flow.md +51 -5
- package/docs/trusted-contact-access.md +13 -12
- package/knip.json +1 -0
- package/node_modules/@vellumai/avatar-catalog/package.json +19 -0
- package/{src/avatar/character-components.ts → node_modules/@vellumai/avatar-catalog/src/catalog.ts} +12 -44
- package/node_modules/@vellumai/avatar-catalog/src/colors.ts +19 -0
- package/node_modules/@vellumai/avatar-catalog/src/index.ts +25 -0
- package/node_modules/@vellumai/avatar-catalog/src/types.ts +47 -0
- package/node_modules/@vellumai/avatar-catalog/tsconfig.json +20 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +1 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/channels.ts +7 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/no-response.ts +84 -0
- package/node_modules/@vellumai/credential-storage/src/static-credentials.ts +43 -12
- package/node_modules/@vellumai/environments/package.json +2 -1
- package/node_modules/@vellumai/environments/src/__tests__/test-path-guard.test.ts +89 -0
- package/node_modules/@vellumai/environments/src/test-path-guard.ts +113 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +1 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/channels.ts +7 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/no-response.ts +84 -0
- package/node_modules/@vellumai/gateway-client/src/__tests__/guardian-request-contract.test.ts +8 -18
- package/node_modules/@vellumai/gateway-client/src/__tests__/inbound-event-kind.test.ts +46 -0
- package/node_modules/@vellumai/gateway-client/src/__tests__/invite-contract.test.ts +1 -0
- package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +46 -0
- package/node_modules/@vellumai/gateway-client/src/guardian-request-contract.ts +62 -21
- package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +14 -0
- package/node_modules/@vellumai/gateway-client/src/inbound-event-kind.ts +53 -0
- package/node_modules/@vellumai/gateway-client/src/index.ts +11 -5
- package/node_modules/@vellumai/gateway-client/src/invite-contract.ts +1 -0
- package/node_modules/@vellumai/service-contracts/package.json +1 -0
- package/node_modules/@vellumai/service-contracts/src/channels.ts +7 -0
- package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/service-contracts/src/no-response.ts +84 -0
- package/openapi.yaml +481 -9
- package/package.json +3 -1
- package/scripts/sync-llm-catalog.ts +3 -0
- package/src/__tests__/agent-loop-callsite-precedence.test.ts +16 -0
- package/src/__tests__/agent-loop-compacted-history-transform.test.ts +304 -0
- package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +3 -0
- package/src/__tests__/agent-wake-override-profile.test.ts +3 -0
- package/src/__tests__/anthropic-provider.test.ts +56 -0
- package/src/__tests__/approval-cascade.test.ts +0 -13
- package/src/__tests__/attachments-store.test.ts +117 -6
- package/src/__tests__/attachments.test.ts +122 -0
- package/src/__tests__/bundled-asset.test.ts +22 -1
- package/src/__tests__/channel-approval.test.ts +14 -7
- package/src/__tests__/channel-approvals.test.ts +3 -3
- package/src/__tests__/channel-delivery-store.test.ts +58 -0
- package/src/__tests__/channel-inbound-disk-pressure.test.ts +1 -0
- package/src/__tests__/channel-invite-transport.test.ts +1 -0
- package/src/__tests__/channel-readiness-slack-remote.test.ts +3 -3
- package/src/__tests__/compaction-events.test.ts +0 -12
- package/src/__tests__/compactor-call-site-logging.test.ts +7 -1
- package/src/__tests__/compactor-retained-image-validation.test.ts +15 -0
- package/src/__tests__/config-schema.test.ts +35 -0
- package/src/__tests__/contacts-write.test.ts +141 -3
- package/src/__tests__/conversation-abort-tool-results.test.ts +0 -13
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +0 -12
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +0 -13
- package/src/__tests__/conversation-agent-loop.test.ts +95 -13
- package/src/__tests__/conversation-confirmation-signals.test.ts +0 -13
- package/src/__tests__/conversation-load-history-repair.test.ts +418 -0
- package/src/__tests__/conversation-media-retry.test.ts +33 -2
- package/src/__tests__/conversation-pairing.test.ts +54 -0
- package/src/__tests__/conversation-process-callsite.test.ts +0 -12
- package/src/__tests__/conversation-provider-retry-repair.test.ts +0 -13
- package/src/__tests__/conversation-queue.test.ts +4 -13
- package/src/__tests__/conversation-retry-route.test.ts +5 -0
- package/src/__tests__/conversation-routes-disk-view.test.ts +1 -1
- package/src/__tests__/conversation-routes-enabled-plugins.test.ts +1 -1
- package/src/__tests__/conversation-routes-hidden-queue.test.ts +1 -1
- package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -1
- package/src/__tests__/conversation-sight-frames.test.ts +615 -0
- package/src/__tests__/conversation-slash-queue.test.ts +0 -13
- package/src/__tests__/conversation-slash-unknown.test.ts +0 -13
- package/src/__tests__/conversation-speed-override.test.ts +0 -12
- package/src/__tests__/conversation-store-ephemeral.test.ts +0 -12
- package/src/__tests__/conversation-summarize-route.test.ts +5 -0
- package/src/__tests__/conversation-summarize-up-to.test.ts +102 -0
- package/src/__tests__/conversation-wait-for-idle.test.ts +0 -12
- package/src/__tests__/conversation-workspace-cache-state.test.ts +0 -5
- package/src/__tests__/conversation-workspace-injection.test.ts +0 -17
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +0 -17
- package/src/__tests__/credential-health-service.test.ts +45 -0
- package/src/__tests__/credential-record-retire.test.ts +256 -0
- package/src/__tests__/credential-record-write-through.test.ts +8 -4
- package/src/__tests__/db-conversation-fork-lineage-migration.test.ts +0 -4
- package/src/__tests__/db-conversation-inference-profile-migration.test.ts +0 -4
- package/src/__tests__/db-llm-request-log-provider-migration.test.ts +0 -8
- package/src/__tests__/delete-propagation.test.ts +121 -3
- package/src/__tests__/evict-conversations-for-reload.test.ts +46 -6
- package/src/__tests__/file-ops-service.test.ts +8 -3
- package/src/__tests__/gemini-provider.test.ts +64 -0
- package/src/__tests__/guardian-action-service-scope.test.ts +118 -0
- package/src/__tests__/guardian-card-withdrawal.test.ts +299 -5
- package/src/__tests__/guardian-expiry-notifier.test.ts +117 -13
- package/src/__tests__/guardian-gateway-sim.test.ts +119 -0
- package/src/__tests__/guardian-gateway-sim.ts +56 -20
- package/src/__tests__/guardian-reply-router-answer-mode.test.ts +6 -0
- package/src/__tests__/guardian-request-status-sync.test.ts +162 -0
- package/src/__tests__/guardian-wait-controller.test.ts +6 -0
- package/src/__tests__/headless-browser-mode.test.ts +27 -0
- package/src/__tests__/helpers/mock-conversation.ts +2 -0
- package/src/__tests__/list-messages-system-card.test.ts +70 -0
- package/src/__tests__/llm-catalog-parity.test.ts +2 -0
- package/src/__tests__/managed-profile-guard.test.ts +2 -2
- package/src/__tests__/media-resolve-image-validation.test.ts +106 -0
- package/src/__tests__/media-stream-server-integration.test.ts +6 -0
- package/src/__tests__/non-member-access-request.test.ts +12 -0
- package/src/__tests__/notification-discord-adapter.test.ts +76 -4
- package/src/__tests__/oauth-commands-routes.test.ts +21 -0
- package/src/__tests__/openai-provider.test.ts +63 -0
- package/src/__tests__/outbound-slack-persistence.test.ts +114 -2
- package/src/__tests__/persist-unsendable-image-downscale.test.ts +63 -1
- package/src/__tests__/persist-unsendable-image.test.ts +19 -0
- package/src/__tests__/plugin-app-serve-routes.test.ts +2 -2
- package/src/__tests__/plugin-import-boundary-guard.test.ts +1 -0
- package/src/__tests__/process-message-background-slack.test.ts +1 -1
- package/src/__tests__/process-message-display-content.test.ts +1 -1
- package/src/__tests__/processing-flag-persist-failure.test.ts +0 -5
- package/src/__tests__/prompt-cache-cross-turn-stability.test.ts +0 -17
- package/src/__tests__/qdrant-manager.test.ts +6 -0
- package/src/__tests__/reaction-persistence.test.ts +58 -62
- package/src/__tests__/retry-thinking-adaptive-only.test.ts +67 -0
- package/src/__tests__/server-history-render.test.ts +22 -0
- package/src/__tests__/sight-frame-retention.test.ts +362 -0
- package/src/__tests__/slack-reaction-approvals.test.ts +44 -24
- package/src/__tests__/slack-reaction-guardian-approval.test.ts +29 -1
- package/src/__tests__/subagent-fork-notifications.test.ts +2 -0
- package/src/__tests__/subagent-manager-notify.test.ts +2 -0
- package/src/__tests__/subagent-notify-parent.test.ts +2 -0
- package/src/__tests__/subagent-notify-stale-rebuild.test.ts +85 -0
- package/src/__tests__/subagent-tool-gate-mode.test.ts +4 -4
- package/src/__tests__/terminal-tools.test.ts +9 -9
- package/src/__tests__/tool-result-spool.test.ts +5 -8
- package/src/__tests__/usage-attribution.test.ts +26 -0
- package/src/acp/__tests__/acp-auth-marker-store.test.ts +191 -0
- package/src/acp/__tests__/acp-claude-oauth.test.ts +262 -0
- package/src/acp/__tests__/acp-connect-card-state.test.ts +17 -0
- package/src/acp/__tests__/helpers/acp-history-db.ts +10 -3
- package/src/acp/acp-auth-marker-store.ts +196 -0
- package/src/acp/acp-claude-oauth.ts +154 -4
- package/src/acp/acp-connect-card-state.ts +49 -8
- package/src/acp/agent-process.ts +1 -0
- package/src/acp/auto-install.ts +1 -0
- package/src/acp/client-handler.ts +1 -0
- package/src/acp/prepare-agent-env.ts +79 -1
- package/src/acp/resolve-agent.ts +12 -0
- package/src/acp/session-manager.test.ts +183 -0
- package/src/acp/session-manager.ts +110 -6
- package/src/acp/types.ts +17 -0
- package/src/agent/attachments.ts +7 -2
- package/src/agent/loop.ts +59 -1
- package/src/api/events/contact-form-closed.ts +32 -0
- package/src/api/events/contact-record-request.ts +63 -0
- package/src/api/events/contact-request.ts +10 -3
- package/src/api/index.ts +20 -0
- package/src/api/responses/conversation-message.ts +20 -0
- package/src/api/responses/home.ts +101 -0
- package/src/approvals/AGENTS.md +1 -1
- package/src/approvals/guardian-card-withdrawal.ts +127 -30
- package/src/approvals/guardian-decision-primitive.ts +9 -0
- package/src/approvals/guardian-expiry-notifier.ts +22 -16
- package/src/approvals/guardian-request-status-sync.ts +98 -0
- package/src/avatar/svg-compositor.ts +1 -1
- package/src/avatar/traits-png-sync.ts +1 -1
- package/src/bundler/app-compiler.ts +1 -0
- package/src/bundler/compiler-tools.ts +1 -0
- package/src/bundler/package-resolver.ts +1 -0
- package/src/calls/__tests__/voice-session-bridge.test.ts +183 -3
- package/src/calls/guardian-action-sweep.ts +6 -1
- package/src/calls/voice-session-bridge.ts +46 -1
- package/src/channels/__tests__/gateway-guardian-requests.test.ts +13 -18
- package/src/channels/gateway-guardian-requests.ts +37 -18
- package/src/cli/__tests__/contacts-record-prompt-cli.test.ts +329 -0
- package/src/cli/commands/__tests__/browser.test.ts +65 -1
- package/src/cli/commands/browser.ts +30 -10
- package/src/cli/commands/contacts.help.ts +173 -11
- package/src/cli/commands/contacts.ts +343 -4
- package/src/cli/commands/db/refresh.ts +1 -0
- package/src/cli/commands/oauth/index.help.ts +6 -3
- package/src/cli/commands/oauth/request.test.ts +37 -0
- package/src/cli/commands/oauth/request.ts +7 -6
- package/src/cli/lib/bundled-marketplace.json +1 -1
- package/src/cli/lib/install-from-github.ts +20 -19
- package/src/cli/lib/install-plugin-dependencies.ts +3 -5
- package/src/cli/lib/merge-plugin-tree.ts +1 -0
- package/src/cli/lib/publish-plugin.ts +4 -1
- package/src/config/__tests__/balanced-model-experiment.test.ts +63 -31
- package/src/config/__tests__/default-profile-catalog.test.ts +2 -2
- package/src/config/balanced-model-experiment.ts +15 -9
- package/src/config/bundled-skills/contacts/SKILL.md +61 -18
- package/src/config/default-profile-catalog.ts +12 -11
- package/src/config/feature-flag-registry.json +22 -13
- package/src/config/schema.ts +2 -0
- package/src/config/schemas/__tests__/live-voice.test.ts +13 -0
- package/src/config/schemas/live-voice.ts +7 -0
- package/src/config/schemas/sight.ts +45 -0
- package/src/config/skills.ts +2 -1
- package/src/contacts/__tests__/mirror-reconciler.test.ts +352 -0
- package/src/contacts/contact-store.ts +47 -4
- package/src/contacts/contacts-write.ts +5 -2
- package/src/contacts/mirror-reconciler.ts +210 -0
- package/src/contacts/mirror-tombstones.ts +42 -0
- package/src/contacts/notify-contacts-changed.ts +11 -2
- package/src/context/compactor.ts +47 -19
- package/src/context/tool-result-spool.ts +21 -14
- package/src/conversations/message-consolidation.ts +2 -2
- package/src/credential-health/credential-health-service.ts +37 -6
- package/src/daemon/conversation-agent-loop-handlers.ts +19 -2
- package/src/daemon/conversation-agent-loop.ts +100 -1
- package/src/daemon/conversation-media-retry.ts +11 -6
- package/src/daemon/conversation-messaging.ts +293 -30
- package/src/daemon/conversation-runtime-assembly.ts +67 -0
- package/src/daemon/conversation-sight-frames.ts +187 -0
- package/src/daemon/conversation-store.ts +18 -28
- package/src/daemon/conversation-surfaces.ts +12 -2
- package/src/daemon/conversation-tool-setup.ts +39 -10
- package/src/daemon/conversation.ts +159 -16
- package/src/daemon/daemon-control.ts +6 -17
- package/src/daemon/handlers/config-model.test.ts +1 -0
- package/src/daemon/handlers/conversations.ts +10 -11
- package/src/daemon/install-assistant-command.ts +1 -0
- package/src/daemon/lifecycle.ts +10 -13
- package/src/daemon/message-types/sync.ts +4 -0
- package/src/daemon/process-message.ts +2 -17
- package/src/daemon/reaction-history-render.test.ts +162 -0
- package/src/daemon/reaction-history-render.ts +115 -0
- package/src/daemon/reaction-record.test.ts +162 -0
- package/src/daemon/reaction-record.ts +83 -0
- package/src/daemon/shutdown-handlers.ts +0 -2
- package/src/daemon/summarize-boundary.ts +12 -0
- package/src/daemon/video-thumbnail.ts +1 -1
- package/src/home/feed-types.ts +4 -0
- package/src/home/feed-writer.ts +25 -0
- package/src/live-voice/__tests__/live-voice-attach-frame.test.ts +679 -0
- package/src/live-voice/__tests__/live-voice-attach-image.test.ts +8 -5
- package/src/live-voice/__tests__/live-voice-photo.test.ts +637 -1
- package/src/live-voice/__tests__/live-voice-sight-frame-inline.test.ts +787 -0
- package/src/live-voice/__tests__/live-voice-sight-frame.test.ts +347 -0
- package/src/live-voice/__tests__/live-voice-vad.test.ts +109 -0
- package/src/live-voice/live-voice-photo.ts +420 -65
- package/src/live-voice/live-voice-session.ts +311 -27
- package/src/live-voice/protocol.ts +147 -11
- package/src/messaging/provider-message-metadata.ts +7 -0
- package/src/messaging/providers/__tests__/transport-dispatch.test.ts +113 -1
- package/src/messaging/providers/channel-transport.ts +37 -0
- package/src/messaging/providers/discord/api.ts +4 -4
- package/src/messaging/providers/discord/send.test.ts +177 -2
- package/src/messaging/providers/discord/send.ts +197 -10
- package/src/messaging/providers/discord/transport.ts +10 -0
- package/src/messaging/providers/index.ts +42 -0
- package/src/messaging/providers/slack/__tests__/adapter-mention-rendering.test.ts +129 -3
- package/src/messaging/providers/slack/__tests__/auth.test.ts +35 -0
- package/src/messaging/providers/slack/__tests__/message-content.test.ts +152 -0
- package/src/messaging/providers/slack/adapter.ts +112 -27
- package/src/messaging/providers/slack/api.test.ts +10 -0
- package/src/messaging/providers/slack/auth.ts +32 -6
- package/src/messaging/providers/slack/client.ts +0 -15
- package/src/messaging/providers/slack/message-content.ts +137 -0
- package/src/messaging/providers/slack/send.test.ts +43 -2
- package/src/messaging/providers/slack/send.ts +18 -5
- package/src/messaging/providers/slack/transport.ts +10 -0
- package/src/messaging/providers/slack/types.ts +21 -2
- package/src/messaging/providers/telegram-bot/adapter.ts +5 -1
- package/src/messaging/providers/telegram-bot/send.test.ts +49 -2
- package/src/messaging/providers/telegram-bot/send.ts +41 -1
- package/src/messaging/providers/telegram-bot/transport.ts +10 -0
- package/src/messaging/providers/telegram-bot/withdraw.ts +13 -5
- package/src/messaging/reaction-envelopes.ts +79 -0
- package/src/monitoring/control.ts +2 -2
- package/src/monitoring/page-cache.ts +5 -1
- package/src/notifications/AGENTS.md +1 -1
- package/src/notifications/__tests__/guardian-feed-projection.test.ts +360 -0
- package/src/notifications/__tests__/home-feed-side-effect.test.ts +8 -0
- package/src/notifications/adapters/discord.ts +62 -9
- package/src/notifications/conversation-pairing.ts +31 -4
- package/src/notifications/emit-signal.ts +8 -5
- package/src/notifications/guardian-delivery-recorder.ts +47 -6
- package/src/notifications/guardian-feed-projection.ts +492 -0
- package/src/notifications/home-feed-side-effect.ts +59 -11
- package/src/oauth/byo-connection.test.ts +22 -0
- package/src/oauth/byo-connection.ts +11 -5
- package/src/oauth/connection.ts +3 -2
- package/src/oauth/platform-connection.test.ts +35 -0
- package/src/oauth/platform-connection.ts +18 -13
- package/src/oauth/scope-utils.ts +48 -0
- package/src/oauth/seed-providers.ts +12 -0
- package/src/outbound-proxy/certs.ts +5 -4
- package/src/permissions/question-guardian-request.test.ts +7 -1
- package/src/persistence/attachments-store.ts +134 -31
- package/src/persistence/conversation-crud.ts +137 -4
- package/src/persistence/conversation-types.ts +109 -0
- package/src/persistence/db-connection.ts +3 -21
- package/src/persistence/delivery-crud.ts +42 -12
- package/src/persistence/embeddings/__tests__/embedding-local-lifecycle.test.ts +15 -2
- package/src/persistence/embeddings/embedding-local.ts +7 -112
- package/src/persistence/embeddings/qdrant-manager.ts +4 -1
- package/src/persistence/llm-request-log-store.ts +1 -1
- package/src/persistence/migrations/370-acp-session-history-auth-error-code.ts +36 -0
- package/src/persistence/migrations/371-acp-session-history-auth-error-credential.ts +40 -0
- package/src/persistence/migrations/372-create-acp-refused-credentials.ts +34 -0
- package/src/persistence/migrations/373-acp-auth-marker-index.ts +27 -0
- package/src/persistence/migrations/__tests__/370-acp-session-history-auth-error-code.test.ts +65 -0
- package/src/persistence/migrations/__tests__/371-acp-session-history-auth-error-credential.test.ts +68 -0
- package/src/persistence/migrations/__tests__/372-create-acp-refused-credentials.test.ts +64 -0
- package/src/persistence/migrations/__tests__/373-acp-auth-marker-index.test.ts +82 -0
- package/src/persistence/schema/acp.ts +25 -0
- package/src/persistence/steps.ts +8 -0
- package/src/platform/platform-patch-queue.test.ts +43 -41
- package/src/plugin-api/index.ts +12 -0
- package/src/plugins/defaults/compaction/window-manager.ts +2 -0
- package/src/plugins/defaults/image-recovery/recover.ts +12 -1
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +166 -1
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-provider-path.test.ts +3 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-wake-chain.test.ts +3 -0
- package/src/plugins/defaults/memory/jobs-worker.ts +1 -2
- package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +53 -9
- package/src/plugins/defaults/memory/src/__tests__/memory-worker-routes.test.ts +4 -7
- package/src/plugins/defaults/memory/src/memory-worker-routes.ts +1 -1
- package/src/plugins/defaults/memory/v1/job-handlers/index-maintenance.ts +15 -1
- package/src/plugins/defaults/memory/worker-control.ts +1 -2
- package/src/providers/content-block-schema.ts +1 -0
- package/src/providers/fireworks/client.ts +9 -1
- package/src/providers/gemini/client.ts +22 -15
- package/src/providers/media-resolve.ts +24 -10
- package/src/providers/model-catalog.ts +91 -0
- package/src/providers/openai/chat-completions-provider.ts +44 -5
- package/src/providers/retry.ts +16 -11
- package/src/providers/types.ts +56 -0
- package/src/routes/__tests__/control.test.ts +4 -3
- package/src/routes/control.ts +1 -1
- package/src/routes/route-host-client.ts +1 -1
- package/src/runtime/AGENTS.md +30 -12
- package/src/runtime/__tests__/agent-wake.test.ts +71 -6
- package/src/runtime/__tests__/guardian-form-registry.test.ts +242 -0
- package/src/runtime/access-request-helper.ts +15 -7
- package/src/runtime/agent-wake.ts +16 -4
- package/src/runtime/channel-approval-types.ts +8 -5
- package/src/runtime/channel-invite-transport.ts +2 -0
- package/src/runtime/channel-invite-transports/discord.ts +27 -0
- package/src/runtime/channel-readiness-service.ts +4 -4
- package/src/runtime/channel-reply-delivery.ts +72 -28
- package/src/runtime/guardian-action-service.ts +9 -5
- package/src/runtime/guardian-form-registry.ts +256 -0
- package/src/runtime/guardian-reply-router.ts +33 -14
- package/src/runtime/invite-instruction-generator.ts +14 -2
- package/src/runtime/migrations/vbundle-metadata-merge.ts +4 -7
- package/src/runtime/no-response.ts +13 -74
- package/src/runtime/routes/__tests__/acp-routes.test.ts +527 -0
- package/src/runtime/routes/__tests__/app-management-shared-apps.test.ts +74 -0
- package/src/runtime/routes/__tests__/contact-form-kinds.test.ts +22 -0
- package/src/runtime/routes/__tests__/contact-record-prompt-route.test.ts +303 -0
- package/src/runtime/routes/__tests__/monitoring-routes.test.ts +3 -4
- package/src/runtime/routes/__tests__/pending-interactions-route.test.ts +6 -0
- package/src/runtime/routes/__tests__/route-host-worker-routes.test.ts +3 -4
- package/src/runtime/routes/__tests__/schedule-worker-routes.test.ts +5 -6
- package/src/runtime/routes/acp-routes-list.test.ts +58 -0
- package/src/runtime/routes/acp-routes.ts +253 -47
- package/src/runtime/routes/app-management-routes.ts +80 -47
- package/src/runtime/routes/approval-routes.ts +9 -0
- package/src/runtime/routes/archive-utils.ts +6 -1
- package/src/runtime/routes/avatar-routes.ts +1 -1
- package/src/runtime/routes/channel-route-shared.ts +18 -20
- package/src/runtime/routes/contact-prompt-routes.ts +271 -105
- package/src/runtime/routes/conversation-routes.ts +41 -32
- package/src/runtime/routes/credential-prompt-routes.ts +10 -1
- package/src/runtime/routes/debug-bash-routes.ts +1 -0
- package/src/runtime/routes/guardian-expiry-sweep.ts +89 -31
- package/src/runtime/routes/guardian-form-routes.ts +115 -0
- package/src/runtime/routes/inbound-message-handler.ts +82 -29
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +16 -3
- package/src/runtime/routes/inbound-stages/edit-intercept.ts +6 -0
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.test.ts +6 -0
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +6 -1
- package/src/runtime/routes/inbound-stages/reaction-intercept.test.ts +50 -17
- package/src/runtime/routes/inbound-stages/reaction-intercept.ts +97 -94
- package/src/runtime/routes/index.ts +2 -0
- package/src/runtime/routes/integrations/slack/channel.ts +39 -18
- package/src/runtime/routes/migration-routes.ts +1 -3
- package/src/runtime/routes/monitoring-routes.ts +0 -3
- package/src/runtime/routes/oauth-commands-routes.ts +18 -5
- package/src/runtime/routes/route-host-worker-routes.ts +1 -1
- package/src/runtime/routes/schedule-worker-routes.ts +1 -1
- package/src/runtime/tool-grant-request-helper.ts +4 -0
- package/src/schedule/run-script.ts +1 -0
- package/src/schedule/scheduler.ts +1 -1
- package/src/schedule/worker-control.ts +2 -2
- package/src/security/oauth2.ts +2 -11
- package/src/security/secure-keys.ts +55 -8
- package/src/skills/catalog-install.ts +7 -4
- package/src/skills/clawhub.ts +1 -0
- package/src/skills/inline-command-runner.ts +1 -0
- package/src/stt/__tests__/room-noise-floor.test.ts +108 -0
- package/src/stt/room-noise-floor.ts +140 -0
- package/src/subagent/notify.ts +49 -3
- package/src/tools/__tests__/tool-input-schemas.test.ts +2 -0
- package/src/tools/acp/spawn.ts +2 -2
- package/src/tools/browser/__tests__/browser-status.test.ts +9 -0
- package/src/tools/browser/browser-execution.ts +10 -43
- package/src/tools/browser/browser-manager.ts +1 -0
- package/src/tools/browser/browser-status-constants.ts +6 -5
- package/src/tools/browser/runtime-check.ts +2 -0
- package/src/tools/channel/react-to-message.test.ts +193 -0
- package/src/tools/channel/react-to-message.ts +159 -0
- package/src/tools/credentials/metadata-store.ts +115 -19
- package/src/tools/credentials/store.ts +0 -2
- package/src/tools/filesystem/read.ts +2 -2
- package/src/tools/host-filesystem/read.ts +2 -2
- package/src/tools/host-terminal/host-shell.test.ts +24 -0
- package/src/tools/host-terminal/host-shell.ts +11 -38
- package/src/tools/shared/filesystem/file-ops-service.ts +10 -7
- package/src/tools/shared/shell-output.test.ts +104 -0
- package/src/tools/shared/shell-output.ts +112 -39
- package/src/tools/skills/sandbox-runner.ts +1 -0
- package/src/tools/system/request-permission.test.ts +34 -0
- package/src/tools/system/request-permission.ts +29 -5
- package/src/tools/terminal/shell.test.ts +63 -0
- package/src/tools/terminal/shell.ts +12 -44
- package/src/tools/tool-input-schemas.ts +2 -0
- package/src/tools/tool-manifest.ts +2 -0
- package/src/usage/attribution.ts +12 -4
- package/src/util/__tests__/worker-ownership.test.ts +77 -0
- package/src/util/__tests__/worker-process-command.test.ts +246 -1
- package/src/util/bun-runtime.ts +44 -13
- package/src/util/bundled-asset.ts +9 -1
- package/src/util/disk-usage.ts +1 -0
- package/src/util/file-use.ts +4 -1
- package/src/util/guardian-form-timeouts.ts +50 -0
- package/src/util/image-conversion.ts +1 -0
- package/src/util/oauth-request-body.test.ts +59 -0
- package/src/util/oauth-request-body.ts +56 -5
- package/src/util/platform.ts +69 -73
- package/src/util/process-table.ts +16 -5
- package/src/util/read-stdin.ts +5 -0
- package/src/util/spawn.ts +1 -0
- package/src/util/sqlite3-runtime.ts +25 -5
- package/src/util/worker-ownership.ts +193 -0
- package/src/util/worker-process.ts +375 -37
- package/src/workspace/git-service.ts +4 -12
- package/src/workspace/migrations/010-app-dir-rename.ts +1 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
2
2
|
|
|
3
|
-
const completeSurfaceAndNotify = mock(() =>
|
|
4
|
-
const markSurfaceCompleted = mock(() =>
|
|
3
|
+
const completeSurfaceAndNotify = mock(() => true);
|
|
4
|
+
const markSurfaceCompleted = mock(() => true);
|
|
5
5
|
mock.module("../daemon/conversation-surfaces.js", () => ({
|
|
6
6
|
completeSurfaceAndNotify,
|
|
7
7
|
markSurfaceCompleted,
|
|
@@ -20,7 +20,7 @@ mock.module("../messaging/providers/discord/withdraw.js", () => ({
|
|
|
20
20
|
}));
|
|
21
21
|
|
|
22
22
|
const withdrawTelegramApprovalCard = mock(
|
|
23
|
-
async (_params: Record<string, unknown>) => {},
|
|
23
|
+
async (_params: Record<string, unknown>) => ({ complete: true }),
|
|
24
24
|
);
|
|
25
25
|
mock.module("../messaging/providers/telegram-bot/withdraw.js", () => ({
|
|
26
26
|
withdrawTelegramApprovalCard,
|
|
@@ -355,12 +355,104 @@ describe("withdrawGuardianRequestCards", () => {
|
|
|
355
355
|
status: "approved",
|
|
356
356
|
originChannel: "telegram",
|
|
357
357
|
}),
|
|
358
|
-
).resolves.
|
|
358
|
+
).resolves.toEqual({ complete: false });
|
|
359
359
|
|
|
360
360
|
// The in-app card was still withdrawn despite the Slack failure.
|
|
361
361
|
expect(completeSurfaceAndNotify).toHaveBeenCalledTimes(1);
|
|
362
362
|
});
|
|
363
363
|
|
|
364
|
+
test("completes the in-app projection of a channel card's paired conversation", async () => {
|
|
365
|
+
// A channel delivery is paired with a conversation whose in-app rendering
|
|
366
|
+
// carries the same actionable card; the channel edit alone leaves that
|
|
367
|
+
// projection clickable after the decision (LUM-3489).
|
|
368
|
+
const req = makeRequest({ kind: "tool_approval", toolName: "shell" });
|
|
369
|
+
const slack = bridgeState.seedDelivery({
|
|
370
|
+
requestId: req.id,
|
|
371
|
+
destinationChannel: "slack",
|
|
372
|
+
destinationChatId: "C1",
|
|
373
|
+
destinationMessageId: "1.0",
|
|
374
|
+
destinationConversationId: "conv-dm",
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
await withdrawGuardianRequestCards({
|
|
378
|
+
request: req,
|
|
379
|
+
status: "approved",
|
|
380
|
+
originChannel: "slack",
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
expect(withdrawSlackApprovalCard).toHaveBeenCalledTimes(1);
|
|
384
|
+
expect(completeSurfaceAndNotify).toHaveBeenCalledWith(
|
|
385
|
+
"conv-dm",
|
|
386
|
+
`tool-approval-${req.id}`,
|
|
387
|
+
"Approved",
|
|
388
|
+
);
|
|
389
|
+
const byId = new Map(deliveriesFor(req.id).map((d) => [d.id, d.status]));
|
|
390
|
+
expect(byId.get(slack.id)).toBe("withdrawn");
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
test("suppresses only the acting conversation's broadcast for an in-app decision", async () => {
|
|
394
|
+
// Same request, two in-app projections: the vellum card and a channel
|
|
395
|
+
// card's paired conversation. The acting client's optimistic completion
|
|
396
|
+
// covers only the conversation it acted in; the sibling still needs its
|
|
397
|
+
// live ui_surface_complete.
|
|
398
|
+
const req = makeRequest({ kind: "tool_approval", toolName: "shell" });
|
|
399
|
+
bridgeState.seedDelivery({
|
|
400
|
+
requestId: req.id,
|
|
401
|
+
destinationChannel: "vellum",
|
|
402
|
+
destinationConversationId: "conv-acting",
|
|
403
|
+
});
|
|
404
|
+
bridgeState.seedDelivery({
|
|
405
|
+
requestId: req.id,
|
|
406
|
+
destinationChannel: "slack",
|
|
407
|
+
destinationChatId: "C1",
|
|
408
|
+
destinationMessageId: "1.0",
|
|
409
|
+
destinationConversationId: "conv-sibling",
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
await withdrawGuardianRequestCards({
|
|
413
|
+
request: req,
|
|
414
|
+
status: "approved",
|
|
415
|
+
originChannel: "vellum",
|
|
416
|
+
originConversationId: "conv-acting",
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
expect(markSurfaceCompleted).toHaveBeenCalledWith(
|
|
420
|
+
{ conversationId: "conv-acting" },
|
|
421
|
+
`tool-approval-${req.id}`,
|
|
422
|
+
"Approved",
|
|
423
|
+
);
|
|
424
|
+
expect(completeSurfaceAndNotify).toHaveBeenCalledWith(
|
|
425
|
+
"conv-sibling",
|
|
426
|
+
`tool-approval-${req.id}`,
|
|
427
|
+
"Approved",
|
|
428
|
+
);
|
|
429
|
+
expect(completeSurfaceAndNotify).toHaveBeenCalledTimes(1);
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
test("a failed in-app persist on a channel-paired projection holds the receipt back", async () => {
|
|
433
|
+
const req = makeRequest({ kind: "tool_approval", toolName: "shell" });
|
|
434
|
+
const slack = bridgeState.seedDelivery({
|
|
435
|
+
requestId: req.id,
|
|
436
|
+
destinationChannel: "slack",
|
|
437
|
+
destinationChatId: "C1",
|
|
438
|
+
destinationMessageId: "1.0",
|
|
439
|
+
destinationConversationId: "conv-dm",
|
|
440
|
+
});
|
|
441
|
+
completeSurfaceAndNotify.mockReturnValueOnce(false);
|
|
442
|
+
|
|
443
|
+
const result = await withdrawGuardianRequestCards({
|
|
444
|
+
request: req,
|
|
445
|
+
status: "approved",
|
|
446
|
+
originChannel: "telegram",
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
// The Slack edit landed, but the paired conversation's block would revert
|
|
450
|
+
// to a clickable card on reload, so the surface stays unreceipted.
|
|
451
|
+
expect(result.complete).toBe(false);
|
|
452
|
+
const byId = new Map(deliveriesFor(req.id).map((d) => [d.id, d.status]));
|
|
453
|
+
expect(byId.get(slack.id)).not.toBe("withdrawn");
|
|
454
|
+
});
|
|
455
|
+
|
|
364
456
|
test("tool-approval cards resolve to the tool-approval surface id", async () => {
|
|
365
457
|
const req = makeRequest({ kind: "tool_approval", toolName: "shell" });
|
|
366
458
|
bridgeState.seedDelivery({
|
|
@@ -438,8 +530,131 @@ describe("withdrawGuardianRequestCards", () => {
|
|
|
438
530
|
"Denied",
|
|
439
531
|
);
|
|
440
532
|
});
|
|
441
|
-
});
|
|
442
533
|
|
|
534
|
+
test("a Telegram keyboard that stayed live holds the completeness back", async () => {
|
|
535
|
+
// The nested boolean is the whole receipt: clearInlineKeyboard already
|
|
536
|
+
// reports the failed edit, and dropping it once made the sweep confirm
|
|
537
|
+
// an expiry while the card's buttons stayed actionable.
|
|
538
|
+
const req = makeRequest({ sourceChannel: "telegram" });
|
|
539
|
+
bridgeState.seedDelivery({
|
|
540
|
+
requestId: req.id,
|
|
541
|
+
destinationChannel: "telegram",
|
|
542
|
+
destinationChatId: "T1",
|
|
543
|
+
destinationMessageId: "9",
|
|
544
|
+
});
|
|
545
|
+
withdrawTelegramApprovalCard.mockResolvedValueOnce({ complete: false });
|
|
546
|
+
|
|
547
|
+
const result = await withdrawGuardianRequestCards({
|
|
548
|
+
request: req,
|
|
549
|
+
status: "expired",
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
expect(result.complete).toBe(false);
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
test("a failed in-app persistence write holds the completeness back", async () => {
|
|
556
|
+
// markSurfaceCompleted's false means the persisted block reverts to a
|
|
557
|
+
// pending, clickable card on the next reload; the broadcast still goes
|
|
558
|
+
// out, but the caller's receipt must wait for a durable write.
|
|
559
|
+
const req = makeRequest();
|
|
560
|
+
bridgeState.seedDelivery({
|
|
561
|
+
requestId: req.id,
|
|
562
|
+
destinationChannel: "vellum",
|
|
563
|
+
destinationConversationId: "conv-1",
|
|
564
|
+
});
|
|
565
|
+
completeSurfaceAndNotify.mockReturnValueOnce(false);
|
|
566
|
+
|
|
567
|
+
const result = await withdrawGuardianRequestCards({
|
|
568
|
+
request: req,
|
|
569
|
+
status: "expired",
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
expect(result.complete).toBe(false);
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
test("a surface that succeeded on retry reports complete", async () => {
|
|
576
|
+
const req = makeRequest({ sourceChannel: "telegram" });
|
|
577
|
+
bridgeState.seedDelivery({
|
|
578
|
+
requestId: req.id,
|
|
579
|
+
destinationChannel: "telegram",
|
|
580
|
+
destinationChatId: "T1",
|
|
581
|
+
destinationMessageId: "9",
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
const result = await withdrawGuardianRequestCards({
|
|
585
|
+
request: req,
|
|
586
|
+
status: "expired",
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
expect(result.complete).toBe(true);
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
test("each durably withdrawn surface marks its delivery row as the receipt", async () => {
|
|
593
|
+
const req = makeRequest();
|
|
594
|
+
const slack = bridgeState.seedDelivery({
|
|
595
|
+
requestId: req.id,
|
|
596
|
+
destinationChannel: "slack",
|
|
597
|
+
destinationChatId: "C1",
|
|
598
|
+
destinationMessageId: "1.0",
|
|
599
|
+
});
|
|
600
|
+
const vellum = bridgeState.seedDelivery({
|
|
601
|
+
requestId: req.id,
|
|
602
|
+
destinationChannel: "vellum",
|
|
603
|
+
destinationConversationId: "conv-1",
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
const result = await withdrawGuardianRequestCards({
|
|
607
|
+
request: req,
|
|
608
|
+
status: "expired",
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
expect(result.complete).toBe(true);
|
|
612
|
+
const byId = new Map(deliveriesFor(req.id).map((d) => [d.id, d.status]));
|
|
613
|
+
expect(byId.get(slack.id)).toBe("withdrawn");
|
|
614
|
+
expect(byId.get(vellum.id)).toBe("withdrawn");
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
test("a retry skips rows already withdrawn: no re-edits, no re-broadcasts", async () => {
|
|
618
|
+
// The delivery row is the per-surface receipt. When one surface fails,
|
|
619
|
+
// the next round must retry only that surface; re-editing the others
|
|
620
|
+
// would re-broadcast in-app completions and re-run channel edits that
|
|
621
|
+
// already landed.
|
|
622
|
+
const req = makeRequest({ sourceChannel: "telegram" });
|
|
623
|
+
const slack = bridgeState.seedDelivery({
|
|
624
|
+
requestId: req.id,
|
|
625
|
+
destinationChannel: "slack",
|
|
626
|
+
destinationChatId: "C1",
|
|
627
|
+
destinationMessageId: "1.0",
|
|
628
|
+
});
|
|
629
|
+
const telegram = bridgeState.seedDelivery({
|
|
630
|
+
requestId: req.id,
|
|
631
|
+
destinationChannel: "telegram",
|
|
632
|
+
destinationChatId: "T1",
|
|
633
|
+
destinationMessageId: "9",
|
|
634
|
+
});
|
|
635
|
+
withdrawTelegramApprovalCard.mockResolvedValueOnce({ complete: false });
|
|
636
|
+
|
|
637
|
+
const first = await withdrawGuardianRequestCards({
|
|
638
|
+
request: req,
|
|
639
|
+
status: "expired",
|
|
640
|
+
});
|
|
641
|
+
expect(first.complete).toBe(false);
|
|
642
|
+
let byId = new Map(deliveriesFor(req.id).map((d) => [d.id, d.status]));
|
|
643
|
+
expect(byId.get(slack.id)).toBe("withdrawn");
|
|
644
|
+
expect(byId.get(telegram.id)).not.toBe("withdrawn");
|
|
645
|
+
|
|
646
|
+
const second = await withdrawGuardianRequestCards({
|
|
647
|
+
request: req,
|
|
648
|
+
status: "expired",
|
|
649
|
+
});
|
|
650
|
+
expect(second.complete).toBe(true);
|
|
651
|
+
byId = new Map(deliveriesFor(req.id).map((d) => [d.id, d.status]));
|
|
652
|
+
expect(byId.get(telegram.id)).toBe("withdrawn");
|
|
653
|
+
// The already-receipted Slack surface was not re-edited.
|
|
654
|
+
expect(withdrawSlackApprovalCard).toHaveBeenCalledTimes(1);
|
|
655
|
+
expect(withdrawTelegramApprovalCard).toHaveBeenCalledTimes(2);
|
|
656
|
+
});
|
|
657
|
+
});
|
|
443
658
|
describe("recordApprovalCardDelivery", () => {
|
|
444
659
|
beforeEach(() => {
|
|
445
660
|
bridgeState.reset();
|
|
@@ -582,6 +797,85 @@ describe("recordGuardianRequestDeliveries", () => {
|
|
|
582
797
|
expect(delivery.destinationChatId).toBeNull();
|
|
583
798
|
});
|
|
584
799
|
|
|
800
|
+
test("re-runs withdrawal when the request resolved before its rows were recorded", async () => {
|
|
801
|
+
// Recording happens after the notification pipeline settles, so a
|
|
802
|
+
// guardian acting the moment the card lands can resolve the request
|
|
803
|
+
// before any delivery row exists; that decision's withdrawal pass finds
|
|
804
|
+
// nothing. The recorder reconciles by withdrawing what it just recorded.
|
|
805
|
+
const req = makeRequest({
|
|
806
|
+
kind: "tool_approval",
|
|
807
|
+
toolName: "shell",
|
|
808
|
+
status: "approved",
|
|
809
|
+
});
|
|
810
|
+
|
|
811
|
+
await recordGuardianRequestDeliveries({
|
|
812
|
+
requestId: req.id,
|
|
813
|
+
deliveryResults: [
|
|
814
|
+
{
|
|
815
|
+
channel: "slack",
|
|
816
|
+
destination: "C999",
|
|
817
|
+
status: "sent",
|
|
818
|
+
messageId: "1700000000.1234",
|
|
819
|
+
conversationId: "conv-dm",
|
|
820
|
+
},
|
|
821
|
+
],
|
|
822
|
+
});
|
|
823
|
+
|
|
824
|
+
expect(withdrawSlackApprovalCard).toHaveBeenCalledTimes(1);
|
|
825
|
+
const [delivery] = deliveriesFor(req.id);
|
|
826
|
+
expect(delivery.status).toBe("withdrawn");
|
|
827
|
+
});
|
|
828
|
+
|
|
829
|
+
test("leaves rows of a still-pending request alone after recording", async () => {
|
|
830
|
+
const req = makeRequest({ kind: "tool_approval", toolName: "shell" });
|
|
831
|
+
await recordGuardianRequestDeliveries({
|
|
832
|
+
requestId: req.id,
|
|
833
|
+
deliveryResults: [
|
|
834
|
+
{
|
|
835
|
+
channel: "slack",
|
|
836
|
+
destination: "C999",
|
|
837
|
+
status: "sent",
|
|
838
|
+
messageId: "1700000000.1234",
|
|
839
|
+
},
|
|
840
|
+
],
|
|
841
|
+
});
|
|
842
|
+
|
|
843
|
+
expect(withdrawSlackApprovalCard).not.toHaveBeenCalled();
|
|
844
|
+
const [delivery] = deliveriesFor(req.id);
|
|
845
|
+
expect(delivery.status).toBe("sent");
|
|
846
|
+
});
|
|
847
|
+
|
|
848
|
+
test("a late status patch cannot resurrect a withdrawn vellum row", async () => {
|
|
849
|
+
// The pre-created vellum row can be withdrawn by a fast decision while
|
|
850
|
+
// the broadcast is still settling; the recorder's sent patch must not
|
|
851
|
+
// overwrite that receipt (mirrors the gateway store's sticky rule).
|
|
852
|
+
const req = makeRequest({ status: "approved" });
|
|
853
|
+
const pre = await recordApprovalCardDelivery({
|
|
854
|
+
requestId: req.id,
|
|
855
|
+
channel: "vellum",
|
|
856
|
+
conversationId: "conv-1",
|
|
857
|
+
});
|
|
858
|
+
await bridgeState.module.updateGuardianRequestDelivery(pre!.id, {
|
|
859
|
+
status: "withdrawn",
|
|
860
|
+
});
|
|
861
|
+
|
|
862
|
+
await recordGuardianRequestDeliveries({
|
|
863
|
+
requestId: req.id,
|
|
864
|
+
deliveryResults: [
|
|
865
|
+
{
|
|
866
|
+
channel: "vellum",
|
|
867
|
+
destination: "",
|
|
868
|
+
status: "sent",
|
|
869
|
+
conversationId: "conv-1",
|
|
870
|
+
},
|
|
871
|
+
],
|
|
872
|
+
vellumDeliveryId: pre?.id,
|
|
873
|
+
});
|
|
874
|
+
|
|
875
|
+
const [delivery] = deliveriesFor(req.id);
|
|
876
|
+
expect(delivery.status).toBe("withdrawn");
|
|
877
|
+
});
|
|
878
|
+
|
|
585
879
|
test("records a Slack delivery the withdrawal path can then edit in place", async () => {
|
|
586
880
|
const req = makeRequest();
|
|
587
881
|
await recordGuardianRequestDeliveries({
|
|
@@ -38,35 +38,49 @@ mock.module("../runtime/assistant-event-hub.js", () => ({
|
|
|
38
38
|
},
|
|
39
39
|
}));
|
|
40
40
|
|
|
41
|
-
// The sweep withdraws cards via this module;
|
|
42
|
-
//
|
|
41
|
+
// The sweep withdraws cards via this module; mocking it keeps the sweep
|
|
42
|
+
// import light (no surface/slack transitive deps). The completeness result
|
|
43
|
+
// gates the sweep's expire confirmation.
|
|
43
44
|
let withdrawCalls = 0;
|
|
45
|
+
let withdrawIncomplete = false;
|
|
44
46
|
mock.module("../approvals/guardian-card-withdrawal.js", () => ({
|
|
45
47
|
withdrawGuardianRequestCards: async () => {
|
|
46
48
|
withdrawCalls++;
|
|
49
|
+
return { complete: !withdrawIncomplete };
|
|
47
50
|
},
|
|
48
51
|
}));
|
|
49
52
|
|
|
50
|
-
// Gateway guardian-request client
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
+
// Gateway guardian-request client, in-memory rows driven by tests. The
|
|
54
|
+
// sweep lists past-deadline pending rows read-only and confirms each with
|
|
55
|
+
// the per-request expire CAS after its side effects run.
|
|
53
56
|
const gatewayRequests = new Map<string, GuardianRequestWire>();
|
|
57
|
+
let expireCasFails = false;
|
|
58
|
+
const actualGatewayGuardianRequests =
|
|
59
|
+
await import("../channels/gateway-guardian-requests.js");
|
|
54
60
|
mock.module("../channels/gateway-guardian-requests.js", () => ({
|
|
55
|
-
|
|
61
|
+
...actualGatewayGuardianRequests,
|
|
62
|
+
listExpiredPendingGuardianRequests: async () => {
|
|
56
63
|
const now = Date.now();
|
|
57
|
-
const
|
|
64
|
+
const stale: GuardianRequestWire[] = [];
|
|
58
65
|
for (const row of gatewayRequests.values()) {
|
|
59
66
|
if (
|
|
60
67
|
row.status === "pending" &&
|
|
61
68
|
row.expiresAt !== null &&
|
|
62
69
|
row.expiresAt <= now
|
|
63
70
|
) {
|
|
64
|
-
|
|
65
|
-
gatewayRequests.set(row.id, flipped);
|
|
66
|
-
expired.push(flipped);
|
|
71
|
+
stale.push(row);
|
|
67
72
|
}
|
|
68
73
|
}
|
|
69
|
-
return
|
|
74
|
+
return stale;
|
|
75
|
+
},
|
|
76
|
+
expireGuardianRequest: async (id: string) => {
|
|
77
|
+
if (expireCasFails) {
|
|
78
|
+
throw new Error("simulated IPC timeout");
|
|
79
|
+
}
|
|
80
|
+
const row = gatewayRequests.get(id);
|
|
81
|
+
if (row?.status === "pending") {
|
|
82
|
+
gatewayRequests.set(id, { ...row, status: "expired" as const });
|
|
83
|
+
}
|
|
70
84
|
},
|
|
71
85
|
}));
|
|
72
86
|
|
|
@@ -117,6 +131,9 @@ beforeEach(() => {
|
|
|
117
131
|
broadcasts.length = 0;
|
|
118
132
|
deliveryError = null;
|
|
119
133
|
withdrawCalls = 0;
|
|
134
|
+
withdrawIncomplete = false;
|
|
135
|
+
expireCasFails = false;
|
|
136
|
+
gatewayRequests.clear();
|
|
120
137
|
pendingInteractions.clear();
|
|
121
138
|
});
|
|
122
139
|
|
|
@@ -229,7 +246,7 @@ describe("notifyExpiredGuardianRequest", () => {
|
|
|
229
246
|
expect(deliveredReplies).toHaveLength(0);
|
|
230
247
|
});
|
|
231
248
|
|
|
232
|
-
test("delivery failure
|
|
249
|
+
test("delivery failure never throws, and reports incomplete", async () => {
|
|
233
250
|
deliveryError = new Error("gateway down");
|
|
234
251
|
|
|
235
252
|
await expect(
|
|
@@ -240,7 +257,7 @@ describe("notifyExpiredGuardianRequest", () => {
|
|
|
240
257
|
requesterChatId: "tg-chat",
|
|
241
258
|
}),
|
|
242
259
|
),
|
|
243
|
-
).resolves.
|
|
260
|
+
).resolves.toEqual({ complete: false });
|
|
244
261
|
});
|
|
245
262
|
});
|
|
246
263
|
|
|
@@ -291,4 +308,91 @@ describe("sweep integration", () => {
|
|
|
291
308
|
expect(gatewayRequests.get("req-fresh")?.status).toBe("pending");
|
|
292
309
|
expect(deliveredReplies).toHaveLength(0);
|
|
293
310
|
});
|
|
311
|
+
|
|
312
|
+
test("an incomplete withdrawal defers the request and holds the notice back", async () => {
|
|
313
|
+
// A card edit that failed leaves an actionable control somewhere, so the
|
|
314
|
+
// request must stay pending and retryable. The notice is held back too:
|
|
315
|
+
// sending it only after withdrawal completes means the retry round can
|
|
316
|
+
// never duplicate a delivered notice.
|
|
317
|
+
gatewayRequests.set(
|
|
318
|
+
"req-withdraw-fail",
|
|
319
|
+
makeRequest({
|
|
320
|
+
id: "req-withdraw-fail",
|
|
321
|
+
kind: "access_request",
|
|
322
|
+
status: "pending",
|
|
323
|
+
requesterChatId: "tg-chat",
|
|
324
|
+
requesterExternalUserId: "tg-user",
|
|
325
|
+
expiresAt: Date.now() - 1000,
|
|
326
|
+
}),
|
|
327
|
+
);
|
|
328
|
+
|
|
329
|
+
withdrawIncomplete = true;
|
|
330
|
+
expect(await runGuardianExpirySweep()).toBe(0);
|
|
331
|
+
expect(gatewayRequests.get("req-withdraw-fail")?.status).toBe("pending");
|
|
332
|
+
expect(deliveredReplies).toHaveLength(0);
|
|
333
|
+
|
|
334
|
+
withdrawIncomplete = false;
|
|
335
|
+
expect(await runGuardianExpirySweep()).toBe(1);
|
|
336
|
+
expect(gatewayRequests.get("req-withdraw-fail")?.status).toBe("expired");
|
|
337
|
+
expect(deliveredReplies).toHaveLength(1);
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
test("a failed requester notice defers the request until a retry delivers it", async () => {
|
|
341
|
+
gatewayRequests.set(
|
|
342
|
+
"req-notice-fail",
|
|
343
|
+
makeRequest({
|
|
344
|
+
id: "req-notice-fail",
|
|
345
|
+
kind: "access_request",
|
|
346
|
+
status: "pending",
|
|
347
|
+
requesterChatId: "tg-chat",
|
|
348
|
+
requesterExternalUserId: "tg-user",
|
|
349
|
+
expiresAt: Date.now() - 1000,
|
|
350
|
+
}),
|
|
351
|
+
);
|
|
352
|
+
|
|
353
|
+
deliveryError = new Error("channel briefly unreachable");
|
|
354
|
+
expect(await runGuardianExpirySweep()).toBe(0);
|
|
355
|
+
expect(gatewayRequests.get("req-notice-fail")?.status).toBe("pending");
|
|
356
|
+
expect(deliveredReplies).toHaveLength(0);
|
|
357
|
+
|
|
358
|
+
deliveryError = null;
|
|
359
|
+
expect(await runGuardianExpirySweep()).toBe(1);
|
|
360
|
+
expect(gatewayRequests.get("req-notice-fail")?.status).toBe("expired");
|
|
361
|
+
// The notice lands exactly once: the failed attempt delivered nothing.
|
|
362
|
+
expect(deliveredReplies).toHaveLength(1);
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
test("a lost expire confirmation leaves the row pending, and the next round recovers it", async () => {
|
|
366
|
+
// The status flip is the receipt that side effects ran, not the
|
|
367
|
+
// announcement that they should. When the confirmation is lost, the row
|
|
368
|
+
// must stay in the pending set so the next round re-runs its fan-out;
|
|
369
|
+
// the cost is at-least-once side effects, never their silent loss.
|
|
370
|
+
gatewayRequests.set(
|
|
371
|
+
"req-lost",
|
|
372
|
+
makeRequest({
|
|
373
|
+
id: "req-lost",
|
|
374
|
+
kind: "access_request",
|
|
375
|
+
status: "pending",
|
|
376
|
+
requesterChatId: "tg-chat",
|
|
377
|
+
requesterExternalUserId: "tg-user",
|
|
378
|
+
expiresAt: Date.now() - 1000,
|
|
379
|
+
}),
|
|
380
|
+
);
|
|
381
|
+
|
|
382
|
+
expireCasFails = true;
|
|
383
|
+
expect(await runGuardianExpirySweep()).toBe(0);
|
|
384
|
+
expect(gatewayRequests.get("req-lost")?.status).toBe("pending");
|
|
385
|
+
expect(withdrawCalls).toBe(1);
|
|
386
|
+
expect(deliveredReplies).toHaveLength(1);
|
|
387
|
+
|
|
388
|
+
expireCasFails = false;
|
|
389
|
+
expect(await runGuardianExpirySweep()).toBe(1);
|
|
390
|
+
expect(gatewayRequests.get("req-lost")?.status).toBe("expired");
|
|
391
|
+
// The re-run repeats the side effects (at-least-once); the third round
|
|
392
|
+
// finds nothing.
|
|
393
|
+
expect(withdrawCalls).toBe(2);
|
|
394
|
+
expect(deliveredReplies).toHaveLength(2);
|
|
395
|
+
expect(await runGuardianExpirySweep()).toBe(0);
|
|
396
|
+
expect(withdrawCalls).toBe(2);
|
|
397
|
+
});
|
|
294
398
|
});
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import { createGuardianGatewaySim } from "./guardian-gateway-sim.js";
|
|
4
|
+
|
|
5
|
+
import "./test-preload.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Pins the simulator to the gateway's authorization-lifecycle contract.
|
|
9
|
+
* Fifteen assistant suites prove behavior against this sim, so a sim whose
|
|
10
|
+
* semantics drift from the gateway's lets them pass against a production
|
|
11
|
+
* shape that does not exist.
|
|
12
|
+
*/
|
|
13
|
+
describe("guardian gateway sim: boot expiry contract", () => {
|
|
14
|
+
test("expires only interaction-bound kinds, never persistent kinds", async () => {
|
|
15
|
+
const sim = createGuardianGatewaySim();
|
|
16
|
+
const past = Date.now() - 60_000;
|
|
17
|
+
sim.seedRequest({ id: "ta", kind: "tool_approval", status: "pending" });
|
|
18
|
+
sim.seedRequest({
|
|
19
|
+
id: "pq",
|
|
20
|
+
kind: "pending_question",
|
|
21
|
+
status: "pending",
|
|
22
|
+
});
|
|
23
|
+
// Past-deadline persistent rows stay pending for the sweep, which owns
|
|
24
|
+
// the card-withdrawal and requester-notice fan-out; boot must not
|
|
25
|
+
// pre-empt it (the gateway contract this sim mirrors).
|
|
26
|
+
sim.seedRequest({
|
|
27
|
+
id: "ar",
|
|
28
|
+
kind: "access_request",
|
|
29
|
+
status: "pending",
|
|
30
|
+
expiresAt: past,
|
|
31
|
+
});
|
|
32
|
+
sim.seedRequest({
|
|
33
|
+
id: "tg",
|
|
34
|
+
kind: "tool_grant_request",
|
|
35
|
+
status: "pending",
|
|
36
|
+
expiresAt: past,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const expired = await sim.module.expireInteractionBoundGuardianRequests();
|
|
40
|
+
|
|
41
|
+
expect(expired).toBe(2);
|
|
42
|
+
expect(sim.getRequest("ta")?.status).toBe("expired");
|
|
43
|
+
expect(sim.getRequest("pq")?.status).toBe("expired");
|
|
44
|
+
expect(sim.getRequest("ar")?.status).toBe("pending");
|
|
45
|
+
expect(sim.getRequest("tg")?.status).toBe("pending");
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe("guardian gateway sim: decision arbitration contract", () => {
|
|
50
|
+
test("a decision past the deadline loses atomically", async () => {
|
|
51
|
+
const sim = createGuardianGatewaySim();
|
|
52
|
+
sim.seedRequest({
|
|
53
|
+
id: "late",
|
|
54
|
+
kind: "access_request",
|
|
55
|
+
status: "pending",
|
|
56
|
+
expiresAt: Date.now() - 1000,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const result = await sim.module.decideGuardianRequest({
|
|
60
|
+
id: "late",
|
|
61
|
+
expectedStatus: "pending",
|
|
62
|
+
status: "approved",
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
expect(result.applied).toBe(false);
|
|
66
|
+
expect(sim.getRequest("late")?.status).toBe("pending");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("a no-op expire never restamps a decided request's deliveries", async () => {
|
|
70
|
+
const sim = createGuardianGatewaySim();
|
|
71
|
+
sim.seedRequest({ id: "done", kind: "access_request", status: "pending" });
|
|
72
|
+
sim.seedDelivery({
|
|
73
|
+
id: "d1",
|
|
74
|
+
requestId: "done",
|
|
75
|
+
destinationChannel: "telegram",
|
|
76
|
+
destinationChatId: "chat-1",
|
|
77
|
+
status: "sent",
|
|
78
|
+
});
|
|
79
|
+
await sim.module.decideGuardianRequest({
|
|
80
|
+
id: "done",
|
|
81
|
+
expectedStatus: "pending",
|
|
82
|
+
status: "approved",
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
await sim.module.expireGuardianRequest("done");
|
|
86
|
+
|
|
87
|
+
expect(sim.getRequest("done")?.status).toBe("approved");
|
|
88
|
+
expect(sim.deliveries.find((d) => d.id === "d1")?.status).toBe("sent");
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("a withdrawn delivery keeps its receipt through the expire flip", async () => {
|
|
92
|
+
const sim = createGuardianGatewaySim();
|
|
93
|
+
sim.seedRequest({ id: "exp", kind: "access_request", status: "pending" });
|
|
94
|
+
sim.seedDelivery({
|
|
95
|
+
id: "kept",
|
|
96
|
+
requestId: "exp",
|
|
97
|
+
destinationChannel: "telegram",
|
|
98
|
+
destinationChatId: "chat-1",
|
|
99
|
+
status: "withdrawn",
|
|
100
|
+
});
|
|
101
|
+
sim.seedDelivery({
|
|
102
|
+
id: "flipped",
|
|
103
|
+
requestId: "exp",
|
|
104
|
+
destinationChannel: "vellum",
|
|
105
|
+
destinationConversationId: "conv-1",
|
|
106
|
+
status: "sent",
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
await sim.module.expireGuardianRequest("exp");
|
|
110
|
+
|
|
111
|
+
expect(sim.getRequest("exp")?.status).toBe("expired");
|
|
112
|
+
expect(sim.deliveries.find((d) => d.id === "kept")?.status).toBe(
|
|
113
|
+
"withdrawn",
|
|
114
|
+
);
|
|
115
|
+
expect(sim.deliveries.find((d) => d.id === "flipped")?.status).toBe(
|
|
116
|
+
"expired",
|
|
117
|
+
);
|
|
118
|
+
});
|
|
119
|
+
});
|