@rynx-ai/daemon 0.1.11-beta.1 → 0.1.11-beta.3

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 (148) hide show
  1. package/bundled-plugins/.rynx-plugin/marketplace.json +10 -0
  2. package/bundled-plugins/plugins/lark/.rynx-plugin/plugin.json +37 -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 +54 -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 +504 -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 +1977 -0
  43. package/bundled-plugins/plugins/lark/dist/runtime.d.ts +13 -0
  44. package/bundled-plugins/plugins/lark/dist/runtime.js +131313 -0
  45. package/bundled-plugins/plugins/lark/dist/settings.d.ts +7 -0
  46. package/bundled-plugins/plugins/lark/dist/settings.js +126958 -0
  47. package/bundled-plugins/plugins/lark/package.json +57 -0
  48. package/bundled-plugins/plugins/lark/ui/assets/index-B2dhMlMU.css +1 -0
  49. package/bundled-plugins/plugins/lark/ui/assets/index-D9N-AkLX.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/browser-artifact-management.d.ts +4 -1
  64. package/dist/browser-artifact-management.js +18 -10
  65. package/dist/browser-artifacts.d.ts +1 -0
  66. package/dist/browser-artifacts.js +1 -0
  67. package/dist/bundled-plugins.d.ts +15 -0
  68. package/dist/bundled-plugins.js +134 -0
  69. package/dist/chrome-for-testing-runtime.d.ts +27 -0
  70. package/dist/chrome-for-testing-runtime.js +193 -0
  71. package/dist/chrome-for-testing-store.d.ts +2 -0
  72. package/dist/chrome-for-testing-store.js +9 -0
  73. package/dist/control-deps.js +2 -79
  74. package/dist/daemon-server.d.ts +7 -1
  75. package/dist/daemon-server.js +299 -84
  76. package/dist/db.js +147 -124
  77. package/dist/entry-path.d.ts +3 -0
  78. package/dist/entry-path.js +12 -0
  79. package/dist/index-daemon.js +3 -66
  80. package/dist/lifecycle.d.ts +12 -0
  81. package/dist/lifecycle.js +16 -0
  82. package/dist/maintenance-lease.d.ts +39 -0
  83. package/dist/maintenance-lease.js +250 -0
  84. package/dist/migrations/cleanup-legacy-sessions.d.ts +1 -1
  85. package/dist/migrations/cleanup-legacy-sessions.js +33 -15
  86. package/dist/official-plugins.d.ts +2 -0
  87. package/dist/official-plugins.js +2 -0
  88. package/dist/plugin-cli.d.ts +1 -3
  89. package/dist/plugin-cli.js +56 -45
  90. package/dist/plugin-host-leases.d.ts +4 -0
  91. package/dist/plugin-host-leases.js +7 -0
  92. package/dist/plugin-host-rpc.d.ts +60 -4
  93. package/dist/plugin-host-rpc.js +300 -50
  94. package/dist/plugin-installer.d.ts +24 -55
  95. package/dist/plugin-installer.js +258 -412
  96. package/dist/plugin-management-service.d.ts +30 -23
  97. package/dist/plugin-management-service.js +290 -105
  98. package/dist/plugin-marketplace.d.ts +35 -0
  99. package/dist/plugin-marketplace.js +299 -0
  100. package/dist/plugin-package.d.ts +22 -24
  101. package/dist/plugin-package.js +520 -571
  102. package/dist/plugin-skill-context.d.ts +4 -0
  103. package/dist/plugin-skill-context.js +35 -0
  104. package/dist/plugin-store.d.ts +36 -37
  105. package/dist/plugin-store.js +140 -92
  106. package/dist/plugin-supervisor.d.ts +50 -125
  107. package/dist/plugin-supervisor.js +682 -1030
  108. package/dist/pm2.d.ts +11 -2
  109. package/dist/pm2.js +130 -28
  110. package/dist/process.d.ts +12 -0
  111. package/dist/process.js +91 -0
  112. package/dist/runtime-project-store.d.ts +9 -0
  113. package/dist/runtime-project-store.js +83 -0
  114. package/dist/session-fallback-workspace.d.ts +22 -0
  115. package/dist/session-fallback-workspace.js +120 -0
  116. package/dist/session-fork-store.d.ts +41 -0
  117. package/dist/session-fork-store.js +288 -0
  118. package/dist/session-launch-operations.d.ts +23 -2
  119. package/dist/session-launch-operations.js +48 -7
  120. package/dist/session-meta-store.d.ts +7 -9
  121. package/dist/session-meta-store.js +28 -49
  122. package/dist/setup-service.d.ts +20 -0
  123. package/dist/setup-service.js +38 -0
  124. package/package.json +36 -20
  125. package/dist/browser-cli-args.d.ts +0 -20
  126. package/dist/browser-cli-args.js +0 -100
  127. package/dist/channel-store.d.ts +0 -22
  128. package/dist/channel-store.js +0 -65
  129. package/dist/cli.d.ts +0 -2
  130. package/dist/cli.js +0 -732
  131. package/dist/control-client.d.ts +0 -122
  132. package/dist/control-client.js +0 -855
  133. package/dist/control-endpoint.d.ts +0 -14
  134. package/dist/control-endpoint.js +0 -124
  135. package/dist/desktop-browser-host-client.d.ts +0 -35
  136. package/dist/desktop-browser-host-client.js +0 -415
  137. package/dist/instance-store.d.ts +0 -38
  138. package/dist/instance-store.js +0 -85
  139. package/dist/plugin-install-restart.d.ts +0 -13
  140. package/dist/plugin-install-restart.js +0 -24
  141. package/dist/plugin-runtime-control.d.ts +0 -2
  142. package/dist/plugin-runtime-control.js +0 -37
  143. package/dist/registry.d.ts +0 -10
  144. package/dist/registry.js +0 -52
  145. package/dist/setup.d.ts +0 -20
  146. package/dist/setup.js +0 -374
  147. package/dist/update.d.ts +0 -25
  148. package/dist/update.js +0 -133
