@rynx-ai/daemon 0.1.11-beta.3 → 0.1.11-beta.31
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/bundled-plugins/plugins/lark/.rynx-plugin/plugin.json +0 -12
- package/bundled-plugins/plugins/lark/dist/cli-mirror/mirror-index.js +3 -1
- package/bundled-plugins/plugins/lark/dist/host-adapters.d.ts +3 -3
- package/bundled-plugins/plugins/lark/dist/host-adapters.js +24 -18
- package/bundled-plugins/plugins/lark/dist/lark.js +18 -12
- package/bundled-plugins/plugins/lark/dist/runtime.js +6046 -6018
- package/bundled-plugins/plugins/lark/dist/settings.js +6006 -5987
- package/bundled-plugins/plugins/lark/package.json +1 -1
- package/bundled-plugins/plugins/lark/ui/assets/index-CGUVAMsv.css +1 -0
- package/bundled-plugins/plugins/lark/ui/index.html +2 -2
- package/dist/agent-file.js +0 -1
- package/dist/browser-artifact-management.js +4 -1
- package/dist/chrome-for-testing-runtime.js +4 -3
- package/dist/chrome-for-testing-store.js +29 -7
- package/dist/control-deps.d.ts +6 -2
- package/dist/control-deps.js +87 -4
- package/dist/control-link-store.js +4 -3
- package/dist/daemon-owner.js +4 -7
- package/dist/daemon-server.js +41 -16
- package/dist/db.d.ts +2 -2
- package/dist/db.js +155 -21
- package/dist/headless-browser-host.js +283 -34
- package/dist/lifecycle.d.ts +1 -0
- package/dist/lifecycle.js +3 -0
- package/dist/migrations/cleanup-legacy-sessions.js +4 -3
- package/dist/plugin-cli.d.ts +10 -0
- package/dist/plugin-cli.js +19 -2
- package/dist/plugin-host-rpc.d.ts +48 -9
- package/dist/plugin-host-rpc.js +552 -80
- package/dist/plugin-installer.js +3 -2
- package/dist/plugin-management-service.js +0 -2
- package/dist/plugin-marketplace.js +2 -1
- package/dist/plugin-store.js +2 -4
- package/dist/plugin-supervisor.js +0 -4
- package/dist/remote-runtime-access-store.d.ts +2 -2
- package/dist/remote-runtime-access-store.js +6 -9
- package/dist/remote-runtime-connection-manager.js +1 -1
- package/dist/remote-runtime-target-store.d.ts +2 -2
- package/dist/remote-runtime-target-store.js +6 -6
- package/dist/session-emulator-binding-store.js +4 -6
- package/dist/session-fork-store.d.ts +1 -0
- package/dist/session-fork-store.js +82 -28
- package/dist/session-launch-operations.js +6 -5
- package/dist/session-log-store.d.ts +1 -0
- package/dist/session-log-store.js +19 -16
- package/dist/session-meta-store.d.ts +5 -0
- package/dist/session-meta-store.js +25 -2
- package/dist/session-portal-grant-store.d.ts +55 -0
- package/dist/session-portal-grant-store.js +160 -0
- package/dist/session-resource-store.d.ts +24 -6
- package/dist/session-resource-store.js +386 -68
- package/dist/session-timeline-projection-store.d.ts +24 -0
- package/dist/session-timeline-projection-store.js +53 -0
- package/dist/setup-service.d.ts +1 -0
- package/dist/setup-service.js +1 -0
- package/dist/skills-catalog.d.ts +11 -2
- package/dist/skills-catalog.js +317 -10
- package/dist/sqlite.d.ts +22 -0
- package/dist/sqlite.js +57 -0
- package/dist/system-dependencies.d.ts +22 -0
- package/dist/system-dependencies.js +79 -0
- package/package.json +15 -12
- package/bundled-plugins/plugins/lark/ui/assets/index-B2dhMlMU.css +0 -1
- /package/bundled-plugins/plugins/lark/ui/assets/{index-D9N-AkLX.js → index-D8Amoe6a.js} +0 -0
package/dist/plugin-host-rpc.js
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import { realpath, stat } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import { AgentRuntimeError, agentSpecSchema, resolvedExecutionSnapshotSchema, sessionWorkspaceSnapshotSchema,
|
|
4
|
+
import { AgentRuntimeError, agentSpecSchema, resolvedExecutionSnapshotSchema, sessionWorkspaceSnapshotSchema, } from "@rynx-ai/core";
|
|
5
5
|
import { PLUGIN_HOST_MAX_ACTIVE_RUNS, } from "@rynx-ai/plugin-sdk";
|
|
6
|
+
import { parseRuntimeBrowserRequestHeadersGetParams, parseRuntimeBrowserRequestHeadersSetParams, } from "@rynx-ai/protocol/runtime-browser";
|
|
7
|
+
import { SessionBrowserServiceError, } from "@rynx-ai/server";
|
|
6
8
|
import { PluginRunnerError } from "@rynx-ai/plugin-runner";
|
|
7
9
|
import { ensurePluginSessionLaunch, replayPluginSessionLaunch, SessionLaunchOperationConflictError, SessionLaunchOperationTargetDeletedError, } from "./session-launch-operations.js";
|
|
8
10
|
import { createControlLink } from "./control-link-store.js";
|
|
11
|
+
import { issueSessionPortalTicket, revokeSessionPortalGrant, } from "./session-portal-grant-store.js";
|
|
12
|
+
import { claimSessionTimelineProjection, completeSessionTimelineProjection, } from "./session-timeline-projection-store.js";
|
|
9
13
|
const MAX_MESSAGE_LENGTH = 1_000_000;
|
|
10
14
|
/** Leaves ample room for the RPC envelope below the runner's 1 MiB line cap. */
|
|
11
15
|
export const MAX_PLUGIN_HOST_EVENT_BYTES = 512 * 1024;
|
|
@@ -13,31 +17,59 @@ const MAX_PLUGIN_RUN_FINAL_RESPONSE_BYTES = 64 * 1024;
|
|
|
13
17
|
const MAX_DELTA_CHUNK_CODE_UNITS = 64 * 1024;
|
|
14
18
|
const SESSION_INTERACTION_RESOLUTION_MAX_BYTES = 128 * 1024;
|
|
15
19
|
const AGENT_ID_PATTERN = /^[a-z0-9][a-z0-9-]{0,63}$/;
|
|
16
|
-
const METHOD_CAPABILITY = {
|
|
17
|
-
"installation.proof.sign": "installation.proof",
|
|
18
|
-
"agent.catalog.list": "agent.catalog.read",
|
|
19
|
-
"agent.catalog.get": "agent.catalog.read",
|
|
20
|
-
"agent.execution.resolve": "agent.catalog.read",
|
|
21
|
-
"session.ensure": "session.provision",
|
|
22
|
-
"session.run": "conversation.run",
|
|
23
|
-
"session.steer": "conversation.run",
|
|
24
|
-
"session.interrupt": "session.cancel",
|
|
25
|
-
"session.interaction.resolve": "conversation.run",
|
|
26
|
-
"session.models": "runtime.catalog.read",
|
|
27
|
-
"session.binding.get": "session.binding.read",
|
|
28
|
-
"session.binding.list": "session.binding.read",
|
|
29
|
-
"session.binding.find": "session.binding.read",
|
|
30
|
-
"session.goal.get": "session.goal.read",
|
|
31
|
-
"session.goal.set": "session.goal.write",
|
|
32
|
-
"session.goal.clear": "session.goal.write",
|
|
33
|
-
"session.fork": "session.fork",
|
|
34
|
-
"session.meta.get": "session.meta.read",
|
|
35
|
-
"session.meta.list": "session.meta.read",
|
|
36
|
-
"session.meta.setTitle": "session.meta.title.write",
|
|
37
|
-
"control.link.issue": "control-link.issue",
|
|
38
|
-
};
|
|
39
20
|
/**
|
|
40
|
-
*
|
|
21
|
+
* Buffers messages that arrive while the first `session.run` is still bringing
|
|
22
|
+
* up its native Turn. Once Core subscribes, later messages are delivered
|
|
23
|
+
* through the same input channel. After that subscription closes, this Run no
|
|
24
|
+
* longer accepts input; the caller waits for release and opens the next Run.
|
|
25
|
+
*/
|
|
26
|
+
class PluginRunInputController {
|
|
27
|
+
listener;
|
|
28
|
+
pending = new Map();
|
|
29
|
+
subscribed = false;
|
|
30
|
+
closed = false;
|
|
31
|
+
enqueue(text) {
|
|
32
|
+
if (this.closed || (this.subscribed && !this.listener)) {
|
|
33
|
+
return { accepted: false, cancel: () => undefined };
|
|
34
|
+
}
|
|
35
|
+
if (this.listener) {
|
|
36
|
+
this.listener(text);
|
|
37
|
+
return { accepted: true, cancel: () => undefined };
|
|
38
|
+
}
|
|
39
|
+
const token = Symbol("plugin-run-input");
|
|
40
|
+
this.pending.set(token, text);
|
|
41
|
+
return {
|
|
42
|
+
accepted: true,
|
|
43
|
+
cancel: () => {
|
|
44
|
+
this.pending.delete(token);
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
subscribe(listener) {
|
|
49
|
+
if (this.closed)
|
|
50
|
+
return () => undefined;
|
|
51
|
+
if (this.listener) {
|
|
52
|
+
throw new Error("plugin Run input already has a subscriber");
|
|
53
|
+
}
|
|
54
|
+
this.subscribed = true;
|
|
55
|
+
this.listener = listener;
|
|
56
|
+
const queued = [...this.pending.values()];
|
|
57
|
+
this.pending.clear();
|
|
58
|
+
for (const text of queued)
|
|
59
|
+
listener(text);
|
|
60
|
+
return () => {
|
|
61
|
+
if (this.listener === listener)
|
|
62
|
+
this.listener = undefined;
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
close() {
|
|
66
|
+
this.closed = true;
|
|
67
|
+
this.listener = undefined;
|
|
68
|
+
this.pending.clear();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* One authenticated plugin's Host RPC surface.
|
|
41
73
|
*
|
|
42
74
|
* This adapter is deliberately the only place that maps public string methods
|
|
43
75
|
* to trusted runtime objects. The runner receives neither this class nor its
|
|
@@ -49,7 +81,6 @@ export class PluginHostRpc {
|
|
|
49
81
|
daemonInstanceId;
|
|
50
82
|
productVersion;
|
|
51
83
|
cliVersion;
|
|
52
|
-
capabilities;
|
|
53
84
|
services;
|
|
54
85
|
managedWorkspaceRoot;
|
|
55
86
|
emit;
|
|
@@ -65,7 +96,6 @@ export class PluginHostRpc {
|
|
|
65
96
|
this.daemonInstanceId = requiredString(options.daemonInstanceId, "daemonInstanceId", 128);
|
|
66
97
|
this.productVersion = requiredString(options.productVersion, "productVersion", 128);
|
|
67
98
|
this.cliVersion = requiredString(options.cliVersion, "cliVersion", 128);
|
|
68
|
-
this.capabilities = new Set(options.capabilities);
|
|
69
99
|
this.services = options.services;
|
|
70
100
|
this.managedWorkspaceRoot = options.managedWorkspaceRoot
|
|
71
101
|
? path.resolve(options.managedWorkspaceRoot)
|
|
@@ -89,15 +119,27 @@ export class PluginHostRpc {
|
|
|
89
119
|
publicKey: this.services.installationProof.publicKey,
|
|
90
120
|
},
|
|
91
121
|
protocolVersion: 1,
|
|
92
|
-
capabilities:
|
|
122
|
+
capabilities: {
|
|
123
|
+
sessionRunContent: true,
|
|
124
|
+
sessionRunFiles: true,
|
|
125
|
+
...(this.services.sessionTimeline
|
|
126
|
+
? { sessionTimelineProjectText: true }
|
|
127
|
+
: {}),
|
|
128
|
+
...(this.services.sessionBrowserRequestHeaders
|
|
129
|
+
? { sessionBrowserRequestHeaders: true }
|
|
130
|
+
: {}),
|
|
131
|
+
...(this.services.sessionPortal
|
|
132
|
+
? {
|
|
133
|
+
sessionPortalEmbedOrigin: true,
|
|
134
|
+
sessionPortalGrantRevocation: true,
|
|
135
|
+
}
|
|
136
|
+
: {}),
|
|
137
|
+
},
|
|
138
|
+
...(this.services.sessionPortal
|
|
139
|
+
? { sessionPortal: { ...this.services.sessionPortal } }
|
|
140
|
+
: {}),
|
|
93
141
|
};
|
|
94
142
|
}
|
|
95
|
-
const capability = METHOD_CAPABILITY[method];
|
|
96
|
-
if (!capability)
|
|
97
|
-
throw new PluginHostCallError("METHOD_NOT_FOUND", `unknown host method: ${method}`);
|
|
98
|
-
if (!this.capabilities.has(capability)) {
|
|
99
|
-
throw new PluginHostCallError("CAPABILITY_NOT_DECLARED", `host method ${method} is missing manifest capability ${capability}`);
|
|
100
|
-
}
|
|
101
143
|
try {
|
|
102
144
|
switch (method) {
|
|
103
145
|
case "installation.proof.sign": {
|
|
@@ -118,8 +160,8 @@ export class PluginHostRpc {
|
|
|
118
160
|
return await this.ensureSession(params);
|
|
119
161
|
case "session.run":
|
|
120
162
|
return await this.runSession(params, signal);
|
|
121
|
-
case "session.
|
|
122
|
-
return this.
|
|
163
|
+
case "session.activity.get":
|
|
164
|
+
return await this.sessionActivity(params);
|
|
123
165
|
case "session.interrupt":
|
|
124
166
|
return await this.interruptRun(params);
|
|
125
167
|
case "session.interaction.resolve":
|
|
@@ -149,8 +191,31 @@ export class PluginHostRpc {
|
|
|
149
191
|
return this.services.sessionRegistry.list().filter((session) => this.isOwned(session));
|
|
150
192
|
case "session.meta.setTitle":
|
|
151
193
|
return this.setSessionTitle(params);
|
|
194
|
+
case "session.browser.requestHeaders.get":
|
|
195
|
+
return await this.getSessionBrowserRequestHeaders(params);
|
|
196
|
+
case "session.browser.requestHeaders.set":
|
|
197
|
+
return await this.setSessionBrowserRequestHeaders(params);
|
|
152
198
|
case "control.link.issue":
|
|
153
199
|
return this.issueControlLink(params);
|
|
200
|
+
case "session.portal.plugin.issue": {
|
|
201
|
+
const input = record(params);
|
|
202
|
+
return this.issueSessionPortalLink("plugin_sessions", requiredSessionPortalAccess(input), undefined, optionalSessionPortalEmbedOrigin(input.embedOrigin), optionalString(input.grantHandle, "grantHandle", 128));
|
|
203
|
+
}
|
|
204
|
+
case "session.portal.session.issue": {
|
|
205
|
+
const input = record(params);
|
|
206
|
+
const sessionId = requiredSessionId(input);
|
|
207
|
+
this.ownedSession(sessionId);
|
|
208
|
+
return this.issueSessionPortalLink("single_session", requiredSessionPortalAccess(input), sessionId, optionalSessionPortalEmbedOrigin(input.embedOrigin), optionalString(input.grantHandle, "grantHandle", 128));
|
|
209
|
+
}
|
|
210
|
+
case "session.portal.grant.revoke": {
|
|
211
|
+
const grantHandle = requiredString(record(params).grantHandle, "grantHandle", 128);
|
|
212
|
+
return revokeSessionPortalGrant({
|
|
213
|
+
pluginId: this.principalId,
|
|
214
|
+
grantHandle,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
case "session.timeline.projectText":
|
|
218
|
+
return await this.projectTimelineText(params);
|
|
154
219
|
default:
|
|
155
220
|
throw new PluginHostCallError("METHOD_NOT_FOUND", `unknown host method: ${method}`);
|
|
156
221
|
}
|
|
@@ -319,17 +384,86 @@ export class PluginHostRpc {
|
|
|
319
384
|
const input = record(params);
|
|
320
385
|
const sessionId = requiredSessionId(input);
|
|
321
386
|
const operationId = optionalString(input.operationId, "operationId", 512);
|
|
322
|
-
const
|
|
323
|
-
|
|
324
|
-
|
|
387
|
+
const hasMessage = input.message !== undefined;
|
|
388
|
+
const hasContent = input.content !== undefined;
|
|
389
|
+
if (hasMessage === hasContent) {
|
|
390
|
+
throw new PluginHostCallError("INVALID_PARAMS", "provide exactly one of message or content");
|
|
325
391
|
}
|
|
326
|
-
|
|
327
|
-
|
|
392
|
+
const message = hasMessage
|
|
393
|
+
? requiredText(input.message, "message", MAX_MESSAGE_LENGTH)
|
|
394
|
+
: undefined;
|
|
395
|
+
const collaborationMode = optionalCollaborationMode(input.collaborationMode);
|
|
396
|
+
let session = this.ownedSession(sessionId);
|
|
397
|
+
if (operationId) {
|
|
398
|
+
const replayed = this.activeRunsByOperation.get(operationId);
|
|
399
|
+
if (replayed) {
|
|
400
|
+
if (replayed.sessionId !== sessionId) {
|
|
401
|
+
throw new PluginHostCallError("RUN_ALREADY_ACTIVE", `operation already belongs to another active Session: ${operationId}`, true);
|
|
402
|
+
}
|
|
403
|
+
return await waitForHostPromise(replayed.started, signal);
|
|
404
|
+
}
|
|
328
405
|
}
|
|
406
|
+
const active = this.activeSessionRuns.get(sessionId);
|
|
407
|
+
if (active) {
|
|
408
|
+
if (hasContent) {
|
|
409
|
+
// Native steering currently accepts text only. Preserve the attachment
|
|
410
|
+
// as a new Turn after the active response reaches its terminal edge.
|
|
411
|
+
await waitForHostPromise(active.settled, signal);
|
|
412
|
+
throwIfHostCallAborted(signal);
|
|
413
|
+
return await this.runSessionAdmitted(params, signal);
|
|
414
|
+
}
|
|
415
|
+
const queued = active.input.enqueue(message);
|
|
416
|
+
if (!queued.accepted) {
|
|
417
|
+
// The response reached its terminal boundary before Host cleanup won
|
|
418
|
+
// the scheduling race. Wait for that old Run to leave the Session slot,
|
|
419
|
+
// then submit this message as a new Turn with a new response identity.
|
|
420
|
+
await waitForHostPromise(active.settled, signal);
|
|
421
|
+
throwIfHostCallAborted(signal);
|
|
422
|
+
return await this.runSessionAdmitted(params, signal);
|
|
423
|
+
}
|
|
424
|
+
if (operationId) {
|
|
425
|
+
active.operationIds.add(operationId);
|
|
426
|
+
this.activeRunsByOperation.set(operationId, active);
|
|
427
|
+
}
|
|
428
|
+
try {
|
|
429
|
+
// Reuse the current Run identity. Core already owns its response stream;
|
|
430
|
+
// the buffered input will become turn/steer once that stream subscribes.
|
|
431
|
+
return await waitForHostPromise(active.started, signal);
|
|
432
|
+
}
|
|
433
|
+
catch (error) {
|
|
434
|
+
queued.cancel();
|
|
435
|
+
if (operationId && this.activeRunsByOperation.get(operationId) === active) {
|
|
436
|
+
this.activeRunsByOperation.delete(operationId);
|
|
437
|
+
active.operationIds.delete(operationId);
|
|
438
|
+
}
|
|
439
|
+
throw error;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
const modelProvided = input.model !== undefined;
|
|
443
|
+
const requestedModel = input.model === null || input.model === ""
|
|
444
|
+
? null
|
|
445
|
+
: optionalString(input.model, "model", 256);
|
|
446
|
+
const reasoningEffortProvided = input.reasoningEffort !== undefined;
|
|
447
|
+
const requestedReasoningEffort = input.reasoningEffort === null || input.reasoningEffort === ""
|
|
448
|
+
? null
|
|
449
|
+
: optionalString(input.reasoningEffort, "reasoningEffort", 64);
|
|
329
450
|
if (this.activeRuns.size >= PLUGIN_HOST_MAX_ACTIVE_RUNS) {
|
|
330
451
|
throw new PluginHostCallError("RUN_LIMIT", "plugin has too many active session runs", true);
|
|
331
452
|
}
|
|
332
|
-
const
|
|
453
|
+
const nextModel = modelProvided ? requestedModel ?? null : session.execution.model;
|
|
454
|
+
const nextReasoningEffort = reasoningEffortProvided
|
|
455
|
+
? requestedReasoningEffort ?? null
|
|
456
|
+
: session.execution.reasoningEffort;
|
|
457
|
+
if (nextModel !== session.execution.model ||
|
|
458
|
+
nextReasoningEffort !== session.execution.reasoningEffort) {
|
|
459
|
+
await this.services.sessionExecution.update({
|
|
460
|
+
sessionId,
|
|
461
|
+
model: nextModel,
|
|
462
|
+
reasoningEffort: nextReasoningEffort,
|
|
463
|
+
});
|
|
464
|
+
session = this.ownedSession(sessionId);
|
|
465
|
+
}
|
|
466
|
+
const content = hasContent ? await pluginRunContent(input.content) : undefined;
|
|
333
467
|
const abort = new AbortController();
|
|
334
468
|
const cancel = () => abort.abort(signal?.reason);
|
|
335
469
|
if (signal?.aborted) {
|
|
@@ -337,15 +471,35 @@ export class PluginHostRpc {
|
|
|
337
471
|
throwIfHostCallAborted(signal);
|
|
338
472
|
}
|
|
339
473
|
signal?.addEventListener("abort", cancel, { once: true });
|
|
340
|
-
const
|
|
474
|
+
const runInput = new PluginRunInputController();
|
|
341
475
|
const runId = randomUUID();
|
|
476
|
+
let resolveStarted;
|
|
477
|
+
let rejectStarted;
|
|
478
|
+
const startedResult = new Promise((resolve, reject) => {
|
|
479
|
+
resolveStarted = resolve;
|
|
480
|
+
rejectStarted = reject;
|
|
481
|
+
});
|
|
482
|
+
// The initiating RPC observes startup directly below. This promise exists
|
|
483
|
+
// for concurrent/replayed calls, so retain its rejection without creating
|
|
484
|
+
// an unhandled-rejection when there are none.
|
|
485
|
+
void startedResult.catch(() => undefined);
|
|
486
|
+
let resolveSettled;
|
|
487
|
+
const settled = new Promise((resolve) => {
|
|
488
|
+
resolveSettled = resolve;
|
|
489
|
+
});
|
|
342
490
|
const run = {
|
|
343
491
|
runId,
|
|
344
492
|
sessionId,
|
|
345
|
-
operationId,
|
|
493
|
+
operationIds: new Set(operationId ? [operationId] : []),
|
|
346
494
|
interruptRequested: false,
|
|
347
495
|
abort,
|
|
348
|
-
|
|
496
|
+
input: runInput,
|
|
497
|
+
started: startedResult,
|
|
498
|
+
startupSettled: false,
|
|
499
|
+
resolveStarted,
|
|
500
|
+
rejectStarted,
|
|
501
|
+
settled,
|
|
502
|
+
resolveSettled,
|
|
349
503
|
};
|
|
350
504
|
this.activeRuns.set(runId, run);
|
|
351
505
|
this.activeSessionRuns.set(sessionId, run);
|
|
@@ -353,16 +507,23 @@ export class PluginHostRpc {
|
|
|
353
507
|
this.activeRunsByOperation.set(operationId, run);
|
|
354
508
|
let started;
|
|
355
509
|
try {
|
|
356
|
-
started = await this.services.
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
510
|
+
started = await this.services.sessionRuns.startRun(sessionId, message !== undefined
|
|
511
|
+
? {
|
|
512
|
+
message,
|
|
513
|
+
clientMessageId: operationId ?? `plugin_run_${runId}`,
|
|
514
|
+
}
|
|
515
|
+
: {
|
|
516
|
+
content: content,
|
|
517
|
+
clientMessageId: operationId ?? `plugin_run_${runId}`,
|
|
518
|
+
}, {
|
|
361
519
|
signal: abort.signal,
|
|
362
|
-
steer,
|
|
520
|
+
steer: runInput,
|
|
521
|
+
...(collaborationMode ? { collaborationMode } : {}),
|
|
363
522
|
});
|
|
364
523
|
}
|
|
365
524
|
catch (error) {
|
|
525
|
+
run.startupSettled = true;
|
|
526
|
+
run.rejectStarted(error);
|
|
366
527
|
this.releaseRun(run);
|
|
367
528
|
throw error;
|
|
368
529
|
}
|
|
@@ -371,20 +532,30 @@ export class PluginHostRpc {
|
|
|
371
532
|
}
|
|
372
533
|
if (signal?.aborted || this.disposed || this.activeRuns.get(runId) !== run) {
|
|
373
534
|
abort.abort(signal?.reason);
|
|
535
|
+
const error = signal?.aborted
|
|
536
|
+
? new PluginHostCallError("REQUEST_CANCELLED", "plugin host request was cancelled")
|
|
537
|
+
: new PluginHostCallError("HOST_CLOSED", "plugin host is closed");
|
|
538
|
+
run.startupSettled = true;
|
|
539
|
+
run.rejectStarted(error);
|
|
374
540
|
this.releaseRun(run);
|
|
375
541
|
throwIfHostCallAborted(signal);
|
|
376
|
-
throw
|
|
542
|
+
throw error;
|
|
377
543
|
}
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
setImmediate(() => void this.pumpRun(run, started.generator));
|
|
381
|
-
return {
|
|
544
|
+
run.responseId = started.responseId;
|
|
545
|
+
const result = {
|
|
382
546
|
runId,
|
|
547
|
+
responseId: started.responseId,
|
|
383
548
|
sessionId,
|
|
384
549
|
provider: started.provider,
|
|
385
550
|
model: started.model,
|
|
386
551
|
authSource: started.authSource,
|
|
387
552
|
};
|
|
553
|
+
run.startupSettled = true;
|
|
554
|
+
run.resolveStarted(result);
|
|
555
|
+
// Let the RPC transport write the request response before stream events are
|
|
556
|
+
// emitted. `runLive` has already subscribed, so no agent event is lost.
|
|
557
|
+
setImmediate(() => void this.pumpRun(run, started.generator));
|
|
558
|
+
return result;
|
|
388
559
|
}
|
|
389
560
|
async pumpRun(run, generator) {
|
|
390
561
|
let terminalFailure;
|
|
@@ -403,6 +574,16 @@ export class PluginHostRpc {
|
|
|
403
574
|
for (const projected of projectSessionEvent(run, event)) {
|
|
404
575
|
await this.emit(projected);
|
|
405
576
|
}
|
|
577
|
+
try {
|
|
578
|
+
const fileEvent = await this.projectSessionFileEvent(run, event);
|
|
579
|
+
if (fileEvent)
|
|
580
|
+
await this.emit(fileEvent);
|
|
581
|
+
}
|
|
582
|
+
catch {
|
|
583
|
+
// File delivery is an optional projection of a canonical event. A
|
|
584
|
+
// missing borrowed path must not turn an otherwise successful native
|
|
585
|
+
// Turn into session.run.failed or stop consuming its event stream.
|
|
586
|
+
}
|
|
406
587
|
terminalFailure = sessionRunFailure(event);
|
|
407
588
|
if (terminalFailure)
|
|
408
589
|
break;
|
|
@@ -465,28 +646,83 @@ export class PluginHostRpc {
|
|
|
465
646
|
this.releaseRun(run);
|
|
466
647
|
}
|
|
467
648
|
}
|
|
649
|
+
async projectSessionFileEvent(run, event) {
|
|
650
|
+
const input = jsonRecord(event);
|
|
651
|
+
if (input?.type !== "response.output_item.done")
|
|
652
|
+
return undefined;
|
|
653
|
+
const item = jsonRecord(input.item);
|
|
654
|
+
if (item?.type !== "function_call_output")
|
|
655
|
+
return undefined;
|
|
656
|
+
const data = jsonRecord(item.data);
|
|
657
|
+
const image = jsonRecord(data?.generatedImage);
|
|
658
|
+
if (!image || typeof image.mediaType !== "string")
|
|
659
|
+
return undefined;
|
|
660
|
+
let filePath;
|
|
661
|
+
if (typeof image.path === "string" && path.isAbsolute(image.path)) {
|
|
662
|
+
filePath = image.path;
|
|
663
|
+
}
|
|
664
|
+
else if (typeof image.resourceId === "string") {
|
|
665
|
+
filePath = await this.services.sessionFiles.resolveResourcePath(run.sessionId, image.resourceId);
|
|
666
|
+
}
|
|
667
|
+
else {
|
|
668
|
+
return undefined;
|
|
669
|
+
}
|
|
670
|
+
return {
|
|
671
|
+
event: "session.run.file",
|
|
672
|
+
payload: {
|
|
673
|
+
runId: run.runId,
|
|
674
|
+
responseId: typeof item.responseId === "string"
|
|
675
|
+
? item.responseId
|
|
676
|
+
: run.responseId,
|
|
677
|
+
sessionId: run.sessionId,
|
|
678
|
+
itemId: typeof item.id === "string" ? item.id : "unknown",
|
|
679
|
+
filename: typeof image.filename === "string" && image.filename
|
|
680
|
+
? image.filename
|
|
681
|
+
: path.basename(filePath),
|
|
682
|
+
mediaType: image.mediaType,
|
|
683
|
+
path: filePath,
|
|
684
|
+
},
|
|
685
|
+
};
|
|
686
|
+
}
|
|
468
687
|
releaseRun(run) {
|
|
688
|
+
run.input.close();
|
|
689
|
+
if (!run.startupSettled) {
|
|
690
|
+
run.startupSettled = true;
|
|
691
|
+
run.rejectStarted(new PluginHostCallError("HOST_CLOSED", "plugin Host closed the active Run"));
|
|
692
|
+
}
|
|
469
693
|
if (this.activeRuns.get(run.runId) === run)
|
|
470
694
|
this.activeRuns.delete(run.runId);
|
|
471
695
|
if (this.activeSessionRuns.get(run.sessionId) === run) {
|
|
472
696
|
this.activeSessionRuns.delete(run.sessionId);
|
|
473
697
|
}
|
|
474
|
-
|
|
475
|
-
this.activeRunsByOperation.
|
|
698
|
+
for (const operationId of run.operationIds) {
|
|
699
|
+
if (this.activeRunsByOperation.get(operationId) === run) {
|
|
700
|
+
this.activeRunsByOperation.delete(operationId);
|
|
701
|
+
}
|
|
476
702
|
}
|
|
477
|
-
|
|
478
|
-
steerSession(params) {
|
|
479
|
-
const input = record(params);
|
|
480
|
-
const run = this.activeRun(input);
|
|
481
|
-
const text = requiredText(input.text, "text", MAX_MESSAGE_LENGTH);
|
|
482
|
-
return { delivered: run.steer.send(text) };
|
|
703
|
+
run.resolveSettled();
|
|
483
704
|
}
|
|
484
705
|
async interruptRun(params) {
|
|
485
706
|
const input = record(params);
|
|
486
|
-
const
|
|
487
|
-
const
|
|
488
|
-
|
|
489
|
-
|
|
707
|
+
const sessionId = optionalString(input.sessionId, "sessionId", 128);
|
|
708
|
+
const runId = optionalString(input.runId, "runId", 128);
|
|
709
|
+
const operationId = optionalString(input.operationId, "operationId", 512);
|
|
710
|
+
if ((sessionId ? 1 : 0) + (runId ? 1 : 0) + (operationId ? 1 : 0) !== 1) {
|
|
711
|
+
throw new PluginHostCallError("INVALID_PARAMS", "exactly one of sessionId, runId, or operationId is required");
|
|
712
|
+
}
|
|
713
|
+
let run;
|
|
714
|
+
if (sessionId) {
|
|
715
|
+
this.ownedSession(sessionId);
|
|
716
|
+
run = this.activeSessionRuns.get(sessionId);
|
|
717
|
+
if (!run) {
|
|
718
|
+
const interrupted = this.services.interruptSession
|
|
719
|
+
? Promise.resolve().then(() => this.services.interruptSession(sessionId))
|
|
720
|
+
: Promise.resolve(false);
|
|
721
|
+
return { interrupted: await interrupted };
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
else {
|
|
725
|
+
run = this.activeRunByIdentity(input);
|
|
490
726
|
}
|
|
491
727
|
if (run.interruptResult) {
|
|
492
728
|
return { interrupted: await run.interruptResult };
|
|
@@ -506,9 +742,7 @@ export class PluginHostRpc {
|
|
|
506
742
|
// effort and cannot prove that native execution stopped.
|
|
507
743
|
return false;
|
|
508
744
|
});
|
|
509
|
-
const interruptResult =
|
|
510
|
-
? graceful
|
|
511
|
-
: forceStoppedInterrupt(graceful, forceAfterMs, () => this.services.terminateSession?.(run.sessionId) ?? Promise.resolve(false));
|
|
745
|
+
const interruptResult = graceful;
|
|
512
746
|
run.interruptResult = interruptResult;
|
|
513
747
|
try {
|
|
514
748
|
const interrupted = await interruptResult;
|
|
@@ -526,6 +760,17 @@ export class PluginHostRpc {
|
|
|
526
760
|
throw error;
|
|
527
761
|
}
|
|
528
762
|
}
|
|
763
|
+
async sessionActivity(params) {
|
|
764
|
+
const sessionId = requiredSessionId(params);
|
|
765
|
+
this.ownedSession(sessionId);
|
|
766
|
+
const snapshot = structuredClone(await this.services.sessionActivity(sessionId));
|
|
767
|
+
const waiting = snapshot.pendingInteractions.length > 0;
|
|
768
|
+
return {
|
|
769
|
+
status: waiting ? "waiting" : snapshot.status,
|
|
770
|
+
activeResponseIds: snapshot.activeResponseIds,
|
|
771
|
+
pendingInteractions: snapshot.pendingInteractions,
|
|
772
|
+
};
|
|
773
|
+
}
|
|
529
774
|
async interruptWon(run) {
|
|
530
775
|
if (!run.interruptRequested)
|
|
531
776
|
return false;
|
|
@@ -533,13 +778,6 @@ export class PluginHostRpc {
|
|
|
533
778
|
return true;
|
|
534
779
|
return run.interruptResult.catch(() => false);
|
|
535
780
|
}
|
|
536
|
-
activeRun(input) {
|
|
537
|
-
const runId = requiredString(input.runId, "runId", 128);
|
|
538
|
-
const run = this.activeRuns.get(runId);
|
|
539
|
-
if (!run)
|
|
540
|
-
throw new PluginHostCallError("RUN_NOT_FOUND", `active run not found: ${runId}`);
|
|
541
|
-
return run;
|
|
542
|
-
}
|
|
543
781
|
activeRunByIdentity(input) {
|
|
544
782
|
const runId = optionalString(input.runId, "runId", 128);
|
|
545
783
|
const operationId = optionalString(input.operationId, "operationId", 512);
|
|
@@ -633,6 +871,36 @@ export class PluginHostRpc {
|
|
|
633
871
|
this.services.sessionRegistry.setTitle(sessionId, title);
|
|
634
872
|
return { updated: true };
|
|
635
873
|
}
|
|
874
|
+
async getSessionBrowserRequestHeaders(params) {
|
|
875
|
+
let input;
|
|
876
|
+
try {
|
|
877
|
+
input = parseRuntimeBrowserRequestHeadersGetParams(params);
|
|
878
|
+
}
|
|
879
|
+
catch {
|
|
880
|
+
throw new PluginHostCallError("INVALID_PARAMS", "Session Browser request headers parameters are invalid");
|
|
881
|
+
}
|
|
882
|
+
this.ownedSession(input.sessionId);
|
|
883
|
+
const service = this.services.sessionBrowserRequestHeaders;
|
|
884
|
+
if (!service) {
|
|
885
|
+
throw new PluginHostCallError("METHOD_NOT_FOUND", "Session Browser request headers are unsupported by this Host");
|
|
886
|
+
}
|
|
887
|
+
return await service.getRequestHeaders(input);
|
|
888
|
+
}
|
|
889
|
+
async setSessionBrowserRequestHeaders(params) {
|
|
890
|
+
let input;
|
|
891
|
+
try {
|
|
892
|
+
input = parseRuntimeBrowserRequestHeadersSetParams(params);
|
|
893
|
+
}
|
|
894
|
+
catch {
|
|
895
|
+
throw new PluginHostCallError("INVALID_PARAMS", "Session Browser request headers parameters are invalid");
|
|
896
|
+
}
|
|
897
|
+
this.ownedSession(input.sessionId);
|
|
898
|
+
const service = this.services.sessionBrowserRequestHeaders;
|
|
899
|
+
if (!service) {
|
|
900
|
+
throw new PluginHostCallError("METHOD_NOT_FOUND", "Session Browser request headers are unsupported by this Host");
|
|
901
|
+
}
|
|
902
|
+
return await service.setRequestHeaders(input);
|
|
903
|
+
}
|
|
636
904
|
resolveAgentExecution(params) {
|
|
637
905
|
const input = record(params);
|
|
638
906
|
const defaultAgent = optionalAgentId(input.defaultAgent, "defaultAgent");
|
|
@@ -652,6 +920,56 @@ export class PluginHostRpc {
|
|
|
652
920
|
...(ttlSeconds === undefined ? {} : { ttlMs: ttlSeconds * 1_000 }),
|
|
653
921
|
});
|
|
654
922
|
}
|
|
923
|
+
issueSessionPortalLink(scope, access, sessionId, embedOrigin, grantHandle) {
|
|
924
|
+
if (!this.services.sessionPortal) {
|
|
925
|
+
throw new PluginHostCallError("HOST_CALL_FAILED", "the Session Portal is not configured");
|
|
926
|
+
}
|
|
927
|
+
return issueSessionPortalTicket({
|
|
928
|
+
pluginId: this.principalId,
|
|
929
|
+
scope,
|
|
930
|
+
access,
|
|
931
|
+
...(sessionId ? { sessionId } : {}),
|
|
932
|
+
...(embedOrigin ? { embedOrigin } : {}),
|
|
933
|
+
...(grantHandle ? { grantHandle } : {}),
|
|
934
|
+
});
|
|
935
|
+
}
|
|
936
|
+
async projectTimelineText(params) {
|
|
937
|
+
const sessionTimeline = this.services.sessionTimeline;
|
|
938
|
+
if (!sessionTimeline) {
|
|
939
|
+
throw new PluginHostCallError("HOST_CALL_FAILED", "Session timeline projection is unavailable");
|
|
940
|
+
}
|
|
941
|
+
const input = record(params);
|
|
942
|
+
const sessionId = requiredSessionId(input);
|
|
943
|
+
this.ownedSession(sessionId);
|
|
944
|
+
const projection = claimSessionTimelineProjection({
|
|
945
|
+
pluginId: this.principalId,
|
|
946
|
+
sessionId,
|
|
947
|
+
projectionId: requiredString(input.projectionId, "projectionId", 256),
|
|
948
|
+
...(input.responseId === undefined
|
|
949
|
+
? {}
|
|
950
|
+
: { responseId: requiredString(input.responseId, "responseId", 256) }),
|
|
951
|
+
text: requiredText(input.text, "text", 64 * 1024),
|
|
952
|
+
});
|
|
953
|
+
if (!(await sessionTimeline.hasItem(sessionId, projection.itemId))) {
|
|
954
|
+
await sessionTimeline.projectText({
|
|
955
|
+
sessionId,
|
|
956
|
+
responseId: projection.responseId,
|
|
957
|
+
itemId: projection.itemId,
|
|
958
|
+
text: projection.text,
|
|
959
|
+
});
|
|
960
|
+
}
|
|
961
|
+
completeSessionTimelineProjection({
|
|
962
|
+
pluginId: this.principalId,
|
|
963
|
+
sessionId,
|
|
964
|
+
projectionId: projection.projectionId,
|
|
965
|
+
});
|
|
966
|
+
return {
|
|
967
|
+
projectionId: projection.projectionId,
|
|
968
|
+
responseId: projection.responseId,
|
|
969
|
+
itemId: projection.itemId,
|
|
970
|
+
disposition: projection.disposition,
|
|
971
|
+
};
|
|
972
|
+
}
|
|
655
973
|
ownedSession(sessionId) {
|
|
656
974
|
const session = this.services.sessionRegistry.get(sessionId);
|
|
657
975
|
if (!session || !this.isOwned(session)) {
|
|
@@ -678,6 +996,24 @@ function throwIfHostCallAborted(signal) {
|
|
|
678
996
|
return;
|
|
679
997
|
throw new PluginHostCallError("REQUEST_CANCELLED", "plugin host request was cancelled");
|
|
680
998
|
}
|
|
999
|
+
async function waitForHostPromise(promise, signal) {
|
|
1000
|
+
throwIfHostCallAborted(signal);
|
|
1001
|
+
if (!signal)
|
|
1002
|
+
return await promise;
|
|
1003
|
+
return await new Promise((resolve, reject) => {
|
|
1004
|
+
const aborted = () => {
|
|
1005
|
+
reject(new PluginHostCallError("REQUEST_CANCELLED", "plugin host request was cancelled"));
|
|
1006
|
+
};
|
|
1007
|
+
signal.addEventListener("abort", aborted, { once: true });
|
|
1008
|
+
void promise.then((value) => {
|
|
1009
|
+
signal.removeEventListener("abort", aborted);
|
|
1010
|
+
resolve(value);
|
|
1011
|
+
}, (error) => {
|
|
1012
|
+
signal.removeEventListener("abort", aborted);
|
|
1013
|
+
reject(error);
|
|
1014
|
+
});
|
|
1015
|
+
});
|
|
1016
|
+
}
|
|
681
1017
|
export class PluginHostCallError extends PluginRunnerError {
|
|
682
1018
|
retryable;
|
|
683
1019
|
data;
|
|
@@ -694,12 +1030,30 @@ export class PluginHostCallError extends PluginRunnerError {
|
|
|
694
1030
|
function toHostCallError(error) {
|
|
695
1031
|
if (error instanceof PluginHostCallError)
|
|
696
1032
|
return error;
|
|
1033
|
+
if (error instanceof SessionBrowserServiceError) {
|
|
1034
|
+
if (error.code === "revision_mismatch") {
|
|
1035
|
+
return new PluginHostCallError("REVISION_CONFLICT", "Session Browser request headers changed since they were read");
|
|
1036
|
+
}
|
|
1037
|
+
if (error.code === "invalid_request") {
|
|
1038
|
+
return new PluginHostCallError("INVALID_PARAMS", "Session Browser request headers parameters are invalid");
|
|
1039
|
+
}
|
|
1040
|
+
if (error.code === "not_found") {
|
|
1041
|
+
return new PluginHostCallError("SESSION_NOT_FOUND", "plugin session not found");
|
|
1042
|
+
}
|
|
1043
|
+
return new PluginHostCallError("HOST_CALL_FAILED", "Session Browser request headers operation failed");
|
|
1044
|
+
}
|
|
697
1045
|
if (error instanceof AgentRuntimeError && error.code === "live_unavailable") {
|
|
698
1046
|
// ConversationRuntime emits this code only before injectMessage is called:
|
|
699
1047
|
// the native live Session (including its exact Skill environment) could
|
|
700
1048
|
// not be brought up, so callers may fail without quarantining capacity.
|
|
701
1049
|
return new PluginHostCallError("LIVE_SESSION_UNAVAILABLE", error.message, false);
|
|
702
1050
|
}
|
|
1051
|
+
if (error instanceof AgentRuntimeError) {
|
|
1052
|
+
// Native lifecycle failures already carry a stable phase code. Preserve it
|
|
1053
|
+
// across the plugin-host boundary instead of erasing it behind the generic
|
|
1054
|
+
// HOST_CALL_FAILED wrapper.
|
|
1055
|
+
return new PluginHostCallError(error.code, error.message, false);
|
|
1056
|
+
}
|
|
703
1057
|
if (error instanceof SessionLaunchOperationConflictError) {
|
|
704
1058
|
return new PluginHostCallError(error.code, error.message, false);
|
|
705
1059
|
}
|
|
@@ -717,7 +1071,10 @@ function sessionLaunchSelection(value) {
|
|
|
717
1071
|
const agent = optionalAgentId(input.agent, "session.agent");
|
|
718
1072
|
const config = input.config === undefined ? undefined : agentSpecSchema.parse(input.config);
|
|
719
1073
|
const provider = optionalRuntimeValue(input.provider);
|
|
720
|
-
const
|
|
1074
|
+
const modelProvided = input.model !== undefined;
|
|
1075
|
+
const model = input.model === null || input.model === ""
|
|
1076
|
+
? null
|
|
1077
|
+
: optionalString(input.model, "session.model", 256);
|
|
721
1078
|
const reasoningEffort = optionalString(input.reasoningEffort, "session.reasoningEffort", 128);
|
|
722
1079
|
const title = optionalString(input.title, "session.title", 1_000);
|
|
723
1080
|
if (Number(Boolean(projectId)) + Number(Boolean(managedCwd)) + Number(Boolean(workspace)) > 1) {
|
|
@@ -736,7 +1093,7 @@ function sessionLaunchSelection(value) {
|
|
|
736
1093
|
...(agent ? { agent } : {}),
|
|
737
1094
|
...(config ? { config } : {}),
|
|
738
1095
|
...(provider ? { provider } : {}),
|
|
739
|
-
...(
|
|
1096
|
+
...(modelProvided ? { model: model ?? null } : {}),
|
|
740
1097
|
...(reasoningEffort ? { reasoningEffort } : {}),
|
|
741
1098
|
...(title ? { title } : {}),
|
|
742
1099
|
};
|
|
@@ -768,6 +1125,114 @@ function sessionExecutionSnapshot(value) {
|
|
|
768
1125
|
function requiredSessionId(params) {
|
|
769
1126
|
return requiredString(record(params).sessionId, "sessionId", 512);
|
|
770
1127
|
}
|
|
1128
|
+
async function pluginRunContent(value) {
|
|
1129
|
+
if (!Array.isArray(value) || value.length === 0 || value.length > 256) {
|
|
1130
|
+
throw new PluginHostCallError("INVALID_PARAMS", "content must be a non-empty array with at most 256 parts");
|
|
1131
|
+
}
|
|
1132
|
+
return Promise.all(value.map(async (entry, index) => {
|
|
1133
|
+
const input = record(entry);
|
|
1134
|
+
if (input.type === "input_text") {
|
|
1135
|
+
return {
|
|
1136
|
+
type: "input_text",
|
|
1137
|
+
text: requiredText(input.text, `content[${index}].text`, MAX_MESSAGE_LENGTH),
|
|
1138
|
+
};
|
|
1139
|
+
}
|
|
1140
|
+
if (input.type !== "input_image" && input.type !== "input_file") {
|
|
1141
|
+
throw new PluginHostCallError("INVALID_PARAMS", `content[${index}].type is invalid`);
|
|
1142
|
+
}
|
|
1143
|
+
const suppliedPath = requiredPathString(input.path, `content[${index}].path`, 16_384);
|
|
1144
|
+
if (!path.isAbsolute(suppliedPath)) {
|
|
1145
|
+
throw new PluginHostCallError("INVALID_PARAMS", `content[${index}].path must be absolute`);
|
|
1146
|
+
}
|
|
1147
|
+
let resolvedPath;
|
|
1148
|
+
let fileSize;
|
|
1149
|
+
try {
|
|
1150
|
+
resolvedPath = await realpath(suppliedPath);
|
|
1151
|
+
const metadata = await stat(resolvedPath);
|
|
1152
|
+
if (!metadata.isFile())
|
|
1153
|
+
throw new Error("not a regular file");
|
|
1154
|
+
fileSize = metadata.size;
|
|
1155
|
+
}
|
|
1156
|
+
catch (error) {
|
|
1157
|
+
throw new PluginHostCallError("INVALID_PARAMS", `content[${index}].path is not a readable regular file: ${error instanceof Error ? error.message : String(error)}`);
|
|
1158
|
+
}
|
|
1159
|
+
const filename = input.type === "input_file"
|
|
1160
|
+
? requiredString(input.filename, `content[${index}].filename`, 255)
|
|
1161
|
+
: optionalString(input.filename, `content[${index}].filename`, 255) ??
|
|
1162
|
+
path.basename(resolvedPath);
|
|
1163
|
+
if (input.type === "input_image") {
|
|
1164
|
+
return {
|
|
1165
|
+
type: "input_image",
|
|
1166
|
+
path: resolvedPath,
|
|
1167
|
+
filename,
|
|
1168
|
+
mediaType: pluginImageMediaType(input.mediaType, resolvedPath, `content[${index}].mediaType`),
|
|
1169
|
+
byteLength: fileSize,
|
|
1170
|
+
};
|
|
1171
|
+
}
|
|
1172
|
+
return {
|
|
1173
|
+
type: "input_file",
|
|
1174
|
+
path: resolvedPath,
|
|
1175
|
+
filename,
|
|
1176
|
+
mediaType: pluginMediaType(input.mediaType ?? "application/octet-stream", `content[${index}].mediaType`),
|
|
1177
|
+
byteLength: fileSize,
|
|
1178
|
+
};
|
|
1179
|
+
}));
|
|
1180
|
+
}
|
|
1181
|
+
function pluginImageMediaType(value, filePath, label) {
|
|
1182
|
+
const inferred = (() => {
|
|
1183
|
+
switch (path.extname(filePath).toLowerCase()) {
|
|
1184
|
+
case ".png": return "image/png";
|
|
1185
|
+
case ".jpg":
|
|
1186
|
+
case ".jpeg": return "image/jpeg";
|
|
1187
|
+
case ".webp": return "image/webp";
|
|
1188
|
+
case ".gif": return "image/gif";
|
|
1189
|
+
case ".svg": return "image/svg+xml";
|
|
1190
|
+
default: return undefined;
|
|
1191
|
+
}
|
|
1192
|
+
})();
|
|
1193
|
+
const mediaType = value === undefined ? inferred : pluginMediaType(value, label);
|
|
1194
|
+
if (mediaType !== "image/png" &&
|
|
1195
|
+
mediaType !== "image/jpeg" &&
|
|
1196
|
+
mediaType !== "image/webp" &&
|
|
1197
|
+
mediaType !== "image/gif" &&
|
|
1198
|
+
mediaType !== "image/svg+xml") {
|
|
1199
|
+
throw new PluginHostCallError("INVALID_PARAMS", `${label} is not a supported image type`);
|
|
1200
|
+
}
|
|
1201
|
+
return mediaType;
|
|
1202
|
+
}
|
|
1203
|
+
function pluginMediaType(value, label) {
|
|
1204
|
+
if (typeof value !== "string") {
|
|
1205
|
+
throw new PluginHostCallError("INVALID_PARAMS", `${label} must be a media type`);
|
|
1206
|
+
}
|
|
1207
|
+
const mediaType = value.trim().toLowerCase();
|
|
1208
|
+
if (mediaType.length === 0 ||
|
|
1209
|
+
mediaType.length > 255 ||
|
|
1210
|
+
!/^[a-z0-9!#$&^_.+-]+\/[a-z0-9!#$&^_.+-]+$/.test(mediaType)) {
|
|
1211
|
+
throw new PluginHostCallError("INVALID_PARAMS", `${label} is invalid`);
|
|
1212
|
+
}
|
|
1213
|
+
return mediaType;
|
|
1214
|
+
}
|
|
1215
|
+
function requiredSessionPortalAccess(params) {
|
|
1216
|
+
const access = record(params).access;
|
|
1217
|
+
if (access === "read_only" || access === "read_write")
|
|
1218
|
+
return access;
|
|
1219
|
+
throw new PluginHostCallError("INVALID_PARAMS", "access must be read_only or read_write");
|
|
1220
|
+
}
|
|
1221
|
+
function optionalSessionPortalEmbedOrigin(value) {
|
|
1222
|
+
if (value === undefined)
|
|
1223
|
+
return undefined;
|
|
1224
|
+
const candidate = requiredString(value, "embedOrigin", 512);
|
|
1225
|
+
try {
|
|
1226
|
+
const parsed = new URL(candidate);
|
|
1227
|
+
if ((parsed.protocol !== "http:" && parsed.protocol !== "https:") ||
|
|
1228
|
+
parsed.origin !== candidate)
|
|
1229
|
+
throw new Error("invalid origin");
|
|
1230
|
+
return parsed.origin;
|
|
1231
|
+
}
|
|
1232
|
+
catch {
|
|
1233
|
+
throw new PluginHostCallError("INVALID_PARAMS", "embedOrigin must be an exact http(s) origin");
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
771
1236
|
function sessionInteractionResolution(value) {
|
|
772
1237
|
const input = record(value);
|
|
773
1238
|
const keys = Object.keys(input);
|
|
@@ -810,6 +1275,13 @@ function sessionInteractionResolution(value) {
|
|
|
810
1275
|
function optionalRuntime(params) {
|
|
811
1276
|
return optionalRuntimeValue(record(params).runtime);
|
|
812
1277
|
}
|
|
1278
|
+
function optionalCollaborationMode(value) {
|
|
1279
|
+
if (value === undefined)
|
|
1280
|
+
return undefined;
|
|
1281
|
+
if (value === "plan" || value === "default")
|
|
1282
|
+
return value;
|
|
1283
|
+
throw new PluginHostCallError("INVALID_PARAMS", "collaborationMode must be plan or default");
|
|
1284
|
+
}
|
|
813
1285
|
function optionalRuntimeValue(value) {
|
|
814
1286
|
if (value === undefined || value === null || value === "")
|
|
815
1287
|
return undefined;
|