@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
|
@@ -0,0 +1,553 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Static plugin-package materialization and inspection.
|
|
3
|
+
*
|
|
4
|
+
* This module deliberately never imports a plugin entry point. Package code is
|
|
5
|
+
* copied/downloaded with lifecycle scripts disabled, then only package.json and
|
|
6
|
+
* the referenced JSON manifest are parsed.
|
|
7
|
+
*/
|
|
8
|
+
import { createHash } from "node:crypto";
|
|
9
|
+
import { spawn } from "node:child_process";
|
|
10
|
+
import { cpSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, readlinkSync, realpathSync, rmSync, statSync, writeFileSync, } from "node:fs";
|
|
11
|
+
import { homedir } from "node:os";
|
|
12
|
+
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
13
|
+
import { fileURLToPath } from "node:url";
|
|
14
|
+
import { create as createTar } from "tar";
|
|
15
|
+
import { parsePluginManifest, } from "@rynx-ai/plugin-sdk";
|
|
16
|
+
const MAX_METADATA_BYTES = 1024 * 1024;
|
|
17
|
+
/** Classify provenance without fetching or executing anything. */
|
|
18
|
+
export function detectPluginSource(spec) {
|
|
19
|
+
if (looksLocal(spec))
|
|
20
|
+
return "local";
|
|
21
|
+
if (/^(?:git\+|git:|git@|ssh:|github:|gitlab:|bitbucket:)/i.test(spec) ||
|
|
22
|
+
/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+(?:#.*)?$/.test(spec)) {
|
|
23
|
+
return "git";
|
|
24
|
+
}
|
|
25
|
+
if (/^https?:\/\//i.test(spec))
|
|
26
|
+
return spec.includes(".git") ? "git" : "url";
|
|
27
|
+
return "npm";
|
|
28
|
+
}
|
|
29
|
+
/** Stable provenance for local sources; remote specs are retained verbatim. */
|
|
30
|
+
export function normalizePluginSpec(spec, source) {
|
|
31
|
+
return source === "local" ? toLocalDir(spec) : spec;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Materialize a package without running package lifecycle scripts.
|
|
35
|
+
*
|
|
36
|
+
* Local workspace packages are archived with their internal dependency closure
|
|
37
|
+
* and then installed into a disposable npm prefix. Other local, npm, git, and
|
|
38
|
+
* URL specs use the same prefix flow. Every path therefore reaches inspection
|
|
39
|
+
* with runtime dependencies present, while lifecycle scripts remain disabled.
|
|
40
|
+
*/
|
|
41
|
+
export const materializePluginPackage = async ({ spec, source, destination, onProgress, signal, }) => {
|
|
42
|
+
if (source === "local") {
|
|
43
|
+
await materializeLocalDirectory(toLocalDir(spec), destination, onProgress, signal);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (source === "git") {
|
|
47
|
+
const checkout = join(resolve(destination, ".."), "git-checkout");
|
|
48
|
+
await cloneGitPackage(spec, checkout, onProgress, signal);
|
|
49
|
+
await materializeLocalDirectory(checkout, destination, onProgress, signal);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const archive = await acquirePublishedPackageArchive(spec, join(resolve(destination, ".."), "package-archive"), onProgress, signal);
|
|
53
|
+
await materializeWithNpm({
|
|
54
|
+
spec: archive,
|
|
55
|
+
prefix: join(resolve(destination, ".."), "remote-prefix"),
|
|
56
|
+
destination,
|
|
57
|
+
onProgress,
|
|
58
|
+
signal,
|
|
59
|
+
});
|
|
60
|
+
};
|
|
61
|
+
async function materializeLocalDirectory(sourceDir, destination, onProgress, signal) {
|
|
62
|
+
if (!existsSync(sourceDir) || !statSync(sourceDir).isDirectory()) {
|
|
63
|
+
throw new Error(`local plugin source is not a directory: ${sourceDir}`);
|
|
64
|
+
}
|
|
65
|
+
const sourceReal = realpathSync(sourceDir);
|
|
66
|
+
if (isWithin(sourceReal, resolve(destination))) {
|
|
67
|
+
throw new Error("plugin staging directory must not be inside the local source");
|
|
68
|
+
}
|
|
69
|
+
if (!hasProductionDependencies(sourceReal)) {
|
|
70
|
+
copyPackageTree(sourceReal, destination, true);
|
|
71
|
+
onProgress(`copied self-contained local plugin package from ${sourceReal}`);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const workspaceRoot = findPnpmWorkspaceRoot(sourceReal);
|
|
75
|
+
if (workspaceRoot) {
|
|
76
|
+
const stagedWorkspace = await stageLocalWorkspaceClosure(sourceReal, workspaceRoot, join(resolve(destination, ".."), "workspace-packages"));
|
|
77
|
+
onProgress(`materializing local workspace plugin with ${stagedWorkspace.packageCount - 1} workspace dependency/dependencies`);
|
|
78
|
+
await materializeWithNpm({
|
|
79
|
+
spec: stagedWorkspace.pluginArchive,
|
|
80
|
+
prefix: join(resolve(destination, ".."), "local-prefix"),
|
|
81
|
+
destination,
|
|
82
|
+
onProgress,
|
|
83
|
+
signal,
|
|
84
|
+
label: "local workspace",
|
|
85
|
+
});
|
|
86
|
+
// The rewritten file: specs are a staging-only resolution aid. Keep the
|
|
87
|
+
// installed plugin's own package metadata identical to its source.
|
|
88
|
+
writeFileSync(join(destination, "package.json"), readFileSync(join(sourceReal, "package.json")));
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const localArchiveRoot = join(resolve(destination, ".."), "local-package");
|
|
92
|
+
const localPackage = join(localArchiveRoot, "package");
|
|
93
|
+
rmSync(localArchiveRoot, { recursive: true, force: true });
|
|
94
|
+
copyPackageTree(sourceReal, localPackage, true);
|
|
95
|
+
const localArchive = join(localArchiveRoot, "package.tgz");
|
|
96
|
+
await createPackageArchive(localPackage, localArchive);
|
|
97
|
+
await materializeWithNpm({
|
|
98
|
+
spec: localArchive,
|
|
99
|
+
prefix: join(resolve(destination, ".."), "local-prefix"),
|
|
100
|
+
destination,
|
|
101
|
+
onProgress,
|
|
102
|
+
signal,
|
|
103
|
+
label: "local",
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
async function acquirePublishedPackageArchive(spec, archiveRoot, onProgress, signal) {
|
|
107
|
+
rmSync(archiveRoot, { recursive: true, force: true });
|
|
108
|
+
mkdirSync(archiveRoot, { recursive: true, mode: 0o700 });
|
|
109
|
+
await runNpm([
|
|
110
|
+
"pack",
|
|
111
|
+
"--ignore-scripts",
|
|
112
|
+
"--pack-destination",
|
|
113
|
+
archiveRoot,
|
|
114
|
+
"--",
|
|
115
|
+
spec,
|
|
116
|
+
], onProgress, signal);
|
|
117
|
+
const archives = readdirSync(archiveRoot)
|
|
118
|
+
.filter((name) => name.endsWith(".tgz"))
|
|
119
|
+
.map((name) => join(archiveRoot, name));
|
|
120
|
+
if (archives.length !== 1 || !statSync(archives[0]).isFile()) {
|
|
121
|
+
throw new Error("package source did not produce exactly one npm archive");
|
|
122
|
+
}
|
|
123
|
+
return archives[0];
|
|
124
|
+
}
|
|
125
|
+
async function cloneGitPackage(spec, destination, onProgress, signal) {
|
|
126
|
+
const { url, ref } = parseGitSpec(spec);
|
|
127
|
+
rmSync(destination, { recursive: true, force: true });
|
|
128
|
+
const args = ["clone", "--no-checkout", "--filter=blob:none", "--", url, destination];
|
|
129
|
+
await runCommand("git", args, onProgress, signal);
|
|
130
|
+
await runCommand("git", ["-C", destination, "checkout", "--detach", ref ?? "HEAD"], onProgress, signal);
|
|
131
|
+
}
|
|
132
|
+
export function parseGitSpec(spec) {
|
|
133
|
+
let value = spec.trim();
|
|
134
|
+
let shorthand = false;
|
|
135
|
+
if (/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+(?:#.*)?$/.test(value)) {
|
|
136
|
+
value = `https://github.com/${value}`;
|
|
137
|
+
shorthand = true;
|
|
138
|
+
}
|
|
139
|
+
else if (value.startsWith("github:")) {
|
|
140
|
+
value = `https://github.com/${value.slice("github:".length)}`;
|
|
141
|
+
shorthand = true;
|
|
142
|
+
}
|
|
143
|
+
else if (value.startsWith("gitlab:")) {
|
|
144
|
+
value = `https://gitlab.com/${value.slice("gitlab:".length)}`;
|
|
145
|
+
shorthand = true;
|
|
146
|
+
}
|
|
147
|
+
else if (value.startsWith("bitbucket:")) {
|
|
148
|
+
value = `https://bitbucket.org/${value.slice("bitbucket:".length)}`;
|
|
149
|
+
shorthand = true;
|
|
150
|
+
}
|
|
151
|
+
if (value.startsWith("git+"))
|
|
152
|
+
value = value.slice(4);
|
|
153
|
+
const hash = value.lastIndexOf("#");
|
|
154
|
+
const ref = hash >= 0 ? value.slice(hash + 1).trim() : undefined;
|
|
155
|
+
const url = hash >= 0 ? value.slice(0, hash) : value;
|
|
156
|
+
if (!url || url.startsWith("-") || /[\r\n\0]/.test(url)) {
|
|
157
|
+
throw new Error("git plugin source has an invalid URL");
|
|
158
|
+
}
|
|
159
|
+
if (ref && (ref.startsWith("-") || /[\r\n\0]/.test(ref))) {
|
|
160
|
+
throw new Error("git plugin source has an invalid ref");
|
|
161
|
+
}
|
|
162
|
+
const normalizedUrl = shorthand && !url.endsWith(".git") ? `${url}.git` : url;
|
|
163
|
+
return { url: normalizedUrl, ...(ref ? { ref } : {}) };
|
|
164
|
+
}
|
|
165
|
+
async function materializeWithNpm({ spec, prefix, destination, onProgress, signal, installLinks = false, label, }) {
|
|
166
|
+
mkdirSync(prefix, { recursive: true, mode: 0o700 });
|
|
167
|
+
writeFileSync(join(prefix, "package.json"), `${JSON.stringify({ name: "rynx-plugin-materializer", private: true }, null, 2)}\n`, { mode: 0o600 });
|
|
168
|
+
onProgress(`materializing ${label ?? (installLinks ? "local" : "remote")} plugin ${spec}`);
|
|
169
|
+
await runNpm([
|
|
170
|
+
"install",
|
|
171
|
+
"--prefix",
|
|
172
|
+
prefix,
|
|
173
|
+
"--ignore-scripts",
|
|
174
|
+
"--no-audit",
|
|
175
|
+
"--no-fund",
|
|
176
|
+
"--package-lock=false",
|
|
177
|
+
"--install-strategy=nested",
|
|
178
|
+
...(installLinks ? ["--install-links=true"] : []),
|
|
179
|
+
"--save-exact",
|
|
180
|
+
"--",
|
|
181
|
+
spec,
|
|
182
|
+
], onProgress, signal);
|
|
183
|
+
const materializerPackage = readJsonObject(join(prefix, "package.json"), "materializer package.json");
|
|
184
|
+
const dependencies = materializerPackage.dependencies;
|
|
185
|
+
if (!isRecord(dependencies)) {
|
|
186
|
+
throw new Error("npm did not record the materialized plugin package");
|
|
187
|
+
}
|
|
188
|
+
const names = Object.keys(dependencies);
|
|
189
|
+
if (names.length !== 1 || !isSafeNpmPackageName(names[0])) {
|
|
190
|
+
throw new Error("npm materialization did not resolve exactly one safe package name");
|
|
191
|
+
}
|
|
192
|
+
const packageDir = join(prefix, "node_modules", ...names[0].split("/"));
|
|
193
|
+
const packageReal = requireContainedFile(prefix, packageDir, "materialized package", "directory");
|
|
194
|
+
copyPackageTree(packageReal, destination, false);
|
|
195
|
+
}
|
|
196
|
+
/** Parse and validate static metadata, validate all executable entry paths, and hash the tree. */
|
|
197
|
+
export function inspectPluginPackage(packageDir, options = {}) {
|
|
198
|
+
const packageRoot = realpathSync(packageDir);
|
|
199
|
+
validatePackageSymlinkClosure(packageRoot);
|
|
200
|
+
const packageJsonPath = requireContainedFile(packageRoot, join(packageRoot, "package.json"), "package.json", "file");
|
|
201
|
+
const packageJson = readJsonObject(packageJsonPath, "package.json");
|
|
202
|
+
if (!isRecord(packageJson.rynx) || typeof packageJson.rynx.manifest !== "string") {
|
|
203
|
+
throw new Error("package.json must declare `rynx.manifest` as a static JSON path");
|
|
204
|
+
}
|
|
205
|
+
const manifestPath = resolvePackageEntry(packageRoot, packageJson.rynx.manifest, "rynx.manifest");
|
|
206
|
+
const manifest = parsePluginManifest(readJsonObject(manifestPath, "plugin manifest"));
|
|
207
|
+
if (options.requireEntries !== false) {
|
|
208
|
+
resolvePackageEntry(packageRoot, manifest.runtimeEntry, "runtimeEntry");
|
|
209
|
+
if (manifest.cliEntry)
|
|
210
|
+
resolvePackageEntry(packageRoot, manifest.cliEntry, "cliEntry");
|
|
211
|
+
}
|
|
212
|
+
const version = typeof packageJson.version === "string" && packageJson.version.trim()
|
|
213
|
+
? packageJson.version
|
|
214
|
+
: undefined;
|
|
215
|
+
return {
|
|
216
|
+
manifest,
|
|
217
|
+
...(version ? { version } : {}),
|
|
218
|
+
integrity: hashPackageTree(packageRoot),
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* An installed package must be an immutable closure. Internal npm `.bin`
|
|
223
|
+
* links are valid, but a link to mutable content outside the promoted package
|
|
224
|
+
* would make the recorded integrity depend only on the link text.
|
|
225
|
+
*/
|
|
226
|
+
function validatePackageSymlinkClosure(root) {
|
|
227
|
+
visit(root, "");
|
|
228
|
+
function visit(candidate, rel) {
|
|
229
|
+
const stat = lstatSync(candidate);
|
|
230
|
+
const normalized = rel.split(sep).join("/");
|
|
231
|
+
if (stat.isSymbolicLink()) {
|
|
232
|
+
let target;
|
|
233
|
+
try {
|
|
234
|
+
target = realpathSync(candidate);
|
|
235
|
+
}
|
|
236
|
+
catch {
|
|
237
|
+
throw new Error(`plugin package contains a broken symlink: ${normalized}`);
|
|
238
|
+
}
|
|
239
|
+
if (!isWithin(root, target)) {
|
|
240
|
+
throw new Error(`plugin package symlink resolves outside the package: ${normalized}`);
|
|
241
|
+
}
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
if (!stat.isDirectory())
|
|
245
|
+
return;
|
|
246
|
+
for (const name of readdirSync(candidate)) {
|
|
247
|
+
visit(join(candidate, name), join(rel, name));
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
/** Run package/dependency build scripts only after explicit user approval. */
|
|
252
|
+
export async function rebuildPluginPackage(packageDir, onProgress, signal) {
|
|
253
|
+
await runNpm([
|
|
254
|
+
"rebuild",
|
|
255
|
+
"--prefix",
|
|
256
|
+
realpathSync(packageDir),
|
|
257
|
+
"--foreground-scripts",
|
|
258
|
+
"--no-audit",
|
|
259
|
+
"--no-fund",
|
|
260
|
+
], onProgress, signal);
|
|
261
|
+
}
|
|
262
|
+
function looksLocal(spec) {
|
|
263
|
+
if (spec.startsWith("file:"))
|
|
264
|
+
return true;
|
|
265
|
+
if (spec === "~" || spec.startsWith("~/"))
|
|
266
|
+
return true;
|
|
267
|
+
if (spec.startsWith("./") || spec.startsWith("../") || isAbsolute(spec))
|
|
268
|
+
return true;
|
|
269
|
+
try {
|
|
270
|
+
return existsSync(spec) && statSync(spec).isDirectory();
|
|
271
|
+
}
|
|
272
|
+
catch {
|
|
273
|
+
return false;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
function toLocalDir(spec) {
|
|
277
|
+
let path;
|
|
278
|
+
if (spec.startsWith("file:")) {
|
|
279
|
+
try {
|
|
280
|
+
path = fileURLToPath(spec);
|
|
281
|
+
}
|
|
282
|
+
catch {
|
|
283
|
+
path = spec.slice("file:".length);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
else {
|
|
287
|
+
path = spec;
|
|
288
|
+
}
|
|
289
|
+
if (path === "~" || path.startsWith("~/"))
|
|
290
|
+
path = join(homedir(), path.slice(1));
|
|
291
|
+
return resolve(path);
|
|
292
|
+
}
|
|
293
|
+
function copyPackageTree(source, destination, excludeNodeModules) {
|
|
294
|
+
cpSync(source, destination, {
|
|
295
|
+
recursive: true,
|
|
296
|
+
errorOnExist: true,
|
|
297
|
+
force: false,
|
|
298
|
+
dereference: false,
|
|
299
|
+
filter: (entry) => {
|
|
300
|
+
const rel = relative(source, entry);
|
|
301
|
+
if (!rel)
|
|
302
|
+
return true;
|
|
303
|
+
const segments = rel.split(sep);
|
|
304
|
+
if (segments.includes(".git"))
|
|
305
|
+
return false;
|
|
306
|
+
// npm generates command shims as absolute symlinks into its disposable
|
|
307
|
+
// materialization prefix. Plugin entries resolve tools by package path,
|
|
308
|
+
// so these non-runtime shims must not escape into the immutable closure.
|
|
309
|
+
if (segments.includes(".bin"))
|
|
310
|
+
return false;
|
|
311
|
+
return !excludeNodeModules || !segments.includes("node_modules");
|
|
312
|
+
},
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
function findPnpmWorkspaceRoot(start) {
|
|
316
|
+
let current = start;
|
|
317
|
+
while (true) {
|
|
318
|
+
if (existsSync(join(current, "pnpm-workspace.yaml")))
|
|
319
|
+
return current;
|
|
320
|
+
const parent = dirname(current);
|
|
321
|
+
if (parent === current)
|
|
322
|
+
return undefined;
|
|
323
|
+
current = parent;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
async function stageLocalWorkspaceClosure(pluginRoot, workspaceRoot, stagingRoot) {
|
|
327
|
+
const packages = new Map();
|
|
328
|
+
const workspaceNodeModules = existsSync(join(workspaceRoot, "node_modules"))
|
|
329
|
+
? realpathSync(join(workspaceRoot, "node_modules"))
|
|
330
|
+
: undefined;
|
|
331
|
+
const visit = (source) => {
|
|
332
|
+
const sourceReal = realpathSync(source);
|
|
333
|
+
const packageJson = readJsonObject(join(sourceReal, "package.json"), "package.json");
|
|
334
|
+
const name = packageJson.name;
|
|
335
|
+
if (typeof name !== "string" || !isSafeNpmPackageName(name)) {
|
|
336
|
+
throw new Error(`local workspace package has an invalid name: ${String(name)}`);
|
|
337
|
+
}
|
|
338
|
+
const existing = packages.get(name);
|
|
339
|
+
if (existing) {
|
|
340
|
+
if (existing.source !== sourceReal) {
|
|
341
|
+
throw new Error(`local workspace contains duplicate package name: ${name}`);
|
|
342
|
+
}
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
const key = createHash("sha256").update(name).digest("hex").slice(0, 16);
|
|
346
|
+
packages.set(name, {
|
|
347
|
+
source: sourceReal,
|
|
348
|
+
staged: join(stagingRoot, "packages", key),
|
|
349
|
+
archive: join(stagingRoot, "archives", `${key}.tgz`),
|
|
350
|
+
});
|
|
351
|
+
for (const field of ["dependencies", "optionalDependencies", "peerDependencies"]) {
|
|
352
|
+
const dependencies = packageJson[field];
|
|
353
|
+
if (!isRecord(dependencies))
|
|
354
|
+
continue;
|
|
355
|
+
for (const [dependencyName, spec] of Object.entries(dependencies)) {
|
|
356
|
+
const dependencyPath = join(sourceReal, "node_modules", ...dependencyName.split("/"));
|
|
357
|
+
if (!existsSync(dependencyPath)) {
|
|
358
|
+
if (typeof spec === "string" && spec.startsWith("workspace:")) {
|
|
359
|
+
throw new Error(`local workspace dependency is not installed: ${name} -> ${dependencyName}`);
|
|
360
|
+
}
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
363
|
+
const dependencyReal = realpathSync(dependencyPath);
|
|
364
|
+
if (!isWithin(workspaceRoot, dependencyReal) ||
|
|
365
|
+
(workspaceNodeModules && isWithin(workspaceNodeModules, dependencyReal))) {
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
const dependencyPackage = readJsonObject(join(dependencyReal, "package.json"), `${dependencyName} package.json`);
|
|
369
|
+
if (dependencyPackage.name !== dependencyName) {
|
|
370
|
+
throw new Error(`local workspace dependency identity mismatch: ${dependencyName}`);
|
|
371
|
+
}
|
|
372
|
+
visit(dependencyReal);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
};
|
|
376
|
+
rmSync(stagingRoot, { recursive: true, force: true });
|
|
377
|
+
mkdirSync(stagingRoot, { recursive: true, mode: 0o700 });
|
|
378
|
+
visit(pluginRoot);
|
|
379
|
+
for (const entry of packages.values())
|
|
380
|
+
copyPackageTree(entry.source, entry.staged, true);
|
|
381
|
+
for (const entry of packages.values()) {
|
|
382
|
+
const file = join(entry.staged, "package.json");
|
|
383
|
+
const packageJson = readJsonObject(file, "staged package.json");
|
|
384
|
+
for (const field of ["dependencies", "optionalDependencies", "peerDependencies"]) {
|
|
385
|
+
const dependencies = packageJson[field];
|
|
386
|
+
if (!isRecord(dependencies))
|
|
387
|
+
continue;
|
|
388
|
+
for (const dependencyName of Object.keys(dependencies)) {
|
|
389
|
+
const target = packages.get(dependencyName);
|
|
390
|
+
if (target)
|
|
391
|
+
dependencies[dependencyName] = `file:${target.archive}`;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
writeFileSync(file, `${JSON.stringify(packageJson, null, 2)}\n`, { mode: 0o600 });
|
|
395
|
+
}
|
|
396
|
+
await Promise.all([...packages.values()].map((entry) => createPackageArchive(entry.staged, entry.archive)));
|
|
397
|
+
const pluginName = readJsonObject(join(pluginRoot, "package.json"), "package.json").name;
|
|
398
|
+
const plugin = typeof pluginName === "string" ? packages.get(pluginName) : undefined;
|
|
399
|
+
if (!plugin)
|
|
400
|
+
throw new Error("local workspace plugin was not staged");
|
|
401
|
+
return { pluginArchive: plugin.archive, packageCount: packages.size };
|
|
402
|
+
}
|
|
403
|
+
async function createPackageArchive(packageRoot, archive) {
|
|
404
|
+
mkdirSync(dirname(archive), { recursive: true, mode: 0o700 });
|
|
405
|
+
await createTar({
|
|
406
|
+
cwd: packageRoot,
|
|
407
|
+
file: archive,
|
|
408
|
+
gzip: true,
|
|
409
|
+
portable: true,
|
|
410
|
+
prefix: "package",
|
|
411
|
+
noMtime: true,
|
|
412
|
+
}, readdirSync(packageRoot));
|
|
413
|
+
}
|
|
414
|
+
function hasProductionDependencies(packageRoot) {
|
|
415
|
+
const packageJson = readJsonObject(join(packageRoot, "package.json"), "package.json");
|
|
416
|
+
return ["dependencies", "optionalDependencies", "peerDependencies"].some((key) => {
|
|
417
|
+
const value = packageJson[key];
|
|
418
|
+
return isRecord(value) && Object.keys(value).length > 0;
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
function resolvePackageEntry(packageRoot, entry, label) {
|
|
422
|
+
if (!entry.startsWith("./") || entry.includes("\\") || entry.includes("\0")) {
|
|
423
|
+
throw new Error(`${label} must be a package-root-relative POSIX path starting with ./`);
|
|
424
|
+
}
|
|
425
|
+
const segments = entry.slice(2).split("/");
|
|
426
|
+
if (segments.some((segment) => !segment || segment === "." || segment === "..")) {
|
|
427
|
+
throw new Error(`${label} must not contain empty, . or .. segments`);
|
|
428
|
+
}
|
|
429
|
+
return requireContainedFile(packageRoot, resolve(packageRoot, entry), label, "file");
|
|
430
|
+
}
|
|
431
|
+
function requireContainedFile(root, candidate, label, kind) {
|
|
432
|
+
let rootReal;
|
|
433
|
+
let candidateReal;
|
|
434
|
+
try {
|
|
435
|
+
rootReal = realpathSync(root);
|
|
436
|
+
candidateReal = realpathSync(candidate);
|
|
437
|
+
}
|
|
438
|
+
catch {
|
|
439
|
+
throw new Error(`${label} does not exist`);
|
|
440
|
+
}
|
|
441
|
+
if (!isWithin(rootReal, candidateReal)) {
|
|
442
|
+
throw new Error(`${label} resolves outside the plugin package`);
|
|
443
|
+
}
|
|
444
|
+
const stat = statSync(candidateReal);
|
|
445
|
+
if (kind === "file" ? !stat.isFile() : !stat.isDirectory()) {
|
|
446
|
+
throw new Error(`${label} must be a ${kind}`);
|
|
447
|
+
}
|
|
448
|
+
return candidateReal;
|
|
449
|
+
}
|
|
450
|
+
function isWithin(root, candidate) {
|
|
451
|
+
const rel = relative(root, candidate);
|
|
452
|
+
return rel === "" || (!rel.startsWith(`..${sep}`) && rel !== ".." && !isAbsolute(rel));
|
|
453
|
+
}
|
|
454
|
+
function readJsonObject(path, label) {
|
|
455
|
+
let raw;
|
|
456
|
+
try {
|
|
457
|
+
const stat = statSync(path);
|
|
458
|
+
if (!stat.isFile())
|
|
459
|
+
throw new Error("not a file");
|
|
460
|
+
if (stat.size > MAX_METADATA_BYTES)
|
|
461
|
+
throw new Error("metadata is too large");
|
|
462
|
+
raw = readFileSync(path, "utf8");
|
|
463
|
+
}
|
|
464
|
+
catch (error) {
|
|
465
|
+
throw new Error(`cannot read ${label}: ${messageOf(error)}`);
|
|
466
|
+
}
|
|
467
|
+
try {
|
|
468
|
+
const parsed = JSON.parse(raw);
|
|
469
|
+
if (!isRecord(parsed))
|
|
470
|
+
throw new Error("must contain a JSON object");
|
|
471
|
+
return parsed;
|
|
472
|
+
}
|
|
473
|
+
catch (error) {
|
|
474
|
+
throw new Error(`invalid ${label}: ${messageOf(error)}`);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
function hashPackageTree(root) {
|
|
478
|
+
const hash = createHash("sha256");
|
|
479
|
+
hash.update("rynx-plugin-package-v1\0");
|
|
480
|
+
visit(root, "");
|
|
481
|
+
return `sha256-${hash.digest("base64")}`;
|
|
482
|
+
function visit(path, rel) {
|
|
483
|
+
const stat = lstatSync(path);
|
|
484
|
+
const normalized = rel.split(sep).join("/");
|
|
485
|
+
if (stat.isDirectory()) {
|
|
486
|
+
hash.update(`d\0${normalized}\0`);
|
|
487
|
+
for (const name of readdirSync(path).sort())
|
|
488
|
+
visit(join(path, name), join(rel, name));
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
if (stat.isFile()) {
|
|
492
|
+
hash.update(`f\0${normalized}\0${stat.size}\0`);
|
|
493
|
+
hash.update(readFileSync(path));
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
if (stat.isSymbolicLink()) {
|
|
497
|
+
hash.update(`l\0${normalized}\0${readlinkSync(path)}\0`);
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
throw new Error(`unsupported package entry: ${normalized}`);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
function isSafeNpmPackageName(name) {
|
|
504
|
+
return /^(?:@[A-Za-z0-9][A-Za-z0-9._-]*\/)?[A-Za-z0-9][A-Za-z0-9._-]*$/.test(name);
|
|
505
|
+
}
|
|
506
|
+
function runNpm(args, onProgress, signal) {
|
|
507
|
+
return runCommand("npm", args, onProgress, signal);
|
|
508
|
+
}
|
|
509
|
+
function runCommand(command, args, onProgress, signal, cwd) {
|
|
510
|
+
return new Promise((resolveDone, reject) => {
|
|
511
|
+
if (signal?.aborted) {
|
|
512
|
+
reject(new Error("plugin installation aborted"));
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
const child = spawn(command, args, {
|
|
516
|
+
cwd,
|
|
517
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
518
|
+
});
|
|
519
|
+
let settled = false;
|
|
520
|
+
const settle = (callback) => {
|
|
521
|
+
if (settled)
|
|
522
|
+
return;
|
|
523
|
+
settled = true;
|
|
524
|
+
signal?.removeEventListener("abort", abort);
|
|
525
|
+
callback();
|
|
526
|
+
};
|
|
527
|
+
const abort = () => child.kill();
|
|
528
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
529
|
+
const onData = (buffer) => {
|
|
530
|
+
for (const line of buffer.toString().split(/\r?\n/)) {
|
|
531
|
+
if (line.trim())
|
|
532
|
+
onProgress(line);
|
|
533
|
+
}
|
|
534
|
+
};
|
|
535
|
+
child.stdout?.on("data", onData);
|
|
536
|
+
child.stderr?.on("data", onData);
|
|
537
|
+
child.on("error", (error) => settle(() => reject(error)));
|
|
538
|
+
child.on("close", (code) => settle(() => {
|
|
539
|
+
if (signal?.aborted)
|
|
540
|
+
reject(new Error("plugin installation aborted"));
|
|
541
|
+
else if (code === 0)
|
|
542
|
+
resolveDone();
|
|
543
|
+
else
|
|
544
|
+
reject(new Error(`${command} exited with code ${code ?? "null"}`));
|
|
545
|
+
}));
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
function isRecord(value) {
|
|
549
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
550
|
+
}
|
|
551
|
+
function messageOf(error) {
|
|
552
|
+
return error instanceof Error ? error.message : String(error);
|
|
553
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { ensureDaemonControlEndpoint } from "./control-endpoint.js";
|
|
2
|
+
const RELOAD_TIMEOUT_MS = 30_000;
|
|
3
|
+
export async function reloadResidentPluginRuntime(pluginId) {
|
|
4
|
+
return requestResidentPluginRuntime(pluginId, "reload");
|
|
5
|
+
}
|
|
6
|
+
export async function ensureResidentPluginRuntime(pluginId) {
|
|
7
|
+
return requestResidentPluginRuntime(pluginId, "ensure");
|
|
8
|
+
}
|
|
9
|
+
async function requestResidentPluginRuntime(pluginId, operation) {
|
|
10
|
+
if (!/^[a-z][a-z0-9-]{0,62}$/.test(pluginId))
|
|
11
|
+
throw new Error("invalid plugin id");
|
|
12
|
+
const endpoint = await ensureDaemonControlEndpoint();
|
|
13
|
+
const response = await fetch(`${endpoint.origin}/api/internal/plugin-runtimes/${encodeURIComponent(pluginId)}/${operation}`, {
|
|
14
|
+
method: "POST",
|
|
15
|
+
headers: { "x-rynx-management-token": endpoint.managementToken },
|
|
16
|
+
signal: AbortSignal.timeout(RELOAD_TIMEOUT_MS),
|
|
17
|
+
});
|
|
18
|
+
const body = await response.text();
|
|
19
|
+
if (!response.ok) {
|
|
20
|
+
throw new Error(readError(body) || `plugin runtime refresh failed (${response.status})`);
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
return JSON.parse(body);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
throw new Error("daemon returned an invalid plugin runtime refresh response");
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function readError(body) {
|
|
30
|
+
try {
|
|
31
|
+
const parsed = JSON.parse(body);
|
|
32
|
+
return typeof parsed.error === "string" ? parsed.error : undefined;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return body.trim().slice(0, 2_000) || undefined;
|
|
36
|
+
}
|
|
37
|
+
}
|
package/dist/plugin-store.d.ts
CHANGED
|
@@ -1,27 +1,56 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Durable inventory of installed plugin packages.
|
|
3
|
+
*
|
|
4
|
+
* The registry stores only static, validated manifest data and a host-owned
|
|
5
|
+
* installation path. Runtime modules are never imported while reading this
|
|
6
|
+
* table. Rows from the pre-v1 executable-manifest format remain visible to the
|
|
7
|
+
* CLI, but are not runnable and must be reinstalled.
|
|
8
|
+
*/
|
|
9
|
+
import { type PluginCapability, type PluginManifestV1 } from "@rynx-ai/plugin-sdk";
|
|
10
|
+
export type PluginSource = "npm" | "local" | "git" | "url";
|
|
2
11
|
export interface PluginRecord {
|
|
3
|
-
/**
|
|
12
|
+
/** Manifest id. The SQLite column remains `name` for migration stability. */
|
|
4
13
|
name: string;
|
|
5
|
-
/**
|
|
14
|
+
/** User-supplied install source, retained for an explicit update. */
|
|
6
15
|
spec: string;
|
|
7
16
|
source: PluginSource;
|
|
8
17
|
version?: string;
|
|
18
|
+
/** Whether daemon runtime contributions are loaded; explicit CLI stays available. */
|
|
9
19
|
enabled: boolean;
|
|
10
20
|
installedAt: string;
|
|
21
|
+
updatedAt: string;
|
|
22
|
+
/** Host-generated immutable installation root. Missing only on legacy rows. */
|
|
23
|
+
installPath?: string;
|
|
24
|
+
/** Validated, pure-data manifest. Missing only on legacy rows. */
|
|
25
|
+
manifest?: PluginManifestV1;
|
|
26
|
+
/** sha256 integrity string for the materialized package. */
|
|
27
|
+
integrity?: string;
|
|
28
|
+
/** Capabilities approved for this installation. */
|
|
29
|
+
grants: PluginCapability[];
|
|
11
30
|
}
|
|
12
|
-
|
|
31
|
+
export interface RunnablePluginRecord extends PluginRecord {
|
|
32
|
+
installPath: string;
|
|
33
|
+
manifest: PluginManifestV1;
|
|
34
|
+
integrity: string;
|
|
35
|
+
}
|
|
36
|
+
/** All installed plugins, including incompatible legacy rows. */
|
|
13
37
|
export declare function listPlugins(): PluginRecord[];
|
|
14
|
-
/**
|
|
15
|
-
export declare function listEnabledPlugins():
|
|
38
|
+
/** Enabled v1 installations that are safe for the runner to consider. */
|
|
39
|
+
export declare function listEnabledPlugins(): RunnablePluginRecord[];
|
|
16
40
|
export declare function getPlugin(name: string): PluginRecord | undefined;
|
|
17
|
-
/**
|
|
41
|
+
/** Atomically point a plugin id at a completely materialized installation. */
|
|
18
42
|
export declare function upsertPlugin(rec: {
|
|
19
43
|
name: string;
|
|
20
44
|
spec: string;
|
|
21
45
|
source: PluginSource;
|
|
22
46
|
version?: string;
|
|
47
|
+
installPath: string;
|
|
48
|
+
manifest: PluginManifestV1;
|
|
49
|
+
integrity: string;
|
|
50
|
+
grants: readonly PluginCapability[];
|
|
23
51
|
enabled?: boolean;
|
|
24
52
|
}): void;
|
|
25
53
|
export declare function setPluginEnabled(name: string, enabled: boolean): void;
|
|
26
|
-
/** Remove
|
|
54
|
+
/** Remove only the registry row. Package deletion is owned by the installer. */
|
|
27
55
|
export declare function removePlugin(name: string): boolean;
|
|
56
|
+
export declare function isRunnablePluginRecord(record: PluginRecord): record is RunnablePluginRecord;
|