@vellumai/assistant 0.11.5 → 0.11.6-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 +5 -1
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +118 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/ingress.ts +103 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +118 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/ingress.ts +103 -0
- package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +70 -0
- package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +16 -1
- package/node_modules/@vellumai/gateway-client/src/index.ts +6 -2
- package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +121 -61
- package/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +118 -0
- package/node_modules/@vellumai/service-contracts/src/ingress.ts +103 -0
- package/openapi.yaml +421 -15
- package/package.json +1 -1
- package/scripts/sync-web-search-catalog.ts +6 -0
- package/src/__tests__/app-pin-store.test.ts +149 -0
- package/src/__tests__/channel-availability-routes.test.ts +23 -1
- package/src/__tests__/channel-readiness-discord.test.ts +231 -0
- package/src/__tests__/channel-readiness-service.test.ts +126 -0
- package/src/__tests__/channel-readiness-slack-remote.test.ts +141 -0
- package/src/__tests__/channel-reply-delivery.test.ts +4 -4
- package/src/__tests__/client-os-metadata-persistence.test.ts +23 -10
- package/src/__tests__/conversation-delete-watch-timeline.test.ts +231 -0
- package/src/__tests__/conversation-error.test.ts +17 -0
- package/src/__tests__/conversation-seed-composer.test.ts +8 -0
- package/src/__tests__/conversation-slash-commands.test.ts +8 -0
- package/src/__tests__/disk-pressure-policy.test.ts +6 -0
- package/src/__tests__/gemini-provider.test.ts +138 -0
- package/src/__tests__/history-repair.test.ts +105 -3
- package/src/__tests__/identity-routes.test.ts +1 -0
- package/src/__tests__/llm-catalog-parity.test.ts +45 -0
- package/src/__tests__/migration-import-from-path.test.ts +349 -0
- package/src/__tests__/notification-telegram-adapter.test.ts +102 -0
- package/src/__tests__/oauth-commands-routes.test.ts +89 -0
- package/src/__tests__/oauth-provider-profiles.test.ts +7 -6
- package/src/__tests__/openai-provider.test.ts +18 -0
- package/src/__tests__/openai-responses-provider.test.ts +18 -0
- package/src/__tests__/platform-callback-registration.test.ts +184 -0
- package/src/__tests__/plugin-api-store-credential.test.ts +71 -3
- package/src/__tests__/pricing.test.ts +2 -2
- package/src/__tests__/public-ingress-urls.test.ts +36 -0
- package/src/__tests__/resolve-trust-class.test.ts +0 -48
- package/src/__tests__/sanitize-config-for-transfer.test.ts +28 -0
- package/src/__tests__/secret-routes-platform-proxy.test.ts +49 -0
- package/src/__tests__/settings-routes.test.ts +85 -3
- package/src/__tests__/web-search-catalog-parity.test.ts +8 -0
- package/src/agent/history-repair/history-repair.ts +45 -14
- package/src/agent/loop.ts +4 -1
- package/src/api/constants/profile-config-validation.ts +60 -0
- package/src/api/events/tool-result.ts +6 -1
- package/src/api/events/watch-retro-completed.ts +52 -0
- package/src/api/index.ts +11 -0
- package/src/apps/app-pin-reconciler.ts +92 -0
- package/src/apps/app-pin-store.ts +125 -0
- package/src/channels/gateway-channel-socket-health.ts +32 -0
- package/src/channels/gateway-discord-admission.ts +32 -0
- package/src/channels/types.ts +20 -0
- package/src/cli/commands/__tests__/conversations-slack.test.ts +1 -1
- package/src/cli/commands/__tests__/inference-profiles.test.ts +16 -4
- package/src/cli/commands/__tests__/inference-providers.test.ts +67 -2
- package/src/cli/commands/channels/__tests__/channels.test.ts +85 -0
- package/src/cli/commands/channels/index.ts +45 -31
- package/src/cli/commands/inference-profiles.ts +56 -3
- package/src/cli/commands/inference-providers.ts +28 -2
- package/src/cli/commands/oauth/index.help.ts +7 -1
- package/src/cli/commands/oauth/request.test.ts +290 -0
- package/src/cli/commands/oauth/request.ts +57 -41
- package/src/cli/lib/bundled-marketplace.json +14 -1
- package/src/cli/lib/open-browser.test.ts +67 -0
- package/src/cli/lib/open-browser.ts +24 -5
- package/src/config/__tests__/profile-materialization.test.ts +26 -0
- package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +6 -0
- package/src/config/bundled-skills/schedule/SKILL.md +1 -1
- package/src/config/feature-flag-registry.json +17 -1
- package/src/config/profile-materialization.ts +29 -0
- package/src/config/sanitize-for-transfer.ts +16 -0
- package/src/config/schemas/llm.ts +7 -0
- package/src/config/schemas/services.ts +6 -0
- package/src/context/outbound-sanitize.ts +6 -0
- package/src/daemon/__tests__/lifecycle-watch-timeline-sweep.test.ts +98 -0
- package/src/daemon/conversation-error.ts +24 -2
- package/src/daemon/conversation-slash.ts +6 -15
- package/src/daemon/daemon-control.ts +1 -0
- package/src/daemon/disk-pressure-policy.ts +7 -1
- package/src/daemon/handlers/__tests__/config-ingress-tunnel-records.test.ts +208 -0
- package/src/daemon/handlers/config-ingress.ts +115 -5
- package/src/daemon/lifecycle.ts +26 -0
- package/src/daemon/message-types/web-activity.ts +3 -2
- package/src/daemon/trust-context.ts +0 -37
- package/src/inbound/__tests__/tunnel-probe.test.ts +448 -0
- package/src/inbound/platform-callback-registration.ts +28 -2
- package/src/inbound/public-ingress-urls.ts +12 -0
- package/src/inbound/tunnel-probe.ts +261 -0
- package/src/live-voice/__tests__/live-voice-connection.test.ts +25 -0
- package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +8 -2
- package/src/live-voice/__tests__/live-voice-session-manager.test.ts +212 -10
- package/src/live-voice/__tests__/live-voice-session-telemetry.test.ts +5 -2
- package/src/live-voice/live-voice-connection.ts +46 -8
- package/src/live-voice/live-voice-manager.ts +25 -0
- package/src/live-voice/live-voice-session-manager.ts +318 -2
- package/src/live-voice/live-voice-session.ts +52 -2
- package/src/messaging/providers/__tests__/transport-dispatch.test.ts +126 -68
- package/src/messaging/providers/channel-transport.ts +64 -47
- package/src/messaging/providers/discord/send.test.ts +46 -1
- package/src/messaging/providers/discord/send.ts +51 -0
- package/src/messaging/providers/discord/transport.ts +26 -3
- package/src/messaging/providers/index.ts +22 -47
- package/src/messaging/providers/slack/send.test.ts +83 -26
- package/src/messaging/providers/slack/send.ts +120 -51
- package/src/messaging/providers/slack/stream-tasks.test.ts +26 -0
- package/src/messaging/providers/slack/stream-tasks.ts +39 -0
- package/src/messaging/providers/slack/transport.ts +24 -22
- package/src/messaging/providers/telegram-bot/send.test.ts +109 -12
- package/src/messaging/providers/telegram-bot/send.ts +43 -0
- package/src/messaging/providers/telegram-bot/transport.ts +25 -8
- package/src/notifications/__tests__/assistant-reply-producer.test.ts +30 -7
- package/src/notifications/adapters/telegram.ts +48 -1
- package/src/notifications/assistant-reply-producer.ts +7 -7
- package/src/notifications/conversation-seed-composer.ts +7 -2
- package/src/oauth/byo-connection.test.ts +63 -0
- package/src/oauth/byo-connection.ts +16 -15
- package/src/oauth/connection.test.ts +111 -0
- package/src/oauth/connection.ts +142 -1
- package/src/oauth/platform-connection.test.ts +34 -0
- package/src/oauth/platform-connection.ts +28 -5
- package/src/oauth/seed-providers.ts +15 -1
- package/src/permissions/types.ts +3 -1
- package/src/persistence/conversation-crud.ts +46 -0
- package/src/persistence/conversation-types.ts +11 -9
- package/src/persistence/db-async-query.ts +2 -1
- package/src/persistence/db-maintenance.ts +15 -0
- package/src/persistence/embeddings/qdrant-manager.ts +1 -0
- package/src/persistence/migrations/367-create-watch-timeline-entries.ts +46 -0
- package/src/persistence/migrations/368-watch-timeline-screenshot-blob.ts +33 -0
- package/src/persistence/migrations/369-create-app-pins.ts +37 -0
- package/src/persistence/migrations/__tests__/367-create-watch-timeline-entries.test.ts +98 -0
- package/src/persistence/migrations/__tests__/368-watch-timeline-screenshot-blob.test.ts +98 -0
- package/src/persistence/schema/index.ts +1 -0
- package/src/persistence/schema/infrastructure.ts +17 -0
- package/src/persistence/schema/watch.ts +29 -0
- package/src/persistence/steps.ts +6 -0
- package/src/plugins/mtime-cache.ts +11 -0
- package/src/providers/__tests__/retry-network-error.test.ts +84 -0
- package/src/providers/connection-resolution.ts +23 -1
- package/src/providers/content-blocks.ts +9 -0
- package/src/providers/fetch-provider-catalog.ts +19 -0
- package/src/providers/gemini/client.ts +13 -5
- package/src/providers/inference/__tests__/endpoint-probe.test.ts +92 -0
- package/src/providers/inference/__tests__/profile-config-validation.test.ts +39 -0
- package/src/providers/inference/__tests__/profile-probe-classify.test.ts +66 -0
- package/src/providers/inference/adapter-factory.ts +0 -9
- package/src/providers/inference/credential-rotation.ts +61 -0
- package/src/providers/inference/endpoint-probe.ts +115 -0
- package/src/providers/inference/profile-probe.ts +256 -0
- package/src/providers/model-catalog.ts +170 -125
- package/src/providers/openai/__tests__/api-error-normalization.test.ts +17 -1
- package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +42 -60
- package/src/providers/openai/__tests__/connection-error-wrap.test.ts +44 -0
- package/src/providers/openai/__tests__/orphan-tool-result-guard.test.ts +34 -2
- package/src/providers/openai/api-error-normalization.ts +16 -2
- package/src/providers/openai/chat-completions-provider.ts +75 -29
- package/src/providers/openai/responses-provider.ts +5 -2
- package/src/providers/openrouter/client.ts +0 -1
- package/src/providers/provider-send-message.ts +11 -0
- package/src/providers/retry.ts +6 -0
- package/src/providers/search-provider-catalog.ts +20 -0
- package/src/providers/vercel-ai-gateway/client.ts +0 -1
- package/src/runtime/AGENTS.md +1 -0
- package/src/runtime/__tests__/desktop-presence.test.ts +27 -4
- package/src/runtime/__tests__/host-observe.test.ts +302 -0
- package/src/runtime/channel-readiness-service.ts +214 -14
- package/src/runtime/channel-readiness-types.ts +49 -2
- package/src/runtime/channel-reply-delivery.ts +2 -2
- package/src/runtime/desktop-presence.ts +24 -21
- package/src/runtime/host-observe.ts +246 -0
- package/src/runtime/http-server.ts +181 -1
- package/src/runtime/migrations/__tests__/staged-import-path.test.ts +104 -0
- package/src/runtime/migrations/staged-import-path.ts +116 -0
- package/src/runtime/routes/__tests__/app-pin-routes.test.ts +383 -0
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +80 -0
- package/src/runtime/routes/__tests__/inference-profiles-routes.test.ts +118 -0
- package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +20 -0
- package/src/runtime/routes/__tests__/ingress-status-routes.test.ts +508 -0
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +35 -56
- package/src/runtime/routes/__tests__/watch-routes-guardian-cache.test.ts +139 -0
- package/src/runtime/routes/__tests__/watch-routes.test.ts +598 -0
- package/src/runtime/routes/app-management-routes.ts +140 -29
- package/src/runtime/routes/channel-availability-routes.ts +1 -0
- package/src/runtime/routes/channel-readiness-routes.ts +14 -2
- package/src/runtime/routes/conversation-query-routes.ts +10 -0
- package/src/runtime/routes/guardian-approval-interception.ts +24 -33
- package/src/runtime/routes/host-cu-routes.ts +18 -0
- package/src/runtime/routes/identity-routes.ts +2 -0
- package/src/runtime/routes/inbound-message-handler.ts +10 -7
- package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +166 -308
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +158 -335
- package/src/runtime/routes/index.ts +2 -0
- package/src/runtime/routes/inference-profiles-routes.ts +232 -31
- package/src/runtime/routes/inference-provider-connection-routes.ts +24 -4
- package/src/runtime/routes/ingress-status-routes.ts +180 -0
- package/src/runtime/routes/live-voice-routes.test.ts +40 -1
- package/src/runtime/routes/live-voice-routes.ts +34 -0
- package/src/runtime/routes/migration-routes.ts +218 -10
- package/src/runtime/routes/oauth-commands-routes.ts +23 -16
- package/src/runtime/routes/plugins-routes.ts +12 -28
- package/src/runtime/routes/question-routes.ts +6 -0
- package/src/runtime/routes/secret-routes.ts +7 -27
- package/src/runtime/routes/settings-routes.ts +9 -6
- package/src/runtime/routes/watch-routes.ts +807 -0
- package/src/runtime/slack-reply-session.test.ts +230 -121
- package/src/runtime/slack-reply-session.ts +113 -81
- package/src/runtime/{slack-task-progress.test.ts → task-progress.test.ts} +1 -28
- package/src/runtime/{slack-task-progress.ts → task-progress.ts} +30 -51
- package/src/security/__tests__/untrusted-content.test.ts +42 -0
- package/src/security/untrusted-content.ts +28 -9
- package/src/telemetry/__tests__/live-voice-funnel.test.ts +108 -0
- package/src/telemetry/live-voice-funnel.ts +75 -8
- package/src/tools/credentials/store.ts +18 -6
- package/src/tools/network/__tests__/firecrawl-compat.test.ts +77 -0
- package/src/tools/network/__tests__/web-fetch-fastcrw.test.ts +169 -0
- package/src/tools/network/__tests__/web-search.test.ts +97 -2
- package/src/tools/network/firecrawl-compat.ts +90 -0
- package/src/tools/network/web-fetch.ts +142 -62
- package/src/tools/network/web-search.ts +141 -55
- package/src/tools/types.ts +2 -1
- package/src/util/oauth-request-body.test.ts +74 -0
- package/src/util/oauth-request-body.ts +60 -0
- package/src/util/worker-process.ts +1 -0
- package/src/watch/__tests__/watch-retro.test.ts +665 -0
- package/src/watch/__tests__/watch-session-manager.test.ts +566 -0
- package/src/watch/__tests__/watch-timeline.test.ts +670 -0
- package/src/watch/watch-retro.ts +480 -0
- package/src/watch/watch-session-manager.ts +575 -0
- package/src/watch/watch-timeline.ts +848 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Tests for the live-voice preflight
|
|
2
|
+
* Tests for the live-voice preflight and session-end routes.
|
|
3
3
|
*
|
|
4
4
|
* The handler must run managed-speech defaulting first, then return the
|
|
5
5
|
* live-voice credential readiness verdict verbatim:
|
|
@@ -7,6 +7,9 @@
|
|
|
7
7
|
* - not-ready → passes through the missing gaps + userMessage
|
|
8
8
|
* - defaulting is invoked BEFORE readiness is resolved
|
|
9
9
|
*
|
|
10
|
+
* The session-end route is the out-of-band recovery path (LUM-3440): it
|
|
11
|
+
* reports whether it actually reclaimed the daemon's single live-voice slot.
|
|
12
|
+
*
|
|
10
13
|
* NOTE: `bun mock.module` leaks across files. Run this file on its own
|
|
11
14
|
* (`bun test <thisfile>`) — a multi-file run may report a spurious failure.
|
|
12
15
|
*/
|
|
@@ -14,9 +17,11 @@
|
|
|
14
17
|
import { describe, expect, mock, test } from "bun:test";
|
|
15
18
|
|
|
16
19
|
import type { LiveVoiceCredentialReadiness } from "../../live-voice/live-voice-credential-preflight.js";
|
|
20
|
+
import type { LiveVoiceSessionReleaseResult } from "../../live-voice/live-voice-session-manager.js";
|
|
17
21
|
|
|
18
22
|
const calls: string[] = [];
|
|
19
23
|
let readiness: LiveVoiceCredentialReadiness = { status: "ready" };
|
|
24
|
+
let releaseResult: LiveVoiceSessionReleaseResult = { released: false };
|
|
20
25
|
|
|
21
26
|
mock.module("../../config/managed-speech-defaults.js", () => ({
|
|
22
27
|
maybeDefaultSpeechToManaged: async () => {
|
|
@@ -31,12 +36,25 @@ mock.module("../../live-voice/live-voice-credential-preflight.js", () => ({
|
|
|
31
36
|
},
|
|
32
37
|
}));
|
|
33
38
|
|
|
39
|
+
mock.module("../../live-voice/live-voice-manager.js", () => ({
|
|
40
|
+
getLiveVoiceSessionManager: () => ({
|
|
41
|
+
endActiveSession: async () => {
|
|
42
|
+
calls.push("endActiveSession");
|
|
43
|
+
return releaseResult;
|
|
44
|
+
},
|
|
45
|
+
}),
|
|
46
|
+
}));
|
|
47
|
+
|
|
34
48
|
const { ROUTES } = await import("./live-voice-routes.js");
|
|
35
49
|
|
|
36
50
|
const preflightRoute = ROUTES.find(
|
|
37
51
|
(r) => r.operationId === "live_voice_preflight_post",
|
|
38
52
|
)!;
|
|
39
53
|
|
|
54
|
+
const sessionEndRoute = ROUTES.find(
|
|
55
|
+
(r) => r.operationId === "live_voice_session_end_post",
|
|
56
|
+
)!;
|
|
57
|
+
|
|
40
58
|
describe("live_voice_preflight_post", () => {
|
|
41
59
|
test("returns { status: 'ready' } when readiness reports ready", async () => {
|
|
42
60
|
calls.length = 0;
|
|
@@ -71,3 +89,24 @@ describe("live_voice_preflight_post", () => {
|
|
|
71
89
|
expect(calls).toEqual(["default", "resolve"]);
|
|
72
90
|
});
|
|
73
91
|
});
|
|
92
|
+
|
|
93
|
+
describe("live_voice_session_end_post", () => {
|
|
94
|
+
test("reports the session it reclaimed", async () => {
|
|
95
|
+
calls.length = 0;
|
|
96
|
+
releaseResult = { released: true, sessionId: "session-1" };
|
|
97
|
+
|
|
98
|
+
const result = await sessionEndRoute.handler({});
|
|
99
|
+
|
|
100
|
+
expect(result).toEqual({ ended: true, sessionId: "session-1" });
|
|
101
|
+
expect(calls).toEqual(["endActiveSession"]);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("reports no session rather than failing when the slot is free", async () => {
|
|
105
|
+
calls.length = 0;
|
|
106
|
+
releaseResult = { released: false };
|
|
107
|
+
|
|
108
|
+
const result = await sessionEndRoute.handler({});
|
|
109
|
+
|
|
110
|
+
expect(result).toEqual({ ended: false, sessionId: null });
|
|
111
|
+
});
|
|
112
|
+
});
|
|
@@ -5,6 +5,11 @@
|
|
|
5
5
|
* then report whether the daemon can run both audio legs of a live voice
|
|
6
6
|
* session. Lets the web client verify voice is configured BEFORE opening the
|
|
7
7
|
* voice-room WebSocket, instead of opening it and reacting to an error frame.
|
|
8
|
+
*
|
|
9
|
+
* POST /v1/live-voice/session/end releases whatever session holds the
|
|
10
|
+
* daemon's single live-voice slot. The out-of-band half of session closure:
|
|
11
|
+
* the in-band `end` frame needs a working transport, and the case that needs
|
|
12
|
+
* ending most is the one where the transport is already gone.
|
|
8
13
|
*/
|
|
9
14
|
|
|
10
15
|
import { z } from "zod";
|
|
@@ -26,6 +31,17 @@ async function handleLiveVoicePreflight() {
|
|
|
26
31
|
return resolveLiveVoiceCredentialReadiness();
|
|
27
32
|
}
|
|
28
33
|
|
|
34
|
+
async function handleLiveVoiceSessionEnd() {
|
|
35
|
+
const { getLiveVoiceSessionManager } =
|
|
36
|
+
await import("../../live-voice/live-voice-manager.js");
|
|
37
|
+
|
|
38
|
+
const result = await getLiveVoiceSessionManager().endActiveSession();
|
|
39
|
+
return {
|
|
40
|
+
ended: result.released,
|
|
41
|
+
sessionId: result.released ? result.sessionId : null,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
29
45
|
// ---------------------------------------------------------------------------
|
|
30
46
|
// Route definitions
|
|
31
47
|
// ---------------------------------------------------------------------------
|
|
@@ -62,4 +78,22 @@ export const ROUTES: RouteDefinition[] = [
|
|
|
62
78
|
}),
|
|
63
79
|
handler: handleLiveVoicePreflight,
|
|
64
80
|
},
|
|
81
|
+
{
|
|
82
|
+
operationId: "live_voice_session_end_post",
|
|
83
|
+
endpoint: "live-voice/session/end",
|
|
84
|
+
method: "POST",
|
|
85
|
+
policy: {
|
|
86
|
+
requiredScopes: ["chat.write"],
|
|
87
|
+
allowedPrincipalTypes: ACTOR_PRINCIPALS,
|
|
88
|
+
},
|
|
89
|
+
summary: "End the active live voice session",
|
|
90
|
+
description:
|
|
91
|
+
"Release the assistant's single live-voice session slot, whichever client holds it. Reports whether a session was actually ended; a slot already tearing down reports false, since that teardown releases it on its own.",
|
|
92
|
+
tags: ["live-voice"],
|
|
93
|
+
responseBody: z.object({
|
|
94
|
+
ended: z.boolean(),
|
|
95
|
+
sessionId: z.string().nullable(),
|
|
96
|
+
}),
|
|
97
|
+
handler: handleLiveVoiceSessionEnd,
|
|
98
|
+
},
|
|
65
99
|
];
|
|
@@ -7,9 +7,11 @@
|
|
|
7
7
|
* POST /v1/migrations/import — commit a .vbundle archive import to disk.
|
|
8
8
|
*
|
|
9
9
|
* Accepts raw binary body (Content-Type: application/octet-stream),
|
|
10
|
-
* multipart form data with a "file" field, or
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* multipart form data with a "file" field, or a JSON body of shape
|
|
11
|
+
* `{ "url": "<signed-gcs-url>" }` (import only) or `{ "path": "<staged>" }`
|
|
12
|
+
* (import and import-preflight). The path form streams a file the CLI
|
|
13
|
+
* staged under `${workspace}/.restore-staging/`. The URL form fetches the
|
|
14
|
+
* bundle from GCS and streams it through `streamCommitImport`.
|
|
13
15
|
* Returns structured validation results with is_valid flag and detailed
|
|
14
16
|
* error descriptions.
|
|
15
17
|
*/
|
|
@@ -58,6 +60,10 @@ import {
|
|
|
58
60
|
migrationJobs,
|
|
59
61
|
} from "../migrations/job-registry.js";
|
|
60
62
|
import { getOriginMode } from "../migrations/origin-mode.js";
|
|
63
|
+
import {
|
|
64
|
+
resolveStagedImportPath,
|
|
65
|
+
StagedImportPathError,
|
|
66
|
+
} from "../migrations/staged-import-path.js";
|
|
61
67
|
import type {
|
|
62
68
|
VBundleAssistantInfo,
|
|
63
69
|
VBundleCompatibility,
|
|
@@ -81,6 +87,11 @@ import {
|
|
|
81
87
|
type ImportCommitResult,
|
|
82
88
|
} from "../migrations/vbundle-importer.js";
|
|
83
89
|
import { streamCommitImport } from "../migrations/vbundle-streaming-importer.js";
|
|
90
|
+
import {
|
|
91
|
+
readAndValidateManifest,
|
|
92
|
+
StreamingValidationError,
|
|
93
|
+
} from "../migrations/vbundle-streaming-validator.js";
|
|
94
|
+
import { parseVBundleStream } from "../migrations/vbundle-tar-stream.js";
|
|
84
95
|
import { validateVBundle } from "../migrations/vbundle-validator.js";
|
|
85
96
|
import {
|
|
86
97
|
BadGatewayError,
|
|
@@ -806,6 +817,8 @@ async function extractFileData(
|
|
|
806
817
|
* The file can be sent as:
|
|
807
818
|
* - Raw binary body with Content-Type: application/octet-stream
|
|
808
819
|
* - Multipart form data with a "file" field
|
|
820
|
+
* - JSON body `{ "path": "<staged-relative-or-absolute>" }` pointing at a
|
|
821
|
+
* `.vbundle` the caller placed under `${workspace}/.restore-staging/`
|
|
809
822
|
*
|
|
810
823
|
* Returns:
|
|
811
824
|
* 200: {
|
|
@@ -825,7 +838,13 @@ async function extractFileData(
|
|
|
825
838
|
export async function handleMigrationImportPreflight({
|
|
826
839
|
rawBody,
|
|
827
840
|
headers,
|
|
841
|
+
body,
|
|
828
842
|
}: RouteHandlerArgs) {
|
|
843
|
+
const contentType = headers?.["content-type"] ?? "";
|
|
844
|
+
if (contentType.includes("application/json")) {
|
|
845
|
+
return handleMigrationPreflightFromPath(body);
|
|
846
|
+
}
|
|
847
|
+
|
|
829
848
|
const fileData = await extractFileData(rawBody, headers);
|
|
830
849
|
|
|
831
850
|
try {
|
|
@@ -872,15 +891,18 @@ export async function handleMigrationImportPreflight({
|
|
|
872
891
|
* 5. Verifies post-write integrity (SHA-256 check)
|
|
873
892
|
* 6. Returns a detailed report of what was imported
|
|
874
893
|
*
|
|
875
|
-
* The bundle can be supplied
|
|
894
|
+
* The bundle can be supplied as:
|
|
876
895
|
* - Raw binary body with Content-Type: application/octet-stream
|
|
877
896
|
* - Multipart form data with a "file" field
|
|
878
897
|
* - JSON body `{ "url": "<signed-gcs-url>" }` (Content-Type:
|
|
879
898
|
* application/json). The daemon fetches and streams the archive
|
|
880
899
|
* through `streamCommitImport`, so peak memory stays bounded by a
|
|
881
900
|
* single tar entry rather than bundle size.
|
|
901
|
+
* - JSON body `{ "path": "<staged>" }` pointing at a `.vbundle` the
|
|
902
|
+
* caller placed under `${workspace}/.restore-staging/`. The daemon
|
|
903
|
+
* streams that file through `streamCommitImport`.
|
|
882
904
|
*
|
|
883
|
-
* Returns
|
|
905
|
+
* Returns:
|
|
884
906
|
* 200: {
|
|
885
907
|
* success: true,
|
|
886
908
|
* summary: { total_files, files_created, files_overwritten, files_skipped, backups_created },
|
|
@@ -900,11 +922,10 @@ export async function handleMigrationImport(
|
|
|
900
922
|
args: RouteHandlerArgs,
|
|
901
923
|
): Promise<unknown> {
|
|
902
924
|
const { body, rawBody, headers } = args;
|
|
903
|
-
// JSON body
|
|
904
|
-
// signed URL and stream it through the importer.
|
|
925
|
+
// JSON body is either a signed URL fetch or a staged local-file stream.
|
|
905
926
|
const contentType = headers?.["content-type"] ?? "";
|
|
906
927
|
if (contentType.includes("application/json")) {
|
|
907
|
-
return
|
|
928
|
+
return handleMigrationImportFromJson(body);
|
|
908
929
|
}
|
|
909
930
|
|
|
910
931
|
const fileData = await extractFileData(rawBody, headers);
|
|
@@ -1015,6 +1036,8 @@ const URL_FETCH_TIMEOUT_MS = 60 * 60 * 1000;
|
|
|
1015
1036
|
|
|
1016
1037
|
const MigrationImportUrlBody = z.object({ url: z.string().min(1) });
|
|
1017
1038
|
|
|
1039
|
+
const MigrationImportPathBody = z.object({ path: z.string().min(1) });
|
|
1040
|
+
|
|
1018
1041
|
const MigrationImportFromGcsBody = z.object({ bundle_url: z.string().url() });
|
|
1019
1042
|
|
|
1020
1043
|
/**
|
|
@@ -1591,6 +1614,174 @@ async function runGcsImport(
|
|
|
1591
1614
|
: { ...result.report };
|
|
1592
1615
|
}
|
|
1593
1616
|
|
|
1617
|
+
/**
|
|
1618
|
+
* Dispatch a JSON import body to the URL-fetch or staged-path streamer.
|
|
1619
|
+
* The body must include exactly one of `url` or `path`.
|
|
1620
|
+
*/
|
|
1621
|
+
async function handleMigrationImportFromJson(
|
|
1622
|
+
body: Record<string, unknown> | undefined,
|
|
1623
|
+
): Promise<unknown> {
|
|
1624
|
+
const hasUrl = typeof body?.url === "string";
|
|
1625
|
+
const hasPath = typeof body?.path === "string";
|
|
1626
|
+
if (hasUrl && hasPath) {
|
|
1627
|
+
throw new BadRequestError(
|
|
1628
|
+
"Request body must include exactly one of url or path",
|
|
1629
|
+
);
|
|
1630
|
+
}
|
|
1631
|
+
if (hasPath) {
|
|
1632
|
+
return handleMigrationImportFromPath(body);
|
|
1633
|
+
}
|
|
1634
|
+
return handleMigrationImportFromUrl(body);
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
async function handleMigrationImportFromPath(
|
|
1638
|
+
body: Record<string, unknown> | undefined,
|
|
1639
|
+
): Promise<unknown> {
|
|
1640
|
+
const parsed = MigrationImportPathBody.safeParse(body);
|
|
1641
|
+
if (!parsed.success) {
|
|
1642
|
+
throw new BadRequestError(
|
|
1643
|
+
"Request body must be { path: string } with a non-empty path",
|
|
1644
|
+
);
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1647
|
+
let resolvedPath: string;
|
|
1648
|
+
try {
|
|
1649
|
+
resolvedPath = resolveStagedImportPath(parsed.data.path, getWorkspaceDir());
|
|
1650
|
+
} catch (err) {
|
|
1651
|
+
if (err instanceof StagedImportPathError) {
|
|
1652
|
+
throw new BadRequestError(err.message);
|
|
1653
|
+
}
|
|
1654
|
+
throw err;
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
const source = createReadStream(resolvedPath);
|
|
1658
|
+
const pathResolver = new DefaultPathResolver(
|
|
1659
|
+
getWorkspaceDir(),
|
|
1660
|
+
getWorkspaceHooksDir(),
|
|
1661
|
+
);
|
|
1662
|
+
let credentialsImported: CredentialImportSummary | undefined;
|
|
1663
|
+
const credentialImportWarningSink: CredentialWarningSink = { warnings: [] };
|
|
1664
|
+
|
|
1665
|
+
try {
|
|
1666
|
+
const result = await streamCommitImport({
|
|
1667
|
+
source,
|
|
1668
|
+
pathResolver,
|
|
1669
|
+
workspaceDir: getWorkspaceDir(),
|
|
1670
|
+
importCredentials: async (bundleCredentials) => {
|
|
1671
|
+
credentialsImported = await importBundleCredentialsIntoCes(
|
|
1672
|
+
bundleCredentials,
|
|
1673
|
+
credentialImportWarningSink,
|
|
1674
|
+
);
|
|
1675
|
+
},
|
|
1676
|
+
});
|
|
1677
|
+
|
|
1678
|
+
if (!result.ok) {
|
|
1679
|
+
throwImportCommitFailure(result);
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
if (credentialImportWarningSink.warnings.length > 0) {
|
|
1683
|
+
result.report.warnings.push(...credentialImportWarningSink.warnings);
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1686
|
+
await reconcileVellumMetadataFromCes(result.report);
|
|
1687
|
+
appendNewerMigrationWarningsIfAny(result.report);
|
|
1688
|
+
return importCommitSuccessResult(result.report, credentialsImported);
|
|
1689
|
+
} catch (err) {
|
|
1690
|
+
if (err instanceof RouteError) {
|
|
1691
|
+
throw err;
|
|
1692
|
+
}
|
|
1693
|
+
log.error({ err }, "Unexpected error during staged-path import");
|
|
1694
|
+
throw new InternalError(
|
|
1695
|
+
err instanceof Error ? err.message : "Unexpected import error",
|
|
1696
|
+
);
|
|
1697
|
+
} finally {
|
|
1698
|
+
source.destroy();
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
async function handleMigrationPreflightFromPath(
|
|
1703
|
+
body: Record<string, unknown> | undefined,
|
|
1704
|
+
): Promise<unknown> {
|
|
1705
|
+
const parsed = MigrationImportPathBody.safeParse(body);
|
|
1706
|
+
if (!parsed.success) {
|
|
1707
|
+
throw new BadRequestError(
|
|
1708
|
+
"Request body must be { path: string } with a non-empty path",
|
|
1709
|
+
);
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1712
|
+
let resolvedPath: string;
|
|
1713
|
+
try {
|
|
1714
|
+
resolvedPath = resolveStagedImportPath(parsed.data.path, getWorkspaceDir());
|
|
1715
|
+
} catch (err) {
|
|
1716
|
+
if (err instanceof StagedImportPathError) {
|
|
1717
|
+
throw new BadRequestError(err.message);
|
|
1718
|
+
}
|
|
1719
|
+
throw err;
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
const source = createReadStream(resolvedPath);
|
|
1723
|
+
try {
|
|
1724
|
+
const entries = parseVBundleStream(source);
|
|
1725
|
+
const first = await entries.next();
|
|
1726
|
+
if (first.done) {
|
|
1727
|
+
return {
|
|
1728
|
+
can_import: false,
|
|
1729
|
+
validation: {
|
|
1730
|
+
is_valid: false as const,
|
|
1731
|
+
errors: [
|
|
1732
|
+
{
|
|
1733
|
+
code: "empty_archive",
|
|
1734
|
+
message: "Bundle archive is empty",
|
|
1735
|
+
},
|
|
1736
|
+
],
|
|
1737
|
+
},
|
|
1738
|
+
};
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1741
|
+
let manifest;
|
|
1742
|
+
try {
|
|
1743
|
+
({ manifest } = await readAndValidateManifest(first.value));
|
|
1744
|
+
} catch (err) {
|
|
1745
|
+
if (err instanceof StreamingValidationError) {
|
|
1746
|
+
return {
|
|
1747
|
+
can_import: false,
|
|
1748
|
+
validation: {
|
|
1749
|
+
is_valid: false as const,
|
|
1750
|
+
errors: [
|
|
1751
|
+
{
|
|
1752
|
+
code: err.code,
|
|
1753
|
+
message: err.message,
|
|
1754
|
+
...(err.archivePath !== undefined && { path: err.archivePath }),
|
|
1755
|
+
},
|
|
1756
|
+
],
|
|
1757
|
+
},
|
|
1758
|
+
};
|
|
1759
|
+
}
|
|
1760
|
+
throw err;
|
|
1761
|
+
}
|
|
1762
|
+
|
|
1763
|
+
for await (const entry of entries) {
|
|
1764
|
+
entry.body.resume();
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
const pathResolver = new DefaultPathResolver(
|
|
1768
|
+
getWorkspaceDir(),
|
|
1769
|
+
getWorkspaceHooksDir(),
|
|
1770
|
+
);
|
|
1771
|
+
return analyzeImport({ manifest, pathResolver });
|
|
1772
|
+
} catch (err) {
|
|
1773
|
+
if (err instanceof RouteError) {
|
|
1774
|
+
throw err;
|
|
1775
|
+
}
|
|
1776
|
+
log.error({ err }, "Unexpected error during staged-path preflight");
|
|
1777
|
+
throw new InternalError(
|
|
1778
|
+
err instanceof Error ? err.message : "Unexpected import preflight error",
|
|
1779
|
+
);
|
|
1780
|
+
} finally {
|
|
1781
|
+
source.destroy();
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
|
|
1594
1785
|
/**
|
|
1595
1786
|
* Handle a JSON `{ "url": "..." }` body on POST /v1/migrations/import.
|
|
1596
1787
|
*
|
|
@@ -2142,8 +2333,17 @@ export const ROUTES: RouteDefinition[] = [
|
|
|
2142
2333
|
},
|
|
2143
2334
|
summary: "Dry-run import analysis",
|
|
2144
2335
|
description:
|
|
2145
|
-
"Validate a .vbundle archive and return a report of what would change on import without modifying data.",
|
|
2336
|
+
"Validate a .vbundle archive and return a report of what would change on import without modifying data. Accepts raw bytes, multipart form data, or JSON `{ path }` pointing at a file staged under the workspace `.restore-staging` directory.",
|
|
2146
2337
|
tags: ["migrations"],
|
|
2338
|
+
requestBody: z.object({
|
|
2339
|
+
path: z
|
|
2340
|
+
.string()
|
|
2341
|
+
.min(1)
|
|
2342
|
+
.optional()
|
|
2343
|
+
.describe(
|
|
2344
|
+
"Workspace-relative or absolute path to a staged .vbundle under .restore-staging (JSON body path only).",
|
|
2345
|
+
),
|
|
2346
|
+
}),
|
|
2147
2347
|
responseBody: z.object({
|
|
2148
2348
|
can_import: z.boolean(),
|
|
2149
2349
|
summary: z.object({}).passthrough(),
|
|
@@ -2163,15 +2363,23 @@ export const ROUTES: RouteDefinition[] = [
|
|
|
2163
2363
|
},
|
|
2164
2364
|
summary: "Import a .vbundle archive",
|
|
2165
2365
|
description:
|
|
2166
|
-
"Commit a .vbundle archive import to disk — destructive. Accepts the bundle as raw bytes (application/octet-stream), multipart/form-data,
|
|
2366
|
+
"Commit a .vbundle archive import to disk — destructive. Accepts the bundle as raw bytes (application/octet-stream), multipart/form-data, a JSON body with `{ url }` carrying a signed URL the daemon fetches, or a JSON body with `{ path }` pointing at a file staged under the workspace `.restore-staging` directory.",
|
|
2167
2367
|
tags: ["migrations"],
|
|
2168
2368
|
requestBody: z.object({
|
|
2169
2369
|
url: z
|
|
2170
2370
|
.string()
|
|
2171
2371
|
.url()
|
|
2372
|
+
.optional()
|
|
2172
2373
|
.describe(
|
|
2173
2374
|
"A signed GCS URL pointing to the .vbundle archive (JSON body path only).",
|
|
2174
2375
|
),
|
|
2376
|
+
path: z
|
|
2377
|
+
.string()
|
|
2378
|
+
.min(1)
|
|
2379
|
+
.optional()
|
|
2380
|
+
.describe(
|
|
2381
|
+
"Workspace-relative or absolute path to a staged .vbundle under .restore-staging (JSON body path only).",
|
|
2382
|
+
),
|
|
2175
2383
|
}),
|
|
2176
2384
|
additionalResponses: {
|
|
2177
2385
|
"502": {
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
ServicesSchema,
|
|
20
20
|
} from "../../config/schemas/services.js";
|
|
21
21
|
import type { OAuthConnectionRequest } from "../../oauth/connection.js";
|
|
22
|
+
import { jsonSafeOAuthBody } from "../../oauth/connection.js";
|
|
22
23
|
import {
|
|
23
24
|
resolveOAuthConnection,
|
|
24
25
|
type ResolveOAuthConnectionOptions,
|
|
@@ -39,6 +40,10 @@ import { VellumPlatformClient } from "../../platform/client.js";
|
|
|
39
40
|
import { withValidToken } from "../../security/token-manager.js";
|
|
40
41
|
import { matchHostPattern } from "../../tools/credentials/host-pattern-match.js";
|
|
41
42
|
import { getLogger } from "../../util/logger.js";
|
|
43
|
+
import {
|
|
44
|
+
findContentTypeHeader,
|
|
45
|
+
parseRequestBodyData,
|
|
46
|
+
} from "../../util/oauth-request-body.js";
|
|
42
47
|
import { LOCAL_PRINCIPALS } from "../auth/route-policy.js";
|
|
43
48
|
import { BadRequestError, InternalError, NotFoundError } from "./errors.js";
|
|
44
49
|
import type { RouteDefinition, RouteHandlerArgs } from "./types.js";
|
|
@@ -721,15 +726,12 @@ async function handleToken({ body = {} }: RouteHandlerArgs) {
|
|
|
721
726
|
// Request handler
|
|
722
727
|
// ---------------------------------------------------------------------------
|
|
723
728
|
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
function readBodyData(data: string): unknown {
|
|
729
|
+
/**
|
|
730
|
+
* Resolve a raw `data` string into a request body. A non-JSON Content-Type
|
|
731
|
+
* keeps the payload as the caller's exact string so multipart and other
|
|
732
|
+
* byte-sensitive payloads survive; files are read as UTF-8 text.
|
|
733
|
+
*/
|
|
734
|
+
function readBodyData(data: string, contentType: string | undefined): unknown {
|
|
733
735
|
if (data === "@-") {
|
|
734
736
|
// This handler runs inside the daemon, whose stdin is a supervisor pipe
|
|
735
737
|
// or /dev/null — never the caller's terminal. Stdin-based body input is
|
|
@@ -742,14 +744,13 @@ function readBodyData(data: string): unknown {
|
|
|
742
744
|
|
|
743
745
|
if (data.startsWith("@")) {
|
|
744
746
|
const filePath = data.slice(1);
|
|
745
|
-
|
|
746
|
-
return tryJsonParse(raw);
|
|
747
|
+
return parseRequestBodyData(readFileSync(filePath, "utf-8"), contentType);
|
|
747
748
|
}
|
|
748
749
|
|
|
749
|
-
return
|
|
750
|
+
return parseRequestBodyData(data, contentType);
|
|
750
751
|
}
|
|
751
752
|
|
|
752
|
-
async function handleRequest({ body = {} }: RouteHandlerArgs) {
|
|
753
|
+
export async function handleRequest({ body = {} }: RouteHandlerArgs) {
|
|
753
754
|
const b = body as {
|
|
754
755
|
provider: string;
|
|
755
756
|
url: string;
|
|
@@ -852,12 +853,14 @@ async function handleRequest({ body = {} }: RouteHandlerArgs) {
|
|
|
852
853
|
let reqBody: unknown = undefined;
|
|
853
854
|
const query: Record<string, string | string[]> = { ...queryFromUrl };
|
|
854
855
|
|
|
855
|
-
// Use pre-parsed data from CLI, or fall back to raw data string for direct
|
|
856
|
+
// Use pre-parsed data from CLI, or fall back to raw data string for direct
|
|
857
|
+
// API callers. A string here (a multipart or form-encoded payload) stays a
|
|
858
|
+
// string all the way to the provider; only objects are serialized as JSON.
|
|
856
859
|
const resolvedData =
|
|
857
860
|
b.parsed_data !== undefined
|
|
858
861
|
? b.parsed_data
|
|
859
862
|
: b.data !== undefined
|
|
860
|
-
? readBodyData(b.data)
|
|
863
|
+
? readBodyData(b.data, findContentTypeHeader(b.headers))
|
|
861
864
|
: undefined;
|
|
862
865
|
|
|
863
866
|
if (resolvedData !== undefined) {
|
|
@@ -924,16 +927,20 @@ async function handleRequest({ body = {} }: RouteHandlerArgs) {
|
|
|
924
927
|
};
|
|
925
928
|
|
|
926
929
|
const response = await connection.request(req);
|
|
930
|
+
const encodedBody = jsonSafeOAuthBody(response.body);
|
|
927
931
|
|
|
928
932
|
const result: Record<string, unknown> = {
|
|
929
933
|
ok: response.status >= 200 && response.status < 300,
|
|
930
934
|
status: response.status,
|
|
931
935
|
headers: response.headers,
|
|
932
|
-
body:
|
|
936
|
+
body: encodedBody.body,
|
|
933
937
|
// Which connected account actually served the request, so the caller can
|
|
934
938
|
// tell whether the intended account was used.
|
|
935
939
|
account: connection.accountInfo,
|
|
936
940
|
};
|
|
941
|
+
if (encodedBody.bodyEncoding) {
|
|
942
|
+
result.bodyEncoding = encodedBody.bodyEncoding;
|
|
943
|
+
}
|
|
937
944
|
|
|
938
945
|
// Surface a caller-visible warning when the provider had several active
|
|
939
946
|
// connections and no account was pinned — the model must see that a
|
|
@@ -1641,29 +1641,6 @@ function mapTogglePluginError(err: unknown): RouteError {
|
|
|
1641
1641
|
);
|
|
1642
1642
|
}
|
|
1643
1643
|
|
|
1644
|
-
/**
|
|
1645
|
-
* Converge plugin-declared schedules against the sentinel this route just
|
|
1646
|
-
* wrote, so a disabled plugin's rows disarm now rather than at the
|
|
1647
|
-
* reconciler's next backstop sweep.
|
|
1648
|
-
*
|
|
1649
|
-
* Fire-and-forget: the toggle itself already succeeded on disk, so a reconcile
|
|
1650
|
-
* failure must not turn it into a route error. Imported lazily, matching the
|
|
1651
|
-
* plugin source reconcile's own hook, to keep the schedule and notification
|
|
1652
|
-
* modules out of this route module's static graph. The scheduler applies the
|
|
1653
|
-
* sentinel at fire time as well, so a slow or failed pass here delays the row
|
|
1654
|
-
* bookkeeping without letting a disabled plugin run.
|
|
1655
|
-
*/
|
|
1656
|
-
function reconcilePluginSchedulesInBackground(): void {
|
|
1657
|
-
void import("../../schedule/plugin-schedule-reconciler.js")
|
|
1658
|
-
.then(({ reconcilePluginSchedules }) => reconcilePluginSchedules())
|
|
1659
|
-
.catch((err: unknown) => {
|
|
1660
|
-
log.error(
|
|
1661
|
-
{ err },
|
|
1662
|
-
"plugin schedule reconcile after a plugin toggle failed",
|
|
1663
|
-
);
|
|
1664
|
-
});
|
|
1665
|
-
}
|
|
1666
|
-
|
|
1667
1644
|
/**
|
|
1668
1645
|
* Toggle a plugin's `.disabled` sentinel through the shared toggle-plugin lib,
|
|
1669
1646
|
* then publish a generic `sync_changed(plugins:list)` so every client refetches
|
|
@@ -1694,15 +1671,22 @@ async function handleEnablePlugin({
|
|
|
1694
1671
|
return { ok: true };
|
|
1695
1672
|
}
|
|
1696
1673
|
|
|
1697
|
-
function handleDisablePlugin({
|
|
1674
|
+
async function handleDisablePlugin({
|
|
1675
|
+
pathParams = {},
|
|
1676
|
+
headers,
|
|
1677
|
+
}: RouteHandlerArgs) {
|
|
1698
1678
|
try {
|
|
1699
1679
|
disablePlugin(pathParams.name ?? "");
|
|
1700
|
-
publishPluginsChanged(getOriginClientId(headers));
|
|
1701
|
-
reconcilePluginSchedulesInBackground();
|
|
1702
|
-
return { ok: true };
|
|
1703
1680
|
} catch (err) {
|
|
1704
1681
|
throw mapTogglePluginError(err);
|
|
1705
1682
|
}
|
|
1683
|
+
// Same poke enable uses. The sentinel hides tools and hooks at read time,
|
|
1684
|
+
// but a plugin that already ran `init` still owns in-process work (a live
|
|
1685
|
+
// gRPC subscribe, a poll worker). The source reconcile is what runs
|
|
1686
|
+
// `shutdown` and drops that work before this route returns.
|
|
1687
|
+
await reconcilePluginSourcesNow();
|
|
1688
|
+
publishPluginsChanged(getOriginClientId(headers));
|
|
1689
|
+
return { ok: true };
|
|
1706
1690
|
}
|
|
1707
1691
|
|
|
1708
1692
|
// ---------------------------------------------------------------------------
|
|
@@ -2146,7 +2130,7 @@ export const ROUTES: RouteDefinition[] = [
|
|
|
2146
2130
|
},
|
|
2147
2131
|
summary: "Disable a plugin",
|
|
2148
2132
|
description:
|
|
2149
|
-
"Disable a plugin in this workspace by dropping a `.disabled` sentinel, mirroring the CLI's `assistant plugins disable <name>`.
|
|
2133
|
+
"Disable a plugin in this workspace by dropping a `.disabled` sentinel, mirroring the CLI's `assistant plugins disable <name>`. Awaits the same source reconcile enable uses so the plugin's shutdown hook runs. Tools, injectors, and hook gates also honor the sentinel at read time. Broadcasts a `sync_changed` invalidation carrying the `plugins:list` tag so other clients refetch `GET /v1/plugins`. An already-disabled plugin returns 409; a user plugin with no directory returns 404 (default plugins are stubbed on demand via the `default-` prefix); a malformed name returns 400.",
|
|
2150
2134
|
tags: ["plugins"],
|
|
2151
2135
|
pathParams: [
|
|
2152
2136
|
{
|
|
@@ -16,6 +16,10 @@
|
|
|
16
16
|
* one-element batch. That branch only succeeds against a single-question
|
|
17
17
|
* batch — multi-question batches reject it with a helpful error.
|
|
18
18
|
*
|
|
19
|
+
* Any assistant serving this route also accepts `submit`, so the shim serves
|
|
20
|
+
* only clients that cannot tell which assistant they are talking to: a stale
|
|
21
|
+
* browser bundle posts the single-question body against a batch-capable route.
|
|
22
|
+
*
|
|
19
23
|
* Cross-talk safety: pending interactions of other kinds (`confirmation`,
|
|
20
24
|
* `secret`, host_*, etc.) return 404 here rather than being mis-resolved.
|
|
21
25
|
*
|
|
@@ -194,6 +198,8 @@ function buildSubmissions(
|
|
|
194
198
|
// prompter stashed the ordered ids on the interaction metadata so we can
|
|
195
199
|
// pick the (single) target questionId here.
|
|
196
200
|
const { orderedIds } = readBatchMetadata(interaction);
|
|
201
|
+
// `QuestionPrompter` is the only registrar of a `question` interaction and
|
|
202
|
+
// refuses an empty `questions` array, so this guard is unreachable.
|
|
197
203
|
if (orderedIds.length === 0) {
|
|
198
204
|
throw new QuestionBatchValidationError(
|
|
199
205
|
"Legacy single-question payload requires a registered batch with at least one question",
|