@rynx-ai/daemon 0.1.0 → 0.1.10-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.
- package/dist/app-browser-host-supervisor.d.ts +97 -0
- package/dist/app-browser-host-supervisor.js +529 -0
- package/dist/browser-artifact-management.d.ts +20 -0
- package/dist/browser-artifact-management.js +61 -0
- package/dist/browser-cli-args.d.ts +20 -0
- package/dist/browser-cli-args.js +100 -0
- package/dist/browser-runner-session-context.d.ts +24 -0
- package/dist/browser-runner-session-context.js +113 -0
- package/dist/cdp-keyboard-input.d.ts +6 -0
- package/dist/cdp-keyboard-input.js +209 -0
- package/dist/channel-store.d.ts +0 -6
- package/dist/channel-store.js +2 -18
- package/dist/chrome-for-testing-release-resolver.d.ts +34 -0
- package/dist/chrome-for-testing-release-resolver.js +214 -0
- package/dist/chrome-for-testing-store.d.ts +85 -0
- package/dist/chrome-for-testing-store.js +836 -0
- package/dist/chrome-inspection-gateway.d.ts +58 -0
- package/dist/chrome-inspection-gateway.js +806 -0
- package/dist/chrome-inspection-manager.d.ts +94 -0
- package/dist/chrome-inspection-manager.js +573 -0
- package/dist/cli.js +478 -30
- package/dist/control-client.d.ts +122 -0
- package/dist/control-client.js +855 -0
- package/dist/control-deps.js +37 -6
- package/dist/control-endpoint.d.ts +14 -0
- package/dist/control-endpoint.js +124 -0
- package/dist/control-link-store.d.ts +16 -0
- package/dist/control-link-store.js +53 -0
- package/dist/daemon-build-status.d.ts +8 -0
- package/dist/daemon-build-status.js +18 -0
- package/dist/daemon-owner.d.ts +14 -0
- package/dist/daemon-owner.js +104 -0
- package/dist/daemon-server.d.ts +29 -2
- package/dist/daemon-server.js +1113 -26
- package/dist/db.js +196 -10
- package/dist/desktop-browser-host-client.d.ts +35 -0
- package/dist/desktop-browser-host-client.js +415 -0
- package/dist/direct-runtime-authenticator.d.ts +13 -0
- package/dist/direct-runtime-authenticator.js +132 -0
- package/dist/direct-runtime-config.d.ts +20 -0
- package/dist/direct-runtime-config.js +101 -0
- package/dist/embedded-browser-profile-cleanup.d.ts +58 -0
- package/dist/embedded-browser-profile-cleanup.js +338 -0
- package/dist/headless-browser-host.d.ts +14 -0
- package/dist/headless-browser-host.js +2173 -0
- package/dist/index-daemon.js +55 -5
- package/dist/installation-id.d.ts +33 -0
- package/dist/installation-id.js +692 -0
- package/dist/maintenance-management.d.ts +11 -0
- package/dist/maintenance-management.js +13 -0
- package/dist/migrations/cleanup-legacy-sessions.d.ts +0 -3
- package/dist/migrations/cleanup-legacy-sessions.js +10 -28
- package/dist/plugin-cli.d.ts +30 -0
- package/dist/plugin-cli.js +270 -0
- package/dist/plugin-host-rpc.d.ts +85 -0
- package/dist/plugin-host-rpc.js +878 -0
- package/dist/plugin-install-restart.d.ts +13 -0
- package/dist/plugin-install-restart.js +24 -0
- package/dist/plugin-installer.d.ts +71 -15
- package/dist/plugin-installer.js +521 -155
- package/dist/plugin-management-service.d.ts +58 -0
- package/dist/plugin-management-service.js +240 -0
- package/dist/plugin-package.d.ts +61 -0
- package/dist/plugin-package.js +732 -0
- package/dist/plugin-runtime-control.d.ts +2 -0
- package/dist/plugin-runtime-control.js +37 -0
- package/dist/plugin-store.d.ts +37 -8
- package/dist/plugin-store.js +80 -15
- package/dist/plugin-supervisor.d.ts +160 -0
- package/dist/plugin-supervisor.js +1143 -0
- package/dist/pm2.d.ts +3 -1
- package/dist/pm2.js +92 -18
- package/dist/provider-cli-service.d.ts +46 -0
- package/dist/provider-cli-service.js +510 -0
- package/dist/registry.d.ts +6 -19
- package/dist/registry.js +22 -87
- package/dist/remote-runtime-access-store.d.ts +62 -0
- package/dist/remote-runtime-access-store.js +393 -0
- package/dist/remote-runtime-admin.d.ts +35 -0
- package/dist/remote-runtime-admin.js +110 -0
- package/dist/remote-runtime-connection-manager.d.ts +103 -0
- package/dist/remote-runtime-connection-manager.js +1575 -0
- package/dist/remote-runtime-credential-store.d.ts +45 -0
- package/dist/remote-runtime-credential-store.js +639 -0
- package/dist/remote-runtime-target-control.d.ts +51 -0
- package/dist/remote-runtime-target-control.js +488 -0
- package/dist/remote-runtime-target-store.d.ts +56 -0
- package/dist/remote-runtime-target-store.js +326 -0
- package/dist/runtime-share-addresses.d.ts +21 -0
- package/dist/runtime-share-addresses.js +50 -0
- package/dist/session-browser-capability-store.d.ts +13 -0
- package/dist/session-browser-capability-store.js +58 -0
- package/dist/session-emulator-binding-store.d.ts +10 -0
- package/dist/session-emulator-binding-store.js +61 -0
- package/dist/session-launch-operations.d.ts +41 -0
- package/dist/session-launch-operations.js +124 -0
- package/dist/session-log-store.js +29 -0
- package/dist/session-meta-store.d.ts +3 -2
- package/dist/session-meta-store.js +59 -7
- package/dist/session-pending-message-store.d.ts +2 -0
- package/dist/session-pending-message-store.js +41 -0
- package/dist/session-resource-store.d.ts +32 -0
- package/dist/session-resource-store.js +700 -0
- package/dist/setup.d.ts +16 -0
- package/dist/setup.js +144 -5
- package/dist/skills-catalog.js +8 -4
- package/dist/update.d.ts +11 -2
- package/dist/update.js +0 -27
- package/package.json +23 -10
package/dist/plugin-installer.js
CHANGED
|
@@ -1,186 +1,552 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Transactional plugin installation.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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";
|
|
13
|
+
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
|
+
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.
|
|
11
26
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
27
|
+
* A failed validation, materialization, atomic move, or registry update leaves
|
|
28
|
+
* the previously registered installation untouched.
|
|
14
29
|
*/
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
import { homedir } from "node:os";
|
|
18
|
-
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
19
|
-
import { fileURLToPath } from "node:url";
|
|
20
|
-
import { getPlugin, removePlugin, upsertPlugin, } from "./plugin-store.js";
|
|
21
|
-
import { importPluginManifest, pluginPkgDir, pluginsDir } from "./registry.js";
|
|
22
|
-
/** A spec is "local" if it's a path/file: URL or an existing directory. */
|
|
23
|
-
function detectSource(spec) {
|
|
24
|
-
if (spec.startsWith("file:"))
|
|
25
|
-
return "local";
|
|
26
|
-
if (spec.startsWith("~") || spec.startsWith("./") || spec.startsWith("../") || isAbsolute(spec)) {
|
|
27
|
-
return "local";
|
|
28
|
-
}
|
|
30
|
+
export async function installPlugin(spec, onProgress = () => { }, opts = {}) {
|
|
31
|
+
const prepared = await preparePluginInstallation(spec, onProgress, opts);
|
|
29
32
|
try {
|
|
30
|
-
if (
|
|
31
|
-
|
|
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");
|
|
36
|
+
}
|
|
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))) {
|
|
55
|
+
return {
|
|
56
|
+
name: prepared.name,
|
|
57
|
+
source: prepared.source,
|
|
58
|
+
...(prepared.version ? { version: prepared.version } : {}),
|
|
59
|
+
skipped: true,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
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
|
+
return await prepared.commit({
|
|
78
|
+
grants,
|
|
79
|
+
expectedDigest: opts.expectedDigest ?? prepared.integrity,
|
|
80
|
+
signal: opts.signal,
|
|
81
|
+
});
|
|
32
82
|
}
|
|
33
|
-
|
|
34
|
-
|
|
83
|
+
finally {
|
|
84
|
+
prepared.discard();
|
|
35
85
|
}
|
|
36
|
-
return "npm";
|
|
37
86
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
87
|
+
/**
|
|
88
|
+
* Materialize, build (only when explicitly allowed), inspect, and hash one
|
|
89
|
+
* plugin into host-owned staging without mutating the registry.
|
|
90
|
+
*/
|
|
91
|
+
export async function preparePluginInstallation(spec, onProgress = () => { }, opts = {}) {
|
|
92
|
+
if (!spec.trim())
|
|
93
|
+
throw new Error("plugin spec must not be empty");
|
|
94
|
+
const progress = safeProgress(onProgress);
|
|
95
|
+
const source = detectPluginSource(spec);
|
|
96
|
+
const storedSpec = normalizePluginSpec(spec, source);
|
|
97
|
+
const storage = ensureStorageRoots();
|
|
98
|
+
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
|
+
const stagedPackage = join(transactionDir, "package");
|
|
107
|
+
let finalPath;
|
|
108
|
+
let registryUpdated = false;
|
|
109
|
+
let consumed = false;
|
|
110
|
+
try {
|
|
111
|
+
await (opts.materializer ?? materializePluginPackage)({
|
|
112
|
+
spec: storedSpec,
|
|
113
|
+
source,
|
|
114
|
+
destination: stagedPackage,
|
|
115
|
+
onProgress: progress,
|
|
116
|
+
signal: opts.signal,
|
|
117
|
+
});
|
|
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");
|
|
125
|
+
}
|
|
126
|
+
const requiresBuildScripts = Boolean(inspected.manifest.requiresBuildScripts);
|
|
127
|
+
if (!requiresBuildScripts) {
|
|
128
|
+
inspected = await inspectPluginPackage(stagedPackage, { signal: opts.signal });
|
|
129
|
+
}
|
|
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}"`);
|
|
133
|
+
}
|
|
134
|
+
const existing = getPlugin(name);
|
|
135
|
+
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 */
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
const preparedManifest = cloneManifest(inspected.manifest);
|
|
147
|
+
const preparedExisting = existing ? clonePluginRecord(existing) : undefined;
|
|
148
|
+
return {
|
|
149
|
+
name,
|
|
150
|
+
source,
|
|
151
|
+
...(inspected.version ? { version: inspected.version } : {}),
|
|
152
|
+
manifest: cloneManifest(preparedManifest),
|
|
153
|
+
integrity: inspected.integrity,
|
|
154
|
+
...(preparedExisting ? { existing: clonePluginRecord(preparedExisting) } : {}),
|
|
155
|
+
commit: async ({ grants, expectedDigest, signal }) => {
|
|
156
|
+
if (consumed)
|
|
157
|
+
throw new Error("prepared plugin installation was already consumed");
|
|
158
|
+
consumed = true;
|
|
159
|
+
try {
|
|
160
|
+
if (signal?.aborted)
|
|
161
|
+
throw new Error("plugin installation aborted");
|
|
162
|
+
if (expectedDigest !== inspected.integrity) {
|
|
163
|
+
throw new Error("prepared plugin digest does not match expected digest");
|
|
164
|
+
}
|
|
165
|
+
const rechecked = await inspectPluginPackage(stagedPackage, {
|
|
166
|
+
...(requiresBuildScripts ? { requireEntries: false } : {}),
|
|
167
|
+
signal,
|
|
168
|
+
});
|
|
169
|
+
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");
|
|
173
|
+
}
|
|
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}`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
if (new Set(grants).size !== grants.length) {
|
|
196
|
+
throw new Error("plugin grants must be unique");
|
|
197
|
+
}
|
|
198
|
+
db().transaction(() => {
|
|
199
|
+
const current = getPlugin(name);
|
|
200
|
+
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`);
|
|
208
|
+
}
|
|
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}`);
|
|
223
|
+
upsertPlugin({
|
|
224
|
+
name,
|
|
225
|
+
spec: storedSpec,
|
|
226
|
+
source,
|
|
227
|
+
version: promoted.version,
|
|
228
|
+
installPath: finalPath,
|
|
229
|
+
manifest: preparedManifest,
|
|
230
|
+
integrity: promoted.integrity,
|
|
231
|
+
grants,
|
|
232
|
+
enabled: current?.enabled,
|
|
233
|
+
});
|
|
234
|
+
}).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}` : ""}`);
|
|
240
|
+
return {
|
|
241
|
+
name,
|
|
242
|
+
source,
|
|
243
|
+
...(promoted.version ? { version: promoted.version } : {}),
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
finally {
|
|
247
|
+
cleanup();
|
|
248
|
+
}
|
|
249
|
+
},
|
|
250
|
+
discard: () => {
|
|
251
|
+
if (consumed)
|
|
252
|
+
return;
|
|
253
|
+
consumed = true;
|
|
254
|
+
cleanup();
|
|
255
|
+
},
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
catch (error) {
|
|
259
|
+
if (!registryUpdated && finalPath) {
|
|
260
|
+
removeOwnedInstallation(storage.installationsRoot, basename(dirname(finalPath)), finalPath);
|
|
261
|
+
}
|
|
262
|
+
rmSync(transactionDir, { recursive: true, force: true });
|
|
263
|
+
throw error;
|
|
264
|
+
}
|
|
43
265
|
}
|
|
44
|
-
/**
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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);
|
|
272
|
+
let record;
|
|
273
|
+
const removed = db().transaction(() => {
|
|
274
|
+
record = getPlugin(name);
|
|
275
|
+
if (!record)
|
|
276
|
+
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`);
|
|
282
|
+
}
|
|
283
|
+
return removePlugin(name);
|
|
284
|
+
}).immediate();
|
|
285
|
+
if (!removed || !record)
|
|
286
|
+
return false;
|
|
287
|
+
if (record.installPath)
|
|
288
|
+
progress(`retained active package until the daemon stops using it`);
|
|
289
|
+
progress(`uninstalled plugin "${name}"`);
|
|
290
|
+
return true;
|
|
48
291
|
}
|
|
49
292
|
/**
|
|
50
|
-
*
|
|
51
|
-
* - **local**: copy the package (minus `node_modules`) into `~/.rynx/plugins/<name>`
|
|
52
|
-
* and load it there. The plugin must be self-contained — it bundles its deps at
|
|
53
|
-
* build time (incl. the unpublished workspace `@rynx-ai/core`) and declares a
|
|
54
|
-
* `rynx.plugin` entry; the daemon installs nothing.
|
|
55
|
-
* - **npm**: `npm install` into `~/.rynx/plugins` (deps resolved by npm).
|
|
293
|
+
* Delete inactive immutable versions at a daemon-start safe point.
|
|
56
294
|
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
* `onProgress` streams human-readable lines.
|
|
295
|
+
* A short grace window prevents a concurrently materializing install (renamed
|
|
296
|
+
* just before its registry transaction) from being mistaken for an orphan.
|
|
60
297
|
*/
|
|
61
|
-
export
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
if (!
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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);
|
|
301
|
+
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) {
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
if (removeOwnedInstallation(installationsRoot, pluginId, candidate)) {
|
|
344
|
+
removed.push(candidate);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
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();
|
|
365
|
+
}
|
|
366
|
+
return removed;
|
|
113
367
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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)) {
|
|
375
|
+
removePluginRuntimeLease(dataDir, lease.leaseId);
|
|
376
|
+
return false;
|
|
377
|
+
}
|
|
378
|
+
const candidate = resolve(lease.installPath);
|
|
379
|
+
if (dirname(candidate) !== join(installationsRoot, pluginId) ||
|
|
380
|
+
!INSTALLATION_ID_PATTERN.test(basename(candidate))) {
|
|
381
|
+
return false;
|
|
382
|
+
}
|
|
120
383
|
try {
|
|
121
|
-
|
|
384
|
+
const stat = lstatSync(candidate);
|
|
385
|
+
return stat.isDirectory() && !stat.isSymbolicLink() && realpathSync(candidate) === candidate;
|
|
122
386
|
}
|
|
123
|
-
catch
|
|
124
|
-
|
|
387
|
+
catch {
|
|
388
|
+
return false;
|
|
125
389
|
}
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
function isProcessAlive(pid) {
|
|
393
|
+
try {
|
|
394
|
+
process.kill(pid, 0);
|
|
395
|
+
return true;
|
|
126
396
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
397
|
+
catch (error) {
|
|
398
|
+
return error.code === "EPERM";
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
function samePluginGeneration(before, current) {
|
|
402
|
+
if (!before || !current)
|
|
403
|
+
return before === current;
|
|
404
|
+
return JSON.stringify(before) === JSON.stringify(current);
|
|
405
|
+
}
|
|
406
|
+
function cloneManifest(manifest) {
|
|
407
|
+
return JSON.parse(JSON.stringify(manifest));
|
|
408
|
+
}
|
|
409
|
+
function clonePluginRecord(record) {
|
|
410
|
+
return {
|
|
411
|
+
...record,
|
|
412
|
+
...(record.manifest ? { manifest: cloneManifest(record.manifest) } : {}),
|
|
413
|
+
grants: [...record.grants],
|
|
414
|
+
};
|
|
415
|
+
}
|
|
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);
|
|
424
|
+
try {
|
|
425
|
+
const stat = lstatSync(candidate);
|
|
426
|
+
if (!stat.isDirectory() ||
|
|
427
|
+
stat.isSymbolicLink() ||
|
|
428
|
+
dirname(realpathSync(candidate)) !== realpathSync(stagingRoot)) {
|
|
429
|
+
continue;
|
|
138
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);
|
|
439
|
+
}
|
|
440
|
+
catch {
|
|
441
|
+
// Invalid or concurrently removed staging data is never followed.
|
|
139
442
|
}
|
|
140
443
|
}
|
|
141
|
-
removePlugin(name);
|
|
142
|
-
onProgress(`uninstalled plugin "${name}"`);
|
|
143
|
-
return true;
|
|
144
444
|
}
|
|
145
|
-
function
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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;
|
|
150
463
|
}
|
|
151
464
|
}
|
|
152
|
-
function
|
|
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) {
|
|
153
472
|
try {
|
|
154
|
-
|
|
155
|
-
|
|
473
|
+
const stat = lstatSync(promotionMarkerPath(idRoot, installationId));
|
|
474
|
+
if (stat.isFile() && !stat.isSymbolicLink() && stat.size <= MAX_HOST_MARKER_BYTES) {
|
|
475
|
+
return stat.mtimeMs;
|
|
476
|
+
}
|
|
156
477
|
}
|
|
157
478
|
catch {
|
|
158
|
-
|
|
479
|
+
// Installations predating promotion markers fall back to rename-sensitive ctime.
|
|
159
480
|
}
|
|
481
|
+
return conservativeFallback;
|
|
160
482
|
}
|
|
161
|
-
function
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
});
|
|
182
|
-
}
|
|
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;
|
|
510
|
+
}
|
|
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;
|
|
524
|
+
}
|
|
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;
|
|
532
|
+
}
|
|
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
|
+
}
|
|
183
542
|
}
|
|
184
|
-
function
|
|
185
|
-
return
|
|
543
|
+
function safeProgress(callback) {
|
|
544
|
+
return (line) => {
|
|
545
|
+
try {
|
|
546
|
+
callback(line);
|
|
547
|
+
}
|
|
548
|
+
catch {
|
|
549
|
+
/* progress reporting must never change installation state */
|
|
550
|
+
}
|
|
551
|
+
};
|
|
186
552
|
}
|