@@ -1,4 +1,4 @@
1
- import { type AgentCapabilities, type AgentSessionStore, type ConversationRuntime, type SessionRegistry } from "@rynx-ai/core";
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
2
  import { type PluginAgentSummary, type PluginCapability, 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";
@@ -12,6 +12,24 @@ export interface PluginAgentCatalogHost {
12
12
  session: PluginSessionExecutionSnapshot;
13
13
  }): Promise<PluginResolvedSessionExecution>;
14
14
  }
15
+ /** Mutable launch selectors are accepted only at the Host boundary. The
16
+ * resolver turns them into immutable Session-owned snapshots before a registry
17
+ * row is created; none of these selectors are persisted on the Session. */
18
+ export interface PluginSessionLaunchSelection {
19
+ projectId?: string;
20
+ managedCwd?: string;
21
+ workspace?: SessionWorkspaceSnapshot;
22
+ agent?: string;
23
+ config?: AgentSpec;
24
+ provider?: AgentRuntimeId;
25
+ model?: string;
26
+ reasoningEffort?: string;
27
+ title?: string;
28
+ }
29
+ export interface PluginResolvedSessionLaunch {
30
+ workspace: SessionWorkspaceSnapshot;
31
+ execution: ResolvedExecutionSnapshot;
32
+ }
15
33
  export interface PluginHostServices {
16
34
  installationId: string;
17
35
  installationProof: {
@@ -24,18 +42,42 @@ export interface PluginHostServices {
24
42
  sessionStore: AgentSessionStore;
25
43
  sessionRegistry: SessionRegistry;
26
44
  agentCatalog: PluginAgentCatalogHost;
45
+ sessionLaunch: {
46
+ resolve(input: PluginSessionLaunchSelection): Promise<PluginResolvedSessionLaunch>;
47
+ };
27
48
  interruptSession?(sessionId: string): Promise<boolean>;
49
+ terminateSession?(sessionId: string): Promise<boolean>;
28
50
  resolveSessionInteraction?(sessionId: string, interactionId: string, resolution: SessionInteractionResolution): Promise<{
29
51
  disposition: PluginSessionInteractionResolveDisposition;
30
52
  }>;
53
+ sessionFork?(input: {
54
+ sourceSessionId: string;
55
+ operationId: string;
56
+ title?: string;
57
+ }): Promise<{
58
+ sessionId: string;
59
+ disposition: "created" | "replayed";
60
+ }>;
61
+ admissionOpen?: () => boolean;
62
+ admissionReserve?: () => AdmissionReservation | undefined;
31
63
  }
32
64
  export interface PluginHostRpcOptions {
65
+ /** Manifest id returned by host.info. */
33
66
  pluginId: string;
67
+ /** Marketplace-qualified principal used for ownership and proof scopes. */
68
+ principalId?: string;
34
69
  daemonInstanceId: string;
35
- grants: readonly PluginCapability[];
70
+ productVersion: string;
71
+ cliVersion: string;
72
+ capabilities: readonly PluginCapability[];
36
73
  services: PluginHostServices;
74
+ /** Canonical plugin-owned root permitted for the Workspace plugin's isolated
75
+ * Session directory. Omitted for every other plugin. */
76
+ managedWorkspaceRoot?: string;
37
77
  /** Resolving this promise is the stream's backpressure acknowledgement. */
38
78
  emit(event: PluginHostEvent): void | Promise<void>;
79
+ /** Bounded graceful-stop window used while disposing this Host. */
80
+ disposeForceAfterMs?: number;
39
81
  }
40
82
  /**
41
83
  * One authenticated plugin's capability-gated Host RPC surface.
@@ -46,19 +88,30 @@ export interface PluginHostRpcOptions {
46
88
  */
47
89
  export declare class PluginHostRpc {
48
90
  private readonly pluginId;
91
+ private readonly principalId;
49
92
  private readonly daemonInstanceId;
50
- private readonly grants;
93
+ private readonly productVersion;
94
+ private readonly cliVersion;
95
+ private readonly capabilities;
51
96
  private readonly services;
97
+ private readonly managedWorkspaceRoot;
52
98
  private readonly emit;
99
+ private readonly disposeForceAfterMs;
53
100
  private readonly activeRuns;
54
101
  private readonly activeSessionRuns;
55
102
  private readonly activeRunsByOperation;
56
103
  private disposed;
104
+ private disposePromise?;
57
105
  constructor(options: PluginHostRpcOptions);
58
- call(method: string, params: unknown): Promise<unknown>;
106
+ call(method: string, params: unknown, signal?: AbortSignal): Promise<unknown>;
59
107
  dispose(): Promise<void>;
60
108
  private ensureSession;
109
+ private ensureSessionAdmitted;
110
+ private resolveManagedWorkspace;
111
+ private resolveWorkspaceSnapshot;
112
+ private sessionEnsureResult;
61
113
  private runSession;
114
+ private runSessionAdmitted;
62
115
  private pumpRun;
63
116
  private releaseRun;
64
117
  private steerSession;
@@ -72,6 +125,9 @@ export declare class PluginHostRpc {
72
125
  private withOwnedSession;
73
126
  private resolveSessionInteraction;
74
127
  private forkSession;
128
+ private forkSessionAdmitted;
129
+ private reserveAdmission;
130
+ private withAdmission;
75
131
  private setSessionTitle;
76
132
  private resolveAgentExecution;
77
133
  private issueControlLink;
@@ -1,8 +1,10 @@
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";
7
9
  const MAX_MESSAGE_LENGTH = 1_000_000;
8
10
  /** Leaves ample room for the RPC envelope below the runner's 1 MiB line cap. */
@@ -43,42 +45,58 @@ const METHOD_CAPABILITY = {
43
45
  */
44
46
  export class PluginHostRpc {
45
47
  pluginId;
48
+ principalId;
46
49
  daemonInstanceId;
47
- grants;
50
+ productVersion;
51
+ cliVersion;
52
+ capabilities;
48
53
  services;
54
+ managedWorkspaceRoot;
49
55
  emit;
56
+ disposeForceAfterMs;
50
57
  activeRuns = new Map();
51
58
  activeSessionRuns = new Map();
52
59
  activeRunsByOperation = new Map();
53
60
  disposed = false;
61
+ disposePromise;
54
62
  constructor(options) {
55
63
  this.pluginId = requiredString(options.pluginId, "pluginId", 64);
64
+ this.principalId = requiredString(options.principalId ?? options.pluginId, "principalId", 160);
56
65
  this.daemonInstanceId = requiredString(options.daemonInstanceId, "daemonInstanceId", 128);
57
- this.grants = new Set(options.grants);
66
+ this.productVersion = requiredString(options.productVersion, "productVersion", 128);
67
+ this.cliVersion = requiredString(options.cliVersion, "cliVersion", 128);
68
+ this.capabilities = new Set(options.capabilities);
58
69
  this.services = options.services;
70
+ this.managedWorkspaceRoot = options.managedWorkspaceRoot
71
+ ? path.resolve(options.managedWorkspaceRoot)
72
+ : undefined;
59
73
  this.emit = options.emit;
74
+ this.disposeForceAfterMs = options.disposeForceAfterMs ?? 10_000;
60
75
  }
61
- async call(method, params) {
76
+ async call(method, params, signal) {
62
77
  if (this.disposed)
63
78
  throw new PluginHostCallError("HOST_CLOSED", "plugin host is closed");
79
+ throwIfHostCallAborted(signal);
64
80
  if (method === "host.info") {
65
81
  return {
66
82
  pluginId: this.pluginId,
67
83
  installationId: this.services.installationId,
68
84
  daemonInstanceId: this.daemonInstanceId,
85
+ productVersion: this.productVersion,
86
+ cliVersion: this.cliVersion,
69
87
  installationProof: {
70
88
  algorithm: this.services.installationProof.algorithm,
71
89
  publicKey: this.services.installationProof.publicKey,
72
90
  },
73
91
  protocolVersion: 1,
74
- grants: [...this.grants],
92
+ capabilities: [...this.capabilities],
75
93
  };
76
94
  }
77
95
  const capability = METHOD_CAPABILITY[method];
78
96
  if (!capability)
79
97
  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}`);
98
+ if (!this.capabilities.has(capability)) {
99
+ throw new PluginHostCallError("CAPABILITY_NOT_DECLARED", `host method ${method} is missing manifest capability ${capability}`);
82
100
  }
83
101
  try {
84
102
  switch (method) {
@@ -87,7 +105,7 @@ export class PluginHostRpc {
87
105
  return {
88
106
  algorithm: this.services.installationProof.algorithm,
89
107
  publicKey: this.services.installationProof.publicKey,
90
- signature: this.services.installationProof.sign(this.pluginId, challenge),
108
+ signature: this.services.installationProof.sign(this.principalId, challenge),
91
109
  };
92
110
  }
93
111
  case "agent.catalog.list":
@@ -97,9 +115,9 @@ export class PluginHostRpc {
97
115
  case "agent.execution.resolve":
98
116
  return await this.resolveAgentExecution(params);
99
117
  case "session.ensure":
100
- return this.ensureSession(params);
118
+ return await this.ensureSession(params);
101
119
  case "session.run":
102
- return await this.runSession(params);
120
+ return await this.runSession(params, signal);
103
121
  case "session.steer":
104
122
  return this.steerSession(params);
105
123
  case "session.interrupt":
@@ -142,40 +160,162 @@ export class PluginHostRpc {
142
160
  }
143
161
  }
144
162
  async dispose() {
145
- if (this.disposed)
146
- return;
163
+ if (this.disposePromise)
164
+ return this.disposePromise;
147
165
  this.disposed = true;
148
166
  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();
167
+ this.disposePromise = (async () => {
168
+ const stopped = await Promise.allSettled(runs.map(async (run) => {
169
+ if (this.activeRuns.get(run.runId) !== run)
170
+ return;
171
+ run.interruptRequested = true;
172
+ const graceful = run.interruptResult ?? (this.services.interruptSession
173
+ ? Promise.resolve().then(() => this.services.interruptSession(run.sessionId))
174
+ : Promise.resolve().then(() => {
175
+ run.abort.abort();
176
+ return false;
177
+ }));
178
+ const authoritative = forceStoppedInterrupt(graceful, this.disposeForceAfterMs, () => this.services.terminateSession?.(run.sessionId) ?? Promise.resolve(false));
179
+ run.interruptResult = authoritative;
180
+ if (!(await authoritative)) {
181
+ throw new Error(`could not prove Session ${run.sessionId} stopped`);
182
+ }
183
+ this.releaseRun(run);
184
+ }));
185
+ const failures = stopped
186
+ .filter((result) => result.status === "rejected")
187
+ .map((result) => result.reason);
188
+ if (failures.length > 0) {
189
+ throw new AggregateError(failures, "plugin Host could not stop every active run");
161
190
  }
162
- }
191
+ })();
192
+ return this.disposePromise;
193
+ }
194
+ async ensureSession(params) {
195
+ return this.withAdmission(() => this.ensureSessionAdmitted(params));
163
196
  }
164
- ensureSession(params) {
197
+ async ensureSessionAdmitted(params) {
165
198
  const input = record(params);
166
199
  const scopeId = requiredString(input.scopeId, "scopeId", 512);
167
200
  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,
201
+ const parsedSelection = sessionLaunchSelection(input.session);
202
+ if (parsedSelection.managedCwd && !this.managedWorkspaceRoot) {
203
+ throw new PluginHostCallError("INVALID_PARAMS", "session.managedCwd is reserved for the built-in Workspace plugin");
204
+ }
205
+ const rawRequestHash = hashCanonicalJson(parsedSelection);
206
+ const replay = replayPluginSessionLaunch({
207
+ pluginId: this.principalId,
208
+ scopeId,
209
+ operationId,
210
+ requestHash: rawRequestHash,
211
+ allowRequestHashMismatch: true,
212
+ });
213
+ if (replay)
214
+ return this.sessionEnsureResult(replay);
215
+ const selection = parsedSelection.managedCwd
216
+ ? {
217
+ ...parsedSelection,
218
+ managedCwd: await this.resolveManagedWorkspace(parsedSelection.managedCwd),
219
+ }
220
+ : parsedSelection.workspace
221
+ ? {
222
+ ...parsedSelection,
223
+ workspace: await this.resolveWorkspaceSnapshot(parsedSelection.workspace),
224
+ }
225
+ : parsedSelection;
226
+ const requestHash = hashCanonicalJson(selection);
227
+ const canonicalReplay = replayPluginSessionLaunch({
228
+ pluginId: this.principalId,
172
229
  scopeId,
173
230
  operationId,
174
231
  requestHash,
175
- session,
176
232
  });
233
+ if (canonicalReplay)
234
+ return this.sessionEnsureResult(canonicalReplay);
235
+ const resolved = await this.services.sessionLaunch.resolve(selection);
236
+ const ensured = ensurePluginSessionLaunch({
237
+ pluginId: this.principalId,
238
+ scopeId,
239
+ operationId,
240
+ requestHash,
241
+ rawRequestHash,
242
+ session: {
243
+ workspace: sessionWorkspaceSnapshotSchema.parse(resolved.workspace),
244
+ execution: resolvedExecutionSnapshotSchema.parse(resolved.execution),
245
+ ...(selection.title ? { title: selection.title } : {}),
246
+ },
247
+ });
248
+ return this.sessionEnsureResult(ensured);
249
+ }
250
+ async resolveManagedWorkspace(directory) {
251
+ const root = this.managedWorkspaceRoot;
252
+ if (!root) {
253
+ throw new PluginHostCallError("INVALID_PARAMS", "session.managedCwd is reserved for the built-in Workspace plugin");
254
+ }
255
+ if (!path.isAbsolute(directory)) {
256
+ throw new PluginHostCallError("INVALID_PARAMS", "session.managedCwd must be absolute");
257
+ }
258
+ let canonicalRoot;
259
+ let canonicalDirectory;
260
+ try {
261
+ [canonicalRoot, canonicalDirectory] = await Promise.all([
262
+ realpath(root),
263
+ realpath(directory),
264
+ ]);
265
+ }
266
+ catch {
267
+ throw new PluginHostCallError("INVALID_PARAMS", "session.managedCwd must be an existing plugin-managed directory");
268
+ }
269
+ const relative = path.relative(canonicalRoot, canonicalDirectory);
270
+ if (relative === ".." ||
271
+ relative.startsWith(`..${path.sep}`) ||
272
+ path.isAbsolute(relative)) {
273
+ throw new PluginHostCallError("INVALID_PARAMS", "session.managedCwd escapes the plugin-managed workspace root");
274
+ }
275
+ return canonicalDirectory;
276
+ }
277
+ async resolveWorkspaceSnapshot(workspace) {
278
+ const directories = [workspace.cwd, ...workspace.allowedDirs];
279
+ if (directories.some((directory) => !path.isAbsolute(directory))) {
280
+ throw new PluginHostCallError("INVALID_PARAMS", "session.workspace paths must be absolute");
281
+ }
282
+ let canonical;
283
+ try {
284
+ canonical = await Promise.all(directories.map(async (directory) => {
285
+ const resolved = await realpath(directory);
286
+ if (!(await stat(resolved)).isDirectory()) {
287
+ throw new Error("not a directory");
288
+ }
289
+ return resolved;
290
+ }));
291
+ }
292
+ catch {
293
+ throw new PluginHostCallError("INVALID_PARAMS", "session.workspace paths must be existing directories");
294
+ }
295
+ const parsed = sessionWorkspaceSnapshotSchema.safeParse({
296
+ cwd: canonical[0],
297
+ allowedDirs: canonical.slice(1),
298
+ });
299
+ if (!parsed.success) {
300
+ throw new PluginHostCallError("INVALID_PARAMS", `invalid session.workspace: ${parsed.error.issues[0]?.message ?? "invalid snapshot"}`);
301
+ }
302
+ return parsed.data;
303
+ }
304
+ sessionEnsureResult(result) {
305
+ const session = this.ownedSession(result.sessionId);
306
+ return {
307
+ ...result,
308
+ execution: {
309
+ provider: session.execution.provider,
310
+ model: session.execution.model,
311
+ reasoningEffort: session.execution.reasoningEffort,
312
+ },
313
+ };
314
+ }
315
+ async runSession(params, signal) {
316
+ return this.withAdmission(() => this.runSessionAdmitted(params, signal));
177
317
  }
178
- async runSession(params) {
318
+ async runSessionAdmitted(params, signal) {
179
319
  const input = record(params);
180
320
  const sessionId = requiredSessionId(input);
181
321
  const operationId = optionalString(input.operationId, "operationId", 512);
@@ -191,6 +331,12 @@ export class PluginHostRpc {
191
331
  }
192
332
  const message = requiredText(input.message, "message", MAX_MESSAGE_LENGTH);
193
333
  const abort = new AbortController();
334
+ const cancel = () => abort.abort(signal?.reason);
335
+ if (signal?.aborted) {
336
+ cancel();
337
+ throwIfHostCallAborted(signal);
338
+ }
339
+ signal?.addEventListener("abort", cancel, { once: true });
194
340
  const steer = new SteerController();
195
341
  const runId = randomUUID();
196
342
  const run = {
@@ -210,23 +356,23 @@ export class PluginHostRpc {
210
356
  started = await this.services.conversationRuntime.runLive({
211
357
  message,
212
358
  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,
359
+ workspace: session.workspace,
360
+ execution: session.execution,
219
361
  signal: abort.signal,
220
362
  steer,
221
- developerInstructions: optionalString(input.developerInstructions, "developerInstructions", 200_000),
222
363
  });
223
364
  }
224
365
  catch (error) {
225
366
  this.releaseRun(run);
226
367
  throw error;
227
368
  }
228
- if (this.disposed || this.activeRuns.get(runId) !== run) {
369
+ finally {
370
+ signal?.removeEventListener("abort", cancel);
371
+ }
372
+ if (signal?.aborted || this.disposed || this.activeRuns.get(runId) !== run) {
373
+ abort.abort(signal?.reason);
229
374
  this.releaseRun(run);
375
+ throwIfHostCallAborted(signal);
230
376
  throw new PluginHostCallError("HOST_CLOSED", "plugin host is closed");
231
377
  }
232
378
  // Let the RPC transport write the request response before stream events are
@@ -338,6 +484,10 @@ export class PluginHostRpc {
338
484
  async interruptRun(params) {
339
485
  const input = record(params);
340
486
  const run = this.activeRunByIdentity(input);
487
+ const forceAfterMs = optionalPositiveNumber(input.forceAfterMs, "forceAfterMs");
488
+ if (forceAfterMs !== undefined && forceAfterMs > 30_000) {
489
+ throw new PluginHostCallError("INVALID_PARAMS", "forceAfterMs must not exceed 30000");
490
+ }
341
491
  if (run.interruptResult) {
342
492
  return { interrupted: await run.interruptResult };
343
493
  }
@@ -345,7 +495,7 @@ export class PluginHostRpc {
345
495
  // the authoritative stop is pending wait for its result: true suppresses
346
496
  // them, while false/error lets genuine native progress/terminal continue.
347
497
  run.interruptRequested = true;
348
- const interruptResult = this.services.interruptSession
498
+ const graceful = this.services.interruptSession
349
499
  // ConversationRuntime wires AbortSignal to this same native operation.
350
500
  // Invoke it directly once so this boolean is the authoritative result;
351
501
  // aborting first would trigger a duplicate, unobserved interrupt.
@@ -356,6 +506,9 @@ export class PluginHostRpc {
356
506
  // effort and cannot prove that native execution stopped.
357
507
  return false;
358
508
  });
509
+ const interruptResult = forceAfterMs === undefined
510
+ ? graceful
511
+ : forceStoppedInterrupt(graceful, forceAfterMs, () => this.services.terminateSession?.(run.sessionId) ?? Promise.resolve(false));
359
512
  run.interruptResult = interruptResult;
360
513
  try {
361
514
  const interrupted = await interruptResult;
@@ -437,12 +590,40 @@ export class PluginHostRpc {
437
590
  return await resolve(sessionId, interactionId, resolution);
438
591
  }
439
592
  async forkSession(params) {
593
+ return this.withAdmission(() => this.forkSessionAdmitted(params));
594
+ }
595
+ async forkSessionAdmitted(params) {
440
596
  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);
597
+ const sourceSessionId = requiredString(input.sourceSessionId, "sourceSessionId", 512);
598
+ const operationId = requiredString(input.operationId, "operationId", 512);
599
+ const title = optionalString(input.title, "title", 1_000);
600
+ this.ownedSession(sourceSessionId);
601
+ if (!this.services.sessionFork) {
602
+ throw new PluginHostCallError("HOST_CALL_FAILED", "Session fork is unavailable");
603
+ }
604
+ return await this.services.sessionFork({
605
+ sourceSessionId,
606
+ operationId: `${this.principalId}:${operationId}`,
607
+ ...(title ? { title } : {}),
608
+ });
609
+ }
610
+ reserveAdmission() {
611
+ const reservation = this.services.admissionReserve?.();
612
+ if (reservation)
613
+ return reservation;
614
+ if (!this.services.admissionReserve && this.services.admissionOpen?.() !== false) {
615
+ return { release() { } };
616
+ }
617
+ throw new PluginHostCallError("HOST_CALL_FAILED", "daemon maintenance is in progress", true);
618
+ }
619
+ async withAdmission(operation) {
620
+ const reservation = this.reserveAdmission();
621
+ try {
622
+ return await operation();
623
+ }
624
+ finally {
625
+ reservation.release();
626
+ }
446
627
  }
447
628
  setSessionTitle(params) {
448
629
  const input = record(params);
@@ -466,7 +647,7 @@ export class PluginHostRpc {
466
647
  this.ownedSession(sessionId);
467
648
  const ttlSeconds = optionalPositiveNumber(input.ttlSeconds, "ttlSeconds");
468
649
  return createControlLink({
469
- pluginId: this.pluginId,
650
+ pluginId: this.principalId,
470
651
  sessionId,
471
652
  ...(ttlSeconds === undefined ? {} : { ttlMs: ttlSeconds * 1_000 }),
472
653
  });
@@ -479,9 +660,24 @@ export class PluginHostRpc {
479
660
  return session;
480
661
  }
481
662
  isOwned(session) {
482
- return session.source === `plugin:${this.pluginId}`;
663
+ const seen = new Set();
664
+ let current = session;
665
+ while (current && !seen.has(current.id)) {
666
+ if (current.source === `plugin:${this.principalId}`)
667
+ return true;
668
+ seen.add(current.id);
669
+ current = current.forkedFromSessionId
670
+ ? this.services.sessionRegistry.get(current.forkedFromSessionId)
671
+ : undefined;
672
+ }
673
+ return false;
483
674
  }
484
675
  }
676
+ function throwIfHostCallAborted(signal) {
677
+ if (!signal?.aborted)
678
+ return;
679
+ throw new PluginHostCallError("REQUEST_CANCELLED", "plugin host request was cancelled");
680
+ }
485
681
  export class PluginHostCallError extends PluginRunnerError {
486
682
  retryable;
487
683
  data;
@@ -512,22 +708,50 @@ function toHostCallError(error) {
512
708
  }
513
709
  return new PluginHostCallError("HOST_CALL_FAILED", error instanceof Error ? error.message : String(error));
514
710
  }
515
- function sessionLaunchMeta(value) {
711
+ function sessionLaunchSelection(value) {
516
712
  const input = record(value);
517
713
  assertJsonValue(input, "session");
714
+ const projectId = optionalString(input.projectId, "session.projectId", 512);
715
+ const managedCwd = optionalString(input.managedCwd, "session.managedCwd", 16_384);
716
+ const workspace = optionalSessionWorkspace(input.workspace);
518
717
  const agent = optionalAgentId(input.agent, "session.agent");
519
718
  const config = input.config === undefined ? undefined : agentSpecSchema.parse(input.config);
719
+ const provider = optionalRuntimeValue(input.provider);
520
720
  const model = optionalString(input.model, "session.model", 256);
521
721
  const reasoningEffort = optionalString(input.reasoningEffort, "session.reasoningEffort", 128);
522
722
  const title = optionalString(input.title, "session.title", 1_000);
723
+ if (Number(Boolean(projectId)) + Number(Boolean(managedCwd)) + Number(Boolean(workspace)) > 1) {
724
+ throw new PluginHostCallError("INVALID_PARAMS", "session.projectId, session.managedCwd, and session.workspace are mutually exclusive");
725
+ }
726
+ if (Number(Boolean(agent)) +
727
+ Number(Boolean(config)) +
728
+ Number(Boolean(provider)) >
729
+ 1) {
730
+ throw new PluginHostCallError("INVALID_PARAMS", "session.agent, session.config, and session.provider are mutually exclusive");
731
+ }
523
732
  return {
733
+ ...(projectId ? { projectId } : {}),
734
+ ...(managedCwd ? { managedCwd } : {}),
735
+ ...(workspace ? { workspace } : {}),
524
736
  ...(agent ? { agent } : {}),
525
737
  ...(config ? { config } : {}),
738
+ ...(provider ? { provider } : {}),
526
739
  ...(model ? { model } : {}),
527
740
  ...(reasoningEffort ? { reasoningEffort } : {}),
528
741
  ...(title ? { title } : {}),
529
742
  };
530
743
  }
744
+ function optionalSessionWorkspace(value) {
745
+ if (value === undefined)
746
+ return undefined;
747
+ const input = record(value);
748
+ const cwd = requiredPathString(input.cwd, "session.workspace.cwd", 16_384);
749
+ if (!Array.isArray(input.allowedDirs) || input.allowedDirs.length === 0) {
750
+ throw new PluginHostCallError("INVALID_PARAMS", "session.workspace.allowedDirs must be a non-empty string array");
751
+ }
752
+ const allowedDirs = input.allowedDirs.map((directory, index) => requiredPathString(directory, `session.workspace.allowedDirs[${index}]`, 16_384));
753
+ return { cwd, allowedDirs };
754
+ }
531
755
  function sessionExecutionSnapshot(value) {
532
756
  const input = record(value);
533
757
  const runtime = optionalRuntimeValue(input.runtime);
@@ -608,6 +832,15 @@ function requiredString(value, label, maxLength) {
608
832
  }
609
833
  return value.trim();
610
834
  }
835
+ function requiredPathString(value, label, maxLength) {
836
+ if (typeof value !== "string" || !value.trim()) {
837
+ throw new PluginHostCallError("INVALID_PARAMS", `${label} is required`);
838
+ }
839
+ if (value.length > maxLength) {
840
+ throw new PluginHostCallError("INVALID_PARAMS", `${label} is too long`);
841
+ }
842
+ return value;
843
+ }
611
844
  function requiredText(value, label, maxLength) {
612
845
  if (typeof value !== "string" || !value.trim()) {
613
846
  throw new PluginHostCallError("INVALID_PARAMS", `${label} is required`);
@@ -648,6 +881,23 @@ function optionalPositiveNumber(value, label) {
648
881
  }
649
882
  return value;
650
883
  }
884
+ async function forceStoppedInterrupt(graceful, forceAfterMs, terminate) {
885
+ let timer;
886
+ const graceExpired = new Promise((resolve) => {
887
+ timer = setTimeout(() => resolve(false), forceAfterMs);
888
+ timer.unref?.();
889
+ });
890
+ try {
891
+ if (await Promise.race([graceful.catch(() => false), graceExpired])) {
892
+ return true;
893
+ }
894
+ }
895
+ finally {
896
+ if (timer)
897
+ clearTimeout(timer);
898
+ }
899
+ return terminate();
900
+ }
651
901
  function assertJsonValue(value, label, seen = new Set()) {
652
902
  if (value === null || typeof value === "string" || typeof value === "boolean")
653
903
  return;