@rynx-ai/daemon 0.1.11-beta.52 → 0.1.11-beta.54

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rynx-ai/plugin-channel-lark",
3
- "version": "0.1.11-beta.52",
3
+ "version": "0.1.11-beta.54",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/rynx-ai/rynx.git",
@@ -2,9 +2,8 @@ import { chmodSync, copyFileSync, existsSync, lstatSync, mkdirSync, mkdtempSync,
2
2
  import { fileURLToPath } from "node:url";
3
3
  import { isAbsolute, join } from "node:path";
4
4
  import { rynxHome } from "@rynx-ai/core";
5
- import { removePluginRuntimeLease } from "@rynx-ai/plugin-sdk";
6
5
  import { pluginDataDir, preparePluginInstallation, } from "./plugin-installer.js";
7
- import { PLUGIN_HOST_RUNTIME_LEASE_IDS } from "./plugin-host-leases.js";
6
+ import { reclaimPluginHostLeases as reclaimHostLeases } from "./plugin-host-leases.js";
8
7
  import { addPluginMarketplace, resolveMarketplacePlugin, } from "./plugin-marketplace.js";
9
8
  import { DEFAULT_BUNDLED_PLUGINS, OFFICIAL_MARKETPLACE_ID, } from "./official-plugins.js";
10
9
  import { getPlugin, setPluginEnabled } from "./plugin-store.js";
@@ -64,9 +63,7 @@ function reclaimPluginHostLeases(identity) {
64
63
  if (!installed)
65
64
  return;
66
65
  const dataDir = pluginDataDir(installed.marketplaceId, installed.pluginId);
67
- for (const leaseId of PLUGIN_HOST_RUNTIME_LEASE_IDS) {
68
- removePluginRuntimeLease(dataDir, leaseId);
69
- }
66
+ reclaimHostLeases(dataDir);
70
67
  }
71
68
  function syncBundledMarketplace(bundleDirectory) {
72
69
  const distributionRoot = join(rynxHome(), "distribution");
@@ -1,3 +1,4 @@
1
+ import { toMarketplaceItem } from "./plugin-management.js";
1
2
  /**
2
3
  * Shared daemon composition root.
3
4
  *
@@ -120,16 +121,6 @@ async function assertWorkspaceDirectories(directories) {
120
121
  throw new MachineSessionServiceFailure("failed_precondition", "one or more Project directories are unavailable");
121
122
  }
122
123
  }
123
- function toMarketplaceItem(record) {
124
- return {
125
- id: record.marketplaceId,
126
- source: record.sourceType,
127
- builtIn: record.builtIn,
128
- plugins: record.catalog.plugins.map((plugin) => ({ id: plugin.id })),
129
- resolvedRevision: record.resolvedRevision,
130
- refreshedAt: record.refreshedAt,
131
- };
132
- }
133
124
  export function daemonActivityBlocksShutdown(activity) {
134
125
  return activity.runningTurns > 0
135
126
  || activity.pendingInteractions > 0
@@ -1,4 +1,11 @@
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. */
1
+ /** Fixed records owned by the resident daemon, not independent process locks. */
4
2
  export declare const PLUGIN_HOST_RUNTIME_LEASE_IDS: readonly ["host-runtime", "host-settings"];
3
+ type HostLeaseId = typeof PLUGIN_HOST_RUNTIME_LEASE_IDS[number];
4
+ export declare function removePluginHostLease(dataDir: string, id: HostLeaseId): void;
5
+ /** The caller owns RYNX_HOME exclusively, or has stopped this plugin under the
6
+ * supervisor's queue. Take over only the previous host's fixed records. Missing
7
+ * PID metadata must not prevent startup or offline repair. Plugin-owned and
8
+ * independent CLI leases continue to protect their installation directories.
9
+ * This does not terminate processes: their lifetime is managed separately. */
10
+ export declare function reclaimPluginHostLeases(dataDir: string, only?: HostLeaseId): void;
11
+ export {};
@@ -1,7 +1,21 @@
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
+ import { rmSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { removePluginRuntimeLease } from "@rynx-ai/plugin-sdk";
4
+ /** Fixed records owned by the resident daemon, not independent process locks. */
5
+ export const PLUGIN_HOST_RUNTIME_LEASE_IDS = ["host-runtime", "host-settings"];
6
+ export function removePluginHostLease(dataDir, id) {
7
+ removePluginRuntimeLease(dataDir, id);
8
+ // Remove diagnostics written by the earlier recovery implementation too.
9
+ rmSync(join(dataDir, `.${id}-process.json`), { force: true });
10
+ }
11
+ /** The caller owns RYNX_HOME exclusively, or has stopped this plugin under the
12
+ * supervisor's queue. Take over only the previous host's fixed records. Missing
13
+ * PID metadata must not prevent startup or offline repair. Plugin-owned and
14
+ * independent CLI leases continue to protect their installation directories.
15
+ * This does not terminate processes: their lifetime is managed separately. */
16
+ export function reclaimPluginHostLeases(dataDir, only) {
17
+ for (const id of PLUGIN_HOST_RUNTIME_LEASE_IDS) {
18
+ if (!only || id === only)
19
+ removePluginHostLease(dataDir, id);
20
+ }
21
+ }
@@ -9,7 +9,7 @@ import { PluginRunnerError } from "@rynx-ai/plugin-runner";
9
9
  import { ensurePluginSessionLaunch, replayPluginSessionLaunch, SessionLaunchOperationConflictError, SessionLaunchOperationTargetDeletedError, } from "./session-launch-operations.js";
10
10
  import { createControlLink } from "./control-link-store.js";
11
11
  import { nodeLogFiles, sessionLogFiles } from "./log-files.js";
12
- import { issueSessionPortalTicket, revokeSessionPortalGrant, } from "./session-portal-grant-store.js";
12
+ import { issueSessionPortalTicket, revokeSessionPortalGrant, renewSessionPortalGrant, } from "./session-portal-grant-store.js";
13
13
  import { claimSessionTimelineProjection, completeSessionTimelineProjection, } from "./session-timeline-projection-store.js";
14
14
  const MAX_MESSAGE_LENGTH = 1_000_000;
15
15
  /** Leaves ample room for the RPC envelope below the runner's 1 MiB line cap. */
@@ -134,6 +134,7 @@ export class PluginHostRpc {
134
134
  ? {
135
135
  sessionPortalEmbedOrigin: true,
136
136
  sessionPortalGrantRevocation: true,
137
+ sessionPortalGrantRenewal: true,
137
138
  }
138
139
  : {}),
139
140
  },
@@ -251,6 +252,33 @@ export class PluginHostRpc {
251
252
  this.ownedSession(sessionId);
252
253
  return this.issueSessionPortalLink("single_session", requiredSessionPortalAccess(input), sessionId, optionalSessionPortalEmbedOrigin(input.embedOrigin), optionalString(input.grantHandle, "grantHandle", 128));
253
254
  }
255
+ case "session.portal.grant.renew": {
256
+ const input = record(params);
257
+ const scope = input.scope;
258
+ if (scope !== "single_session" && scope !== "plugin_sessions") {
259
+ throw new PluginHostCallError("INVALID_PARAMS", "scope must be single_session or plugin_sessions");
260
+ }
261
+ const sessionId = scope === "single_session" ? requiredSessionId(input) : undefined;
262
+ if (sessionId)
263
+ this.ownedSession(sessionId);
264
+ else if (input.sessionId !== undefined) {
265
+ throw new PluginHostCallError("INVALID_PARAMS", "plugin_sessions scope cannot specify a Session");
266
+ }
267
+ const embedOrigin = optionalSessionPortalEmbedOrigin(input.embedOrigin);
268
+ if (!embedOrigin)
269
+ throw new PluginHostCallError("INVALID_PARAMS", "embedOrigin is required for renewal");
270
+ const renewed = renewSessionPortalGrant({
271
+ pluginId: this.principalId,
272
+ grantHandle: requiredString(input.grantHandle, "grantHandle", 128),
273
+ scope,
274
+ access: requiredSessionPortalAccess(input),
275
+ sessionId,
276
+ embedOrigin,
277
+ });
278
+ if (!renewed)
279
+ throw new PluginHostCallError("HOST_CALL_FAILED", "Session Portal grant expired, revoked, or does not match the authorized scope");
280
+ return renewed;
281
+ }
254
282
  case "session.portal.grant.revoke": {
255
283
  const grantHandle = requiredString(record(params).grantHandle, "grantHandle", 128);
256
284
  return revokeSessionPortalGrant({
@@ -50,3 +50,4 @@ export declare function pruneInactivePluginInstallations(): string[];
50
50
  export declare function pluginDataDir(marketplaceId: string, pluginId: string): string;
51
51
  export declare function pluginRuntimeCacheDir(marketplaceId: string, pluginId: string, integrity: string): string;
52
52
  export declare function marketplaceStorageSegment(marketplaceId: string): string;
53
+ export declare function assertNoActiveLeases(record: PluginRecord | undefined): void;
@@ -270,7 +270,7 @@ function ensureStorageRoots() {
270
270
  mkdirSync(stagingRoot, { recursive: true, mode: 0o700 });
271
271
  return { cacheRoot, stagingRoot };
272
272
  }
273
- function assertNoActiveLeases(record) {
273
+ export function assertNoActiveLeases(record) {
274
274
  if (!record)
275
275
  return;
276
276
  const dataDir = pluginDataDir(record.marketplaceId, record.pluginId);
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,19 @@
1
+ import { capturePluginRecovery, restorePluginRecovery, inspectPluginInstallation } from "./plugin-maintenance.js";
2
+ async function main(args) {
3
+ if (args[0] === "capabilities")
4
+ return { recoveryFormat: 1, minimumNode: "22.16.0", offlineManagement: 1 };
5
+ if (args[0] === "inspect" && args.length === 2)
6
+ return inspectPluginInstallation(args[1]);
7
+ if (args[0] === "capture" && (args.length === 3 || args.length === 5)) {
8
+ return capturePluginRecovery(args[1], args[2], {
9
+ marketPackage: args[3], retainedMarketPackage: args[4],
10
+ });
11
+ }
12
+ if (args[0] === "restore" && args.length === 2)
13
+ return restorePluginRecovery(args[1]);
14
+ throw new Error("Usage: node plugin-maintenance.mjs capabilities | capture <plugin@market> <backup-directory> [<current-market-package> <retained-market-package>] | restore <backup-directory>. Run capture/restore after stopping Rynx; recovery restores the plugin enabled.");
15
+ }
16
+ main(process.argv.slice(2)).then((result) => console.log(JSON.stringify(result))).catch((error) => {
17
+ console.error(error instanceof Error ? error.message : String(error));
18
+ process.exitCode = 1;
19
+ });
@@ -0,0 +1,21 @@
1
+ export declare function capturePluginRecovery(identity: string, directory: string, options?: {
2
+ marketPackage?: string;
3
+ retainedMarketPackage?: string;
4
+ }): Promise<{
5
+ version: string;
6
+ digest: string;
7
+ }>;
8
+ export declare function restorePluginRecovery(directory: string): Promise<{
9
+ version: string;
10
+ digest: string;
11
+ enabled: true;
12
+ }>;
13
+ /** Read-only version/integrity evidence, also usable with historical CLIs. */
14
+ export declare function inspectPluginInstallation(identity: string): Promise<{
15
+ id: string;
16
+ version: string | number | null;
17
+ digest: string | number | null;
18
+ enabled: boolean;
19
+ runtimeState: string | number | null;
20
+ runtimeError: string | number | null;
21
+ }>;
@@ -0,0 +1,189 @@
1
+ /** Rynx-owned recovery format. This entry never migrates the daemon database. */
2
+ import { cpSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
3
+ import { dirname, join, resolve } from "node:path";
4
+ import { DatabaseSync } from "node:sqlite";
5
+ import { acquireDaemonOwner } from "./daemon-owner.js";
6
+ import { dbPath } from "./db.js";
7
+ import { assertNoActiveLeases, pluginDataDir } from "./plugin-installer.js";
8
+ import { reclaimPluginHostLeases } from "./plugin-host-leases.js";
9
+ import { validatePluginPackageSnapshot } from "./plugin-package.js";
10
+ const columns = {
11
+ plugin_installations: "canonical_id marketplace_id plugin_id source_type requested_source resolved_revision source_fingerprint version enabled installed_at updated_at install_path manifest integrity runtime_state runtime_error".split(" "),
12
+ plugin_marketplaces: "marketplace_id source_type requested_source resolved_revision source_fingerprint built_in catalog created_at refreshed_at".split(" "),
13
+ plugin_marketplace_bindings: "marketplace_id source_fingerprint created_at".split(" "),
14
+ };
15
+ function openExisting(readOnly = false) {
16
+ if (!existsSync(dbPath()))
17
+ throw new Error("Rynx database does not exist; use plugin install for a new installation");
18
+ const connection = new DatabaseSync(dbPath(), { timeout: 5000, readOnly });
19
+ try {
20
+ for (const [table, expected] of Object.entries(columns)) {
21
+ const actual = connection.prepare(`PRAGMA table_info(${table})`).all().map((row) => row.name).sort();
22
+ if (JSON.stringify(actual) !== JSON.stringify([...expected].sort())) {
23
+ throw new Error(`Unsupported recovery schema for ${table}; keep the original CLI and installation unchanged`);
24
+ }
25
+ }
26
+ return connection;
27
+ }
28
+ catch (error) {
29
+ connection.close();
30
+ throw error;
31
+ }
32
+ }
33
+ function checkLeases(row) {
34
+ const record = {
35
+ canonicalId: row.canonical_id, marketplaceId: row.marketplace_id,
36
+ pluginId: row.plugin_id, installPath: row.install_path,
37
+ };
38
+ reclaimPluginHostLeases(pluginDataDir(record.marketplaceId, record.pluginId));
39
+ assertNoActiveLeases(record);
40
+ }
41
+ async function checkPackage(row, directory) {
42
+ const digest = await validatePluginPackageSnapshot(directory);
43
+ if (digest !== row.integrity)
44
+ throw new Error(`Recovery package digest differs from installed ${row.canonical_id}`);
45
+ }
46
+ function atomicJson(file, data) {
47
+ const temporary = `${file}.tmp-${process.pid}`;
48
+ writeFileSync(temporary, JSON.stringify(data, null, 2) + "\n", { mode: 0o600 });
49
+ renameSync(temporary, file);
50
+ }
51
+ export async function capturePluginRecovery(identity, directory, options = {}) {
52
+ const owner = await acquireDaemonOwner();
53
+ let connection;
54
+ try {
55
+ connection = openExisting();
56
+ const installation = connection.prepare("SELECT * FROM plugin_installations WHERE canonical_id = ?").get(identity);
57
+ if (!installation)
58
+ throw new Error(`Plugin ${identity} is not installed`);
59
+ checkLeases(installation);
60
+ await checkPackage(installation, String(installation.install_path));
61
+ mkdirSync(directory, { recursive: true, mode: 0o700 });
62
+ if (existsSync(join(directory, "snapshot.json")))
63
+ throw new Error("Recovery snapshot already exists; preserve it and use a new attempt directory");
64
+ let packagePath = join(resolve(directory), "package");
65
+ let reuseMarket = false;
66
+ if (options.marketPackage && options.retainedMarketPackage) {
67
+ try {
68
+ await checkPackage(installation, options.marketPackage);
69
+ reuseMarket = true;
70
+ }
71
+ catch { }
72
+ }
73
+ if (reuseMarket)
74
+ packagePath = resolve(options.retainedMarketPackage);
75
+ else
76
+ cpSync(String(installation.install_path), packagePath, { recursive: true, errorOnExist: true, force: false });
77
+ const snapshot = {
78
+ schemaVersion: 1, database: dbPath(), installation, packagePath,
79
+ ...(reuseMarket ? { alternatePackagePath: resolve(options.marketPackage) } : {}),
80
+ marketplace: connection.prepare("SELECT * FROM plugin_marketplaces WHERE marketplace_id = ?").get(installation.marketplace_id) ?? null,
81
+ binding: connection.prepare("SELECT * FROM plugin_marketplace_bindings WHERE marketplace_id = ?").get(installation.marketplace_id) ?? null,
82
+ };
83
+ atomicJson(join(directory, "snapshot.json"), snapshot);
84
+ return { version: String(installation.version), digest: String(installation.integrity) };
85
+ }
86
+ finally {
87
+ connection?.close();
88
+ await owner.release();
89
+ }
90
+ }
91
+ export async function restorePluginRecovery(directory) {
92
+ const owner = await acquireDaemonOwner();
93
+ let connection;
94
+ try {
95
+ connection = openExisting();
96
+ const snapshot = JSON.parse(readFileSync(join(directory, "snapshot.json"), "utf8"));
97
+ if (snapshot.schemaVersion !== 1 || snapshot.database !== dbPath())
98
+ throw new Error("Recovery snapshot belongs to a different format or Rynx database");
99
+ for (const [key, table] of [["installation", "plugin_installations"], ["marketplace", "plugin_marketplaces"], ["binding", "plugin_marketplace_bindings"]]) {
100
+ const row = snapshot[key];
101
+ if (!row && key !== "installation")
102
+ continue;
103
+ if (!row || JSON.stringify(Object.keys(row).sort()) !== JSON.stringify([...columns[table]].sort()))
104
+ throw new Error(`Invalid recovery ${key}`);
105
+ }
106
+ const original = snapshot.installation;
107
+ const current = connection.prepare("SELECT * FROM plugin_installations WHERE canonical_id = ?").get(original.canonical_id);
108
+ if (current)
109
+ checkLeases(current);
110
+ checkLeases(original);
111
+ let packagePath = snapshot.packagePath;
112
+ try {
113
+ await checkPackage(original, packagePath);
114
+ }
115
+ catch (error) {
116
+ if (!snapshot.alternatePackagePath)
117
+ throw error;
118
+ packagePath = snapshot.alternatePackagePath;
119
+ await checkPackage(original, packagePath);
120
+ }
121
+ atomicJson(join(directory, "restore-progress.json"), { phase: "package-verified", at: new Date().toISOString() });
122
+ const destination = String(original.install_path);
123
+ let intact = false;
124
+ try {
125
+ await checkPackage(original, destination);
126
+ intact = true;
127
+ }
128
+ catch { }
129
+ if (!intact) {
130
+ mkdirSync(dirname(destination), { recursive: true, mode: 0o700 });
131
+ const staged = `${destination}.recover-${process.pid}`;
132
+ try {
133
+ cpSync(packagePath, staged, { recursive: true, errorOnExist: true, force: false });
134
+ await checkPackage(original, staged);
135
+ if (existsSync(destination))
136
+ renameSync(destination, `${destination}.damaged-${Date.now()}`);
137
+ renameSync(staged, destination);
138
+ }
139
+ finally {
140
+ rmSync(staged, { recursive: true, force: true });
141
+ }
142
+ }
143
+ connection.exec("BEGIN IMMEDIATE");
144
+ try {
145
+ for (const [table, value] of [
146
+ ["plugin_marketplace_bindings", snapshot.binding],
147
+ ["plugin_marketplaces", snapshot.marketplace],
148
+ ["plugin_installations", { ...original, enabled: 1, runtime_state: "stopped", runtime_error: null }],
149
+ ]) {
150
+ const primary = table === "plugin_installations" ? "canonical_id" : "marketplace_id";
151
+ // Only this plugin and its existing Market registration are restored.
152
+ // No session, private plugin data, or unrelated table is copied back.
153
+ if (!value) {
154
+ connection.prepare(`DELETE FROM ${table} WHERE ${primary} = ?`).run(original.marketplace_id);
155
+ continue;
156
+ }
157
+ const keys = [...columns[table]];
158
+ connection.prepare(`INSERT INTO ${table} (${keys.join(",")}) VALUES (${keys.map(() => "?").join(",")}) ON CONFLICT(${primary}) DO UPDATE SET ${keys.filter((key) => key !== primary).map((key) => `${key}=excluded.${key}`).join(",")}`)
159
+ .run(...keys.map((key) => value[key]));
160
+ }
161
+ connection.exec("COMMIT");
162
+ }
163
+ catch (error) {
164
+ connection.exec("ROLLBACK");
165
+ throw error;
166
+ }
167
+ atomicJson(join(directory, "restore-progress.json"), { phase: "installation-restored", at: new Date().toISOString() });
168
+ return { version: String(original.version), digest: String(original.integrity), enabled: true };
169
+ }
170
+ finally {
171
+ connection?.close();
172
+ await owner.release();
173
+ }
174
+ }
175
+ /** Read-only version/integrity evidence, also usable with historical CLIs. */
176
+ export async function inspectPluginInstallation(identity) {
177
+ const connection = openExisting(true);
178
+ try {
179
+ const row = connection.prepare("SELECT * FROM plugin_installations WHERE canonical_id = ?").get(identity);
180
+ if (!row)
181
+ throw new Error(`Plugin ${identity} is not installed`);
182
+ await checkPackage(row, String(row.install_path));
183
+ return { id: identity, version: row.version, digest: row.integrity, enabled: row.enabled === 1,
184
+ runtimeState: row.runtime_state, runtimeError: row.runtime_error };
185
+ }
186
+ finally {
187
+ connection.close();
188
+ }
189
+ }