@rynx-ai/daemon 0.1.10 → 0.1.11-beta.10

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.
Files changed (156) hide show
  1. package/bundled-plugins/.rynx-plugin/marketplace.json +10 -0
  2. package/bundled-plugins/plugins/lark/.rynx-plugin/plugin.json +25 -0
  3. package/bundled-plugins/plugins/lark/dist/cli-mirror/mirror-index.d.ts +48 -0
  4. package/bundled-plugins/plugins/lark/dist/cli-mirror/mirror-index.js +56 -0
  5. package/bundled-plugins/plugins/lark/dist/cli-mirror/rollout-mirror.d.ts +85 -0
  6. package/bundled-plugins/plugins/lark/dist/cli-mirror/rollout-mirror.js +333 -0
  7. package/bundled-plugins/plugins/lark/dist/cli-mirror/rollout-watcher.d.ts +109 -0
  8. package/bundled-plugins/plugins/lark/dist/cli-mirror/rollout-watcher.js +347 -0
  9. package/bundled-plugins/plugins/lark/dist/host-adapters.d.ts +95 -0
  10. package/bundled-plugins/plugins/lark/dist/host-adapters.js +506 -0
  11. package/bundled-plugins/plugins/lark/dist/index.d.ts +7 -0
  12. package/bundled-plugins/plugins/lark/dist/index.js +7 -0
  13. package/bundled-plugins/plugins/lark/dist/lark-card-stream.d.ts +363 -0
  14. package/bundled-plugins/plugins/lark/dist/lark-card-stream.js +1335 -0
  15. package/bundled-plugins/plugins/lark/dist/lark-config-store.d.ts +30 -0
  16. package/bundled-plugins/plugins/lark/dist/lark-config-store.js +135 -0
  17. package/bundled-plugins/plugins/lark/dist/lark-content.d.ts +30 -0
  18. package/bundled-plugins/plugins/lark/dist/lark-content.js +138 -0
  19. package/bundled-plugins/plugins/lark/dist/lark-conversation-directory.d.ts +65 -0
  20. package/bundled-plugins/plugins/lark/dist/lark-conversation-directory.js +106 -0
  21. package/bundled-plugins/plugins/lark/dist/lark-diff-card.d.ts +25 -0
  22. package/bundled-plugins/plugins/lark/dist/lark-diff-card.js +58 -0
  23. package/bundled-plugins/plugins/lark/dist/lark-fork-transcript.d.ts +16 -0
  24. package/bundled-plugins/plugins/lark/dist/lark-fork-transcript.js +141 -0
  25. package/bundled-plugins/plugins/lark/dist/lark-resume-card.d.ts +31 -0
  26. package/bundled-plugins/plugins/lark/dist/lark-resume-card.js +105 -0
  27. package/bundled-plugins/plugins/lark/dist/lark-runtime-state-store.d.ts +18 -0
  28. package/bundled-plugins/plugins/lark/dist/lark-runtime-state-store.js +95 -0
  29. package/bundled-plugins/plugins/lark/dist/lark-sdk/client.d.ts +8 -0
  30. package/bundled-plugins/plugins/lark/dist/lark-sdk/client.js +32 -0
  31. package/bundled-plugins/plugins/lark/dist/lark-sdk/index.d.ts +1 -0
  32. package/bundled-plugins/plugins/lark/dist/lark-sdk/index.js +1 -0
  33. package/bundled-plugins/plugins/lark/dist/lark-sender.d.ts +114 -0
  34. package/bundled-plugins/plugins/lark/dist/lark-sender.js +323 -0
  35. package/bundled-plugins/plugins/lark/dist/lark-session-store.d.ts +13 -0
  36. package/bundled-plugins/plugins/lark/dist/lark-session-store.js +14 -0
  37. package/bundled-plugins/plugins/lark/dist/lark-settings-card.d.ts +55 -0
  38. package/bundled-plugins/plugins/lark/dist/lark-settings-card.js +158 -0
  39. package/bundled-plugins/plugins/lark/dist/lark-setup.d.ts +39 -0
  40. package/bundled-plugins/plugins/lark/dist/lark-setup.js +103 -0
  41. package/bundled-plugins/plugins/lark/dist/lark.d.ts +507 -0
  42. package/bundled-plugins/plugins/lark/dist/lark.js +1975 -0
  43. package/bundled-plugins/plugins/lark/dist/runtime.d.ts +13 -0
  44. package/bundled-plugins/plugins/lark/dist/runtime.js +131328 -0
  45. package/bundled-plugins/plugins/lark/dist/settings.d.ts +7 -0
  46. package/bundled-plugins/plugins/lark/dist/settings.js +126971 -0
  47. package/bundled-plugins/plugins/lark/package.json +57 -0
  48. package/bundled-plugins/plugins/lark/ui/assets/index-CGUVAMsv.css +1 -0
  49. package/bundled-plugins/plugins/lark/ui/assets/index-D8Amoe6a.js +201 -0
  50. package/bundled-plugins/plugins/lark/ui/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
  51. package/bundled-plugins/plugins/lark/ui/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
  52. package/bundled-plugins/plugins/lark/ui/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
  53. package/bundled-plugins/plugins/lark/ui/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
  54. package/bundled-plugins/plugins/lark/ui/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
  55. package/bundled-plugins/plugins/lark/ui/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
  56. package/bundled-plugins/plugins/lark/ui/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
  57. package/bundled-plugins/plugins/lark/ui/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
  58. package/bundled-plugins/plugins/lark/ui/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
  59. package/bundled-plugins/plugins/lark/ui/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
  60. package/bundled-plugins/plugins/lark/ui/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
  61. package/bundled-plugins/plugins/lark/ui/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
  62. package/bundled-plugins/plugins/lark/ui/index.html +13 -0
  63. package/dist/agent-file.js +0 -1
  64. package/dist/browser-artifact-management.d.ts +4 -1
  65. package/dist/browser-artifact-management.js +18 -10
  66. package/dist/browser-artifacts.d.ts +1 -0
  67. package/dist/browser-artifacts.js +1 -0
  68. package/dist/bundled-plugins.d.ts +15 -0
  69. package/dist/bundled-plugins.js +134 -0
  70. package/dist/chrome-for-testing-runtime.d.ts +27 -0
  71. package/dist/chrome-for-testing-runtime.js +193 -0
  72. package/dist/chrome-for-testing-store.d.ts +2 -0
  73. package/dist/chrome-for-testing-store.js +9 -0
  74. package/dist/control-deps.d.ts +6 -2
  75. package/dist/control-deps.js +88 -82
  76. package/dist/daemon-server.d.ts +7 -1
  77. package/dist/daemon-server.js +310 -87
  78. package/dist/db.js +181 -124
  79. package/dist/entry-path.d.ts +3 -0
  80. package/dist/entry-path.js +12 -0
  81. package/dist/index-daemon.js +3 -66
  82. package/dist/lifecycle.d.ts +12 -0
  83. package/dist/lifecycle.js +16 -0
  84. package/dist/maintenance-lease.d.ts +39 -0
  85. package/dist/maintenance-lease.js +250 -0
  86. package/dist/migrations/cleanup-legacy-sessions.d.ts +1 -1
  87. package/dist/migrations/cleanup-legacy-sessions.js +33 -15
  88. package/dist/official-plugins.d.ts +2 -0
  89. package/dist/official-plugins.js +2 -0
  90. package/dist/plugin-cli.d.ts +1 -3
  91. package/dist/plugin-cli.js +56 -45
  92. package/dist/plugin-host-leases.d.ts +4 -0
  93. package/dist/plugin-host-leases.js +7 -0
  94. package/dist/plugin-host-rpc.d.ts +73 -6
  95. package/dist/plugin-host-rpc.js +354 -81
  96. package/dist/plugin-installer.d.ts +24 -55
  97. package/dist/plugin-installer.js +258 -412
  98. package/dist/plugin-management-service.d.ts +30 -23
  99. package/dist/plugin-management-service.js +288 -105
  100. package/dist/plugin-marketplace.d.ts +35 -0
  101. package/dist/plugin-marketplace.js +299 -0
  102. package/dist/plugin-package.d.ts +22 -24
  103. package/dist/plugin-package.js +520 -571
  104. package/dist/plugin-skill-context.d.ts +4 -0
  105. package/dist/plugin-skill-context.js +35 -0
  106. package/dist/plugin-store.d.ts +36 -37
  107. package/dist/plugin-store.js +138 -92
  108. package/dist/plugin-supervisor.d.ts +50 -125
  109. package/dist/plugin-supervisor.js +678 -1030
  110. package/dist/pm2.d.ts +11 -2
  111. package/dist/pm2.js +130 -28
  112. package/dist/process.d.ts +12 -0
  113. package/dist/process.js +91 -0
  114. package/dist/runtime-project-store.d.ts +9 -0
  115. package/dist/runtime-project-store.js +83 -0
  116. package/dist/session-fallback-workspace.d.ts +22 -0
  117. package/dist/session-fallback-workspace.js +120 -0
  118. package/dist/session-fork-store.d.ts +41 -0
  119. package/dist/session-fork-store.js +288 -0
  120. package/dist/session-launch-operations.d.ts +23 -2
  121. package/dist/session-launch-operations.js +48 -7
  122. package/dist/session-meta-store.d.ts +12 -9
  123. package/dist/session-meta-store.js +50 -49
  124. package/dist/session-portal-grant-store.d.ts +29 -0
  125. package/dist/session-portal-grant-store.js +84 -0
  126. package/dist/setup-service.d.ts +21 -0
  127. package/dist/setup-service.js +39 -0
  128. package/dist/skills-catalog.d.ts +11 -2
  129. package/dist/skills-catalog.js +307 -7
  130. package/dist/system-dependencies.d.ts +26 -0
  131. package/dist/system-dependencies.js +149 -0
  132. package/package.json +36 -20
  133. package/dist/browser-cli-args.d.ts +0 -20
  134. package/dist/browser-cli-args.js +0 -100
  135. package/dist/channel-store.d.ts +0 -22
  136. package/dist/channel-store.js +0 -65
  137. package/dist/cli.d.ts +0 -2
  138. package/dist/cli.js +0 -732
  139. package/dist/control-client.d.ts +0 -122
  140. package/dist/control-client.js +0 -855
  141. package/dist/control-endpoint.d.ts +0 -14
  142. package/dist/control-endpoint.js +0 -124
  143. package/dist/desktop-browser-host-client.d.ts +0 -35
  144. package/dist/desktop-browser-host-client.js +0 -415
  145. package/dist/instance-store.d.ts +0 -38
  146. package/dist/instance-store.js +0 -85
  147. package/dist/plugin-install-restart.d.ts +0 -13
  148. package/dist/plugin-install-restart.js +0 -24
  149. package/dist/plugin-runtime-control.d.ts +0 -2
  150. package/dist/plugin-runtime-control.js +0 -37
  151. package/dist/registry.d.ts +0 -10
  152. package/dist/registry.js +0 -52
  153. package/dist/setup.d.ts +0 -20
  154. package/dist/setup.js +0 -374
  155. package/dist/update.d.ts +0 -25
  156. package/dist/update.js +0 -133
