@vellumai/assistant 0.8.12 → 0.9.0-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/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/openapi.yaml +6935 -5545
- 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-dir-path-guard.test.ts +27 -3
- 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-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 +125 -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__/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__/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/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/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 +75 -0
- 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 +1 -1
- 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/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-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-stages/acl-enforcement.ts +26 -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 +122 -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/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 +48 -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
|
@@ -20,10 +20,16 @@
|
|
|
20
20
|
* - Total size: 256 KB
|
|
21
21
|
* - Age: 30 seconds
|
|
22
22
|
*
|
|
23
|
-
* The ring is in-memory and per-daemon-process
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
23
|
+
* The ring is in-memory and per-daemon-process, but the seq counter
|
|
24
|
+
* itself survives restarts: blocks of seq values are reserved ahead of
|
|
25
|
+
* use and the reserved ceiling is persisted to the workspace
|
|
26
|
+
* (`data/stream-seq.json`), so a restarted daemon resumes stamping
|
|
27
|
+
* above every seq the previous process could have emitted. Clients
|
|
28
|
+
* therefore never observe the counter moving backwards — a restart
|
|
29
|
+
* shows up as (at most) a bounded forward gap, which the normal
|
|
30
|
+
* gap-reconcile / snapshot-resync path already handles. The ring is
|
|
31
|
+
* sized generously enough that a typical refresh round-trip (~1-3s)
|
|
32
|
+
* is well within window.
|
|
27
33
|
*
|
|
28
34
|
* Persisted-seq map: alongside the live counter and ring, this module
|
|
29
35
|
* tracks, per conversation, the `seq` of the last event whose content is
|
|
@@ -34,12 +40,17 @@
|
|
|
34
40
|
* persistence flush (assistant rows persist incrementally, debounced, so
|
|
35
41
|
* the snapshot can lag the live counter) -- never the live counter
|
|
36
42
|
* itself, which would over-claim events that have streamed but not yet
|
|
37
|
-
* been written.
|
|
38
|
-
*
|
|
39
|
-
*
|
|
43
|
+
* been written. The map is in-memory and clears on restart; because the
|
|
44
|
+
* counter resumes above the persisted reservation, a value recorded by
|
|
45
|
+
* a previous process could only ever be lower than any seq the new
|
|
46
|
+
* process assigns -- never ambiguous against it. The map is LRU-bounded; an
|
|
40
47
|
* evicted conversation reports no seq and the client cold-starts.
|
|
41
48
|
*/
|
|
42
49
|
|
|
50
|
+
import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
51
|
+
import { dirname, join } from "node:path";
|
|
52
|
+
|
|
53
|
+
import { getWorkspaceDir } from "../util/platform.js";
|
|
43
54
|
import type { AssistantEvent } from "./assistant-event.js";
|
|
44
55
|
|
|
45
56
|
// ── Tunables ─────────────────────────────────────────────────────────
|
|
@@ -58,6 +69,15 @@ const RING_AGE_LIMIT_MS = 30_000;
|
|
|
58
69
|
*/
|
|
59
70
|
const PERSISTED_SEQ_CONVERSATION_LIMIT = 1024;
|
|
60
71
|
|
|
72
|
+
/**
|
|
73
|
+
* How many seq values are reserved per persisted write. The counter can
|
|
74
|
+
* hand out seqs up to the persisted ceiling without touching disk, so
|
|
75
|
+
* the file is written once per block rather than once per event. A
|
|
76
|
+
* restart skips at most one block's worth of unused seqs — a bounded
|
|
77
|
+
* forward gap that clients already treat as a missed-events signal.
|
|
78
|
+
*/
|
|
79
|
+
const SEQ_RESERVATION_BLOCK = 1024;
|
|
80
|
+
|
|
61
81
|
// ── Types ────────────────────────────────────────────────────────────
|
|
62
82
|
|
|
63
83
|
/**
|
|
@@ -97,6 +117,21 @@ interface RingEntry {
|
|
|
97
117
|
|
|
98
118
|
interface AssistantStreamState {
|
|
99
119
|
nextSeq: number;
|
|
120
|
+
/**
|
|
121
|
+
* Highest seq this process may assign without persisting a new
|
|
122
|
+
* reservation. `0` until the first stamp loads (or creates) the
|
|
123
|
+
* persisted reservation.
|
|
124
|
+
*/
|
|
125
|
+
reservedSeqCeiling: number;
|
|
126
|
+
/** Whether the persisted reservation has been loaded this process. */
|
|
127
|
+
seqReservationLoaded: boolean;
|
|
128
|
+
/**
|
|
129
|
+
* Seq of the first event stamped by this process, or `0` before any
|
|
130
|
+
* stamp. Distinguishes a reservation skip (seqs below this value were
|
|
131
|
+
* never assigned by this process, so nothing is missing from the
|
|
132
|
+
* ring) from genuine ring eviction when judging replay validity.
|
|
133
|
+
*/
|
|
134
|
+
firstStampedSeq: number;
|
|
100
135
|
ring: RingEntry[];
|
|
101
136
|
totalSizeBytes: number;
|
|
102
137
|
/**
|
|
@@ -112,6 +147,9 @@ interface AssistantStreamState {
|
|
|
112
147
|
|
|
113
148
|
const state: AssistantStreamState = {
|
|
114
149
|
nextSeq: 1,
|
|
150
|
+
reservedSeqCeiling: 0,
|
|
151
|
+
seqReservationLoaded: false,
|
|
152
|
+
firstStampedSeq: 0,
|
|
115
153
|
ring: [],
|
|
116
154
|
totalSizeBytes: 0,
|
|
117
155
|
persistedSeqByConversation: new Map(),
|
|
@@ -138,7 +176,9 @@ export function stampAndBuffer(
|
|
|
138
176
|
): void {
|
|
139
177
|
if (event.conversationId == null) return;
|
|
140
178
|
|
|
179
|
+
reserveSeqCapacity();
|
|
141
180
|
event.seq = state.nextSeq++;
|
|
181
|
+
if (state.firstStampedSeq === 0) state.firstStampedSeq = event.seq;
|
|
142
182
|
|
|
143
183
|
// Approximate size by serialized JSON length. This is the same
|
|
144
184
|
// bytes-on-wire we'll send, so it tracks ring memory pressure
|
|
@@ -187,8 +227,16 @@ export function getReplayWindow(
|
|
|
187
227
|
|
|
188
228
|
if (state.ring.length === 0) return [];
|
|
189
229
|
|
|
230
|
+
// A cursor from before this process started can skip over the
|
|
231
|
+
// reservation gap: seqs below `firstStampedSeq` were never assigned
|
|
232
|
+
// by this process, so as long as the ring still holds everything
|
|
233
|
+
// this process stamped, no replayable event is missing. (Events from
|
|
234
|
+
// the previous process are unrecoverable either way — clients catch
|
|
235
|
+
// up on those via the snapshot path, triggered by the seq jump.)
|
|
190
236
|
const oldest = state.ring[0]?.seq ?? Infinity;
|
|
191
|
-
|
|
237
|
+
const coversRestartGap =
|
|
238
|
+
lastSeenSeq < state.firstStampedSeq && oldest === state.firstStampedSeq;
|
|
239
|
+
if (lastSeenSeq < oldest - 1 && !coversRestartGap) return null;
|
|
192
240
|
|
|
193
241
|
return state.ring
|
|
194
242
|
.filter(
|
|
@@ -260,6 +308,26 @@ export function getPersistedSeq(conversationId: string): number | null {
|
|
|
260
308
|
*/
|
|
261
309
|
export function _resetStreamStateForTesting(): void {
|
|
262
310
|
state.nextSeq = 1;
|
|
311
|
+
// Mark the reservation as loaded with no ceiling so the next stamp
|
|
312
|
+
// re-reserves from 1, ignoring any reservation file a previous test
|
|
313
|
+
// wrote into the (per-process temp) workspace.
|
|
314
|
+
state.reservedSeqCeiling = 0;
|
|
315
|
+
state.seqReservationLoaded = true;
|
|
316
|
+
state.firstStampedSeq = 0;
|
|
317
|
+
state.ring = [];
|
|
318
|
+
state.totalSizeBytes = 0;
|
|
319
|
+
state.persistedSeqByConversation.clear();
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Simulate a daemon restart: clear all in-memory state and force the
|
|
324
|
+
* next stamp to reload the persisted seq reservation. Test-only.
|
|
325
|
+
*/
|
|
326
|
+
export function _simulateRestartForTesting(): void {
|
|
327
|
+
state.nextSeq = 1;
|
|
328
|
+
state.reservedSeqCeiling = 0;
|
|
329
|
+
state.seqReservationLoaded = false;
|
|
330
|
+
state.firstStampedSeq = 0;
|
|
263
331
|
state.ring = [];
|
|
264
332
|
state.totalSizeBytes = 0;
|
|
265
333
|
state.persistedSeqByConversation.clear();
|
|
@@ -286,6 +354,71 @@ export function _peekStreamForTesting(): {
|
|
|
286
354
|
|
|
287
355
|
// ── Internals ────────────────────────────────────────────────────────
|
|
288
356
|
|
|
357
|
+
function seqReservationPath(): string {
|
|
358
|
+
return join(getWorkspaceDir(), "data", "stream-seq.json");
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Ensure `state.nextSeq` is covered by the persisted reservation,
|
|
363
|
+
* loading the reservation file on the first stamp of the process and
|
|
364
|
+
* extending it by {@link SEQ_RESERVATION_BLOCK} whenever the counter
|
|
365
|
+
* reaches the ceiling.
|
|
366
|
+
*
|
|
367
|
+
* Persistence is best-effort: if the file cannot be read or written
|
|
368
|
+
* the counter falls back to in-memory-only behavior (the ceiling is
|
|
369
|
+
* still advanced so the write is retried at most once per block, not
|
|
370
|
+
* per event), matching the daemon's degraded-mode philosophy.
|
|
371
|
+
*/
|
|
372
|
+
function reserveSeqCapacity(): void {
|
|
373
|
+
if (!state.seqReservationLoaded) {
|
|
374
|
+
state.seqReservationLoaded = true;
|
|
375
|
+
state.reservedSeqCeiling = readReservedCeiling();
|
|
376
|
+
if (state.reservedSeqCeiling >= state.nextSeq) {
|
|
377
|
+
state.nextSeq = state.reservedSeqCeiling + 1;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (state.nextSeq <= state.reservedSeqCeiling) return;
|
|
382
|
+
|
|
383
|
+
const ceiling = state.nextSeq + SEQ_RESERVATION_BLOCK - 1;
|
|
384
|
+
try {
|
|
385
|
+
const path = seqReservationPath();
|
|
386
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
387
|
+
const tmp = `${path}.tmp`;
|
|
388
|
+
writeFileSync(tmp, JSON.stringify({ reservedSeqCeiling: ceiling }));
|
|
389
|
+
renameSync(tmp, path);
|
|
390
|
+
} catch {
|
|
391
|
+
// Degraded mode: keep stamping from memory.
|
|
392
|
+
}
|
|
393
|
+
state.reservedSeqCeiling = ceiling;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function readReservedCeiling(): number {
|
|
397
|
+
try {
|
|
398
|
+
const parsed: unknown = JSON.parse(
|
|
399
|
+
readFileSync(seqReservationPath(), "utf8"),
|
|
400
|
+
);
|
|
401
|
+
if (
|
|
402
|
+
typeof parsed === "object" &&
|
|
403
|
+
parsed !== null &&
|
|
404
|
+
"reservedSeqCeiling" in parsed
|
|
405
|
+
) {
|
|
406
|
+
const ceiling = (parsed as { reservedSeqCeiling: unknown })
|
|
407
|
+
.reservedSeqCeiling;
|
|
408
|
+
if (
|
|
409
|
+
typeof ceiling === "number" &&
|
|
410
|
+
Number.isFinite(ceiling) &&
|
|
411
|
+
ceiling > 0
|
|
412
|
+
) {
|
|
413
|
+
return Math.floor(ceiling);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
} catch {
|
|
417
|
+
// Missing or unreadable file: cold start from seq 1.
|
|
418
|
+
}
|
|
419
|
+
return 0;
|
|
420
|
+
}
|
|
421
|
+
|
|
289
422
|
/**
|
|
290
423
|
* Mirrors the delivery logic in `AssistantEventHub.publish()`. Returns
|
|
291
424
|
* `true` when `subscriber` would have received the entry during live
|
|
@@ -67,6 +67,12 @@ export interface RunBackgroundJobOptions {
|
|
|
67
67
|
trustContext: TrustContext;
|
|
68
68
|
/** LLM call-site identifier — drives provider/model/effort/etc. resolution. */
|
|
69
69
|
callSite: LLMCallSite;
|
|
70
|
+
/**
|
|
71
|
+
* Optional ad-hoc inference-profile override (`llm.profiles` key) applied
|
|
72
|
+
* to every LLM call the job's turn issues. Used by schedules with a pinned
|
|
73
|
+
* profile; omitted = the call site's default resolution.
|
|
74
|
+
*/
|
|
75
|
+
overrideProfile?: string;
|
|
70
76
|
/** Hard timeout for `processMessage` in milliseconds. */
|
|
71
77
|
timeoutMs: number;
|
|
72
78
|
/**
|
|
@@ -267,6 +273,9 @@ export async function runBackgroundJob(
|
|
|
267
273
|
const work = processMessage(conversation.id, opts.prompt, {
|
|
268
274
|
trustContext: opts.trustContext,
|
|
269
275
|
callSite: opts.callSite,
|
|
276
|
+
...(opts.overrideProfile
|
|
277
|
+
? { overrideProfile: opts.overrideProfile }
|
|
278
|
+
: {}),
|
|
270
279
|
});
|
|
271
280
|
// Absorb late rejections: if the timeout wins the race, `work` keeps
|
|
272
281
|
// running and may eventually reject — swallow so it doesn't surface as
|
|
@@ -8,10 +8,8 @@
|
|
|
8
8
|
*
|
|
9
9
|
* Follows the same pattern as approval-message-composer.ts.
|
|
10
10
|
*/
|
|
11
|
-
import { getLogger } from "../util/logger.js";
|
|
12
11
|
import type {
|
|
13
12
|
ComposeGuardianActionMessageOptions,
|
|
14
|
-
GuardianActionCopyGenerator,
|
|
15
13
|
GuardianActionMessageContext,
|
|
16
14
|
} from "./message-composer-types.js";
|
|
17
15
|
|
|
@@ -21,21 +19,6 @@ export type {
|
|
|
21
19
|
GuardianActionMessageScenario,
|
|
22
20
|
} from "./message-composer-types.js";
|
|
23
21
|
|
|
24
|
-
const log = getLogger("guardian-action-message-composer");
|
|
25
|
-
|
|
26
|
-
// ---------------------------------------------------------------------------
|
|
27
|
-
// Constants (exported for the daemon-injected generator implementation)
|
|
28
|
-
// ---------------------------------------------------------------------------
|
|
29
|
-
|
|
30
|
-
export const GUARDIAN_ACTION_COPY_TIMEOUT_MS = 4_000;
|
|
31
|
-
export const GUARDIAN_ACTION_COPY_MAX_TOKENS = 200;
|
|
32
|
-
export const GUARDIAN_ACTION_COPY_SYSTEM_PROMPT =
|
|
33
|
-
"You are an assistant writing one user-facing message about a guardian action in a voice call scenario. " +
|
|
34
|
-
"Keep it concise, natural, and conversational. Preserve factual details exactly. " +
|
|
35
|
-
"These messages are spoken aloud, so use a warm, human tone. " +
|
|
36
|
-
"Do not mention internal systems, scenario IDs, or technical details. " +
|
|
37
|
-
"Return plain text only.";
|
|
38
|
-
|
|
39
22
|
// ---------------------------------------------------------------------------
|
|
40
23
|
// Public API
|
|
41
24
|
// ---------------------------------------------------------------------------
|
|
@@ -51,7 +34,6 @@ export const GUARDIAN_ACTION_COPY_SYSTEM_PROMPT =
|
|
|
51
34
|
export async function composeGuardianActionMessageGenerative(
|
|
52
35
|
context: GuardianActionMessageContext,
|
|
53
36
|
options: ComposeGuardianActionMessageOptions = {},
|
|
54
|
-
generator?: GuardianActionCopyGenerator,
|
|
55
37
|
): Promise<string> {
|
|
56
38
|
const fallbackText =
|
|
57
39
|
options.fallbackText?.trim() || getGuardianActionFallbackMessage(context);
|
|
@@ -60,45 +42,9 @@ export async function composeGuardianActionMessageGenerative(
|
|
|
60
42
|
return fallbackText;
|
|
61
43
|
}
|
|
62
44
|
|
|
63
|
-
if (generator) {
|
|
64
|
-
try {
|
|
65
|
-
const generated = await generator(context, options);
|
|
66
|
-
if (generated) return generated;
|
|
67
|
-
} catch (err) {
|
|
68
|
-
log.warn(
|
|
69
|
-
{ err, scenario: context.scenario },
|
|
70
|
-
"Failed to generate guardian action copy, using fallback",
|
|
71
|
-
);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
45
|
return fallbackText;
|
|
76
46
|
}
|
|
77
47
|
|
|
78
|
-
/** @internal Exported for use by the daemon-injected generator implementation. */
|
|
79
|
-
export function buildGuardianActionGenerationPrompt(
|
|
80
|
-
context: GuardianActionMessageContext,
|
|
81
|
-
fallbackText: string,
|
|
82
|
-
requiredKeywords: string[] | undefined,
|
|
83
|
-
): string {
|
|
84
|
-
const keywordClause =
|
|
85
|
-
requiredKeywords && requiredKeywords.length > 0
|
|
86
|
-
? `Required words to include (as standalone words): ${requiredKeywords.join(
|
|
87
|
-
", ",
|
|
88
|
-
)}.\n`
|
|
89
|
-
: "";
|
|
90
|
-
return [
|
|
91
|
-
"Rewrite the following guardian action message as a natural, conversational reply.",
|
|
92
|
-
"These messages are for voice call scenarios and may be spoken aloud.",
|
|
93
|
-
"Keep the same concrete facts and next-step guidance.",
|
|
94
|
-
keywordClause,
|
|
95
|
-
`Context JSON: ${JSON.stringify(context)}`,
|
|
96
|
-
`Fallback message: ${fallbackText}`,
|
|
97
|
-
]
|
|
98
|
-
.filter(Boolean)
|
|
99
|
-
.join("\n\n");
|
|
100
|
-
}
|
|
101
|
-
|
|
102
48
|
/** @internal Exported for use by the daemon-injected generator implementation. */
|
|
103
49
|
export function includesRequiredKeywords(
|
|
104
50
|
text: string,
|
|
@@ -7,10 +7,6 @@
|
|
|
7
7
|
|
|
8
8
|
import type { ServerWebSocket } from "bun";
|
|
9
9
|
|
|
10
|
-
import {
|
|
11
|
-
startGuardianActionSweep,
|
|
12
|
-
stopGuardianActionSweep,
|
|
13
|
-
} from "../calls/guardian-action-sweep.js";
|
|
14
10
|
import {
|
|
15
11
|
activeMediaStreamSessions,
|
|
16
12
|
MediaStreamCallSession,
|
|
@@ -29,7 +25,6 @@ import { isHttpAuthDisabled } from "../config/env.js";
|
|
|
29
25
|
import { getIsPlatform } from "../config/env-registry.js";
|
|
30
26
|
import { getConfig } from "../config/loader.js";
|
|
31
27
|
import { createApprovalCopyGenerator } from "../daemon/approval-generators.js";
|
|
32
|
-
import { createGuardianActionCopyGenerator } from "../daemon/guardian-action-generators.js";
|
|
33
28
|
import { processMessage } from "../daemon/process-message.js";
|
|
34
29
|
import { createLiveVoiceSession } from "../live-voice/live-voice-session.js";
|
|
35
30
|
import { LiveVoiceSessionManager } from "../live-voice/live-voice-session-manager.js";
|
|
@@ -61,11 +56,11 @@ import {
|
|
|
61
56
|
} from "./middleware/auth.js";
|
|
62
57
|
import { withErrorHandling } from "./middleware/error-handler.js";
|
|
63
58
|
import {
|
|
64
|
-
apiRateLimiter,
|
|
65
59
|
extractClientIp,
|
|
66
60
|
ipRateLimiter,
|
|
67
61
|
rateLimitHeaders,
|
|
68
62
|
rateLimitResponse,
|
|
63
|
+
selectAuthenticatedRateLimiter,
|
|
69
64
|
} from "./middleware/rate-limiter.js";
|
|
70
65
|
import { withRequestLogging } from "./middleware/request-logger.js";
|
|
71
66
|
import {
|
|
@@ -99,7 +94,6 @@ export { isPrivateAddress } from "./middleware/auth.js";
|
|
|
99
94
|
|
|
100
95
|
import type {
|
|
101
96
|
ApprovalCopyGenerator,
|
|
102
|
-
GuardianActionCopyGenerator,
|
|
103
97
|
RuntimeHttpServerOptions,
|
|
104
98
|
} from "./http-types.js";
|
|
105
99
|
|
|
@@ -162,7 +156,6 @@ export class RuntimeHttpServer {
|
|
|
162
156
|
private hostname: string;
|
|
163
157
|
|
|
164
158
|
private readonly approvalCopyGenerator: ApprovalCopyGenerator;
|
|
165
|
-
private readonly guardianActionCopyGenerator: GuardianActionCopyGenerator;
|
|
166
159
|
private retrySweepTimer: ReturnType<typeof setInterval> | null = null;
|
|
167
160
|
private sweepInProgress = false;
|
|
168
161
|
|
|
@@ -174,7 +167,6 @@ export class RuntimeHttpServer {
|
|
|
174
167
|
this.hostname = options.hostname ?? DEFAULT_HOSTNAME;
|
|
175
168
|
|
|
176
169
|
this.approvalCopyGenerator = createApprovalCopyGenerator();
|
|
177
|
-
this.guardianActionCopyGenerator = createGuardianActionCopyGenerator();
|
|
178
170
|
this.liveVoiceSessionManager = new LiveVoiceSessionManager({
|
|
179
171
|
createSession: (context) => createLiveVoiceSession(context),
|
|
180
172
|
});
|
|
@@ -486,9 +478,6 @@ export class RuntimeHttpServer {
|
|
|
486
478
|
startGuardianExpirySweep(this.approvalCopyGenerator);
|
|
487
479
|
log.info("Guardian approval expiry sweep started");
|
|
488
480
|
|
|
489
|
-
startGuardianActionSweep(this.guardianActionCopyGenerator);
|
|
490
|
-
log.info("Guardian action expiry sweep started");
|
|
491
|
-
|
|
492
481
|
startCanonicalGuardianExpirySweep();
|
|
493
482
|
log.info("Canonical guardian request expiry sweep started");
|
|
494
483
|
|
|
@@ -498,7 +487,6 @@ export class RuntimeHttpServer {
|
|
|
498
487
|
|
|
499
488
|
async stop(): Promise<void> {
|
|
500
489
|
stopGuardianExpirySweep();
|
|
501
|
-
stopGuardianActionSweep();
|
|
502
490
|
stopCanonicalGuardianExpirySweep();
|
|
503
491
|
stopInferenceProfileSessionReaper();
|
|
504
492
|
if (this.retrySweepTimer) {
|
|
@@ -702,7 +690,11 @@ export class RuntimeHttpServer {
|
|
|
702
690
|
if (!isHttpAuthDisabled()) {
|
|
703
691
|
const clientIp = extractClientIp(req, server);
|
|
704
692
|
const token = extractBearerToken(req);
|
|
705
|
-
|
|
693
|
+
// Authenticated loopback clients (desktop app, CLI — anything on the
|
|
694
|
+
// daemon's own host) get a higher budget than remote clients.
|
|
695
|
+
const limiter = token
|
|
696
|
+
? selectAuthenticatedRateLimiter(clientIp)
|
|
697
|
+
: ipRateLimiter;
|
|
706
698
|
const limiterKind = token ? "authenticated" : "unauthenticated";
|
|
707
699
|
const result = limiter.check(clientIp, path);
|
|
708
700
|
if (!result.allowed) {
|
|
@@ -22,7 +22,6 @@ import type { AssistantEventHub } from "./assistant-event-hub.js";
|
|
|
22
22
|
export type {
|
|
23
23
|
ApprovalCopyGenerator,
|
|
24
24
|
ComposeApprovalMessageGenerativeOptions,
|
|
25
|
-
GuardianActionCopyGenerator,
|
|
26
25
|
} from "./message-composer-types.js";
|
|
27
26
|
import type { TrustContext } from "../daemon/trust-context.js";
|
|
28
27
|
|
|
@@ -123,12 +123,3 @@ export interface ComposeGuardianActionMessageOptions {
|
|
|
123
123
|
maxTokens?: number;
|
|
124
124
|
timeoutMs?: number;
|
|
125
125
|
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Daemon-injected function that generates guardian action copy using a provider.
|
|
129
|
-
* Returns generated text or `null` on failure (caller falls back to deterministic text).
|
|
130
|
-
*/
|
|
131
|
-
export type GuardianActionCopyGenerator = (
|
|
132
|
-
context: GuardianActionMessageContext,
|
|
133
|
-
options?: ComposeGuardianActionMessageOptions,
|
|
134
|
-
) => Promise<string | null>;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import { isLoopbackAddress } from "../auth.js";
|
|
4
|
+
import {
|
|
5
|
+
apiRateLimiter,
|
|
6
|
+
loopbackApiRateLimiter,
|
|
7
|
+
selectAuthenticatedRateLimiter,
|
|
8
|
+
} from "../rate-limiter.js";
|
|
9
|
+
|
|
10
|
+
describe("isLoopbackAddress", () => {
|
|
11
|
+
test("accepts loopback addresses", () => {
|
|
12
|
+
expect(isLoopbackAddress("127.0.0.1")).toBe(true);
|
|
13
|
+
expect(isLoopbackAddress("127.42.0.7")).toBe(true);
|
|
14
|
+
expect(isLoopbackAddress("::1")).toBe(true);
|
|
15
|
+
expect(isLoopbackAddress("::ffff:127.0.0.1")).toBe(true);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test("rejects private (non-loopback) and public addresses", () => {
|
|
19
|
+
expect(isLoopbackAddress("10.0.0.1")).toBe(false);
|
|
20
|
+
expect(isLoopbackAddress("192.168.1.20")).toBe(false);
|
|
21
|
+
expect(isLoopbackAddress("172.16.0.5")).toBe(false);
|
|
22
|
+
expect(isLoopbackAddress("169.254.0.1")).toBe(false);
|
|
23
|
+
expect(isLoopbackAddress("::ffff:10.0.0.1")).toBe(false);
|
|
24
|
+
expect(isLoopbackAddress("fe80::1")).toBe(false);
|
|
25
|
+
expect(isLoopbackAddress("fd00::1")).toBe(false);
|
|
26
|
+
expect(isLoopbackAddress("8.8.8.8")).toBe(false);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("rejects malformed input", () => {
|
|
30
|
+
expect(isLoopbackAddress("")).toBe(false);
|
|
31
|
+
expect(isLoopbackAddress("localhost")).toBe(false);
|
|
32
|
+
expect(isLoopbackAddress("127.0.0")).toBe(false);
|
|
33
|
+
expect(isLoopbackAddress("127.0.0.999")).toBe(false);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe("selectAuthenticatedRateLimiter", () => {
|
|
38
|
+
test("loopback clients get the higher-budget limiter", () => {
|
|
39
|
+
expect(selectAuthenticatedRateLimiter("127.0.0.1")).toBe(
|
|
40
|
+
loopbackApiRateLimiter,
|
|
41
|
+
);
|
|
42
|
+
expect(selectAuthenticatedRateLimiter("::1")).toBe(loopbackApiRateLimiter);
|
|
43
|
+
expect(selectAuthenticatedRateLimiter("::ffff:127.0.0.1")).toBe(
|
|
44
|
+
loopbackApiRateLimiter,
|
|
45
|
+
);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("remote and LAN clients get the standard limiter", () => {
|
|
49
|
+
expect(selectAuthenticatedRateLimiter("192.168.1.20")).toBe(apiRateLimiter);
|
|
50
|
+
expect(selectAuthenticatedRateLimiter("203.0.113.9")).toBe(apiRateLimiter);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("loopback budget exceeds the standard budget", () => {
|
|
54
|
+
const loopback = loopbackApiRateLimiter.check(
|
|
55
|
+
"test-loopback-budget",
|
|
56
|
+
"/v1/test",
|
|
57
|
+
);
|
|
58
|
+
const standard = apiRateLimiter.check("test-standard-budget", "/v1/test");
|
|
59
|
+
expect(loopback.limit).toBeGreaterThan(standard.limit);
|
|
60
|
+
expect(loopback.limit).toBe(1200);
|
|
61
|
+
expect(standard.limit).toBe(300);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
@@ -12,6 +12,32 @@ export function isLoopbackHost(hostname: string): boolean {
|
|
|
12
12
|
);
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
/** Unwrap IPv4-mapped IPv6 (e.g. ::ffff:10.0.0.1) to its IPv4 part. */
|
|
16
|
+
function unwrapV4Mapped(addr: string): string {
|
|
17
|
+
const v4Mapped = addr.match(/^::ffff:(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/i);
|
|
18
|
+
return v4Mapped ? v4Mapped[1] : addr;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Determine whether an IP address string is a loopback address —
|
|
23
|
+
* 127.0.0.0/8, ::1, or their IPv4-mapped IPv6 forms. Strictly narrower
|
|
24
|
+
* than {@link isPrivateAddress}: LAN peers (RFC 1918, link-local) are
|
|
25
|
+
* private but not loopback.
|
|
26
|
+
*/
|
|
27
|
+
export function isLoopbackAddress(addr: string): boolean {
|
|
28
|
+
const normalized = unwrapV4Mapped(addr);
|
|
29
|
+
|
|
30
|
+
if (normalized.includes(".")) {
|
|
31
|
+
const parts = normalized.split(".").map(Number);
|
|
32
|
+
if (parts.length !== 4 || parts.some((p) => isNaN(p) || p < 0 || p > 255))
|
|
33
|
+
return false;
|
|
34
|
+
// Loopback: 127.0.0.0/8
|
|
35
|
+
return parts[0] === 127;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return normalized.toLowerCase() === "::1";
|
|
39
|
+
}
|
|
40
|
+
|
|
15
41
|
/**
|
|
16
42
|
* @internal Exported for testing.
|
|
17
43
|
*
|
|
@@ -24,9 +50,7 @@ export function isLoopbackHost(hostname: string): boolean {
|
|
|
24
50
|
* - IPv4-mapped IPv6 variants of all of the above (::ffff:x.x.x.x)
|
|
25
51
|
*/
|
|
26
52
|
export function isPrivateAddress(addr: string): boolean {
|
|
27
|
-
|
|
28
|
-
const v4Mapped = addr.match(/^::ffff:(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/i);
|
|
29
|
-
const normalized = v4Mapped ? v4Mapped[1] : addr;
|
|
53
|
+
const normalized = unwrapV4Mapped(addr);
|
|
30
54
|
|
|
31
55
|
// IPv4 checks
|
|
32
56
|
if (normalized.includes(".")) {
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import { getLogger } from "../../util/logger.js";
|
|
6
6
|
import type { HttpErrorResponse } from "../http-errors.js";
|
|
7
|
-
import { isPrivateAddress } from "./auth.js";
|
|
7
|
+
import { isLoopbackAddress, isPrivateAddress } from "./auth.js";
|
|
8
8
|
|
|
9
9
|
const log = getLogger("rate-limiter");
|
|
10
10
|
|
|
@@ -12,6 +12,14 @@ const DEFAULT_MAX_REQUESTS = 300;
|
|
|
12
12
|
const DEFAULT_WINDOW_MS = 60_000; // 60 seconds
|
|
13
13
|
const MAX_TRACKED_TOKENS = 10_000;
|
|
14
14
|
|
|
15
|
+
// Higher budget for authenticated loopback clients. Loopback means the
|
|
16
|
+
// request originated on the daemon's own host (desktop app, CLI, local
|
|
17
|
+
// scripts) — proxied remote traffic resolves to its forwarded client IP
|
|
18
|
+
// instead (see extractClientIp). Local clients legitimately burst far
|
|
19
|
+
// beyond the remote budget: a cold sidebar load at thousands of
|
|
20
|
+
// conversations pages through hundreds of GETs in a few seconds.
|
|
21
|
+
const LOOPBACK_MAX_REQUESTS = 1200;
|
|
22
|
+
|
|
15
23
|
// Lower limit for unauthenticated (IP-based) requests to reduce abuse surface.
|
|
16
24
|
const DEFAULT_IP_MAX_REQUESTS = 20;
|
|
17
25
|
const DEFAULT_IP_WINDOW_MS = 60_000;
|
|
@@ -186,6 +194,15 @@ export function rateLimitResponse(
|
|
|
186
194
|
/** Singleton rate limiter for authenticated /v1/* requests (per-client-IP). */
|
|
187
195
|
export const apiRateLimiter = new TokenRateLimiter();
|
|
188
196
|
|
|
197
|
+
/**
|
|
198
|
+
* Singleton rate limiter for authenticated requests from loopback clients.
|
|
199
|
+
* Separate instance (not just a higher cap) so local and remote traffic
|
|
200
|
+
* never share or evict each other's buckets.
|
|
201
|
+
*/
|
|
202
|
+
export const loopbackApiRateLimiter = new TokenRateLimiter(
|
|
203
|
+
LOOPBACK_MAX_REQUESTS,
|
|
204
|
+
);
|
|
205
|
+
|
|
189
206
|
/** Singleton rate limiter for unauthenticated requests (per-IP, lower limits). */
|
|
190
207
|
export const ipRateLimiter = new TokenRateLimiter(
|
|
191
208
|
DEFAULT_IP_MAX_REQUESTS,
|
|
@@ -193,6 +210,16 @@ export const ipRateLimiter = new TokenRateLimiter(
|
|
|
193
210
|
MAX_TRACKED_IPS,
|
|
194
211
|
);
|
|
195
212
|
|
|
213
|
+
/**
|
|
214
|
+
* Pick the rate limiter for an authenticated request: loopback client IPs
|
|
215
|
+
* get the higher local budget, everything else gets the standard one.
|
|
216
|
+
*/
|
|
217
|
+
export function selectAuthenticatedRateLimiter(
|
|
218
|
+
clientIp: string,
|
|
219
|
+
): TokenRateLimiter {
|
|
220
|
+
return isLoopbackAddress(clientIp) ? loopbackApiRateLimiter : apiRateLimiter;
|
|
221
|
+
}
|
|
222
|
+
|
|
196
223
|
/**
|
|
197
224
|
* Extract the client IP from a request. Only trusts proxy headers
|
|
198
225
|
* (X-Forwarded-For, X-Real-IP) when the peer IP is loopback or private,
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* A .vbundle is a gzip-compressed tar archive containing:
|
|
5
5
|
* - manifest.json: metadata with schema_version, checksums, and bundle info
|
|
6
|
-
* - workspace/: the entire
|
|
6
|
+
* - workspace/: the entire $VELLUM_WORKSPACE_DIR/ directory tree (DB, config,
|
|
7
7
|
* skills, prompts, attachments, etc.) — excluding large/regenerable
|
|
8
8
|
* dirs (embedding-models/, data/qdrant/)
|
|
9
9
|
*/
|
|
@@ -668,7 +668,7 @@ export interface BuildExportVBundleOptions {
|
|
|
668
668
|
/** Whether secrets were stripped from the bundle before archiving. */
|
|
669
669
|
secretsRedacted: boolean;
|
|
670
670
|
/**
|
|
671
|
-
* Absolute path to the workspace directory (
|
|
671
|
+
* Absolute path to the workspace directory ($VELLUM_WORKSPACE_DIR/).
|
|
672
672
|
* When provided and exists, the entire directory tree is walked and
|
|
673
673
|
* included in the archive under the "workspace/" prefix, skipping
|
|
674
674
|
* large/regenerable dirs (embedding-models/, data/qdrant/).
|
|
@@ -679,8 +679,9 @@ export interface BuildExportVBundleOptions {
|
|
|
679
679
|
* Optional callback to checkpoint the WAL before reading the database file.
|
|
680
680
|
* In WAL mode, committed rows may live in the -wal file and not yet be
|
|
681
681
|
* flushed to the main .db file. Callers should pass a function that runs
|
|
682
|
-
* PRAGMA wal_checkpoint(FULL) on the live database connection
|
|
683
|
-
*
|
|
682
|
+
* PRAGMA wal_checkpoint(FULL) on the live database connection — FULL
|
|
683
|
+
* flushes committed frames to the .db file without the extra WAL restart
|
|
684
|
+
* and truncation that TRUNCATE performs.
|
|
684
685
|
* Called before the workspace walk so the DB file is up to date.
|
|
685
686
|
*
|
|
686
687
|
* May return a Promise — `streamExportVBundle` awaits the result so an
|
|
@@ -696,7 +697,7 @@ export interface BuildExportVBundleOptions {
|
|
|
696
697
|
/**
|
|
697
698
|
* Build a .vbundle archive populated with real assistant data.
|
|
698
699
|
*
|
|
699
|
-
* Walks the entire workspace directory (
|
|
700
|
+
* Walks the entire workspace directory ($VELLUM_WORKSPACE_DIR/) and includes
|
|
700
701
|
* all files in the archive, skipping only large/regenerable directories
|
|
701
702
|
* (embedding-models/, data/qdrant/). Binary files (SQLite DB, attachments)
|
|
702
703
|
* are included.
|