@byok-sdk/client 0.14.0 → 0.16.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/README.md +15 -0
- package/dist/adapters/codex/codex-adapter.d.ts +2 -0
- package/dist/adapters/codex/process-runner.d.ts +16 -17
- package/dist/adapters/detect-outcome.d.ts +18 -0
- package/dist/adapters/index.d.ts +1 -1
- package/dist/adapters/index.js +189 -50
- package/dist/adapters/index.js.map +1 -1
- package/dist/bin/byok-agent-memory-mcp.js +29 -0
- package/dist/bin/byok-agent-memory-mcp.js.map +1 -1
- package/dist/bin/byok-agent-message-mcp.js +29 -0
- package/dist/bin/byok-agent-message-mcp.js.map +1 -1
- package/dist/bin/byok-agent-team-mcp.js +29 -0
- package/dist/bin/byok-agent-team-mcp.js.map +1 -1
- package/dist/bin/byok-agent.js +3026 -2122
- package/dist/bin/byok-agent.js.map +1 -1
- package/dist/bin/byok-approval-mcp.js +29 -0
- package/dist/bin/byok-approval-mcp.js.map +1 -1
- package/dist/bin/byok-mcp-env.d.ts +2 -0
- package/dist/bin/byok-mcp-env.js +36 -0
- package/dist/bin/byok-mcp-env.js.map +1 -0
- package/dist/bin/commands/doctor.d.ts +3 -0
- package/dist/bin/mcp-env-launcher.d.ts +5 -0
- package/dist/bin/runtime-probe.d.ts +6 -7
- package/dist/daemon/admission-wait.d.ts +2 -0
- package/dist/daemon/agent-egress-spool.d.ts +1 -0
- package/dist/daemon/agent-message-outbox.d.ts +17 -3
- package/dist/daemon/artifact-read.d.ts +7 -0
- package/dist/daemon/connection-manager.d.ts +18 -77
- package/dist/daemon/create-daemon.d.ts +11 -1
- package/dist/daemon/runtime-start.d.ts +3 -0
- package/dist/daemon/store.d.ts +2 -0
- package/dist/daemon/task-runner.d.ts +23 -0
- package/dist/daemon/terminal-commit-queue.d.ts +21 -0
- package/dist/daemon/terminal-identity.d.ts +6 -0
- package/dist/diagnostics/device-doctor.d.ts +46 -0
- package/dist/diagnostics/diagnostics.d.ts +5 -90
- package/dist/diagnostics/operator-actions.d.ts +63 -0
- package/dist/diagnostics/types.d.ts +92 -0
- package/dist/index.d.ts +5 -1
- package/dist/index.js +2575 -470
- package/dist/index.js.map +1 -1
- package/dist/runtime-detection.d.ts +3 -0
- package/dist/runtime-failure.d.ts +6 -0
- package/dist/sdk-reserved-helper-host.d.ts +1 -1
- package/dist/types.d.ts +20 -10
- package/dist/util/durable-jsonl.d.ts +12 -0
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import { randomUUID, createHash, randomBytes, createPrivateKey, generateKeyPairSync, sign, timingSafeEqual, createHmac } from 'crypto';
|
|
2
|
-
import { promises, mkdirSync, constants, existsSync, linkSync, fstatSync, lstatSync, unlinkSync, renameSync, writeFileSync, chmodSync, statSync, readdirSync, readFileSync, realpathSync } from 'fs';
|
|
2
|
+
import { promises, mkdirSync, constants, existsSync, linkSync, fstatSync, lstatSync, unlinkSync, renameSync, writeFileSync, chmodSync, statSync, readdirSync, readFileSync, readSync, openSync, fchmodSync, fsyncSync, closeSync, opendirSync, realpathSync } from 'fs';
|
|
3
3
|
import * as path3 from 'path';
|
|
4
4
|
import path3__default, { join, isAbsolute } from 'path';
|
|
5
|
-
import { AGENT_CONTENT_ARTIFACT_READ_CAPABILITY, AGENT_CONTENT_TRANSCRIPT_READ_CAPABILITY, AGENT_CONTENT_WORKSPACE_READ_CAPABILITY, AgentRefSchema, AgentHomeProjectionPayloadSchema, byokBlobUrlPath, BYOK_BLOBS_PATH, byokBlobFinalizePath, isTaskOfferType, partitionAgentEvents, BYOK_SKILL_PACKS_PATH, byokSkillPackFilePath, BYOK_RECORDS_PATH, byokRecordPath, TASK_TRANSITIONS, AgentEgressPolicySchema, BYOK_PAIR_PATH, PairResponseSchema, BYOK_CHALLENGE_PATH, BYOK_TOKEN_PATH, CONFIGURED_TOOLSETS_MAX_ITEMS, ToolsetIdSchema, AgentContentReceiptPayloadSchema, decodeEnvelope, createEnvelope, encodeEnvelope, PROTOCOL_VERSION, AGENT_EGRESS_RELIABLE_ACK_CAPABILITY, TERMINAL_PROJECTION_SELECTION_CAPABILITY, PROVIDER_PROFILE_BINDING_CAPABILITY, STRICT_AGENT_ONLY_CAPABILITY, AGENT_HOME_PROJECTION_CAPABILITY, AGENT_EGRESS_POLICY_CAPABILITY, AGENT_EGRESS_FRESH_SESSION_CAPABILITY, AGENT_MESSAGE_EGRESS_CAPABILITY, AgentHomeProjectionCompletionRequestSchema, byokAgentHomeProjectionCompletionPath, AgentHomeProjectionReadbackSchema, parseMessage, checkResultDocument, MAX_MESSAGES_PER_BATCH, isServerToDaemonType, BYOK_CAPABILITIES_PATH, BYOK_PRESENCE_PATH,
|
|
5
|
+
import { AGENT_CONTENT_ARTIFACT_READ_CAPABILITY, AGENT_CONTENT_TRANSCRIPT_READ_CAPABILITY, AGENT_CONTENT_WORKSPACE_READ_CAPABILITY, AgentRefSchema, AgentHomeProjectionPayloadSchema, byokBlobUrlPath, BYOK_BLOBS_PATH, byokBlobFinalizePath, isTaskOfferType, partitionAgentEvents, BYOK_SKILL_PACKS_PATH, byokSkillPackFilePath, BYOK_RECORDS_PATH, byokRecordPath, TASK_TRANSITIONS, AgentEgressPolicySchema, AgentMessagePublishPayloadSchema, AgentMessageDispositionPayloadSchema, BYOK_PAIR_PATH, PairResponseSchema, BYOK_CHALLENGE_PATH, BYOK_TOKEN_PATH, CONFIGURED_TOOLSETS_MAX_ITEMS, ToolsetIdSchema, AgentContentReceiptPayloadSchema, decodeEnvelope, createEnvelope, encodeEnvelope, PROTOCOL_VERSION, AGENT_EGRESS_RELIABLE_ACK_CAPABILITY, RuntimeIdSchema, HarnessIdSchema, TERMINAL_PROJECTION_SELECTION_CAPABILITY, PROVIDER_PROFILE_BINDING_CAPABILITY, STRICT_AGENT_ONLY_CAPABILITY, AGENT_HOME_PROJECTION_CAPABILITY, AGENT_EGRESS_POLICY_CAPABILITY, AGENT_EGRESS_FRESH_SESSION_CAPABILITY, AGENT_MESSAGE_EGRESS_CAPABILITY, AgentHomeProjectionCompletionRequestSchema, byokAgentHomeProjectionCompletionPath, AgentHomeProjectionReadbackSchema, parseMessage, checkResultDocument, MAX_MESSAGES_PER_BATCH, isServerToDaemonType, BYOK_CAPABILITIES_PATH, BYOK_PRESENCE_PATH, AGENT_MEMORY_PROJECTION_CAPABILITY, TERMINAL_INFERENCE_USAGE_MAX_DURATION_MS, TERMINAL_INFERENCE_USAGE_MAX_TOKENS, RESULT_DOCUMENT_MAX_BYTES, BYOK_EVENTS_PATH, BYOK_MESSAGES_PATH, MessagesSendResponseSchema, MAILBOX_READ_AHEAD_MAX_SEQS, MAILBOX_READ_AHEAD_CAPABILITY, MESSAGE_TYPES, AGENT_MEMORY_PROJECTION_MAX_ORDERING_VALUE, AgentMemoryProjectionMutationSchema, AGENT_MEMORY_PROJECTION_MAX_REDACTED_BYTES } from '@byok-sdk/protocol';
|
|
6
6
|
import net2, { createServer, createConnection } from 'net';
|
|
7
|
-
import * as
|
|
8
|
-
import
|
|
7
|
+
import * as os from 'os';
|
|
8
|
+
import os__default from 'os';
|
|
9
9
|
import { execFile, spawn, spawnSync } from 'child_process';
|
|
10
10
|
import { fileURLToPath } from 'url';
|
|
11
11
|
import { createInterface } from 'readline';
|
|
12
|
-
import { parseDeviceAssertionEnvelope, SKILL_PACK_MAX_BYTES, hasCapability, parseSkillPackManifest, checkSkillPackManifest, skillPackContentHashInput, checkSkillPackFileContent, SKILL_PACK_ENTRY_PATH, checkSkillPackEntry, isSkillPackPathSafe, DEVICE_PROOF_HEADER, contentHash as contentHash$1, TRUTH_RECORD_KINDS, DEVICE_ASSERTION_AUDIENCE_MAX_BYTES, DEVICE_ASSERTION_DEFAULT_TTL_MS, DEVICE_ASSERTION_MAX_TTL_MS, CONTENT_HASH_PATTERN,
|
|
12
|
+
import { parseDeviceAssertionEnvelope, SKILL_PACK_MAX_BYTES, hasCapability, parseSkillPackManifest, checkSkillPackManifest, skillPackContentHashInput, checkSkillPackFileContent, SKILL_PACK_ENTRY_PATH, checkSkillPackEntry, isSkillPackPathSafe, DEVICE_PROOF_HEADER, contentHash as contentHash$1, TRUTH_RECORD_KINDS, isTenantId, DEVICE_ASSERTION_AUDIENCE_MAX_BYTES, DEVICE_ASSERTION_DEFAULT_TTL_MS, DEVICE_ASSERTION_MAX_TTL_MS, CONTENT_HASH_PATTERN, nonceSigningBytes, CapabilityDeclarationSchema, DeviceAssertionClaimsSchema, deviceAssertionSigningInput, DEVICE_ASSERTION_SCHEMA_ID } from '@byok-sdk/core';
|
|
13
13
|
import { promisify } from 'util';
|
|
14
14
|
import * as fs14 from 'fs/promises';
|
|
15
15
|
import { createRequire } from 'module';
|
|
@@ -246,6 +246,18 @@ var ADMINISTRATORS_SID = "*S-1-5-32-544";
|
|
|
246
246
|
function buildIcaclsArgs(dir, username) {
|
|
247
247
|
return [dir, "/inheritance:r", "/grant:r", `${username}:(OI)(CI)F`, "/grant", `${SYSTEM_SID}:(OI)(CI)F`, "/grant", `${ADMINISTRATORS_SID}:(OI)(CI)F`];
|
|
248
248
|
}
|
|
249
|
+
function buildIcaclsFileArgs(filePath, username) {
|
|
250
|
+
return [
|
|
251
|
+
filePath,
|
|
252
|
+
"/inheritance:r",
|
|
253
|
+
"/grant:r",
|
|
254
|
+
`${username}:F`,
|
|
255
|
+
"/grant",
|
|
256
|
+
`${SYSTEM_SID}:F`,
|
|
257
|
+
"/grant",
|
|
258
|
+
`${ADMINISTRATORS_SID}:F`
|
|
259
|
+
];
|
|
260
|
+
}
|
|
249
261
|
var SecureDirHardeningError = class extends Error {
|
|
250
262
|
constructor(dir, reason) {
|
|
251
263
|
super(
|
|
@@ -256,6 +268,14 @@ var SecureDirHardeningError = class extends Error {
|
|
|
256
268
|
}
|
|
257
269
|
dir;
|
|
258
270
|
};
|
|
271
|
+
var SecureFileHardeningError = class extends Error {
|
|
272
|
+
constructor(filePath, reason) {
|
|
273
|
+
super(`failed to apply a restrictive Windows ACL to "${filePath}": ${reason}`);
|
|
274
|
+
this.filePath = filePath;
|
|
275
|
+
this.name = "SecureFileHardeningError";
|
|
276
|
+
}
|
|
277
|
+
filePath;
|
|
278
|
+
};
|
|
259
279
|
async function ensureSecureDir(dir, opts = {}) {
|
|
260
280
|
const platform = opts.platform ?? process.platform;
|
|
261
281
|
const run = opts.run ?? defaultRunner;
|
|
@@ -263,7 +283,7 @@ async function ensureSecureDir(dir, opts = {}) {
|
|
|
263
283
|
await promises.chmod(dir, 448).catch(() => {
|
|
264
284
|
});
|
|
265
285
|
if (platform !== "win32") return;
|
|
266
|
-
const { username } =
|
|
286
|
+
const { username } = os__default.userInfo();
|
|
267
287
|
let result;
|
|
268
288
|
try {
|
|
269
289
|
result = await run("icacls", buildIcaclsArgs(dir, username));
|
|
@@ -274,6 +294,22 @@ async function ensureSecureDir(dir, opts = {}) {
|
|
|
274
294
|
throw new SecureDirHardeningError(dir, `icacls exited ${result.code}: ${(result.stderr || result.stdout).trim()}`);
|
|
275
295
|
}
|
|
276
296
|
}
|
|
297
|
+
async function ensureSecureFile(filePath, opts = {}) {
|
|
298
|
+
const platform = opts.platform ?? process.platform;
|
|
299
|
+
const run = opts.run ?? defaultRunner;
|
|
300
|
+
await promises.chmod(filePath, 384);
|
|
301
|
+
if (platform !== "win32") return;
|
|
302
|
+
const { username } = os__default.userInfo();
|
|
303
|
+
let result;
|
|
304
|
+
try {
|
|
305
|
+
result = await run("icacls", buildIcaclsFileArgs(filePath, username));
|
|
306
|
+
} catch (err) {
|
|
307
|
+
throw new SecureFileHardeningError(filePath, `could not run icacls: ${err instanceof Error ? err.message : String(err)}`);
|
|
308
|
+
}
|
|
309
|
+
if (result.code !== 0) {
|
|
310
|
+
throw new SecureFileHardeningError(filePath, `icacls exited ${result.code}: ${(result.stderr || result.stdout).trim()}`);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
277
313
|
|
|
278
314
|
// src/daemon/path-mutation-gate.ts
|
|
279
315
|
var PathMutationGateBusyError = class extends Error {
|
|
@@ -2211,6 +2247,7 @@ async function acquire(input) {
|
|
|
2211
2247
|
var localStateRelocation = Object.freeze({ acquire });
|
|
2212
2248
|
|
|
2213
2249
|
// src/types.ts
|
|
2250
|
+
var RUNTIME_DETECTION_FAILURE_KINDS = ["not-found", "not-executable", "timeout", "probe-failed"];
|
|
2214
2251
|
function frozenStrings(values) {
|
|
2215
2252
|
return values === void 0 ? void 0 : Object.freeze([...values]);
|
|
2216
2253
|
}
|
|
@@ -2307,6 +2344,31 @@ function resolveLocalAgentReleaseIdentity(input) {
|
|
|
2307
2344
|
...input.buildId === void 0 ? {} : { buildId: input.buildId }
|
|
2308
2345
|
});
|
|
2309
2346
|
}
|
|
2347
|
+
async function runMcpEnvLauncher() {
|
|
2348
|
+
const key = process.env.BYOK_MCP_ENV_KEY;
|
|
2349
|
+
if (!key || !/^BYOK_MCP_PAYLOAD_[A-F0-9]{32}$/.test(key)) throw new Error("invalid MCP environment binding");
|
|
2350
|
+
const encoded = process.env[key];
|
|
2351
|
+
if (!encoded) throw new Error("missing sealed MCP environment");
|
|
2352
|
+
let config;
|
|
2353
|
+
try {
|
|
2354
|
+
config = JSON.parse(encoded);
|
|
2355
|
+
if (!config || typeof config.command !== "string" || !config.command || config.args !== void 0 && (!Array.isArray(config.args) || config.args.some((value) => typeof value !== "string")) || config.env !== void 0 && (typeof config.env !== "object" || config.env === null || Array.isArray(config.env) || Object.values(config.env).some((value) => typeof value !== "string"))) throw new Error();
|
|
2356
|
+
} catch {
|
|
2357
|
+
throw new Error("invalid sealed MCP environment");
|
|
2358
|
+
}
|
|
2359
|
+
const env = { ...process.env };
|
|
2360
|
+
delete env.BYOK_MCP_ENV_KEY;
|
|
2361
|
+
for (const name of Object.keys(env)) if (name.startsWith("BYOK_MCP_PAYLOAD_")) delete env[name];
|
|
2362
|
+
Object.assign(env, config.env);
|
|
2363
|
+
await new Promise((resolve, reject) => {
|
|
2364
|
+
const child = spawn(config.command, config.args ?? [], { env, stdio: "inherit", windowsHide: true });
|
|
2365
|
+
child.once("error", () => reject(new Error("MCP child could not be spawned")));
|
|
2366
|
+
child.once("close", (code) => {
|
|
2367
|
+
if (code === 0) resolve();
|
|
2368
|
+
else reject(new Error("MCP child exited unsuccessfully"));
|
|
2369
|
+
});
|
|
2370
|
+
});
|
|
2371
|
+
}
|
|
2310
2372
|
var CONTROL_PROTOCOL_VERSION = 1;
|
|
2311
2373
|
var HANDSHAKE_TIMEOUT_MS = 3e3;
|
|
2312
2374
|
var UNIX_SOCKET_PATH_SOFT_LIMIT2 = 100;
|
|
@@ -3174,6 +3236,9 @@ async function runAgentTeamMcp() {
|
|
|
3174
3236
|
}
|
|
3175
3237
|
async function runSdkReservedHelper(kind) {
|
|
3176
3238
|
switch (kind) {
|
|
3239
|
+
case "mcp-env":
|
|
3240
|
+
await runMcpEnvLauncher();
|
|
3241
|
+
return;
|
|
3177
3242
|
case "agent-message-mcp":
|
|
3178
3243
|
await runAgentMessageMcp();
|
|
3179
3244
|
return;
|
|
@@ -3195,7 +3260,8 @@ var DIST_SCRIPT_BY_KIND = Object.freeze({
|
|
|
3195
3260
|
"agent-message-mcp": "byok-agent-message-mcp.js",
|
|
3196
3261
|
"agent-memory-mcp": "byok-agent-memory-mcp.js",
|
|
3197
3262
|
"approval-mcp": "byok-approval-mcp.js",
|
|
3198
|
-
"agent-team-mcp": "byok-agent-team-mcp.js"
|
|
3263
|
+
"agent-team-mcp": "byok-agent-team-mcp.js",
|
|
3264
|
+
"mcp-env": "byok-mcp-env.js"
|
|
3199
3265
|
});
|
|
3200
3266
|
function assertExecutable(executable) {
|
|
3201
3267
|
if (!path3__default.isAbsolute(executable) || /[\u0000\r\n]/u.test(executable)) {
|
|
@@ -3213,8 +3279,11 @@ function resolveSdkReservedHelperBin(kind, host) {
|
|
|
3213
3279
|
source: "self-executable"
|
|
3214
3280
|
});
|
|
3215
3281
|
}
|
|
3282
|
+
const entryDir = path3__default.dirname(fileURLToPath(import.meta.url));
|
|
3283
|
+
const entryKind = path3__default.basename(entryDir);
|
|
3284
|
+
const distDir = entryKind === "adapters" || entryKind === "bin" ? path3__default.dirname(entryDir) : entryDir;
|
|
3216
3285
|
const script = path3__default.join(
|
|
3217
|
-
|
|
3286
|
+
distDir,
|
|
3218
3287
|
"bin",
|
|
3219
3288
|
DIST_SCRIPT_BY_KIND[kind]
|
|
3220
3289
|
);
|
|
@@ -3225,7 +3294,7 @@ function resolveSdkReservedHelperBin(kind, host) {
|
|
|
3225
3294
|
});
|
|
3226
3295
|
}
|
|
3227
3296
|
function isHelperKind(value) {
|
|
3228
|
-
return value === "agent-message-mcp" || value === "agent-memory-mcp" || value === "approval-mcp" || value === "agent-team-mcp";
|
|
3297
|
+
return value === "agent-message-mcp" || value === "agent-memory-mcp" || value === "approval-mcp" || value === "agent-team-mcp" || value === "mcp-env";
|
|
3229
3298
|
}
|
|
3230
3299
|
async function runSdkReservedHelperCommand(argv = process.argv.slice(2)) {
|
|
3231
3300
|
if (argv[0] !== BYOK_SDK_HELPER_SUBCOMMAND) return false;
|
|
@@ -3319,6 +3388,32 @@ function projectRuntimeBoundaryFailure(value, expectedPhase) {
|
|
|
3319
3388
|
contractViolation: true
|
|
3320
3389
|
};
|
|
3321
3390
|
}
|
|
3391
|
+
var RuntimeStartupDisposalFailure = class extends Error {
|
|
3392
|
+
retryDisposal;
|
|
3393
|
+
constructor(retryDisposal, options) {
|
|
3394
|
+
super("runtime startup failed and process ownership remains quarantined", options);
|
|
3395
|
+
this.name = "RuntimeStartupDisposalFailure";
|
|
3396
|
+
this.retryDisposal = retryDisposal;
|
|
3397
|
+
Object.defineProperty(this, /* @__PURE__ */ Symbol.for("@byok-sdk/client/RuntimeStartupDisposalFailure/v1"), { value: true });
|
|
3398
|
+
Object.freeze(this);
|
|
3399
|
+
}
|
|
3400
|
+
};
|
|
3401
|
+
function isRuntimeStartupDisposalFailure(value) {
|
|
3402
|
+
return typeof value === "object" && value !== null && value[/* @__PURE__ */ Symbol.for("@byok-sdk/client/RuntimeStartupDisposalFailure/v1")] === true && typeof value.retryDisposal === "function";
|
|
3403
|
+
}
|
|
3404
|
+
var GIT_ERROR_CATEGORIES = [
|
|
3405
|
+
"git-unavailable",
|
|
3406
|
+
"git-timeout",
|
|
3407
|
+
"git-output-limit",
|
|
3408
|
+
"git-command-failed",
|
|
3409
|
+
"workspace-root-invalid",
|
|
3410
|
+
"workspace-root-conflict",
|
|
3411
|
+
"workspace-not-owned",
|
|
3412
|
+
"repository-root-mismatch",
|
|
3413
|
+
"repository-invalid",
|
|
3414
|
+
"lease-busy",
|
|
3415
|
+
"ledger-invalid"
|
|
3416
|
+
];
|
|
3322
3417
|
var GitWorkspaceError = class extends Error {
|
|
3323
3418
|
constructor(category, message = category) {
|
|
3324
3419
|
super(message);
|
|
@@ -3824,6 +3919,132 @@ var GitWorkspaceStore = class {
|
|
|
3824
3919
|
await atomicWriteFile(this.filePath, JSON.stringify(ledger, null, 2), { mode: 384 });
|
|
3825
3920
|
}
|
|
3826
3921
|
};
|
|
3922
|
+
function terminalIdentity(runtimeId, agentRef) {
|
|
3923
|
+
return {
|
|
3924
|
+
...runtimeId === void 0 || RuntimeIdSchema.safeParse(runtimeId).success ? {} : { harnessId: HarnessIdSchema.parse(runtimeId) },
|
|
3925
|
+
...agentRef === void 0 ? {} : { agentRef }
|
|
3926
|
+
};
|
|
3927
|
+
}
|
|
3928
|
+
|
|
3929
|
+
// src/daemon/terminal-commit-queue.ts
|
|
3930
|
+
var TerminalCommitQueue = class {
|
|
3931
|
+
constructor(committed) {
|
|
3932
|
+
this.committed = committed;
|
|
3933
|
+
}
|
|
3934
|
+
committed;
|
|
3935
|
+
pending = /* @__PURE__ */ new Map();
|
|
3936
|
+
tail = Promise.resolve();
|
|
3937
|
+
timer;
|
|
3938
|
+
stopped = false;
|
|
3939
|
+
get pendingCount() {
|
|
3940
|
+
return this.pending.size;
|
|
3941
|
+
}
|
|
3942
|
+
hasPending(taskId) {
|
|
3943
|
+
return this.pending.has(taskId);
|
|
3944
|
+
}
|
|
3945
|
+
enqueue(taskId, commit) {
|
|
3946
|
+
if (!this.pending.has(taskId)) this.pending.set(taskId, { commit, receipt: Promise.resolve(), running: false });
|
|
3947
|
+
void this.attempt(taskId).catch(() => void 0);
|
|
3948
|
+
}
|
|
3949
|
+
receipt(taskId) {
|
|
3950
|
+
return this.pending.get(taskId)?.receipt ?? Promise.resolve();
|
|
3951
|
+
}
|
|
3952
|
+
retry(taskId) {
|
|
3953
|
+
return this.attempt(taskId);
|
|
3954
|
+
}
|
|
3955
|
+
resume() {
|
|
3956
|
+
this.stopped = false;
|
|
3957
|
+
if (this.pending.size > 0) this.armRetry();
|
|
3958
|
+
}
|
|
3959
|
+
async stop() {
|
|
3960
|
+
this.stopped = true;
|
|
3961
|
+
if (this.timer) clearTimeout(this.timer);
|
|
3962
|
+
this.timer = void 0;
|
|
3963
|
+
await Promise.all([...this.pending.keys()].map((taskId) => this.attempt(taskId)));
|
|
3964
|
+
}
|
|
3965
|
+
attempt(taskId) {
|
|
3966
|
+
const entry = this.pending.get(taskId);
|
|
3967
|
+
if (!entry) return Promise.resolve();
|
|
3968
|
+
if (entry.running) return entry.receipt;
|
|
3969
|
+
entry.running = true;
|
|
3970
|
+
const receipt = this.tail.catch(() => void 0).then(entry.commit);
|
|
3971
|
+
entry.receipt = receipt.then(() => {
|
|
3972
|
+
if (this.pending.get(taskId) === entry) this.pending.delete(taskId);
|
|
3973
|
+
this.committed(taskId);
|
|
3974
|
+
}, (error) => {
|
|
3975
|
+
entry.running = false;
|
|
3976
|
+
this.armRetry();
|
|
3977
|
+
throw error;
|
|
3978
|
+
});
|
|
3979
|
+
this.tail = entry.receipt;
|
|
3980
|
+
void entry.receipt.catch(() => void 0);
|
|
3981
|
+
return entry.receipt;
|
|
3982
|
+
}
|
|
3983
|
+
armRetry() {
|
|
3984
|
+
if (this.timer || this.stopped) return;
|
|
3985
|
+
this.timer = setTimeout(() => {
|
|
3986
|
+
this.timer = void 0;
|
|
3987
|
+
for (const taskId of this.pending.keys()) void this.attempt(taskId).catch(() => void 0);
|
|
3988
|
+
}, 1e3);
|
|
3989
|
+
this.timer.unref?.();
|
|
3990
|
+
}
|
|
3991
|
+
};
|
|
3992
|
+
|
|
3993
|
+
// src/runtime-detection.ts
|
|
3994
|
+
function validateRuntimeDetectResult(value) {
|
|
3995
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
3996
|
+
throw new TypeError("invalid runtime detection result");
|
|
3997
|
+
}
|
|
3998
|
+
const result = value;
|
|
3999
|
+
if (!Object.hasOwn(result, "kind")) throw new TypeError("invalid runtime detection result");
|
|
4000
|
+
const { kind, version, authPresent } = result;
|
|
4001
|
+
if (kind === "available") {
|
|
4002
|
+
if (Object.keys(result).some((key) => !["kind", "version", "authPresent"].includes(key)) || version !== void 0 && typeof version !== "string" || authPresent !== void 0 && typeof authPresent !== "boolean") {
|
|
4003
|
+
throw new TypeError("invalid runtime detection result");
|
|
4004
|
+
}
|
|
4005
|
+
return Object.freeze({
|
|
4006
|
+
kind,
|
|
4007
|
+
...version === void 0 ? {} : { version },
|
|
4008
|
+
...authPresent === void 0 ? {} : { authPresent }
|
|
4009
|
+
});
|
|
4010
|
+
}
|
|
4011
|
+
if (RUNTIME_DETECTION_FAILURE_KINDS.some((candidate) => candidate === kind) && Object.keys(result).every((key) => key === "kind")) {
|
|
4012
|
+
return Object.freeze({ kind });
|
|
4013
|
+
}
|
|
4014
|
+
throw new TypeError("invalid runtime detection result");
|
|
4015
|
+
}
|
|
4016
|
+
function classifyDetectError(error) {
|
|
4017
|
+
const code = error !== null && typeof error === "object" ? error.code : void 0;
|
|
4018
|
+
if (code === "ENOENT") return { kind: "not-found" };
|
|
4019
|
+
if (code === "EACCES" || code === "EPERM" || code === "ENOEXEC") return { kind: "not-executable" };
|
|
4020
|
+
return { kind: "probe-failed" };
|
|
4021
|
+
}
|
|
4022
|
+
async function probeRuntimeVersion(command, timeoutMs) {
|
|
4023
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0) throw new TypeError("invalid runtime probe timeout");
|
|
4024
|
+
return new Promise((resolve) => {
|
|
4025
|
+
let timer;
|
|
4026
|
+
let timedOut = false;
|
|
4027
|
+
try {
|
|
4028
|
+
const child = execFile(command, ["--version"], { encoding: "utf8" }, (error, stdout, stderr) => {
|
|
4029
|
+
if (timer) clearTimeout(timer);
|
|
4030
|
+
if (error?.code === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER") resolve({ kind: "probe-failed" });
|
|
4031
|
+
else if (timedOut) resolve({ kind: "timeout" });
|
|
4032
|
+
else if (error) resolve(classifyDetectError(error));
|
|
4033
|
+
else resolve({ kind: "available", stdout, stderr });
|
|
4034
|
+
});
|
|
4035
|
+
timer = setTimeout(() => {
|
|
4036
|
+
timedOut = true;
|
|
4037
|
+
child.stdout?.destroy();
|
|
4038
|
+
child.stderr?.destroy();
|
|
4039
|
+
child.kill("SIGKILL");
|
|
4040
|
+
}, timeoutMs);
|
|
4041
|
+
timer.unref?.();
|
|
4042
|
+
} catch (error) {
|
|
4043
|
+
if (timer) clearTimeout(timer);
|
|
4044
|
+
resolve(classifyDetectError(error));
|
|
4045
|
+
}
|
|
4046
|
+
});
|
|
4047
|
+
}
|
|
3827
4048
|
|
|
3828
4049
|
// src/adapters/pi/mcp-config.ts
|
|
3829
4050
|
var BYOK_PI_MCP_CONFIG_PATH = "BYOK_PI_MCP_CONFIG_PATH";
|
|
@@ -4834,12 +5055,13 @@ var PiAdapter = class {
|
|
|
4834
5055
|
async detect() {
|
|
4835
5056
|
try {
|
|
4836
5057
|
const bin = this.resolveBin();
|
|
4837
|
-
const
|
|
4838
|
-
|
|
5058
|
+
const probe = await probeRuntimeVersion(bin.command, DETECT_TIMEOUT_MS);
|
|
5059
|
+
if (probe.kind !== "available") return probe;
|
|
5060
|
+
const version = probe.stdout.trim() || probe.stderr.trim();
|
|
4839
5061
|
const authPresent = PROVIDER_CREDENTIAL_ENV_NAMES.some((name) => process.env[name] !== void 0);
|
|
4840
|
-
return {
|
|
4841
|
-
} catch {
|
|
4842
|
-
return
|
|
5062
|
+
return { kind: "available", version, authPresent };
|
|
5063
|
+
} catch (error) {
|
|
5064
|
+
return classifyDetectError(error);
|
|
4843
5065
|
}
|
|
4844
5066
|
}
|
|
4845
5067
|
async prepare(input) {
|
|
@@ -4952,7 +5174,7 @@ var PiAdapter = class {
|
|
|
4952
5174
|
const taskMcpServers = startInput.mcpServers ?? {};
|
|
4953
5175
|
let mcpConfigPath;
|
|
4954
5176
|
try {
|
|
4955
|
-
mcpConfigDir = await promises.mkdtemp(path3__default.join(
|
|
5177
|
+
mcpConfigDir = await promises.mkdtemp(path3__default.join(os__default.tmpdir(), "byok-pi-mcp-"));
|
|
4956
5178
|
await promises.chmod(mcpConfigDir, 448).catch(() => {
|
|
4957
5179
|
});
|
|
4958
5180
|
mcpConfigPath = path3__default.join(mcpConfigDir, "mcp-config.json");
|
|
@@ -5969,14 +6191,15 @@ var ClaudeAdapter = class {
|
|
|
5969
6191
|
environmentRequirements: { credentialNames: [] }
|
|
5970
6192
|
});
|
|
5971
6193
|
async detect() {
|
|
5972
|
-
const bin = this.resolveBin();
|
|
5973
6194
|
try {
|
|
5974
|
-
const
|
|
5975
|
-
const
|
|
6195
|
+
const bin = this.resolveBin();
|
|
6196
|
+
const probe = await probeRuntimeVersion(bin.command, DETECT_TIMEOUT_MS2);
|
|
6197
|
+
if (probe.kind !== "available") return probe;
|
|
6198
|
+
const version = probe.stdout.trim();
|
|
5976
6199
|
const authPresent = await this.probeAuthPresent(bin.command);
|
|
5977
|
-
return {
|
|
5978
|
-
} catch {
|
|
5979
|
-
return
|
|
6200
|
+
return { kind: "available", version, authPresent };
|
|
6201
|
+
} catch (error) {
|
|
6202
|
+
return classifyDetectError(error);
|
|
5980
6203
|
}
|
|
5981
6204
|
}
|
|
5982
6205
|
async prepare(input) {
|
|
@@ -6063,7 +6286,7 @@ var ClaudeAdapter = class {
|
|
|
6063
6286
|
});
|
|
6064
6287
|
}
|
|
6065
6288
|
if (needsMcpConfig) {
|
|
6066
|
-
mcpConfigDir = await promises.mkdtemp(path3__default.join(
|
|
6289
|
+
mcpConfigDir = await promises.mkdtemp(path3__default.join(os__default.tmpdir(), "byok-mcp-"));
|
|
6067
6290
|
await promises.chmod(mcpConfigDir, 448).catch(() => {
|
|
6068
6291
|
});
|
|
6069
6292
|
const mcpConfigPath = path3__default.join(mcpConfigDir, "mcp-config.json");
|
|
@@ -6595,10 +6818,18 @@ function unmappedFrameKey(evt) {
|
|
|
6595
6818
|
return evt.type;
|
|
6596
6819
|
}
|
|
6597
6820
|
var STDERR_RING_CAPACITY3 = 20;
|
|
6821
|
+
var CODEX_MAX_FRAME_BYTES = 1024 * 1024;
|
|
6822
|
+
var CODEX_MAX_DEFERRED_BYTES = 4 * 1024 * 1024;
|
|
6823
|
+
var CODEX_MAX_STDERR_BYTES = 64 * 1024;
|
|
6598
6824
|
var CodexProcessRunner = class {
|
|
6599
6825
|
child;
|
|
6600
6826
|
onEvent;
|
|
6601
|
-
|
|
6827
|
+
frameChunks = [];
|
|
6828
|
+
frameBytes = 0;
|
|
6829
|
+
deferredBytes = 0;
|
|
6830
|
+
stderrBytes = 0;
|
|
6831
|
+
transportFailure;
|
|
6832
|
+
onFailure;
|
|
6602
6833
|
stderrRing = [];
|
|
6603
6834
|
closed = false;
|
|
6604
6835
|
exitCode = null;
|
|
@@ -6621,11 +6852,12 @@ var CodexProcessRunner = class {
|
|
|
6621
6852
|
deferredEvents = [];
|
|
6622
6853
|
constructor(options) {
|
|
6623
6854
|
this.onEvent = options.onEvent;
|
|
6855
|
+
this.onFailure = options.onFailure;
|
|
6624
6856
|
const spawnFn = options.spawnFn ?? spawn;
|
|
6625
6857
|
this.child = spawnFn(options.command, options.args, withOwnedProcessTree({
|
|
6626
6858
|
cwd: options.cwd,
|
|
6627
6859
|
env: options.env,
|
|
6628
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
6860
|
+
stdio: [options.instruction === void 0 ? "ignore" : "pipe", "pipe", "pipe"]
|
|
6629
6861
|
}));
|
|
6630
6862
|
this.closedPromise = new Promise((resolve) => {
|
|
6631
6863
|
this.resolveClosed = resolve;
|
|
@@ -6633,7 +6865,6 @@ var CodexProcessRunner = class {
|
|
|
6633
6865
|
this.adopted = this.adoptOwnedTree(options);
|
|
6634
6866
|
this.adopted.catch(() => {
|
|
6635
6867
|
});
|
|
6636
|
-
this.child.stdout.setEncoding("utf8");
|
|
6637
6868
|
this.child.stdout.on("data", (chunk) => this.onData(chunk));
|
|
6638
6869
|
this.child.stderr.setEncoding("utf8");
|
|
6639
6870
|
this.child.stderr.on("data", (chunk) => this.onStderr(chunk));
|
|
@@ -6642,6 +6873,12 @@ var CodexProcessRunner = class {
|
|
|
6642
6873
|
this.exitSignal = signal;
|
|
6643
6874
|
this.finishClosing();
|
|
6644
6875
|
});
|
|
6876
|
+
if (options.instruction !== void 0) {
|
|
6877
|
+
this.child.stdin.on("error", () => {
|
|
6878
|
+
this.failTransport("codex prompt stdin failed before delivery");
|
|
6879
|
+
});
|
|
6880
|
+
this.child.stdin.end(options.instruction, "utf8");
|
|
6881
|
+
}
|
|
6645
6882
|
this.child.on("error", () => {
|
|
6646
6883
|
this.finishClosing();
|
|
6647
6884
|
});
|
|
@@ -6727,12 +6964,19 @@ var CodexProcessRunner = class {
|
|
|
6727
6964
|
throw failure;
|
|
6728
6965
|
}
|
|
6729
6966
|
this.adoption = "adopted";
|
|
6967
|
+
this.deferredBytes = 0;
|
|
6730
6968
|
for (const evt of this.deferredEvents.splice(0)) this.onEvent(evt);
|
|
6731
6969
|
}
|
|
6732
6970
|
/** Arrival-order delivery, held back until the tree is backstopped (see `deferredEvents`). */
|
|
6733
|
-
deliver(evt) {
|
|
6971
|
+
deliver(evt, bytes) {
|
|
6972
|
+
if (this.transportFailure) return;
|
|
6734
6973
|
if (this.adoption === "failed") return;
|
|
6735
6974
|
if (this.adoption === "pending") {
|
|
6975
|
+
if (this.deferredBytes + bytes > CODEX_MAX_DEFERRED_BYTES) {
|
|
6976
|
+
this.failTransport("codex pre-adoption event buffer exceeded its byte budget");
|
|
6977
|
+
return;
|
|
6978
|
+
}
|
|
6979
|
+
this.deferredBytes += bytes;
|
|
6736
6980
|
this.deferredEvents.push(evt);
|
|
6737
6981
|
return;
|
|
6738
6982
|
}
|
|
@@ -6749,6 +6993,7 @@ var CodexProcessRunner = class {
|
|
|
6749
6993
|
* plus an empty stderr tail would bury the only reason anyone can act on.
|
|
6750
6994
|
*/
|
|
6751
6995
|
buildExitError(context) {
|
|
6996
|
+
if (this.transportFailure) return this.transportFailure;
|
|
6752
6997
|
if (this.adoptionFailure !== void 0) return this.adoptionFailure;
|
|
6753
6998
|
const parts = [`${context} (exit code=${this.exitCode}, signal=${this.exitSignal})`];
|
|
6754
6999
|
if (this.stderrRing.length > 0) {
|
|
@@ -6756,18 +7001,45 @@ var CodexProcessRunner = class {
|
|
|
6756
7001
|
}
|
|
6757
7002
|
return new Error(parts.join("; "));
|
|
6758
7003
|
}
|
|
7004
|
+
failTransport(reason) {
|
|
7005
|
+
if (this.transportFailure) return;
|
|
7006
|
+
this.transportFailure = new RuntimeExecutionFailure({
|
|
7007
|
+
phase: "run",
|
|
7008
|
+
category: "infrastructure",
|
|
7009
|
+
retry: "non-retryable",
|
|
7010
|
+
reason
|
|
7011
|
+
});
|
|
7012
|
+
this.frameChunks.length = 0;
|
|
7013
|
+
this.frameBytes = 0;
|
|
7014
|
+
this.deferredEvents.length = 0;
|
|
7015
|
+
this.deferredBytes = 0;
|
|
7016
|
+
this.onFailure?.(this.transportFailure);
|
|
7017
|
+
this.kill();
|
|
7018
|
+
}
|
|
6759
7019
|
onData(chunk) {
|
|
6760
|
-
this.
|
|
6761
|
-
let
|
|
6762
|
-
while (
|
|
6763
|
-
|
|
6764
|
-
|
|
6765
|
-
|
|
6766
|
-
if (
|
|
6767
|
-
|
|
7020
|
+
if (this.transportFailure) return;
|
|
7021
|
+
let offset = 0;
|
|
7022
|
+
while (offset < chunk.length) {
|
|
7023
|
+
const newline = chunk.indexOf(10, offset);
|
|
7024
|
+
const end = newline === -1 ? chunk.length : newline;
|
|
7025
|
+
const bytes = end - offset;
|
|
7026
|
+
if (this.frameBytes + bytes > CODEX_MAX_FRAME_BYTES) {
|
|
7027
|
+
this.failTransport("codex raw JSONL frame exceeded its byte budget");
|
|
7028
|
+
return;
|
|
7029
|
+
}
|
|
7030
|
+
if (bytes > 0) this.frameChunks.push(Buffer.from(chunk.subarray(offset, end)));
|
|
7031
|
+
this.frameBytes += bytes;
|
|
7032
|
+
if (newline === -1) return;
|
|
7033
|
+
const frame = Buffer.concat(this.frameChunks, this.frameBytes);
|
|
7034
|
+
this.frameChunks.length = 0;
|
|
7035
|
+
this.frameBytes = 0;
|
|
7036
|
+
const line = frame.toString("utf8").replace(/\r$/, "");
|
|
7037
|
+
if (line.length > 0) this.parseLine(line, frame.length);
|
|
7038
|
+
if (this.transportFailure) return;
|
|
7039
|
+
offset = newline + 1;
|
|
6768
7040
|
}
|
|
6769
7041
|
}
|
|
6770
|
-
parseLine(line) {
|
|
7042
|
+
parseLine(line, bytes) {
|
|
6771
7043
|
let parsed;
|
|
6772
7044
|
try {
|
|
6773
7045
|
parsed = JSON.parse(line);
|
|
@@ -6777,14 +7049,18 @@ var CodexProcessRunner = class {
|
|
|
6777
7049
|
if (!parsed || typeof parsed !== "object" || typeof parsed.type !== "string") {
|
|
6778
7050
|
return;
|
|
6779
7051
|
}
|
|
6780
|
-
this.deliver(parsed);
|
|
7052
|
+
this.deliver(parsed, bytes);
|
|
6781
7053
|
}
|
|
6782
7054
|
onStderr(chunk) {
|
|
6783
|
-
|
|
7055
|
+
const bounded2 = Buffer.from(chunk).subarray(-CODEX_MAX_STDERR_BYTES).toString("utf8");
|
|
7056
|
+
for (const rawLine of bounded2.split("\n")) {
|
|
6784
7057
|
const line = rawLine.trim();
|
|
6785
7058
|
if (line.length === 0) continue;
|
|
6786
7059
|
this.stderrRing.push(line);
|
|
6787
|
-
|
|
7060
|
+
this.stderrBytes += Buffer.byteLength(line);
|
|
7061
|
+
while (this.stderrRing.length > STDERR_RING_CAPACITY3 || this.stderrBytes > CODEX_MAX_STDERR_BYTES) {
|
|
7062
|
+
this.stderrBytes -= Buffer.byteLength(this.stderrRing.shift());
|
|
7063
|
+
}
|
|
6788
7064
|
}
|
|
6789
7065
|
}
|
|
6790
7066
|
};
|
|
@@ -6817,14 +7093,15 @@ var CodexAdapter = class {
|
|
|
6817
7093
|
environmentRequirements: { credentialNames: [] }
|
|
6818
7094
|
});
|
|
6819
7095
|
async detect() {
|
|
6820
|
-
const bin = this.resolveBin();
|
|
6821
7096
|
try {
|
|
6822
|
-
const
|
|
6823
|
-
const
|
|
7097
|
+
const bin = this.resolveBin();
|
|
7098
|
+
const probe = await probeRuntimeVersion(bin.command, DETECT_TIMEOUT_MS3);
|
|
7099
|
+
if (probe.kind !== "available") return probe;
|
|
7100
|
+
const version = probe.stdout.trim() || probe.stderr.trim();
|
|
6824
7101
|
const authPresent = await this.probeAuthPresent(bin);
|
|
6825
|
-
return {
|
|
6826
|
-
} catch {
|
|
6827
|
-
return
|
|
7102
|
+
return { kind: "available", version, authPresent };
|
|
7103
|
+
} catch (error) {
|
|
7104
|
+
return classifyDetectError(error);
|
|
6828
7105
|
}
|
|
6829
7106
|
}
|
|
6830
7107
|
/**
|
|
@@ -6967,7 +7244,7 @@ ${withStreams.stderr ?? ""}`);
|
|
|
6967
7244
|
reason: "prepared codex operation received a manifest with different runtime selection"
|
|
6968
7245
|
});
|
|
6969
7246
|
}
|
|
6970
|
-
const mcpConfigArgs = codexMcpConfigArgs(startInput.mcpServers, preparedMcpGrants);
|
|
7247
|
+
const mcpConfigArgs = codexMcpConfigArgs(startInput.mcpServers, runtimeEnv, this.options.sdkHelperHost, preparedMcpGrants);
|
|
6971
7248
|
const { sessionRef, runner } = await runCodexTurn({
|
|
6972
7249
|
command,
|
|
6973
7250
|
resumeRef: startInput.manifest.sessionRef,
|
|
@@ -6983,6 +7260,7 @@ ${withStreams.stderr ?? ""}`);
|
|
|
6983
7260
|
expectedSessionRef: startInput.manifest.sessionRef,
|
|
6984
7261
|
preparedGit: startInput.manifest.workspace.workspaceId !== void 0,
|
|
6985
7262
|
failurePhase: "start",
|
|
7263
|
+
signal: startInput.signal,
|
|
6986
7264
|
terminal
|
|
6987
7265
|
});
|
|
6988
7266
|
return new CodexSession({
|
|
@@ -6990,6 +7268,7 @@ ${withStreams.stderr ?? ""}`);
|
|
|
6990
7268
|
command,
|
|
6991
7269
|
workspaceDir,
|
|
6992
7270
|
env: startInput.env,
|
|
7271
|
+
mcpEnvironment: Object.fromEntries(Object.entries(runtimeEnv).filter(([key]) => key.startsWith("BYOK_MCP_PAYLOAD_"))),
|
|
6993
7272
|
spawnFn: this.options.spawnFn,
|
|
6994
7273
|
queue,
|
|
6995
7274
|
recordUnmapped,
|
|
@@ -7042,16 +7321,18 @@ function codexMcpToolApprovalArgs(name, tools) {
|
|
|
7042
7321
|
}
|
|
7043
7322
|
return args;
|
|
7044
7323
|
}
|
|
7045
|
-
function codexMcpConfigArgs(servers, grants = []) {
|
|
7324
|
+
function codexMcpConfigArgs(servers, env, helperHost, grants = []) {
|
|
7046
7325
|
if (servers === void 0 || Object.keys(servers).length === 0) return [];
|
|
7047
7326
|
const grantedTools = new Map(grants.map((grant) => [grant.server, grant.tools]));
|
|
7048
7327
|
const args = ["--ignore-user-config"];
|
|
7049
7328
|
for (const [name, server] of Object.entries(servers).sort(([left], [right]) => left.localeCompare(right))) {
|
|
7050
|
-
|
|
7051
|
-
|
|
7052
|
-
|
|
7053
|
-
|
|
7054
|
-
}
|
|
7329
|
+
const key = `BYOK_MCP_PAYLOAD_${randomBytes(16).toString("hex").toUpperCase()}`;
|
|
7330
|
+
env[key] = JSON.stringify(server);
|
|
7331
|
+
const helper = resolveSdkReservedHelperBin("mcp-env", helperHost);
|
|
7332
|
+
args.push("-c", `mcp_servers.${name}.command=${JSON.stringify(helper.command)}`);
|
|
7333
|
+
args.push("-c", `mcp_servers.${name}.args=${JSON.stringify([...helper.args])}`);
|
|
7334
|
+
args.push("-c", `mcp_servers.${name}.env.BYOK_MCP_ENV_KEY=${JSON.stringify(key)}`);
|
|
7335
|
+
args.push("-c", `mcp_servers.${name}.env_vars=${JSON.stringify([key])}`);
|
|
7055
7336
|
const granted = grantedTools.get(name);
|
|
7056
7337
|
if (granted !== void 0) args.push(...codexMcpToolApprovalArgs(name, granted));
|
|
7057
7338
|
}
|
|
@@ -7079,7 +7360,7 @@ function buildArgv(resumeRef, policyArgs, instruction, modelId, preparedGit = fa
|
|
|
7079
7360
|
...modelId ? ["--model", modelId] : [],
|
|
7080
7361
|
...preparedGit ? [] : ["--skip-git-repo-check"],
|
|
7081
7362
|
...policyArgs,
|
|
7082
|
-
|
|
7363
|
+
"-"
|
|
7083
7364
|
];
|
|
7084
7365
|
}
|
|
7085
7366
|
async function runCodexTurn(params) {
|
|
@@ -7103,9 +7384,18 @@ async function runCodexTurn(params) {
|
|
|
7103
7384
|
runner = new CodexProcessRunner({
|
|
7104
7385
|
command: params.command,
|
|
7105
7386
|
args: argv,
|
|
7387
|
+
instruction: params.instruction,
|
|
7106
7388
|
cwd: params.cwd,
|
|
7107
7389
|
env: params.env,
|
|
7108
7390
|
spawnFn: params.spawnFn,
|
|
7391
|
+
onFailure: (failure) => {
|
|
7392
|
+
if (!firstLineSettled) {
|
|
7393
|
+
firstLineSettled = true;
|
|
7394
|
+
rejectFirstLine(failure);
|
|
7395
|
+
}
|
|
7396
|
+
params.terminal.failure = failure;
|
|
7397
|
+
params.queue.end();
|
|
7398
|
+
},
|
|
7109
7399
|
onEvent: (evt) => {
|
|
7110
7400
|
if (!firstLineSettled) {
|
|
7111
7401
|
firstLineSettled = true;
|
|
@@ -7172,6 +7462,16 @@ async function runCodexTurn(params) {
|
|
|
7172
7462
|
params.queue.end();
|
|
7173
7463
|
});
|
|
7174
7464
|
let sessionRef;
|
|
7465
|
+
const abortStartup = () => rejectFirstLine(new RuntimeExecutionFailure({
|
|
7466
|
+
phase: params.failurePhase,
|
|
7467
|
+
category: "infrastructure",
|
|
7468
|
+
retry: "non-retryable",
|
|
7469
|
+
reason: "codex startup cancelled or exceeded its deadline"
|
|
7470
|
+
}));
|
|
7471
|
+
const startupTimer = setTimeout(abortStartup, 3e4);
|
|
7472
|
+
startupTimer.unref?.();
|
|
7473
|
+
params.signal?.addEventListener("abort", abortStartup, { once: true });
|
|
7474
|
+
if (params.signal?.aborted) abortStartup();
|
|
7175
7475
|
try {
|
|
7176
7476
|
sessionRef = await new Promise((resolve, reject) => {
|
|
7177
7477
|
let settled = false;
|
|
@@ -7203,20 +7503,31 @@ async function runCodexTurn(params) {
|
|
|
7203
7503
|
});
|
|
7204
7504
|
});
|
|
7205
7505
|
} catch (err) {
|
|
7206
|
-
runner
|
|
7506
|
+
await disposeFailedStartup(runner, err);
|
|
7207
7507
|
throw err;
|
|
7508
|
+
} finally {
|
|
7509
|
+
clearTimeout(startupTimer);
|
|
7510
|
+
params.signal?.removeEventListener("abort", abortStartup);
|
|
7208
7511
|
}
|
|
7209
7512
|
if (params.expectedSessionRef !== void 0 && sessionRef !== params.expectedSessionRef) {
|
|
7210
|
-
|
|
7211
|
-
throw new RuntimeExecutionFailure({
|
|
7513
|
+
const failure = new RuntimeExecutionFailure({
|
|
7212
7514
|
phase: params.failurePhase,
|
|
7213
7515
|
category: "authority",
|
|
7214
7516
|
retry: "non-retryable",
|
|
7215
7517
|
reason: `codex exec resume echoed a different thread id than requested (requested ${params.expectedSessionRef}, got ${sessionRef})`
|
|
7216
7518
|
});
|
|
7519
|
+
await disposeFailedStartup(runner, failure);
|
|
7520
|
+
throw failure;
|
|
7217
7521
|
}
|
|
7218
7522
|
return { sessionRef, runner };
|
|
7219
7523
|
}
|
|
7524
|
+
async function disposeFailedStartup(runner, cause) {
|
|
7525
|
+
try {
|
|
7526
|
+
await runner.dispose();
|
|
7527
|
+
} catch (disposalFailure) {
|
|
7528
|
+
throw new RuntimeStartupDisposalFailure(() => runner.dispose(), { cause: new AggregateError([cause, disposalFailure]) });
|
|
7529
|
+
}
|
|
7530
|
+
}
|
|
7220
7531
|
var CodexSession = class {
|
|
7221
7532
|
/**
|
|
7222
7533
|
* NOT `readonly` (cross-model review finding): `followUp()` below
|
|
@@ -7252,6 +7563,7 @@ var CodexSession = class {
|
|
|
7252
7563
|
* recomputed from anything a later turn supplies, so a follow-up can never
|
|
7253
7564
|
* widen this session's MCP authority.
|
|
7254
7565
|
*/
|
|
7566
|
+
mcpEnvironment;
|
|
7255
7567
|
mcpConfigArgs;
|
|
7256
7568
|
terminal;
|
|
7257
7569
|
currentRunner;
|
|
@@ -7271,6 +7583,7 @@ var CodexSession = class {
|
|
|
7271
7583
|
this.modelId = options.modelId;
|
|
7272
7584
|
this.terminal = options.terminal;
|
|
7273
7585
|
this.mcpConfigArgs = options.mcpConfigArgs;
|
|
7586
|
+
this.mcpEnvironment = Object.freeze({ ...options.mcpEnvironment });
|
|
7274
7587
|
this.currentRunner = options.initialRunner;
|
|
7275
7588
|
this.ownedRunners.add(options.initialRunner);
|
|
7276
7589
|
void this.forgetRunnerOnceClosed(options.initialRunner);
|
|
@@ -7383,7 +7696,7 @@ var CodexSession = class {
|
|
|
7383
7696
|
// frozen MCP config replayed verbatim — see `mcpConfigArgs`.
|
|
7384
7697
|
policyArgs: [...mapping.args, ...this.mcpConfigArgs],
|
|
7385
7698
|
cwd: this.workspaceDir,
|
|
7386
|
-
env: withoutProviderCredentials(this.env),
|
|
7699
|
+
env: { ...withoutProviderCredentials(this.env), ...this.mcpEnvironment },
|
|
7387
7700
|
spawnFn: this.spawnFn,
|
|
7388
7701
|
workspaceDir: this.workspaceDir,
|
|
7389
7702
|
queue: this.queue,
|
|
@@ -7560,7 +7873,7 @@ function isNodeError(error, code) {
|
|
|
7560
7873
|
return error instanceof Error && "code" in error && error.code === code;
|
|
7561
7874
|
}
|
|
7562
7875
|
async function scavengeStaleWindowsBridges() {
|
|
7563
|
-
const temporaryRoot =
|
|
7876
|
+
const temporaryRoot = os.tmpdir();
|
|
7564
7877
|
let entries;
|
|
7565
7878
|
try {
|
|
7566
7879
|
entries = await fs14.readdir(temporaryRoot, { withFileTypes: true });
|
|
@@ -7779,7 +8092,7 @@ var DeviceCredentialStore = class {
|
|
|
7779
8092
|
}
|
|
7780
8093
|
async #windows(operation, encoded) {
|
|
7781
8094
|
await scavengeStaleWindowsBridges();
|
|
7782
|
-
const directory = await fs14.mkdtemp(path3.join(
|
|
8095
|
+
const directory = await fs14.mkdtemp(path3.join(os.tmpdir(), WINDOWS_BRIDGE_DIRECTORY_PREFIX));
|
|
7783
8096
|
const executable = path3.join(directory, "credential-bridge.exe");
|
|
7784
8097
|
const request = [
|
|
7785
8098
|
operation,
|
|
@@ -7966,7 +8279,7 @@ var DeviceStore = class _DeviceStore {
|
|
|
7966
8279
|
/** Internal test seam. Product construction always supplies productId and gets an OS store. */
|
|
7967
8280
|
credentials;
|
|
7968
8281
|
static defaultDir(productId) {
|
|
7969
|
-
return path3__default.join(
|
|
8282
|
+
return path3__default.join(os__default.homedir(), ".byok", productId);
|
|
7970
8283
|
}
|
|
7971
8284
|
/**
|
|
7972
8285
|
* Resolve the one store pathname every daemon/CLI component must share.
|
|
@@ -8015,6 +8328,20 @@ var DeviceStore = class _DeviceStore {
|
|
|
8015
8328
|
await opened.handle.close();
|
|
8016
8329
|
}
|
|
8017
8330
|
}
|
|
8331
|
+
/** Rebuild only the non-secret projection, while the caller owns the store lease. */
|
|
8332
|
+
async reconcileMetadata(authority) {
|
|
8333
|
+
const projection = {
|
|
8334
|
+
deviceId: authority.deviceId,
|
|
8335
|
+
tenantId: authority.tenantId,
|
|
8336
|
+
devicePublicKey: authority.devicePublicKey
|
|
8337
|
+
};
|
|
8338
|
+
const current = await this.load();
|
|
8339
|
+
if (current !== void 0 && current.deviceId === projection.deviceId && current.tenantId === projection.tenantId && current.devicePublicKey === projection.devicePublicKey) {
|
|
8340
|
+
return false;
|
|
8341
|
+
}
|
|
8342
|
+
await this.save(projection);
|
|
8343
|
+
return true;
|
|
8344
|
+
}
|
|
8018
8345
|
async save(record3) {
|
|
8019
8346
|
assertDeviceMetadata(record3);
|
|
8020
8347
|
const storeDir = path3__default.dirname(this.filePath);
|
|
@@ -8224,7 +8551,7 @@ var AuthManager = class {
|
|
|
8224
8551
|
if (!(error instanceof DeviceRecordRePairRequiredError)) throw error;
|
|
8225
8552
|
}
|
|
8226
8553
|
}
|
|
8227
|
-
const observedDeviceName = this.opts.deviceName ??
|
|
8554
|
+
const observedDeviceName = this.opts.deviceName ?? os__default.hostname();
|
|
8228
8555
|
const observedMachineId = await this.opts.machineId?.();
|
|
8229
8556
|
let keyPair;
|
|
8230
8557
|
let pairingDeviceName = observedDeviceName;
|
|
@@ -8433,15 +8760,7 @@ var AuthManager = class {
|
|
|
8433
8760
|
if (await this.opts.store.load() === void 0) return void 0;
|
|
8434
8761
|
throw new DeviceRecordRePairRequiredError();
|
|
8435
8762
|
}
|
|
8436
|
-
|
|
8437
|
-
deviceId: authority.deviceId,
|
|
8438
|
-
tenantId: authority.tenantId,
|
|
8439
|
-
devicePublicKey: authority.devicePublicKey
|
|
8440
|
-
};
|
|
8441
|
-
const current = await this.opts.store.load();
|
|
8442
|
-
if (current === void 0 || current.deviceId !== projection.deviceId || current.tenantId !== projection.tenantId || current.devicePublicKey !== projection.devicePublicKey) {
|
|
8443
|
-
await this.opts.store.save(projection);
|
|
8444
|
-
}
|
|
8763
|
+
await this.opts.store.reconcileMetadata(authority);
|
|
8445
8764
|
return Object.freeze({ ...authority });
|
|
8446
8765
|
}
|
|
8447
8766
|
};
|
|
@@ -8738,9 +9057,9 @@ async function probeDarwin(run) {
|
|
|
8738
9057
|
return DARWIN_UUID_RE.exec(result.stdout)?.[1];
|
|
8739
9058
|
}
|
|
8740
9059
|
async function probeLinux(readFile3) {
|
|
8741
|
-
for (const
|
|
9060
|
+
for (const path43 of LINUX_MACHINE_ID_PATHS) {
|
|
8742
9061
|
try {
|
|
8743
|
-
const contents = (await readFile3(
|
|
9062
|
+
const contents = (await readFile3(path43)).trim();
|
|
8744
9063
|
if (contents.length > 0) return contents;
|
|
8745
9064
|
} catch {
|
|
8746
9065
|
}
|
|
@@ -8772,7 +9091,7 @@ async function withTimeout2(probe, timeoutMs) {
|
|
|
8772
9091
|
async function resolveMachineId(options) {
|
|
8773
9092
|
const platform = options.platform ?? process.platform;
|
|
8774
9093
|
const run = options.run ?? runDeviceCommand;
|
|
8775
|
-
const readFile3 = options.readFile ?? ((
|
|
9094
|
+
const readFile3 = options.readFile ?? ((path43) => fs14.readFile(path43, "utf8"));
|
|
8776
9095
|
let probe;
|
|
8777
9096
|
if (platform === "darwin") probe = probeDarwin(run);
|
|
8778
9097
|
else if (platform === "linux") probe = probeLinux(readFile3);
|
|
@@ -10410,13 +10729,35 @@ var LongPollClient = class {
|
|
|
10410
10729
|
}
|
|
10411
10730
|
async loop(signal) {
|
|
10412
10731
|
let retryAttempt = 0;
|
|
10732
|
+
let readCursor;
|
|
10733
|
+
let readAheadSupported = false;
|
|
10734
|
+
let warnedWindowAt;
|
|
10413
10735
|
while (this.running && !signal.aborted) {
|
|
10414
10736
|
try {
|
|
10415
10737
|
await this.opts.auth.getValidAccessToken();
|
|
10416
10738
|
const base = toHttpBase(this.opts.serverUrl);
|
|
10417
10739
|
const url = new URL(BYOK_EVENTS_PATH, base);
|
|
10418
10740
|
const cursor = this.opts.getCursor();
|
|
10741
|
+
const ack = cursor ?? 0;
|
|
10742
|
+
if (readCursor !== void 0 && readCursor - ack >= MAILBOX_READ_AHEAD_MAX_SEQS) {
|
|
10743
|
+
if (warnedWindowAt !== ack) {
|
|
10744
|
+
warnedWindowAt = ack;
|
|
10745
|
+
console.warn("[byok/client] mailbox read-ahead window full; acknowledgement is stalled");
|
|
10746
|
+
}
|
|
10747
|
+
readCursor = ack;
|
|
10748
|
+
await sleep(this.opts.retryDelayMs ?? 2e3, signal);
|
|
10749
|
+
continue;
|
|
10750
|
+
}
|
|
10751
|
+
const afterSeq = Math.max(ack, readCursor ?? ack);
|
|
10419
10752
|
if (cursor !== void 0) url.searchParams.set("cursor", String(cursor));
|
|
10753
|
+
if (afterSeq > ack) {
|
|
10754
|
+
if (!readAheadSupported) {
|
|
10755
|
+
const error = new Error("server does not advertise mailbox-read-ahead; cannot read past unacknowledged work");
|
|
10756
|
+
this.warnRouteFailure(this.eventsEndpoint, void 0, error);
|
|
10757
|
+
throw error;
|
|
10758
|
+
}
|
|
10759
|
+
url.searchParams.set("afterSeq", String(afterSeq));
|
|
10760
|
+
}
|
|
10420
10761
|
let res;
|
|
10421
10762
|
try {
|
|
10422
10763
|
res = await authedFetch(url, { method: "GET", signal }, this.opts.auth);
|
|
@@ -10435,6 +10776,8 @@ var LongPollClient = class {
|
|
|
10435
10776
|
return;
|
|
10436
10777
|
}
|
|
10437
10778
|
this.warnRouteFailure(this.eventsEndpoint, res.status, void 0);
|
|
10779
|
+
readCursor = void 0;
|
|
10780
|
+
readAheadSupported = false;
|
|
10438
10781
|
this.opts.onServerCapabilitiesInvalidated?.();
|
|
10439
10782
|
this.opts.onPollFailure?.();
|
|
10440
10783
|
this.opts.onOperationalOutcome?.("failure");
|
|
@@ -10444,12 +10787,16 @@ var LongPollClient = class {
|
|
|
10444
10787
|
}
|
|
10445
10788
|
let parsed;
|
|
10446
10789
|
try {
|
|
10447
|
-
parsed = parseLooseEventsPollResponse(await res.json(),
|
|
10448
|
-
validateTrustedEventsPage(parsed.events,
|
|
10790
|
+
parsed = parseLooseEventsPollResponse(await res.json(), afterSeq);
|
|
10791
|
+
validateTrustedEventsPage(parsed.events, afterSeq, parsed.cursor);
|
|
10792
|
+
if (afterSeq > ack && !parsed.capabilities.includes(MAILBOX_READ_AHEAD_CAPABILITY)) throw new Error("mailbox read-ahead capability was withdrawn");
|
|
10449
10793
|
} catch (err) {
|
|
10450
10794
|
this.warnRouteFailure(this.eventsEndpoint, res.status, err);
|
|
10451
10795
|
throw err;
|
|
10452
10796
|
}
|
|
10797
|
+
readAheadSupported = parsed.capabilities.includes(MAILBOX_READ_AHEAD_CAPABILITY);
|
|
10798
|
+
const madeReadProgress = parsed.cursor > afterSeq;
|
|
10799
|
+
readCursor = parsed.cursor;
|
|
10453
10800
|
this.opts.onServerCapabilities?.(parsed.capabilities);
|
|
10454
10801
|
let hadValidationFailureThisBatch = false;
|
|
10455
10802
|
let acceptedAnyEntry = false;
|
|
@@ -10473,10 +10820,11 @@ var LongPollClient = class {
|
|
|
10473
10820
|
if (this.opts.onEnvelope(envelope) !== false) acceptedAnyEntry = true;
|
|
10474
10821
|
}
|
|
10475
10822
|
if (parsed.events.length === 0) {
|
|
10823
|
+
readCursor = this.opts.getCursor() ?? 0;
|
|
10476
10824
|
retryAttempt = 0;
|
|
10477
10825
|
this.opts.onOperationalOutcome?.("success");
|
|
10478
10826
|
await sleep(this.opts.idleDelayMs ?? 250, signal);
|
|
10479
|
-
} else if (this.opts.isStalled?.() || hadValidationFailureThisBatch || !acceptedAnyEntry && this.opts.getCursor() === cursor) {
|
|
10827
|
+
} else if (!madeReadProgress && (this.opts.isStalled?.() || hadValidationFailureThisBatch || !acceptedAnyEntry && this.opts.getCursor() === cursor)) {
|
|
10480
10828
|
this.opts.onOperationalOutcome?.("failure");
|
|
10481
10829
|
const baseMs = this.opts.retryDelayMs ?? 2e3;
|
|
10482
10830
|
await sleep(this.opts.retryDelayForAttempt?.(retryAttempt++, baseMs) ?? baseMs, signal);
|
|
@@ -10485,6 +10833,8 @@ var LongPollClient = class {
|
|
|
10485
10833
|
this.opts.onOperationalOutcome?.("success");
|
|
10486
10834
|
}
|
|
10487
10835
|
} catch (err) {
|
|
10836
|
+
readCursor = void 0;
|
|
10837
|
+
readAheadSupported = false;
|
|
10488
10838
|
this.opts.onServerCapabilitiesInvalidated?.();
|
|
10489
10839
|
if (this.noteRevoked(err)) return;
|
|
10490
10840
|
if (!this.running || signal.aborted) return;
|
|
@@ -10537,6 +10887,7 @@ function createConnectionHelloEnvelope(opts, getCursor) {
|
|
|
10537
10887
|
productId: opts.productId,
|
|
10538
10888
|
clientVersion: opts.clientVersion,
|
|
10539
10889
|
runtimes: opts.runtimes,
|
|
10890
|
+
harnesses: opts.harnesses,
|
|
10540
10891
|
configuredToolsets: configuredToolsets === void 0 ? void 0 : [...configuredToolsets],
|
|
10541
10892
|
cursor: getCursor()
|
|
10542
10893
|
});
|
|
@@ -10550,8 +10901,8 @@ var ConnectionManager = class {
|
|
|
10550
10901
|
serverUrl: opts.serverUrl,
|
|
10551
10902
|
auth: opts.auth,
|
|
10552
10903
|
// The long-poll query cursor is also the kernel's irreversible ack.
|
|
10553
|
-
// Only report the successfully
|
|
10554
|
-
//
|
|
10904
|
+
// Only report the successfully persisted cursor; using received work
|
|
10905
|
+
// on the wire would ack an
|
|
10555
10906
|
// in-flight envelope before its handler settles and make a later
|
|
10556
10907
|
// failure impossible to redeliver.
|
|
10557
10908
|
getCursor: () => this.cursor,
|
|
@@ -10600,25 +10951,11 @@ var ConnectionManager = class {
|
|
|
10600
10951
|
* guarantees in docs/protocol.md §9.
|
|
10601
10952
|
*/
|
|
10602
10953
|
stalledAtSeq;
|
|
10603
|
-
/**
|
|
10604
|
-
* Design A (Wave 2, F3-on-long-poll): the second, in-memory watermark
|
|
10605
|
-
* alongside the durable `cursor`. `cursor` only ever advances AFTER a
|
|
10606
|
-
* `task.*` handler's side effects resolve successfully, and is persisted
|
|
10607
|
-
* (see `advanceCursor`) — that semantics is unchanged. `deliveredSeq`
|
|
10608
|
-
* advances eagerly, the instant a `task.*` envelope is admitted past
|
|
10609
|
-
* dedup (see `deliver`/`noteDelivered`), independent of whether its
|
|
10610
|
-
* handler has even started, let alone succeeded. It exists so a repeated
|
|
10611
|
-
* read at the durable cursor does not re-dispatch an envelope already in
|
|
10612
|
-
* flight — `handleOffer` must not start a second adapter session while a
|
|
10613
|
-
* first attempt is still running. On WS this same field is written the
|
|
10614
|
-
* same way, but since a live WS connection only ever pushes a given `seq`
|
|
10615
|
-
* once, it never has an observable effect there beyond mirroring
|
|
10616
|
-
* `cursor` (see `dedupWatermark`'s doc comment for why redelivery
|
|
10617
|
-
* correctness doesn't depend on resetting it anywhere).
|
|
10618
|
-
*/
|
|
10619
|
-
deliveredSeq;
|
|
10620
|
-
/** Finding F3: serializes `onEnvelope` calls into a per-connection FIFO — one envelope's handler always fully settles before the next one starts. */
|
|
10954
|
+
/** Drain barrier for independently running handlers; not an admission lock. */
|
|
10621
10955
|
processingChain = Promise.resolve();
|
|
10956
|
+
cursorInitialization;
|
|
10957
|
+
completionTail = Promise.resolve();
|
|
10958
|
+
controlTails = /* @__PURE__ */ new Map();
|
|
10622
10959
|
/**
|
|
10623
10960
|
* Design B (finding N4): the ONE outbound queue holds `Envelope` OBJECTS,
|
|
10624
10961
|
* never re-encoded/rebuilt strings, so a
|
|
@@ -10644,38 +10981,13 @@ var ConnectionManager = class {
|
|
|
10644
10981
|
terminalError;
|
|
10645
10982
|
settledWaiters = [];
|
|
10646
10983
|
pendingCursorSave = Promise.resolve();
|
|
10647
|
-
/**
|
|
10648
|
-
* Finding P2 (Fix 2b): seqs currently admitted into `processingChain` but
|
|
10649
|
-
* not yet settled — added in `deliver()` the moment a `task.*` envelope is
|
|
10650
|
-
* accepted past the ordinary watermark check, removed in `process()`'s
|
|
10651
|
-
* `finally` once that specific attempt resolves (success OR failure).
|
|
10652
|
-
* While stalled, `dedupWatermark()` deliberately stays frozen below
|
|
10653
|
-
* already-delivered seqs (see its own doc comment) so the failed seq's own
|
|
10654
|
-
* redelivery can get through — but that same frozen watermark also means
|
|
10655
|
-
* every OTHER seq above it rides along on every re-poll too. Without this,
|
|
10656
|
-
* a seq already mid-flight (e.g. a `task.offer` whose prepared operation start()
|
|
10657
|
-
* hasn't resolved yet) would be re-enqueued into `processingChain` on
|
|
10658
|
-
* every such re-poll, piling up duplicate copies that — once the first
|
|
10659
|
-
* finally resolves and the chain unwinds through them — run its handler
|
|
10660
|
-
* again; for `task.offer` specifically, a second adapter session
|
|
10661
|
-
* orphaning the first (`TaskRunner`'s own `this.tasks.has` guard, finding
|
|
10662
|
-
* P2c, is the second, independent layer against exactly that).
|
|
10663
|
-
*/
|
|
10984
|
+
/** Admission dedup while an individual handler is running. */
|
|
10664
10985
|
inFlightSeqs = /* @__PURE__ */ new Set();
|
|
10665
|
-
/**
|
|
10666
|
-
* Finding P2 (Fix 2b): seqs whose handler has already resolved
|
|
10667
|
-
* successfully at least once this session, tracked only while a stall is
|
|
10668
|
-
* in effect — cleared the moment `stalledAtSeq` itself clears (see
|
|
10669
|
-
* `process()`), since once unstalled the ordinary watermark check via
|
|
10670
|
-
* `deliveredSeq` already covers everything delivered so far, making this
|
|
10671
|
-
* redundant. Needed because the stall-gap-prevention rule in `process()`
|
|
10672
|
-
* deliberately does NOT advance `cursor` past a seq above the
|
|
10673
|
-
* still-unresolved `stalledAtSeq`, even once that seq's own handler
|
|
10674
|
-
* succeeds — so `dedupWatermark()` alone can't distinguish "already
|
|
10675
|
-
* succeeded, don't re-run" from "never yet attempted" for anything in
|
|
10676
|
-
* that gap.
|
|
10677
|
-
*/
|
|
10986
|
+
/** Successful side effects retained until their durable acknowledgement. */
|
|
10678
10987
|
processedSeqs = /* @__PURE__ */ new Set();
|
|
10988
|
+
failedEnvelopes = /* @__PURE__ */ new Map();
|
|
10989
|
+
/** Received work lacking a successful handler receipt, including malformed frames. */
|
|
10990
|
+
unresolvedSeqs = /* @__PURE__ */ new Set();
|
|
10679
10991
|
/**
|
|
10680
10992
|
* Finding P3: the pending `drainOutbox` long-poll retry backoff, if any —
|
|
10681
10993
|
* cancellable so `enterRevoked()` can unblock it immediately instead of
|
|
@@ -10968,8 +11280,8 @@ var ConnectionManager = class {
|
|
|
10968
11280
|
* - F3 (at-most-once): the old code persisted the cursor advance BEFORE
|
|
10969
11281
|
* `onEnvelope` even ran (fire-and-forget) — a handler that then failed
|
|
10970
11282
|
* left a redelivery-proof envelope permanently marked processed. Inbound
|
|
10971
|
-
*
|
|
10972
|
-
*
|
|
11283
|
+
* handlers may run independently; their receipt commits are serialized.
|
|
11284
|
+
* The cursor only advances
|
|
10973
11285
|
* AFTER the handler resolves successfully; a rejection leaves the
|
|
10974
11286
|
* cursor where it was (see `stalledAtSeq`), so a future reconnect's
|
|
10975
11287
|
* redelivery re-attempts it — safe because every server->daemon type is
|
|
@@ -10985,62 +11297,72 @@ var ConnectionManager = class {
|
|
|
10985
11297
|
if (tracked && watermark !== void 0 && envelope.seq <= watermark) return false;
|
|
10986
11298
|
if (tracked) {
|
|
10987
11299
|
const seq = envelope.seq;
|
|
10988
|
-
if (this.inFlightSeqs.has(seq)
|
|
11300
|
+
if (this.inFlightSeqs.has(seq)) return false;
|
|
11301
|
+
if (!this.processedSeqs.has(seq)) this.unresolvedSeqs.add(seq);
|
|
10989
11302
|
this.inFlightSeqs.add(seq);
|
|
10990
|
-
this.noteDelivered(seq);
|
|
10991
11303
|
}
|
|
10992
|
-
|
|
11304
|
+
const controlKey = envelope.task_id !== void 0 && !envelope.type.startsWith("task.offer") ? envelope.task_id : void 0;
|
|
11305
|
+
const prior = controlKey === void 0 ? Promise.resolve() : this.controlTails.get(controlKey) ?? Promise.resolve();
|
|
11306
|
+
const operation = prior.then(() => this.process(envelope, tracked));
|
|
11307
|
+
if (controlKey !== void 0) {
|
|
11308
|
+
this.controlTails.set(controlKey, operation);
|
|
11309
|
+
void operation.finally(() => {
|
|
11310
|
+
if (this.controlTails.get(controlKey) === operation) this.controlTails.delete(controlKey);
|
|
11311
|
+
});
|
|
11312
|
+
}
|
|
11313
|
+
this.processingChain = Promise.all([this.processingChain, operation]).then(() => void 0);
|
|
10993
11314
|
return true;
|
|
10994
11315
|
}
|
|
10995
|
-
/**
|
|
10996
|
-
*
|
|
10997
|
-
|
|
10998
|
-
|
|
10999
|
-
|
|
11000
|
-
|
|
11001
|
-
* reaches `advanceCursor` already passed through `noteDelivered` first,
|
|
11002
|
-
* see `deliver`) — so this is the literal `max(cursor, deliveredSeq)` the
|
|
11003
|
-
* design calls for, just expressed via that invariant rather than an
|
|
11004
|
-
* explicit `Math.max`.
|
|
11005
|
-
*
|
|
11006
|
-
* While `stalledAtSeq` is set, this collapses to the durable `cursor`
|
|
11007
|
-
* alone, deliberately ignoring however far `deliveredSeq` had already run
|
|
11008
|
-
* ahead before the failure was known: that's what lets the stalled
|
|
11009
|
-
* envelope's own redelivery (and everything after it, right up to a
|
|
11010
|
-
* fresh success) get past this same dedup check instead of being
|
|
11011
|
-
* self-deduped by the client's own earlier eager tracking of envelopes
|
|
11012
|
-
* whose outcome wasn't known yet. No separate "reset deliveredSeq on
|
|
11013
|
-
* reconnect" step is needed for this to be correct — collapsing to
|
|
11014
|
-
* `cursor` exactly while stalled already produces the right answer on
|
|
11015
|
-
* every long-poll retry path. NOT resetting it unconditionally on every
|
|
11016
|
-
* retry lets `deliveredSeq` keep doing its job of not re-dispatching
|
|
11017
|
-
* something already in flight while a handler is still running.
|
|
11018
|
-
*/
|
|
11019
|
-
dedupWatermark() {
|
|
11020
|
-
return this.stalledAtSeq !== void 0 ? this.cursor : this.deliveredSeq ?? this.cursor;
|
|
11316
|
+
/** A committed terminal can settle a failed offer receipt even if cloud
|
|
11317
|
+
* cancellation has since filtered that offer from mailbox replay. */
|
|
11318
|
+
retryTaskReceipts(taskId) {
|
|
11319
|
+
for (const envelope of this.failedEnvelopes.values()) {
|
|
11320
|
+
if (envelope.task_id === taskId) this.deliver(envelope);
|
|
11321
|
+
}
|
|
11021
11322
|
}
|
|
11022
|
-
/**
|
|
11023
|
-
|
|
11024
|
-
|
|
11323
|
+
/** Only a durable acknowledgement can deduplicate a whole prefix. */
|
|
11324
|
+
dedupWatermark() {
|
|
11325
|
+
return this.cursor;
|
|
11025
11326
|
}
|
|
11026
11327
|
async process(envelope, tracked) {
|
|
11027
11328
|
const seq = tracked ? envelope.seq : void 0;
|
|
11028
11329
|
try {
|
|
11330
|
+
if (tracked && seq <= (this.cursor ?? 0)) return;
|
|
11029
11331
|
if (tracked && this.cursor === void 0) {
|
|
11030
|
-
|
|
11031
|
-
|
|
11332
|
+
this.cursorInitialization ??= this.opts.cursorStore.save(this.opts.serverUrl, this.opts.deviceId, 0).then(() => {
|
|
11333
|
+
this.cursor = 0;
|
|
11334
|
+
}).catch((error) => {
|
|
11335
|
+
this.cursorInitialization = void 0;
|
|
11336
|
+
throw error;
|
|
11337
|
+
});
|
|
11338
|
+
await this.cursorInitialization;
|
|
11032
11339
|
}
|
|
11033
|
-
await this.opts.onEnvelope(envelope);
|
|
11340
|
+
if (!tracked || !this.processedSeqs.has(seq)) await this.opts.onEnvelope(envelope);
|
|
11034
11341
|
if (!tracked) return;
|
|
11035
|
-
|
|
11036
|
-
|
|
11037
|
-
|
|
11038
|
-
|
|
11039
|
-
|
|
11040
|
-
|
|
11041
|
-
|
|
11342
|
+
this.failedEnvelopes.delete(seq);
|
|
11343
|
+
this.processedSeqs.add(seq);
|
|
11344
|
+
this.unresolvedSeqs.delete(seq);
|
|
11345
|
+
const completion = this.completionTail.catch(() => void 0).then(async () => {
|
|
11346
|
+
let firstUnresolved = Infinity;
|
|
11347
|
+
for (const value of this.unresolvedSeqs) firstUnresolved = Math.min(firstUnresolved, value);
|
|
11348
|
+
let prefix = this.cursor ?? 0;
|
|
11349
|
+
for (const value of this.processedSeqs) if (value < firstUnresolved) prefix = Math.max(prefix, value);
|
|
11350
|
+
if (prefix > (this.cursor ?? 0)) await this.advanceCursor(prefix);
|
|
11351
|
+
for (const value of this.processedSeqs) {
|
|
11352
|
+
if (value <= (this.cursor ?? 0)) {
|
|
11353
|
+
this.processedSeqs.delete(value);
|
|
11354
|
+
this.failedEnvelopes.delete(value);
|
|
11355
|
+
}
|
|
11356
|
+
}
|
|
11357
|
+
this.stalledAtSeq = this.unresolvedSeqs.size > 0 ? firstUnresolved : void 0;
|
|
11358
|
+
});
|
|
11359
|
+
this.completionTail = completion;
|
|
11360
|
+
await completion;
|
|
11042
11361
|
} catch (err) {
|
|
11043
|
-
if (tracked
|
|
11362
|
+
if (tracked) {
|
|
11363
|
+
this.failedEnvelopes.set(seq, envelope);
|
|
11364
|
+
this.stalledAtSeq = Math.min(envelope.seq, this.stalledAtSeq ?? Infinity);
|
|
11365
|
+
}
|
|
11044
11366
|
console.error(
|
|
11045
11367
|
`[byok/client] envelope handler failed for ${envelope.type}${typeof envelope.seq === "number" ? ` (seq=${envelope.seq})` : ""}; cursor left unadvanced so a reconnect redelivers it:`,
|
|
11046
11368
|
err
|
|
@@ -11049,11 +11371,10 @@ var ConnectionManager = class {
|
|
|
11049
11371
|
if (tracked) this.inFlightSeqs.delete(seq);
|
|
11050
11372
|
}
|
|
11051
11373
|
}
|
|
11052
|
-
/**
|
|
11374
|
+
/** Recorded synchronously at receive, before a later successful receipt can commit. */
|
|
11053
11375
|
noteValidationFailure(seq) {
|
|
11054
|
-
this.
|
|
11055
|
-
|
|
11056
|
-
});
|
|
11376
|
+
this.unresolvedSeqs.add(seq);
|
|
11377
|
+
this.stalledAtSeq = Math.min(seq, this.stalledAtSeq ?? seq);
|
|
11057
11378
|
}
|
|
11058
11379
|
async advanceCursor(seq) {
|
|
11059
11380
|
if (this.cursor !== void 0 && seq <= this.cursor) return;
|
|
@@ -11288,6 +11609,29 @@ var OperationalHealthTracker = class {
|
|
|
11288
11609
|
}
|
|
11289
11610
|
}
|
|
11290
11611
|
};
|
|
11612
|
+
async function inspectOperationalHealthHandle(handle, expected) {
|
|
11613
|
+
const read = await readOperationalHealthHandle(handle, expected);
|
|
11614
|
+
if (read.status !== "read") return read;
|
|
11615
|
+
const { raw, sizeBytes } = read;
|
|
11616
|
+
let parsed;
|
|
11617
|
+
try {
|
|
11618
|
+
parsed = JSON.parse(raw);
|
|
11619
|
+
} catch {
|
|
11620
|
+
return { status: "corrupt", sizeBytes, reason: "operational health state is corrupt JSON" };
|
|
11621
|
+
}
|
|
11622
|
+
if (!isHealthFile(parsed)) {
|
|
11623
|
+
return { status: "corrupt", sizeBytes, reason: "operational health state has an invalid shape" };
|
|
11624
|
+
}
|
|
11625
|
+
return {
|
|
11626
|
+
status: "valid",
|
|
11627
|
+
sizeBytes,
|
|
11628
|
+
state: parsed.state,
|
|
11629
|
+
failureCount: parsed.failures.length,
|
|
11630
|
+
crashCount: parsed.crashes.length,
|
|
11631
|
+
...parsed.currentRun ? { currentRunStartedAt: new Date(Date.parse(parsed.currentRun.startedAt)).toISOString() } : {},
|
|
11632
|
+
...parsed.crashes.at(-1) ? { lastCrashAt: new Date(Date.parse(parsed.crashes.at(-1).detectedAt)).toISOString() } : {}
|
|
11633
|
+
};
|
|
11634
|
+
}
|
|
11291
11635
|
async function readOperationalHealthHandle(handle, expected) {
|
|
11292
11636
|
let before;
|
|
11293
11637
|
try {
|
|
@@ -11321,6 +11665,20 @@ async function readOperationalHealthHandle(handle, expected) {
|
|
|
11321
11665
|
return { status: "unavailable", sizeBytes, reason: "operational health state could not be read" };
|
|
11322
11666
|
}
|
|
11323
11667
|
}
|
|
11668
|
+
async function inspectOperationalHealthFile(storeDir) {
|
|
11669
|
+
let opened;
|
|
11670
|
+
try {
|
|
11671
|
+
opened = await openOperationalHealthFile(storeDir);
|
|
11672
|
+
} catch {
|
|
11673
|
+
return { status: "unavailable", reason: "operational health state could not be opened safely" };
|
|
11674
|
+
}
|
|
11675
|
+
if (!opened) return { status: "missing" };
|
|
11676
|
+
try {
|
|
11677
|
+
return await inspectOperationalHealthHandle(opened.handle, opened.stat);
|
|
11678
|
+
} finally {
|
|
11679
|
+
await opened.handle.close();
|
|
11680
|
+
}
|
|
11681
|
+
}
|
|
11324
11682
|
function isHealthFile(value) {
|
|
11325
11683
|
if (typeof value !== "object" || value === null) return false;
|
|
11326
11684
|
const candidate = value;
|
|
@@ -11386,7 +11744,7 @@ var CursorStore = class {
|
|
|
11386
11744
|
async save(serverUrl, deviceId, cursor) {
|
|
11387
11745
|
const file = this.fileFor(serverUrl, deviceId);
|
|
11388
11746
|
await promises.mkdir(path3__default.dirname(file), { recursive: true, mode: 448 });
|
|
11389
|
-
await atomicWriteFile(file, JSON.stringify({ cursor }));
|
|
11747
|
+
await atomicWriteFile(file, JSON.stringify({ cursor }), { fsync: true });
|
|
11390
11748
|
}
|
|
11391
11749
|
/** Remove any persisted cursor for (serverUrl, deviceId) — a no-op if none exists. Called from `pair()` (finding F5) so a device that's about to be replaced never leaves a cursor a future, unrelated device could somehow inherit. */
|
|
11392
11750
|
async clear(serverUrl, deviceId) {
|
|
@@ -11879,9 +12237,9 @@ function isSqliteAvailable() {
|
|
|
11879
12237
|
}
|
|
11880
12238
|
}
|
|
11881
12239
|
var SECURE_FILE_MODE = 384;
|
|
11882
|
-
function openJournalDatabase(
|
|
12240
|
+
function openJournalDatabase(path43, busyTimeoutMs, faults) {
|
|
11883
12241
|
const { DatabaseSync } = loadSqliteModule();
|
|
11884
|
-
const db = new DatabaseSync(
|
|
12242
|
+
const db = new DatabaseSync(path43, { timeout: busyTimeoutMs });
|
|
11885
12243
|
try {
|
|
11886
12244
|
faults?.onStep?.("after-open");
|
|
11887
12245
|
db.exec("PRAGMA auto_vacuum = INCREMENTAL;");
|
|
@@ -12025,9 +12383,9 @@ var RECEIVED_STATE = "received";
|
|
|
12025
12383
|
function byteLength2(value) {
|
|
12026
12384
|
return Buffer.byteLength(value, "utf8");
|
|
12027
12385
|
}
|
|
12028
|
-
function fileBytes(
|
|
12386
|
+
function fileBytes(path43) {
|
|
12029
12387
|
try {
|
|
12030
|
-
return statSync(
|
|
12388
|
+
return statSync(path43).size;
|
|
12031
12389
|
} catch {
|
|
12032
12390
|
return 0;
|
|
12033
12391
|
}
|
|
@@ -13370,6 +13728,109 @@ async function spillOversizedEvent(event, deps) {
|
|
|
13370
13728
|
return bounded2.result;
|
|
13371
13729
|
}
|
|
13372
13730
|
|
|
13731
|
+
// src/daemon/admission-wait.ts
|
|
13732
|
+
async function awaitAdmission(operation, signal) {
|
|
13733
|
+
signal.throwIfAborted();
|
|
13734
|
+
let onAbort;
|
|
13735
|
+
const aborted = new Promise((_, reject) => {
|
|
13736
|
+
onAbort = () => reject(signal.reason);
|
|
13737
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
13738
|
+
});
|
|
13739
|
+
try {
|
|
13740
|
+
return await Promise.race([Promise.resolve().then(() => {
|
|
13741
|
+
signal.throwIfAborted();
|
|
13742
|
+
return operation();
|
|
13743
|
+
}), aborted]);
|
|
13744
|
+
} finally {
|
|
13745
|
+
signal.removeEventListener("abort", onAbort);
|
|
13746
|
+
}
|
|
13747
|
+
}
|
|
13748
|
+
|
|
13749
|
+
// src/daemon/runtime-start.ts
|
|
13750
|
+
async function startOwnedRuntime(start, input, signal, timeoutMs) {
|
|
13751
|
+
const controller = new AbortController();
|
|
13752
|
+
let session;
|
|
13753
|
+
let failure;
|
|
13754
|
+
let settled = false;
|
|
13755
|
+
const retryDisposal = async () => {
|
|
13756
|
+
if (session) {
|
|
13757
|
+
let timer2;
|
|
13758
|
+
try {
|
|
13759
|
+
await Promise.race([
|
|
13760
|
+
Promise.resolve().then(() => session.interrupt()).catch(() => void 0),
|
|
13761
|
+
new Promise((resolve) => {
|
|
13762
|
+
timer2 = setTimeout(resolve, 1e3);
|
|
13763
|
+
timer2.unref?.();
|
|
13764
|
+
})
|
|
13765
|
+
]);
|
|
13766
|
+
} finally {
|
|
13767
|
+
if (timer2) clearTimeout(timer2);
|
|
13768
|
+
}
|
|
13769
|
+
return session.close();
|
|
13770
|
+
}
|
|
13771
|
+
if (isRuntimeStartupDisposalFailure(failure)) return failure.retryDisposal();
|
|
13772
|
+
if (settled && isRuntimeExecutionFailure(failure)) return;
|
|
13773
|
+
throw new RuntimeDisposalFailure({ stage: "quiescence", reason: "runtime startup has not returned a cleanup receipt" });
|
|
13774
|
+
};
|
|
13775
|
+
let rejectAbort;
|
|
13776
|
+
const aborted = new Promise((_, reject) => {
|
|
13777
|
+
rejectAbort = reject;
|
|
13778
|
+
});
|
|
13779
|
+
const abort = () => {
|
|
13780
|
+
controller.abort();
|
|
13781
|
+
rejectAbort(new RuntimeStartupDisposalFailure(retryDisposal));
|
|
13782
|
+
};
|
|
13783
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
13784
|
+
const timer = setTimeout(abort, timeoutMs);
|
|
13785
|
+
timer.unref?.();
|
|
13786
|
+
try {
|
|
13787
|
+
if (signal.aborted) {
|
|
13788
|
+
throw new RuntimeDisposalFailure({ stage: "quiescence", reason: "runtime startup was cancelled before invocation" });
|
|
13789
|
+
}
|
|
13790
|
+
const startup = Promise.resolve().then(() => start({ ...input, signal: controller.signal })).then((value) => {
|
|
13791
|
+
session = value;
|
|
13792
|
+
settled = true;
|
|
13793
|
+
return value;
|
|
13794
|
+
}, (error) => {
|
|
13795
|
+
failure = error;
|
|
13796
|
+
settled = true;
|
|
13797
|
+
throw error;
|
|
13798
|
+
});
|
|
13799
|
+
return await Promise.race([startup, aborted]);
|
|
13800
|
+
} finally {
|
|
13801
|
+
clearTimeout(timer);
|
|
13802
|
+
signal.removeEventListener("abort", abort);
|
|
13803
|
+
}
|
|
13804
|
+
}
|
|
13805
|
+
|
|
13806
|
+
// src/daemon/artifact-read.ts
|
|
13807
|
+
var DEFAULT_ARTIFACT_LIMITS = Object.freeze({
|
|
13808
|
+
maxFileBytes: 16 * 1024 * 1024,
|
|
13809
|
+
maxTaskBytes: 64 * 1024 * 1024
|
|
13810
|
+
});
|
|
13811
|
+
async function readArtifactBytes(handle, maxBytes, signal, account) {
|
|
13812
|
+
signal.throwIfAborted();
|
|
13813
|
+
const stat = await handle.stat();
|
|
13814
|
+
if (stat.size > maxBytes) throw new Error("artifact local byte budget exceeded");
|
|
13815
|
+
const chunks = [];
|
|
13816
|
+
let total = 0;
|
|
13817
|
+
const stream = handle.createReadStream({ autoClose: false, start: 0, signal, highWaterMark: 64 * 1024 });
|
|
13818
|
+
try {
|
|
13819
|
+
for await (const data of stream) {
|
|
13820
|
+
signal.throwIfAborted();
|
|
13821
|
+
const chunk = data;
|
|
13822
|
+
account(chunk.length);
|
|
13823
|
+
total += chunk.length;
|
|
13824
|
+
if (total > maxBytes) throw new Error("artifact local byte budget exceeded");
|
|
13825
|
+
chunks.push(chunk);
|
|
13826
|
+
}
|
|
13827
|
+
signal.throwIfAborted();
|
|
13828
|
+
return Buffer.concat(chunks, total);
|
|
13829
|
+
} finally {
|
|
13830
|
+
stream.destroy();
|
|
13831
|
+
}
|
|
13832
|
+
}
|
|
13833
|
+
|
|
13373
13834
|
// src/daemon/environment.ts
|
|
13374
13835
|
var BASE_PLATFORM_ALLOWLIST = [
|
|
13375
13836
|
"PATH",
|
|
@@ -13569,45 +14030,121 @@ var ProgressBatcher = class {
|
|
|
13569
14030
|
}
|
|
13570
14031
|
}
|
|
13571
14032
|
};
|
|
13572
|
-
var
|
|
13573
|
-
|
|
13574
|
-
|
|
13575
|
-
constructor(message) {
|
|
13576
|
-
super(message);
|
|
13577
|
-
this.name = "AgentMessageOutboxError";
|
|
14033
|
+
var DurableJsonlFile = class {
|
|
14034
|
+
constructor(filePath) {
|
|
14035
|
+
this.filePath = filePath;
|
|
13578
14036
|
}
|
|
13579
|
-
|
|
13580
|
-
|
|
13581
|
-
|
|
13582
|
-
|
|
13583
|
-
|
|
13584
|
-
}
|
|
13585
|
-
function hashBody(body) {
|
|
13586
|
-
return `sha256:${createHash("sha256").update(body, "utf8").digest("hex")}`;
|
|
13587
|
-
}
|
|
13588
|
-
function sameAgentRef(left, right) {
|
|
13589
|
-
return left.agentId === right.agentId && left.profileRevision === right.profileRevision;
|
|
13590
|
-
}
|
|
13591
|
-
function exactDisposition(record3, value) {
|
|
13592
|
-
return value.messageId === record3.messageId && value.cursor === record3.cursor && value.contract === record3.contract && value.contentHash === record3.contentHash && value.sessionRef === record3.sessionRef && sameAgentRef(value.agentRef, record3.agentRef);
|
|
13593
|
-
}
|
|
13594
|
-
var AgentMessageOutbox = class _AgentMessageOutbox {
|
|
13595
|
-
constructor(homeDir, outboxPath) {
|
|
13596
|
-
this.homeDir = homeDir;
|
|
13597
|
-
this.outboxPath = outboxPath;
|
|
14037
|
+
filePath;
|
|
14038
|
+
failure;
|
|
14039
|
+
assertWritable() {
|
|
14040
|
+
if (this.failure !== void 0) {
|
|
14041
|
+
throw new Error("durable JSONL writer is quarantined after uncertain I/O; reopen and validate the log before retrying", { cause: this.failure });
|
|
14042
|
+
}
|
|
13598
14043
|
}
|
|
13599
|
-
|
|
13600
|
-
|
|
13601
|
-
|
|
13602
|
-
|
|
13603
|
-
|
|
13604
|
-
|
|
13605
|
-
|
|
13606
|
-
|
|
14044
|
+
/** Replay is not a durability receipt: confirm the recovered bytes before use. */
|
|
14045
|
+
async confirmRecovered() {
|
|
14046
|
+
let handle;
|
|
14047
|
+
try {
|
|
14048
|
+
handle = await promises.open(this.filePath, "r+");
|
|
14049
|
+
} catch (error) {
|
|
14050
|
+
if (error.code === "ENOENT") return;
|
|
14051
|
+
throw error;
|
|
14052
|
+
}
|
|
14053
|
+
try {
|
|
14054
|
+
await handle.sync();
|
|
14055
|
+
} finally {
|
|
14056
|
+
await handle.close();
|
|
14057
|
+
}
|
|
14058
|
+
await this.syncDirectory();
|
|
14059
|
+
}
|
|
14060
|
+
async append(json) {
|
|
14061
|
+
this.assertWritable();
|
|
14062
|
+
try {
|
|
14063
|
+
const handle = await promises.open(this.filePath, constants.O_APPEND | constants.O_CREAT | constants.O_WRONLY, 384);
|
|
14064
|
+
try {
|
|
14065
|
+
const bytes = Buffer.from(`${json}
|
|
14066
|
+
`, "utf8");
|
|
14067
|
+
let offset = 0;
|
|
14068
|
+
while (offset < bytes.length) {
|
|
14069
|
+
const { bytesWritten } = await handle.write(bytes, offset, bytes.length - offset);
|
|
14070
|
+
if (bytesWritten <= 0) throw new Error("durable JSONL append made no progress");
|
|
14071
|
+
offset += bytesWritten;
|
|
14072
|
+
}
|
|
14073
|
+
await handle.sync();
|
|
14074
|
+
} finally {
|
|
14075
|
+
await handle.close();
|
|
14076
|
+
}
|
|
14077
|
+
await this.syncDirectory();
|
|
14078
|
+
} catch (error) {
|
|
14079
|
+
this.failure = error;
|
|
14080
|
+
throw error;
|
|
14081
|
+
}
|
|
14082
|
+
}
|
|
14083
|
+
async replace(lines) {
|
|
14084
|
+
this.assertWritable();
|
|
14085
|
+
try {
|
|
14086
|
+
await atomicWriteFile(this.filePath, lines.map((line) => `${line}
|
|
14087
|
+
`).join(""), { mode: 384, fsync: true });
|
|
14088
|
+
} catch (error) {
|
|
14089
|
+
this.failure = error;
|
|
14090
|
+
throw error;
|
|
14091
|
+
}
|
|
14092
|
+
}
|
|
14093
|
+
async syncDirectory() {
|
|
14094
|
+
if (process.platform === "win32") return;
|
|
14095
|
+
const directory = await promises.open(path3__default.dirname(this.filePath), "r");
|
|
14096
|
+
try {
|
|
14097
|
+
await directory.sync();
|
|
14098
|
+
} finally {
|
|
14099
|
+
await directory.close();
|
|
14100
|
+
}
|
|
14101
|
+
}
|
|
14102
|
+
};
|
|
14103
|
+
|
|
14104
|
+
// src/daemon/agent-message-outbox.ts
|
|
14105
|
+
var AGENT_MESSAGE_DIRECTORY = path3__default.join(".byok", "messages");
|
|
14106
|
+
var AGENT_MESSAGE_OUTBOX_FILENAME = "outbox-v1.jsonl";
|
|
14107
|
+
var AgentMessageOutboxError = class extends Error {
|
|
14108
|
+
constructor(message) {
|
|
14109
|
+
super(message);
|
|
14110
|
+
this.name = "AgentMessageOutboxError";
|
|
14111
|
+
}
|
|
14112
|
+
};
|
|
14113
|
+
function stableJson(value) {
|
|
14114
|
+
const encoded = JSON.stringify(value);
|
|
14115
|
+
if (encoded === void 0) throw new AgentMessageOutboxError("message outbox value is not serializable");
|
|
14116
|
+
return encoded;
|
|
14117
|
+
}
|
|
14118
|
+
function hashBody(body) {
|
|
14119
|
+
return `sha256:${createHash("sha256").update(body, "utf8").digest("hex")}`;
|
|
14120
|
+
}
|
|
14121
|
+
function sameAgentRef(left, right) {
|
|
14122
|
+
return left.agentId === right.agentId && left.profileRevision === right.profileRevision;
|
|
14123
|
+
}
|
|
14124
|
+
function exactDisposition(record3, value) {
|
|
14125
|
+
return value.messageId === record3.messageId && value.cursor === record3.cursor && value.contract === record3.contract && value.contentHash === record3.contentHash && value.sessionRef === record3.sessionRef && sameAgentRef(value.agentRef, record3.agentRef);
|
|
14126
|
+
}
|
|
14127
|
+
var AgentMessageOutbox = class _AgentMessageOutbox {
|
|
14128
|
+
constructor(homeDir, outboxPath) {
|
|
14129
|
+
this.homeDir = homeDir;
|
|
14130
|
+
this.outboxPath = outboxPath;
|
|
14131
|
+
this.file = new DurableJsonlFile(outboxPath);
|
|
14132
|
+
}
|
|
14133
|
+
homeDir;
|
|
14134
|
+
outboxPath;
|
|
14135
|
+
pendingByTask = /* @__PURE__ */ new Map();
|
|
14136
|
+
revokedTasks = /* @__PURE__ */ new Set();
|
|
14137
|
+
dispositionByTask = /* @__PURE__ */ new Map();
|
|
14138
|
+
file;
|
|
14139
|
+
nextCursor = 1;
|
|
14140
|
+
logEntries = 0;
|
|
14141
|
+
writeTail = Promise.resolve();
|
|
14142
|
+
static async open(homeDir) {
|
|
13607
14143
|
const directory = path3__default.join(homeDir, AGENT_MESSAGE_DIRECTORY);
|
|
13608
14144
|
await ensureSecureDir(directory);
|
|
13609
14145
|
const outbox = new _AgentMessageOutbox(homeDir, path3__default.join(directory, AGENT_MESSAGE_OUTBOX_FILENAME));
|
|
13610
14146
|
await outbox.load();
|
|
14147
|
+
await outbox.file.confirmRecovered();
|
|
13611
14148
|
return outbox;
|
|
13612
14149
|
}
|
|
13613
14150
|
/** Re-open every existing Agent-local message outbox without following Agent-home symlinks. */
|
|
@@ -13648,9 +14185,9 @@ var AgentMessageOutbox = class _AgentMessageOutbox {
|
|
|
13648
14185
|
records() {
|
|
13649
14186
|
return Object.freeze([...this.pendingByTask.values()].sort((a, b) => a.cursor - b.cursor));
|
|
13650
14187
|
}
|
|
13651
|
-
/**
|
|
14188
|
+
/** Records without disposition or local revoke; transport replay additionally requires session binding. */
|
|
13652
14189
|
retryableRecords() {
|
|
13653
|
-
return Object.freeze(this.records().filter((record3) => !this.dispositionByTask.has(record3.taskId)));
|
|
14190
|
+
return Object.freeze(this.records().filter((record3) => !this.dispositionByTask.has(record3.taskId) && !this.revokedTasks.has(record3.taskId)));
|
|
13654
14191
|
}
|
|
13655
14192
|
get(taskId) {
|
|
13656
14193
|
return this.pendingByTask.get(taskId);
|
|
@@ -13662,13 +14199,14 @@ var AgentMessageOutbox = class _AgentMessageOutbox {
|
|
|
13662
14199
|
if (byteCount < 1 || byteCount > input.requirement.maxBytes) throw new AgentMessageOutboxError("message body exceeds the offer byte contract");
|
|
13663
14200
|
const existing = this.pendingByTask.get(input.taskId);
|
|
13664
14201
|
if (existing !== void 0) {
|
|
13665
|
-
if (existing.tenantId !== input.tenantId || existing.contentType !== input.contentType || existing.body !== input.body || existing.contract !== input.requirement.contract || !sameAgentRef(existing.agentRef, input.agentRef)) {
|
|
14202
|
+
if (existing.tenantId !== input.tenantId || existing.contentType !== input.contentType || existing.body !== input.body || existing.contract !== input.requirement.contract || input.sessionRef !== void 0 && existing.sessionRef !== input.sessionRef || !sameAgentRef(existing.agentRef, input.agentRef)) {
|
|
13666
14203
|
throw new AgentMessageOutboxError("required-message task already has a different immutable draft");
|
|
13667
14204
|
}
|
|
13668
14205
|
return existing;
|
|
13669
14206
|
}
|
|
13670
|
-
|
|
13671
|
-
|
|
14207
|
+
const sending = this.records().filter((record4) => !this.isTerminalEvidence(record4.taskId));
|
|
14208
|
+
if (sending.length >= input.maxPendingEvents) throw new AgentMessageOutboxError("message outbox event quota is exhausted");
|
|
14209
|
+
const pendingBytes = sending.reduce((sum, record4) => sum + record4.byteCount, 0);
|
|
13672
14210
|
if (pendingBytes + byteCount > input.maxPendingBytes) throw new AgentMessageOutboxError("message outbox byte quota is exhausted");
|
|
13673
14211
|
const record3 = Object.freeze({
|
|
13674
14212
|
schema: 1,
|
|
@@ -13690,8 +14228,9 @@ var AgentMessageOutbox = class _AgentMessageOutbox {
|
|
|
13690
14228
|
return record3;
|
|
13691
14229
|
});
|
|
13692
14230
|
}
|
|
13693
|
-
async activate(taskId, sessionRef) {
|
|
14231
|
+
async activate(taskId, sessionRef, isCurrent = () => true) {
|
|
13694
14232
|
return this.exclusive(async () => {
|
|
14233
|
+
if (!isCurrent() || this.revokedTasks.has(taskId)) return void 0;
|
|
13695
14234
|
const record3 = this.pendingByTask.get(taskId);
|
|
13696
14235
|
if (record3 === void 0) return void 0;
|
|
13697
14236
|
if (record3.sessionRef !== void 0) {
|
|
@@ -13704,6 +14243,22 @@ var AgentMessageOutbox = class _AgentMessageOutbox {
|
|
|
13704
14243
|
return activated;
|
|
13705
14244
|
});
|
|
13706
14245
|
}
|
|
14246
|
+
/** Local lifecycle cancellation, never a synthesized consumer disposition. */
|
|
14247
|
+
async revoke(taskId) {
|
|
14248
|
+
return this.exclusive(async () => {
|
|
14249
|
+
const record3 = this.pendingByTask.get(taskId);
|
|
14250
|
+
if (record3 === void 0 || this.revokedTasks.has(taskId)) return;
|
|
14251
|
+
await this.appendEntry({ schema: 1, kind: "revoke", taskId, messageId: record3.messageId });
|
|
14252
|
+
this.revokedTasks.add(taskId);
|
|
14253
|
+
});
|
|
14254
|
+
}
|
|
14255
|
+
/** The same terminal classification used by operator receipts and archival. */
|
|
14256
|
+
terminalRecords() {
|
|
14257
|
+
return Object.freeze(this.records().filter((record3) => this.isTerminalEvidence(record3.taskId)));
|
|
14258
|
+
}
|
|
14259
|
+
isTerminalEvidence(taskId) {
|
|
14260
|
+
return this.revokedTasks.has(taskId) || this.dispositionByTask.get(taskId)?.outcome === "refused";
|
|
14261
|
+
}
|
|
13707
14262
|
publishPayload(record3) {
|
|
13708
14263
|
if (record3.sessionRef === void 0) throw new AgentMessageOutboxError("message draft is not bound to a durable session");
|
|
13709
14264
|
return AgentMessagePublishPayloadSchema.parse({
|
|
@@ -13727,6 +14282,7 @@ var AgentMessageOutbox = class _AgentMessageOutbox {
|
|
|
13727
14282
|
await this.appendEntry({ schema: 1, kind: "disposition", taskId, disposition });
|
|
13728
14283
|
if (disposition.outcome === "accepted") {
|
|
13729
14284
|
this.pendingByTask.delete(taskId);
|
|
14285
|
+
this.revokedTasks.delete(taskId);
|
|
13730
14286
|
this.dispositionByTask.delete(taskId);
|
|
13731
14287
|
} else {
|
|
13732
14288
|
this.dispositionByTask.set(taskId, disposition);
|
|
@@ -13745,6 +14301,7 @@ var AgentMessageOutbox = class _AgentMessageOutbox {
|
|
|
13745
14301
|
if (error.code === "ENOENT") return;
|
|
13746
14302
|
throw error;
|
|
13747
14303
|
}
|
|
14304
|
+
if (raw.length > 0 && !raw.endsWith("\n")) throw new AgentMessageOutboxError("durable JSONL log has an incomplete trailing frame; preserve the file for explicit repair");
|
|
13748
14305
|
for (const line of raw.split("\n")) {
|
|
13749
14306
|
if (line.length === 0) continue;
|
|
13750
14307
|
const entry = JSON.parse(line);
|
|
@@ -13759,11 +14316,16 @@ var AgentMessageOutbox = class _AgentMessageOutbox {
|
|
|
13759
14316
|
if (record3 === void 0 || record3.messageId !== entry.messageId) throw new AgentMessageOutboxError("message activation has no exact draft");
|
|
13760
14317
|
if (record3.sessionRef !== void 0 && record3.sessionRef !== entry.sessionRef) throw new AgentMessageOutboxError("message activation session conflicts");
|
|
13761
14318
|
this.pendingByTask.set(entry.taskId, Object.freeze({ ...record3, sessionRef: entry.sessionRef }));
|
|
14319
|
+
} else if (entry.kind === "revoke") {
|
|
14320
|
+
const record3 = this.pendingByTask.get(entry.taskId);
|
|
14321
|
+
if (record3 === void 0 || record3.messageId !== entry.messageId) throw new AgentMessageOutboxError("message revocation has no exact draft");
|
|
14322
|
+
this.revokedTasks.add(entry.taskId);
|
|
13762
14323
|
} else if (entry.kind === "disposition") {
|
|
13763
14324
|
const record3 = this.pendingByTask.get(entry.taskId);
|
|
13764
14325
|
if (record3 === void 0 || !exactDisposition(record3, entry.disposition)) throw new AgentMessageOutboxError("message outbox contains a mismatched disposition");
|
|
13765
14326
|
if (entry.disposition.outcome === "accepted") {
|
|
13766
14327
|
this.pendingByTask.delete(entry.taskId);
|
|
14328
|
+
this.revokedTasks.delete(entry.taskId);
|
|
13767
14329
|
this.dispositionByTask.delete(entry.taskId);
|
|
13768
14330
|
} else {
|
|
13769
14331
|
this.dispositionByTask.set(entry.taskId, entry.disposition);
|
|
@@ -13774,23 +14336,47 @@ var AgentMessageOutbox = class _AgentMessageOutbox {
|
|
|
13774
14336
|
}
|
|
13775
14337
|
}
|
|
13776
14338
|
async appendEntry(entry) {
|
|
13777
|
-
|
|
13778
|
-
|
|
13779
|
-
await handle.write(`${stableJson(entry)}
|
|
13780
|
-
`, void 0, "utf8");
|
|
13781
|
-
await handle.sync();
|
|
13782
|
-
this.logEntries += 1;
|
|
13783
|
-
} finally {
|
|
13784
|
-
await handle.close();
|
|
13785
|
-
}
|
|
14339
|
+
await this.file.append(stableJson(entry));
|
|
14340
|
+
this.logEntries += 1;
|
|
13786
14341
|
}
|
|
13787
|
-
|
|
13788
|
-
|
|
14342
|
+
/**
|
|
14343
|
+
* Explicit operator maintenance under the Agent-home single-writer lease.
|
|
14344
|
+
* Copy complete terminal evidence durably before removing it from the live log.
|
|
14345
|
+
* Archives are audit artifacts, never another replay input. held remains live.
|
|
14346
|
+
*/
|
|
14347
|
+
async archiveTerminalRecords(archiveDirectory) {
|
|
14348
|
+
return this.exclusive(async () => {
|
|
14349
|
+
if (!path3__default.isAbsolute(archiveDirectory)) throw new AgentMessageOutboxError("message archive directory must be absolute");
|
|
14350
|
+
const terminal = this.terminalRecords();
|
|
14351
|
+
if (terminal.length === 0) return void 0;
|
|
14352
|
+
await ensureSecureDir(archiveDirectory);
|
|
14353
|
+
const archivePath = path3__default.join(archiveDirectory, `message-terminal-${randomUUID()}.jsonl`);
|
|
14354
|
+
await atomicWriteFile(archivePath, this.snapshotEntries(terminal).map((entry) => `${stableJson(entry)}
|
|
14355
|
+
`).join(""), { mode: 384, fsync: true });
|
|
14356
|
+
const retained = this.records().filter((record3) => !this.isTerminalEvidence(record3.taskId));
|
|
14357
|
+
const entries = this.snapshotEntries(retained);
|
|
14358
|
+
await this.file.replace(entries.map((entry) => stableJson(entry)));
|
|
14359
|
+
for (const record3 of terminal) {
|
|
14360
|
+
this.pendingByTask.delete(record3.taskId);
|
|
14361
|
+
this.dispositionByTask.delete(record3.taskId);
|
|
14362
|
+
this.revokedTasks.delete(record3.taskId);
|
|
14363
|
+
}
|
|
14364
|
+
this.logEntries = entries.length;
|
|
14365
|
+
return archivePath;
|
|
14366
|
+
});
|
|
14367
|
+
}
|
|
14368
|
+
snapshotEntries(records) {
|
|
14369
|
+
return records.flatMap((record3) => {
|
|
13789
14370
|
const append = { schema: 1, kind: "append", record: record3 };
|
|
13790
14371
|
const disposition = this.dispositionByTask.get(record3.taskId);
|
|
13791
|
-
|
|
14372
|
+
const entries = disposition === void 0 ? [append] : [append, { schema: 1, kind: "disposition", taskId: record3.taskId, disposition }];
|
|
14373
|
+
if (this.revokedTasks.has(record3.taskId)) entries.push({ schema: 1, kind: "revoke", taskId: record3.taskId, messageId: record3.messageId });
|
|
14374
|
+
return entries;
|
|
13792
14375
|
});
|
|
13793
|
-
|
|
14376
|
+
}
|
|
14377
|
+
async compact() {
|
|
14378
|
+
const entries = this.snapshotEntries(this.records());
|
|
14379
|
+
await this.file.replace(entries.map((entry) => stableJson(entry)));
|
|
13794
14380
|
this.logEntries = entries.length;
|
|
13795
14381
|
}
|
|
13796
14382
|
async exclusive(fn) {
|
|
@@ -13801,6 +14387,7 @@ var AgentMessageOutbox = class _AgentMessageOutbox {
|
|
|
13801
14387
|
});
|
|
13802
14388
|
await prior;
|
|
13803
14389
|
try {
|
|
14390
|
+
this.file.assertWritable();
|
|
13804
14391
|
return await fn();
|
|
13805
14392
|
} finally {
|
|
13806
14393
|
release();
|
|
@@ -14981,7 +15568,7 @@ var TaskRunner = class {
|
|
|
14981
15568
|
*/
|
|
14982
15569
|
stoppingOffers = false;
|
|
14983
15570
|
get activeTaskCount() {
|
|
14984
|
-
return this.tasks.size;
|
|
15571
|
+
return this.tasks.size + this.startupOwners.size;
|
|
14985
15572
|
}
|
|
14986
15573
|
/**
|
|
14987
15574
|
* Transport-boundary classification for the currently active task. Legacy
|
|
@@ -15027,6 +15614,12 @@ var TaskRunner = class {
|
|
|
15027
15614
|
const active = this.tasks.get(taskId);
|
|
15028
15615
|
const context = pending ?? (active?.messageRequirement && active.agentRef && active.messageOutbox ? { agentRef: active.agentRef, requirement: active.messageRequirement, outbox: active.messageOutbox } : void 0);
|
|
15029
15616
|
if (context === void 0) throw new Error("task has no active required Agent message contract");
|
|
15617
|
+
const isCurrent = () => {
|
|
15618
|
+
if (this.messageContextByToken.get(input.contextToken) !== taskId || this.messageContextByTask.get(taskId) !== input.contextToken || this.pendingCancelled.has(taskId)) return false;
|
|
15619
|
+
const current2 = this.tasks.get(taskId);
|
|
15620
|
+
return current2 === void 0 ? active === void 0 && this.pendingMessageTasks.get(taskId) === pending : (active === void 0 || current2 === active) && current2.messageOutbox === context.outbox && this.canPublishAgentMessage(current2);
|
|
15621
|
+
};
|
|
15622
|
+
if (!isCurrent()) throw new Error("invalid or expired Agent message task context");
|
|
15030
15623
|
const record3 = await context.outbox.appendDraft({
|
|
15031
15624
|
taskId,
|
|
15032
15625
|
tenantId: this.deps.tenantId,
|
|
@@ -15037,8 +15630,17 @@ var TaskRunner = class {
|
|
|
15037
15630
|
maxPendingEvents: 64,
|
|
15038
15631
|
maxPendingBytes: 4 * 1024 * 1024
|
|
15039
15632
|
});
|
|
15040
|
-
if (
|
|
15041
|
-
const
|
|
15633
|
+
if (!isCurrent()) throw new Error("invalid or expired Agent message task context");
|
|
15634
|
+
const current = this.tasks.get(taskId);
|
|
15635
|
+
if (current === void 0) return { messageId: record3.messageId, state: "staged" };
|
|
15636
|
+
const activated = await this.activateAgentMessage(
|
|
15637
|
+
context.outbox,
|
|
15638
|
+
taskId,
|
|
15639
|
+
current.session.sessionRef,
|
|
15640
|
+
isCurrent,
|
|
15641
|
+
() => current.messageSendAttempted === true
|
|
15642
|
+
);
|
|
15643
|
+
if (!isCurrent()) throw new Error("invalid or expired Agent message task context");
|
|
15042
15644
|
if (activated === void 0) throw new Error("Agent message draft disappeared before activation");
|
|
15043
15645
|
this.sendAgentMessageRecord(context.outbox, activated);
|
|
15044
15646
|
return { messageId: activated.messageId, state: "pending" };
|
|
@@ -15074,19 +15676,39 @@ var TaskRunner = class {
|
|
|
15074
15676
|
});
|
|
15075
15677
|
return opened;
|
|
15076
15678
|
}
|
|
15679
|
+
/** A recovery terminal must not close first-message admission before this durable draft has a disposition. */
|
|
15680
|
+
hasPendingRecoveredAgentMessage(taskId) {
|
|
15681
|
+
return this.recoveredMessageOutboxes.get(taskId)?.retryableRecords().some((record3) => record3.taskId === taskId && record3.sessionRef !== void 0) === true;
|
|
15682
|
+
}
|
|
15077
15683
|
/** Retry stable recovered records after a transport handshake/re-handshake. */
|
|
15078
15684
|
retryRecoveredAgentMessages() {
|
|
15079
15685
|
for (const [taskId, outbox] of this.recoveredMessageOutboxes) {
|
|
15080
15686
|
const record3 = outbox.get(taskId);
|
|
15081
|
-
if (record3?.sessionRef !== void 0) this.sendAgentMessageRecord(outbox, record3);
|
|
15687
|
+
if (record3?.sessionRef !== void 0 && this.hasPendingRecoveredAgentMessage(taskId)) this.sendAgentMessageRecord(outbox, record3);
|
|
15082
15688
|
}
|
|
15083
15689
|
}
|
|
15690
|
+
canPublishAgentMessage(active) {
|
|
15691
|
+
return this.tasks.get(active.taskId) === active && !active.beingTornDown;
|
|
15692
|
+
}
|
|
15693
|
+
/** All authors and startup use the same queue-time and post-I/O authority check. */
|
|
15694
|
+
async activateAgentMessage(outbox, taskId, sessionRef, isCurrent, wasSendAttempted = () => false) {
|
|
15695
|
+
if (!isCurrent()) return void 0;
|
|
15696
|
+
const record3 = await outbox.activate(taskId, sessionRef, isCurrent);
|
|
15697
|
+
if (!isCurrent()) {
|
|
15698
|
+
if (!wasSendAttempted()) await outbox.revoke(taskId);
|
|
15699
|
+
return void 0;
|
|
15700
|
+
}
|
|
15701
|
+
return record3;
|
|
15702
|
+
}
|
|
15084
15703
|
sendAgentMessageRecord(outbox, record3) {
|
|
15704
|
+
const active = this.tasks.get(record3.taskId);
|
|
15705
|
+
const authorized = active === void 0 ? this.recoveredMessageOutboxes.get(record3.taskId) === outbox : active.messageOutbox === outbox && this.canPublishAgentMessage(active);
|
|
15706
|
+
if (!authorized || record3.sessionRef === void 0 || !outbox.retryableRecords().some((pending) => pending.messageId === record3.messageId)) return;
|
|
15707
|
+
if (active !== void 0) active.messageSendAttempted = true;
|
|
15085
15708
|
this.deps.send(createEnvelope("agent.message.publish", outbox.publishPayload(record3), {
|
|
15086
15709
|
taskId: record3.taskId,
|
|
15087
15710
|
sessionRef: record3.sessionRef
|
|
15088
15711
|
}));
|
|
15089
|
-
const active = this.tasks.get(record3.taskId);
|
|
15090
15712
|
if (active?.messageOutbox === outbox) {
|
|
15091
15713
|
if (active.messageRetryTimer !== void 0) clearTimeout(active.messageRetryTimer);
|
|
15092
15714
|
active.messageRetryTimer = setTimeout(() => {
|
|
@@ -15126,6 +15748,7 @@ var TaskRunner = class {
|
|
|
15126
15748
|
if (outcome === "accepted") this.recoveredMessageOutboxes.delete(taskId);
|
|
15127
15749
|
return;
|
|
15128
15750
|
}
|
|
15751
|
+
if (this.tasks.get(taskId) !== active) return;
|
|
15129
15752
|
if (outcome === "held") {
|
|
15130
15753
|
if (active.messageRetryTimer !== void 0) clearTimeout(active.messageRetryTimer);
|
|
15131
15754
|
active.messageRetryTimer = void 0;
|
|
@@ -15135,6 +15758,8 @@ var TaskRunner = class {
|
|
|
15135
15758
|
if (active.messageRetryTimer !== void 0) clearTimeout(active.messageRetryTimer);
|
|
15136
15759
|
active.messageRetryTimer = void 0;
|
|
15137
15760
|
this.revokeAgentMessageContext(taskId);
|
|
15761
|
+
active.pendingMessageCompletion = void 0;
|
|
15762
|
+
await this.fail(taskId, "required Agent message was refused", false);
|
|
15138
15763
|
return;
|
|
15139
15764
|
}
|
|
15140
15765
|
if (outcome !== "accepted") return;
|
|
@@ -15153,6 +15778,9 @@ var TaskRunner = class {
|
|
|
15153
15778
|
/** M4 Phase 2: stop claiming any FUTURE `task.offer` — see `stoppingOffers`'s own doc comment. Idempotent. */
|
|
15154
15779
|
stopAcceptingOffers() {
|
|
15155
15780
|
this.stoppingOffers = true;
|
|
15781
|
+
if (this.startupRetryTimer) clearTimeout(this.startupRetryTimer);
|
|
15782
|
+
this.startupRetryTimer = void 0;
|
|
15783
|
+
for (const abort of this.inFlightBlobAborts.values()) abort.abort();
|
|
15156
15784
|
}
|
|
15157
15785
|
/**
|
|
15158
15786
|
* Shutdown of every currently ACTIVE task for the control socket's
|
|
@@ -15191,9 +15819,65 @@ var TaskRunner = class {
|
|
|
15191
15819
|
* ahead of this method — see `daemon-control-socket.test.ts`'s dedicated
|
|
15192
15820
|
* regression test for the exact scenario.
|
|
15193
15821
|
*/
|
|
15822
|
+
/** Startup resources have an owner even before a Session can become active. */
|
|
15823
|
+
startupRetryTimer;
|
|
15824
|
+
claimedHarnesses = /* @__PURE__ */ new Map();
|
|
15825
|
+
homeReservations = /* @__PURE__ */ new Map();
|
|
15826
|
+
startupOwners = /* @__PURE__ */ new Map();
|
|
15827
|
+
startupDisposals = /* @__PURE__ */ new Map();
|
|
15828
|
+
disposeStartupOwner(taskId) {
|
|
15829
|
+
const existing = this.startupDisposals.get(taskId);
|
|
15830
|
+
if (existing) return existing;
|
|
15831
|
+
const attempt = this.disposeStartupOwnerOnce(taskId).finally(() => this.startupDisposals.delete(taskId));
|
|
15832
|
+
this.startupDisposals.set(taskId, attempt);
|
|
15833
|
+
return attempt;
|
|
15834
|
+
}
|
|
15835
|
+
async disposeStartupOwnerOnce(taskId) {
|
|
15836
|
+
const owner = this.startupOwners.get(taskId);
|
|
15837
|
+
if (!owner) return true;
|
|
15838
|
+
try {
|
|
15839
|
+
await owner.settle?.();
|
|
15840
|
+
await owner.dispose();
|
|
15841
|
+
await this.deps.awaitTerminalCommit?.(taskId);
|
|
15842
|
+
await owner.release();
|
|
15843
|
+
} catch (caught) {
|
|
15844
|
+
const failure = isRuntimeDisposalFailure(caught) ? caught : new RuntimeDisposalFailure({
|
|
15845
|
+
stage: "quiescence",
|
|
15846
|
+
reason: "startup runtime disposal has not been confirmed"
|
|
15847
|
+
}, { cause: caught });
|
|
15848
|
+
this.deps.onRuntimeDisposalFailure?.({
|
|
15849
|
+
taskId,
|
|
15850
|
+
runtimeId: owner.runtimeId,
|
|
15851
|
+
stage: failure.stage,
|
|
15852
|
+
reason: failure.message
|
|
15853
|
+
});
|
|
15854
|
+
console.error(`[byok/client] startup ownership retained for ${taskId}: ${failure.message}`);
|
|
15855
|
+
if (!this.startupRetryTimer && !this.stoppingOffers) {
|
|
15856
|
+
this.startupRetryTimer = setTimeout(() => {
|
|
15857
|
+
this.startupRetryTimer = void 0;
|
|
15858
|
+
for (const id of this.startupOwners.keys()) {
|
|
15859
|
+
if (!this.inFlightOffers.has(id)) void this.disposeStartupOwner(id);
|
|
15860
|
+
}
|
|
15861
|
+
}, 1e3);
|
|
15862
|
+
this.startupRetryTimer.unref?.();
|
|
15863
|
+
}
|
|
15864
|
+
return false;
|
|
15865
|
+
}
|
|
15866
|
+
if (this.startupOwners.get(taskId) === owner) this.startupOwners.delete(taskId);
|
|
15867
|
+
this.pendingMessageTasks.delete(taskId);
|
|
15868
|
+
this.revokeAgentMessageContext(taskId);
|
|
15869
|
+
this.revokeAgentMemoryContext(taskId);
|
|
15870
|
+
return true;
|
|
15871
|
+
}
|
|
15194
15872
|
async shutdownActiveTasks(reason) {
|
|
15195
15873
|
const active = [...this.tasks.values()];
|
|
15196
15874
|
await Promise.all(active.map((task) => this.shutdownTask(task, reason)));
|
|
15875
|
+
for (const taskId of this.startupOwners.keys()) {
|
|
15876
|
+
if (!await this.disposeStartupOwner(taskId)) throw new RuntimeDisposalFailure({
|
|
15877
|
+
stage: "quiescence",
|
|
15878
|
+
reason: "startup runtime ownership remains quarantined"
|
|
15879
|
+
});
|
|
15880
|
+
}
|
|
15197
15881
|
}
|
|
15198
15882
|
/**
|
|
15199
15883
|
* M5 batch-3 (workstream 2): the ONE shared per-task teardown sequence —
|
|
@@ -15225,14 +15909,19 @@ var TaskRunner = class {
|
|
|
15225
15909
|
* a genuine protocol bug, not a benign race — mirrors `pump()`'s own
|
|
15226
15910
|
* identity-check guard for the same class of race.
|
|
15227
15911
|
*/
|
|
15912
|
+
async interruptBounded(session) {
|
|
15913
|
+
await raceSettleFirst(
|
|
15914
|
+
() => session.interrupt(),
|
|
15915
|
+
this.deps.shutdownInterruptTimeoutMs ?? DEFAULT_SHUTDOWN_INTERRUPT_TIMEOUT_MS
|
|
15916
|
+
);
|
|
15917
|
+
}
|
|
15228
15918
|
async teardownActiveTask(active, reason, retryable) {
|
|
15229
15919
|
if (active.finalizationStarted) return this.finish(active.taskId);
|
|
15230
15920
|
if (!this.reserveSemanticTerminal(active)) return active.semanticTerminalSettled ?? false;
|
|
15231
15921
|
active.beingTornDown = true;
|
|
15232
15922
|
active.blobAbort.abort();
|
|
15233
15923
|
await this.observeGit(active, "salvage");
|
|
15234
|
-
|
|
15235
|
-
await raceSettleFirst(() => active.session.interrupt(), timeoutMs);
|
|
15924
|
+
await this.interruptBounded(active.session);
|
|
15236
15925
|
if (this.tasks.get(active.taskId) !== active) return true;
|
|
15237
15926
|
await this.persistAgentTerminalEvidence(active, "failed", reason);
|
|
15238
15927
|
this.deps.send(
|
|
@@ -15334,7 +16023,7 @@ var TaskRunner = class {
|
|
|
15334
16023
|
}
|
|
15335
16024
|
}
|
|
15336
16025
|
async handleOffer(taskId, payload, strictAgentOffer) {
|
|
15337
|
-
if (this.tasks.has(taskId) || this.finishedTaskIds.has(taskId) || this.strictDeclinedTaskIds.has(taskId)) {
|
|
16026
|
+
if (this.tasks.has(taskId) || this.startupOwners.has(taskId) || this.inFlightOffers.has(taskId) || this.finishedTaskIds.has(taskId) || this.strictDeclinedTaskIds.has(taskId)) {
|
|
15338
16027
|
return;
|
|
15339
16028
|
}
|
|
15340
16029
|
let agentRef;
|
|
@@ -15377,9 +16066,29 @@ var TaskRunner = class {
|
|
|
15377
16066
|
}
|
|
15378
16067
|
this.inFlightOffers.add(taskId);
|
|
15379
16068
|
const blobAbort = new AbortController();
|
|
16069
|
+
const startupDeadline = Date.now() + (this.deps.startupTimeoutMs ?? 3e4);
|
|
16070
|
+
const startupTimer = setTimeout(
|
|
16071
|
+
() => blobAbort.abort(new Error("runtime startup deadline exceeded during admission/start")),
|
|
16072
|
+
this.deps.startupTimeoutMs ?? 3e4
|
|
16073
|
+
);
|
|
16074
|
+
startupTimer.unref?.();
|
|
16075
|
+
const admissionWithdrawn = () => {
|
|
16076
|
+
if (!blobAbort.signal.aborted) return false;
|
|
16077
|
+
const cancelled = this.pendingCancelled.has(taskId);
|
|
16078
|
+
decline(cancelled ? "cancelled before claim" : this.stoppingOffers ? "daemon is shutting down" : "runtime startup deadline exceeded", !cancelled);
|
|
16079
|
+
return true;
|
|
16080
|
+
};
|
|
15380
16081
|
this.inFlightBlobAborts.set(taskId, blobAbort);
|
|
15381
16082
|
let agentBinding;
|
|
15382
16083
|
let agentLeaseTransferred = false;
|
|
16084
|
+
let reservedHome;
|
|
16085
|
+
const releaseReservation = () => {
|
|
16086
|
+
if (reservedHome === void 0) return;
|
|
16087
|
+
const remaining = this.homeReservations.get(reservedHome) - 1;
|
|
16088
|
+
if (remaining === 0) this.homeReservations.delete(reservedHome);
|
|
16089
|
+
else this.homeReservations.set(reservedHome, remaining);
|
|
16090
|
+
reservedHome = void 0;
|
|
16091
|
+
};
|
|
15383
16092
|
try {
|
|
15384
16093
|
if (this.pendingCancelled.has(taskId)) {
|
|
15385
16094
|
const reason = this.pendingCancelled.get(taskId);
|
|
@@ -15404,11 +16113,14 @@ var TaskRunner = class {
|
|
|
15404
16113
|
);
|
|
15405
16114
|
return;
|
|
15406
16115
|
}
|
|
15407
|
-
|
|
15408
|
-
|
|
15409
|
-
|
|
16116
|
+
if (admissionWithdrawn()) return;
|
|
16117
|
+
const active = this.deps.agentHome.executionLeaseManager.activeAttemptCount(canonicalHome) + (this.homeReservations.get(canonicalHome) ?? 0);
|
|
16118
|
+
if (active >= limit) {
|
|
16119
|
+
decline(`agent home busy: ${active} active attempt(s)`, true);
|
|
15410
16120
|
return;
|
|
15411
16121
|
}
|
|
16122
|
+
this.homeReservations.set(canonicalHome, (this.homeReservations.get(canonicalHome) ?? 0) + 1);
|
|
16123
|
+
reservedHome = canonicalHome;
|
|
15412
16124
|
}
|
|
15413
16125
|
const guarded = this.deps.admissionGuard?.({ taskId, payload });
|
|
15414
16126
|
if (guarded !== void 0 && !guarded.admit) {
|
|
@@ -15440,6 +16152,14 @@ var TaskRunner = class {
|
|
|
15440
16152
|
);
|
|
15441
16153
|
return;
|
|
15442
16154
|
}
|
|
16155
|
+
if (payload.harnessId !== void 0 && (payload.runtime !== void 0 || payload.dispatchSelection !== void 0)) {
|
|
16156
|
+
decline("harnessId cannot be combined with a built-in runtime selection", false);
|
|
16157
|
+
return;
|
|
16158
|
+
}
|
|
16159
|
+
if (payload.harnessId !== void 0 && !(this.deps.getServerCapabilities?.() ?? []).includes("custom-harness")) {
|
|
16160
|
+
decline("server does not support custom-harness identity", false);
|
|
16161
|
+
return;
|
|
16162
|
+
}
|
|
15443
16163
|
const requiredToolsets = "requiredToolsets" in payload ? payload.requiredToolsets : void 0;
|
|
15444
16164
|
const resolvedMcp = requiredToolsets ? this.resolveMcpServers(requiredToolsets) : void 0;
|
|
15445
16165
|
if (resolvedMcp && !resolvedMcp.ok) {
|
|
@@ -15452,17 +16172,29 @@ var TaskRunner = class {
|
|
|
15452
16172
|
return;
|
|
15453
16173
|
}
|
|
15454
16174
|
const offered = withoutRequiredToolsets(payload);
|
|
15455
|
-
const requestedRuntime = payload.dispatchSelection?.runtimeId ?? payload.runtime;
|
|
16175
|
+
const requestedRuntime = payload.harnessId ?? payload.dispatchSelection?.runtimeId ?? payload.runtime;
|
|
15456
16176
|
const requiresAgentMemoryMcp = agentRef !== void 0 && this.deps.agentMemoryMcpBin !== void 0 && isAgentMemorySecureFilesystemAvailable(this.deps.agentMemoryFilesystemHelperBin !== void 0);
|
|
15457
|
-
|
|
15458
|
-
|
|
15459
|
-
|
|
15460
|
-
|
|
15461
|
-
|
|
16177
|
+
let pick;
|
|
16178
|
+
try {
|
|
16179
|
+
pick = await this.pickAdapter(
|
|
16180
|
+
requestedRuntime,
|
|
16181
|
+
payload.policy.mode,
|
|
16182
|
+
requiredToolsets !== void 0 || messageRequirement !== void 0 || requiresAgentMemoryMcp,
|
|
16183
|
+
blobAbort.signal
|
|
16184
|
+
);
|
|
16185
|
+
} catch (error) {
|
|
16186
|
+
if (!admissionWithdrawn()) decline(`runtime detection failed: ${errorMessage6(error)}`, true);
|
|
16187
|
+
return;
|
|
16188
|
+
}
|
|
16189
|
+
if (admissionWithdrawn()) return;
|
|
15462
16190
|
if (!pick.ok) {
|
|
15463
16191
|
decline(pick.reason, pick.retryable);
|
|
15464
16192
|
return;
|
|
15465
16193
|
}
|
|
16194
|
+
if (!isKnownRuntimeId(pick.descriptor.id) && !(this.deps.getServerCapabilities?.() ?? []).includes("custom-harness")) {
|
|
16195
|
+
decline("server does not support the selected custom harness identity", false);
|
|
16196
|
+
return;
|
|
16197
|
+
}
|
|
15466
16198
|
const env = buildRuntimeEnv({
|
|
15467
16199
|
ambient: process.env,
|
|
15468
16200
|
requirements: pick.descriptor.environmentRequirements,
|
|
@@ -15536,18 +16268,20 @@ var TaskRunner = class {
|
|
|
15536
16268
|
}
|
|
15537
16269
|
let prepared;
|
|
15538
16270
|
try {
|
|
15539
|
-
prepared = await pick.adapter.prepare({
|
|
16271
|
+
prepared = await awaitAdmission(() => pick.adapter.prepare({
|
|
16272
|
+
signal: blobAbort.signal,
|
|
15540
16273
|
offer: offered,
|
|
15541
16274
|
policy: decision.policy,
|
|
15542
16275
|
descriptor: pick.descriptor,
|
|
15543
16276
|
requiredToolsetIds: requiredToolsets ?? [],
|
|
15544
16277
|
...taskMcpServers === void 0 ? {} : { mcpServers: taskMcpServers },
|
|
15545
16278
|
...mcpToolsetTools === void 0 ? {} : { mcpToolsetTools }
|
|
15546
|
-
});
|
|
16279
|
+
}), blobAbort.signal);
|
|
15547
16280
|
} catch (error) {
|
|
15548
|
-
decline(`runtime preparation failed: ${errorMessage6(error)}`, true);
|
|
16281
|
+
if (!admissionWithdrawn()) decline(`runtime preparation failed: ${errorMessage6(error)}`, true);
|
|
15549
16282
|
return;
|
|
15550
16283
|
}
|
|
16284
|
+
if (admissionWithdrawn()) return;
|
|
15551
16285
|
if (prepared.kind === "reject") {
|
|
15552
16286
|
decline(prepared.reason, prepared.retryable);
|
|
15553
16287
|
return;
|
|
@@ -15555,6 +16289,7 @@ var TaskRunner = class {
|
|
|
15555
16289
|
if (agentRef !== void 0) {
|
|
15556
16290
|
try {
|
|
15557
16291
|
agentBinding = await this.deps.agentHome.acquireExecution(agentRef, { taskId, sessionRef });
|
|
16292
|
+
releaseReservation();
|
|
15558
16293
|
} catch (error) {
|
|
15559
16294
|
decline(
|
|
15560
16295
|
`Agent home admission failed: ${errorMessage6(error)}`,
|
|
@@ -15679,7 +16414,16 @@ var TaskRunner = class {
|
|
|
15679
16414
|
},
|
|
15680
16415
|
forwardedEnvironmentNames: Object.freeze(Object.keys(env).sort())
|
|
15681
16416
|
});
|
|
16417
|
+
if (admissionWithdrawn()) {
|
|
16418
|
+
gitLease?.release();
|
|
16419
|
+
return;
|
|
16420
|
+
}
|
|
15682
16421
|
await this.deps.beforeClaim?.(taskId, manifest.descriptor.id);
|
|
16422
|
+
if (admissionWithdrawn()) {
|
|
16423
|
+
gitLease?.release();
|
|
16424
|
+
return;
|
|
16425
|
+
}
|
|
16426
|
+
if (!isKnownRuntimeId(manifest.descriptor.id)) this.claimedHarnesses.set(taskId, manifest.descriptor.id);
|
|
15683
16427
|
this.deps.send(
|
|
15684
16428
|
createEnvelope(
|
|
15685
16429
|
"task.claim",
|
|
@@ -15697,6 +16441,7 @@ var TaskRunner = class {
|
|
|
15697
16441
|
// where an auto-selected task left the server never learning
|
|
15698
16442
|
// which runtime actually ran.
|
|
15699
16443
|
runtime: isKnownRuntimeId(manifest.descriptor.id) ? manifest.descriptor.id : void 0,
|
|
16444
|
+
...!isKnownRuntimeId(manifest.descriptor.id) ? { harnessId: manifest.descriptor.id } : {},
|
|
15700
16445
|
// S0/D-4 (`task.claim.capabilities`, docs/protocol.md §2.4): the
|
|
15701
16446
|
// selected adapter's own capability self-report, carried on the
|
|
15702
16447
|
// same message that establishes the task↔runtime binding. The
|
|
@@ -15814,8 +16559,57 @@ var TaskRunner = class {
|
|
|
15814
16559
|
};
|
|
15815
16560
|
let session;
|
|
15816
16561
|
try {
|
|
15817
|
-
session = await
|
|
16562
|
+
session = await startOwnedRuntime(
|
|
16563
|
+
(input) => prepared.operation.start(input),
|
|
16564
|
+
startInput,
|
|
16565
|
+
blobAbort.signal,
|
|
16566
|
+
Math.max(0, startupDeadline - Date.now())
|
|
16567
|
+
);
|
|
15818
16568
|
} catch (err) {
|
|
16569
|
+
const disposalFailure = isRuntimeStartupDisposalFailure(err);
|
|
16570
|
+
const cancelled = this.pendingCancelled.has(taskId);
|
|
16571
|
+
if (disposalFailure || cancelled) {
|
|
16572
|
+
if (cancelled) this.revokeAgentMessageContext(taskId);
|
|
16573
|
+
const ownedBinding2 = agentBinding;
|
|
16574
|
+
const cancellation = createEnvelope("task.cancelled", {
|
|
16575
|
+
reason: this.pendingCancelled.get(taskId),
|
|
16576
|
+
...terminalIdentity(this.claimedHarnesses.get(taskId)),
|
|
16577
|
+
...agentBinding === void 0 ? {} : { agentRef: agentBinding.resolution.agentRef }
|
|
16578
|
+
}, { taskId });
|
|
16579
|
+
let cancellationSettled = false;
|
|
16580
|
+
this.startupOwners.set(taskId, {
|
|
16581
|
+
runtimeId: pick.descriptor.id,
|
|
16582
|
+
dispose: disposalFailure ? err.retryDisposal : async () => {
|
|
16583
|
+
},
|
|
16584
|
+
...cancelled ? { settle: async () => {
|
|
16585
|
+
if (cancellationSettled) return;
|
|
16586
|
+
await this.pendingMessageTasks.get(taskId)?.outbox.revoke(taskId);
|
|
16587
|
+
this.deps.send(cancellation);
|
|
16588
|
+
cancellationSettled = true;
|
|
16589
|
+
this.pendingCancelled.delete(taskId);
|
|
16590
|
+
this.addFinishedTaskId(taskId);
|
|
16591
|
+
} } : {},
|
|
16592
|
+
release: async () => {
|
|
16593
|
+
await ownedBinding2?.lease.release();
|
|
16594
|
+
gitLease?.release();
|
|
16595
|
+
}
|
|
16596
|
+
});
|
|
16597
|
+
agentLeaseTransferred = true;
|
|
16598
|
+
if (disposalFailure) this.deps.onRuntimeDisposalFailure?.({
|
|
16599
|
+
taskId,
|
|
16600
|
+
runtimeId: pick.descriptor.id,
|
|
16601
|
+
stage: "quiescence",
|
|
16602
|
+
reason: err.message
|
|
16603
|
+
});
|
|
16604
|
+
if (!cancelled) {
|
|
16605
|
+
if (agentBinding === void 0) await this.fail(taskId, errorMessage6(err), false);
|
|
16606
|
+
else await this.failClaimedAgent(taskId, errorMessage6(err), false, {
|
|
16607
|
+
binding: agentBinding,
|
|
16608
|
+
runtimeId: pick.descriptor.id
|
|
16609
|
+
});
|
|
16610
|
+
}
|
|
16611
|
+
return;
|
|
16612
|
+
}
|
|
15819
16613
|
const failure = projectRuntimeBoundaryFailure(err, "start");
|
|
15820
16614
|
if (failure.contractViolation) {
|
|
15821
16615
|
console.error("[byok/client] runtime adapter start() returned an untyped failure", err);
|
|
@@ -15832,143 +16626,169 @@ var TaskRunner = class {
|
|
|
15832
16626
|
}
|
|
15833
16627
|
return;
|
|
15834
16628
|
}
|
|
15835
|
-
|
|
15836
|
-
|
|
15837
|
-
|
|
15838
|
-
|
|
15839
|
-
|
|
15840
|
-
|
|
15841
|
-
|
|
15842
|
-
taskId,
|
|
15843
|
-
`Agent session execution lease could not bind the runtime session: ${errorMessage6(error)}`,
|
|
15844
|
-
false,
|
|
15845
|
-
{
|
|
15846
|
-
binding: agentBinding,
|
|
15847
|
-
runtimeId: pick.descriptor.id,
|
|
15848
|
-
sessionRef: session.sessionRef
|
|
15849
|
-
}
|
|
15850
|
-
);
|
|
15851
|
-
return;
|
|
16629
|
+
const ownedBinding = agentBinding;
|
|
16630
|
+
this.startupOwners.set(taskId, {
|
|
16631
|
+
runtimeId: pick.descriptor.id,
|
|
16632
|
+
dispose: () => session.close(),
|
|
16633
|
+
release: async () => {
|
|
16634
|
+
await ownedBinding?.lease.release();
|
|
16635
|
+
gitLease?.release();
|
|
15852
16636
|
}
|
|
15853
|
-
}
|
|
15854
|
-
|
|
15855
|
-
|
|
15856
|
-
|
|
15857
|
-
|
|
15858
|
-
|
|
15859
|
-
|
|
15860
|
-
session,
|
|
15861
|
-
workspaceDir,
|
|
15862
|
-
...agentBinding === void 0 ? {} : {
|
|
15863
|
-
agentBinding,
|
|
15864
|
-
agentRef: agentBinding.resolution.agentRef,
|
|
15865
|
-
agentHandoff: {
|
|
15866
|
-
sessionRef: session.sessionRef,
|
|
15867
|
-
runtimeId: pick.descriptor.id,
|
|
15868
|
-
cwd: workspaceDir
|
|
15869
|
-
}
|
|
15870
|
-
},
|
|
15871
|
-
gitWorkspaceId,
|
|
15872
|
-
gitLease,
|
|
15873
|
-
gitBaseline,
|
|
15874
|
-
summaryParts: [],
|
|
15875
|
-
finalTextParts: [],
|
|
15876
|
-
batcher: new ProgressBatcher(
|
|
15877
|
-
(seq, events) => {
|
|
15878
|
-
const projected = active.egressEnabled ? this.deps.agentEgress?.projectLatestValue({
|
|
15879
|
-
agentRef: active.agentRef,
|
|
16637
|
+
});
|
|
16638
|
+
try {
|
|
16639
|
+
if (agentBinding !== void 0) {
|
|
16640
|
+
try {
|
|
16641
|
+
await agentBinding.lease.bindSession(session.sessionRef);
|
|
16642
|
+
} catch (error) {
|
|
16643
|
+
await this.failClaimedAgent(
|
|
15880
16644
|
taskId,
|
|
15881
|
-
|
|
15882
|
-
|
|
15883
|
-
|
|
15884
|
-
|
|
15885
|
-
|
|
15886
|
-
|
|
15887
|
-
|
|
15888
|
-
|
|
15889
|
-
|
|
15890
|
-
|
|
15891
|
-
|
|
15892
|
-
|
|
15893
|
-
|
|
15894
|
-
|
|
15895
|
-
|
|
15896
|
-
|
|
15897
|
-
|
|
15898
|
-
|
|
15899
|
-
|
|
15900
|
-
|
|
15901
|
-
|
|
15902
|
-
|
|
15903
|
-
|
|
15904
|
-
|
|
15905
|
-
cwd: workspaceDir,
|
|
15906
|
-
leaseId: binding.lease.leaseId
|
|
15907
|
-
}));
|
|
15908
|
-
} catch (error) {
|
|
15909
|
-
await session.close().catch(() => {
|
|
15910
|
-
});
|
|
15911
|
-
await this.failClaimedAgent(
|
|
15912
|
-
taskId,
|
|
15913
|
-
`Agent session handoff could not be durably written before start: ${errorMessage6(error)}`,
|
|
15914
|
-
false,
|
|
15915
|
-
{
|
|
15916
|
-
binding: agentBinding,
|
|
16645
|
+
`Agent session execution lease could not bind the runtime session: ${errorMessage6(error)}`,
|
|
16646
|
+
false,
|
|
16647
|
+
{
|
|
16648
|
+
binding: agentBinding,
|
|
16649
|
+
runtimeId: pick.descriptor.id,
|
|
16650
|
+
sessionRef: session.sessionRef
|
|
16651
|
+
}
|
|
16652
|
+
);
|
|
16653
|
+
return;
|
|
16654
|
+
}
|
|
16655
|
+
}
|
|
16656
|
+
let active;
|
|
16657
|
+
active = {
|
|
16658
|
+
taskId,
|
|
16659
|
+
blobAbort,
|
|
16660
|
+
egressEnabled: "egressPolicy" in payload,
|
|
16661
|
+
adapter: pick.adapter,
|
|
16662
|
+
session,
|
|
16663
|
+
workspaceDir,
|
|
16664
|
+
...agentBinding === void 0 ? {} : {
|
|
16665
|
+
agentBinding,
|
|
16666
|
+
agentRef: agentBinding.resolution.agentRef,
|
|
16667
|
+
agentHandoff: {
|
|
16668
|
+
sessionRef: session.sessionRef,
|
|
15917
16669
|
runtimeId: pick.descriptor.id,
|
|
15918
|
-
|
|
16670
|
+
cwd: workspaceDir
|
|
15919
16671
|
}
|
|
16672
|
+
},
|
|
16673
|
+
gitWorkspaceId,
|
|
16674
|
+
gitLease,
|
|
16675
|
+
gitBaseline,
|
|
16676
|
+
summaryParts: [],
|
|
16677
|
+
finalTextParts: [],
|
|
16678
|
+
batcher: new ProgressBatcher(
|
|
16679
|
+
(seq, events) => {
|
|
16680
|
+
const projected = active.egressEnabled ? this.deps.agentEgress?.projectLatestValue({
|
|
16681
|
+
agentRef: active.agentRef,
|
|
16682
|
+
taskId,
|
|
16683
|
+
events,
|
|
16684
|
+
serverCapabilities: this.deps.getServerCapabilities?.() ?? []
|
|
16685
|
+
}) ?? [] : events;
|
|
16686
|
+
if (projected.length > 0) this.deps.send(createEnvelope("task.progress", { seq, events: [...projected] }, { taskId, seq }));
|
|
16687
|
+
},
|
|
16688
|
+
this.deps.batcherOptions
|
|
16689
|
+
),
|
|
16690
|
+
approvalQueue: [],
|
|
16691
|
+
outputBytesSoFar: 0,
|
|
16692
|
+
startedAtMs: Date.now(),
|
|
16693
|
+
...messageRequirement === void 0 ? {} : {
|
|
16694
|
+
messageRequirement,
|
|
16695
|
+
messageOutbox: this.pendingMessageTasks.get(taskId).outbox
|
|
16696
|
+
},
|
|
16697
|
+
...terminalProjection === void 0 ? {} : { terminalProjection }
|
|
16698
|
+
};
|
|
16699
|
+
if (agentBinding !== void 0) {
|
|
16700
|
+
const binding = agentBinding;
|
|
16701
|
+
try {
|
|
16702
|
+
await this.deps.agentHome.mutateExecution(binding, () => this.deps.agentSessionHandoffs.record({
|
|
16703
|
+
agentRef: binding.resolution.agentRef,
|
|
16704
|
+
taskId,
|
|
16705
|
+
sessionRef: session.sessionRef,
|
|
16706
|
+
runtimeId: pick.descriptor.id,
|
|
16707
|
+
cwd: workspaceDir,
|
|
16708
|
+
leaseId: binding.lease.leaseId
|
|
16709
|
+
}));
|
|
16710
|
+
} catch (error) {
|
|
16711
|
+
await this.failClaimedAgent(
|
|
16712
|
+
taskId,
|
|
16713
|
+
`Agent session handoff could not be durably written before start: ${errorMessage6(error)}`,
|
|
16714
|
+
false,
|
|
16715
|
+
{
|
|
16716
|
+
binding: agentBinding,
|
|
16717
|
+
runtimeId: pick.descriptor.id,
|
|
16718
|
+
sessionRef: session.sessionRef
|
|
16719
|
+
}
|
|
16720
|
+
);
|
|
16721
|
+
return;
|
|
16722
|
+
}
|
|
16723
|
+
}
|
|
16724
|
+
const activatedMessageRecord = active.messageOutbox === void 0 ? void 0 : await this.activateAgentMessage(
|
|
16725
|
+
active.messageOutbox,
|
|
16726
|
+
taskId,
|
|
16727
|
+
session.sessionRef,
|
|
16728
|
+
() => !this.pendingCancelled.has(taskId) && !blobAbort.signal.aborted
|
|
16729
|
+
);
|
|
16730
|
+
if (this.pendingCancelled.has(taskId)) {
|
|
16731
|
+
const reason = this.pendingCancelled.get(taskId);
|
|
16732
|
+
this.pendingCancelled.delete(taskId);
|
|
16733
|
+
agentLeaseTransferred = agentBinding !== void 0;
|
|
16734
|
+
this.startupOwners.delete(taskId);
|
|
16735
|
+
this.tasks.set(taskId, active);
|
|
16736
|
+
this.reserveSemanticTerminal(active);
|
|
16737
|
+
await this.interruptBounded(session);
|
|
16738
|
+
await this.updateGitPhaseBestEffort(gitWorkspaceId, "cancelled");
|
|
16739
|
+
await this.persistAgentTerminalEvidence(active, "cancelled", reason);
|
|
16740
|
+
this.deps.send(
|
|
16741
|
+
createEnvelope(
|
|
16742
|
+
"task.cancelled",
|
|
16743
|
+
{ reason, ...this.terminalInferenceUsagePayload(active), ...this.agentTerminalPayload(active) },
|
|
16744
|
+
{ taskId }
|
|
16745
|
+
)
|
|
15920
16746
|
);
|
|
16747
|
+
await this.finish(taskId);
|
|
15921
16748
|
return;
|
|
15922
16749
|
}
|
|
15923
|
-
|
|
15924
|
-
const activatedMessageRecord = active.messageOutbox === void 0 ? void 0 : await active.messageOutbox.activate(taskId, session.sessionRef);
|
|
15925
|
-
if (this.pendingCancelled.has(taskId)) {
|
|
15926
|
-
const reason = this.pendingCancelled.get(taskId);
|
|
15927
|
-
this.pendingCancelled.delete(taskId);
|
|
16750
|
+
this.deps.send(createEnvelope("task.started", {}, { taskId }));
|
|
15928
16751
|
agentLeaseTransferred = agentBinding !== void 0;
|
|
16752
|
+
this.inFlightBlobAborts.delete(taskId);
|
|
16753
|
+
this.startupOwners.delete(taskId);
|
|
15929
16754
|
this.tasks.set(taskId, active);
|
|
15930
|
-
this.
|
|
15931
|
-
|
|
15932
|
-
|
|
15933
|
-
} catch {
|
|
16755
|
+
this.pendingMessageTasks.delete(taskId);
|
|
16756
|
+
if (active.messageOutbox !== void 0 && activatedMessageRecord !== void 0) {
|
|
16757
|
+
this.sendAgentMessageRecord(active.messageOutbox, activatedMessageRecord);
|
|
15934
16758
|
}
|
|
15935
|
-
|
|
15936
|
-
|
|
15937
|
-
|
|
15938
|
-
|
|
15939
|
-
|
|
15940
|
-
|
|
15941
|
-
|
|
15942
|
-
|
|
15943
|
-
|
|
15944
|
-
|
|
15945
|
-
|
|
15946
|
-
|
|
15947
|
-
|
|
15948
|
-
|
|
15949
|
-
|
|
15950
|
-
|
|
15951
|
-
|
|
15952
|
-
|
|
15953
|
-
|
|
15954
|
-
|
|
15955
|
-
|
|
15956
|
-
|
|
15957
|
-
|
|
15958
|
-
|
|
15959
|
-
if (agentBinding === void 0) {
|
|
15960
|
-
void this.deps.sessionWorkspaces.record(session.sessionRef, {
|
|
15961
|
-
workspaceDir,
|
|
15962
|
-
runtimeSessionId: session.sessionRef,
|
|
15963
|
-
...gitWorkspaceId ? { workspaceKind: "git", gitWorkspaceId } : {}
|
|
15964
|
-
}).catch(() => {
|
|
15965
|
-
});
|
|
15966
|
-
}
|
|
15967
|
-
if (gitWorkspaceId && this.deps.gitWorkspaceStore) {
|
|
15968
|
-
void this.deps.gitWorkspaceStore.attachSession(gitWorkspaceId, session.sessionRef).catch(() => {
|
|
16759
|
+
if (payload.limits?.maxDurationMs !== void 0) {
|
|
16760
|
+
this.armMaxDurationTimer(active, payload.limits.maxDurationMs);
|
|
16761
|
+
}
|
|
16762
|
+
void this.pump(active);
|
|
16763
|
+
if (agentBinding === void 0) {
|
|
16764
|
+
void this.deps.sessionWorkspaces.record(session.sessionRef, {
|
|
16765
|
+
workspaceDir,
|
|
16766
|
+
runtimeSessionId: session.sessionRef,
|
|
16767
|
+
...gitWorkspaceId ? { workspaceKind: "git", gitWorkspaceId } : {}
|
|
16768
|
+
}).catch(() => {
|
|
16769
|
+
});
|
|
16770
|
+
}
|
|
16771
|
+
if (gitWorkspaceId && this.deps.gitWorkspaceStore) {
|
|
16772
|
+
void this.deps.gitWorkspaceStore.attachSession(gitWorkspaceId, session.sessionRef).catch(() => {
|
|
16773
|
+
});
|
|
16774
|
+
}
|
|
16775
|
+
} catch (error) {
|
|
16776
|
+
if (this.tasks.has(taskId)) throw error;
|
|
16777
|
+
const reason = `runtime activation failed: ${errorMessage6(error)}`;
|
|
16778
|
+
if (agentBinding === void 0) await this.fail(taskId, reason, false);
|
|
16779
|
+
else await this.failClaimedAgent(taskId, reason, false, {
|
|
16780
|
+
binding: agentBinding,
|
|
16781
|
+
runtimeId: pick.descriptor.id,
|
|
16782
|
+
sessionRef: session.sessionRef
|
|
15969
16783
|
});
|
|
15970
16784
|
}
|
|
15971
16785
|
} finally {
|
|
16786
|
+
clearTimeout(startupTimer);
|
|
16787
|
+
releaseReservation();
|
|
16788
|
+
if (this.startupOwners.has(taskId)) {
|
|
16789
|
+
agentLeaseTransferred = true;
|
|
16790
|
+
await this.disposeStartupOwner(taskId);
|
|
16791
|
+
}
|
|
15972
16792
|
if (!agentLeaseTransferred) {
|
|
15973
16793
|
this.pendingMessageTasks.delete(taskId);
|
|
15974
16794
|
this.revokeAgentMessageContext(taskId);
|
|
@@ -15980,6 +16800,7 @@ var TaskRunner = class {
|
|
|
15980
16800
|
}
|
|
15981
16801
|
this.inFlightBlobAborts.delete(taskId);
|
|
15982
16802
|
this.inFlightOffers.delete(taskId);
|
|
16803
|
+
this.claimedHarnesses.delete(taskId);
|
|
15983
16804
|
}
|
|
15984
16805
|
}
|
|
15985
16806
|
withAgentMessageMcp(existing, taskId, requirement) {
|
|
@@ -16249,7 +17070,13 @@ var TaskRunner = class {
|
|
|
16249
17070
|
maxPendingBytes: 4 * 1024 * 1024
|
|
16250
17071
|
});
|
|
16251
17072
|
if (this.tasks.get(active.taskId) !== active || active.beingTornDown) return;
|
|
16252
|
-
const activated = await
|
|
17073
|
+
const activated = await this.activateAgentMessage(
|
|
17074
|
+
outbox,
|
|
17075
|
+
active.taskId,
|
|
17076
|
+
active.session.sessionRef,
|
|
17077
|
+
() => this.canPublishAgentMessage(active),
|
|
17078
|
+
() => active.messageSendAttempted === true
|
|
17079
|
+
);
|
|
16253
17080
|
if (this.tasks.get(active.taskId) !== active || active.beingTornDown) return;
|
|
16254
17081
|
if (activated === void 0) throw new Error("Agent message draft disappeared before activation");
|
|
16255
17082
|
this.sendAgentMessageRecord(outbox, activated);
|
|
@@ -16356,14 +17183,24 @@ var TaskRunner = class {
|
|
|
16356
17183
|
}
|
|
16357
17184
|
let bytes;
|
|
16358
17185
|
try {
|
|
16359
|
-
|
|
16360
|
-
|
|
17186
|
+
const limits = this.deps.artifactLimits ?? DEFAULT_ARTIFACT_LIMITS;
|
|
17187
|
+
const remaining = limits.maxTaskBytes - (active.artifactBytesSoFar ?? 0);
|
|
17188
|
+
bytes = await readArtifactBytes(
|
|
17189
|
+
opened.handle,
|
|
17190
|
+
Math.min(limits.maxFileBytes, remaining),
|
|
17191
|
+
active.blobAbort.signal,
|
|
17192
|
+
(count) => {
|
|
17193
|
+
active.artifactBytesSoFar = (active.artifactBytesSoFar ?? 0) + count;
|
|
17194
|
+
}
|
|
17195
|
+
);
|
|
17196
|
+
} catch (err) {
|
|
16361
17197
|
this.reportArtifactError(active, name, `failed to read artifact "${name}": ${errorMessage6(err)}`);
|
|
16362
17198
|
return;
|
|
16363
17199
|
} finally {
|
|
16364
17200
|
await opened.handle.close().catch(() => {
|
|
16365
17201
|
});
|
|
16366
17202
|
}
|
|
17203
|
+
if (this.tasks.get(active.taskId) !== active || active.blobAbort.signal.aborted || active.beingTornDown) return;
|
|
16367
17204
|
if (bytes.length <= MAX_INLINE_ARTIFACT_BYTES) {
|
|
16368
17205
|
const inline = bytes.toString("base64");
|
|
16369
17206
|
if (new TextEncoder().encode(inline).length <= MAX_INLINE_ARTIFACT_BYTES) {
|
|
@@ -16375,6 +17212,7 @@ var TaskRunner = class {
|
|
|
16375
17212
|
const blobRef = await this.deps.blobClient.uploadArtifact(bytes, contentType, {
|
|
16376
17213
|
signal: active.blobAbort.signal
|
|
16377
17214
|
});
|
|
17215
|
+
if (this.tasks.get(active.taskId) !== active || active.blobAbort.signal.aborted || active.beingTornDown) return;
|
|
16378
17216
|
this.deps.send(createEnvelope("task.artifact", { name, contentType, blobRef }, { taskId: active.taskId }));
|
|
16379
17217
|
} catch (err) {
|
|
16380
17218
|
this.reportArtifactError(active, name, `failed to upload artifact "${name}": ${errorMessage6(err)}`);
|
|
@@ -16382,12 +17220,17 @@ var TaskRunner = class {
|
|
|
16382
17220
|
}
|
|
16383
17221
|
/** Loud, non-silent artifact failure (finding F7): logged, and folded into this task's own progress stream as an `error` AgentEvent rather than swallowed — the task itself can still complete normally, but the omission is now visible. */
|
|
16384
17222
|
reportArtifactError(active, name, reason) {
|
|
17223
|
+
if (active.blobAbort.signal.aborted || active.beingTornDown) return;
|
|
16385
17224
|
console.error(`[byok/client] artifact "${name}" for task ${active.taskId} dropped: ${reason}`);
|
|
16386
17225
|
active.batcher.push({ type: "error", message: reason });
|
|
16387
17226
|
}
|
|
16388
17227
|
async handleCancel(taskId, reason) {
|
|
16389
17228
|
const active = this.tasks.get(taskId);
|
|
16390
17229
|
if (!active) {
|
|
17230
|
+
if (this.startupOwners.has(taskId) && !this.inFlightOffers.has(taskId)) {
|
|
17231
|
+
await this.disposeStartupOwner(taskId);
|
|
17232
|
+
return;
|
|
17233
|
+
}
|
|
16391
17234
|
this.setPendingCancelled(taskId, reason);
|
|
16392
17235
|
this.inFlightBlobAborts.get(taskId)?.abort();
|
|
16393
17236
|
return;
|
|
@@ -16401,10 +17244,7 @@ var TaskRunner = class {
|
|
|
16401
17244
|
return;
|
|
16402
17245
|
}
|
|
16403
17246
|
active.blobAbort.abort();
|
|
16404
|
-
|
|
16405
|
-
await active.session.interrupt();
|
|
16406
|
-
} catch {
|
|
16407
|
-
}
|
|
17247
|
+
await this.interruptBounded(active.session);
|
|
16408
17248
|
await this.observeGit(active, "salvage");
|
|
16409
17249
|
await this.persistAgentTerminalEvidence(active, "cancelled", reason);
|
|
16410
17250
|
this.deps.send(
|
|
@@ -16846,10 +17686,7 @@ var TaskRunner = class {
|
|
|
16846
17686
|
await active.semanticTerminalSettled;
|
|
16847
17687
|
return;
|
|
16848
17688
|
}
|
|
16849
|
-
|
|
16850
|
-
await active.session.interrupt();
|
|
16851
|
-
} catch {
|
|
16852
|
-
}
|
|
17689
|
+
await this.interruptBounded(active.session);
|
|
16853
17690
|
await this.observeGit(active, "salvage");
|
|
16854
17691
|
await this.persistAgentTerminalEvidence(active, "failed", reason ?? "rejected");
|
|
16855
17692
|
this.deps.send(
|
|
@@ -16884,7 +17721,7 @@ var TaskRunner = class {
|
|
|
16884
17721
|
this.deps.send(
|
|
16885
17722
|
createEnvelope(
|
|
16886
17723
|
"task.fail",
|
|
16887
|
-
active === void 0 ? { reason, retryable } : { reason, retryable, ...this.terminalInferenceUsagePayload(active), ...this.agentTerminalPayload(active) },
|
|
17724
|
+
active === void 0 ? { reason, retryable, ...terminalIdentity(this.claimedHarnesses.get(taskId)) } : { reason, retryable, ...this.terminalInferenceUsagePayload(active), ...this.agentTerminalPayload(active) },
|
|
16888
17725
|
{ taskId }
|
|
16889
17726
|
)
|
|
16890
17727
|
);
|
|
@@ -16898,6 +17735,7 @@ var TaskRunner = class {
|
|
|
16898
17735
|
* the wire and handleOffer's finally block releases the lease.
|
|
16899
17736
|
*/
|
|
16900
17737
|
async failClaimedAgent(taskId, reason, retryable, context) {
|
|
17738
|
+
await this.pendingMessageTasks.get(taskId)?.outbox.revoke(taskId);
|
|
16901
17739
|
const agentRef = context.binding.resolution.agentRef;
|
|
16902
17740
|
const result = await this.retryAgentTerminalEvidence(
|
|
16903
17741
|
() => this.deps.agentHome.mutateExecution(context.binding, () => this.deps.agentSessionHandoffs.recordTaskTerminal({
|
|
@@ -16923,7 +17761,7 @@ var TaskRunner = class {
|
|
|
16923
17761
|
}
|
|
16924
17762
|
this.deps.send(createEnvelope(
|
|
16925
17763
|
"task.fail",
|
|
16926
|
-
{ reason, retryable, agentRef },
|
|
17764
|
+
{ reason, retryable, ...terminalIdentity(context.runtimeId, agentRef) },
|
|
16927
17765
|
{ taskId }
|
|
16928
17766
|
));
|
|
16929
17767
|
return result.ok;
|
|
@@ -16941,7 +17779,8 @@ var TaskRunner = class {
|
|
|
16941
17779
|
terminalInferenceUsagePayload(active) {
|
|
16942
17780
|
const release = this.deps.localAgentRelease;
|
|
16943
17781
|
const runtimeId = active.adapter.descriptor.id;
|
|
16944
|
-
if (
|
|
17782
|
+
if (!isKnownRuntimeId(runtimeId)) return {};
|
|
17783
|
+
if (release === void 0 || active.lastUsage === void 0) return {};
|
|
16945
17784
|
const nowMs = Date.now();
|
|
16946
17785
|
const durationMs = terminalUsageNumber(nowMs - active.startedAtMs, TERMINAL_INFERENCE_USAGE_MAX_DURATION_MS);
|
|
16947
17786
|
const promptTokens = terminalUsageNumber(active.lastUsage?.inputTokens, TERMINAL_INFERENCE_USAGE_MAX_TOKENS);
|
|
@@ -16959,7 +17798,7 @@ var TaskRunner = class {
|
|
|
16959
17798
|
}
|
|
16960
17799
|
/** Exact Agent identity projection for claim/terminal wire payloads. */
|
|
16961
17800
|
agentTerminalPayload(active) {
|
|
16962
|
-
return active.
|
|
17801
|
+
return terminalIdentity(active.adapter.descriptor.id, active.agentRef);
|
|
16963
17802
|
}
|
|
16964
17803
|
/**
|
|
16965
17804
|
* additive-minor (`task.complete.document`): the whole daemon-side gate
|
|
@@ -17116,6 +17955,14 @@ var TaskRunner = class {
|
|
|
17116
17955
|
}
|
|
17117
17956
|
/** Persist Agent terminal truth before wire when local storage is available. */
|
|
17118
17957
|
async persistAgentTerminalEvidence(active, cause, reason) {
|
|
17958
|
+
if (cause === "cancelled" || cause === "failed" && !active.messageSendAttempted) {
|
|
17959
|
+
try {
|
|
17960
|
+
await active.messageOutbox?.revoke(active.taskId);
|
|
17961
|
+
} catch (error) {
|
|
17962
|
+
active.resolveSemanticTerminalSettled?.(false);
|
|
17963
|
+
throw error;
|
|
17964
|
+
}
|
|
17965
|
+
}
|
|
17119
17966
|
active.terminalCause = cause;
|
|
17120
17967
|
active.terminalReason = reason;
|
|
17121
17968
|
if (active.agentBinding === void 0 || active.agentRef === void 0 || active.agentHandoff === void 0) {
|
|
@@ -17178,7 +18025,19 @@ var TaskRunner = class {
|
|
|
17178
18025
|
error
|
|
17179
18026
|
});
|
|
17180
18027
|
}
|
|
17181
|
-
async
|
|
18028
|
+
async retryTerminalFinalization(taskId) {
|
|
18029
|
+
const active = this.tasks.get(taskId);
|
|
18030
|
+
if (active?.finalizationStarted) await this.finish(taskId);
|
|
18031
|
+
}
|
|
18032
|
+
finalizationAttempts = /* @__PURE__ */ new Map();
|
|
18033
|
+
finish(taskId) {
|
|
18034
|
+
const existing = this.finalizationAttempts.get(taskId);
|
|
18035
|
+
if (existing) return existing;
|
|
18036
|
+
const attempt = this.finishOnce(taskId).finally(() => this.finalizationAttempts.delete(taskId));
|
|
18037
|
+
this.finalizationAttempts.set(taskId, attempt);
|
|
18038
|
+
return attempt;
|
|
18039
|
+
}
|
|
18040
|
+
async finishOnce(taskId) {
|
|
17182
18041
|
const active = this.tasks.get(taskId);
|
|
17183
18042
|
if (!active) return true;
|
|
17184
18043
|
if (!active.finalizationStarted) {
|
|
@@ -17229,6 +18088,13 @@ var TaskRunner = class {
|
|
|
17229
18088
|
return false;
|
|
17230
18089
|
}
|
|
17231
18090
|
if (this.tasks.get(taskId) !== active) return true;
|
|
18091
|
+
try {
|
|
18092
|
+
await this.deps.awaitTerminalCommit?.(taskId);
|
|
18093
|
+
} catch {
|
|
18094
|
+
active.resolveSemanticTerminalSettled?.(false);
|
|
18095
|
+
return false;
|
|
18096
|
+
}
|
|
18097
|
+
if (this.tasks.get(taskId) !== active) return true;
|
|
17232
18098
|
if (active.agentBinding !== void 0 && active.agentRef !== void 0 && active.agentHandoff !== void 0) {
|
|
17233
18099
|
const agentRef = active.agentRef;
|
|
17234
18100
|
const handoff = active.agentHandoff;
|
|
@@ -17340,7 +18206,7 @@ var TaskRunner = class {
|
|
|
17340
18206
|
* is device-specific (which runtimes happen to be installed here), so a
|
|
17341
18207
|
* different device's installed runtime set might satisfy it.
|
|
17342
18208
|
*/
|
|
17343
|
-
async pickAdapter(requestedRuntime, policyMode, requiresMcpToolsets) {
|
|
18209
|
+
async pickAdapter(requestedRuntime, policyMode, requiresMcpToolsets, signal) {
|
|
17344
18210
|
const allowlist = this.deps.runtimeAllowlist;
|
|
17345
18211
|
if (requestedRuntime) {
|
|
17346
18212
|
if (allowlist && !allowlist.includes(requestedRuntime)) {
|
|
@@ -17369,8 +18235,8 @@ var TaskRunner = class {
|
|
|
17369
18235
|
retryable: false
|
|
17370
18236
|
};
|
|
17371
18237
|
}
|
|
17372
|
-
const detected = await adapter.detect();
|
|
17373
|
-
if (
|
|
18238
|
+
const detected = validateRuntimeDetectResult(await awaitAdmission(() => adapter.detect(signal), signal));
|
|
18239
|
+
if (detected.kind !== "available") {
|
|
17374
18240
|
return {
|
|
17375
18241
|
ok: false,
|
|
17376
18242
|
reason: `runtime "${requestedRuntime}" is not installed/available on this device`,
|
|
@@ -17385,8 +18251,8 @@ var TaskRunner = class {
|
|
|
17385
18251
|
const descriptor = freezeRuntimeAdapterDescriptor(adapter.descriptor);
|
|
17386
18252
|
if (!adapterSupportsMode(descriptor, policyMode)) continue;
|
|
17387
18253
|
if (requiresMcpToolsets && !adapterSupportsMcpToolsets(descriptor)) continue;
|
|
17388
|
-
const detected = await adapter.detect();
|
|
17389
|
-
if (detected.
|
|
18254
|
+
const detected = validateRuntimeDetectResult(await awaitAdmission(() => adapter.detect(signal), signal));
|
|
18255
|
+
if (detected.kind === "available") return { ok: true, adapter, descriptor };
|
|
17390
18256
|
}
|
|
17391
18257
|
return {
|
|
17392
18258
|
ok: false,
|
|
@@ -17550,10 +18416,12 @@ var AgentReliableSpool = class _AgentReliableSpool {
|
|
|
17550
18416
|
constructor(homeDir, spoolPath) {
|
|
17551
18417
|
this.homeDir = homeDir;
|
|
17552
18418
|
this.spoolPath = spoolPath;
|
|
18419
|
+
this.file = new DurableJsonlFile(spoolPath);
|
|
17553
18420
|
}
|
|
17554
18421
|
homeDir;
|
|
17555
18422
|
spoolPath;
|
|
17556
18423
|
pending = /* @__PURE__ */ new Map();
|
|
18424
|
+
file;
|
|
17557
18425
|
nextCursor = 1;
|
|
17558
18426
|
logEntries = 0;
|
|
17559
18427
|
writeTail = Promise.resolve();
|
|
@@ -17562,6 +18430,7 @@ var AgentReliableSpool = class _AgentReliableSpool {
|
|
|
17562
18430
|
await ensureSecureDir(directory);
|
|
17563
18431
|
const spool = new _AgentReliableSpool(homeDir, path3__default.join(directory, AGENT_RELIABLE_SPOOL_FILENAME));
|
|
17564
18432
|
await spool.load();
|
|
18433
|
+
await spool.file.confirmRecovered();
|
|
17565
18434
|
return spool;
|
|
17566
18435
|
}
|
|
17567
18436
|
get pendingEvents() {
|
|
@@ -17581,6 +18450,25 @@ var AgentReliableSpool = class _AgentReliableSpool {
|
|
|
17581
18450
|
const payloadJson = stableRecordJson(input.payload);
|
|
17582
18451
|
const byteCount = Buffer.byteLength(payloadJson, "utf8");
|
|
17583
18452
|
if (byteCount <= 0) throw new AgentReliableSpoolError("reliable payload must have positive serialized bytes");
|
|
18453
|
+
const eventId = input.eventId ?? randomUUID();
|
|
18454
|
+
const existing = this.pending.get(eventId);
|
|
18455
|
+
if (existing !== void 0) {
|
|
18456
|
+
if (!sameRecord(existing, {
|
|
18457
|
+
wireType: "agent.egress.reliable",
|
|
18458
|
+
agentRef: input.agentRef,
|
|
18459
|
+
tenantId: input.tenantId,
|
|
18460
|
+
policyRevision: input.policyRevision,
|
|
18461
|
+
eventId,
|
|
18462
|
+
cursor: existing.cursor,
|
|
18463
|
+
payloadHash: contentHash(payloadJson),
|
|
18464
|
+
byteCount,
|
|
18465
|
+
sessionRef: input.sessionRef,
|
|
18466
|
+
taskId: input.taskId
|
|
18467
|
+
}) || stableRecordJson(existing.payload) !== payloadJson || input.createdAt !== void 0 && input.createdAt !== existing.createdAt) {
|
|
18468
|
+
throw new AgentReliableSpoolError(`reliable eventId ${eventId} differs from its existing record`);
|
|
18469
|
+
}
|
|
18470
|
+
return existing;
|
|
18471
|
+
}
|
|
17584
18472
|
if (this.pending.size >= policy.reliable.maxPendingEventsPerAgent) {
|
|
17585
18473
|
throw new AgentReliableQuotaError("quota_exceeded", "reliable per-Agent event quota is exhausted");
|
|
17586
18474
|
}
|
|
@@ -17590,8 +18478,6 @@ var AgentReliableSpool = class _AgentReliableSpool {
|
|
|
17590
18478
|
if (tenantPendingBytes + byteCount > policy.reliable.maxPendingBytesPerTenant) {
|
|
17591
18479
|
throw new AgentReliableQuotaError("quota_exceeded", "reliable tenant byte quota is exhausted");
|
|
17592
18480
|
}
|
|
17593
|
-
const eventId = input.eventId ?? randomUUID();
|
|
17594
|
-
if (this.pending.has(eventId)) throw new AgentReliableSpoolError(`reliable eventId ${eventId} already exists`);
|
|
17595
18481
|
const record3 = Object.freeze({
|
|
17596
18482
|
schema: 1,
|
|
17597
18483
|
wireType: "agent.egress.reliable",
|
|
@@ -17700,6 +18586,7 @@ var AgentReliableSpool = class _AgentReliableSpool {
|
|
|
17700
18586
|
if (error.code === "ENOENT") return;
|
|
17701
18587
|
throw error;
|
|
17702
18588
|
}
|
|
18589
|
+
if (raw.length > 0 && !raw.endsWith("\n")) throw new AgentReliableSpoolError("durable JSONL log has an incomplete trailing frame; preserve the file for explicit repair");
|
|
17703
18590
|
for (const line of raw.split("\n")) {
|
|
17704
18591
|
if (line.length === 0) continue;
|
|
17705
18592
|
let entry;
|
|
@@ -17722,19 +18609,12 @@ var AgentReliableSpool = class _AgentReliableSpool {
|
|
|
17722
18609
|
}
|
|
17723
18610
|
}
|
|
17724
18611
|
async appendEntry(entry) {
|
|
17725
|
-
|
|
17726
|
-
|
|
17727
|
-
await handle.write(`${stableRecordJson(entry)}
|
|
17728
|
-
`, void 0, "utf8");
|
|
17729
|
-
await handle.sync();
|
|
17730
|
-
this.logEntries += 1;
|
|
17731
|
-
} finally {
|
|
17732
|
-
await handle.close();
|
|
17733
|
-
}
|
|
18612
|
+
await this.file.append(stableRecordJson(entry));
|
|
18613
|
+
this.logEntries += 1;
|
|
17734
18614
|
}
|
|
17735
18615
|
async compact() {
|
|
17736
18616
|
const entries = this.records().map((record3) => ({ schema: 1, kind: "append", record: record3 }));
|
|
17737
|
-
await
|
|
18617
|
+
await this.file.replace(entries.map((entry) => stableRecordJson(entry)));
|
|
17738
18618
|
this.logEntries = entries.length;
|
|
17739
18619
|
}
|
|
17740
18620
|
async exclusive(fn) {
|
|
@@ -17745,6 +18625,7 @@ var AgentReliableSpool = class _AgentReliableSpool {
|
|
|
17745
18625
|
});
|
|
17746
18626
|
await prior;
|
|
17747
18627
|
try {
|
|
18628
|
+
this.file.assertWritable();
|
|
17748
18629
|
return await fn();
|
|
17749
18630
|
} finally {
|
|
17750
18631
|
release();
|
|
@@ -19119,17 +20000,26 @@ function isRuntimeId(id) {
|
|
|
19119
20000
|
return id === "pi" || id === "claude" || id === "codex";
|
|
19120
20001
|
}
|
|
19121
20002
|
async function detectRuntimes(adapters) {
|
|
19122
|
-
const detections = await Promise.all(adapters.map(async (adapter) => ({ adapter, detected: await adapter.detect() })));
|
|
20003
|
+
const detections = await Promise.all(adapters.map(async (adapter) => ({ adapter, detected: validateRuntimeDetectResult(await adapter.detect()) })));
|
|
19123
20004
|
const runtimes = [];
|
|
20005
|
+
const harnesses = [];
|
|
19124
20006
|
for (const { adapter, detected } of detections) {
|
|
19125
|
-
if (
|
|
20007
|
+
if (detected.kind !== "available") continue;
|
|
20008
|
+
if (!isRuntimeId(adapter.descriptor.id)) {
|
|
20009
|
+
harnesses.push({
|
|
20010
|
+
id: HarnessIdSchema.parse(adapter.descriptor.id),
|
|
20011
|
+
...detected.version === void 0 ? {} : { version: detected.version },
|
|
20012
|
+
capabilities: toRuntimeInfoCapabilities(adapter.descriptor.capabilities)
|
|
20013
|
+
});
|
|
20014
|
+
continue;
|
|
20015
|
+
}
|
|
19126
20016
|
const info = { id: adapter.descriptor.id };
|
|
19127
20017
|
if (detected.version !== void 0) info.version = detected.version;
|
|
19128
20018
|
if (detected.authPresent !== void 0) info.authPresent = detected.authPresent;
|
|
19129
20019
|
info.capabilities = toRuntimeInfoCapabilities(adapter.descriptor.capabilities);
|
|
19130
20020
|
runtimes.push(info);
|
|
19131
20021
|
}
|
|
19132
|
-
return runtimes;
|
|
20022
|
+
return { runtimes, harnesses };
|
|
19133
20023
|
}
|
|
19134
20024
|
function computeCapabilities(adapters, agentHomeConfigured = false, strictAgentOnly = false, agentHomeProjectionConfigured = false, agentEgressConfigured = false, contentReadPolicies, providerProfileBindingConfigured = false) {
|
|
19135
20025
|
const flags = [];
|
|
@@ -19219,7 +20109,7 @@ function buildAdapter(id, config) {
|
|
|
19219
20109
|
resolveApprovalMcpBin: () => resolveApprovalMcpBin(config.sdkHelperHost)
|
|
19220
20110
|
});
|
|
19221
20111
|
case "codex":
|
|
19222
|
-
return new CodexAdapter();
|
|
20112
|
+
return new CodexAdapter({ sdkHelperHost: config.sdkHelperHost });
|
|
19223
20113
|
}
|
|
19224
20114
|
}
|
|
19225
20115
|
function buildDefaultAdapters(config) {
|
|
@@ -19296,6 +20186,12 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
19296
20186
|
const localAgentRelease = resolveLocalAgentReleaseIdentity(config.localAgentRelease);
|
|
19297
20187
|
const toolsetRegistry = new McpToolsetRegistry(config.mcpToolsets);
|
|
19298
20188
|
validatePiByokLauncherConfig(config.piByokLauncher);
|
|
20189
|
+
if (config.startupTimeoutMs !== void 0 && (!Number.isSafeInteger(config.startupTimeoutMs) || config.startupTimeoutMs <= 0)) throw new Error("startupTimeoutMs must be a positive safe integer");
|
|
20190
|
+
if (config.artifactLimits !== void 0) {
|
|
20191
|
+
for (const value of [config.artifactLimits.maxFileBytes, config.artifactLimits.maxTaskBytes]) {
|
|
20192
|
+
if (!Number.isSafeInteger(value) || value <= 0) throw new Error("artifactLimits must contain positive safe integer byte budgets");
|
|
20193
|
+
}
|
|
20194
|
+
}
|
|
19299
20195
|
if (config.maxTaskOutputBytes !== void 0 && !(config.maxTaskOutputBytes > 0)) {
|
|
19300
20196
|
throw new Error(
|
|
19301
20197
|
`DaemonConfig.maxTaskOutputBytes must be a positive number (or omitted to use the ${DEFAULT_MAX_TASK_OUTPUT_BYTES}-byte default) \u2014 got ${config.maxTaskOutputBytes}. Pass Number.POSITIVE_INFINITY to explicitly disable the cap; 0 or a negative number is rejected rather than silently treated as "disabled".`
|
|
@@ -19425,7 +20321,12 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
19425
20321
|
pressureEngine = ownedPressureEngine;
|
|
19426
20322
|
}
|
|
19427
20323
|
}
|
|
19428
|
-
|
|
20324
|
+
const terminalCommits = new TerminalCommitQueue((taskId) => {
|
|
20325
|
+
connection?.retryTaskReceipts(taskId);
|
|
20326
|
+
void runner?.retryTerminalFinalization(taskId).catch((error) => {
|
|
20327
|
+
console.error("[byok/client] terminal finalization still requires recovery", error);
|
|
20328
|
+
});
|
|
20329
|
+
});
|
|
19429
20330
|
const observer = new DaemonObserver();
|
|
19430
20331
|
const approvalRegistry = new ApprovalRegistry();
|
|
19431
20332
|
let connection;
|
|
@@ -19560,6 +20461,7 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
19560
20461
|
await runLifecycleMutation(startUnderLease);
|
|
19561
20462
|
}
|
|
19562
20463
|
async function startUnderLease() {
|
|
20464
|
+
terminalCommits.resume();
|
|
19563
20465
|
if (!daemonOwnerLease) daemonOwnerLease = await acquireDaemonOwner(storeDir, "daemon");
|
|
19564
20466
|
try {
|
|
19565
20467
|
let record3;
|
|
@@ -19611,7 +20513,8 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
19611
20513
|
const terminal = createEnvelope("task.fail", {
|
|
19612
20514
|
reason: "daemon_interrupted",
|
|
19613
20515
|
retryable: false,
|
|
19614
|
-
...
|
|
20516
|
+
...terminalIdentity(task.claimedRuntime, payload.agentRef),
|
|
20517
|
+
recovery: { kind: "daemon_interrupted", offerId: offer.id }
|
|
19615
20518
|
}, { taskId: task.taskId });
|
|
19616
20519
|
const bytes = encodeEnvelope(terminal);
|
|
19617
20520
|
await activeJournal.recordTerminal({
|
|
@@ -19639,7 +20542,7 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
19639
20542
|
await gitWorkspaceStore?.reconcile();
|
|
19640
20543
|
}
|
|
19641
20544
|
blobLifecycleAbort = new AbortController();
|
|
19642
|
-
const [runtimes, blobClient] = await Promise.all([
|
|
20545
|
+
const [{ runtimes, harnesses }, blobClient] = await Promise.all([
|
|
19643
20546
|
detectRuntimes(adapters),
|
|
19644
20547
|
Promise.resolve(new BlobClient(config.serverUrl, auth, { signal: blobLifecycleAbort.signal }))
|
|
19645
20548
|
]);
|
|
@@ -19660,6 +20563,7 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
19660
20563
|
tenantId: record3.tenantId,
|
|
19661
20564
|
deviceId: record3.deviceId
|
|
19662
20565
|
}) : void 0;
|
|
20566
|
+
capabilities.push("custom-harness");
|
|
19663
20567
|
const sendSanitizedEnvelope = (envelope) => {
|
|
19664
20568
|
const terminalKind = terminalKindOf(envelope.type);
|
|
19665
20569
|
if (!activeJournal || !journalIdentity || terminalKind === void 0 || envelope.task_id === void 0) {
|
|
@@ -19669,7 +20573,7 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
19669
20573
|
}
|
|
19670
20574
|
const taskId = envelope.task_id;
|
|
19671
20575
|
const bytes = encodeEnvelope(envelope);
|
|
19672
|
-
|
|
20576
|
+
terminalCommits.enqueue(taskId, async () => {
|
|
19673
20577
|
try {
|
|
19674
20578
|
await activeJournal.recordTerminal({
|
|
19675
20579
|
taskId,
|
|
@@ -19697,7 +20601,7 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
19697
20601
|
const failure = createEnvelope("task.fail", {
|
|
19698
20602
|
reason: "terminal_result_too_large",
|
|
19699
20603
|
retryable: false,
|
|
19700
|
-
...
|
|
20604
|
+
...terminalIdentity(task.claimedRuntime, offerPayload.agentRef)
|
|
19701
20605
|
}, { taskId });
|
|
19702
20606
|
const failureBytes = encodeEnvelope(failure);
|
|
19703
20607
|
await activeJournal.recordTerminal({
|
|
@@ -19715,8 +20619,6 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
19715
20619
|
observer.handleOutboundEnvelope(decoded);
|
|
19716
20620
|
connection?.send(decoded);
|
|
19717
20621
|
}
|
|
19718
|
-
}).catch((error) => {
|
|
19719
|
-
console.error("[byok/client] terminal durability failed; execution remains unsettled", error);
|
|
19720
20622
|
});
|
|
19721
20623
|
};
|
|
19722
20624
|
const sendEnvelope = (candidate) => {
|
|
@@ -19798,7 +20700,10 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
19798
20700
|
// Finding F5(a): see TaskRunnerDeps.shutdownInterruptTimeoutMs's own doc comment.
|
|
19799
20701
|
shutdownInterruptTimeoutMs: overrides.shutdown?.taskInterruptTimeoutMs,
|
|
19800
20702
|
// M5 batch-3 (workstream 2): see DaemonConfig.maxTaskOutputBytes's own doc comment — already validated above.
|
|
20703
|
+
awaitTerminalCommit: (taskId) => terminalCommits.receipt(taskId),
|
|
19801
20704
|
maxTaskOutputBytes: config.maxTaskOutputBytes,
|
|
20705
|
+
artifactLimits: config.artifactLimits,
|
|
20706
|
+
startupTimeoutMs: config.startupTimeoutMs,
|
|
19802
20707
|
// See DaemonConfig.maxInlineEventBytes's own doc comment — already validated above.
|
|
19803
20708
|
maxInlineEventBytes: config.maxInlineEventBytes,
|
|
19804
20709
|
// additive-minor (`task.complete.document`): passed through verbatim,
|
|
@@ -19846,6 +20751,20 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
19846
20751
|
if (config.agentHome !== void 0 && config.agentEgress !== void 0) {
|
|
19847
20752
|
await runner.recoverAgentMessageOutboxes(path3__default.join(config.agentHome.hostStorageRoot, "agents"));
|
|
19848
20753
|
}
|
|
20754
|
+
const replayRecoveryTerminals = async (taskId) => {
|
|
20755
|
+
if (!activeJournal || !journalIdentity) return;
|
|
20756
|
+
for (const terminal of await activeJournal.listPendingTerminals(journalIdentity)) {
|
|
20757
|
+
if (terminal.truthState !== "pending" || taskId !== void 0 && terminal.taskId !== taskId) continue;
|
|
20758
|
+
if (runner?.hasPendingRecoveredAgentMessage(terminal.taskId)) continue;
|
|
20759
|
+
const envelope = decodeEnvelope(terminal.bytes);
|
|
20760
|
+
if (envelope.task_id !== terminal.taskId || terminalKindOf(envelope.type) !== terminal.terminalType) {
|
|
20761
|
+
throw new Error("durable terminal execution binding mismatch");
|
|
20762
|
+
}
|
|
20763
|
+
overrides.executionRecoveryFault?.("terminal:before-send");
|
|
20764
|
+
observer.handleOutboundEnvelope(envelope);
|
|
20765
|
+
connection?.send(envelope);
|
|
20766
|
+
}
|
|
20767
|
+
};
|
|
19849
20768
|
const handleAgentHomeProjectionEnvelope = async (envelope) => {
|
|
19850
20769
|
if (envelope.type !== "agent.home.projection") return false;
|
|
19851
20770
|
if (agentHomeManager === void 0 || agentHomeProjectionCompletion === void 0) {
|
|
@@ -19980,6 +20899,7 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
19980
20899
|
capabilities,
|
|
19981
20900
|
clientVersion: localAgentRelease.version,
|
|
19982
20901
|
runtimes,
|
|
20902
|
+
harnesses,
|
|
19983
20903
|
getConfiguredToolsets: () => toolsetRegistry.snapshot().configuredToolsets,
|
|
19984
20904
|
auth,
|
|
19985
20905
|
cursorStore,
|
|
@@ -20021,12 +20941,22 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
20021
20941
|
activePressureEngine?.assertAckCriticalAllowed();
|
|
20022
20942
|
await activeJournal.appendEnvelope(toJournalEnvelopeRecord(envelope, journalIdentity));
|
|
20023
20943
|
if (isTaskOfferType(envelope.type) && envelope.task_id !== void 0) {
|
|
20944
|
+
if (terminalCommits.hasPending(envelope.task_id)) {
|
|
20945
|
+
await terminalCommits.retry(envelope.task_id);
|
|
20946
|
+
return;
|
|
20947
|
+
}
|
|
20024
20948
|
const task = await activeJournal.readTask(envelope.task_id, journalIdentity);
|
|
20025
20949
|
if (!task) throw new Error("durable offer has no execution record");
|
|
20026
|
-
if (task.localState !== "received")
|
|
20950
|
+
if (task.localState !== "received") {
|
|
20951
|
+
await terminalCommits.retry(envelope.task_id);
|
|
20952
|
+
return;
|
|
20953
|
+
}
|
|
20027
20954
|
}
|
|
20028
20955
|
await runner?.handleEnvelope(envelope);
|
|
20029
|
-
|
|
20956
|
+
if (envelope.type === "agent.message.disposition") {
|
|
20957
|
+
await replayRecoveryTerminals(envelope.task_id);
|
|
20958
|
+
}
|
|
20959
|
+
if (envelope.task_id !== void 0) await terminalCommits.receipt(envelope.task_id);
|
|
20030
20960
|
} : (envelope) => {
|
|
20031
20961
|
if (tenantRebinding) {
|
|
20032
20962
|
return Promise.reject(new Error("tenant enrollment is being re-paired; inbound work is blocked until restart"));
|
|
@@ -20074,18 +21004,7 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
20074
21004
|
});
|
|
20075
21005
|
}
|
|
20076
21006
|
});
|
|
20077
|
-
|
|
20078
|
-
for (const terminal of await activeJournal.listPendingTerminals(journalIdentity)) {
|
|
20079
|
-
if (terminal.truthState !== "pending") continue;
|
|
20080
|
-
const envelope = decodeEnvelope(terminal.bytes);
|
|
20081
|
-
if (envelope.task_id !== terminal.taskId || terminalKindOf(envelope.type) !== terminal.terminalType) {
|
|
20082
|
-
throw new Error("durable terminal execution binding mismatch");
|
|
20083
|
-
}
|
|
20084
|
-
overrides.executionRecoveryFault?.("terminal:before-send");
|
|
20085
|
-
observer.handleOutboundEnvelope(envelope);
|
|
20086
|
-
connection.send(envelope);
|
|
20087
|
-
}
|
|
20088
|
-
}
|
|
21007
|
+
await replayRecoveryTerminals();
|
|
20089
21008
|
await connection.start();
|
|
20090
21009
|
await connection.waitForConnection();
|
|
20091
21010
|
runner.retryRecoveredAgentMessages();
|
|
@@ -20200,7 +21119,7 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
20200
21119
|
pendingLateMutationBarriers.add(observedActiveTeardown);
|
|
20201
21120
|
void observedActiveTeardown.finally(() => pendingLateMutationBarriers.delete(observedActiveTeardown)).catch(() => void 0);
|
|
20202
21121
|
}
|
|
20203
|
-
const journalTailSettled = journal ? await attempt(() =>
|
|
21122
|
+
const journalTailSettled = journal ? await attempt(() => terminalCommits.stop(), true) : true;
|
|
20204
21123
|
const maintenanceSettled = await attempt(() => maintenanceStopped, true);
|
|
20205
21124
|
const drainTimeoutMs = opts.drainTimeoutMs ?? overrides.shutdown?.outboxDrainTimeoutMs ?? DEFAULT_SHUTDOWN_OUTBOX_DRAIN_TIMEOUT_MS;
|
|
20206
21125
|
const connectionStopped = await attempt(() => connection?.stop(drainTimeoutMs), true);
|
|
@@ -20720,6 +21639,7 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
20720
21639
|
revoked: connection?.isRevoked() ?? auth.isRevoked(),
|
|
20721
21640
|
deviceId: auth.deviceId,
|
|
20722
21641
|
activeTaskCount: runner?.activeTaskCount ?? 0,
|
|
21642
|
+
pendingTerminalCommits: terminalCommits.pendingCount,
|
|
20723
21643
|
branding: config.branding,
|
|
20724
21644
|
operationalHealth: operationalHealth.snapshot(),
|
|
20725
21645
|
toolsets: toolsetRegistry.status(),
|
|
@@ -21185,8 +22105,8 @@ var TruthMemoryClient = class {
|
|
|
21185
22105
|
#requestId;
|
|
21186
22106
|
#allowedObjectDownloadOrigins;
|
|
21187
22107
|
async listManifest(query = {}) {
|
|
21188
|
-
const
|
|
21189
|
-
const response = await this.#proofFetch(
|
|
22108
|
+
const path43 = manifestPath(query);
|
|
22109
|
+
const response = await this.#proofFetch(path43, {
|
|
21190
22110
|
method: "GET",
|
|
21191
22111
|
operation: "truth.list",
|
|
21192
22112
|
resource: "records",
|
|
@@ -21293,9 +22213,9 @@ var TruthMemoryClient = class {
|
|
|
21293
22213
|
}
|
|
21294
22214
|
async #write(kind, recordKey, requestId, payload, expectedPrimary, expectedSnapshots) {
|
|
21295
22215
|
assertDistinctExpectedWrites([expectedPrimary, ...expectedSnapshots]);
|
|
21296
|
-
const
|
|
22216
|
+
const path43 = recordPath(kind, recordKey);
|
|
21297
22217
|
const body = new TextEncoder().encode(JSON.stringify(payload));
|
|
21298
|
-
const response = await this.#proofFetch(
|
|
22218
|
+
const response = await this.#proofFetch(path43, {
|
|
21299
22219
|
method: "PUT",
|
|
21300
22220
|
operation: "truth.write",
|
|
21301
22221
|
resource: `${kind}/${recordKey}`,
|
|
@@ -21321,8 +22241,8 @@ var TruthMemoryClient = class {
|
|
|
21321
22241
|
};
|
|
21322
22242
|
}
|
|
21323
22243
|
async #readVerified(listed) {
|
|
21324
|
-
const
|
|
21325
|
-
const response = await this.#proofFetch(
|
|
22244
|
+
const path43 = recordPath(listed.kind, listed.recordKey);
|
|
22245
|
+
const response = await this.#proofFetch(path43, {
|
|
21326
22246
|
method: "GET",
|
|
21327
22247
|
operation: "truth.read",
|
|
21328
22248
|
resource: `${listed.kind}/${listed.recordKey}`,
|
|
@@ -21383,16 +22303,16 @@ var TruthMemoryClient = class {
|
|
|
21383
22303
|
}
|
|
21384
22304
|
return { ...listed, bytes };
|
|
21385
22305
|
}
|
|
21386
|
-
async #proofFetch(
|
|
22306
|
+
async #proofFetch(path43, request) {
|
|
21387
22307
|
const proof = await this.options.signer.sign({
|
|
21388
22308
|
method: request.method,
|
|
21389
|
-
path:
|
|
22309
|
+
path: path43,
|
|
21390
22310
|
operation: request.operation,
|
|
21391
22311
|
resource: request.resource,
|
|
21392
22312
|
requestId: request.requestId,
|
|
21393
22313
|
body: request.body
|
|
21394
22314
|
});
|
|
21395
|
-
const response = await this.#fetch(new URL(
|
|
22315
|
+
const response = await this.#fetch(new URL(path43, this.#base), {
|
|
21396
22316
|
method: request.method,
|
|
21397
22317
|
headers: {
|
|
21398
22318
|
...request.headers,
|
|
@@ -21403,7 +22323,7 @@ var TruthMemoryClient = class {
|
|
|
21403
22323
|
if (!response.ok) {
|
|
21404
22324
|
throw new TruthMemoryClientError(
|
|
21405
22325
|
"truth_http_failed",
|
|
21406
|
-
`truth request ${request.method} ${
|
|
22326
|
+
`truth request ${request.method} ${path43} failed with HTTP ${response.status}`,
|
|
21407
22327
|
response.status
|
|
21408
22328
|
);
|
|
21409
22329
|
}
|
|
@@ -21661,7 +22581,7 @@ function plistString(value) {
|
|
|
21661
22581
|
function generateLaunchdPlist(def) {
|
|
21662
22582
|
const { label, program, logDir } = def;
|
|
21663
22583
|
const args = [program.command, ...program.args];
|
|
21664
|
-
const cwd = program.cwd ??
|
|
22584
|
+
const cwd = program.cwd ?? os__default.homedir();
|
|
21665
22585
|
const outLog = path3__default.join(logDir, `${label}.out.log`);
|
|
21666
22586
|
const errLog = path3__default.join(logDir, `${label}.err.log`);
|
|
21667
22587
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
@@ -21695,8 +22615,8 @@ ${args.map((a) => ` ${plistString(a)}`).join("\n")}
|
|
|
21695
22615
|
}
|
|
21696
22616
|
function createLaunchdLifecycle(def, deps = {}) {
|
|
21697
22617
|
const run = deps.run ?? defaultRunner;
|
|
21698
|
-
const
|
|
21699
|
-
const homedir = deps.homedir ?? (() =>
|
|
22618
|
+
const fs36 = deps.fs ?? promises;
|
|
22619
|
+
const homedir = deps.homedir ?? (() => os__default.homedir());
|
|
21700
22620
|
const getuid = deps.getuid ?? (() => {
|
|
21701
22621
|
if (typeof process.getuid !== "function") {
|
|
21702
22622
|
throw new Error("launchd lifecycle requires a POSIX uid (process.getuid unavailable) \u2014 this module only runs on macOS");
|
|
@@ -21709,7 +22629,7 @@ function createLaunchdLifecycle(def, deps = {}) {
|
|
|
21709
22629
|
const serviceTarget = () => `${domainTarget()}/${label}`;
|
|
21710
22630
|
async function fileExists(p) {
|
|
21711
22631
|
try {
|
|
21712
|
-
await
|
|
22632
|
+
await fs36.stat(p);
|
|
21713
22633
|
return true;
|
|
21714
22634
|
} catch {
|
|
21715
22635
|
return false;
|
|
@@ -21717,9 +22637,9 @@ function createLaunchdLifecycle(def, deps = {}) {
|
|
|
21717
22637
|
}
|
|
21718
22638
|
async function writePlist(program) {
|
|
21719
22639
|
const xml = generateLaunchdPlist({ label, program, logDir: def.logDir });
|
|
21720
|
-
await
|
|
21721
|
-
await
|
|
21722
|
-
await
|
|
22640
|
+
await fs36.mkdir(path3__default.dirname(plistPath()), { recursive: true });
|
|
22641
|
+
await fs36.mkdir(def.logDir, { recursive: true });
|
|
22642
|
+
await fs36.writeFile(plistPath(), xml, "utf8");
|
|
21723
22643
|
}
|
|
21724
22644
|
async function install(opts = {}) {
|
|
21725
22645
|
await writePlist(opts.program ?? def.program);
|
|
@@ -21730,7 +22650,7 @@ function createLaunchdLifecycle(def, deps = {}) {
|
|
|
21730
22650
|
}
|
|
21731
22651
|
async function uninstall() {
|
|
21732
22652
|
await runIdempotent(run, "launchctl", ["bootout", serviceTarget()], "launchctl bootout", LAUNCHD_NOT_LOADED);
|
|
21733
|
-
await
|
|
22653
|
+
await fs36.rm(plistPath(), { force: true });
|
|
21734
22654
|
}
|
|
21735
22655
|
async function start() {
|
|
21736
22656
|
if (!await fileExists(plistPath())) {
|
|
@@ -21789,7 +22709,7 @@ function generateSystemdUnit(def) {
|
|
|
21789
22709
|
const { name, displayName, program, logDir } = def;
|
|
21790
22710
|
assertNoControlChars(name, "name");
|
|
21791
22711
|
assertNoControlChars(displayName, "displayName");
|
|
21792
|
-
const cwd = program.cwd ??
|
|
22712
|
+
const cwd = program.cwd ?? os__default.homedir();
|
|
21793
22713
|
assertNoControlChars(cwd, "program.cwd");
|
|
21794
22714
|
const outLog = path3__default.join(logDir, `${name}.out.log`);
|
|
21795
22715
|
const errLog = path3__default.join(logDir, `${name}.err.log`);
|
|
@@ -21814,14 +22734,14 @@ WantedBy=default.target
|
|
|
21814
22734
|
}
|
|
21815
22735
|
function createSystemdLifecycle(def, deps = {}) {
|
|
21816
22736
|
const run = deps.run ?? defaultRunner;
|
|
21817
|
-
const
|
|
21818
|
-
const homedir = deps.homedir ?? (() =>
|
|
22737
|
+
const fs36 = deps.fs ?? promises;
|
|
22738
|
+
const homedir = deps.homedir ?? (() => os__default.homedir());
|
|
21819
22739
|
const name = sanitizeServiceName(def.name);
|
|
21820
22740
|
const unitName = `${name}.service`;
|
|
21821
22741
|
const unitPath = () => path3__default.join(homedir(), ".config", "systemd", "user", unitName);
|
|
21822
22742
|
async function fileExists(p) {
|
|
21823
22743
|
try {
|
|
21824
|
-
await
|
|
22744
|
+
await fs36.stat(p);
|
|
21825
22745
|
return true;
|
|
21826
22746
|
} catch {
|
|
21827
22747
|
return false;
|
|
@@ -21829,9 +22749,9 @@ function createSystemdLifecycle(def, deps = {}) {
|
|
|
21829
22749
|
}
|
|
21830
22750
|
async function writeUnit(program) {
|
|
21831
22751
|
const unit = generateSystemdUnit({ name, displayName: def.displayName ?? def.name, program, logDir: def.logDir });
|
|
21832
|
-
await
|
|
21833
|
-
await
|
|
21834
|
-
await
|
|
22752
|
+
await fs36.mkdir(path3__default.dirname(unitPath()), { recursive: true });
|
|
22753
|
+
await fs36.mkdir(def.logDir, { recursive: true });
|
|
22754
|
+
await fs36.writeFile(unitPath(), unit, "utf8");
|
|
21835
22755
|
}
|
|
21836
22756
|
async function install(opts = {}) {
|
|
21837
22757
|
await writeUnit(opts.program ?? def.program);
|
|
@@ -21840,7 +22760,7 @@ function createSystemdLifecycle(def, deps = {}) {
|
|
|
21840
22760
|
}
|
|
21841
22761
|
async function uninstall() {
|
|
21842
22762
|
await runIdempotent(run, "systemctl", ["--user", "disable", "--now", unitName], "systemctl disable --now", SYSTEMD_NOT_LOADED);
|
|
21843
|
-
await
|
|
22763
|
+
await fs36.rm(unitPath(), { force: true });
|
|
21844
22764
|
await run("systemctl", ["--user", "daemon-reload"]);
|
|
21845
22765
|
}
|
|
21846
22766
|
async function start() {
|
|
@@ -21903,7 +22823,7 @@ ${argXml}${cwdXml}
|
|
|
21903
22823
|
}
|
|
21904
22824
|
function createWinswLifecycle(def, deps = {}) {
|
|
21905
22825
|
const run = deps.run ?? defaultRunner;
|
|
21906
|
-
const
|
|
22826
|
+
const fs36 = deps.fs ?? promises;
|
|
21907
22827
|
const windows = def.windows;
|
|
21908
22828
|
if (!windows) {
|
|
21909
22829
|
throw new Error("WinSW service lifecycle requires `ServiceDefinition.windows.winswBin` (the product-bundled WinSW executable path)");
|
|
@@ -21915,7 +22835,7 @@ function createWinswLifecycle(def, deps = {}) {
|
|
|
21915
22835
|
const xmlPath = path3__default.join(installDir, `${id}.xml`);
|
|
21916
22836
|
async function fileExists(p) {
|
|
21917
22837
|
try {
|
|
21918
|
-
await
|
|
22838
|
+
await fs36.stat(p);
|
|
21919
22839
|
return true;
|
|
21920
22840
|
} catch {
|
|
21921
22841
|
return false;
|
|
@@ -21923,10 +22843,10 @@ function createWinswLifecycle(def, deps = {}) {
|
|
|
21923
22843
|
}
|
|
21924
22844
|
async function writeFiles(program) {
|
|
21925
22845
|
const xml = generateWinswXml({ id, displayName: def.displayName ?? def.name, program, logDir: def.logDir });
|
|
21926
|
-
await
|
|
21927
|
-
await
|
|
21928
|
-
await
|
|
21929
|
-
await
|
|
22846
|
+
await fs36.mkdir(installDir, { recursive: true });
|
|
22847
|
+
await fs36.mkdir(def.logDir, { recursive: true });
|
|
22848
|
+
await fs36.copyFile(winswBin, exePath);
|
|
22849
|
+
await fs36.writeFile(xmlPath, xml, "utf8");
|
|
21930
22850
|
}
|
|
21931
22851
|
async function install(opts = {}) {
|
|
21932
22852
|
await writeFiles(opts.program ?? def.program);
|
|
@@ -21936,8 +22856,8 @@ function createWinswLifecycle(def, deps = {}) {
|
|
|
21936
22856
|
async function uninstall() {
|
|
21937
22857
|
await runIdempotent(run, exePath, ["stop"], "winsw stop", WINSW_NOT_INSTALLED);
|
|
21938
22858
|
await runIdempotent(run, exePath, ["uninstall"], "winsw uninstall", WINSW_NOT_INSTALLED);
|
|
21939
|
-
await
|
|
21940
|
-
await
|
|
22859
|
+
await fs36.rm(exePath, { force: true });
|
|
22860
|
+
await fs36.rm(xmlPath, { force: true });
|
|
21941
22861
|
}
|
|
21942
22862
|
async function start() {
|
|
21943
22863
|
if (!await fileExists(xmlPath)) {
|
|
@@ -21980,6 +22900,1191 @@ function createServiceLifecycle(def, opts = {}) {
|
|
|
21980
22900
|
}
|
|
21981
22901
|
}
|
|
21982
22902
|
|
|
21983
|
-
|
|
22903
|
+
// src/bin/runtime-probe.ts
|
|
22904
|
+
var ALL_RUNTIME_IDS2 = ["pi", "claude", "codex"];
|
|
22905
|
+
var RUNTIME_PROBE_TIMEOUT_MS = 5e3;
|
|
22906
|
+
var MAX_RUNTIME_ID_CHARS = 128;
|
|
22907
|
+
var MAX_RUNTIME_VERSION_CHARS = 256;
|
|
22908
|
+
var MAX_PERMISSION_MODES = 32;
|
|
22909
|
+
var MAX_PERMISSION_MODE_CHARS = 64;
|
|
22910
|
+
function boundedSingleLine(value, maxChars) {
|
|
22911
|
+
return value.replace(/[\r\n\t]/g, " ").slice(0, maxChars);
|
|
22912
|
+
}
|
|
22913
|
+
var RuntimeProbeTimeout = class extends Error {
|
|
22914
|
+
};
|
|
22915
|
+
async function detectWithTimeout(adapter, timeoutMs) {
|
|
22916
|
+
let timer;
|
|
22917
|
+
try {
|
|
22918
|
+
return await Promise.race([
|
|
22919
|
+
adapter.detect(),
|
|
22920
|
+
new Promise((_resolve, reject) => {
|
|
22921
|
+
timer = setTimeout(() => reject(new RuntimeProbeTimeout()), timeoutMs);
|
|
22922
|
+
timer.unref?.();
|
|
22923
|
+
})
|
|
22924
|
+
]);
|
|
22925
|
+
} finally {
|
|
22926
|
+
if (timer) clearTimeout(timer);
|
|
22927
|
+
}
|
|
22928
|
+
}
|
|
22929
|
+
function defaultRuntimeAdapters(runtimeAllowlist) {
|
|
22930
|
+
const ids = runtimeAllowlist ? ALL_RUNTIME_IDS2.filter((id) => runtimeAllowlist.includes(id)) : ALL_RUNTIME_IDS2;
|
|
22931
|
+
return ids.map((id) => {
|
|
22932
|
+
switch (id) {
|
|
22933
|
+
case "pi":
|
|
22934
|
+
return new PiAdapter();
|
|
22935
|
+
case "claude":
|
|
22936
|
+
return new ClaudeAdapter();
|
|
22937
|
+
case "codex":
|
|
22938
|
+
return new CodexAdapter();
|
|
22939
|
+
}
|
|
22940
|
+
});
|
|
22941
|
+
}
|
|
22942
|
+
async function probeRuntimes(adapters, options = {}) {
|
|
22943
|
+
const timeoutMs = options.timeoutMs ?? RUNTIME_PROBE_TIMEOUT_MS;
|
|
22944
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0) throw new Error("runtime probe timeout must be a positive integer");
|
|
22945
|
+
return Promise.all(
|
|
22946
|
+
adapters.map(async (adapter) => {
|
|
22947
|
+
let id = "unavailable";
|
|
22948
|
+
let steer = false;
|
|
22949
|
+
let resume = false;
|
|
22950
|
+
let permissionModes = [];
|
|
22951
|
+
try {
|
|
22952
|
+
id = boundedSingleLine(adapter.descriptor.id, MAX_RUNTIME_ID_CHARS);
|
|
22953
|
+
const caps = adapter.descriptor.capabilities;
|
|
22954
|
+
steer = caps.steer === true;
|
|
22955
|
+
resume = caps.resume === true;
|
|
22956
|
+
permissionModes = caps.permissionModes.slice(0, MAX_PERMISSION_MODES).map((mode) => boundedSingleLine(mode, MAX_PERMISSION_MODE_CHARS));
|
|
22957
|
+
const detected = validateRuntimeDetectResult(await detectWithTimeout(adapter, timeoutMs));
|
|
22958
|
+
return {
|
|
22959
|
+
id,
|
|
22960
|
+
present: detected.kind === "available",
|
|
22961
|
+
outcome: detected.kind,
|
|
22962
|
+
...detected.kind !== "available" || detected.version === void 0 ? {} : { version: boundedSingleLine(detected.version, MAX_RUNTIME_VERSION_CHARS) },
|
|
22963
|
+
...detected.kind === "available" && typeof detected.authPresent === "boolean" ? { authPresent: detected.authPresent } : {},
|
|
22964
|
+
steer,
|
|
22965
|
+
resume,
|
|
22966
|
+
permissionModes
|
|
22967
|
+
};
|
|
22968
|
+
} catch (error) {
|
|
22969
|
+
return { id, present: false, outcome: error instanceof RuntimeProbeTimeout ? "timeout" : "probe-failed", steer, resume, permissionModes };
|
|
22970
|
+
}
|
|
22971
|
+
})
|
|
22972
|
+
);
|
|
22973
|
+
}
|
|
22974
|
+
|
|
22975
|
+
// src/diagnostics/diagnostics.ts
|
|
22976
|
+
var MAX_QUARANTINE_ENTRIES = 100;
|
|
22977
|
+
var MAX_QUARANTINE_SCAN_ENTRIES = MAX_QUARANTINE_ENTRIES * 2;
|
|
22978
|
+
var MAX_QUARANTINE_READ_BYTES = 128 * 1024 * 1024;
|
|
22979
|
+
var MAX_DEVICE_RECORD_BYTES2 = 64 * 1024;
|
|
22980
|
+
var MAX_JOURNAL_COPY_BYTES = 512 * 1024 * 1024;
|
|
22981
|
+
var MAX_QUARANTINE_MANIFEST_BYTES = 64 * 1024;
|
|
22982
|
+
function safeProtocol(serverUrl) {
|
|
22983
|
+
try {
|
|
22984
|
+
const protocol = new URL(serverUrl).protocol.replace(/:$/, "");
|
|
22985
|
+
return protocol === "http" || protocol === "https" || protocol === "ws" || protocol === "wss" ? protocol : "unsupported";
|
|
22986
|
+
} catch {
|
|
22987
|
+
return "invalid";
|
|
22988
|
+
}
|
|
22989
|
+
}
|
|
22990
|
+
async function inspectDevice(storeDir) {
|
|
22991
|
+
const filePath = path3__default.join(storeDir, "device.json");
|
|
22992
|
+
let pathStat;
|
|
22993
|
+
try {
|
|
22994
|
+
pathStat = await promises.lstat(filePath);
|
|
22995
|
+
if (!pathStat.isFile()) return { status: "unavailable" };
|
|
22996
|
+
} catch (err) {
|
|
22997
|
+
if (err.code === "ENOENT") return { status: "unpaired" };
|
|
22998
|
+
return { status: "unavailable" };
|
|
22999
|
+
}
|
|
23000
|
+
let handle;
|
|
23001
|
+
try {
|
|
23002
|
+
handle = await promises.open(
|
|
23003
|
+
filePath,
|
|
23004
|
+
constants.O_RDONLY | constants.O_NONBLOCK | (constants.O_NOFOLLOW ?? 0)
|
|
23005
|
+
);
|
|
23006
|
+
} catch (err) {
|
|
23007
|
+
if (err.code === "ENOENT") return { status: "unpaired" };
|
|
23008
|
+
return { status: "unavailable" };
|
|
23009
|
+
}
|
|
23010
|
+
try {
|
|
23011
|
+
const stat = await handle.stat();
|
|
23012
|
+
const namedAfterOpen = await promises.lstat(filePath);
|
|
23013
|
+
if (!stat.isFile() || !namedAfterOpen.isFile() || namedAfterOpen.isSymbolicLink() || stat.dev !== pathStat.dev || stat.ino !== pathStat.ino || stat.size !== pathStat.size || stat.mtimeMs !== pathStat.mtimeMs || stat.ctimeMs !== pathStat.ctimeMs || stat.dev !== namedAfterOpen.dev || stat.ino !== namedAfterOpen.ino || stat.size !== namedAfterOpen.size || stat.mtimeMs !== namedAfterOpen.mtimeMs || stat.ctimeMs !== namedAfterOpen.ctimeMs || stat.size <= 0 || stat.size > MAX_DEVICE_RECORD_BYTES2) return { status: "unavailable" };
|
|
23014
|
+
const bytes = Buffer.alloc(stat.size);
|
|
23015
|
+
const { bytesRead } = await handle.read(bytes, 0, bytes.length, 0);
|
|
23016
|
+
const after = await handle.stat();
|
|
23017
|
+
const namedAfterRead = await promises.lstat(filePath);
|
|
23018
|
+
if (bytesRead !== bytes.length || after.dev !== stat.dev || after.ino !== stat.ino || after.size !== stat.size || after.mtimeMs !== stat.mtimeMs || after.ctimeMs !== stat.ctimeMs || namedAfterRead.dev !== stat.dev || namedAfterRead.ino !== stat.ino || namedAfterRead.size !== stat.size || namedAfterRead.mtimeMs !== stat.mtimeMs || namedAfterRead.ctimeMs !== stat.ctimeMs) {
|
|
23019
|
+
return { status: "unavailable" };
|
|
23020
|
+
}
|
|
23021
|
+
const parsed = JSON.parse(bytes.toString("utf8"));
|
|
23022
|
+
return typeof parsed.deviceId === "string" && parsed.deviceId.length > 0 ? { status: "paired", deviceIdHash: stableIdentifierHash(parsed.deviceId) } : { status: "unavailable" };
|
|
23023
|
+
} catch {
|
|
23024
|
+
return { status: "unavailable" };
|
|
23025
|
+
} finally {
|
|
23026
|
+
await handle.close();
|
|
23027
|
+
}
|
|
23028
|
+
}
|
|
23029
|
+
async function regularFileIdentity(filePath) {
|
|
23030
|
+
try {
|
|
23031
|
+
const stat = await promises.lstat(filePath, { bigint: true });
|
|
23032
|
+
if (!stat.isFile()) throw new Error("journal component is not a regular file");
|
|
23033
|
+
return { dev: stat.dev, ino: stat.ino, size: stat.size, mtimeNs: stat.mtimeNs, ctimeNs: stat.ctimeNs };
|
|
23034
|
+
} catch (err) {
|
|
23035
|
+
if (err.code === "ENOENT") return void 0;
|
|
23036
|
+
throw err;
|
|
23037
|
+
}
|
|
23038
|
+
}
|
|
23039
|
+
function sameIdentity(left, right) {
|
|
23040
|
+
if (!left || !right) return left === right;
|
|
23041
|
+
return left.dev === right.dev && left.ino === right.ino && left.size === right.size && left.mtimeNs === right.mtimeNs && left.ctimeNs === right.ctimeNs;
|
|
23042
|
+
}
|
|
23043
|
+
function identityFromBigIntStat(stat) {
|
|
23044
|
+
return { dev: stat.dev, ino: stat.ino, size: stat.size, mtimeNs: stat.mtimeNs, ctimeNs: stat.ctimeNs };
|
|
23045
|
+
}
|
|
23046
|
+
async function copyOpenFileBounded(source, expected, destinationPath) {
|
|
23047
|
+
const destination = await promises.open(destinationPath, "wx", 384);
|
|
23048
|
+
try {
|
|
23049
|
+
const buffer = Buffer.allocUnsafe(64 * 1024);
|
|
23050
|
+
const expectedBytes = Number(expected.size);
|
|
23051
|
+
let position = 0;
|
|
23052
|
+
while (position < expectedBytes) {
|
|
23053
|
+
const length = Math.min(buffer.length, expectedBytes - position);
|
|
23054
|
+
const { bytesRead } = await source.read(buffer, 0, length, position);
|
|
23055
|
+
if (bytesRead === 0) return false;
|
|
23056
|
+
let written = 0;
|
|
23057
|
+
while (written < bytesRead) {
|
|
23058
|
+
const result = await destination.write(buffer, written, bytesRead - written, position + written);
|
|
23059
|
+
if (result.bytesWritten === 0) throw new Error("diagnostics snapshot destination stopped accepting bytes");
|
|
23060
|
+
written += result.bytesWritten;
|
|
23061
|
+
}
|
|
23062
|
+
position += bytesRead;
|
|
23063
|
+
}
|
|
23064
|
+
await destination.sync();
|
|
23065
|
+
return sameIdentity(expected, identityFromBigIntStat(await source.stat({ bigint: true })));
|
|
23066
|
+
} finally {
|
|
23067
|
+
await destination.close();
|
|
23068
|
+
}
|
|
23069
|
+
}
|
|
23070
|
+
async function inspectJournal(storeDir) {
|
|
23071
|
+
const journalPath = path3__default.join(storeDir, JOURNAL_DB_FILENAME);
|
|
23072
|
+
try {
|
|
23073
|
+
const mainIdentity = await regularFileIdentity(journalPath);
|
|
23074
|
+
if (mainIdentity === void 0) return { status: "missing" };
|
|
23075
|
+
const walIdentity = await regularFileIdentity(path3__default.join(storeDir, `${JOURNAL_DB_FILENAME}-wal`));
|
|
23076
|
+
let sizeBytes = Number(mainIdentity.size);
|
|
23077
|
+
let walBytes = walIdentity === void 0 ? void 0 : Number(walIdentity.size);
|
|
23078
|
+
if (!isSqliteAvailable()) {
|
|
23079
|
+
return { status: "present", sizeBytes, ...walBytes === void 0 ? {} : { walBytes }, integrity: "not-checked" };
|
|
23080
|
+
}
|
|
23081
|
+
const componentNames = [JOURNAL_DB_FILENAME, `${JOURNAL_DB_FILENAME}-wal`, `${JOURNAL_DB_FILENAME}-shm`];
|
|
23082
|
+
const initial = /* @__PURE__ */ new Map();
|
|
23083
|
+
for (const name of componentNames) initial.set(name, await regularFileIdentity(path3__default.join(storeDir, name)));
|
|
23084
|
+
const snapshotMain = initial.get(JOURNAL_DB_FILENAME);
|
|
23085
|
+
if (!snapshotMain) return { status: "unavailable", reason: "journal changed during diagnostics snapshot" };
|
|
23086
|
+
sizeBytes = Number(snapshotMain.size);
|
|
23087
|
+
walBytes = initial.get(`${JOURNAL_DB_FILENAME}-wal`) === void 0 ? void 0 : Number(initial.get(`${JOURNAL_DB_FILENAME}-wal`).size);
|
|
23088
|
+
const opened = /* @__PURE__ */ new Map();
|
|
23089
|
+
let totalBytes = 0n;
|
|
23090
|
+
try {
|
|
23091
|
+
for (const name of componentNames) {
|
|
23092
|
+
let handle;
|
|
23093
|
+
try {
|
|
23094
|
+
handle = await promises.open(
|
|
23095
|
+
path3__default.join(storeDir, name),
|
|
23096
|
+
constants.O_RDONLY | constants.O_NONBLOCK | (constants.O_NOFOLLOW ?? 0)
|
|
23097
|
+
);
|
|
23098
|
+
} catch (err) {
|
|
23099
|
+
if (err.code === "ENOENT" && name !== JOURNAL_DB_FILENAME) continue;
|
|
23100
|
+
throw err;
|
|
23101
|
+
}
|
|
23102
|
+
const stat = await handle.stat({ bigint: true });
|
|
23103
|
+
if (!stat.isFile()) {
|
|
23104
|
+
await handle.close();
|
|
23105
|
+
throw new Error("journal component is not a regular file");
|
|
23106
|
+
}
|
|
23107
|
+
const identity = identityFromBigIntStat(stat);
|
|
23108
|
+
if (!sameIdentity(initial.get(name), identity)) {
|
|
23109
|
+
await handle.close();
|
|
23110
|
+
return { status: "unavailable", sizeBytes, ...walBytes === void 0 ? {} : { walBytes }, reason: "journal changed during diagnostics snapshot" };
|
|
23111
|
+
}
|
|
23112
|
+
totalBytes += identity.size;
|
|
23113
|
+
opened.set(name, { handle, identity });
|
|
23114
|
+
}
|
|
23115
|
+
if (totalBytes > BigInt(MAX_JOURNAL_COPY_BYTES)) {
|
|
23116
|
+
return {
|
|
23117
|
+
status: "present",
|
|
23118
|
+
sizeBytes,
|
|
23119
|
+
...walBytes === void 0 ? {} : { walBytes },
|
|
23120
|
+
integrity: "not-checked",
|
|
23121
|
+
reason: "journal exceeds the bounded diagnostics copy limit"
|
|
23122
|
+
};
|
|
23123
|
+
}
|
|
23124
|
+
const tempDir = await promises.mkdtemp(path3__default.join(os__default.tmpdir(), "byok-journal-inspect-"));
|
|
23125
|
+
const { DatabaseSync } = loadSqliteModule();
|
|
23126
|
+
let db;
|
|
23127
|
+
try {
|
|
23128
|
+
for (const name of componentNames) {
|
|
23129
|
+
const component = opened.get(name);
|
|
23130
|
+
if (component && !await copyOpenFileBounded(component.handle, component.identity, path3__default.join(tempDir, name))) {
|
|
23131
|
+
return { status: "unavailable", sizeBytes, ...walBytes === void 0 ? {} : { walBytes }, reason: "journal changed during diagnostics snapshot" };
|
|
23132
|
+
}
|
|
23133
|
+
}
|
|
23134
|
+
for (const [name, component] of opened) {
|
|
23135
|
+
if (!sameIdentity(component.identity, identityFromBigIntStat(await component.handle.stat({ bigint: true }))) || !sameIdentity(component.identity, await regularFileIdentity(path3__default.join(storeDir, name)))) {
|
|
23136
|
+
return { status: "unavailable", sizeBytes, ...walBytes === void 0 ? {} : { walBytes }, reason: "journal changed during diagnostics snapshot" };
|
|
23137
|
+
}
|
|
23138
|
+
}
|
|
23139
|
+
for (const name of componentNames) {
|
|
23140
|
+
if (!opened.has(name) && await regularFileIdentity(path3__default.join(storeDir, name)) !== void 0) {
|
|
23141
|
+
return { status: "unavailable", sizeBytes, ...walBytes === void 0 ? {} : { walBytes }, reason: "journal changed during diagnostics snapshot" };
|
|
23142
|
+
}
|
|
23143
|
+
}
|
|
23144
|
+
const header = Buffer.alloc(16);
|
|
23145
|
+
const copiedHandle = await promises.open(path3__default.join(tempDir, JOURNAL_DB_FILENAME), "r");
|
|
23146
|
+
try {
|
|
23147
|
+
const { bytesRead } = await copiedHandle.read(header, 0, header.length, 0);
|
|
23148
|
+
if (bytesRead !== 16 || header.toString("binary") !== "SQLite format 3\0") {
|
|
23149
|
+
return { status: "corrupt", sizeBytes, ...walBytes === void 0 ? {} : { walBytes }, reason: "journal has an invalid SQLite header" };
|
|
23150
|
+
}
|
|
23151
|
+
} finally {
|
|
23152
|
+
await copiedHandle.close();
|
|
23153
|
+
}
|
|
23154
|
+
db = new DatabaseSync(path3__default.join(tempDir, JOURNAL_DB_FILENAME), { readOnly: true });
|
|
23155
|
+
const result = db.prepare("PRAGMA quick_check(1)").get();
|
|
23156
|
+
if (result?.quick_check !== "ok") {
|
|
23157
|
+
return { status: "corrupt", sizeBytes, ...walBytes === void 0 ? {} : { walBytes }, reason: "journal quick_check failed" };
|
|
23158
|
+
}
|
|
23159
|
+
return { status: "present", sizeBytes, ...walBytes === void 0 ? {} : { walBytes }, integrity: "ok" };
|
|
23160
|
+
} catch {
|
|
23161
|
+
return { status: "unavailable", sizeBytes, ...walBytes === void 0 ? {} : { walBytes }, reason: "journal snapshot could not be checked" };
|
|
23162
|
+
} finally {
|
|
23163
|
+
db?.close();
|
|
23164
|
+
await promises.rm(tempDir, { recursive: true, force: true });
|
|
23165
|
+
}
|
|
23166
|
+
} finally {
|
|
23167
|
+
await Promise.all([...opened.values()].map(({ handle }) => handle.close().catch(() => void 0)));
|
|
23168
|
+
}
|
|
23169
|
+
} catch {
|
|
23170
|
+
return { status: "unavailable", reason: "journal files could not be inspected" };
|
|
23171
|
+
}
|
|
23172
|
+
}
|
|
23173
|
+
async function inspectWorkspace(workspaceRoot) {
|
|
23174
|
+
try {
|
|
23175
|
+
const stat = await promises.stat(workspaceRoot);
|
|
23176
|
+
if (!stat.isDirectory()) return { status: "unavailable", reason: "workspace root is not a directory" };
|
|
23177
|
+
try {
|
|
23178
|
+
await promises.access(workspaceRoot, constants.R_OK | constants.W_OK);
|
|
23179
|
+
return { status: "available", writable: true };
|
|
23180
|
+
} catch {
|
|
23181
|
+
return { status: "available", writable: false };
|
|
23182
|
+
}
|
|
23183
|
+
} catch (err) {
|
|
23184
|
+
if (err.code === "ENOENT") return { status: "missing" };
|
|
23185
|
+
return { status: "unavailable", reason: "workspace root could not be inspected" };
|
|
23186
|
+
}
|
|
23187
|
+
}
|
|
23188
|
+
function readPinnedQuarantineFile(name, maxBytes, includeBytes, budget) {
|
|
23189
|
+
if (path3__default.basename(name) !== name || name === "." || name === "..") {
|
|
23190
|
+
throw new Error("quarantine manifest contains an invalid evidence name");
|
|
23191
|
+
}
|
|
23192
|
+
const namedBefore = lstatSync(name, { bigint: true });
|
|
23193
|
+
if (!namedBefore.isFile() || namedBefore.isSymbolicLink()) {
|
|
23194
|
+
throw new Error("quarantine entry is not a real regular file");
|
|
23195
|
+
}
|
|
23196
|
+
const fd = openSync(name, constants.O_RDONLY | constants.O_NONBLOCK | (constants.O_NOFOLLOW ?? 0));
|
|
23197
|
+
try {
|
|
23198
|
+
const opened = fstatSync(fd, { bigint: true });
|
|
23199
|
+
if (!opened.isFile() || !sameInode2(opened, namedBefore) || opened.size < 0 || opened.size > BigInt(maxBytes)) {
|
|
23200
|
+
throw new Error("quarantine entry could not be opened as a bounded regular file");
|
|
23201
|
+
}
|
|
23202
|
+
let bytes;
|
|
23203
|
+
if (includeBytes) {
|
|
23204
|
+
const sizeBytes = Number(opened.size);
|
|
23205
|
+
if (sizeBytes > budget.remainingBytes) {
|
|
23206
|
+
throw new Error("quarantine validation exceeds the 128 MiB cumulative read limit");
|
|
23207
|
+
}
|
|
23208
|
+
budget.remainingBytes -= sizeBytes;
|
|
23209
|
+
bytes = Buffer.alloc(sizeBytes);
|
|
23210
|
+
let position = 0;
|
|
23211
|
+
while (position < bytes.length) {
|
|
23212
|
+
const bytesRead = readSync(fd, bytes, position, bytes.length - position, position);
|
|
23213
|
+
if (bytesRead === 0) throw new Error("quarantine entry ended during bounded read");
|
|
23214
|
+
position += bytesRead;
|
|
23215
|
+
}
|
|
23216
|
+
}
|
|
23217
|
+
const openedAfter = fstatSync(fd, { bigint: true });
|
|
23218
|
+
const namedAfter = lstatSync(name, { bigint: true });
|
|
23219
|
+
if (!sameFileState5(openedAfter, opened) || !sameFileState5(namedAfter, opened)) {
|
|
23220
|
+
throw new Error("quarantine entry changed during inspection");
|
|
23221
|
+
}
|
|
23222
|
+
return {
|
|
23223
|
+
sizeBytes: Number(opened.size),
|
|
23224
|
+
modifiedAt: new Date(Number(opened.mtimeMs)).toISOString(),
|
|
23225
|
+
...bytes === void 0 ? {} : { bytes }
|
|
23226
|
+
};
|
|
23227
|
+
} finally {
|
|
23228
|
+
closeSync(fd);
|
|
23229
|
+
}
|
|
23230
|
+
}
|
|
23231
|
+
function isHealthQuarantineManifest(value) {
|
|
23232
|
+
if (typeof value !== "object" || value === null) return false;
|
|
23233
|
+
const candidate = value;
|
|
23234
|
+
return candidate.version === 1 && typeof candidate.quarantinedAt === "string" && Number.isFinite(Date.parse(candidate.quarantinedAt)) && typeof candidate.reason === "string" && typeof candidate.sourcePath === "string" && typeof candidate.evidenceFile === "string" && typeof candidate.sha256 === "string" && /^[0-9a-f]{64}$/.test(candidate.sha256) && Number.isSafeInteger(candidate.sizeBytes) && candidate.sizeBytes >= 0 && candidate.sizeBytes <= MAX_OPERATIONAL_HEALTH_FILE_BYTES;
|
|
23235
|
+
}
|
|
23236
|
+
function isJournalQuarantineManifest(value) {
|
|
23237
|
+
if (typeof value !== "object" || value === null) return false;
|
|
23238
|
+
const candidate = value;
|
|
23239
|
+
return typeof candidate.quarantinedAt === "string" && Number.isFinite(Date.parse(candidate.quarantinedAt)) && typeof candidate.reason === "string" && typeof candidate.originalPath === "string" && Array.isArray(candidate.files) && candidate.files.length > 0 && candidate.files.length <= 3 && candidate.files.every((file) => typeof file === "string");
|
|
23240
|
+
}
|
|
23241
|
+
function inspectQuarantinePinned(dir, expectedDirectory) {
|
|
23242
|
+
const previousCwd = process.cwd();
|
|
23243
|
+
try {
|
|
23244
|
+
process.chdir(dir);
|
|
23245
|
+
const pinned = lstatSync(".", { bigint: true });
|
|
23246
|
+
if (!pinned.isDirectory() || !sameInode2(pinned, expectedDirectory)) {
|
|
23247
|
+
throw new Error("quarantine directory changed during inspection");
|
|
23248
|
+
}
|
|
23249
|
+
const selected = [];
|
|
23250
|
+
let scanTruncated = false;
|
|
23251
|
+
const directory = opendirSync(".");
|
|
23252
|
+
try {
|
|
23253
|
+
for (; ; ) {
|
|
23254
|
+
const entry = directory.readSync();
|
|
23255
|
+
if (!entry) break;
|
|
23256
|
+
if (selected.length === MAX_QUARANTINE_SCAN_ENTRIES) {
|
|
23257
|
+
scanTruncated = true;
|
|
23258
|
+
break;
|
|
23259
|
+
}
|
|
23260
|
+
selected.push(entry);
|
|
23261
|
+
}
|
|
23262
|
+
} finally {
|
|
23263
|
+
directory.closeSync();
|
|
23264
|
+
}
|
|
23265
|
+
const evidence = /* @__PURE__ */ new Map();
|
|
23266
|
+
const processedManifests = /* @__PURE__ */ new Set();
|
|
23267
|
+
const readBudget = { remainingBytes: MAX_QUARANTINE_READ_BYTES };
|
|
23268
|
+
const processManifest = (manifestName) => {
|
|
23269
|
+
if (processedManifests.has(manifestName)) return;
|
|
23270
|
+
processedManifests.add(manifestName);
|
|
23271
|
+
const manifestFile = readPinnedQuarantineFile(manifestName, MAX_QUARANTINE_MANIFEST_BYTES, true, readBudget);
|
|
23272
|
+
let parsed;
|
|
23273
|
+
try {
|
|
23274
|
+
parsed = JSON.parse(manifestFile.bytes?.toString("utf8") ?? "");
|
|
23275
|
+
} catch {
|
|
23276
|
+
throw new Error("quarantine manifest is invalid JSON");
|
|
23277
|
+
}
|
|
23278
|
+
if (isHealthQuarantineManifest(parsed)) {
|
|
23279
|
+
if (`${parsed.evidenceFile}.manifest.json` !== manifestName) {
|
|
23280
|
+
throw new Error("health quarantine manifest is not bound to its evidence name");
|
|
23281
|
+
}
|
|
23282
|
+
const file = readPinnedQuarantineFile(parsed.evidenceFile, MAX_OPERATIONAL_HEALTH_FILE_BYTES, true, readBudget);
|
|
23283
|
+
if (file.sizeBytes !== parsed.sizeBytes || createHash("sha256").update(file.bytes ?? Buffer.alloc(0)).digest("hex") !== parsed.sha256) {
|
|
23284
|
+
throw new Error("health quarantine evidence does not match its manifest");
|
|
23285
|
+
}
|
|
23286
|
+
if (evidence.has(parsed.evidenceFile)) throw new Error("quarantine evidence is referenced more than once");
|
|
23287
|
+
evidence.set(parsed.evidenceFile, file);
|
|
23288
|
+
return;
|
|
23289
|
+
}
|
|
23290
|
+
if (isJournalQuarantineManifest(parsed)) {
|
|
23291
|
+
const manifestBase = manifestName.slice(0, -".manifest.json".length);
|
|
23292
|
+
const boundNames = parsed.files.map((file) => {
|
|
23293
|
+
if (path3__default.dirname(path3__default.resolve(file)) !== path3__default.resolve(".")) {
|
|
23294
|
+
throw new Error("journal quarantine manifest points outside quarantine");
|
|
23295
|
+
}
|
|
23296
|
+
return path3__default.basename(file);
|
|
23297
|
+
});
|
|
23298
|
+
if (!boundNames.includes(manifestBase)) {
|
|
23299
|
+
throw new Error("journal quarantine manifest is not bound to its primary database evidence");
|
|
23300
|
+
}
|
|
23301
|
+
for (const name of boundNames) {
|
|
23302
|
+
if (evidence.has(name)) throw new Error("quarantine evidence is referenced more than once");
|
|
23303
|
+
evidence.set(name, readPinnedQuarantineFile(name, MAX_JOURNAL_COPY_BYTES, false, readBudget));
|
|
23304
|
+
}
|
|
23305
|
+
return;
|
|
23306
|
+
}
|
|
23307
|
+
throw new Error("quarantine manifest shape is invalid");
|
|
23308
|
+
};
|
|
23309
|
+
for (const entry of selected) {
|
|
23310
|
+
if (entry.name.endsWith(".manifest.json")) processManifest(entry.name);
|
|
23311
|
+
}
|
|
23312
|
+
for (const entry of selected) {
|
|
23313
|
+
if (!entry.name.endsWith(".manifest.json") && !evidence.has(entry.name)) {
|
|
23314
|
+
const primaryName = entry.name.endsWith("-wal") || entry.name.endsWith("-shm") ? entry.name.slice(0, -4) : entry.name;
|
|
23315
|
+
try {
|
|
23316
|
+
processManifest(`${primaryName}.manifest.json`);
|
|
23317
|
+
} catch {
|
|
23318
|
+
throw new Error("quarantine evidence has no valid manifest binding");
|
|
23319
|
+
}
|
|
23320
|
+
if (!evidence.has(entry.name)) throw new Error("quarantine evidence has no valid manifest binding");
|
|
23321
|
+
}
|
|
23322
|
+
}
|
|
23323
|
+
const entries = [...evidence.entries()].slice(0, MAX_QUARANTINE_ENTRIES).map(([name, file]) => ({ nameHash: stableIdentifierHash(name), sizeBytes: file.sizeBytes, modifiedAt: file.modifiedAt })).sort((a, b) => a.nameHash.localeCompare(b.nameHash));
|
|
23324
|
+
return {
|
|
23325
|
+
status: "available",
|
|
23326
|
+
count: evidence.size,
|
|
23327
|
+
scannedCount: selected.length,
|
|
23328
|
+
truncated: scanTruncated || evidence.size > entries.length,
|
|
23329
|
+
entries
|
|
23330
|
+
};
|
|
23331
|
+
} finally {
|
|
23332
|
+
process.chdir(previousCwd);
|
|
23333
|
+
}
|
|
23334
|
+
}
|
|
23335
|
+
async function inspectQuarantine(storeDir) {
|
|
23336
|
+
const dir = path3__default.join(storeDir, JOURNAL_QUARANTINE_DIRNAME);
|
|
23337
|
+
let directory;
|
|
23338
|
+
try {
|
|
23339
|
+
directory = await promises.lstat(dir, { bigint: true });
|
|
23340
|
+
} catch (err) {
|
|
23341
|
+
if (err.code === "ENOENT") {
|
|
23342
|
+
return { status: "missing", count: 0, scannedCount: 0, truncated: false, entries: [] };
|
|
23343
|
+
}
|
|
23344
|
+
return {
|
|
23345
|
+
status: "unavailable",
|
|
23346
|
+
count: 0,
|
|
23347
|
+
scannedCount: 0,
|
|
23348
|
+
truncated: false,
|
|
23349
|
+
entries: [],
|
|
23350
|
+
reason: "quarantine directory could not be inspected"
|
|
23351
|
+
};
|
|
23352
|
+
}
|
|
23353
|
+
if (!directory.isDirectory() || directory.isSymbolicLink()) {
|
|
23354
|
+
return { status: "unavailable", count: 0, scannedCount: 0, truncated: false, entries: [], reason: "quarantine path is not a real directory" };
|
|
23355
|
+
}
|
|
23356
|
+
try {
|
|
23357
|
+
return inspectQuarantinePinned(dir, directory);
|
|
23358
|
+
} catch {
|
|
23359
|
+
return { status: "unavailable", count: 0, scannedCount: 0, truncated: false, entries: [], reason: "quarantine manifest or evidence binding is invalid" };
|
|
23360
|
+
}
|
|
23361
|
+
}
|
|
23362
|
+
function checksFor(snapshot) {
|
|
23363
|
+
return [
|
|
23364
|
+
{
|
|
23365
|
+
id: "config",
|
|
23366
|
+
status: snapshot.config.serverProtocol === "invalid" ? "fail" : "pass",
|
|
23367
|
+
summary: `server protocol=${snapshot.config.serverProtocol}; hostedJournal=${snapshot.config.hostedJournal}`
|
|
23368
|
+
},
|
|
23369
|
+
{
|
|
23370
|
+
id: "device",
|
|
23371
|
+
status: snapshot.device.status === "unavailable" ? "fail" : snapshot.device.status === "unpaired" ? "warn" : "pass",
|
|
23372
|
+
summary: snapshot.device.status
|
|
23373
|
+
},
|
|
23374
|
+
{
|
|
23375
|
+
id: "runtimes",
|
|
23376
|
+
status: snapshot.runtimes.some((runtime) => runtime.present) ? "pass" : "warn",
|
|
23377
|
+
summary: `${snapshot.runtimes.filter((runtime) => runtime.present).length}/${snapshot.runtimes.length} present; ` + RUNTIME_DETECTION_FAILURE_KINDS.map((kind) => `${kind}=${snapshot.runtimes.filter((runtime) => runtime.outcome === kind).length}`).join(" ")
|
|
23378
|
+
},
|
|
23379
|
+
{
|
|
23380
|
+
id: "control",
|
|
23381
|
+
status: snapshot.control.status === "online" ? "pass" : "warn",
|
|
23382
|
+
summary: snapshot.control.status === "online" ? `online transport=${snapshot.control.transport}` : "daemon offline"
|
|
23383
|
+
},
|
|
23384
|
+
{
|
|
23385
|
+
id: "health",
|
|
23386
|
+
status: snapshot.health.status === "corrupt" || snapshot.health.status === "unavailable" ? "fail" : snapshot.health.status === "missing" ? "warn" : "pass",
|
|
23387
|
+
summary: snapshot.health.status
|
|
23388
|
+
},
|
|
23389
|
+
{
|
|
23390
|
+
id: "journal",
|
|
23391
|
+
status: snapshot.journal.status === "unavailable" || snapshot.journal.status === "corrupt" ? "fail" : snapshot.journal.status === "missing" ? "warn" : "pass",
|
|
23392
|
+
summary: snapshot.journal.status
|
|
23393
|
+
},
|
|
23394
|
+
{
|
|
23395
|
+
id: "workspace",
|
|
23396
|
+
status: snapshot.workspace.status === "unavailable" ? "fail" : snapshot.workspace.status === "missing" || snapshot.workspace.writable === false ? "warn" : "pass",
|
|
23397
|
+
summary: `${snapshot.workspace.status}${snapshot.workspace.writable === false ? "; read-only" : ""}`
|
|
23398
|
+
},
|
|
23399
|
+
{
|
|
23400
|
+
id: "quarantine",
|
|
23401
|
+
status: snapshot.quarantine.status === "unavailable" ? "fail" : snapshot.quarantine.count > 0 ? "warn" : "pass",
|
|
23402
|
+
summary: `${snapshot.quarantine.count} evidence file(s)`
|
|
23403
|
+
}
|
|
23404
|
+
];
|
|
23405
|
+
}
|
|
23406
|
+
async function collectDiagnostics(config, storeDir, options = {}) {
|
|
23407
|
+
const resolvedStoreDir = path3__default.resolve(storeDir);
|
|
23408
|
+
const adapters = options.adapters ?? defaultRuntimeAdapters(config.runtimeAllowlist);
|
|
23409
|
+
const connectControl = options.connectControl ?? connectControlClient;
|
|
23410
|
+
const [device, probedRuntimes, health, journal, workspace, quarantine, controlConnection] = await Promise.all([
|
|
23411
|
+
inspectDevice(resolvedStoreDir),
|
|
23412
|
+
probeRuntimes(adapters, { timeoutMs: options.runtimeProbeTimeoutMs }),
|
|
23413
|
+
inspectOperationalHealthFile(resolvedStoreDir),
|
|
23414
|
+
inspectJournal(resolvedStoreDir),
|
|
23415
|
+
inspectWorkspace(config.workspaceRoot),
|
|
23416
|
+
inspectQuarantine(resolvedStoreDir),
|
|
23417
|
+
connectControl({ storeDir: resolvedStoreDir, productId: config.productId })
|
|
23418
|
+
]);
|
|
23419
|
+
const runtimes = probedRuntimes.map((runtime) => ({
|
|
23420
|
+
idHash: stableIdentifierHash(runtime.id),
|
|
23421
|
+
present: runtime.present,
|
|
23422
|
+
outcome: runtime.outcome,
|
|
23423
|
+
versionPresent: runtime.version !== void 0,
|
|
23424
|
+
...runtime.authPresent === void 0 ? {} : { authPresent: runtime.authPresent },
|
|
23425
|
+
steer: runtime.steer,
|
|
23426
|
+
resume: runtime.resume,
|
|
23427
|
+
permissionModeCount: runtime.permissionModes.length
|
|
23428
|
+
}));
|
|
23429
|
+
let control;
|
|
23430
|
+
if (!controlConnection.ok) {
|
|
23431
|
+
control = { status: "offline", reason: "daemon control socket unavailable" };
|
|
23432
|
+
} else {
|
|
23433
|
+
try {
|
|
23434
|
+
const live = await controlConnection.client.request("status");
|
|
23435
|
+
control = {
|
|
23436
|
+
status: "online",
|
|
23437
|
+
pid: live.pid,
|
|
23438
|
+
uptimeMs: live.uptimeMs,
|
|
23439
|
+
transport: live.transport,
|
|
23440
|
+
activeTaskCount: live.activeTasks.length,
|
|
23441
|
+
pendingApprovalCount: live.approvalsPending,
|
|
23442
|
+
operationalHealth: live.operationalHealth,
|
|
23443
|
+
...live.storage ? { storage: live.storage } : {}
|
|
23444
|
+
};
|
|
23445
|
+
} catch {
|
|
23446
|
+
control = { status: "offline", reason: "daemon status request failed" };
|
|
23447
|
+
} finally {
|
|
23448
|
+
controlConnection.client.close();
|
|
23449
|
+
}
|
|
23450
|
+
}
|
|
23451
|
+
const withoutChecks = {
|
|
23452
|
+
version: 1,
|
|
23453
|
+
generatedAt: (options.clock ?? (() => /* @__PURE__ */ new Date()))().toISOString(),
|
|
23454
|
+
product: { nameHash: stableIdentifierHash(config.productName), idHash: stableIdentifierHash(config.productId) },
|
|
23455
|
+
system: { node: process.versions.node, platform: process.platform, arch: process.arch, sqliteAvailable: isSqliteAvailable() },
|
|
23456
|
+
config: {
|
|
23457
|
+
serverProtocol: safeProtocol(config.serverUrl),
|
|
23458
|
+
customStoreDir: config.storeDir !== void 0,
|
|
23459
|
+
hostedJournal: config.hostedJournal !== void 0,
|
|
23460
|
+
...config.runtimeAllowlist ? { runtimeAllowlistCount: config.runtimeAllowlist.length } : {}
|
|
23461
|
+
},
|
|
23462
|
+
device,
|
|
23463
|
+
runtimes,
|
|
23464
|
+
control,
|
|
23465
|
+
health,
|
|
23466
|
+
journal,
|
|
23467
|
+
workspace,
|
|
23468
|
+
quarantine
|
|
23469
|
+
};
|
|
23470
|
+
return { ...withoutChecks, checks: checksFor(withoutChecks) };
|
|
23471
|
+
}
|
|
23472
|
+
function stableIdentifierHash(value) {
|
|
23473
|
+
return createHash("sha256").update(value, "utf8").digest("hex");
|
|
23474
|
+
}
|
|
23475
|
+
function sameInode2(left, right) {
|
|
23476
|
+
return left.dev === right.dev && left.ino === right.ino;
|
|
23477
|
+
}
|
|
23478
|
+
function sameFileState5(left, right) {
|
|
23479
|
+
return sameInode2(left, right) && left.size === right.size && left.mtimeNs === right.mtimeNs && left.ctimeNs === right.ctimeNs;
|
|
23480
|
+
}
|
|
23481
|
+
function publishQuarantineEvidencePinned(quarantineDir, expectedDirectory, sourcePath, sourceFd, sourceStat, evidenceName, manifestName, quarantinedAt, reason) {
|
|
23482
|
+
const previousCwd = process.cwd();
|
|
23483
|
+
let entered = false;
|
|
23484
|
+
let evidenceCreated = false;
|
|
23485
|
+
let manifestCreated = false;
|
|
23486
|
+
let sourceRemoved = false;
|
|
23487
|
+
let evidenceTemp;
|
|
23488
|
+
let manifestTemp;
|
|
23489
|
+
try {
|
|
23490
|
+
process.chdir(quarantineDir);
|
|
23491
|
+
entered = true;
|
|
23492
|
+
const pinned = lstatSync(".", { bigint: true });
|
|
23493
|
+
if (!pinned.isDirectory() || !sameInode2(pinned, expectedDirectory)) {
|
|
23494
|
+
throw new Error("quarantine path changed before publication; refusing quarantine");
|
|
23495
|
+
}
|
|
23496
|
+
const openSource = fstatSync(sourceFd, { bigint: true });
|
|
23497
|
+
const namedSource = lstatSync(sourcePath, { bigint: true });
|
|
23498
|
+
if (!sameFileState5(openSource, sourceStat) || !sameFileState5(namedSource, sourceStat)) {
|
|
23499
|
+
throw new Error("operational health source identity changed before quarantine");
|
|
23500
|
+
}
|
|
23501
|
+
const sizeBytes = Number(sourceStat.size);
|
|
23502
|
+
if (sizeBytes > MAX_OPERATIONAL_HEALTH_FILE_BYTES) {
|
|
23503
|
+
throw new Error("operational health state exceeds the bounded quarantine read limit");
|
|
23504
|
+
}
|
|
23505
|
+
const bytes = Buffer.alloc(sizeBytes);
|
|
23506
|
+
let position = 0;
|
|
23507
|
+
while (position < sizeBytes) {
|
|
23508
|
+
const bytesRead = readSync(sourceFd, bytes, position, sizeBytes - position, position);
|
|
23509
|
+
if (bytesRead === 0) throw new Error("operational health state ended during bounded copy");
|
|
23510
|
+
position += bytesRead;
|
|
23511
|
+
}
|
|
23512
|
+
const afterRead = fstatSync(sourceFd, { bigint: true });
|
|
23513
|
+
if (!sameFileState5(afterRead, sourceStat)) {
|
|
23514
|
+
throw new Error("operational health state changed during bounded copy");
|
|
23515
|
+
}
|
|
23516
|
+
const sha2562 = createHash("sha256").update(bytes).digest("hex");
|
|
23517
|
+
evidenceTemp = `.${evidenceName}.${randomUUID()}.tmp`;
|
|
23518
|
+
const evidenceFd = openSync(evidenceTemp, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL, 384);
|
|
23519
|
+
try {
|
|
23520
|
+
writeFileSync(evidenceFd, bytes);
|
|
23521
|
+
fchmodSync(evidenceFd, 384);
|
|
23522
|
+
fsyncSync(evidenceFd);
|
|
23523
|
+
} finally {
|
|
23524
|
+
closeSync(evidenceFd);
|
|
23525
|
+
}
|
|
23526
|
+
linkSync(evidenceTemp, evidenceName);
|
|
23527
|
+
evidenceCreated = true;
|
|
23528
|
+
const evidence = lstatSync(evidenceName, { bigint: true });
|
|
23529
|
+
const tempEvidence = lstatSync(evidenceTemp, { bigint: true });
|
|
23530
|
+
if (!evidence.isFile() || !sameInode2(evidence, tempEvidence) || evidence.size !== sourceStat.size) {
|
|
23531
|
+
throw new Error("quarantined operational health evidence does not match the bounded copy");
|
|
23532
|
+
}
|
|
23533
|
+
unlinkSync(evidenceTemp);
|
|
23534
|
+
evidenceTemp = void 0;
|
|
23535
|
+
const finalOpenSource = fstatSync(sourceFd, { bigint: true });
|
|
23536
|
+
const finalNamedSource = lstatSync(sourcePath, { bigint: true });
|
|
23537
|
+
if (!sameFileState5(finalOpenSource, sourceStat) || !sameFileState5(finalNamedSource, sourceStat)) {
|
|
23538
|
+
throw new Error("operational health source changed before removal");
|
|
23539
|
+
}
|
|
23540
|
+
const manifestBody = `${JSON.stringify(
|
|
23541
|
+
{
|
|
23542
|
+
version: 1,
|
|
23543
|
+
quarantinedAt,
|
|
23544
|
+
reason,
|
|
23545
|
+
sourcePath,
|
|
23546
|
+
evidenceFile: evidenceName,
|
|
23547
|
+
sha256: sha2562,
|
|
23548
|
+
sizeBytes
|
|
23549
|
+
},
|
|
23550
|
+
null,
|
|
23551
|
+
2
|
|
23552
|
+
)}
|
|
23553
|
+
`;
|
|
23554
|
+
manifestTemp = `.${manifestName}.${randomUUID()}.tmp`;
|
|
23555
|
+
const manifestFd = openSync(manifestTemp, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL, 384);
|
|
23556
|
+
try {
|
|
23557
|
+
writeFileSync(manifestFd, manifestBody, "utf8");
|
|
23558
|
+
fchmodSync(manifestFd, 384);
|
|
23559
|
+
fsyncSync(manifestFd);
|
|
23560
|
+
} finally {
|
|
23561
|
+
closeSync(manifestFd);
|
|
23562
|
+
}
|
|
23563
|
+
linkSync(manifestTemp, manifestName);
|
|
23564
|
+
manifestCreated = true;
|
|
23565
|
+
unlinkSync(manifestTemp);
|
|
23566
|
+
manifestTemp = void 0;
|
|
23567
|
+
if (process.platform !== "win32") {
|
|
23568
|
+
const quarantineDirectoryFd = openSync(".", constants.O_RDONLY);
|
|
23569
|
+
try {
|
|
23570
|
+
fsyncSync(quarantineDirectoryFd);
|
|
23571
|
+
} finally {
|
|
23572
|
+
closeSync(quarantineDirectoryFd);
|
|
23573
|
+
}
|
|
23574
|
+
}
|
|
23575
|
+
const removalOpenSource = fstatSync(sourceFd, { bigint: true });
|
|
23576
|
+
const removalNamedSource = lstatSync(sourcePath, { bigint: true });
|
|
23577
|
+
if (!sameFileState5(removalOpenSource, sourceStat) || !sameFileState5(removalNamedSource, sourceStat)) {
|
|
23578
|
+
throw new Error("operational health source changed before removal");
|
|
23579
|
+
}
|
|
23580
|
+
unlinkSync(sourcePath);
|
|
23581
|
+
sourceRemoved = true;
|
|
23582
|
+
if (process.platform !== "win32") {
|
|
23583
|
+
const directoryFd = openSync(path3__default.dirname(sourcePath), constants.O_RDONLY);
|
|
23584
|
+
try {
|
|
23585
|
+
fsyncSync(directoryFd);
|
|
23586
|
+
} finally {
|
|
23587
|
+
closeSync(directoryFd);
|
|
23588
|
+
}
|
|
23589
|
+
}
|
|
23590
|
+
return { sha256: sha2562, sizeBytes };
|
|
23591
|
+
} catch (err) {
|
|
23592
|
+
if (evidenceTemp) {
|
|
23593
|
+
try {
|
|
23594
|
+
unlinkSync(evidenceTemp);
|
|
23595
|
+
} catch {
|
|
23596
|
+
}
|
|
23597
|
+
}
|
|
23598
|
+
if (manifestTemp) {
|
|
23599
|
+
try {
|
|
23600
|
+
unlinkSync(manifestTemp);
|
|
23601
|
+
} catch {
|
|
23602
|
+
}
|
|
23603
|
+
}
|
|
23604
|
+
if (manifestCreated && !sourceRemoved) {
|
|
23605
|
+
try {
|
|
23606
|
+
unlinkSync(manifestName);
|
|
23607
|
+
} catch {
|
|
23608
|
+
}
|
|
23609
|
+
}
|
|
23610
|
+
if (evidenceCreated && !sourceRemoved) {
|
|
23611
|
+
try {
|
|
23612
|
+
unlinkSync(evidenceName);
|
|
23613
|
+
} catch {
|
|
23614
|
+
}
|
|
23615
|
+
}
|
|
23616
|
+
throw err;
|
|
23617
|
+
} finally {
|
|
23618
|
+
if (entered) process.chdir(previousCwd);
|
|
23619
|
+
}
|
|
23620
|
+
}
|
|
23621
|
+
async function quarantineCorruptOperationalHealth(storeDir, options = {}) {
|
|
23622
|
+
const resolvedStoreDir = path3__default.resolve(storeDir);
|
|
23623
|
+
const owner = await acquireDaemonOwner(resolvedStoreDir, "doctor", options.clock);
|
|
23624
|
+
try {
|
|
23625
|
+
const sourcePath = path3__default.join(resolvedStoreDir, OPERATIONAL_HEALTH_FILENAME);
|
|
23626
|
+
let opened;
|
|
23627
|
+
try {
|
|
23628
|
+
opened = await openOperationalHealthFile(resolvedStoreDir);
|
|
23629
|
+
} catch (err) {
|
|
23630
|
+
throw new Error("operational health state could not be opened safely; refusing quarantine", { cause: err });
|
|
23631
|
+
}
|
|
23632
|
+
if (!opened) return { status: "not-needed", reason: "missing" };
|
|
23633
|
+
const source = opened.handle;
|
|
23634
|
+
try {
|
|
23635
|
+
const inspection = await inspectOperationalHealthHandle(source, opened.stat);
|
|
23636
|
+
if (inspection.status === "valid") return { status: "not-needed", reason: "valid" };
|
|
23637
|
+
if (inspection.status === "unavailable") {
|
|
23638
|
+
throw new Error(`${inspection.reason}; refusing to quarantine unconfirmed corruption`);
|
|
23639
|
+
}
|
|
23640
|
+
const sourceStat = await source.stat({ bigint: true });
|
|
23641
|
+
if (!sourceStat.isFile()) throw new Error("operational health state is not a regular file; refusing quarantine");
|
|
23642
|
+
const quarantineDir = path3__default.join(resolvedStoreDir, JOURNAL_QUARANTINE_DIRNAME);
|
|
23643
|
+
try {
|
|
23644
|
+
const existing = await promises.lstat(quarantineDir);
|
|
23645
|
+
if (!existing.isDirectory() || existing.isSymbolicLink()) {
|
|
23646
|
+
throw new Error("quarantine path is not a real directory; refusing quarantine");
|
|
23647
|
+
}
|
|
23648
|
+
} catch (err) {
|
|
23649
|
+
if (err.code !== "ENOENT") throw err;
|
|
23650
|
+
await promises.mkdir(quarantineDir, { mode: 448 });
|
|
23651
|
+
}
|
|
23652
|
+
await ensureSecureDir(quarantineDir);
|
|
23653
|
+
const securedQuarantine = await promises.lstat(quarantineDir, { bigint: true });
|
|
23654
|
+
if (!securedQuarantine.isDirectory() || securedQuarantine.isSymbolicLink()) {
|
|
23655
|
+
throw new Error("quarantine path changed during validation; refusing quarantine");
|
|
23656
|
+
}
|
|
23657
|
+
const stamp = (options.clock ?? (() => /* @__PURE__ */ new Date()))().toISOString().replace(/[:.]/g, "-");
|
|
23658
|
+
const evidenceName = `${stamp}-${randomUUID()}-${OPERATIONAL_HEALTH_FILENAME}`;
|
|
23659
|
+
const manifestName = `${evidenceName}.manifest.json`;
|
|
23660
|
+
const hashed = publishQuarantineEvidencePinned(
|
|
23661
|
+
quarantineDir,
|
|
23662
|
+
securedQuarantine,
|
|
23663
|
+
sourcePath,
|
|
23664
|
+
source.fd,
|
|
23665
|
+
sourceStat,
|
|
23666
|
+
evidenceName,
|
|
23667
|
+
manifestName,
|
|
23668
|
+
(options.clock ?? (() => /* @__PURE__ */ new Date()))().toISOString(),
|
|
23669
|
+
inspection.reason
|
|
23670
|
+
);
|
|
23671
|
+
return { status: "quarantined", evidenceName, manifestName, sha256: hashed.sha256, sizeBytes: hashed.sizeBytes };
|
|
23672
|
+
} finally {
|
|
23673
|
+
await source.close();
|
|
23674
|
+
}
|
|
23675
|
+
} finally {
|
|
23676
|
+
await owner.release();
|
|
23677
|
+
}
|
|
23678
|
+
}
|
|
23679
|
+
|
|
23680
|
+
// src/diagnostics/device-doctor.ts
|
|
23681
|
+
function diagnoseDevice(config, options = {}) {
|
|
23682
|
+
return collectDiagnostics(config, DeviceStore.resolveDir(config.productId, config.storeDir), {
|
|
23683
|
+
adapters: options.adapters,
|
|
23684
|
+
runtimeProbeTimeoutMs: options.runtimeProbeTimeoutMs
|
|
23685
|
+
});
|
|
23686
|
+
}
|
|
23687
|
+
var MESSAGES = {
|
|
23688
|
+
"confirmation-required": "Explicit confirmation is required for enrollment metadata repair.",
|
|
23689
|
+
"invalid-target": "An explicit expected tenant and device are required.",
|
|
23690
|
+
"daemon-running": "Stop the daemon before enrollment metadata repair.",
|
|
23691
|
+
"store-busy": "The store is owned by another operation; enrollment metadata repair refused.",
|
|
23692
|
+
"authority-unavailable": "The OS enrollment authority could not be read.",
|
|
23693
|
+
"authority-missing": "No complete OS enrollment exists; use explicit authenticated pairing.",
|
|
23694
|
+
"target-mismatch": "The OS enrollment does not match the expected tenant and device.",
|
|
23695
|
+
"projection-unavailable": "The enrollment metadata could not be read safely; repair refused.",
|
|
23696
|
+
"repair-failed": "Enrollment metadata repair did not complete; inspect the state before retrying."
|
|
23697
|
+
};
|
|
23698
|
+
var DeviceMetadataRepairError = class extends Error {
|
|
23699
|
+
constructor(code) {
|
|
23700
|
+
super(MESSAGES[code]);
|
|
23701
|
+
this.code = code;
|
|
23702
|
+
this.name = "DeviceMetadataRepairError";
|
|
23703
|
+
}
|
|
23704
|
+
code;
|
|
23705
|
+
};
|
|
23706
|
+
async function repairDeviceEnrollmentMetadata(config, input) {
|
|
23707
|
+
if (input?.confirmed !== true) throw new DeviceMetadataRepairError("confirmation-required");
|
|
23708
|
+
if (typeof input.expectedDeviceId !== "string" || input.expectedDeviceId.trim().length === 0 || !isTenantId(input.expectedTenantId)) {
|
|
23709
|
+
throw new DeviceMetadataRepairError("invalid-target");
|
|
23710
|
+
}
|
|
23711
|
+
const { expectedDeviceId, expectedTenantId } = input;
|
|
23712
|
+
const productId = config.productId;
|
|
23713
|
+
let failureCode = "repair-failed";
|
|
23714
|
+
try {
|
|
23715
|
+
const storeDir = DeviceStore.resolveDir(productId, config.storeDir);
|
|
23716
|
+
const control = await connectControlClient({ storeDir, productId });
|
|
23717
|
+
if (control.ok) {
|
|
23718
|
+
control.client.close();
|
|
23719
|
+
throw new DeviceMetadataRepairError("daemon-running");
|
|
23720
|
+
}
|
|
23721
|
+
const owner = await acquireDaemonOwner(storeDir, "doctor");
|
|
23722
|
+
try {
|
|
23723
|
+
const store = new DeviceStore(storeDir, void 0, productId);
|
|
23724
|
+
failureCode = "projection-unavailable";
|
|
23725
|
+
await store.load();
|
|
23726
|
+
failureCode = "authority-unavailable";
|
|
23727
|
+
const authority = await store.credentials.read();
|
|
23728
|
+
if (authority === void 0) throw new DeviceMetadataRepairError("authority-missing");
|
|
23729
|
+
if (authority.deviceId !== expectedDeviceId || authority.tenantId !== expectedTenantId) {
|
|
23730
|
+
throw new DeviceMetadataRepairError("target-mismatch");
|
|
23731
|
+
}
|
|
23732
|
+
failureCode = "repair-failed";
|
|
23733
|
+
const changed = await store.reconcileMetadata(authority);
|
|
23734
|
+
const readback = await store.load();
|
|
23735
|
+
if (readback === void 0 || readback.deviceId !== authority.deviceId || readback.tenantId !== authority.tenantId || readback.devicePublicKey !== authority.devicePublicKey) {
|
|
23736
|
+
throw new DeviceMetadataRepairError("repair-failed");
|
|
23737
|
+
}
|
|
23738
|
+
return { action: "restore-enrollment-metadata", scope: "device", status: changed ? "repaired" : "not-needed" };
|
|
23739
|
+
} catch (error) {
|
|
23740
|
+
if (error instanceof DeviceMetadataRepairError) throw error;
|
|
23741
|
+
throw new DeviceMetadataRepairError(failureCode);
|
|
23742
|
+
} finally {
|
|
23743
|
+
failureCode = "repair-failed";
|
|
23744
|
+
await owner.release();
|
|
23745
|
+
}
|
|
23746
|
+
} catch (error) {
|
|
23747
|
+
if (error instanceof DeviceMetadataRepairError) throw error;
|
|
23748
|
+
if (error instanceof DaemonOwnerActiveError) throw new DeviceMetadataRepairError("store-busy");
|
|
23749
|
+
throw new DeviceMetadataRepairError(failureCode);
|
|
23750
|
+
}
|
|
23751
|
+
}
|
|
23752
|
+
function auditLogPath(storeDir) {
|
|
23753
|
+
return path3__default.join(storeDir, "audit.jsonl");
|
|
23754
|
+
}
|
|
23755
|
+
new Set(GIT_ERROR_CATEGORIES);
|
|
23756
|
+
|
|
23757
|
+
// src/diagnostics/support-bundle.ts
|
|
23758
|
+
var MAX_SUPPORT_AUDIT_TAIL_BYTES = 256 * 1024;
|
|
23759
|
+
var MAX_SUPPORT_AUDIT_FACTS = 200;
|
|
23760
|
+
var AUDIT_KINDS = /* @__PURE__ */ new Set([
|
|
23761
|
+
"artifact",
|
|
23762
|
+
"awaiting-approval",
|
|
23763
|
+
"cancelled",
|
|
23764
|
+
"claimed",
|
|
23765
|
+
"completed",
|
|
23766
|
+
"connection",
|
|
23767
|
+
"failed",
|
|
23768
|
+
"git-workspace",
|
|
23769
|
+
"offered",
|
|
23770
|
+
"paired",
|
|
23771
|
+
"progress",
|
|
23772
|
+
"runtimes-detected",
|
|
23773
|
+
"shutdown-complete",
|
|
23774
|
+
"shutdown-requested",
|
|
23775
|
+
"stale-approval-decision",
|
|
23776
|
+
"started",
|
|
23777
|
+
"unpaired"
|
|
23778
|
+
]);
|
|
23779
|
+
function isAuditKind(value) {
|
|
23780
|
+
return typeof value === "string" && AUDIT_KINDS.has(value);
|
|
23781
|
+
}
|
|
23782
|
+
function sameFileState6(left, right) {
|
|
23783
|
+
return left.dev === right.dev && left.ino === right.ino && left.size === right.size && left.mtimeNs === right.mtimeNs && left.ctimeNs === right.ctimeNs;
|
|
23784
|
+
}
|
|
23785
|
+
async function recentAuditFacts(storeDir) {
|
|
23786
|
+
const filePath = auditLogPath(storeDir);
|
|
23787
|
+
let pathStat;
|
|
23788
|
+
try {
|
|
23789
|
+
pathStat = await promises.lstat(filePath, { bigint: true });
|
|
23790
|
+
if (!pathStat.isFile() || pathStat.isSymbolicLink()) {
|
|
23791
|
+
return { status: "unavailable", included: 0, sourceBytesRead: 0, sourceTruncated: false, facts: [] };
|
|
23792
|
+
}
|
|
23793
|
+
} catch (err) {
|
|
23794
|
+
if (err.code === "ENOENT") {
|
|
23795
|
+
return { status: "missing", included: 0, sourceBytesRead: 0, sourceTruncated: false, facts: [] };
|
|
23796
|
+
}
|
|
23797
|
+
return { status: "unavailable", included: 0, sourceBytesRead: 0, sourceTruncated: false, facts: [] };
|
|
23798
|
+
}
|
|
23799
|
+
let handle;
|
|
23800
|
+
try {
|
|
23801
|
+
handle = await promises.open(
|
|
23802
|
+
filePath,
|
|
23803
|
+
constants.O_RDONLY | constants.O_NONBLOCK | (constants.O_NOFOLLOW ?? 0)
|
|
23804
|
+
);
|
|
23805
|
+
} catch (err) {
|
|
23806
|
+
if (err.code === "ENOENT") {
|
|
23807
|
+
return { status: "missing", included: 0, sourceBytesRead: 0, sourceTruncated: false, facts: [] };
|
|
23808
|
+
}
|
|
23809
|
+
return { status: "unavailable", included: 0, sourceBytesRead: 0, sourceTruncated: false, facts: [] };
|
|
23810
|
+
}
|
|
23811
|
+
try {
|
|
23812
|
+
const stat = await handle.stat({ bigint: true });
|
|
23813
|
+
const namedAfterOpen = await promises.lstat(filePath, { bigint: true });
|
|
23814
|
+
if (!stat.isFile() || !namedAfterOpen.isFile() || namedAfterOpen.isSymbolicLink() || !sameFileState6(pathStat, stat) || !sameFileState6(stat, namedAfterOpen)) {
|
|
23815
|
+
return { status: "unavailable", included: 0, sourceBytesRead: 0, sourceTruncated: false, facts: [] };
|
|
23816
|
+
}
|
|
23817
|
+
const sourceSize = Number(stat.size);
|
|
23818
|
+
const length = Math.min(sourceSize, MAX_SUPPORT_AUDIT_TAIL_BYTES);
|
|
23819
|
+
const start = Math.max(0, sourceSize - length);
|
|
23820
|
+
const buffer = Buffer.alloc(length);
|
|
23821
|
+
const { bytesRead } = await handle.read(buffer, 0, length, start);
|
|
23822
|
+
const afterRead = await handle.stat({ bigint: true });
|
|
23823
|
+
const namedAfterRead = await promises.lstat(filePath, { bigint: true });
|
|
23824
|
+
if (bytesRead !== length || namedAfterRead.isSymbolicLink() || !sameFileState6(stat, afterRead) || !sameFileState6(afterRead, namedAfterRead)) {
|
|
23825
|
+
return { status: "unavailable", included: 0, sourceBytesRead: 0, sourceTruncated: false, facts: [] };
|
|
23826
|
+
}
|
|
23827
|
+
let text = buffer.subarray(0, bytesRead).toString("utf8");
|
|
23828
|
+
if (start > 0) text = text.slice(Math.max(0, text.indexOf("\n") + 1));
|
|
23829
|
+
const lines = text.split("\n").filter(Boolean).slice(-MAX_SUPPORT_AUDIT_FACTS);
|
|
23830
|
+
const facts = [];
|
|
23831
|
+
for (const line of lines) {
|
|
23832
|
+
try {
|
|
23833
|
+
const value = JSON.parse(line);
|
|
23834
|
+
if (isAuditKind(value.kind) && typeof value.ts === "string" && Number.isFinite(Date.parse(value.ts))) {
|
|
23835
|
+
facts.push({ kind: value.kind, ts: new Date(Date.parse(value.ts)).toISOString() });
|
|
23836
|
+
}
|
|
23837
|
+
} catch {
|
|
23838
|
+
}
|
|
23839
|
+
}
|
|
23840
|
+
return { status: "available", included: facts.length, sourceBytesRead: bytesRead, sourceTruncated: sourceSize > bytesRead, facts };
|
|
23841
|
+
} catch {
|
|
23842
|
+
return { status: "unavailable", included: 0, sourceBytesRead: 0, sourceTruncated: false, facts: [] };
|
|
23843
|
+
} finally {
|
|
23844
|
+
await handle.close();
|
|
23845
|
+
}
|
|
23846
|
+
}
|
|
23847
|
+
function projectControl(control) {
|
|
23848
|
+
if (control.status === "offline") return { status: "offline" };
|
|
23849
|
+
const transport = ["connecting", "open", "closed", "revoked"].includes(control.transport) ? control.transport : "unavailable";
|
|
23850
|
+
const operationalHealth = control.operationalHealth.availability === "available" ? {
|
|
23851
|
+
availability: "available",
|
|
23852
|
+
state: control.operationalHealth.state,
|
|
23853
|
+
failureCount: control.operationalHealth.failureCount,
|
|
23854
|
+
windowMs: control.operationalHealth.windowMs,
|
|
23855
|
+
failureThreshold: control.operationalHealth.failureThreshold,
|
|
23856
|
+
crashCount: control.operationalHealth.crashCount
|
|
23857
|
+
} : { availability: "unavailable" };
|
|
23858
|
+
return {
|
|
23859
|
+
status: "online",
|
|
23860
|
+
pid: control.pid,
|
|
23861
|
+
uptimeMs: control.uptimeMs,
|
|
23862
|
+
transport,
|
|
23863
|
+
activeTaskCount: control.activeTaskCount,
|
|
23864
|
+
pendingApprovalCount: control.pendingApprovalCount,
|
|
23865
|
+
operationalHealth,
|
|
23866
|
+
storagePresent: control.storage !== void 0
|
|
23867
|
+
};
|
|
23868
|
+
}
|
|
23869
|
+
async function createSupportBundle(config, storeDir, options = {}) {
|
|
23870
|
+
const [snapshot, recentEvents] = await Promise.all([collectDiagnostics(config, storeDir, options), recentAuditFacts(storeDir)]);
|
|
23871
|
+
return {
|
|
23872
|
+
version: 1,
|
|
23873
|
+
generatedAt: snapshot.generatedAt,
|
|
23874
|
+
product: snapshot.product,
|
|
23875
|
+
system: snapshot.system,
|
|
23876
|
+
config: {
|
|
23877
|
+
serverProtocol: snapshot.config.serverProtocol,
|
|
23878
|
+
customStoreDir: snapshot.config.customStoreDir,
|
|
23879
|
+
hostedJournal: snapshot.config.hostedJournal,
|
|
23880
|
+
...snapshot.config.runtimeAllowlistCount === void 0 ? {} : { runtimeAllowlistCount: snapshot.config.runtimeAllowlistCount }
|
|
23881
|
+
},
|
|
23882
|
+
device: {
|
|
23883
|
+
status: snapshot.device.status,
|
|
23884
|
+
...snapshot.device.deviceIdHash ? { deviceIdHash: snapshot.device.deviceIdHash } : {}
|
|
23885
|
+
},
|
|
23886
|
+
runtimes: snapshot.runtimes,
|
|
23887
|
+
control: projectControl(snapshot.control),
|
|
23888
|
+
health: snapshot.health,
|
|
23889
|
+
journal: snapshot.journal,
|
|
23890
|
+
workspace: snapshot.workspace,
|
|
23891
|
+
quarantine: {
|
|
23892
|
+
status: snapshot.quarantine.status,
|
|
23893
|
+
count: snapshot.quarantine.count,
|
|
23894
|
+
scannedCount: snapshot.quarantine.scannedCount,
|
|
23895
|
+
truncated: snapshot.quarantine.truncated,
|
|
23896
|
+
entries: snapshot.quarantine.entries
|
|
23897
|
+
},
|
|
23898
|
+
checks: snapshot.checks.map(({ id, status }) => ({ id, status })),
|
|
23899
|
+
recentEvents,
|
|
23900
|
+
redaction: {
|
|
23901
|
+
policy: "allowlist-v1",
|
|
23902
|
+
omitted: [
|
|
23903
|
+
"server URL host/path/query",
|
|
23904
|
+
"store/workspace/config paths",
|
|
23905
|
+
"control token and provider credentials",
|
|
23906
|
+
"task, prompt, tool input/output and approval text",
|
|
23907
|
+
"raw audit and quarantine contents"
|
|
23908
|
+
],
|
|
23909
|
+
transformed: ["productName/productId/deviceId/runtimeId/quarantine filenames -> SHA-256", "audit events -> closed kind/timestamp only"]
|
|
23910
|
+
}
|
|
23911
|
+
};
|
|
23912
|
+
}
|
|
23913
|
+
async function writeSupportBundle(outputPath, bundle, secureFileOptions = {}) {
|
|
23914
|
+
const dir = path3__default.dirname(outputPath);
|
|
23915
|
+
const parentStat = await promises.stat(dir);
|
|
23916
|
+
if (!parentStat.isDirectory()) throw new Error("support bundle output parent is not a directory");
|
|
23917
|
+
const privateDir = path3__default.join(dir, `.${path3__default.basename(outputPath)}.${process.pid}.${randomUUID()}.private`);
|
|
23918
|
+
const tempPath = path3__default.join(privateDir, "bundle.tmp");
|
|
23919
|
+
try {
|
|
23920
|
+
await promises.mkdir(privateDir, { mode: 448 });
|
|
23921
|
+
await ensureSecureDir(privateDir, secureFileOptions);
|
|
23922
|
+
await atomicWriteFile(tempPath, `${JSON.stringify(bundle, null, 2)}
|
|
23923
|
+
`, { mode: 384, fsync: true });
|
|
23924
|
+
await ensureSecureFile(tempPath, secureFileOptions);
|
|
23925
|
+
await promises.link(tempPath, outputPath);
|
|
23926
|
+
const published = await promises.open(outputPath, "r+");
|
|
23927
|
+
try {
|
|
23928
|
+
await published.sync();
|
|
23929
|
+
} finally {
|
|
23930
|
+
await published.close();
|
|
23931
|
+
}
|
|
23932
|
+
if (process.platform !== "win32") {
|
|
23933
|
+
const parent = await promises.open(dir, "r");
|
|
23934
|
+
try {
|
|
23935
|
+
await parent.sync();
|
|
23936
|
+
} finally {
|
|
23937
|
+
await parent.close();
|
|
23938
|
+
}
|
|
23939
|
+
}
|
|
23940
|
+
} finally {
|
|
23941
|
+
await promises.rm(privateDir, { recursive: true, force: true }).catch(() => void 0);
|
|
23942
|
+
}
|
|
23943
|
+
}
|
|
23944
|
+
|
|
23945
|
+
// src/diagnostics/operator-actions.ts
|
|
23946
|
+
var MESSAGES2 = {
|
|
23947
|
+
"confirmation-required": "Explicit confirmation is required for this local maintenance action.",
|
|
23948
|
+
"invalid-input": "The maintenance action requires valid explicit inputs.",
|
|
23949
|
+
"daemon-running": "Stop the host daemon before local maintenance.",
|
|
23950
|
+
"store-busy": "Another operation owns the device store.",
|
|
23951
|
+
"agent-busy": "Another operation owns the Agent home.",
|
|
23952
|
+
"target-mismatch": "The stored device or Agent records do not match the authorized target.",
|
|
23953
|
+
"source-unavailable": "The local source cannot be accessed safely; preserve it for inspection.",
|
|
23954
|
+
"output-exists": "The selected output already exists; it will not be overwritten.",
|
|
23955
|
+
"operation-failed": "The maintenance operation did not complete; preserve evidence before retrying."
|
|
23956
|
+
};
|
|
23957
|
+
var DeviceOperatorError = class extends Error {
|
|
23958
|
+
constructor(code) {
|
|
23959
|
+
super(MESSAGES2[code]);
|
|
23960
|
+
this.code = code;
|
|
23961
|
+
this.name = "DeviceOperatorError";
|
|
23962
|
+
}
|
|
23963
|
+
code;
|
|
23964
|
+
};
|
|
23965
|
+
function requireConfirmation(input) {
|
|
23966
|
+
if (input?.confirmed !== true) throw new DeviceOperatorError("confirmation-required");
|
|
23967
|
+
}
|
|
23968
|
+
function closedError(error) {
|
|
23969
|
+
if (error instanceof DeviceOperatorError) return error;
|
|
23970
|
+
if (error instanceof DaemonOwnerActiveError) return new DeviceOperatorError("store-busy");
|
|
23971
|
+
if (error instanceof AgentHomeBusyError) return new DeviceOperatorError("agent-busy");
|
|
23972
|
+
return new DeviceOperatorError("operation-failed");
|
|
23973
|
+
}
|
|
23974
|
+
async function requireOffline(config, storeDir) {
|
|
23975
|
+
const control = await connectControlClient({ productId: config.productId, storeDir });
|
|
23976
|
+
if (control.ok) {
|
|
23977
|
+
control.client.close();
|
|
23978
|
+
throw new DeviceOperatorError("daemon-running");
|
|
23979
|
+
}
|
|
23980
|
+
}
|
|
23981
|
+
async function quarantineDeviceOperationalHealth(config, input) {
|
|
23982
|
+
requireConfirmation(input);
|
|
23983
|
+
try {
|
|
23984
|
+
const storeDir = DeviceStore.resolveDir(config.productId, config.storeDir);
|
|
23985
|
+
await requireOffline(config, storeDir);
|
|
23986
|
+
const result = await quarantineCorruptOperationalHealth(storeDir);
|
|
23987
|
+
return { action: "quarantine-operational-health", scope: "device", ...result };
|
|
23988
|
+
} catch (error) {
|
|
23989
|
+
throw closedError(error);
|
|
23990
|
+
}
|
|
23991
|
+
}
|
|
23992
|
+
async function exportDeviceSupportBundle(config, input) {
|
|
23993
|
+
if (typeof input?.outputPath !== "string" || !path3__default.isAbsolute(input.outputPath)) {
|
|
23994
|
+
throw new DeviceOperatorError("invalid-input");
|
|
23995
|
+
}
|
|
23996
|
+
try {
|
|
23997
|
+
const storeDir = DeviceStore.resolveDir(config.productId, config.storeDir);
|
|
23998
|
+
const bundle = await createSupportBundle(config, storeDir, {
|
|
23999
|
+
adapters: input.adapters,
|
|
24000
|
+
runtimeProbeTimeoutMs: input.runtimeProbeTimeoutMs
|
|
24001
|
+
});
|
|
24002
|
+
await writeSupportBundle(input.outputPath, bundle);
|
|
24003
|
+
return { action: "export-support-bundle", scope: "device", status: "written", bundleVersion: bundle.version };
|
|
24004
|
+
} catch (error) {
|
|
24005
|
+
if (error?.code === "EEXIST") throw new DeviceOperatorError("output-exists");
|
|
24006
|
+
throw closedError(error);
|
|
24007
|
+
}
|
|
24008
|
+
}
|
|
24009
|
+
async function existingOutbox(home) {
|
|
24010
|
+
const components = [
|
|
24011
|
+
home,
|
|
24012
|
+
path3__default.join(home, AGENT_HOME_INTERNAL_DIRECTORY),
|
|
24013
|
+
path3__default.join(home, AGENT_MESSAGE_DIRECTORY),
|
|
24014
|
+
path3__default.join(home, AGENT_MESSAGE_DIRECTORY, AGENT_MESSAGE_OUTBOX_FILENAME)
|
|
24015
|
+
];
|
|
24016
|
+
for (let index = 0; index < components.length; index += 1) {
|
|
24017
|
+
let stat;
|
|
24018
|
+
try {
|
|
24019
|
+
stat = await promises.lstat(components[index]);
|
|
24020
|
+
} catch (error) {
|
|
24021
|
+
if (error.code === "ENOENT") return false;
|
|
24022
|
+
throw new DeviceOperatorError("source-unavailable");
|
|
24023
|
+
}
|
|
24024
|
+
if (stat.isSymbolicLink() || (index === components.length - 1 ? !stat.isFile() : !stat.isDirectory())) {
|
|
24025
|
+
throw new DeviceOperatorError("source-unavailable");
|
|
24026
|
+
}
|
|
24027
|
+
}
|
|
24028
|
+
return true;
|
|
24029
|
+
}
|
|
24030
|
+
async function archiveAgentTerminalMessages(config, input) {
|
|
24031
|
+
requireConfirmation(input);
|
|
24032
|
+
const parsedRef = AgentRefSchema.safeParse(input.agentRef);
|
|
24033
|
+
if (!parsedRef.success || !isTenantId(input.expectedTenantId) || typeof input.expectedDeviceId !== "string" || !input.expectedDeviceId.trim() || typeof input.archiveDirectory !== "string" || !path3__default.isAbsolute(input.archiveDirectory) || !config.agentHome) {
|
|
24034
|
+
throw new DeviceOperatorError("invalid-input");
|
|
24035
|
+
}
|
|
24036
|
+
const notNeeded = { action: "archive-terminal-messages", scope: "agent", status: "not-needed" };
|
|
24037
|
+
try {
|
|
24038
|
+
const storeDir = DeviceStore.resolveDir(config.productId, config.storeDir);
|
|
24039
|
+
await requireOffline(config, storeDir);
|
|
24040
|
+
const owner = await acquireDaemonOwner(storeDir, "doctor");
|
|
24041
|
+
try {
|
|
24042
|
+
const metadata = await new DeviceStore(storeDir, void 0, config.productId).load();
|
|
24043
|
+
if (!metadata || metadata.deviceId !== input.expectedDeviceId || metadata.tenantId !== input.expectedTenantId) {
|
|
24044
|
+
throw new DeviceOperatorError("target-mismatch");
|
|
24045
|
+
}
|
|
24046
|
+
const manager = new AgentHomeManager({
|
|
24047
|
+
hostStorageRoot: config.agentHome.hostStorageRoot,
|
|
24048
|
+
leaseManager: new AgentHomeLeaseManager({ ownerId: stableAgentHomeOwnerId(storeDir, config.productId) })
|
|
24049
|
+
});
|
|
24050
|
+
const home = await manager.layout.canonicalHomePath(parsedRef.data);
|
|
24051
|
+
if (!await existingOutbox(home)) return notNeeded;
|
|
24052
|
+
const binding = await manager.acquire(parsedRef.data);
|
|
24053
|
+
try {
|
|
24054
|
+
if (!await existingOutbox(binding.lease.cwd)) throw new DeviceOperatorError("source-unavailable");
|
|
24055
|
+
const outbox = await AgentMessageOutbox.open(binding.lease.cwd);
|
|
24056
|
+
const records = outbox.records();
|
|
24057
|
+
if (records.some((record3) => record3.tenantId !== input.expectedTenantId || record3.agentRef.agentId !== parsedRef.data.agentId)) {
|
|
24058
|
+
throw new DeviceOperatorError("target-mismatch");
|
|
24059
|
+
}
|
|
24060
|
+
const archivedRecords = outbox.terminalRecords().length;
|
|
24061
|
+
if (archivedRecords === 0) return notNeeded;
|
|
24062
|
+
try {
|
|
24063
|
+
await promises.mkdir(input.archiveDirectory, { mode: 448 });
|
|
24064
|
+
} catch (error) {
|
|
24065
|
+
if (error.code === "EEXIST") throw new DeviceOperatorError("output-exists");
|
|
24066
|
+
throw error;
|
|
24067
|
+
}
|
|
24068
|
+
const archivePath = await outbox.archiveTerminalRecords(input.archiveDirectory);
|
|
24069
|
+
if (!archivePath) throw new DeviceOperatorError("operation-failed");
|
|
24070
|
+
return {
|
|
24071
|
+
action: "archive-terminal-messages",
|
|
24072
|
+
scope: "agent",
|
|
24073
|
+
status: "archived",
|
|
24074
|
+
archivedRecords,
|
|
24075
|
+
archiveFileName: path3__default.basename(archivePath)
|
|
24076
|
+
};
|
|
24077
|
+
} finally {
|
|
24078
|
+
await binding.lease.release();
|
|
24079
|
+
}
|
|
24080
|
+
} finally {
|
|
24081
|
+
await owner.release();
|
|
24082
|
+
}
|
|
24083
|
+
} catch (error) {
|
|
24084
|
+
throw closedError(error);
|
|
24085
|
+
}
|
|
24086
|
+
}
|
|
24087
|
+
|
|
24088
|
+
export { AGENT_CONTENT_READ_CAPABILITIES, AGENT_CONTENT_READ_CAPABILITY_ARTIFACT, AGENT_CONTENT_READ_CAPABILITY_TRANSCRIPT, AGENT_CONTENT_READ_CAPABILITY_WORKSPACE, AGENT_HOME_PROJECTION_STATE_FILE, AGENT_MEMORY_AUDIT_FILENAME, AGENT_MEMORY_OUTBOX_FILENAME, AgentHomeBusyError, AgentHomeCollisionError, AgentHomeError, AgentHomeLayout, AgentHomeLeaseCorruptError, AgentHomeLeaseManager, AgentHomeManager, AgentHomeResolutionError, AgentMemoryError, AgentMemoryRevisionConflictError, AgentRefValidationError, AgentSessionHandoffCorruptError, AgentSessionHandoffMismatchError, AgentSessionHandoffStore, AgentSessionHandoffStoreError, BYOK_SDK_HELPER_SUBCOMMAND, BlobClient, BlobRequestAbortedError, ClaudeAdapter, CodexAdapter, DEFAULT_ACK_CRITICAL_RESERVE_BYTES, DEFAULT_CLEANUP_BATCH_LIMIT, DEFAULT_HARD_BUDGET_RATIO, DEFAULT_INCREMENTAL_VACUUM_PAGES, DEFAULT_LOG_ROTATION, DEFAULT_NORMAL_COMPACTION_INTERVAL_MS, DEFAULT_PRESSURE_COMPACTION_INTERVAL_MS, DEFAULT_RETENTION_MS, DEFAULT_SOFT_BUDGET_RATIO, DaemonObserver, DeviceMetadataRepairError, DeviceOperatorError, GitWorkspaceError, GitWorkspaceManager, GitWorkspaceStore, JOURNAL_DB_FILENAME, JOURNAL_QUARANTINE_DIRNAME, JOURNAL_TASK_REF_PREFIX, JournalClosedError, JournalCorruptError, JournalRecordTooLargeError, JournalUnavailableError, JournalUnknownTaskError, LocalStateRelocationBusyError, LocalStateRelocationError, LocalStateRelocationIntegrityError, LocalStorageEmergencyError, LocalStoragePolicyError, LocalStoragePressureEngine, LocalTeamWorkspace, LocalTeamWorkspace as LocalTeamWorkspaceService, McpToolsetDefinitionRevisionConflictError, McpToolsetRevisionConflictError, PI_PACKAGE_NAME, PiAdapter, PolicyUnsupportedError, RUNTIME_ADAPTER_CONTRACT_VIOLATION_REASON, ReplayCursorTooOldError, RuntimeDisposalFailure, RuntimeExecutionFailure, RuntimeStartupDisposalFailure, SKILL_PACKS_CAPABILITY, SKILL_PACKS_DIRNAME, SKILL_PACK_AUDIT_FILENAME, SKILL_PACK_INSTALL_ERROR_CODES, SKILL_PACK_LOCK_FILENAME, SKILL_PACK_LOCK_SCHEMA, SKILL_PACK_RESPONSE_MAX_BYTES, SecureDirHardeningError, SkillPackInstallError, SqliteLocalTaskJournal, SteerUnsupportedError, TeamTmuxViewError, TeamWorkspaceConflictError, TeamWorkspaceCorruptError, TeamWorkspaceError, TeamWorkspaceLeaseError, TeamWorkspaceNotFoundError, TeamWorkspaceQuotaError, TeamWorkspaceReceiptError, TeamWorkspaceValidationError, TruthMemoryClient, TruthMemoryClientError, UnsupportedServicePlatformError, archiveAgentTerminalMessages, buildIcaclsArgs, cleanupEligibleAt, cleanupOrderFor, computePressureState, createAgentHomeProjection, createAgentHomeProjectionConsumer, createDaemon, createDaemonWithAdapters, createFilesystemCleanupExecutor, createServiceLifecycle, createStatfsFreeBytesProvider, decodeTeamMemberContext, diagnoseDevice, encodeTeamMemberContext, ensureSecureDir, exportDeviceSupportBundle, freezeRuntimeAdapterDescriptor, generateLaunchdPlist, generateSystemdUnit, generateWinswXml, installSkillPacks, isAgentMemorySecureFilesystemAvailable, isGitWorkspaceConfig, isRuntimeDisposalFailure, isRuntimeExecutionFailure, isRuntimeStartupDisposalFailure, journalHash, listInstalledSkillPacks, localStateRelocation, nodeAgentProgram, openTeamTmuxView, prependGitWorkspaceGuidance, projectRuntimeBoundaryFailure, projectRuntimeExecutionFailure, projectSkillPack, quarantineDeviceOperationalHealth, readDeviceEnrollmentStatus, repairDeviceEnrollmentMetadata, requestDeviceAssertion, resolveLocalAgentReleaseIdentity, resolveLocalStoragePolicy, resolveSdkReservedHelperBin, runSdkReservedHelperCommand, sanitizeServiceName, sealRuntimeOperationManifest, skillPacksRoot, stableAgentHomeOwnerId, validateAgentRef };
|
|
21984
24089
|
//# sourceMappingURL=index.js.map
|
|
21985
24090
|
//# sourceMappingURL=index.js.map
|