@corenel/harness 0.2.0 → 0.4.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/dist/acp/agentMap.d.ts +34 -0
- package/dist/acp/agentMap.d.ts.map +1 -0
- package/dist/acp/agentMap.js +110 -0
- package/dist/acp/agentMap.js.map +1 -0
- package/dist/acp/index.d.ts +4 -0
- package/dist/acp/index.d.ts.map +1 -0
- package/dist/acp/index.js +6 -0
- package/dist/acp/index.js.map +1 -0
- package/dist/acp/policyMap.d.ts +55 -0
- package/dist/acp/policyMap.d.ts.map +1 -0
- package/dist/acp/policyMap.js +121 -0
- package/dist/acp/policyMap.js.map +1 -0
- package/dist/acp/types.d.ts +41 -0
- package/dist/acp/types.d.ts.map +1 -0
- package/dist/acp/types.js +14 -0
- package/dist/acp/types.js.map +1 -0
- package/dist/api/backend.d.ts +39 -0
- package/dist/api/backend.d.ts.map +1 -0
- package/dist/api/backend.js +126 -0
- package/dist/api/backend.js.map +1 -0
- package/dist/api/client.d.ts +95 -0
- package/dist/api/client.d.ts.map +1 -0
- package/dist/api/client.js +212 -0
- package/dist/api/client.js.map +1 -0
- package/dist/api/fileMirrorSource.d.ts +24 -0
- package/dist/api/fileMirrorSource.d.ts.map +1 -0
- package/dist/api/fileMirrorSource.js +59 -0
- package/dist/api/fileMirrorSource.js.map +1 -0
- package/dist/api/mirror.d.ts +102 -0
- package/dist/api/mirror.d.ts.map +1 -0
- package/dist/api/mirror.js +204 -0
- package/dist/api/mirror.js.map +1 -0
- package/dist/api/mirrorScheduler.d.ts +56 -0
- package/dist/api/mirrorScheduler.d.ts.map +1 -0
- package/dist/api/mirrorScheduler.js +167 -0
- package/dist/api/mirrorScheduler.js.map +1 -0
- package/dist/compression.d.ts +15 -1
- package/dist/compression.d.ts.map +1 -1
- package/dist/compression.js +32 -1
- package/dist/compression.js.map +1 -1
- package/dist/core/loop.d.ts +12 -0
- package/dist/core/loop.d.ts.map +1 -1
- package/dist/core/loop.js +138 -14
- package/dist/core/loop.js.map +1 -1
- package/dist/core/spawn.d.ts +54 -0
- package/dist/core/spawn.d.ts.map +1 -0
- package/dist/core/spawn.js +58 -0
- package/dist/core/spawn.js.map +1 -0
- package/dist/dag/executeNode.js +2 -2
- package/dist/fileSchema.js +1 -1
- package/dist/guardrail/guardrail.js +1 -1
- package/dist/guardrail/index.js +7 -7
- package/dist/guardrail/permission-service.js +1 -1
- package/dist/guardrail/serialize.js +1 -1
- package/dist/guardrail/store.js +2 -2
- package/dist/install/lockfile.d.ts +79 -0
- package/dist/install/lockfile.d.ts.map +1 -0
- package/dist/install/lockfile.js +155 -0
- package/dist/install/lockfile.js.map +1 -0
- package/dist/install/store.d.ts +13 -0
- package/dist/install/store.d.ts.map +1 -0
- package/dist/install/store.js +38 -0
- package/dist/install/store.js.map +1 -0
- package/dist/log.d.ts.map +1 -1
- package/dist/log.js +6 -1
- package/dist/log.js.map +1 -1
- package/dist/prompts/config.js +2 -2
- package/dist/prompts/host.d.ts +4 -0
- package/dist/prompts/host.d.ts.map +1 -1
- package/dist/prompts/host.js.map +1 -1
- package/dist/prompts/index.d.ts.map +1 -1
- package/dist/prompts/index.js +13 -6
- package/dist/prompts/index.js.map +1 -1
- package/dist/prompts/library/defaults.generated.js +2 -2
- package/dist/prompts/library/defaults.generated.js.map +1 -1
- package/dist/prompts/library/index.js +1 -1
- package/dist/providers/contextWindow.d.ts +19 -0
- package/dist/providers/contextWindow.d.ts.map +1 -0
- package/dist/providers/contextWindow.js +68 -0
- package/dist/providers/contextWindow.js.map +1 -0
- package/dist/providers/gateway.d.ts +3 -0
- package/dist/providers/gateway.d.ts.map +1 -1
- package/dist/providers/gateway.js +4 -1
- package/dist/providers/gateway.js.map +1 -1
- package/dist/sessions/apiLeaseStore.d.ts +10 -0
- package/dist/sessions/apiLeaseStore.d.ts.map +1 -0
- package/dist/sessions/apiLeaseStore.js +41 -0
- package/dist/sessions/apiLeaseStore.js.map +1 -0
- package/dist/sessions/blobGc.d.ts +62 -0
- package/dist/sessions/blobGc.d.ts.map +1 -0
- package/dist/sessions/blobGc.js +154 -0
- package/dist/sessions/blobGc.js.map +1 -0
- package/dist/sessions/fileRecallStore.js +1 -1
- package/dist/sessions/fileSessionStore.js +1 -1
- package/dist/sessions/head.d.ts +90 -0
- package/dist/sessions/head.d.ts.map +1 -0
- package/dist/sessions/head.js +82 -0
- package/dist/sessions/head.js.map +1 -0
- package/dist/sessions/inflight.d.ts +58 -0
- package/dist/sessions/inflight.d.ts.map +1 -0
- package/dist/sessions/inflight.js +0 -0
- package/dist/sessions/inflight.js.map +1 -0
- package/dist/sessions/lease.d.ts +59 -0
- package/dist/sessions/lease.d.ts.map +1 -0
- package/dist/sessions/lease.js +115 -0
- package/dist/sessions/lease.js.map +1 -0
- package/dist/sessions/leaseStore.d.ts +46 -0
- package/dist/sessions/leaseStore.d.ts.map +1 -0
- package/dist/sessions/leaseStore.js +58 -0
- package/dist/sessions/leaseStore.js.map +1 -0
- package/dist/sessions/timeline.d.ts +134 -0
- package/dist/sessions/timeline.d.ts.map +1 -0
- package/dist/sessions/timeline.js +127 -0
- package/dist/sessions/timeline.js.map +1 -0
- package/dist/sidecar/host.d.ts +241 -5
- package/dist/sidecar/host.d.ts.map +1 -1
- package/dist/sidecar/host.js +630 -5
- package/dist/sidecar/host.js.map +1 -1
- package/dist/sidecar/server.d.ts +124 -2
- package/dist/sidecar/server.d.ts.map +1 -1
- package/dist/sidecar/server.js +620 -9
- package/dist/sidecar/server.js.map +1 -1
- package/dist/sidecar/wsChannel.d.ts +4 -0
- package/dist/sidecar/wsChannel.d.ts.map +1 -0
- package/dist/sidecar/wsChannel.js +62 -0
- package/dist/sidecar/wsChannel.js.map +1 -0
- package/dist/skills.js +2 -2
- package/dist/snapshot/effects.d.ts +69 -0
- package/dist/snapshot/effects.d.ts.map +1 -0
- package/dist/snapshot/effects.js +67 -0
- package/dist/snapshot/effects.js.map +1 -0
- package/dist/snapshot/manifest.d.ts +73 -0
- package/dist/snapshot/manifest.d.ts.map +1 -0
- package/dist/snapshot/manifest.js +141 -0
- package/dist/snapshot/manifest.js.map +1 -0
- package/dist/snapshot/tree.d.ts +62 -0
- package/dist/snapshot/tree.d.ts.map +1 -0
- package/dist/snapshot/tree.js +147 -0
- package/dist/snapshot/tree.js.map +1 -0
- package/dist/tools/builtins.d.ts.map +1 -1
- package/dist/tools/builtins.js +67 -8
- package/dist/tools/builtins.js.map +1 -1
- package/dist/tools/callAgent.d.ts +16 -0
- package/dist/tools/callAgent.d.ts.map +1 -0
- package/dist/tools/callAgent.js +53 -0
- package/dist/tools/callAgent.js.map +1 -0
- package/dist/tools/files.d.ts +10 -0
- package/dist/tools/files.d.ts.map +1 -1
- package/dist/tools/files.js +159 -8
- package/dist/tools/files.js.map +1 -1
- package/dist/tools/http.js +1 -1
- package/dist/tools/namespaces.d.ts +1 -1
- package/dist/tools/namespaces.d.ts.map +1 -1
- package/dist/tools/namespaces.js +40 -2
- package/dist/tools/namespaces.js.map +1 -1
- package/dist/tools/procs.d.ts +18 -0
- package/dist/tools/procs.d.ts.map +1 -0
- package/dist/tools/procs.js +281 -0
- package/dist/tools/procs.js.map +1 -0
- package/dist/tools/todo.d.ts.map +1 -1
- package/dist/tools/todo.js +12 -0
- package/dist/tools/todo.js.map +1 -1
- package/package.json +18 -7
package/dist/sidecar/server.js
CHANGED
|
@@ -100,6 +100,43 @@ export class SidecarServer {
|
|
|
100
100
|
writable: true,
|
|
101
101
|
value: void 0
|
|
102
102
|
});
|
|
103
|
+
/** Detach callbacks for runs this connection is watching, so a dropped socket
|
|
104
|
+
* stops the hub fanning out into a dead channel. */
|
|
105
|
+
Object.defineProperty(this, "runDetachers", {
|
|
106
|
+
enumerable: true,
|
|
107
|
+
configurable: true,
|
|
108
|
+
writable: true,
|
|
109
|
+
value: new Set()
|
|
110
|
+
});
|
|
111
|
+
/** Processes THIS connection is watching. A shared process table means two
|
|
112
|
+
* browsers can be paired to one daemon, and a client that never asked about a
|
|
113
|
+
* process should not be streamed its output - so output is fanned out per
|
|
114
|
+
* attachment, not to everyone. Spawning attaches you; `proc-attach` is how a
|
|
115
|
+
* second client (or the same one after a reload) opts in. */
|
|
116
|
+
Object.defineProperty(this, "attachedProcs", {
|
|
117
|
+
enumerable: true,
|
|
118
|
+
configurable: true,
|
|
119
|
+
writable: true,
|
|
120
|
+
value: new Set()
|
|
121
|
+
});
|
|
122
|
+
Object.defineProperty(this, "procUnsub", {
|
|
123
|
+
enumerable: true,
|
|
124
|
+
configurable: true,
|
|
125
|
+
writable: true,
|
|
126
|
+
value: void 0
|
|
127
|
+
});
|
|
128
|
+
/* ---- sessions ---- */
|
|
129
|
+
/** Per-session stream stoppers for THIS connection.
|
|
130
|
+
*
|
|
131
|
+
* Per connection, not global: two browsers may watch one session, and a
|
|
132
|
+
* dropped socket must stop only its own follows. Keyed by session because a
|
|
133
|
+
* session outlives the request that started watching it. */
|
|
134
|
+
Object.defineProperty(this, "sessionWatch", {
|
|
135
|
+
enumerable: true,
|
|
136
|
+
configurable: true,
|
|
137
|
+
writable: true,
|
|
138
|
+
value: new Map()
|
|
139
|
+
});
|
|
103
140
|
Object.defineProperty(this, "memory", {
|
|
104
141
|
enumerable: true,
|
|
105
142
|
configurable: true,
|
|
@@ -182,9 +219,30 @@ export class SidecarServer {
|
|
|
182
219
|
for (const t of opts.tools)
|
|
183
220
|
this.tools.set(t.name, t);
|
|
184
221
|
this.ch.onMessage((m) => this.onMessage(m));
|
|
222
|
+
// Every connected server subscribes, which is what makes a root added by one
|
|
223
|
+
// paired client reach ALL of them: two browsers on one daemon must never
|
|
224
|
+
// disagree about what is reachable. The payload is the COMPLETE set.
|
|
225
|
+
const offRoots = opts.roots?.subscribe((s) => this.ch.send({ t: 'roots', root: s.root, roots: s.roots }));
|
|
185
226
|
this.ch.onClose?.(() => {
|
|
186
227
|
this.attendedDetach?.();
|
|
187
228
|
this.attendedDetach = undefined;
|
|
229
|
+
// A dropped connection must stop receiving run events, or the hub keeps
|
|
230
|
+
// fanning out into a dead channel for the rest of the run.
|
|
231
|
+
for (const detach of this.runDetachers)
|
|
232
|
+
detach();
|
|
233
|
+
this.runDetachers.clear();
|
|
234
|
+
offRoots?.();
|
|
235
|
+
// Stop streaming process output into a dead socket. The PROCESSES keep
|
|
236
|
+
// running - that is the point of the table living on the daemon - but this
|
|
237
|
+
// connection stops hearing about them until something re-attaches.
|
|
238
|
+
this.procUnsub?.();
|
|
239
|
+
this.procUnsub = undefined;
|
|
240
|
+
this.attachedProcs.clear();
|
|
241
|
+
// Same reasoning for sessions: the SESSIONS keep running — that is what
|
|
242
|
+
// makes resume cheap — but this connection stops being sent their
|
|
243
|
+
// metrics and logs.
|
|
244
|
+
for (const sid of [...this.sessionWatch.keys()])
|
|
245
|
+
this.stopWatching(sid);
|
|
188
246
|
});
|
|
189
247
|
}
|
|
190
248
|
onMessage(m) {
|
|
@@ -232,10 +290,31 @@ export class SidecarServer {
|
|
|
232
290
|
// pre-auth behavior (do not invent an ack variant).
|
|
233
291
|
if (!this.authed)
|
|
234
292
|
return;
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
293
|
+
this.onSubscribe(m);
|
|
294
|
+
return;
|
|
295
|
+
case 'acp-start':
|
|
296
|
+
if (!this.authed) {
|
|
297
|
+
this.ch.send({ t: 'acp-error', id: m.id, error: 'not authenticated' });
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
void this.onAcpStart(m);
|
|
301
|
+
return;
|
|
302
|
+
case 'acp-set-mode':
|
|
303
|
+
void this.onAcpSetMode(m);
|
|
304
|
+
return;
|
|
305
|
+
case 'acp-prompt':
|
|
306
|
+
if (!this.authed) {
|
|
307
|
+
this.ch.send({ t: 'acp-error', id: m.id, error: 'not authenticated' });
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
void this.onAcpPrompt(m);
|
|
311
|
+
return;
|
|
312
|
+
case 'acp-cancel':
|
|
313
|
+
// A notification-shaped message like `subscribe`: no response channel,
|
|
314
|
+
// so a pre-auth cancel is dropped silently rather than answered.
|
|
315
|
+
if (!this.authed)
|
|
316
|
+
return;
|
|
317
|
+
this.opts.acp?.cancel();
|
|
239
318
|
return;
|
|
240
319
|
case 'permission-answer':
|
|
241
320
|
// Guarded by auth like `run`/`subscribe` (auth boundary): a pre-auth
|
|
@@ -244,6 +323,100 @@ export class SidecarServer {
|
|
|
244
323
|
return;
|
|
245
324
|
this.opts.attendance?.answer(m.id, m.decision);
|
|
246
325
|
return;
|
|
326
|
+
case 'roots-browse':
|
|
327
|
+
// Auth-gated like every other request: directory enumeration must never
|
|
328
|
+
// precede the pairing token. Failures answer ok:false rather than throwing
|
|
329
|
+
// across the channel.
|
|
330
|
+
if (!this.authed) {
|
|
331
|
+
this.ch.send({ t: 'roots-browse-result', id: m.id, ok: false, error: 'not authenticated' });
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
void this.onRootsBrowse(m);
|
|
335
|
+
return;
|
|
336
|
+
case 'roots-add':
|
|
337
|
+
if (!this.authed) {
|
|
338
|
+
this.ch.send({ t: 'roots-add-result', id: m.id, ok: false, error: 'not authenticated' });
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
void this.onRootsAdd(m);
|
|
342
|
+
return;
|
|
343
|
+
case 'proc-spawn':
|
|
344
|
+
if (!this.authed) {
|
|
345
|
+
this.ch.send({ t: 'proc-error', id: m.id, error: 'not authenticated' });
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
void this.onProcSpawn(m);
|
|
349
|
+
return;
|
|
350
|
+
case 'proc-attach':
|
|
351
|
+
if (!this.authed) {
|
|
352
|
+
this.ch.send({ t: 'proc-attach-result', id: m.id, ok: false, error: 'not authenticated' });
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
this.onProcAttach(m);
|
|
356
|
+
return;
|
|
357
|
+
case 'proc-input':
|
|
358
|
+
if (!this.authed) {
|
|
359
|
+
this.ch.send({ t: 'proc-input-result', id: m.id, ok: false, error: 'not authenticated' });
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
this.onProcSimple(m.id, 'proc-input-result', () => this.requireProcs().write(m.procId, m.text));
|
|
363
|
+
return;
|
|
364
|
+
case 'proc-kill':
|
|
365
|
+
if (!this.authed) {
|
|
366
|
+
this.ch.send({ t: 'proc-kill-result', id: m.id, ok: false, error: 'not authenticated' });
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
this.onProcSimple(m.id, 'proc-kill-result', () => this.requireProcs().kill(m.procId));
|
|
370
|
+
return;
|
|
371
|
+
case 'proc-list':
|
|
372
|
+
// An empty list is the honest answer for a daemon without the capability:
|
|
373
|
+
// it is running nothing, and an error here would make the common "what is
|
|
374
|
+
// running?" probe noisy on every sidecar that never allowed shell.
|
|
375
|
+
if (!this.authed) {
|
|
376
|
+
this.ch.send({ t: 'proc-list-result', id: m.id, procs: [] });
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
this.ch.send({ t: 'proc-list-result', id: m.id, procs: this.opts.procs?.list() ?? [] });
|
|
380
|
+
return;
|
|
381
|
+
case 'session-list':
|
|
382
|
+
// An empty list is the honest answer from a daemon with no session
|
|
383
|
+
// layer: it is running none. An error would make the common "what is
|
|
384
|
+
// there?" probe noisy on every daemon that never enabled sessions.
|
|
385
|
+
if (!this.authed) {
|
|
386
|
+
this.ch.send({ t: 'session-list-result', id: m.id, sessions: [] });
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
this.ch.send({ t: 'session-list-result', id: m.id, sessions: this.opts.sessions?.list() ?? [] });
|
|
390
|
+
return;
|
|
391
|
+
case 'session-create':
|
|
392
|
+
case 'session-pause':
|
|
393
|
+
case 'session-resume':
|
|
394
|
+
case 'session-destroy':
|
|
395
|
+
case 'session-commit':
|
|
396
|
+
case 'session-snapshots':
|
|
397
|
+
case 'session-snapshot-usage':
|
|
398
|
+
case 'session-watch':
|
|
399
|
+
case 'session-unwatch':
|
|
400
|
+
if (!this.authed) {
|
|
401
|
+
this.ch.send({ t: 'session-error', id: m.id, error: 'not authenticated' });
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
void this.onSession(m);
|
|
405
|
+
return;
|
|
406
|
+
case 'run-history':
|
|
407
|
+
if (!this.authed) {
|
|
408
|
+
this.ch.send({ t: 'run-history-result', id: m.id, runs: [] });
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
void this.onRunHistory(m);
|
|
412
|
+
return;
|
|
413
|
+
case 'run-transcript':
|
|
414
|
+
if (!this.authed) {
|
|
415
|
+
this.ch.send({ t: 'run-transcript-result', id: m.id, lines: [] });
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
void this.onRunTranscript(m);
|
|
419
|
+
return;
|
|
247
420
|
case 'host-sync-put':
|
|
248
421
|
void this.onHostSyncPut(m);
|
|
249
422
|
return;
|
|
@@ -305,6 +478,8 @@ export class SidecarServer {
|
|
|
305
478
|
this.attendedDetach = this.opts.attendance.attach({
|
|
306
479
|
send: (ask) => this.ch.send({ t: 'permission-ask', ...ask }),
|
|
307
480
|
resolved: (id, decision, by) => this.ch.send({ t: 'permission-resolved', id, decision, by }),
|
|
481
|
+
runActivity: (activity) => this.ch.send({ t: 'run-activity', activity }),
|
|
482
|
+
registrations: (registrations) => this.ch.send({ t: 'registrations', registrations }),
|
|
308
483
|
});
|
|
309
484
|
}
|
|
310
485
|
this.opts.log?.(`paired — ${this.tools.size} ${this.tools.size === 1 ? 'tool' : 'tools'} advertised (host dir: ${this.hostStateDir})`);
|
|
@@ -316,7 +491,24 @@ export class SidecarServer {
|
|
|
316
491
|
// instead of adding one that would silently never fire. `info` (the confined
|
|
317
492
|
// root + platform) lets the host show the working directory when browsing
|
|
318
493
|
// this sidecar as a file source; omitted cleanly when not provided.
|
|
319
|
-
const features = [
|
|
494
|
+
const features = [
|
|
495
|
+
'http-proxy',
|
|
496
|
+
...(this.opts.runner ? ['runner'] : []),
|
|
497
|
+
...(this.opts.acp ? ['acp'] : []),
|
|
498
|
+
...(this.opts.roots?.areas.length ? ['add-roots'] : []),
|
|
499
|
+
...(this.opts.procs ? ['procs'] : []),
|
|
500
|
+
...(this.opts.sessions ? ['sessions'] : []),
|
|
501
|
+
// The operator's transcript CEILING, advertised so a client offers the
|
|
502
|
+
// per-run choice only where it could be honoured. Withheld means every
|
|
503
|
+
// `run` here keeps nothing however it asks — a client showing a "keep
|
|
504
|
+
// this" control against such a daemon would be promising storage that
|
|
505
|
+
// never happens.
|
|
506
|
+
...(this.opts.sessions?.canKeepTranscripts ? ['keep-transcripts'] : []),
|
|
507
|
+
// Whether a session can be created FROM a repository. Off unless the
|
|
508
|
+
// operator declared somewhere a checkout may be written, so a client only
|
|
509
|
+
// offers "start from a repository" where it would actually work.
|
|
510
|
+
...(this.opts.sessions?.canClone ? ['session-clone'] : []),
|
|
511
|
+
];
|
|
320
512
|
this.ch.send({ t: 'welcome', tools, features, ...(this.opts.info ? { info: this.opts.info } : {}) });
|
|
321
513
|
}
|
|
322
514
|
/** Proxy a host HTTP request server-side. Restricted to LOOPBACK targets — the
|
|
@@ -355,6 +547,224 @@ export class SidecarServer {
|
|
|
355
547
|
this.ch.send({ t: 'http-err', id: m.id, error: errMessage(e) });
|
|
356
548
|
}
|
|
357
549
|
}
|
|
550
|
+
requireSessions() {
|
|
551
|
+
const s = this.opts.sessions;
|
|
552
|
+
if (!s)
|
|
553
|
+
throw new Error('this sidecar runs no sessions (start the daemon with --daemon)');
|
|
554
|
+
return s;
|
|
555
|
+
}
|
|
556
|
+
async onSession(m) {
|
|
557
|
+
const id = m.id;
|
|
558
|
+
try {
|
|
559
|
+
const sessions = this.requireSessions();
|
|
560
|
+
switch (m.t) {
|
|
561
|
+
case 'session-create':
|
|
562
|
+
this.ch.send({
|
|
563
|
+
t: 'session-created',
|
|
564
|
+
id,
|
|
565
|
+
session: await sessions.create({
|
|
566
|
+
...(m.workdir ? { workdir: m.workdir } : {}),
|
|
567
|
+
/* `repo` asks the daemon to CLONE the source rather than mount a
|
|
568
|
+
* directory the client named. Forwarded, not judged: whether
|
|
569
|
+
* cloning is permitted at all, where a checkout may land, and
|
|
570
|
+
* which URLs are refused all live on the daemon side, where the
|
|
571
|
+
* operator's declarations are. */
|
|
572
|
+
...(m.repo ? { repo: m.repo } : {}),
|
|
573
|
+
...(m.owner ? { owner: m.owner } : {}),
|
|
574
|
+
...(m.fromSnapshot ? { fromSnapshot: m.fromSnapshot } : {}),
|
|
575
|
+
// From the HANDSHAKE, never from this message: the dot-dir is a
|
|
576
|
+
// property of the client's workspace, and taking it from the
|
|
577
|
+
// request would let a caller point a session's state anywhere.
|
|
578
|
+
stateDir: this.hostStateDir,
|
|
579
|
+
}),
|
|
580
|
+
});
|
|
581
|
+
return;
|
|
582
|
+
case 'session-pause': {
|
|
583
|
+
// The drain result is carried through rather than reduced to ok/not:
|
|
584
|
+
// a caller about to capture has to know whether work was still
|
|
585
|
+
// running, because that is what makes a snapshot torn.
|
|
586
|
+
const r = await sessions.pause(m.sessionId, m.drainMs);
|
|
587
|
+
this.ch.send({ t: 'session-paused', id, drained: r.drained, remaining: r.remaining });
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
case 'session-resume':
|
|
591
|
+
await sessions.resume(m.sessionId);
|
|
592
|
+
this.ch.send({ t: 'session-ok', id });
|
|
593
|
+
return;
|
|
594
|
+
case 'session-destroy':
|
|
595
|
+
this.stopWatching(m.sessionId);
|
|
596
|
+
await sessions.destroy(m.sessionId);
|
|
597
|
+
this.ch.send({ t: 'session-ok', id });
|
|
598
|
+
return;
|
|
599
|
+
case 'session-commit': {
|
|
600
|
+
if (!sessions.commit)
|
|
601
|
+
throw new Error('this session backend cannot capture snapshots');
|
|
602
|
+
const snapshot = await sessions.commit(m.sessionId, m.name);
|
|
603
|
+
this.ch.send({ t: 'session-committed', id, snapshot });
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
case 'session-snapshots': {
|
|
607
|
+
// An empty list, not an error, when the backend cannot capture: the
|
|
608
|
+
// question "what could I restore from?" has an answer there, and it
|
|
609
|
+
// is "nothing". The client uses it to decide whether to offer the
|
|
610
|
+
// choice at all, and an error would make it look broken instead.
|
|
611
|
+
const snapshots = sessions.snapshots ? await sessions.snapshots() : [];
|
|
612
|
+
this.ch.send({ t: 'session-snapshots-result', id, snapshots });
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
case 'session-snapshot-usage': {
|
|
616
|
+
// Same shape as the empty-list answer above: a backend with no
|
|
617
|
+
// budget to report has zero usage against zero budget, not an
|
|
618
|
+
// error — a status surface should render that as "not applicable"
|
|
619
|
+
// rather than treat it as broken.
|
|
620
|
+
const usage = sessions.snapshotUsage
|
|
621
|
+
? await sessions.snapshotUsage()
|
|
622
|
+
: { usedBytes: 0, budgetBytes: 0, count: 0 };
|
|
623
|
+
this.ch.send({ t: 'session-snapshot-usage-result', id, usage });
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
case 'session-watch': {
|
|
627
|
+
// Idempotent: watching twice from one connection would double every
|
|
628
|
+
// sample rather than doing nothing, which is the kind of bug a
|
|
629
|
+
// reconnecting UI creates by accident.
|
|
630
|
+
if (this.sessionWatch.has(m.sessionId)) {
|
|
631
|
+
this.ch.send({ t: 'session-ok', id });
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
const stops = [];
|
|
635
|
+
const sid = m.sessionId;
|
|
636
|
+
if (sessions.watchMetrics) {
|
|
637
|
+
stops.push(sessions.watchMetrics(sid, (sample) => this.ch.send({ t: 'session-metrics', sessionId: sid, sample })));
|
|
638
|
+
}
|
|
639
|
+
if (sessions.watchLogs) {
|
|
640
|
+
stops.push(sessions.watchLogs(sid, (line) => this.ch.send({ t: 'session-log', sessionId: sid, line })));
|
|
641
|
+
}
|
|
642
|
+
this.sessionWatch.set(sid, stops);
|
|
643
|
+
this.ch.send({ t: 'session-ok', id });
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
case 'session-unwatch':
|
|
647
|
+
this.stopWatching(m.sessionId);
|
|
648
|
+
this.ch.send({ t: 'session-ok', id });
|
|
649
|
+
return;
|
|
650
|
+
default:
|
|
651
|
+
this.ch.send({ t: 'session-error', id, error: `unhandled session message: ${m.t}` });
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
catch (e) {
|
|
655
|
+
this.ch.send({ t: 'session-error', id, error: errMessage(e) });
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
stopWatching(sessionId) {
|
|
659
|
+
for (const stop of this.sessionWatch.get(sessionId) ?? []) {
|
|
660
|
+
try {
|
|
661
|
+
stop();
|
|
662
|
+
}
|
|
663
|
+
catch { /* already stopped */ }
|
|
664
|
+
}
|
|
665
|
+
this.sessionWatch.delete(sessionId);
|
|
666
|
+
}
|
|
667
|
+
/* ---- long-running processes ---- */
|
|
668
|
+
/** The table, or a refusal that names WHY. "unknown message" would send an
|
|
669
|
+
* operator hunting for a bug when the real answer is that they did not pass
|
|
670
|
+
* --allow-shell. */
|
|
671
|
+
requireProcs() {
|
|
672
|
+
const procs = this.opts.procs;
|
|
673
|
+
if (!procs)
|
|
674
|
+
throw new Error('this sidecar was not started with --allow-shell, so it cannot run processes');
|
|
675
|
+
return procs;
|
|
676
|
+
}
|
|
677
|
+
/** Begin fanning this connection's attached processes back over the wire.
|
|
678
|
+
* Subscribed lazily: a client that never touches a process should not pay to
|
|
679
|
+
* be woken for someone else's build output. */
|
|
680
|
+
ensureProcStream() {
|
|
681
|
+
if (this.procUnsub)
|
|
682
|
+
return;
|
|
683
|
+
this.procUnsub = this.requireProcs().subscribe((ev) => {
|
|
684
|
+
if (!this.attachedProcs.has(ev.procId))
|
|
685
|
+
return;
|
|
686
|
+
if (ev.kind === 'out') {
|
|
687
|
+
this.ch.send({ t: 'proc-out', procId: ev.procId, seq: ev.chunk.seq, stream: ev.chunk.stream, text: ev.chunk.text });
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
this.ch.send({
|
|
691
|
+
t: 'proc-exit',
|
|
692
|
+
procId: ev.procId,
|
|
693
|
+
code: ev.info.exitCode ?? null,
|
|
694
|
+
...(ev.info.signal ? { signal: ev.info.signal } : {}),
|
|
695
|
+
});
|
|
696
|
+
// Attachment ends with the process. Keeping it would leak an entry per run
|
|
697
|
+
// on a connection that stays open for hours.
|
|
698
|
+
this.attachedProcs.delete(ev.procId);
|
|
699
|
+
});
|
|
700
|
+
}
|
|
701
|
+
async onProcSpawn(m) {
|
|
702
|
+
try {
|
|
703
|
+
const procs = this.requireProcs();
|
|
704
|
+
// Attach BEFORE spawning: a command that prints immediately would otherwise
|
|
705
|
+
// race the subscription and lose its first lines.
|
|
706
|
+
this.ensureProcStream();
|
|
707
|
+
const info = await procs.spawn({
|
|
708
|
+
command: m.command,
|
|
709
|
+
...(m.cwd ? { cwd: m.cwd } : {}),
|
|
710
|
+
...(m.root ? { root: m.root } : {}),
|
|
711
|
+
...(m.label ? { label: m.label } : {}),
|
|
712
|
+
});
|
|
713
|
+
this.attachedProcs.add(info.procId);
|
|
714
|
+
this.ch.send({ t: 'proc-started', id: m.id, proc: info });
|
|
715
|
+
// A process can finish before the caller ever sees `proc-started` (`echo
|
|
716
|
+
// hi`). Replay whatever the table already holds so no output is lost to
|
|
717
|
+
// that race, and report the exit it already had.
|
|
718
|
+
const { chunks } = procs.backlog(info.procId);
|
|
719
|
+
for (const c of chunks)
|
|
720
|
+
this.ch.send({ t: 'proc-out', procId: info.procId, seq: c.seq, stream: c.stream, text: c.text });
|
|
721
|
+
const now = procs.get(info.procId);
|
|
722
|
+
if (now && !now.running) {
|
|
723
|
+
this.attachedProcs.delete(info.procId);
|
|
724
|
+
this.ch.send({ t: 'proc-exit', procId: info.procId, code: now.exitCode ?? null, ...(now.signal ? { signal: now.signal } : {}) });
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
catch (e) {
|
|
728
|
+
this.ch.send({ t: 'proc-error', id: m.id, error: errMessage(e) });
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
/** Re-attach to a process after a reload or from a second client. `sinceSeq`
|
|
732
|
+
* is what the client already has, so the replay is exactly the rest. */
|
|
733
|
+
onProcAttach(m) {
|
|
734
|
+
try {
|
|
735
|
+
const procs = this.requireProcs();
|
|
736
|
+
const info = procs.get(m.procId);
|
|
737
|
+
if (!info) {
|
|
738
|
+
this.ch.send({ t: 'proc-attach-result', id: m.id, ok: false, error: `no such process: ${m.procId}` });
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
this.ensureProcStream();
|
|
742
|
+
if (info.running)
|
|
743
|
+
this.attachedProcs.add(m.procId);
|
|
744
|
+
const { chunks, truncated } = procs.backlog(m.procId, m.sinceSeq);
|
|
745
|
+
this.ch.send({ t: 'proc-attach-result', id: m.id, ok: true, proc: info, ...(truncated ? { truncated: true } : {}) });
|
|
746
|
+
for (const c of chunks)
|
|
747
|
+
this.ch.send({ t: 'proc-out', procId: m.procId, seq: c.seq, stream: c.stream, text: c.text });
|
|
748
|
+
// An already-exited process still owes the client its ending, or a
|
|
749
|
+
// reattaching UI shows a spinner for something finished an hour ago.
|
|
750
|
+
if (!info.running) {
|
|
751
|
+
this.ch.send({ t: 'proc-exit', procId: m.procId, code: info.exitCode ?? null, ...(info.signal ? { signal: info.signal } : {}) });
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
catch (e) {
|
|
755
|
+
this.ch.send({ t: 'proc-attach-result', id: m.id, ok: false, error: errMessage(e) });
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
/** The kill/input pair: act, then answer with ok or the reason. */
|
|
759
|
+
onProcSimple(id, reply, act) {
|
|
760
|
+
try {
|
|
761
|
+
act();
|
|
762
|
+
this.ch.send({ t: reply, id, ok: true });
|
|
763
|
+
}
|
|
764
|
+
catch (e) {
|
|
765
|
+
this.ch.send({ t: reply, id, ok: false, error: errMessage(e) });
|
|
766
|
+
}
|
|
767
|
+
}
|
|
358
768
|
async onInvoke(m) {
|
|
359
769
|
const tool = this.tools.get(m.name);
|
|
360
770
|
if (!tool) {
|
|
@@ -382,14 +792,179 @@ export class SidecarServer {
|
|
|
382
792
|
* turn). Events that arrive before we know the runId are buffered and flushed
|
|
383
793
|
* right after `run-accepted`, so they still carry the correct runId and never
|
|
384
794
|
* race ahead of it on the wire. */
|
|
795
|
+
async onAcpStart(m) {
|
|
796
|
+
const acp = this.opts.acp;
|
|
797
|
+
if (!acp) {
|
|
798
|
+
this.ch.send({ t: 'acp-error', id: m.id, error: 'this sidecar was not started with --acp' });
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
801
|
+
try {
|
|
802
|
+
const started = await acp.start(m.target, (update) => {
|
|
803
|
+
this.ch.send({ t: 'acp-update', id: m.id, update });
|
|
804
|
+
}, {
|
|
805
|
+
...(m.root ? { root: m.root } : {}),
|
|
806
|
+
...(m.modeId ? { modeId: m.modeId } : {}),
|
|
807
|
+
...(m.resume ? { resume: m.resume } : {}),
|
|
808
|
+
});
|
|
809
|
+
this.ch.send({ t: 'acp-started', id: m.id, ...started });
|
|
810
|
+
}
|
|
811
|
+
catch (err) {
|
|
812
|
+
this.ch.send({ t: 'acp-error', id: m.id, error: errMessage(err) });
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
async onAcpSetMode(m) {
|
|
816
|
+
const acp = this.opts.acp;
|
|
817
|
+
if (!acp) {
|
|
818
|
+
this.ch.send({ t: 'acp-error', id: m.id, error: 'this sidecar was not started with --acp' });
|
|
819
|
+
return;
|
|
820
|
+
}
|
|
821
|
+
try {
|
|
822
|
+
await acp.setMode(m.modeId);
|
|
823
|
+
this.ch.send({ t: 'acp-mode-set', id: m.id, modeId: m.modeId });
|
|
824
|
+
}
|
|
825
|
+
catch (err) {
|
|
826
|
+
this.ch.send({ t: 'acp-error', id: m.id, error: errMessage(err) });
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
async onAcpPrompt(m) {
|
|
830
|
+
const acp = this.opts.acp;
|
|
831
|
+
if (!acp) {
|
|
832
|
+
this.ch.send({ t: 'acp-error', id: m.id, error: 'this sidecar was not started with --acp' });
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
835
|
+
try {
|
|
836
|
+
const { stopReason } = await acp.prompt(m.text, m.content);
|
|
837
|
+
this.ch.send({ t: 'acp-done', id: m.id, stopReason });
|
|
838
|
+
}
|
|
839
|
+
catch (err) {
|
|
840
|
+
this.ch.send({ t: 'acp-error', id: m.id, error: errMessage(err) });
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
/**
|
|
844
|
+
* Attach this connection as a VIEWER of a run it did not start.
|
|
845
|
+
*
|
|
846
|
+
* The replay comes first, so a client joining mid-run sees the whole run
|
|
847
|
+
* rather than the tail — a viewer that only sees what happens next cannot tell
|
|
848
|
+
* a stalled run from a quiet one. Events are tagged with the SUBSCRIBER's own
|
|
849
|
+
* message id, so a client correlating by id sees its own stream.
|
|
850
|
+
*/
|
|
851
|
+
onSubscribe(m) {
|
|
852
|
+
const hub = this.opts.runs;
|
|
853
|
+
if (!hub) {
|
|
854
|
+
this.ch.send({ t: 'run-error', id: m.id, error: 'this sidecar does not support watching runs' });
|
|
855
|
+
return;
|
|
856
|
+
}
|
|
857
|
+
const attached = hub.subscribe(m.runId, {
|
|
858
|
+
event: (event) => this.ch.send({ t: 'event', id: m.id, runId: m.runId, event }),
|
|
859
|
+
done: (r) => {
|
|
860
|
+
this.ch.send({ t: 'run-done', id: m.id, runId: m.runId, text: r.text, stopReason: r.stopReason });
|
|
861
|
+
detachOnce();
|
|
862
|
+
},
|
|
863
|
+
error: (message) => {
|
|
864
|
+
this.ch.send({ t: 'run-error', id: m.id, error: message });
|
|
865
|
+
detachOnce();
|
|
866
|
+
},
|
|
867
|
+
});
|
|
868
|
+
if (!attached) {
|
|
869
|
+
this.ch.send({ t: 'run-error', id: m.id, error: `no such run: ${m.runId}` });
|
|
870
|
+
return;
|
|
871
|
+
}
|
|
872
|
+
// Held so a dropped connection detaches; also removed on done/error below.
|
|
873
|
+
const detachOnce = () => {
|
|
874
|
+
if (!this.runDetachers.has(attached.detach))
|
|
875
|
+
return;
|
|
876
|
+
this.runDetachers.delete(attached.detach);
|
|
877
|
+
attached.detach();
|
|
878
|
+
};
|
|
879
|
+
this.runDetachers.add(attached.detach);
|
|
880
|
+
this.ch.send({ t: 'run-accepted', id: m.id, runId: m.runId });
|
|
881
|
+
if (attached.replay.dropped > 0) {
|
|
882
|
+
// Say so rather than handing over a tail that looks complete.
|
|
883
|
+
this.opts.log?.(`subscribe ${m.runId}: replaying with ${attached.replay.dropped} earlier event(s) dropped`);
|
|
884
|
+
}
|
|
885
|
+
for (const event of attached.replay.events) {
|
|
886
|
+
this.ch.send({ t: 'event', id: m.id, runId: m.runId, event });
|
|
887
|
+
}
|
|
888
|
+
// A run that already finished still replays, then closes immediately.
|
|
889
|
+
if (attached.replay.done) {
|
|
890
|
+
this.ch.send({ t: 'run-done', id: m.id, runId: m.runId, ...attached.replay.done });
|
|
891
|
+
detachOnce();
|
|
892
|
+
}
|
|
893
|
+
else if (attached.replay.error) {
|
|
894
|
+
this.ch.send({ t: 'run-error', id: m.id, error: attached.replay.error });
|
|
895
|
+
detachOnce();
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
/**
|
|
899
|
+
* What this caller's agent has actually done.
|
|
900
|
+
*
|
|
901
|
+
* AN EMPTY LIST IS AN ANSWER, not a failure: a daemon that keeps no record
|
|
902
|
+
* has no history, and a client uses the rows to render rather than to detect
|
|
903
|
+
* a broken daemon. The identity handed down is the untrusted `{ clientId }`
|
|
904
|
+
* this connection authenticated with — the runner resolves it through the
|
|
905
|
+
* hosts manifest, which is what makes the result owner-scoped by
|
|
906
|
+
* construction rather than by a filter written here.
|
|
907
|
+
*/
|
|
908
|
+
async onRunHistory(m) {
|
|
909
|
+
try {
|
|
910
|
+
const runs = this.opts.runner?.history
|
|
911
|
+
? await this.opts.runner.history({ clientId: this.clientId }, m.agent, m.limit)
|
|
912
|
+
: [];
|
|
913
|
+
this.ch.send({ t: 'run-history-result', id: m.id, runs });
|
|
914
|
+
}
|
|
915
|
+
catch {
|
|
916
|
+
// A history that cannot be read is not a reason to break the caller's
|
|
917
|
+
// page; the rows it already has stay, and it sees none added.
|
|
918
|
+
this.ch.send({ t: 'run-history-result', id: m.id, runs: [] });
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
/** One run's kept events. Empty when they were never kept — the ordinary
|
|
922
|
+
* case, and a state the caller renders rather than treats as a failure. */
|
|
923
|
+
async onRunTranscript(m) {
|
|
924
|
+
try {
|
|
925
|
+
const lines = this.opts.runner?.transcript
|
|
926
|
+
? await this.opts.runner.transcript({ clientId: this.clientId }, m.agent, m.runId)
|
|
927
|
+
: [];
|
|
928
|
+
this.ch.send({ t: 'run-transcript-result', id: m.id, lines });
|
|
929
|
+
}
|
|
930
|
+
catch {
|
|
931
|
+
this.ch.send({ t: 'run-transcript-result', id: m.id, lines: [] });
|
|
932
|
+
}
|
|
933
|
+
}
|
|
385
934
|
async onRun(m) {
|
|
386
|
-
|
|
935
|
+
/* WHERE the run happens, decided once, up front.
|
|
936
|
+
*
|
|
937
|
+
* A `sessionId` is a confinement request. A daemon that cannot honour it
|
|
938
|
+
* says so rather than running the work in its own process anyway — silently
|
|
939
|
+
* downgrading is the failure IsolationLevel exists to prevent, and it would
|
|
940
|
+
* be invisible to the client that asked. */
|
|
941
|
+
if (m.sessionId !== undefined) {
|
|
942
|
+
const sessions = this.opts.sessions;
|
|
943
|
+
if (!sessions?.run) {
|
|
944
|
+
this.ch.send({ t: 'run-error', id: m.id, error: 'this sidecar cannot run agents inside a session' });
|
|
945
|
+
return;
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
else if (!this.opts.runner) {
|
|
387
949
|
this.ch.send({ t: 'run-error', id: m.id, error: 'this sidecar cannot run agents' });
|
|
388
950
|
return;
|
|
389
951
|
}
|
|
952
|
+
const start = m.sessionId !== undefined
|
|
953
|
+
? (agent, input, onEvent, identity) =>
|
|
954
|
+
/* The request is forwarded, never resolved here. Whether it is honoured
|
|
955
|
+
* is the session layer's call, because the operator's permission lives
|
|
956
|
+
* there; this class would have to duplicate that rule to decide, and a
|
|
957
|
+
* duplicated permission rule is one that eventually disagrees. */
|
|
958
|
+
this.opts.sessions.run(m.sessionId, agent, input, onEvent, identity, { keepTranscript: m.keepTranscript === true })
|
|
959
|
+
: (agent, input, onEvent, identity) => this.opts.runner.run(agent, input, onEvent, identity);
|
|
390
960
|
let runId;
|
|
391
961
|
const pending = [];
|
|
392
962
|
const onEvent = (event) => {
|
|
963
|
+
// Publish to the hub as well as this connection, so another device can
|
|
964
|
+
// watch. Buffered until the runner reports its id, since the hub is keyed
|
|
965
|
+
// by it — the same reason `pending` exists for the commanding client.
|
|
966
|
+
if (runId !== undefined)
|
|
967
|
+
this.opts.runs?.publish(runId, event);
|
|
393
968
|
if (runId === undefined) {
|
|
394
969
|
pending.push(event);
|
|
395
970
|
return;
|
|
@@ -403,16 +978,25 @@ export class SidecarServer {
|
|
|
403
978
|
// (no manifest access here, by design; see the module doc), it just
|
|
404
979
|
// hands the identity it already holds to the runner, exactly like
|
|
405
980
|
// `onHostSyncPut`/`onHostSyncDelete` do for `hostSync.put`/`remove`.
|
|
406
|
-
const started = await
|
|
981
|
+
const started = await start(m.agent, m.input, onEvent, { clientId: this.clientId });
|
|
407
982
|
runId = started.runId;
|
|
983
|
+
this.opts.runs?.open(runId);
|
|
408
984
|
this.ch.send({ t: 'run-accepted', id: m.id, runId });
|
|
409
|
-
for (const event of pending)
|
|
985
|
+
for (const event of pending) {
|
|
986
|
+
this.opts.runs?.publish(runId, event);
|
|
410
987
|
this.ch.send({ t: 'event', id: m.id, runId, event });
|
|
988
|
+
}
|
|
411
989
|
const fin = await started.done;
|
|
990
|
+
this.opts.runs?.finish(runId, { text: fin.text, stopReason: fin.stopReason });
|
|
412
991
|
this.ch.send({ t: 'run-done', id: m.id, runId, text: fin.text, stopReason: fin.stopReason });
|
|
413
992
|
}
|
|
414
993
|
catch (e) {
|
|
415
|
-
|
|
994
|
+
const message = errMessage(e);
|
|
995
|
+
// Tell watchers too: a run that failed must not look merely silent to a
|
|
996
|
+
// second viewer.
|
|
997
|
+
if (runId !== undefined)
|
|
998
|
+
this.opts.runs?.fail(runId, message);
|
|
999
|
+
this.ch.send({ t: 'run-error', id: m.id, error: message });
|
|
416
1000
|
}
|
|
417
1001
|
}
|
|
418
1002
|
/** The identity to attribute a host-sync write to. Returns either the
|
|
@@ -429,6 +1013,33 @@ export class SidecarServer {
|
|
|
429
1013
|
}
|
|
430
1014
|
return { clientId: this.clientId };
|
|
431
1015
|
}
|
|
1016
|
+
async onRootsBrowse(m) {
|
|
1017
|
+
if (!this.opts.roots) {
|
|
1018
|
+
this.ch.send({ t: 'roots-browse-result', id: m.id, ok: false, error: 'this sidecar cannot add working directories' });
|
|
1019
|
+
return;
|
|
1020
|
+
}
|
|
1021
|
+
try {
|
|
1022
|
+
this.ch.send({ t: 'roots-browse-result', id: m.id, ok: true, dirs: await this.opts.roots.browse(m.path) });
|
|
1023
|
+
}
|
|
1024
|
+
catch (e) {
|
|
1025
|
+
this.ch.send({ t: 'roots-browse-result', id: m.id, ok: false, error: errMessage(e) });
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
async onRootsAdd(m) {
|
|
1029
|
+
if (!this.opts.roots) {
|
|
1030
|
+
this.ch.send({ t: 'roots-add-result', id: m.id, ok: false, error: 'this sidecar cannot add working directories' });
|
|
1031
|
+
return;
|
|
1032
|
+
}
|
|
1033
|
+
try {
|
|
1034
|
+
// The broadcast to every client is the controller's subscription, not this
|
|
1035
|
+
// reply -- so a refusal (below) reaches only the asker and changes nothing.
|
|
1036
|
+
await this.opts.roots.add(m.path);
|
|
1037
|
+
this.ch.send({ t: 'roots-add-result', id: m.id, ok: true });
|
|
1038
|
+
}
|
|
1039
|
+
catch (e) {
|
|
1040
|
+
this.ch.send({ t: 'roots-add-result', id: m.id, ok: false, error: errMessage(e) });
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
432
1043
|
async onHostSyncPut(m) {
|
|
433
1044
|
const identity = this.requireHostSyncIdentity();
|
|
434
1045
|
if ('error' in identity) {
|