@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,81 +1,30 @@
1
- /**
2
- * Transactional plugin installation.
3
- *
4
- * Packages are first materialized in a host-owned staging directory. The
5
- * installer reads only static JSON metadata, validates every declared entry,
6
- * hashes the package, and atomically renames the validated tree into an
7
- * immutable installation path. Plugin code is never imported here.
8
- */
9
- import { randomUUID } from "node:crypto";
10
- import { existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, writeFileSync, } from "node:fs";
11
- import { basename, dirname, join, resolve } from "node:path";
12
- import { PLUGIN_ID_PATTERN, pluginCliInvocationLeasePid, readPluginRuntimeLeases, removePluginRuntimeLease, } from "@rynx-ai/plugin-sdk";
1
+ /** Transactional, package-manager-neutral plugin installation. */
2
+ import { existsSync, lstatSync, mkdirSync, mkdtempSync, readdirSync, realpathSync, renameSync, rmSync, } from "node:fs";
3
+ import { join, resolve } from "node:path";
4
+ import { MARKETPLACE_ID_PATTERN, pluginCliInvocationLeasePid, readPluginRuntimeLeases, removePluginRuntimeLease, } from "@rynx-ai/plugin-sdk";
13
5
  import { rynxHome } from "@rynx-ai/core";
14
- import { getPlugin, isRunnablePluginRecord, removePlugin, upsertPlugin, } from "./plugin-store.js";
15
- import { detectPluginSource, inspectPluginPackage, materializePluginPackage, normalizePluginSpec, rebuildPluginPackage, } from "./plugin-package.js";
16
- import { pluginsDir } from "./registry.js";
17
6
  import { db } from "./db.js";
