@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
|
@@ -11,6 +11,7 @@ import * as path from 'node:path';
|
|
|
11
11
|
import * as crypto from 'node:crypto';
|
|
12
12
|
import { execFileSync } from 'node:child_process';
|
|
13
13
|
import { pipeline } from 'node:stream/promises';
|
|
14
|
+
import { findBunBinary } from './bun-resolver.js';
|
|
14
15
|
const DEFAULT_REGISTRY_URL = 'https://api.pinixai.com';
|
|
15
16
|
// ---------------------------------------------------------------------------
|
|
16
17
|
// Source parsing
|
|
@@ -406,28 +407,68 @@ function installDeps(clipDir) {
|
|
|
406
407
|
catch {
|
|
407
408
|
/* ignore parse error, try install anyway */
|
|
408
409
|
}
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
410
|
+
// Reasons each attempt failed — surfaced in the thrown error so a clip that
|
|
411
|
+
// genuinely needs deps reports *why* installation failed, not just "no
|
|
412
|
+
// package manager succeeded".
|
|
413
|
+
const failures = [];
|
|
414
|
+
// Bun is the reliable path. Resolve it the same way the process manager
|
|
415
|
+
// resolves the runtime bun (embedded-first, then ~/.bun / Homebrew / PATH) —
|
|
416
|
+
// a bare `bun` would not be found under the daemon's minimal launchd PATH
|
|
417
|
+
// (/usr/bin:/bin:/usr/sbin:/sbin). We invoke bun by absolute path below, and
|
|
418
|
+
// also prepend its directory to the child PATH so a dependency's postinstall
|
|
419
|
+
// hook that shells out to `bun` by name resolves it. The embedded binary is
|
|
420
|
+
// landed under the canonical name `bun` (Frameworks/bun) precisely so this
|
|
421
|
+
// by-name lookup works — a `parall-bun` filename would defeat it.
|
|
422
|
+
let bunPath = null;
|
|
423
|
+
try {
|
|
424
|
+
bunPath = findBunBinary();
|
|
425
|
+
}
|
|
426
|
+
catch (err) {
|
|
427
|
+
failures.push(`resolve bun: ${err instanceof Error ? err.message : String(err)}`);
|
|
428
|
+
}
|
|
429
|
+
if (bunPath) {
|
|
430
|
+
const bunEnv = {
|
|
431
|
+
...process.env,
|
|
432
|
+
PATH: `${path.dirname(bunPath)}${path.delimiter}${process.env.PATH ?? ''}`,
|
|
433
|
+
};
|
|
434
|
+
// Prefer a reproducible install when a lockfile is present, then fall back
|
|
435
|
+
// to a plain install (no lockfile / lockfile drift).
|
|
436
|
+
for (const args of [['install', '--frozen-lockfile'], ['install']]) {
|
|
437
|
+
try {
|
|
438
|
+
execFileSync(bunPath, args, {
|
|
439
|
+
cwd: clipDir,
|
|
440
|
+
stdio: 'pipe',
|
|
441
|
+
timeout: 120_000,
|
|
442
|
+
env: bunEnv,
|
|
443
|
+
});
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
catch (err) {
|
|
447
|
+
failures.push(`bun ${args.join(' ')}: ${err instanceof Error ? err.message : String(err)}`);
|
|
448
|
+
}
|
|
425
449
|
}
|
|
426
450
|
}
|
|
451
|
+
// Best-effort npm fallback. The packaged desktop app embeds bun but NOT npm,
|
|
452
|
+
// so this only succeeds on dev/CLI hosts that already have npm on PATH; it is
|
|
453
|
+
// never the reliable path. Kept so a developer running the bare daemon with
|
|
454
|
+
// npm-but-no-bun still resolves dependencies.
|
|
455
|
+
try {
|
|
456
|
+
execFileSync('npm', ['install', '--production'], {
|
|
457
|
+
cwd: clipDir,
|
|
458
|
+
stdio: 'pipe',
|
|
459
|
+
timeout: 120_000,
|
|
460
|
+
});
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
catch (err) {
|
|
464
|
+
failures.push(`npm install: ${err instanceof Error ? err.message : String(err)}`);
|
|
465
|
+
}
|
|
427
466
|
if (hasDeps) {
|
|
428
|
-
throw new Error(`failed to install dependencies in ${clipDir} — clip declares dependencies but no
|
|
467
|
+
throw new Error(`failed to install dependencies in ${clipDir} — clip declares dependencies but no ` +
|
|
468
|
+
`package manager succeeded:\n ${failures.join('\n ')}`);
|
|
429
469
|
}
|
|
430
|
-
console.warn(`[clip-installer] could not run package manager in ${clipDir}
|
|
470
|
+
console.warn(`[clip-installer] could not run a package manager in ${clipDir} ` +
|
|
471
|
+
`(no dependencies declared, continuing): ${failures.join('; ')}`);
|
|
431
472
|
}
|
|
432
473
|
// ---------------------------------------------------------------------------
|
|
433
474
|
// Public API
|
|
@@ -19,7 +19,7 @@ export interface ClipProviderOptions {
|
|
|
19
19
|
serviceUrl: string;
|
|
20
20
|
/** mck_xxx machine key for authorization. */
|
|
21
21
|
authKey: string;
|
|
22
|
-
/**
|
|
22
|
+
/** Machine org, used for local logging; Hub derives org from auth. */
|
|
23
23
|
orgId: string;
|
|
24
24
|
/** Unique provider name (typically machine ID). */
|
|
25
25
|
providerName: string;
|
|
@@ -31,6 +31,14 @@ export interface ClipProviderOptions {
|
|
|
31
31
|
warn(msg: string): void;
|
|
32
32
|
error(msg: string): void;
|
|
33
33
|
};
|
|
34
|
+
/**
|
|
35
|
+
* Invoked after several consecutive reconnect failures (throttled by backoff).
|
|
36
|
+
* Lets the supervisor refetch machine config and, if the endpoint changed,
|
|
37
|
+
* rebuild this provider — e.g. when clip_provider_url rolls out server-side
|
|
38
|
+
* after this daemon booted while its ws-gateway WS never bounced to
|
|
39
|
+
* re-trigger machine.hello. Optional.
|
|
40
|
+
*/
|
|
41
|
+
onPersistentFailure?: () => void;
|
|
34
42
|
}
|
|
35
43
|
export declare class ClipProvider {
|
|
36
44
|
private readonly opts;
|
|
@@ -44,10 +52,11 @@ export declare class ClipProvider {
|
|
|
44
52
|
private statusUnsubscribe;
|
|
45
53
|
private manifestUnsubscribe;
|
|
46
54
|
private needsReregister;
|
|
47
|
-
private
|
|
55
|
+
private streamGeneration;
|
|
48
56
|
private static RECONNECT_BASE_MS;
|
|
49
57
|
private static RECONNECT_MAX_MS;
|
|
50
58
|
private static HEARTBEAT_INTERVAL_MS;
|
|
59
|
+
private static PERSISTENT_FAILURE_ATTEMPT;
|
|
51
60
|
constructor(opts: ClipProviderOptions);
|
|
52
61
|
/** Connect to the Clip Service and register local clips. Reconnects on failure. */
|
|
53
62
|
connect(): Promise<void>;
|
|
@@ -72,5 +81,7 @@ export declare class ClipProvider {
|
|
|
72
81
|
private buildClipRegistrations;
|
|
73
82
|
private clipConfigToRegistration;
|
|
74
83
|
private commandDetailToInfo;
|
|
84
|
+
private dependencyNames;
|
|
85
|
+
private stringEntities;
|
|
75
86
|
}
|
|
76
87
|
//# sourceMappingURL=clip-provider.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"clip-provider.d.ts","sourceRoot":"","sources":["../../src/clip-runtime/clip-provider.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAGH,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"clip-provider.d.ts","sourceRoot":"","sources":["../../src/clip-runtime/clip-provider.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAGH,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AA+I/D,MAAM,WAAW,mBAAmB;IAClC,sDAAsD;IACtD,UAAU,EAAE,MAAM,CAAC;IACnB,6CAA6C;IAC7C,OAAO,EAAE,MAAM,CAAC;IAChB,sEAAsE;IACtE,KAAK,EAAE,MAAM,CAAC;IACd,mDAAmD;IACnD,YAAY,EAAE,MAAM,CAAC;IACrB,yDAAyD;IACzD,WAAW,EAAE,kBAAkB,CAAC;IAChC,mBAAmB;IACnB,GAAG,EAAE;QAAE,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC;IACpF;;;;;;OAMG;IACH,mBAAmB,CAAC,EAAE,MAAM,IAAI,CAAC;CAClC;AAED,qBAAa,YAAY;IAwBX,OAAO,CAAC,QAAQ,CAAC,IAAI;IAvBjC,OAAO,CAAC,OAAO,CAAyC;IACxD,OAAO,CAAC,MAAM,CAAwC;IACtD,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,gBAAgB,CAAK;IAC7B,OAAO,CAAC,cAAc,CAA+B;IACrD,OAAO,CAAC,cAAc,CAA+B;IACrD,OAAO,CAAC,iBAAiB,CAA6B;IACtD,OAAO,CAAC,mBAAmB,CAA6B;IACxD,OAAO,CAAC,eAAe,CAAS;IAIhC,OAAO,CAAC,gBAAgB,CAAK;IAE7B,OAAO,CAAC,MAAM,CAAC,iBAAiB,CAAS;IACzC,OAAO,CAAC,MAAM,CAAC,gBAAgB,CAAU;IACzC,OAAO,CAAC,MAAM,CAAC,qBAAqB,CAAU;IAI9C,OAAO,CAAC,MAAM,CAAC,0BAA0B,CAAK;gBAEjB,IAAI,EAAE,mBAAmB;IAEtD,mFAAmF;IAC7E,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAS9B,wCAAwC;IAClC,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAUjC,WAAW,IAAI,OAAO;YAQR,UAAU;IAuExB,OAAO,CAAC,gBAAgB;IASxB,OAAO,CAAC,iBAAiB;IA6BzB,OAAO,CAAC,mBAAmB;IAO3B,OAAO,CAAC,mBAAmB;IAO3B,OAAO,CAAC,cAAc;IAuBtB,OAAO,CAAC,WAAW;IAWnB,OAAO,CAAC,YAAY;IAepB,OAAO,CAAC,sBAAsB;IAyB9B,OAAO,CAAC,wBAAwB;IAWhC,OAAO,CAAC,gBAAgB;IAaxB,OAAO,CAAC,gBAAgB;YAeV,mBAAmB;YA0EnB,YAAY;YAUZ,mBAAmB;IAiBjC,OAAO,CAAC,sBAAsB;IAQ9B,OAAO,CAAC,wBAAwB;IAuBhC,OAAO,CAAC,mBAAmB;IAS3B,OAAO,CAAC,eAAe;IAWvB,OAAO,CAAC,cAAc;CAUvB"}
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import * as http2 from 'node:http2';
|
|
17
17
|
import { ClipCommandError } from './process.js';
|
|
18
|
+
import { isBrowserDependencyName } from './browser-dependency.js';
|
|
18
19
|
// ---------------------------------------------------------------------------
|
|
19
20
|
// Connect envelope helpers
|
|
20
21
|
// ---------------------------------------------------------------------------
|
|
@@ -66,10 +67,17 @@ export class ClipProvider {
|
|
|
66
67
|
statusUnsubscribe = null;
|
|
67
68
|
manifestUnsubscribe = null;
|
|
68
69
|
needsReregister = false;
|
|
69
|
-
|
|
70
|
+
// Monotonic stream id, bumped by openStream(). Handlers from a stream that a
|
|
71
|
+
// newer openStream() has superseded bail in handleDisconnect(gen), so their
|
|
72
|
+
// late close/end/error can't reconnect against the healthy replacement.
|
|
73
|
+
streamGeneration = 0;
|
|
70
74
|
static RECONNECT_BASE_MS = 5_000;
|
|
71
75
|
static RECONNECT_MAX_MS = 60_000;
|
|
72
76
|
static HEARTBEAT_INTERVAL_MS = 30_000;
|
|
77
|
+
// After this many consecutive reconnect failures (and every multiple
|
|
78
|
+
// thereafter), ask the supervisor to refetch the endpoint. See
|
|
79
|
+
// onPersistentFailure.
|
|
80
|
+
static PERSISTENT_FAILURE_ATTEMPT = 3;
|
|
73
81
|
constructor(opts) {
|
|
74
82
|
this.opts = opts;
|
|
75
83
|
}
|
|
@@ -99,6 +107,8 @@ export class ClipProvider {
|
|
|
99
107
|
async openStream() {
|
|
100
108
|
if (this.stopped)
|
|
101
109
|
return;
|
|
110
|
+
// Bind this stream's handlers to a fresh generation; older streams go stale.
|
|
111
|
+
const gen = ++this.streamGeneration;
|
|
102
112
|
try {
|
|
103
113
|
this.closeStream();
|
|
104
114
|
this.closeSession();
|
|
@@ -107,10 +117,10 @@ export class ClipProvider {
|
|
|
107
117
|
this.session = http2.connect(authority);
|
|
108
118
|
this.session.on('error', (err) => {
|
|
109
119
|
this.opts.log.warn(`[clip-provider] h2 session error: ${String(err)}`);
|
|
110
|
-
this.handleDisconnect();
|
|
120
|
+
this.handleDisconnect(gen);
|
|
111
121
|
});
|
|
112
122
|
this.session.on('close', () => {
|
|
113
|
-
this.handleDisconnect();
|
|
123
|
+
this.handleDisconnect(gen);
|
|
114
124
|
});
|
|
115
125
|
this.stream = this.session.request({
|
|
116
126
|
':method': 'POST',
|
|
@@ -121,7 +131,7 @@ export class ClipProvider {
|
|
|
121
131
|
});
|
|
122
132
|
this.stream.on('error', (err) => {
|
|
123
133
|
this.opts.log.warn(`[clip-provider] stream error: ${String(err)}`);
|
|
124
|
-
this.handleDisconnect();
|
|
134
|
+
this.handleDisconnect(gen);
|
|
125
135
|
});
|
|
126
136
|
// Read response envelopes
|
|
127
137
|
const decoder = new EnvelopeDecoder();
|
|
@@ -144,21 +154,23 @@ export class ClipProvider {
|
|
|
144
154
|
});
|
|
145
155
|
this.stream.on('end', () => {
|
|
146
156
|
this.opts.log.info('[clip-provider] stream ended by hub');
|
|
147
|
-
this.handleDisconnect();
|
|
157
|
+
this.handleDisconnect(gen);
|
|
148
158
|
});
|
|
149
159
|
this.stream.on('close', () => {
|
|
150
|
-
this.handleDisconnect();
|
|
160
|
+
this.handleDisconnect(gen);
|
|
151
161
|
});
|
|
152
|
-
// Send
|
|
162
|
+
// Send RegisterRequest as the first message.
|
|
153
163
|
await this.sendRegister();
|
|
154
164
|
}
|
|
155
165
|
catch (err) {
|
|
156
166
|
this.opts.log.warn(`[clip-provider] connect failed: ${String(err)}`);
|
|
157
|
-
this.
|
|
167
|
+
this.handleDisconnect(gen);
|
|
158
168
|
}
|
|
159
169
|
}
|
|
160
|
-
handleDisconnect() {
|
|
161
|
-
if (
|
|
170
|
+
handleDisconnect(gen) {
|
|
171
|
+
if (gen !== this.streamGeneration)
|
|
172
|
+
return; // stale event from a superseded stream
|
|
173
|
+
if (this.stopped)
|
|
162
174
|
return;
|
|
163
175
|
if (!this.connected && this.reconnectTimer)
|
|
164
176
|
return; // already scheduling
|
|
@@ -172,6 +184,19 @@ export class ClipProvider {
|
|
|
172
184
|
const delay = Math.min(ClipProvider.RECONNECT_BASE_MS * Math.pow(2, this.reconnectAttempt), ClipProvider.RECONNECT_MAX_MS);
|
|
173
185
|
this.reconnectAttempt++;
|
|
174
186
|
this.opts.log.info(`[clip-provider] reconnecting in ${delay}ms (attempt ${this.reconnectAttempt})`);
|
|
187
|
+
if (this.reconnectAttempt % ClipProvider.PERSISTENT_FAILURE_ATTEMPT === 0) {
|
|
188
|
+
// The endpoint may have changed server-side since we booted (e.g.
|
|
189
|
+
// clip_provider_url rolled out while our ws-gateway WS never bounced to
|
|
190
|
+
// re-trigger machine.hello). Ask the supervisor to refetch machine config;
|
|
191
|
+
// if the URL changed, applyClipProviderState rebuilds this provider against
|
|
192
|
+
// the new endpoint. Backoff naturally throttles these refetches.
|
|
193
|
+
try {
|
|
194
|
+
this.opts.onPersistentFailure?.();
|
|
195
|
+
}
|
|
196
|
+
catch (err) {
|
|
197
|
+
this.opts.log.warn(`[clip-provider] onPersistentFailure threw: ${String(err)}`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
175
200
|
this.reconnectTimer = setTimeout(() => {
|
|
176
201
|
this.reconnectTimer = null;
|
|
177
202
|
void this.openStream();
|
|
@@ -198,17 +223,15 @@ export class ClipProvider {
|
|
|
198
223
|
if (this.needsReregister) {
|
|
199
224
|
this.needsReregister = false;
|
|
200
225
|
this.opts.log.info('[clip-provider] manifest updated — reconnecting to re-register clips');
|
|
201
|
-
this.intentionalClose = true;
|
|
202
|
-
this.closeStream();
|
|
203
|
-
this.closeSession();
|
|
204
|
-
this.intentionalClose = false;
|
|
205
226
|
this.connected = false;
|
|
206
227
|
this.clearHeartbeatTimer();
|
|
207
228
|
this.clearReconnectTimer();
|
|
229
|
+
// openStream() bumps the generation before closing the old stream, so its
|
|
230
|
+
// late close events fail the gen check (closing inline would re-arm the flap).
|
|
208
231
|
void this.openStream();
|
|
209
232
|
return;
|
|
210
233
|
}
|
|
211
|
-
this.sendProviderMessage({
|
|
234
|
+
this.sendProviderMessage({ ping: { sentAtUnixMs: Date.now() } }).catch((err) => {
|
|
212
235
|
this.opts.log.warn(`[clip-provider] heartbeat send failed: ${String(err)}`);
|
|
213
236
|
});
|
|
214
237
|
}, ClipProvider.HEARTBEAT_INTERVAL_MS);
|
|
@@ -242,16 +265,16 @@ export class ClipProvider {
|
|
|
242
265
|
subscribeStatusChanges() {
|
|
243
266
|
const mgr = this.opts.clipManager;
|
|
244
267
|
// Forward clip status changes to the Hub
|
|
245
|
-
this.statusUnsubscribe = mgr.addStatusListener((name, status,
|
|
268
|
+
this.statusUnsubscribe = mgr.addStatusListener((name, status, message) => {
|
|
246
269
|
if (!this.connected)
|
|
247
270
|
return;
|
|
248
271
|
const protoStatus = status === 'running'
|
|
249
|
-
? '
|
|
272
|
+
? 'CLIP_STATUS_RUNNING'
|
|
250
273
|
: status === 'error'
|
|
251
|
-
? '
|
|
252
|
-
: '
|
|
274
|
+
? 'CLIP_STATUS_ERROR'
|
|
275
|
+
: 'CLIP_STATUS_SLEEPING';
|
|
253
276
|
this.sendProviderMessage({
|
|
254
|
-
clipStatusChanged: {
|
|
277
|
+
clipStatusChanged: { name, status: protoStatus, message },
|
|
255
278
|
}).catch((err) => {
|
|
256
279
|
this.opts.log.warn(`[clip-provider] status change send failed: ${String(err)}`);
|
|
257
280
|
});
|
|
@@ -271,20 +294,22 @@ export class ClipProvider {
|
|
|
271
294
|
// Message handling
|
|
272
295
|
// ---------------------------------------------------------------------------
|
|
273
296
|
handleHubMessage(msg) {
|
|
274
|
-
if (msg.
|
|
275
|
-
this.handleRegistered(msg.
|
|
297
|
+
if (msg.registerResponse) {
|
|
298
|
+
this.handleRegistered(msg.registerResponse);
|
|
276
299
|
}
|
|
277
300
|
else if (msg.invokeCommand) {
|
|
278
301
|
void this.handleInvokeCommand(msg.invokeCommand);
|
|
279
302
|
}
|
|
280
|
-
else if (msg.
|
|
281
|
-
//
|
|
282
|
-
|
|
303
|
+
else if (msg.pong !== undefined) {
|
|
304
|
+
// Pong is the hub's keepalive ack. Do NOT echo a ping here — the
|
|
305
|
+
// heartbeat timer (startHeartbeat) is the sole ping driver. Replying to
|
|
306
|
+
// every pong with a ping turns the 30s interval into an unbounded
|
|
307
|
+
// ping↔pong loop the moment the first timer tick fires.
|
|
283
308
|
}
|
|
284
309
|
}
|
|
285
310
|
handleRegistered(reg) {
|
|
286
311
|
if (reg.accepted) {
|
|
287
|
-
this.opts.log.info(`[clip-provider] registered with hub (org=${
|
|
312
|
+
this.opts.log.info(`[clip-provider] registered with hub (org=${this.opts.orgId})`);
|
|
288
313
|
this.connected = true;
|
|
289
314
|
this.reconnectAttempt = 0;
|
|
290
315
|
this.startHeartbeat();
|
|
@@ -298,7 +323,7 @@ export class ClipProvider {
|
|
|
298
323
|
}
|
|
299
324
|
}
|
|
300
325
|
async handleInvokeCommand(cmd) {
|
|
301
|
-
const { requestId,
|
|
326
|
+
const { requestId, clipName, command } = cmd;
|
|
302
327
|
const timeoutMs = cmd.timeoutMs ?? 30_000;
|
|
303
328
|
try {
|
|
304
329
|
let input;
|
|
@@ -311,9 +336,25 @@ export class ClipProvider {
|
|
|
311
336
|
input = decoded;
|
|
312
337
|
}
|
|
313
338
|
}
|
|
339
|
+
// Two routing shapes share this handler, distinguished by clip_name:
|
|
340
|
+
// - "browser" capability (cross-machine host hop): clip_token is the
|
|
341
|
+
// PLAINTEXT browser_profile_id (used directly as the bb-browser
|
|
342
|
+
// account). No local subprocess — serve it against the host
|
|
343
|
+
// BrowserProfileManager.
|
|
344
|
+
// - ordinary main invoke (this machine is the execution machine): the
|
|
345
|
+
// hub carries the executing clip_id in clip_token (Pinix wire
|
|
346
|
+
// compatibility — no dedicated field). It shards the process pool and
|
|
347
|
+
// fixes the caller identity so the clip's nested browser dependency
|
|
348
|
+
// invoke can resolve its binding via the hub. Empty clip_token (plain
|
|
349
|
+
// non-browser clips) falls back to the default unsharded process.
|
|
350
|
+
const invocation = isBrowserDependencyName(clipName)
|
|
351
|
+
? this.opts.clipManager.invokeBrowserCapability(cmd.clipToken ?? '', command, input)
|
|
352
|
+
: this.opts.clipManager.invoke(clipName, command, input, {
|
|
353
|
+
clipId: cmd.clipToken || undefined,
|
|
354
|
+
});
|
|
314
355
|
let timer;
|
|
315
356
|
const output = await Promise.race([
|
|
316
|
-
|
|
357
|
+
invocation,
|
|
317
358
|
new Promise((_, reject) => {
|
|
318
359
|
timer = setTimeout(() => reject(new Error('invoke timeout')), timeoutMs);
|
|
319
360
|
}),
|
|
@@ -336,7 +377,10 @@ export class ClipProvider {
|
|
|
336
377
|
await this.sendProviderMessage({
|
|
337
378
|
invokeResult: {
|
|
338
379
|
requestId,
|
|
339
|
-
error:
|
|
380
|
+
error: {
|
|
381
|
+
code: isCommandError ? 'CLIP_COMMAND_ERROR' : 'INVOKE_FAILED',
|
|
382
|
+
message: isCommandError ? err.message : String(err),
|
|
383
|
+
},
|
|
340
384
|
errorIsCommand: isCommandError,
|
|
341
385
|
done: true,
|
|
342
386
|
},
|
|
@@ -352,7 +396,6 @@ export class ClipProvider {
|
|
|
352
396
|
register: {
|
|
353
397
|
providerName: this.opts.providerName,
|
|
354
398
|
clips,
|
|
355
|
-
orgId: this.opts.orgId,
|
|
356
399
|
},
|
|
357
400
|
});
|
|
358
401
|
}
|
|
@@ -374,7 +417,10 @@ export class ClipProvider {
|
|
|
374
417
|
// Clip registration helpers
|
|
375
418
|
// ---------------------------------------------------------------------------
|
|
376
419
|
buildClipRegistrations() {
|
|
377
|
-
|
|
420
|
+
// getProviderClips() appends the synthetic "browser" capability on hosts
|
|
421
|
+
// that can serve BrowserProfiles, so the hub can route cross-machine
|
|
422
|
+
// browser invokes to this provider.
|
|
423
|
+
const clips = this.opts.clipManager.getProviderClips();
|
|
378
424
|
return clips.map((clip) => this.clipConfigToRegistration(clip));
|
|
379
425
|
}
|
|
380
426
|
clipConfigToRegistration(clip) {
|
|
@@ -382,21 +428,45 @@ export class ClipProvider {
|
|
|
382
428
|
const commands = manifest
|
|
383
429
|
? manifest.commandDetails.map((cmd) => this.commandDetailToInfo(cmd))
|
|
384
430
|
: [];
|
|
431
|
+
const pkg = manifest?.package ?? clip.package ?? clip.name;
|
|
385
432
|
return {
|
|
386
433
|
alias: clip.name,
|
|
387
|
-
|
|
434
|
+
package: pkg,
|
|
388
435
|
version: manifest?.version ?? clip.version ?? '',
|
|
436
|
+
domain: manifest?.domain,
|
|
389
437
|
commands,
|
|
438
|
+
hasWeb: manifest?.hasWeb ?? false,
|
|
439
|
+
dependencies: this.dependencyNames(manifest?.dependencies, manifest?.dependencySlots),
|
|
440
|
+
tokenProtected: Boolean(clip.token),
|
|
441
|
+
name: pkg,
|
|
442
|
+
patterns: manifest?.patterns,
|
|
443
|
+
entities: this.stringEntities(manifest?.entities),
|
|
444
|
+
description: manifest?.description,
|
|
390
445
|
};
|
|
391
446
|
}
|
|
392
447
|
commandDetailToInfo(detail) {
|
|
393
448
|
return {
|
|
394
449
|
name: detail.name,
|
|
395
450
|
description: detail.description ?? '',
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
? Buffer.from(detail.output, 'utf-8').toString('base64')
|
|
399
|
-
: undefined,
|
|
451
|
+
input: detail.input,
|
|
452
|
+
output: detail.output,
|
|
400
453
|
};
|
|
401
454
|
}
|
|
455
|
+
dependencyNames(...depMaps) {
|
|
456
|
+
const names = depMaps
|
|
457
|
+
.flatMap((deps) => (deps ? Object.entries(deps) : []))
|
|
458
|
+
.map(([slot, dep]) => dep.package ?? slot)
|
|
459
|
+
.filter((name) => name.length > 0);
|
|
460
|
+
const uniqueNames = Array.from(new Set(names));
|
|
461
|
+
return uniqueNames.length > 0 ? uniqueNames : undefined;
|
|
462
|
+
}
|
|
463
|
+
stringEntities(entities) {
|
|
464
|
+
if (!entities)
|
|
465
|
+
return undefined;
|
|
466
|
+
const out = {};
|
|
467
|
+
for (const [key, value] of Object.entries(entities)) {
|
|
468
|
+
out[key] = typeof value === 'string' ? value : JSON.stringify(value);
|
|
469
|
+
}
|
|
470
|
+
return out;
|
|
471
|
+
}
|
|
402
472
|
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HubClient — Connect-RPC unary/server-streaming client the execution-side
|
|
3
|
+
* daemon uses to resolve cross-machine dependency bindings and forward
|
|
4
|
+
* dependency invokes through the Clip Service (Hub).
|
|
5
|
+
*
|
|
6
|
+
* The execution daemon (machine A) does NOT verify the binding's clip_token —
|
|
7
|
+
* it reads its binding via GetBindings, then passes clip_token straight to
|
|
8
|
+
* Invoke(clip_name="browser", ...). The hub verifies the signed token, resolves
|
|
9
|
+
* the bound BrowserProfile's host machine (B), and forwards the InvokeCommand
|
|
10
|
+
* there. See docs/engineering-design/browser-profile-clip-integration.md §11.3.
|
|
11
|
+
*
|
|
12
|
+
* Protocol (same Connect envelope framing as clip-provider.ts):
|
|
13
|
+
* POST /clip.v1.ClipHubService/GetBindings (unary)
|
|
14
|
+
* POST /clip.v1.ClipHubService/Invoke (server-streaming)
|
|
15
|
+
* Content-Type: application/connect+json
|
|
16
|
+
* Authorization: Bearer mck_xxx (scope-gated to the caller's
|
|
17
|
+
* own executing Clip)
|
|
18
|
+
* Body / Response: envelope-framed JSON
|
|
19
|
+
* [flags:1][length:4 big-endian][JSON payload]
|
|
20
|
+
* flags=0x00 data, flags=0x02 end-of-stream (trailers)
|
|
21
|
+
*/
|
|
22
|
+
/** Pinix v2 ClipBinding — a resolved dependency-slot target. */
|
|
23
|
+
export interface ClipBinding {
|
|
24
|
+
alias?: string;
|
|
25
|
+
hub?: string;
|
|
26
|
+
hubToken?: string;
|
|
27
|
+
clipToken?: string;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Error surfaced by a hub Invoke that the hub flagged as a clip
|
|
31
|
+
* application-level rejection (errorIsCommand=true). The execution-side IPC
|
|
32
|
+
* layer maps this to a non-fatal ClipCommandError so the calling clip is not
|
|
33
|
+
* crashed by a recoverable browser command failure.
|
|
34
|
+
*/
|
|
35
|
+
export declare class HubCommandError extends Error {
|
|
36
|
+
readonly code?: string;
|
|
37
|
+
constructor(message: string, code?: string);
|
|
38
|
+
}
|
|
39
|
+
export interface HubClientOptions {
|
|
40
|
+
/** Clip Service URL (grey-cloud clip-rpc host for BYOC). */
|
|
41
|
+
serviceUrl: string;
|
|
42
|
+
/** mck_xxx machine key — scope-gated by the hub to this machine's Clips. */
|
|
43
|
+
authKey: string;
|
|
44
|
+
/** Optional per-request timeout. */
|
|
45
|
+
requestTimeoutMs?: number;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Stateless Connect-RPC client. Each call opens a short-lived HTTP/2 session;
|
|
49
|
+
* dependency invokes are infrequent relative to the agent dispatch loop, so a
|
|
50
|
+
* pooled session is not worth the lifecycle complexity here.
|
|
51
|
+
*/
|
|
52
|
+
export declare class HubClient {
|
|
53
|
+
private readonly opts;
|
|
54
|
+
constructor(opts: HubClientOptions);
|
|
55
|
+
/**
|
|
56
|
+
* GetBindings(clip_name=clipId) → slot→binding map. The hub gates
|
|
57
|
+
* this to Clips the caller machine actually executes. GetBindings is a
|
|
58
|
+
* UNARY Connect RPC, so it uses raw JSON framing (not the streaming envelope).
|
|
59
|
+
*/
|
|
60
|
+
getBindings(clipId: string): Promise<Record<string, ClipBinding>>;
|
|
61
|
+
/**
|
|
62
|
+
* Invoke a clip command through the hub, passing the binding's clip_token
|
|
63
|
+
* through untouched. Aggregates server-streamed output chunks into one value.
|
|
64
|
+
* The execution daemon does not verify clip_token — the hub does.
|
|
65
|
+
*/
|
|
66
|
+
invoke(clipName: string, command: string, input: unknown, clipToken: string): Promise<unknown>;
|
|
67
|
+
/**
|
|
68
|
+
* Unary Connect call: raw JSON body (no envelope), Content-Type
|
|
69
|
+
* application/json. The response is raw JSON; a non-2xx status carries a
|
|
70
|
+
* Connect error envelope ({code,message}) in the body.
|
|
71
|
+
*/
|
|
72
|
+
private unary;
|
|
73
|
+
/**
|
|
74
|
+
* Server-streaming Connect call: enveloped frames over
|
|
75
|
+
* application/connect+json. Collects all response envelopes.
|
|
76
|
+
*/
|
|
77
|
+
private serverStream;
|
|
78
|
+
}
|
|
79
|
+
//# sourceMappingURL=hub-client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hub-client.d.ts","sourceRoot":"","sources":["../../src/clip-runtime/hub-client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAgDH,gEAAgE;AAChE,MAAM,WAAW,WAAW;IAC1B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAaD;;;;;GAKG;AACH,qBAAa,eAAgB,SAAQ,KAAK;IACxC,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;gBACX,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM;CAK3C;AAED,MAAM,WAAW,gBAAgB;IAC/B,4DAA4D;IAC5D,UAAU,EAAE,MAAM,CAAC;IACnB,4EAA4E;IAC5E,OAAO,EAAE,MAAM,CAAC;IAChB,oCAAoC;IACpC,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED;;;;GAIG;AACH,qBAAa,SAAS;IACR,OAAO,CAAC,QAAQ,CAAC,IAAI;gBAAJ,IAAI,EAAE,gBAAgB;IAEnD;;;;OAIG;IACG,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;IAOvE;;;;OAIG;IACG,MAAM,CACV,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,OAAO,EACd,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,OAAO,CAAC;IAgCnB;;;;OAIG;IACH,OAAO,CAAC,KAAK;IAgEb;;;OAGG;IACH,OAAO,CAAC,YAAY;CAiGrB"}
|