@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.
Files changed (51) hide show
  1. package/bundle/bb-browser-daemon.js +15628 -0
  2. package/bundle/buildDomTree.js +1501 -0
  3. package/bundle/manifest.json +19 -11
  4. package/bundle/parall-claude-agent.js +224 -58
  5. package/bundle/parall-codex-agent.js +224 -58
  6. package/bundle/parall-daemon.js +4507 -2668
  7. package/bundle/parall-openclaw-agent.js +4 -3
  8. package/dist/cli.d.ts.map +1 -1
  9. package/dist/cli.js +8 -2
  10. package/dist/clip-runtime/browser-dependency.d.ts +20 -0
  11. package/dist/clip-runtime/browser-dependency.d.ts.map +1 -0
  12. package/dist/clip-runtime/browser-dependency.js +52 -0
  13. package/dist/clip-runtime/browser-profile-manager.d.ts +67 -0
  14. package/dist/clip-runtime/browser-profile-manager.d.ts.map +1 -0
  15. package/dist/clip-runtime/browser-profile-manager.js +595 -0
  16. package/dist/clip-runtime/bun-resolver.d.ts +24 -0
  17. package/dist/clip-runtime/bun-resolver.d.ts.map +1 -0
  18. package/dist/clip-runtime/bun-resolver.js +58 -0
  19. package/dist/clip-runtime/clip-installer.d.ts.map +1 -1
  20. package/dist/clip-runtime/clip-installer.js +59 -18
  21. package/dist/clip-runtime/clip-provider.d.ts +13 -2
  22. package/dist/clip-runtime/clip-provider.d.ts.map +1 -1
  23. package/dist/clip-runtime/clip-provider.js +106 -36
  24. package/dist/clip-runtime/hub-client.d.ts +79 -0
  25. package/dist/clip-runtime/hub-client.d.ts.map +1 -0
  26. package/dist/clip-runtime/hub-client.js +320 -0
  27. package/dist/clip-runtime/index.d.ts +2 -0
  28. package/dist/clip-runtime/index.d.ts.map +1 -1
  29. package/dist/clip-runtime/index.js +2 -0
  30. package/dist/clip-runtime/ipc.d.ts +6 -0
  31. package/dist/clip-runtime/ipc.d.ts.map +1 -1
  32. package/dist/clip-runtime/manifest.d.ts +16 -8
  33. package/dist/clip-runtime/manifest.d.ts.map +1 -1
  34. package/dist/clip-runtime/manifest.js +13 -0
  35. package/dist/clip-runtime/process-manager.d.ts +55 -3
  36. package/dist/clip-runtime/process-manager.d.ts.map +1 -1
  37. package/dist/clip-runtime/process-manager.js +233 -76
  38. package/dist/clip-runtime/process.d.ts +15 -1
  39. package/dist/clip-runtime/process.d.ts.map +1 -1
  40. package/dist/clip-runtime/process.js +73 -10
  41. package/dist/config.d.ts +9 -0
  42. package/dist/config.d.ts.map +1 -1
  43. package/dist/config.js +12 -0
  44. package/dist/index.js +46 -5
  45. package/dist/runtime-bin-resolver.d.ts +7 -0
  46. package/dist/runtime-bin-resolver.d.ts.map +1 -0
  47. package/dist/runtime-bin-resolver.js +292 -0
  48. package/dist/supervisor.d.ts +53 -4
  49. package/dist/supervisor.d.ts.map +1 -1
  50. package/dist/supervisor.js +449 -117
  51. 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
- const runners = [
410
- { cmd: 'bun', args: ['install', '--frozen-lockfile'] },
411
- { cmd: 'bun', args: ['install'] },
412
- { cmd: 'npm', args: ['install', '--production'] },
413
- ];
414
- for (const runner of runners) {
415
- try {
416
- execFileSync(runner.cmd, runner.args, {
417
- cwd: clipDir,
418
- stdio: 'pipe',
419
- timeout: 120_000,
420
- });
421
- return;
422
- }
423
- catch {
424
- // try next runner
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 package manager succeeded`);
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} (no dependencies declared, continuing)`);
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
- /** Org to register for. */
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 intentionalClose;
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;AA4H/D,MAAM,WAAW,mBAAmB;IAClC,sDAAsD;IACtD,UAAU,EAAE,MAAM,CAAC;IACnB,6CAA6C;IAC7C,OAAO,EAAE,MAAM,CAAC;IAChB,2BAA2B;IAC3B,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;CACrF;AAED,qBAAa,YAAY;IAiBX,OAAO,CAAC,QAAQ,CAAC,IAAI;IAhBjC,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;IAChC,OAAO,CAAC,gBAAgB,CAAS;IAEjC,OAAO,CAAC,MAAM,CAAC,iBAAiB,CAAS;IACzC,OAAO,CAAC,MAAM,CAAC,gBAAgB,CAAU;IACzC,OAAO,CAAC,MAAM,CAAC,qBAAqB,CAAU;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;IAoExB,OAAO,CAAC,gBAAgB;IAQxB,OAAO,CAAC,iBAAiB;IAiBzB,OAAO,CAAC,mBAAmB;IAO3B,OAAO,CAAC,mBAAmB;IAO3B,OAAO,CAAC,cAAc;IAyBtB,OAAO,CAAC,WAAW;IAWnB,OAAO,CAAC,YAAY;IAepB,OAAO,CAAC,sBAAsB;IAyB9B,OAAO,CAAC,wBAAwB;IAWhC,OAAO,CAAC,gBAAgB;IAWxB,OAAO,CAAC,gBAAgB;YAeV,mBAAmB;YAsDnB,YAAY;YAWZ,mBAAmB;IAiBjC,OAAO,CAAC,sBAAsB;IAK9B,OAAO,CAAC,wBAAwB;IAchC,OAAO,CAAC,mBAAmB;CAU5B"}
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
- intentionalClose = false;
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 ProviderRegister as the first message
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.scheduleReconnect();
167
+ this.handleDisconnect(gen);
158
168
  }
159
169
  }
160
- handleDisconnect() {
161
- if (this.stopped || this.intentionalClose)
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({ heartbeat: {} }).catch((err) => {
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, _message) => {
268
+ this.statusUnsubscribe = mgr.addStatusListener((name, status, message) => {
246
269
  if (!this.connected)
247
270
  return;
248
271
  const protoStatus = status === 'running'
249
- ? 'CLIP_ONLINE_STATUS_ONLINE'
272
+ ? 'CLIP_STATUS_RUNNING'
250
273
  : status === 'error'
251
- ? 'CLIP_ONLINE_STATUS_ERROR'
252
- : 'CLIP_ONLINE_STATUS_OFFLINE';
274
+ ? 'CLIP_STATUS_ERROR'
275
+ : 'CLIP_STATUS_SLEEPING';
253
276
  this.sendProviderMessage({
254
- clipStatusChanged: { alias: name, status: protoStatus },
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.registered) {
275
- this.handleRegistered(msg.registered);
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.heartbeat !== undefined) {
281
- // Respond to hub heartbeat with provider heartbeat
282
- this.sendProviderMessage({ heartbeat: {} }).catch(() => { });
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=${reg.orgId})`);
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, clipAlias, commandName } = cmd;
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
- this.opts.clipManager.invoke(clipAlias, commandName, input),
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: isCommandError ? err.message : String(err),
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
- const clips = this.opts.clipManager.getRegisteredClips();
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
- name: manifest?.package ?? clip.package ?? clip.name,
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
- inputSchema: detail.input ? Buffer.from(detail.input, 'utf-8').toString('base64') : undefined,
397
- outputSchema: detail.output
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"}