@rynx-ai/daemon 0.1.11-beta.2 → 0.1.11-beta.21
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 +20 -18
- package/bundled-plugins/plugins/lark/dist/lark.js +18 -12
- package/bundled-plugins/plugins/lark/dist/runtime.js +6046 -6022
- package/bundled-plugins/plugins/lark/dist/settings.js +6006 -5988
- 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.d.ts +4 -1
- package/dist/browser-artifact-management.js +22 -11
- package/dist/browser-artifacts.d.ts +1 -0
- package/dist/browser-artifacts.js +1 -0
- package/dist/chrome-for-testing-runtime.d.ts +27 -0
- package/dist/chrome-for-testing-runtime.js +194 -0
- package/dist/chrome-for-testing-store.d.ts +2 -0
- package/dist/chrome-for-testing-store.js +37 -6
- 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.d.ts +4 -1
- package/dist/daemon-server.js +158 -45
- package/dist/db.d.ts +2 -2
- package/dist/db.js +55 -20
- package/dist/entry-path.d.ts +3 -0
- package/dist/entry-path.js +12 -0
- package/dist/index-daemon.js +3 -87
- package/dist/lifecycle.d.ts +13 -0
- package/dist/lifecycle.js +19 -0
- package/dist/maintenance-lease.d.ts +39 -0
- package/dist/maintenance-lease.js +250 -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 +35 -6
- package/dist/plugin-host-rpc.js +222 -59
- package/dist/plugin-installer.js +62 -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.d.ts +5 -0
- package/dist/plugin-supervisor.js +218 -33
- package/dist/pm2.d.ts +11 -2
- package/dist/pm2.js +116 -19
- package/dist/process.d.ts +12 -0
- package/dist/process.js +91 -0
- package/dist/remote-runtime-access-store.d.ts +2 -2
- package/dist/remote-runtime-access-store.js +6 -9
- 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.js +24 -8
- package/dist/session-launch-operations.d.ts +10 -1
- package/dist/session-launch-operations.js +21 -11
- package/dist/session-log-store.js +2 -1
- 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 +29 -0
- package/dist/session-portal-grant-store.js +85 -0
- package/dist/session-resource-store.js +3 -2
- package/dist/setup-service.d.ts +21 -0
- package/dist/setup-service.js +39 -0
- package/dist/skills-catalog.d.ts +11 -2
- package/dist/skills-catalog.js +307 -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 +37 -21
- package/bundled-plugins/plugins/lark/ui/assets/index-B2dhMlMU.css +0 -1
- package/dist/browser-cli-args.d.ts +0 -20
- package/dist/browser-cli-args.js +0 -100
- package/dist/cli.d.ts +0 -2
- package/dist/cli.js +0 -550
- package/dist/control-client.d.ts +0 -122
- package/dist/control-client.js +0 -855
- package/dist/control-endpoint.d.ts +0 -14
- package/dist/control-endpoint.js +0 -124
- package/dist/desktop-browser-host-client.d.ts +0 -35
- package/dist/desktop-browser-host-client.js +0 -415
- package/dist/setup.d.ts +0 -20
- package/dist/setup.js +0 -386
- package/dist/update.d.ts +0 -25
- package/dist/update.js +0 -133
- /package/bundled-plugins/plugins/lark/ui/assets/{index-D9N-AkLX.js → index-D8Amoe6a.js} +0 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { type AgentSpec, type AgentCapabilities, type AgentRuntimeId, type AgentSessionStore, type ConversationRuntime, type ResolvedExecutionSnapshot, type SessionRegistry, type SessionWorkspaceSnapshot } from "@rynx-ai/core";
|
|
2
|
-
import { type PluginAgentSummary, type
|
|
1
|
+
import { type AdmissionReservation, type AgentSpec, type AgentCapabilities, type AgentRuntimeId, type AgentSessionStore, type ConversationRuntime, type ResolvedExecutionSnapshot, type SessionRegistry, type SessionWorkspaceSnapshot } from "@rynx-ai/core";
|
|
2
|
+
import { type PluginAgentSummary, type PluginHostEvent, type PluginResolvedSessionExecution, type PluginSessionExecutionSnapshot, type PluginSessionInteractionResolveDisposition } from "@rynx-ai/plugin-sdk";
|
|
3
3
|
import type { SessionInteractionResolution } from "@rynx-ai/protocol";
|
|
4
4
|
import { PluginRunnerError } from "@rynx-ai/plugin-runner";
|
|
5
5
|
/** Leaves ample room for the RPC envelope below the runner's 1 MiB line cap. */
|
|
@@ -18,10 +18,11 @@ export interface PluginAgentCatalogHost {
|
|
|
18
18
|
export interface PluginSessionLaunchSelection {
|
|
19
19
|
projectId?: string;
|
|
20
20
|
managedCwd?: string;
|
|
21
|
+
workspace?: SessionWorkspaceSnapshot;
|
|
21
22
|
agent?: string;
|
|
22
23
|
config?: AgentSpec;
|
|
23
24
|
provider?: AgentRuntimeId;
|
|
24
|
-
model?: string;
|
|
25
|
+
model?: string | null;
|
|
25
26
|
reasoningEffort?: string;
|
|
26
27
|
title?: string;
|
|
27
28
|
}
|
|
@@ -44,7 +45,15 @@ export interface PluginHostServices {
|
|
|
44
45
|
sessionLaunch: {
|
|
45
46
|
resolve(input: PluginSessionLaunchSelection): Promise<PluginResolvedSessionLaunch>;
|
|
46
47
|
};
|
|
48
|
+
sessionExecution: {
|
|
49
|
+
update(input: {
|
|
50
|
+
sessionId: string;
|
|
51
|
+
model: string | null;
|
|
52
|
+
reasoningEffort: string | null;
|
|
53
|
+
}): Promise<void>;
|
|
54
|
+
};
|
|
47
55
|
interruptSession?(sessionId: string): Promise<boolean>;
|
|
56
|
+
terminateSession?(sessionId: string): Promise<boolean>;
|
|
48
57
|
resolveSessionInteraction?(sessionId: string, interactionId: string, resolution: SessionInteractionResolution): Promise<{
|
|
49
58
|
disposition: PluginSessionInteractionResolveDisposition;
|
|
50
59
|
}>;
|
|
@@ -56,6 +65,13 @@ export interface PluginHostServices {
|
|
|
56
65
|
sessionId: string;
|
|
57
66
|
disposition: "created" | "replayed";
|
|
58
67
|
}>;
|
|
68
|
+
admissionOpen?: () => boolean;
|
|
69
|
+
admissionReserve?: () => AdmissionReservation | undefined;
|
|
70
|
+
sessionPortal?: {
|
|
71
|
+
scheme: "http" | "https";
|
|
72
|
+
bindHost: string;
|
|
73
|
+
port: number;
|
|
74
|
+
};
|
|
59
75
|
}
|
|
60
76
|
export interface PluginHostRpcOptions {
|
|
61
77
|
/** Manifest id returned by host.info. */
|
|
@@ -63,16 +79,19 @@ export interface PluginHostRpcOptions {
|
|
|
63
79
|
/** Marketplace-qualified principal used for ownership and proof scopes. */
|
|
64
80
|
principalId?: string;
|
|
65
81
|
daemonInstanceId: string;
|
|
66
|
-
|
|
82
|
+
productVersion: string;
|
|
83
|
+
cliVersion: string;
|
|
67
84
|
services: PluginHostServices;
|
|
68
85
|
/** Canonical plugin-owned root permitted for the Workspace plugin's isolated
|
|
69
86
|
* Session directory. Omitted for every other plugin. */
|
|
70
87
|
managedWorkspaceRoot?: string;
|
|
71
88
|
/** Resolving this promise is the stream's backpressure acknowledgement. */
|
|
72
89
|
emit(event: PluginHostEvent): void | Promise<void>;
|
|
90
|
+
/** Bounded graceful-stop window used while disposing this Host. */
|
|
91
|
+
disposeForceAfterMs?: number;
|
|
73
92
|
}
|
|
74
93
|
/**
|
|
75
|
-
* One authenticated plugin's
|
|
94
|
+
* One authenticated plugin's Host RPC surface.
|
|
76
95
|
*
|
|
77
96
|
* This adapter is deliberately the only place that maps public string methods
|
|
78
97
|
* to trusted runtime objects. The runner receives neither this class nor its
|
|
@@ -82,21 +101,27 @@ export declare class PluginHostRpc {
|
|
|
82
101
|
private readonly pluginId;
|
|
83
102
|
private readonly principalId;
|
|
84
103
|
private readonly daemonInstanceId;
|
|
85
|
-
private readonly
|
|
104
|
+
private readonly productVersion;
|
|
105
|
+
private readonly cliVersion;
|
|
86
106
|
private readonly services;
|
|
87
107
|
private readonly managedWorkspaceRoot;
|
|
88
108
|
private readonly emit;
|
|
109
|
+
private readonly disposeForceAfterMs;
|
|
89
110
|
private readonly activeRuns;
|
|
90
111
|
private readonly activeSessionRuns;
|
|
91
112
|
private readonly activeRunsByOperation;
|
|
92
113
|
private disposed;
|
|
114
|
+
private disposePromise?;
|
|
93
115
|
constructor(options: PluginHostRpcOptions);
|
|
94
116
|
call(method: string, params: unknown, signal?: AbortSignal): Promise<unknown>;
|
|
95
117
|
dispose(): Promise<void>;
|
|
96
118
|
private ensureSession;
|
|
119
|
+
private ensureSessionAdmitted;
|
|
97
120
|
private resolveManagedWorkspace;
|
|
121
|
+
private resolveWorkspaceSnapshot;
|
|
98
122
|
private sessionEnsureResult;
|
|
99
123
|
private runSession;
|
|
124
|
+
private runSessionAdmitted;
|
|
100
125
|
private pumpRun;
|
|
101
126
|
private releaseRun;
|
|
102
127
|
private steerSession;
|
|
@@ -110,9 +135,13 @@ export declare class PluginHostRpc {
|
|
|
110
135
|
private withOwnedSession;
|
|
111
136
|
private resolveSessionInteraction;
|
|
112
137
|
private forkSession;
|
|
138
|
+
private forkSessionAdmitted;
|
|
139
|
+
private reserveAdmission;
|
|
140
|
+
private withAdmission;
|
|
113
141
|
private setSessionTitle;
|
|
114
142
|
private resolveAgentExecution;
|
|
115
143
|
private issueControlLink;
|
|
144
|
+
private issueSessionPortalLink;
|
|
116
145
|
private ownedSession;
|
|
117
146
|
private isOwned;
|
|
118
147
|
}
|
package/dist/plugin-host-rpc.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
-
import { realpath } from "node:fs/promises";
|
|
2
|
+
import { realpath, stat } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { AgentRuntimeError, agentSpecSchema, resolvedExecutionSnapshotSchema, sessionWorkspaceSnapshotSchema, SteerController, } from "@rynx-ai/core";
|
|
5
5
|
import { PLUGIN_HOST_MAX_ACTIVE_RUNS, } from "@rynx-ai/plugin-sdk";
|
|
6
6
|
import { PluginRunnerError } from "@rynx-ai/plugin-runner";
|
|
7
7
|
import { ensurePluginSessionLaunch, replayPluginSessionLaunch, SessionLaunchOperationConflictError, SessionLaunchOperationTargetDeletedError, } from "./session-launch-operations.js";
|
|
8
8
|
import { createControlLink } from "./control-link-store.js";
|
|
9
|
+
import { issueSessionPortalTicket } from "./session-portal-grant-store.js";
|
|
9
10
|
const MAX_MESSAGE_LENGTH = 1_000_000;
|
|
10
11
|
/** Leaves ample room for the RPC envelope below the runner's 1 MiB line cap. */
|
|
11
12
|
export const MAX_PLUGIN_HOST_EVENT_BYTES = 512 * 1024;
|
|
@@ -13,31 +14,8 @@ const MAX_PLUGIN_RUN_FINAL_RESPONSE_BYTES = 64 * 1024;
|
|
|
13
14
|
const MAX_DELTA_CHUNK_CODE_UNITS = 64 * 1024;
|
|
14
15
|
const SESSION_INTERACTION_RESOLUTION_MAX_BYTES = 128 * 1024;
|
|
15
16
|
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
17
|
/**
|
|
40
|
-
* One authenticated plugin's
|
|
18
|
+
* One authenticated plugin's Host RPC surface.
|
|
41
19
|
*
|
|
42
20
|
* This adapter is deliberately the only place that maps public string methods
|
|
43
21
|
* to trusted runtime objects. The runner receives neither this class nor its
|
|
@@ -47,24 +25,29 @@ export class PluginHostRpc {
|
|
|
47
25
|
pluginId;
|
|
48
26
|
principalId;
|
|
49
27
|
daemonInstanceId;
|
|
50
|
-
|
|
28
|
+
productVersion;
|
|
29
|
+
cliVersion;
|
|
51
30
|
services;
|
|
52
31
|
managedWorkspaceRoot;
|
|
53
32
|
emit;
|
|
33
|
+
disposeForceAfterMs;
|
|
54
34
|
activeRuns = new Map();
|
|
55
35
|
activeSessionRuns = new Map();
|
|
56
36
|
activeRunsByOperation = new Map();
|
|
57
37
|
disposed = false;
|
|
38
|
+
disposePromise;
|
|
58
39
|
constructor(options) {
|
|
59
40
|
this.pluginId = requiredString(options.pluginId, "pluginId", 64);
|
|
60
41
|
this.principalId = requiredString(options.principalId ?? options.pluginId, "principalId", 160);
|
|
61
42
|
this.daemonInstanceId = requiredString(options.daemonInstanceId, "daemonInstanceId", 128);
|
|
62
|
-
this.
|
|
43
|
+
this.productVersion = requiredString(options.productVersion, "productVersion", 128);
|
|
44
|
+
this.cliVersion = requiredString(options.cliVersion, "cliVersion", 128);
|
|
63
45
|
this.services = options.services;
|
|
64
46
|
this.managedWorkspaceRoot = options.managedWorkspaceRoot
|
|
65
47
|
? path.resolve(options.managedWorkspaceRoot)
|
|
66
48
|
: undefined;
|
|
67
49
|
this.emit = options.emit;
|
|
50
|
+
this.disposeForceAfterMs = options.disposeForceAfterMs ?? 10_000;
|
|
68
51
|
}
|
|
69
52
|
async call(method, params, signal) {
|
|
70
53
|
if (this.disposed)
|
|
@@ -75,20 +58,18 @@ export class PluginHostRpc {
|
|
|
75
58
|
pluginId: this.pluginId,
|
|
76
59
|
installationId: this.services.installationId,
|
|
77
60
|
daemonInstanceId: this.daemonInstanceId,
|
|
61
|
+
productVersion: this.productVersion,
|
|
62
|
+
cliVersion: this.cliVersion,
|
|
78
63
|
installationProof: {
|
|
79
64
|
algorithm: this.services.installationProof.algorithm,
|
|
80
65
|
publicKey: this.services.installationProof.publicKey,
|
|
81
66
|
},
|
|
82
67
|
protocolVersion: 1,
|
|
83
|
-
|
|
68
|
+
...(this.services.sessionPortal
|
|
69
|
+
? { sessionPortal: { ...this.services.sessionPortal } }
|
|
70
|
+
: {}),
|
|
84
71
|
};
|
|
85
72
|
}
|
|
86
|
-
const capability = METHOD_CAPABILITY[method];
|
|
87
|
-
if (!capability)
|
|
88
|
-
throw new PluginHostCallError("METHOD_NOT_FOUND", `unknown host method: ${method}`);
|
|
89
|
-
if (!this.capabilities.has(capability)) {
|
|
90
|
-
throw new PluginHostCallError("CAPABILITY_NOT_DECLARED", `host method ${method} is missing manifest capability ${capability}`);
|
|
91
|
-
}
|
|
92
73
|
try {
|
|
93
74
|
switch (method) {
|
|
94
75
|
case "installation.proof.sign": {
|
|
@@ -142,6 +123,16 @@ export class PluginHostRpc {
|
|
|
142
123
|
return this.setSessionTitle(params);
|
|
143
124
|
case "control.link.issue":
|
|
144
125
|
return this.issueControlLink(params);
|
|
126
|
+
case "session.portal.plugin.issue": {
|
|
127
|
+
const access = requiredSessionPortalAccess(params);
|
|
128
|
+
return this.issueSessionPortalLink("plugin_sessions", access);
|
|
129
|
+
}
|
|
130
|
+
case "session.portal.session.issue": {
|
|
131
|
+
const input = record(params);
|
|
132
|
+
const sessionId = requiredSessionId(input);
|
|
133
|
+
this.ownedSession(sessionId);
|
|
134
|
+
return this.issueSessionPortalLink("single_session", requiredSessionPortalAccess(input), sessionId);
|
|
135
|
+
}
|
|
145
136
|
default:
|
|
146
137
|
throw new PluginHostCallError("METHOD_NOT_FOUND", `unknown host method: ${method}`);
|
|
147
138
|
}
|
|
@@ -151,51 +142,85 @@ export class PluginHostRpc {
|
|
|
151
142
|
}
|
|
152
143
|
}
|
|
153
144
|
async dispose() {
|
|
154
|
-
if (this.
|
|
155
|
-
return;
|
|
145
|
+
if (this.disposePromise)
|
|
146
|
+
return this.disposePromise;
|
|
156
147
|
this.disposed = true;
|
|
157
148
|
const runs = [...this.activeRuns.values()];
|
|
158
|
-
this.
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
run.
|
|
149
|
+
this.disposePromise = (async () => {
|
|
150
|
+
const stopped = await Promise.allSettled(runs.map(async (run) => {
|
|
151
|
+
if (this.activeRuns.get(run.runId) !== run)
|
|
152
|
+
return;
|
|
153
|
+
run.interruptRequested = true;
|
|
154
|
+
const graceful = run.interruptResult ?? (this.services.interruptSession
|
|
155
|
+
? Promise.resolve().then(() => this.services.interruptSession(run.sessionId))
|
|
156
|
+
: Promise.resolve().then(() => {
|
|
157
|
+
run.abort.abort();
|
|
158
|
+
return false;
|
|
159
|
+
}));
|
|
160
|
+
const authoritative = forceStoppedInterrupt(graceful, this.disposeForceAfterMs, () => this.services.terminateSession?.(run.sessionId) ?? Promise.resolve(false));
|
|
161
|
+
run.interruptResult = authoritative;
|
|
162
|
+
if (!(await authoritative)) {
|
|
163
|
+
throw new Error(`could not prove Session ${run.sessionId} stopped`);
|
|
164
|
+
}
|
|
165
|
+
this.releaseRun(run);
|
|
166
|
+
}));
|
|
167
|
+
const failures = stopped
|
|
168
|
+
.filter((result) => result.status === "rejected")
|
|
169
|
+
.map((result) => result.reason);
|
|
170
|
+
if (failures.length > 0) {
|
|
171
|
+
throw new AggregateError(failures, "plugin Host could not stop every active run");
|
|
170
172
|
}
|
|
171
|
-
}
|
|
173
|
+
})();
|
|
174
|
+
return this.disposePromise;
|
|
172
175
|
}
|
|
173
176
|
async ensureSession(params) {
|
|
177
|
+
return this.withAdmission(() => this.ensureSessionAdmitted(params));
|
|
178
|
+
}
|
|
179
|
+
async ensureSessionAdmitted(params) {
|
|
174
180
|
const input = record(params);
|
|
175
181
|
const scopeId = requiredString(input.scopeId, "scopeId", 512);
|
|
176
182
|
const operationId = requiredString(input.operationId, "operationId", 512);
|
|
177
183
|
const parsedSelection = sessionLaunchSelection(input.session);
|
|
184
|
+
if (parsedSelection.managedCwd && !this.managedWorkspaceRoot) {
|
|
185
|
+
throw new PluginHostCallError("INVALID_PARAMS", "session.managedCwd is reserved for the built-in Workspace plugin");
|
|
186
|
+
}
|
|
187
|
+
const rawRequestHash = hashCanonicalJson(parsedSelection);
|
|
188
|
+
const replay = replayPluginSessionLaunch({
|
|
189
|
+
pluginId: this.principalId,
|
|
190
|
+
scopeId,
|
|
191
|
+
operationId,
|
|
192
|
+
requestHash: rawRequestHash,
|
|
193
|
+
allowRequestHashMismatch: true,
|
|
194
|
+
});
|
|
195
|
+
if (replay)
|
|
196
|
+
return this.sessionEnsureResult(replay);
|
|
178
197
|
const selection = parsedSelection.managedCwd
|
|
179
198
|
? {
|
|
180
199
|
...parsedSelection,
|
|
181
200
|
managedCwd: await this.resolveManagedWorkspace(parsedSelection.managedCwd),
|
|
182
201
|
}
|
|
183
|
-
: parsedSelection
|
|
202
|
+
: parsedSelection.workspace
|
|
203
|
+
? {
|
|
204
|
+
...parsedSelection,
|
|
205
|
+
workspace: await this.resolveWorkspaceSnapshot(parsedSelection.workspace),
|
|
206
|
+
}
|
|
207
|
+
: parsedSelection;
|
|
184
208
|
const requestHash = hashCanonicalJson(selection);
|
|
185
|
-
const
|
|
209
|
+
const canonicalReplay = replayPluginSessionLaunch({
|
|
186
210
|
pluginId: this.principalId,
|
|
187
211
|
scopeId,
|
|
188
212
|
operationId,
|
|
189
213
|
requestHash,
|
|
190
214
|
});
|
|
191
|
-
if (
|
|
192
|
-
return this.sessionEnsureResult(
|
|
215
|
+
if (canonicalReplay)
|
|
216
|
+
return this.sessionEnsureResult(canonicalReplay);
|
|
193
217
|
const resolved = await this.services.sessionLaunch.resolve(selection);
|
|
194
218
|
const ensured = ensurePluginSessionLaunch({
|
|
195
219
|
pluginId: this.principalId,
|
|
196
220
|
scopeId,
|
|
197
221
|
operationId,
|
|
198
222
|
requestHash,
|
|
223
|
+
rawRequestHash,
|
|
199
224
|
session: {
|
|
200
225
|
workspace: sessionWorkspaceSnapshotSchema.parse(resolved.workspace),
|
|
201
226
|
execution: resolvedExecutionSnapshotSchema.parse(resolved.execution),
|
|
@@ -231,6 +256,33 @@ export class PluginHostRpc {
|
|
|
231
256
|
}
|
|
232
257
|
return canonicalDirectory;
|
|
233
258
|
}
|
|
259
|
+
async resolveWorkspaceSnapshot(workspace) {
|
|
260
|
+
const directories = [workspace.cwd, ...workspace.allowedDirs];
|
|
261
|
+
if (directories.some((directory) => !path.isAbsolute(directory))) {
|
|
262
|
+
throw new PluginHostCallError("INVALID_PARAMS", "session.workspace paths must be absolute");
|
|
263
|
+
}
|
|
264
|
+
let canonical;
|
|
265
|
+
try {
|
|
266
|
+
canonical = await Promise.all(directories.map(async (directory) => {
|
|
267
|
+
const resolved = await realpath(directory);
|
|
268
|
+
if (!(await stat(resolved)).isDirectory()) {
|
|
269
|
+
throw new Error("not a directory");
|
|
270
|
+
}
|
|
271
|
+
return resolved;
|
|
272
|
+
}));
|
|
273
|
+
}
|
|
274
|
+
catch {
|
|
275
|
+
throw new PluginHostCallError("INVALID_PARAMS", "session.workspace paths must be existing directories");
|
|
276
|
+
}
|
|
277
|
+
const parsed = sessionWorkspaceSnapshotSchema.safeParse({
|
|
278
|
+
cwd: canonical[0],
|
|
279
|
+
allowedDirs: canonical.slice(1),
|
|
280
|
+
});
|
|
281
|
+
if (!parsed.success) {
|
|
282
|
+
throw new PluginHostCallError("INVALID_PARAMS", `invalid session.workspace: ${parsed.error.issues[0]?.message ?? "invalid snapshot"}`);
|
|
283
|
+
}
|
|
284
|
+
return parsed.data;
|
|
285
|
+
}
|
|
234
286
|
sessionEnsureResult(result) {
|
|
235
287
|
const session = this.ownedSession(result.sessionId);
|
|
236
288
|
return {
|
|
@@ -243,10 +295,21 @@ export class PluginHostRpc {
|
|
|
243
295
|
};
|
|
244
296
|
}
|
|
245
297
|
async runSession(params, signal) {
|
|
298
|
+
return this.withAdmission(() => this.runSessionAdmitted(params, signal));
|
|
299
|
+
}
|
|
300
|
+
async runSessionAdmitted(params, signal) {
|
|
246
301
|
const input = record(params);
|
|
247
302
|
const sessionId = requiredSessionId(input);
|
|
248
303
|
const operationId = optionalString(input.operationId, "operationId", 512);
|
|
249
|
-
const
|
|
304
|
+
const modelProvided = input.model !== undefined;
|
|
305
|
+
const requestedModel = input.model === null || input.model === ""
|
|
306
|
+
? null
|
|
307
|
+
: optionalString(input.model, "model", 256);
|
|
308
|
+
const reasoningEffortProvided = input.reasoningEffort !== undefined;
|
|
309
|
+
const requestedReasoningEffort = input.reasoningEffort === null || input.reasoningEffort === ""
|
|
310
|
+
? null
|
|
311
|
+
: optionalString(input.reasoningEffort, "reasoningEffort", 64);
|
|
312
|
+
let session = this.ownedSession(sessionId);
|
|
250
313
|
if (operationId && this.activeRunsByOperation.has(operationId)) {
|
|
251
314
|
throw new PluginHostCallError("RUN_ALREADY_ACTIVE", `operation already has an active run: ${operationId}`, true);
|
|
252
315
|
}
|
|
@@ -257,6 +320,19 @@ export class PluginHostRpc {
|
|
|
257
320
|
throw new PluginHostCallError("RUN_LIMIT", "plugin has too many active session runs", true);
|
|
258
321
|
}
|
|
259
322
|
const message = requiredText(input.message, "message", MAX_MESSAGE_LENGTH);
|
|
323
|
+
const nextModel = modelProvided ? requestedModel ?? null : session.execution.model;
|
|
324
|
+
const nextReasoningEffort = reasoningEffortProvided
|
|
325
|
+
? requestedReasoningEffort ?? null
|
|
326
|
+
: session.execution.reasoningEffort;
|
|
327
|
+
if (nextModel !== session.execution.model ||
|
|
328
|
+
nextReasoningEffort !== session.execution.reasoningEffort) {
|
|
329
|
+
await this.services.sessionExecution.update({
|
|
330
|
+
sessionId,
|
|
331
|
+
model: nextModel,
|
|
332
|
+
reasoningEffort: nextReasoningEffort,
|
|
333
|
+
});
|
|
334
|
+
session = this.ownedSession(sessionId);
|
|
335
|
+
}
|
|
260
336
|
const abort = new AbortController();
|
|
261
337
|
const cancel = () => abort.abort(signal?.reason);
|
|
262
338
|
if (signal?.aborted) {
|
|
@@ -411,6 +487,10 @@ export class PluginHostRpc {
|
|
|
411
487
|
async interruptRun(params) {
|
|
412
488
|
const input = record(params);
|
|
413
489
|
const run = this.activeRunByIdentity(input);
|
|
490
|
+
const forceAfterMs = optionalPositiveNumber(input.forceAfterMs, "forceAfterMs");
|
|
491
|
+
if (forceAfterMs !== undefined && forceAfterMs > 30_000) {
|
|
492
|
+
throw new PluginHostCallError("INVALID_PARAMS", "forceAfterMs must not exceed 30000");
|
|
493
|
+
}
|
|
414
494
|
if (run.interruptResult) {
|
|
415
495
|
return { interrupted: await run.interruptResult };
|
|
416
496
|
}
|
|
@@ -418,7 +498,7 @@ export class PluginHostRpc {
|
|
|
418
498
|
// the authoritative stop is pending wait for its result: true suppresses
|
|
419
499
|
// them, while false/error lets genuine native progress/terminal continue.
|
|
420
500
|
run.interruptRequested = true;
|
|
421
|
-
const
|
|
501
|
+
const graceful = this.services.interruptSession
|
|
422
502
|
// ConversationRuntime wires AbortSignal to this same native operation.
|
|
423
503
|
// Invoke it directly once so this boolean is the authoritative result;
|
|
424
504
|
// aborting first would trigger a duplicate, unobserved interrupt.
|
|
@@ -429,6 +509,9 @@ export class PluginHostRpc {
|
|
|
429
509
|
// effort and cannot prove that native execution stopped.
|
|
430
510
|
return false;
|
|
431
511
|
});
|
|
512
|
+
const interruptResult = forceAfterMs === undefined
|
|
513
|
+
? graceful
|
|
514
|
+
: forceStoppedInterrupt(graceful, forceAfterMs, () => this.services.terminateSession?.(run.sessionId) ?? Promise.resolve(false));
|
|
432
515
|
run.interruptResult = interruptResult;
|
|
433
516
|
try {
|
|
434
517
|
const interrupted = await interruptResult;
|
|
@@ -510,6 +593,9 @@ export class PluginHostRpc {
|
|
|
510
593
|
return await resolve(sessionId, interactionId, resolution);
|
|
511
594
|
}
|
|
512
595
|
async forkSession(params) {
|
|
596
|
+
return this.withAdmission(() => this.forkSessionAdmitted(params));
|
|
597
|
+
}
|
|
598
|
+
async forkSessionAdmitted(params) {
|
|
513
599
|
const input = record(params);
|
|
514
600
|
const sourceSessionId = requiredString(input.sourceSessionId, "sourceSessionId", 512);
|
|
515
601
|
const operationId = requiredString(input.operationId, "operationId", 512);
|
|
@@ -524,6 +610,24 @@ export class PluginHostRpc {
|
|
|
524
610
|
...(title ? { title } : {}),
|
|
525
611
|
});
|
|
526
612
|
}
|
|
613
|
+
reserveAdmission() {
|
|
614
|
+
const reservation = this.services.admissionReserve?.();
|
|
615
|
+
if (reservation)
|
|
616
|
+
return reservation;
|
|
617
|
+
if (!this.services.admissionReserve && this.services.admissionOpen?.() !== false) {
|
|
618
|
+
return { release() { } };
|
|
619
|
+
}
|
|
620
|
+
throw new PluginHostCallError("HOST_CALL_FAILED", "daemon maintenance is in progress", true);
|
|
621
|
+
}
|
|
622
|
+
async withAdmission(operation) {
|
|
623
|
+
const reservation = this.reserveAdmission();
|
|
624
|
+
try {
|
|
625
|
+
return await operation();
|
|
626
|
+
}
|
|
627
|
+
finally {
|
|
628
|
+
reservation.release();
|
|
629
|
+
}
|
|
630
|
+
}
|
|
527
631
|
setSessionTitle(params) {
|
|
528
632
|
const input = record(params);
|
|
529
633
|
const sessionId = requiredSessionId(input);
|
|
@@ -551,6 +655,17 @@ export class PluginHostRpc {
|
|
|
551
655
|
...(ttlSeconds === undefined ? {} : { ttlMs: ttlSeconds * 1_000 }),
|
|
552
656
|
});
|
|
553
657
|
}
|
|
658
|
+
issueSessionPortalLink(scope, access, sessionId) {
|
|
659
|
+
if (!this.services.sessionPortal) {
|
|
660
|
+
throw new PluginHostCallError("HOST_CALL_FAILED", "the Session Portal is not configured");
|
|
661
|
+
}
|
|
662
|
+
return issueSessionPortalTicket({
|
|
663
|
+
pluginId: this.principalId,
|
|
664
|
+
scope,
|
|
665
|
+
access,
|
|
666
|
+
...(sessionId ? { sessionId } : {}),
|
|
667
|
+
});
|
|
668
|
+
}
|
|
554
669
|
ownedSession(sessionId) {
|
|
555
670
|
const session = this.services.sessionRegistry.get(sessionId);
|
|
556
671
|
if (!session || !this.isOwned(session)) {
|
|
@@ -612,14 +727,18 @@ function sessionLaunchSelection(value) {
|
|
|
612
727
|
assertJsonValue(input, "session");
|
|
613
728
|
const projectId = optionalString(input.projectId, "session.projectId", 512);
|
|
614
729
|
const managedCwd = optionalString(input.managedCwd, "session.managedCwd", 16_384);
|
|
730
|
+
const workspace = optionalSessionWorkspace(input.workspace);
|
|
615
731
|
const agent = optionalAgentId(input.agent, "session.agent");
|
|
616
732
|
const config = input.config === undefined ? undefined : agentSpecSchema.parse(input.config);
|
|
617
733
|
const provider = optionalRuntimeValue(input.provider);
|
|
618
|
-
const
|
|
734
|
+
const modelProvided = input.model !== undefined;
|
|
735
|
+
const model = input.model === null || input.model === ""
|
|
736
|
+
? null
|
|
737
|
+
: optionalString(input.model, "session.model", 256);
|
|
619
738
|
const reasoningEffort = optionalString(input.reasoningEffort, "session.reasoningEffort", 128);
|
|
620
739
|
const title = optionalString(input.title, "session.title", 1_000);
|
|
621
|
-
if (Number(Boolean(projectId)) + Number(Boolean(managedCwd)) > 1) {
|
|
622
|
-
throw new PluginHostCallError("INVALID_PARAMS", "session.projectId and session.
|
|
740
|
+
if (Number(Boolean(projectId)) + Number(Boolean(managedCwd)) + Number(Boolean(workspace)) > 1) {
|
|
741
|
+
throw new PluginHostCallError("INVALID_PARAMS", "session.projectId, session.managedCwd, and session.workspace are mutually exclusive");
|
|
623
742
|
}
|
|
624
743
|
if (Number(Boolean(agent)) +
|
|
625
744
|
Number(Boolean(config)) +
|
|
@@ -630,14 +749,26 @@ function sessionLaunchSelection(value) {
|
|
|
630
749
|
return {
|
|
631
750
|
...(projectId ? { projectId } : {}),
|
|
632
751
|
...(managedCwd ? { managedCwd } : {}),
|
|
752
|
+
...(workspace ? { workspace } : {}),
|
|
633
753
|
...(agent ? { agent } : {}),
|
|
634
754
|
...(config ? { config } : {}),
|
|
635
755
|
...(provider ? { provider } : {}),
|
|
636
|
-
...(
|
|
756
|
+
...(modelProvided ? { model: model ?? null } : {}),
|
|
637
757
|
...(reasoningEffort ? { reasoningEffort } : {}),
|
|
638
758
|
...(title ? { title } : {}),
|
|
639
759
|
};
|
|
640
760
|
}
|
|
761
|
+
function optionalSessionWorkspace(value) {
|
|
762
|
+
if (value === undefined)
|
|
763
|
+
return undefined;
|
|
764
|
+
const input = record(value);
|
|
765
|
+
const cwd = requiredPathString(input.cwd, "session.workspace.cwd", 16_384);
|
|
766
|
+
if (!Array.isArray(input.allowedDirs) || input.allowedDirs.length === 0) {
|
|
767
|
+
throw new PluginHostCallError("INVALID_PARAMS", "session.workspace.allowedDirs must be a non-empty string array");
|
|
768
|
+
}
|
|
769
|
+
const allowedDirs = input.allowedDirs.map((directory, index) => requiredPathString(directory, `session.workspace.allowedDirs[${index}]`, 16_384));
|
|
770
|
+
return { cwd, allowedDirs };
|
|
771
|
+
}
|
|
641
772
|
function sessionExecutionSnapshot(value) {
|
|
642
773
|
const input = record(value);
|
|
643
774
|
const runtime = optionalRuntimeValue(input.runtime);
|
|
@@ -654,6 +785,12 @@ function sessionExecutionSnapshot(value) {
|
|
|
654
785
|
function requiredSessionId(params) {
|
|
655
786
|
return requiredString(record(params).sessionId, "sessionId", 512);
|
|
656
787
|
}
|
|
788
|
+
function requiredSessionPortalAccess(params) {
|
|
789
|
+
const access = record(params).access;
|
|
790
|
+
if (access === "read_only" || access === "read_write")
|
|
791
|
+
return access;
|
|
792
|
+
throw new PluginHostCallError("INVALID_PARAMS", "access must be read_only or read_write");
|
|
793
|
+
}
|
|
657
794
|
function sessionInteractionResolution(value) {
|
|
658
795
|
const input = record(value);
|
|
659
796
|
const keys = Object.keys(input);
|
|
@@ -718,6 +855,15 @@ function requiredString(value, label, maxLength) {
|
|
|
718
855
|
}
|
|
719
856
|
return value.trim();
|
|
720
857
|
}
|
|
858
|
+
function requiredPathString(value, label, maxLength) {
|
|
859
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
860
|
+
throw new PluginHostCallError("INVALID_PARAMS", `${label} is required`);
|
|
861
|
+
}
|
|
862
|
+
if (value.length > maxLength) {
|
|
863
|
+
throw new PluginHostCallError("INVALID_PARAMS", `${label} is too long`);
|
|
864
|
+
}
|
|
865
|
+
return value;
|
|
866
|
+
}
|
|
721
867
|
function requiredText(value, label, maxLength) {
|
|
722
868
|
if (typeof value !== "string" || !value.trim()) {
|
|
723
869
|
throw new PluginHostCallError("INVALID_PARAMS", `${label} is required`);
|
|
@@ -758,6 +904,23 @@ function optionalPositiveNumber(value, label) {
|
|
|
758
904
|
}
|
|
759
905
|
return value;
|
|
760
906
|
}
|
|
907
|
+
async function forceStoppedInterrupt(graceful, forceAfterMs, terminate) {
|
|
908
|
+
let timer;
|
|
909
|
+
const graceExpired = new Promise((resolve) => {
|
|
910
|
+
timer = setTimeout(() => resolve(false), forceAfterMs);
|
|
911
|
+
timer.unref?.();
|
|
912
|
+
});
|
|
913
|
+
try {
|
|
914
|
+
if (await Promise.race([graceful.catch(() => false), graceExpired])) {
|
|
915
|
+
return true;
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
finally {
|
|
919
|
+
if (timer)
|
|
920
|
+
clearTimeout(timer);
|
|
921
|
+
}
|
|
922
|
+
return terminate();
|
|
923
|
+
}
|
|
761
924
|
function assertJsonValue(value, label, seen = new Set()) {
|
|
762
925
|
if (value === null || typeof value === "string" || typeof value === "boolean")
|
|
763
926
|
return;
|