@rynx-ai/daemon 0.1.9 → 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/chrome-for-testing-store.js +1 -1
- 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 +13 -2
- package/dist/control-client.d.ts +9 -0
- package/dist/control-client.js +63 -0
- package/dist/daemon-build-status.d.ts +8 -0
- package/dist/daemon-build-status.js +18 -0
- package/dist/daemon-server.d.ts +19 -2
- package/dist/daemon-server.js +615 -59
- package/dist/db.js +56 -0
- package/dist/desktop-browser-host-client.d.ts +2 -1
- package/dist/desktop-browser-host-client.js +3 -1
- package/dist/direct-runtime-authenticator.js +11 -6
- package/dist/headless-browser-host.js +18 -1
- package/dist/index-daemon.js +28 -1
- package/dist/maintenance-management.d.ts +11 -0
- package/dist/maintenance-management.js +13 -0
- package/dist/plugin-installer.d.ts +35 -0
- package/dist/plugin-installer.js +195 -94
- package/dist/plugin-management-service.d.ts +58 -0
- package/dist/plugin-management-service.js +240 -0
- package/dist/plugin-package.d.ts +26 -3
- package/dist/plugin-package.js +235 -56
- package/dist/pm2.js +37 -5
- package/dist/remote-runtime-access-store.d.ts +1 -0
- package/dist/remote-runtime-access-store.js +7 -0
- package/dist/remote-runtime-admin.d.ts +3 -0
- package/dist/remote-runtime-admin.js +3 -0
- package/dist/remote-runtime-connection-manager.d.ts +12 -1
- package/dist/remote-runtime-connection-manager.js +170 -4
- package/dist/remote-runtime-target-control.d.ts +5 -1
- package/dist/remote-runtime-target-control.js +62 -7
- package/dist/remote-runtime-target-store.d.ts +4 -1
- package/dist/remote-runtime-target-store.js +21 -2
- package/dist/session-log-store.js +29 -0
- package/dist/session-meta-store.js +3 -1
- 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 +136 -2
- package/package.json +14 -9
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { type PluginInstallCommitInput, type PluginInstallCommitResult, type PluginInstallPreparation, type PluginInstallPrepareInput } from "@rynx-ai/protocol/plugin-management";
|
|
2
|
+
import { type PluginCapability } from "@rynx-ai/plugin-sdk";
|
|
3
|
+
import { type PreparePluginInstallationOptions, type PreparedPluginInstallation } from "./plugin-installer.js";
|
|
4
|
+
import { type PluginRecord, type PluginSource } from "./plugin-store.js";
|
|
5
|
+
export type PluginManagementState = "enabled" | "disabled" | "reinstall-required";
|
|
6
|
+
/** Redacted management view; installation paths and source specs never cross RPC. */
|
|
7
|
+
export interface PluginManagementItem {
|
|
8
|
+
id: string;
|
|
9
|
+
version?: string;
|
|
10
|
+
source: PluginSource;
|
|
11
|
+
state: PluginManagementState;
|
|
12
|
+
enabled: boolean;
|
|
13
|
+
requestedCapabilities: PluginCapability[];
|
|
14
|
+
grants: PluginCapability[];
|
|
15
|
+
updatedAt: string;
|
|
16
|
+
}
|
|
17
|
+
export interface PluginManagementServiceDependencies {
|
|
18
|
+
list(): PluginRecord[];
|
|
19
|
+
get(id: string): PluginRecord | undefined;
|
|
20
|
+
setEnabled(id: string, enabled: boolean): void;
|
|
21
|
+
uninstall(id: string): Promise<boolean>;
|
|
22
|
+
prepare(spec: string, onProgress: (line: string) => void, options: PreparePluginInstallationOptions): Promise<PreparedPluginInstallation>;
|
|
23
|
+
}
|
|
24
|
+
export interface PluginManagementServiceOptions {
|
|
25
|
+
preparationTtlMs?: number;
|
|
26
|
+
now?: () => number;
|
|
27
|
+
token?: () => string;
|
|
28
|
+
schedule?: (callback: () => void, delayMs: number) => ReturnType<typeof setTimeout>;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Daemon-owned plugin registry mutations exposed to the authenticated local
|
|
32
|
+
* management API. Package installation remains outside this service until its
|
|
33
|
+
* capability approval is cryptographically bound to one staged artifact.
|
|
34
|
+
*/
|
|
35
|
+
export declare class PluginManagementService {
|
|
36
|
+
private readonly dependencies;
|
|
37
|
+
private readonly pending;
|
|
38
|
+
private readonly now;
|
|
39
|
+
private readonly token;
|
|
40
|
+
private readonly schedule;
|
|
41
|
+
private readonly preparationTtlMs;
|
|
42
|
+
constructor(dependencies?: PluginManagementServiceDependencies, options?: PluginManagementServiceOptions);
|
|
43
|
+
list(): {
|
|
44
|
+
plugins: PluginManagementItem[];
|
|
45
|
+
};
|
|
46
|
+
setEnabled(id: string, enabled: boolean): PluginManagementItem | undefined;
|
|
47
|
+
uninstall(id: string): Promise<boolean>;
|
|
48
|
+
prepareInstallation(input: PluginInstallPrepareInput, options?: {
|
|
49
|
+
signal?: AbortSignal;
|
|
50
|
+
}): Promise<PluginInstallPreparation>;
|
|
51
|
+
commitInstallation(token: string, input: PluginInstallCommitInput, options?: {
|
|
52
|
+
signal?: AbortSignal;
|
|
53
|
+
}): Promise<PluginInstallCommitResult>;
|
|
54
|
+
cancelPreparation(token: string): boolean;
|
|
55
|
+
dispose(): void;
|
|
56
|
+
private takePreparation;
|
|
57
|
+
private uniqueToken;
|
|
58
|
+
}
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { PLUGIN_ID_PATTERN } from "@rynx-ai/plugin-sdk";
|
|
3
|
+
import { preparePluginInstallation, uninstallPlugin, } from "./plugin-installer.js";
|
|
4
|
+
import { getPlugin, isRunnablePluginRecord, listPlugins, setPluginEnabled, } from "./plugin-store.js";
|
|
5
|
+
const defaultDependencies = {
|
|
6
|
+
list: listPlugins,
|
|
7
|
+
get: getPlugin,
|
|
8
|
+
setEnabled: setPluginEnabled,
|
|
9
|
+
uninstall: (id) => uninstallPlugin(id),
|
|
10
|
+
prepare: preparePluginInstallation,
|
|
11
|
+
};
|
|
12
|
+
const DEFAULT_PREPARATION_TTL_MS = 5 * 60 * 1_000;
|
|
13
|
+
/**
|
|
14
|
+
* Daemon-owned plugin registry mutations exposed to the authenticated local
|
|
15
|
+
* management API. Package installation remains outside this service until its
|
|
16
|
+
* capability approval is cryptographically bound to one staged artifact.
|
|
17
|
+
*/
|
|
18
|
+
export class PluginManagementService {
|
|
19
|
+
dependencies;
|
|
20
|
+
pending = new Map();
|
|
21
|
+
now;
|
|
22
|
+
token;
|
|
23
|
+
schedule;
|
|
24
|
+
preparationTtlMs;
|
|
25
|
+
constructor(dependencies = defaultDependencies, options = {}) {
|
|
26
|
+
this.dependencies = dependencies;
|
|
27
|
+
this.preparationTtlMs = options.preparationTtlMs ?? DEFAULT_PREPARATION_TTL_MS;
|
|
28
|
+
if (!Number.isSafeInteger(this.preparationTtlMs) || this.preparationTtlMs <= 0) {
|
|
29
|
+
throw new Error("plugin preparation TTL must be a positive safe integer");
|
|
30
|
+
}
|
|
31
|
+
this.now = options.now ?? Date.now;
|
|
32
|
+
this.token = options.token ?? (() => randomBytes(32).toString("base64url"));
|
|
33
|
+
this.schedule = options.schedule ?? setTimeout;
|
|
34
|
+
}
|
|
35
|
+
list() {
|
|
36
|
+
return { plugins: this.dependencies.list().map(toManagementItem) };
|
|
37
|
+
}
|
|
38
|
+
setEnabled(id, enabled) {
|
|
39
|
+
validatePluginId(id);
|
|
40
|
+
const current = this.dependencies.get(id);
|
|
41
|
+
if (!current)
|
|
42
|
+
return undefined;
|
|
43
|
+
if (!isRunnablePluginRecord(current)) {
|
|
44
|
+
throw new Error(`plugin "${id}" must be reinstalled before it can be ${enabled ? "enabled" : "disabled"}`);
|
|
45
|
+
}
|
|
46
|
+
this.dependencies.setEnabled(id, enabled);
|
|
47
|
+
const updated = this.dependencies.get(id);
|
|
48
|
+
if (!updated) {
|
|
49
|
+
throw new Error(`plugin "${id}" disappeared while updating its state`);
|
|
50
|
+
}
|
|
51
|
+
return toManagementItem(updated);
|
|
52
|
+
}
|
|
53
|
+
async uninstall(id) {
|
|
54
|
+
validatePluginId(id);
|
|
55
|
+
return this.dependencies.uninstall(id);
|
|
56
|
+
}
|
|
57
|
+
async prepareInstallation(input, options = {}) {
|
|
58
|
+
options.signal?.throwIfAborted();
|
|
59
|
+
let spec;
|
|
60
|
+
let expectedId;
|
|
61
|
+
if (input.operation === "install") {
|
|
62
|
+
if (!input.spec)
|
|
63
|
+
throw new Error("plugin install source is required");
|
|
64
|
+
spec = input.spec;
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
if (!input.pluginId)
|
|
68
|
+
throw new Error("plugin update id is required");
|
|
69
|
+
validatePluginId(input.pluginId);
|
|
70
|
+
const existing = this.dependencies.get(input.pluginId);
|
|
71
|
+
if (!existing)
|
|
72
|
+
throw new Error(`plugin "${input.pluginId}" not found`);
|
|
73
|
+
if (!isRunnablePluginRecord(existing)) {
|
|
74
|
+
throw new Error(`plugin "${input.pluginId}" uses the removed legacy format; reinstall it`);
|
|
75
|
+
}
|
|
76
|
+
spec = existing.spec;
|
|
77
|
+
expectedId = existing.name;
|
|
78
|
+
}
|
|
79
|
+
const prepared = await this.dependencies.prepare(spec, () => { }, {
|
|
80
|
+
allowScripts: input.allowScripts,
|
|
81
|
+
...(expectedId ? { expectedId } : {}),
|
|
82
|
+
...(options.signal ? { signal: options.signal } : {}),
|
|
83
|
+
});
|
|
84
|
+
try {
|
|
85
|
+
options.signal?.throwIfAborted();
|
|
86
|
+
const token = this.uniqueToken();
|
|
87
|
+
const expiresAtMs = this.now() + this.preparationTtlMs;
|
|
88
|
+
const expiryTimer = this.schedule(() => {
|
|
89
|
+
const current = this.pending.get(token);
|
|
90
|
+
if (!current || current.prepared !== prepared)
|
|
91
|
+
return;
|
|
92
|
+
this.pending.delete(token);
|
|
93
|
+
current.prepared.discard();
|
|
94
|
+
}, this.preparationTtlMs);
|
|
95
|
+
expiryTimer.unref?.();
|
|
96
|
+
this.pending.set(token, { prepared, expiresAtMs, expiryTimer });
|
|
97
|
+
return toInstallPreparation(token, expiresAtMs, prepared);
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
prepared.discard();
|
|
101
|
+
throw error;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
async commitInstallation(token, input, options = {}) {
|
|
105
|
+
const pending = this.takePreparation(token);
|
|
106
|
+
if (!pending)
|
|
107
|
+
throw invalidPreparationError();
|
|
108
|
+
clearTimeout(pending.expiryTimer);
|
|
109
|
+
if (this.now() >= pending.expiresAtMs) {
|
|
110
|
+
pending.prepared.discard();
|
|
111
|
+
throw invalidPreparationError();
|
|
112
|
+
}
|
|
113
|
+
const { prepared } = pending;
|
|
114
|
+
try {
|
|
115
|
+
options.signal?.throwIfAborted();
|
|
116
|
+
if (input.expectedDigest !== prepared.integrity) {
|
|
117
|
+
throw new Error("prepared plugin digest does not match expected digest");
|
|
118
|
+
}
|
|
119
|
+
if (prepared.existing && !input.overwrite) {
|
|
120
|
+
throw new Error(`plugin "${prepared.name}" already exists; explicit overwrite approval is required`);
|
|
121
|
+
}
|
|
122
|
+
const grants = input.grants.map((grant) => {
|
|
123
|
+
const capability = prepared.manifest.requestedCapabilities.find((requested) => requested === grant);
|
|
124
|
+
if (!capability)
|
|
125
|
+
throw new Error(`plugin capability was not requested: ${grant}`);
|
|
126
|
+
return capability;
|
|
127
|
+
});
|
|
128
|
+
await prepared.commit({
|
|
129
|
+
grants,
|
|
130
|
+
expectedDigest: input.expectedDigest,
|
|
131
|
+
signal: options.signal,
|
|
132
|
+
});
|
|
133
|
+
const installed = this.dependencies.get(prepared.name);
|
|
134
|
+
if (!installed)
|
|
135
|
+
throw new Error(`plugin "${prepared.name}" was not registered after commit`);
|
|
136
|
+
return { plugin: toManagementItem(installed) };
|
|
137
|
+
}
|
|
138
|
+
finally {
|
|
139
|
+
prepared.discard();
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
cancelPreparation(token) {
|
|
143
|
+
const pending = this.takePreparation(token);
|
|
144
|
+
if (!pending)
|
|
145
|
+
return false;
|
|
146
|
+
clearTimeout(pending.expiryTimer);
|
|
147
|
+
pending.prepared.discard();
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
dispose() {
|
|
151
|
+
for (const [token, pending] of this.pending) {
|
|
152
|
+
this.pending.delete(token);
|
|
153
|
+
clearTimeout(pending.expiryTimer);
|
|
154
|
+
pending.prepared.discard();
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
takePreparation(token) {
|
|
158
|
+
if (!/^[A-Za-z0-9_-]{32,128}$/.test(token))
|
|
159
|
+
return undefined;
|
|
160
|
+
const pending = this.pending.get(token);
|
|
161
|
+
if (pending)
|
|
162
|
+
this.pending.delete(token);
|
|
163
|
+
return pending;
|
|
164
|
+
}
|
|
165
|
+
uniqueToken() {
|
|
166
|
+
for (let attempt = 0; attempt < 4; attempt += 1) {
|
|
167
|
+
const token = this.token();
|
|
168
|
+
if (/^[A-Za-z0-9_-]{32,128}$/.test(token) && !this.pending.has(token))
|
|
169
|
+
return token;
|
|
170
|
+
}
|
|
171
|
+
throw new Error("failed to allocate a plugin preparation token");
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
function toManagementItem(record) {
|
|
175
|
+
const runnable = isRunnablePluginRecord(record);
|
|
176
|
+
return {
|
|
177
|
+
id: record.name,
|
|
178
|
+
...(record.version ? { version: record.version } : {}),
|
|
179
|
+
source: record.source,
|
|
180
|
+
state: !runnable
|
|
181
|
+
? "reinstall-required"
|
|
182
|
+
: record.enabled
|
|
183
|
+
? "enabled"
|
|
184
|
+
: "disabled",
|
|
185
|
+
enabled: runnable && record.enabled,
|
|
186
|
+
requestedCapabilities: runnable ? [...record.manifest.requestedCapabilities] : [],
|
|
187
|
+
grants: [...record.grants],
|
|
188
|
+
updatedAt: record.updatedAt,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
function toInstallPreparation(token, expiresAtMs, prepared) {
|
|
192
|
+
const requested = [...prepared.manifest.requestedCapabilities];
|
|
193
|
+
const prior = prepared.existing?.grants ?? [];
|
|
194
|
+
const requestedSet = new Set(requested);
|
|
195
|
+
const priorSet = new Set(prior);
|
|
196
|
+
return {
|
|
197
|
+
token,
|
|
198
|
+
expiresAt: new Date(expiresAtMs).toISOString(),
|
|
199
|
+
artifact: {
|
|
200
|
+
id: prepared.name,
|
|
201
|
+
...(prepared.version ? { version: prepared.version } : {}),
|
|
202
|
+
source: prepared.source,
|
|
203
|
+
digest: prepared.integrity,
|
|
204
|
+
requestedCapabilities: requested,
|
|
205
|
+
requiresBuildScripts: Boolean(prepared.manifest.requiresBuildScripts),
|
|
206
|
+
},
|
|
207
|
+
...(prepared.existing
|
|
208
|
+
? {
|
|
209
|
+
existing: {
|
|
210
|
+
id: prepared.existing.name,
|
|
211
|
+
...(prepared.existing.version
|
|
212
|
+
? { version: prepared.existing.version }
|
|
213
|
+
: {}),
|
|
214
|
+
source: prepared.existing.source,
|
|
215
|
+
state: managementState(prepared.existing),
|
|
216
|
+
grants: [...prepared.existing.grants],
|
|
217
|
+
},
|
|
218
|
+
}
|
|
219
|
+
: {}),
|
|
220
|
+
capabilityDiff: {
|
|
221
|
+
added: requested.filter((capability) => !priorSet.has(capability)),
|
|
222
|
+
retained: requested.filter((capability) => priorSet.has(capability)),
|
|
223
|
+
removed: prior.filter((capability) => !requestedSet.has(capability)),
|
|
224
|
+
},
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
function managementState(record) {
|
|
228
|
+
return !isRunnablePluginRecord(record)
|
|
229
|
+
? "reinstall-required"
|
|
230
|
+
: record.enabled
|
|
231
|
+
? "enabled"
|
|
232
|
+
: "disabled";
|
|
233
|
+
}
|
|
234
|
+
function invalidPreparationError() {
|
|
235
|
+
return new Error("plugin installation preparation is invalid or expired");
|
|
236
|
+
}
|
|
237
|
+
function validatePluginId(id) {
|
|
238
|
+
if (!PLUGIN_ID_PATTERN.test(id))
|
|
239
|
+
throw new Error("invalid plugin id");
|
|
240
|
+
}
|
package/dist/plugin-package.d.ts
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
import { type PluginManifestV1 } from "@rynx-ai/plugin-sdk";
|
|
2
2
|
import type { PluginSource } from "./plugin-store.js";
|
|
3
|
+
export interface PluginPackageInspectionLimits {
|
|
4
|
+
maxFileBytes: number;
|
|
5
|
+
maxTotalBytes: number;
|
|
6
|
+
maxEntries: number;
|
|
7
|
+
maxDepth: number;
|
|
8
|
+
}
|
|
9
|
+
export declare const DEFAULT_PLUGIN_PACKAGE_INSPECTION_LIMITS: Readonly<PluginPackageInspectionLimits>;
|
|
10
|
+
export interface InspectPluginPackageOptions {
|
|
11
|
+
requireEntries?: boolean;
|
|
12
|
+
signal?: AbortSignal;
|
|
13
|
+
/** Test/internal override; daemon installation always uses the safe defaults. */
|
|
14
|
+
limits?: Partial<PluginPackageInspectionLimits>;
|
|
15
|
+
}
|
|
3
16
|
export interface MaterializePluginPackageRequest {
|
|
4
17
|
spec: string;
|
|
5
18
|
source: PluginSource;
|
|
@@ -31,8 +44,18 @@ export declare function parseGitSpec(spec: string): {
|
|
|
31
44
|
ref?: string;
|
|
32
45
|
};
|
|
33
46
|
/** 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;
|
|
47
|
+
export declare function inspectPluginPackage(packageDir: string, options?: InspectPluginPackageOptions): Promise<InspectedPluginPackage>;
|
|
37
48
|
/** Run package/dependency build scripts only after explicit user approval. */
|
|
38
49
|
export declare function rebuildPluginPackage(packageDir: string, onProgress: (line: string) => void, signal?: AbortSignal): Promise<void>;
|
|
50
|
+
export interface RunPluginPackageCommandOptions {
|
|
51
|
+
signal?: AbortSignal;
|
|
52
|
+
cwd?: string;
|
|
53
|
+
/** Internal/test override; production uses a bounded two-second grace. */
|
|
54
|
+
abortGraceMs?: number;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Run one package tool without a shell and tear down its full POSIX process
|
|
58
|
+
* group on cancellation. Windows cannot signal process groups through Node,
|
|
59
|
+
* so it safely degrades to TERM→KILL on the direct child.
|
|
60
|
+
*/
|
|
61
|
+
export declare function runPluginPackageCommand(command: string, args: string[], onProgress: (line: string) => void, options?: RunPluginPackageCommandOptions): Promise<void>;
|