@vellumai/assistant 0.12.2 → 0.12.3-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/.env.example +2 -0
- package/AGENTS.md +1 -1
- package/docs/architecture/integrations.md +7 -0
- package/docs/architecture/memory.md +17 -2
- package/docs/credential-execution-service.md +1 -1
- package/docs/desktop-browser-cli.md +7 -3
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/secret-detection.test.ts +1 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/secret-detection.ts +4 -1
- package/node_modules/@vellumai/environments/src/shell.test.ts +21 -0
- package/node_modules/@vellumai/environments/src/shell.ts +24 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/secret-detection.test.ts +1 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/secret-detection.ts +4 -1
- package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +8 -2
- package/node_modules/@vellumai/service-contracts/src/__tests__/secret-detection.test.ts +1 -0
- package/node_modules/@vellumai/service-contracts/src/secret-detection.ts +4 -1
- package/openapi.yaml +54 -5
- package/package.json +1 -1
- package/scripts/smoke-desktop-browser-cli.ts +1 -0
- package/scripts/sync-llm-catalog.ts +3 -0
- package/scripts/voice-ttft-spike.ts +2 -2
- package/src/__tests__/agent-loop.test.ts +259 -0
- package/src/__tests__/agent-wake-delegation-prompt.test.ts +64 -2
- package/src/__tests__/anthropic-provider.test.ts +55 -0
- package/src/__tests__/approval-interception-trust-gates.test.ts +40 -0
- package/src/__tests__/attachments-store.test.ts +22 -3
- package/src/__tests__/channel-approval.test.ts +9 -14
- package/src/__tests__/channel-reply-delivery.test.ts +56 -0
- package/src/__tests__/chat-credential-redaction.test.ts +23 -0
- package/src/__tests__/computer-use-screenshot-attachments.test.ts +411 -0
- package/src/__tests__/computer-use-screenshot-selection.test.ts +50 -0
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +2 -0
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +2 -0
- package/src/__tests__/conversation-agent-loop.test.ts +257 -6
- package/src/__tests__/conversation-attachments.test.ts +106 -0
- package/src/__tests__/conversation-fork-crud.test.ts +191 -0
- package/src/__tests__/conversation-rendered-delegation-state.test.ts +276 -0
- package/src/__tests__/credential-execution-client.test.ts +70 -53
- package/src/__tests__/db-conversation-tool-surface.test.ts +221 -0
- package/src/__tests__/events-tail-route.test.ts +33 -0
- package/src/__tests__/history-repair.test.ts +53 -0
- package/src/__tests__/inference-profile-session-handler.test.ts +26 -0
- package/src/__tests__/list-messages-tool-merge.test.ts +85 -1
- package/src/__tests__/llm-catalog-parity.test.ts +27 -4
- package/src/__tests__/oauth-apps-routes.test.ts +1 -0
- package/src/__tests__/oauth-commands-routes.test.ts +114 -101
- package/src/__tests__/oauth-connect-orchestrator.test.ts +2 -0
- package/src/__tests__/oauth-provider-serializer.test.ts +1 -0
- package/src/__tests__/oauth-providers-routes.test.ts +2 -0
- package/src/__tests__/persist-media-references.test.ts +50 -0
- package/src/__tests__/plugin-import-boundary-guard.test.ts +0 -1
- package/src/__tests__/run-conversation-turn-persistence.test.ts +138 -1
- package/src/__tests__/schedule-routes.test.ts +20 -0
- package/src/__tests__/scheduler-result-notification.test.ts +23 -4
- package/src/__tests__/script-proxy-certs.test.ts +1 -1
- package/src/__tests__/secret-routes-platform-proxy.test.ts +86 -31
- package/src/__tests__/secret-routes-scrub.test.ts +22 -27
- package/src/__tests__/secret-scanner.test.ts +20 -0
- package/src/__tests__/secure-keys-managed-failover.test.ts +8 -0
- package/src/__tests__/secure-keys.test.ts +7 -3
- package/src/__tests__/server-tool-pairing.test.ts +107 -0
- package/src/__tests__/skills.test.ts +5 -4
- package/src/__tests__/subagent-tool-gate-mode.test.ts +226 -0
- package/src/__tests__/terminal-tools.test.ts +8 -0
- package/src/__tests__/tool-result-follow-up.test.ts +99 -0
- package/src/__tests__/tool-result-metadata-plumbing.test.ts +63 -0
- package/src/__tests__/unicode.test.ts +36 -0
- package/src/agent/loop.ts +37 -12
- package/src/agent/tool-result-follow-up.ts +70 -0
- package/src/api/attachment-provenance.test.ts +68 -0
- package/src/api/computer-use-tool.test.ts +55 -0
- package/src/api/computer-use-tool.ts +27 -0
- package/src/api/events/assistant-outbound-attachment.ts +3 -0
- package/src/api/events/desktop-activity-changed.ts +10 -0
- package/src/api/events/question-request.ts +1 -0
- package/src/api/index.ts +11 -0
- package/src/api/responses/conversation-message.ts +3 -0
- package/src/approvals/approval-primitive.ts +5 -2
- package/src/approvals/scoped-approval-grants.ts +6 -2
- package/src/browser/virtual-desktop-target.ts +1 -2
- package/src/cli/commands/__tests__/cli-test-harness.ts +21 -3
- package/src/cli/commands/__tests__/plugins.test.ts +20 -1
- package/src/cli/commands/__tests__/schedules.test.ts +14 -0
- package/src/cli/commands/bash.help.ts +4 -3
- package/src/cli/commands/browser.help.ts +5 -1
- package/src/cli/commands/credentials.help.ts +3 -3
- package/src/cli/commands/oauth/index.help.ts +10 -0
- package/src/cli/commands/oauth/providers.ts +12 -0
- package/src/cli/commands/oauth/request.test.ts +179 -2
- package/src/cli/commands/oauth/request.ts +41 -10
- package/src/cli/commands/plugins.ts +12 -5
- package/src/cli/commands/schedules.ts +2 -0
- package/src/cli/lib/__tests__/inspect-plugin.test.ts +54 -0
- package/src/cli/lib/__tests__/install-from-github.test.ts +41 -0
- package/src/cli/lib/__tests__/local-plugin-upgrade.test.ts +133 -51
- package/src/cli/lib/__tests__/plugin-catalog-cache.test.ts +8 -2
- package/src/cli/lib/bundled-marketplace.json +855 -0
- package/src/cli/lib/bundled-plugin-packages.json +783 -1
- package/src/cli/lib/inspect-plugin.ts +11 -4
- package/src/cli/lib/upgrade-plugin.ts +19 -11
- package/src/cli/output.ts +18 -4
- package/src/config/__tests__/default-provider.test.ts +4 -0
- package/src/config/bundled-skills/schedule/SKILL.md +6 -6
- package/src/config/loader.ts +9 -17
- package/src/config/platform-identity.ts +8 -8
- package/src/config/profile-text-generation.test.ts +51 -0
- package/src/config/profile-text-generation.ts +51 -0
- package/src/config/schemas/__tests__/memory-v3.test.ts +12 -0
- package/src/config/schemas/llm.ts +7 -2
- package/src/config/schemas/mcp.ts +5 -1
- package/src/config/schemas/memory-v3.ts +12 -0
- package/src/credential-execution/ces-runtime.ts +2 -2
- package/src/credential-execution/executable-discovery.ts +36 -134
- package/src/credential-execution/process-manager.test.ts +20 -24
- package/src/credential-execution/process-manager.ts +24 -19
- package/src/daemon/__tests__/conversation-tool-setup.test.ts +43 -0
- package/src/daemon/__tests__/turn-tail-deleted-conversation.test.ts +36 -0
- package/src/daemon/assistant-attachments.ts +20 -12
- package/src/daemon/chat-credential-redaction.ts +16 -1
- package/src/daemon/conversation-agent-loop-handlers.ts +74 -3
- package/src/daemon/conversation-agent-loop.ts +12 -0
- package/src/daemon/conversation-attachments.ts +93 -12
- package/src/daemon/conversation-tool-setup.ts +71 -3
- package/src/daemon/conversation-turn-finalize.ts +26 -13
- package/src/daemon/conversation.ts +72 -17
- package/src/daemon/daemon-control.ts +3 -7
- package/src/daemon/lifecycle.ts +4 -4
- package/src/daemon/mcp-reload-service.ts +1 -1
- package/src/daemon/message-types/shared.ts +1 -0
- package/src/daemon/orphan-reaper.ts +4 -3
- package/src/daemon/persist-media-references.ts +20 -3
- package/src/daemon/tool-setup-types.ts +6 -0
- package/src/daemon/wake-conversation-ops.ts +50 -15
- package/src/desktop/desktop-automation-lease.test.ts +230 -0
- package/src/desktop/desktop-automation-lease.ts +90 -6
- package/src/desktop/desktop-help.ts +15 -0
- package/src/desktop/desktop-stream-bridge.test.ts +65 -10
- package/src/desktop/desktop-stream-bridge.ts +3 -3
- package/src/desktop/desktop-wallpaper-renderer.ts +132 -0
- package/src/desktop/desktop-wallpaper-worker.ts +17 -0
- package/src/desktop/desktop-wallpaper.test.ts +48 -7
- package/src/desktop/desktop-wallpaper.ts +34 -102
- package/src/desktop/virtual-desktop-feature.ts +1 -1
- package/src/mcp/__tests__/manager-tool-caps.test.ts +111 -0
- package/src/mcp/__tests__/startup.test.ts +30 -7
- package/src/mcp/__tests__/tool-caps.test.ts +107 -0
- package/src/mcp/manager.ts +168 -101
- package/src/mcp/startup.ts +33 -13
- package/src/mcp/tool-caps.ts +176 -0
- package/src/messaging/provider-message-metadata.ts +3 -3
- package/src/notifications/__tests__/copy-composer.test.ts +70 -0
- package/src/notifications/copy-composer.ts +11 -3
- package/src/notifications/schedule-result-producer.ts +4 -6
- package/src/oauth/AGENTS.md +2 -0
- package/src/oauth/__tests__/identity-verifier.test.ts +36 -1
- package/src/oauth/identity-verifier.ts +24 -0
- package/src/oauth/oauth-store.ts +11 -1
- package/src/oauth/provider-serializer.ts +1 -0
- package/src/oauth/seed-providers.ts +12 -0
- package/src/permissions/confirmation-guardian-request.test.ts +16 -0
- package/src/permissions/confirmation-guardian-request.ts +2 -3
- package/src/permissions/question-prompter.test.ts +31 -0
- package/src/permissions/question-prompter.ts +2 -0
- package/src/persistence/attachments-store.ts +84 -44
- package/src/persistence/conversation-crud.ts +111 -20
- package/src/persistence/conversation-plugin-facade.ts +30 -0
- package/src/persistence/conversation-tool-surface.ts +135 -0
- package/src/persistence/conversation-types.test.ts +32 -0
- package/src/persistence/conversation-types.ts +26 -4
- package/src/persistence/migrations/378-create-conversation-tool-surfaces.test.ts +78 -0
- package/src/persistence/migrations/378-create-conversation-tool-surfaces.ts +29 -0
- package/src/persistence/migrations/379-oauth-providers-response-ok-field.test.ts +92 -0
- package/src/persistence/migrations/379-oauth-providers-response-ok-field.ts +32 -0
- package/src/persistence/migrations/380-conversation-tool-surfaces-delegate-independent-tasks.test.ts +90 -0
- package/src/persistence/migrations/380-conversation-tool-surfaces-delegate-independent-tasks.ts +34 -0
- package/src/persistence/schema/conversation-tool-surfaces.ts +32 -0
- package/src/persistence/schema/index.ts +1 -0
- package/src/persistence/schema/oauth.ts +1 -0
- package/src/persistence/steps.ts +24 -3
- package/src/plugin-api/conversation-turn.ts +31 -7
- package/src/plugin-api/index.ts +11 -1
- package/src/plugin-api/plugin-channel-turn-trust.test.ts +133 -0
- package/src/plugin-api/plugin-channel-turn-trust.ts +71 -0
- package/src/plugins/defaults/memory/AGENTS.md +39 -5
- package/src/plugins/defaults/memory/__tests__/buffer-file.test.ts +365 -0
- package/src/plugins/defaults/memory/__tests__/buffer-format.test.ts +43 -0
- package/src/plugins/defaults/memory/__tests__/conversation-memory-purge.test.ts +1 -0
- package/src/plugins/defaults/memory/__tests__/db-memory-attach.test.ts +2 -0
- package/src/plugins/defaults/memory/__tests__/fixtures/buffer-appender.ts +17 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +74 -0
- package/src/plugins/defaults/memory/__tests__/memory-run-evidence.test.ts +161 -0
- package/src/plugins/defaults/memory/__tests__/relocated-memory-test-rows.ts +10 -0
- package/src/plugins/defaults/memory/buffer-file.ts +354 -0
- package/src/plugins/defaults/memory/buffer-format.ts +40 -0
- package/src/plugins/defaults/memory/context-search/agent-runner.ts +1 -2
- package/src/plugins/defaults/memory/context-search/format.ts +2 -1
- package/src/plugins/defaults/memory/context-search/sources/memory-v2.ts +2 -1
- package/src/plugins/defaults/memory/context-search/sources/workspace.ts +2 -1
- package/src/plugins/defaults/memory/conversation-memory-purge.ts +4 -0
- package/src/plugins/defaults/memory/graph/capability-seed.ts +1 -2
- package/src/plugins/defaults/memory/graph/tool-handlers.ts +1 -42
- package/src/plugins/defaults/memory/host-utils.ts +0 -10
- package/src/plugins/defaults/memory/injectors.ts +4 -3
- package/src/plugins/defaults/memory/memory-retrospective-job.ts +72 -183
- package/src/plugins/defaults/memory/memory-run-evidence.ts +213 -0
- package/src/plugins/defaults/memory/src/memory-item-routes.test.ts +1 -1
- package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +407 -99
- package/src/plugins/defaults/memory/substrate/__tests__/consolidation-prompt-flag-gating-guard.test.ts +10 -0
- package/src/plugins/defaults/memory/substrate/__tests__/prompts-consolidation.test.ts +107 -7
- package/src/plugins/defaults/memory/substrate/consolidation-job.ts +307 -86
- package/src/plugins/defaults/memory/substrate/consolidation-tool-surface.ts +34 -0
- package/src/plugins/defaults/memory/substrate/page-index.ts +2 -1
- package/src/plugins/defaults/memory/substrate/prompts/consolidation.ts +89 -49
- package/src/plugins/defaults/memory/substrate/sweep-job.ts +1 -1
- package/src/plugins/defaults/memory/tools.ts +1 -1
- package/src/plugins/defaults/memory/v1/graph/consolidation.ts +2 -2
- package/src/plugins/defaults/memory/v1/graph/extraction.ts +2 -1
- package/src/plugins/defaults/memory/v1/graph/retriever.ts +1 -1
- package/src/plugins/defaults/memory/v2/__tests__/migration.test.ts +5 -0
- package/src/plugins/defaults/memory/v2/__tests__/reranker.test.ts +5 -2
- package/src/plugins/defaults/memory/v2/reranker.ts +2 -1
- package/src/plugins/defaults/memory/v3/__tests__/injection.test.ts +81 -1
- package/src/plugins/defaults/memory/v3/__tests__/orchestrate.test.ts +87 -0
- package/src/plugins/defaults/memory/v3/__tests__/plugin-schema.test.ts +12 -0
- package/src/plugins/defaults/memory/v3/__tests__/pool-log-store.test.ts +183 -4
- package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +103 -2
- package/src/plugins/defaults/memory/v3/card.ts +2 -1
- package/src/plugins/defaults/memory/v3/injector.ts +212 -178
- package/src/plugins/defaults/memory/v3/orchestrate.ts +112 -25
- package/src/plugins/defaults/memory/v3/plugin-schema.ts +54 -4
- package/src/plugins/defaults/memory/v3/pool-log-store.ts +253 -1
- package/src/plugins/defaults/memory/v3/pool-select.test.ts +35 -2
- package/src/plugins/defaults/memory/v3/pool-select.ts +31 -19
- package/src/plugins/defaults/memory/v3/sections.ts +2 -1
- package/src/plugins/defaults/memory/v3/shadow-plugin.ts +41 -14
- package/src/plugins/defaults/tool-error/hooks/post-tool-use.ts +4 -1
- package/src/plugins/defaults/tool-result-truncate/terminal.ts +1 -46
- package/src/prompts/__tests__/parallel-tasks-section.test.ts +25 -0
- package/src/prompts/delegation-gate.ts +57 -0
- package/src/prompts/system-prompt.ts +14 -32
- package/src/providers/inference/adapter-factory.ts +6 -0
- package/src/providers/jev/client.test.ts +260 -0
- package/src/providers/jev/client.ts +518 -0
- package/src/providers/model-catalog.ts +58 -3
- package/src/providers/server-tool-pairing.ts +16 -7
- package/src/runtime/AGENTS.md +2 -2
- package/src/runtime/__tests__/agent-wake.test.ts +94 -1
- package/src/runtime/agent-wake.ts +31 -4
- package/src/runtime/guardian-action-service.ts +2 -17
- package/src/runtime/guardian-reply-router.ts +1 -8
- package/src/runtime/http-server.ts +2 -2
- package/src/runtime/migrations/__tests__/vbundle-import-policy.test.ts +89 -0
- package/src/runtime/migrations/vbundle-import-policy.ts +33 -11
- package/src/runtime/routes/__tests__/inference-profiles-routes.test.ts +24 -2
- package/src/runtime/routes/channel-route-shared.ts +1 -9
- package/src/runtime/routes/conversation-routes.ts +29 -4
- package/src/runtime/routes/desktop-setup-routes.test.ts +2 -2
- package/src/runtime/routes/desktop-setup-routes.ts +8 -5
- package/src/runtime/routes/guardian-approval-interception.ts +24 -0
- package/src/runtime/routes/inbound-message-handler.ts +2 -3
- package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +1 -1
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +7 -4
- package/src/runtime/routes/inference-profile-session-handler.ts +11 -1
- package/src/runtime/routes/inference-profiles-routes.ts +9 -0
- package/src/runtime/routes/oauth-commands-routes.ts +57 -54
- package/src/runtime/routes/oauth-providers.ts +4 -0
- package/src/runtime/routes/oauth-request-hints.test.ts +246 -0
- package/src/runtime/routes/oauth-request-hints.ts +149 -0
- package/src/runtime/routes/schedule-routes.ts +2 -0
- package/src/runtime/routes/secret-routes.ts +99 -119
- package/src/schedule/__tests__/worker-mcp-bootstrap.test.ts +107 -0
- package/src/schedule/__tests__/worker-mcp-readiness.test.ts +173 -0
- package/src/schedule/__tests__/worker-mcp-tools.test.ts +13 -47
- package/src/schedule/run-script.ts +2 -2
- package/src/schedule/scheduler.ts +47 -11
- package/src/schedule/tool-surface-readiness.ts +65 -0
- package/src/schedule/worker-mcp.ts +85 -0
- package/src/schedule/worker.ts +9 -31
- package/src/security/secure-keys.ts +60 -48
- package/src/tools/ask-question/ask-question-tool.test.ts +167 -5
- package/src/tools/ask-question/ask-question-tool.ts +119 -23
- package/src/tools/browser/browser-execution.ts +7 -1
- package/src/tools/host-terminal/host-shell.ts +12 -6
- package/src/tools/shared/filesystem/file-ops-service.ts +1 -31
- package/src/tools/shared/shell-output.test.ts +10 -0
- package/src/tools/shared/shell-output.ts +14 -2
- package/src/tools/skills/sandbox-runner.ts +13 -2
- package/src/tools/skills/scaffold-managed.ts +2 -1
- package/src/tools/terminal/__tests__/safe-env.test.ts +33 -4
- package/src/tools/terminal/__tests__/sanitized-bash.test.ts +0 -14
- package/src/tools/terminal/safe-env.ts +35 -24
- package/src/tools/terminal/sanitized-bash.ts +15 -2
- package/src/tools/terminal/shell-launch.test.ts +162 -0
- package/src/tools/terminal/shell.test.ts +29 -0
- package/src/tools/terminal/shell.ts +13 -7
- package/src/util/browser-human-verification.ts +14 -0
- package/src/util/host-process.test.ts +17 -1
- package/src/util/host-process.ts +24 -0
- package/src/util/unicode.ts +29 -0
|
@@ -213,6 +213,7 @@ const scheduleSchema = z.object({
|
|
|
213
213
|
mode: z.enum(["notify", "execute", "script", "wake", "workflow"]),
|
|
214
214
|
status: z.enum(["active", "firing", "fired", "cancelled"]),
|
|
215
215
|
routingIntent: z.enum(["single_channel", "multi_channel", "all_channels"]),
|
|
216
|
+
quiet: z.boolean(),
|
|
216
217
|
reuseConversation: z.boolean(),
|
|
217
218
|
wakeConversationId: z.string().nullable(),
|
|
218
219
|
workflowName: z.string().nullable(),
|
|
@@ -381,6 +382,7 @@ function serializeSchedule(
|
|
|
381
382
|
mode: j.mode,
|
|
382
383
|
status: j.status,
|
|
383
384
|
routingIntent: j.routingIntent,
|
|
385
|
+
quiet: j.quiet,
|
|
384
386
|
reuseConversation: j.reuseConversation,
|
|
385
387
|
wakeConversationId: j.wakeConversationId,
|
|
386
388
|
workflowName: j.workflowName,
|
|
@@ -16,10 +16,7 @@ import {
|
|
|
16
16
|
} from "../../acp/acp-credentials.js";
|
|
17
17
|
import {
|
|
18
18
|
getPlatformAssistantId,
|
|
19
|
-
setPlatformAssistantId,
|
|
20
19
|
setPlatformBaseUrl,
|
|
21
|
-
setPlatformOrganizationId,
|
|
22
|
-
setPlatformUserId,
|
|
23
20
|
} from "../../config/env.js";
|
|
24
21
|
import { getConfig } from "../../config/loader.js";
|
|
25
22
|
import { maybeDefaultSpeechToManaged } from "../../config/managed-speech-defaults.js";
|
|
@@ -41,6 +38,7 @@ import {
|
|
|
41
38
|
refreshProvidersAfterSecretChange,
|
|
42
39
|
refreshProvidersForRotatedCredential,
|
|
43
40
|
} from "../../providers/inference/credential-rotation.js";
|
|
41
|
+
import { validateJevApiKey } from "../../providers/jev/client.js";
|
|
44
42
|
import { validateMinimaxApiKey } from "../../providers/minimax/client.js";
|
|
45
43
|
import { validateOpenAIApiKey } from "../../providers/openai/client.js";
|
|
46
44
|
import { validatePoolsideApiKey } from "../../providers/poolside/client.js";
|
|
@@ -79,6 +77,48 @@ const log = getLogger("runtime-http");
|
|
|
79
77
|
const CES_READY_POLL_INTERVAL_MS = 500;
|
|
80
78
|
const CES_READY_POLL_TIMEOUT_MS = 30_000;
|
|
81
79
|
|
|
80
|
+
const WRITABLE_VELLUM_CREDENTIAL_FIELDS = [
|
|
81
|
+
"assistant_api_key",
|
|
82
|
+
"webhook_secret",
|
|
83
|
+
"platform_base_url",
|
|
84
|
+
] as const;
|
|
85
|
+
|
|
86
|
+
function isWritableVellumCredentialField(field: string): boolean {
|
|
87
|
+
return (WRITABLE_VELLUM_CREDENTIAL_FIELDS as readonly string[]).includes(
|
|
88
|
+
field,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function persistCredential(
|
|
93
|
+
service: string,
|
|
94
|
+
field: string,
|
|
95
|
+
value: string,
|
|
96
|
+
): Promise<void> {
|
|
97
|
+
const key = credentialKey(service, field);
|
|
98
|
+
const stored = await setSecureKeyAsync(key, value);
|
|
99
|
+
if (!stored) {
|
|
100
|
+
throw new InternalError(
|
|
101
|
+
`Failed to store credential in secure storage (backend: ${getActiveBackendName()})`,
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
if (!isNonSecretPlatformField(service, field)) {
|
|
105
|
+
try {
|
|
106
|
+
const scrubbed = await scrubStoredCredentialFromTranscripts(value);
|
|
107
|
+
log.info(
|
|
108
|
+
{ service, field, ...scrubbed },
|
|
109
|
+
"Credential stored; scrubbed value from recent transcripts",
|
|
110
|
+
);
|
|
111
|
+
} catch (err) {
|
|
112
|
+
log.warn(
|
|
113
|
+
{ err, service, field },
|
|
114
|
+
"Credential stored, but transcript scrub failed",
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
upsertCredentialMetadata(service, field, {});
|
|
119
|
+
await syncManualTokenConnection(service);
|
|
120
|
+
}
|
|
121
|
+
|
|
82
122
|
let apiKeyGeneration = 0;
|
|
83
123
|
|
|
84
124
|
async function queueApiKeyPropagation(
|
|
@@ -263,6 +303,15 @@ async function handleAddSecret({ body }: RouteHandlerArgs) {
|
|
|
263
303
|
);
|
|
264
304
|
return { success: false, error: validation.reason };
|
|
265
305
|
}
|
|
306
|
+
} else if (name === "jev") {
|
|
307
|
+
const validation = await validateJevApiKey(value);
|
|
308
|
+
if (!validation.valid) {
|
|
309
|
+
log.warn(
|
|
310
|
+
{ provider: name, reason: validation.reason },
|
|
311
|
+
"API key validation failed",
|
|
312
|
+
);
|
|
313
|
+
return { success: false, error: validation.reason };
|
|
314
|
+
}
|
|
266
315
|
}
|
|
267
316
|
|
|
268
317
|
const stored = await setSecureKeyAsync(
|
|
@@ -307,10 +356,50 @@ async function handleAddSecret({ body }: RouteHandlerArgs) {
|
|
|
307
356
|
'For credential type, name must be in "service:field" format (e.g. "github:api_token")',
|
|
308
357
|
);
|
|
309
358
|
}
|
|
310
|
-
assertMetadataWritable();
|
|
311
359
|
const service = name.slice(0, colonIdx);
|
|
312
360
|
const field = name.slice(colonIdx + 1);
|
|
313
361
|
|
|
362
|
+
if (service === "vellum") {
|
|
363
|
+
if (!isWritableVellumCredentialField(field)) {
|
|
364
|
+
throw new BadRequestError(
|
|
365
|
+
`Unknown vellum credential: ${field}. Allowed fields: ${WRITABLE_VELLUM_CREDENTIAL_FIELDS.join(", ")}`,
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
assertMetadataWritable();
|
|
369
|
+
await persistCredential(service, field, value);
|
|
370
|
+
if (field === "platform_base_url") {
|
|
371
|
+
setPlatformBaseUrl(value);
|
|
372
|
+
}
|
|
373
|
+
if (isPlatformManagedCredential(service, field)) {
|
|
374
|
+
await refreshProvidersAfterSecretChange();
|
|
375
|
+
// Close the first-boot race where the startup capability seed ran before
|
|
376
|
+
// the managed embedding credential was provisioned, leaving skill/CLI
|
|
377
|
+
// pages unseeded until restart. Detached, must not block the response.
|
|
378
|
+
void maybeReseedCapabilitiesAfterManagedCredential(getConfig());
|
|
379
|
+
if (field === "assistant_api_key") {
|
|
380
|
+
await notifyCesOfAssistantApiKeyUpdate(value, getCesClient());
|
|
381
|
+
}
|
|
382
|
+
} else {
|
|
383
|
+
await refreshProvidersForRotatedCredential(service, field);
|
|
384
|
+
}
|
|
385
|
+
if (field === "assistant_api_key" || field === "platform_base_url") {
|
|
386
|
+
// Managed-speech availability needs the API key, the assistant ID,
|
|
387
|
+
// and the base URL. Fire on each write that completes a piece of
|
|
388
|
+
// that set; the hook no-ops until the connection is complete.
|
|
389
|
+
// Detached, must not block the response.
|
|
390
|
+
void maybeDefaultSpeechToManaged();
|
|
391
|
+
// Same last-write-wins shape: the startup syncs no-op before live
|
|
392
|
+
// registration, so re-enqueue them here. Both dedup and no-op until
|
|
393
|
+
// the client and assistant id exist.
|
|
394
|
+
syncWorkspaceIdentityToPlatform();
|
|
395
|
+
syncAvatarToPlatform();
|
|
396
|
+
}
|
|
397
|
+
log.info({ service, field }, "Credential added via HTTP");
|
|
398
|
+
return { success: true, type, name };
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
assertMetadataWritable();
|
|
402
|
+
|
|
314
403
|
// Reject an Anthropic API key pasted into the ACP OAuth-token field (a 401
|
|
315
404
|
// footgun) as a 400 rather than letting it persist and fail at runtime.
|
|
316
405
|
if (service === ACP_SERVICE) {
|
|
@@ -324,104 +413,8 @@ async function handleAddSecret({ body }: RouteHandlerArgs) {
|
|
|
324
413
|
}
|
|
325
414
|
}
|
|
326
415
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
const TRIMMED_IDENTITY_FIELDS = new Set([
|
|
330
|
-
"platform_assistant_id",
|
|
331
|
-
"platform_organization_id",
|
|
332
|
-
"platform_user_id",
|
|
333
|
-
]);
|
|
334
|
-
const isTrimmedIdentity =
|
|
335
|
-
service === "vellum" && TRIMMED_IDENTITY_FIELDS.has(field);
|
|
336
|
-
const effectiveValue = isTrimmedIdentity ? value.trim() : value;
|
|
337
|
-
|
|
338
|
-
if (isTrimmedIdentity && effectiveValue === "") {
|
|
339
|
-
const deleteResult = await deleteSecureKeyAsync(key);
|
|
340
|
-
if (deleteResult === "error") {
|
|
341
|
-
throw new InternalError(
|
|
342
|
-
`Failed to delete stale credential from secure storage: ${service}:${field}`,
|
|
343
|
-
);
|
|
344
|
-
}
|
|
345
|
-
if (field === "platform_assistant_id") {
|
|
346
|
-
setPlatformAssistantId(undefined);
|
|
347
|
-
} else if (field === "platform_organization_id") {
|
|
348
|
-
setPlatformOrganizationId(undefined);
|
|
349
|
-
} else if (field === "platform_user_id") {
|
|
350
|
-
setPlatformUserId(undefined);
|
|
351
|
-
}
|
|
352
|
-
deleteCredentialMetadata(service, field);
|
|
353
|
-
} else {
|
|
354
|
-
const stored = await setSecureKeyAsync(key, effectiveValue);
|
|
355
|
-
if (!stored) {
|
|
356
|
-
throw new InternalError(
|
|
357
|
-
`Failed to store credential in secure storage (backend: ${getActiveBackendName()})`,
|
|
358
|
-
);
|
|
359
|
-
}
|
|
360
|
-
if (!isNonSecretPlatformField(service, field)) {
|
|
361
|
-
// Same seam as the api_key branch: the scrub runs immediately after
|
|
362
|
-
// the secure-store write, before side effects that can throw. The
|
|
363
|
-
// value IS stored at this point; the scrub is best-effort hygiene
|
|
364
|
-
// and must stay invisible to the caller. Counts only — never the
|
|
365
|
-
// value.
|
|
366
|
-
try {
|
|
367
|
-
const scrubbed =
|
|
368
|
-
await scrubStoredCredentialFromTranscripts(effectiveValue);
|
|
369
|
-
log.info(
|
|
370
|
-
{ service, field, ...scrubbed },
|
|
371
|
-
"Credential stored; scrubbed value from recent transcripts",
|
|
372
|
-
);
|
|
373
|
-
} catch (err) {
|
|
374
|
-
log.warn(
|
|
375
|
-
{ err, service, field },
|
|
376
|
-
"Credential stored, but transcript scrub failed",
|
|
377
|
-
);
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
upsertCredentialMetadata(service, field, {});
|
|
381
|
-
await syncManualTokenConnection(service);
|
|
382
|
-
if (service === "vellum" && field === "platform_base_url") {
|
|
383
|
-
setPlatformBaseUrl(effectiveValue);
|
|
384
|
-
}
|
|
385
|
-
if (service === "vellum" && field === "platform_assistant_id") {
|
|
386
|
-
setPlatformAssistantId(effectiveValue || undefined);
|
|
387
|
-
}
|
|
388
|
-
if (service === "vellum" && field === "platform_organization_id") {
|
|
389
|
-
setPlatformOrganizationId(effectiveValue || undefined);
|
|
390
|
-
}
|
|
391
|
-
if (service === "vellum" && field === "platform_user_id") {
|
|
392
|
-
setPlatformUserId(effectiveValue || undefined);
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
if (isPlatformManagedCredential(service, field)) {
|
|
396
|
-
await refreshProvidersAfterSecretChange();
|
|
397
|
-
// Close the first-boot race where the startup capability seed ran before
|
|
398
|
-
// the managed embedding credential was provisioned, leaving skill/CLI
|
|
399
|
-
// pages unseeded until restart. Detached — must not block the response.
|
|
400
|
-
void maybeReseedCapabilitiesAfterManagedCredential(getConfig());
|
|
401
|
-
if (service === "vellum" && field === "assistant_api_key") {
|
|
402
|
-
await notifyCesOfAssistantApiKeyUpdate(value, getCesClient());
|
|
403
|
-
}
|
|
404
|
-
} else if (!isTrimmedIdentity) {
|
|
405
|
-
await refreshProvidersForRotatedCredential(service, field);
|
|
406
|
-
}
|
|
407
|
-
if (
|
|
408
|
-
service === "vellum" &&
|
|
409
|
-
(field === "assistant_api_key" ||
|
|
410
|
-
field === "platform_assistant_id" ||
|
|
411
|
-
field === "platform_base_url")
|
|
412
|
-
) {
|
|
413
|
-
// Managed-speech availability needs the API key, the assistant ID,
|
|
414
|
-
// and the base URL, and the CLI connect path stores all three
|
|
415
|
-
// concurrently — fire on each so the last write to land triggers the
|
|
416
|
-
// defaulting; the hook no-ops until the connection is complete.
|
|
417
|
-
// Detached — must not block the response.
|
|
418
|
-
void maybeDefaultSpeechToManaged();
|
|
419
|
-
// Same last-write-wins shape: the startup syncs no-op before live
|
|
420
|
-
// registration, so re-enqueue them here. Both dedup and no-op until
|
|
421
|
-
// the client and assistant id exist.
|
|
422
|
-
syncWorkspaceIdentityToPlatform();
|
|
423
|
-
syncAvatarToPlatform();
|
|
424
|
-
}
|
|
416
|
+
await persistCredential(service, field, value);
|
|
417
|
+
await refreshProvidersForRotatedCredential(service, field);
|
|
425
418
|
log.info({ service, field }, "Credential added via HTTP");
|
|
426
419
|
return { success: true, type, name };
|
|
427
420
|
}
|
|
@@ -464,8 +457,7 @@ async function handleReadSecret({ body }: RouteHandlerArgs) {
|
|
|
464
457
|
try {
|
|
465
458
|
let accountKey: string;
|
|
466
459
|
let prefetchedResult:
|
|
467
|
-
|
|
468
|
-
| undefined;
|
|
460
|
+
Awaited<ReturnType<typeof getSecureKeyResultAsync>> | undefined;
|
|
469
461
|
|
|
470
462
|
if (type === "api_key") {
|
|
471
463
|
if (
|
|
@@ -609,6 +601,9 @@ async function handleDeleteSecret({ body }: RouteHandlerArgs) {
|
|
|
609
601
|
);
|
|
610
602
|
}
|
|
611
603
|
const service = name.slice(0, colonIdx);
|
|
604
|
+
if (service === "vellum") {
|
|
605
|
+
throw new BadRequestError("Vellum credentials cannot be deleted");
|
|
606
|
+
}
|
|
612
607
|
const field = name.slice(colonIdx + 1);
|
|
613
608
|
assertMetadataWritable();
|
|
614
609
|
const key = credentialKey(service, field);
|
|
@@ -624,21 +619,6 @@ async function handleDeleteSecret({ body }: RouteHandlerArgs) {
|
|
|
624
619
|
);
|
|
625
620
|
}
|
|
626
621
|
deleteCredentialMetadata(service, field);
|
|
627
|
-
if (service === "vellum" && field === "platform_base_url") {
|
|
628
|
-
setPlatformBaseUrl(undefined);
|
|
629
|
-
}
|
|
630
|
-
if (service === "vellum" && field === "platform_assistant_id") {
|
|
631
|
-
setPlatformAssistantId(undefined);
|
|
632
|
-
}
|
|
633
|
-
if (service === "vellum" && field === "platform_organization_id") {
|
|
634
|
-
setPlatformOrganizationId(undefined);
|
|
635
|
-
}
|
|
636
|
-
if (service === "vellum" && field === "platform_user_id") {
|
|
637
|
-
setPlatformUserId(undefined);
|
|
638
|
-
}
|
|
639
|
-
if (isPlatformManagedCredential(service, field)) {
|
|
640
|
-
await refreshProvidersAfterSecretChange();
|
|
641
|
-
}
|
|
642
622
|
invalidateConnectionsAfterCredentialDelete(affectedConnections);
|
|
643
623
|
log.info({ service, field }, "Credential deleted via HTTP");
|
|
644
624
|
return { success: true, type, name };
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Timing coverage for the schedule-worker MCP bootstrap and reload helpers.
|
|
3
|
+
*
|
|
4
|
+
* The worker entrypoint cannot be imported (it calls process.exit). These
|
|
5
|
+
* tests drive the extracted helpers with a delayed start so a due schedule
|
|
6
|
+
* cannot observe a ready surface before registration settles.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
10
|
+
|
|
11
|
+
import { EMPTY_MCP_STARTUP_SNAPSHOT } from "../../mcp/tool-caps.js";
|
|
12
|
+
import {
|
|
13
|
+
getScheduleToolSurfaceState,
|
|
14
|
+
isScheduleToolSurfaceReady,
|
|
15
|
+
resetScheduleToolSurfaceReadinessForTests,
|
|
16
|
+
} from "../tool-surface-readiness.js";
|
|
17
|
+
import {
|
|
18
|
+
bootstrapScheduleWorkerMcp,
|
|
19
|
+
reloadScheduleWorkerMcp,
|
|
20
|
+
} from "../worker-mcp.js";
|
|
21
|
+
|
|
22
|
+
function deferred<T>(): {
|
|
23
|
+
promise: Promise<T>;
|
|
24
|
+
resolve: (value: T) => void;
|
|
25
|
+
} {
|
|
26
|
+
let resolve!: (value: T) => void;
|
|
27
|
+
const promise = new Promise<T>((res) => {
|
|
28
|
+
resolve = res;
|
|
29
|
+
});
|
|
30
|
+
return { promise, resolve };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
afterEach(() => {
|
|
34
|
+
resetScheduleToolSurfaceReadinessForTests();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe("bootstrapScheduleWorkerMcp", () => {
|
|
38
|
+
test("stays unready past the grace period until start settles", async () => {
|
|
39
|
+
const gate = deferred<typeof EMPTY_MCP_STARTUP_SNAPSHOT>();
|
|
40
|
+
const snapshot = {
|
|
41
|
+
...EMPTY_MCP_STARTUP_SNAPSHOT,
|
|
42
|
+
configuredServerCount: 8,
|
|
43
|
+
connectedServerCount: 8,
|
|
44
|
+
registeredToolCount: 3,
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const boot = bootstrapScheduleWorkerMcp({
|
|
48
|
+
start: () => gate.promise,
|
|
49
|
+
graceMs: 20,
|
|
50
|
+
});
|
|
51
|
+
await boot;
|
|
52
|
+
|
|
53
|
+
expect(isScheduleToolSurfaceReady()).toBe(false);
|
|
54
|
+
expect(getScheduleToolSurfaceState()).toBe("starting");
|
|
55
|
+
|
|
56
|
+
gate.resolve(snapshot);
|
|
57
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
58
|
+
|
|
59
|
+
expect(isScheduleToolSurfaceReady()).toBe(true);
|
|
60
|
+
expect(getScheduleToolSurfaceState()).toBe("ready");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("marks ready when start finishes before the grace deadline", async () => {
|
|
64
|
+
await bootstrapScheduleWorkerMcp({
|
|
65
|
+
start: async () => ({
|
|
66
|
+
...EMPTY_MCP_STARTUP_SNAPSHOT,
|
|
67
|
+
registeredToolCount: 2,
|
|
68
|
+
}),
|
|
69
|
+
graceMs: 50,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
expect(isScheduleToolSurfaceReady()).toBe(true);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe("reloadScheduleWorkerMcp", () => {
|
|
77
|
+
test("is not ready while reload is in flight", async () => {
|
|
78
|
+
const gate = deferred<typeof EMPTY_MCP_STARTUP_SNAPSHOT>();
|
|
79
|
+
const reloading = reloadScheduleWorkerMcp({
|
|
80
|
+
restart: () => gate.promise,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
expect(getScheduleToolSurfaceState()).toBe("reloading");
|
|
84
|
+
expect(isScheduleToolSurfaceReady()).toBe(false);
|
|
85
|
+
|
|
86
|
+
gate.resolve({
|
|
87
|
+
...EMPTY_MCP_STARTUP_SNAPSHOT,
|
|
88
|
+
registeredToolCount: 4,
|
|
89
|
+
});
|
|
90
|
+
await reloading;
|
|
91
|
+
|
|
92
|
+
expect(isScheduleToolSurfaceReady()).toBe(true);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("failed reload does not stay in reloading", async () => {
|
|
96
|
+
await expect(
|
|
97
|
+
reloadScheduleWorkerMcp({
|
|
98
|
+
restart: async () => {
|
|
99
|
+
throw new Error("reconnect failed");
|
|
100
|
+
},
|
|
101
|
+
}),
|
|
102
|
+
).rejects.toThrow("reconnect failed");
|
|
103
|
+
|
|
104
|
+
expect(getScheduleToolSurfaceState()).toBe("ready");
|
|
105
|
+
expect(isScheduleToolSurfaceReady()).toBe(true);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scheduler gate for tool-backed schedules while the MCP surface is not ready.
|
|
3
|
+
*
|
|
4
|
+
* A due execute schedule must not record success while worker MCP
|
|
5
|
+
* initialization is still incomplete. Once ready, the expected MCP tool
|
|
6
|
+
* has to be present in the registry the turn would use.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { afterEach, describe, expect, mock, test } from "bun:test";
|
|
10
|
+
|
|
11
|
+
import { getDb } from "../../persistence/db-connection.js";
|
|
12
|
+
import { initializeDb } from "../../persistence/db-init.js";
|
|
13
|
+
import { RiskLevel } from "../../tools/tool-types.js";
|
|
14
|
+
import { createSchedule } from "../schedule-store.js";
|
|
15
|
+
import {
|
|
16
|
+
markScheduleToolSurfaceStarting,
|
|
17
|
+
resetScheduleToolSurfaceReadinessForTests,
|
|
18
|
+
} from "../tool-surface-readiness.js";
|
|
19
|
+
|
|
20
|
+
const processedPrompts: string[] = [];
|
|
21
|
+
mock.module("../../runtime/background-job-runner.js", () => ({
|
|
22
|
+
runBackgroundJob: async (opts: { prompt: string }) => {
|
|
23
|
+
const { getMcpToolDefinitions } = await import("../../tools/registry.js");
|
|
24
|
+
capturedMcpTools = getMcpToolDefinitions().map((tool) => tool.name);
|
|
25
|
+
processedPrompts.push(opts.prompt);
|
|
26
|
+
return { conversationId: "conv-xyz", ok: true };
|
|
27
|
+
},
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
mock.module("../../daemon/process-message.js", () => ({
|
|
31
|
+
processMessage: async () => ({ messageId: "msg-xyz" }),
|
|
32
|
+
processMessageInBackground: async () => {},
|
|
33
|
+
}));
|
|
34
|
+
|
|
35
|
+
mock.module("../../background-wake/publisher.js", () => ({
|
|
36
|
+
refreshBackgroundWakeIntent: () => {},
|
|
37
|
+
}));
|
|
38
|
+
|
|
39
|
+
mock.module("../../daemon/disk-pressure-background-gate.js", () => ({
|
|
40
|
+
checkDiskPressureBackgroundGate: () => ({
|
|
41
|
+
action: "allow",
|
|
42
|
+
status: {
|
|
43
|
+
enabled: false,
|
|
44
|
+
state: "disabled",
|
|
45
|
+
locked: false,
|
|
46
|
+
acknowledged: false,
|
|
47
|
+
overrideActive: false,
|
|
48
|
+
effectivelyLocked: false,
|
|
49
|
+
lockId: null,
|
|
50
|
+
usagePercent: null,
|
|
51
|
+
thresholdPercent: 95,
|
|
52
|
+
path: null,
|
|
53
|
+
lastCheckedAt: null,
|
|
54
|
+
blockedCapabilities: [],
|
|
55
|
+
error: null,
|
|
56
|
+
},
|
|
57
|
+
}),
|
|
58
|
+
diskPressureBackgroundSkipLogFields: () => ({}),
|
|
59
|
+
shouldLogDiskPressureBackgroundSkip: () => false,
|
|
60
|
+
}));
|
|
61
|
+
|
|
62
|
+
const mockEmitNotificationSignal = mock(() => Promise.resolve());
|
|
63
|
+
mock.module("../../notifications/emit-signal.js", () => ({
|
|
64
|
+
emitNotificationSignal: mockEmitNotificationSignal,
|
|
65
|
+
}));
|
|
66
|
+
|
|
67
|
+
const { runDueSchedulesOnce } = await import("../scheduler.js");
|
|
68
|
+
const { __resetRegistryForTesting, getTool, registerMcpTools } =
|
|
69
|
+
await import("../../tools/registry.js");
|
|
70
|
+
|
|
71
|
+
await initializeDb();
|
|
72
|
+
|
|
73
|
+
let capturedMcpTools: string[] = [];
|
|
74
|
+
|
|
75
|
+
function rawDb(): import("bun:sqlite").Database {
|
|
76
|
+
return (getDb() as unknown as { $client: import("bun:sqlite").Database })
|
|
77
|
+
.$client;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
afterEach(() => {
|
|
81
|
+
resetScheduleToolSurfaceReadinessForTests();
|
|
82
|
+
processedPrompts.length = 0;
|
|
83
|
+
capturedMcpTools = [];
|
|
84
|
+
mockEmitNotificationSignal.mockClear();
|
|
85
|
+
__resetRegistryForTesting();
|
|
86
|
+
const db = getDb();
|
|
87
|
+
db.run("DELETE FROM cron_runs");
|
|
88
|
+
db.run("DELETE FROM cron_jobs");
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe("automatic schedule MCP readiness", () => {
|
|
92
|
+
test("defers a due execute schedule while MCP is still starting", async () => {
|
|
93
|
+
markScheduleToolSurfaceStarting(8);
|
|
94
|
+
const job = await createSchedule({
|
|
95
|
+
name: "Friday tagging",
|
|
96
|
+
cronExpression: "* * * * *",
|
|
97
|
+
message: "Tag new Reader documents",
|
|
98
|
+
mode: "execute",
|
|
99
|
+
});
|
|
100
|
+
rawDb().run("UPDATE cron_jobs SET next_run_at = ?", [Date.now() - 1000]);
|
|
101
|
+
|
|
102
|
+
const result = await runDueSchedulesOnce();
|
|
103
|
+
|
|
104
|
+
expect(result.claimed).toBe(1);
|
|
105
|
+
expect(result.skipped).toBe(1);
|
|
106
|
+
expect(result.completed).toBe(0);
|
|
107
|
+
expect(processedPrompts).toEqual([]);
|
|
108
|
+
const runs = rawDb()
|
|
109
|
+
.query("SELECT status FROM cron_runs WHERE job_id = ?")
|
|
110
|
+
.all(job.id) as Array<{ status: string }>;
|
|
111
|
+
expect(runs).toEqual([]);
|
|
112
|
+
const row = rawDb()
|
|
113
|
+
.query("SELECT status, enabled FROM cron_jobs WHERE id = ?")
|
|
114
|
+
.get(job.id) as { status: string; enabled: number };
|
|
115
|
+
expect(row.status).toBe("active");
|
|
116
|
+
expect(row.enabled).toBe(1);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("still fires notify and script schedules while MCP is starting", async () => {
|
|
120
|
+
markScheduleToolSurfaceStarting(8);
|
|
121
|
+
await createSchedule({
|
|
122
|
+
name: "Ping",
|
|
123
|
+
cronExpression: "* * * * *",
|
|
124
|
+
message: "ping",
|
|
125
|
+
mode: "notify",
|
|
126
|
+
});
|
|
127
|
+
await createSchedule({
|
|
128
|
+
name: "Script",
|
|
129
|
+
cronExpression: "* * * * *",
|
|
130
|
+
message: "run script",
|
|
131
|
+
mode: "script",
|
|
132
|
+
script: "true",
|
|
133
|
+
});
|
|
134
|
+
rawDb().run("UPDATE cron_jobs SET next_run_at = ?", [Date.now() - 1000]);
|
|
135
|
+
|
|
136
|
+
const result = await runDueSchedulesOnce();
|
|
137
|
+
|
|
138
|
+
expect(result.completed).toBe(2);
|
|
139
|
+
expect(mockEmitNotificationSignal).toHaveBeenCalledTimes(1);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test("an automatic execute run sees the registered MCP tool once ready", async () => {
|
|
143
|
+
registerMcpTools("reader", [
|
|
144
|
+
{
|
|
145
|
+
name: "mcp__reader__list_documents",
|
|
146
|
+
description: "List documents",
|
|
147
|
+
defaultRiskLevel: RiskLevel.Low,
|
|
148
|
+
executionTarget: "sandbox",
|
|
149
|
+
input_schema: { type: "object", properties: {}, required: [] },
|
|
150
|
+
category: "",
|
|
151
|
+
async execute() {
|
|
152
|
+
return { content: "ok", isError: false };
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
]);
|
|
156
|
+
resetScheduleToolSurfaceReadinessForTests();
|
|
157
|
+
|
|
158
|
+
await createSchedule({
|
|
159
|
+
name: "Saturday digest",
|
|
160
|
+
cronExpression: "* * * * *",
|
|
161
|
+
message: "Build the Reader digest",
|
|
162
|
+
mode: "execute",
|
|
163
|
+
});
|
|
164
|
+
rawDb().run("UPDATE cron_jobs SET next_run_at = ?", [Date.now() - 1000]);
|
|
165
|
+
|
|
166
|
+
const result = await runDueSchedulesOnce();
|
|
167
|
+
|
|
168
|
+
expect(result.completed).toBe(1);
|
|
169
|
+
expect(processedPrompts).toEqual(["Build the Reader digest"]);
|
|
170
|
+
expect(capturedMcpTools).toContain("mcp__reader__list_documents");
|
|
171
|
+
expect(getTool("mcp__reader__list_documents")).toBeDefined();
|
|
172
|
+
});
|
|
173
|
+
});
|
|
@@ -1,38 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Guard over the schedule worker entrypoint's MCP bootstrap.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* makes as "Unknown tool", while `assistant tools list` reads the daemon's
|
|
9
|
-
* registry over IPC and reports the same tool as registered. The mismatch is
|
|
10
|
-
* what makes the failure hard to place, so it is worth a guard.
|
|
11
|
-
*
|
|
12
|
-
* The worker is also long-lived, so the set it connects at startup goes stale
|
|
13
|
-
* as soon as the config moves. It watches for the daemon's reload signal to
|
|
14
|
-
* rebuild, which is covered here too.
|
|
15
|
-
*
|
|
16
|
-
* The entrypoint installs signal handlers and calls `process.exit`, so it
|
|
17
|
-
* cannot be imported into a test process; these assertions read its source the
|
|
18
|
-
* way `worker-feature-flags.test.ts` does. The behavior of the steps
|
|
19
|
-
* themselves is covered in `src/mcp/__tests__/startup.test.ts` and
|
|
20
|
-
* `src/mcp/__tests__/reload-signal.test.ts`.
|
|
4
|
+
* Behavioral coverage lives in worker-mcp-readiness.test.ts. This file only
|
|
5
|
+
* asserts the process entrypoint still calls the extracted helpers before
|
|
6
|
+
* the first tick, because the entrypoint installs signal handlers and
|
|
7
|
+
* calls process.exit and cannot be imported into a test process.
|
|
21
8
|
*/
|
|
22
9
|
|
|
23
10
|
import { readFileSync } from "node:fs";
|
|
24
11
|
import { join } from "node:path";
|
|
25
12
|
import { describe, expect, test } from "bun:test";
|
|
26
13
|
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
/** The bounded wait that keeps unreachable servers off the critical path. */
|
|
30
|
-
const MCP_GRACE = "MCP_STARTUP_GRACE_MS";
|
|
31
|
-
|
|
32
|
-
/** The reload watcher that keeps the connected set current. */
|
|
14
|
+
const MCP_BOOTSTRAP = "bootstrapScheduleWorkerMcp(";
|
|
15
|
+
const MCP_RELOAD = "reloadScheduleWorkerMcp(";
|
|
33
16
|
const MCP_WATCH = "watchForMcpReload(";
|
|
34
|
-
|
|
35
|
-
/** The worker's first call that can execute a schedule. */
|
|
36
17
|
const WORK_START = "void tick()";
|
|
37
18
|
|
|
38
19
|
function readWorkerSource(): string {
|
|
@@ -40,37 +21,22 @@ function readWorkerSource(): string {
|
|
|
40
21
|
}
|
|
41
22
|
|
|
42
23
|
describe("schedule worker MCP tools", () => {
|
|
43
|
-
test("
|
|
24
|
+
test("bootstraps its own MCP servers before the first schedule tick", () => {
|
|
44
25
|
const source = readWorkerSource();
|
|
45
|
-
const mcpAt = source.indexOf(
|
|
26
|
+
const mcpAt = source.indexOf(MCP_BOOTSTRAP);
|
|
46
27
|
const workAt = source.indexOf(WORK_START);
|
|
47
28
|
|
|
48
29
|
expect(
|
|
49
30
|
mcpAt,
|
|
50
|
-
`The schedule worker must call \`${
|
|
31
|
+
`The schedule worker must call \`${MCP_BOOTSTRAP})\` at startup. This ` +
|
|
51
32
|
"process hosts execute-mode schedule turns, and its tool registry is " +
|
|
52
33
|
"its own: without the call it has no mcp__* tools at all.",
|
|
53
34
|
).toBeGreaterThanOrEqual(0);
|
|
54
35
|
expect(workAt).toBeGreaterThanOrEqual(0);
|
|
55
36
|
expect(
|
|
56
37
|
mcpAt < workAt,
|
|
57
|
-
"The connect starts and is waited on before the first tick
|
|
58
|
-
"
|
|
59
|
-
"against. The wait is bounded (see below), so a server that never " +
|
|
60
|
-
"answers delays it rather than blocking it.",
|
|
61
|
-
).toBe(true);
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
test("bounds the startup wait so unrelated schedules still fire", () => {
|
|
65
|
-
const source = readWorkerSource();
|
|
66
|
-
|
|
67
|
-
expect(
|
|
68
|
-
source.includes(MCP_GRACE),
|
|
69
|
-
"`McpServerManager.start()` walks servers one at a time and allows " +
|
|
70
|
-
"each 30s to connect and 30s more to list its tools. An unbounded " +
|
|
71
|
-
"wait lets a few unreachable servers hold every due schedule, " +
|
|
72
|
-
"including the notify and script ones that never touch MCP, while " +
|
|
73
|
-
"the PID file already reports this worker as running.",
|
|
38
|
+
"The connect starts and is waited on before the first tick. Tool-backed " +
|
|
39
|
+
"schedules stay deferred until that surface is marked ready.",
|
|
74
40
|
).toBe(true);
|
|
75
41
|
});
|
|
76
42
|
|
|
@@ -78,11 +44,11 @@ describe("schedule worker MCP tools", () => {
|
|
|
78
44
|
const source = readWorkerSource();
|
|
79
45
|
|
|
80
46
|
expect(
|
|
81
|
-
source.includes(MCP_WATCH),
|
|
47
|
+
source.includes(MCP_WATCH) && source.includes(MCP_RELOAD),
|
|
82
48
|
"This process is long-lived and holds its own connections. Without " +
|
|
83
49
|
"the watcher, a server disabled or removed in the config stays " +
|
|
84
50
|
"registered and callable by background schedules until the worker " +
|
|
85
|
-
"restarts.",
|
|
51
|
+
"restarts. Reload must mark the tool surface unready until it settles.",
|
|
86
52
|
).toBe(true);
|
|
87
53
|
});
|
|
88
54
|
});
|