@parall/daemon 1.32.0 → 1.33.0
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/bundle/bb-browser-daemon.js +15628 -0
- package/bundle/buildDomTree.js +1501 -0
- package/bundle/manifest.json +19 -11
- package/bundle/parall-claude-agent.js +224 -58
- package/bundle/parall-codex-agent.js +224 -58
- package/bundle/parall-daemon.js +4507 -2668
- package/bundle/parall-openclaw-agent.js +4 -3
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +8 -2
- package/dist/clip-runtime/browser-dependency.d.ts +20 -0
- package/dist/clip-runtime/browser-dependency.d.ts.map +1 -0
- package/dist/clip-runtime/browser-dependency.js +52 -0
- package/dist/clip-runtime/browser-profile-manager.d.ts +67 -0
- package/dist/clip-runtime/browser-profile-manager.d.ts.map +1 -0
- package/dist/clip-runtime/browser-profile-manager.js +595 -0
- package/dist/clip-runtime/bun-resolver.d.ts +24 -0
- package/dist/clip-runtime/bun-resolver.d.ts.map +1 -0
- package/dist/clip-runtime/bun-resolver.js +58 -0
- package/dist/clip-runtime/clip-installer.d.ts.map +1 -1
- package/dist/clip-runtime/clip-installer.js +59 -18
- package/dist/clip-runtime/clip-provider.d.ts +13 -2
- package/dist/clip-runtime/clip-provider.d.ts.map +1 -1
- package/dist/clip-runtime/clip-provider.js +106 -36
- package/dist/clip-runtime/hub-client.d.ts +79 -0
- package/dist/clip-runtime/hub-client.d.ts.map +1 -0
- package/dist/clip-runtime/hub-client.js +320 -0
- package/dist/clip-runtime/index.d.ts +2 -0
- package/dist/clip-runtime/index.d.ts.map +1 -1
- package/dist/clip-runtime/index.js +2 -0
- package/dist/clip-runtime/ipc.d.ts +6 -0
- package/dist/clip-runtime/ipc.d.ts.map +1 -1
- package/dist/clip-runtime/manifest.d.ts +16 -8
- package/dist/clip-runtime/manifest.d.ts.map +1 -1
- package/dist/clip-runtime/manifest.js +13 -0
- package/dist/clip-runtime/process-manager.d.ts +55 -3
- package/dist/clip-runtime/process-manager.d.ts.map +1 -1
- package/dist/clip-runtime/process-manager.js +233 -76
- package/dist/clip-runtime/process.d.ts +15 -1
- package/dist/clip-runtime/process.d.ts.map +1 -1
- package/dist/clip-runtime/process.js +73 -10
- package/dist/config.d.ts +9 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +12 -0
- package/dist/index.js +46 -5
- package/dist/runtime-bin-resolver.d.ts +7 -0
- package/dist/runtime-bin-resolver.d.ts.map +1 -0
- package/dist/runtime-bin-resolver.js +292 -0
- package/dist/supervisor.d.ts +53 -4
- package/dist/supervisor.d.ts.map +1 -1
- package/dist/supervisor.js +449 -117
- package/package.json +7 -6
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
import * as fs from 'node:fs';
|
|
2
2
|
import * as path from 'node:path';
|
|
3
|
-
import { execFileSync } from 'node:child_process';
|
|
4
3
|
import { ClipProcess, ClipCommandError, } from './process.js';
|
|
4
|
+
import { findBunBinary } from './bun-resolver.js';
|
|
5
|
+
import { BROWSER_CAPABILITY_NAME, BROWSER_CAPABILITY_PACKAGE, browserCapabilityCommands, isBrowserDependencyName, } from './browser-dependency.js';
|
|
6
|
+
import { HubCommandError } from './hub-client.js';
|
|
5
7
|
export class ClipProcessManager {
|
|
6
8
|
bunPath;
|
|
7
9
|
clipsDir;
|
|
8
10
|
dataDir;
|
|
11
|
+
browserProfileManager;
|
|
12
|
+
hubClient;
|
|
13
|
+
ensureInstalled;
|
|
9
14
|
processes = new Map();
|
|
10
15
|
startingUp = new Map();
|
|
11
16
|
statuses = new Map();
|
|
@@ -17,6 +22,13 @@ export class ClipProcessManager {
|
|
|
17
22
|
this.bunPath = opts.bunPath ?? '';
|
|
18
23
|
this.clipsDir = opts.clipsDir;
|
|
19
24
|
this.dataDir = opts.dataDir;
|
|
25
|
+
this.browserProfileManager = opts.browserProfileManager;
|
|
26
|
+
this.hubClient = opts.hubClient;
|
|
27
|
+
this.ensureInstalled = opts.ensureInstalled;
|
|
28
|
+
}
|
|
29
|
+
/** True when this daemon can host BrowserProfiles (serve inbound hub browser invokes). */
|
|
30
|
+
canHostBrowser() {
|
|
31
|
+
return this.browserProfileManager != null;
|
|
20
32
|
}
|
|
21
33
|
/** Backward-compat: set a single status listener (wraps addStatusListener). */
|
|
22
34
|
set onStatusChange(fn) {
|
|
@@ -68,17 +80,26 @@ export class ClipProcessManager {
|
|
|
68
80
|
this.setStatus(config.name, 'sleeping', 'registered');
|
|
69
81
|
}
|
|
70
82
|
}
|
|
83
|
+
async replaceClipConfig(config) {
|
|
84
|
+
this.registerClip(config);
|
|
85
|
+
await this.stopClip(config.name);
|
|
86
|
+
// An in-flight cold start may have resolved ensureInstalled() for the old
|
|
87
|
+
// config while stopClip was waiting. Re-apply the desired config after the
|
|
88
|
+
// stop window so the next lazy spawn uses the server-declared state.
|
|
89
|
+
this.registerClip(config);
|
|
90
|
+
}
|
|
71
91
|
async unregisterClip(name) {
|
|
72
92
|
this.shuttingDown.add(name);
|
|
73
93
|
try {
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
this.
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
await this.stopClip(name);
|
|
94
|
+
const startupStops = [];
|
|
95
|
+
for (const [key, startup] of this.startingUp) {
|
|
96
|
+
if (!this.processKeyBelongsToName(key, name))
|
|
97
|
+
continue;
|
|
98
|
+
this.startingUp.delete(key);
|
|
99
|
+
startupStops.push(startup.then((proc) => proc.stop()).catch(() => { }));
|
|
81
100
|
}
|
|
101
|
+
await Promise.allSettled(startupStops);
|
|
102
|
+
await this.stopClip(name);
|
|
82
103
|
this.clipConfigs.delete(name);
|
|
83
104
|
this.statuses.delete(name);
|
|
84
105
|
}
|
|
@@ -92,11 +113,27 @@ export class ClipProcessManager {
|
|
|
92
113
|
async stopClip(name) {
|
|
93
114
|
this.shuttingDown.add(name);
|
|
94
115
|
try {
|
|
95
|
-
|
|
96
|
-
|
|
116
|
+
// Also tear down any same-name shards still in their cold-start window.
|
|
117
|
+
// Without this an in-flight startup would complete and re-register into
|
|
118
|
+
// this.processes after stopClip returns, defeating the explicit stop.
|
|
119
|
+
const startupStops = [];
|
|
120
|
+
for (const [key, startup] of this.startingUp) {
|
|
121
|
+
if (!this.processKeyBelongsToName(key, name))
|
|
122
|
+
continue;
|
|
123
|
+
this.startingUp.delete(key);
|
|
124
|
+
startupStops.push(startup.then((proc) => proc.stop()).catch(() => { }));
|
|
125
|
+
}
|
|
126
|
+
const stops = [];
|
|
127
|
+
for (const [key, proc] of this.processes) {
|
|
128
|
+
if (!this.processKeyBelongsToName(key, name))
|
|
129
|
+
continue;
|
|
130
|
+
stops.push(proc.stop().finally(() => {
|
|
131
|
+
this.processes.delete(key);
|
|
132
|
+
}));
|
|
133
|
+
}
|
|
134
|
+
if (stops.length === 0 && startupStops.length === 0)
|
|
97
135
|
return;
|
|
98
|
-
await
|
|
99
|
-
this.processes.delete(name);
|
|
136
|
+
await Promise.allSettled([...startupStops, ...stops]);
|
|
100
137
|
this.setStatus(name, 'sleeping', 'stopped');
|
|
101
138
|
}
|
|
102
139
|
finally {
|
|
@@ -105,11 +142,19 @@ export class ClipProcessManager {
|
|
|
105
142
|
}
|
|
106
143
|
async stopAll() {
|
|
107
144
|
// Mark all clips as shutting down before any async work
|
|
108
|
-
|
|
145
|
+
const names = new Set();
|
|
146
|
+
for (const key of this.processes.keys())
|
|
147
|
+
names.add(this.processNameFromKey(key));
|
|
148
|
+
for (const key of this.startingUp.keys())
|
|
149
|
+
names.add(this.processNameFromKey(key));
|
|
150
|
+
for (const name of names)
|
|
109
151
|
this.shuttingDown.add(name);
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
152
|
+
const stops = [];
|
|
153
|
+
for (const [key, proc] of this.processes) {
|
|
154
|
+
stops.push(proc.stop().finally(() => {
|
|
155
|
+
this.processes.delete(key);
|
|
156
|
+
}));
|
|
157
|
+
}
|
|
113
158
|
// Also abort any in-flight startups (register window)
|
|
114
159
|
for (const [, startup] of this.startingUp) {
|
|
115
160
|
stops.push(startup.then((proc) => proc.stop()).catch(() => { }));
|
|
@@ -125,38 +170,40 @@ export class ClipProcessManager {
|
|
|
125
170
|
* Application-level errors (ClipCommandError) are NOT retried.
|
|
126
171
|
* Corresponds to Pinix ProcessManager.Invoke().
|
|
127
172
|
*/
|
|
128
|
-
async invoke(name, command, input) {
|
|
173
|
+
async invoke(name, command, input, context = {}) {
|
|
174
|
+
const key = this.processKey(name, context);
|
|
129
175
|
try {
|
|
130
|
-
const proc = await this.ensureProcess(name);
|
|
176
|
+
const proc = await this.ensureProcess(name, context);
|
|
131
177
|
const result = await proc.invoke(command, input);
|
|
132
178
|
return result.output;
|
|
133
179
|
}
|
|
134
180
|
catch (err) {
|
|
135
181
|
if (err instanceof ClipCommandError)
|
|
136
182
|
throw err;
|
|
137
|
-
const old = this.processes.get(
|
|
183
|
+
const old = this.processes.get(key);
|
|
138
184
|
if (old?.alive())
|
|
139
185
|
await old.stop().catch(() => { });
|
|
140
|
-
this.processes.delete(
|
|
141
|
-
const proc = await this.ensureProcess(name);
|
|
186
|
+
this.processes.delete(key);
|
|
187
|
+
const proc = await this.ensureProcess(name, context);
|
|
142
188
|
const result = await proc.invoke(command, input);
|
|
143
189
|
return result.output;
|
|
144
190
|
}
|
|
145
191
|
}
|
|
146
|
-
async invokeStream(name, command, input, onChunk) {
|
|
192
|
+
async invokeStream(name, command, input, onChunk, context = {}) {
|
|
193
|
+
const key = this.processKey(name, context);
|
|
147
194
|
try {
|
|
148
|
-
const proc = await this.ensureProcess(name);
|
|
195
|
+
const proc = await this.ensureProcess(name, context);
|
|
149
196
|
const result = await proc.invokeStream(command, input, onChunk);
|
|
150
197
|
return result.output;
|
|
151
198
|
}
|
|
152
199
|
catch (err) {
|
|
153
200
|
if (err instanceof ClipCommandError)
|
|
154
201
|
throw err;
|
|
155
|
-
const old = this.processes.get(
|
|
202
|
+
const old = this.processes.get(key);
|
|
156
203
|
if (old?.alive())
|
|
157
204
|
await old.stop().catch(() => { });
|
|
158
|
-
this.processes.delete(
|
|
159
|
-
const proc = await this.ensureProcess(name);
|
|
205
|
+
this.processes.delete(key);
|
|
206
|
+
const proc = await this.ensureProcess(name, context);
|
|
160
207
|
const result = await proc.invokeStream(command, input, onChunk);
|
|
161
208
|
return result.output;
|
|
162
209
|
}
|
|
@@ -175,12 +222,29 @@ export class ClipProcessManager {
|
|
|
175
222
|
return { status: entry.status, message: entry.message };
|
|
176
223
|
}
|
|
177
224
|
isRunning(name) {
|
|
178
|
-
const proc
|
|
179
|
-
|
|
225
|
+
for (const [key, proc] of this.processes) {
|
|
226
|
+
if (this.processKeyBelongsToName(key, name) && proc.alive())
|
|
227
|
+
return true;
|
|
228
|
+
}
|
|
229
|
+
return false;
|
|
180
230
|
}
|
|
181
231
|
getRegisteredClips() {
|
|
182
232
|
return Array.from(this.clipConfigs.values());
|
|
183
233
|
}
|
|
234
|
+
/**
|
|
235
|
+
* Clips this daemon advertises to the hub as a provider: the locally
|
|
236
|
+
* installed clips plus, when this machine hosts BrowserProfiles, a synthetic
|
|
237
|
+
* "browser" capability so the hub can route cross-machine browser invokes
|
|
238
|
+
* here. The browser capability has no local subprocess — it is served by
|
|
239
|
+
* invokeBrowserCapability() against bb-browser.
|
|
240
|
+
*/
|
|
241
|
+
getProviderClips() {
|
|
242
|
+
const clips = this.getRegisteredClips();
|
|
243
|
+
if (this.canHostBrowser()) {
|
|
244
|
+
clips.push(browserCapabilityConfig());
|
|
245
|
+
}
|
|
246
|
+
return clips;
|
|
247
|
+
}
|
|
184
248
|
/**
|
|
185
249
|
* Load clip configs from the clips directory.
|
|
186
250
|
* Reads clip-config.json if present, or scans subdirectories for clip.json files.
|
|
@@ -210,11 +274,20 @@ export class ClipProcessManager {
|
|
|
210
274
|
// Try clip.json first, fall back to package.json
|
|
211
275
|
const clipJsonPath = path.join(clipDir, 'clip.json');
|
|
212
276
|
const pkgJsonPath = path.join(clipDir, 'package.json');
|
|
277
|
+
// Key by the install-directory alias, NOT the manifest name. The
|
|
278
|
+
// installer writes to clipsDir/{org alias}/ and the install-time
|
|
279
|
+
// registration (supervisor handleClipInstall) keys by that alias — the
|
|
280
|
+
// hub routes org invokes by it (sess.clips[alias]). Preferring
|
|
281
|
+
// clipJson.name here re-keyed clips by their package name
|
|
282
|
+
// ("@pinix/reddit") after a daemon restart, so org-alias invokes
|
|
283
|
+
// ("reddit") failed with "no online provider" until reinstall (caught
|
|
284
|
+
// on the staging closed-loop E2E, 2026-06-04). The manifest/package
|
|
285
|
+
// identity stays on `package`.
|
|
213
286
|
if (fs.existsSync(clipJsonPath)) {
|
|
214
287
|
try {
|
|
215
288
|
const clipJson = JSON.parse(fs.readFileSync(clipJsonPath, 'utf-8'));
|
|
216
289
|
this.registerClip({
|
|
217
|
-
name:
|
|
290
|
+
name: entry.name,
|
|
218
291
|
package: clipJson.name,
|
|
219
292
|
version: clipJson.version,
|
|
220
293
|
source: clipDir,
|
|
@@ -229,7 +302,7 @@ export class ClipProcessManager {
|
|
|
229
302
|
try {
|
|
230
303
|
const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8'));
|
|
231
304
|
this.registerClip({
|
|
232
|
-
name:
|
|
305
|
+
name: entry.name,
|
|
233
306
|
package: pkgJson.name,
|
|
234
307
|
version: pkgJson.version,
|
|
235
308
|
source: clipDir,
|
|
@@ -243,40 +316,54 @@ export class ClipProcessManager {
|
|
|
243
316
|
}
|
|
244
317
|
}
|
|
245
318
|
// --- internal ---
|
|
246
|
-
async ensureProcess(name) {
|
|
319
|
+
async ensureProcess(name, context = {}) {
|
|
247
320
|
if (this.shuttingDown.has(name)) {
|
|
248
321
|
throw new Error(`clip "${name}" is shutting down`);
|
|
249
322
|
}
|
|
250
|
-
const
|
|
323
|
+
const key = this.processKey(name, context);
|
|
324
|
+
const existing = this.processes.get(key);
|
|
251
325
|
if (existing && existing.alive())
|
|
252
326
|
return existing;
|
|
253
327
|
// Deduplicate concurrent cold-start requests
|
|
254
|
-
const inflight = this.startingUp.get(
|
|
328
|
+
const inflight = this.startingUp.get(key);
|
|
255
329
|
if (inflight)
|
|
256
330
|
return inflight;
|
|
257
|
-
const
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
331
|
+
const startup = (async () => {
|
|
332
|
+
let config = this.clipConfigs.get(name);
|
|
333
|
+
if (!config)
|
|
334
|
+
throw new Error(`clip "${name}" not registered`);
|
|
335
|
+
if (this.ensureInstalled) {
|
|
336
|
+
config = await this.ensureInstalled(config);
|
|
337
|
+
this.clipConfigs.set(name, config);
|
|
338
|
+
}
|
|
339
|
+
return this.spawnProcess(config, context);
|
|
340
|
+
})();
|
|
341
|
+
this.startingUp.set(key, startup);
|
|
262
342
|
try {
|
|
263
343
|
return await startup;
|
|
264
344
|
}
|
|
265
345
|
finally {
|
|
266
|
-
this.startingUp.delete(
|
|
346
|
+
this.startingUp.delete(key);
|
|
267
347
|
}
|
|
268
348
|
}
|
|
269
|
-
async spawnProcess(config) {
|
|
270
|
-
const proc = new ClipProcess(config);
|
|
271
|
-
const clipDataDir = this.ensureClipDataDir(config);
|
|
272
|
-
|
|
349
|
+
async spawnProcess(config, context) {
|
|
350
|
+
const proc = new ClipProcess(config, context, (clipName, command, input, invokeContext) => this.invokeDependency(clipName, command, input, invokeContext));
|
|
351
|
+
const clipDataDir = this.ensureClipDataDir(config, context);
|
|
352
|
+
const bunPath = this.resolveBun();
|
|
353
|
+
proc.spawn(bunPath, {
|
|
273
354
|
PINIX_DATA_DIR: clipDataDir,
|
|
355
|
+
PRLL_CLIP_ID: context.clipId ?? '',
|
|
356
|
+
// Expose the embedded bun's directory on the clip's PATH so a clip that
|
|
357
|
+
// shells out to `bun` by name resolves it — the daemon's launchd PATH
|
|
358
|
+
// does not include Frameworks/. Symmetric with installDeps(); relies on
|
|
359
|
+
// the embedded binary being named `bun` (Frameworks/bun).
|
|
360
|
+
PATH: `${path.dirname(bunPath)}${path.delimiter}${process.env.PATH ?? ''}`,
|
|
274
361
|
});
|
|
275
362
|
this.setStatus(config.name, 'running', 'starting');
|
|
276
363
|
try {
|
|
277
364
|
await proc.waitReady();
|
|
278
365
|
// Only expose to concurrent callers after registration completes
|
|
279
|
-
this.processes.set(config.name, proc);
|
|
366
|
+
this.processes.set(this.processKey(config.name, context), proc);
|
|
280
367
|
this.setStatus(config.name, 'running', 'ready');
|
|
281
368
|
// Always cache manifest so Provider registrations have commands
|
|
282
369
|
const manifest = proc.getManifest();
|
|
@@ -292,7 +379,7 @@ export class ClipProcessManager {
|
|
|
292
379
|
}
|
|
293
380
|
// Monitor process exit in background
|
|
294
381
|
proc.waitDone().then(() => {
|
|
295
|
-
this.removeIfSame(config.name, proc);
|
|
382
|
+
this.removeIfSame(config.name, proc, context);
|
|
296
383
|
const exitErr = proc.getError();
|
|
297
384
|
if (exitErr) {
|
|
298
385
|
this.setStatus(config.name, 'error', exitErr.message);
|
|
@@ -303,16 +390,91 @@ export class ClipProcessManager {
|
|
|
303
390
|
});
|
|
304
391
|
return proc;
|
|
305
392
|
}
|
|
306
|
-
ensureClipDataDir(config) {
|
|
307
|
-
const dir = path.join(this.dataDir, config.name);
|
|
393
|
+
ensureClipDataDir(config, context = {}) {
|
|
394
|
+
const dir = path.join(this.dataDir, config.name, context.clipId ?? 'default');
|
|
308
395
|
fs.mkdirSync(dir, { recursive: true });
|
|
309
396
|
return dir;
|
|
310
397
|
}
|
|
311
|
-
removeIfSame(name, proc) {
|
|
312
|
-
|
|
313
|
-
|
|
398
|
+
removeIfSame(name, proc, context = {}) {
|
|
399
|
+
const key = this.processKey(name, context);
|
|
400
|
+
if (this.processes.get(key) === proc) {
|
|
401
|
+
this.processes.delete(key);
|
|
314
402
|
}
|
|
315
403
|
}
|
|
404
|
+
processKey(name, context = {}) {
|
|
405
|
+
return context.clipId ? `${name}::${context.clipId}` : name;
|
|
406
|
+
}
|
|
407
|
+
processKeyBelongsToName(key, name) {
|
|
408
|
+
return key === name || key.startsWith(`${name}::`);
|
|
409
|
+
}
|
|
410
|
+
processNameFromKey(key) {
|
|
411
|
+
const marker = key.indexOf('::');
|
|
412
|
+
return marker === -1 ? key : key.slice(0, marker);
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
415
|
+
* Execution-side nested dependency invoke (from a child clip's IPC). The
|
|
416
|
+
* browser slot is resolved UNIQUELY via the hub: read the Clip's binding
|
|
417
|
+
* (GetBindings), then forward the invoke through the hub (Invoke) to the bound
|
|
418
|
+
* BrowserProfile's host. No local shortcut, no fallback — unbound is rejected.
|
|
419
|
+
* The execution daemon never verifies clip_token; the hub does.
|
|
420
|
+
* See docs/engineering-design/browser-profile-clip-integration.md §11.3.
|
|
421
|
+
*/
|
|
422
|
+
async invokeDependency(clipName, command, input, context) {
|
|
423
|
+
if (!isBrowserDependencyName(clipName)) {
|
|
424
|
+
throw new Error(`clip dependency "${clipName}" is not supported`);
|
|
425
|
+
}
|
|
426
|
+
const clipId = context.clipId;
|
|
427
|
+
if (!clipId) {
|
|
428
|
+
// Without the executing Clip identity we cannot resolve the binding. The
|
|
429
|
+
// process key shards by clipId, so this should not happen for a
|
|
430
|
+
// browser-bound Clip.
|
|
431
|
+
throw new ClipCommandError('browser dependency requires a clip binding');
|
|
432
|
+
}
|
|
433
|
+
const hub = this.hubClient?.();
|
|
434
|
+
if (!hub) {
|
|
435
|
+
throw new Error('browser dependency runtime is not configured (no hub client)');
|
|
436
|
+
}
|
|
437
|
+
const bindings = await hub.getBindings(clipId);
|
|
438
|
+
const binding = bindings[BROWSER_CAPABILITY_NAME];
|
|
439
|
+
if (!binding || !binding.clipToken) {
|
|
440
|
+
// Unbound browser slot: surface as a clip-level error so the calling clip
|
|
441
|
+
// gets a recoverable rejection instead of a process crash.
|
|
442
|
+
throw new ClipCommandError(`clip "${clipId}" has no browser binding`, 'CLIP_PROFILE_UNBOUND');
|
|
443
|
+
}
|
|
444
|
+
try {
|
|
445
|
+
return await hub.invoke(BROWSER_CAPABILITY_NAME, command, input, binding.clipToken);
|
|
446
|
+
}
|
|
447
|
+
catch (err) {
|
|
448
|
+
// A hub-flagged application error (e.g. BROWSER_COMMAND_ERROR) is a
|
|
449
|
+
// recoverable clip-level rejection; translate it so the calling clip is
|
|
450
|
+
// not torn down. Transport/internal errors propagate as fatal.
|
|
451
|
+
if (err instanceof HubCommandError) {
|
|
452
|
+
throw new ClipCommandError(err.message, err.code);
|
|
453
|
+
}
|
|
454
|
+
throw err;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* Host-side browser capability handler (inbound hub InvokeCommand for the
|
|
459
|
+
* synthetic "browser" clip). The hub forwards clip_token as the PLAINTEXT
|
|
460
|
+
* browser_profile_id, which the host daemon uses directly as the bb-browser
|
|
461
|
+
* account. Only daemons that host BrowserProfiles register this capability.
|
|
462
|
+
* See docs/engineering-design/browser-profile-clip-integration.md §11.2.
|
|
463
|
+
*/
|
|
464
|
+
async invokeBrowserCapability(clipToken, command, input) {
|
|
465
|
+
if (!this.browserProfileManager) {
|
|
466
|
+
throw new Error('browser capability is not hosted on this machine');
|
|
467
|
+
}
|
|
468
|
+
if (!clipToken) {
|
|
469
|
+
throw new ClipCommandError('browser invoke missing profile identity (clip_token)');
|
|
470
|
+
}
|
|
471
|
+
return this.browserProfileManager.invoke({
|
|
472
|
+
profileId: clipToken,
|
|
473
|
+
account: clipToken,
|
|
474
|
+
command,
|
|
475
|
+
input,
|
|
476
|
+
});
|
|
477
|
+
}
|
|
316
478
|
setStatus(name, status, message) {
|
|
317
479
|
this.statuses.set(name, { status, message, since: new Date() });
|
|
318
480
|
for (const fn of this.statusListeners) {
|
|
@@ -325,30 +487,25 @@ export class ClipProcessManager {
|
|
|
325
487
|
}
|
|
326
488
|
}
|
|
327
489
|
}
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
}
|
|
350
|
-
catch {
|
|
351
|
-
// not found in PATH
|
|
352
|
-
}
|
|
353
|
-
throw new Error('bun binary not found — install Bun (https://bun.sh) or set bunPath option');
|
|
490
|
+
/**
|
|
491
|
+
* Synthetic ClipConfig for the host-side "browser" capability. It has no local
|
|
492
|
+
* subprocess (no `path` to spawn); the hub routes browser invokes here and they
|
|
493
|
+
* are served by ClipProcessManager.invokeBrowserCapability() against bb-browser.
|
|
494
|
+
*/
|
|
495
|
+
function browserCapabilityConfig() {
|
|
496
|
+
return {
|
|
497
|
+
name: BROWSER_CAPABILITY_NAME,
|
|
498
|
+
package: BROWSER_CAPABILITY_PACKAGE,
|
|
499
|
+
version: '0.15.0',
|
|
500
|
+
source: BROWSER_CAPABILITY_PACKAGE,
|
|
501
|
+
path: '',
|
|
502
|
+
manifest: {
|
|
503
|
+
name: BROWSER_CAPABILITY_NAME,
|
|
504
|
+
package: BROWSER_CAPABILITY_PACKAGE,
|
|
505
|
+
version: '0.15.0',
|
|
506
|
+
description: 'Profile-bound browser capability (bb-browser-pro)',
|
|
507
|
+
commands: browserCapabilityCommands().map((c) => c.name),
|
|
508
|
+
commandDetails: browserCapabilityCommands(),
|
|
509
|
+
},
|
|
510
|
+
};
|
|
354
511
|
}
|
|
@@ -12,11 +12,24 @@ export interface InvokeEvent {
|
|
|
12
12
|
export interface InvokeResult {
|
|
13
13
|
output: unknown;
|
|
14
14
|
}
|
|
15
|
+
export interface ClipInvokeContext {
|
|
16
|
+
/**
|
|
17
|
+
* The executing Clip id. It shards the process pool and fixes the caller
|
|
18
|
+
* identity for nested dependency invokes, so the execution side can resolve
|
|
19
|
+
* the Clip's browser binding via the hub. Browser
|
|
20
|
+
* profile / account identity is NOT carried here — it is resolved from the
|
|
21
|
+
* hub binding (GetBindings) and rides on clip_token end-to-end.
|
|
22
|
+
*/
|
|
23
|
+
clipId?: string;
|
|
24
|
+
}
|
|
25
|
+
export type DependencyInvokeResolver = (clipName: string, command: string, input: unknown, context: ClipInvokeContext) => Promise<unknown>;
|
|
15
26
|
export declare class ClipCommandError extends Error {
|
|
16
27
|
readonly code?: string;
|
|
17
28
|
constructor(message: string, code?: string);
|
|
18
29
|
}
|
|
19
30
|
export declare class ClipProcess {
|
|
31
|
+
private readonly context;
|
|
32
|
+
private readonly dependencyResolver?;
|
|
20
33
|
readonly clip: ClipConfig;
|
|
21
34
|
private child;
|
|
22
35
|
private reader;
|
|
@@ -32,7 +45,7 @@ export declare class ClipProcess {
|
|
|
32
45
|
private doneDeferred;
|
|
33
46
|
private doneResolved;
|
|
34
47
|
private exitError;
|
|
35
|
-
constructor(clip: ClipConfig);
|
|
48
|
+
constructor(clip: ClipConfig, context?: ClipInvokeContext, dependencyResolver?: DependencyInvokeResolver | undefined);
|
|
36
49
|
/**
|
|
37
50
|
* Spawn the Bun subprocess and start the IPC read loop.
|
|
38
51
|
* Corresponds to Pinix startLocked().
|
|
@@ -51,6 +64,7 @@ export declare class ClipProcess {
|
|
|
51
64
|
private handleRegister;
|
|
52
65
|
private dispatchInvokeEvent;
|
|
53
66
|
private handleData;
|
|
67
|
+
private handleDependencyInvoke;
|
|
54
68
|
private send;
|
|
55
69
|
private signalReady;
|
|
56
70
|
private finish;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"process.d.ts","sourceRoot":"","sources":["../../src/clip-runtime/process.ts"],"names":[],"mappings":"AAIA,OAAO,EACL,KAAK,UAAU,EACf,KAAK,aAAa,EAInB,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"process.d.ts","sourceRoot":"","sources":["../../src/clip-runtime/process.ts"],"names":[],"mappings":"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"}
|
|
@@ -2,7 +2,8 @@ import { spawn } from 'node:child_process';
|
|
|
2
2
|
import { createInterface } from 'node:readline';
|
|
3
3
|
import { clearAllProviderCreds } from '../runtimes.js';
|
|
4
4
|
import { NdjsonReader, NdjsonWriter, MessageType } from './ipc.js';
|
|
5
|
-
import {
|
|
5
|
+
import { enrichManifest, manifestFromIpc, resolveEntrypoint, } from './manifest.js';
|
|
6
|
+
import { manifestDeclaresBrowserDependency, manifestMapsDependencyToBrowser, } from './browser-dependency.js';
|
|
6
7
|
const CLIP_REGISTER_TIMEOUT_MS = 10_000;
|
|
7
8
|
const CLIP_STOP_TIMEOUT_MS = 5_000;
|
|
8
9
|
function sanitizeEnvForClip(extra) {
|
|
@@ -32,6 +33,8 @@ function makeDeferred() {
|
|
|
32
33
|
return { promise, resolve, reject };
|
|
33
34
|
}
|
|
34
35
|
export class ClipProcess {
|
|
36
|
+
context;
|
|
37
|
+
dependencyResolver;
|
|
35
38
|
clip;
|
|
36
39
|
child = null;
|
|
37
40
|
reader = null;
|
|
@@ -47,7 +50,9 @@ export class ClipProcess {
|
|
|
47
50
|
doneDeferred = makeDeferred();
|
|
48
51
|
doneResolved = false;
|
|
49
52
|
exitError;
|
|
50
|
-
constructor(clip) {
|
|
53
|
+
constructor(clip, context = {}, dependencyResolver) {
|
|
54
|
+
this.context = context;
|
|
55
|
+
this.dependencyResolver = dependencyResolver;
|
|
51
56
|
this.clip = clip;
|
|
52
57
|
}
|
|
53
58
|
/**
|
|
@@ -231,20 +236,27 @@ export class ClipProcess {
|
|
|
231
236
|
this.handleData(msg);
|
|
232
237
|
break;
|
|
233
238
|
case MessageType.ListClips:
|
|
234
|
-
//
|
|
239
|
+
// Advertise the "browser" dependency to the child whenever its manifest
|
|
240
|
+
// declares it. Binding resolution + cross-machine routing happen at
|
|
241
|
+
// invoke time via the hub (GetBindings → Invoke); an unbound or
|
|
242
|
+
// unavailable profile surfaces as an invoke-time error, not here.
|
|
235
243
|
this.send({
|
|
236
244
|
id: msg.id,
|
|
237
245
|
type: MessageType.ListClipsResult,
|
|
238
|
-
clips:
|
|
246
|
+
clips: this.context.clipId && manifestDeclaresBrowserDependency(this.manifest)
|
|
247
|
+
? [
|
|
248
|
+
{
|
|
249
|
+
name: 'browser',
|
|
250
|
+
package: '@pinixai/bb-browser-pro',
|
|
251
|
+
version: '0.15.0',
|
|
252
|
+
description: 'Profile-bound browser capability',
|
|
253
|
+
},
|
|
254
|
+
]
|
|
255
|
+
: [],
|
|
239
256
|
}).catch(() => { });
|
|
240
257
|
break;
|
|
241
258
|
case MessageType.Invoke:
|
|
242
|
-
|
|
243
|
-
this.send({
|
|
244
|
-
id: msg.id,
|
|
245
|
-
type: MessageType.Error,
|
|
246
|
-
error: 'clip-to-clip invoke not supported yet',
|
|
247
|
-
}).catch(() => { });
|
|
259
|
+
void this.handleDependencyInvoke(msg);
|
|
248
260
|
break;
|
|
249
261
|
case MessageType.Heartbeat:
|
|
250
262
|
break;
|
|
@@ -291,6 +303,8 @@ export class ClipProcess {
|
|
|
291
303
|
event.output = msg.output;
|
|
292
304
|
if (msg.error)
|
|
293
305
|
event.error = { message: msg.error };
|
|
306
|
+
if (msg.processExit || msg.process_exit)
|
|
307
|
+
event.processExit = true;
|
|
294
308
|
callback(event);
|
|
295
309
|
}
|
|
296
310
|
handleData(msg) {
|
|
@@ -301,6 +315,55 @@ export class ClipProcess {
|
|
|
301
315
|
error: 'data operations not implemented yet',
|
|
302
316
|
}).catch(() => { });
|
|
303
317
|
}
|
|
318
|
+
async handleDependencyInvoke(msg) {
|
|
319
|
+
const dependencyName = msg.clip ?? msg.alias ?? '';
|
|
320
|
+
if (!msg.id) {
|
|
321
|
+
console.warn(`[clip:${this.clip.name}] dependency invoke missing id`);
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
if (!dependencyName || !msg.command) {
|
|
325
|
+
await this.send({
|
|
326
|
+
id: msg.id,
|
|
327
|
+
type: MessageType.Error,
|
|
328
|
+
error: 'dependency clip and command are required',
|
|
329
|
+
}).catch(() => { });
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
if (!manifestMapsDependencyToBrowser(this.manifest, dependencyName)) {
|
|
333
|
+
await this.send({
|
|
334
|
+
id: msg.id,
|
|
335
|
+
type: MessageType.Error,
|
|
336
|
+
error: 'browser dependency is not declared by this clip',
|
|
337
|
+
}).catch(() => { });
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
if (!this.dependencyResolver) {
|
|
341
|
+
await this.send({
|
|
342
|
+
id: msg.id,
|
|
343
|
+
type: MessageType.Error,
|
|
344
|
+
error: 'clip dependency invoke is not configured',
|
|
345
|
+
}).catch(() => { });
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
try {
|
|
349
|
+
const output = await this.dependencyResolver('browser', msg.command, msg.input ?? {}, this.context);
|
|
350
|
+
await this.send({
|
|
351
|
+
id: msg.id,
|
|
352
|
+
type: MessageType.Result,
|
|
353
|
+
output,
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
catch (err) {
|
|
357
|
+
const isCommandError = err instanceof ClipCommandError;
|
|
358
|
+
await this.send({
|
|
359
|
+
id: msg.id,
|
|
360
|
+
type: MessageType.Error,
|
|
361
|
+
error: err instanceof Error ? err.message : String(err),
|
|
362
|
+
processExit: !isCommandError,
|
|
363
|
+
process_exit: !isCommandError,
|
|
364
|
+
}).catch(() => { });
|
|
365
|
+
}
|
|
366
|
+
}
|
|
304
367
|
async send(msg) {
|
|
305
368
|
if (!this.writer)
|
|
306
369
|
throw new Error('writer not initialized');
|
package/dist/config.d.ts
CHANGED
|
@@ -54,6 +54,15 @@ export type ClaudeDaemonConfig = {
|
|
|
54
54
|
/** Disable self-update entirely (K8s env auto-disables). */
|
|
55
55
|
updateDisabled: boolean;
|
|
56
56
|
};
|
|
57
|
+
/**
|
|
58
|
+
* Root dir for all on-disk daemon state: `config.json`, the self-update
|
|
59
|
+
* `bundle/` overlay (versions + current symlink + update-state.json), and
|
|
60
|
+
* bridge-bin resolution all derive from this single path. `PRLL_DAEMON_CONFIG_DIR`
|
|
61
|
+
* overrides it so multiple daemons can coexist on one host without colliding —
|
|
62
|
+
* e.g. a staging desktop install points its launchd agent at
|
|
63
|
+
* `~/.parall-daemon-staging` while prod keeps the default `~/.parall-daemon`.
|
|
64
|
+
* Unset → default (no behavior change for existing single-env installs).
|
|
65
|
+
*/
|
|
57
66
|
export declare function daemonConfigDir(env?: NodeJS.ProcessEnv): string;
|
|
58
67
|
export declare function daemonConfigPath(env?: NodeJS.ProcessEnv): string;
|
|
59
68
|
export declare function resolveClaudeDaemonConfig(env?: NodeJS.ProcessEnv): ClaudeDaemonConfig;
|