@@ -1,9 +1,12 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
- import { AgentRuntimeError, agentSpecSchema, SteerController, } from "@rynx-ai/core";
2
+ import { realpath, stat } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { AgentRuntimeError, agentSpecSchema, resolvedExecutionSnapshotSchema, sessionWorkspaceSnapshotSchema, SteerController, } from "@rynx-ai/core";
3
5
  import { PLUGIN_HOST_MAX_ACTIVE_RUNS, } from "@rynx-ai/plugin-sdk";
4
6
  import { PluginRunnerError } from "@rynx-ai/plugin-runner";
5
- import { ensurePluginSessionLaunch, SessionLaunchOperationConflictError, SessionLaunchOperationTargetDeletedError, } from "./session-launch-operations.js";
7
+ import { ensurePluginSessionLaunch, replayPluginSessionLaunch, SessionLaunchOperationConflictError, SessionLaunchOperationTargetDeletedError, } from "./session-launch-operations.js";
6
8
  import { createControlLink } from "./control-link-store.js";
9
+ import { issueSessionPortalTicket } from "./session-portal-grant-store.js";
7
10
  const MAX_MESSAGE_LENGTH = 1_000_000;
8
11
  /** Leaves ample room for the RPC envelope below the runner's 1 MiB line cap. */
