@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
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical "Needs attention" projection of guardian requests onto the
|
|
3
|
+
* home feed.
|
|
4
|
+
*
|
|
5
|
+
* One pending guardian request is exactly one feed item, keyed by
|
|
6
|
+
* `guardianFeedItemId(requestId)` so the feed writer's replace-in-place
|
|
7
|
+
* merge keeps it canonical: re-emits update the same row, and the
|
|
8
|
+
* terminal-status fan-out rewrites it into a receipt instead of leaving
|
|
9
|
+
* a stale actionable card. The item's `guardianRequest` field
|
|
10
|
+
* (`FeedItemGuardianRequestSchema`) is a read projection of the
|
|
11
|
+
* gateway-owned `guardian_requests` row, never an independent delivery
|
|
12
|
+
* record: clients derive every affordance from its `status`, and the
|
|
13
|
+
* daemon updates it wherever `withdrawGuardianRequestCards` settles the
|
|
14
|
+
* other surfaces.
|
|
15
|
+
*/
|
|
16
|
+
import { z } from "zod";
|
|
17
|
+
|
|
18
|
+
import {
|
|
19
|
+
type FeedItem,
|
|
20
|
+
type FeedItemGuardianIntent,
|
|
21
|
+
type FeedItemGuardianRequest,
|
|
22
|
+
FeedItemGuardianStatusSchema,
|
|
23
|
+
isPendingGuardianFeedItem,
|
|
24
|
+
} from "../api/responses/home.js";
|
|
25
|
+
import {
|
|
26
|
+
getGuardianRequest,
|
|
27
|
+
type GuardianRequestStatus,
|
|
28
|
+
type GuardianRequestWire,
|
|
29
|
+
listGuardianRequests,
|
|
30
|
+
} from "../channels/gateway-guardian-requests.js";
|
|
31
|
+
import { getConfig } from "../config/loader.js";
|
|
32
|
+
import {
|
|
33
|
+
appendFeedItem,
|
|
34
|
+
patchFeedItemContent,
|
|
35
|
+
readHomeFeed,
|
|
36
|
+
} from "../home/feed-writer.js";
|
|
37
|
+
import { buildSlackMessageDeepLinks } from "../messaging/providers/slack/deep-link.js";
|
|
38
|
+
import {
|
|
39
|
+
DEFAULT_USER_REFERENCE,
|
|
40
|
+
resolveGuardianName,
|
|
41
|
+
} from "../prompts/user-reference.js";
|
|
42
|
+
import { getLogger } from "../util/logger.js";
|
|
43
|
+
import {
|
|
44
|
+
buildToolApprovalSourceView,
|
|
45
|
+
type GuardianQuestionRequestKind,
|
|
46
|
+
LenientToolApprovalPayloadSchema,
|
|
47
|
+
resolveGuardianInstructionModeFromFields,
|
|
48
|
+
resolveGuardianQuestionInstructionMode,
|
|
49
|
+
} from "./guardian-question-mode.js";
|
|
50
|
+
import { readPayloadString } from "./notification-utils.js";
|
|
51
|
+
import type { NotificationDeliveryResult } from "./types.js";
|
|
52
|
+
|
|
53
|
+
const log = getLogger("guardian-feed-projection");
|
|
54
|
+
|
|
55
|
+
// The wire enum in `api/responses/home.ts` cannot import the gateway
|
|
56
|
+
// contract (the api directory is copied verbatim into client packages),
|
|
57
|
+
// so it mirrors `GuardianRequestStatusSchema` by value. This assignment
|
|
58
|
+
// fails to compile if the enums ever diverge in either direction.
|
|
59
|
+
type FeedStatus = z.infer<typeof FeedItemGuardianStatusSchema>;
|
|
60
|
+
type StatusEnumsAligned = [GuardianRequestStatus] extends [FeedStatus]
|
|
61
|
+
? [FeedStatus] extends [GuardianRequestStatus]
|
|
62
|
+
? true
|
|
63
|
+
: never
|
|
64
|
+
: never;
|
|
65
|
+
const _statusEnumsAligned: StatusEnumsAligned = true;
|
|
66
|
+
void _statusEnumsAligned;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The signal events that carry a guardian request. `guardian.question`
|
|
70
|
+
* covers tool approvals, tool grants, and questions; access requests
|
|
71
|
+
* predate that registry and still ride their own event with an implied
|
|
72
|
+
* `access_request` kind.
|
|
73
|
+
*/
|
|
74
|
+
export function isGuardianRequestSignalEvent(sourceEventName: string): boolean {
|
|
75
|
+
return (
|
|
76
|
+
sourceEventName === "guardian.question" ||
|
|
77
|
+
sourceEventName === "ingress.access_request"
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Feed-item id of the canonical projection for a guardian request. */
|
|
82
|
+
export function guardianFeedItemId(requestId: string): string {
|
|
83
|
+
return `guardian:${requestId}`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** The `requestId` a guardian feed-item id was built from, or null. */
|
|
87
|
+
export function requestIdFromGuardianFeedItemId(itemId: string): string | null {
|
|
88
|
+
return itemId.startsWith("guardian:")
|
|
89
|
+
? itemId.slice("guardian:".length)
|
|
90
|
+
: null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Guardian display name for receipt copy, or undefined when only the
|
|
95
|
+
* prompt-voice fallback ("my human") is available. A receipt with no
|
|
96
|
+
* decider label renders as the bare status word.
|
|
97
|
+
*/
|
|
98
|
+
function guardianReceiptName(): string | undefined {
|
|
99
|
+
const name = resolveGuardianName();
|
|
100
|
+
return name === DEFAULT_USER_REFERENCE ? undefined : name;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Build the pending `guardianRequest` projection for a `guardian.question`
|
|
105
|
+
* signal's feed item. Returns null when the payload does not carry the
|
|
106
|
+
* request id the projection is keyed by.
|
|
107
|
+
*
|
|
108
|
+
* `slackDelivery` is the signal's Slack delivery outcome, when one
|
|
109
|
+
* shipped: its destination chat + message ts locate the guardian-DM
|
|
110
|
+
* approval card, and the derived deep links are what the client's
|
|
111
|
+
* "Open in Slack" affordance opens.
|
|
112
|
+
*/
|
|
113
|
+
export function buildPendingGuardianProjection(
|
|
114
|
+
contextPayload: unknown,
|
|
115
|
+
slackDelivery?: NotificationDeliveryResult,
|
|
116
|
+
fallbackKind?: GuardianQuestionRequestKind,
|
|
117
|
+
): FeedItemGuardianRequest | null {
|
|
118
|
+
// Access-request payloads predate the kind registry and carry no
|
|
119
|
+
// `requestKind`; the producer's event name supplies it instead.
|
|
120
|
+
const normalizedPayload =
|
|
121
|
+
contextPayload &&
|
|
122
|
+
typeof contextPayload === "object" &&
|
|
123
|
+
!Array.isArray(contextPayload) &&
|
|
124
|
+
!("requestKind" in contextPayload) &&
|
|
125
|
+
fallbackKind
|
|
126
|
+
? { ...contextPayload, requestKind: fallbackKind }
|
|
127
|
+
: contextPayload;
|
|
128
|
+
const parsed = LenientToolApprovalPayloadSchema.safeParse(normalizedPayload);
|
|
129
|
+
if (!parsed.success) {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
const payload = parsed.data;
|
|
133
|
+
const requestId = payload.requestId?.trim();
|
|
134
|
+
if (!requestId) {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const intent = intentFromInstructionMode(
|
|
139
|
+
resolveGuardianQuestionInstructionMode(payload).mode,
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
const sourceView = buildToolApprovalSourceView(payload);
|
|
143
|
+
const slackLinks = buildSlackCardLinks(slackDelivery);
|
|
144
|
+
// Access-request payloads name their requester differently.
|
|
145
|
+
const requesterLabel =
|
|
146
|
+
payload.requesterIdentifier?.trim() ||
|
|
147
|
+
readPayloadString(contextPayload, "actorDisplayName")?.trim() ||
|
|
148
|
+
readPayloadString(contextPayload, "senderIdentifier")?.trim();
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
requestId,
|
|
152
|
+
kind: payload.requestKind,
|
|
153
|
+
intent,
|
|
154
|
+
status: "pending",
|
|
155
|
+
...(requesterLabel ? { requesterLabel } : {}),
|
|
156
|
+
...(payload.toolName?.trim() ? { toolName: payload.toolName.trim() } : {}),
|
|
157
|
+
...(sourceView?.channel ? { sourceChannel: sourceView.channel } : {}),
|
|
158
|
+
...(sourceView
|
|
159
|
+
? { sourceContextLabel: describeApprovalSourceContext(sourceView) }
|
|
160
|
+
: {}),
|
|
161
|
+
...(sourceView?.permalink ? { sourceUrl: sourceView.permalink } : {}),
|
|
162
|
+
...(slackLinks?.webUrl ? { slackCardUrl: slackLinks.webUrl } : {}),
|
|
163
|
+
...(slackLinks?.appUrl ? { slackCardAppUrl: slackLinks.appUrl } : {}),
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* A request in answer mode asks the guardian a question; every other
|
|
169
|
+
* mode asks for an approval. The instruction mode already encodes this
|
|
170
|
+
* (it decides whether cards render answer options or approve/reject),
|
|
171
|
+
* so the projection's `intent` is a straight reading of it.
|
|
172
|
+
*/
|
|
173
|
+
function intentFromInstructionMode(
|
|
174
|
+
mode: "approval" | "answer" | undefined,
|
|
175
|
+
): FeedItemGuardianIntent {
|
|
176
|
+
return mode === "answer" ? "question" : "approval";
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Display label for the originating chat, mirroring the wording the
|
|
181
|
+
* in-app approval card's source row uses (`sourceMetadataRow` in
|
|
182
|
+
* `approval-card-data.ts`): Slack chats are named, other channels fall
|
|
183
|
+
* back to the channel id.
|
|
184
|
+
*/
|
|
185
|
+
function describeApprovalSourceContext(view: {
|
|
186
|
+
channel: string;
|
|
187
|
+
chatId?: string | null;
|
|
188
|
+
isSlackDm: boolean;
|
|
189
|
+
}): string {
|
|
190
|
+
if (view.channel === "slack" && view.chatId) {
|
|
191
|
+
return view.isSlackDm ? "Slack direct message" : `Slack #${view.chatId}`;
|
|
192
|
+
}
|
|
193
|
+
return view.channel;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** Deep links to the Slack guardian-DM approval card, when one shipped. */
|
|
197
|
+
function buildSlackCardLinks(
|
|
198
|
+
slackDelivery?: NotificationDeliveryResult,
|
|
199
|
+
): { appUrl?: string; webUrl?: string } | undefined {
|
|
200
|
+
if (
|
|
201
|
+
!slackDelivery ||
|
|
202
|
+
slackDelivery.status !== "sent" ||
|
|
203
|
+
!slackDelivery.destination ||
|
|
204
|
+
!slackDelivery.messageId
|
|
205
|
+
) {
|
|
206
|
+
return undefined;
|
|
207
|
+
}
|
|
208
|
+
const slackConfig = getConfig().slack;
|
|
209
|
+
return buildSlackMessageDeepLinks({
|
|
210
|
+
teamId: slackConfig?.teamId,
|
|
211
|
+
teamUrl: slackConfig?.teamUrl,
|
|
212
|
+
channelId: slackDelivery.destination,
|
|
213
|
+
messageTs: slackDelivery.messageId,
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export interface GuardianFeedReceiptParams {
|
|
218
|
+
requestId: string;
|
|
219
|
+
status: GuardianRequestStatus;
|
|
220
|
+
/** Action that produced the terminal status, when it was a decision. */
|
|
221
|
+
decidedAction?: string;
|
|
222
|
+
/** Epoch ms the request reached its terminal status. */
|
|
223
|
+
decidedAtMs?: number;
|
|
224
|
+
/** Non-decision cause of the terminal status (e.g. "superseded"). */
|
|
225
|
+
terminalReason?: string;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Rewrite a request's canonical feed item into its terminal receipt.
|
|
230
|
+
*
|
|
231
|
+
* The item keeps its title and summary; `guardianRequest` flips to the
|
|
232
|
+
* terminal status (which is what removes the client's action buttons),
|
|
233
|
+
* and urgency drops to `medium` so the item leaves the protected
|
|
234
|
+
* "Needs attention" treatment and becomes an ordinary clearable
|
|
235
|
+
* notification. Returns false only when the write itself failed, so the
|
|
236
|
+
* withdrawal fan-out can hold its per-request receipt back and retry
|
|
237
|
+
* (same contract as the other surfaces it settles). A request with no
|
|
238
|
+
* projection item resolves true: the pending writer converges the
|
|
239
|
+
* write-vs-resolve race by re-checking canonical status after its
|
|
240
|
+
* append, so there is nothing here for a retry to fix.
|
|
241
|
+
*/
|
|
242
|
+
export async function writeGuardianFeedReceipt(
|
|
243
|
+
params: GuardianFeedReceiptParams,
|
|
244
|
+
): Promise<boolean> {
|
|
245
|
+
const itemId = guardianFeedItemId(params.requestId);
|
|
246
|
+
try {
|
|
247
|
+
// `patchFeedItemContent` resolves null both for a missing item and
|
|
248
|
+
// for a failed file write, and only the second may gate a retry.
|
|
249
|
+
// The pre-check splits them: an id present here that nulls below is
|
|
250
|
+
// a lost write (or a concurrent removal, where a retry is harmless).
|
|
251
|
+
if (!readHomeFeed().items.some((item) => item.id === itemId)) {
|
|
252
|
+
// No projection item: either the request predates the projection
|
|
253
|
+
// or the pending write raced the resolution and has not landed
|
|
254
|
+
// yet. The pending writer converges the race by re-checking
|
|
255
|
+
// canonical status after its append.
|
|
256
|
+
log.debug(
|
|
257
|
+
{ requestId: params.requestId, status: params.status },
|
|
258
|
+
"No guardian feed item to receipt",
|
|
259
|
+
);
|
|
260
|
+
return true;
|
|
261
|
+
}
|
|
262
|
+
const updated = await patchFeedItemContent(itemId, {
|
|
263
|
+
urgency: "medium",
|
|
264
|
+
guardianRequest: (existing) => ({
|
|
265
|
+
...existing,
|
|
266
|
+
status: params.status,
|
|
267
|
+
...(params.decidedAction
|
|
268
|
+
? { decidedAction: params.decidedAction }
|
|
269
|
+
: {}),
|
|
270
|
+
// A decider label only when a person decided: a non-decision
|
|
271
|
+
// terminal (superseded, expired) must not read as the guardian's
|
|
272
|
+
// own rejection.
|
|
273
|
+
...((params.status === "approved" || params.status === "denied") &&
|
|
274
|
+
!params.terminalReason
|
|
275
|
+
? decidedByLabelPatch()
|
|
276
|
+
: {}),
|
|
277
|
+
decidedAt: new Date(params.decidedAtMs ?? Date.now()).toISOString(),
|
|
278
|
+
...(params.terminalReason
|
|
279
|
+
? { terminalReason: params.terminalReason }
|
|
280
|
+
: {}),
|
|
281
|
+
}),
|
|
282
|
+
});
|
|
283
|
+
if (!updated) {
|
|
284
|
+
log.warn(
|
|
285
|
+
{ requestId: params.requestId, status: params.status },
|
|
286
|
+
"Guardian feed receipt did not persist",
|
|
287
|
+
);
|
|
288
|
+
return false;
|
|
289
|
+
}
|
|
290
|
+
return true;
|
|
291
|
+
} catch (err) {
|
|
292
|
+
log.warn(
|
|
293
|
+
{ err, requestId: params.requestId, status: params.status },
|
|
294
|
+
"Failed to write guardian feed receipt",
|
|
295
|
+
);
|
|
296
|
+
return false;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/** `{ decidedByLabel }` when a real display name exists, `{}` otherwise. */
|
|
301
|
+
function decidedByLabelPatch(): Pick<
|
|
302
|
+
Partial<FeedItemGuardianRequest>,
|
|
303
|
+
"decidedByLabel"
|
|
304
|
+
> {
|
|
305
|
+
const name = guardianReceiptName();
|
|
306
|
+
return name === undefined ? {} : { decidedByLabel: name };
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Rewrite a request's feed item into its receipt when the canonical row
|
|
311
|
+
* is already terminal. Converges the write-vs-resolve race from the
|
|
312
|
+
* writer's side: a request decided while a pending item was being
|
|
313
|
+
* written already ran its receipt fan-out against an item that did not
|
|
314
|
+
* exist yet. Best-effort: a transport failure skips (never misreads a
|
|
315
|
+
* hiccup as "request gone"), and the reconciliation sweep heals what
|
|
316
|
+
* this misses.
|
|
317
|
+
*/
|
|
318
|
+
export async function receiptGuardianFeedItemIfRequestTerminal(
|
|
319
|
+
requestId: string,
|
|
320
|
+
): Promise<void> {
|
|
321
|
+
try {
|
|
322
|
+
const request = await getGuardianRequest(requestId);
|
|
323
|
+
if (!request || request.status === "pending") {
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
await writeGuardianFeedReceipt({
|
|
327
|
+
requestId,
|
|
328
|
+
status: request.status,
|
|
329
|
+
decidedAtMs: request.updatedAt,
|
|
330
|
+
});
|
|
331
|
+
} catch (err) {
|
|
332
|
+
log.warn(
|
|
333
|
+
{ err, requestId },
|
|
334
|
+
"Failed to converge guardian feed item with terminal request",
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// ---------------------------------------------------------------------------
|
|
340
|
+
// Reconciliation
|
|
341
|
+
// ---------------------------------------------------------------------------
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Ceiling on backfills per reconciliation round, mirroring the expiry
|
|
345
|
+
* sweep's batch bound: one round's IPC and file writes stay bounded
|
|
346
|
+
* however large a backlog grows, and the remainder drains on later
|
|
347
|
+
* rounds (the sweep runs every minute).
|
|
348
|
+
*/
|
|
349
|
+
const RECONCILE_BACKFILL_LIMIT = 50;
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Converge the feed against canonical guardian state. Run from the
|
|
353
|
+
* guardian expiry sweep, so drift heals on the same cadence expiry does:
|
|
354
|
+
*
|
|
355
|
+
* 1. A pending request with no feed item (it predates the projection,
|
|
356
|
+
* or its write was lost) gets one backfilled from the request row.
|
|
357
|
+
* No conversation is generated: the item links to the request's
|
|
358
|
+
* source conversation when it names one, and nothing else.
|
|
359
|
+
* 2. A feed item still offering actions for a request that is
|
|
360
|
+
* terminal (or gone) is rewritten into its receipt, and the drift
|
|
361
|
+
* is logged loudly: a terminal request with an actionable
|
|
362
|
+
* projection is the invariant this system exists to prevent.
|
|
363
|
+
*
|
|
364
|
+
* Skips the round when the gateway is unreachable; an empty list must
|
|
365
|
+
* not be mistaken for "everything resolved".
|
|
366
|
+
*/
|
|
367
|
+
export async function reconcileGuardianFeedProjections(): Promise<void> {
|
|
368
|
+
let pending: GuardianRequestWire[];
|
|
369
|
+
try {
|
|
370
|
+
pending = await listGuardianRequests({ status: "pending" });
|
|
371
|
+
} catch (err) {
|
|
372
|
+
log.debug(
|
|
373
|
+
{ err },
|
|
374
|
+
"Guardian feed reconciliation skipped: gateway unreachable",
|
|
375
|
+
);
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const items = readHomeFeed().items;
|
|
380
|
+
const itemsByRequestId = new Map<string, FeedItem>();
|
|
381
|
+
for (const item of items) {
|
|
382
|
+
const requestId = requestIdFromGuardianFeedItemId(item.id);
|
|
383
|
+
if (requestId) {
|
|
384
|
+
itemsByRequestId.set(requestId, item);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const pendingIds = new Set(pending.map((request) => request.id));
|
|
389
|
+
|
|
390
|
+
const missing = pending.filter(
|
|
391
|
+
(request) => !itemsByRequestId.has(request.id),
|
|
392
|
+
);
|
|
393
|
+
if (missing.length > RECONCILE_BACKFILL_LIMIT) {
|
|
394
|
+
log.warn(
|
|
395
|
+
{ missing: missing.length, limit: RECONCILE_BACKFILL_LIMIT },
|
|
396
|
+
"Guardian feed backfill backlog exceeds one round; remainder drains next round",
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
for (const request of missing.slice(0, RECONCILE_BACKFILL_LIMIT)) {
|
|
400
|
+
log.info(
|
|
401
|
+
{ requestId: request.id, kind: request.kind, surface: "home_feed" },
|
|
402
|
+
"Backfilling feed projection for pending guardian request",
|
|
403
|
+
);
|
|
404
|
+
await appendFeedItem(buildBackfillGuardianFeedItem(request));
|
|
405
|
+
// The request may have resolved between the pending listing and this
|
|
406
|
+
// append; without the recheck the fresh item would restore live
|
|
407
|
+
// actions for a terminal request until the next round.
|
|
408
|
+
await receiptGuardianFeedItemIfRequestTerminal(request.id);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
for (const [requestId, item] of itemsByRequestId) {
|
|
412
|
+
if (!isPendingGuardianFeedItem(item) || pendingIds.has(requestId)) {
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
// The item says pending but the canonical row does not: read the row
|
|
416
|
+
// to learn its terminal status. Deliberately the throwing lookup: the
|
|
417
|
+
// degrading variant returns null on a transport failure too, and a
|
|
418
|
+
// hiccup must skip the row rather than receipt a live request as
|
|
419
|
+
// cancelled. A row absent from the gateway reads as
|
|
420
|
+
// cancelled.
|
|
421
|
+
let request: GuardianRequestWire | null;
|
|
422
|
+
try {
|
|
423
|
+
request = await getGuardianRequest(requestId);
|
|
424
|
+
} catch {
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
if (request?.status === "pending") {
|
|
428
|
+
// Raced a fresh listing; the request really is pending.
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
log.warn(
|
|
432
|
+
{
|
|
433
|
+
requestId,
|
|
434
|
+
surface: "home_feed",
|
|
435
|
+
canonicalStatus: request?.status ?? "missing",
|
|
436
|
+
},
|
|
437
|
+
"Terminal guardian request still had an actionable feed projection",
|
|
438
|
+
);
|
|
439
|
+
await writeGuardianFeedReceipt({
|
|
440
|
+
requestId,
|
|
441
|
+
status: request?.status ?? "cancelled",
|
|
442
|
+
...(request ? { decidedAtMs: request.updatedAt } : {}),
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Minimal feed item for a pending request discovered without one,
|
|
449
|
+
* assembled from the canonical row alone (reconciliation has no
|
|
450
|
+
* notification copy to draw on). `questionText` is the human-readable
|
|
451
|
+
* ask every producer writes; `activityText` names the action in plain
|
|
452
|
+
* language when the producer recorded one.
|
|
453
|
+
*/
|
|
454
|
+
function buildBackfillGuardianFeedItem(request: GuardianRequestWire): FeedItem {
|
|
455
|
+
const summary =
|
|
456
|
+
request.questionText?.trim() ||
|
|
457
|
+
request.activityText?.trim() ||
|
|
458
|
+
`Guardian request ${request.requestCode ?? request.id}`;
|
|
459
|
+
const intent = intentFromInstructionMode(
|
|
460
|
+
resolveGuardianInstructionModeFromFields(
|
|
461
|
+
request.kind,
|
|
462
|
+
request.toolName ?? undefined,
|
|
463
|
+
)?.mode,
|
|
464
|
+
);
|
|
465
|
+
const now = new Date().toISOString();
|
|
466
|
+
return {
|
|
467
|
+
id: guardianFeedItemId(request.id),
|
|
468
|
+
type: "notification",
|
|
469
|
+
priority: 50,
|
|
470
|
+
summary,
|
|
471
|
+
timestamp: new Date(request.createdAt).toISOString(),
|
|
472
|
+
createdAt: now,
|
|
473
|
+
status: "new",
|
|
474
|
+
category: "security",
|
|
475
|
+
noteworthy: true,
|
|
476
|
+
urgency: "high",
|
|
477
|
+
detailPanel: { kind: "permissionChat" },
|
|
478
|
+
...(request.sourceConversationId
|
|
479
|
+
? { conversationId: request.sourceConversationId }
|
|
480
|
+
: {}),
|
|
481
|
+
guardianRequest: {
|
|
482
|
+
requestId: request.id,
|
|
483
|
+
kind: request.kind,
|
|
484
|
+
intent,
|
|
485
|
+
status: "pending",
|
|
486
|
+
...(request.toolName ? { toolName: request.toolName } : {}),
|
|
487
|
+
...(request.sourceChannel
|
|
488
|
+
? { sourceChannel: request.sourceChannel }
|
|
489
|
+
: {}),
|
|
490
|
+
},
|
|
491
|
+
};
|
|
492
|
+
}
|
|
@@ -29,6 +29,12 @@ import { getLogger } from "../util/logger.js";
|
|
|
29
29
|
import { normalizeTitle, stripMarkdown } from "../util/short-title.js";
|
|
30
30
|
import { isConversationSeedSane } from "./conversation-seed-composer.js";
|
|
31
31
|
import { deriveTitle } from "./copy-composer.js";
|
|
32
|
+
import {
|
|
33
|
+
buildPendingGuardianProjection,
|
|
34
|
+
guardianFeedItemId,
|
|
35
|
+
isGuardianRequestSignalEvent,
|
|
36
|
+
receiptGuardianFeedItemIfRequestTerminal,
|
|
37
|
+
} from "./guardian-feed-projection.js";
|
|
32
38
|
import { readPayloadString } from "./notification-utils.js";
|
|
33
39
|
import type { NotificationSignal } from "./signal.js";
|
|
34
40
|
import type {
|
|
@@ -79,6 +85,7 @@ export async function writeHomeFeedItemForSignal(
|
|
|
79
85
|
signal: NotificationSignal,
|
|
80
86
|
decision: NotificationDecision,
|
|
81
87
|
vellumDelivery?: NotificationDeliveryResult,
|
|
88
|
+
deliveryResults?: NotificationDeliveryResult[],
|
|
82
89
|
): Promise<FeedItem | null> {
|
|
83
90
|
const { mirror, sourceConversationId, sourceScheduleJobId } =
|
|
84
91
|
resolveHomeFeedMirror(signal, vellumDelivery?.conversationId);
|
|
@@ -107,7 +114,14 @@ export async function writeHomeFeedItemForSignal(
|
|
|
107
114
|
: undefined) ||
|
|
108
115
|
renderedCopy?.body?.trim() ||
|
|
109
116
|
payloadBody?.trim() ||
|
|
110
|
-
|
|
117
|
+
// Guardian requests must never be dropped for missing copy: the
|
|
118
|
+
// producer's questionText (or, for access requests, the triggering
|
|
119
|
+
// message preview) is already the human-readable ask.
|
|
120
|
+
(isGuardianRequestSignalEvent(signal.sourceEventName)
|
|
121
|
+
? (readPayloadString(signal.contextPayload, "questionText")?.trim() ??
|
|
122
|
+
readPayloadString(signal.contextPayload, "messagePreview")?.trim() ??
|
|
123
|
+
"")
|
|
124
|
+
: "");
|
|
111
125
|
if (!resolvedSummary) {
|
|
112
126
|
log.warn(
|
|
113
127
|
{ signalId: signal.signalId, sourceEventName: signal.sourceEventName },
|
|
@@ -155,8 +169,28 @@ export async function writeHomeFeedItemForSignal(
|
|
|
155
169
|
? { ...(baseMetadata ?? {}), scheduleId }
|
|
156
170
|
: baseMetadata;
|
|
157
171
|
|
|
172
|
+
// A guardian request's item is keyed by the request rather than the
|
|
173
|
+
// signal, so the feed's replace-in-place merge keeps exactly one
|
|
174
|
+
// canonical "Needs attention" item per request across re-emits and
|
|
175
|
+
// the terminal-status receipt rewrite.
|
|
176
|
+
const guardianProjection = isGuardianRequestSignalEvent(
|
|
177
|
+
signal.sourceEventName,
|
|
178
|
+
)
|
|
179
|
+
? buildPendingGuardianProjection(
|
|
180
|
+
signal.contextPayload,
|
|
181
|
+
deliveryResults?.find(
|
|
182
|
+
(r) => r.channel === "slack" && r.status === "sent",
|
|
183
|
+
),
|
|
184
|
+
signal.sourceEventName === "ingress.access_request"
|
|
185
|
+
? "access_request"
|
|
186
|
+
: undefined,
|
|
187
|
+
)
|
|
188
|
+
: null;
|
|
189
|
+
|
|
158
190
|
const item: FeedItem = {
|
|
159
|
-
id:
|
|
191
|
+
id: guardianProjection
|
|
192
|
+
? guardianFeedItemId(guardianProjection.requestId)
|
|
193
|
+
: `notif:${signal.signalId}`,
|
|
160
194
|
type: "notification",
|
|
161
195
|
priority: 50,
|
|
162
196
|
title: resolvedTitle,
|
|
@@ -167,6 +201,7 @@ export async function writeHomeFeedItemForSignal(
|
|
|
167
201
|
category,
|
|
168
202
|
noteworthy: deriveNoteworthy(signal),
|
|
169
203
|
fromAssistant: signal.sourceChannel === "assistant_tool",
|
|
204
|
+
...(guardianProjection ? { guardianRequest: guardianProjection } : {}),
|
|
170
205
|
...(urgency ? { urgency } : {}),
|
|
171
206
|
...(sourceConversationId ? { conversationId: sourceConversationId } : {}),
|
|
172
207
|
...(panelKind ? { detailPanel: { kind: panelKind } } : {}),
|
|
@@ -205,6 +240,16 @@ export async function writeHomeFeedItemForSignal(
|
|
|
205
240
|
: item;
|
|
206
241
|
|
|
207
242
|
await appendFeedItem(card);
|
|
243
|
+
|
|
244
|
+
// Converge the write-vs-resolve race: a request decided while this
|
|
245
|
+
// write was in flight already ran its receipt fan-out against an item
|
|
246
|
+
// that did not exist yet. Mirrors the delivery recorder's
|
|
247
|
+
// `withdrawIfRequestAlreadyTerminal`.
|
|
248
|
+
if (guardianProjection) {
|
|
249
|
+
await receiptGuardianFeedItemIfRequestTerminal(
|
|
250
|
+
guardianProjection.requestId,
|
|
251
|
+
);
|
|
252
|
+
}
|
|
208
253
|
return card;
|
|
209
254
|
}
|
|
210
255
|
|
|
@@ -418,15 +463,12 @@ function deriveDetailPanelKind(
|
|
|
418
463
|
return "toolPermission";
|
|
419
464
|
}
|
|
420
465
|
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
if (kind === "tool_approval" || kind === "tool_grant_request") {
|
|
428
|
-
return "permissionChat";
|
|
429
|
-
}
|
|
466
|
+
// Every guardian request opens the permission-style panel: the client
|
|
467
|
+
// dispatches the guardian card off the item's `guardianRequest`
|
|
468
|
+
// projection, and the panel kind keeps pre-projection clients (and
|
|
469
|
+
// bucket-fallback derivations) treating the row as an approval.
|
|
470
|
+
if (isGuardianRequestSignalEvent(signal.sourceEventName)) {
|
|
471
|
+
return "permissionChat";
|
|
430
472
|
}
|
|
431
473
|
|
|
432
474
|
return undefined;
|
|
@@ -479,6 +521,12 @@ function resolveHomeFeedMirror(
|
|
|
479
521
|
if (signal.attentionHints.isAsyncBackground) {
|
|
480
522
|
return { mirror: true, sourceConversationId, sourceScheduleJobId };
|
|
481
523
|
}
|
|
524
|
+
// Guardian requests always project into the feed: the "Needs
|
|
525
|
+
// attention" item is the request's canonical home, and the request
|
|
526
|
+
// blocks on the guardian whatever kind of conversation raised it.
|
|
527
|
+
if (isGuardianRequestSignalEvent(signal.sourceEventName)) {
|
|
528
|
+
return { mirror: true, sourceConversationId, sourceScheduleJobId };
|
|
529
|
+
}
|
|
482
530
|
if (isBackgroundConversationType(sourceRow?.conversationType)) {
|
|
483
531
|
return { mirror: true, sourceConversationId, sourceScheduleJobId };
|
|
484
532
|
}
|
|
@@ -358,6 +358,28 @@ describe("BYOOAuthConnection", () => {
|
|
|
358
358
|
expect(headers.get("Content-Type")).toBe("application/json");
|
|
359
359
|
});
|
|
360
360
|
|
|
361
|
+
test("sends a Buffer body as raw bytes under the caller's Content-Type", async () => {
|
|
362
|
+
await setupCredential("google");
|
|
363
|
+
const conn = createConnection();
|
|
364
|
+
const binary = Buffer.from([
|
|
365
|
+
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0xff, 0xfe,
|
|
366
|
+
]);
|
|
367
|
+
|
|
368
|
+
await conn.request({
|
|
369
|
+
method: "POST",
|
|
370
|
+
path: "/upload/drive/v3/files",
|
|
371
|
+
headers: { "Content-Type": "application/pdf" },
|
|
372
|
+
body: binary,
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
const [, init] = mockFetch.mock.calls[0];
|
|
376
|
+
const sent = (init as RequestInit).body;
|
|
377
|
+
expect(Buffer.isBuffer(sent)).toBe(true);
|
|
378
|
+
expect(Buffer.from(sent as Buffer).equals(binary)).toBe(true);
|
|
379
|
+
const headers = (init as RequestInit).headers as Headers;
|
|
380
|
+
expect(headers.get("Content-Type")).toBe("application/pdf");
|
|
381
|
+
});
|
|
382
|
+
|
|
361
383
|
test("sends a string body verbatim under the caller's Content-Type", async () => {
|
|
362
384
|
await setupCredential("google");
|
|
363
385
|
const conn = createConnection();
|
|
@@ -14,7 +14,7 @@ import type {
|
|
|
14
14
|
OAuthConnectionRequest,
|
|
15
15
|
OAuthConnectionResponse,
|
|
16
16
|
} from "./connection.js";
|
|
17
|
-
import { decodeOAuthResponseBytes } from "./connection.js";
|
|
17
|
+
import { decodeOAuthResponseBytes, isBinaryOAuthBody } from "./connection.js";
|
|
18
18
|
|
|
19
19
|
const log = getLogger("byo-oauth-connection");
|
|
20
20
|
|
|
@@ -83,15 +83,17 @@ export class BYOOAuthConnection implements OAuthConnection {
|
|
|
83
83
|
|
|
84
84
|
// A string body is already in its wire form (multipart, XML,
|
|
85
85
|
// form-encoded, or pre-serialized JSON) and travels verbatim under
|
|
86
|
-
// the caller's own Content-Type.
|
|
87
|
-
// as JSON and get the JSON
|
|
86
|
+
// the caller's own Content-Type. A Buffer travels as raw bytes.
|
|
87
|
+
// Objects and arrays are serialized as JSON and get the JSON
|
|
88
|
+
// Content-Type by default.
|
|
88
89
|
const hasBody = req.body !== undefined && req.body !== null;
|
|
90
|
+
const binaryBody = isBinaryOAuthBody(req.body) ? req.body : undefined;
|
|
89
91
|
const rawBody = typeof req.body === "string" ? req.body : undefined;
|
|
90
92
|
|
|
91
93
|
// Use the Headers API for case-insensitive merging. Set defaults
|
|
92
94
|
// first so caller-supplied headers (in any casing) override them.
|
|
93
95
|
const headers = new Headers();
|
|
94
|
-
if (hasBody && rawBody === undefined) {
|
|
96
|
+
if (hasBody && rawBody === undefined && binaryBody === undefined) {
|
|
95
97
|
headers.set("Content-Type", "application/json");
|
|
96
98
|
}
|
|
97
99
|
if (req.headers) {
|
|
@@ -106,7 +108,11 @@ export class BYOOAuthConnection implements OAuthConnection {
|
|
|
106
108
|
const resp = await fetch(fullUrl, {
|
|
107
109
|
method: req.method,
|
|
108
110
|
headers,
|
|
109
|
-
body: hasBody
|
|
111
|
+
body: hasBody
|
|
112
|
+
? (binaryBody !== undefined
|
|
113
|
+
? Buffer.from(binaryBody)
|
|
114
|
+
: (rawBody ?? JSON.stringify(req.body)))
|
|
115
|
+
: undefined,
|
|
110
116
|
signal: req.signal
|
|
111
117
|
? AbortSignal.any([
|
|
112
118
|
req.signal,
|