18
- const INSTALLATION_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
19
- const STAGING_TRANSACTION_PATTERN = /^install-[A-Za-z0-9]{6}$/;
20
- const STAGING_OWNER_FILE = ".rynx-staging-owner.json";
21
- const PROMOTION_MARKER_SUFFIX = ".rynx-promoted";
22
- const MAX_HOST_MARKER_BYTES = 4 * 1024;
23
- const DEFAULT_STAGING_GC_AGE_MS = 24 * 60 * 60 * 1_000;
24
- /**
25
- * Install or replace one arbitrary npm/local/git/URL plugin package.
26
- *
27
- * A failed validation, materialization, atomic move, or registry update leaves
28
- * the previously registered installation untouched.
29
- */
7
+ import { isBuiltInPluginMarketplace } from "./plugin-marketplace.js";
8
+ import { detectPluginSource, inspectPluginPackage, materializePluginPackage, } from "./plugin-package.js";
9
+ import { canonicalPluginId, getPlugin, removePlugin, upsertPlugin, } from "./plugin-store.js";
30
10
  export async function installPlugin(spec, onProgress = () => { }, opts = {}) {
31
11
  const prepared = await preparePluginInstallation(spec, onProgress, opts);
32
12
  try {
33
- if (opts.expectedDigest !== undefined &&
34
- !/^sha256-[A-Za-z0-9+/]{43}={0,2}$/.test(opts.expectedDigest)) {
35
- throw new Error("expected plugin digest is invalid");
13
+ if (opts.expectedDigest && opts.expectedDigest !== prepared.integrity) {
14
+ throw new Error(`prepared plugin digest does not match ${opts.expectedDigest}`);
36
15
  }
37
- if (opts.expectedDigest !== undefined &&
38
- opts.expectedDigest !== prepared.integrity) {
39
- throw new Error(`prepared plugin digest ${prepared.integrity} does not match ` +
40
- `expected digest ${opts.expectedDigest}`);
41
- }
42
- if (prepared.manifest.requiresBuildScripts) {
43
- if (!opts.allowScripts) {
44
- throw new Error(`plugin "${prepared.name}" requires package build scripts; review digest ` +
45
- `${prepared.integrity} and re-run with --allow-scripts ` +
46
- `--expect-digest ${prepared.integrity}`);
47
- }
48
- if (opts.expectedDigest === undefined) {
49
- throw new Error(`build-script approval must be bound to the prepared artifact; ` +
50
- `re-run with --expect-digest ${prepared.integrity}`);
51
- }
52
- }
53
- const existing = prepared.existing;
54
- if (existing && opts.onConflict && !(await opts.onConflict(existing))) {
16
+ if (prepared.existing && opts.onConflict && !(await opts.onConflict(prepared.existing))) {
55
17
  return {
56
- name: prepared.name,
18
+ name: prepared.canonicalId,
19
+ canonicalId: prepared.canonicalId,
20
+ marketplaceId: prepared.marketplaceId,
21
+ pluginId: prepared.pluginId,
57
22
  source: prepared.source,
58
- ...(prepared.version ? { version: prepared.version } : {}),
23
+ version: prepared.version,
59
24
  skipped: true,
60
25
  };
61
26
  }
62
- if (opts.signal?.aborted)
63
- throw new Error("plugin installation aborted");
64
- const requested = new Set(prepared.manifest.requestedCapabilities);
65
- if (!opts.approveCapabilities &&
66
- opts.grants === undefined &&
67
- !existing &&
68
- requested.size > 0) {
69
- throw new Error(`plugin "${prepared.name}" requests host capabilities; explicit grants or approval are required`);
70
- }
71
- const grants = opts.approveCapabilities
72
- ? await opts.approveCapabilities(prepared.manifest, existing)
73
- : opts.grants ??
74
- (existing
75
- ? existing.grants.filter((capability) => requested.has(capability))
76
- : []);
77
27
  return await prepared.commit({
78
- grants,
79
28
  expectedDigest: opts.expectedDigest ?? prepared.integrity,
80
29
  signal: opts.signal,
81
30
  });
@@ -84,163 +33,124 @@ export async function installPlugin(spec, onProgress = () => { }, opts = {}) {
84
33
  prepared.discard();
85
34
  }
86
35
  }
87
- /**
88
- * Materialize, build (only when explicitly allowed), inspect, and hash one
89
- * plugin into host-owned staging without mutating the registry.
90
- */
91
36
  export async function preparePluginInstallation(spec, onProgress = () => { }, opts = {}) {
92
37
  if (!spec.trim())
93
- throw new Error("plugin spec must not be empty");
94
- const progress = safeProgress(onProgress);
38
+ throw new Error("plugin source must not be empty");
39
+ opts.signal?.throwIfAborted();
95
40
  const source = detectPluginSource(spec);
96
- const storedSpec = normalizePluginSpec(spec, source);
97
41
  const storage = ensureStorageRoots();
98
42
  const transactionDir = mkdtempSync(join(storage.stagingRoot, "install-"));
99
- try {
100
- writeStagingOwner(transactionDir);
101
- }
102
- catch (error) {
103
- rmSync(transactionDir, { recursive: true, force: true });
104
- throw error;
105
- }
106
43
  const stagedPackage = join(transactionDir, "package");
107
- let finalPath;
108
- let registryUpdated = false;
109
44
  let consumed = false;
45
+ let promotedPath;
46
+ let promotedByThisTransaction = false;
110
47
  try {
111
- await (opts.materializer ?? materializePluginPackage)({
112
- spec: storedSpec,
48
+ const materialized = await (opts.materializer ?? materializePluginPackage)({
49
+ spec,
113
50
  source,
114
51
  destination: stagedPackage,
115
- onProgress: progress,
52
+ onProgress: safeProgress(onProgress),
116
53
  signal: opts.signal,
117
54
  });
118
- let inspected = await inspectPluginPackage(stagedPackage, {
119
- requireEntries: false,
120
- signal: opts.signal,
121
- });
122
- if (opts.allowScripts && !inspected.manifest.requiresBuildScripts) {
123
- throw new Error(`plugin "${inspected.manifest.id}" does not declare requiresBuildScripts; ` +
124
- "refusing unnecessary --allow-scripts approval");
55
+ validateMaterialized(materialized);
56
+ const inspected = await inspectPluginPackage(stagedPackage, { signal: opts.signal });
57
+ if (opts.expectedId && inspected.manifest.id !== opts.expectedId) {
58
+ throw new Error(`plugin update id mismatch: expected "${opts.expectedId}", received "${inspected.manifest.id}"`);
125
59
  }
126
- const requiresBuildScripts = Boolean(inspected.manifest.requiresBuildScripts);
127
- if (!requiresBuildScripts) {
128
- inspected = await inspectPluginPackage(stagedPackage, { signal: opts.signal });
60
+ const marketplaceId = opts.marketplaceId ?? `direct-${materialized.sourceFingerprint}`;
61
+ if (!MARKETPLACE_ID_PATTERN.test(marketplaceId)) {
62
+ throw new Error(`invalid marketplace id "${marketplaceId}"`);
129
63
  }
130
- const name = inspected.manifest.id;
131
- if (opts.expectedId && name !== opts.expectedId) {
132
- throw new Error(`plugin update id mismatch: expected "${opts.expectedId}", received "${name}"`);
64
+ const canonicalId = canonicalPluginId(inspected.manifest.id, marketplaceId);
65
+ if (opts.expectedIdentity && opts.expectedIdentity !== canonicalId) {
66
+ throw new Error(`plugin update identity mismatch: expected "${opts.expectedIdentity}", received "${canonicalId}"`);
133
67
  }
134
- const existing = getPlugin(name);
68
+ const existing = getPlugin(canonicalId);
69
+ const manifest = cloneManifest(inspected.manifest);
135
70
  const cleanup = () => {
136
- if (!registryUpdated && finalPath) {
137
- removeOwnedInstallation(storage.installationsRoot, basename(dirname(finalPath)), finalPath);
138
- }
139
- try {
140
- rmSync(transactionDir, { recursive: true, force: true });
141
- }
142
- catch {
143
- /* orphaned staging is inactive and can be collected later */
71
+ rmSync(transactionDir, { recursive: true, force: true });
72
+ if (promotedByThisTransaction && promotedPath && !getPlugin(canonicalId)) {
73
+ rmSync(promotedPath, { recursive: true, force: true });
144
74
  }
145
75
  };
146
- const preparedManifest = cloneManifest(inspected.manifest);
147
- const preparedExisting = existing ? clonePluginRecord(existing) : undefined;
148
76
  return {
149
- name,
77
+ name: canonicalId,
78
+ canonicalId,
79
+ marketplaceId,
80
+ pluginId: manifest.id,
150
81
  source,
151
- ...(inspected.version ? { version: inspected.version } : {}),
152
- manifest: cloneManifest(preparedManifest),
82
+ version: manifest.version,
83
+ manifest: cloneManifest(manifest),
153
84
  integrity: inspected.integrity,
154
- ...(preparedExisting ? { existing: clonePluginRecord(preparedExisting) } : {}),
155
- commit: async ({ grants, expectedDigest, signal }) => {
85
+ requestedSource: structuredClone(materialized.requestedSource),
86
+ resolvedRevision: materialized.resolvedRevision,
87
+ sourceFingerprint: materialized.sourceFingerprint,
88
+ ...(existing ? { existing: clonePluginRecord(existing) } : {}),
89
+ commit: async ({ expectedDigest, signal }) => {
156
90
  if (consumed)
157
91
  throw new Error("prepared plugin installation was already consumed");
158
92
  consumed = true;
159
93
  try {
160
- if (signal?.aborted)
161
- throw new Error("plugin installation aborted");
94
+ signal?.throwIfAborted();
162
95
  if (expectedDigest !== inspected.integrity) {
163
96
  throw new Error("prepared plugin digest does not match expected digest");
164
97
  }
165
- const rechecked = await inspectPluginPackage(stagedPackage, {
166
- ...(requiresBuildScripts ? { requireEntries: false } : {}),
167
- signal,
168
- });
98
+ const rechecked = await inspectPluginPackage(stagedPackage, { signal });
169
99
  if (rechecked.integrity !== inspected.integrity ||
170
- rechecked.version !== inspected.version ||
171
- JSON.stringify(rechecked.manifest) !== JSON.stringify(preparedManifest)) {
172
- throw new Error("prepared plugin artifact changed after approval");
100
+ JSON.stringify(rechecked.manifest) !== JSON.stringify(manifest)) {
101
+ throw new Error("prepared plugin artifact changed after preparation");
173
102
  }
174
- let promoted = rechecked;
175
- if (requiresBuildScripts) {
176
- if (!opts.allowScripts) {
177
- throw new Error(`plugin "${name}" requires package build scripts; ` +
178
- `approve digest ${inspected.integrity} before running scripts`);
179
- }
180
- progress(`running digest-approved build scripts for plugin "${name}"`);
181
- await rebuildPluginPackage(stagedPackage, progress, signal);
182
- const built = await inspectPluginPackage(stagedPackage, { signal });
183
- if (JSON.stringify(built.manifest) !== JSON.stringify(preparedManifest) ||
184
- built.version !== inspected.version) {
185
- throw new Error("plugin build scripts changed static package identity or manifest metadata");
186
- }
187
- promoted = built;
188
- }
189
- const requested = new Set(preparedManifest.requestedCapabilities);
190
- for (const grant of grants) {
191
- if (!requested.has(grant)) {
192
- throw new Error(`plugin capability was not requested: ${grant}`);
103
+ const target = pluginCachePath(marketplaceId, manifest.id, inspected.integrity, storage.cacheRoot);
104
+ mkdirSync(join(storage.cacheRoot, marketplaceStorageSegment(marketplaceId), manifest.id), {
105
+ recursive: true,
106
+ mode: 0o700,
107
+ });
108
+ if (existsSync(target)) {
109
+ const cached = await inspectPluginPackage(target, { signal });
110
+ if (cached.integrity !== inspected.integrity) {
111
+ throw new Error("plugin cache integrity collision");
193
112
  }
113
+ rmSync(stagedPackage, { recursive: true, force: true });
194
114
  }
195
- if (new Set(grants).size !== grants.length) {
196
- throw new Error("plugin grants must be unique");
115
+ else {
116
+ renameSync(stagedPackage, target);
117
+ promotedByThisTransaction = true;
197
118
  }
119
+ promotedPath = realpathSync(target);
198
120
  db().transaction(() => {
199
- const current = getPlugin(name);
121
+ const current = getPlugin(canonicalId);
200
122
  if (!samePluginGeneration(existing, current)) {
201
- throw new Error(`plugin "${name}" changed concurrently; retry the installation`);
202
- }
203
- const leases = activeRuntimeLeases(name, storage.installationsRoot);
204
- if (leases.length > 0) {
205
- throw new Error(`plugin "${name}" has active runtime lease(s): ${leases
206
- .map((lease) => lease.leaseId)
207
- .join(", ")}; stop its managed services or CLI commands before updating`);
123
+ throw new Error(`plugin "${canonicalId}" changed concurrently; retry`);
208
124
  }
209
- const idRoot = ensureDirectDirectory(storage.installationsRoot, name, "plugin id directory");
210
- const installationId = randomUUID();
211
- finalPath = join(idRoot, installationId);
212
- const marker = promotionMarkerPath(idRoot, installationId);
213
- writePromotionMarker(marker);
214
- try {
215
- renameSync(stagedPackage, finalPath);
216
- }
217
- catch (error) {
218
- rmSync(marker, { force: true });
219
- finalPath = undefined;
220
- throw error;
221
- }
222
- progress(`staged validated plugin "${name}" → ${finalPath}`);
125
+ // Keep the generation check and lease check in the same synchronous
126
+ // transaction as the registry pointer switch. A CLI invocation
127
+ // acquires its lease in its own immediate transaction, so the two
128
+ // operations cannot pass each other's checks.
129
+ assertNoActiveLeases(current);
223
130
  upsertPlugin({
224
- name,
225
- spec: storedSpec,
131
+ canonicalId,
132
+ marketplaceId,
133
+ pluginId: manifest.id,
226
134
  source,
227
- version: promoted.version,
228
- installPath: finalPath,
229
- manifest: preparedManifest,
230
- integrity: promoted.integrity,
231
- grants,
135
+ requestedSource: materialized.requestedSource,
136
+ resolvedRevision: materialized.resolvedRevision,
137
+ sourceFingerprint: materialized.sourceFingerprint,
138
+ version: manifest.version,
139
+ installPath: promotedPath,
140
+ manifest,
141
+ integrity: inspected.integrity,
232
142
  enabled: current?.enabled,
233
143
  });
234
144
  }).immediate();
235
- registryUpdated = true;
236
- if (existing?.installPath && existing.installPath !== finalPath) {
237
- progress("retained superseded installation until the daemon switches versions");
238
- }
239
- progress(`installed plugin "${name}"${promoted.version ? `@${promoted.version}` : ""}`);
145
+ promotedByThisTransaction = false;
146
+ onProgress(`installed plugin "${canonicalId}" at ${manifest.version}`);
240
147
  return {
241
- name,
148
+ name: canonicalId,
149
+ canonicalId,
150
+ marketplaceId,
151
+ pluginId: manifest.id,
242
152
  source,
243
- ...(promoted.version ? { version: promoted.version } : {}),
153
+ version: manifest.version,
244
154
  };
245
155
  }
246
156
  finally {
@@ -256,138 +166,105 @@ export async function preparePluginInstallation(spec, onProgress = () => { }, op
256
166
  };
257
167
  }
258
168
  catch (error) {
259
- if (!registryUpdated && finalPath) {
260
- removeOwnedInstallation(storage.installationsRoot, basename(dirname(finalPath)), finalPath);
261
- }
262
169
  rmSync(transactionDir, { recursive: true, force: true });
263
170
  throw error;
264
171
  }
265
172
  }
266
- /**
267
- * Remove the registry row but retain the immutable package until the next
268
- * daemon boot. A currently running supervisor may still be executing it.
269
- */
270
- export async function uninstallPlugin(name, onProgress = () => { }) {
271
- const progress = safeProgress(onProgress);
173
+ export async function uninstallPlugin(identity, onProgress = () => { }, options = {}) {
272
174
  let record;
273
175
  const removed = db().transaction(() => {
274
- record = getPlugin(name);
176
+ record = getPlugin(identity);
275
177
  if (!record)
276
178
  return false;
277
- const leases = activeRuntimeLeases(name, ensureStorageRoots().installationsRoot);
278
- if (leases.length > 0) {
279
- throw new Error(`plugin "${name}" has active runtime lease(s): ${leases
280
- .map((lease) => lease.leaseId)
281
- .join(", ")}; stop its managed services or CLI commands before uninstalling`);
179
+ if (isBuiltInPluginMarketplace(record.marketplaceId)) {
180
+ throw new Error(`bundled plugin "${record.canonicalId}" cannot be uninstalled; disable it instead`);
282
181
  }
283
- return removePlugin(name);
182
+ assertNoActiveLeases(record);
183
+ return removePlugin(record.canonicalId);
284
184
  }).immediate();
285
- if (!removed || !record)
185
+ if (!removed)
286
186
  return false;
287
- if (record.installPath)
288
- progress(`retained active package until the daemon stops using it`);
289
- progress(`uninstalled plugin "${name}"`);
187
+ if (!record)
188
+ return false;
189
+ if (options.purgeData) {
190
+ rmSync(pluginDataDir(record.marketplaceId, record.pluginId), { recursive: true, force: true });
191
+ onProgress(`removed plugin data for "${record.canonicalId}"`);
192
+ }
193
+ onProgress(`uninstalled plugin "${record.canonicalId}"`);
290
194
  return true;
291
195
  }
292
- /**
293
- * Delete inactive immutable versions at a daemon-start safe point.
294
- *
295
- * A short grace window prevents a concurrently materializing install (renamed
296
- * just before its registry transaction) from being mistaken for an orphan.
297
- */
298
- export function pruneInactivePluginInstallations({ minAgeMs = 60_000, stagingMinAgeMs = DEFAULT_STAGING_GC_AGE_MS, now = Date.now(), } = {}) {
299
- const { stagingRoot, installationsRoot } = ensureStorageRoots();
300
- const minimumAge = Math.max(0, minAgeMs);
196
+ export function pruneInactivePluginInstallations() {
197
+ const { cacheRoot } = ensureStorageRoots();
198
+ const active = new Set(listActivePaths().map(canonicalFilesystemPath));
301
199
  const removed = [];
302
- pruneStagingTransactions(stagingRoot, Math.max(0, stagingMinAgeMs), now, removed);
303
- for (const pluginId of readdirSync(installationsRoot)) {
304
- if (!PLUGIN_ID_PATTERN.test(pluginId))
305
- continue;
306
- db().transaction(() => {
307
- const active = new Set();
308
- const current = getPlugin(pluginId);
309
- if (current && isRunnablePluginRecord(current))
310
- active.add(resolve(current.installPath));
311
- for (const lease of activeRuntimeLeases(pluginId, installationsRoot)) {
312
- active.add(resolve(lease.installPath));
313
- }
314
- const idRoot = join(installationsRoot, pluginId);
315
- let idStat;
316
- try {
317
- idStat = lstatSync(idRoot);
318
- }
319
- catch {
320
- return;
321
- }
322
- if (!idStat.isDirectory() || idStat.isSymbolicLink())
323
- return;
324
- const entries = readdirSync(idRoot);
325
- for (const installationId of entries) {
326
- if (!INSTALLATION_ID_PATTERN.test(installationId))
327
- continue;
328
- const candidate = join(idRoot, installationId);
329
- if (active.has(resolve(candidate)))
330
- continue;
331
- let candidateStat;
332
- try {
333
- candidateStat = lstatSync(candidate);
334
- }
335
- catch {
336
- continue;
337
- }
338
- if (!candidateStat.isDirectory() ||
339
- candidateStat.isSymbolicLink() ||
340
- now - promotionTimestampMs(idRoot, installationId, candidateStat.ctimeMs) < minimumAge) {
200
+ for (const marketplace of safeDirectories(cacheRoot)) {
201
+ for (const plugin of safeDirectories(join(cacheRoot, marketplace))) {
202
+ for (const integrity of safeDirectories(join(cacheRoot, marketplace, plugin))) {
203
+ const candidate = resolve(join(cacheRoot, marketplace, plugin, integrity));
204
+ if (active.has(canonicalFilesystemPath(candidate)))
341
205
  continue;
342
- }
343
- if (removeOwnedInstallation(installationsRoot, pluginId, candidate)) {
344
- removed.push(candidate);
345
- }
206
+ rmSync(candidate, { recursive: true, force: true });
207
+ removed.push(candidate);
346
208
  }
347
- for (const entry of entries) {
348
- const installationId = promotionMarkerInstallationId(entry);
349
- if (!installationId || existsSync(join(idRoot, installationId)))
350
- continue;
351
- const marker = join(idRoot, entry);
352
- try {
353
- const stat = lstatSync(marker);
354
- if (stat.isFile() &&
355
- !stat.isSymbolicLink() &&
356
- now - stat.mtimeMs >= minimumAge) {
357
- rmSync(marker, { force: true });
358
- }
359
- }
360
- catch {
361
- // A concurrently removed orphan marker needs no further work.
362
- }
363
- }
364
- }).immediate();
209
+ }
365
210
  }
366
211
  return removed;
367
212
  }
368
- function activeRuntimeLeases(pluginId, installationsRoot) {
369
- const dataDir = join(rynxHome(), "plugin-data", pluginId);
370
- return readPluginRuntimeLeases(dataDir).filter((lease) => {
371
- if (lease.pluginId !== pluginId)
372
- return false;
373
- const invocationPid = pluginCliInvocationLeasePid(lease.leaseId);
374
- if (invocationPid !== undefined && !isProcessAlive(invocationPid)) {
213
+ export function pluginDataDir(marketplaceId, pluginId) {
214
+ return join(rynxHome(), "plugins", "data", marketplaceStorageSegment(marketplaceId), pluginId);
215
+ }
216
+ export function pluginRuntimeCacheDir(marketplaceId, pluginId, integrity) {
217
+ return join(rynxHome(), "plugins", "runtime-cache", marketplaceStorageSegment(marketplaceId), pluginId, integrityPath(integrity));
218
+ }
219
+ function pluginCachePath(marketplaceId, pluginId, integrity, root = join(rynxHome(), "plugins", "cache")) {
220
+ return join(root, marketplaceStorageSegment(marketplaceId), pluginId, integrityPath(integrity));
221
+ }
222
+ export function marketplaceStorageSegment(marketplaceId) {
223
+ if (!MARKETPLACE_ID_PATTERN.test(marketplaceId)) {
224
+ throw new Error(`invalid marketplace id "${marketplaceId}"`);
225
+ }
226
+ return encodeURIComponent(marketplaceId);
227
+ }
228
+ function integrityPath(integrity) {
229
+ const match = /^sha256-([A-Za-z0-9+/]{43}=?)$/.exec(integrity);
230
+ if (!match)
231
+ throw new Error("plugin integrity is invalid");
232
+ return `sha256-${match[1].replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "")}`;
233
+ }
234
+ function ensureStorageRoots() {
235
+ const root = join(rynxHome(), "plugins");
236
+ const cacheRoot = join(root, "cache");
237
+ const stagingRoot = join(root, "staging");
238
+ mkdirSync(cacheRoot, { recursive: true, mode: 0o700 });
239
+ mkdirSync(stagingRoot, { recursive: true, mode: 0o700 });
240
+ return { cacheRoot, stagingRoot };
241
+ }
242
+ function assertNoActiveLeases(record) {
243
+ if (!record)
244
+ return;
245
+ const dataDir = pluginDataDir(record.marketplaceId, record.pluginId);
246
+ const active = readPluginRuntimeLeases(dataDir).filter((lease) => {
247
+ const pid = pluginCliInvocationLeasePid(lease.leaseId);
248
+ if (pid !== undefined && !isProcessAlive(pid)) {
375
249
  removePluginRuntimeLease(dataDir, lease.leaseId);
376
250
  return false;
377
251
  }
378
- const candidate = resolve(lease.installPath);
379
- if (dirname(candidate) !== join(installationsRoot, pluginId) ||
380
- !INSTALLATION_ID_PATTERN.test(basename(candidate))) {
381
- return false;
382
- }
383
- try {
384
- const stat = lstatSync(candidate);
385
- return stat.isDirectory() && !stat.isSymbolicLink() && realpathSync(candidate) === candidate;
386
- }
387
- catch {
388
- return false;
389
- }
252
+ return sameFilesystemPath(lease.installPath, record.installPath);
390
253
  });
254
+ if (active.length > 0) {
255
+ throw new Error(`plugin "${record.canonicalId}" has active generation lease(s): ${active.map((lease) => lease.leaseId).join(", ")}`);
256
+ }
257
+ }
258
+ function sameFilesystemPath(left, right) {
259
+ return canonicalFilesystemPath(left) === canonicalFilesystemPath(right);
260
+ }
261
+ function canonicalFilesystemPath(value) {
262
+ try {
263
+ return realpathSync(value);
264
+ }
265
+ catch {
266
+ return resolve(value);
267
+ }
391
268
  }
392
269
  function isProcessAlive(pid) {
393
270
  try {
@@ -401,144 +278,54 @@ function isProcessAlive(pid) {
401
278
  function samePluginGeneration(before, current) {
402
279
  if (!before || !current)
403
280
  return before === current;
404
- return JSON.stringify(before) === JSON.stringify(current);
281
+ return before.canonicalId === current.canonicalId &&
282
+ before.integrity === current.integrity &&
283
+ before.updatedAt === current.updatedAt;
284
+ }
285
+ function validateMaterialized(value) {
286
+ if (!value ||
287
+ typeof value.resolvedRevision !== "string" ||
288
+ !value.resolvedRevision ||
289
+ !/^[0-9a-f]{32}$/.test(value.sourceFingerprint) ||
290
+ !value.requestedSource) {
291
+ throw new Error("plugin source materializer returned invalid provenance");
292
+ }
405
293
  }
406
294
  function cloneManifest(manifest) {
407
- return JSON.parse(JSON.stringify(manifest));
295
+ return structuredClone(manifest);
408
296
  }
409
297
  function clonePluginRecord(record) {
410
- return {
411
- ...record,
412
- ...(record.manifest ? { manifest: cloneManifest(record.manifest) } : {}),
413
- grants: [...record.grants],
414
- };
298
+ return structuredClone(record);
415
299
  }
416
- function writeStagingOwner(transactionDir) {
417
- writeFileSync(join(transactionDir, STAGING_OWNER_FILE), `${JSON.stringify({ schemaVersion: 1, pid: process.pid, createdAt: new Date().toISOString() })}\n`, { encoding: "utf8", mode: 0o600, flag: "wx" });
418
- }
419
- function pruneStagingTransactions(stagingRoot, minAgeMs, now, removed) {
420
- for (const entry of readdirSync(stagingRoot)) {
421
- if (!STAGING_TRANSACTION_PATTERN.test(entry))
422
- continue;
423
- const candidate = join(stagingRoot, entry);
300
+ function safeDirectories(root) {
301
+ if (!existsSync(root))
302
+ return [];
303
+ return readdirSync(root).filter((entry) => {
424
304
  try {
425
- const stat = lstatSync(candidate);
426
- if (!stat.isDirectory() ||
427
- stat.isSymbolicLink() ||
428
- dirname(realpathSync(candidate)) !== realpathSync(stagingRoot)) {
429
- continue;
430
- }
431
- const owner = readStagingOwner(candidate);
432
- if (owner?.pid && isProcessAlive(owner.pid))
433
- continue;
434
- const createdAt = owner?.createdAtMs ?? Math.max(stat.ctimeMs, stat.mtimeMs);
435
- if (now - createdAt < minAgeMs)
436
- continue;
437
- rmSync(candidate, { recursive: true, force: true });
438
- removed.push(candidate);
305
+ const info = lstatSync(join(root, entry));
306
+ return info.isDirectory() && !info.isSymbolicLink();
439
307
  }
440
308
  catch {
441
- // Invalid or concurrently removed staging data is never followed.
442
- }
443
- }
444
- }
445
- function readStagingOwner(transactionDir) {
446
- const file = join(transactionDir, STAGING_OWNER_FILE);
447
- try {
448
- const stat = lstatSync(file);
449
- if (!stat.isFile() || stat.isSymbolicLink() || stat.size > MAX_HOST_MARKER_BYTES)
450
- return undefined;
451
- const parsed = JSON.parse(readFileSync(file, "utf8"));
452
- if (parsed.schemaVersion !== 1 ||
453
- !Number.isSafeInteger(parsed.pid) ||
454
- Number(parsed.pid) <= 0 ||
455
- typeof parsed.createdAt !== "string" ||
456
- !Number.isFinite(Date.parse(parsed.createdAt))) {
457
- return undefined;
458
- }
459
- return { pid: Number(parsed.pid), createdAtMs: stat.mtimeMs };
460
- }
461
- catch {
462
- return undefined;
463
- }
464
- }
465
- function promotionMarkerPath(idRoot, installationId) {
466
- return join(idRoot, `${installationId}${PROMOTION_MARKER_SUFFIX}`);
467
- }
468
- function writePromotionMarker(marker) {
469
- writeFileSync(marker, `${JSON.stringify({ schemaVersion: 1, promotedAt: new Date().toISOString() })}\n`, { encoding: "utf8", mode: 0o600, flag: "wx" });
470
- }
471
- function promotionTimestampMs(idRoot, installationId, conservativeFallback) {
472
- try {
473
- const stat = lstatSync(promotionMarkerPath(idRoot, installationId));
474
- if (stat.isFile() && !stat.isSymbolicLink() && stat.size <= MAX_HOST_MARKER_BYTES) {
475
- return stat.mtimeMs;
309
+ return false;
476
310
  }
477
- }
478
- catch {
479
- // Installations predating promotion markers fall back to rename-sensitive ctime.
480
- }
481
- return conservativeFallback;
482
- }
483
- function promotionMarkerInstallationId(entry) {
484
- const match = /^([0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})\.rynx-promoted$/i.exec(entry);
485
- return match?.[1];
486
- }
487
- function ensureStorageRoots() {
488
- const configuredRoot = resolve(pluginsDir());
489
- mkdirSync(configuredRoot, { recursive: true, mode: 0o700 });
490
- const root = realpathSync(configuredRoot);
491
- return {
492
- stagingRoot: ensureDirectDirectory(root, "staging", "plugin staging directory"),
493
- installationsRoot: ensureDirectDirectory(root, "installed", "plugin installations directory"),
494
- };
495
- }
496
- /** Create/validate one non-symlink direct child of a canonical parent. */
497
- function ensureDirectDirectory(parent, child, label) {
498
- const path = join(parent, child);
499
- if (!existsSync(path))
500
- mkdirSync(path, { mode: 0o700 });
501
- const stat = lstatSync(path);
502
- if (!stat.isDirectory() || stat.isSymbolicLink()) {
503
- throw new Error(`${label} must be a host-owned directory, not a symlink`);
504
- }
505
- const real = realpathSync(path);
506
- if (dirname(real) !== realpathSync(parent)) {
507
- throw new Error(`${label} resolves outside its host-owned parent`);
508
- }
509
- return real;
311
+ });
510
312
  }
511
- /**
512
- * Delete only `<installations>/<validated-id>/<host UUID>`.
513
- * Legacy paths, source directories, traversal strings, and symlinks are kept.
514
- */
515
- function removeOwnedInstallation(installationsRoot, pluginId, installPath) {
516
- try {
517
- if (!PLUGIN_ID_PATTERN.test(pluginId))
518
- return false;
519
- const root = realpathSync(installationsRoot);
520
- const idRoot = join(root, pluginId);
521
- const candidate = resolve(installPath);
522
- if (dirname(candidate) !== idRoot || !INSTALLATION_ID_PATTERN.test(basename(candidate))) {
523
- return false;
313
+ function listActivePaths() {
314
+ const connection = db();
315
+ const paths = connection.prepare("SELECT install_path FROM plugin_installations").all().map((row) => resolve(row.install_path));
316
+ for (const row of connection.prepare("SELECT execution FROM sessions").all()) {
317
+ try {
318
+ const execution = JSON.parse(row.execution);
319
+ for (const skill of execution.pluginSkills ?? []) {
320
+ if (typeof skill.packageRoot === "string")
321
+ paths.push(resolve(skill.packageRoot));
322
+ }
524
323
  }
525
- const idStat = lstatSync(idRoot);
526
- const candidateStat = lstatSync(candidate);
527
- if (!idStat.isDirectory() ||
528
- idStat.isSymbolicLink() ||
529
- !candidateStat.isDirectory() ||
530
- candidateStat.isSymbolicLink()) {
531
- return false;
324
+ catch {
325
+ // Invalid Session snapshots pin nothing; the Session store rejects them.
532
326
  }
533
- if (realpathSync(idRoot) !== idRoot || realpathSync(candidate) !== candidate)
534
- return false;
535
- rmSync(candidate, { recursive: true, force: true });
536
- rmSync(promotionMarkerPath(idRoot, basename(candidate)), { force: true });
537
- return true;
538
- }
539
- catch {
540
- return false;
541
327
  }
328
+ return paths;
542
329
  }
543
330
  function safeProgress(callback) {
544
331
  return (line) => {
@@ -546,7 +333,7 @@ function safeProgress(callback) {
546
333
  callback(line);
547
334
  }
548
335
  catch {
549
- /* progress reporting must never change installation state */
336
+ // Progress reporting never changes installation correctness.
550
337
  }
551
338
  };
552
339
  }