9
12
  export const MAX_PLUGIN_HOST_EVENT_BYTES = 512 * 1024;
@@ -11,31 +14,8 @@ const MAX_PLUGIN_RUN_FINAL_RESPONSE_BYTES = 64 * 1024;
11
14
  const MAX_DELTA_CHUNK_CODE_UNITS = 64 * 1024;
12
15
  const SESSION_INTERACTION_RESOLUTION_MAX_BYTES = 128 * 1024;
13
16
  const AGENT_ID_PATTERN = /^[a-z0-9][a-z0-9-]{0,63}$/;
14
- const METHOD_CAPABILITY = {
15
- "installation.proof.sign": "installation.proof",
16
- "agent.catalog.list": "agent.catalog.read",
17
- "agent.catalog.get": "agent.catalog.read",
18
- "agent.execution.resolve": "agent.catalog.read",
19
- "session.ensure": "session.provision",
20
- "session.run": "conversation.run",
21
- "session.steer": "conversation.run",
22
- "session.interrupt": "session.cancel",
23
- "session.interaction.resolve": "conversation.run",
24
- "session.models": "runtime.catalog.read",
25
- "session.binding.get": "session.binding.read",
26
- "session.binding.list": "session.binding.read",
27
- "session.binding.find": "session.binding.read",
28
- "session.goal.get": "session.goal.read",
29
- "session.goal.set": "session.goal.write",
30
- "session.goal.clear": "session.goal.write",
31
- "session.fork": "session.fork",
32
- "session.meta.get": "session.meta.read",
33
- "session.meta.list": "session.meta.read",
34
- "session.meta.setTitle": "session.meta.title.write",
35
- "control.link.issue": "control-link.issue",
36
- };
37
17
  /**
38
- * One authenticated plugin's capability-gated Host RPC surface.
18
+ * One authenticated plugin's Host RPC surface.
39
19
  *
40
20
  * This adapter is deliberately the only place that maps public string methods
41
21
  * to trusted runtime objects. The runner receives neither this class nor its
@@ -43,43 +23,53 @@ const METHOD_CAPABILITY = {
43
23
  */
