@dougbots/avenor-core 0.14.0 → 0.15.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/index.d.ts CHANGED
@@ -149,6 +149,15 @@ declare class RpcError extends Error {
149
149
  readonly data?: unknown;
150
150
  constructor(code: number, message: string, data?: unknown);
151
151
  }
152
+ type OwnerAuthFailureKind = 'unauthenticated' | 'unsupported' | 'timeout';
153
+ /** A classified owner-authentication rejection, not generic transport failure. */
154
+ declare class OwnerAuthError extends Error {
155
+ readonly kind: OwnerAuthFailureKind;
156
+ constructor(kind: OwnerAuthFailureKind, options?: {
157
+ code?: number;
158
+ reason?: string;
159
+ });
160
+ }
152
161
  declare class Client {
153
162
  private socket;
154
163
  private rl;
@@ -171,6 +180,22 @@ declare class Client {
171
180
  call(method: string, params?: unknown, opts?: {
172
181
  timeoutMs?: number;
173
182
  }): Promise<unknown>;
183
+ /**
184
+ * Send a credential-free identity probe. The control server returns the
185
+ * startup token only after it has published its own owner record.
186
+ */
187
+ identity(timeoutMs?: number): Promise<string>;
188
+ /**
189
+ * Present the owner capability on this connection. The caller must already
190
+ * have bound the connection to a trusted peer: an owner attach checks the
191
+ * socket's ownership and identity, and an owned start proves the child with
192
+ * its attempt token.
193
+ */
194
+ authenticateOwner(capability: {
195
+ id: string;
196
+ secret: string;
197
+ }, timeoutMs?: number): Promise<void>;
198
+ private sendCall;
174
199
  subscribe(options?: SubscribeOptions): Promise<void>;
175
200
  events(options?: SubscribeOptions): AsyncIterableIterator<Event>;
176
201
  history(options: HistoryOptions): Promise<HistoryResult>;
@@ -216,8 +241,42 @@ declare class Client {
216
241
  }
217
242
  declare function dial(socketPath: string, opts?: {
218
243
  callTimeoutMs?: number;
244
+ connectTimeoutMs?: number;
219
245
  }): Promise<Client>;
220
246
  //#endregion
247
+ //#region src/owner-attach.d.ts
248
+ /** The owner record published beside a control socket is absent. */
249
+ declare class OwnerRecordMissingError extends Error {
250
+ readonly socketPath: string;
251
+ constructor(socketPath: string);
252
+ }
253
+ /** The record or the socket it names no longer describes the live path. */
254
+ declare class StaleOwnerRecordError extends Error {
255
+ readonly socketPath: string;
256
+ readonly reason: string;
257
+ constructor(socketPath: string, reason: string);
258
+ }
259
+ /**
260
+ * The control socket path is not bound to this euid, or its directory is
261
+ * writable by another user. No credential byte may be sent to that peer.
262
+ */
263
+ declare class ForeignPeerError extends Error {
264
+ readonly socketPath: string;
265
+ readonly euid: number;
266
+ readonly reason: string;
267
+ readonly peerUID?: number | undefined;
268
+ constructor(socketPath: string, euid: number, reason: string, peerUID?: number | undefined);
269
+ }
270
+ interface OwnerAttachOptions {
271
+ timeoutMs?: number;
272
+ }
273
+ /**
274
+ * Load the owner record, bind the socket path to this euid and require it to be
275
+ * the record's socket, dial, re-check the socket identity and authenticate. The
276
+ * whole attach is bounded by the timeout.
277
+ */
278
+ declare function attachOwner(socketPath: string, options?: OwnerAttachOptions): Promise<Client>;
279
+ //#endregion
221
280
  //#region src/run-events.d.ts
222
281
  type RunChunkRole = 'assistant' | 'thought' | 'user';
223
282
  interface RunPermissionOption {
@@ -452,7 +511,11 @@ declare class Supervisor {
452
511
  declare function socketsRoot(): string;
453
512
  //#endregion
454
513
  //#region src/tools/get-supervisor-client.d.ts
455
- declare function getSupervisorClient(supervisorId: string | undefined): Promise<{
514
+ interface SupervisorClientOptions {
515
+ /** Attach with the socket's published owner capability instead of a bare dial. */
516
+ owner?: boolean;
517
+ }
518
+ declare function getSupervisorClient(supervisorId: string | undefined, options?: SupervisorClientOptions): Promise<{
456
519
  client: Client;
457
520
  isSingleton: boolean;
458
521
  sup: Supervisor | null;
@@ -660,6 +723,18 @@ interface AskResult {
660
723
  reply: string;
661
724
  from_run_id: string;
662
725
  }
726
+ declare function extractReplyMessage(result: Record<string, unknown>): string;
727
+ /**
728
+ * Build the failure message every ask path reports: the broker message id plus
729
+ * whether the ask edge is still pending (withdrawable via avenor_cancel) or
730
+ * already gone.
731
+ */
732
+ declare function formatAskFailure(messageId: string, pending: boolean, detail: string): string;
733
+ /**
734
+ * Reduce a caller-supplied ask timeout to what the broker accepts: a positive
735
+ * finite number of milliseconds, or undefined to keep the broker's own wait.
736
+ */
737
+ declare function normalizeAskTimeoutMs(timeoutMs: number | undefined): number | undefined;
663
738
  declare const askTool: (args: AskToolArgs) => Promise<AskResult>;
664
739
  //#endregion
665
740
  //#region src/tools/peers.d.ts
@@ -805,4 +880,4 @@ type WorkflowReadyResult = Record<string, unknown>;
805
880
  declare function createWorkflowReadyTool(getSupervisorClient: GetSupervisorClient): (args: WorkflowReadyToolArgs) => Promise<WorkflowReadyResult>;
806
881
  declare const workflowReadyTool: (args: WorkflowReadyToolArgs) => Promise<WorkflowReadyResult>;
807
882
  //#endregion
808
- export { type AskResult, type AskToolArgs, type BrokerReceiveArgs, type BrokerReceiveResult, type BrokerReplyArgs, type BrokerReplyResult, type CancelResult, type CancelToolArgs, type Client, DEFAULT_RUN_REDUCER_LIMITS, type Event, type ExecutionIdentity, type FollowUpToolArgs, type FollowUpToolResult, type HistoryOptions, type HistoryResult, type InboundAsk, type InspectResult, type KillResult, type KillToolArgs, type NormalizedRunEvent, type ObserveRunOptions, type PeerEntry, type PeersResult, type ResultResult, type ResultToolArgs, RpcError, type RunChunkRole, type RunIdentity, type RunInfo, type RunObserver, type RunObserverClient, type RunPermissionOption, type RunPermissionSummary, RunReducer, type RunReducerLimits, type RunSnapshot, type RunSnapshotSeed, type RunToolState, type RunToolSummary, type RunTranscriptEntry, type SpawnMetadata, type SpawnParams, type SpawnResult, type SpawnSelectionInput, type SpawnToolArgs, type SpawnToolResult, type StatusListResult, type StatusResult, type StatusToolArgs, type StatusView, type SubscribeOptions, Supervisor, type SupervisorOptions, THINKING_LEVELS, type ThinkingLevel, type ThinkingOutcome, type WorkflowCompleteParams, type WorkflowCompleteResult, type WorkflowCompleteToolArgs, type WorkflowControllerCreateParams, type WorkflowControllerStatusResult, type WorkflowControllerStatusToolArgs, type WorkflowEventsResult, type WorkflowEventsToolArgs, type WorkflowGateOperation, type WorkflowGateParams, type WorkflowGateResult, type WorkflowGateToolArgs, type WorkflowHeartbeatParams, type WorkflowInspectResult, type WorkflowInspectToolArgs, type WorkflowReadyResult, type WorkflowReadyToolArgs, type WorkflowStatusResult, type WorkflowStatusToolArgs, type WorkflowWaitResult, type WorkflowWaitToolArgs, answerPermissionTool, askTool, brokerReceiveTool, brokerReplyTool, cancelTool, createRunSnapshot, createWorkflowCompleteTool, createWorkflowControllerStatusTool, createWorkflowEventsTool, createWorkflowGateTool, createWorkflowInspectTool, createWorkflowReadyTool, createWorkflowStatusTool, createWorkflowWaitTool, dial, evaluateThinkingPolicy, eventsTool, extractEventText, findAvenorBinary, followUpTool, getInstallDir, getVersion, inspectTool, installerBinaryPath, isThinkingLevel, killTool, normalizeRunEvent, observeRun, peersTool, reduceRunSnapshot, resultTool, shutdownTool, socketsRoot, spawnTool, statusTool, validateSpawnSelection, validateThinking, validateThinkingForBackend, validateThinkingForBackendResume, workflowCompleteTool, workflowControllerStatusTool, workflowEventsTool, workflowGateTool, workflowInspectTool, workflowReadyTool, workflowStatusTool, workflowWaitTool };
883
+ export { type AskResult, type AskToolArgs, type BrokerReceiveArgs, type BrokerReceiveResult, type BrokerReplyArgs, type BrokerReplyResult, type CancelResult, type CancelToolArgs, type Client, DEFAULT_RUN_REDUCER_LIMITS, type Event, type ExecutionIdentity, type FollowUpToolArgs, type FollowUpToolResult, ForeignPeerError, type HistoryOptions, type HistoryResult, type InboundAsk, type InspectResult, type KillResult, type KillToolArgs, type NormalizedRunEvent, type ObserveRunOptions, type OwnerAttachOptions, OwnerAuthError, OwnerRecordMissingError, type PeerEntry, type PeersResult, type ResultResult, type ResultToolArgs, RpcError, type RunChunkRole, type RunIdentity, type RunInfo, type RunObserver, type RunObserverClient, type RunPermissionOption, type RunPermissionSummary, RunReducer, type RunReducerLimits, type RunSnapshot, type RunSnapshotSeed, type RunToolState, type RunToolSummary, type RunTranscriptEntry, type SpawnMetadata, type SpawnParams, type SpawnResult, type SpawnSelectionInput, type SpawnToolArgs, type SpawnToolResult, StaleOwnerRecordError, type StatusListResult, type StatusResult, type StatusToolArgs, type StatusView, type SubscribeOptions, Supervisor, type SupervisorOptions, THINKING_LEVELS, type ThinkingLevel, type ThinkingOutcome, type WorkflowCompleteParams, type WorkflowCompleteResult, type WorkflowCompleteToolArgs, type WorkflowControllerCreateParams, type WorkflowControllerStatusResult, type WorkflowControllerStatusToolArgs, type WorkflowEventsResult, type WorkflowEventsToolArgs, type WorkflowGateOperation, type WorkflowGateParams, type WorkflowGateResult, type WorkflowGateToolArgs, type WorkflowHeartbeatParams, type WorkflowInspectResult, type WorkflowInspectToolArgs, type WorkflowReadyResult, type WorkflowReadyToolArgs, type WorkflowStatusResult, type WorkflowStatusToolArgs, type WorkflowWaitResult, type WorkflowWaitToolArgs, answerPermissionTool, askTool, attachOwner, brokerReceiveTool, brokerReplyTool, cancelTool, createRunSnapshot, createWorkflowCompleteTool, createWorkflowControllerStatusTool, createWorkflowEventsTool, createWorkflowGateTool, createWorkflowInspectTool, createWorkflowReadyTool, createWorkflowStatusTool, createWorkflowWaitTool, dial, evaluateThinkingPolicy, eventsTool, extractEventText, extractReplyMessage, findAvenorBinary, followUpTool, formatAskFailure, getInstallDir, getVersion, inspectTool, installerBinaryPath, isThinkingLevel, killTool, normalizeAskTimeoutMs, normalizeRunEvent, observeRun, peersTool, reduceRunSnapshot, resultTool, shutdownTool, socketsRoot, spawnTool, statusTool, validateSpawnSelection, validateThinking, validateThinkingForBackend, validateThinkingForBackendResume, workflowCompleteTool, workflowControllerStatusTool, workflowEventsTool, workflowGateTool, workflowInspectTool, workflowReadyTool, workflowStatusTool, workflowWaitTool };
package/dist/index.js CHANGED
@@ -1,10 +1,10 @@
1
- import { n as getVersion, r as installerBinaryPath, t as getInstallDir } from "./install-path-C7QXqbJO.js";
1
+ import { n as getVersion, r as installerBinaryPath, t as getInstallDir } from "./install-path-DKsk-86J.js";
2
2
  import * as net from "node:net";
3
3
  import * as readline from "node:readline";
4
- import { execSync, spawn } from "node:child_process";
5
4
  import * as fs from "node:fs";
6
- import * as os from "node:os";
7
5
  import * as path from "node:path";
6
+ import { execSync, spawn } from "node:child_process";
7
+ import * as os from "node:os";
8
8
  import * as crypto from "node:crypto";
9
9
  import { umpire } from "@umpire/core";
10
10
  import { fromJson } from "@umpire/json";
@@ -35,6 +35,67 @@ var RpcError = class extends Error {
35
35
  this.data = data;
36
36
  }
37
37
  };
38
+ /**
39
+ * A call or connect that produced no result inside the caller's timeout. It is
40
+ * a transport outcome, not a peer's structured rejection.
41
+ */
42
+ var CallTimeoutError = class extends Error {
43
+ code = "ETIMEDOUT";
44
+ constructor(message = "read response: timeout") {
45
+ super(message);
46
+ this.name = "CallTimeoutError";
47
+ }
48
+ };
49
+ const OWNER_AUTH_VERSION = 1;
50
+ const OWNER_UNAUTHENTICATED_CODE = -32011;
51
+ const UNSUPPORTED_OWNER_AUTH_CODE = -32012;
52
+ const OWNER_AUTH_TIMEOUT_CODE = -32013;
53
+ /** A classified owner-authentication rejection, not generic transport failure. */
54
+ var OwnerAuthError = class extends Error {
55
+ kind;
56
+ constructor(kind, options = {}) {
57
+ const label = kind === "unauthenticated" ? "owner authentication rejected" : kind === "unsupported" ? "unsupported owner authentication" : "owner authentication timed out";
58
+ super(options.reason ? `${label}: ${options.reason}` : label);
59
+ this.kind = kind;
60
+ this.name = "OwnerAuthError";
61
+ }
62
+ };
63
+ /**
64
+ * Map authenticate_owner failures onto the three client-side classes. A peer
65
+ * that never answered the handshake call timed the capability out, which is an
66
+ * authentication verdict; a dial that never connected rejects before this
67
+ * mapping runs.
68
+ */
69
+ function classifyOwnerAuthError(err) {
70
+ if (err instanceof CallTimeoutError) return new OwnerAuthError("timeout");
71
+ if (err instanceof RpcError) {
72
+ const reason = ownerAuthReason(err.data);
73
+ switch (err.code) {
74
+ case OWNER_UNAUTHENTICATED_CODE: return new OwnerAuthError("unauthenticated", {
75
+ code: err.code,
76
+ reason
77
+ });
78
+ case UNSUPPORTED_OWNER_AUTH_CODE: return new OwnerAuthError("unsupported", {
79
+ code: err.code,
80
+ reason
81
+ });
82
+ case OWNER_AUTH_TIMEOUT_CODE: return new OwnerAuthError("timeout", {
83
+ code: err.code,
84
+ reason
85
+ });
86
+ default: return new OwnerAuthError("unsupported", {
87
+ code: err.code,
88
+ reason
89
+ });
90
+ }
91
+ }
92
+ return err;
93
+ }
94
+ function ownerAuthReason(data) {
95
+ if (typeof data !== "object" || data === null) return void 0;
96
+ const reason = data.reason;
97
+ return typeof reason === "string" ? reason : void 0;
98
+ }
38
99
  var Client = class {
39
100
  socket;
40
101
  rl;
@@ -134,7 +195,41 @@ var Client = class {
134
195
  else this.dropped++;
135
196
  }
136
197
  async call(method, params, opts) {
137
- if (this.isClosed()) throw new Error("control socket is closed");
198
+ const MAX_TIMER_MS = 2147483647;
199
+ const requested = opts?.timeoutMs !== void 0 ? !Number.isFinite(opts.timeoutMs) || opts.timeoutMs > MAX_TIMER_MS - 1e4 ? MAX_TIMER_MS : Math.max(this.callTimeout, opts.timeoutMs + 1e4) : this.callTimeout;
200
+ const timeout = Number.isFinite(requested) ? Math.min(requested, MAX_TIMER_MS) : MAX_TIMER_MS;
201
+ return this.sendCall(method, params, timeout);
202
+ }
203
+ /**
204
+ * Send a credential-free identity probe. The control server returns the
205
+ * startup token only after it has published its own owner record.
206
+ */
207
+ async identity(timeoutMs) {
208
+ const result = await this.sendCall("identity", void 0, timeoutMs ?? this.callTimeout);
209
+ return typeof result?.token === "string" ? result.token : "";
210
+ }
211
+ /**
212
+ * Present the owner capability on this connection. The caller must already
213
+ * have bound the connection to a trusted peer: an owner attach checks the
214
+ * socket's ownership and identity, and an owned start proves the child with
215
+ * its attempt token.
216
+ */
217
+ async authenticateOwner(capability, timeoutMs) {
218
+ let result;
219
+ try {
220
+ result = await this.sendCall("authenticate_owner", {
221
+ version: OWNER_AUTH_VERSION,
222
+ capability_id: capability.id,
223
+ secret: capability.secret
224
+ }, timeoutMs ?? this.callTimeout);
225
+ } catch (err) {
226
+ throw classifyOwnerAuthError(err);
227
+ }
228
+ const value = result;
229
+ if (!value || value.authenticated !== true || value.version !== OWNER_AUTH_VERSION || value.capability_id !== capability.id) throw new OwnerAuthError("unsupported", { reason: "unexpected-result" });
230
+ }
231
+ sendCall(method, params, timeout) {
232
+ if (this.isClosed()) return Promise.reject(/* @__PURE__ */ new Error("control socket is closed"));
138
233
  this.startReadLoop();
139
234
  const id = ++this.nextID;
140
235
  const req = {
@@ -144,13 +239,10 @@ var Client = class {
144
239
  };
145
240
  if (params !== void 0) req.params = params;
146
241
  const data = JSON.stringify(req) + "\n";
147
- const MAX_TIMER_MS = 2147483647;
148
- const requested = opts?.timeoutMs !== void 0 ? !Number.isFinite(opts.timeoutMs) || opts.timeoutMs > MAX_TIMER_MS - 1e4 ? MAX_TIMER_MS : Math.max(this.callTimeout, opts.timeoutMs + 1e4) : this.callTimeout;
149
- const timeout = Number.isFinite(requested) ? Math.min(requested, MAX_TIMER_MS) : MAX_TIMER_MS;
150
242
  return new Promise((resolve, reject) => {
151
243
  const timer = setTimeout(() => {
152
244
  this.pending.delete(id);
153
- reject(/* @__PURE__ */ new Error("read response: timeout"));
245
+ reject(new CallTimeoutError());
154
246
  }, timeout);
155
247
  this.pending.set(id, {
156
248
  resolve,
@@ -436,19 +528,305 @@ var Client = class {
436
528
  function dial(socketPath, opts) {
437
529
  return new Promise((resolve, reject) => {
438
530
  const socket = net.createConnection({ path: socketPath });
439
- const onConnect = () => {
531
+ let timer;
532
+ const cleanup = () => {
533
+ if (timer !== void 0) clearTimeout(timer);
534
+ socket.removeListener("connect", onConnect);
440
535
  socket.removeListener("error", onError);
536
+ };
537
+ const onConnect = () => {
538
+ cleanup();
441
539
  resolve(new Client(socket, opts));
442
540
  };
443
541
  const onError = (err) => {
444
- socket.removeListener("connect", onConnect);
542
+ cleanup();
445
543
  reject(/* @__PURE__ */ new Error(`dial control socket: ${err.message}`));
446
544
  };
545
+ if (opts?.connectTimeoutMs !== void 0) timer = setTimeout(() => {
546
+ cleanup();
547
+ socket.on("error", () => {});
548
+ socket.destroy();
549
+ reject(new CallTimeoutError(`dial control socket: timed out after ${opts.connectTimeoutMs}ms`));
550
+ }, opts.connectTimeoutMs);
447
551
  socket.on("connect", onConnect);
448
552
  socket.on("error", onError);
449
553
  });
450
554
  }
451
555
  //#endregion
556
+ //#region src/owner-attach.ts
557
+ const OWNER_RECORD_VERSION = 1;
558
+ const OWNER_RECORD_MAX_BYTES = 64 * 1024;
559
+ const OWNER_ATTACH_TIMEOUT_MS = 5e3;
560
+ const OWNER_PROBE_TIMEOUT_MS = 250;
561
+ /** The owner record published beside a control socket is absent. */
562
+ var OwnerRecordMissingError = class extends Error {
563
+ socketPath;
564
+ constructor(socketPath) {
565
+ super(`owner record missing for control socket ${socketPath}`);
566
+ this.socketPath = socketPath;
567
+ this.name = "OwnerRecordMissingError";
568
+ }
569
+ };
570
+ /**
571
+ * The control socket path has no directory entry, so no listener holds that
572
+ * path.
573
+ */
574
+ var OwnerSocketAbsentError = class extends Error {
575
+ socketPath;
576
+ code = "ENOENT";
577
+ constructor(socketPath) {
578
+ super(`control socket ${socketPath} is absent`);
579
+ this.socketPath = socketPath;
580
+ this.name = "OwnerSocketAbsentError";
581
+ }
582
+ };
583
+ /** The record or the socket it names no longer describes the live path. */
584
+ var StaleOwnerRecordError = class extends Error {
585
+ socketPath;
586
+ reason;
587
+ constructor(socketPath, reason) {
588
+ super(`stale owner record for control socket ${socketPath}: ${reason}`);
589
+ this.socketPath = socketPath;
590
+ this.reason = reason;
591
+ this.name = "StaleOwnerRecordError";
592
+ }
593
+ };
594
+ /**
595
+ * The control socket path is not bound to this euid, or its directory is
596
+ * writable by another user. No credential byte may be sent to that peer.
597
+ */
598
+ var ForeignPeerError = class extends Error {
599
+ socketPath;
600
+ euid;
601
+ reason;
602
+ peerUID;
603
+ constructor(socketPath, euid, reason, peerUID) {
604
+ super(`control socket ${socketPath} ${reason}, not uid ${euid}: refusing to send owner credentials`);
605
+ this.socketPath = socketPath;
606
+ this.euid = euid;
607
+ this.reason = reason;
608
+ this.peerUID = peerUID;
609
+ this.name = "ForeignPeerError";
610
+ }
611
+ };
612
+ function ownerRecordPath(socketPath) {
613
+ return `${socketPath}.owner.json`;
614
+ }
615
+ function lstatBig(path) {
616
+ return fs.lstatSync(path, { bigint: true });
617
+ }
618
+ function processEUID() {
619
+ const geteuid = process.geteuid;
620
+ if (!geteuid) throw new Error("control socket ownership requires a POSIX effective uid");
621
+ return geteuid();
622
+ }
623
+ function stale(socketPath, reason) {
624
+ return new StaleOwnerRecordError(socketPath, reason);
625
+ }
626
+ function inspectOwnerRecord(recordPath, socketPath) {
627
+ let info;
628
+ try {
629
+ info = lstatBig(recordPath);
630
+ } catch (err) {
631
+ if (err.code === "ENOENT") throw new OwnerRecordMissingError(socketPath);
632
+ throw stale(socketPath, `stat ${recordPath}: ${errorMessage$1(err)}`);
633
+ }
634
+ if (info.isSymbolicLink()) throw stale(socketPath, `${recordPath} is a symlink`);
635
+ if (!info.isFile()) throw stale(socketPath, `${recordPath} is not a regular file`);
636
+ if (Number(info.mode & 511n) !== 384) throw stale(socketPath, `${recordPath} has mode ${(Number(info.mode) & 511).toString(8)}, want 600`);
637
+ if (Number(info.uid) !== processEUID()) throw stale(socketPath, `${recordPath} is not owned by uid ${processEUID()}`);
638
+ if (info.size > BigInt(OWNER_RECORD_MAX_BYTES)) throw stale(socketPath, `${recordPath} is too large`);
639
+ return info;
640
+ }
641
+ function rejectSymlinkedParent(recordPath, socketPath) {
642
+ const parent = path.dirname(recordPath);
643
+ let info;
644
+ try {
645
+ info = lstatBig(parent);
646
+ } catch (err) {
647
+ throw stale(socketPath, `stat ${parent}: ${errorMessage$1(err)}`);
648
+ }
649
+ if (info.isSymbolicLink()) throw stale(socketPath, `${recordPath} has a symlinked parent ${parent}`);
650
+ }
651
+ function decodeOwnerRecord(body, socketPath) {
652
+ let parsed;
653
+ try {
654
+ parsed = JSON.parse(body);
655
+ } catch (err) {
656
+ throw stale(socketPath, `decode ${ownerRecordPath(socketPath)}: ${errorMessage$1(err)}`);
657
+ }
658
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw stale(socketPath, `${ownerRecordPath(socketPath)} is not an object`);
659
+ const value = parsed;
660
+ const expected = [
661
+ "version",
662
+ "socket",
663
+ "socket_identity",
664
+ "capability_id",
665
+ "secret",
666
+ "owner_pid"
667
+ ];
668
+ for (const key of Object.keys(value)) if (!expected.includes(key)) throw stale(socketPath, `${ownerRecordPath(socketPath)} has unknown field ${key}`);
669
+ const identity = value.socket_identity;
670
+ if (typeof value.version !== "number" || typeof value.socket !== "string" || typeof value.capability_id !== "string" || typeof value.secret !== "string" || typeof value.owner_pid !== "number" || typeof identity !== "object" || identity === null || typeof identity.dev !== "string" || typeof identity.ino !== "string") throw stale(socketPath, `${ownerRecordPath(socketPath)} is malformed`);
671
+ if (value.version !== OWNER_RECORD_VERSION) throw stale(socketPath, `${ownerRecordPath(socketPath)} has version ${value.version}`);
672
+ if (value.socket !== socketPath) throw stale(socketPath, `${ownerRecordPath(socketPath)} names socket ${value.socket}`);
673
+ return {
674
+ version: value.version,
675
+ socket: value.socket,
676
+ socket_identity: {
677
+ dev: identity.dev,
678
+ ino: identity.ino
679
+ },
680
+ capability_id: value.capability_id,
681
+ secret: value.secret,
682
+ owner_pid: value.owner_pid
683
+ };
684
+ }
685
+ /** Load and validate the owner record published beside socketPath. */
686
+ function loadOwnerRecord(socketPath) {
687
+ const recordPath = ownerRecordPath(socketPath);
688
+ rejectSymlinkedParent(recordPath, socketPath);
689
+ const inspected = inspectOwnerRecord(recordPath, socketPath);
690
+ const fd = fs.openSync(recordPath, "r");
691
+ try {
692
+ const opened = fs.fstatSync(fd, { bigint: true });
693
+ if (opened.dev !== inspected.dev || opened.ino !== inspected.ino) throw stale(socketPath, `${recordPath} changed while reading`);
694
+ return decodeOwnerRecord(fs.readFileSync(fd, "utf8"), socketPath);
695
+ } finally {
696
+ fs.closeSync(fd);
697
+ }
698
+ }
699
+ function socketFileIdentity(socketPath) {
700
+ let info;
701
+ try {
702
+ info = lstatBig(socketPath);
703
+ } catch (err) {
704
+ if (err.code === "ENOENT") throw new OwnerSocketAbsentError(socketPath);
705
+ throw stale(socketPath, `stat ${socketPath}: ${errorMessage$1(err)}`);
706
+ }
707
+ if (!info.isSocket()) throw stale(socketPath, `${socketPath} is not a socket`);
708
+ return info;
709
+ }
710
+ /** Require identity to be the socket the record names. */
711
+ function requireRecordedSocket(socketPath, record, identity) {
712
+ if (identity.dev !== record.socket_identity.dev || identity.ino !== record.socket_identity.ino) throw stale(socketPath, `${socketPath} does not match the published record`);
713
+ return identity;
714
+ }
715
+ /**
716
+ * Require the live socket to be the one the record names. Returns the socket's
717
+ * device and inode as decimal strings.
718
+ */
719
+ function validateOwnerRecordSocket(socketPath, record) {
720
+ const live = socketFileIdentity(socketPath);
721
+ return requireRecordedSocket(socketPath, record, {
722
+ dev: String(live.dev),
723
+ ino: String(live.ino)
724
+ });
725
+ }
726
+ /**
727
+ * Bind the socket path to this euid before any credential byte: the socket file
728
+ * must be a socket this euid owns, in a directory this euid or root owns that is
729
+ * not group- or other-writable unless it is sticky.
730
+ */
731
+ function bindOwnerSocket(socketPath, euid = processEUID()) {
732
+ const info = socketFileIdentity(socketPath);
733
+ if (Number(info.uid) !== euid) throw new ForeignPeerError(socketPath, euid, `is owned by uid ${info.uid}`, Number(info.uid));
734
+ const parent = path.dirname(socketPath);
735
+ let parentInfo;
736
+ try {
737
+ parentInfo = lstatBig(parent);
738
+ } catch (err) {
739
+ throw new ForeignPeerError(socketPath, euid, `has an unreadable directory ${parent}: ${errorMessage$1(err)}`);
740
+ }
741
+ if (parentInfo.isSymbolicLink()) throw new ForeignPeerError(socketPath, euid, `is reached through the symlink ${parent}`);
742
+ const parentUID = Number(parentInfo.uid);
743
+ if (parentUID !== euid && parentUID !== 0) throw new ForeignPeerError(socketPath, euid, `lives in ${parent}, owned by uid ${parentUID}`);
744
+ const writable = (parentInfo.mode & 18n) !== 0n;
745
+ const sticky = (parentInfo.mode & 512n) !== 0n;
746
+ if (writable && !sticky) throw new ForeignPeerError(socketPath, euid, `lives in ${parent}, which is group/other-writable without the sticky bit`);
747
+ return {
748
+ dev: String(info.dev),
749
+ ino: String(info.ino)
750
+ };
751
+ }
752
+ /** Probe a control socket path without presenting any credential. */
753
+ function probeControlSocket(socketPath, timeoutMs = OWNER_PROBE_TIMEOUT_MS) {
754
+ return new Promise((resolve) => {
755
+ const socket = net.createConnection({ path: socketPath });
756
+ let done = false;
757
+ const finish = (state) => {
758
+ if (done) return;
759
+ done = true;
760
+ clearTimeout(timer);
761
+ socket.destroy();
762
+ resolve(state);
763
+ };
764
+ const timer = setTimeout(() => finish("uncertain"), timeoutMs);
765
+ socket.once("connect", () => finish("live"));
766
+ socket.once("error", (err) => {
767
+ finish(err.code === "ECONNREFUSED" || err.code === "ENOENT" ? "dead" : "uncertain");
768
+ });
769
+ });
770
+ }
771
+ function connectOwnerSocket(socketPath, timeoutMs) {
772
+ return new Promise((resolve, reject) => {
773
+ const socket = net.createConnection({ path: socketPath });
774
+ const timer = setTimeout(() => {
775
+ socket.destroy();
776
+ reject(new CallTimeoutError(`dial ${socketPath}: timed out`));
777
+ }, timeoutMs);
778
+ socket.once("connect", () => {
779
+ clearTimeout(timer);
780
+ resolve(socket);
781
+ });
782
+ socket.once("error", (err) => {
783
+ clearTimeout(timer);
784
+ reject(err);
785
+ });
786
+ });
787
+ }
788
+ function remainingMs(deadline) {
789
+ return Math.max(1, deadline - Date.now());
790
+ }
791
+ /**
792
+ * Load the owner record, bind the socket path to this euid and require it to be
793
+ * the record's socket, dial, re-check the socket identity and authenticate. The
794
+ * whole attach is bounded by the timeout.
795
+ */
796
+ async function attachOwner(socketPath, options = {}) {
797
+ return attachOwnerWithSeams(socketPath, options);
798
+ }
799
+ /** The attach itself, with the seams the package's tests inject. */
800
+ async function attachOwnerWithSeams(socketPath, options = {}) {
801
+ const deadline = Date.now() + (options.timeoutMs ?? OWNER_ATTACH_TIMEOUT_MS);
802
+ const euid = options.euid ?? processEUID();
803
+ const record = loadOwnerRecord(socketPath);
804
+ requireRecordedSocket(socketPath, record, bindOwnerSocket(socketPath, euid));
805
+ options.beforeDial?.();
806
+ const socket = await connectOwnerSocket(socketPath, remainingMs(deadline));
807
+ let client;
808
+ try {
809
+ validateOwnerRecordSocket(socketPath, record);
810
+ client = new Client(socket);
811
+ } catch (err) {
812
+ socket.destroy();
813
+ throw err;
814
+ }
815
+ try {
816
+ await client.authenticateOwner({
817
+ id: record.capability_id,
818
+ secret: record.secret
819
+ }, remainingMs(deadline));
820
+ } catch (err) {
821
+ client.close();
822
+ throw err;
823
+ }
824
+ return client;
825
+ }
826
+ function errorMessage$1(err) {
827
+ return err instanceof Error ? err.message : String(err);
828
+ }
829
+ //#endregion
452
830
  //#region src/value-fields.ts
453
831
  function asRecord(value) {
454
832
  if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
@@ -1331,6 +1709,88 @@ function ensureRunPaths(runId) {
1331
1709
  }
1332
1710
  //#endregion
1333
1711
  //#region src/supervisor.ts
1712
+ const STARTUP_TIMEOUT_MS = 1e4;
1713
+ const STARTUP_PROBE_CALL_TIMEOUT_MS = 1e3;
1714
+ const CHILD_REAP_GRACE_MS = 5e3;
1715
+ function isOwnerRejection(err) {
1716
+ return err instanceof OwnerAuthError || err instanceof ForeignPeerError || err instanceof OwnerRecordMissingError || err instanceof StaleOwnerRecordError;
1717
+ }
1718
+ function classifyAttachError(err) {
1719
+ return isOwnerRejection(err) ? "auth-rejected" : "uncertain";
1720
+ }
1721
+ function errorMessage(err) {
1722
+ return err instanceof Error ? err.message : String(err);
1723
+ }
1724
+ function sleep$1(ms) {
1725
+ return new Promise((resolve) => setTimeout(resolve, ms));
1726
+ }
1727
+ /** Wait until the Go server has removed its socket, bounded by the timeout. */
1728
+ async function waitForSocketRemoval(socketPath, timeoutMs = CHILD_REAP_GRACE_MS) {
1729
+ const deadline = Date.now() + timeoutMs;
1730
+ while (Date.now() < deadline) {
1731
+ try {
1732
+ fs.lstatSync(socketPath);
1733
+ } catch {
1734
+ return;
1735
+ }
1736
+ await sleep$1(50);
1737
+ }
1738
+ }
1739
+ /** Wait for the child to exit on its own, bounded by the timeout. */
1740
+ async function waitForChildExit(child, timeoutMs) {
1741
+ if (child.exitCode !== null || child.signalCode !== null) return true;
1742
+ return new Promise((resolve) => {
1743
+ const onExit = () => {
1744
+ clearTimeout(timer);
1745
+ resolve(true);
1746
+ };
1747
+ const timer = setTimeout(() => {
1748
+ child.removeListener("exit", onExit);
1749
+ resolve(false);
1750
+ }, timeoutMs);
1751
+ child.once("exit", onExit);
1752
+ });
1753
+ }
1754
+ /** Terminate the captured child and wait for it to exit. */
1755
+ async function reapChild(child) {
1756
+ if (child.exitCode !== null || child.signalCode !== null) return;
1757
+ const exited = new Promise((resolve) => child.once("exit", () => resolve()));
1758
+ try {
1759
+ child.kill("SIGTERM");
1760
+ } catch {
1761
+ return;
1762
+ }
1763
+ const escalate = setTimeout(() => {
1764
+ try {
1765
+ child.kill("SIGKILL");
1766
+ } catch {}
1767
+ }, CHILD_REAP_GRACE_MS);
1768
+ await exited;
1769
+ clearTimeout(escalate);
1770
+ }
1771
+ /**
1772
+ * Classify an existing control socket path without presenting a credential.
1773
+ * A bound unix listener never refuses, so ECONNREFUSED (or an absent path)
1774
+ * proves no listener holds it; anything else is uncertain.
1775
+ */
1776
+ async function attachExistingSupervisor(socketPath) {
1777
+ const probe = await probeControlSocket(socketPath);
1778
+ if (probe !== "live") return {
1779
+ state: probe,
1780
+ error: probe === "uncertain" ? /* @__PURE__ */ new Error(`control socket probe for ${socketPath} was inconclusive`) : void 0
1781
+ };
1782
+ try {
1783
+ return {
1784
+ client: await attachOwner(socketPath),
1785
+ state: "live"
1786
+ };
1787
+ } catch (err) {
1788
+ return {
1789
+ state: classifyAttachError(err),
1790
+ error: err instanceof Error ? err : new Error(String(err))
1791
+ };
1792
+ }
1793
+ }
1334
1794
  function retainLiveIdentity(runInfo, liveStatus) {
1335
1795
  const value = (candidate) => typeof candidate === "string" && candidate.length > 0 ? candidate : void 0;
1336
1796
  const liveIdentity = (effectiveKey, directKey) => {
@@ -1458,26 +1918,20 @@ var Supervisor = class Supervisor {
1458
1918
  await fs.promises.access(socketPath, fs.constants.R_OK | fs.constants.W_OK);
1459
1919
  socketExists = true;
1460
1920
  } catch {}
1461
- if (socketExists) try {
1462
- const probeClient = await dial(socketPath, { callTimeoutMs: 2e3 });
1463
- try {
1464
- await probeClient.status();
1465
- probeClient.close();
1466
- const client = await dial(socketPath);
1921
+ if (socketExists) {
1922
+ const existing = await attachExistingSupervisor(socketPath);
1923
+ if (existing.client) {
1467
1924
  const sup = new Supervisor(binaryPath, socketPath, opts);
1468
- sup.client = client;
1925
+ sup.client = existing.client;
1926
+ const previous = Supervisor.instance;
1927
+ if (previous && previous.socketPath === socketPath && previous.childProcess) {
1928
+ sup.childProcess = previous.childProcess;
1929
+ previous.childProcess = null;
1930
+ }
1469
1931
  Supervisor.instance = sup;
1470
1932
  return sup;
1471
- } catch {
1472
- probeClient.close();
1473
- try {
1474
- await fs.promises.unlink(socketPath);
1475
- } catch {}
1476
1933
  }
1477
- } catch {
1478
- try {
1479
- await fs.promises.unlink(socketPath);
1480
- } catch {}
1934
+ if (existing.state !== "dead") throw existing.error ?? /* @__PURE__ */ new Error(`control socket ${socketPath} is not attachable`);
1481
1935
  }
1482
1936
  const sup = new Supervisor(binaryPath, socketPath, opts);
1483
1937
  await sup.startProcess();
@@ -1486,18 +1940,48 @@ var Supervisor = class Supervisor {
1486
1940
  }
1487
1941
  startProcess() {
1488
1942
  return new Promise((resolve, reject) => {
1489
- let resolved = false;
1490
- this.childProcess = spawn(this.binaryPath, [
1943
+ let settled = false;
1944
+ let ignoreChildExit = false;
1945
+ const attemptToken = crypto.randomBytes(32).toString("hex");
1946
+ const deadline = Date.now() + STARTUP_TIMEOUT_MS;
1947
+ const child = spawn(this.binaryPath, [
1491
1948
  "stable",
1492
1949
  "--control-socket",
1493
1950
  this.socketPath,
1494
1951
  "--idle-timeout",
1495
1952
  "30m"
1496
- ], { detached: false });
1497
- this.childProcess.on("exit", (code, signal) => {
1498
- if (!resolved) {
1499
- resolved = true;
1500
- reject(/* @__PURE__ */ new Error(`avenor exited with code ${code}${signal ? " signal " + signal : ""} during startup`));
1953
+ ], {
1954
+ detached: false,
1955
+ env: {
1956
+ ...process.env,
1957
+ AVENOR_CONTROL_IDENTITY_TOKEN: attemptToken
1958
+ }
1959
+ });
1960
+ this.childProcess = child;
1961
+ const settleFailure = (err) => {
1962
+ if (settled) return;
1963
+ settled = true;
1964
+ ignoreChildExit = true;
1965
+ reapChild(child).finally(() => {
1966
+ if (this.childProcess === child) this.childProcess = null;
1967
+ reject(err);
1968
+ });
1969
+ };
1970
+ child.on("exit", (code, signal) => {
1971
+ if (ignoreChildExit) return;
1972
+ if (!settled) {
1973
+ const exitError = /* @__PURE__ */ new Error(`avenor exited with code ${code}${signal ? " signal " + signal : ""} during startup`);
1974
+ settled = true;
1975
+ ignoreChildExit = true;
1976
+ (async () => {
1977
+ if (this.childProcess === child) this.childProcess = null;
1978
+ try {
1979
+ this.client = await attachOwner(this.socketPath);
1980
+ resolve();
1981
+ } catch {
1982
+ reject(exitError);
1983
+ }
1984
+ })();
1501
1985
  return;
1502
1986
  }
1503
1987
  this.crashed = true;
@@ -1509,29 +1993,85 @@ var Supervisor = class Supervisor {
1509
1993
  this.client = null;
1510
1994
  }
1511
1995
  });
1512
- this.childProcess.on("error", (err) => {
1513
- if (!resolved) {
1514
- resolved = true;
1515
- reject(/* @__PURE__ */ new Error(`failed to start avenor: ${err.message}`));
1516
- }
1996
+ child.on("error", (err) => {
1997
+ settleFailure(/* @__PURE__ */ new Error(`failed to start avenor: ${err.message}`));
1517
1998
  });
1518
- const timeout = setTimeout(() => {
1519
- if (!resolved) {
1520
- resolved = true;
1521
- reject(/* @__PURE__ */ new Error("timed out waiting for avenor socket"));
1999
+ const attachOwned = async () => {
2000
+ while (!settled) {
2001
+ if (Date.now() >= deadline) {
2002
+ settleFailure(/* @__PURE__ */ new Error("timed out waiting for avenor socket"));
2003
+ return;
2004
+ }
2005
+ if (child.exitCode !== null || child.signalCode !== null) return;
2006
+ let client;
2007
+ try {
2008
+ client = await dial(this.socketPath, {
2009
+ callTimeoutMs: this.callTimeoutMs,
2010
+ connectTimeoutMs: Math.max(1, deadline - Date.now())
2011
+ });
2012
+ } catch {
2013
+ await sleep$1(100);
2014
+ continue;
2015
+ }
2016
+ if (settled) {
2017
+ client.close();
2018
+ return;
2019
+ }
2020
+ let token;
2021
+ try {
2022
+ token = await client.identity(STARTUP_PROBE_CALL_TIMEOUT_MS);
2023
+ } catch {
2024
+ client.close();
2025
+ if (settled) return;
2026
+ await sleep$1(100);
2027
+ continue;
2028
+ }
2029
+ if (settled) {
2030
+ client.close();
2031
+ return;
2032
+ }
2033
+ if (token !== attemptToken) {
2034
+ client.close();
2035
+ settled = true;
2036
+ ignoreChildExit = true;
2037
+ await reapChild(child);
2038
+ try {
2039
+ this.client = await attachOwner(this.socketPath);
2040
+ resolve();
2041
+ } catch (err) {
2042
+ reject(/* @__PURE__ */ new Error(`supervisor exited during startup: ${errorMessage(err)}`));
2043
+ }
2044
+ return;
2045
+ }
2046
+ try {
2047
+ const record = loadOwnerRecord(this.socketPath);
2048
+ validateOwnerRecordSocket(this.socketPath, record);
2049
+ if (record.owner_pid !== child.pid) throw new StaleOwnerRecordError(this.socketPath, `record names pid ${record.owner_pid}, not this child ${child.pid}`);
2050
+ await client.authenticateOwner({
2051
+ id: record.capability_id,
2052
+ secret: record.secret
2053
+ }, STARTUP_PROBE_CALL_TIMEOUT_MS * 2);
2054
+ this.client = client;
2055
+ settled = true;
2056
+ resolve();
2057
+ return;
2058
+ } catch (err) {
2059
+ client.close();
2060
+ if (settled) return;
2061
+ if (err instanceof OwnerRecordMissingError || err instanceof StaleOwnerRecordError) {
2062
+ await sleep$1(100);
2063
+ continue;
2064
+ }
2065
+ if (err instanceof OwnerSocketAbsentError) {
2066
+ await sleep$1(100);
2067
+ continue;
2068
+ }
2069
+ settleFailure(err instanceof Error ? err : new Error(String(err)));
2070
+ return;
2071
+ }
1522
2072
  }
1523
- }, 1e4);
1524
- const retryDial = async () => {
1525
- while (!resolved) try {
1526
- await new Promise((r) => setTimeout(r, 100));
1527
- this.client = await dial(this.socketPath, { callTimeoutMs: this.callTimeoutMs });
1528
- clearTimeout(timeout);
1529
- resolved = true;
1530
- resolve();
1531
- return;
1532
- } catch {}
1533
2073
  };
1534
- retryDial();
2074
+ attachOwned();
1535
2075
  });
1536
2076
  }
1537
2077
  get supervisorId() {
@@ -1654,22 +2194,25 @@ var Supervisor = class Supervisor {
1654
2194
  return runInfo;
1655
2195
  }
1656
2196
  async close(options = {}) {
1657
- if (this.client && !options.skipShutdown) try {
1658
- await this.client.shutdown("graceful");
1659
- } catch {}
2197
+ let shutdownSent = false;
2198
+ if (this.client && !options.skipShutdown) {
2199
+ shutdownSent = true;
2200
+ try {
2201
+ await this.client.shutdown("graceful");
2202
+ } catch {}
2203
+ }
1660
2204
  if (this.childProcess) {
1661
- this.childProcess.kill();
2205
+ const child = this.childProcess;
1662
2206
  this.childProcess = null;
1663
- }
2207
+ if (!shutdownSent || !await waitForChildExit(child, CHILD_REAP_GRACE_MS)) await reapChild(child);
2208
+ await waitForSocketRemoval(this.socketPath);
2209
+ } else if (shutdownSent) await waitForSocketRemoval(this.socketPath);
1664
2210
  if (this.client) {
1665
2211
  try {
1666
2212
  this.client.close();
1667
2213
  } catch {}
1668
2214
  this.client = null;
1669
2215
  }
1670
- try {
1671
- await fs.promises.unlink(this.socketPath);
1672
- } catch {}
1673
2216
  this.crashed = false;
1674
2217
  this.crashCode = null;
1675
2218
  this.runs.clear();
@@ -1705,7 +2248,7 @@ function validateObservedAt(observedAt) {
1705
2248
  }
1706
2249
  //#endregion
1707
2250
  //#region src/tools/get-supervisor-client.ts
1708
- async function getSupervisorClient(supervisorId) {
2251
+ async function getSupervisorClient(supervisorId, options = {}) {
1709
2252
  if (supervisorId === void 0) {
1710
2253
  const sup = await Supervisor.get();
1711
2254
  return {
@@ -1724,7 +2267,7 @@ async function getSupervisorClient(supervisorId) {
1724
2267
  supervisorId: id
1725
2268
  };
1726
2269
  return {
1727
- client: await dial(id),
2270
+ client: options.owner ? await attachOwner(id) : await dial(id),
1728
2271
  isSingleton: false,
1729
2272
  sup: null,
1730
2273
  supervisorId: id
@@ -1870,7 +2413,7 @@ async function executeSpawnTool(args, getSupervisorClient) {
1870
2413
  if (args.agent) baseParams.agent = args.agent;
1871
2414
  if (args.thinking !== void 0) baseParams.thinking = args.thinking;
1872
2415
  if (args.supervisorId) {
1873
- const { client, isSingleton, sup, supervisorId } = await getSupervisorClient(args.supervisorId);
2416
+ const { client, isSingleton, sup, supervisorId } = await getSupervisorClient(args.supervisorId, { owner: true });
1874
2417
  if (isSingleton && sup) {
1875
2418
  const runInfo = await sup.spawn(baseParams, runId);
1876
2419
  return {
@@ -2814,7 +3357,7 @@ async function answerPermissionTool(args) {
2814
3357
  let requestId = args.requestId;
2815
3358
  let supervisorId;
2816
3359
  let isSingleton = false;
2817
- if (args.supervisorId) ({client, isSingleton, sup, supervisorId} = await getSupervisorClient(args.supervisorId));
3360
+ if (args.supervisorId) ({client, isSingleton, sup, supervisorId} = await getSupervisorClient(args.supervisorId, { owner: true }));
2818
3361
  else {
2819
3362
  sup = await Supervisor.get();
2820
3363
  client = sup.getClient();
@@ -2881,7 +3424,7 @@ function rejectNotResumableStatus(sentinel) {
2881
3424
  }
2882
3425
  async function executeFollowUpTool(args, getSupervisorClient) {
2883
3426
  if (args.supervisorId) {
2884
- const { client, isSingleton, sup, supervisorId } = await getSupervisorClient(args.supervisorId);
3427
+ const { client, isSingleton, sup, supervisorId } = await getSupervisorClient(args.supervisorId, { owner: true });
2885
3428
  try {
2886
3429
  validateRunId(args.runId);
2887
3430
  let runInfo;
@@ -3045,7 +3588,7 @@ async function shutdownTool(args) {
3045
3588
  await Promise.all(cleanups);
3046
3589
  await sup.close({ skipShutdown: true });
3047
3590
  } else {
3048
- const client = await dial(supervisorId);
3591
+ const client = await attachOwner(supervisorId);
3049
3592
  try {
3050
3593
  await client.shutdown(args.force ? "force" : "graceful");
3051
3594
  } finally {
@@ -3104,11 +3647,26 @@ function extractReplyMessage(result) {
3104
3647
  }
3105
3648
  return JSON.stringify(result);
3106
3649
  }
3650
+ /**
3651
+ * Build the failure message every ask path reports: the broker message id plus
3652
+ * whether the ask edge is still pending (withdrawable via avenor_cancel) or
3653
+ * already gone.
3654
+ */
3655
+ function formatAskFailure(messageId, pending, detail) {
3656
+ return pending ? `ask failed (message_id: ${messageId} — pass to avenor_cancel to withdraw): ${detail}` : `ask failed (message_id: ${messageId} — the pending ask was already withdrawn): ${detail}`;
3657
+ }
3658
+ /**
3659
+ * Reduce a caller-supplied ask timeout to what the broker accepts: a positive
3660
+ * finite number of milliseconds, or undefined to keep the broker's own wait.
3661
+ */
3662
+ function normalizeAskTimeoutMs(timeoutMs) {
3663
+ return timeoutMs !== void 0 && Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : void 0;
3664
+ }
3107
3665
  async function executeAskTool(args, getSupervisorClient) {
3108
- const { client, isSingleton, sup, supervisorId } = await getSupervisorClient(args.supervisorId);
3666
+ const { client, isSingleton, sup, supervisorId } = await getSupervisorClient(args.supervisorId, { owner: true });
3109
3667
  try {
3110
3668
  const target = (isSingleton && sup ? findLocalRunByReference(sup, args.toRunId) : findExternalRun(supervisorId, args.toRunId))?.runtimeId ?? args.toRunId;
3111
- const timeoutMs = args.timeoutMs !== void 0 && Number.isFinite(args.timeoutMs) && args.timeoutMs > 0 ? args.timeoutMs : void 0;
3669
+ const timeoutMs = normalizeAskTimeoutMs(args.timeoutMs);
3112
3670
  try {
3113
3671
  const result = await client.brokerAsk(target, args.message, void 0, timeoutMs);
3114
3672
  return {
@@ -3119,10 +3677,7 @@ async function executeAskTool(args, getSupervisorClient) {
3119
3677
  if (error instanceof RpcError && error.data && typeof error.data === "object") {
3120
3678
  const data = error.data;
3121
3679
  const messageId = data.message_id;
3122
- if (typeof messageId === "string" && messageId) {
3123
- if (data.pending === false) throw new Error(`ask failed (message_id: ${messageId} — the pending ask was already withdrawn): ${error.message}`);
3124
- throw new Error(`ask failed (message_id: ${messageId} — pass to avenor_cancel to withdraw): ${error.message}`);
3125
- }
3680
+ if (typeof messageId === "string" && messageId) throw new Error(formatAskFailure(messageId, data.pending !== false, error.message));
3126
3681
  }
3127
3682
  throw error;
3128
3683
  }
@@ -3151,7 +3706,7 @@ const peersTool = createPeersTool(getSupervisorClient);
3151
3706
  //#endregion
3152
3707
  //#region src/tools/cancel.ts
3153
3708
  async function executeCancelTool(args, getSupervisorClient) {
3154
- const { client, isSingleton } = await getSupervisorClient(args.supervisorId);
3709
+ const { client, isSingleton } = await getSupervisorClient(args.supervisorId, { owner: true });
3155
3710
  try {
3156
3711
  await client.brokerCancel(args.messageId);
3157
3712
  return { cancelled: true };
@@ -3183,7 +3738,7 @@ async function resolveRuntimeId(client, reference) {
3183
3738
  }
3184
3739
  async function executeKillTool(args, getSupervisorClient) {
3185
3740
  validateRunId(args.runId);
3186
- const { client, isSingleton, sup, supervisorId } = await getSupervisorClient(args.supervisorId);
3741
+ const { client, isSingleton, sup, supervisorId } = await getSupervisorClient(args.supervisorId, { owner: true });
3187
3742
  try {
3188
3743
  const runtimeId = (isSingleton && sup ? findLocalRunByReference(sup, args.runId) : findExternalRun(supervisorId, args.runId))?.runtimeId ?? await resolveRuntimeId(client, args.runId);
3189
3744
  await client.cancel(runtimeId);
@@ -3217,7 +3772,7 @@ const brokerReceiveTool = createBrokerReceiveTool(getSupervisorClient);
3217
3772
  //#endregion
3218
3773
  //#region src/tools/broker-reply.ts
3219
3774
  async function executeBrokerReply(args, getSupervisorClient) {
3220
- const { client, isSingleton } = await getSupervisorClient(args.supervisorId);
3775
+ const { client, isSingleton } = await getSupervisorClient(args.supervisorId, { owner: true });
3221
3776
  try {
3222
3777
  await client.brokerReply(args.toRunId, args.replyTo, args.message);
3223
3778
  return { ok: true };
@@ -3342,4 +3897,4 @@ function createWorkflowReadyTool(getSupervisorClient) {
3342
3897
  }
3343
3898
  const workflowReadyTool = createWorkflowReadyTool(getSupervisorClient);
3344
3899
  //#endregion
3345
- export { DEFAULT_RUN_REDUCER_LIMITS, RpcError, RunReducer, Supervisor, THINKING_LEVELS, answerPermissionTool, askTool, brokerReceiveTool, brokerReplyTool, cancelTool, createRunSnapshot, createWorkflowCompleteTool, createWorkflowControllerStatusTool, createWorkflowEventsTool, createWorkflowGateTool, createWorkflowInspectTool, createWorkflowReadyTool, createWorkflowStatusTool, createWorkflowWaitTool, dial, evaluateThinkingPolicy, eventsTool, extractEventText, findAvenorBinary, followUpTool, getInstallDir, getVersion, inspectTool, installerBinaryPath, isThinkingLevel, killTool, normalizeRunEvent, observeRun, peersTool, reduceRunSnapshot, resultTool, shutdownTool, socketsRoot, spawnTool, statusTool, validateSpawnSelection, validateThinking, validateThinkingForBackend, validateThinkingForBackendResume, workflowCompleteTool, workflowControllerStatusTool, workflowEventsTool, workflowGateTool, workflowInspectTool, workflowReadyTool, workflowStatusTool, workflowWaitTool };
3900
+ export { DEFAULT_RUN_REDUCER_LIMITS, ForeignPeerError, OwnerAuthError, OwnerRecordMissingError, RpcError, RunReducer, StaleOwnerRecordError, Supervisor, THINKING_LEVELS, answerPermissionTool, askTool, attachOwner, brokerReceiveTool, brokerReplyTool, cancelTool, createRunSnapshot, createWorkflowCompleteTool, createWorkflowControllerStatusTool, createWorkflowEventsTool, createWorkflowGateTool, createWorkflowInspectTool, createWorkflowReadyTool, createWorkflowStatusTool, createWorkflowWaitTool, dial, evaluateThinkingPolicy, eventsTool, extractEventText, extractReplyMessage, findAvenorBinary, followUpTool, formatAskFailure, getInstallDir, getVersion, inspectTool, installerBinaryPath, isThinkingLevel, killTool, normalizeAskTimeoutMs, normalizeRunEvent, observeRun, peersTool, reduceRunSnapshot, resultTool, shutdownTool, socketsRoot, spawnTool, statusTool, validateSpawnSelection, validateThinking, validateThinkingForBackend, validateThinkingForBackendResume, workflowCompleteTool, workflowControllerStatusTool, workflowEventsTool, workflowGateTool, workflowInspectTool, workflowReadyTool, workflowStatusTool, workflowWaitTool };
@@ -1,6 +1,6 @@
1
- import { n as getVersion, r as installerBinaryPath, t as getInstallDir } from "./install-path-C7QXqbJO.js";
2
- import { execSync } from "node:child_process";
1
+ import { n as getVersion, r as installerBinaryPath, t as getInstallDir } from "./install-path-DKsk-86J.js";
3
2
  import * as fs from "node:fs";
3
+ import { execSync } from "node:child_process";
4
4
  import * as crypto from "node:crypto";
5
5
  //#region src/postinstall.ts
6
6
  function platformMapping(platform, arch) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dougbots/avenor-core",
3
- "version": "0.14.0",
3
+ "version": "0.15.0",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -1,6 +1,6 @@
1
1
  import * as fs from "node:fs";
2
- import * as os from "node:os";
3
2
  import * as path from "node:path";
3
+ import * as os from "node:os";
4
4
  //#region src/install-path.ts
5
5
  const SEMVER_RE = /^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.-]+)?$/;
6
6
  function getVersion(packageJsonPath) {