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

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 (117) 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 +131316 -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/bundled-plugins.d.ts +15 -0
  64. package/dist/bundled-plugins.js +134 -0
  65. package/dist/cli.js +26 -208
  66. package/dist/control-deps.js +2 -79
  67. package/dist/daemon-server.d.ts +4 -1
  68. package/dist/daemon-server.js +167 -57
  69. package/dist/db.js +145 -124
  70. package/dist/index-daemon.js +25 -4
  71. package/dist/migrations/cleanup-legacy-sessions.d.ts +1 -1
  72. package/dist/migrations/cleanup-legacy-sessions.js +33 -15
  73. package/dist/official-plugins.d.ts +2 -0
  74. package/dist/official-plugins.js +2 -0
  75. package/dist/plugin-cli.d.ts +1 -3
  76. package/dist/plugin-cli.js +56 -45
  77. package/dist/plugin-host-leases.d.ts +4 -0
  78. package/dist/plugin-host-leases.js +7 -0
  79. package/dist/plugin-host-rpc.d.ts +42 -4
  80. package/dist/plugin-host-rpc.js +144 -34
  81. package/dist/plugin-installer.d.ts +24 -55
  82. package/dist/plugin-installer.js +202 -415
  83. package/dist/plugin-management-service.d.ts +30 -23
  84. package/dist/plugin-management-service.js +290 -105
  85. package/dist/plugin-marketplace.d.ts +35 -0
  86. package/dist/plugin-marketplace.js +299 -0
  87. package/dist/plugin-package.d.ts +22 -24
  88. package/dist/plugin-package.js +520 -571
  89. package/dist/plugin-skill-context.d.ts +4 -0
  90. package/dist/plugin-skill-context.js +35 -0
  91. package/dist/plugin-store.d.ts +36 -37
  92. package/dist/plugin-store.js +140 -92
  93. package/dist/plugin-supervisor.d.ts +45 -125
  94. package/dist/plugin-supervisor.js +516 -1053
  95. package/dist/pm2.js +15 -10
  96. package/dist/runtime-project-store.d.ts +9 -0
  97. package/dist/runtime-project-store.js +83 -0
  98. package/dist/session-fallback-workspace.d.ts +22 -0
  99. package/dist/session-fallback-workspace.js +120 -0
  100. package/dist/session-fork-store.d.ts +41 -0
  101. package/dist/session-fork-store.js +288 -0
  102. package/dist/session-launch-operations.d.ts +14 -2
  103. package/dist/session-launch-operations.js +34 -2
  104. package/dist/session-meta-store.d.ts +7 -9
  105. package/dist/session-meta-store.js +28 -49
  106. package/dist/setup.js +26 -14
  107. package/package.json +14 -11
  108. package/dist/channel-store.d.ts +0 -22
  109. package/dist/channel-store.js +0 -65
  110. package/dist/instance-store.d.ts +0 -38
  111. package/dist/instance-store.js +0 -85
  112. package/dist/plugin-install-restart.d.ts +0 -13
  113. package/dist/plugin-install-restart.js +0 -24
  114. package/dist/plugin-runtime-control.d.ts +0 -2
  115. package/dist/plugin-runtime-control.js +0 -37
  116. package/dist/registry.d.ts +0 -10
  117. package/dist/registry.js +0 -52
@@ -1,14 +1,15 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { mkdirSync, realpathSync, statSync } from "node:fs";
3
3
  import path from "node:path";
4
- import { rynxHome } from "@rynx-ai/core";
5
- import { createPluginCliInvocationLeaseId, parsePluginCliHostEffect, removePluginRuntimeLease, writePluginRuntimeLease, } from "@rynx-ai/plugin-sdk";
4
+ import { createPluginCliInvocationLeaseId, removePluginRuntimeLease, writePluginRuntimeLease, } from "@rynx-ai/plugin-sdk";
6
5
  import { db } from "./db.js";
