@vellumai/assistant 0.8.12 → 0.9.0-staging.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +0 -14
- package/ARCHITECTURE.md +45 -45
- package/README.md +1 -1
- package/bun.lock +200 -154
- package/docs/architecture/integrations.md +3 -3
- package/docs/architecture/memory.md +2 -2
- package/docs/architecture/security.md +10 -10
- package/docs/runbook-trusted-contacts.md +12 -12
- package/docs/skills.md +6 -6
- package/docs/workflows-testing.md +221 -0
- package/docs/workflows.md +510 -0
- package/examples/plugins/echo/README.md +5 -5
- package/knip.json +2 -0
- package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +105 -0
- package/node_modules/@vellumai/gateway-client/src/index.ts +12 -0
- package/openapi.yaml +7197 -5708
- package/package.json +8 -4
- package/scripts/generate-openapi.ts +66 -114
- package/src/__tests__/access-request-seed-content-blocks.test.ts +213 -0
- package/src/__tests__/adaptive-thinking-repair.test.ts +32 -3
- package/src/__tests__/agent-loop-output-hooks.test.ts +183 -0
- package/src/__tests__/agent-loop-regrowth-guard.test.ts +506 -0
- package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +2 -0
- package/src/__tests__/agent-wake-override-profile.test.ts +77 -0
- package/src/__tests__/app-compiler.test.ts +7 -1
- package/src/__tests__/app-dir-path-guard.test.ts +27 -3
- package/src/__tests__/app-executors.test.ts +43 -0
- package/src/__tests__/approval-cascade.test.ts +0 -5
- package/src/__tests__/approval-routes-http.test.ts +91 -0
- package/src/__tests__/assistant-stream-state.test.ts +107 -0
- package/src/__tests__/browser-fill-credential.test.ts +3 -3
- package/src/__tests__/bundled-skill-retrieval-guard.test.ts +1 -1
- package/src/__tests__/compaction-events.test.ts +63 -7
- package/src/__tests__/compaction-trail-store.test.ts +74 -1
- package/src/__tests__/compaction.benchmark.test.ts +63 -41
- package/src/__tests__/compactor-low-watermark-cut.test.ts +349 -0
- package/src/__tests__/context-window-manager-compact-retry.test.ts +64 -0
- package/src/__tests__/conversation-abort-tool-results.test.ts +0 -5
- package/src/__tests__/conversation-confirmation-signals.test.ts +0 -5
- package/src/__tests__/conversation-history-web-search.test.ts +7 -0
- package/src/__tests__/conversation-process-callsite.test.ts +0 -5
- package/src/__tests__/conversation-provider-retry-repair.test.ts +0 -5
- package/src/__tests__/conversation-queue.test.ts +0 -5
- package/src/__tests__/conversation-slash-queue.test.ts +0 -5
- package/src/__tests__/conversation-slash-unknown.test.ts +0 -5
- package/src/__tests__/conversation-speed-override.test.ts +0 -5
- package/src/__tests__/conversation-surfaces-data-persist.test.ts +97 -0
- package/src/__tests__/conversation-surfaces-task-progress.test.ts +67 -0
- package/src/__tests__/conversation-usage.test.ts +2 -0
- package/src/__tests__/conversation-workspace-injection.test.ts +0 -5
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +0 -5
- package/src/__tests__/credential-broker-browser-fill.test.ts +2 -2
- package/src/__tests__/credential-broker-server-use.test.ts +2 -2
- package/src/__tests__/credential-broker.test.ts +1 -1
- package/src/__tests__/credential-prompt-route.test.ts +417 -0
- package/src/__tests__/credential-security-invariants.test.ts +1 -0
- package/src/__tests__/credential-vault.test.ts +37 -0
- package/src/__tests__/db-schedule-syntax-migration.test.ts +24 -0
- package/src/__tests__/dynamic-page-surface.test.ts +219 -0
- package/src/__tests__/empty-state-greeting-cache.test.ts +94 -0
- package/src/__tests__/gateway-flag-listener.test.ts +24 -7
- package/src/__tests__/guardian-action-sweep.test.ts +56 -219
- package/src/__tests__/guardian-routing-invariants.test.ts +138 -0
- package/src/__tests__/helpers/channel-test-adapter.ts +0 -2
- package/src/__tests__/list-messages-hidden-metadata.test.ts +99 -0
- package/src/__tests__/llm-request-log-source-clickhouse.test.ts +87 -1
- package/src/__tests__/llm-resolver.test.ts +115 -0
- package/src/__tests__/managed-profile-guard.test.ts +6 -5
- package/src/__tests__/max-tokens-continue-hook.test.ts +184 -0
- package/src/__tests__/media-generate-image.test.ts +20 -9
- package/src/__tests__/mock-gateway-ipc.ts +23 -0
- package/src/__tests__/model-intents.test.ts +1 -1
- package/src/__tests__/normalize-onboarding.test.ts +26 -0
- package/src/__tests__/notification-decision-strategy.test.ts +4 -2
- package/src/__tests__/notification-telegram-adapter.test.ts +21 -3
- package/src/__tests__/pending-interactions-resolved-event.test.ts +62 -0
- package/src/__tests__/post-turn-tool-result-truncation.test.ts +72 -18
- package/src/__tests__/require-fresh-approval.test.ts +425 -1
- package/src/__tests__/resolve-app-id.test.ts +56 -0
- package/src/__tests__/runtime-events-sse-parity.test.ts +2 -0
- package/src/__tests__/schedule-routes-workflow-validation.test.ts +408 -0
- package/src/__tests__/schedule-routes.test.ts +257 -4
- package/src/__tests__/schedule-store.test.ts +60 -0
- package/src/__tests__/schedule-tools.test.ts +247 -2
- package/src/__tests__/skill-execute-input.test.ts +85 -0
- package/src/__tests__/skill-secret-handling-guard.test.ts +21 -20
- package/src/__tests__/skills.test.ts +3 -3
- package/src/__tests__/slack-app-setup-skill-regression.test.ts +1 -1
- package/src/__tests__/subagent-tool-filtering.test.ts +50 -0
- package/src/__tests__/subagent-tool-gate-mode.test.ts +547 -0
- package/src/__tests__/system-prompt.test.ts +1 -1
- package/src/__tests__/task-progress-nudge-hook.test.ts +372 -0
- package/src/__tests__/task-scheduler.test.ts +299 -0
- package/src/__tests__/tool-approval-seed-content-blocks.test.ts +209 -0
- package/src/__tests__/tool-result-spool.test.ts +3 -1
- package/src/__tests__/workspace-migration-102-preserve-heartbeat-enabled-for-existing-workspaces.test.ts +181 -0
- package/src/__tests__/workspace-migration-103-upgrade-quality-profile-to-opus-4-8.test.ts +174 -0
- package/src/agent/compaction-circuit.ts +11 -0
- package/src/agent/loop.ts +181 -12
- package/src/api/constants/call-sites.ts +12 -0
- package/src/api/events/assistant-thinking-delta.ts +10 -0
- package/src/api/events/usage-update.ts +7 -0
- package/src/api/index.ts +4 -1
- package/src/api/responses/memory-v3-selection-log.ts +18 -11
- package/src/approvals/approval-primitive.ts +2 -2
- package/src/background-wake/background-wake-routes.test.ts +5 -2
- package/src/bundler/compiler-tools.ts +1 -1
- package/src/bundler/package-resolver.ts +0 -1
- package/src/calls/call-domain.ts +1 -1
- package/src/calls/guardian-action-sweep.ts +16 -93
- package/src/cli/AGENTS.md +4 -0
- package/src/cli/commands/__tests__/schedules.test.ts +430 -1
- package/src/cli/commands/credentials.ts +28 -24
- package/src/cli/commands/image-generation.ts +23 -9
- package/src/cli/commands/notifications.ts +1 -1
- package/src/cli/commands/plugins.ts +89 -46
- package/src/cli/commands/schedules.ts +384 -11
- package/src/cli/lib/__tests__/inspect-plugin.test.ts +69 -5
- package/src/cli/lib/__tests__/install-from-github.test.ts +15 -0
- package/src/cli/lib/__tests__/upgrade-plugin.test.ts +81 -4
- package/src/cli/lib/inspect-plugin.ts +62 -1
- package/src/cli/lib/install-from-github.ts +52 -5
- package/src/cli/lib/upgrade-plugin.ts +18 -0
- package/src/config/__tests__/workflows-schema.test.ts +60 -0
- package/src/config/bundled-skills/acp/SKILL.md +2 -2
- package/src/config/bundled-skills/app-builder/SKILL.md +1 -1
- package/src/config/bundled-skills/app-builder/tools/app-create.ts +6 -1
- package/src/config/bundled-skills/app-builder/tools/app-generate-icon.ts +7 -1
- package/src/config/bundled-skills/app-builder/tools/app-refresh.ts +7 -1
- package/src/config/bundled-skills/app-builder/tools/app-update.ts +10 -1
- package/src/config/bundled-skills/image-studio/SKILL.md +66 -19
- package/src/config/bundled-skills/image-studio/TOOLS.json +1 -6
- package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +22 -3
- package/src/config/bundled-skills/personal-page/SKILL.md +57 -0
- package/src/config/bundled-skills/personal-page/TOOLS.json +27 -0
- package/src/config/bundled-skills/personal-page/tools/app-refresh.ts +17 -0
- package/src/config/bundled-skills/schedule/SKILL.md +7 -2
- package/src/config/bundled-skills/schedule/TOOLS.json +48 -4
- package/src/config/bundled-skills/workflows/SKILL.md +214 -0
- package/src/config/bundled-skills/workflows/TOOLS.json +84 -0
- package/src/config/bundled-skills/workflows/tools/manage-workflows.ts +12 -0
- package/src/config/bundled-skills/workflows/tools/run-workflow.ts +12 -0
- package/src/config/bundled-tool-registry.ts +14 -2
- package/src/config/call-site-defaults.ts +5 -0
- package/src/config/feature-flag-registry.json +12 -4
- package/src/config/llm-context-resolution.ts +8 -0
- package/src/config/llm-resolver.ts +30 -0
- package/src/config/preloaded-apps/personal-page/src/components/About.tsx +22 -0
- package/src/config/preloaded-apps/personal-page/src/components/App.tsx +16 -0
- package/src/config/preloaded-apps/personal-page/src/components/Features.tsx +77 -0
- package/src/config/preloaded-apps/personal-page/src/components/Hero.tsx +57 -0
- package/src/config/preloaded-apps/personal-page/src/components/Pending.tsx +28 -0
- package/src/config/preloaded-apps/personal-page/src/components/animations.tsx +234 -0
- package/src/config/preloaded-apps/personal-page/src/components/icons.tsx +48 -0
- package/src/config/preloaded-apps/personal-page/src/components/media.ts +16 -0
- package/src/config/preloaded-apps/personal-page/src/index.html +20 -0
- package/src/config/preloaded-apps/personal-page/src/main.tsx +7 -0
- package/src/config/preloaded-apps/personal-page/src/profile-data.ts +82 -0
- package/src/config/preloaded-apps/personal-page/src/styles.css +759 -0
- package/src/config/schema.ts +2 -0
- package/src/config/schemas/call-site-catalog.ts +7 -0
- package/src/config/schemas/heartbeat.ts +4 -1
- package/src/config/schemas/llm.ts +33 -26
- package/src/config/schemas/memory-retrospective.ts +19 -0
- package/src/config/schemas/platform.ts +8 -0
- package/src/config/schemas/services.ts +5 -2
- package/src/config/schemas/workflows.ts +42 -0
- package/src/config/skills.ts +3 -3
- package/src/context/compactor.ts +273 -39
- package/src/context/post-turn-tool-result-truncation.ts +23 -6
- package/src/context/tool-result-spool.ts +12 -17
- package/src/credential-execution/executable-discovery.ts +1 -1
- package/src/credential-execution/process-manager.ts +37 -3
- package/src/credential-execution/prompted-credential.ts +205 -0
- package/src/daemon/conversation-agent-loop-handlers.ts +14 -0
- package/src/daemon/conversation-process.ts +11 -2
- package/src/daemon/conversation-surfaces.ts +167 -3
- package/src/daemon/conversation-tool-setup.ts +103 -26
- package/src/daemon/conversation-usage.ts +2 -0
- package/src/daemon/conversation.ts +115 -11
- package/src/daemon/handlers/shared.ts +26 -14
- package/src/daemon/host-cu-proxy.ts +15 -12
- package/src/daemon/host-file-proxy.ts +15 -12
- package/src/daemon/host-transfer-proxy.ts +30 -24
- package/src/daemon/lifecycle.ts +40 -3
- package/src/daemon/message-protocol.ts +3 -0
- package/src/daemon/message-types/messages.ts +2 -10
- package/src/daemon/message-types/workflows.ts +49 -0
- package/src/daemon/parse-actual-tokens-from-error.test.ts +62 -1
- package/src/daemon/parse-actual-tokens-from-error.ts +43 -4
- package/src/daemon/process-message.ts +6 -0
- package/src/daemon/tool-setup-types.ts +57 -0
- package/src/daemon/wake-conversation-ops.ts +18 -0
- package/src/heartbeat/heartbeat-run-store.ts +8 -2
- package/src/home/feed-types.ts +1 -1
- package/src/ipc/gateway-flag-listener.ts +28 -6
- package/src/mcp/mcp-auth-state.ts +8 -20
- package/src/media/__tests__/image-models.test.ts +57 -0
- package/src/media/image-models.ts +66 -0
- package/src/memory/__tests__/auto-analysis-enqueue.test.ts +38 -0
- package/src/memory/__tests__/find-most-recent-retrospective-for.test.ts +12 -2
- package/src/memory/__tests__/memory-retrospective-job.test.ts +911 -34
- package/src/memory/__tests__/memory-retrospective-startup-cleanup.test.ts +227 -5
- package/src/memory/__tests__/memory-retrospective-state.test.ts +195 -0
- package/src/memory/__tests__/preloaded-apps.test.ts +85 -0
- package/src/memory/auto-analysis-enqueue.ts +14 -1
- package/src/memory/compaction-log-store-clickhouse.ts +6 -4
- package/src/memory/conversation-crud.ts +9 -2
- package/src/memory/conversation-disk-view.ts +1 -1
- package/src/memory/conversation-queries.ts +22 -7
- package/src/memory/db-init.ts +20 -0
- package/src/memory/db-maintenance.ts +16 -0
- package/src/memory/embedding-runtime-manager.ts +1 -1
- package/src/memory/llm-request-log-source-clickhouse.ts +112 -14
- package/src/memory/llm-request-log-source-local.ts +19 -1
- package/src/memory/llm-request-log-source.ts +35 -6
- package/src/memory/llm-request-log-store.ts +90 -2
- package/src/memory/memory-retrospective-constants.ts +9 -0
- package/src/memory/memory-retrospective-enqueue.ts +3 -6
- package/src/memory/memory-retrospective-fork-boundary.ts +94 -0
- package/src/memory/memory-retrospective-job.ts +500 -208
- package/src/memory/memory-retrospective-startup-cleanup.ts +97 -19
- package/src/memory/memory-retrospective-state.ts +85 -2
- package/src/memory/migrations/281-memory-retrospective-remembered-log.ts +40 -0
- package/src/memory/migrations/282-schedule-inference-profile.test.ts +77 -0
- package/src/memory/migrations/282-schedule-inference-profile.ts +26 -0
- package/src/memory/migrations/283-memory-v3-selections-message-id-and-sections.test.ts +102 -0
- package/src/memory/migrations/283-memory-v3-selections-message-id-and-sections.ts +53 -0
- package/src/memory/migrations/284-workflow-runs.ts +51 -0
- package/src/memory/migrations/285-schedule-workflow-mode.ts +26 -0
- package/src/memory/migrations/286-workflow-run-trust.ts +27 -0
- package/src/memory/migrations/287-conversation-origin-channel-index.ts +15 -0
- package/src/memory/migrations/288-backfill-origin-channel-from-bindings.ts +43 -0
- package/src/memory/migrations/289-contact-channels-unique-ext-user.ts +115 -0
- package/src/memory/migrations/290-schedule-capabilities.test.ts +77 -0
- package/src/memory/migrations/290-schedule-capabilities.ts +25 -0
- package/src/memory/migrations/__tests__/281-memory-retrospective-remembered-log.test.ts +96 -0
- package/src/memory/migrations/__tests__/289-contact-channels-unique-ext-user.test.ts +571 -0
- package/src/memory/migrations/index.ts +10 -0
- package/src/memory/preloaded-apps.ts +116 -0
- package/src/memory/schema/infrastructure.ts +4 -0
- package/src/memory/schema/memory-core.ts +4 -0
- package/src/memory/v2/__tests__/concept-page-frontmatter-schema.test.ts +45 -0
- package/src/memory/v2/__tests__/frontmatter-sweep.test.ts +11 -7
- package/src/memory/v2/__tests__/page-store.test.ts +13 -2
- package/src/memory/v2/__tests__/qdrant.test.ts +24 -0
- package/src/memory/v2/frontmatter-sweep.ts +7 -6
- package/src/memory/v2/page-store.ts +4 -3
- package/src/memory/v2/qdrant.ts +42 -3
- package/src/memory/v2/types.ts +16 -10
- package/src/messaging/draft-store.ts +1 -1
- package/src/notifications/access-request-copy.ts +200 -113
- package/src/notifications/adapters/slack.ts +250 -111
- package/src/notifications/adapters/telegram.ts +7 -44
- package/src/notifications/approval-card-builder.ts +93 -0
- package/src/notifications/broadcaster.ts +74 -0
- package/src/notifications/conversation-pairing.ts +8 -6
- package/src/notifications/copy-composer.ts +32 -26
- package/src/notifications/decision-engine.ts +59 -7
- package/src/notifications/guardian-question-mode.ts +145 -155
- package/src/notifications/home-feed-side-effect.ts +28 -11
- package/src/notifications/notification-utils.ts +66 -0
- package/src/notifications/signal.ts +6 -0
- package/src/notifications/tool-approval-copy.ts +142 -0
- package/src/notifications/types.ts +19 -0
- package/src/permissions/threshold.ts +11 -0
- package/src/plugin-api/types.ts +16 -4
- package/src/plugins/defaults/compaction/window-manager.ts +44 -0
- package/src/plugins/defaults/index.ts +46 -0
- package/src/plugins/defaults/max-tokens-continue/continue-state-store.ts +53 -0
- package/src/plugins/defaults/max-tokens-continue/hooks/post-model-call.ts +80 -0
- package/src/plugins/defaults/max-tokens-continue/hooks/stop.ts +20 -0
- package/src/plugins/defaults/max-tokens-continue/package.json +14 -0
- package/src/plugins/defaults/memory-retrieval/hooks/__tests__/user-prompt-submit.test.ts +37 -0
- package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit.ts +29 -1
- package/src/plugins/defaults/memory-v3-shadow/__tests__/carry-integration.test.ts +8 -3
- package/src/plugins/defaults/memory-v3-shadow/__tests__/injection.test.ts +4 -2
- package/src/plugins/defaults/memory-v3-shadow/__tests__/pool-select.test.ts +28 -18
- package/src/plugins/defaults/memory-v3-shadow/__tests__/section-dense-store.test.ts +67 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/selection-log-store.test.ts +122 -22
- package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-integration.test.ts +2 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +63 -1
- package/src/plugins/defaults/memory-v3-shadow/injector.ts +61 -18
- package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +1 -1
- package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +39 -10
- package/src/plugins/defaults/memory-v3-shadow/section-dense-store.ts +34 -1
- package/src/plugins/defaults/memory-v3-shadow/selection-log-store.ts +112 -47
- package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +78 -15
- package/src/plugins/defaults/task-progress-nudge/hooks/post-tool-use.ts +206 -0
- package/src/plugins/defaults/task-progress-nudge/package.json +15 -0
- package/src/prompts/__tests__/system-prompt.test.ts +100 -1
- package/src/prompts/__tests__/task-progress-hint-section.test.ts +5 -7
- package/src/prompts/normalize-onboarding.ts +2 -0
- package/src/prompts/persona-resolver.ts +3 -0
- package/src/prompts/system-prompt.ts +51 -2
- package/src/prompts/templates/BOOTSTRAP-ACTIVATION-RAIL.md +3 -1
- package/src/prompts/templates/system-sections.ts +8 -3
- package/src/providers/call-site-routing.ts +6 -3
- package/src/providers/fireworks/client.ts +3 -0
- package/src/providers/inference/auth.ts +52 -46
- package/src/providers/model-intents.ts +2 -2
- package/src/providers/openai/__tests__/coerce-object-args.test.ts +105 -0
- package/src/providers/openai/chat-completions-provider.ts +47 -9
- package/src/providers/openai/coerce-object-args.ts +104 -0
- package/src/providers/retry.ts +8 -5
- package/src/providers/types.ts +10 -0
- package/src/runtime/__tests__/agent-wake.test.ts +629 -7
- package/src/runtime/access-request-helper.ts +16 -9
- package/src/runtime/agent-wake.ts +302 -51
- package/src/runtime/assistant-stream-state.ts +141 -8
- package/src/runtime/background-job-runner.ts +9 -0
- package/src/runtime/channel-approval-types.ts +1 -0
- package/src/runtime/channel-invite-transports/telegram.ts +6 -5
- package/src/runtime/channel-invite-transports/voice.ts +2 -2
- package/src/runtime/channel-invite-types.ts +4 -2
- package/src/runtime/channel-retry-sweep.ts +19 -41
- package/src/runtime/finalize-event-delivery.ts +72 -0
- package/src/runtime/guardian-action-message-composer.ts +0 -54
- package/src/runtime/http-server.ts +6 -14
- package/src/runtime/http-types.ts +0 -1
- package/src/runtime/message-composer-types.ts +0 -9
- package/src/runtime/middleware/__tests__/rate-limiter.test.ts +63 -0
- package/src/runtime/middleware/auth.ts +27 -3
- package/src/runtime/middleware/rate-limiter.ts +28 -1
- package/src/runtime/migrations/vbundle-builder.ts +6 -5
- package/src/runtime/pending-interactions.ts +20 -1
- package/src/runtime/routes/__tests__/conversation-compaction-routes.test.ts +232 -173
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +18 -0
- package/src/runtime/routes/__tests__/retrospective-routes.test.ts +436 -0
- package/src/runtime/routes/__tests__/surface-action-routes.test.ts +11 -0
- package/src/runtime/routes/approval-routes.ts +35 -8
- package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +2 -1
- package/src/runtime/routes/btw-routes.ts +37 -1
- package/src/runtime/routes/channel-delivery-routes.ts +11 -7
- package/src/runtime/routes/channel-route-definitions.ts +3 -0
- package/src/runtime/routes/channel-route-shared.ts +3 -1
- package/src/runtime/routes/consolidation-routes.ts +17 -13
- package/src/runtime/routes/conversation-compaction-routes.ts +159 -119
- package/src/runtime/routes/conversation-list-routes.ts +41 -4
- package/src/runtime/routes/conversation-query-routes.ts +196 -11
- package/src/runtime/routes/conversation-routes.ts +15 -1
- package/src/runtime/routes/credential-prompt-routes.ts +39 -17
- package/src/runtime/routes/empty-state-greeting-cache.ts +65 -0
- package/src/runtime/routes/heartbeat-routes.ts +18 -13
- package/src/runtime/routes/image-generation-routes.ts +20 -2
- package/src/runtime/routes/inbound-message-handler.ts +13 -12
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +32 -31
- package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +7 -5
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +7 -21
- package/src/runtime/routes/inbound-stages/escalation-intercept.ts +5 -5
- package/src/runtime/routes/inbound-stages/guardian-activation-intercept.ts +6 -15
- package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +1 -1
- package/src/runtime/routes/index.ts +6 -0
- package/src/runtime/routes/log-export/AGENTS.md +1 -1
- package/src/runtime/routes/log-export/workspace-allowlist.ts +1 -1
- package/src/runtime/routes/migration-routes.ts +5 -9
- package/src/runtime/routes/plugins-routes.ts +26 -0
- package/src/runtime/routes/ps-routes.ts +10 -8
- package/src/runtime/routes/retrospective-routes.ts +235 -0
- package/src/runtime/routes/runs-pagination.ts +75 -0
- package/src/runtime/routes/schedule-routes.ts +367 -59
- package/src/runtime/routes/sounds-config-routes.ts +239 -0
- package/src/runtime/routes/surface-action-routes.ts +84 -4
- package/src/runtime/routes/workflow-routes.test.ts +372 -0
- package/src/runtime/routes/workflow-routes.ts +363 -0
- package/src/runtime/routes/workspace-routes.test.ts +61 -1
- package/src/runtime/routes/workspace-routes.ts +26 -1
- package/src/runtime/services/__tests__/analyze-conversation.test.ts +38 -0
- package/src/runtime/services/analyze-conversation.ts +26 -13
- package/src/schedule/inference-profile.ts +28 -0
- package/src/schedule/schedule-store.ts +152 -4
- package/src/schedule/scheduler-types.ts +6 -0
- package/src/schedule/scheduler.ts +96 -0
- package/src/security/secret-allowlist.ts +1 -1
- package/src/skills/path-classifier.ts +1 -1
- package/src/tools/apps/executors.ts +23 -0
- package/src/tools/apps/resolve-app-id.ts +42 -0
- package/src/tools/browser/browser-execution.ts +9 -11
- package/src/tools/credentials/broker.ts +4 -4
- package/src/tools/credentials/vault.ts +26 -137
- package/src/tools/executor.ts +69 -0
- package/src/tools/flag-gated-tools.test.ts +76 -0
- package/src/tools/permission-checker.ts +8 -1
- package/src/tools/registry.ts +51 -0
- package/src/tools/schedule/create.ts +77 -1
- package/src/tools/schedule/list.ts +1 -0
- package/src/tools/schedule/update.ts +73 -1
- package/src/tools/skills/execute.ts +56 -0
- package/src/tools/terminal/shell.ts +1 -1
- package/src/tools/ui-surface/definitions.ts +91 -2
- package/src/tools/workflows/manage-workflows.ts +183 -0
- package/src/tools/workflows/run-workflow.test.ts +442 -0
- package/src/tools/workflows/run-workflow.ts +88 -0
- package/src/types/onboarding-context.ts +2 -0
- package/src/usage/attribution.ts +24 -0
- package/src/util/canonicalize-identity.ts +12 -3
- package/src/util/platform.ts +17 -17
- package/src/watcher/__tests__/engine.test.ts +24 -0
- package/src/watcher/__tests__/telemetry.test.ts +135 -0
- package/src/watcher/engine.ts +7 -0
- package/src/watcher/telemetry.ts +74 -0
- package/src/workflows/capabilities.test.ts +365 -0
- package/src/workflows/capabilities.ts +359 -0
- package/src/workflows/deterministic-stringify.ts +27 -0
- package/src/workflows/engine-integration.test.ts +656 -0
- package/src/workflows/engine.test.ts +1144 -0
- package/src/workflows/engine.ts +1078 -0
- package/src/workflows/fanout-load.test.ts +168 -0
- package/src/workflows/journal-store.test.ts +369 -0
- package/src/workflows/journal-store.ts +470 -0
- package/src/workflows/leaf-runner.test.ts +704 -0
- package/src/workflows/leaf-runner.ts +589 -0
- package/src/workflows/library.test.ts +134 -0
- package/src/workflows/library.ts +124 -0
- package/src/workflows/run-manager.test.ts +711 -0
- package/src/workflows/run-manager.ts +593 -0
- package/src/workflows/sandbox-escape.test.ts +339 -0
- package/src/workflows/sandbox.test.ts +251 -0
- package/src/workflows/sandbox.ts +447 -0
- package/src/workspace/adaptive-thinking-repair.ts +33 -11
- package/src/workspace/migrations/021-move-signals-to-workspace.ts +1 -1
- package/src/workspace/migrations/022-move-hooks-to-workspace.ts +1 -1
- package/src/workspace/migrations/026-backfill-install-meta.ts +1 -1
- package/src/workspace/migrations/030-seed-pkb-autoinject.ts +2 -1
- package/src/workspace/migrations/031-drop-user-md.ts +1 -4
- package/src/workspace/migrations/048-remove-workspace-hooks.ts +1 -1
- package/src/workspace/migrations/056-release-notes-inference-profile-reordering.ts +5 -2
- package/src/workspace/migrations/061-move-backup-key-to-workspace.ts +1 -1
- package/src/workspace/migrations/082-backfill-managed-profile-labels.ts +8 -2
- package/src/workspace/migrations/097-enable-adaptive-thinking-managed-profiles.ts +41 -14
- package/src/workspace/migrations/102-preserve-heartbeat-enabled-for-existing-workspaces.ts +69 -0
- package/src/workspace/migrations/103-upgrade-quality-profile-to-opus-4-8.ts +83 -0
- package/src/workspace/migrations/104-recheck-adaptive-thinking-model-implied-anthropic.ts +133 -0
- package/src/workspace/migrations/registry.ts +6 -0
- package/src/workspace/migrations/runner.ts +1 -1
- package/tsconfig.json +1 -1
- package/src/__tests__/guardian-action-copy-generator.test.ts +0 -200
- package/src/__tests__/guardian-action-grant-mint-consume.test.ts +0 -579
- package/src/__tests__/guardian-action-store.test.ts +0 -106
- package/src/daemon/guardian-action-generators.ts +0 -71
- package/src/memory/guardian-action-store.ts +0 -484
- package/src/runtime/guardian-action-grant-minter.ts +0 -150
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
// Silence the logger everywhere this graph reaches.
|
|
4
|
+
const realLogger = await import("../../util/logger.js");
|
|
5
|
+
mock.module("../../util/logger.js", () => ({
|
|
6
|
+
...realLogger,
|
|
7
|
+
getLogger: () =>
|
|
8
|
+
new Proxy({} as Record<string, unknown>, {
|
|
9
|
+
get: () => () => {},
|
|
10
|
+
}),
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
// ── Mutable mock state ────────────────────────────────────────────────
|
|
14
|
+
// `flagEnabled` toggles the `workflows` feature flag; `configThrows`
|
|
15
|
+
// simulates config not yet loaded (test-setup race). The run-manager mock
|
|
16
|
+
// records the args of the last `start()` call for assertion.
|
|
17
|
+
|
|
18
|
+
let flagEnabled = true;
|
|
19
|
+
let configThrows = false;
|
|
20
|
+
|
|
21
|
+
const realLoader = await import("../../config/loader.js");
|
|
22
|
+
mock.module("../../config/loader.js", () => ({
|
|
23
|
+
...realLoader,
|
|
24
|
+
getConfig: () => {
|
|
25
|
+
if (configThrows) throw new Error("config not loaded");
|
|
26
|
+
return {} as ReturnType<typeof realLoader.getConfig>;
|
|
27
|
+
},
|
|
28
|
+
// manage_workflows `list_profiles` reads profiles via loadConfig().
|
|
29
|
+
loadConfig: () =>
|
|
30
|
+
({
|
|
31
|
+
llm: {
|
|
32
|
+
profiles: { "cost-optimized": {}, balanced: {} },
|
|
33
|
+
activeProfile: "balanced",
|
|
34
|
+
},
|
|
35
|
+
}) as unknown as ReturnType<typeof realLoader.loadConfig>,
|
|
36
|
+
}));
|
|
37
|
+
|
|
38
|
+
const realFlags = await import("../../config/assistant-feature-flags.js");
|
|
39
|
+
mock.module("../../config/assistant-feature-flags.js", () => ({
|
|
40
|
+
...realFlags,
|
|
41
|
+
isAssistantFeatureFlagEnabled: (key: string) =>
|
|
42
|
+
key === "workflows" ? flagEnabled : false,
|
|
43
|
+
}));
|
|
44
|
+
|
|
45
|
+
// No live conversation in tests — the tool falls back to a synthetic trust
|
|
46
|
+
// context built from the tool context's trustClass.
|
|
47
|
+
const realRegistry = await import("../../daemon/conversation-registry.js");
|
|
48
|
+
mock.module("../../daemon/conversation-registry.js", () => ({
|
|
49
|
+
...realRegistry,
|
|
50
|
+
findConversation: () => undefined,
|
|
51
|
+
}));
|
|
52
|
+
|
|
53
|
+
let lastStartArgs: Record<string, unknown> | null = null;
|
|
54
|
+
let startThrows: Error | null = null;
|
|
55
|
+
const startMock = mock((opts: Record<string, unknown>) => {
|
|
56
|
+
if (startThrows) throw startThrows;
|
|
57
|
+
lastStartArgs = opts;
|
|
58
|
+
return { runId: "run-123" };
|
|
59
|
+
});
|
|
60
|
+
const statusMock = mock(() => null as unknown);
|
|
61
|
+
const abortMock = mock(() => {});
|
|
62
|
+
const listMock = mock(() => [] as unknown[]);
|
|
63
|
+
let resumeThrows: Error | null = null;
|
|
64
|
+
const resumeMock = mock((runId: string) => {
|
|
65
|
+
if (resumeThrows) throw resumeThrows;
|
|
66
|
+
return { runId };
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
mock.module("../../workflows/run-manager.js", () => ({
|
|
70
|
+
getWorkflowRunManager: () => ({
|
|
71
|
+
start: startMock,
|
|
72
|
+
status: statusMock,
|
|
73
|
+
abort: abortMock,
|
|
74
|
+
list: listMock,
|
|
75
|
+
resume: resumeMock,
|
|
76
|
+
}),
|
|
77
|
+
}));
|
|
78
|
+
|
|
79
|
+
// Imports AFTER mocks so the mocked modules are picked up.
|
|
80
|
+
const { executeRunWorkflow } = await import("./run-workflow.js");
|
|
81
|
+
const { executeManageWorkflows } = await import("./manage-workflows.js");
|
|
82
|
+
const { WORKFLOW_READONLY_BASELINE } =
|
|
83
|
+
await import("../../workflows/capabilities.js");
|
|
84
|
+
|
|
85
|
+
// Minimal tool context — only the fields the workflow tools read.
|
|
86
|
+
function makeContext(): Parameters<typeof executeRunWorkflow>[1] {
|
|
87
|
+
return {
|
|
88
|
+
conversationId: "conv-1",
|
|
89
|
+
workingDir: "/tmp",
|
|
90
|
+
trustClass: "guardian",
|
|
91
|
+
} as Parameters<typeof executeRunWorkflow>[1];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
beforeEach(() => {
|
|
95
|
+
flagEnabled = true;
|
|
96
|
+
configThrows = false;
|
|
97
|
+
startThrows = null;
|
|
98
|
+
lastStartArgs = null;
|
|
99
|
+
resumeThrows = null;
|
|
100
|
+
startMock.mockClear();
|
|
101
|
+
statusMock.mockClear();
|
|
102
|
+
abortMock.mockClear();
|
|
103
|
+
listMock.mockClear();
|
|
104
|
+
resumeMock.mockClear();
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
describe("workflow tools are served by the workflows skill", () => {
|
|
108
|
+
// The cutover moved run_workflow / manage_workflows out of the always-on tool
|
|
109
|
+
// manifest into the flag-gated `workflows` bundled skill (loaded via
|
|
110
|
+
// skill_load, invoked via skill_execute). The skill manifest is the
|
|
111
|
+
// source of truth; assert it declares both as in-process host tools.
|
|
112
|
+
test("the workflows skill TOOLS.json declares both host-executed tools", async () => {
|
|
113
|
+
const { readFileSync } = await import("node:fs");
|
|
114
|
+
const { join } = await import("node:path");
|
|
115
|
+
const manifest = JSON.parse(
|
|
116
|
+
readFileSync(
|
|
117
|
+
join(
|
|
118
|
+
import.meta.dir,
|
|
119
|
+
"../../config/bundled-skills/workflows/TOOLS.json",
|
|
120
|
+
),
|
|
121
|
+
"utf-8",
|
|
122
|
+
),
|
|
123
|
+
) as {
|
|
124
|
+
tools: Array<{ name: string; execution_target: string }>;
|
|
125
|
+
};
|
|
126
|
+
const byName = new Map(manifest.tools.map((t) => [t.name, t]));
|
|
127
|
+
expect(byName.get("run_workflow")?.execution_target).toBe("host");
|
|
128
|
+
expect(byName.get("manage_workflows")?.execution_target).toBe("host");
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
describe("run_workflow capability contract", () => {
|
|
133
|
+
test("SKILL.md advertises the actual read-only baseline (no drift from the code)", async () => {
|
|
134
|
+
// The workflows skill's SKILL.md is the authoring contract the model reads
|
|
135
|
+
// when generating scripts. Guard it against the code: every
|
|
136
|
+
// WORKFLOW_READONLY_BASELINE entry must appear, so the documented baseline
|
|
137
|
+
// never drifts from what resolveCapabilities actually grants (e.g.
|
|
138
|
+
// re-advertising web_fetch as a default).
|
|
139
|
+
const { readFileSync } = await import("node:fs");
|
|
140
|
+
const { join } = await import("node:path");
|
|
141
|
+
const skillMd = readFileSync(
|
|
142
|
+
join(import.meta.dir, "../../config/bundled-skills/workflows/SKILL.md"),
|
|
143
|
+
"utf-8",
|
|
144
|
+
);
|
|
145
|
+
for (const name of WORKFLOW_READONLY_BASELINE) {
|
|
146
|
+
expect(skillMd).toContain(name);
|
|
147
|
+
}
|
|
148
|
+
// web_fetch is side-effecting, so it is NOT a default — the contract must
|
|
149
|
+
// present it as a tool to DECLARE, never as part of the baseline.
|
|
150
|
+
expect(WORKFLOW_READONLY_BASELINE).not.toContain("web_fetch");
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
describe("run_workflow input validation", () => {
|
|
155
|
+
test("rejects when neither script nor name is provided", async () => {
|
|
156
|
+
const res = await executeRunWorkflow({}, makeContext());
|
|
157
|
+
expect(res.isError).toBe(true);
|
|
158
|
+
expect(startMock).not.toHaveBeenCalled();
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test("rejects when BOTH script and name are provided", async () => {
|
|
162
|
+
const res = await executeRunWorkflow(
|
|
163
|
+
{ script: "export const meta = {};", name: "saved" },
|
|
164
|
+
makeContext(),
|
|
165
|
+
);
|
|
166
|
+
expect(res.isError).toBe(true);
|
|
167
|
+
expect(startMock).not.toHaveBeenCalled();
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
describe("run_workflow launch", () => {
|
|
172
|
+
test("starts an inline-script run with the built manifest and returns runId", async () => {
|
|
173
|
+
const res = await executeRunWorkflow(
|
|
174
|
+
{
|
|
175
|
+
script: "export const meta = { name: 'x', description: 'y' };",
|
|
176
|
+
args: { foo: 1 },
|
|
177
|
+
capabilities: { tools: ["file_write"], persona: true },
|
|
178
|
+
label: "My Run",
|
|
179
|
+
},
|
|
180
|
+
makeContext(),
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
expect(res.isError).toBe(false);
|
|
184
|
+
expect(JSON.parse(res.content).runId).toBe("run-123");
|
|
185
|
+
|
|
186
|
+
expect(startMock).toHaveBeenCalledTimes(1);
|
|
187
|
+
expect(lastStartArgs).toMatchObject({
|
|
188
|
+
scriptSource: "export const meta = { name: 'x', description: 'y' };",
|
|
189
|
+
args: { foo: 1 },
|
|
190
|
+
label: "My Run",
|
|
191
|
+
conversationId: "conv-1",
|
|
192
|
+
manifest: { tools: ["file_write"], hostFunctions: [], persona: true },
|
|
193
|
+
});
|
|
194
|
+
// Trust context falls back to the tool context's trust class.
|
|
195
|
+
expect(
|
|
196
|
+
(lastStartArgs as Record<string, unknown>).trustContext,
|
|
197
|
+
).toMatchObject({
|
|
198
|
+
trustClass: "guardian",
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
test("starts a saved-name run and defaults the manifest to empties", async () => {
|
|
203
|
+
await executeRunWorkflow({ name: "saved-flow" }, makeContext());
|
|
204
|
+
expect(lastStartArgs).toMatchObject({
|
|
205
|
+
name: "saved-flow",
|
|
206
|
+
manifest: { tools: [], hostFunctions: [], persona: false },
|
|
207
|
+
});
|
|
208
|
+
expect(
|
|
209
|
+
(lastStartArgs as Record<string, unknown>).scriptSource,
|
|
210
|
+
).toBeUndefined();
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
test("surfaces a run-manager start error as a tool error", async () => {
|
|
214
|
+
startThrows = new Error("Workflows are not enabled.");
|
|
215
|
+
const res = await executeRunWorkflow(
|
|
216
|
+
{ script: "export const meta = {};" },
|
|
217
|
+
makeContext(),
|
|
218
|
+
);
|
|
219
|
+
expect(res.isError).toBe(true);
|
|
220
|
+
expect(res.content).toContain("Workflows are not enabled.");
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
describe("manage_workflows", () => {
|
|
225
|
+
test("list_runs delegates to list()", async () => {
|
|
226
|
+
listMock.mockReturnValueOnce([
|
|
227
|
+
{
|
|
228
|
+
id: "r1",
|
|
229
|
+
name: "flow",
|
|
230
|
+
status: "completed",
|
|
231
|
+
agentsSpawned: 3,
|
|
232
|
+
},
|
|
233
|
+
] as unknown[]);
|
|
234
|
+
const res = await executeManageWorkflows(
|
|
235
|
+
{ action: "list_runs" },
|
|
236
|
+
makeContext(),
|
|
237
|
+
);
|
|
238
|
+
expect(res.isError).toBe(false);
|
|
239
|
+
expect(listMock).toHaveBeenCalledTimes(1);
|
|
240
|
+
expect(JSON.parse(res.content).runs[0].runId).toBe("r1");
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test("abort requires run_id and delegates to abort()", async () => {
|
|
244
|
+
const missing = await executeManageWorkflows(
|
|
245
|
+
{ action: "abort" },
|
|
246
|
+
makeContext(),
|
|
247
|
+
);
|
|
248
|
+
expect(missing.isError).toBe(true);
|
|
249
|
+
expect(abortMock).not.toHaveBeenCalled();
|
|
250
|
+
|
|
251
|
+
// abort now fetches the run first to enforce ownership; seed an owned run.
|
|
252
|
+
statusMock.mockReturnValueOnce({
|
|
253
|
+
id: "r9",
|
|
254
|
+
conversationId: "conv-1",
|
|
255
|
+
} as unknown);
|
|
256
|
+
const ok = await executeManageWorkflows(
|
|
257
|
+
{ action: "abort", run_id: "r9" },
|
|
258
|
+
makeContext(),
|
|
259
|
+
);
|
|
260
|
+
expect(ok.isError).toBe(false);
|
|
261
|
+
expect(abortMock).toHaveBeenCalledWith("r9");
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
test("status requires run_id and reports not-found cleanly", async () => {
|
|
265
|
+
const missing = await executeManageWorkflows(
|
|
266
|
+
{ action: "status" },
|
|
267
|
+
makeContext(),
|
|
268
|
+
);
|
|
269
|
+
expect(missing.isError).toBe(true);
|
|
270
|
+
|
|
271
|
+
const res = await executeManageWorkflows(
|
|
272
|
+
{ action: "status", run_id: "nope" },
|
|
273
|
+
makeContext(),
|
|
274
|
+
);
|
|
275
|
+
expect(res.isError).toBe(false);
|
|
276
|
+
expect(JSON.parse(res.content).found).toBe(false);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
test("get_result requires run_id and returns the full result", async () => {
|
|
280
|
+
const missing = await executeManageWorkflows(
|
|
281
|
+
{ action: "get_result" },
|
|
282
|
+
makeContext(),
|
|
283
|
+
);
|
|
284
|
+
expect(missing.isError).toBe(true);
|
|
285
|
+
|
|
286
|
+
// not-found / not-owned reports cleanly, without leaking a result
|
|
287
|
+
const notFound = await executeManageWorkflows(
|
|
288
|
+
{ action: "get_result", run_id: "nope" },
|
|
289
|
+
makeContext(),
|
|
290
|
+
);
|
|
291
|
+
expect(notFound.isError).toBe(false);
|
|
292
|
+
expect(JSON.parse(notFound.content).found).toBe(false);
|
|
293
|
+
|
|
294
|
+
// an owned, finished run returns the full (untruncated) result payload
|
|
295
|
+
statusMock.mockReturnValueOnce({
|
|
296
|
+
id: "r5",
|
|
297
|
+
conversationId: "conv-1",
|
|
298
|
+
status: "completed",
|
|
299
|
+
result: { scores: [1, 2, 3], note: "the full result lives here" },
|
|
300
|
+
error: null,
|
|
301
|
+
} as unknown);
|
|
302
|
+
const ok = await executeManageWorkflows(
|
|
303
|
+
{ action: "get_result", run_id: "r5" },
|
|
304
|
+
makeContext(),
|
|
305
|
+
);
|
|
306
|
+
expect(ok.isError).toBe(false);
|
|
307
|
+
const parsed = JSON.parse(ok.content);
|
|
308
|
+
expect(parsed.runId).toBe("r5");
|
|
309
|
+
expect(parsed.status).toBe("completed");
|
|
310
|
+
expect(parsed.result).toEqual({
|
|
311
|
+
scores: [1, 2, 3],
|
|
312
|
+
note: "the full result lives here",
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
test("resume requires run_id and delegates to resume()", async () => {
|
|
317
|
+
const missing = await executeManageWorkflows(
|
|
318
|
+
{ action: "resume" },
|
|
319
|
+
makeContext(),
|
|
320
|
+
);
|
|
321
|
+
expect(missing.isError).toBe(true);
|
|
322
|
+
expect(resumeMock).not.toHaveBeenCalled();
|
|
323
|
+
|
|
324
|
+
// resume now fetches the run first to enforce ownership; seed an owned run.
|
|
325
|
+
statusMock.mockReturnValueOnce({
|
|
326
|
+
id: "r9",
|
|
327
|
+
conversationId: "conv-1",
|
|
328
|
+
} as unknown);
|
|
329
|
+
const ok = await executeManageWorkflows(
|
|
330
|
+
{ action: "resume", run_id: "r9" },
|
|
331
|
+
makeContext(),
|
|
332
|
+
);
|
|
333
|
+
expect(ok.isError).toBe(false);
|
|
334
|
+
expect(resumeMock).toHaveBeenCalledWith("r9");
|
|
335
|
+
expect(JSON.parse(ok.content).runId).toBe("r9");
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
test("resume surfaces a run-manager error as a tool error", async () => {
|
|
339
|
+
statusMock.mockReturnValueOnce({
|
|
340
|
+
id: "r9",
|
|
341
|
+
conversationId: "conv-1",
|
|
342
|
+
} as unknown);
|
|
343
|
+
resumeThrows = new Error(
|
|
344
|
+
"Workflow run r9 is not resumable (status: completed).",
|
|
345
|
+
);
|
|
346
|
+
const res = await executeManageWorkflows(
|
|
347
|
+
{ action: "resume", run_id: "r9" },
|
|
348
|
+
makeContext(),
|
|
349
|
+
);
|
|
350
|
+
expect(res.isError).toBe(true);
|
|
351
|
+
expect(res.content).toContain("not resumable");
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
test("list_profiles returns sorted profile names and the active profile", async () => {
|
|
355
|
+
const res = await executeManageWorkflows(
|
|
356
|
+
{ action: "list_profiles" },
|
|
357
|
+
makeContext(),
|
|
358
|
+
);
|
|
359
|
+
expect(res.isError).toBe(false);
|
|
360
|
+
const parsed = JSON.parse(res.content);
|
|
361
|
+
expect(parsed.profiles).toEqual(["balanced", "cost-optimized"]);
|
|
362
|
+
expect(parsed.activeProfile).toBe("balanced");
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
test("rejects an unknown action", async () => {
|
|
366
|
+
const res = await executeManageWorkflows(
|
|
367
|
+
{ action: "bogus" },
|
|
368
|
+
makeContext(),
|
|
369
|
+
);
|
|
370
|
+
expect(res.isError).toBe(true);
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
// Non-guardian authorization scope: a contact conversation may only see and
|
|
374
|
+
// control runs it originated, never another conversation's (or a guardian's).
|
|
375
|
+
const contactContext = () =>
|
|
376
|
+
({
|
|
377
|
+
conversationId: "conv-1",
|
|
378
|
+
workingDir: "/tmp",
|
|
379
|
+
trustClass: "trusted_contact",
|
|
380
|
+
}) as Parameters<typeof executeManageWorkflows>[1];
|
|
381
|
+
|
|
382
|
+
test("list_runs is scoped to the caller's conversation for a non-guardian", async () => {
|
|
383
|
+
listMock.mockReturnValueOnce([
|
|
384
|
+
{ id: "mine", name: "a", status: "running", conversationId: "conv-1" },
|
|
385
|
+
{ id: "theirs", name: "b", status: "running", conversationId: "conv-2" },
|
|
386
|
+
{ id: "nullconv", name: "c", status: "running", conversationId: null },
|
|
387
|
+
] as unknown[]);
|
|
388
|
+
const res = await executeManageWorkflows(
|
|
389
|
+
{ action: "list_runs" },
|
|
390
|
+
contactContext(),
|
|
391
|
+
);
|
|
392
|
+
const ids = JSON.parse(res.content).runs.map(
|
|
393
|
+
(r: { runId: string }) => r.runId,
|
|
394
|
+
);
|
|
395
|
+
expect(ids).toEqual(["mine"]);
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
test("a non-guardian cannot status/abort/resume another conversation's run", async () => {
|
|
399
|
+
const foreign = {
|
|
400
|
+
id: "theirs",
|
|
401
|
+
name: "b",
|
|
402
|
+
status: "interrupted",
|
|
403
|
+
conversationId: "conv-2",
|
|
404
|
+
} as unknown;
|
|
405
|
+
|
|
406
|
+
statusMock.mockReturnValueOnce(foreign);
|
|
407
|
+
const status = await executeManageWorkflows(
|
|
408
|
+
{ action: "status", run_id: "theirs" },
|
|
409
|
+
contactContext(),
|
|
410
|
+
);
|
|
411
|
+
// Reads as not-found — never reveals the foreign run's details.
|
|
412
|
+
expect(JSON.parse(status.content).found).toBe(false);
|
|
413
|
+
|
|
414
|
+
statusMock.mockReturnValueOnce(foreign);
|
|
415
|
+
await executeManageWorkflows(
|
|
416
|
+
{ action: "abort", run_id: "theirs" },
|
|
417
|
+
contactContext(),
|
|
418
|
+
);
|
|
419
|
+
expect(abortMock).not.toHaveBeenCalled();
|
|
420
|
+
|
|
421
|
+
statusMock.mockReturnValueOnce(foreign);
|
|
422
|
+
const resume = await executeManageWorkflows(
|
|
423
|
+
{ action: "resume", run_id: "theirs" },
|
|
424
|
+
contactContext(),
|
|
425
|
+
);
|
|
426
|
+
expect(resume.isError).toBe(true);
|
|
427
|
+
expect(resume.content).toContain("not found");
|
|
428
|
+
expect(resumeMock).not.toHaveBeenCalled();
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
test("a non-guardian CAN control a run it originated", async () => {
|
|
432
|
+
statusMock.mockReturnValueOnce({
|
|
433
|
+
id: "mine",
|
|
434
|
+
conversationId: "conv-1",
|
|
435
|
+
} as unknown);
|
|
436
|
+
await executeManageWorkflows(
|
|
437
|
+
{ action: "abort", run_id: "mine" },
|
|
438
|
+
contactContext(),
|
|
439
|
+
);
|
|
440
|
+
expect(abortMock).toHaveBeenCalledWith("mine");
|
|
441
|
+
});
|
|
442
|
+
});
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `run_workflow` core implementation — a thin launcher over
|
|
3
|
+
* {@link WorkflowRunManager}.
|
|
4
|
+
*
|
|
5
|
+
* The assistant calls this to kick off an autonomous, multi-agent workflow from
|
|
6
|
+
* either an inline script or a saved workflow name. All orchestration logic
|
|
7
|
+
* lives in the run manager and engine; this implementation only validates
|
|
8
|
+
* input, builds the per-run capability manifest, resolves the originating
|
|
9
|
+
* conversation's trust context, and fires `start()` (which returns immediately —
|
|
10
|
+
* the run is asynchronous; its completion is surfaced later as a conversation
|
|
11
|
+
* injection).
|
|
12
|
+
*
|
|
13
|
+
* The script-authoring contract the assistant follows when writing workflow
|
|
14
|
+
* scripts lives in the `workflows` bundled skill's SKILL.md, the single source
|
|
15
|
+
* of truth the model reads when generating the `script`.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { findConversation } from "../../daemon/conversation-registry.js";
|
|
19
|
+
import {
|
|
20
|
+
FALLBACK_TURN_TRUST,
|
|
21
|
+
type TrustContext,
|
|
22
|
+
} from "../../daemon/trust-context.js";
|
|
23
|
+
import { CapabilityManifestSchema } from "../../workflows/capabilities.js";
|
|
24
|
+
import { getWorkflowRunManager } from "../../workflows/run-manager.js";
|
|
25
|
+
import type { ToolContext, ToolExecutionResult } from "../types.js";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Resolve the {@link TrustContext} to forward to the run's leaves. Prefer the
|
|
29
|
+
* originating conversation's per-turn snapshot (the same context the live turn
|
|
30
|
+
* is running under), then its resolved trust context, and finally a synthetic
|
|
31
|
+
* fallback built from the tool context's `trustClass`. We never elevate beyond
|
|
32
|
+
* the tool context's own trust class.
|
|
33
|
+
*/
|
|
34
|
+
function resolveTrustContext(context: ToolContext): TrustContext {
|
|
35
|
+
const conversation = findConversation(context.conversationId);
|
|
36
|
+
const fromConversation =
|
|
37
|
+
conversation?.currentTurnTrustContext ?? conversation?.trustContext;
|
|
38
|
+
if (fromConversation) return fromConversation;
|
|
39
|
+
return { ...FALLBACK_TURN_TRUST, trustClass: context.trustClass };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function executeRunWorkflow(
|
|
43
|
+
input: Record<string, unknown>,
|
|
44
|
+
context: ToolContext,
|
|
45
|
+
): Promise<ToolExecutionResult> {
|
|
46
|
+
const script = input.script as string | undefined;
|
|
47
|
+
const name = input.name as string | undefined;
|
|
48
|
+
|
|
49
|
+
// Exactly one of script/name is required.
|
|
50
|
+
if ((script == null) === (name == null)) {
|
|
51
|
+
return {
|
|
52
|
+
content:
|
|
53
|
+
'Provide exactly one of "script" (inline workflow source) or "name" (saved workflow name).',
|
|
54
|
+
isError: true,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const args = (input.args as Record<string, unknown> | undefined) ?? {};
|
|
59
|
+
const label = input.label as string | undefined;
|
|
60
|
+
const trustContext = resolveTrustContext(context);
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
// The schema fills defaults (empty arrays, persona:false) for omitted
|
|
64
|
+
// fields and throws (caught below) on a malformed `capabilities` object.
|
|
65
|
+
const manifest = CapabilityManifestSchema.parse(input.capabilities ?? {});
|
|
66
|
+
const { runId } = getWorkflowRunManager().start({
|
|
67
|
+
...(script != null ? { scriptSource: script } : { name: name as string }),
|
|
68
|
+
args,
|
|
69
|
+
manifest,
|
|
70
|
+
conversationId: context.conversationId,
|
|
71
|
+
...(label ? { label } : {}),
|
|
72
|
+
trustContext,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
content: JSON.stringify({
|
|
77
|
+
runId,
|
|
78
|
+
status: "running",
|
|
79
|
+
message:
|
|
80
|
+
"Workflow started. You will be notified in this conversation when it completes — do NOT poll. Use manage_workflows to check status or abort if needed.",
|
|
81
|
+
}),
|
|
82
|
+
isError: false,
|
|
83
|
+
};
|
|
84
|
+
} catch (err) {
|
|
85
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
86
|
+
return { content: `Failed to start workflow: ${msg}`, isError: true };
|
|
87
|
+
}
|
|
88
|
+
}
|
package/src/usage/attribution.ts
CHANGED
|
@@ -15,6 +15,12 @@ export type UsageAttributionProfileSource =
|
|
|
15
15
|
export interface UsageAttributionInput {
|
|
16
16
|
callSite: LLMCallSite | null;
|
|
17
17
|
overrideProfile?: string | null;
|
|
18
|
+
/**
|
|
19
|
+
* Mirrors `ResolveCallSiteOpts.forceOverrideProfile`: the override profile
|
|
20
|
+
* was floated above the call-site layers for this request, so attribution
|
|
21
|
+
* must credit it ahead of the call-site profile too.
|
|
22
|
+
*/
|
|
23
|
+
forceOverrideProfile?: boolean;
|
|
18
24
|
/**
|
|
19
25
|
* Per-conversation seed for `mix`-profile expansion (the conversation id).
|
|
20
26
|
* When the applied profile is a mix, threading the same seed the dispatch
|
|
@@ -116,6 +122,9 @@ export function resolveUsageAttribution(
|
|
|
116
122
|
const mixSelections = new Map<string, string>();
|
|
117
123
|
const resolved = resolveCallSiteConfig(callSite, llm, {
|
|
118
124
|
...(overrideProfile != null ? { overrideProfile } : {}),
|
|
125
|
+
...(input.forceOverrideProfile === true
|
|
126
|
+
? { forceOverrideProfile: true }
|
|
127
|
+
: {}),
|
|
119
128
|
...(input.selectionSeed != null
|
|
120
129
|
? { selectionSeed: input.selectionSeed }
|
|
121
130
|
: {}),
|
|
@@ -131,6 +140,7 @@ export function resolveUsageAttribution(
|
|
|
131
140
|
profiles: llm.profiles ?? {},
|
|
132
141
|
activeProfile,
|
|
133
142
|
overrideProfile,
|
|
143
|
+
forceOverrideProfile: input.forceOverrideProfile === true,
|
|
134
144
|
callSiteProfile,
|
|
135
145
|
});
|
|
136
146
|
|
|
@@ -155,6 +165,7 @@ function resolveAppliedProfile(input: {
|
|
|
155
165
|
profiles: Record<string, unknown>;
|
|
156
166
|
activeProfile: string | null;
|
|
157
167
|
overrideProfile: string | null;
|
|
168
|
+
forceOverrideProfile: boolean;
|
|
158
169
|
callSiteProfile: string | null;
|
|
159
170
|
}): Pick<UsageAttributionSnapshot, "appliedProfile" | "profileSource"> {
|
|
160
171
|
if (input.callSite === "mainAgent") {
|
|
@@ -194,6 +205,19 @@ function resolveAppliedProfile(input: {
|
|
|
194
205
|
};
|
|
195
206
|
}
|
|
196
207
|
|
|
208
|
+
// Forced override floats above the call-site profile (the resolver's
|
|
209
|
+
// `forceOverrideProfile` escape hatch), so it wins attribution too.
|
|
210
|
+
if (
|
|
211
|
+
input.forceOverrideProfile &&
|
|
212
|
+
input.overrideProfile != null &&
|
|
213
|
+
input.profiles[input.overrideProfile] != null
|
|
214
|
+
) {
|
|
215
|
+
return {
|
|
216
|
+
appliedProfile: input.overrideProfile,
|
|
217
|
+
profileSource: "conversation",
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
197
221
|
if (
|
|
198
222
|
input.callSiteProfile != null &&
|
|
199
223
|
input.profiles[input.callSiteProfile] != null
|
|
@@ -6,8 +6,9 @@
|
|
|
6
6
|
* immune to formatting variance across channels.
|
|
7
7
|
*
|
|
8
8
|
* Phone-like channels (voice, whatsapp) normalize to E.164 using the
|
|
9
|
-
* existing phone utilities.
|
|
10
|
-
* pass through the platform-stable
|
|
9
|
+
* existing phone utilities. Email channels are lowercased (RFC 5321).
|
|
10
|
+
* Other channels (telegram, slack, etc.) pass through the platform-stable
|
|
11
|
+
* ID as-is after whitespace trimming.
|
|
11
12
|
*/
|
|
12
13
|
|
|
13
14
|
import type { ChannelId } from "../channels/types.js";
|
|
@@ -16,6 +17,9 @@ import { normalizePhoneNumber } from "./phone.js";
|
|
|
16
17
|
/** Channels whose raw sender IDs are phone numbers. */
|
|
17
18
|
const PHONE_CHANNELS: ReadonlySet<ChannelId> = new Set(["phone", "whatsapp"]);
|
|
18
19
|
|
|
20
|
+
/** Channels whose raw sender IDs are email addresses (case-insensitive per RFC 5321). */
|
|
21
|
+
const EMAIL_CHANNELS: ReadonlySet<ChannelId> = new Set(["email"]);
|
|
22
|
+
|
|
19
23
|
/**
|
|
20
24
|
* Canonicalize a raw inbound sender identity for the given channel.
|
|
21
25
|
*
|
|
@@ -23,7 +27,8 @@ const PHONE_CHANNELS: ReadonlySet<ChannelId> = new Set(["phone", "whatsapp"]);
|
|
|
23
27
|
* normalized E.164 string on success, or the trimmed raw ID if
|
|
24
28
|
* normalization fails (defensive: don't discard an identity just because
|
|
25
29
|
* it doesn't parse as a phone number).
|
|
26
|
-
* - For
|
|
30
|
+
* - For email channels: lowercases the address (case-insensitive per RFC 5321).
|
|
31
|
+
* - For other channels: returns the trimmed raw ID unchanged (these
|
|
27
32
|
* platforms provide stable, unique identifiers that don't need normalization).
|
|
28
33
|
*
|
|
29
34
|
* Returns `null` only when `rawId` is empty/whitespace-only.
|
|
@@ -42,5 +47,9 @@ export function canonicalizeInboundIdentity(
|
|
|
42
47
|
return e164 ?? trimmed;
|
|
43
48
|
}
|
|
44
49
|
|
|
50
|
+
if (EMAIL_CHANNELS.has(channel)) {
|
|
51
|
+
return trimmed.toLowerCase();
|
|
52
|
+
}
|
|
53
|
+
|
|
45
54
|
return trimmed;
|
|
46
55
|
}
|