@parall/daemon 1.32.1 → 1.34.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 +185 -25
- package/bundle/parall-codex-agent.js +185 -25
- package/bundle/parall-daemon.js +4647 -2856
- 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 +70 -0
- package/dist/clip-runtime/browser-profile-manager.d.ts.map +1 -0
- package/dist/clip-runtime/browser-profile-manager.js +608 -0
- package/dist/clip-runtime/clip-provider.d.ts +10 -1
- package/dist/clip-runtime/clip-provider.d.ts.map +1 -1
- package/dist/clip-runtime/clip-provider.js +63 -21
- 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 +226 -48
- 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,10 +1,17 @@
|
|
|
1
1
|
import type { IpcManifest } from './ipc.js';
|
|
2
|
+
export type DependencyMap = Record<string, {
|
|
3
|
+
package?: string;
|
|
4
|
+
version?: string;
|
|
5
|
+
}>;
|
|
2
6
|
export interface ClipConfig {
|
|
7
|
+
clipId?: string;
|
|
3
8
|
name: string;
|
|
4
9
|
package?: string;
|
|
5
10
|
version?: string;
|
|
6
11
|
source: string;
|
|
7
12
|
path: string;
|
|
13
|
+
sourceType?: string;
|
|
14
|
+
sourceRef?: string;
|
|
8
15
|
token?: string;
|
|
9
16
|
manifest?: ManifestCache;
|
|
10
17
|
}
|
|
@@ -17,10 +24,8 @@ export interface ManifestCache {
|
|
|
17
24
|
commands: string[];
|
|
18
25
|
commandDetails: CommandDetail[];
|
|
19
26
|
hasWeb?: boolean;
|
|
20
|
-
dependencies?:
|
|
21
|
-
|
|
22
|
-
version?: string;
|
|
23
|
-
}>;
|
|
27
|
+
dependencies?: DependencyMap;
|
|
28
|
+
dependencySlots?: DependencyMap;
|
|
24
29
|
patterns?: string[];
|
|
25
30
|
entities?: Record<string, unknown>;
|
|
26
31
|
}
|
|
@@ -37,6 +42,11 @@ export interface ClipJson {
|
|
|
37
42
|
runtime?: string;
|
|
38
43
|
main?: string;
|
|
39
44
|
web?: string;
|
|
45
|
+
domain?: string;
|
|
46
|
+
dependencies?: DependencyMap;
|
|
47
|
+
dependency_slots?: DependencyMap;
|
|
48
|
+
patterns?: string[];
|
|
49
|
+
commands?: CommandDetail[];
|
|
40
50
|
author?: string;
|
|
41
51
|
license?: string;
|
|
42
52
|
repository?: string;
|
|
@@ -48,10 +58,8 @@ interface ProjectMetadata {
|
|
|
48
58
|
domain?: string;
|
|
49
59
|
main?: string;
|
|
50
60
|
web?: string;
|
|
51
|
-
dependencies?:
|
|
52
|
-
|
|
53
|
-
version?: string;
|
|
54
|
-
}>;
|
|
61
|
+
dependencies?: DependencyMap;
|
|
62
|
+
dependencySlots?: DependencyMap;
|
|
55
63
|
patterns?: string[];
|
|
56
64
|
commands?: CommandDetail[];
|
|
57
65
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../../src/clip-runtime/manifest.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAE5C,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,aAAa,CAAC;CAC1B;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,cAAc,EAAE,aAAa,EAAE,CAAC;IAChC,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,YAAY,CAAC,EAAE,
|
|
1
|
+
{"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../../src/clip-runtime/manifest.ts"],"names":[],"mappings":"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"}
|
|
@@ -27,8 +27,13 @@ export function loadProjectMetadata(clip) {
|
|
|
27
27
|
meta.package = clipJson.name;
|
|
28
28
|
meta.version = clipJson.version;
|
|
29
29
|
meta.description = clipJson.description;
|
|
30
|
+
meta.domain = clipJson.domain;
|
|
30
31
|
meta.main = clipJson.main;
|
|
31
32
|
meta.web = clipJson.web;
|
|
33
|
+
meta.dependencies = clipJson.dependencies;
|
|
34
|
+
meta.dependencySlots = clipJson.dependency_slots;
|
|
35
|
+
meta.patterns = clipJson.patterns;
|
|
36
|
+
meta.commands = clipJson.commands;
|
|
32
37
|
}
|
|
33
38
|
const pkgJson = loadPackageJson(clip.path);
|
|
34
39
|
if (pkgJson) {
|
|
@@ -112,6 +117,13 @@ export function enrichManifest(clip, manifest) {
|
|
|
112
117
|
enriched.description = meta.description;
|
|
113
118
|
if (!enriched.domain && meta.domain)
|
|
114
119
|
enriched.domain = meta.domain;
|
|
120
|
+
if (!enriched.dependencies && meta.dependencies)
|
|
121
|
+
enriched.dependencies = meta.dependencies;
|
|
122
|
+
if (!enriched.dependencySlots && meta.dependencySlots) {
|
|
123
|
+
enriched.dependencySlots = meta.dependencySlots;
|
|
124
|
+
}
|
|
125
|
+
if (!enriched.patterns && meta.patterns)
|
|
126
|
+
enriched.patterns = meta.patterns;
|
|
115
127
|
if (meta.commands && meta.commands.length > 0 && enriched.commandDetails.length === 0) {
|
|
116
128
|
enriched.commandDetails = meta.commands;
|
|
117
129
|
enriched.commands = meta.commands.map((c) => c.name);
|
|
@@ -136,6 +148,7 @@ export function manifestFromIpc(ipcManifest) {
|
|
|
136
148
|
commandDetails: details,
|
|
137
149
|
hasWeb: ipcManifest.has_web,
|
|
138
150
|
dependencies: ipcManifest.dependencies,
|
|
151
|
+
dependencySlots: ipcManifest.dependency_slots,
|
|
139
152
|
patterns: ipcManifest.patterns,
|
|
140
153
|
entities: ipcManifest.entities,
|
|
141
154
|
};
|
|
@@ -1,14 +1,35 @@
|
|
|
1
|
-
import { type ClipProcessStatus } from './process.js';
|
|
1
|
+
import { type ClipInvokeContext, type ClipProcessStatus } from './process.js';
|
|
2
2
|
import type { ClipConfig, ManifestCache } from './manifest.js';
|
|
3
|
+
import type { BrowserProfileManager } from './browser-profile-manager.js';
|
|
4
|
+
import { type HubClient } from './hub-client.js';
|
|
3
5
|
export interface ClipProcessManagerOptions {
|
|
4
6
|
bunPath?: string;
|
|
5
7
|
clipsDir: string;
|
|
6
8
|
dataDir: string;
|
|
9
|
+
/**
|
|
10
|
+
* Host-side browser runtime. Present only on daemons that can host a
|
|
11
|
+
* BrowserProfile; drives inbound hub browser InvokeCommands. The execution
|
|
12
|
+
* side never calls this directly — nested browser invokes go out through the
|
|
13
|
+
* hub (see hubClient).
|
|
14
|
+
*/
|
|
15
|
+
browserProfileManager?: BrowserProfileManager;
|
|
16
|
+
/**
|
|
17
|
+
* Lazily-supplied hub client used by the execution side to resolve a
|
|
18
|
+
* dependency binding (GetBindings) and forward the dependency invoke
|
|
19
|
+
* (Invoke). Provided by the supervisor once machine identity + the clip
|
|
20
|
+
* service endpoint are resolved; may change on endpoint rollout.
|
|
21
|
+
*/
|
|
22
|
+
hubClient?: () => HubClient | null;
|
|
23
|
+
/** Ensure a server-declared clip is installed locally before spawn. */
|
|
24
|
+
ensureInstalled?: (config: ClipConfig) => Promise<ClipConfig>;
|
|
7
25
|
}
|
|
8
26
|
export declare class ClipProcessManager {
|
|
9
27
|
private bunPath;
|
|
10
28
|
private clipsDir;
|
|
11
29
|
private dataDir;
|
|
30
|
+
private browserProfileManager?;
|
|
31
|
+
private hubClient?;
|
|
32
|
+
private ensureInstalled?;
|
|
12
33
|
private processes;
|
|
13
34
|
private startingUp;
|
|
14
35
|
private statuses;
|
|
@@ -17,6 +38,8 @@ export declare class ClipProcessManager {
|
|
|
17
38
|
private statusListeners;
|
|
18
39
|
private manifestListeners;
|
|
19
40
|
constructor(opts: ClipProcessManagerOptions);
|
|
41
|
+
/** True when this daemon can host BrowserProfiles (serve inbound hub browser invokes). */
|
|
42
|
+
canHostBrowser(): boolean;
|
|
20
43
|
/** Backward-compat: set a single status listener (wraps addStatusListener). */
|
|
21
44
|
set onStatusChange(fn: ((name: string, status: ClipProcessStatus, message: string) => void) | undefined);
|
|
22
45
|
/** Subscribe to clip status changes. Returns an unsubscribe function. */
|
|
@@ -25,6 +48,7 @@ export declare class ClipProcessManager {
|
|
|
25
48
|
private notifyManifestUpdate;
|
|
26
49
|
private resolveBun;
|
|
27
50
|
registerClip(config: ClipConfig): void;
|
|
51
|
+
replaceClipConfig(config: ClipConfig): Promise<void>;
|
|
28
52
|
unregisterClip(name: string): Promise<void>;
|
|
29
53
|
startClip(name: string): Promise<void>;
|
|
30
54
|
stopClip(name: string): Promise<void>;
|
|
@@ -34,8 +58,8 @@ export declare class ClipProcessManager {
|
|
|
34
58
|
* Application-level errors (ClipCommandError) are NOT retried.
|
|
35
59
|
* Corresponds to Pinix ProcessManager.Invoke().
|
|
36
60
|
*/
|
|
37
|
-
invoke(name: string, command: string, input?: unknown): Promise<unknown>;
|
|
38
|
-
invokeStream(name: string, command: string, input?: unknown, onChunk?: (chunk: unknown) => void): Promise<unknown>;
|
|
61
|
+
invoke(name: string, command: string, input?: unknown, context?: ClipInvokeContext): Promise<unknown>;
|
|
62
|
+
invokeStream(name: string, command: string, input?: unknown, onChunk?: (chunk: unknown) => void, context?: ClipInvokeContext): Promise<unknown>;
|
|
39
63
|
loadManifest(name: string): Promise<ManifestCache>;
|
|
40
64
|
clipStatus(name: string): {
|
|
41
65
|
status: ClipProcessStatus;
|
|
@@ -43,6 +67,14 @@ export declare class ClipProcessManager {
|
|
|
43
67
|
};
|
|
44
68
|
isRunning(name: string): boolean;
|
|
45
69
|
getRegisteredClips(): ClipConfig[];
|
|
70
|
+
/**
|
|
71
|
+
* Clips this daemon advertises to the hub as a provider: the locally
|
|
72
|
+
* installed clips plus, when this machine hosts BrowserProfiles, a synthetic
|
|
73
|
+
* "browser" capability so the hub can route cross-machine browser invokes
|
|
74
|
+
* here. The browser capability has no local subprocess — it is served by
|
|
75
|
+
* invokeBrowserCapability() against bb-browser.
|
|
76
|
+
*/
|
|
77
|
+
getProviderClips(): ClipConfig[];
|
|
46
78
|
/**
|
|
47
79
|
* Load clip configs from the clips directory.
|
|
48
80
|
* Reads clip-config.json if present, or scans subdirectories for clip.json files.
|
|
@@ -52,6 +84,26 @@ export declare class ClipProcessManager {
|
|
|
52
84
|
private spawnProcess;
|
|
53
85
|
private ensureClipDataDir;
|
|
54
86
|
private removeIfSame;
|
|
87
|
+
private processKey;
|
|
88
|
+
private processKeyBelongsToName;
|
|
89
|
+
private processNameFromKey;
|
|
90
|
+
/**
|
|
91
|
+
* Execution-side nested dependency invoke (from a child clip's IPC). The
|
|
92
|
+
* browser slot is resolved UNIQUELY via the hub: read the Clip's binding
|
|
93
|
+
* (GetBindings), then forward the invoke through the hub (Invoke) to the bound
|
|
94
|
+
* BrowserProfile's host. No local shortcut, no fallback — unbound is rejected.
|
|
95
|
+
* The execution daemon never verifies clip_token; the hub does.
|
|
96
|
+
* See docs/engineering-design/browser-profile-clip-integration.md §11.3.
|
|
97
|
+
*/
|
|
98
|
+
private invokeDependency;
|
|
99
|
+
/**
|
|
100
|
+
* Host-side browser capability handler (inbound hub InvokeCommand for the
|
|
101
|
+
* synthetic "browser" clip). The hub forwards clip_token as the PLAINTEXT
|
|
102
|
+
* browser_profile_id, which the host daemon uses directly as the bb-browser
|
|
103
|
+
* account. Only daemons that host BrowserProfiles register this capability.
|
|
104
|
+
* See docs/engineering-design/browser-profile-clip-integration.md §11.2.
|
|
105
|
+
*/
|
|
106
|
+
invokeBrowserCapability(clipToken: string, command: string, input: unknown): Promise<unknown>;
|
|
55
107
|
private setStatus;
|
|
56
108
|
}
|
|
57
109
|
//# sourceMappingURL=process-manager.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"process-manager.d.ts","sourceRoot":"","sources":["../../src/clip-runtime/process-manager.ts"],"names":[],"mappings":"AAEA,OAAO,EAGL,KAAK,iBAAiB,EAEvB,MAAM,cAAc,CAAC;AAEtB,OAAO,KAAK,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"process-manager.d.ts","sourceRoot":"","sources":["../../src/clip-runtime/process-manager.ts"],"names":[],"mappings":"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"}
|
|
@@ -2,10 +2,15 @@ import * as fs from 'node:fs';
|
|
|
2
2
|
import * as path from 'node:path';
|
|
3
3
|
import { ClipProcess, ClipCommandError, } from './process.js';
|
|
4
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,35 +316,43 @@ 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);
|
|
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);
|
|
272
352
|
const bunPath = this.resolveBun();
|
|
273
353
|
proc.spawn(bunPath, {
|
|
274
354
|
PINIX_DATA_DIR: clipDataDir,
|
|
355
|
+
PRLL_CLIP_ID: context.clipId ?? '',
|
|
275
356
|
// Expose the embedded bun's directory on the clip's PATH so a clip that
|
|
276
357
|
// shells out to `bun` by name resolves it — the daemon's launchd PATH
|
|
277
358
|
// does not include Frameworks/. Symmetric with installDeps(); relies on
|
|
@@ -282,7 +363,7 @@ export class ClipProcessManager {
|
|
|
282
363
|
try {
|
|
283
364
|
await proc.waitReady();
|
|
284
365
|
// Only expose to concurrent callers after registration completes
|
|
285
|
-
this.processes.set(config.name, proc);
|
|
366
|
+
this.processes.set(this.processKey(config.name, context), proc);
|
|
286
367
|
this.setStatus(config.name, 'running', 'ready');
|
|
287
368
|
// Always cache manifest so Provider registrations have commands
|
|
288
369
|
const manifest = proc.getManifest();
|
|
@@ -298,7 +379,7 @@ export class ClipProcessManager {
|
|
|
298
379
|
}
|
|
299
380
|
// Monitor process exit in background
|
|
300
381
|
proc.waitDone().then(() => {
|
|
301
|
-
this.removeIfSame(config.name, proc);
|
|
382
|
+
this.removeIfSame(config.name, proc, context);
|
|
302
383
|
const exitErr = proc.getError();
|
|
303
384
|
if (exitErr) {
|
|
304
385
|
this.setStatus(config.name, 'error', exitErr.message);
|
|
@@ -309,16 +390,91 @@ export class ClipProcessManager {
|
|
|
309
390
|
});
|
|
310
391
|
return proc;
|
|
311
392
|
}
|
|
312
|
-
ensureClipDataDir(config) {
|
|
313
|
-
const dir = path.join(this.dataDir, config.name);
|
|
393
|
+
ensureClipDataDir(config, context = {}) {
|
|
394
|
+
const dir = path.join(this.dataDir, config.name, context.clipId ?? 'default');
|
|
314
395
|
fs.mkdirSync(dir, { recursive: true });
|
|
315
396
|
return dir;
|
|
316
397
|
}
|
|
317
|
-
removeIfSame(name, proc) {
|
|
318
|
-
|
|
319
|
-
|
|
398
|
+
removeIfSame(name, proc, context = {}) {
|
|
399
|
+
const key = this.processKey(name, context);
|
|
400
|
+
if (this.processes.get(key) === proc) {
|
|
401
|
+
this.processes.delete(key);
|
|
402
|
+
}
|
|
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;
|
|
320
455
|
}
|
|
321
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
|
+
}
|
|
322
478
|
setStatus(name, status, message) {
|
|
323
479
|
this.statuses.set(name, { status, message, since: new Date() });
|
|
324
480
|
for (const fn of this.statusListeners) {
|
|
@@ -331,3 +487,25 @@ export class ClipProcessManager {
|
|
|
331
487
|
}
|
|
332
488
|
}
|
|
333
489
|
}
|
|
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
|
+
};
|
|
511
|
+
}
|