@dougbots/avenor-core 0.13.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
|
-
|
|
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-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
1462
|
-
const
|
|
1463
|
-
|
|
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
|
-
|
|
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
|
|
1490
|
-
|
|
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
|
-
], {
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
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
|
-
|
|
1513
|
-
|
|
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
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
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
|
|
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
|
|
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 =
|
|
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 };
|
package/dist/postinstall.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { n as getVersion, r as installerBinaryPath, t as getInstallDir } from "./install-path-
|
|
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
|
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) {
|