@rynx-ai/daemon 0.1.0 → 0.1.9
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/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/cli.js +467 -30
- package/dist/control-client.d.ts +113 -0
- package/dist/control-client.js +792 -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-owner.d.ts +14 -0
- package/dist/daemon-owner.js +104 -0
- package/dist/daemon-server.d.ts +12 -2
- package/dist/daemon-server.js +559 -28
- package/dist/db.js +140 -10
- package/dist/desktop-browser-host-client.d.ts +34 -0
- package/dist/desktop-browser-host-client.js +413 -0
- package/dist/direct-runtime-authenticator.d.ts +13 -0
- package/dist/direct-runtime-authenticator.js +127 -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 +2156 -0
- package/dist/index-daemon.js +28 -5
- package/dist/installation-id.d.ts +33 -0
- package/dist/installation-id.js +692 -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 +36 -15
- package/dist/plugin-installer.js +420 -155
- package/dist/plugin-package.d.ts +38 -0
- package/dist/plugin-package.js +553 -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 +55 -13
- 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 +61 -0
- package/dist/remote-runtime-access-store.js +386 -0
- package/dist/remote-runtime-admin.d.ts +32 -0
- package/dist/remote-runtime-admin.js +107 -0
- package/dist/remote-runtime-connection-manager.d.ts +92 -0
- package/dist/remote-runtime-connection-manager.js +1409 -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 +47 -0
- package/dist/remote-runtime-target-control.js +433 -0
- package/dist/remote-runtime-target-store.d.ts +53 -0
- package/dist/remote-runtime-target-store.js +307 -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-meta-store.d.ts +3 -2
- package/dist/session-meta-store.js +57 -7
- package/dist/setup.js +9 -4
- package/dist/skills-catalog.js +8 -4
- package/dist/update.d.ts +11 -2
- package/dist/update.js +0 -27
- package/package.json +18 -10
package/dist/plugin-installer.js
CHANGED
|
@@ -1,186 +1,451 @@
|
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
30
|
+
export async function installPlugin(spec, onProgress = () => { }, opts = {}) {
|
|
31
|
+
if (!spec.trim())
|
|
32
|
+
throw new Error("plugin spec must not be empty");
|
|
33
|
+
const progress = safeProgress(onProgress);
|
|
34
|
+
const source = detectPluginSource(spec);
|
|
35
|
+
const storedSpec = normalizePluginSpec(spec, source);
|
|
36
|
+
const storage = ensureStorageRoots();
|
|
37
|
+
const transactionDir = mkdtempSync(join(storage.stagingRoot, "install-"));
|
|
38
|
+
try {
|
|
39
|
+
writeStagingOwner(transactionDir);
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
rmSync(transactionDir, { recursive: true, force: true });
|
|
43
|
+
throw error;
|
|
28
44
|
}
|
|
45
|
+
const stagedPackage = join(transactionDir, "package");
|
|
46
|
+
let finalPath;
|
|
47
|
+
let registryUpdated = false;
|
|
29
48
|
try {
|
|
30
|
-
|
|
31
|
-
|
|
49
|
+
await (opts.materializer ?? materializePluginPackage)({
|
|
50
|
+
spec: storedSpec,
|
|
51
|
+
source,
|
|
52
|
+
destination: stagedPackage,
|
|
53
|
+
onProgress: progress,
|
|
54
|
+
signal: opts.signal,
|
|
55
|
+
});
|
|
56
|
+
let inspected = inspectPluginPackage(stagedPackage, { requireEntries: false });
|
|
57
|
+
if (inspected.manifest.requiresBuildScripts && !opts.allowScripts) {
|
|
58
|
+
throw new Error(`plugin "${inspected.manifest.id}" requires package build scripts; ` +
|
|
59
|
+
"review the package and re-run with --allow-scripts");
|
|
60
|
+
}
|
|
61
|
+
if (opts.allowScripts && !inspected.manifest.requiresBuildScripts) {
|
|
62
|
+
throw new Error(`plugin "${inspected.manifest.id}" does not declare requiresBuildScripts; ` +
|
|
63
|
+
"refusing unnecessary --allow-scripts approval");
|
|
64
|
+
}
|
|
65
|
+
if (inspected.manifest.requiresBuildScripts) {
|
|
66
|
+
const manifestBeforeBuild = JSON.stringify(inspected.manifest);
|
|
67
|
+
const versionBeforeBuild = inspected.version;
|
|
68
|
+
progress(`running explicitly approved build scripts for plugin "${inspected.manifest.id}"`);
|
|
69
|
+
await rebuildPluginPackage(stagedPackage, progress, opts.signal);
|
|
70
|
+
const built = inspectPluginPackage(stagedPackage);
|
|
71
|
+
if (JSON.stringify(built.manifest) !== manifestBeforeBuild ||
|
|
72
|
+
built.version !== versionBeforeBuild) {
|
|
73
|
+
throw new Error("plugin build scripts changed static package identity or manifest metadata");
|
|
74
|
+
}
|
|
75
|
+
inspected = built;
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
inspected = inspectPluginPackage(stagedPackage);
|
|
79
|
+
}
|
|
80
|
+
const name = inspected.manifest.id;
|
|
81
|
+
if (opts.expectedId && name !== opts.expectedId) {
|
|
82
|
+
throw new Error(`plugin update id mismatch: expected "${opts.expectedId}", received "${name}"`);
|
|
83
|
+
}
|
|
84
|
+
const existing = getPlugin(name);
|
|
85
|
+
if (existing && opts.onConflict && !(await opts.onConflict(existing))) {
|
|
86
|
+
return {
|
|
87
|
+
name,
|
|
88
|
+
source,
|
|
89
|
+
...(inspected.version ? { version: inspected.version } : {}),
|
|
90
|
+
skipped: true,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
if (opts.signal?.aborted)
|
|
94
|
+
throw new Error("plugin installation aborted");
|
|
95
|
+
const requested = new Set(inspected.manifest.requestedCapabilities);
|
|
96
|
+
if (!opts.approveCapabilities &&
|
|
97
|
+
opts.grants === undefined &&
|
|
98
|
+
!existing &&
|
|
99
|
+
requested.size > 0) {
|
|
100
|
+
throw new Error(`plugin "${name}" requests host capabilities; explicit grants or approval are required`);
|
|
101
|
+
}
|
|
102
|
+
const grants = opts.approveCapabilities
|
|
103
|
+
? await opts.approveCapabilities(inspected.manifest, existing)
|
|
104
|
+
: opts.grants ??
|
|
105
|
+
(existing
|
|
106
|
+
? existing.grants.filter((capability) => requested.has(capability))
|
|
107
|
+
: []);
|
|
108
|
+
for (const grant of grants) {
|
|
109
|
+
if (!requested.has(grant)) {
|
|
110
|
+
throw new Error(`plugin capability was not requested: ${grant}`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
db().transaction(() => {
|
|
114
|
+
const current = getPlugin(name);
|
|
115
|
+
if (!samePluginGeneration(existing, current)) {
|
|
116
|
+
throw new Error(`plugin "${name}" changed concurrently; retry the installation`);
|
|
117
|
+
}
|
|
118
|
+
const leases = activeRuntimeLeases(name, storage.installationsRoot);
|
|
119
|
+
if (leases.length > 0) {
|
|
120
|
+
throw new Error(`plugin "${name}" has active runtime lease(s): ${leases
|
|
121
|
+
.map((lease) => lease.leaseId)
|
|
122
|
+
.join(", ")}; stop its managed services or CLI commands before updating`);
|
|
123
|
+
}
|
|
124
|
+
const idRoot = ensureDirectDirectory(storage.installationsRoot, name, "plugin id directory");
|
|
125
|
+
const installationId = randomUUID();
|
|
126
|
+
finalPath = join(idRoot, installationId);
|
|
127
|
+
const marker = promotionMarkerPath(idRoot, installationId);
|
|
128
|
+
writePromotionMarker(marker);
|
|
129
|
+
try {
|
|
130
|
+
renameSync(stagedPackage, finalPath);
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
rmSync(marker, { force: true });
|
|
134
|
+
finalPath = undefined;
|
|
135
|
+
throw error;
|
|
136
|
+
}
|
|
137
|
+
progress(`staged validated plugin "${name}" → ${finalPath}`);
|
|
138
|
+
upsertPlugin({
|
|
139
|
+
name,
|
|
140
|
+
spec: storedSpec,
|
|
141
|
+
source,
|
|
142
|
+
version: inspected.version,
|
|
143
|
+
installPath: finalPath,
|
|
144
|
+
manifest: inspected.manifest,
|
|
145
|
+
integrity: inspected.integrity,
|
|
146
|
+
grants,
|
|
147
|
+
enabled: current?.enabled,
|
|
148
|
+
});
|
|
149
|
+
}).immediate();
|
|
150
|
+
registryUpdated = true;
|
|
151
|
+
if (existing?.installPath && existing.installPath !== finalPath) {
|
|
152
|
+
progress(`retained superseded installation until the daemon switches versions`);
|
|
153
|
+
}
|
|
154
|
+
progress(`installed plugin "${name}"${inspected.version ? `@${inspected.version}` : ""}`);
|
|
155
|
+
return {
|
|
156
|
+
name,
|
|
157
|
+
source,
|
|
158
|
+
...(inspected.version ? { version: inspected.version } : {}),
|
|
159
|
+
};
|
|
32
160
|
}
|
|
33
|
-
|
|
34
|
-
|
|
161
|
+
finally {
|
|
162
|
+
// Once registered, finalPath is the active immutable installation and must
|
|
163
|
+
// outlive this transaction. Everything else under staging is disposable.
|
|
164
|
+
if (!registryUpdated && finalPath) {
|
|
165
|
+
removeOwnedInstallation(storage.installationsRoot, basename(dirname(finalPath)), finalPath);
|
|
166
|
+
}
|
|
167
|
+
try {
|
|
168
|
+
rmSync(transactionDir, { recursive: true, force: true });
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
/* orphaned staging is inactive and can be collected later */
|
|
172
|
+
}
|
|
35
173
|
}
|
|
36
|
-
return "npm";
|
|
37
174
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
175
|
+
/**
|
|
176
|
+
* Remove the registry row but retain the immutable package until the next
|
|
177
|
+
* daemon boot. A currently running supervisor may still be executing it.
|
|
178
|
+
*/
|
|
179
|
+
export async function uninstallPlugin(name, onProgress = () => { }) {
|
|
180
|
+
const progress = safeProgress(onProgress);
|
|
181
|
+
let record;
|
|
182
|
+
const removed = db().transaction(() => {
|
|
183
|
+
record = getPlugin(name);
|
|
184
|
+
if (!record)
|
|
185
|
+
return false;
|
|
186
|
+
const leases = activeRuntimeLeases(name, ensureStorageRoots().installationsRoot);
|
|
187
|
+
if (leases.length > 0) {
|
|
188
|
+
throw new Error(`plugin "${name}" has active runtime lease(s): ${leases
|
|
189
|
+
.map((lease) => lease.leaseId)
|
|
190
|
+
.join(", ")}; stop its managed services or CLI commands before uninstalling`);
|
|
191
|
+
}
|
|
192
|
+
return removePlugin(name);
|
|
193
|
+
}).immediate();
|
|
194
|
+
if (!removed || !record)
|
|
195
|
+
return false;
|
|
196
|
+
if (record.installPath)
|
|
197
|
+
progress(`retained active package until the daemon stops using it`);
|
|
198
|
+
progress(`uninstalled plugin "${name}"`);
|
|
199
|
+
return true;
|
|
48
200
|
}
|
|
49
201
|
/**
|
|
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).
|
|
202
|
+
* Delete inactive immutable versions at a daemon-start safe point.
|
|
56
203
|
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
* `onProgress` streams human-readable lines.
|
|
204
|
+
* A short grace window prevents a concurrently materializing install (renamed
|
|
205
|
+
* just before its registry transaction) from being mistaken for an orphan.
|
|
60
206
|
*/
|
|
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
|
-
|
|
207
|
+
export function pruneInactivePluginInstallations({ minAgeMs = 60_000, stagingMinAgeMs = DEFAULT_STAGING_GC_AGE_MS, now = Date.now(), } = {}) {
|
|
208
|
+
const { stagingRoot, installationsRoot } = ensureStorageRoots();
|
|
209
|
+
const minimumAge = Math.max(0, minAgeMs);
|
|
210
|
+
const removed = [];
|
|
211
|
+
pruneStagingTransactions(stagingRoot, Math.max(0, stagingMinAgeMs), now, removed);
|
|
212
|
+
for (const pluginId of readdirSync(installationsRoot)) {
|
|
213
|
+
if (!PLUGIN_ID_PATTERN.test(pluginId))
|
|
214
|
+
continue;
|
|
215
|
+
db().transaction(() => {
|
|
216
|
+
const active = new Set();
|
|
217
|
+
const current = getPlugin(pluginId);
|
|
218
|
+
if (current && isRunnablePluginRecord(current))
|
|
219
|
+
active.add(resolve(current.installPath));
|
|
220
|
+
for (const lease of activeRuntimeLeases(pluginId, installationsRoot)) {
|
|
221
|
+
active.add(resolve(lease.installPath));
|
|
222
|
+
}
|
|
223
|
+
const idRoot = join(installationsRoot, pluginId);
|
|
224
|
+
let idStat;
|
|
225
|
+
try {
|
|
226
|
+
idStat = lstatSync(idRoot);
|
|
227
|
+
}
|
|
228
|
+
catch {
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
if (!idStat.isDirectory() || idStat.isSymbolicLink())
|
|
232
|
+
return;
|
|
233
|
+
const entries = readdirSync(idRoot);
|
|
234
|
+
for (const installationId of entries) {
|
|
235
|
+
if (!INSTALLATION_ID_PATTERN.test(installationId))
|
|
236
|
+
continue;
|
|
237
|
+
const candidate = join(idRoot, installationId);
|
|
238
|
+
if (active.has(resolve(candidate)))
|
|
239
|
+
continue;
|
|
240
|
+
let candidateStat;
|
|
241
|
+
try {
|
|
242
|
+
candidateStat = lstatSync(candidate);
|
|
243
|
+
}
|
|
244
|
+
catch {
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
if (!candidateStat.isDirectory() ||
|
|
248
|
+
candidateStat.isSymbolicLink() ||
|
|
249
|
+
now - promotionTimestampMs(idRoot, installationId, candidateStat.ctimeMs) < minimumAge) {
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
if (removeOwnedInstallation(installationsRoot, pluginId, candidate)) {
|
|
253
|
+
removed.push(candidate);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
for (const entry of entries) {
|
|
257
|
+
const installationId = promotionMarkerInstallationId(entry);
|
|
258
|
+
if (!installationId || existsSync(join(idRoot, installationId)))
|
|
259
|
+
continue;
|
|
260
|
+
const marker = join(idRoot, entry);
|
|
261
|
+
try {
|
|
262
|
+
const stat = lstatSync(marker);
|
|
263
|
+
if (stat.isFile() &&
|
|
264
|
+
!stat.isSymbolicLink() &&
|
|
265
|
+
now - stat.mtimeMs >= minimumAge) {
|
|
266
|
+
rmSync(marker, { force: true });
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
catch {
|
|
270
|
+
// A concurrently removed orphan marker needs no further work.
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}).immediate();
|
|
274
|
+
}
|
|
275
|
+
return removed;
|
|
113
276
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
277
|
+
function activeRuntimeLeases(pluginId, installationsRoot) {
|
|
278
|
+
const dataDir = join(rynxHome(), "plugin-data", pluginId);
|
|
279
|
+
return readPluginRuntimeLeases(dataDir).filter((lease) => {
|
|
280
|
+
if (lease.pluginId !== pluginId)
|
|
281
|
+
return false;
|
|
282
|
+
const invocationPid = pluginCliInvocationLeasePid(lease.leaseId);
|
|
283
|
+
if (invocationPid !== undefined && !isProcessAlive(invocationPid)) {
|
|
284
|
+
removePluginRuntimeLease(dataDir, lease.leaseId);
|
|
285
|
+
return false;
|
|
286
|
+
}
|
|
287
|
+
const candidate = resolve(lease.installPath);
|
|
288
|
+
if (dirname(candidate) !== join(installationsRoot, pluginId) ||
|
|
289
|
+
!INSTALLATION_ID_PATTERN.test(basename(candidate))) {
|
|
290
|
+
return false;
|
|
291
|
+
}
|
|
120
292
|
try {
|
|
121
|
-
|
|
293
|
+
const stat = lstatSync(candidate);
|
|
294
|
+
return stat.isDirectory() && !stat.isSymbolicLink() && realpathSync(candidate) === candidate;
|
|
122
295
|
}
|
|
123
|
-
catch
|
|
124
|
-
|
|
296
|
+
catch {
|
|
297
|
+
return false;
|
|
125
298
|
}
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
function isProcessAlive(pid) {
|
|
302
|
+
try {
|
|
303
|
+
process.kill(pid, 0);
|
|
304
|
+
return true;
|
|
126
305
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
306
|
+
catch (error) {
|
|
307
|
+
return error.code === "EPERM";
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
function samePluginGeneration(before, current) {
|
|
311
|
+
if (!before || !current)
|
|
312
|
+
return before === current;
|
|
313
|
+
return JSON.stringify(before) === JSON.stringify(current);
|
|
314
|
+
}
|
|
315
|
+
function writeStagingOwner(transactionDir) {
|
|
316
|
+
writeFileSync(join(transactionDir, STAGING_OWNER_FILE), `${JSON.stringify({ schemaVersion: 1, pid: process.pid, createdAt: new Date().toISOString() })}\n`, { encoding: "utf8", mode: 0o600, flag: "wx" });
|
|
317
|
+
}
|
|
318
|
+
function pruneStagingTransactions(stagingRoot, minAgeMs, now, removed) {
|
|
319
|
+
for (const entry of readdirSync(stagingRoot)) {
|
|
320
|
+
if (!STAGING_TRANSACTION_PATTERN.test(entry))
|
|
321
|
+
continue;
|
|
322
|
+
const candidate = join(stagingRoot, entry);
|
|
323
|
+
try {
|
|
324
|
+
const stat = lstatSync(candidate);
|
|
325
|
+
if (!stat.isDirectory() ||
|
|
326
|
+
stat.isSymbolicLink() ||
|
|
327
|
+
dirname(realpathSync(candidate)) !== realpathSync(stagingRoot)) {
|
|
328
|
+
continue;
|
|
138
329
|
}
|
|
330
|
+
const owner = readStagingOwner(candidate);
|
|
331
|
+
if (owner?.pid && isProcessAlive(owner.pid))
|
|
332
|
+
continue;
|
|
333
|
+
const createdAt = owner?.createdAtMs ?? Math.max(stat.ctimeMs, stat.mtimeMs);
|
|
334
|
+
if (now - createdAt < minAgeMs)
|
|
335
|
+
continue;
|
|
336
|
+
rmSync(candidate, { recursive: true, force: true });
|
|
337
|
+
removed.push(candidate);
|
|
338
|
+
}
|
|
339
|
+
catch {
|
|
340
|
+
// Invalid or concurrently removed staging data is never followed.
|
|
139
341
|
}
|
|
140
342
|
}
|
|
141
|
-
removePlugin(name);
|
|
142
|
-
onProgress(`uninstalled plugin "${name}"`);
|
|
143
|
-
return true;
|
|
144
343
|
}
|
|
145
|
-
function
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
344
|
+
function readStagingOwner(transactionDir) {
|
|
345
|
+
const file = join(transactionDir, STAGING_OWNER_FILE);
|
|
346
|
+
try {
|
|
347
|
+
const stat = lstatSync(file);
|
|
348
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > MAX_HOST_MARKER_BYTES)
|
|
349
|
+
return undefined;
|
|
350
|
+
const parsed = JSON.parse(readFileSync(file, "utf8"));
|
|
351
|
+
if (parsed.schemaVersion !== 1 ||
|
|
352
|
+
!Number.isSafeInteger(parsed.pid) ||
|
|
353
|
+
Number(parsed.pid) <= 0 ||
|
|
354
|
+
typeof parsed.createdAt !== "string" ||
|
|
355
|
+
!Number.isFinite(Date.parse(parsed.createdAt))) {
|
|
356
|
+
return undefined;
|
|
357
|
+
}
|
|
358
|
+
return { pid: Number(parsed.pid), createdAtMs: stat.mtimeMs };
|
|
359
|
+
}
|
|
360
|
+
catch {
|
|
361
|
+
return undefined;
|
|
150
362
|
}
|
|
151
363
|
}
|
|
152
|
-
function
|
|
364
|
+
function promotionMarkerPath(idRoot, installationId) {
|
|
365
|
+
return join(idRoot, `${installationId}${PROMOTION_MARKER_SUFFIX}`);
|
|
366
|
+
}
|
|
367
|
+
function writePromotionMarker(marker) {
|
|
368
|
+
writeFileSync(marker, `${JSON.stringify({ schemaVersion: 1, promotedAt: new Date().toISOString() })}\n`, { encoding: "utf8", mode: 0o600, flag: "wx" });
|
|
369
|
+
}
|
|
370
|
+
function promotionTimestampMs(idRoot, installationId, conservativeFallback) {
|
|
153
371
|
try {
|
|
154
|
-
|
|
155
|
-
|
|
372
|
+
const stat = lstatSync(promotionMarkerPath(idRoot, installationId));
|
|
373
|
+
if (stat.isFile() && !stat.isSymbolicLink() && stat.size <= MAX_HOST_MARKER_BYTES) {
|
|
374
|
+
return stat.mtimeMs;
|
|
375
|
+
}
|
|
156
376
|
}
|
|
157
377
|
catch {
|
|
158
|
-
|
|
378
|
+
// Installations predating promotion markers fall back to rename-sensitive ctime.
|
|
159
379
|
}
|
|
380
|
+
return conservativeFallback;
|
|
160
381
|
}
|
|
161
|
-
function
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
const abort = () => child.kill();
|
|
165
|
-
signal?.addEventListener("abort", abort, { once: true });
|
|
166
|
-
const onData = (buf) => {
|
|
167
|
-
for (const line of buf.toString().split(/\r?\n/)) {
|
|
168
|
-
if (line.trim())
|
|
169
|
-
onProgress(line);
|
|
170
|
-
}
|
|
171
|
-
};
|
|
172
|
-
child.stdout?.on("data", onData);
|
|
173
|
-
child.stderr?.on("data", onData);
|
|
174
|
-
child.on("error", reject);
|
|
175
|
-
child.on("close", (code) => {
|
|
176
|
-
signal?.removeEventListener("abort", abort);
|
|
177
|
-
if (code === 0)
|
|
178
|
-
resolveDone();
|
|
179
|
-
else
|
|
180
|
-
reject(new Error(`npm exited with code ${code ?? "null"}`));
|
|
181
|
-
});
|
|
182
|
-
});
|
|
382
|
+
function promotionMarkerInstallationId(entry) {
|
|
383
|
+
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);
|
|
384
|
+
return match?.[1];
|
|
183
385
|
}
|
|
184
|
-
function
|
|
185
|
-
|
|
386
|
+
function ensureStorageRoots() {
|
|
387
|
+
const configuredRoot = resolve(pluginsDir());
|
|
388
|
+
mkdirSync(configuredRoot, { recursive: true, mode: 0o700 });
|
|
389
|
+
const root = realpathSync(configuredRoot);
|
|
390
|
+
return {
|
|
391
|
+
stagingRoot: ensureDirectDirectory(root, "staging", "plugin staging directory"),
|
|
392
|
+
installationsRoot: ensureDirectDirectory(root, "installed", "plugin installations directory"),
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
/** Create/validate one non-symlink direct child of a canonical parent. */
|
|
396
|
+
function ensureDirectDirectory(parent, child, label) {
|
|
397
|
+
const path = join(parent, child);
|
|
398
|
+
if (!existsSync(path))
|
|
399
|
+
mkdirSync(path, { mode: 0o700 });
|
|
400
|
+
const stat = lstatSync(path);
|
|
401
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) {
|
|
402
|
+
throw new Error(`${label} must be a host-owned directory, not a symlink`);
|
|
403
|
+
}
|
|
404
|
+
const real = realpathSync(path);
|
|
405
|
+
if (dirname(real) !== realpathSync(parent)) {
|
|
406
|
+
throw new Error(`${label} resolves outside its host-owned parent`);
|
|
407
|
+
}
|
|
408
|
+
return real;
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* Delete only `<installations>/<validated-id>/<host UUID>`.
|
|
412
|
+
* Legacy paths, source directories, traversal strings, and symlinks are kept.
|
|
413
|
+
*/
|
|
414
|
+
function removeOwnedInstallation(installationsRoot, pluginId, installPath) {
|
|
415
|
+
try {
|
|
416
|
+
if (!PLUGIN_ID_PATTERN.test(pluginId))
|
|
417
|
+
return false;
|
|
418
|
+
const root = realpathSync(installationsRoot);
|
|
419
|
+
const idRoot = join(root, pluginId);
|
|
420
|
+
const candidate = resolve(installPath);
|
|
421
|
+
if (dirname(candidate) !== idRoot || !INSTALLATION_ID_PATTERN.test(basename(candidate))) {
|
|
422
|
+
return false;
|
|
423
|
+
}
|
|
424
|
+
const idStat = lstatSync(idRoot);
|
|
425
|
+
const candidateStat = lstatSync(candidate);
|
|
426
|
+
if (!idStat.isDirectory() ||
|
|
427
|
+
idStat.isSymbolicLink() ||
|
|
428
|
+
!candidateStat.isDirectory() ||
|
|
429
|
+
candidateStat.isSymbolicLink()) {
|
|
430
|
+
return false;
|
|
431
|
+
}
|
|
432
|
+
if (realpathSync(idRoot) !== idRoot || realpathSync(candidate) !== candidate)
|
|
433
|
+
return false;
|
|
434
|
+
rmSync(candidate, { recursive: true, force: true });
|
|
435
|
+
rmSync(promotionMarkerPath(idRoot, basename(candidate)), { force: true });
|
|
436
|
+
return true;
|
|
437
|
+
}
|
|
438
|
+
catch {
|
|
439
|
+
return false;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
function safeProgress(callback) {
|
|
443
|
+
return (line) => {
|
|
444
|
+
try {
|
|
445
|
+
callback(line);
|
|
446
|
+
}
|
|
447
|
+
catch {
|
|
448
|
+
/* progress reporting must never change installation state */
|
|
449
|
+
}
|
|
450
|
+
};
|
|
186
451
|
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { type PluginManifestV1 } from "@rynx-ai/plugin-sdk";
|
|
2
|
+
import type { PluginSource } from "./plugin-store.js";
|
|
3
|
+
export interface MaterializePluginPackageRequest {
|
|
4
|
+
spec: string;
|
|
5
|
+
source: PluginSource;
|
|
6
|
+
destination: string;
|
|
7
|
+
onProgress: (line: string) => void;
|
|
8
|
+
signal?: AbortSignal;
|
|
9
|
+
}
|
|
10
|
+
export type PluginPackageMaterializer = (request: MaterializePluginPackageRequest) => Promise<void>;
|
|
11
|
+
export interface InspectedPluginPackage {
|
|
12
|
+
manifest: PluginManifestV1;
|
|
13
|
+
version?: string;
|
|
14
|
+
integrity: string;
|
|
15
|
+
}
|
|
16
|
+
/** Classify provenance without fetching or executing anything. */
|
|
17
|
+
export declare function detectPluginSource(spec: string): PluginSource;
|
|
18
|
+
/** Stable provenance for local sources; remote specs are retained verbatim. */
|
|
19
|
+
export declare function normalizePluginSpec(spec: string, source: PluginSource): string;
|
|
20
|
+
/**
|
|
21
|
+
* Materialize a package without running package lifecycle scripts.
|
|
22
|
+
*
|
|
23
|
+
* Local workspace packages are archived with their internal dependency closure
|
|
24
|
+
* and then installed into a disposable npm prefix. Other local, npm, git, and
|
|
25
|
+
* URL specs use the same prefix flow. Every path therefore reaches inspection
|
|
26
|
+
* with runtime dependencies present, while lifecycle scripts remain disabled.
|
|
27
|
+
*/
|
|
28
|
+
export declare const materializePluginPackage: PluginPackageMaterializer;
|
|
29
|
+
export declare function parseGitSpec(spec: string): {
|
|
30
|
+
url: string;
|
|
31
|
+
ref?: string;
|
|
32
|
+
};
|
|
33
|
+
/** Parse and validate static metadata, validate all executable entry paths, and hash the tree. */
|
|
34
|
+
export declare function inspectPluginPackage(packageDir: string, options?: {
|
|
35
|
+
requireEntries?: boolean;
|
|
36
|
+
}): InspectedPluginPackage;
|
|
37
|
+
/** Run package/dependency build scripts only after explicit user approval. */
|
|
38
|
+
export declare function rebuildPluginPackage(packageDir: string, onProgress: (line: string) => void, signal?: AbortSignal): Promise<void>;
|