7
- import { getPlugin, isRunnablePluginRecord } from "./plugin-store.js";
6
+ import { pluginDataDir, pluginRuntimeCacheDir } from "./plugin-installer.js";
7
+ import { validatePluginPackageSnapshot } from "./plugin-package.js";
8
+ import { getPlugin } from "./plugin-store.js";
8
9
  import { loadOrCreateDaemonInstallationIdentitySync } from "./installation-id.js";
9
10
  const DEFAULT_TERMINATION_GRACE_MS = 2_000;
10
11
  const MAX_TERMINATION_GRACE_MS = 30_000;
11
- /** Execute one statically declared plugin command in a separate Node process. */
12
+ /** Execute one statically declared, language-neutral plugin command. */
12
13
  export async function runPluginCliCommand(pluginId, args, options = {}) {
13
14
  const hostEnv = options.env ?? process.env;
14
15
  const terminationGraceMs = options.terminationGraceMs ?? DEFAULT_TERMINATION_GRACE_MS;
@@ -21,7 +22,6 @@ export async function runPluginCliCommand(pluginId, args, options = {}) {
21
22
  let terminationReason;
22
23
  let forceKillTimer;
23
24
  let abortListener;
24
- const effects = new Map();
25
25
  if (options.stdin !== undefined && options.stdio !== "pipe") {
26
26
  throw new Error("plugin CLI stdin requires piped stdio");
27
27
  }
@@ -37,6 +37,13 @@ export async function runPluginCliCommand(pluginId, args, options = {}) {
37
37
  if (options.signal?.aborted)
38
38
  throw pluginCliAbortError(options.signal.reason);
39
39
  const installationIdentity = loadOrCreateDaemonInstallationIdentitySync(hostEnv);
40
+ const verifiedRecord = getPlugin(pluginId);
41
+ if (!verifiedRecord)
42
+ throw new Error(`plugin "${pluginId}" is not installed`);
43
+ const verifiedIntegrity = await validatePluginPackageSnapshot(verifiedRecord.installPath, options.signal ? { signal: options.signal } : {});
44
+ if (verifiedIntegrity !== verifiedRecord.integrity) {
45
+ throw new Error(`plugin package integrity changed for "${verifiedRecord.canonicalId}"`);
46
+ }
40
47
  const clearForceKill = () => {
41
48
  if (forceKillTimer)
42
49
  clearTimeout(forceKillTimer);
@@ -47,24 +54,14 @@ export async function runPluginCliCommand(pluginId, args, options = {}) {
47
54
  terminationReason ??= reason;
48
55
  if (!childIsRunning())
49
56
  return;
50
- try {
51
- child.kill("SIGTERM");
52
- }
53
- catch {
54
- // The close/error event remains authoritative for cleanup.
55
- }
57
+ signalChildTree(child, "SIGTERM");
56
58
  if (forceKillTimer)
57
59
  return;
58
60
  forceKillTimer = setTimeout(() => {
59
61
  forceKillTimer = undefined;
60
62
  if (!childIsRunning())
61
63
  return;
62
- try {
63
- child.kill("SIGKILL");
64
- }
65
- catch {
66
- // Completion still waits for close/error; no lease is removed early.
67
- }
64
+ signalChildTree(child, "SIGKILL");
68
65
  }, terminationGraceMs);
69
66
  forceKillTimer.unref?.();
70
67
  };
@@ -81,14 +78,13 @@ export async function runPluginCliCommand(pluginId, args, options = {}) {
81
78
  const record = getPlugin(pluginId);
82
79
  if (!record)
83
80
  throw new Error(`plugin "${pluginId}" is not installed`);
84
- if (!isRunnablePluginRecord(record)) {
85
- throw new Error(`plugin "${pluginId}" uses the removed legacy format; reinstall it`);
81
+ if (record.integrity !== verifiedRecord.integrity ||
82
+ record.installPath !== verifiedRecord.installPath ||
83
+ record.updatedAt !== verifiedRecord.updatedAt) {
84
+ throw new Error(`plugin "${pluginId}" changed while its command was being prepared; retry`);
86
85
  }
87
- // `enabled` controls daemon runtime contributions, not explicit CLI calls.
88
- // Keeping commands available lets a user stop/status a managed service before
89
- // uninstalling a disabled plugin.
90
86
  const command = args[0];
91
- const declared = record.manifest.contributions?.commands?.find((entry) => entry.id === command);
87
+ const declared = record.manifest.commands?.items.find((entry) => entry.id === command);
92
88
  if (!command ||
93
89
  command === "--help" ||
94
90
  command === "-h" ||
@@ -98,41 +94,40 @@ export async function runPluginCliCommand(pluginId, args, options = {}) {
98
94
  if (!declared) {
99
95
  throw new PluginCliUsageError(`plugin "${pluginId}" does not declare command "${command}"\n\n${pluginCommandUsage(record)}`);
100
96
  }
101
- if (!record.manifest.cliEntry) {
102
- throw new Error(`plugin "${pluginId}" declares commands but has no cliEntry`);
97
+ if (!record.manifest.commands) {
98
+ throw new Error(`plugin "${pluginId}" declares no CLI command executable`);
103
99
  }
104
100
  const packageRoot = realpathSync(record.installPath);
105
- const cliEntry = realpathSync(path.resolve(packageRoot, record.manifest.cliEntry));
101
+ const cliEntry = realpathSync(path.resolve(packageRoot, record.manifest.commands.command));
106
102
  if (!isPathInside(packageRoot, cliEntry) || !statSync(cliEntry).isFile()) {
107
- throw new Error(`plugin "${pluginId}" cliEntry resolves outside its package`);
103
+ throw new Error(`plugin "${pluginId}" commands.command resolves outside its package`);
108
104
  }
109
- dataDir = path.join(rynxHome(), "plugin-data", record.name);
105
+ dataDir = pluginDataDir(record.marketplaceId, record.pluginId);
106
+ const cacheDir = pluginRuntimeCacheDir(record.marketplaceId, record.pluginId, record.integrity);
110
107
  mkdirSync(dataDir, { recursive: true, mode: 0o700 });
111
- child = spawn(process.execPath, [cliEntry, ...args], {
108
+ mkdirSync(cacheDir, { recursive: true, mode: 0o700 });
109
+ child = spawn(cliEntry, [...args], {
112
110
  cwd: dataDir,
113
111
  env: {
114
- ...hostEnv,
112
+ PATH: hostEnv.PATH,
113
+ HOME: hostEnv.HOME,
114
+ TMPDIR: hostEnv.TMPDIR,
115
+ LANG: hostEnv.LANG,
115
116
  RYNX_HOST_INSTALLATION_ID: installationIdentity.installationId,
116
117
  RYNX_HOST_INSTALLATION_PROOF_ALGORITHM: installationIdentity.algorithm,
117
118
  RYNX_HOST_INSTALLATION_PUBLIC_KEY: installationIdentity.publicKey,
118
- RYNX_PLUGIN_ID: record.name,
119
+ RYNX_PLUGIN_ID: record.pluginId,
120
+ RYNX_PLUGIN_INSTALLATION_ID: record.canonicalId,
119
121
  RYNX_PLUGIN_PACKAGE_ROOT: packageRoot,
120
122
  RYNX_PLUGIN_DATA_DIR: dataDir,
123
+ RYNX_PLUGIN_CACHE_DIR: cacheDir,
121
124
  },
122
125
  stdio: options.stdio === "pipe"
123
- ? ["pipe", "pipe", "pipe", "ipc"]
124
- : ["inherit", "inherit", "inherit", "ipc"],
126
+ ? ["pipe", "pipe", "pipe"]
127
+ : ["inherit", "inherit", "inherit"],
128
+ detached: process.platform !== "win32",
125
129
  });
126
130
  completion = new Promise((resolve, reject) => {
127
- child.on("message", (message) => {
128
- try {
129
- const effect = parsePluginCliHostEffect(message);
130
- effects.set(effect.effect, effect);
131
- }
132
- catch (error) {
133
- terminateChild(error instanceof Error ? error : new Error(String(error)));
134
- }
135
- });
136
131
  child.once("error", (error) => {
137
132
  if (!childIsRunning()) {
138
133
  reject(error);
@@ -172,7 +167,7 @@ export async function runPluginCliCommand(pluginId, args, options = {}) {
172
167
  leaseId = createPluginCliInvocationLeaseId(child.pid ?? process.pid);
173
168
  writePluginRuntimeLease({
174
169
  dataDir,
175
- pluginId: record.name,
170
+ pluginId: record.pluginId,
176
171
  leaseId,
177
172
  installPath: packageRoot,
178
173
  });
@@ -193,7 +188,6 @@ export async function runPluginCliCommand(pluginId, args, options = {}) {
193
188
  const code = await completion;
194
189
  return {
195
190
  code,
196
- effects: code === 0 ? [...effects.values()] : [],
197
191
  ...(options.captureOutputBytes !== undefined && stdout && stderr
198
192
  ? {
199
193
  output: {
@@ -210,6 +204,23 @@ export async function runPluginCliCommand(pluginId, args, options = {}) {
210
204
  cleanup();
211
205
  }
212
206
  }
207
+ function signalChildTree(child, signal) {
208
+ if (process.platform !== "win32" && child.pid) {
209
+ try {
210
+ process.kill(-child.pid, signal);
211
+ return;
212
+ }
213
+ catch {
214
+ // Fall back to the direct child if its process group already exited.
215
+ }
216
+ }
217
+ try {
218
+ child.kill(signal);
219
+ }
220
+ catch {
221
+ // Completion still waits for close/error; no lease is removed early.
222
+ }
223
+ }
213
224
  function pluginCliAbortError(reason) {
214
225
  const error = new Error(reason instanceof Error && reason.message ? reason.message : "plugin CLI command aborted");
215
226
  error.name = reason instanceof Error && reason.name === "TimeoutError"
@@ -246,7 +257,7 @@ function captureStream(stream, limit) {
246
257
  };
247
258
  }
248
259
  export function pluginCommandUsage(record) {
249
- const commands = record.manifest.contributions?.commands ?? [];
260
+ const commands = record.manifest.commands?.items ?? [];
250
261
  const lines = [`Usage: rynx plugin ${record.name} <command> [args...]`];
251
262
  if (commands.length === 0)
252
263
  return [...lines, "", "This plugin declares no CLI commands."].join("\n");
@@ -0,0 +1,4 @@
1
+ /** Fixed leases owned by the resident daemon's plugin supervisor. A new daemon
2
+ * may reclaim these only after it has acquired the exclusive RYNX_HOME owner
3
+ * lock and before starting any plugin runners. */
4
+ export declare const PLUGIN_HOST_RUNTIME_LEASE_IDS: readonly ["host-runtime", "host-settings"];
@@ -0,0 +1,7 @@
1
+ /** Fixed leases owned by the resident daemon's plugin supervisor. A new daemon
2
+ * may reclaim these only after it has acquired the exclusive RYNX_HOME owner
3
+ * lock and before starting any plugin runners. */
4
+ export const PLUGIN_HOST_RUNTIME_LEASE_IDS = [
5
+ "host-runtime",
6
+ "host-settings",
7
+ ];
@@ -1,4 +1,4 @@
1
- import { type AgentCapabilities, type AgentSessionStore, type ConversationRuntime, type SessionRegistry } from "@rynx-ai/core";
1
+ import { 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,23 @@ 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
+ agent?: string;
22
+ config?: AgentSpec;
23
+ provider?: AgentRuntimeId;
24
+ model?: string;
25
+ reasoningEffort?: string;
26
+ title?: string;
27
+ }
28
+ export interface PluginResolvedSessionLaunch {
29
+ workspace: SessionWorkspaceSnapshot;
30
+ execution: ResolvedExecutionSnapshot;
31
+ }
15
32
  export interface PluginHostServices {
16
33
  installationId: string;
17
34
  installationProof: {
@@ -24,16 +41,33 @@ export interface PluginHostServices {
24
41
  sessionStore: AgentSessionStore;
25
42
  sessionRegistry: SessionRegistry;
26
43
  agentCatalog: PluginAgentCatalogHost;
44
+ sessionLaunch: {
45
+ resolve(input: PluginSessionLaunchSelection): Promise<PluginResolvedSessionLaunch>;
46
+ };
27
47
  interruptSession?(sessionId: string): Promise<boolean>;
28
48
  resolveSessionInteraction?(sessionId: string, interactionId: string, resolution: SessionInteractionResolution): Promise<{
29
49
  disposition: PluginSessionInteractionResolveDisposition;
30
50
  }>;
51
+ sessionFork?(input: {
52
+ sourceSessionId: string;
53
+ operationId: string;
54
+ title?: string;
55
+ }): Promise<{
56
+ sessionId: string;
57
+ disposition: "created" | "replayed";
58
+ }>;
31
59
  }
32
60
  export interface PluginHostRpcOptions {
61
+ /** Manifest id returned by host.info. */
33
62
  pluginId: string;
63
+ /** Marketplace-qualified principal used for ownership and proof scopes. */
64
+ principalId?: string;
34
65
  daemonInstanceId: string;
35
- grants: readonly PluginCapability[];
66
+ capabilities: readonly PluginCapability[];
36
67
  services: PluginHostServices;
68
+ /** Canonical plugin-owned root permitted for the Workspace plugin's isolated
69
+ * Session directory. Omitted for every other plugin. */
70
+ managedWorkspaceRoot?: string;
37
71
  /** Resolving this promise is the stream's backpressure acknowledgement. */
38
72
  emit(event: PluginHostEvent): void | Promise<void>;
39
73
  }
@@ -46,18 +80,22 @@ export interface PluginHostRpcOptions {
46
80
  */
47
81
  export declare class PluginHostRpc {
48
82
  private readonly pluginId;
83
+ private readonly principalId;
49
84
  private readonly daemonInstanceId;
50
- private readonly grants;
85
+ private readonly capabilities;
51
86
  private readonly services;
87
+ private readonly managedWorkspaceRoot;
52
88
  private readonly emit;
53
89
  private readonly activeRuns;
54
90
  private readonly activeSessionRuns;
55
91
  private readonly activeRunsByOperation;
56
92
  private disposed;
57
93
  constructor(options: PluginHostRpcOptions);
58
- call(method: string, params: unknown): Promise<unknown>;
94
+ call(method: string, params: unknown, signal?: AbortSignal): Promise<unknown>;
59
95
  dispose(): Promise<void>;
60
96
  private ensureSession;
97
+ private resolveManagedWorkspace;
98
+ private sessionEnsureResult;
61
99
  private runSession;
62
100
  private pumpRun;
63
101
  private releaseRun;
@@ -1,8 +1,10 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
- import { AgentRuntimeError, agentSpecSchema, SteerController, } from "@rynx-ai/core";
2
+ import { realpath } 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,9 +45,11 @@ const METHOD_CAPABILITY = {
43
45
  */
44
46
  export class PluginHostRpc {
45
47
  pluginId;
48
+ principalId;
46
49
  daemonInstanceId;
47
- grants;
50
+ capabilities;
48
51
  services;
52
+ managedWorkspaceRoot;
49
53
  emit;
50
54
  activeRuns = new Map();
51
55
  activeSessionRuns = new Map();
@@ -53,14 +57,19 @@ export class PluginHostRpc {
53
57
  disposed = false;
54
58
  constructor(options) {
55
59
  this.pluginId = requiredString(options.pluginId, "pluginId", 64);
60
+ this.principalId = requiredString(options.principalId ?? options.pluginId, "principalId", 160);
56
61
  this.daemonInstanceId = requiredString(options.daemonInstanceId, "daemonInstanceId", 128);
57
- this.grants = new Set(options.grants);
62
+ this.capabilities = new Set(options.capabilities);
58
63
  this.services = options.services;
64
+ this.managedWorkspaceRoot = options.managedWorkspaceRoot
65
+ ? path.resolve(options.managedWorkspaceRoot)
66
+ : undefined;
59
67
  this.emit = options.emit;
60
68
  }
61
- async call(method, params) {
69
+ async call(method, params, signal) {
62
70
  if (this.disposed)
63
71
  throw new PluginHostCallError("HOST_CLOSED", "plugin host is closed");
72
+ throwIfHostCallAborted(signal);
64
73
  if (method === "host.info") {
65
74
  return {
66
75
  pluginId: this.pluginId,
@@ -71,14 +80,14 @@ export class PluginHostRpc {
71
80
  publicKey: this.services.installationProof.publicKey,
72
81
  },
73
82
  protocolVersion: 1,
74
- grants: [...this.grants],
83
+ capabilities: [...this.capabilities],
75
84
  };
76
85
  }
77
86
  const capability = METHOD_CAPABILITY[method];
78
87
  if (!capability)
79
88
  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}`);
89
+ if (!this.capabilities.has(capability)) {
90
+ throw new PluginHostCallError("CAPABILITY_NOT_DECLARED", `host method ${method} is missing manifest capability ${capability}`);
82
91
  }
83
92
  try {
84
93
  switch (method) {
@@ -87,7 +96,7 @@ export class PluginHostRpc {
87
96
  return {
88
97
  algorithm: this.services.installationProof.algorithm,
89
98
  publicKey: this.services.installationProof.publicKey,
90
- signature: this.services.installationProof.sign(this.pluginId, challenge),
99
+ signature: this.services.installationProof.sign(this.principalId, challenge),
91
100
  };
92
101
  }
93
102
  case "agent.catalog.list":
@@ -97,9 +106,9 @@ export class PluginHostRpc {
97
106
  case "agent.execution.resolve":
98
107
  return await this.resolveAgentExecution(params);
99
108
  case "session.ensure":
100
- return this.ensureSession(params);
109
+ return await this.ensureSession(params);
101
110
  case "session.run":
102
- return await this.runSession(params);
111
+ return await this.runSession(params, signal);
103
112
  case "session.steer":
104
113
  return this.steerSession(params);
105
114
  case "session.interrupt":
@@ -161,21 +170,79 @@ export class PluginHostRpc {
161
170
  }
162
171
  }
163
172
  }
164
- ensureSession(params) {
173
+ async ensureSession(params) {
165
174
  const input = record(params);
166
175
  const scopeId = requiredString(input.scopeId, "scopeId", 512);
167
176
  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,
177
+ const parsedSelection = sessionLaunchSelection(input.session);
178
+ const selection = parsedSelection.managedCwd
179
+ ? {
180
+ ...parsedSelection,
181
+ managedCwd: await this.resolveManagedWorkspace(parsedSelection.managedCwd),
182
+ }
183
+ : parsedSelection;
184
+ const requestHash = hashCanonicalJson(selection);
185
+ const replay = replayPluginSessionLaunch({
186
+ pluginId: this.principalId,
172
187
  scopeId,
173
188
  operationId,
174
189
  requestHash,
175
- session,
176
190
  });
191
+ if (replay)
192
+ return this.sessionEnsureResult(replay);
193
+ const resolved = await this.services.sessionLaunch.resolve(selection);
194
+ const ensured = ensurePluginSessionLaunch({
195
+ pluginId: this.principalId,
196
+ scopeId,
197
+ operationId,
198
+ requestHash,
199
+ session: {
200
+ workspace: sessionWorkspaceSnapshotSchema.parse(resolved.workspace),
201
+ execution: resolvedExecutionSnapshotSchema.parse(resolved.execution),
202
+ ...(selection.title ? { title: selection.title } : {}),
203
+ },
204
+ });
205
+ return this.sessionEnsureResult(ensured);
206
+ }
207
+ async resolveManagedWorkspace(directory) {
208
+ const root = this.managedWorkspaceRoot;
209
+ if (!root) {
210
+ throw new PluginHostCallError("INVALID_PARAMS", "session.managedCwd is reserved for the built-in Workspace plugin");
211
+ }
212
+ if (!path.isAbsolute(directory)) {
213
+ throw new PluginHostCallError("INVALID_PARAMS", "session.managedCwd must be absolute");
214
+ }
215
+ let canonicalRoot;
216
+ let canonicalDirectory;
217
+ try {
218
+ [canonicalRoot, canonicalDirectory] = await Promise.all([
219
+ realpath(root),
220
+ realpath(directory),
221
+ ]);
222
+ }
223
+ catch {
224
+ throw new PluginHostCallError("INVALID_PARAMS", "session.managedCwd must be an existing plugin-managed directory");
225
+ }
226
+ const relative = path.relative(canonicalRoot, canonicalDirectory);
227
+ if (relative === ".." ||
228
+ relative.startsWith(`..${path.sep}`) ||
229
+ path.isAbsolute(relative)) {
230
+ throw new PluginHostCallError("INVALID_PARAMS", "session.managedCwd escapes the plugin-managed workspace root");
231
+ }
232
+ return canonicalDirectory;
233
+ }
234
+ sessionEnsureResult(result) {
235
+ const session = this.ownedSession(result.sessionId);
236
+ return {
237
+ ...result,
238
+ execution: {
239
+ provider: session.execution.provider,
240
+ model: session.execution.model,
241
+ reasoningEffort: session.execution.reasoningEffort,
242
+ },
243
+ };
177
244
  }
178
- async runSession(params) {
245
+ async runSession(params, signal) {
179
246
  const input = record(params);
180
247
  const sessionId = requiredSessionId(input);
181
248
  const operationId = optionalString(input.operationId, "operationId", 512);
@@ -191,6 +258,12 @@ export class PluginHostRpc {
191
258
  }
192
259
  const message = requiredText(input.message, "message", MAX_MESSAGE_LENGTH);
193
260
  const abort = new AbortController();
261
+ const cancel = () => abort.abort(signal?.reason);
262
+ if (signal?.aborted) {
263
+ cancel();
264
+ throwIfHostCallAborted(signal);
265
+ }
266
+ signal?.addEventListener("abort", cancel, { once: true });
194
267
  const steer = new SteerController();
195
268
  const runId = randomUUID();
196
269
  const run = {
@@ -210,23 +283,23 @@ export class PluginHostRpc {
210
283
  started = await this.services.conversationRuntime.runLive({
211
284
  message,
212
285
  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,
286
+ workspace: session.workspace,
287
+ execution: session.execution,
219
288
  signal: abort.signal,
220
289
  steer,
221
- developerInstructions: optionalString(input.developerInstructions, "developerInstructions", 200_000),
222
290
  });
223
291
  }
224
292
  catch (error) {
225
293
  this.releaseRun(run);
226
294
  throw error;
227
295
  }
228
- if (this.disposed || this.activeRuns.get(runId) !== run) {
296
+ finally {
297
+ signal?.removeEventListener("abort", cancel);
298
+ }
299
+ if (signal?.aborted || this.disposed || this.activeRuns.get(runId) !== run) {
300
+ abort.abort(signal?.reason);
229
301
  this.releaseRun(run);
302
+ throwIfHostCallAborted(signal);
230
303
  throw new PluginHostCallError("HOST_CLOSED", "plugin host is closed");
231
304
  }
232
305
  // Let the RPC transport write the request response before stream events are
@@ -438,11 +511,18 @@ export class PluginHostRpc {
438
511
  }
439
512
  async forkSession(params) {
440
513
  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);
514
+ const sourceSessionId = requiredString(input.sourceSessionId, "sourceSessionId", 512);
515
+ const operationId = requiredString(input.operationId, "operationId", 512);
516
+ const title = optionalString(input.title, "title", 1_000);
517
+ this.ownedSession(sourceSessionId);
518
+ if (!this.services.sessionFork) {
519
+ throw new PluginHostCallError("HOST_CALL_FAILED", "Session fork is unavailable");
520
+ }
521
+ return await this.services.sessionFork({
522
+ sourceSessionId,
523
+ operationId: `${this.principalId}:${operationId}`,
524
+ ...(title ? { title } : {}),
525
+ });
446
526
  }
447
527
  setSessionTitle(params) {
448
528
  const input = record(params);
@@ -466,7 +546,7 @@ export class PluginHostRpc {
466
546
  this.ownedSession(sessionId);
467
547
  const ttlSeconds = optionalPositiveNumber(input.ttlSeconds, "ttlSeconds");
468
548
  return createControlLink({
469
- pluginId: this.pluginId,
549
+ pluginId: this.principalId,
470
550
  sessionId,
471
551
  ...(ttlSeconds === undefined ? {} : { ttlMs: ttlSeconds * 1_000 }),
472
552
  });
@@ -479,9 +559,24 @@ export class PluginHostRpc {
479
559
  return session;
480
560
  }
481
561
  isOwned(session) {
482
- return session.source === `plugin:${this.pluginId}`;
562
+ const seen = new Set();
563
+ let current = session;
564
+ while (current && !seen.has(current.id)) {
565
+ if (current.source === `plugin:${this.principalId}`)
566
+ return true;
567
+ seen.add(current.id);
568
+ current = current.forkedFromSessionId
569
+ ? this.services.sessionRegistry.get(current.forkedFromSessionId)
570
+ : undefined;
571
+ }
572
+ return false;
483
573
  }
484
574
  }
575
+ function throwIfHostCallAborted(signal) {
576
+ if (!signal?.aborted)
577
+ return;
578
+ throw new PluginHostCallError("REQUEST_CANCELLED", "plugin host request was cancelled");
579
+ }
485
580
  export class PluginHostCallError extends PluginRunnerError {
486
581
  retryable;
487
582
  data;
@@ -512,17 +607,32 @@ function toHostCallError(error) {
512
607
  }
513
608
  return new PluginHostCallError("HOST_CALL_FAILED", error instanceof Error ? error.message : String(error));
514
609
  }
515
- function sessionLaunchMeta(value) {
610
+ function sessionLaunchSelection(value) {
516
611
  const input = record(value);
517
612
  assertJsonValue(input, "session");
613
+ const projectId = optionalString(input.projectId, "session.projectId", 512);
614
+ const managedCwd = optionalString(input.managedCwd, "session.managedCwd", 16_384);
518
615
  const agent = optionalAgentId(input.agent, "session.agent");
519
616
  const config = input.config === undefined ? undefined : agentSpecSchema.parse(input.config);
617
+ const provider = optionalRuntimeValue(input.provider);
520
618
  const model = optionalString(input.model, "session.model", 256);
521
619
  const reasoningEffort = optionalString(input.reasoningEffort, "session.reasoningEffort", 128);
522
620
  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.managedCwd are mutually exclusive");
623
+ }
624
+ if (Number(Boolean(agent)) +
625
+ Number(Boolean(config)) +
626
+ Number(Boolean(provider)) >
627
+ 1) {
628
+ throw new PluginHostCallError("INVALID_PARAMS", "session.agent, session.config, and session.provider are mutually exclusive");
629
+ }
523
630
  return {
631
+ ...(projectId ? { projectId } : {}),
632
+ ...(managedCwd ? { managedCwd } : {}),
524
633
  ...(agent ? { agent } : {}),
525
634
  ...(config ? { config } : {}),
635
+ ...(provider ? { provider } : {}),
526
636
  ...(model ? { model } : {}),
527
637
  ...(reasoningEffort ? { reasoningEffort } : {}),
528
638
  ...(title ? { title } : {}),