44
24
  export class PluginHostRpc {
45
25
  pluginId;
26
+ principalId;
46
27
  daemonInstanceId;
47
- grants;
28
+ productVersion;
29
+ cliVersion;
48
30
  services;
31
+ managedWorkspaceRoot;
49
32
  emit;
33
+ disposeForceAfterMs;
50
34
  activeRuns = new Map();
51
35
  activeSessionRuns = new Map();
52
36
  activeRunsByOperation = new Map();
53
37
  disposed = false;
38
+ disposePromise;
54
39
  constructor(options) {
55
40
  this.pluginId = requiredString(options.pluginId, "pluginId", 64);
41
+ this.principalId = requiredString(options.principalId ?? options.pluginId, "principalId", 160);
56
42
  this.daemonInstanceId = requiredString(options.daemonInstanceId, "daemonInstanceId", 128);
57
- this.grants = new Set(options.grants);
43
+ this.productVersion = requiredString(options.productVersion, "productVersion", 128);
44
+ this.cliVersion = requiredString(options.cliVersion, "cliVersion", 128);
58
45
  this.services = options.services;
46
+ this.managedWorkspaceRoot = options.managedWorkspaceRoot
47
+ ? path.resolve(options.managedWorkspaceRoot)
48
+ : undefined;
59
49
  this.emit = options.emit;
50
+ this.disposeForceAfterMs = options.disposeForceAfterMs ?? 10_000;
60
51
  }
61
- async call(method, params) {
52
+ async call(method, params, signal) {
62
53
  if (this.disposed)
63
54
  throw new PluginHostCallError("HOST_CLOSED", "plugin host is closed");
55
+ throwIfHostCallAborted(signal);
64
56
  if (method === "host.info") {
65
57
  return {
66
58
  pluginId: this.pluginId,
67
59
  installationId: this.services.installationId,
68
60
  daemonInstanceId: this.daemonInstanceId,
61
+ productVersion: this.productVersion,
62
+ cliVersion: this.cliVersion,
69
63
  installationProof: {
70
64
  algorithm: this.services.installationProof.algorithm,
71
65
  publicKey: this.services.installationProof.publicKey,
72
66
  },
73
67
  protocolVersion: 1,
74
- grants: [...this.grants],
68
+ ...(this.services.sessionPortal
69
+ ? { sessionPortal: { ...this.services.sessionPortal } }
70
+ : {}),
75
71
  };
76
72
  }
77
- const capability = METHOD_CAPABILITY[method];
78
- if (!capability)
79
- throw new PluginHostCallError("METHOD_NOT_FOUND", `unknown host method: ${method}`);
80
- if (!this.grants.has(capability)) {
81
- throw new PluginHostCallError("CAPABILITY_DENIED", `host method ${method} requires capability ${capability}`);
82
- }
83
73
  try {
84
74
  switch (method) {
85
75
  case "installation.proof.sign": {
@@ -87,7 +77,7 @@ export class PluginHostRpc {
87
77
  return {
88
78
  algorithm: this.services.installationProof.algorithm,
89
79
  publicKey: this.services.installationProof.publicKey,
90
- signature: this.services.installationProof.sign(this.pluginId, challenge),
80
+ signature: this.services.installationProof.sign(this.principalId, challenge),
91
81
  };
92
82
  }
93
83
  case "agent.catalog.list":
@@ -97,9 +87,9 @@ export class PluginHostRpc {
97
87
  case "agent.execution.resolve":
98
88
  return await this.resolveAgentExecution(params);
99
89
  case "session.ensure":
100
- return this.ensureSession(params);
90
+ return await this.ensureSession(params);
101
91
  case "session.run":
102
- return await this.runSession(params);
92
+ return await this.runSession(params, signal);
103
93
  case "session.steer":
104
94
  return this.steerSession(params);
105
95
  case "session.interrupt":
@@ -133,6 +123,16 @@ export class PluginHostRpc {
133
123
  return this.setSessionTitle(params);
134
124
  case "control.link.issue":
135
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
+ }
136
136
  default:
137
137
  throw new PluginHostCallError("METHOD_NOT_FOUND", `unknown host method: ${method}`);
138
138
  }
@@ -142,44 +142,174 @@ export class PluginHostRpc {
142
142
  }
143
143
  }
144
144
  async dispose() {
145
- if (this.disposed)
146
- return;
145
+ if (this.disposePromise)
146
+ return this.disposePromise;
147
147
  this.disposed = true;
148
148
  const runs = [...this.activeRuns.values()];
149
- this.activeRuns.clear();
150
- this.activeSessionRuns.clear();
151
- this.activeRunsByOperation.clear();
152
- for (const run of runs) {
153
- run.interruptRequested = true;
154
- if (run.interruptResult && await run.interruptResult.catch(() => false))
155
- continue;
156
- if (this.services.interruptSession) {
157
- await this.services.interruptSession(run.sessionId).catch(() => false);
158
- }
159
- else {
160
- run.abort.abort();
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");
161
172
  }
162
- }
173
+ })();
174
+ return this.disposePromise;
175
+ }
176
+ async ensureSession(params) {
177
+ return this.withAdmission(() => this.ensureSessionAdmitted(params));
163
178
  }
164
- ensureSession(params) {
179
+ async ensureSessionAdmitted(params) {
165
180
  const input = record(params);
166
181
  const scopeId = requiredString(input.scopeId, "scopeId", 512);
167
182
  const operationId = requiredString(input.operationId, "operationId", 512);
168
- const session = sessionLaunchMeta(input.session);
169
- const requestHash = hashCanonicalJson(session);
170
- return ensurePluginSessionLaunch({
171
- pluginId: this.pluginId,
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);
197
+ const selection = parsedSelection.managedCwd
198
+ ? {
199
+ ...parsedSelection,
200
+ managedCwd: await this.resolveManagedWorkspace(parsedSelection.managedCwd),
201
+ }
202
+ : parsedSelection.workspace
203
+ ? {
204
+ ...parsedSelection,
205
+ workspace: await this.resolveWorkspaceSnapshot(parsedSelection.workspace),
206
+ }
207
+ : parsedSelection;
208
+ const requestHash = hashCanonicalJson(selection);
209
+ const canonicalReplay = replayPluginSessionLaunch({
210
+ pluginId: this.principalId,
172
211
  scopeId,
173
212
  operationId,
174
213
  requestHash,
175
- session,
176
214
  });
215
+ if (canonicalReplay)
216
+ return this.sessionEnsureResult(canonicalReplay);
217
+ const resolved = await this.services.sessionLaunch.resolve(selection);
218
+ const ensured = ensurePluginSessionLaunch({
219
+ pluginId: this.principalId,
220
+ scopeId,
221
+ operationId,
222
+ requestHash,
223
+ rawRequestHash,
224
+ session: {
225
+ workspace: sessionWorkspaceSnapshotSchema.parse(resolved.workspace),
226
+ execution: resolvedExecutionSnapshotSchema.parse(resolved.execution),
227
+ ...(selection.title ? { title: selection.title } : {}),
228
+ },
229
+ });
230
+ return this.sessionEnsureResult(ensured);
231
+ }
232
+ async resolveManagedWorkspace(directory) {
233
+ const root = this.managedWorkspaceRoot;
234
+ if (!root) {
235
+ throw new PluginHostCallError("INVALID_PARAMS", "session.managedCwd is reserved for the built-in Workspace plugin");
236
+ }
237
+ if (!path.isAbsolute(directory)) {
238
+ throw new PluginHostCallError("INVALID_PARAMS", "session.managedCwd must be absolute");
239
+ }
240
+ let canonicalRoot;
241
+ let canonicalDirectory;
242
+ try {
243
+ [canonicalRoot, canonicalDirectory] = await Promise.all([
244
+ realpath(root),
245
+ realpath(directory),
246
+ ]);
247
+ }
248
+ catch {
249
+ throw new PluginHostCallError("INVALID_PARAMS", "session.managedCwd must be an existing plugin-managed directory");
250
+ }
251
+ const relative = path.relative(canonicalRoot, canonicalDirectory);
252
+ if (relative === ".." ||
253
+ relative.startsWith(`..${path.sep}`) ||
254
+ path.isAbsolute(relative)) {
255
+ throw new PluginHostCallError("INVALID_PARAMS", "session.managedCwd escapes the plugin-managed workspace root");
256
+ }
257
+ return canonicalDirectory;
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
+ }
286
+ sessionEnsureResult(result) {
287
+ const session = this.ownedSession(result.sessionId);
288
+ return {
289
+ ...result,
290
+ execution: {
291
+ provider: session.execution.provider,
292
+ model: session.execution.model,
293
+ reasoningEffort: session.execution.reasoningEffort,
294
+ },
295
+ };
296
+ }
297
+ async runSession(params, signal) {
298
+ return this.withAdmission(() => this.runSessionAdmitted(params, signal));
177
299
  }
178
- async runSession(params) {
300
+ async runSessionAdmitted(params, signal) {
179
301
  const input = record(params);
180
302
  const sessionId = requiredSessionId(input);
181
303
  const operationId = optionalString(input.operationId, "operationId", 512);
182
- const session = this.ownedSession(sessionId);
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);
183
313
  if (operationId && this.activeRunsByOperation.has(operationId)) {
184
314
  throw new PluginHostCallError("RUN_ALREADY_ACTIVE", `operation already has an active run: ${operationId}`, true);
185
315
  }
@@ -190,7 +320,26 @@ export class PluginHostRpc {
190
320
  throw new PluginHostCallError("RUN_LIMIT", "plugin has too many active session runs", true);
191
321
  }
192
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
+ }
193
336
  const abort = new AbortController();
337
+ const cancel = () => abort.abort(signal?.reason);
338
+ if (signal?.aborted) {
339
+ cancel();
340
+ throwIfHostCallAborted(signal);
341
+ }
342
+ signal?.addEventListener("abort", cancel, { once: true });
194
343
  const steer = new SteerController();
195
344
  const runId = randomUUID();
196
345
  const run = {
@@ -210,23 +359,23 @@ export class PluginHostRpc {
210
359
  started = await this.services.conversationRuntime.runLive({
211
360
  message,
212
361
  threadId: sessionId,
213
- provider: optionalRuntimeValue(input.provider),
214
- model: optionalString(input.model, "model", 256) ?? session.model,
215
- reasoningEffort: optionalString(input.reasoningEffort, "reasoningEffort", 128) ?? session.reasoningEffort,
216
- cwd: optionalString(input.cwd, "cwd", 16_384),
217
- agentName: optionalAgentId(input.agent, "agent") ?? session.agent,
218
- agentSpec: session.config,
362
+ workspace: session.workspace,
363
+ execution: session.execution,
219
364
  signal: abort.signal,
220
365
  steer,
221
- developerInstructions: optionalString(input.developerInstructions, "developerInstructions", 200_000),
222
366
  });
223
367
  }
224
368
  catch (error) {
225
369
  this.releaseRun(run);
226
370
  throw error;
227
371
  }
228
- if (this.disposed || this.activeRuns.get(runId) !== run) {
372
+ finally {
373
+ signal?.removeEventListener("abort", cancel);
374
+ }
375
+ if (signal?.aborted || this.disposed || this.activeRuns.get(runId) !== run) {
376
+ abort.abort(signal?.reason);
229
377
  this.releaseRun(run);
378
+ throwIfHostCallAborted(signal);
230
379
  throw new PluginHostCallError("HOST_CLOSED", "plugin host is closed");
231
380
  }
232
381
  // Let the RPC transport write the request response before stream events are
@@ -338,6 +487,10 @@ export class PluginHostRpc {
338
487
  async interruptRun(params) {
339
488
  const input = record(params);
340
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
+ }
341
494
  if (run.interruptResult) {
342
495
  return { interrupted: await run.interruptResult };
343
496
  }
@@ -345,7 +498,7 @@ export class PluginHostRpc {
345
498
  // the authoritative stop is pending wait for its result: true suppresses
346
499
  // them, while false/error lets genuine native progress/terminal continue.
347
500
  run.interruptRequested = true;
348
- const interruptResult = this.services.interruptSession
501
+ const graceful = this.services.interruptSession
349
502
  // ConversationRuntime wires AbortSignal to this same native operation.
350
503
  // Invoke it directly once so this boolean is the authoritative result;
351
504
  // aborting first would trigger a duplicate, unobserved interrupt.
@@ -356,6 +509,9 @@ export class PluginHostRpc {
356
509
  // effort and cannot prove that native execution stopped.
357
510
  return false;
358
511
  });
512
+ const interruptResult = forceAfterMs === undefined
513
+ ? graceful
514
+ : forceStoppedInterrupt(graceful, forceAfterMs, () => this.services.terminateSession?.(run.sessionId) ?? Promise.resolve(false));
359
515
  run.interruptResult = interruptResult;
360
516
  try {
361
517
  const interrupted = await interruptResult;
@@ -437,12 +593,40 @@ export class PluginHostRpc {
437
593
  return await resolve(sessionId, interactionId, resolution);
438
594
  }
439
595
  async forkSession(params) {
596
+ return this.withAdmission(() => this.forkSessionAdmitted(params));
597
+ }
598
+ async forkSessionAdmitted(params) {
440
599
  const input = record(params);
441
- const fromSessionId = requiredString(input.fromSessionId, "fromSessionId", 512);
442
- const toSessionId = requiredString(input.toSessionId, "toSessionId", 512);
443
- this.ownedSession(fromSessionId);
444
- this.ownedSession(toSessionId);
445
- return await this.services.capabilities.forkSession(fromSessionId, toSessionId);
600
+ const sourceSessionId = requiredString(input.sourceSessionId, "sourceSessionId", 512);
601
+ const operationId = requiredString(input.operationId, "operationId", 512);
602
+ const title = optionalString(input.title, "title", 1_000);
603
+ this.ownedSession(sourceSessionId);
604
+ if (!this.services.sessionFork) {
605
+ throw new PluginHostCallError("HOST_CALL_FAILED", "Session fork is unavailable");
606
+ }
607
+ return await this.services.sessionFork({
608
+ sourceSessionId,
609
+ operationId: `${this.principalId}:${operationId}`,
610
+ ...(title ? { title } : {}),
611
+ });
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
+ }
446
630
  }
447
631
  setSessionTitle(params) {
448
632
  const input = record(params);
@@ -466,11 +650,22 @@ export class PluginHostRpc {
466
650
  this.ownedSession(sessionId);
467
651
  const ttlSeconds = optionalPositiveNumber(input.ttlSeconds, "ttlSeconds");
468
652
  return createControlLink({
469
- pluginId: this.pluginId,
653
+ pluginId: this.principalId,
470
654
  sessionId,
471
655
  ...(ttlSeconds === undefined ? {} : { ttlMs: ttlSeconds * 1_000 }),
472
656
  });
473
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
+ }
474
669
  ownedSession(sessionId) {
475
670
  const session = this.services.sessionRegistry.get(sessionId);
476
671
  if (!session || !this.isOwned(session)) {
@@ -479,9 +674,24 @@ export class PluginHostRpc {
479
674
  return session;
480
675
  }
481
676
  isOwned(session) {
482
- return session.source === `plugin:${this.pluginId}`;
677
+ const seen = new Set();
678
+ let current = session;
679
+ while (current && !seen.has(current.id)) {
680
+ if (current.source === `plugin:${this.principalId}`)
681
+ return true;
682
+ seen.add(current.id);
683
+ current = current.forkedFromSessionId
684
+ ? this.services.sessionRegistry.get(current.forkedFromSessionId)
685
+ : undefined;
686
+ }
687
+ return false;
483
688
  }
484
689
  }
690
+ function throwIfHostCallAborted(signal) {
691
+ if (!signal?.aborted)
692
+ return;
693
+ throw new PluginHostCallError("REQUEST_CANCELLED", "plugin host request was cancelled");
694
+ }
485
695
  export class PluginHostCallError extends PluginRunnerError {
486
696
  retryable;
487
697
  data;
@@ -512,22 +722,53 @@ function toHostCallError(error) {
512
722
  }
513
723
  return new PluginHostCallError("HOST_CALL_FAILED", error instanceof Error ? error.message : String(error));
514
724
  }
515
- function sessionLaunchMeta(value) {
725
+ function sessionLaunchSelection(value) {
516
726
  const input = record(value);
517
727
  assertJsonValue(input, "session");
728
+ const projectId = optionalString(input.projectId, "session.projectId", 512);
729
+ const managedCwd = optionalString(input.managedCwd, "session.managedCwd", 16_384);
730
+ const workspace = optionalSessionWorkspace(input.workspace);
518
731
  const agent = optionalAgentId(input.agent, "session.agent");
519
732
  const config = input.config === undefined ? undefined : agentSpecSchema.parse(input.config);
520
- const model = optionalString(input.model, "session.model", 256);
733
+ const provider = optionalRuntimeValue(input.provider);
734
+ const modelProvided = input.model !== undefined;
735
+ const model = input.model === null || input.model === ""
736
+ ? null
737
+ : optionalString(input.model, "session.model", 256);
521
738
  const reasoningEffort = optionalString(input.reasoningEffort, "session.reasoningEffort", 128);
522
739
  const title = optionalString(input.title, "session.title", 1_000);
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");
742
+ }
743
+ if (Number(Boolean(agent)) +
744
+ Number(Boolean(config)) +
745
+ Number(Boolean(provider)) >
746
+ 1) {
747
+ throw new PluginHostCallError("INVALID_PARAMS", "session.agent, session.config, and session.provider are mutually exclusive");
748
+ }
523
749
  return {
750
+ ...(projectId ? { projectId } : {}),
751
+ ...(managedCwd ? { managedCwd } : {}),
752
+ ...(workspace ? { workspace } : {}),
524
753
  ...(agent ? { agent } : {}),
525
754
  ...(config ? { config } : {}),
526
- ...(model ? { model } : {}),
755
+ ...(provider ? { provider } : {}),
756
+ ...(modelProvided ? { model: model ?? null } : {}),
527
757
  ...(reasoningEffort ? { reasoningEffort } : {}),
528
758
  ...(title ? { title } : {}),
529
759
  };
530
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
+ }
531
772
  function sessionExecutionSnapshot(value) {
532
773
  const input = record(value);
533
774
  const runtime = optionalRuntimeValue(input.runtime);
@@ -544,6 +785,12 @@ function sessionExecutionSnapshot(value) {
544
785
  function requiredSessionId(params) {
545
786
  return requiredString(record(params).sessionId, "sessionId", 512);
546
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
+ }
547
794
  function sessionInteractionResolution(value) {
548
795
  const input = record(value);
549
796
  const keys = Object.keys(input);
@@ -608,6 +855,15 @@ function requiredString(value, label, maxLength) {
608
855
  }
609
856
  return value.trim();
610
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
+ }
611
867
  function requiredText(value, label, maxLength) {
612
868
  if (typeof value !== "string" || !value.trim()) {
613
869
  throw new PluginHostCallError("INVALID_PARAMS", `${label} is required`);
@@ -648,6 +904,23 @@ function optionalPositiveNumber(value, label) {
648
904
  }
649
905
  return value;
650
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
+ }
651
924
  function assertJsonValue(value, label, seen = new Set()) {
652
925
  if (value === null || typeof value === "string" || typeof value === "boolean")
653
926
  return;