@byok-sdk/client 0.8.0-beta.0 → 0.8.1
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/agent-home.d.ts +41 -2
- package/dist/bin/byok-agent.js +2210 -1295
- package/dist/bin/byok-agent.js.map +1 -1
- package/dist/bin/byok-approval-mcp.js.map +1 -1
- package/dist/bin/commands/pair.d.ts +3 -0
- package/dist/daemon/agent-home-projection-client.d.ts +20 -0
- package/dist/daemon/auth-manager.d.ts +8 -0
- package/dist/daemon/control-protocol.d.ts +11 -0
- package/dist/daemon/create-daemon.d.ts +19 -2
- package/dist/daemon/daemon-owner.d.ts +9 -0
- package/dist/daemon/device-credential-store.d.ts +58 -0
- package/dist/daemon/device-proof-signer.d.ts +5 -4
- package/dist/daemon/path-mutation-gate.d.ts +25 -0
- package/dist/daemon/store.d.ts +45 -24
- package/dist/daemon/task-runner.d.ts +9 -0
- package/dist/index.d.ts +6 -6
- package/dist/index.js +7700 -6591
- package/dist/index.js.map +1 -1
- package/dist/local-state-relocation.d.ts +24 -0
- package/package.json +4 -4
package/dist/bin/byok-agent.js
CHANGED
|
@@ -1,184 +1,543 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { randomUUID, createHash, randomBytes, timingSafeEqual, createHmac, createPrivateKey, generateKeyPairSync, sign } from 'crypto';
|
|
3
3
|
import { readFileSync, promises, linkSync, fstatSync, lstatSync, unlinkSync, constants, readSync, openSync, writeFileSync, fchmodSync, fsyncSync, closeSync, opendirSync, existsSync, realpathSync, mkdirSync, renameSync, chmodSync, statSync, readdirSync } from 'fs';
|
|
4
|
-
import
|
|
5
|
-
import
|
|
4
|
+
import * as path3 from 'path';
|
|
5
|
+
import path3__default, { isAbsolute, join } from 'path';
|
|
6
|
+
import { AGENT_CONTENT_ARTIFACT_READ_CAPABILITY, AGENT_CONTENT_TRANSCRIPT_READ_CAPABILITY, AGENT_CONTENT_WORKSPACE_READ_CAPABILITY, TASK_STATES, AgentEgressPolicySchema, AgentHomeProjectionPayloadSchema, CONFIGURED_TOOLSETS_MAX_ITEMS, ToolsetIdSchema, AgentRefSchema, AgentContentReceiptPayloadSchema, BYOK_PAIR_PATH, PairResponseSchema, BYOK_CHALLENGE_PATH, BYOK_TOKEN_PATH, partitionAgentEvents, TASK_TRANSITIONS, encodeEnvelope, PROTOCOL_VERSION, createEnvelope, AGENT_EGRESS_RELIABLE_ACK_CAPABILITY, byokBlobUrlPath, BYOK_BLOBS_PATH, byokBlobFinalizePath, STRICT_AGENT_ONLY_CAPABILITY, AGENT_HOME_PROJECTION_CAPABILITY, AGENT_EGRESS_POLICY_CAPABILITY, AGENT_EGRESS_FRESH_SESSION_CAPABILITY, AgentHomeProjectionCompletionRequestSchema, byokAgentHomeProjectionCompletionPath, AgentHomeProjectionReadbackSchema, parseMessage, checkResultDocument, MAX_MESSAGES_PER_BATCH, BYOK_CAPABILITIES_PATH, BYOK_PRESENCE_PATH, RuntimeIdSchema, TERMINAL_INFERENCE_USAGE_MAX_DURATION_MS, TERMINAL_INFERENCE_USAGE_MAX_TOKENS, RESULT_DOCUMENT_MAX_BYTES, decodeEnvelope, BYOK_EVENTS_PATH, BYOK_MESSAGES_PATH, MessagesSendResponseSchema, UnknownMessageTypeError, BYOK_WS_PATH } from '@byok-sdk/protocol';
|
|
7
|
+
import net, { createServer, createConnection } from 'net';
|
|
8
|
+
import * as os from 'os';
|
|
9
|
+
import os__default from 'os';
|
|
6
10
|
import { execFile, spawn } from 'child_process';
|
|
7
|
-
import os from 'os';
|
|
8
11
|
import { isTenantId, DEVICE_ASSERTION_AUDIENCE_MAX_BYTES, DEVICE_ASSERTION_DEFAULT_TTL_MS, DEVICE_ASSERTION_MAX_TTL_MS, nonceSigningBytes, CapabilityDeclarationSchema, hasCapability, DeviceAssertionClaimsSchema, deviceAssertionSigningInput, DEVICE_ASSERTION_SCHEMA_ID } from '@byok-sdk/core';
|
|
9
12
|
import { promisify } from 'util';
|
|
10
13
|
import { fileURLToPath } from 'url';
|
|
11
14
|
import 'readline';
|
|
12
|
-
import
|
|
15
|
+
import * as fs12 from 'fs/promises';
|
|
13
16
|
import { WebSocket } from 'ws';
|
|
14
17
|
import { createRequire } from 'module';
|
|
15
18
|
import { createInterface } from 'readline/promises';
|
|
16
19
|
|
|
17
|
-
var
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
var tmpSeq = 0;
|
|
21
|
+
async function atomicWriteFile(filePath, data, options = {}) {
|
|
22
|
+
const tmpPath = `${filePath}.${process.pid}-${tmpSeq++}.tmp`;
|
|
23
|
+
try {
|
|
24
|
+
const handle = await promises.open(tmpPath, "w", options.mode);
|
|
25
|
+
try {
|
|
26
|
+
await handle.writeFile(data);
|
|
27
|
+
if (options.mode !== void 0) {
|
|
28
|
+
await handle.chmod(options.mode);
|
|
29
|
+
}
|
|
30
|
+
if (options.fsync) {
|
|
31
|
+
await handle.sync();
|
|
32
|
+
}
|
|
33
|
+
} finally {
|
|
34
|
+
await handle.close();
|
|
35
|
+
}
|
|
36
|
+
} catch (err) {
|
|
37
|
+
await promises.rm(tmpPath, { force: true }).catch(() => {
|
|
38
|
+
});
|
|
39
|
+
throw err;
|
|
23
40
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
super(message);
|
|
28
|
-
this.name = "AgentRefValidationError";
|
|
41
|
+
await renameOnto(tmpPath, filePath);
|
|
42
|
+
if (options.mode !== void 0) {
|
|
43
|
+
await promises.chmod(filePath, options.mode);
|
|
29
44
|
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
45
|
+
if (options.fsync) {
|
|
46
|
+
const target = await promises.open(filePath, process.platform === "win32" ? "r+" : "r");
|
|
47
|
+
try {
|
|
48
|
+
await target.sync();
|
|
49
|
+
} finally {
|
|
50
|
+
await target.close();
|
|
51
|
+
}
|
|
52
|
+
if (process.platform !== "win32") {
|
|
53
|
+
const directory = await promises.open(path3__default.dirname(filePath), "r");
|
|
54
|
+
try {
|
|
55
|
+
await directory.sync();
|
|
56
|
+
} finally {
|
|
57
|
+
await directory.close();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
35
60
|
}
|
|
36
|
-
}
|
|
37
|
-
var
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
61
|
+
}
|
|
62
|
+
var RENAME_RETRY_ATTEMPTS = 5;
|
|
63
|
+
var RENAME_RETRY_DELAY_MS = 20;
|
|
64
|
+
function delay(ms) {
|
|
65
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
66
|
+
}
|
|
67
|
+
async function renameOnto(tmpPath, targetPath) {
|
|
68
|
+
for (let attempt = 1; attempt <= RENAME_RETRY_ATTEMPTS; attempt++) {
|
|
69
|
+
try {
|
|
70
|
+
await promises.rename(tmpPath, targetPath);
|
|
71
|
+
return;
|
|
72
|
+
} catch (err) {
|
|
73
|
+
const code = err.code;
|
|
74
|
+
if (code !== "EPERM" && code !== "EEXIST") {
|
|
75
|
+
await promises.rm(tmpPath, { force: true }).catch(() => {
|
|
76
|
+
});
|
|
77
|
+
throw err;
|
|
78
|
+
}
|
|
79
|
+
if (attempt === RENAME_RETRY_ATTEMPTS) {
|
|
80
|
+
await promises.rm(tmpPath, { force: true }).catch(() => {
|
|
81
|
+
});
|
|
82
|
+
throw err;
|
|
83
|
+
}
|
|
84
|
+
await delay(RENAME_RETRY_DELAY_MS * attempt);
|
|
85
|
+
}
|
|
41
86
|
}
|
|
42
|
-
}
|
|
43
|
-
var
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
87
|
+
}
|
|
88
|
+
var defaultRunner = (command, args) => new Promise((resolve, reject) => {
|
|
89
|
+
execFile(command, args, (error, stdout, stderr) => {
|
|
90
|
+
if (error && typeof error.code !== "number") {
|
|
91
|
+
reject(error);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
resolve({ code: error ? error.code : 0, stdout, stderr });
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
async function runOrThrow(run, command, args, label) {
|
|
98
|
+
const result = await run(command, args);
|
|
99
|
+
if (result.code !== 0) {
|
|
100
|
+
const detail = (result.stderr || result.stdout).trim();
|
|
101
|
+
throw new Error(`${label} failed (exit ${result.code})${detail ? `: ${detail}` : ""}`);
|
|
102
|
+
}
|
|
103
|
+
return result;
|
|
104
|
+
}
|
|
105
|
+
function isIdempotentAbsence(result, absence) {
|
|
106
|
+
if (result.code === 0) return true;
|
|
107
|
+
const detail = `${result.stdout}
|
|
108
|
+
${result.stderr}`;
|
|
109
|
+
if (absence.neverAbsence?.some((pattern) => pattern.test(detail))) return false;
|
|
110
|
+
if (absence.codes?.includes(result.code)) return true;
|
|
111
|
+
return absence.patterns.some((pattern) => pattern.test(detail));
|
|
112
|
+
}
|
|
113
|
+
async function runIdempotent(run, command, args, label, absence) {
|
|
114
|
+
const result = await run(command, args);
|
|
115
|
+
if (!isIdempotentAbsence(result, absence)) {
|
|
116
|
+
const detail = (result.stderr || result.stdout).trim();
|
|
117
|
+
throw new Error(`${label} failed (exit ${result.code})${detail ? `: ${detail}` : ""}`);
|
|
118
|
+
}
|
|
119
|
+
return result;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// src/util/secure-dir.ts
|
|
123
|
+
var SYSTEM_SID = "*S-1-5-18";
|
|
124
|
+
var ADMINISTRATORS_SID = "*S-1-5-32-544";
|
|
125
|
+
function buildIcaclsArgs(dir, username) {
|
|
126
|
+
return [dir, "/inheritance:r", "/grant:r", `${username}:(OI)(CI)F`, "/grant", `${SYSTEM_SID}:(OI)(CI)F`, "/grant", `${ADMINISTRATORS_SID}:(OI)(CI)F`];
|
|
127
|
+
}
|
|
128
|
+
function buildIcaclsFileArgs(filePath, username) {
|
|
129
|
+
return [
|
|
130
|
+
filePath,
|
|
131
|
+
"/inheritance:r",
|
|
132
|
+
"/grant:r",
|
|
133
|
+
`${username}:F`,
|
|
134
|
+
"/grant",
|
|
135
|
+
`${SYSTEM_SID}:F`,
|
|
136
|
+
"/grant",
|
|
137
|
+
`${ADMINISTRATORS_SID}:F`
|
|
138
|
+
];
|
|
139
|
+
}
|
|
140
|
+
var SecureDirHardeningError = class extends Error {
|
|
141
|
+
constructor(dir, reason) {
|
|
142
|
+
super(
|
|
143
|
+
`failed to apply a restrictive Windows ACL to "${dir}": ${reason} \u2014 refusing to leave this directory unprotected (it holds device credentials and/or the control-socket token, otherwise readable by any other local user); see docs/security.md`
|
|
144
|
+
);
|
|
145
|
+
this.dir = dir;
|
|
146
|
+
this.name = "SecureDirHardeningError";
|
|
47
147
|
}
|
|
148
|
+
dir;
|
|
48
149
|
};
|
|
49
|
-
var
|
|
50
|
-
constructor(
|
|
51
|
-
super(
|
|
52
|
-
this.
|
|
150
|
+
var SecureFileHardeningError = class extends Error {
|
|
151
|
+
constructor(filePath, reason) {
|
|
152
|
+
super(`failed to apply a restrictive Windows ACL to "${filePath}": ${reason}`);
|
|
153
|
+
this.filePath = filePath;
|
|
154
|
+
this.name = "SecureFileHardeningError";
|
|
53
155
|
}
|
|
156
|
+
filePath;
|
|
54
157
|
};
|
|
55
|
-
function
|
|
56
|
-
|
|
158
|
+
async function ensureSecureDir(dir, opts = {}) {
|
|
159
|
+
const platform = opts.platform ?? process.platform;
|
|
160
|
+
const run = opts.run ?? defaultRunner;
|
|
161
|
+
await promises.mkdir(dir, { recursive: true, mode: 448 });
|
|
162
|
+
await promises.chmod(dir, 448).catch(() => {
|
|
163
|
+
});
|
|
164
|
+
if (platform !== "win32") return;
|
|
165
|
+
const { username } = os__default.userInfo();
|
|
166
|
+
let result;
|
|
57
167
|
try {
|
|
58
|
-
|
|
59
|
-
} catch (
|
|
60
|
-
throw new
|
|
61
|
-
|
|
62
|
-
|
|
168
|
+
result = await run("icacls", buildIcaclsArgs(dir, username));
|
|
169
|
+
} catch (err) {
|
|
170
|
+
throw new SecureDirHardeningError(dir, `could not run icacls: ${err instanceof Error ? err.message : String(err)}`);
|
|
171
|
+
}
|
|
172
|
+
if (result.code !== 0) {
|
|
173
|
+
throw new SecureDirHardeningError(dir, `icacls exited ${result.code}: ${(result.stderr || result.stdout).trim()}`);
|
|
63
174
|
}
|
|
64
|
-
return Object.freeze({ agentId: candidate.agentId, profileRevision: candidate.profileRevision });
|
|
65
|
-
}
|
|
66
|
-
function isWithin(root, candidate) {
|
|
67
|
-
const relative = path.relative(root, candidate);
|
|
68
|
-
return relative === "" || relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
|
|
69
175
|
}
|
|
70
|
-
function
|
|
71
|
-
|
|
72
|
-
|
|
176
|
+
async function ensureSecureFile(filePath, opts = {}) {
|
|
177
|
+
const platform = opts.platform ?? process.platform;
|
|
178
|
+
const run = opts.run ?? defaultRunner;
|
|
179
|
+
await promises.chmod(filePath, 384);
|
|
180
|
+
if (platform !== "win32") return;
|
|
181
|
+
const { username } = os__default.userInfo();
|
|
182
|
+
let result;
|
|
183
|
+
try {
|
|
184
|
+
result = await run("icacls", buildIcaclsFileArgs(filePath, username));
|
|
185
|
+
} catch (err) {
|
|
186
|
+
throw new SecureFileHardeningError(filePath, `could not run icacls: ${err instanceof Error ? err.message : String(err)}`);
|
|
73
187
|
}
|
|
74
|
-
if (
|
|
75
|
-
throw new
|
|
188
|
+
if (result.code !== 0) {
|
|
189
|
+
throw new SecureFileHardeningError(filePath, `icacls exited ${result.code}: ${(result.stderr || result.stdout).trim()}`);
|
|
76
190
|
}
|
|
77
191
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
192
|
+
|
|
193
|
+
// src/daemon/path-mutation-gate.ts
|
|
194
|
+
var PathMutationGateBusyError = class extends Error {
|
|
195
|
+
constructor(scope, targetPath) {
|
|
196
|
+
super(`${scope} mutation gate is already held for ${targetPath}`);
|
|
197
|
+
this.scope = scope;
|
|
198
|
+
this.targetPath = targetPath;
|
|
199
|
+
this.name = "PathMutationGateBusyError";
|
|
200
|
+
}
|
|
201
|
+
scope;
|
|
202
|
+
targetPath;
|
|
203
|
+
};
|
|
204
|
+
var GATE_PROTOCOL_PREFIX = "byok-path-mutation-v1:";
|
|
205
|
+
var GATE_PROBE_TIMEOUT_MS = 1e3;
|
|
206
|
+
var POSIX_GATE_ROOT = `/tmp/byok-pm-${process.getuid?.() ?? "unknown"}`;
|
|
207
|
+
function assertAbsolutePath(value) {
|
|
208
|
+
if (!path3__default.isAbsolute(value)) throw new Error("path mutation gate target must be absolute");
|
|
209
|
+
if (/[\x00\r\n]/u.test(value)) throw new Error("path mutation gate target must not contain NUL or line breaks");
|
|
210
|
+
}
|
|
211
|
+
async function resolvePathWithoutCreate(input) {
|
|
212
|
+
assertAbsolutePath(input);
|
|
213
|
+
let cursor = path3__default.resolve(input);
|
|
214
|
+
const missing = [];
|
|
81
215
|
for (; ; ) {
|
|
82
216
|
try {
|
|
83
|
-
return
|
|
217
|
+
return path3__default.resolve(await promises.realpath(cursor), ...missing);
|
|
84
218
|
} catch (error) {
|
|
85
219
|
const code = error.code;
|
|
86
220
|
if (code !== "ENOENT" && code !== "ENOTDIR") throw error;
|
|
87
|
-
const parent =
|
|
88
|
-
if (parent === cursor) throw new
|
|
89
|
-
|
|
221
|
+
const parent = path3__default.dirname(cursor);
|
|
222
|
+
if (parent === cursor) throw new Error(`no existing ancestor for ${input}`);
|
|
223
|
+
missing.unshift(path3__default.basename(cursor));
|
|
90
224
|
cursor = parent;
|
|
91
225
|
}
|
|
92
226
|
}
|
|
93
227
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
let cursor = canonical2;
|
|
97
|
-
for (const component of tail) {
|
|
98
|
-
cursor = path.join(cursor, component);
|
|
99
|
-
try {
|
|
100
|
-
const stat = await promises.lstat(cursor);
|
|
101
|
-
if (!stat.isDirectory() || stat.isSymbolicLink()) {
|
|
102
|
-
throw new AgentHomeResolutionError(`Agent home path component is not a real directory: ${cursor}`);
|
|
103
|
-
}
|
|
104
|
-
} catch (error) {
|
|
105
|
-
if (error.code !== "ENOENT") throw error;
|
|
106
|
-
await promises.mkdir(cursor, { mode: 448 });
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
const realized = await promises.realpath(inputPath);
|
|
110
|
-
if (realized !== cursor) {
|
|
111
|
-
throw new AgentHomeResolutionError(`Agent home path changed through a symlink while being created: ${inputPath}`);
|
|
112
|
-
}
|
|
113
|
-
return realized;
|
|
228
|
+
function gateIdentity(scope, canonicalTarget) {
|
|
229
|
+
return createHash("sha256").update(`${scope}\0${canonicalTarget}`).digest("hex");
|
|
114
230
|
}
|
|
115
|
-
|
|
116
|
-
if (
|
|
117
|
-
|
|
118
|
-
const components = relative === "" ? [] : relative.split(path.sep);
|
|
119
|
-
let cursor = root;
|
|
120
|
-
for (const component of components) {
|
|
121
|
-
cursor = path.join(cursor, component);
|
|
122
|
-
try {
|
|
123
|
-
const stat = await promises.lstat(cursor);
|
|
124
|
-
if (!stat.isDirectory() || stat.isSymbolicLink()) {
|
|
125
|
-
throw new AgentHomeResolutionError(`Agent home path component is not a real directory: ${cursor}`);
|
|
126
|
-
}
|
|
127
|
-
} catch (error) {
|
|
128
|
-
if (error.code !== "ENOENT") throw error;
|
|
129
|
-
await promises.mkdir(cursor, { mode: 448 });
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
const realized = await promises.realpath(target);
|
|
133
|
-
if (!isWithin(root, realized) || realized !== target) {
|
|
134
|
-
throw new AgentHomeResolutionError("Agent home changed through a symlink while it was being prepared");
|
|
135
|
-
}
|
|
136
|
-
return realized;
|
|
231
|
+
function gateEndpoint(identity) {
|
|
232
|
+
if (process.platform === "win32") return `\\\\.\\pipe\\byok-path-mutation-${identity}`;
|
|
233
|
+
return path3__default.join(POSIX_GATE_ROOT, `${identity}.sock`);
|
|
137
234
|
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
235
|
+
function endProbe(socket, identity) {
|
|
236
|
+
socket.on("error", () => {
|
|
237
|
+
});
|
|
238
|
+
socket.end(`${GATE_PROTOCOL_PREFIX}${identity}
|
|
239
|
+
`);
|
|
240
|
+
}
|
|
241
|
+
async function probeEndpoint(endpoint, identity) {
|
|
242
|
+
return new Promise((resolve) => {
|
|
243
|
+
const socket = createConnection(endpoint);
|
|
244
|
+
let settled = false;
|
|
245
|
+
let raw = "";
|
|
246
|
+
const finish = (result) => {
|
|
247
|
+
if (settled) return;
|
|
248
|
+
settled = true;
|
|
249
|
+
clearTimeout(timer);
|
|
250
|
+
socket.removeAllListeners();
|
|
251
|
+
socket.destroy();
|
|
252
|
+
resolve(result);
|
|
253
|
+
};
|
|
254
|
+
const timer = setTimeout(() => finish("occupied"), GATE_PROBE_TIMEOUT_MS);
|
|
255
|
+
socket.setEncoding("utf8");
|
|
256
|
+
socket.on("data", (chunk) => {
|
|
257
|
+
raw += chunk;
|
|
258
|
+
if (raw.length > GATE_PROTOCOL_PREFIX.length + 65) finish("occupied");
|
|
259
|
+
});
|
|
260
|
+
socket.once("end", () => {
|
|
261
|
+
finish(raw.trimEnd() === `${GATE_PROTOCOL_PREFIX}${identity}` ? "holder" : "occupied");
|
|
262
|
+
});
|
|
263
|
+
socket.once("error", (error) => {
|
|
264
|
+
finish(error.code === "ECONNREFUSED" || error.code === "ENOENT" ? "unbound" : "occupied");
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
async function assertPrivateGateRoot() {
|
|
269
|
+
if (process.platform === "win32") return;
|
|
270
|
+
await ensureSecureDir(POSIX_GATE_ROOT);
|
|
271
|
+
const stat = await promises.lstat(POSIX_GATE_ROOT);
|
|
272
|
+
const uid = process.getuid?.();
|
|
273
|
+
if (!stat.isDirectory() || stat.isSymbolicLink() || uid !== void 0 && stat.uid !== uid) {
|
|
274
|
+
throw new Error(`path mutation gate root is not a private owned directory: ${POSIX_GATE_ROOT}`);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
async function clearProvenStaleEndpoint(endpoint, identity, scope, targetPath) {
|
|
278
|
+
if (process.platform === "win32") return;
|
|
279
|
+
let stat;
|
|
280
|
+
try {
|
|
281
|
+
stat = await promises.lstat(endpoint);
|
|
282
|
+
} catch (error) {
|
|
283
|
+
if (error.code === "ENOENT") return;
|
|
284
|
+
throw error;
|
|
285
|
+
}
|
|
286
|
+
if (!stat.isSocket()) throw new Error(`path mutation gate endpoint is not a socket: ${endpoint}`);
|
|
287
|
+
if (await probeEndpoint(endpoint, identity) !== "unbound") {
|
|
288
|
+
throw new PathMutationGateBusyError(scope, targetPath);
|
|
289
|
+
}
|
|
290
|
+
await promises.rm(endpoint, { force: true });
|
|
291
|
+
}
|
|
292
|
+
async function acquireOne(input) {
|
|
293
|
+
const canonicalTarget = await resolvePathWithoutCreate(input.targetPath);
|
|
294
|
+
const identity = gateIdentity(input.scope, canonicalTarget);
|
|
295
|
+
const endpoint = gateEndpoint(identity);
|
|
296
|
+
await assertPrivateGateRoot();
|
|
297
|
+
await clearProvenStaleEndpoint(endpoint, identity, input.scope, canonicalTarget);
|
|
298
|
+
const server = createServer((socket) => endProbe(socket, identity));
|
|
299
|
+
try {
|
|
300
|
+
await new Promise((resolve, reject) => {
|
|
301
|
+
server.once("error", reject);
|
|
302
|
+
server.listen(endpoint, () => {
|
|
303
|
+
server.removeListener("error", reject);
|
|
304
|
+
resolve();
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
} catch (error) {
|
|
308
|
+
server.close();
|
|
309
|
+
if (error.code === "EADDRINUSE") {
|
|
310
|
+
throw new PathMutationGateBusyError(input.scope, canonicalTarget);
|
|
311
|
+
}
|
|
312
|
+
throw error;
|
|
313
|
+
}
|
|
314
|
+
if (process.platform !== "win32") await promises.chmod(endpoint, 384).catch(() => void 0);
|
|
315
|
+
server.unref();
|
|
316
|
+
try {
|
|
317
|
+
const rechecked = await resolvePathWithoutCreate(input.targetPath);
|
|
318
|
+
if (rechecked !== canonicalTarget) {
|
|
319
|
+
throw new Error(`path mutation gate target changed during acquisition: ${input.targetPath}`);
|
|
320
|
+
}
|
|
321
|
+
} catch (error) {
|
|
322
|
+
await new Promise((resolve) => server.close(() => resolve()));
|
|
323
|
+
if (process.platform !== "win32") await promises.rm(endpoint, { force: true }).catch(() => void 0);
|
|
324
|
+
throw error;
|
|
325
|
+
}
|
|
326
|
+
let serverClosed = false;
|
|
327
|
+
let released = false;
|
|
328
|
+
let releaseAttempt;
|
|
329
|
+
return Object.freeze({
|
|
330
|
+
scope: input.scope,
|
|
331
|
+
targetPath: canonicalTarget,
|
|
332
|
+
identity,
|
|
333
|
+
release: () => {
|
|
334
|
+
if (released) return Promise.resolve();
|
|
335
|
+
if (releaseAttempt !== void 0) return releaseAttempt;
|
|
336
|
+
releaseAttempt = (async () => {
|
|
337
|
+
if (!serverClosed) {
|
|
338
|
+
await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
|
|
339
|
+
serverClosed = true;
|
|
340
|
+
}
|
|
341
|
+
if (process.platform !== "win32") await promises.rm(endpoint, { force: true });
|
|
342
|
+
released = true;
|
|
343
|
+
})().catch((error) => {
|
|
344
|
+
releaseAttempt = void 0;
|
|
345
|
+
throw error;
|
|
346
|
+
});
|
|
347
|
+
return releaseAttempt;
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
async function acquirePathMutationGate(input, options = {}) {
|
|
352
|
+
const waitMs = options.waitMs ?? 0;
|
|
353
|
+
if (!Number.isSafeInteger(waitMs) || waitMs < 0 || waitMs > 3e4) {
|
|
354
|
+
throw new Error("path mutation gate waitMs must be an integer from 0 through 30000");
|
|
355
|
+
}
|
|
356
|
+
const deadline = Date.now() + waitMs;
|
|
357
|
+
for (; ; ) {
|
|
358
|
+
try {
|
|
359
|
+
return await acquireOne(input);
|
|
360
|
+
} catch (error) {
|
|
361
|
+
if (!(error instanceof PathMutationGateBusyError) || Date.now() >= deadline) throw error;
|
|
362
|
+
await new Promise((resolve) => {
|
|
363
|
+
setTimeout(resolve, Math.min(10, Math.max(1, deadline - Date.now())));
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// src/agent-home.ts
|
|
370
|
+
var AGENT_HOME_DIRECTORY = "agents";
|
|
371
|
+
var AGENT_HOME_INTERNAL_DIRECTORY = ".byok";
|
|
372
|
+
var AGENT_HOME_PROJECTION_STATE_FILE = "agent-home-projection.json";
|
|
373
|
+
var AgentHomeError = class extends Error {
|
|
374
|
+
constructor(message) {
|
|
375
|
+
super(message);
|
|
376
|
+
this.name = "AgentHomeError";
|
|
377
|
+
}
|
|
378
|
+
};
|
|
379
|
+
var AgentRefValidationError = class extends AgentHomeError {
|
|
380
|
+
constructor(message) {
|
|
381
|
+
super(message);
|
|
382
|
+
this.name = "AgentRefValidationError";
|
|
383
|
+
}
|
|
384
|
+
};
|
|
385
|
+
var AgentHomeResolutionError = class extends AgentHomeError {
|
|
386
|
+
constructor(message) {
|
|
387
|
+
super(message);
|
|
388
|
+
this.name = "AgentHomeResolutionError";
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
var AgentHomeCollisionError = class extends AgentHomeResolutionError {
|
|
392
|
+
constructor(message) {
|
|
393
|
+
super(message);
|
|
394
|
+
this.name = "AgentHomeCollisionError";
|
|
395
|
+
}
|
|
396
|
+
};
|
|
397
|
+
var AgentHomeBusyError = class extends AgentHomeError {
|
|
398
|
+
constructor(message) {
|
|
399
|
+
super(message);
|
|
400
|
+
this.name = "AgentHomeBusyError";
|
|
401
|
+
}
|
|
402
|
+
};
|
|
403
|
+
var AgentHomeLeaseCorruptError = class extends AgentHomeResolutionError {
|
|
404
|
+
constructor(message) {
|
|
405
|
+
super(message);
|
|
406
|
+
this.name = "AgentHomeLeaseCorruptError";
|
|
407
|
+
}
|
|
408
|
+
};
|
|
409
|
+
function validateAgentRef(value) {
|
|
410
|
+
let candidate;
|
|
411
|
+
try {
|
|
412
|
+
candidate = AgentRefSchema.parse(value);
|
|
413
|
+
} catch (error) {
|
|
414
|
+
throw new AgentRefValidationError(
|
|
415
|
+
`AgentRef does not match the protocol contract: ${error instanceof Error ? error.message : String(error)}`
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
return Object.freeze({ agentId: candidate.agentId, profileRevision: candidate.profileRevision });
|
|
419
|
+
}
|
|
420
|
+
function isWithin(root, candidate) {
|
|
421
|
+
const relative = path3__default.relative(root, candidate);
|
|
422
|
+
return relative === "" || relative !== ".." && !relative.startsWith(`..${path3__default.sep}`) && !path3__default.isAbsolute(relative);
|
|
423
|
+
}
|
|
424
|
+
function assertAbsolutePath2(value, label) {
|
|
425
|
+
if (typeof value !== "string" || value.length === 0 || !path3__default.isAbsolute(value)) {
|
|
426
|
+
throw new AgentHomeResolutionError(`${label} must be an absolute path`);
|
|
427
|
+
}
|
|
428
|
+
if (/[\u0000\r\n]/u.test(value)) {
|
|
429
|
+
throw new AgentHomeResolutionError(`${label} must not contain NUL or line breaks`);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
async function resolveExistingAncestor(inputPath) {
|
|
433
|
+
let cursor = path3__default.resolve(inputPath);
|
|
434
|
+
const tail = [];
|
|
435
|
+
for (; ; ) {
|
|
436
|
+
try {
|
|
437
|
+
return { canonical: await promises.realpath(cursor), tail };
|
|
438
|
+
} catch (error) {
|
|
439
|
+
const code = error.code;
|
|
440
|
+
if (code !== "ENOENT" && code !== "ENOTDIR") throw error;
|
|
441
|
+
const parent = path3__default.dirname(cursor);
|
|
442
|
+
if (parent === cursor) throw new AgentHomeResolutionError(`no existing ancestor for ${inputPath}`);
|
|
443
|
+
tail.unshift(path3__default.basename(cursor));
|
|
444
|
+
cursor = parent;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
async function materializeDirectory(inputPath) {
|
|
449
|
+
const { canonical: canonical2, tail } = await resolveExistingAncestor(inputPath);
|
|
450
|
+
let cursor = canonical2;
|
|
451
|
+
for (const component of tail) {
|
|
452
|
+
cursor = path3__default.join(cursor, component);
|
|
453
|
+
try {
|
|
454
|
+
const stat = await promises.lstat(cursor);
|
|
455
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) {
|
|
456
|
+
throw new AgentHomeResolutionError(`Agent home path component is not a real directory: ${cursor}`);
|
|
457
|
+
}
|
|
458
|
+
} catch (error) {
|
|
459
|
+
if (error.code !== "ENOENT") throw error;
|
|
460
|
+
await promises.mkdir(cursor, { mode: 448 });
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
const realized = await promises.realpath(inputPath);
|
|
464
|
+
if (realized !== cursor) {
|
|
465
|
+
throw new AgentHomeResolutionError(`Agent home path changed through a symlink while being created: ${inputPath}`);
|
|
466
|
+
}
|
|
467
|
+
return realized;
|
|
468
|
+
}
|
|
469
|
+
async function ensureDirectoryNoSymlink(root, target) {
|
|
470
|
+
if (!isWithin(root, target)) throw new AgentHomeResolutionError("Agent home is outside hostStorageRoot");
|
|
471
|
+
const relative = path3__default.relative(root, target);
|
|
472
|
+
const components = relative === "" ? [] : relative.split(path3__default.sep);
|
|
473
|
+
let cursor = root;
|
|
474
|
+
for (const component of components) {
|
|
475
|
+
cursor = path3__default.join(cursor, component);
|
|
476
|
+
try {
|
|
477
|
+
const stat = await promises.lstat(cursor);
|
|
478
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) {
|
|
479
|
+
throw new AgentHomeResolutionError(`Agent home path component is not a real directory: ${cursor}`);
|
|
480
|
+
}
|
|
481
|
+
} catch (error) {
|
|
482
|
+
if (error.code !== "ENOENT") throw error;
|
|
483
|
+
await promises.mkdir(cursor, { mode: 448 });
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
const realized = await promises.realpath(target);
|
|
487
|
+
if (!isWithin(root, realized) || realized !== target) {
|
|
488
|
+
throw new AgentHomeResolutionError("Agent home changed through a symlink while it was being prepared");
|
|
489
|
+
}
|
|
490
|
+
return realized;
|
|
491
|
+
}
|
|
492
|
+
async function ensurePreservedFile(filePath) {
|
|
493
|
+
try {
|
|
494
|
+
const handle = await promises.open(filePath, "wx", 384);
|
|
495
|
+
await handle.close();
|
|
496
|
+
return;
|
|
497
|
+
} catch (error) {
|
|
498
|
+
if (error.code !== "EEXIST") throw error;
|
|
499
|
+
}
|
|
500
|
+
const stat = await promises.lstat(filePath);
|
|
501
|
+
if (!stat.isFile() || stat.isSymbolicLink()) {
|
|
502
|
+
throw new AgentHomeResolutionError(`Agent home preserved file is not a regular file: ${filePath}`);
|
|
503
|
+
}
|
|
150
504
|
}
|
|
151
505
|
var AgentHomeLayout = class {
|
|
152
506
|
hostStorageRootInput;
|
|
153
507
|
agentIdByCanonicalHome = /* @__PURE__ */ new Map();
|
|
154
508
|
canonicalRoot;
|
|
155
509
|
constructor(hostStorageRoot) {
|
|
156
|
-
|
|
157
|
-
this.hostStorageRootInput =
|
|
510
|
+
assertAbsolutePath2(hostStorageRoot, "agentHome.hostStorageRoot");
|
|
511
|
+
this.hostStorageRootInput = path3__default.resolve(hostStorageRoot);
|
|
158
512
|
}
|
|
159
513
|
async resolve(agentRefInput) {
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
hostStorageRoot
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
const canonicalHome = await ensureDirectoryNoSymlink(agentsRoot, lexicalHome);
|
|
168
|
-
const priorAgentId = this.agentIdByCanonicalHome.get(canonicalHome);
|
|
169
|
-
if (priorAgentId !== void 0 && priorAgentId !== agentRef.agentId) {
|
|
170
|
-
throw new AgentHomeCollisionError(
|
|
171
|
-
`canonical Agent home ${canonicalHome} is already bound to Agent ${priorAgentId}`
|
|
514
|
+
const gate = await this.acquireRootMutationGate();
|
|
515
|
+
try {
|
|
516
|
+
const agentRef = validateAgentRef(agentRefInput);
|
|
517
|
+
const hostStorageRoot = await this.resolveRoot();
|
|
518
|
+
const agentsRoot = await ensureDirectoryNoSymlink(
|
|
519
|
+
hostStorageRoot,
|
|
520
|
+
path3__default.join(hostStorageRoot, AGENT_HOME_DIRECTORY)
|
|
172
521
|
);
|
|
522
|
+
const lexicalHome = path3__default.join(agentsRoot, agentRef.agentId);
|
|
523
|
+
const canonicalHome = await ensureDirectoryNoSymlink(agentsRoot, lexicalHome);
|
|
524
|
+
const priorAgentId = this.agentIdByCanonicalHome.get(canonicalHome);
|
|
525
|
+
if (priorAgentId !== void 0 && priorAgentId !== agentRef.agentId) {
|
|
526
|
+
throw new AgentHomeCollisionError(
|
|
527
|
+
`canonical Agent home ${canonicalHome} is already bound to Agent ${priorAgentId}`
|
|
528
|
+
);
|
|
529
|
+
}
|
|
530
|
+
this.agentIdByCanonicalHome.set(canonicalHome, agentRef.agentId);
|
|
531
|
+
return Object.freeze({
|
|
532
|
+
agentRef,
|
|
533
|
+
hostStorageRoot,
|
|
534
|
+
agentsRoot,
|
|
535
|
+
homeDir: canonicalHome,
|
|
536
|
+
canonicalHome
|
|
537
|
+
});
|
|
538
|
+
} finally {
|
|
539
|
+
await gate.release();
|
|
173
540
|
}
|
|
174
|
-
this.agentIdByCanonicalHome.set(canonicalHome, agentRef.agentId);
|
|
175
|
-
return Object.freeze({
|
|
176
|
-
agentRef,
|
|
177
|
-
hostStorageRoot,
|
|
178
|
-
agentsRoot,
|
|
179
|
-
homeDir: canonicalHome,
|
|
180
|
-
canonicalHome
|
|
181
|
-
});
|
|
182
541
|
}
|
|
183
542
|
/**
|
|
184
543
|
* Prove the canonical root is materializable and writable before the daemon
|
|
@@ -186,6 +545,7 @@ var AgentHomeLayout = class {
|
|
|
186
545
|
* file is created by this preflight.
|
|
187
546
|
*/
|
|
188
547
|
async preflight() {
|
|
548
|
+
const gate = await this.acquireRootMutationGate();
|
|
189
549
|
let probePath;
|
|
190
550
|
let handle;
|
|
191
551
|
let created = false;
|
|
@@ -193,9 +553,9 @@ var AgentHomeLayout = class {
|
|
|
193
553
|
const hostStorageRoot = await this.resolveRoot();
|
|
194
554
|
const agentsRoot = await ensureDirectoryNoSymlink(
|
|
195
555
|
hostStorageRoot,
|
|
196
|
-
|
|
556
|
+
path3__default.join(hostStorageRoot, AGENT_HOME_DIRECTORY)
|
|
197
557
|
);
|
|
198
|
-
probePath =
|
|
558
|
+
probePath = path3__default.join(agentsRoot, `.byok-agent-home-preflight-${randomUUID()}`);
|
|
199
559
|
handle = await promises.open(probePath, "wx", 384);
|
|
200
560
|
created = true;
|
|
201
561
|
await handle.sync();
|
|
@@ -211,8 +571,19 @@ var AgentHomeLayout = class {
|
|
|
211
571
|
throw new AgentHomeResolutionError(
|
|
212
572
|
`agentHome.hostStorageRoot preflight failed: ${error instanceof Error ? error.message : String(error)}`
|
|
213
573
|
);
|
|
574
|
+
} finally {
|
|
575
|
+
await gate.release();
|
|
214
576
|
}
|
|
215
577
|
}
|
|
578
|
+
/**
|
|
579
|
+
* Construction-time validation is deliberately non-mutating. The actual
|
|
580
|
+
* writable preflight runs asynchronously after daemon ownership is acquired
|
|
581
|
+
* and before transport/capability publication, where it can participate in
|
|
582
|
+
* the cross-process relocation gate without a sync shadow lock.
|
|
583
|
+
*/
|
|
584
|
+
preflightSync() {
|
|
585
|
+
assertAbsolutePath2(this.hostStorageRootInput, "agentHome.hostStorageRoot");
|
|
586
|
+
}
|
|
216
587
|
async resolveRoot() {
|
|
217
588
|
if (this.canonicalRoot !== void 0) return this.canonicalRoot;
|
|
218
589
|
try {
|
|
@@ -224,9 +595,22 @@ var AgentHomeLayout = class {
|
|
|
224
595
|
}
|
|
225
596
|
return this.canonicalRoot;
|
|
226
597
|
}
|
|
598
|
+
async acquireRootMutationGate() {
|
|
599
|
+
try {
|
|
600
|
+
return await acquirePathMutationGate({
|
|
601
|
+
scope: "agent-home-root",
|
|
602
|
+
targetPath: path3__default.join(this.hostStorageRootInput, AGENT_HOME_DIRECTORY)
|
|
603
|
+
}, { waitMs: 1e3 });
|
|
604
|
+
} catch (error) {
|
|
605
|
+
if (error instanceof PathMutationGateBusyError) {
|
|
606
|
+
throw new AgentHomeBusyError("Agent-home root is reserved for local-state relocation");
|
|
607
|
+
}
|
|
608
|
+
throw error;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
227
611
|
};
|
|
228
612
|
function stableAgentHomeOwnerId(storeDir, productId) {
|
|
229
|
-
const identity = `${
|
|
613
|
+
const identity = `${path3__default.resolve(storeDir)}\0${productId}`;
|
|
230
614
|
return `store-product:${createHash("sha256").update(identity).digest("hex")}`;
|
|
231
615
|
}
|
|
232
616
|
function parseLeaseMarker(value, lockPath) {
|
|
@@ -236,7 +620,7 @@ function parseLeaseMarker(value, lockPath) {
|
|
|
236
620
|
} catch {
|
|
237
621
|
throw new AgentHomeLeaseCorruptError(`Agent home lease marker ${lockPath} is corrupt`);
|
|
238
622
|
}
|
|
239
|
-
if (typeof parsed !== "object" || parsed === null || parsed.version !== 1 || typeof parsed.ownerId !== "string" || typeof parsed.leaseId !== "string" || typeof parsed.canonicalHome !== "string" || !
|
|
623
|
+
if (typeof parsed !== "object" || parsed === null || parsed.version !== 1 || typeof parsed.ownerId !== "string" || typeof parsed.leaseId !== "string" || typeof parsed.canonicalHome !== "string" || !path3__default.isAbsolute(parsed.canonicalHome)) {
|
|
240
624
|
throw new AgentHomeLeaseCorruptError(`Agent home lease marker ${lockPath} has an invalid shape`);
|
|
241
625
|
}
|
|
242
626
|
let agentRef;
|
|
@@ -246,7 +630,7 @@ function parseLeaseMarker(value, lockPath) {
|
|
|
246
630
|
throw new AgentHomeLeaseCorruptError(`Agent home lease marker ${lockPath} has an invalid AgentRef`);
|
|
247
631
|
}
|
|
248
632
|
const marker = parsed;
|
|
249
|
-
return { ...marker, agentRef, canonicalHome:
|
|
633
|
+
return { ...marker, agentRef, canonicalHome: path3__default.resolve(marker.canonicalHome) };
|
|
250
634
|
}
|
|
251
635
|
var AgentHomeLeaseManager = class _AgentHomeLeaseManager {
|
|
252
636
|
static held = /* @__PURE__ */ new Map();
|
|
@@ -263,14 +647,19 @@ var AgentHomeLeaseManager = class _AgentHomeLeaseManager {
|
|
|
263
647
|
_AgentHomeLeaseManager.held.set(canonicalHome, leaseId);
|
|
264
648
|
let lockPath;
|
|
265
649
|
let handle;
|
|
650
|
+
let rootGate;
|
|
266
651
|
let ownsMarker = false;
|
|
267
652
|
try {
|
|
653
|
+
rootGate = await acquirePathMutationGate({
|
|
654
|
+
scope: "agent-home-root",
|
|
655
|
+
targetPath: resolution.agentsRoot
|
|
656
|
+
}, { waitMs: 1e3 });
|
|
268
657
|
await ensureDirectoryNoSymlink(resolution.agentsRoot, canonicalHome);
|
|
269
658
|
const internalDir = await ensureDirectoryNoSymlink(
|
|
270
659
|
canonicalHome,
|
|
271
|
-
|
|
660
|
+
path3__default.join(canonicalHome, AGENT_HOME_INTERNAL_DIRECTORY)
|
|
272
661
|
);
|
|
273
|
-
lockPath =
|
|
662
|
+
lockPath = path3__default.join(internalDir, "agent-home.lease");
|
|
274
663
|
handle = await this.openLeaseMarker(lockPath, canonicalHome, agentRef.agentId);
|
|
275
664
|
ownsMarker = true;
|
|
276
665
|
const marker = {
|
|
@@ -284,14 +673,21 @@ var AgentHomeLeaseManager = class _AgentHomeLeaseManager {
|
|
|
284
673
|
await handle.sync();
|
|
285
674
|
await handle.close();
|
|
286
675
|
handle = void 0;
|
|
676
|
+
await rootGate.release();
|
|
677
|
+
rootGate = void 0;
|
|
287
678
|
} catch (error) {
|
|
288
679
|
await handle?.close().catch(() => {
|
|
289
680
|
});
|
|
290
681
|
if (ownsMarker && lockPath !== void 0) await promises.rm(lockPath, { force: true }).catch(() => {
|
|
291
682
|
});
|
|
683
|
+
await rootGate?.release().catch(() => {
|
|
684
|
+
});
|
|
292
685
|
if (_AgentHomeLeaseManager.held.get(canonicalHome) === leaseId) {
|
|
293
686
|
_AgentHomeLeaseManager.held.delete(canonicalHome);
|
|
294
687
|
}
|
|
688
|
+
if (error instanceof PathMutationGateBusyError) {
|
|
689
|
+
throw new AgentHomeBusyError("Agent-home root is reserved for local-state relocation");
|
|
690
|
+
}
|
|
295
691
|
if (error instanceof AgentHomeError) throw error;
|
|
296
692
|
throw new AgentHomeError(`could not acquire Agent home lease: ${error instanceof Error ? error.message : String(error)}`);
|
|
297
693
|
}
|
|
@@ -369,9 +765,73 @@ var AgentHomeLeaseManager = class _AgentHomeLeaseManager {
|
|
|
369
765
|
async function initializeAgentHome(resolution) {
|
|
370
766
|
await ensureDirectoryNoSymlink(
|
|
371
767
|
resolution.canonicalHome,
|
|
372
|
-
|
|
768
|
+
path3__default.join(resolution.canonicalHome, "notes")
|
|
373
769
|
);
|
|
374
|
-
await ensurePreservedFile(
|
|
770
|
+
await ensurePreservedFile(path3__default.join(resolution.canonicalHome, "MEMORY.md"));
|
|
771
|
+
}
|
|
772
|
+
function projectionStatePath(resolution) {
|
|
773
|
+
return path3__default.join(
|
|
774
|
+
resolution.canonicalHome,
|
|
775
|
+
AGENT_HOME_INTERNAL_DIRECTORY,
|
|
776
|
+
AGENT_HOME_PROJECTION_STATE_FILE
|
|
777
|
+
);
|
|
778
|
+
}
|
|
779
|
+
async function readProjectionState(resolution) {
|
|
780
|
+
const filePath = projectionStatePath(resolution);
|
|
781
|
+
try {
|
|
782
|
+
const stat = await promises.lstat(filePath);
|
|
783
|
+
if (!stat.isFile() || stat.isSymbolicLink()) {
|
|
784
|
+
throw new AgentHomeResolutionError(`Agent projection state is not a regular file: ${filePath}`);
|
|
785
|
+
}
|
|
786
|
+
} catch (error) {
|
|
787
|
+
if (error.code === "ENOENT") return void 0;
|
|
788
|
+
throw error;
|
|
789
|
+
}
|
|
790
|
+
let parsed;
|
|
791
|
+
try {
|
|
792
|
+
parsed = JSON.parse(await promises.readFile(filePath, "utf8"));
|
|
793
|
+
} catch (error) {
|
|
794
|
+
throw new AgentHomeResolutionError(
|
|
795
|
+
`Agent projection state is corrupt: ${error instanceof Error ? error.message : String(error)}`
|
|
796
|
+
);
|
|
797
|
+
}
|
|
798
|
+
if (typeof parsed !== "object" || parsed === null || parsed.version !== 1 || typeof parsed.requestId !== "string" || typeof parsed.projectionHash !== "string") {
|
|
799
|
+
throw new AgentHomeResolutionError("Agent projection state has an invalid shape");
|
|
800
|
+
}
|
|
801
|
+
const candidate = parsed;
|
|
802
|
+
const agentRef = validateAgentRef(candidate.agentRef);
|
|
803
|
+
if (agentRef.agentId !== resolution.agentRef.agentId) {
|
|
804
|
+
throw new AgentHomeCollisionError("Agent projection state belongs to a different Agent home");
|
|
805
|
+
}
|
|
806
|
+
return Object.freeze({
|
|
807
|
+
version: 1,
|
|
808
|
+
agentRef,
|
|
809
|
+
requestId: candidate.requestId,
|
|
810
|
+
projectionHash: candidate.projectionHash
|
|
811
|
+
});
|
|
812
|
+
}
|
|
813
|
+
async function writeProjectionState(resolution, payload) {
|
|
814
|
+
const filePath = projectionStatePath(resolution);
|
|
815
|
+
const existing = await promises.lstat(filePath).catch((error) => {
|
|
816
|
+
if (error.code === "ENOENT") return void 0;
|
|
817
|
+
throw error;
|
|
818
|
+
});
|
|
819
|
+
if (existing !== void 0 && (!existing.isFile() || existing.isSymbolicLink())) {
|
|
820
|
+
throw new AgentHomeResolutionError(`Agent projection state is not a regular file: ${filePath}`);
|
|
821
|
+
}
|
|
822
|
+
const state = {
|
|
823
|
+
version: 1,
|
|
824
|
+
agentRef: payload.agentRef,
|
|
825
|
+
requestId: payload.requestId,
|
|
826
|
+
projectionHash: payload.projectionHash
|
|
827
|
+
};
|
|
828
|
+
await atomicWriteFile(filePath, `${JSON.stringify(state)}
|
|
829
|
+
`, { mode: 384, fsync: true });
|
|
830
|
+
}
|
|
831
|
+
function compareProjectionRevision(left, right) {
|
|
832
|
+
const leftRevision = BigInt(left);
|
|
833
|
+
const rightRevision = BigInt(right);
|
|
834
|
+
return leftRevision < rightRevision ? -1 : leftRevision > rightRevision ? 1 : 0;
|
|
375
835
|
}
|
|
376
836
|
var AgentHomeManager = class {
|
|
377
837
|
layout;
|
|
@@ -397,6 +857,10 @@ var AgentHomeManager = class {
|
|
|
397
857
|
async preflight() {
|
|
398
858
|
await this.layout.preflight();
|
|
399
859
|
}
|
|
860
|
+
/** Synchronous construction-time preflight for strict Agent-only admission. */
|
|
861
|
+
preflightSync() {
|
|
862
|
+
this.layout.preflightSync();
|
|
863
|
+
}
|
|
400
864
|
/** Resolve and lease without applying downstream projection side effects. */
|
|
401
865
|
async acquire(agentRef) {
|
|
402
866
|
const resolution = await this.layout.resolve(agentRef);
|
|
@@ -407,14 +871,68 @@ var AgentHomeManager = class {
|
|
|
407
871
|
async initialize(binding) {
|
|
408
872
|
const { resolution, lease } = binding;
|
|
409
873
|
await initializeAgentHome(resolution);
|
|
410
|
-
|
|
874
|
+
const prepare = this.projection?.prepare;
|
|
875
|
+
if (prepare !== void 0) await prepare({ ...resolution, cwd: lease.cwd });
|
|
411
876
|
if (await promises.realpath(resolution.homeDir) !== resolution.canonicalHome) {
|
|
412
877
|
throw new AgentHomeResolutionError("Agent projection changed the canonical home path");
|
|
413
878
|
}
|
|
414
879
|
await initializeAgentHome(resolution);
|
|
415
880
|
}
|
|
416
|
-
|
|
417
|
-
|
|
881
|
+
supportsTaskFreeProjection() {
|
|
882
|
+
return this.projection?.apply !== void 0;
|
|
883
|
+
}
|
|
884
|
+
/**
|
|
885
|
+
* Apply one task-free projection under the same canonical-home writer lease
|
|
886
|
+
* used by Agent execution. The host hook owns an atomic/idempotent ensure of
|
|
887
|
+
* its opaque product bytes, so an exact desired-state replay invokes it again
|
|
888
|
+
* before returning `idempotent`. Only a successful new-state hook followed
|
|
889
|
+
* by the SDK-owned fsynced ordering record can return `applied`.
|
|
890
|
+
*/
|
|
891
|
+
async project(input) {
|
|
892
|
+
const payload = AgentHomeProjectionPayloadSchema.parse(input);
|
|
893
|
+
const binding = await this.acquire(payload.agentRef);
|
|
894
|
+
try {
|
|
895
|
+
const { resolution, lease } = binding;
|
|
896
|
+
await initializeAgentHome(resolution);
|
|
897
|
+
const applyProjection = async () => {
|
|
898
|
+
const apply = this.projection?.apply;
|
|
899
|
+
if (apply === void 0) {
|
|
900
|
+
throw new AgentHomeError("task-free Agent-home projection is not configured");
|
|
901
|
+
}
|
|
902
|
+
await apply({
|
|
903
|
+
...resolution,
|
|
904
|
+
cwd: lease.cwd,
|
|
905
|
+
requestId: payload.requestId,
|
|
906
|
+
projectionHash: payload.projectionHash,
|
|
907
|
+
projection: payload.projection
|
|
908
|
+
});
|
|
909
|
+
if (await promises.realpath(resolution.homeDir) !== resolution.canonicalHome) {
|
|
910
|
+
throw new AgentHomeResolutionError("Agent projection changed the canonical home path");
|
|
911
|
+
}
|
|
912
|
+
await initializeAgentHome(resolution);
|
|
913
|
+
};
|
|
914
|
+
const current = await readProjectionState(resolution);
|
|
915
|
+
if (current !== void 0) {
|
|
916
|
+
const order = compareProjectionRevision(
|
|
917
|
+
payload.agentRef.profileRevision,
|
|
918
|
+
current.agentRef.profileRevision
|
|
919
|
+
);
|
|
920
|
+
if (order < 0) return "stale";
|
|
921
|
+
if (order === 0) {
|
|
922
|
+
if (payload.projectionHash !== current.projectionHash) return "conflict";
|
|
923
|
+
await applyProjection();
|
|
924
|
+
return "idempotent";
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
await applyProjection();
|
|
928
|
+
await writeProjectionState(resolution, payload);
|
|
929
|
+
return "applied";
|
|
930
|
+
} finally {
|
|
931
|
+
await binding.lease.release();
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
};
|
|
935
|
+
var AgentSessionHandoffStoreError = class extends Error {
|
|
418
936
|
constructor(message) {
|
|
419
937
|
super(message);
|
|
420
938
|
this.name = "AgentSessionHandoffStoreError";
|
|
@@ -453,7 +971,7 @@ function parseTaskTerminalEntry(value) {
|
|
|
453
971
|
assertNonEmptyString(value.terminalReason, "taskTerminal.terminalReason");
|
|
454
972
|
assertNonEmptyString(value.updatedAt, "taskTerminal.updatedAt");
|
|
455
973
|
if (value.sessionRef !== void 0) assertNonEmptyString(value.sessionRef, "taskTerminal.sessionRef");
|
|
456
|
-
if (typeof value.cwd !== "string" || !
|
|
974
|
+
if (typeof value.cwd !== "string" || !path3__default.isAbsolute(value.cwd)) {
|
|
457
975
|
throw new AgentSessionHandoffCorruptError("taskTerminal.cwd must be an absolute path");
|
|
458
976
|
}
|
|
459
977
|
if (value.terminalCause !== "failed") {
|
|
@@ -466,7 +984,7 @@ function parseTaskTerminalEntry(value) {
|
|
|
466
984
|
agentRef,
|
|
467
985
|
taskId: value.taskId,
|
|
468
986
|
runtimeId: value.runtimeId,
|
|
469
|
-
cwd:
|
|
987
|
+
cwd: path3__default.resolve(value.cwd),
|
|
470
988
|
leaseId: value.leaseId,
|
|
471
989
|
...value.sessionRef === void 0 ? {} : { sessionRef: value.sessionRef },
|
|
472
990
|
terminalCause: "failed",
|
|
@@ -496,7 +1014,7 @@ function parseEntry(value) {
|
|
|
496
1014
|
assertNonEmptyString(value.runtimeId, "handoff.runtimeId");
|
|
497
1015
|
assertNonEmptyString(value.leaseId, "handoff.leaseId");
|
|
498
1016
|
assertNonEmptyString(value.updatedAt, "handoff.updatedAt");
|
|
499
|
-
if (typeof value.cwd !== "string" || !
|
|
1017
|
+
if (typeof value.cwd !== "string" || !path3__default.isAbsolute(value.cwd)) {
|
|
500
1018
|
throw new AgentSessionHandoffCorruptError("handoff.cwd must be an absolute path");
|
|
501
1019
|
}
|
|
502
1020
|
if (Number.isNaN(Date.parse(value.updatedAt))) {
|
|
@@ -513,7 +1031,7 @@ function parseEntry(value) {
|
|
|
513
1031
|
taskId: value.taskId,
|
|
514
1032
|
sessionRef: value.sessionRef,
|
|
515
1033
|
runtimeId: value.runtimeId,
|
|
516
|
-
cwd:
|
|
1034
|
+
cwd: path3__default.resolve(value.cwd),
|
|
517
1035
|
leaseId: value.leaseId,
|
|
518
1036
|
...value.terminalCause === void 0 ? {} : { terminalCause: value.terminalCause },
|
|
519
1037
|
...value.terminalReason === void 0 ? {} : { terminalReason: value.terminalReason },
|
|
@@ -524,10 +1042,10 @@ function sameRef(left, right) {
|
|
|
524
1042
|
return left.agentId === right.agentId && left.profileRevision === right.profileRevision;
|
|
525
1043
|
}
|
|
526
1044
|
function sameMatch(entry, expected) {
|
|
527
|
-
return sameRef(entry.agentRef, expected.agentRef) && entry.sessionRef === expected.sessionRef && entry.runtimeId === expected.runtimeId && entry.cwd ===
|
|
1045
|
+
return sameRef(entry.agentRef, expected.agentRef) && entry.sessionRef === expected.sessionRef && entry.runtimeId === expected.runtimeId && entry.cwd === path3__default.resolve(expected.cwd);
|
|
528
1046
|
}
|
|
529
1047
|
function sameTaskTerminalMatch(entry, expected) {
|
|
530
|
-
return sameRef(entry.agentRef, expected.agentRef) && entry.taskId === expected.taskId && entry.runtimeId === expected.runtimeId && entry.cwd ===
|
|
1048
|
+
return sameRef(entry.agentRef, expected.agentRef) && entry.taskId === expected.taskId && entry.runtimeId === expected.runtimeId && entry.cwd === path3__default.resolve(expected.cwd);
|
|
531
1049
|
}
|
|
532
1050
|
function sessionFileName(runtimeId, sessionRef) {
|
|
533
1051
|
const digest2 = createHash("sha256").update(sessionRef, "utf8").digest("hex");
|
|
@@ -540,13 +1058,13 @@ function taskTerminalFileName(runtimeId, taskId) {
|
|
|
540
1058
|
return `${runtime}-task-${digest2}.jsonl`;
|
|
541
1059
|
}
|
|
542
1060
|
async function evidenceDirectory(cwdInput) {
|
|
543
|
-
if (!
|
|
1061
|
+
if (!path3__default.isAbsolute(cwdInput)) {
|
|
544
1062
|
throw new AgentSessionHandoffStoreError("Agent session cwd must be absolute");
|
|
545
1063
|
}
|
|
546
1064
|
const cwd = await promises.realpath(cwdInput);
|
|
547
1065
|
let cursor = cwd;
|
|
548
1066
|
for (const component of [".byok", "runtime-sessions"]) {
|
|
549
|
-
cursor =
|
|
1067
|
+
cursor = path3__default.join(cursor, component);
|
|
550
1068
|
try {
|
|
551
1069
|
const stat = await promises.lstat(cursor);
|
|
552
1070
|
if (!stat.isDirectory() || stat.isSymbolicLink()) {
|
|
@@ -558,8 +1076,8 @@ async function evidenceDirectory(cwdInput) {
|
|
|
558
1076
|
}
|
|
559
1077
|
}
|
|
560
1078
|
const canonical2 = await promises.realpath(cursor);
|
|
561
|
-
const relative =
|
|
562
|
-
if (relative === ".." || relative.startsWith(`..${
|
|
1079
|
+
const relative = path3__default.relative(cwd, canonical2);
|
|
1080
|
+
if (relative === ".." || relative.startsWith(`..${path3__default.sep}`) || path3__default.isAbsolute(relative)) {
|
|
563
1081
|
throw new AgentSessionHandoffStoreError("Agent session evidence path escaped the canonical Agent home");
|
|
564
1082
|
}
|
|
565
1083
|
return canonical2;
|
|
@@ -605,7 +1123,7 @@ var AgentSessionHandoffStore = class {
|
|
|
605
1123
|
taskId: input.taskId,
|
|
606
1124
|
sessionRef: input.sessionRef,
|
|
607
1125
|
runtimeId: input.runtimeId,
|
|
608
|
-
cwd:
|
|
1126
|
+
cwd: path3__default.resolve(input.cwd),
|
|
609
1127
|
leaseId: input.leaseId,
|
|
610
1128
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
611
1129
|
});
|
|
@@ -648,7 +1166,7 @@ var AgentSessionHandoffStore = class {
|
|
|
648
1166
|
agentRef: validateAgentRef(input.agentRef),
|
|
649
1167
|
taskId: input.taskId,
|
|
650
1168
|
runtimeId: input.runtimeId,
|
|
651
|
-
cwd:
|
|
1169
|
+
cwd: path3__default.resolve(input.cwd)
|
|
652
1170
|
};
|
|
653
1171
|
const filePath = await this.taskTerminalFilePath(expected);
|
|
654
1172
|
return this.enqueue(filePath, async () => {
|
|
@@ -675,7 +1193,7 @@ var AgentSessionHandoffStore = class {
|
|
|
675
1193
|
agentRef: validateAgentRef(expectedInput.agentRef),
|
|
676
1194
|
taskId: expectedInput.taskId,
|
|
677
1195
|
runtimeId: expectedInput.runtimeId,
|
|
678
|
-
cwd:
|
|
1196
|
+
cwd: path3__default.resolve(expectedInput.cwd)
|
|
679
1197
|
};
|
|
680
1198
|
const filePath = await this.taskTerminalFilePath(expected);
|
|
681
1199
|
return this.enqueue(filePath, async () => {
|
|
@@ -693,14 +1211,14 @@ var AgentSessionHandoffStore = class {
|
|
|
693
1211
|
assertNonEmptyString(match.sessionRef, "handoff.sessionRef");
|
|
694
1212
|
assertNonEmptyString(match.runtimeId, "handoff.runtimeId");
|
|
695
1213
|
const directory = await evidenceDirectory(match.cwd);
|
|
696
|
-
return
|
|
1214
|
+
return path3__default.join(directory, sessionFileName(match.runtimeId, match.sessionRef));
|
|
697
1215
|
}
|
|
698
1216
|
async taskTerminalFilePath(match) {
|
|
699
1217
|
validateAgentRef(match.agentRef);
|
|
700
1218
|
assertNonEmptyString(match.taskId, "taskTerminal.taskId");
|
|
701
1219
|
assertNonEmptyString(match.runtimeId, "taskTerminal.runtimeId");
|
|
702
1220
|
const directory = await evidenceDirectory(match.cwd);
|
|
703
|
-
return
|
|
1221
|
+
return path3__default.join(directory, taskTerminalFileName(match.runtimeId, match.taskId));
|
|
704
1222
|
}
|
|
705
1223
|
enqueue(key, task) {
|
|
706
1224
|
const previous = this.queues.get(key) ?? Promise.resolve();
|
|
@@ -774,183 +1292,537 @@ var AgentSessionHandoffStore = class {
|
|
|
774
1292
|
}
|
|
775
1293
|
}
|
|
776
1294
|
};
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
function
|
|
783
|
-
|
|
784
|
-
const denyTools = policy.denyTools === void 0 ? void 0 : Object.freeze([...policy.denyTools]);
|
|
785
|
-
return Object.freeze({
|
|
786
|
-
mode: policy.mode,
|
|
787
|
-
...allowTools === void 0 ? {} : { allowTools },
|
|
788
|
-
...denyTools === void 0 ? {} : { denyTools },
|
|
789
|
-
...policy.workspaceRoot === void 0 ? {} : { workspaceRoot: policy.workspaceRoot },
|
|
790
|
-
...policy.network === void 0 ? {} : { network: policy.network }
|
|
1295
|
+
var DAEMON_OWNER_FILENAME = "daemon-owner.json";
|
|
1296
|
+
var RECLAIM_FILENAME = `${DAEMON_OWNER_FILENAME}.reclaim`;
|
|
1297
|
+
var MAX_OWNER_BYTES = 4096;
|
|
1298
|
+
var RECLAIM_MALFORMED_GRACE_MS = 3e4;
|
|
1299
|
+
var SELF_PROCESS_STARTED_AT = new Date(Date.now() - process.uptime() * 1e3).toISOString();
|
|
1300
|
+
function endOwnershipProbe(socket, response) {
|
|
1301
|
+
socket.on("error", () => {
|
|
791
1302
|
});
|
|
1303
|
+
if (response === void 0) socket.end();
|
|
1304
|
+
else socket.end(response);
|
|
792
1305
|
}
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
steer: descriptor.capabilities.steer === true,
|
|
801
|
-
resume: descriptor.capabilities.resume === true,
|
|
802
|
-
approvalInteractive: descriptor.capabilities.approvalInteractive === true,
|
|
803
|
-
...descriptor.capabilities.mcpToolsets === void 0 ? {} : { mcpToolsets: descriptor.capabilities.mcpToolsets === true },
|
|
804
|
-
permissionModes: Object.freeze([...descriptor.capabilities.permissionModes])
|
|
805
|
-
}),
|
|
806
|
-
environmentRequirements: Object.freeze({
|
|
807
|
-
...baseNames === void 0 ? {} : { baseNames },
|
|
808
|
-
...credentialNames === void 0 ? {} : { credentialNames }
|
|
809
|
-
})
|
|
810
|
-
});
|
|
1306
|
+
var STORE_MUTEX_ID_PREFIX = "byok-store-mutex-v1:";
|
|
1307
|
+
var STORE_MUTEX_PROBE_TIMEOUT_MS = 1e3;
|
|
1308
|
+
var STORE_MUTEX_SOCKET_FILENAME = "mutex.sock";
|
|
1309
|
+
var UNIX_SOCKET_PATH_SOFT_LIMIT = 100;
|
|
1310
|
+
var STORE_MUTEX_FALLBACK_ROOT = "/tmp";
|
|
1311
|
+
function storeMutexIdentity(canonicalStoreDir) {
|
|
1312
|
+
return createHash("sha256").update(canonicalStoreDir).digest("hex");
|
|
811
1313
|
}
|
|
812
|
-
function
|
|
813
|
-
return
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
policy: frozenPolicy(manifest.policy),
|
|
818
|
-
requiredToolsetIds: Object.freeze([...manifest.requiredToolsetIds]),
|
|
819
|
-
...manifest.dispatchSelection === void 0 ? {} : { dispatchSelection: Object.freeze({ ...manifest.dispatchSelection }) },
|
|
820
|
-
...manifest.sessionRef === void 0 ? {} : { sessionRef: manifest.sessionRef },
|
|
821
|
-
...manifest.agentRef === void 0 ? {} : { agentRef: Object.freeze({ agentId: manifest.agentRef.agentId, profileRevision: manifest.agentRef.profileRevision }) },
|
|
822
|
-
cwd: manifest.cwd ?? manifest.workspace.workspaceDir,
|
|
823
|
-
...manifest.lease === void 0 ? {} : { lease: Object.freeze({ leaseId: manifest.lease.leaseId, canonicalHome: manifest.lease.canonicalHome }) },
|
|
824
|
-
workspace: Object.freeze({ ...manifest.workspace }),
|
|
825
|
-
forwardedEnvironmentNames: Object.freeze([...manifest.forwardedEnvironmentNames])
|
|
826
|
-
});
|
|
1314
|
+
function storeMutexEndpoint(canonicalStoreDir, identity, platform = process.platform) {
|
|
1315
|
+
if (platform === "win32") return `\\\\.\\pipe\\byok-store-mutex-${identity.slice(0, 16)}`;
|
|
1316
|
+
const candidate = path3__default.join(canonicalStoreDir, STORE_MUTEX_SOCKET_FILENAME);
|
|
1317
|
+
if (Buffer.byteLength(candidate, "utf8") <= UNIX_SOCKET_PATH_SOFT_LIMIT) return candidate;
|
|
1318
|
+
return path3__default.join(STORE_MUTEX_FALLBACK_ROOT, `byok-store-mutex-${identity.slice(0, 16)}`, "sock");
|
|
827
1319
|
}
|
|
828
|
-
var
|
|
829
|
-
constructor(
|
|
830
|
-
super(
|
|
831
|
-
this.
|
|
1320
|
+
var DaemonOwnerActiveError = class extends Error {
|
|
1321
|
+
constructor(role) {
|
|
1322
|
+
super(`store mutation lease is already held by an active ${role} process`);
|
|
1323
|
+
this.role = role;
|
|
1324
|
+
this.name = "DaemonOwnerActiveError";
|
|
832
1325
|
}
|
|
1326
|
+
role;
|
|
833
1327
|
};
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
1328
|
+
function isOwnerRecord(value) {
|
|
1329
|
+
if (typeof value !== "object" || value === null) return false;
|
|
1330
|
+
const candidate = value;
|
|
1331
|
+
return candidate.version === 2 && Number.isSafeInteger(candidate.pid) && (candidate.pid ?? 0) > 0 && typeof candidate.nonce === "string" && candidate.nonce.length === 36 && (candidate.role === "daemon" || candidate.role === "doctor") && typeof candidate.acquiredAt === "string" && Number.isFinite(Date.parse(candidate.acquiredAt)) && typeof candidate.processStartedAt === "string" && Number.isFinite(Date.parse(candidate.processStartedAt)) && Number.isSafeInteger(candidate.livenessPort) && (candidate.livenessPort ?? 0) > 0 && (candidate.livenessPort ?? 0) <= 65535;
|
|
1332
|
+
}
|
|
1333
|
+
async function readOwner(filePath) {
|
|
1334
|
+
let namedBefore;
|
|
1335
|
+
try {
|
|
1336
|
+
namedBefore = await promises.lstat(filePath, { bigint: true });
|
|
1337
|
+
} catch (err) {
|
|
1338
|
+
if (err.code === "ENOENT") return void 0;
|
|
1339
|
+
throw err;
|
|
841
1340
|
}
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
// src/release-identity.ts
|
|
845
|
-
var LOCAL_AGENT_RELEASE_VERSION_MAX_LENGTH = 128;
|
|
846
|
-
var LOCAL_AGENT_RELEASE_BUILD_ID_MAX_LENGTH = 128;
|
|
847
|
-
var STRICT_SEMVER_PATTERN = new RegExp(
|
|
848
|
-
"^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-(?:0|[1-9]\\d*|\\d*[A-Za-z-][0-9A-Za-z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[A-Za-z-][0-9A-Za-z-]*))*)?(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?$"
|
|
849
|
-
);
|
|
850
|
-
var BUILD_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]*$/;
|
|
851
|
-
function resolveLocalAgentReleaseIdentity(input) {
|
|
852
|
-
if (input === void 0 || typeof input.version !== "string" || input.version.length > LOCAL_AGENT_RELEASE_VERSION_MAX_LENGTH || !STRICT_SEMVER_PATTERN.test(input.version)) {
|
|
853
|
-
throw new Error("DaemonConfig.localAgentRelease.version must be canonical strict SemVer");
|
|
1341
|
+
if (!namedBefore.isFile() || namedBefore.isSymbolicLink()) {
|
|
1342
|
+
throw new Error("store mutation owner path is not a real regular file");
|
|
854
1343
|
}
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
1344
|
+
let handle;
|
|
1345
|
+
try {
|
|
1346
|
+
handle = await promises.open(
|
|
1347
|
+
filePath,
|
|
1348
|
+
constants.O_RDONLY | (constants.O_NONBLOCK ?? 0) | (constants.O_NOFOLLOW ?? 0)
|
|
858
1349
|
);
|
|
1350
|
+
} catch (err) {
|
|
1351
|
+
if (err.code === "ENOENT") return void 0;
|
|
1352
|
+
throw err;
|
|
859
1353
|
}
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
// src/runtime-failure.ts
|
|
867
|
-
var RUNTIME_EXECUTION_FAILURE_BRAND = /* @__PURE__ */ Symbol.for("@byok-sdk/client/RuntimeExecutionFailure/v1");
|
|
868
|
-
var RUNTIME_DISPOSAL_FAILURE_BRAND = /* @__PURE__ */ Symbol.for("@byok-sdk/client/RuntimeDisposalFailure/v1");
|
|
869
|
-
var RuntimeDisposalFailure = class extends Error {
|
|
870
|
-
stage;
|
|
871
|
-
constructor(input, options) {
|
|
872
|
-
if (!isRuntimeDisposalStage(input.stage) || typeof input.reason !== "string" || input.reason.length === 0) {
|
|
873
|
-
throw new TypeError("invalid RuntimeDisposalFailure input");
|
|
1354
|
+
try {
|
|
1355
|
+
const stat = await handle.stat({ bigint: true });
|
|
1356
|
+
const namedAfterOpen = await promises.lstat(filePath, { bigint: true });
|
|
1357
|
+
if (!stat.isFile() || !namedAfterOpen.isFile() || namedAfterOpen.isSymbolicLink() || stat.dev !== namedBefore.dev || stat.ino !== namedBefore.ino || stat.size !== namedBefore.size || stat.mtimeNs !== namedBefore.mtimeNs || stat.ctimeNs !== namedBefore.ctimeNs || stat.dev !== namedAfterOpen.dev || stat.ino !== namedAfterOpen.ino || stat.size !== namedAfterOpen.size || stat.mtimeNs !== namedAfterOpen.mtimeNs || stat.ctimeNs !== namedAfterOpen.ctimeNs) {
|
|
1358
|
+
throw new Error("store mutation owner path changed before safe open");
|
|
874
1359
|
}
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
return value === "signal" || value === "quiescence" || value === "cleanup";
|
|
884
|
-
}
|
|
885
|
-
function isRuntimeDisposalFailure(value) {
|
|
886
|
-
if (typeof value !== "object" || value === null) return false;
|
|
887
|
-
const candidate = value;
|
|
888
|
-
return candidate[RUNTIME_DISPOSAL_FAILURE_BRAND] === true && isRuntimeDisposalStage(candidate.stage) && typeof candidate.message === "string" && candidate.message.length > 0;
|
|
889
|
-
}
|
|
890
|
-
var RuntimeExecutionFailure = class extends Error {
|
|
891
|
-
phase;
|
|
892
|
-
category;
|
|
893
|
-
retry;
|
|
894
|
-
constructor(input, options) {
|
|
895
|
-
if (!isRuntimeFailurePhase(input.phase) || !isRuntimeFailureCategory(input.category) || !isRuntimeRetryDisposition(input.retry) || typeof input.reason !== "string" || input.reason.length === 0) {
|
|
896
|
-
throw new TypeError("invalid RuntimeExecutionFailure input");
|
|
1360
|
+
if (stat.size <= 0 || stat.size > MAX_OWNER_BYTES) return void 0;
|
|
1361
|
+
const size = Number(stat.size);
|
|
1362
|
+
const buffer = Buffer.alloc(size);
|
|
1363
|
+
const { bytesRead } = await handle.read(buffer, 0, size, 0);
|
|
1364
|
+
const after = await handle.stat({ bigint: true });
|
|
1365
|
+
const namedAfterRead = await promises.lstat(filePath, { bigint: true });
|
|
1366
|
+
if (bytesRead !== size || after.dev !== stat.dev || after.ino !== stat.ino || after.size !== stat.size || after.mtimeNs !== stat.mtimeNs || after.ctimeNs !== stat.ctimeNs || namedAfterRead.dev !== stat.dev || namedAfterRead.ino !== stat.ino || namedAfterRead.size !== stat.size || namedAfterRead.mtimeNs !== stat.mtimeNs || namedAfterRead.ctimeNs !== stat.ctimeNs) {
|
|
1367
|
+
throw new Error("store mutation owner path changed during inspection");
|
|
897
1368
|
}
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
1369
|
+
const raw = buffer.toString("utf8");
|
|
1370
|
+
const parsed = JSON.parse(raw);
|
|
1371
|
+
return isOwnerRecord(parsed) ? parsed : void 0;
|
|
1372
|
+
} catch (err) {
|
|
1373
|
+
if (err instanceof Error && err.message.startsWith("store mutation owner path changed")) throw err;
|
|
1374
|
+
return void 0;
|
|
1375
|
+
} finally {
|
|
1376
|
+
await handle.close();
|
|
905
1377
|
}
|
|
906
|
-
};
|
|
907
|
-
function isRuntimeFailurePhase(value) {
|
|
908
|
-
return value === "start" || value === "run";
|
|
909
|
-
}
|
|
910
|
-
function isRuntimeFailureCategory(value) {
|
|
911
|
-
return value === "semantic" || value === "infrastructure" || value === "authority";
|
|
912
1378
|
}
|
|
913
|
-
function
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
return candidate[RUNTIME_EXECUTION_FAILURE_BRAND] === true && isRuntimeFailurePhase(candidate.phase) && isRuntimeFailureCategory(candidate.category) && isRuntimeRetryDisposition(candidate.retry) && typeof candidate.message === "string" && candidate.message.length > 0;
|
|
920
|
-
}
|
|
921
|
-
function retryableFromDisposition(disposition) {
|
|
922
|
-
switch (disposition) {
|
|
923
|
-
case "retryable":
|
|
924
|
-
return true;
|
|
925
|
-
case "non-retryable":
|
|
926
|
-
return false;
|
|
1379
|
+
function pidIsAlive(pid) {
|
|
1380
|
+
try {
|
|
1381
|
+
process.kill(pid, 0);
|
|
1382
|
+
return true;
|
|
1383
|
+
} catch (err) {
|
|
1384
|
+
return err.code === "EPERM";
|
|
927
1385
|
}
|
|
928
1386
|
}
|
|
929
|
-
function
|
|
930
|
-
return
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
};
|
|
934
|
-
}
|
|
935
|
-
var RUNTIME_ADAPTER_CONTRACT_VIOLATION_REASON = Object.freeze({
|
|
936
|
-
start: "runtime adapter contract violation during start",
|
|
937
|
-
run: "runtime adapter contract violation during run"
|
|
938
|
-
});
|
|
939
|
-
function projectRuntimeBoundaryFailure(value, expectedPhase) {
|
|
940
|
-
if (isRuntimeExecutionFailure(value) && value.phase === expectedPhase) {
|
|
941
|
-
return { ...projectRuntimeExecutionFailure(value), contractViolation: false };
|
|
942
|
-
}
|
|
943
|
-
return {
|
|
944
|
-
reason: RUNTIME_ADAPTER_CONTRACT_VIOLATION_REASON[expectedPhase],
|
|
945
|
-
retryable: false,
|
|
946
|
-
contractViolation: true
|
|
947
|
-
};
|
|
1387
|
+
async function processOwnsRecord(record) {
|
|
1388
|
+
if (!pidIsAlive(record.pid)) return false;
|
|
1389
|
+
if (record.pid === process.pid && record.processStartedAt !== SELF_PROCESS_STARTED_AT) return false;
|
|
1390
|
+
return portIsBound(record.livenessPort);
|
|
948
1391
|
}
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
1392
|
+
async function portIsBound(port) {
|
|
1393
|
+
const probe = createServer();
|
|
1394
|
+
return new Promise((resolve, reject) => {
|
|
1395
|
+
const finish = (result) => {
|
|
1396
|
+
probe.removeAllListeners();
|
|
1397
|
+
resolve(result);
|
|
1398
|
+
};
|
|
1399
|
+
probe.once("error", (err) => {
|
|
1400
|
+
if (err.code === "EADDRINUSE") finish(true);
|
|
1401
|
+
else reject(err);
|
|
1402
|
+
});
|
|
1403
|
+
probe.listen({ host: "127.0.0.1", port, exclusive: true }, () => {
|
|
1404
|
+
probe.close((err) => {
|
|
1405
|
+
if (err) reject(err);
|
|
1406
|
+
else finish(false);
|
|
1407
|
+
});
|
|
1408
|
+
});
|
|
1409
|
+
});
|
|
1410
|
+
}
|
|
1411
|
+
async function createLivenessListener() {
|
|
1412
|
+
const server = createServer((socket) => endOwnershipProbe(socket));
|
|
1413
|
+
const port = await new Promise((resolve, reject) => {
|
|
1414
|
+
server.once("error", reject);
|
|
1415
|
+
server.listen({ host: "127.0.0.1", port: 0, exclusive: true }, () => {
|
|
1416
|
+
server.removeListener("error", reject);
|
|
1417
|
+
const address = server.address();
|
|
1418
|
+
if (!address || typeof address === "string") {
|
|
1419
|
+
reject(new Error("store mutation liveness listener did not expose a TCP port"));
|
|
1420
|
+
return;
|
|
1421
|
+
}
|
|
1422
|
+
resolve(address.port);
|
|
1423
|
+
});
|
|
1424
|
+
});
|
|
1425
|
+
server.unref();
|
|
1426
|
+
let closed = false;
|
|
1427
|
+
return {
|
|
1428
|
+
port,
|
|
1429
|
+
close: () => new Promise((resolve, reject) => {
|
|
1430
|
+
if (closed) {
|
|
1431
|
+
resolve();
|
|
1432
|
+
return;
|
|
1433
|
+
}
|
|
1434
|
+
closed = true;
|
|
1435
|
+
server.close((err) => err ? reject(err) : resolve());
|
|
1436
|
+
})
|
|
1437
|
+
};
|
|
1438
|
+
}
|
|
1439
|
+
async function probeStoreMutex(endpoint, identity) {
|
|
1440
|
+
return new Promise((resolve) => {
|
|
1441
|
+
const socket = createConnection(endpoint);
|
|
1442
|
+
let settled = false;
|
|
1443
|
+
let raw = "";
|
|
1444
|
+
const finish = (result) => {
|
|
1445
|
+
if (settled) return;
|
|
1446
|
+
settled = true;
|
|
1447
|
+
clearTimeout(timer);
|
|
1448
|
+
socket.removeAllListeners();
|
|
1449
|
+
socket.destroy();
|
|
1450
|
+
resolve(result);
|
|
1451
|
+
};
|
|
1452
|
+
const timer = setTimeout(() => finish({ kind: "occupied" }), STORE_MUTEX_PROBE_TIMEOUT_MS);
|
|
1453
|
+
socket.setEncoding("utf8");
|
|
1454
|
+
socket.on("data", (chunk) => {
|
|
1455
|
+
raw += chunk;
|
|
1456
|
+
if (raw.length > STORE_MUTEX_ID_PREFIX.length + 64 + 1) finish({ kind: "occupied" });
|
|
1457
|
+
});
|
|
1458
|
+
socket.once("end", () => finish(raw.trimEnd() === `${STORE_MUTEX_ID_PREFIX}${identity}` ? { kind: "holder" } : { kind: "occupied" }));
|
|
1459
|
+
socket.once(
|
|
1460
|
+
"error",
|
|
1461
|
+
(err) => finish(err.code === "ECONNREFUSED" || err.code === "ENOENT" ? { kind: "unbound" } : { kind: "occupied" })
|
|
1462
|
+
);
|
|
1463
|
+
});
|
|
1464
|
+
}
|
|
1465
|
+
async function clearStaleStoreMutexSocket(endpoint, identity) {
|
|
1466
|
+
let stat;
|
|
1467
|
+
try {
|
|
1468
|
+
stat = await promises.lstat(endpoint);
|
|
1469
|
+
} catch (err) {
|
|
1470
|
+
if (err.code === "ENOENT") return;
|
|
1471
|
+
throw err;
|
|
1472
|
+
}
|
|
1473
|
+
if (!stat.isSocket()) throw new Error("store mutation lock path exists but is not a socket");
|
|
1474
|
+
if ((await probeStoreMutex(endpoint, identity)).kind !== "unbound") throw new DaemonOwnerActiveError("unknown");
|
|
1475
|
+
await promises.rm(endpoint, { force: true });
|
|
1476
|
+
}
|
|
1477
|
+
async function assertOwnedPrivateDir(dir) {
|
|
1478
|
+
const uid = process.getuid?.();
|
|
1479
|
+
if (uid === void 0) return;
|
|
1480
|
+
const stat = await promises.lstat(dir);
|
|
1481
|
+
if (stat.isSymbolicLink() || stat.uid !== uid) {
|
|
1482
|
+
throw new Error(`refusing to bind the store mutation lock under "${dir}": not a real directory owned by this process's own uid`);
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
async function acquireStoreMutex(canonicalStoreDir) {
|
|
1486
|
+
const identity = storeMutexIdentity(canonicalStoreDir);
|
|
1487
|
+
const endpoint = storeMutexEndpoint(canonicalStoreDir, identity);
|
|
1488
|
+
const isPipe = process.platform === "win32";
|
|
1489
|
+
if (!isPipe) {
|
|
1490
|
+
const endpointDir = path3__default.dirname(endpoint);
|
|
1491
|
+
if (endpointDir !== canonicalStoreDir) {
|
|
1492
|
+
await ensureSecureDir(endpointDir);
|
|
1493
|
+
await assertOwnedPrivateDir(endpointDir);
|
|
1494
|
+
}
|
|
1495
|
+
await clearStaleStoreMutexSocket(endpoint, identity);
|
|
1496
|
+
}
|
|
1497
|
+
const server = createServer((socket) => endOwnershipProbe(socket, `${STORE_MUTEX_ID_PREFIX}${identity}
|
|
1498
|
+
`));
|
|
1499
|
+
try {
|
|
1500
|
+
await new Promise((resolve, reject) => {
|
|
1501
|
+
server.once("error", reject);
|
|
1502
|
+
server.listen(endpoint, () => {
|
|
1503
|
+
server.removeListener("error", reject);
|
|
1504
|
+
resolve();
|
|
1505
|
+
});
|
|
1506
|
+
});
|
|
1507
|
+
} catch (err) {
|
|
1508
|
+
if (err.code === "EADDRINUSE") throw new DaemonOwnerActiveError("unknown");
|
|
1509
|
+
throw err;
|
|
1510
|
+
}
|
|
1511
|
+
if (!isPipe) await promises.chmod(endpoint, 384).catch(() => void 0);
|
|
1512
|
+
server.unref();
|
|
1513
|
+
let closed = false;
|
|
1514
|
+
return {
|
|
1515
|
+
endpoint,
|
|
1516
|
+
close: async () => {
|
|
1517
|
+
if (closed) return;
|
|
1518
|
+
closed = true;
|
|
1519
|
+
await new Promise((resolve, reject) => server.close((err) => err ? reject(err) : resolve()));
|
|
1520
|
+
if (!isPipe) await promises.rm(endpoint, { force: true }).catch(() => void 0);
|
|
1521
|
+
}
|
|
1522
|
+
};
|
|
1523
|
+
}
|
|
1524
|
+
async function reclaimExistsAndIsActive(reclaimPath) {
|
|
1525
|
+
let stat;
|
|
1526
|
+
try {
|
|
1527
|
+
stat = await promises.lstat(reclaimPath);
|
|
1528
|
+
} catch (err) {
|
|
1529
|
+
if (err.code === "ENOENT") return false;
|
|
1530
|
+
throw err;
|
|
1531
|
+
}
|
|
1532
|
+
if (!stat.isFile()) throw new Error("store mutation reclaim marker is not a regular file");
|
|
1533
|
+
const owner = await readOwner(reclaimPath);
|
|
1534
|
+
if (owner) return processOwnsRecord(owner);
|
|
1535
|
+
return Date.now() - stat.mtimeMs <= RECLAIM_MALFORMED_GRACE_MS;
|
|
1536
|
+
}
|
|
1537
|
+
async function createOwner(filePath, record) {
|
|
1538
|
+
const tempPath = `${filePath}.${record.pid}.${record.nonce}.tmp`;
|
|
1539
|
+
let handle;
|
|
1540
|
+
try {
|
|
1541
|
+
handle = await promises.open(tempPath, "wx", 384);
|
|
1542
|
+
await handle.writeFile(`${JSON.stringify(record)}
|
|
1543
|
+
`, "utf8");
|
|
1544
|
+
await handle.chmod(384);
|
|
1545
|
+
await handle.sync();
|
|
1546
|
+
await handle.close();
|
|
1547
|
+
try {
|
|
1548
|
+
await promises.link(tempPath, filePath);
|
|
1549
|
+
return true;
|
|
1550
|
+
} catch (err) {
|
|
1551
|
+
if (err.code === "EEXIST") return false;
|
|
1552
|
+
throw err;
|
|
1553
|
+
}
|
|
1554
|
+
} finally {
|
|
1555
|
+
await handle?.close().catch(() => void 0);
|
|
1556
|
+
await promises.rm(tempPath, { force: true }).catch(() => void 0);
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
async function acquireDaemonOwner(storeDir, role, clock = () => /* @__PURE__ */ new Date()) {
|
|
1560
|
+
let pathGate;
|
|
1561
|
+
try {
|
|
1562
|
+
pathGate = await acquirePathMutationGate({ scope: "store", targetPath: storeDir }, { waitMs: 1e3 });
|
|
1563
|
+
} catch (error) {
|
|
1564
|
+
if (error instanceof PathMutationGateBusyError) throw new DaemonOwnerActiveError("unknown");
|
|
1565
|
+
throw error;
|
|
1566
|
+
}
|
|
1567
|
+
let mutex;
|
|
1568
|
+
let liveness;
|
|
1569
|
+
try {
|
|
1570
|
+
await ensureSecureDir(storeDir);
|
|
1571
|
+
const canonicalStoreDir = await promises.realpath(storeDir);
|
|
1572
|
+
mutex = await acquireStoreMutex(canonicalStoreDir);
|
|
1573
|
+
liveness = await createLivenessListener();
|
|
1574
|
+
const ownerPath = path3__default.join(canonicalStoreDir, DAEMON_OWNER_FILENAME);
|
|
1575
|
+
const reclaimPath = path3__default.join(canonicalStoreDir, RECLAIM_FILENAME);
|
|
1576
|
+
const record = {
|
|
1577
|
+
version: 2,
|
|
1578
|
+
pid: process.pid,
|
|
1579
|
+
nonce: randomUUID(),
|
|
1580
|
+
role,
|
|
1581
|
+
acquiredAt: clock().toISOString(),
|
|
1582
|
+
processStartedAt: SELF_PROCESS_STARTED_AT,
|
|
1583
|
+
livenessPort: liveness.port
|
|
1584
|
+
};
|
|
1585
|
+
for (; ; ) {
|
|
1586
|
+
if (await reclaimExistsAndIsActive(reclaimPath)) {
|
|
1587
|
+
throw new Error("store mutation lease is being reclaimed; retry after the current operation finishes");
|
|
1588
|
+
}
|
|
1589
|
+
await promises.rm(reclaimPath, { force: true });
|
|
1590
|
+
if (await createOwner(ownerPath, record)) {
|
|
1591
|
+
try {
|
|
1592
|
+
await pathGate.release();
|
|
1593
|
+
pathGate = void 0;
|
|
1594
|
+
} catch (error) {
|
|
1595
|
+
await promises.rm(ownerPath, { force: true }).catch(() => void 0);
|
|
1596
|
+
throw error;
|
|
1597
|
+
}
|
|
1598
|
+
const ownedLiveness = liveness;
|
|
1599
|
+
const ownedMutex = mutex;
|
|
1600
|
+
let released = false;
|
|
1601
|
+
return {
|
|
1602
|
+
release: async () => {
|
|
1603
|
+
if (released) return;
|
|
1604
|
+
const releaseGate = await acquirePathMutationGate({
|
|
1605
|
+
scope: "store",
|
|
1606
|
+
targetPath: canonicalStoreDir
|
|
1607
|
+
}, { waitMs: 2e3 });
|
|
1608
|
+
try {
|
|
1609
|
+
const current = await readOwner(ownerPath);
|
|
1610
|
+
if (current?.nonce !== record.nonce) {
|
|
1611
|
+
throw new Error("store mutation lease identity changed before release");
|
|
1612
|
+
}
|
|
1613
|
+
await ownedLiveness.close();
|
|
1614
|
+
await ownedMutex.close();
|
|
1615
|
+
await promises.rm(ownerPath);
|
|
1616
|
+
released = true;
|
|
1617
|
+
} finally {
|
|
1618
|
+
await releaseGate.release();
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
};
|
|
1622
|
+
}
|
|
1623
|
+
const existing = await readOwner(ownerPath);
|
|
1624
|
+
if (existing && await processOwnsRecord(existing)) throw new DaemonOwnerActiveError(existing.role);
|
|
1625
|
+
if (!await createOwner(reclaimPath, record)) {
|
|
1626
|
+
throw new Error("store mutation lease is being reclaimed; retry after the current operation finishes");
|
|
1627
|
+
}
|
|
1628
|
+
try {
|
|
1629
|
+
const rechecked = await readOwner(ownerPath);
|
|
1630
|
+
if (rechecked && await processOwnsRecord(rechecked)) throw new DaemonOwnerActiveError(rechecked.role);
|
|
1631
|
+
if (rechecked || await promises.stat(ownerPath).then(() => true, (err) => {
|
|
1632
|
+
if (err.code === "ENOENT") return false;
|
|
1633
|
+
throw err;
|
|
1634
|
+
})) {
|
|
1635
|
+
await promises.rm(ownerPath);
|
|
1636
|
+
}
|
|
1637
|
+
} finally {
|
|
1638
|
+
const currentReclaim = await readOwner(reclaimPath);
|
|
1639
|
+
if (currentReclaim?.nonce === record.nonce) await promises.rm(reclaimPath, { force: true });
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
} catch (err) {
|
|
1643
|
+
await liveness?.close().catch(() => void 0);
|
|
1644
|
+
await mutex?.close().catch(() => void 0);
|
|
1645
|
+
await pathGate?.release().catch(() => void 0);
|
|
1646
|
+
throw err;
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
// src/types.ts
|
|
1651
|
+
function frozenStrings(values) {
|
|
1652
|
+
return values === void 0 ? void 0 : Object.freeze([...values]);
|
|
1653
|
+
}
|
|
1654
|
+
function frozenPolicy(policy) {
|
|
1655
|
+
const allowTools = policy.allowTools === void 0 ? void 0 : Object.freeze([...policy.allowTools]);
|
|
1656
|
+
const denyTools = policy.denyTools === void 0 ? void 0 : Object.freeze([...policy.denyTools]);
|
|
1657
|
+
return Object.freeze({
|
|
1658
|
+
mode: policy.mode,
|
|
1659
|
+
...allowTools === void 0 ? {} : { allowTools },
|
|
1660
|
+
...denyTools === void 0 ? {} : { denyTools },
|
|
1661
|
+
...policy.workspaceRoot === void 0 ? {} : { workspaceRoot: policy.workspaceRoot },
|
|
1662
|
+
...policy.network === void 0 ? {} : { network: policy.network }
|
|
1663
|
+
});
|
|
1664
|
+
}
|
|
1665
|
+
function freezeRuntimeAdapterDescriptor(descriptor) {
|
|
1666
|
+
const baseNames = frozenStrings(descriptor.environmentRequirements.baseNames);
|
|
1667
|
+
const credentialNames = frozenStrings(descriptor.environmentRequirements.credentialNames);
|
|
1668
|
+
return Object.freeze({
|
|
1669
|
+
id: descriptor.id,
|
|
1670
|
+
supportsDispatchSelection: descriptor.supportsDispatchSelection === true,
|
|
1671
|
+
capabilities: Object.freeze({
|
|
1672
|
+
steer: descriptor.capabilities.steer === true,
|
|
1673
|
+
resume: descriptor.capabilities.resume === true,
|
|
1674
|
+
approvalInteractive: descriptor.capabilities.approvalInteractive === true,
|
|
1675
|
+
...descriptor.capabilities.mcpToolsets === void 0 ? {} : { mcpToolsets: descriptor.capabilities.mcpToolsets === true },
|
|
1676
|
+
permissionModes: Object.freeze([...descriptor.capabilities.permissionModes])
|
|
1677
|
+
}),
|
|
1678
|
+
environmentRequirements: Object.freeze({
|
|
1679
|
+
...baseNames === void 0 ? {} : { baseNames },
|
|
1680
|
+
...credentialNames === void 0 ? {} : { credentialNames }
|
|
1681
|
+
})
|
|
1682
|
+
});
|
|
1683
|
+
}
|
|
1684
|
+
function sealRuntimeOperationManifest(manifest) {
|
|
1685
|
+
return Object.freeze({
|
|
1686
|
+
taskId: manifest.taskId,
|
|
1687
|
+
runtimeId: manifest.runtimeId,
|
|
1688
|
+
descriptor: freezeRuntimeAdapterDescriptor(manifest.descriptor),
|
|
1689
|
+
policy: frozenPolicy(manifest.policy),
|
|
1690
|
+
requiredToolsetIds: Object.freeze([...manifest.requiredToolsetIds]),
|
|
1691
|
+
...manifest.dispatchSelection === void 0 ? {} : { dispatchSelection: Object.freeze({ ...manifest.dispatchSelection }) },
|
|
1692
|
+
...manifest.sessionRef === void 0 ? {} : { sessionRef: manifest.sessionRef },
|
|
1693
|
+
...manifest.agentRef === void 0 ? {} : { agentRef: Object.freeze({ agentId: manifest.agentRef.agentId, profileRevision: manifest.agentRef.profileRevision }) },
|
|
1694
|
+
cwd: manifest.cwd ?? manifest.workspace.workspaceDir,
|
|
1695
|
+
...manifest.lease === void 0 ? {} : { lease: Object.freeze({ leaseId: manifest.lease.leaseId, canonicalHome: manifest.lease.canonicalHome }) },
|
|
1696
|
+
workspace: Object.freeze({ ...manifest.workspace }),
|
|
1697
|
+
forwardedEnvironmentNames: Object.freeze([...manifest.forwardedEnvironmentNames])
|
|
1698
|
+
});
|
|
1699
|
+
}
|
|
1700
|
+
var PolicyUnsupportedError = class extends Error {
|
|
1701
|
+
constructor(message) {
|
|
1702
|
+
super(message);
|
|
1703
|
+
this.name = "PolicyUnsupportedError";
|
|
1704
|
+
}
|
|
1705
|
+
};
|
|
1706
|
+
var SteerUnsupportedError = class extends Error {
|
|
1707
|
+
/** The `RuntimeAdapter.descriptor.id` that cannot steer (e.g. `claude`, `codex`). */
|
|
1708
|
+
runtimeId;
|
|
1709
|
+
constructor(runtimeId, message) {
|
|
1710
|
+
super(message);
|
|
1711
|
+
this.name = "SteerUnsupportedError";
|
|
1712
|
+
this.runtimeId = runtimeId;
|
|
1713
|
+
}
|
|
1714
|
+
};
|
|
1715
|
+
|
|
1716
|
+
// src/release-identity.ts
|
|
1717
|
+
var LOCAL_AGENT_RELEASE_VERSION_MAX_LENGTH = 128;
|
|
1718
|
+
var LOCAL_AGENT_RELEASE_BUILD_ID_MAX_LENGTH = 128;
|
|
1719
|
+
var STRICT_SEMVER_PATTERN = new RegExp(
|
|
1720
|
+
"^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-(?:0|[1-9]\\d*|\\d*[A-Za-z-][0-9A-Za-z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[A-Za-z-][0-9A-Za-z-]*))*)?(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?$"
|
|
1721
|
+
);
|
|
1722
|
+
var BUILD_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]*$/;
|
|
1723
|
+
function resolveLocalAgentReleaseIdentity(input) {
|
|
1724
|
+
if (input === void 0 || typeof input.version !== "string" || input.version.length > LOCAL_AGENT_RELEASE_VERSION_MAX_LENGTH || !STRICT_SEMVER_PATTERN.test(input.version)) {
|
|
1725
|
+
throw new Error("DaemonConfig.localAgentRelease.version must be canonical strict SemVer");
|
|
1726
|
+
}
|
|
1727
|
+
if (input.buildId !== void 0 && (typeof input.buildId !== "string" || input.buildId.length > LOCAL_AGENT_RELEASE_BUILD_ID_MAX_LENGTH || !BUILD_ID_PATTERN.test(input.buildId))) {
|
|
1728
|
+
throw new Error(
|
|
1729
|
+
`DaemonConfig.localAgentRelease.buildId must be 1-${LOCAL_AGENT_RELEASE_BUILD_ID_MAX_LENGTH} safe opaque characters`
|
|
1730
|
+
);
|
|
1731
|
+
}
|
|
1732
|
+
return Object.freeze({
|
|
1733
|
+
version: input.version,
|
|
1734
|
+
...input.buildId === void 0 ? {} : { buildId: input.buildId }
|
|
1735
|
+
});
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
// src/runtime-failure.ts
|
|
1739
|
+
var RUNTIME_EXECUTION_FAILURE_BRAND = /* @__PURE__ */ Symbol.for("@byok-sdk/client/RuntimeExecutionFailure/v1");
|
|
1740
|
+
var RUNTIME_DISPOSAL_FAILURE_BRAND = /* @__PURE__ */ Symbol.for("@byok-sdk/client/RuntimeDisposalFailure/v1");
|
|
1741
|
+
var RuntimeDisposalFailure = class extends Error {
|
|
1742
|
+
stage;
|
|
1743
|
+
constructor(input, options) {
|
|
1744
|
+
if (!isRuntimeDisposalStage(input.stage) || typeof input.reason !== "string" || input.reason.length === 0) {
|
|
1745
|
+
throw new TypeError("invalid RuntimeDisposalFailure input");
|
|
1746
|
+
}
|
|
1747
|
+
super(input.reason, options);
|
|
1748
|
+
this.name = "RuntimeDisposalFailure";
|
|
1749
|
+
this.stage = input.stage;
|
|
1750
|
+
Object.defineProperty(this, RUNTIME_DISPOSAL_FAILURE_BRAND, { value: true });
|
|
1751
|
+
Object.freeze(this);
|
|
1752
|
+
}
|
|
1753
|
+
};
|
|
1754
|
+
function isRuntimeDisposalStage(value) {
|
|
1755
|
+
return value === "signal" || value === "quiescence" || value === "cleanup";
|
|
1756
|
+
}
|
|
1757
|
+
function isRuntimeDisposalFailure(value) {
|
|
1758
|
+
if (typeof value !== "object" || value === null) return false;
|
|
1759
|
+
const candidate = value;
|
|
1760
|
+
return candidate[RUNTIME_DISPOSAL_FAILURE_BRAND] === true && isRuntimeDisposalStage(candidate.stage) && typeof candidate.message === "string" && candidate.message.length > 0;
|
|
1761
|
+
}
|
|
1762
|
+
var RuntimeExecutionFailure = class extends Error {
|
|
1763
|
+
phase;
|
|
1764
|
+
category;
|
|
1765
|
+
retry;
|
|
1766
|
+
constructor(input, options) {
|
|
1767
|
+
if (!isRuntimeFailurePhase(input.phase) || !isRuntimeFailureCategory(input.category) || !isRuntimeRetryDisposition(input.retry) || typeof input.reason !== "string" || input.reason.length === 0) {
|
|
1768
|
+
throw new TypeError("invalid RuntimeExecutionFailure input");
|
|
1769
|
+
}
|
|
1770
|
+
super(input.reason, options);
|
|
1771
|
+
this.name = "RuntimeExecutionFailure";
|
|
1772
|
+
this.phase = input.phase;
|
|
1773
|
+
this.category = input.category;
|
|
1774
|
+
this.retry = input.retry;
|
|
1775
|
+
Object.defineProperty(this, RUNTIME_EXECUTION_FAILURE_BRAND, { value: true });
|
|
1776
|
+
Object.freeze(this);
|
|
1777
|
+
}
|
|
1778
|
+
};
|
|
1779
|
+
function isRuntimeFailurePhase(value) {
|
|
1780
|
+
return value === "start" || value === "run";
|
|
1781
|
+
}
|
|
1782
|
+
function isRuntimeFailureCategory(value) {
|
|
1783
|
+
return value === "semantic" || value === "infrastructure" || value === "authority";
|
|
1784
|
+
}
|
|
1785
|
+
function isRuntimeRetryDisposition(value) {
|
|
1786
|
+
return value === "retryable" || value === "non-retryable";
|
|
1787
|
+
}
|
|
1788
|
+
function isRuntimeExecutionFailure(value) {
|
|
1789
|
+
if (typeof value !== "object" || value === null) return false;
|
|
1790
|
+
const candidate = value;
|
|
1791
|
+
return candidate[RUNTIME_EXECUTION_FAILURE_BRAND] === true && isRuntimeFailurePhase(candidate.phase) && isRuntimeFailureCategory(candidate.category) && isRuntimeRetryDisposition(candidate.retry) && typeof candidate.message === "string" && candidate.message.length > 0;
|
|
1792
|
+
}
|
|
1793
|
+
function retryableFromDisposition(disposition) {
|
|
1794
|
+
switch (disposition) {
|
|
1795
|
+
case "retryable":
|
|
1796
|
+
return true;
|
|
1797
|
+
case "non-retryable":
|
|
1798
|
+
return false;
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
function projectRuntimeExecutionFailure(failure) {
|
|
1802
|
+
return {
|
|
1803
|
+
reason: failure.message,
|
|
1804
|
+
retryable: retryableFromDisposition(failure.retry)
|
|
1805
|
+
};
|
|
1806
|
+
}
|
|
1807
|
+
var RUNTIME_ADAPTER_CONTRACT_VIOLATION_REASON = Object.freeze({
|
|
1808
|
+
start: "runtime adapter contract violation during start",
|
|
1809
|
+
run: "runtime adapter contract violation during run"
|
|
1810
|
+
});
|
|
1811
|
+
function projectRuntimeBoundaryFailure(value, expectedPhase) {
|
|
1812
|
+
if (isRuntimeExecutionFailure(value) && value.phase === expectedPhase) {
|
|
1813
|
+
return { ...projectRuntimeExecutionFailure(value), contractViolation: false };
|
|
1814
|
+
}
|
|
1815
|
+
return {
|
|
1816
|
+
reason: RUNTIME_ADAPTER_CONTRACT_VIOLATION_REASON[expectedPhase],
|
|
1817
|
+
retryable: false,
|
|
1818
|
+
contractViolation: true
|
|
1819
|
+
};
|
|
1820
|
+
}
|
|
1821
|
+
var GIT_ERROR_CATEGORIES = [
|
|
1822
|
+
"git-unavailable",
|
|
1823
|
+
"git-timeout",
|
|
1824
|
+
"git-output-limit",
|
|
1825
|
+
"git-command-failed",
|
|
954
1826
|
"workspace-root-invalid",
|
|
955
1827
|
"workspace-root-conflict",
|
|
956
1828
|
"workspace-not-owned",
|
|
@@ -1011,7 +1883,7 @@ function gitEnvironment(readOnly) {
|
|
|
1011
1883
|
return env;
|
|
1012
1884
|
}
|
|
1013
1885
|
function stableGitWorkspaceOwnerId(storeDir, productId) {
|
|
1014
|
-
const identity = `${
|
|
1886
|
+
const identity = `${path3__default.resolve(storeDir)}\\0${productId}`;
|
|
1015
1887
|
return `store-product:${createHash("sha256").update(identity).digest("hex")}`;
|
|
1016
1888
|
}
|
|
1017
1889
|
var GUIDANCE = [
|
|
@@ -1023,11 +1895,11 @@ var GUIDANCE = [
|
|
|
1023
1895
|
"Leave incomplete work visible for recovery."
|
|
1024
1896
|
].join("\n");
|
|
1025
1897
|
function canonical(value) {
|
|
1026
|
-
return
|
|
1898
|
+
return path3__default.resolve(value);
|
|
1027
1899
|
}
|
|
1028
1900
|
function isContained(root, candidate) {
|
|
1029
|
-
const relative =
|
|
1030
|
-
return relative === "" || !relative.startsWith(`..${
|
|
1901
|
+
const relative = path3__default.relative(root, candidate);
|
|
1902
|
+
return relative === "" || !relative.startsWith(`..${path3__default.sep}`) && !path3__default.isAbsolute(relative);
|
|
1031
1903
|
}
|
|
1032
1904
|
function bounded(value, max) {
|
|
1033
1905
|
return Buffer.byteLength(value, "utf8") <= max ? value : value.slice(0, max);
|
|
@@ -1132,7 +2004,7 @@ var GitWorkspaceManager = class {
|
|
|
1132
2004
|
await this.ensureOwnerMarker();
|
|
1133
2005
|
}
|
|
1134
2006
|
async ensureOwnerMarker() {
|
|
1135
|
-
const markerPath =
|
|
2007
|
+
const markerPath = path3__default.join(this.workspaceRoot, OWNER_MARKER);
|
|
1136
2008
|
let existing;
|
|
1137
2009
|
try {
|
|
1138
2010
|
existing = JSON.parse(await promises.readFile(markerPath, "utf8"));
|
|
@@ -1226,270 +2098,96 @@ var GitWorkspaceManager = class {
|
|
|
1226
2098
|
}
|
|
1227
2099
|
};
|
|
1228
2100
|
workspaceLeases.set(root, lease);
|
|
1229
|
-
if (sessionRef) sessionLeases.set(sessionRef, lease);
|
|
1230
|
-
return lease;
|
|
1231
|
-
}
|
|
1232
|
-
static guidance() {
|
|
1233
|
-
return GUIDANCE;
|
|
1234
|
-
}
|
|
1235
|
-
static prependGuidance(instruction) {
|
|
1236
|
-
return `${GUIDANCE}
|
|
1237
|
-
|
|
1238
|
-
${instruction}`;
|
|
1239
|
-
}
|
|
1240
|
-
commandOptions(cwd, readOnly = false) {
|
|
1241
|
-
return {
|
|
1242
|
-
cwd,
|
|
1243
|
-
timeout: this.timeoutMs,
|
|
1244
|
-
maxBuffer: this.maxOutputBytes,
|
|
1245
|
-
env: gitEnvironment(readOnly)
|
|
1246
|
-
};
|
|
1247
|
-
}
|
|
1248
|
-
async read(args, cwd) {
|
|
1249
|
-
try {
|
|
1250
|
-
return await this.run(args, this.commandOptions(cwd, true));
|
|
1251
|
-
} catch (error) {
|
|
1252
|
-
throw asGitError(error, "git-command-failed", "Git observation failed");
|
|
1253
|
-
}
|
|
1254
|
-
}
|
|
1255
|
-
async readTopLevel(root) {
|
|
1256
|
-
const result = await this.read(["rev-parse", "--show-toplevel"], root);
|
|
1257
|
-
if (result.code !== 0) throw new GitWorkspaceError("repository-invalid", "workspace is not a Git repository");
|
|
1258
|
-
return canonical(result.stdout.trim());
|
|
1259
|
-
}
|
|
1260
|
-
async assertTaskRoot(workspaceDir) {
|
|
1261
|
-
const candidate = canonical(workspaceDir);
|
|
1262
|
-
const realWorkspaceRoot = await promises.realpath(this.workspaceRoot).catch(() => {
|
|
1263
|
-
throw new GitWorkspaceError("workspace-root-invalid", "workspace root is unavailable");
|
|
1264
|
-
});
|
|
1265
|
-
if (!isContained(this.workspaceRoot, candidate) && !isContained(realWorkspaceRoot, candidate)) {
|
|
1266
|
-
throw new GitWorkspaceError("workspace-root-invalid", "workspace directory is invalid");
|
|
1267
|
-
}
|
|
1268
|
-
await this.assertExistingAncestry(candidate, realWorkspaceRoot);
|
|
1269
|
-
await promises.mkdir(candidate, { recursive: true, mode: 448 }).catch(() => {
|
|
1270
|
-
throw new GitWorkspaceError("workspace-root-invalid", "workspace directory is unavailable");
|
|
1271
|
-
});
|
|
1272
|
-
const realCandidate = await promises.realpath(candidate).catch(() => {
|
|
1273
|
-
throw new GitWorkspaceError("workspace-root-invalid", "workspace directory is unavailable");
|
|
1274
|
-
});
|
|
1275
|
-
if (!isContained(realWorkspaceRoot, realCandidate)) {
|
|
1276
|
-
throw new GitWorkspaceError("workspace-root-invalid", "workspace directory is invalid");
|
|
1277
|
-
}
|
|
1278
|
-
return realCandidate;
|
|
1279
|
-
}
|
|
1280
|
-
async assertExistingAncestry(candidate, realWorkspaceRoot) {
|
|
1281
|
-
let current = candidate;
|
|
1282
|
-
while (true) {
|
|
1283
|
-
try {
|
|
1284
|
-
const realCurrent = await promises.realpath(current);
|
|
1285
|
-
if (!isContained(realWorkspaceRoot, realCurrent)) {
|
|
1286
|
-
throw new GitWorkspaceError("workspace-root-invalid", "workspace directory is invalid");
|
|
1287
|
-
}
|
|
1288
|
-
return;
|
|
1289
|
-
} catch (error) {
|
|
1290
|
-
const code = error.code;
|
|
1291
|
-
if (error instanceof GitWorkspaceError || code !== "ENOENT" && code !== "ENOTDIR") {
|
|
1292
|
-
throw new GitWorkspaceError("workspace-root-invalid", "workspace directory is invalid");
|
|
1293
|
-
}
|
|
1294
|
-
const parent = path.dirname(current);
|
|
1295
|
-
if (parent === current) {
|
|
1296
|
-
throw new GitWorkspaceError("workspace-root-invalid", "workspace directory is invalid");
|
|
1297
|
-
}
|
|
1298
|
-
current = parent;
|
|
1299
|
-
}
|
|
1300
|
-
}
|
|
1301
|
-
}
|
|
1302
|
-
async assertExistingTaskRoot(workspaceDir) {
|
|
1303
|
-
const candidate = canonical(workspaceDir);
|
|
1304
|
-
const realWorkspaceRoot = await promises.realpath(this.workspaceRoot).catch(() => {
|
|
1305
|
-
throw new GitWorkspaceError("workspace-root-invalid", "workspace root is unavailable");
|
|
1306
|
-
});
|
|
1307
|
-
const realCandidate = await promises.realpath(candidate).catch(() => {
|
|
1308
|
-
throw new GitWorkspaceError("repository-invalid", "workspace directory is unavailable");
|
|
1309
|
-
});
|
|
1310
|
-
if (!isContained(realWorkspaceRoot, realCandidate)) {
|
|
1311
|
-
throw new GitWorkspaceError("workspace-root-invalid", "workspace directory is invalid");
|
|
1312
|
-
}
|
|
1313
|
-
return realCandidate;
|
|
1314
|
-
}
|
|
1315
|
-
};
|
|
1316
|
-
function prependGitWorkspaceGuidance(instruction) {
|
|
1317
|
-
return GitWorkspaceManager.prependGuidance(instruction);
|
|
1318
|
-
}
|
|
1319
|
-
var tmpSeq = 0;
|
|
1320
|
-
async function atomicWriteFile(filePath, data, options = {}) {
|
|
1321
|
-
const tmpPath = `${filePath}.${process.pid}-${tmpSeq++}.tmp`;
|
|
1322
|
-
try {
|
|
1323
|
-
const handle = await promises.open(tmpPath, "w", options.mode);
|
|
1324
|
-
try {
|
|
1325
|
-
await handle.writeFile(data);
|
|
1326
|
-
if (options.mode !== void 0) {
|
|
1327
|
-
await handle.chmod(options.mode);
|
|
1328
|
-
}
|
|
1329
|
-
if (options.fsync) {
|
|
1330
|
-
await handle.sync();
|
|
1331
|
-
}
|
|
1332
|
-
} finally {
|
|
1333
|
-
await handle.close();
|
|
1334
|
-
}
|
|
1335
|
-
} catch (err) {
|
|
1336
|
-
await promises.rm(tmpPath, { force: true }).catch(() => {
|
|
1337
|
-
});
|
|
1338
|
-
throw err;
|
|
1339
|
-
}
|
|
1340
|
-
await renameOnto(tmpPath, filePath);
|
|
1341
|
-
if (options.mode !== void 0) {
|
|
1342
|
-
await promises.chmod(filePath, options.mode);
|
|
1343
|
-
}
|
|
1344
|
-
if (options.fsync) {
|
|
1345
|
-
const target = await promises.open(filePath, process.platform === "win32" ? "r+" : "r");
|
|
1346
|
-
try {
|
|
1347
|
-
await target.sync();
|
|
1348
|
-
} finally {
|
|
1349
|
-
await target.close();
|
|
1350
|
-
}
|
|
1351
|
-
if (process.platform !== "win32") {
|
|
1352
|
-
const directory = await promises.open(path.dirname(filePath), "r");
|
|
1353
|
-
try {
|
|
1354
|
-
await directory.sync();
|
|
1355
|
-
} finally {
|
|
1356
|
-
await directory.close();
|
|
1357
|
-
}
|
|
1358
|
-
}
|
|
1359
|
-
}
|
|
1360
|
-
}
|
|
1361
|
-
var RENAME_RETRY_ATTEMPTS = 5;
|
|
1362
|
-
var RENAME_RETRY_DELAY_MS = 20;
|
|
1363
|
-
function delay(ms) {
|
|
1364
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1365
|
-
}
|
|
1366
|
-
async function renameOnto(tmpPath, targetPath) {
|
|
1367
|
-
for (let attempt = 1; attempt <= RENAME_RETRY_ATTEMPTS; attempt++) {
|
|
1368
|
-
try {
|
|
1369
|
-
await promises.rename(tmpPath, targetPath);
|
|
1370
|
-
return;
|
|
1371
|
-
} catch (err) {
|
|
1372
|
-
const code = err.code;
|
|
1373
|
-
if (code !== "EPERM" && code !== "EEXIST") {
|
|
1374
|
-
await promises.rm(tmpPath, { force: true }).catch(() => {
|
|
1375
|
-
});
|
|
1376
|
-
throw err;
|
|
1377
|
-
}
|
|
1378
|
-
if (attempt === RENAME_RETRY_ATTEMPTS) {
|
|
1379
|
-
await promises.rm(tmpPath, { force: true }).catch(() => {
|
|
1380
|
-
});
|
|
1381
|
-
throw err;
|
|
1382
|
-
}
|
|
1383
|
-
await delay(RENAME_RETRY_DELAY_MS * attempt);
|
|
1384
|
-
}
|
|
1385
|
-
}
|
|
1386
|
-
}
|
|
1387
|
-
var defaultRunner = (command, args) => new Promise((resolve, reject) => {
|
|
1388
|
-
execFile(command, args, (error, stdout, stderr) => {
|
|
1389
|
-
if (error && typeof error.code !== "number") {
|
|
1390
|
-
reject(error);
|
|
1391
|
-
return;
|
|
1392
|
-
}
|
|
1393
|
-
resolve({ code: error ? error.code : 0, stdout, stderr });
|
|
1394
|
-
});
|
|
1395
|
-
});
|
|
1396
|
-
async function runOrThrow(run, command, args, label) {
|
|
1397
|
-
const result = await run(command, args);
|
|
1398
|
-
if (result.code !== 0) {
|
|
1399
|
-
const detail = (result.stderr || result.stdout).trim();
|
|
1400
|
-
throw new Error(`${label} failed (exit ${result.code})${detail ? `: ${detail}` : ""}`);
|
|
1401
|
-
}
|
|
1402
|
-
return result;
|
|
1403
|
-
}
|
|
1404
|
-
function isIdempotentAbsence(result, absence) {
|
|
1405
|
-
if (result.code === 0) return true;
|
|
1406
|
-
const detail = `${result.stdout}
|
|
1407
|
-
${result.stderr}`;
|
|
1408
|
-
if (absence.neverAbsence?.some((pattern) => pattern.test(detail))) return false;
|
|
1409
|
-
if (absence.codes?.includes(result.code)) return true;
|
|
1410
|
-
return absence.patterns.some((pattern) => pattern.test(detail));
|
|
1411
|
-
}
|
|
1412
|
-
async function runIdempotent(run, command, args, label, absence) {
|
|
1413
|
-
const result = await run(command, args);
|
|
1414
|
-
if (!isIdempotentAbsence(result, absence)) {
|
|
1415
|
-
const detail = (result.stderr || result.stdout).trim();
|
|
1416
|
-
throw new Error(`${label} failed (exit ${result.code})${detail ? `: ${detail}` : ""}`);
|
|
2101
|
+
if (sessionRef) sessionLeases.set(sessionRef, lease);
|
|
2102
|
+
return lease;
|
|
1417
2103
|
}
|
|
1418
|
-
|
|
1419
|
-
|
|
2104
|
+
static guidance() {
|
|
2105
|
+
return GUIDANCE;
|
|
2106
|
+
}
|
|
2107
|
+
static prependGuidance(instruction) {
|
|
2108
|
+
return `${GUIDANCE}
|
|
1420
2109
|
|
|
1421
|
-
|
|
1422
|
-
var SYSTEM_SID = "*S-1-5-18";
|
|
1423
|
-
var ADMINISTRATORS_SID = "*S-1-5-32-544";
|
|
1424
|
-
function buildIcaclsArgs(dir, username) {
|
|
1425
|
-
return [dir, "/inheritance:r", "/grant:r", `${username}:(OI)(CI)F`, "/grant", `${SYSTEM_SID}:(OI)(CI)F`, "/grant", `${ADMINISTRATORS_SID}:(OI)(CI)F`];
|
|
1426
|
-
}
|
|
1427
|
-
function buildIcaclsFileArgs(filePath, username) {
|
|
1428
|
-
return [
|
|
1429
|
-
filePath,
|
|
1430
|
-
"/inheritance:r",
|
|
1431
|
-
"/grant:r",
|
|
1432
|
-
`${username}:F`,
|
|
1433
|
-
"/grant",
|
|
1434
|
-
`${SYSTEM_SID}:F`,
|
|
1435
|
-
"/grant",
|
|
1436
|
-
`${ADMINISTRATORS_SID}:F`
|
|
1437
|
-
];
|
|
1438
|
-
}
|
|
1439
|
-
var SecureDirHardeningError = class extends Error {
|
|
1440
|
-
constructor(dir, reason) {
|
|
1441
|
-
super(
|
|
1442
|
-
`failed to apply a restrictive Windows ACL to "${dir}": ${reason} \u2014 refusing to leave this directory unprotected (it holds device credentials and/or the control-socket token, otherwise readable by any other local user); see docs/security.md`
|
|
1443
|
-
);
|
|
1444
|
-
this.dir = dir;
|
|
1445
|
-
this.name = "SecureDirHardeningError";
|
|
2110
|
+
${instruction}`;
|
|
1446
2111
|
}
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
2112
|
+
commandOptions(cwd, readOnly = false) {
|
|
2113
|
+
return {
|
|
2114
|
+
cwd,
|
|
2115
|
+
timeout: this.timeoutMs,
|
|
2116
|
+
maxBuffer: this.maxOutputBytes,
|
|
2117
|
+
env: gitEnvironment(readOnly)
|
|
2118
|
+
};
|
|
1454
2119
|
}
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
await promises.chmod(dir, 448).catch(() => {
|
|
1462
|
-
});
|
|
1463
|
-
if (platform !== "win32") return;
|
|
1464
|
-
const { username } = os.userInfo();
|
|
1465
|
-
let result;
|
|
1466
|
-
try {
|
|
1467
|
-
result = await run("icacls", buildIcaclsArgs(dir, username));
|
|
1468
|
-
} catch (err) {
|
|
1469
|
-
throw new SecureDirHardeningError(dir, `could not run icacls: ${err instanceof Error ? err.message : String(err)}`);
|
|
2120
|
+
async read(args, cwd) {
|
|
2121
|
+
try {
|
|
2122
|
+
return await this.run(args, this.commandOptions(cwd, true));
|
|
2123
|
+
} catch (error) {
|
|
2124
|
+
throw asGitError(error, "git-command-failed", "Git observation failed");
|
|
2125
|
+
}
|
|
1470
2126
|
}
|
|
1471
|
-
|
|
1472
|
-
|
|
2127
|
+
async readTopLevel(root) {
|
|
2128
|
+
const result = await this.read(["rev-parse", "--show-toplevel"], root);
|
|
2129
|
+
if (result.code !== 0) throw new GitWorkspaceError("repository-invalid", "workspace is not a Git repository");
|
|
2130
|
+
return canonical(result.stdout.trim());
|
|
1473
2131
|
}
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
2132
|
+
async assertTaskRoot(workspaceDir) {
|
|
2133
|
+
const candidate = canonical(workspaceDir);
|
|
2134
|
+
const realWorkspaceRoot = await promises.realpath(this.workspaceRoot).catch(() => {
|
|
2135
|
+
throw new GitWorkspaceError("workspace-root-invalid", "workspace root is unavailable");
|
|
2136
|
+
});
|
|
2137
|
+
if (!isContained(this.workspaceRoot, candidate) && !isContained(realWorkspaceRoot, candidate)) {
|
|
2138
|
+
throw new GitWorkspaceError("workspace-root-invalid", "workspace directory is invalid");
|
|
2139
|
+
}
|
|
2140
|
+
await this.assertExistingAncestry(candidate, realWorkspaceRoot);
|
|
2141
|
+
await promises.mkdir(candidate, { recursive: true, mode: 448 }).catch(() => {
|
|
2142
|
+
throw new GitWorkspaceError("workspace-root-invalid", "workspace directory is unavailable");
|
|
2143
|
+
});
|
|
2144
|
+
const realCandidate = await promises.realpath(candidate).catch(() => {
|
|
2145
|
+
throw new GitWorkspaceError("workspace-root-invalid", "workspace directory is unavailable");
|
|
2146
|
+
});
|
|
2147
|
+
if (!isContained(realWorkspaceRoot, realCandidate)) {
|
|
2148
|
+
throw new GitWorkspaceError("workspace-root-invalid", "workspace directory is invalid");
|
|
2149
|
+
}
|
|
2150
|
+
return realCandidate;
|
|
1486
2151
|
}
|
|
1487
|
-
|
|
1488
|
-
|
|
2152
|
+
async assertExistingAncestry(candidate, realWorkspaceRoot) {
|
|
2153
|
+
let current = candidate;
|
|
2154
|
+
while (true) {
|
|
2155
|
+
try {
|
|
2156
|
+
const realCurrent = await promises.realpath(current);
|
|
2157
|
+
if (!isContained(realWorkspaceRoot, realCurrent)) {
|
|
2158
|
+
throw new GitWorkspaceError("workspace-root-invalid", "workspace directory is invalid");
|
|
2159
|
+
}
|
|
2160
|
+
return;
|
|
2161
|
+
} catch (error) {
|
|
2162
|
+
const code = error.code;
|
|
2163
|
+
if (error instanceof GitWorkspaceError || code !== "ENOENT" && code !== "ENOTDIR") {
|
|
2164
|
+
throw new GitWorkspaceError("workspace-root-invalid", "workspace directory is invalid");
|
|
2165
|
+
}
|
|
2166
|
+
const parent = path3__default.dirname(current);
|
|
2167
|
+
if (parent === current) {
|
|
2168
|
+
throw new GitWorkspaceError("workspace-root-invalid", "workspace directory is invalid");
|
|
2169
|
+
}
|
|
2170
|
+
current = parent;
|
|
2171
|
+
}
|
|
2172
|
+
}
|
|
2173
|
+
}
|
|
2174
|
+
async assertExistingTaskRoot(workspaceDir) {
|
|
2175
|
+
const candidate = canonical(workspaceDir);
|
|
2176
|
+
const realWorkspaceRoot = await promises.realpath(this.workspaceRoot).catch(() => {
|
|
2177
|
+
throw new GitWorkspaceError("workspace-root-invalid", "workspace root is unavailable");
|
|
2178
|
+
});
|
|
2179
|
+
const realCandidate = await promises.realpath(candidate).catch(() => {
|
|
2180
|
+
throw new GitWorkspaceError("repository-invalid", "workspace directory is unavailable");
|
|
2181
|
+
});
|
|
2182
|
+
if (!isContained(realWorkspaceRoot, realCandidate)) {
|
|
2183
|
+
throw new GitWorkspaceError("workspace-root-invalid", "workspace directory is invalid");
|
|
2184
|
+
}
|
|
2185
|
+
return realCandidate;
|
|
1489
2186
|
}
|
|
2187
|
+
};
|
|
2188
|
+
function prependGitWorkspaceGuidance(instruction) {
|
|
2189
|
+
return GitWorkspaceManager.prependGuidance(instruction);
|
|
1490
2190
|
}
|
|
1491
|
-
|
|
1492
|
-
// src/daemon/git-workspace-store.ts
|
|
1493
2191
|
var FILE_NAME = "git-workspaces.json";
|
|
1494
2192
|
var MAX_RECORDS = 500;
|
|
1495
2193
|
var PHASES = /* @__PURE__ */ new Set(["preparing", "active", "completed", "failed", "cancelled", "interrupted", "salvage"]);
|
|
@@ -1510,7 +2208,7 @@ function isProtected(record) {
|
|
|
1510
2208
|
var GitWorkspaceStore = class {
|
|
1511
2209
|
constructor(storeDir, options = {}) {
|
|
1512
2210
|
this.storeDir = storeDir;
|
|
1513
|
-
this.filePath =
|
|
2211
|
+
this.filePath = path3__default.join(storeDir, FILE_NAME);
|
|
1514
2212
|
this.maxRecords = Math.max(1, Math.floor(options.maxRecords ?? MAX_RECORDS));
|
|
1515
2213
|
}
|
|
1516
2214
|
storeDir;
|
|
@@ -1645,7 +2343,7 @@ var GitWorkspaceStore = class {
|
|
|
1645
2343
|
var BYOK_PI_MCP_CONFIG_PATH = "BYOK_PI_MCP_CONFIG_PATH";
|
|
1646
2344
|
var PI_PACKAGE_NAME = "@earendil-works/pi-coding-agent";
|
|
1647
2345
|
function readPackageJson(dir) {
|
|
1648
|
-
const candidate =
|
|
2346
|
+
const candidate = path3__default.join(dir, "package.json");
|
|
1649
2347
|
if (!existsSync(candidate)) return void 0;
|
|
1650
2348
|
try {
|
|
1651
2349
|
return JSON.parse(readFileSync(candidate, "utf8"));
|
|
@@ -1660,17 +2358,17 @@ function resolvePiBin() {
|
|
|
1660
2358
|
}
|
|
1661
2359
|
try {
|
|
1662
2360
|
const mainEntryUrl = import.meta.resolve(PI_PACKAGE_NAME);
|
|
1663
|
-
let dir =
|
|
2361
|
+
let dir = path3__default.dirname(fileURLToPath(mainEntryUrl));
|
|
1664
2362
|
for (let depth = 0; depth < 6; depth++) {
|
|
1665
2363
|
const pkg = readPackageJson(dir);
|
|
1666
2364
|
if (pkg?.name === PI_PACKAGE_NAME) {
|
|
1667
2365
|
const binRel = typeof pkg.bin === "string" ? pkg.bin : pkg.bin?.pi;
|
|
1668
2366
|
if (binRel) {
|
|
1669
|
-
return { command:
|
|
2367
|
+
return { command: path3__default.join(dir, binRel), source: "package" };
|
|
1670
2368
|
}
|
|
1671
2369
|
break;
|
|
1672
2370
|
}
|
|
1673
|
-
const parent =
|
|
2371
|
+
const parent = path3__default.dirname(dir);
|
|
1674
2372
|
if (parent === dir) break;
|
|
1675
2373
|
dir = parent;
|
|
1676
2374
|
}
|
|
@@ -1688,7 +2386,7 @@ function resolvePiExtensions() {
|
|
|
1688
2386
|
const clientManifest = fileURLToPath(import.meta.resolve("@byok-sdk/client/package.json"));
|
|
1689
2387
|
return {
|
|
1690
2388
|
webAccess: fileURLToPath(import.meta.resolve("pi-web-access/index.ts")),
|
|
1691
|
-
mcpAdapter:
|
|
2389
|
+
mcpAdapter: path3__default.join(path3__default.dirname(clientManifest), "dist", "adapters", "pi", "mcp-extension.js")
|
|
1692
2390
|
};
|
|
1693
2391
|
}
|
|
1694
2392
|
|
|
@@ -2614,10 +3312,10 @@ var PiAdapter = class {
|
|
|
2614
3312
|
const taskMcpServers = startInput.mcpServers ?? {};
|
|
2615
3313
|
const hasMcpServers = Object.keys(taskMcpServers).length > 0;
|
|
2616
3314
|
if (hasMcpServers) {
|
|
2617
|
-
mcpConfigDir = await promises.mkdtemp(
|
|
3315
|
+
mcpConfigDir = await promises.mkdtemp(path3__default.join(os__default.tmpdir(), "byok-pi-mcp-"));
|
|
2618
3316
|
await promises.chmod(mcpConfigDir, 448).catch(() => {
|
|
2619
3317
|
});
|
|
2620
|
-
const mcpConfigPath =
|
|
3318
|
+
const mcpConfigPath = path3__default.join(mcpConfigDir, "mcp-config.json");
|
|
2621
3319
|
await promises.writeFile(mcpConfigPath, JSON.stringify({ mcpServers: taskMcpServers }), { mode: 384 });
|
|
2622
3320
|
runtimeEnv = { ...runtimeEnv, [BYOK_PI_MCP_CONFIG_PATH]: mcpConfigPath };
|
|
2623
3321
|
}
|
|
@@ -2848,7 +3546,7 @@ function resolveApprovalMcpBin() {
|
|
|
2848
3546
|
if (override) {
|
|
2849
3547
|
return { command: override, args: [], source: "env" };
|
|
2850
3548
|
}
|
|
2851
|
-
const distBin =
|
|
3549
|
+
const distBin = path3__default.join(path3__default.dirname(fileURLToPath(import.meta.url)), "bin", "byok-approval-mcp.js");
|
|
2852
3550
|
return { command: process.execPath, args: [distBin], source: "dist" };
|
|
2853
3551
|
}
|
|
2854
3552
|
|
|
@@ -2939,7 +3637,7 @@ var EXTENSION_CONTENT_TYPES = {
|
|
|
2939
3637
|
".yml": "application/yaml"
|
|
2940
3638
|
};
|
|
2941
3639
|
function guessContentType(filePath) {
|
|
2942
|
-
const ext =
|
|
3640
|
+
const ext = path3__default.extname(filePath).toLowerCase();
|
|
2943
3641
|
return EXTENSION_CONTENT_TYPES[ext] ?? "application/octet-stream";
|
|
2944
3642
|
}
|
|
2945
3643
|
function mapAssistant(msg, correlation) {
|
|
@@ -3023,11 +3721,11 @@ function tryBuildArtifactEvent(msg, workspaceDir) {
|
|
|
3023
3721
|
const filePath = toolUseResult && typeof toolUseResult.filePath === "string" ? toolUseResult.filePath : void 0;
|
|
3024
3722
|
if (!filePath) return void 0;
|
|
3025
3723
|
const realWorkspaceDir = tryRealpath(workspaceDir) ?? workspaceDir;
|
|
3026
|
-
const fileDir =
|
|
3724
|
+
const fileDir = path3__default.dirname(filePath);
|
|
3027
3725
|
const realFileDir = tryRealpath(fileDir) ?? fileDir;
|
|
3028
|
-
const realFilePath =
|
|
3029
|
-
const relative =
|
|
3030
|
-
if (relative === "" || relative.startsWith("..") ||
|
|
3726
|
+
const realFilePath = path3__default.join(realFileDir, path3__default.basename(filePath));
|
|
3727
|
+
const relative = path3__default.relative(realWorkspaceDir, realFilePath);
|
|
3728
|
+
if (relative === "" || relative.startsWith("..") || path3__default.isAbsolute(relative)) {
|
|
3031
3729
|
return void 0;
|
|
3032
3730
|
}
|
|
3033
3731
|
return { type: "artifact", name: relative, contentType: guessContentType(filePath) };
|
|
@@ -3421,10 +4119,10 @@ var ClaudeAdapter = class {
|
|
|
3421
4119
|
});
|
|
3422
4120
|
}
|
|
3423
4121
|
if (needsMcpConfig) {
|
|
3424
|
-
mcpConfigDir = await promises.mkdtemp(
|
|
4122
|
+
mcpConfigDir = await promises.mkdtemp(path3__default.join(os__default.tmpdir(), "byok-mcp-"));
|
|
3425
4123
|
await promises.chmod(mcpConfigDir, 448).catch(() => {
|
|
3426
4124
|
});
|
|
3427
|
-
const mcpConfigPath =
|
|
4125
|
+
const mcpConfigPath = path3__default.join(mcpConfigDir, "mcp-config.json");
|
|
3428
4126
|
const mcpServers = { ...taskMcpServers };
|
|
3429
4127
|
if (mapping.needsApprovalMcp) {
|
|
3430
4128
|
const approvalChannel = startInput.approvalChannel;
|
|
@@ -3910,8 +4608,8 @@ function extractArtifactEvents(changes, workspaceDir) {
|
|
|
3910
4608
|
const absolutePath = typeof change.path === "string" ? change.path : void 0;
|
|
3911
4609
|
const kind = typeof change.kind === "string" ? change.kind : void 0;
|
|
3912
4610
|
if (!absolutePath || kind === "delete") continue;
|
|
3913
|
-
const relative =
|
|
3914
|
-
if (relative.length === 0 || relative.startsWith("..") ||
|
|
4611
|
+
const relative = path3__default.relative(workspaceDir, absolutePath);
|
|
4612
|
+
if (relative.length === 0 || relative.startsWith("..") || path3__default.isAbsolute(relative)) continue;
|
|
3915
4613
|
events.push({ type: "artifact", name: relative, contentType: guessContentType2(relative) });
|
|
3916
4614
|
}
|
|
3917
4615
|
return events;
|
|
@@ -3932,7 +4630,7 @@ var CONTENT_TYPE_BY_EXTENSION = {
|
|
|
3932
4630
|
".csv": "text/csv"
|
|
3933
4631
|
};
|
|
3934
4632
|
function guessContentType2(relativePath) {
|
|
3935
|
-
return CONTENT_TYPE_BY_EXTENSION[
|
|
4633
|
+
return CONTENT_TYPE_BY_EXTENSION[path3__default.extname(relativePath).toLowerCase()] ?? "application/octet-stream";
|
|
3936
4634
|
}
|
|
3937
4635
|
function extractErrorMessage(rawError) {
|
|
3938
4636
|
if (typeof rawError === "string") return rawError;
|
|
@@ -4731,6 +5429,301 @@ var ApprovalRegistry = class {
|
|
|
4731
5429
|
entry.onResolve(decision, reason, origin);
|
|
4732
5430
|
}
|
|
4733
5431
|
};
|
|
5432
|
+
var ENTRY_ACCOUNT = "device-enrollment";
|
|
5433
|
+
var ENCODED_PREFIX = "byok-device-credential-v1:";
|
|
5434
|
+
var NOT_FOUND = 44;
|
|
5435
|
+
var WINDOWS_BRIDGE_DIRECTORY_PREFIX = "byok-device-credential-";
|
|
5436
|
+
var WINDOWS_BRIDGE_STALE_MS = 24 * 60 * 60 * 1e3;
|
|
5437
|
+
function providerDiagnostic(stderr) {
|
|
5438
|
+
const match = /credential operation failed \((win32=\d{1,10}|hresult=-?\d{1,11}|stage=\d{1,2},kind=\d{1,2},hresult=-?\d{1,11})\)/u.exec(stderr);
|
|
5439
|
+
return match === null ? "" : ` (${match[1]})`;
|
|
5440
|
+
}
|
|
5441
|
+
function isNodeError(error, code) {
|
|
5442
|
+
return error instanceof Error && "code" in error && error.code === code;
|
|
5443
|
+
}
|
|
5444
|
+
async function scavengeStaleWindowsBridges() {
|
|
5445
|
+
const temporaryRoot = os.tmpdir();
|
|
5446
|
+
let entries;
|
|
5447
|
+
try {
|
|
5448
|
+
entries = await fs12.readdir(temporaryRoot, { withFileTypes: true });
|
|
5449
|
+
} catch {
|
|
5450
|
+
throw new DeviceCredentialStoreError("temporary Windows credential bridge root is unavailable");
|
|
5451
|
+
}
|
|
5452
|
+
const staleBefore = Date.now() - WINDOWS_BRIDGE_STALE_MS;
|
|
5453
|
+
for (const entry of entries) {
|
|
5454
|
+
if (!entry.isDirectory() || !entry.name.startsWith(WINDOWS_BRIDGE_DIRECTORY_PREFIX)) continue;
|
|
5455
|
+
const candidate = path3.join(temporaryRoot, entry.name);
|
|
5456
|
+
try {
|
|
5457
|
+
const info = await fs12.lstat(candidate);
|
|
5458
|
+
if (!info.isDirectory() || info.isSymbolicLink() || info.mtimeMs >= staleBefore) continue;
|
|
5459
|
+
await fs12.rm(candidate, { recursive: true, force: true, maxRetries: 5, retryDelay: 50 });
|
|
5460
|
+
} catch (error) {
|
|
5461
|
+
if (isNodeError(error, "ENOENT")) continue;
|
|
5462
|
+
throw new DeviceCredentialStoreError("stale Windows credential bridge cleanup failed");
|
|
5463
|
+
}
|
|
5464
|
+
}
|
|
5465
|
+
}
|
|
5466
|
+
function windowsPowerShellExecutable() {
|
|
5467
|
+
const systemRoot = process.env.SystemRoot;
|
|
5468
|
+
if (systemRoot === void 0 || !path3.win32.isAbsolute(systemRoot)) {
|
|
5469
|
+
throw new DeviceCredentialStoreError("Windows system root is unavailable");
|
|
5470
|
+
}
|
|
5471
|
+
return path3.win32.join(systemRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
|
|
5472
|
+
}
|
|
5473
|
+
var DeviceCredentialStoreUnavailableError = class extends Error {
|
|
5474
|
+
constructor(message = "no supported operating-system credential provider is available") {
|
|
5475
|
+
super(message);
|
|
5476
|
+
this.name = "DeviceCredentialStoreUnavailableError";
|
|
5477
|
+
}
|
|
5478
|
+
};
|
|
5479
|
+
var DeviceCredentialStoreError = class extends Error {
|
|
5480
|
+
constructor(message) {
|
|
5481
|
+
super(message);
|
|
5482
|
+
this.name = "DeviceCredentialStoreError";
|
|
5483
|
+
}
|
|
5484
|
+
};
|
|
5485
|
+
function assertRecord(value) {
|
|
5486
|
+
if (typeof value !== "object" || value === null || Array.isArray(value) || typeof value.deviceId !== "string" || !isTenantId(value.tenantId) || typeof value.devicePublicKey !== "string" || typeof value.accessToken !== "string" || typeof value.expiresAt !== "string" || typeof value.devicePrivateKeyPem !== "string") {
|
|
5487
|
+
throw new DeviceCredentialStoreError("OS credential entry has an invalid device credential shape");
|
|
5488
|
+
}
|
|
5489
|
+
const credential = value;
|
|
5490
|
+
if (credential.deviceId.length === 0 || credential.devicePublicKey.length === 0 || credential.accessToken.length === 0 || credential.expiresAt.length === 0 || credential.devicePrivateKeyPem.length === 0) {
|
|
5491
|
+
throw new DeviceCredentialStoreError("OS credential entry has an incomplete device credential");
|
|
5492
|
+
}
|
|
5493
|
+
}
|
|
5494
|
+
function encode(record) {
|
|
5495
|
+
assertRecord(record);
|
|
5496
|
+
const encoded = Buffer.from(JSON.stringify(record), "utf8").toString("base64");
|
|
5497
|
+
const value = `${ENCODED_PREFIX}${encoded}`;
|
|
5498
|
+
if (Buffer.byteLength(value, "utf8") > 2400) {
|
|
5499
|
+
throw new DeviceCredentialStoreError("device credential exceeds the OS credential entry bound");
|
|
5500
|
+
}
|
|
5501
|
+
return value;
|
|
5502
|
+
}
|
|
5503
|
+
function decode(value) {
|
|
5504
|
+
if (!value.startsWith(ENCODED_PREFIX)) {
|
|
5505
|
+
throw new DeviceCredentialStoreError("OS credential entry is not owned by this client credential store");
|
|
5506
|
+
}
|
|
5507
|
+
const encoded = value.slice(ENCODED_PREFIX.length);
|
|
5508
|
+
if (encoded.length === 0 || encoded.length % 4 !== 0 || !/^[A-Za-z0-9+/]*={0,2}$/u.test(encoded)) {
|
|
5509
|
+
throw new DeviceCredentialStoreError("OS credential entry is not strict base64");
|
|
5510
|
+
}
|
|
5511
|
+
const bytes = Buffer.from(encoded, "base64");
|
|
5512
|
+
if (bytes.toString("base64") !== encoded) {
|
|
5513
|
+
throw new DeviceCredentialStoreError("OS credential entry is not canonical base64");
|
|
5514
|
+
}
|
|
5515
|
+
const raw = bytes.toString("utf8");
|
|
5516
|
+
if (!Buffer.from(raw, "utf8").equals(bytes)) {
|
|
5517
|
+
throw new DeviceCredentialStoreError("OS credential entry is not valid UTF-8");
|
|
5518
|
+
}
|
|
5519
|
+
let parsed;
|
|
5520
|
+
try {
|
|
5521
|
+
parsed = JSON.parse(raw);
|
|
5522
|
+
} catch {
|
|
5523
|
+
throw new DeviceCredentialStoreError("OS credential entry is not valid JSON");
|
|
5524
|
+
}
|
|
5525
|
+
assertRecord(parsed);
|
|
5526
|
+
return Object.freeze({
|
|
5527
|
+
deviceId: parsed.deviceId,
|
|
5528
|
+
tenantId: parsed.tenantId,
|
|
5529
|
+
devicePublicKey: parsed.devicePublicKey,
|
|
5530
|
+
accessToken: parsed.accessToken,
|
|
5531
|
+
expiresAt: parsed.expiresAt,
|
|
5532
|
+
devicePrivateKeyPem: parsed.devicePrivateKeyPem
|
|
5533
|
+
});
|
|
5534
|
+
}
|
|
5535
|
+
function serviceFor(productId) {
|
|
5536
|
+
if (typeof productId !== "string" || productId.length === 0 || /[\u0000\r\n]/u.test(productId)) {
|
|
5537
|
+
throw new DeviceCredentialStoreError("productId must be a non-empty single-line string");
|
|
5538
|
+
}
|
|
5539
|
+
return `com.byok.client.device.${createHash("sha256").update(productId, "utf8").digest("hex")}`;
|
|
5540
|
+
}
|
|
5541
|
+
function quoteInteractive(value) {
|
|
5542
|
+
if (/[\u0000\r\n]/u.test(value)) throw new DeviceCredentialStoreError("credential command argument is invalid");
|
|
5543
|
+
return `"${value.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`;
|
|
5544
|
+
}
|
|
5545
|
+
var DeviceCredentialStore = class {
|
|
5546
|
+
#service;
|
|
5547
|
+
#platform;
|
|
5548
|
+
#run;
|
|
5549
|
+
constructor(options) {
|
|
5550
|
+
this.#service = serviceFor(options.productId);
|
|
5551
|
+
this.#platform = options.platform ?? process.platform;
|
|
5552
|
+
this.#run = options.commandRunner ?? runDeviceCommand;
|
|
5553
|
+
}
|
|
5554
|
+
async read() {
|
|
5555
|
+
const result = await this.#invoke("read");
|
|
5556
|
+
if (result.exitCode === NOT_FOUND || this.#platform === "linux" && result.exitCode === 1 && result.stderr.trim().length === 0) return void 0;
|
|
5557
|
+
if (result.exitCode === 127) throw new DeviceCredentialStoreUnavailableError();
|
|
5558
|
+
if (result.exitCode !== 0) {
|
|
5559
|
+
throw new DeviceCredentialStoreError(
|
|
5560
|
+
`operating-system credential provider could not read device credentials${providerDiagnostic(result.stderr)}`
|
|
5561
|
+
);
|
|
5562
|
+
}
|
|
5563
|
+
return decode(result.stdout.trimEnd());
|
|
5564
|
+
}
|
|
5565
|
+
async replace(record) {
|
|
5566
|
+
const encoded = encode(record);
|
|
5567
|
+
const result = await this.#invoke("replace", encoded);
|
|
5568
|
+
if (result.exitCode === 127) throw new DeviceCredentialStoreUnavailableError();
|
|
5569
|
+
if (result.exitCode !== 0) {
|
|
5570
|
+
throw new DeviceCredentialStoreError(
|
|
5571
|
+
`operating-system credential provider could not replace device credentials${providerDiagnostic(result.stderr)}`
|
|
5572
|
+
);
|
|
5573
|
+
}
|
|
5574
|
+
}
|
|
5575
|
+
/** Returns true only after the sole secret authority is confirmed absent. */
|
|
5576
|
+
async clear() {
|
|
5577
|
+
const before = await this.read();
|
|
5578
|
+
if (before === void 0) return false;
|
|
5579
|
+
const result = await this.#invoke("clear");
|
|
5580
|
+
if (result.exitCode === 127) throw new DeviceCredentialStoreUnavailableError();
|
|
5581
|
+
if (result.exitCode !== 0 && result.exitCode !== NOT_FOUND) {
|
|
5582
|
+
throw new DeviceCredentialStoreError(
|
|
5583
|
+
`operating-system credential provider could not clear device credentials${providerDiagnostic(result.stderr)}`
|
|
5584
|
+
);
|
|
5585
|
+
}
|
|
5586
|
+
if (await this.read() !== void 0) {
|
|
5587
|
+
throw new DeviceCredentialStoreError("operating-system credential provider reported deletion but device credentials remain");
|
|
5588
|
+
}
|
|
5589
|
+
return true;
|
|
5590
|
+
}
|
|
5591
|
+
async #invoke(operation, encoded) {
|
|
5592
|
+
switch (this.#platform) {
|
|
5593
|
+
case "darwin":
|
|
5594
|
+
return this.#macos(operation, encoded);
|
|
5595
|
+
case "win32":
|
|
5596
|
+
return this.#windows(operation, encoded);
|
|
5597
|
+
case "linux":
|
|
5598
|
+
return this.#linux(operation, encoded);
|
|
5599
|
+
default:
|
|
5600
|
+
throw new DeviceCredentialStoreUnavailableError(`no operating-system credential provider is supported on ${this.#platform}`);
|
|
5601
|
+
}
|
|
5602
|
+
}
|
|
5603
|
+
#macos(operation, encoded) {
|
|
5604
|
+
if (operation === "read") return this.#run("/usr/bin/security", ["find-generic-password", "-a", ENTRY_ACCOUNT, "-s", this.#service, "-w"]);
|
|
5605
|
+
if (operation === "clear") return this.#run("/usr/bin/security", ["delete-generic-password", "-a", ENTRY_ACCOUNT, "-s", this.#service]);
|
|
5606
|
+
const command = [
|
|
5607
|
+
"add-generic-password",
|
|
5608
|
+
"-U",
|
|
5609
|
+
"-a",
|
|
5610
|
+
quoteInteractive(ENTRY_ACCOUNT),
|
|
5611
|
+
"-s",
|
|
5612
|
+
quoteInteractive(this.#service),
|
|
5613
|
+
"-w",
|
|
5614
|
+
quoteInteractive(encoded)
|
|
5615
|
+
].join(" ");
|
|
5616
|
+
return this.#run("/usr/bin/security", ["-i"], `${command}
|
|
5617
|
+
`);
|
|
5618
|
+
}
|
|
5619
|
+
#linux(operation, encoded) {
|
|
5620
|
+
const attrs = ["service", this.#service, "account", ENTRY_ACCOUNT];
|
|
5621
|
+
if (operation === "read") return this.#run("secret-tool", ["lookup", ...attrs]);
|
|
5622
|
+
if (operation === "clear") return this.#run("secret-tool", ["clear", ...attrs]);
|
|
5623
|
+
return this.#run("secret-tool", ["store", "--label=BYOK device enrollment", ...attrs], encoded);
|
|
5624
|
+
}
|
|
5625
|
+
async #windows(operation, encoded) {
|
|
5626
|
+
await scavengeStaleWindowsBridges();
|
|
5627
|
+
const directory = await fs12.mkdtemp(path3.join(os.tmpdir(), WINDOWS_BRIDGE_DIRECTORY_PREFIX));
|
|
5628
|
+
const executable = path3.join(directory, "credential-bridge.exe");
|
|
5629
|
+
const request = [
|
|
5630
|
+
operation,
|
|
5631
|
+
Buffer.from(this.#service, "utf8").toString("base64"),
|
|
5632
|
+
Buffer.from(ENTRY_ACCOUNT, "utf8").toString("base64"),
|
|
5633
|
+
encoded === void 0 ? "" : Buffer.from(encoded, "utf8").toString("base64")
|
|
5634
|
+
].join("\n");
|
|
5635
|
+
let result;
|
|
5636
|
+
let cleanupFailed = false;
|
|
5637
|
+
try {
|
|
5638
|
+
const compiler = await this.#run(
|
|
5639
|
+
windowsPowerShellExecutable(),
|
|
5640
|
+
["-NoLogo", "-NoProfile", "-NonInteractive", "-EncodedCommand", WINDOWS_CREDENTIAL_COMPILER_SCRIPT],
|
|
5641
|
+
executable
|
|
5642
|
+
);
|
|
5643
|
+
result = compiler.exitCode === 0 ? await this.#run(executable, [], request) : { exitCode: compiler.exitCode, stdout: "", stderr: compiler.stderr };
|
|
5644
|
+
} finally {
|
|
5645
|
+
try {
|
|
5646
|
+
await fs12.rm(directory, { recursive: true, force: true, maxRetries: 5, retryDelay: 50 });
|
|
5647
|
+
} catch {
|
|
5648
|
+
cleanupFailed = true;
|
|
5649
|
+
}
|
|
5650
|
+
}
|
|
5651
|
+
if (cleanupFailed) {
|
|
5652
|
+
throw new DeviceCredentialStoreError("temporary Windows credential bridge cleanup failed");
|
|
5653
|
+
}
|
|
5654
|
+
if (result === void 0) {
|
|
5655
|
+
throw new DeviceCredentialStoreError("Windows credential bridge did not produce a result");
|
|
5656
|
+
}
|
|
5657
|
+
return result;
|
|
5658
|
+
}
|
|
5659
|
+
};
|
|
5660
|
+
var InMemoryDeviceCredentialStore = class {
|
|
5661
|
+
#record;
|
|
5662
|
+
async read() {
|
|
5663
|
+
return this.#record === void 0 ? void 0 : Object.freeze({ ...this.#record });
|
|
5664
|
+
}
|
|
5665
|
+
async replace(record) {
|
|
5666
|
+
assertRecord(record);
|
|
5667
|
+
this.#record = Object.freeze({ ...record });
|
|
5668
|
+
}
|
|
5669
|
+
async clear() {
|
|
5670
|
+
const had = this.#record !== void 0;
|
|
5671
|
+
this.#record = void 0;
|
|
5672
|
+
return had;
|
|
5673
|
+
}
|
|
5674
|
+
};
|
|
5675
|
+
async function runDeviceCommand(executable, args, stdin) {
|
|
5676
|
+
return new Promise((resolve) => {
|
|
5677
|
+
const child = spawn(executable, [...args], { env: process.env, stdio: ["pipe", "pipe", "pipe"] });
|
|
5678
|
+
let stdout = "";
|
|
5679
|
+
let stderr = "";
|
|
5680
|
+
child.stdout.setEncoding("utf8");
|
|
5681
|
+
child.stderr.setEncoding("utf8");
|
|
5682
|
+
child.stdout.on("data", (chunk) => {
|
|
5683
|
+
stdout += chunk;
|
|
5684
|
+
});
|
|
5685
|
+
child.stderr.on("data", (chunk) => {
|
|
5686
|
+
stderr += chunk;
|
|
5687
|
+
});
|
|
5688
|
+
child.once("error", () => resolve({ exitCode: 127, stdout: "", stderr: "command unavailable" }));
|
|
5689
|
+
child.once("close", (code) => resolve({ exitCode: code ?? 1, stdout, stderr }));
|
|
5690
|
+
child.stdin.end(stdin);
|
|
5691
|
+
});
|
|
5692
|
+
}
|
|
5693
|
+
var WINDOWS_CREDENTIAL_COMPILER_SCRIPT = Buffer.from(String.raw`
|
|
5694
|
+
$assembly=[Console]::In.ReadToEnd()
|
|
5695
|
+
try {
|
|
5696
|
+
Add-Type -OutputAssembly $assembly -OutputType ConsoleApplication -ErrorAction Stop -TypeDefinition @"
|
|
5697
|
+
using System; using System.Runtime.InteropServices; using System.Text;
|
|
5698
|
+
public static class ByokDeviceCredential {
|
|
5699
|
+
[StructLayout(LayoutKind.Sequential, CharSet=CharSet.Unicode)] private struct C { public UInt32 Flags; public UInt32 Type; [MarshalAs(UnmanagedType.LPWStr)] public string TargetName; [MarshalAs(UnmanagedType.LPWStr)] public string Comment; public System.Runtime.InteropServices.ComTypes.FILETIME LastWritten; public UInt32 CredentialBlobSize; public IntPtr CredentialBlob; public UInt32 Persist; public UInt32 AttributeCount; public IntPtr Attributes; [MarshalAs(UnmanagedType.LPWStr)] public string TargetAlias; [MarshalAs(UnmanagedType.LPWStr)] public string UserName; }
|
|
5700
|
+
[DllImport("Advapi32.dll", EntryPoint="CredWriteW", CharSet=CharSet.Unicode, SetLastError=true)] private static extern bool W(ref C c, UInt32 f);
|
|
5701
|
+
[DllImport("Advapi32.dll", EntryPoint="CredReadW", CharSet=CharSet.Unicode, SetLastError=true)] private static extern bool R(string t, UInt32 ty, UInt32 f, out IntPtr p);
|
|
5702
|
+
[DllImport("Advapi32.dll", EntryPoint="CredDeleteW", CharSet=CharSet.Unicode, SetLastError=true)] private static extern bool D(string t, UInt32 ty, UInt32 f);
|
|
5703
|
+
[DllImport("Advapi32.dll")] private static extern void CredFree(IntPtr p);
|
|
5704
|
+
private static int Set(string t,string u,byte[] b) { IntPtr p=IntPtr.Zero; try { p=Marshal.AllocHGlobal(b.Length); Marshal.Copy(b,0,p,b.Length); C c=new C { Type=1,TargetName=t,CredentialBlobSize=(UInt32)b.Length,CredentialBlob=p,Persist=2,UserName=u}; if(!W(ref c,0)) return Marshal.GetLastWin32Error(); return 0; } finally { if(p!=IntPtr.Zero) { Marshal.Copy(new byte[b.Length],0,p,b.Length); Marshal.FreeHGlobal(p); } } }
|
|
5705
|
+
private static int Kind(Exception e) { if(e is DllNotFoundException)return 1;if(e is EntryPointNotFoundException)return 2;if(e is BadImageFormatException)return 3;if(e is MarshalDirectiveException)return 4;if(e is SEHException)return 5;if(e is AccessViolationException)return 6;if(e is TypeInitializationException)return 7;if(e is TypeLoadException)return 8;if(e is InvalidCastException)return 9;if(e is ArgumentException)return 10;if(e is InvalidOperationException)return 11;if(e.GetType()==typeof(SystemException))return 12;if(e is SystemException)return 13;return 99; }
|
|
5706
|
+
private static int Failure(int stage,Exception e) { Console.Error.Write("credential operation failed (stage="+stage+",kind="+Kind(e)+",hresult="+e.HResult+")"); return -1; }
|
|
5707
|
+
private static int InputFailure(int kind) { Console.Error.Write("credential operation failed (stage=0,kind="+kind+",hresult=0)"); return 2; }
|
|
5708
|
+
private static int Get(string t) { IntPtr p=IntPtr.Zero; bool found; try { found=R(t,1,0,out p); } catch(Exception e) { return Failure(1,e); } if(!found) return Marshal.GetLastWin32Error(); int code=0; try { C c=(C)Marshal.PtrToStructure(p,typeof(C)); byte[] b=new byte[c.CredentialBlobSize]; if(b.Length>0) { Marshal.Copy(c.CredentialBlob,b,0,b.Length); using(var output=Console.OpenStandardOutput()) { output.Write(b,0,b.Length); output.Flush(); } } } catch(Exception e) { code=Failure(2,e); } try { CredFree(p); } catch(Exception e) { return Failure(3,e); } return code; }
|
|
5709
|
+
private static int Delete(string t) { if(D(t,1,0)) return 0; return Marshal.GetLastWin32Error(); }
|
|
5710
|
+
private static int ExitFor(int code,bool missingIsAbsent) { if(code==0)return 0;if(missingIsAbsent&&code==1168)return 44;if(code<0)return 1;Console.Error.Write("credential operation failed (win32="+code+")");return 1; }
|
|
5711
|
+
private static string Decode(string value) { return Encoding.UTF8.GetString(Convert.FromBase64String(value)); }
|
|
5712
|
+
public static int Main() { int stage=1;byte[] secret=null;try { string[] fields=Console.In.ReadToEnd().Split(new[]{'\n'},StringSplitOptions.None);if(fields.Length!=4)return InputFailure(fields.Length<100?fields.Length:99);string operation=fields[0];string target=Decode(fields[1]);string username=Decode(fields[2]);if(target.Length==0||username.Length==0)return InputFailure(20);if(operation=="replace") { stage=2;secret=Convert.FromBase64String(fields[3]);stage=3;return ExitFor(Set(target,username,secret),false); } if(operation=="read") { stage=4;return ExitFor(Get(target),true); } if(operation=="clear") { stage=6;return ExitFor(Delete(target),true); } return InputFailure(21); } catch(Exception e) { Failure(stage,e);return 1; } finally { if(secret!=null)Array.Clear(secret,0,secret.Length); } }
|
|
5713
|
+
}
|
|
5714
|
+
"@
|
|
5715
|
+
if(Test-Path -LiteralPath $assembly -PathType Leaf){exit 0}
|
|
5716
|
+
[Console]::Error.Write("credential operation failed (stage=8,kind=2,hresult=0)")
|
|
5717
|
+
} catch {
|
|
5718
|
+
$compilerCode=99
|
|
5719
|
+
$errorNumber=[string]$_.TargetObject.ErrorNumber
|
|
5720
|
+
if($errorNumber -match '\ACS([0-9]{4})\z'){$compilerCode=[Convert]::ToInt32($Matches[1])}
|
|
5721
|
+
[Console]::Error.Write("credential operation failed (stage=8,kind=3,hresult="+$compilerCode+")")
|
|
5722
|
+
}
|
|
5723
|
+
exit 1
|
|
5724
|
+
`, "utf16le").toString("base64");
|
|
5725
|
+
|
|
5726
|
+
// src/daemon/store.ts
|
|
4734
5727
|
var MAX_DEVICE_RECORD_BYTES = 256 * 1024;
|
|
4735
5728
|
var REPAIR_REQUIRED_MESSAGE = "device enrollment record is missing or has an invalid authenticated tenant binding; re-pair required";
|
|
4736
5729
|
var DeviceRecordRePairRequiredError = class extends Error {
|
|
@@ -4748,30 +5741,31 @@ function sameFileState(left, right) {
|
|
|
4748
5741
|
function sameContentState(left, right) {
|
|
4749
5742
|
return sameInode(left, right) && left.size === right.size && left.mtimeNs === right.mtimeNs;
|
|
4750
5743
|
}
|
|
4751
|
-
|
|
5744
|
+
var LEGACY_SECRET_FIELDS = /* @__PURE__ */ new Set(["accessToken", "expiresAt", "devicePrivateKeyPem"]);
|
|
5745
|
+
function assertDeviceMetadata(value) {
|
|
4752
5746
|
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
4753
5747
|
throw new DeviceRecordRePairRequiredError();
|
|
4754
5748
|
}
|
|
4755
5749
|
const parsed = value;
|
|
4756
|
-
if (
|
|
5750
|
+
if (Object.keys(parsed).some((key) => LEGACY_SECRET_FIELDS.has(key))) {
|
|
5751
|
+
throw new DeviceRecordRePairRequiredError();
|
|
5752
|
+
}
|
|
5753
|
+
if (typeof parsed.deviceId === "string" && isTenantId(parsed.tenantId) && typeof parsed.devicePublicKey === "string") {
|
|
4757
5754
|
return;
|
|
4758
5755
|
}
|
|
4759
5756
|
throw new DeviceRecordRePairRequiredError();
|
|
4760
5757
|
}
|
|
4761
|
-
function
|
|
5758
|
+
function parseDeviceMetadata(raw) {
|
|
4762
5759
|
let parsed;
|
|
4763
5760
|
try {
|
|
4764
5761
|
parsed = JSON.parse(raw);
|
|
4765
5762
|
} catch {
|
|
4766
5763
|
throw new DeviceRecordRePairRequiredError();
|
|
4767
5764
|
}
|
|
4768
|
-
|
|
5765
|
+
assertDeviceMetadata(parsed);
|
|
4769
5766
|
return {
|
|
4770
5767
|
deviceId: parsed.deviceId,
|
|
4771
5768
|
tenantId: parsed.tenantId,
|
|
4772
|
-
accessToken: parsed.accessToken,
|
|
4773
|
-
expiresAt: parsed.expiresAt,
|
|
4774
|
-
devicePrivateKeyPem: parsed.devicePrivateKeyPem,
|
|
4775
5769
|
devicePublicKey: parsed.devicePublicKey
|
|
4776
5770
|
};
|
|
4777
5771
|
}
|
|
@@ -4786,14 +5780,31 @@ var DeviceStore = class _DeviceStore {
|
|
|
4786
5780
|
* ACL-unprotected credential") is verifiable from a real `darwin`/`linux`
|
|
4787
5781
|
* CI/dev machine, not just asserted.
|
|
4788
5782
|
*/
|
|
4789
|
-
constructor(storeDir, secureDirOptions) {
|
|
5783
|
+
constructor(storeDir, secureDirOptions, productId) {
|
|
4790
5784
|
this.secureDirOptions = secureDirOptions;
|
|
4791
|
-
this.filePath =
|
|
5785
|
+
this.filePath = path3__default.join(storeDir, "device.json");
|
|
5786
|
+
if (productId === void 0) {
|
|
5787
|
+
this.credentials = new InMemoryDeviceCredentialStore();
|
|
5788
|
+
} else if (process.env.BYOK_TEST_DEVICE_CREDENTIAL_STORE === "1") {
|
|
5789
|
+
const key = `${path3__default.resolve(storeDir)}\0${productId}`;
|
|
5790
|
+
let credentials = _DeviceStore.testCredentials.get(key);
|
|
5791
|
+
if (credentials === void 0) {
|
|
5792
|
+
credentials = new InMemoryDeviceCredentialStore();
|
|
5793
|
+
_DeviceStore.testCredentials.set(key, credentials);
|
|
5794
|
+
}
|
|
5795
|
+
this.credentials = credentials;
|
|
5796
|
+
} else {
|
|
5797
|
+
this.credentials = new DeviceCredentialStore({ productId });
|
|
5798
|
+
}
|
|
4792
5799
|
}
|
|
4793
5800
|
secureDirOptions;
|
|
5801
|
+
/** Process-local keyed doubles preserve restart semantics in isolated tests. */
|
|
5802
|
+
static testCredentials = /* @__PURE__ */ new Map();
|
|
4794
5803
|
filePath;
|
|
5804
|
+
/** Internal test seam. Product construction always supplies productId and gets an OS store. */
|
|
5805
|
+
credentials;
|
|
4795
5806
|
static defaultDir(productId) {
|
|
4796
|
-
return
|
|
5807
|
+
return path3__default.join(os__default.homedir(), ".byok", productId);
|
|
4797
5808
|
}
|
|
4798
5809
|
/**
|
|
4799
5810
|
* Resolve the one store pathname every daemon/CLI component must share.
|
|
@@ -4802,13 +5813,13 @@ var DeviceStore = class _DeviceStore {
|
|
|
4802
5813
|
* cwd to pin a quarantine directory inode.
|
|
4803
5814
|
*/
|
|
4804
5815
|
static resolveDir(productId, configured) {
|
|
4805
|
-
return
|
|
5816
|
+
return path3__default.resolve(configured ?? _DeviceStore.defaultDir(productId));
|
|
4806
5817
|
}
|
|
4807
5818
|
async load() {
|
|
4808
5819
|
const opened = await this.openBounded();
|
|
4809
5820
|
if (!opened) return void 0;
|
|
4810
5821
|
try {
|
|
4811
|
-
return
|
|
5822
|
+
return parseDeviceMetadata(opened.raw);
|
|
4812
5823
|
} finally {
|
|
4813
5824
|
await opened.handle.close();
|
|
4814
5825
|
}
|
|
@@ -4823,7 +5834,7 @@ var DeviceStore = class _DeviceStore {
|
|
|
4823
5834
|
if (!opened) return void 0;
|
|
4824
5835
|
const guardPath = `${this.filePath}.${process.pid}.${randomUUID()}.remove`;
|
|
4825
5836
|
try {
|
|
4826
|
-
const record =
|
|
5837
|
+
const record = parseDeviceMetadata(opened.raw);
|
|
4827
5838
|
linkSync(this.filePath, guardPath);
|
|
4828
5839
|
const openStat = fstatSync(opened.handle.fd, { bigint: true });
|
|
4829
5840
|
const guarded = lstatSync(guardPath, { bigint: true });
|
|
@@ -4843,14 +5854,11 @@ var DeviceStore = class _DeviceStore {
|
|
|
4843
5854
|
}
|
|
4844
5855
|
}
|
|
4845
5856
|
async save(record) {
|
|
4846
|
-
|
|
4847
|
-
const storeDir =
|
|
5857
|
+
assertDeviceMetadata(record);
|
|
5858
|
+
const storeDir = path3__default.dirname(this.filePath);
|
|
4848
5859
|
await ensureSecureDir(storeDir, this.secureDirOptions);
|
|
4849
5860
|
await atomicWriteFile(this.filePath, JSON.stringify(record, null, 2), { mode: 384 });
|
|
4850
5861
|
}
|
|
4851
|
-
async clear() {
|
|
4852
|
-
await promises.rm(this.filePath, { force: true });
|
|
4853
|
-
}
|
|
4854
5862
|
async openBounded() {
|
|
4855
5863
|
let namedBefore;
|
|
4856
5864
|
try {
|
|
@@ -4982,6 +5990,7 @@ var RENEW_MARGIN_MS = 60 * 1e3;
|
|
|
4982
5990
|
var AuthManager = class {
|
|
4983
5991
|
constructor(opts) {
|
|
4984
5992
|
this.opts = opts;
|
|
5993
|
+
this.credentials = opts.credentials ?? opts.store.credentials;
|
|
4985
5994
|
}
|
|
4986
5995
|
opts;
|
|
4987
5996
|
record;
|
|
@@ -4991,17 +6000,22 @@ var AuthManager = class {
|
|
|
4991
6000
|
stopped = false;
|
|
4992
6001
|
pairing = false;
|
|
4993
6002
|
credentialMutationTail = Promise.resolve();
|
|
6003
|
+
credentials;
|
|
4994
6004
|
get deviceId() {
|
|
4995
6005
|
return this.record?.deviceId;
|
|
4996
6006
|
}
|
|
4997
6007
|
isRevoked() {
|
|
4998
6008
|
return this.revoked;
|
|
4999
6009
|
}
|
|
6010
|
+
/** Internal signer read: always recompose metadata with the current OS secret authority. */
|
|
6011
|
+
async readCurrent() {
|
|
6012
|
+
return this.loadRecord();
|
|
6013
|
+
}
|
|
5000
6014
|
/** Load a previously-paired device record from disk, if any (idempotent — a second call is a no-op once loaded). */
|
|
5001
6015
|
async loadExisting() {
|
|
5002
6016
|
this.stopped = false;
|
|
5003
6017
|
if (!this.record) {
|
|
5004
|
-
this.record = await this.
|
|
6018
|
+
this.record = await this.loadRecord();
|
|
5005
6019
|
}
|
|
5006
6020
|
if (this.record) this.scheduleProactiveRenewal();
|
|
5007
6021
|
return this.record;
|
|
@@ -5017,7 +6031,7 @@ var AuthManager = class {
|
|
|
5017
6031
|
let existing = this.record;
|
|
5018
6032
|
if (!existing) {
|
|
5019
6033
|
try {
|
|
5020
|
-
existing = await this.
|
|
6034
|
+
existing = await this.loadRecord();
|
|
5021
6035
|
} catch (error) {
|
|
5022
6036
|
if (!(error instanceof DeviceRecordRePairRequiredError)) throw error;
|
|
5023
6037
|
}
|
|
@@ -5029,7 +6043,7 @@ var AuthManager = class {
|
|
|
5029
6043
|
headers: { "content-type": "application/json" },
|
|
5030
6044
|
body: JSON.stringify({
|
|
5031
6045
|
pairingCode,
|
|
5032
|
-
deviceName: this.opts.deviceName ??
|
|
6046
|
+
deviceName: this.opts.deviceName ?? os__default.hostname(),
|
|
5033
6047
|
devicePublicKey: keyPair.publicKeyBase64Url
|
|
5034
6048
|
})
|
|
5035
6049
|
});
|
|
@@ -5037,15 +6051,19 @@ var AuthManager = class {
|
|
|
5037
6051
|
throw new Error(`pairing failed: HTTP ${res.status} ${await safeErrorText(res)}`.trimEnd());
|
|
5038
6052
|
}
|
|
5039
6053
|
const body = PairResponseSchema.parse(await res.json());
|
|
5040
|
-
const
|
|
6054
|
+
const metadata = {
|
|
5041
6055
|
deviceId: body.deviceId,
|
|
5042
6056
|
tenantId: body.tenantId,
|
|
6057
|
+
devicePublicKey: keyPair.publicKeyBase64Url
|
|
6058
|
+
};
|
|
6059
|
+
const record = {
|
|
6060
|
+
...metadata,
|
|
5043
6061
|
accessToken: body.accessToken,
|
|
5044
6062
|
expiresAt: resolvePairExpiry(body.refreshHint),
|
|
5045
|
-
devicePrivateKeyPem: exportPrivateKeyPem(keyPair.privateKey)
|
|
5046
|
-
devicePublicKey: keyPair.publicKeyBase64Url
|
|
6063
|
+
devicePrivateKeyPem: exportPrivateKeyPem(keyPair.privateKey)
|
|
5047
6064
|
};
|
|
5048
|
-
await this.opts.store.save(
|
|
6065
|
+
await this.opts.store.save(metadata);
|
|
6066
|
+
await this.credentials.replace(record);
|
|
5049
6067
|
this.record = record;
|
|
5050
6068
|
this.revoked = false;
|
|
5051
6069
|
return record;
|
|
@@ -5110,8 +6128,12 @@ var AuthManager = class {
|
|
|
5110
6128
|
throw new Error(`token renewal (token) failed: HTTP ${tokenRes.status} ${await safeErrorText(tokenRes)}`.trimEnd());
|
|
5111
6129
|
}
|
|
5112
6130
|
const body = await tokenRes.json();
|
|
5113
|
-
const updated = {
|
|
5114
|
-
|
|
6131
|
+
const updated = {
|
|
6132
|
+
...record,
|
|
6133
|
+
accessToken: body.accessToken,
|
|
6134
|
+
expiresAt: body.expiresAt
|
|
6135
|
+
};
|
|
6136
|
+
await this.credentials.replace(updated);
|
|
5115
6137
|
this.record = updated;
|
|
5116
6138
|
this.scheduleProactiveRenewal();
|
|
5117
6139
|
return updated.accessToken;
|
|
@@ -5148,6 +6170,24 @@ var AuthManager = class {
|
|
|
5148
6170
|
release();
|
|
5149
6171
|
}
|
|
5150
6172
|
}
|
|
6173
|
+
/** Read the current paired authority afresh; metadata without its OS secret is re-pair required. */
|
|
6174
|
+
async loadRecord() {
|
|
6175
|
+
const authority = await this.credentials.read();
|
|
6176
|
+
if (authority === void 0) {
|
|
6177
|
+
if (await this.opts.store.load() === void 0) return void 0;
|
|
6178
|
+
throw new DeviceRecordRePairRequiredError();
|
|
6179
|
+
}
|
|
6180
|
+
const projection = {
|
|
6181
|
+
deviceId: authority.deviceId,
|
|
6182
|
+
tenantId: authority.tenantId,
|
|
6183
|
+
devicePublicKey: authority.devicePublicKey
|
|
6184
|
+
};
|
|
6185
|
+
const current = await this.opts.store.load();
|
|
6186
|
+
if (current === void 0 || current.deviceId !== projection.deviceId || current.tenantId !== projection.tenantId || current.devicePublicKey !== projection.devicePublicKey) {
|
|
6187
|
+
await this.opts.store.save(projection);
|
|
6188
|
+
}
|
|
6189
|
+
return Object.freeze({ ...authority });
|
|
6190
|
+
}
|
|
5151
6191
|
};
|
|
5152
6192
|
function msUntilExpiry(expiresAt) {
|
|
5153
6193
|
return new Date(expiresAt).getTime() - Date.now();
|
|
@@ -5492,25 +6532,25 @@ function mintDeviceAssertion(input) {
|
|
|
5492
6532
|
}
|
|
5493
6533
|
var CONTROL_PROTOCOL_VERSION = 1;
|
|
5494
6534
|
var HANDSHAKE_TIMEOUT_MS = 3e3;
|
|
5495
|
-
var
|
|
6535
|
+
var UNIX_SOCKET_PATH_SOFT_LIMIT2 = 100;
|
|
5496
6536
|
var CONTROL_SOCKET_FALLBACK_ROOT = "/tmp";
|
|
5497
6537
|
function shortHash(input) {
|
|
5498
6538
|
return createHash("sha256").update(input, "utf8").digest("hex").slice(0, 16);
|
|
5499
6539
|
}
|
|
5500
6540
|
function controlSocketPath(storeDir) {
|
|
5501
|
-
const candidate =
|
|
5502
|
-
if (Buffer.byteLength(candidate, "utf8") <=
|
|
5503
|
-
return
|
|
6541
|
+
const candidate = path3__default.join(storeDir, "control.sock");
|
|
6542
|
+
if (Buffer.byteLength(candidate, "utf8") <= UNIX_SOCKET_PATH_SOFT_LIMIT2) return candidate;
|
|
6543
|
+
return path3__default.join(CONTROL_SOCKET_FALLBACK_ROOT, `byok-${shortHash(storeDir)}`, "sock");
|
|
5504
6544
|
}
|
|
5505
6545
|
function controlPipeName(productId, storeDir) {
|
|
5506
|
-
const id = shortHash(`${productId}|${
|
|
6546
|
+
const id = shortHash(`${productId}|${path3__default.resolve(storeDir)}`);
|
|
5507
6547
|
return `\\\\.\\pipe\\byok-${id}`;
|
|
5508
6548
|
}
|
|
5509
6549
|
function controlEndpointPath(productId, storeDir, platform = process.platform) {
|
|
5510
6550
|
return platform === "win32" ? controlPipeName(productId, storeDir) : controlSocketPath(storeDir);
|
|
5511
6551
|
}
|
|
5512
6552
|
function controlTokenPath(storeDir) {
|
|
5513
|
-
return
|
|
6553
|
+
return path3__default.join(storeDir, "control.token");
|
|
5514
6554
|
}
|
|
5515
6555
|
var SERVER_PROOF_LABEL = "byok-control-server|";
|
|
5516
6556
|
var CLIENT_AUTH_LABEL = "byok-control-client|";
|
|
@@ -5592,7 +6632,15 @@ var NdjsonLineReader = class {
|
|
|
5592
6632
|
}
|
|
5593
6633
|
return lines;
|
|
5594
6634
|
}
|
|
5595
|
-
};
|
|
6635
|
+
};
|
|
6636
|
+
var ENROLLMENT_PAIRING_CODE_MAX_BYTES = 1024;
|
|
6637
|
+
function parseEnrollmentPairParams(value) {
|
|
6638
|
+
if (!isRecord3(value) || Object.keys(value).some((key) => key !== "pairingCode")) return void 0;
|
|
6639
|
+
if (typeof value.pairingCode !== "string" || value.pairingCode.length === 0 || Buffer.byteLength(value.pairingCode, "utf8") > ENROLLMENT_PAIRING_CODE_MAX_BYTES) {
|
|
6640
|
+
return void 0;
|
|
6641
|
+
}
|
|
6642
|
+
return { pairingCode: value.pairingCode };
|
|
6643
|
+
}
|
|
5596
6644
|
function parseToolsetsReloadParams(value) {
|
|
5597
6645
|
if (!isRecord3(value) || Object.keys(value).some((key) => key !== "expectedRevision" && key !== "mcpToolsets")) {
|
|
5598
6646
|
return void 0;
|
|
@@ -5672,7 +6720,7 @@ async function handleStaleUnixSocket(socketPath) {
|
|
|
5672
6720
|
if (alive) throw new AnotherControlServerRunningError(socketPath);
|
|
5673
6721
|
await promises.rm(socketPath, { force: true });
|
|
5674
6722
|
}
|
|
5675
|
-
async function
|
|
6723
|
+
async function assertOwnedPrivateDir2(dir) {
|
|
5676
6724
|
const uid = process.getuid?.();
|
|
5677
6725
|
if (uid === void 0) return;
|
|
5678
6726
|
const st = await promises.lstat(dir);
|
|
@@ -5689,11 +6737,11 @@ async function assertOwnedPrivateDir(dir) {
|
|
|
5689
6737
|
}
|
|
5690
6738
|
async function bindControlEndpoint(server, endpoint) {
|
|
5691
6739
|
if (process.platform !== "win32") {
|
|
5692
|
-
const endpointDir =
|
|
6740
|
+
const endpointDir = path3__default.dirname(endpoint);
|
|
5693
6741
|
await promises.mkdir(endpointDir, { recursive: true, mode: 448 });
|
|
5694
6742
|
await promises.chmod(endpointDir, 448).catch(() => {
|
|
5695
6743
|
});
|
|
5696
|
-
await
|
|
6744
|
+
await assertOwnedPrivateDir2(endpointDir);
|
|
5697
6745
|
await handleStaleUnixSocket(endpoint);
|
|
5698
6746
|
}
|
|
5699
6747
|
await new Promise((resolve, reject) => {
|
|
@@ -6588,7 +7636,7 @@ function toBytes(data, _isBinary) {
|
|
|
6588
7636
|
|
|
6589
7637
|
// src/daemon/connection-manager.ts
|
|
6590
7638
|
function isCursorEnvelopeType(type) {
|
|
6591
|
-
return type.startsWith("task.") || type === "agent.egress.ack" || type === "agent.content.read";
|
|
7639
|
+
return type.startsWith("task.") || type === "agent.egress.ack" || type === "agent.content.read" || type === "agent.home.projection";
|
|
6592
7640
|
}
|
|
6593
7641
|
var ConnectionManager = class {
|
|
6594
7642
|
constructor(opts) {
|
|
@@ -7103,6 +8151,10 @@ var ConnectionManager = class {
|
|
|
7103
8151
|
async process(envelope, tracked) {
|
|
7104
8152
|
const seq = tracked ? envelope.seq : void 0;
|
|
7105
8153
|
try {
|
|
8154
|
+
if (tracked && this.cursor === void 0) {
|
|
8155
|
+
await this.opts.cursorStore.save(this.opts.serverUrl, this.opts.deviceId, 0);
|
|
8156
|
+
this.cursor = 0;
|
|
8157
|
+
}
|
|
7106
8158
|
await this.opts.onEnvelope(envelope);
|
|
7107
8159
|
if (!tracked) return;
|
|
7108
8160
|
this.processedSeqs.add(seq);
|
|
@@ -7332,7 +8384,7 @@ function sameFileState2(left, right) {
|
|
|
7332
8384
|
return left.dev === right.dev && left.ino === right.ino && left.size === right.size && left.mtimeNs === right.mtimeNs && left.ctimeNs === right.ctimeNs;
|
|
7333
8385
|
}
|
|
7334
8386
|
async function openOperationalHealthFile(storeDir) {
|
|
7335
|
-
const filePath =
|
|
8387
|
+
const filePath = path3__default.join(storeDir, OPERATIONAL_HEALTH_FILENAME);
|
|
7336
8388
|
let namedBefore;
|
|
7337
8389
|
try {
|
|
7338
8390
|
namedBefore = await promises.lstat(filePath, { bigint: true });
|
|
@@ -7374,7 +8426,7 @@ var OperationalHealthTracker = class {
|
|
|
7374
8426
|
#writeTail = Promise.resolve();
|
|
7375
8427
|
#started = false;
|
|
7376
8428
|
constructor(storeDir, options = {}) {
|
|
7377
|
-
this.#filePath =
|
|
8429
|
+
this.#filePath = path3__default.join(storeDir, OPERATIONAL_HEALTH_FILENAME);
|
|
7378
8430
|
this.#windowMs = options.windowMs ?? 6e4;
|
|
7379
8431
|
this.#failureThreshold = options.failureThreshold ?? 3;
|
|
7380
8432
|
this.#maxFailures = options.maxFailures ?? 128;
|
|
@@ -7407,539 +8459,203 @@ var OperationalHealthTracker = class {
|
|
|
7407
8459
|
if (loaded.currentRun) {
|
|
7408
8460
|
loaded.crashes.push({ detectedAt: now.toISOString(), previousRunStartedAt: loaded.currentRun.startedAt });
|
|
7409
8461
|
loaded.crashes = loaded.crashes.slice(-this.#maxCrashes);
|
|
7410
|
-
}
|
|
7411
|
-
loaded.currentRun = { id: this.#runId(), startedAt: now.toISOString(), pid: this.#pid };
|
|
7412
|
-
await this.#persist();
|
|
7413
|
-
return this.snapshot();
|
|
7414
|
-
}
|
|
7415
|
-
async recordFailure(source) {
|
|
7416
|
-
if (!this.#state || this.#unavailableReason) return;
|
|
7417
|
-
const now = this.#clock();
|
|
7418
|
-
this.#prune(now);
|
|
7419
|
-
this.#state.failures.push({ at: now.toISOString(), source });
|
|
7420
|
-
this.#state.failures = this.#state.failures.slice(-this.#maxFailures);
|
|
7421
|
-
if (this.#state.failures.length >= this.#failureThreshold || this.#state.state === "recovering") {
|
|
7422
|
-
this.#state.state = "degraded";
|
|
7423
|
-
}
|
|
7424
|
-
await this.#persist();
|
|
7425
|
-
}
|
|
7426
|
-
async recordSuccess(_source) {
|
|
7427
|
-
if (!this.#state || this.#unavailableReason) return;
|
|
7428
|
-
const now = this.#clock();
|
|
7429
|
-
this.#prune(now);
|
|
7430
|
-
if (this.#state.state === "healthy") return;
|
|
7431
|
-
if (this.#state.state === "degraded") {
|
|
7432
|
-
this.#state.state = "recovering";
|
|
7433
|
-
} else if (this.#state.state === "recovering" && this.#state.failures.length < this.#failureThreshold) {
|
|
7434
|
-
this.#state.state = "healthy";
|
|
7435
|
-
} else {
|
|
7436
|
-
return;
|
|
7437
|
-
}
|
|
7438
|
-
await this.#persist();
|
|
7439
|
-
}
|
|
7440
|
-
async markCleanStop() {
|
|
7441
|
-
if (!this.#state || this.#unavailableReason || !this.#started) return;
|
|
7442
|
-
this.#state.currentRun = void 0;
|
|
7443
|
-
await this.#persist();
|
|
7444
|
-
this.#started = false;
|
|
7445
|
-
}
|
|
7446
|
-
snapshot() {
|
|
7447
|
-
if (this.#unavailableReason) return { availability: "unavailable", reason: this.#unavailableReason };
|
|
7448
|
-
if (!this.#state) return { availability: "unavailable", reason: "operational health has not been loaded" };
|
|
7449
|
-
this.#prune(this.#clock());
|
|
7450
|
-
return {
|
|
7451
|
-
availability: "available",
|
|
7452
|
-
state: this.#state.state,
|
|
7453
|
-
failureCount: this.#state.failures.length,
|
|
7454
|
-
windowMs: this.#windowMs,
|
|
7455
|
-
failureThreshold: this.#failureThreshold,
|
|
7456
|
-
crashCount: this.#state.crashes.length,
|
|
7457
|
-
...this.#state.crashes.at(-1) ? { lastCrashAt: this.#state.crashes.at(-1).detectedAt } : {},
|
|
7458
|
-
...this.#state.currentRun ? { currentRunStartedAt: this.#state.currentRun.startedAt } : {}
|
|
7459
|
-
};
|
|
7460
|
-
}
|
|
7461
|
-
async #load() {
|
|
7462
|
-
let opened;
|
|
7463
|
-
try {
|
|
7464
|
-
opened = await openOperationalHealthFile(path.dirname(this.#filePath));
|
|
7465
|
-
} catch (err) {
|
|
7466
|
-
throw new Error("operational health state could not be read");
|
|
7467
|
-
}
|
|
7468
|
-
if (!opened) return { version: 1, state: "healthy", failures: [], crashes: [] };
|
|
7469
|
-
try {
|
|
7470
|
-
const inspected = await readOperationalHealthHandle(opened.handle, opened.stat);
|
|
7471
|
-
if (inspected.status !== "read") throw new Error(inspected.reason);
|
|
7472
|
-
let parsed;
|
|
7473
|
-
try {
|
|
7474
|
-
parsed = JSON.parse(inspected.raw);
|
|
7475
|
-
} catch {
|
|
7476
|
-
throw new Error("operational health state is corrupt JSON");
|
|
7477
|
-
}
|
|
7478
|
-
if (!isHealthFile(parsed)) throw new Error("operational health state has an invalid shape");
|
|
7479
|
-
return parsed;
|
|
7480
|
-
} finally {
|
|
7481
|
-
await opened.handle.close();
|
|
7482
|
-
}
|
|
7483
|
-
}
|
|
7484
|
-
#prune(now) {
|
|
7485
|
-
if (!this.#state) return;
|
|
7486
|
-
const nowMs = now.getTime();
|
|
7487
|
-
const cutoff = nowMs - this.#windowMs;
|
|
7488
|
-
this.#state.failures = this.#state.failures.filter((event) => {
|
|
7489
|
-
const eventMs = Date.parse(event.at);
|
|
7490
|
-
return eventMs >= cutoff && eventMs <= nowMs;
|
|
7491
|
-
});
|
|
7492
|
-
if (this.#state.state === "recovering" && this.#state.failures.length < this.#failureThreshold) {
|
|
7493
|
-
this.#state.state = "healthy";
|
|
7494
|
-
}
|
|
7495
|
-
}
|
|
7496
|
-
async #persist() {
|
|
7497
|
-
if (!this.#state) return;
|
|
7498
|
-
const body = JSON.stringify(this.#state, null, 2);
|
|
7499
|
-
this.#writeTail = this.#writeTail.then(async () => {
|
|
7500
|
-
await ensureSecureDir(path.dirname(this.#filePath));
|
|
7501
|
-
await atomicWriteFile(this.#filePath, body, { mode: 384, fsync: true });
|
|
7502
|
-
});
|
|
7503
|
-
try {
|
|
7504
|
-
await this.#writeTail;
|
|
7505
|
-
} catch (err) {
|
|
7506
|
-
this.#unavailableReason = "operational health state could not be persisted";
|
|
7507
|
-
throw err;
|
|
7508
|
-
}
|
|
7509
|
-
}
|
|
7510
|
-
};
|
|
7511
|
-
async function inspectOperationalHealthHandle(handle, expected) {
|
|
7512
|
-
const read = await readOperationalHealthHandle(handle, expected);
|
|
7513
|
-
if (read.status !== "read") return read;
|
|
7514
|
-
const { raw, sizeBytes } = read;
|
|
7515
|
-
let parsed;
|
|
7516
|
-
try {
|
|
7517
|
-
parsed = JSON.parse(raw);
|
|
7518
|
-
} catch {
|
|
7519
|
-
return { status: "corrupt", sizeBytes, reason: "operational health state is corrupt JSON" };
|
|
7520
|
-
}
|
|
7521
|
-
if (!isHealthFile(parsed)) {
|
|
7522
|
-
return { status: "corrupt", sizeBytes, reason: "operational health state has an invalid shape" };
|
|
7523
|
-
}
|
|
7524
|
-
return {
|
|
7525
|
-
status: "valid",
|
|
7526
|
-
sizeBytes,
|
|
7527
|
-
state: parsed.state,
|
|
7528
|
-
failureCount: parsed.failures.length,
|
|
7529
|
-
crashCount: parsed.crashes.length,
|
|
7530
|
-
...parsed.currentRun ? { currentRunStartedAt: new Date(Date.parse(parsed.currentRun.startedAt)).toISOString() } : {},
|
|
7531
|
-
...parsed.crashes.at(-1) ? { lastCrashAt: new Date(Date.parse(parsed.crashes.at(-1).detectedAt)).toISOString() } : {}
|
|
7532
|
-
};
|
|
7533
|
-
}
|
|
7534
|
-
async function readOperationalHealthHandle(handle, expected) {
|
|
7535
|
-
let before;
|
|
7536
|
-
try {
|
|
7537
|
-
before = await handle.stat({ bigint: true });
|
|
7538
|
-
} catch {
|
|
7539
|
-
return { status: "unavailable", reason: "operational health state could not be inspected" };
|
|
7540
|
-
}
|
|
7541
|
-
const sizeBytes = Number(before.size);
|
|
7542
|
-
if (expected && !sameFileState2(before, expected)) {
|
|
7543
|
-
return { status: "unavailable", sizeBytes, reason: "operational health handle identity changed before inspection" };
|
|
7544
|
-
}
|
|
7545
|
-
if (!before.isFile()) {
|
|
7546
|
-
return { status: "unavailable", sizeBytes, reason: "operational health state is not a regular file" };
|
|
7547
|
-
}
|
|
7548
|
-
if (sizeBytes > MAX_OPERATIONAL_HEALTH_FILE_BYTES) {
|
|
7549
|
-
return {
|
|
7550
|
-
status: "unavailable",
|
|
7551
|
-
sizeBytes,
|
|
7552
|
-
reason: "operational health state exceeds the 1 MiB read limit; corruption is unconfirmed"
|
|
7553
|
-
};
|
|
7554
|
-
}
|
|
7555
|
-
try {
|
|
7556
|
-
const buffer = Buffer.alloc(sizeBytes);
|
|
7557
|
-
const { bytesRead } = await handle.read(buffer, 0, sizeBytes, 0);
|
|
7558
|
-
const after = await handle.stat({ bigint: true });
|
|
7559
|
-
if (bytesRead !== sizeBytes || after.size !== before.size || after.mtimeNs !== before.mtimeNs || after.ctimeNs !== before.ctimeNs) {
|
|
7560
|
-
return { status: "unavailable", sizeBytes: Number(after.size), reason: "operational health state changed during inspection" };
|
|
7561
|
-
}
|
|
7562
|
-
return { status: "read", raw: buffer.toString("utf8"), sizeBytes };
|
|
7563
|
-
} catch {
|
|
7564
|
-
return { status: "unavailable", sizeBytes, reason: "operational health state could not be read" };
|
|
7565
|
-
}
|
|
7566
|
-
}
|
|
7567
|
-
async function inspectOperationalHealthFile(storeDir) {
|
|
7568
|
-
let opened;
|
|
7569
|
-
try {
|
|
7570
|
-
opened = await openOperationalHealthFile(storeDir);
|
|
7571
|
-
} catch {
|
|
7572
|
-
return { status: "unavailable", reason: "operational health state could not be opened safely" };
|
|
7573
|
-
}
|
|
7574
|
-
if (!opened) return { status: "missing" };
|
|
7575
|
-
try {
|
|
7576
|
-
return await inspectOperationalHealthHandle(opened.handle, opened.stat);
|
|
7577
|
-
} finally {
|
|
7578
|
-
await opened.handle.close();
|
|
7579
|
-
}
|
|
7580
|
-
}
|
|
7581
|
-
function isHealthFile(value) {
|
|
7582
|
-
if (typeof value !== "object" || value === null) return false;
|
|
7583
|
-
const candidate = value;
|
|
7584
|
-
const keys = Object.keys(candidate);
|
|
7585
|
-
if (keys.some((key) => !["version", "state", "failures", "crashes", "currentRun"].includes(key))) return false;
|
|
7586
|
-
if (candidate.version !== 1 || !["healthy", "degraded", "recovering"].includes(String(candidate.state))) return false;
|
|
7587
|
-
if (!Array.isArray(candidate.failures) || !candidate.failures.every(isFailureEvent)) return false;
|
|
7588
|
-
if (!Array.isArray(candidate.crashes) || !candidate.crashes.every(isCrashRecord)) return false;
|
|
7589
|
-
if (candidate.currentRun !== void 0 && !isRunMarker(candidate.currentRun)) return false;
|
|
7590
|
-
return true;
|
|
7591
|
-
}
|
|
7592
|
-
function isFailureEvent(value) {
|
|
7593
|
-
if (typeof value !== "object" || value === null) return false;
|
|
7594
|
-
const event = value;
|
|
7595
|
-
return Object.keys(event).every((key) => ["at", "source"].includes(key)) && typeof event.at === "string" && Number.isFinite(Date.parse(event.at)) && ["reconnect", "upload", "maintenance", "lifecycle"].includes(String(event.source));
|
|
7596
|
-
}
|
|
7597
|
-
function isCrashRecord(value) {
|
|
7598
|
-
if (typeof value !== "object" || value === null) return false;
|
|
7599
|
-
const record = value;
|
|
7600
|
-
return Object.keys(record).every((key) => ["detectedAt", "previousRunStartedAt"].includes(key)) && typeof record.detectedAt === "string" && Number.isFinite(Date.parse(record.detectedAt)) && typeof record.previousRunStartedAt === "string" && Number.isFinite(Date.parse(record.previousRunStartedAt));
|
|
7601
|
-
}
|
|
7602
|
-
function isRunMarker(value) {
|
|
7603
|
-
if (typeof value !== "object" || value === null) return false;
|
|
7604
|
-
const marker = value;
|
|
7605
|
-
return Object.keys(marker).every((key) => ["id", "startedAt", "pid"].includes(key)) && typeof marker.id === "string" && typeof marker.startedAt === "string" && Number.isFinite(Date.parse(marker.startedAt)) && typeof marker.pid === "number" && Number.isSafeInteger(marker.pid);
|
|
7606
|
-
}
|
|
7607
|
-
var DAEMON_OWNER_FILENAME = "daemon-owner.json";
|
|
7608
|
-
var RECLAIM_FILENAME = `${DAEMON_OWNER_FILENAME}.reclaim`;
|
|
7609
|
-
var MAX_OWNER_BYTES = 4096;
|
|
7610
|
-
var RECLAIM_MALFORMED_GRACE_MS = 3e4;
|
|
7611
|
-
var SELF_PROCESS_STARTED_AT = new Date(Date.now() - process.uptime() * 1e3).toISOString();
|
|
7612
|
-
function endOwnershipProbe(socket, response) {
|
|
7613
|
-
socket.on("error", () => {
|
|
7614
|
-
});
|
|
7615
|
-
if (response === void 0) socket.end();
|
|
7616
|
-
else socket.end(response);
|
|
7617
|
-
}
|
|
7618
|
-
var STORE_MUTEX_ID_PREFIX = "byok-store-mutex-v1:";
|
|
7619
|
-
var STORE_MUTEX_PROBE_TIMEOUT_MS = 1e3;
|
|
7620
|
-
var STORE_MUTEX_SOCKET_FILENAME = "mutex.sock";
|
|
7621
|
-
var UNIX_SOCKET_PATH_SOFT_LIMIT2 = 100;
|
|
7622
|
-
var STORE_MUTEX_FALLBACK_ROOT = "/tmp";
|
|
7623
|
-
function storeMutexIdentity(canonicalStoreDir) {
|
|
7624
|
-
return createHash("sha256").update(canonicalStoreDir).digest("hex");
|
|
7625
|
-
}
|
|
7626
|
-
function storeMutexEndpoint(canonicalStoreDir, identity, platform = process.platform) {
|
|
7627
|
-
if (platform === "win32") return `\\\\.\\pipe\\byok-store-mutex-${identity.slice(0, 16)}`;
|
|
7628
|
-
const candidate = path.join(canonicalStoreDir, STORE_MUTEX_SOCKET_FILENAME);
|
|
7629
|
-
if (Buffer.byteLength(candidate, "utf8") <= UNIX_SOCKET_PATH_SOFT_LIMIT2) return candidate;
|
|
7630
|
-
return path.join(STORE_MUTEX_FALLBACK_ROOT, `byok-store-mutex-${identity.slice(0, 16)}`, "sock");
|
|
7631
|
-
}
|
|
7632
|
-
var DaemonOwnerActiveError = class extends Error {
|
|
7633
|
-
constructor(role) {
|
|
7634
|
-
super(`store mutation lease is already held by an active ${role} process`);
|
|
7635
|
-
this.role = role;
|
|
7636
|
-
this.name = "DaemonOwnerActiveError";
|
|
7637
|
-
}
|
|
7638
|
-
role;
|
|
7639
|
-
};
|
|
7640
|
-
function isOwnerRecord(value) {
|
|
7641
|
-
if (typeof value !== "object" || value === null) return false;
|
|
7642
|
-
const candidate = value;
|
|
7643
|
-
return candidate.version === 2 && Number.isSafeInteger(candidate.pid) && (candidate.pid ?? 0) > 0 && typeof candidate.nonce === "string" && candidate.nonce.length === 36 && (candidate.role === "daemon" || candidate.role === "doctor") && typeof candidate.acquiredAt === "string" && Number.isFinite(Date.parse(candidate.acquiredAt)) && typeof candidate.processStartedAt === "string" && Number.isFinite(Date.parse(candidate.processStartedAt)) && Number.isSafeInteger(candidate.livenessPort) && (candidate.livenessPort ?? 0) > 0 && (candidate.livenessPort ?? 0) <= 65535;
|
|
7644
|
-
}
|
|
7645
|
-
async function readOwner(filePath) {
|
|
7646
|
-
let namedBefore;
|
|
7647
|
-
try {
|
|
7648
|
-
namedBefore = await promises.lstat(filePath, { bigint: true });
|
|
7649
|
-
} catch (err) {
|
|
7650
|
-
if (err.code === "ENOENT") return void 0;
|
|
7651
|
-
throw err;
|
|
7652
|
-
}
|
|
7653
|
-
if (!namedBefore.isFile() || namedBefore.isSymbolicLink()) {
|
|
7654
|
-
throw new Error("store mutation owner path is not a real regular file");
|
|
7655
|
-
}
|
|
7656
|
-
let handle;
|
|
7657
|
-
try {
|
|
7658
|
-
handle = await promises.open(
|
|
7659
|
-
filePath,
|
|
7660
|
-
constants.O_RDONLY | (constants.O_NONBLOCK ?? 0) | (constants.O_NOFOLLOW ?? 0)
|
|
7661
|
-
);
|
|
7662
|
-
} catch (err) {
|
|
7663
|
-
if (err.code === "ENOENT") return void 0;
|
|
7664
|
-
throw err;
|
|
8462
|
+
}
|
|
8463
|
+
loaded.currentRun = { id: this.#runId(), startedAt: now.toISOString(), pid: this.#pid };
|
|
8464
|
+
await this.#persist();
|
|
8465
|
+
return this.snapshot();
|
|
7665
8466
|
}
|
|
7666
|
-
|
|
7667
|
-
|
|
7668
|
-
const
|
|
7669
|
-
|
|
7670
|
-
|
|
8467
|
+
async recordFailure(source) {
|
|
8468
|
+
if (!this.#state || this.#unavailableReason) return;
|
|
8469
|
+
const now = this.#clock();
|
|
8470
|
+
this.#prune(now);
|
|
8471
|
+
this.#state.failures.push({ at: now.toISOString(), source });
|
|
8472
|
+
this.#state.failures = this.#state.failures.slice(-this.#maxFailures);
|
|
8473
|
+
if (this.#state.failures.length >= this.#failureThreshold || this.#state.state === "recovering") {
|
|
8474
|
+
this.#state.state = "degraded";
|
|
7671
8475
|
}
|
|
7672
|
-
|
|
7673
|
-
|
|
7674
|
-
|
|
7675
|
-
|
|
7676
|
-
const
|
|
7677
|
-
|
|
7678
|
-
if (
|
|
7679
|
-
|
|
8476
|
+
await this.#persist();
|
|
8477
|
+
}
|
|
8478
|
+
async recordSuccess(_source) {
|
|
8479
|
+
if (!this.#state || this.#unavailableReason) return;
|
|
8480
|
+
const now = this.#clock();
|
|
8481
|
+
this.#prune(now);
|
|
8482
|
+
if (this.#state.state === "healthy") return;
|
|
8483
|
+
if (this.#state.state === "degraded") {
|
|
8484
|
+
this.#state.state = "recovering";
|
|
8485
|
+
} else if (this.#state.state === "recovering" && this.#state.failures.length < this.#failureThreshold) {
|
|
8486
|
+
this.#state.state = "healthy";
|
|
8487
|
+
} else {
|
|
8488
|
+
return;
|
|
7680
8489
|
}
|
|
7681
|
-
|
|
7682
|
-
const parsed = JSON.parse(raw);
|
|
7683
|
-
return isOwnerRecord(parsed) ? parsed : void 0;
|
|
7684
|
-
} catch (err) {
|
|
7685
|
-
if (err instanceof Error && err.message.startsWith("store mutation owner path changed")) throw err;
|
|
7686
|
-
return void 0;
|
|
7687
|
-
} finally {
|
|
7688
|
-
await handle.close();
|
|
8490
|
+
await this.#persist();
|
|
7689
8491
|
}
|
|
7690
|
-
|
|
7691
|
-
|
|
7692
|
-
|
|
7693
|
-
|
|
7694
|
-
|
|
7695
|
-
} catch (err) {
|
|
7696
|
-
return err.code === "EPERM";
|
|
8492
|
+
async markCleanStop() {
|
|
8493
|
+
if (!this.#state || this.#unavailableReason || !this.#started) return;
|
|
8494
|
+
this.#state.currentRun = void 0;
|
|
8495
|
+
await this.#persist();
|
|
8496
|
+
this.#started = false;
|
|
7697
8497
|
}
|
|
7698
|
-
|
|
7699
|
-
|
|
7700
|
-
|
|
7701
|
-
|
|
7702
|
-
|
|
7703
|
-
|
|
7704
|
-
|
|
7705
|
-
|
|
7706
|
-
|
|
7707
|
-
|
|
7708
|
-
|
|
7709
|
-
|
|
8498
|
+
snapshot() {
|
|
8499
|
+
if (this.#unavailableReason) return { availability: "unavailable", reason: this.#unavailableReason };
|
|
8500
|
+
if (!this.#state) return { availability: "unavailable", reason: "operational health has not been loaded" };
|
|
8501
|
+
this.#prune(this.#clock());
|
|
8502
|
+
return {
|
|
8503
|
+
availability: "available",
|
|
8504
|
+
state: this.#state.state,
|
|
8505
|
+
failureCount: this.#state.failures.length,
|
|
8506
|
+
windowMs: this.#windowMs,
|
|
8507
|
+
failureThreshold: this.#failureThreshold,
|
|
8508
|
+
crashCount: this.#state.crashes.length,
|
|
8509
|
+
...this.#state.crashes.at(-1) ? { lastCrashAt: this.#state.crashes.at(-1).detectedAt } : {},
|
|
8510
|
+
...this.#state.currentRun ? { currentRunStartedAt: this.#state.currentRun.startedAt } : {}
|
|
7710
8511
|
};
|
|
7711
|
-
|
|
7712
|
-
|
|
7713
|
-
|
|
7714
|
-
|
|
7715
|
-
|
|
7716
|
-
|
|
7717
|
-
|
|
7718
|
-
|
|
7719
|
-
|
|
7720
|
-
|
|
7721
|
-
|
|
7722
|
-
|
|
7723
|
-
|
|
7724
|
-
|
|
7725
|
-
|
|
7726
|
-
|
|
7727
|
-
|
|
7728
|
-
server.removeListener("error", reject);
|
|
7729
|
-
const address = server.address();
|
|
7730
|
-
if (!address || typeof address === "string") {
|
|
7731
|
-
reject(new Error("store mutation liveness listener did not expose a TCP port"));
|
|
7732
|
-
return;
|
|
7733
|
-
}
|
|
7734
|
-
resolve(address.port);
|
|
7735
|
-
});
|
|
7736
|
-
});
|
|
7737
|
-
server.unref();
|
|
7738
|
-
let closed = false;
|
|
7739
|
-
return {
|
|
7740
|
-
port,
|
|
7741
|
-
close: () => new Promise((resolve, reject) => {
|
|
7742
|
-
if (closed) {
|
|
7743
|
-
resolve();
|
|
7744
|
-
return;
|
|
8512
|
+
}
|
|
8513
|
+
async #load() {
|
|
8514
|
+
let opened;
|
|
8515
|
+
try {
|
|
8516
|
+
opened = await openOperationalHealthFile(path3__default.dirname(this.#filePath));
|
|
8517
|
+
} catch (err) {
|
|
8518
|
+
throw new Error("operational health state could not be read");
|
|
8519
|
+
}
|
|
8520
|
+
if (!opened) return { version: 1, state: "healthy", failures: [], crashes: [] };
|
|
8521
|
+
try {
|
|
8522
|
+
const inspected = await readOperationalHealthHandle(opened.handle, opened.stat);
|
|
8523
|
+
if (inspected.status !== "read") throw new Error(inspected.reason);
|
|
8524
|
+
let parsed;
|
|
8525
|
+
try {
|
|
8526
|
+
parsed = JSON.parse(inspected.raw);
|
|
8527
|
+
} catch {
|
|
8528
|
+
throw new Error("operational health state is corrupt JSON");
|
|
7745
8529
|
}
|
|
7746
|
-
|
|
7747
|
-
|
|
7748
|
-
}
|
|
7749
|
-
|
|
7750
|
-
}
|
|
7751
|
-
async function probeStoreMutex(endpoint, identity) {
|
|
7752
|
-
return new Promise((resolve) => {
|
|
7753
|
-
const socket = createConnection(endpoint);
|
|
7754
|
-
let settled = false;
|
|
7755
|
-
let raw = "";
|
|
7756
|
-
const finish = (result) => {
|
|
7757
|
-
if (settled) return;
|
|
7758
|
-
settled = true;
|
|
7759
|
-
clearTimeout(timer);
|
|
7760
|
-
socket.removeAllListeners();
|
|
7761
|
-
socket.destroy();
|
|
7762
|
-
resolve(result);
|
|
7763
|
-
};
|
|
7764
|
-
const timer = setTimeout(() => finish({ kind: "occupied" }), STORE_MUTEX_PROBE_TIMEOUT_MS);
|
|
7765
|
-
socket.setEncoding("utf8");
|
|
7766
|
-
socket.on("data", (chunk) => {
|
|
7767
|
-
raw += chunk;
|
|
7768
|
-
if (raw.length > STORE_MUTEX_ID_PREFIX.length + 64 + 1) finish({ kind: "occupied" });
|
|
7769
|
-
});
|
|
7770
|
-
socket.once("end", () => finish(raw.trimEnd() === `${STORE_MUTEX_ID_PREFIX}${identity}` ? { kind: "holder" } : { kind: "occupied" }));
|
|
7771
|
-
socket.once(
|
|
7772
|
-
"error",
|
|
7773
|
-
(err) => finish(err.code === "ECONNREFUSED" || err.code === "ENOENT" ? { kind: "unbound" } : { kind: "occupied" })
|
|
7774
|
-
);
|
|
7775
|
-
});
|
|
7776
|
-
}
|
|
7777
|
-
async function clearStaleStoreMutexSocket(endpoint, identity) {
|
|
7778
|
-
let stat;
|
|
7779
|
-
try {
|
|
7780
|
-
stat = await promises.lstat(endpoint);
|
|
7781
|
-
} catch (err) {
|
|
7782
|
-
if (err.code === "ENOENT") return;
|
|
7783
|
-
throw err;
|
|
8530
|
+
if (!isHealthFile(parsed)) throw new Error("operational health state has an invalid shape");
|
|
8531
|
+
return parsed;
|
|
8532
|
+
} finally {
|
|
8533
|
+
await opened.handle.close();
|
|
8534
|
+
}
|
|
7784
8535
|
}
|
|
7785
|
-
|
|
7786
|
-
|
|
7787
|
-
|
|
7788
|
-
|
|
7789
|
-
|
|
7790
|
-
|
|
7791
|
-
|
|
7792
|
-
|
|
7793
|
-
|
|
7794
|
-
|
|
8536
|
+
#prune(now) {
|
|
8537
|
+
if (!this.#state) return;
|
|
8538
|
+
const nowMs = now.getTime();
|
|
8539
|
+
const cutoff = nowMs - this.#windowMs;
|
|
8540
|
+
this.#state.failures = this.#state.failures.filter((event) => {
|
|
8541
|
+
const eventMs = Date.parse(event.at);
|
|
8542
|
+
return eventMs >= cutoff && eventMs <= nowMs;
|
|
8543
|
+
});
|
|
8544
|
+
if (this.#state.state === "recovering" && this.#state.failures.length < this.#failureThreshold) {
|
|
8545
|
+
this.#state.state = "healthy";
|
|
8546
|
+
}
|
|
7795
8547
|
}
|
|
7796
|
-
|
|
7797
|
-
|
|
7798
|
-
|
|
7799
|
-
|
|
7800
|
-
|
|
7801
|
-
|
|
7802
|
-
|
|
7803
|
-
|
|
7804
|
-
await
|
|
7805
|
-
|
|
8548
|
+
async #persist() {
|
|
8549
|
+
if (!this.#state) return;
|
|
8550
|
+
const body = JSON.stringify(this.#state, null, 2);
|
|
8551
|
+
this.#writeTail = this.#writeTail.then(async () => {
|
|
8552
|
+
await ensureSecureDir(path3__default.dirname(this.#filePath));
|
|
8553
|
+
await atomicWriteFile(this.#filePath, body, { mode: 384, fsync: true });
|
|
8554
|
+
});
|
|
8555
|
+
try {
|
|
8556
|
+
await this.#writeTail;
|
|
8557
|
+
} catch (err) {
|
|
8558
|
+
this.#unavailableReason = "operational health state could not be persisted";
|
|
8559
|
+
throw err;
|
|
7806
8560
|
}
|
|
7807
|
-
await clearStaleStoreMutexSocket(endpoint, identity);
|
|
7808
8561
|
}
|
|
7809
|
-
|
|
7810
|
-
|
|
8562
|
+
};
|
|
8563
|
+
async function inspectOperationalHealthHandle(handle, expected) {
|
|
8564
|
+
const read = await readOperationalHealthHandle(handle, expected);
|
|
8565
|
+
if (read.status !== "read") return read;
|
|
8566
|
+
const { raw, sizeBytes } = read;
|
|
8567
|
+
let parsed;
|
|
7811
8568
|
try {
|
|
7812
|
-
|
|
7813
|
-
|
|
7814
|
-
|
|
7815
|
-
|
|
7816
|
-
|
|
7817
|
-
|
|
7818
|
-
});
|
|
7819
|
-
} catch (err) {
|
|
7820
|
-
if (err.code === "EADDRINUSE") throw new DaemonOwnerActiveError("unknown");
|
|
7821
|
-
throw err;
|
|
8569
|
+
parsed = JSON.parse(raw);
|
|
8570
|
+
} catch {
|
|
8571
|
+
return { status: "corrupt", sizeBytes, reason: "operational health state is corrupt JSON" };
|
|
8572
|
+
}
|
|
8573
|
+
if (!isHealthFile(parsed)) {
|
|
8574
|
+
return { status: "corrupt", sizeBytes, reason: "operational health state has an invalid shape" };
|
|
7822
8575
|
}
|
|
7823
|
-
if (!isPipe) await promises.chmod(endpoint, 384).catch(() => void 0);
|
|
7824
|
-
server.unref();
|
|
7825
|
-
let closed = false;
|
|
7826
8576
|
return {
|
|
7827
|
-
|
|
7828
|
-
|
|
7829
|
-
|
|
7830
|
-
|
|
7831
|
-
|
|
7832
|
-
|
|
7833
|
-
}
|
|
8577
|
+
status: "valid",
|
|
8578
|
+
sizeBytes,
|
|
8579
|
+
state: parsed.state,
|
|
8580
|
+
failureCount: parsed.failures.length,
|
|
8581
|
+
crashCount: parsed.crashes.length,
|
|
8582
|
+
...parsed.currentRun ? { currentRunStartedAt: new Date(Date.parse(parsed.currentRun.startedAt)).toISOString() } : {},
|
|
8583
|
+
...parsed.crashes.at(-1) ? { lastCrashAt: new Date(Date.parse(parsed.crashes.at(-1).detectedAt)).toISOString() } : {}
|
|
7834
8584
|
};
|
|
7835
8585
|
}
|
|
7836
|
-
async function
|
|
7837
|
-
let
|
|
8586
|
+
async function readOperationalHealthHandle(handle, expected) {
|
|
8587
|
+
let before;
|
|
7838
8588
|
try {
|
|
7839
|
-
|
|
7840
|
-
} catch
|
|
7841
|
-
|
|
7842
|
-
|
|
8589
|
+
before = await handle.stat({ bigint: true });
|
|
8590
|
+
} catch {
|
|
8591
|
+
return { status: "unavailable", reason: "operational health state could not be inspected" };
|
|
8592
|
+
}
|
|
8593
|
+
const sizeBytes = Number(before.size);
|
|
8594
|
+
if (expected && !sameFileState2(before, expected)) {
|
|
8595
|
+
return { status: "unavailable", sizeBytes, reason: "operational health handle identity changed before inspection" };
|
|
8596
|
+
}
|
|
8597
|
+
if (!before.isFile()) {
|
|
8598
|
+
return { status: "unavailable", sizeBytes, reason: "operational health state is not a regular file" };
|
|
8599
|
+
}
|
|
8600
|
+
if (sizeBytes > MAX_OPERATIONAL_HEALTH_FILE_BYTES) {
|
|
8601
|
+
return {
|
|
8602
|
+
status: "unavailable",
|
|
8603
|
+
sizeBytes,
|
|
8604
|
+
reason: "operational health state exceeds the 1 MiB read limit; corruption is unconfirmed"
|
|
8605
|
+
};
|
|
7843
8606
|
}
|
|
7844
|
-
if (!stat.isFile()) throw new Error("store mutation reclaim marker is not a regular file");
|
|
7845
|
-
const owner = await readOwner(reclaimPath);
|
|
7846
|
-
if (owner) return processOwnsRecord(owner);
|
|
7847
|
-
return Date.now() - stat.mtimeMs <= RECLAIM_MALFORMED_GRACE_MS;
|
|
7848
|
-
}
|
|
7849
|
-
async function createOwner(filePath, record) {
|
|
7850
|
-
const tempPath = `${filePath}.${record.pid}.${record.nonce}.tmp`;
|
|
7851
|
-
let handle;
|
|
7852
8607
|
try {
|
|
7853
|
-
|
|
7854
|
-
await handle.
|
|
7855
|
-
|
|
7856
|
-
|
|
7857
|
-
|
|
7858
|
-
await handle.close();
|
|
7859
|
-
try {
|
|
7860
|
-
await promises.link(tempPath, filePath);
|
|
7861
|
-
return true;
|
|
7862
|
-
} catch (err) {
|
|
7863
|
-
if (err.code === "EEXIST") return false;
|
|
7864
|
-
throw err;
|
|
8608
|
+
const buffer = Buffer.alloc(sizeBytes);
|
|
8609
|
+
const { bytesRead } = await handle.read(buffer, 0, sizeBytes, 0);
|
|
8610
|
+
const after = await handle.stat({ bigint: true });
|
|
8611
|
+
if (bytesRead !== sizeBytes || after.size !== before.size || after.mtimeNs !== before.mtimeNs || after.ctimeNs !== before.ctimeNs) {
|
|
8612
|
+
return { status: "unavailable", sizeBytes: Number(after.size), reason: "operational health state changed during inspection" };
|
|
7865
8613
|
}
|
|
7866
|
-
|
|
7867
|
-
|
|
7868
|
-
|
|
8614
|
+
return { status: "read", raw: buffer.toString("utf8"), sizeBytes };
|
|
8615
|
+
} catch {
|
|
8616
|
+
return { status: "unavailable", sizeBytes, reason: "operational health state could not be read" };
|
|
7869
8617
|
}
|
|
7870
8618
|
}
|
|
7871
|
-
async function
|
|
7872
|
-
|
|
7873
|
-
const canonicalStoreDir = await promises.realpath(storeDir);
|
|
7874
|
-
const mutex = await acquireStoreMutex(canonicalStoreDir);
|
|
7875
|
-
let liveness;
|
|
8619
|
+
async function inspectOperationalHealthFile(storeDir) {
|
|
8620
|
+
let opened;
|
|
7876
8621
|
try {
|
|
7877
|
-
|
|
7878
|
-
} catch
|
|
7879
|
-
|
|
7880
|
-
throw err;
|
|
8622
|
+
opened = await openOperationalHealthFile(storeDir);
|
|
8623
|
+
} catch {
|
|
8624
|
+
return { status: "unavailable", reason: "operational health state could not be opened safely" };
|
|
7881
8625
|
}
|
|
7882
|
-
|
|
7883
|
-
const reclaimPath = path.join(storeDir, RECLAIM_FILENAME);
|
|
7884
|
-
const record = {
|
|
7885
|
-
version: 2,
|
|
7886
|
-
pid: process.pid,
|
|
7887
|
-
nonce: randomUUID(),
|
|
7888
|
-
role,
|
|
7889
|
-
acquiredAt: clock().toISOString(),
|
|
7890
|
-
processStartedAt: SELF_PROCESS_STARTED_AT,
|
|
7891
|
-
livenessPort: liveness.port
|
|
7892
|
-
};
|
|
8626
|
+
if (!opened) return { status: "missing" };
|
|
7893
8627
|
try {
|
|
7894
|
-
|
|
7895
|
-
|
|
7896
|
-
|
|
7897
|
-
}
|
|
7898
|
-
await promises.rm(reclaimPath, { force: true });
|
|
7899
|
-
if (await createOwner(ownerPath, record)) {
|
|
7900
|
-
let released = false;
|
|
7901
|
-
return {
|
|
7902
|
-
release: async () => {
|
|
7903
|
-
if (released) return;
|
|
7904
|
-
const current = await readOwner(ownerPath);
|
|
7905
|
-
if (current?.nonce !== record.nonce) {
|
|
7906
|
-
throw new Error("store mutation lease identity changed before release");
|
|
7907
|
-
}
|
|
7908
|
-
await promises.rm(ownerPath);
|
|
7909
|
-
released = true;
|
|
7910
|
-
try {
|
|
7911
|
-
await liveness.close();
|
|
7912
|
-
} finally {
|
|
7913
|
-
await mutex.close();
|
|
7914
|
-
}
|
|
7915
|
-
}
|
|
7916
|
-
};
|
|
7917
|
-
}
|
|
7918
|
-
const existing = await readOwner(ownerPath);
|
|
7919
|
-
if (existing && await processOwnsRecord(existing)) throw new DaemonOwnerActiveError(existing.role);
|
|
7920
|
-
if (!await createOwner(reclaimPath, record)) {
|
|
7921
|
-
throw new Error("store mutation lease is being reclaimed; retry after the current operation finishes");
|
|
7922
|
-
}
|
|
7923
|
-
try {
|
|
7924
|
-
const rechecked = await readOwner(ownerPath);
|
|
7925
|
-
if (rechecked && await processOwnsRecord(rechecked)) throw new DaemonOwnerActiveError(rechecked.role);
|
|
7926
|
-
if (rechecked || await promises.stat(ownerPath).then(() => true, (err) => {
|
|
7927
|
-
if (err.code === "ENOENT") return false;
|
|
7928
|
-
throw err;
|
|
7929
|
-
})) {
|
|
7930
|
-
await promises.rm(ownerPath);
|
|
7931
|
-
}
|
|
7932
|
-
} finally {
|
|
7933
|
-
const currentReclaim = await readOwner(reclaimPath);
|
|
7934
|
-
if (currentReclaim?.nonce === record.nonce) await promises.rm(reclaimPath, { force: true });
|
|
7935
|
-
}
|
|
7936
|
-
}
|
|
7937
|
-
} catch (err) {
|
|
7938
|
-
await liveness.close().catch(() => void 0);
|
|
7939
|
-
await mutex.close().catch(() => void 0);
|
|
7940
|
-
throw err;
|
|
8628
|
+
return await inspectOperationalHealthHandle(opened.handle, opened.stat);
|
|
8629
|
+
} finally {
|
|
8630
|
+
await opened.handle.close();
|
|
7941
8631
|
}
|
|
7942
8632
|
}
|
|
8633
|
+
function isHealthFile(value) {
|
|
8634
|
+
if (typeof value !== "object" || value === null) return false;
|
|
8635
|
+
const candidate = value;
|
|
8636
|
+
const keys = Object.keys(candidate);
|
|
8637
|
+
if (keys.some((key) => !["version", "state", "failures", "crashes", "currentRun"].includes(key))) return false;
|
|
8638
|
+
if (candidate.version !== 1 || !["healthy", "degraded", "recovering"].includes(String(candidate.state))) return false;
|
|
8639
|
+
if (!Array.isArray(candidate.failures) || !candidate.failures.every(isFailureEvent)) return false;
|
|
8640
|
+
if (!Array.isArray(candidate.crashes) || !candidate.crashes.every(isCrashRecord)) return false;
|
|
8641
|
+
if (candidate.currentRun !== void 0 && !isRunMarker(candidate.currentRun)) return false;
|
|
8642
|
+
return true;
|
|
8643
|
+
}
|
|
8644
|
+
function isFailureEvent(value) {
|
|
8645
|
+
if (typeof value !== "object" || value === null) return false;
|
|
8646
|
+
const event = value;
|
|
8647
|
+
return Object.keys(event).every((key) => ["at", "source"].includes(key)) && typeof event.at === "string" && Number.isFinite(Date.parse(event.at)) && ["reconnect", "upload", "maintenance", "lifecycle"].includes(String(event.source));
|
|
8648
|
+
}
|
|
8649
|
+
function isCrashRecord(value) {
|
|
8650
|
+
if (typeof value !== "object" || value === null) return false;
|
|
8651
|
+
const record = value;
|
|
8652
|
+
return Object.keys(record).every((key) => ["detectedAt", "previousRunStartedAt"].includes(key)) && typeof record.detectedAt === "string" && Number.isFinite(Date.parse(record.detectedAt)) && typeof record.previousRunStartedAt === "string" && Number.isFinite(Date.parse(record.previousRunStartedAt));
|
|
8653
|
+
}
|
|
8654
|
+
function isRunMarker(value) {
|
|
8655
|
+
if (typeof value !== "object" || value === null) return false;
|
|
8656
|
+
const marker = value;
|
|
8657
|
+
return Object.keys(marker).every((key) => ["id", "startedAt", "pid"].includes(key)) && typeof marker.id === "string" && typeof marker.startedAt === "string" && Number.isFinite(Date.parse(marker.startedAt)) && typeof marker.pid === "number" && Number.isSafeInteger(marker.pid);
|
|
8658
|
+
}
|
|
7943
8659
|
|
|
7944
8660
|
// src/daemon/runtime-capabilities.ts
|
|
7945
8661
|
function toRuntimeInfoCapabilities(caps) {
|
|
@@ -7958,7 +8674,7 @@ var CursorStore = class {
|
|
|
7958
8674
|
storeDir;
|
|
7959
8675
|
fileFor(serverUrl, deviceId) {
|
|
7960
8676
|
const key = createHash("sha256").update(`${serverUrl}::${deviceId}`).digest("hex").slice(0, 32);
|
|
7961
|
-
return
|
|
8677
|
+
return path3__default.join(this.storeDir, `cursor-${key}.json`);
|
|
7962
8678
|
}
|
|
7963
8679
|
async load(serverUrl, deviceId) {
|
|
7964
8680
|
let raw;
|
|
@@ -7978,7 +8694,7 @@ var CursorStore = class {
|
|
|
7978
8694
|
}
|
|
7979
8695
|
async save(serverUrl, deviceId, cursor) {
|
|
7980
8696
|
const file = this.fileFor(serverUrl, deviceId);
|
|
7981
|
-
await promises.mkdir(
|
|
8697
|
+
await promises.mkdir(path3__default.dirname(file), { recursive: true, mode: 448 });
|
|
7982
8698
|
await atomicWriteFile(file, JSON.stringify({ cursor }));
|
|
7983
8699
|
}
|
|
7984
8700
|
/** 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. */
|
|
@@ -8308,7 +9024,7 @@ var SessionWorkspaceStore = class {
|
|
|
8308
9024
|
*/
|
|
8309
9025
|
queue = Promise.resolve();
|
|
8310
9026
|
constructor(storeDir) {
|
|
8311
|
-
this.filePath =
|
|
9027
|
+
this.filePath = path3__default.join(storeDir, "session-workspaces.json");
|
|
8312
9028
|
}
|
|
8313
9029
|
async get(sessionRef) {
|
|
8314
9030
|
return this.enqueue(async () => {
|
|
@@ -8366,7 +9082,7 @@ var SessionWorkspaceStore = class {
|
|
|
8366
9082
|
}
|
|
8367
9083
|
}
|
|
8368
9084
|
async save(all) {
|
|
8369
|
-
const dir =
|
|
9085
|
+
const dir = path3__default.dirname(this.filePath);
|
|
8370
9086
|
await promises.mkdir(dir, { recursive: true, mode: 448 });
|
|
8371
9087
|
const tmpPath = `${this.filePath}.${process.pid}-${tmpSeq2++}.tmp`;
|
|
8372
9088
|
try {
|
|
@@ -8471,9 +9187,9 @@ function isSqliteAvailable() {
|
|
|
8471
9187
|
}
|
|
8472
9188
|
}
|
|
8473
9189
|
var SECURE_FILE_MODE = 384;
|
|
8474
|
-
function openJournalDatabase(
|
|
9190
|
+
function openJournalDatabase(path36, busyTimeoutMs, faults) {
|
|
8475
9191
|
const { DatabaseSync } = loadSqliteModule();
|
|
8476
|
-
const db = new DatabaseSync(
|
|
9192
|
+
const db = new DatabaseSync(path36, { timeout: busyTimeoutMs });
|
|
8477
9193
|
try {
|
|
8478
9194
|
faults?.onStep?.("after-open");
|
|
8479
9195
|
db.exec("PRAGMA auto_vacuum = INCREMENTAL;");
|
|
@@ -8616,9 +9332,9 @@ var RECEIVED_STATE = "received";
|
|
|
8616
9332
|
function byteLength(value) {
|
|
8617
9333
|
return Buffer.byteLength(value, "utf8");
|
|
8618
9334
|
}
|
|
8619
|
-
function fileBytes(
|
|
9335
|
+
function fileBytes(path36) {
|
|
8620
9336
|
try {
|
|
8621
|
-
return statSync(
|
|
9337
|
+
return statSync(path36).size;
|
|
8622
9338
|
} catch {
|
|
8623
9339
|
return 0;
|
|
8624
9340
|
}
|
|
@@ -9950,8 +10666,8 @@ function estimateEventBytes(event) {
|
|
|
9950
10666
|
}
|
|
9951
10667
|
async function openArtifact(workspaceDir, name) {
|
|
9952
10668
|
const realWorkspaceDir = await promises.realpath(workspaceDir).catch(() => workspaceDir);
|
|
9953
|
-
const candidate =
|
|
9954
|
-
const prefix = realWorkspaceDir.endsWith(
|
|
10669
|
+
const candidate = path3__default.resolve(realWorkspaceDir, name);
|
|
10670
|
+
const prefix = realWorkspaceDir.endsWith(path3__default.sep) ? realWorkspaceDir : realWorkspaceDir + path3__default.sep;
|
|
9955
10671
|
if (candidate !== realWorkspaceDir && !candidate.startsWith(prefix)) {
|
|
9956
10672
|
return { ok: false, reason: `artifact name "${name}" resolves outside the task workspace \u2014 rejected` };
|
|
9957
10673
|
}
|
|
@@ -10084,6 +10800,12 @@ var TaskRunner = class {
|
|
|
10084
10800
|
* cancellation intent.
|
|
10085
10801
|
*/
|
|
10086
10802
|
finishedTaskIds = /* @__PURE__ */ new Set();
|
|
10803
|
+
/**
|
|
10804
|
+
* Bounded local receive dedup for strict legacy declines. A decline is not a
|
|
10805
|
+
* task terminal receipt, so it must never enter `finishedTaskIds`; retaining
|
|
10806
|
+
* it separately keeps replay idempotent without claiming or finishing work.
|
|
10807
|
+
*/
|
|
10808
|
+
strictDeclinedTaskIds = /* @__PURE__ */ new Set();
|
|
10087
10809
|
/**
|
|
10088
10810
|
* M4 Phase 2 (daemon control socket `shutdown` RPC): set once by
|
|
10089
10811
|
* {@link stopAcceptingOffers}, checked at the very top of `handleOffer` —
|
|
@@ -10303,7 +11025,7 @@ var TaskRunner = class {
|
|
|
10303
11025
|
}
|
|
10304
11026
|
}
|
|
10305
11027
|
async handleOffer(taskId, payload, strictAgentOffer) {
|
|
10306
|
-
if (this.tasks.has(taskId) || this.finishedTaskIds.has(taskId)) {
|
|
11028
|
+
if (this.tasks.has(taskId) || this.finishedTaskIds.has(taskId) || this.strictDeclinedTaskIds.has(taskId)) {
|
|
10307
11029
|
return;
|
|
10308
11030
|
}
|
|
10309
11031
|
let agentRef;
|
|
@@ -10334,15 +11056,6 @@ var TaskRunner = class {
|
|
|
10334
11056
|
return;
|
|
10335
11057
|
}
|
|
10336
11058
|
}
|
|
10337
|
-
const guarded = this.deps.admissionGuard?.({ taskId, payload });
|
|
10338
|
-
if (guarded !== void 0 && !guarded.admit) {
|
|
10339
|
-
decline(guarded.reason, guarded.retryable);
|
|
10340
|
-
return;
|
|
10341
|
-
}
|
|
10342
|
-
if (this.stoppingOffers) {
|
|
10343
|
-
decline("daemon is shutting down", true);
|
|
10344
|
-
return;
|
|
10345
|
-
}
|
|
10346
11059
|
this.inFlightOffers.add(taskId);
|
|
10347
11060
|
let agentBinding;
|
|
10348
11061
|
let agentLeaseTransferred = false;
|
|
@@ -10353,6 +11066,20 @@ var TaskRunner = class {
|
|
|
10353
11066
|
decline(reason ? `cancelled before claim: ${reason}` : "cancelled before claim", false);
|
|
10354
11067
|
return;
|
|
10355
11068
|
}
|
|
11069
|
+
if (this.deps.strictAgentOnly === true && !strictAgentOffer) {
|
|
11070
|
+
this.addStrictDeclinedTaskId(taskId);
|
|
11071
|
+
decline("strict Agent-only daemon refuses legacy task offers", false);
|
|
11072
|
+
return;
|
|
11073
|
+
}
|
|
11074
|
+
const guarded = this.deps.admissionGuard?.({ taskId, payload });
|
|
11075
|
+
if (guarded !== void 0 && !guarded.admit) {
|
|
11076
|
+
decline(guarded.reason, guarded.retryable);
|
|
11077
|
+
return;
|
|
11078
|
+
}
|
|
11079
|
+
if (this.stoppingOffers) {
|
|
11080
|
+
decline("daemon is shutting down", true);
|
|
11081
|
+
return;
|
|
11082
|
+
}
|
|
10356
11083
|
if (payload.limits?.maxTokens !== void 0) {
|
|
10357
11084
|
decline(
|
|
10358
11085
|
`offer requests limits.maxTokens (${payload.limits.maxTokens}), which no bundled runtime adapter enforces \u2014 declining fail-closed rather than silently ignoring it`,
|
|
@@ -10463,7 +11190,7 @@ var TaskRunner = class {
|
|
|
10463
11190
|
const sameProtocolTask = ledger?.taskId === taskId;
|
|
10464
11191
|
const interruptedOldTask = ledger?.phase === "interrupted" && sameProtocolTask;
|
|
10465
11192
|
const activeDifferentTask = ledger !== void 0 && ledger.taskId !== taskId && (ledger.phase === "preparing" || ledger.phase === "active");
|
|
10466
|
-
if (!known || known.workspaceKind !== "git" || !known.gitWorkspaceId || !ledger || ledger.workspaceId !== known.gitWorkspaceId || ledger.sessionRef !== sessionRef ||
|
|
11193
|
+
if (!known || known.workspaceKind !== "git" || !known.gitWorkspaceId || !ledger || ledger.workspaceId !== known.gitWorkspaceId || ledger.sessionRef !== sessionRef || path3__default.resolve(ledger.workspaceDir) !== path3__default.resolve(known.workspaceDir) || interruptedOldTask || activeDifferentTask) {
|
|
10467
11194
|
decline("session is incompatible with Git workspace mode", true);
|
|
10468
11195
|
return;
|
|
10469
11196
|
}
|
|
@@ -10478,7 +11205,7 @@ var TaskRunner = class {
|
|
|
10478
11205
|
return;
|
|
10479
11206
|
}
|
|
10480
11207
|
} else {
|
|
10481
|
-
workspaceDir =
|
|
11208
|
+
workspaceDir = path3__default.join(this.deps.workspaceRoot, taskId);
|
|
10482
11209
|
gitWorkspaceId = randomUUID();
|
|
10483
11210
|
}
|
|
10484
11211
|
try {
|
|
@@ -10489,7 +11216,7 @@ var TaskRunner = class {
|
|
|
10489
11216
|
}
|
|
10490
11217
|
} else if (!this.deps.gitWorkspaceManager && !this.deps.gitWorkspaceStore) {
|
|
10491
11218
|
known = sessionRef ? await this.deps.sessionWorkspaces.get(sessionRef) : void 0;
|
|
10492
|
-
workspaceDir = known?.workspaceDir ??
|
|
11219
|
+
workspaceDir = known?.workspaceDir ?? path3__default.join(this.deps.workspaceRoot, taskId);
|
|
10493
11220
|
plainWorkspaceNeedsResolve = true;
|
|
10494
11221
|
} else {
|
|
10495
11222
|
decline("workspace mode is unavailable", true);
|
|
@@ -11874,9 +12601,16 @@ var TaskRunner = class {
|
|
|
11874
12601
|
if (oldest !== void 0) this.finishedTaskIds.delete(oldest);
|
|
11875
12602
|
}
|
|
11876
12603
|
}
|
|
12604
|
+
addStrictDeclinedTaskId(taskId) {
|
|
12605
|
+
this.strictDeclinedTaskIds.add(taskId);
|
|
12606
|
+
if (this.strictDeclinedTaskIds.size > MAX_TRACKED_TASK_IDS) {
|
|
12607
|
+
const oldest = this.strictDeclinedTaskIds.values().next().value;
|
|
12608
|
+
if (oldest !== void 0) this.strictDeclinedTaskIds.delete(oldest);
|
|
12609
|
+
}
|
|
12610
|
+
}
|
|
11877
12611
|
/** `reuseDir`, when set (a known sessionRef's recorded workspace), is used verbatim instead of a fresh `workspaceRoot/<taskId>` directory — `mkdir recursive` is idempotent either way, so ensuring-exists is safe to do unconditionally. */
|
|
11878
12612
|
async resolveWorkspaceDir(taskId, reuseDir) {
|
|
11879
|
-
const dir = reuseDir ??
|
|
12613
|
+
const dir = reuseDir ?? path3__default.join(this.deps.workspaceRoot, taskId);
|
|
11880
12614
|
await promises.mkdir(dir, { recursive: true });
|
|
11881
12615
|
return dir;
|
|
11882
12616
|
}
|
|
@@ -12014,7 +12748,7 @@ var encoder2 = new TextEncoder();
|
|
|
12014
12748
|
function eventBytes(event) {
|
|
12015
12749
|
return encoder2.encode(JSON.stringify(event)).length;
|
|
12016
12750
|
}
|
|
12017
|
-
var AGENT_EGRESS_DIRECTORY =
|
|
12751
|
+
var AGENT_EGRESS_DIRECTORY = path3__default.join(".byok", "egress");
|
|
12018
12752
|
var AGENT_RELIABLE_SPOOL_FILENAME = "reliable-v1.jsonl";
|
|
12019
12753
|
var AgentReliableSpoolError = class extends Error {
|
|
12020
12754
|
constructor(message) {
|
|
@@ -12125,9 +12859,9 @@ var AgentReliableSpool = class _AgentReliableSpool {
|
|
|
12125
12859
|
logEntries = 0;
|
|
12126
12860
|
writeTail = Promise.resolve();
|
|
12127
12861
|
static async open(homeDir) {
|
|
12128
|
-
const directory =
|
|
12862
|
+
const directory = path3__default.join(homeDir, AGENT_EGRESS_DIRECTORY);
|
|
12129
12863
|
await ensureSecureDir(directory);
|
|
12130
|
-
const spool = new _AgentReliableSpool(homeDir,
|
|
12864
|
+
const spool = new _AgentReliableSpool(homeDir, path3__default.join(directory, AGENT_RELIABLE_SPOOL_FILENAME));
|
|
12131
12865
|
await spool.load();
|
|
12132
12866
|
return spool;
|
|
12133
12867
|
}
|
|
@@ -12590,7 +13324,7 @@ var AgentEgressController = class {
|
|
|
12590
13324
|
}
|
|
12591
13325
|
/** Re-open every existing Agent-local spool before retrying stable records after restart. */
|
|
12592
13326
|
async recover(agentsRoot) {
|
|
12593
|
-
if (!
|
|
13327
|
+
if (!path3__default.isAbsolute(agentsRoot)) throw new Error("Agent egress recovery root must be absolute");
|
|
12594
13328
|
if (!this.active) throw new Error("Agent egress recovery requires an active authenticated enrollment");
|
|
12595
13329
|
if (this.options.tenantId === void 0) {
|
|
12596
13330
|
throw new Error("Agent egress recovery requires one authenticated tenant authority");
|
|
@@ -12605,14 +13339,14 @@ var AgentEgressController = class {
|
|
|
12605
13339
|
}
|
|
12606
13340
|
for (const entry of entries) {
|
|
12607
13341
|
if (!entry.isDirectory() || entry.isSymbolicLink()) continue;
|
|
12608
|
-
const homeDir =
|
|
13342
|
+
const homeDir = path3__default.join(canonicalAgentsRoot, entry.name);
|
|
12609
13343
|
const canonicalHome = await promises.realpath(homeDir);
|
|
12610
|
-
const relativeHome =
|
|
12611
|
-
if (relativeHome !== entry.name || relativeHome.includes(
|
|
13344
|
+
const relativeHome = path3__default.relative(canonicalAgentsRoot, canonicalHome);
|
|
13345
|
+
if (relativeHome !== entry.name || relativeHome.includes(path3__default.sep) || path3__default.isAbsolute(relativeHome)) {
|
|
12612
13346
|
throw new Error(`Agent egress recovery home escaped the canonical agents root: ${entry.name}`);
|
|
12613
13347
|
}
|
|
12614
13348
|
try {
|
|
12615
|
-
await promises.lstat(
|
|
13349
|
+
await promises.lstat(path3__default.join(homeDir, AGENT_EGRESS_DIRECTORY));
|
|
12616
13350
|
} catch (error) {
|
|
12617
13351
|
if (error.code === "ENOENT") continue;
|
|
12618
13352
|
throw error;
|
|
@@ -12699,12 +13433,12 @@ function isAgentRef(value) {
|
|
|
12699
13433
|
}
|
|
12700
13434
|
function isCanonicalRelativeTarget(value) {
|
|
12701
13435
|
if (value === "[invalid-target]") return true;
|
|
12702
|
-
if (
|
|
13436
|
+
if (path3__default.isAbsolute(value) || value.includes("\\")) return false;
|
|
12703
13437
|
const segments = value.split("/");
|
|
12704
13438
|
return value.length > 0 && segments.every((segment) => segment.length > 0 && segment !== "." && segment !== "..");
|
|
12705
13439
|
}
|
|
12706
13440
|
function validateIdentity(value, label) {
|
|
12707
|
-
if (!isRecord4(value) || !isAgentRef(value.agentRef) || !isNonEmptyString(value.sessionRef) || !isNonEmptyString(value.runtimeId) || !isNonEmptyString(value.cwd) || !
|
|
13441
|
+
if (!isRecord4(value) || !isAgentRef(value.agentRef) || !isNonEmptyString(value.sessionRef) || !isNonEmptyString(value.runtimeId) || !isNonEmptyString(value.cwd) || !path3__default.isAbsolute(value.cwd)) {
|
|
12708
13442
|
throw new AgentContentAuditStoreError(`${label} has an invalid exact Agent/session identity`);
|
|
12709
13443
|
}
|
|
12710
13444
|
return Object.freeze({
|
|
@@ -12714,7 +13448,7 @@ function validateIdentity(value, label) {
|
|
|
12714
13448
|
}),
|
|
12715
13449
|
sessionRef: value.sessionRef,
|
|
12716
13450
|
runtimeId: value.runtimeId,
|
|
12717
|
-
cwd:
|
|
13451
|
+
cwd: path3__default.resolve(value.cwd)
|
|
12718
13452
|
});
|
|
12719
13453
|
}
|
|
12720
13454
|
function validateReceipt(value) {
|
|
@@ -12796,16 +13530,16 @@ function assertUniqueRequestIds(entries) {
|
|
|
12796
13530
|
}
|
|
12797
13531
|
}
|
|
12798
13532
|
function assertAbsoluteFilePath(filePath) {
|
|
12799
|
-
if (typeof filePath !== "string" || filePath.length === 0 || !
|
|
13533
|
+
if (typeof filePath !== "string" || filePath.length === 0 || !path3__default.isAbsolute(filePath)) {
|
|
12800
13534
|
throw new AgentContentAuditStoreError("content audit path must be absolute");
|
|
12801
13535
|
}
|
|
12802
13536
|
if (/[\u0000\r\n]/u.test(filePath)) {
|
|
12803
13537
|
throw new AgentContentAuditStoreError("content audit path must not contain NUL or line breaks");
|
|
12804
13538
|
}
|
|
12805
|
-
return
|
|
13539
|
+
return path3__default.resolve(filePath);
|
|
12806
13540
|
}
|
|
12807
13541
|
async function ensureDirectoryNoSymlink2(directory) {
|
|
12808
|
-
const absolute =
|
|
13542
|
+
const absolute = path3__default.resolve(directory);
|
|
12809
13543
|
await promises.mkdir(absolute, { recursive: true, mode: 448 });
|
|
12810
13544
|
const stat = await promises.lstat(absolute);
|
|
12811
13545
|
if (!stat.isDirectory() || stat.isSymbolicLink()) {
|
|
@@ -12839,12 +13573,12 @@ var AgentContentAuditStore = class _AgentContentAuditStore {
|
|
|
12839
13573
|
/** The daemon may address this ledger only through an AgentHomeLayout resolution. */
|
|
12840
13574
|
static forCanonicalAgentHome(canonicalHome) {
|
|
12841
13575
|
const home = assertAbsoluteFilePath(canonicalHome);
|
|
12842
|
-
return new _AgentContentAuditStore(
|
|
13576
|
+
return new _AgentContentAuditStore(path3__default.join(home, AGENT_HOME_INTERNAL_DIRECTORY, AGENT_CONTENT_AUDIT_FILENAME));
|
|
12843
13577
|
}
|
|
12844
13578
|
async append(receipt) {
|
|
12845
13579
|
const validated = validateReceipt(receipt);
|
|
12846
13580
|
return this.enqueue(async () => {
|
|
12847
|
-
await ensureDirectoryNoSymlink2(
|
|
13581
|
+
await ensureDirectoryNoSymlink2(path3__default.dirname(this.filePath));
|
|
12848
13582
|
await assertAuditFile(this.filePath);
|
|
12849
13583
|
const entries = await this.readAllUnlocked();
|
|
12850
13584
|
const prior = entries.find((entry) => entry.requestId === validated.requestId);
|
|
@@ -12939,6 +13673,63 @@ var AgentContentAuditStore = class _AgentContentAuditStore {
|
|
|
12939
13673
|
return result;
|
|
12940
13674
|
}
|
|
12941
13675
|
};
|
|
13676
|
+
var AgentHomeProjectionCompletionError = class extends Error {
|
|
13677
|
+
constructor(message, options) {
|
|
13678
|
+
super(message, options);
|
|
13679
|
+
this.name = "AgentHomeProjectionCompletionError";
|
|
13680
|
+
}
|
|
13681
|
+
};
|
|
13682
|
+
function sameAgentRef2(left, right) {
|
|
13683
|
+
return left.agentId === right.agentId && left.profileRevision === right.profileRevision;
|
|
13684
|
+
}
|
|
13685
|
+
var AgentHomeProjectionCompletionClient = class {
|
|
13686
|
+
constructor(options) {
|
|
13687
|
+
this.options = options;
|
|
13688
|
+
}
|
|
13689
|
+
options;
|
|
13690
|
+
async complete(input) {
|
|
13691
|
+
const completion = AgentHomeProjectionCompletionRequestSchema.parse(input);
|
|
13692
|
+
const url = new URL(
|
|
13693
|
+
byokAgentHomeProjectionCompletionPath(completion.requestId),
|
|
13694
|
+
toHttpBase(this.options.serverUrl)
|
|
13695
|
+
);
|
|
13696
|
+
let response;
|
|
13697
|
+
try {
|
|
13698
|
+
response = await authedFetch(
|
|
13699
|
+
url,
|
|
13700
|
+
{
|
|
13701
|
+
method: "PUT",
|
|
13702
|
+
headers: { "content-type": "application/json" },
|
|
13703
|
+
body: JSON.stringify(completion)
|
|
13704
|
+
},
|
|
13705
|
+
this.options.auth
|
|
13706
|
+
);
|
|
13707
|
+
} catch (error) {
|
|
13708
|
+
throw new AgentHomeProjectionCompletionError("Agent-home projection completion transport failed", {
|
|
13709
|
+
cause: error
|
|
13710
|
+
});
|
|
13711
|
+
}
|
|
13712
|
+
if (!response.ok) {
|
|
13713
|
+
throw new AgentHomeProjectionCompletionError(
|
|
13714
|
+
`Agent-home projection completion was rejected with HTTP ${response.status}`
|
|
13715
|
+
);
|
|
13716
|
+
}
|
|
13717
|
+
let readback;
|
|
13718
|
+
try {
|
|
13719
|
+
readback = AgentHomeProjectionReadbackSchema.parse(await response.json());
|
|
13720
|
+
} catch (error) {
|
|
13721
|
+
throw new AgentHomeProjectionCompletionError("Agent-home projection completion readback is invalid", {
|
|
13722
|
+
cause: error
|
|
13723
|
+
});
|
|
13724
|
+
}
|
|
13725
|
+
if (readback.tenantId !== this.options.tenantId || readback.deviceId !== this.options.deviceId || readback.requestId !== completion.requestId || !sameAgentRef2(readback.agentRef, completion.agentRef) || readback.projectionHash !== completion.projectionHash || readback.status !== completion.outcome || readback.completedAt === void 0) {
|
|
13726
|
+
throw new AgentHomeProjectionCompletionError(
|
|
13727
|
+
"Agent-home projection completion readback does not exactly match the authenticated request"
|
|
13728
|
+
);
|
|
13729
|
+
}
|
|
13730
|
+
return readback;
|
|
13731
|
+
}
|
|
13732
|
+
};
|
|
12942
13733
|
var AGENT_CONTENT_READ_SURFACES = ["workspace", "transcript", "artifact"];
|
|
12943
13734
|
var AGENT_CONTENT_READ_CAPABILITIES = Object.freeze({
|
|
12944
13735
|
workspace: AGENT_CONTENT_WORKSPACE_READ_CAPABILITY,
|
|
@@ -13042,8 +13833,8 @@ function normalizeIdentity(value, field) {
|
|
|
13042
13833
|
const sessionRef = nonEmptyString(value.sessionRef, `${field}.sessionRef`);
|
|
13043
13834
|
const runtimeId = nonEmptyString(value.runtimeId, `${field}.runtimeId`);
|
|
13044
13835
|
const cwd = nonEmptyString(value.cwd, `${field}.cwd`);
|
|
13045
|
-
if (!
|
|
13046
|
-
return Object.freeze({ agentRef, sessionRef, runtimeId, cwd:
|
|
13836
|
+
if (!path3__default.isAbsolute(cwd)) throw new AgentContentReadPolicyError(`${field}.cwd must be absolute`);
|
|
13837
|
+
return Object.freeze({ agentRef, sessionRef, runtimeId, cwd: path3__default.resolve(cwd) });
|
|
13047
13838
|
}
|
|
13048
13839
|
function createAgentContentReadPolicy(input) {
|
|
13049
13840
|
if (!isRecord5(input) || input.enabled !== true) {
|
|
@@ -13071,10 +13862,10 @@ function createAgentContentReadPolicy(input) {
|
|
|
13071
13862
|
root = Object.freeze({ kind: "agent-home" });
|
|
13072
13863
|
} else if (input.root.kind === "runtime-allowlisted") {
|
|
13073
13864
|
const configuredRoot = nonEmptyString(input.root.root, "contentRead.root.root");
|
|
13074
|
-
if (!
|
|
13865
|
+
if (!path3__default.isAbsolute(configuredRoot)) {
|
|
13075
13866
|
throw new AgentContentReadPolicyError("contentRead.root.root must be absolute");
|
|
13076
13867
|
}
|
|
13077
|
-
root = Object.freeze({ kind: "runtime-allowlisted", root:
|
|
13868
|
+
root = Object.freeze({ kind: "runtime-allowlisted", root: path3__default.resolve(configuredRoot) });
|
|
13078
13869
|
} else {
|
|
13079
13870
|
throw new AgentContentReadPolicyError("contentRead.root.kind is not supported");
|
|
13080
13871
|
}
|
|
@@ -13097,11 +13888,11 @@ function createAgentContentReadPolicy(input) {
|
|
|
13097
13888
|
});
|
|
13098
13889
|
}
|
|
13099
13890
|
function isWithin2(root, candidate) {
|
|
13100
|
-
const relative =
|
|
13101
|
-
return relative === "" || relative !== ".." && !relative.startsWith(`..${
|
|
13891
|
+
const relative = path3__default.relative(root, candidate);
|
|
13892
|
+
return relative === "" || relative !== ".." && !relative.startsWith(`..${path3__default.sep}`) && !path3__default.isAbsolute(relative);
|
|
13102
13893
|
}
|
|
13103
13894
|
function isPortableAbsoluteTarget(value) {
|
|
13104
|
-
return
|
|
13895
|
+
return path3__default.isAbsolute(value) || /^[a-z]:[\\/]/iu.test(value) || /^[/\\]/u.test(value);
|
|
13105
13896
|
}
|
|
13106
13897
|
function canonicalAuditTarget(value) {
|
|
13107
13898
|
if (typeof value !== "string" || value.length === 0 || /[\u0000\r\n]/u.test(value) || isPortableAbsoluteTarget(value) || value.includes("\\")) {
|
|
@@ -13139,7 +13930,7 @@ function isSensitiveTarget(segments, productNames) {
|
|
|
13139
13930
|
return segments.some((segment) => patterns.some((pattern) => nameMatches(pattern, segment)));
|
|
13140
13931
|
}
|
|
13141
13932
|
async function resolveExistingAncestor2(inputPath) {
|
|
13142
|
-
let cursor =
|
|
13933
|
+
let cursor = path3__default.resolve(inputPath);
|
|
13143
13934
|
const tail = [];
|
|
13144
13935
|
for (; ; ) {
|
|
13145
13936
|
try {
|
|
@@ -13147,9 +13938,9 @@ async function resolveExistingAncestor2(inputPath) {
|
|
|
13147
13938
|
} catch (error) {
|
|
13148
13939
|
const code = error.code;
|
|
13149
13940
|
if (code !== "ENOENT" && code !== "ENOTDIR") throw error;
|
|
13150
|
-
const parent =
|
|
13941
|
+
const parent = path3__default.dirname(cursor);
|
|
13151
13942
|
if (parent === cursor) throw new TargetPolicyError("target-missing");
|
|
13152
|
-
tail.unshift(
|
|
13943
|
+
tail.unshift(path3__default.basename(cursor));
|
|
13153
13944
|
cursor = parent;
|
|
13154
13945
|
}
|
|
13155
13946
|
}
|
|
@@ -13170,11 +13961,11 @@ var RootPolicyError = class extends Error {
|
|
|
13170
13961
|
this.reason = reason;
|
|
13171
13962
|
}
|
|
13172
13963
|
};
|
|
13173
|
-
function
|
|
13964
|
+
function sameAgentRef3(left, right) {
|
|
13174
13965
|
return left.agentId === right.agentId && left.profileRevision === right.profileRevision;
|
|
13175
13966
|
}
|
|
13176
13967
|
function sameSessionIdentity(left, right) {
|
|
13177
|
-
return
|
|
13968
|
+
return sameAgentRef3(left.agentRef, right.agentRef) && left.sessionRef === right.sessionRef && left.runtimeId === right.runtimeId && left.cwd === right.cwd;
|
|
13178
13969
|
}
|
|
13179
13970
|
function validateRequest(request) {
|
|
13180
13971
|
if (!isRecord5(request)) throw new AgentContentReadRequestError("content read request must be an object");
|
|
@@ -13236,18 +14027,18 @@ function normalizeRequestIdentity(value, field) {
|
|
|
13236
14027
|
const sessionRef = requestString(value.sessionRef, `${field}.sessionRef`);
|
|
13237
14028
|
const runtimeId = requestString(value.runtimeId, `${field}.runtimeId`);
|
|
13238
14029
|
const cwd = requestString(value.cwd, `${field}.cwd`);
|
|
13239
|
-
if (!
|
|
13240
|
-
return Object.freeze({ agentRef, sessionRef, runtimeId, cwd:
|
|
14030
|
+
if (!path3__default.isAbsolute(cwd)) throw new AgentContentReadRequestError(`${field}.cwd must be absolute`);
|
|
14031
|
+
return Object.freeze({ agentRef, sessionRef, runtimeId, cwd: path3__default.resolve(cwd) });
|
|
13241
14032
|
}
|
|
13242
14033
|
async function inspectRegularTarget(root, target) {
|
|
13243
14034
|
const ancestor = await resolveExistingAncestor2(target);
|
|
13244
14035
|
if (!isWithin2(root, ancestor.canonical)) {
|
|
13245
14036
|
throw new TargetPolicyError("path-escape");
|
|
13246
14037
|
}
|
|
13247
|
-
const components =
|
|
14038
|
+
const components = path3__default.relative(root, target).split(path3__default.sep).filter((component) => component.length > 0);
|
|
13248
14039
|
let cursor = root;
|
|
13249
14040
|
for (const [index, component] of components.entries()) {
|
|
13250
|
-
cursor =
|
|
14041
|
+
cursor = path3__default.join(cursor, component);
|
|
13251
14042
|
let stat;
|
|
13252
14043
|
try {
|
|
13253
14044
|
stat = await promises.lstat(cursor);
|
|
@@ -13312,8 +14103,8 @@ var AgentContentReadPolicyEngine = class {
|
|
|
13312
14103
|
this.capabilities = new Set(options.capabilities);
|
|
13313
14104
|
this.runtimeRoots = Object.freeze((options.runtimeAllowlistedRoots ?? []).map((root, index) => {
|
|
13314
14105
|
const value = nonEmptyString(root, `contentRead.runtimeAllowlistedRoots[${index}]`);
|
|
13315
|
-
if (!
|
|
13316
|
-
return
|
|
14106
|
+
if (!path3__default.isAbsolute(value)) throw new AgentContentReadPolicyError("runtime allowlisted roots must be absolute");
|
|
14107
|
+
return path3__default.resolve(value);
|
|
13317
14108
|
}));
|
|
13318
14109
|
this.resolveSessionIdentity = options.resolveSessionIdentity;
|
|
13319
14110
|
this.resolveTranscriptIdentity = options.resolveTranscriptIdentity;
|
|
@@ -13365,7 +14156,7 @@ var AgentContentReadPolicyEngine = class {
|
|
|
13365
14156
|
if (request.decodeAs === "utf8" && !policy.textMimeTypes.includes(request.mimeType)) {
|
|
13366
14157
|
return this.deny(request, relativeTarget, "text-not-allowlisted");
|
|
13367
14158
|
}
|
|
13368
|
-
const target =
|
|
14159
|
+
const target = path3__default.resolve(root, ...segments);
|
|
13369
14160
|
if (!isWithin2(root, target)) return this.deny(request, relativeTarget, "path-escape");
|
|
13370
14161
|
try {
|
|
13371
14162
|
await inspectRegularTarget(root, target);
|
|
@@ -13446,7 +14237,7 @@ var AgentContentReadPolicyEngine = class {
|
|
|
13446
14237
|
}
|
|
13447
14238
|
async checkSessionIdentity(request, resolver, requiredCwd) {
|
|
13448
14239
|
const session = request.session;
|
|
13449
|
-
if (session === void 0 || !
|
|
14240
|
+
if (session === void 0 || !sameAgentRef3(session.agentRef, request.agentRef) || requiredCwd !== void 0 && session.cwd !== requiredCwd) {
|
|
13450
14241
|
return "identity-mismatch";
|
|
13451
14242
|
}
|
|
13452
14243
|
let expected;
|
|
@@ -13562,7 +14353,7 @@ async function detectRuntimes(adapters) {
|
|
|
13562
14353
|
}
|
|
13563
14354
|
return runtimes;
|
|
13564
14355
|
}
|
|
13565
|
-
function computeCapabilities(adapters, agentHomeConfigured = false, agentEgressConfigured = false, contentReadPolicies) {
|
|
14356
|
+
function computeCapabilities(adapters, agentHomeConfigured = false, strictAgentOnly = false, agentHomeProjectionConfigured = false, agentEgressConfigured = false, contentReadPolicies) {
|
|
13566
14357
|
const flags = [];
|
|
13567
14358
|
if (adapters.some((adapter) => adapter.descriptor.capabilities.steer)) flags.push("steer");
|
|
13568
14359
|
flags.push("blob-upload");
|
|
@@ -13577,6 +14368,8 @@ function computeCapabilities(adapters, agentHomeConfigured = false, agentEgressC
|
|
|
13577
14368
|
flags.push("toolset-selection");
|
|
13578
14369
|
}
|
|
13579
14370
|
if (agentHomeConfigured) flags.push("agent-home-contract");
|
|
14371
|
+
if (strictAgentOnly) flags.push(STRICT_AGENT_ONLY_CAPABILITY);
|
|
14372
|
+
if (agentHomeProjectionConfigured) flags.push(AGENT_HOME_PROJECTION_CAPABILITY);
|
|
13580
14373
|
if (agentEgressConfigured) {
|
|
13581
14374
|
flags.push(
|
|
13582
14375
|
AGENT_EGRESS_POLICY_CAPABILITY,
|
|
@@ -13705,6 +14498,9 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
13705
14498
|
if (config.agentEgress !== void 0 && config.agentHome === void 0) {
|
|
13706
14499
|
throw new Error("DaemonConfig.agentEgress requires DaemonConfig.agentHome for the per-Agent local spool");
|
|
13707
14500
|
}
|
|
14501
|
+
if (config.strictAgentOnly === true && config.agentHome === void 0) {
|
|
14502
|
+
throw new Error("DaemonConfig.strictAgentOnly requires DaemonConfig.agentHome");
|
|
14503
|
+
}
|
|
13708
14504
|
const egressPolicy = resolveAgentEgressPolicy(config.agentEgress?.policy);
|
|
13709
14505
|
const egressBatcherOptions = egressPolicy.activity.mode === "contentful-trajectory" ? {
|
|
13710
14506
|
...config.progressBatch,
|
|
@@ -13720,7 +14516,7 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
13720
14516
|
const deviceAssertionTtlMs = resolveDeviceAssertionTtlMs(config.deviceAssertion);
|
|
13721
14517
|
let shuttingDown = false;
|
|
13722
14518
|
const storeDir = DeviceStore.resolveDir(config.productId, config.storeDir);
|
|
13723
|
-
const store = new DeviceStore(storeDir);
|
|
14519
|
+
const store = new DeviceStore(storeDir, void 0, config.productId);
|
|
13724
14520
|
const operationalHealth = new OperationalHealthTracker(storeDir);
|
|
13725
14521
|
let fleetJitter;
|
|
13726
14522
|
let maintenanceSequence = 0;
|
|
@@ -13734,6 +14530,7 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
13734
14530
|
})
|
|
13735
14531
|
});
|
|
13736
14532
|
const agentSessionHandoffs = config.agentHome === void 0 ? void 0 : new AgentSessionHandoffStore();
|
|
14533
|
+
if (config.strictAgentOnly === true) agentHomeManager?.preflightSync();
|
|
13737
14534
|
const agentContentReadPolicies = resolveContentReadPolicies(egressPolicy, config.agentEgress?.contentRead);
|
|
13738
14535
|
let agentEgress = new AgentEgressController({
|
|
13739
14536
|
policy: egressPolicy,
|
|
@@ -13818,6 +14615,8 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
13818
14615
|
let tenantRebinding = false;
|
|
13819
14616
|
let runner;
|
|
13820
14617
|
let controlServerHandle;
|
|
14618
|
+
let serviceEnrollmentWaiting = false;
|
|
14619
|
+
let serviceEnrollmentTransitioning = false;
|
|
13821
14620
|
let daemonOwnerLease;
|
|
13822
14621
|
let presencePublisher;
|
|
13823
14622
|
let detectedRuntimeFacts = [];
|
|
@@ -13865,7 +14664,8 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
13865
14664
|
}
|
|
13866
14665
|
async function pair(pairingCode) {
|
|
13867
14666
|
checkServerUrl();
|
|
13868
|
-
|
|
14667
|
+
const record = await runLifecycleMutation(() => pairUnderLease(pairingCode));
|
|
14668
|
+
return Object.freeze({ deviceId: record.deviceId });
|
|
13869
14669
|
}
|
|
13870
14670
|
async function pairUnderLease(pairingCode) {
|
|
13871
14671
|
const wasRunning = daemonStarted;
|
|
@@ -13876,7 +14676,7 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
13876
14676
|
try {
|
|
13877
14677
|
let previous;
|
|
13878
14678
|
try {
|
|
13879
|
-
previous = await
|
|
14679
|
+
previous = await auth.readCurrent();
|
|
13880
14680
|
} catch (error) {
|
|
13881
14681
|
if (!(error instanceof DeviceRecordRePairRequiredError)) throw error;
|
|
13882
14682
|
}
|
|
@@ -13911,6 +14711,28 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
13911
14711
|
throw err;
|
|
13912
14712
|
}
|
|
13913
14713
|
}
|
|
14714
|
+
async function replaceControlServer(required, enrollmentOnly = false) {
|
|
14715
|
+
if (controlServerHandle) {
|
|
14716
|
+
await controlServerHandle.close();
|
|
14717
|
+
controlServerHandle = void 0;
|
|
14718
|
+
}
|
|
14719
|
+
try {
|
|
14720
|
+
const methods = enrollmentOnly ? {
|
|
14721
|
+
unary: {
|
|
14722
|
+
status: controlMethods.unary.status,
|
|
14723
|
+
"enrollment.pair": controlMethods.unary["enrollment.pair"]
|
|
14724
|
+
},
|
|
14725
|
+
stream: {}
|
|
14726
|
+
} : controlMethods;
|
|
14727
|
+
controlServerHandle = await startControlServer({ storeDir, productId: config.productId, methods });
|
|
14728
|
+
} catch (err) {
|
|
14729
|
+
if (required || err instanceof AnotherControlServerRunningError) throw err;
|
|
14730
|
+
console.warn(
|
|
14731
|
+
`[byok/client] control socket failed to start (continuing without it): ${err instanceof Error ? err.message : String(err)}`
|
|
14732
|
+
);
|
|
14733
|
+
controlServerHandle = void 0;
|
|
14734
|
+
}
|
|
14735
|
+
}
|
|
13914
14736
|
async function start() {
|
|
13915
14737
|
checkServerUrl();
|
|
13916
14738
|
await runLifecycleMutation(startUnderLease);
|
|
@@ -13918,14 +14740,27 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
13918
14740
|
async function startUnderLease() {
|
|
13919
14741
|
if (!daemonOwnerLease) daemonOwnerLease = await acquireDaemonOwner(storeDir, "daemon");
|
|
13920
14742
|
try {
|
|
14743
|
+
let record;
|
|
14744
|
+
if (config.serviceEnrollment?.enabled === true) {
|
|
14745
|
+
record = await auth.loadExisting();
|
|
14746
|
+
if (!record) {
|
|
14747
|
+
startedAt = Date.now();
|
|
14748
|
+
serviceEnrollmentWaiting = true;
|
|
14749
|
+
serviceEnrollmentTransitioning = false;
|
|
14750
|
+
await replaceControlServer(true, true);
|
|
14751
|
+
return;
|
|
14752
|
+
}
|
|
14753
|
+
}
|
|
14754
|
+
serviceEnrollmentWaiting = false;
|
|
14755
|
+
serviceEnrollmentTransitioning = false;
|
|
13921
14756
|
initializeOwnedHostedStorage();
|
|
13922
|
-
|
|
13923
|
-
const activePressureEngine = pressureEngine;
|
|
13924
|
-
const activeOwnedPressureEngine = ownedPressureEngine;
|
|
13925
|
-
const record = await auth.loadExisting();
|
|
14757
|
+
record ??= await auth.loadExisting();
|
|
13926
14758
|
if (!record) {
|
|
13927
14759
|
throw new Error("device is not paired yet; call pair(pairingCode) first");
|
|
13928
14760
|
}
|
|
14761
|
+
const activeJournal = journal;
|
|
14762
|
+
const activePressureEngine = pressureEngine;
|
|
14763
|
+
const activeOwnedPressureEngine = ownedPressureEngine;
|
|
13929
14764
|
if (config.agentEgress !== void 0) {
|
|
13930
14765
|
agentEgress = new AgentEgressController({
|
|
13931
14766
|
policy: egressPolicy,
|
|
@@ -13935,7 +14770,7 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
13935
14770
|
}
|
|
13936
14771
|
await agentHomeManager?.preflight();
|
|
13937
14772
|
if (config.agentEgress !== void 0 && config.agentHome !== void 0) {
|
|
13938
|
-
await agentEgress.recover(
|
|
14773
|
+
await agentEgress.recover(path3__default.join(config.agentHome.hostStorageRoot, "agents"));
|
|
13939
14774
|
}
|
|
13940
14775
|
fleetJitter = createFleetJitter(config.productId, record.deviceId);
|
|
13941
14776
|
if (config.permissionDefaults?.workspaceRoot !== void 0) {
|
|
@@ -13962,19 +14797,7 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
13962
14797
|
activeOwnedPressureEngine.start();
|
|
13963
14798
|
}
|
|
13964
14799
|
startedAt = Date.now();
|
|
13965
|
-
|
|
13966
|
-
await controlServerHandle.close();
|
|
13967
|
-
controlServerHandle = void 0;
|
|
13968
|
-
}
|
|
13969
|
-
try {
|
|
13970
|
-
controlServerHandle = await startControlServer({ storeDir, productId: config.productId, methods: controlMethods });
|
|
13971
|
-
} catch (err) {
|
|
13972
|
-
if (err instanceof AnotherControlServerRunningError) throw err;
|
|
13973
|
-
console.warn(
|
|
13974
|
-
`[byok/client] control socket failed to start (continuing without it): ${err instanceof Error ? err.message : String(err)}`
|
|
13975
|
-
);
|
|
13976
|
-
controlServerHandle = void 0;
|
|
13977
|
-
}
|
|
14800
|
+
await replaceControlServer(false);
|
|
13978
14801
|
await operationalHealth.startRun();
|
|
13979
14802
|
if (gitWorkspaceManager) {
|
|
13980
14803
|
await gitWorkspaceManager.preflight();
|
|
@@ -13990,9 +14813,17 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
13990
14813
|
const capabilities = computeCapabilities(
|
|
13991
14814
|
adapters,
|
|
13992
14815
|
config.agentHome !== void 0,
|
|
14816
|
+
config.strictAgentOnly === true,
|
|
14817
|
+
agentHomeManager?.supportsTaskFreeProjection() === true,
|
|
13993
14818
|
config.agentEgress !== void 0,
|
|
13994
14819
|
agentContentReadPolicies
|
|
13995
14820
|
);
|
|
14821
|
+
const agentHomeProjectionCompletion = agentHomeManager?.supportsTaskFreeProjection() === true ? new AgentHomeProjectionCompletionClient({
|
|
14822
|
+
serverUrl: config.serverUrl,
|
|
14823
|
+
auth,
|
|
14824
|
+
tenantId: record.tenantId,
|
|
14825
|
+
deviceId: record.deviceId
|
|
14826
|
+
}) : void 0;
|
|
13996
14827
|
const journalIdentity = config.hostedJournal ? { tenantId: record.tenantId, productId: config.productId, deviceId: record.deviceId } : void 0;
|
|
13997
14828
|
const sendSanitizedEnvelope = activeJournal && journalIdentity ? (envelope) => {
|
|
13998
14829
|
observer.handleOutboundEnvelope(envelope);
|
|
@@ -14043,6 +14874,7 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
14043
14874
|
permissionDefaults: config.permissionDefaults,
|
|
14044
14875
|
workspaceRoot: config.workspaceRoot,
|
|
14045
14876
|
...agentHomeManager === void 0 ? {} : { agentHome: agentHomeManager },
|
|
14877
|
+
...config.strictAgentOnly === true ? { strictAgentOnly: true } : {},
|
|
14046
14878
|
...agentSessionHandoffs === void 0 ? {} : { agentSessionHandoffs },
|
|
14047
14879
|
deviceId: record.deviceId,
|
|
14048
14880
|
// M5: see `DaemonConfig.runtimeEnvironment`'s own doc comment above.
|
|
@@ -14129,6 +14961,20 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
14129
14961
|
...activePressureEngine ? { admissionGuard: () => activePressureEngine.admissionGuard() } : {}
|
|
14130
14962
|
};
|
|
14131
14963
|
runner = new TaskRunner(deps);
|
|
14964
|
+
const handleAgentHomeProjectionEnvelope = async (envelope) => {
|
|
14965
|
+
if (envelope.type !== "agent.home.projection") return false;
|
|
14966
|
+
if (agentHomeManager === void 0 || agentHomeProjectionCompletion === void 0) {
|
|
14967
|
+
throw new Error("task-free Agent-home projection is not configured on this daemon");
|
|
14968
|
+
}
|
|
14969
|
+
const outcome = await agentHomeManager.project(envelope.payload);
|
|
14970
|
+
await agentHomeProjectionCompletion.complete({
|
|
14971
|
+
requestId: envelope.payload.requestId,
|
|
14972
|
+
agentRef: envelope.payload.agentRef,
|
|
14973
|
+
projectionHash: envelope.payload.projectionHash,
|
|
14974
|
+
outcome
|
|
14975
|
+
});
|
|
14976
|
+
return true;
|
|
14977
|
+
};
|
|
14132
14978
|
const handleAgentEgressEnvelope = async (envelope) => {
|
|
14133
14979
|
if (envelope.type !== "agent.egress.ack") return false;
|
|
14134
14980
|
if (config.agentEgress === void 0) return true;
|
|
@@ -14284,6 +15130,7 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
14284
15130
|
throw new Error("tenant enrollment is being re-paired; inbound work is blocked until restart");
|
|
14285
15131
|
}
|
|
14286
15132
|
observer.handleInboundEnvelope(envelope);
|
|
15133
|
+
if (await handleAgentHomeProjectionEnvelope(envelope)) return;
|
|
14287
15134
|
if (await handleAgentEgressEnvelope(envelope)) return;
|
|
14288
15135
|
if (await handleAgentContentReadEnvelope(envelope)) return;
|
|
14289
15136
|
activePressureEngine?.assertAckCriticalAllowed();
|
|
@@ -14294,6 +15141,7 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
14294
15141
|
return Promise.reject(new Error("tenant enrollment is being re-paired; inbound work is blocked until restart"));
|
|
14295
15142
|
}
|
|
14296
15143
|
observer.handleInboundEnvelope(envelope);
|
|
15144
|
+
if (envelope.type === "agent.home.projection") return handleAgentHomeProjectionEnvelope(envelope).then(() => void 0);
|
|
14297
15145
|
if (envelope.type === "agent.egress.ack") return handleAgentEgressEnvelope(envelope).then(() => void 0);
|
|
14298
15146
|
if (envelope.type === "agent.content.read") return handleAgentContentReadEnvelope(envelope).then(() => void 0);
|
|
14299
15147
|
return runner?.handleEnvelope(envelope) ?? Promise.resolve();
|
|
@@ -14492,10 +15340,24 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
14492
15340
|
await runShutdownSequence("operator");
|
|
14493
15341
|
const cleanupLease = await acquireDaemonOwner(storeDir, "daemon");
|
|
14494
15342
|
try {
|
|
14495
|
-
|
|
14496
|
-
|
|
14497
|
-
await
|
|
15343
|
+
let current;
|
|
15344
|
+
try {
|
|
15345
|
+
current = await auth.readCurrent();
|
|
15346
|
+
} catch (error) {
|
|
15347
|
+
if (!(error instanceof DeviceRecordRePairRequiredError)) throw error;
|
|
15348
|
+
}
|
|
15349
|
+
try {
|
|
15350
|
+
await store.credentials.clear();
|
|
15351
|
+
} catch {
|
|
15352
|
+
throw new Error("device credential could not be cleared; enrollment remains paired and fail-closed");
|
|
15353
|
+
}
|
|
15354
|
+
try {
|
|
15355
|
+
await store.remove();
|
|
15356
|
+
} catch (error) {
|
|
15357
|
+
auth = buildAuthManager();
|
|
15358
|
+
throw error;
|
|
14498
15359
|
}
|
|
15360
|
+
if (current) await cursorStore.clear(config.serverUrl, current.deviceId);
|
|
14499
15361
|
} finally {
|
|
14500
15362
|
await cleanupLease.release();
|
|
14501
15363
|
}
|
|
@@ -14577,6 +15439,38 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
14577
15439
|
const controlMethods = {
|
|
14578
15440
|
unary: {
|
|
14579
15441
|
status: () => buildControlStatus(),
|
|
15442
|
+
"enrollment.pair": async (params) => {
|
|
15443
|
+
if (config.serviceEnrollment?.enabled !== true) {
|
|
15444
|
+
throw new ControlError("enrollment_disabled", "service enrollment is not enabled for this daemon");
|
|
15445
|
+
}
|
|
15446
|
+
const parsed = parseEnrollmentPairParams(params);
|
|
15447
|
+
if (!parsed) {
|
|
15448
|
+
throw new ControlError(
|
|
15449
|
+
"bad_request",
|
|
15450
|
+
`enrollment.pair requires exactly {pairingCode} with 1-${ENROLLMENT_PAIRING_CODE_MAX_BYTES} UTF-8 bytes`
|
|
15451
|
+
);
|
|
15452
|
+
}
|
|
15453
|
+
if (!serviceEnrollmentWaiting || daemonStarted) {
|
|
15454
|
+
throw new ControlError("already_paired", "this service daemon is not waiting for initial enrollment");
|
|
15455
|
+
}
|
|
15456
|
+
if (serviceEnrollmentTransitioning) {
|
|
15457
|
+
throw new ControlError("pairing_in_progress", "service enrollment is already in progress");
|
|
15458
|
+
}
|
|
15459
|
+
serviceEnrollmentTransitioning = true;
|
|
15460
|
+
let record;
|
|
15461
|
+
try {
|
|
15462
|
+
record = await runLifecycleMutation(() => pairUnderLease(parsed.pairingCode));
|
|
15463
|
+
} catch (err) {
|
|
15464
|
+
serviceEnrollmentTransitioning = false;
|
|
15465
|
+
throw err;
|
|
15466
|
+
}
|
|
15467
|
+
setImmediate(() => {
|
|
15468
|
+
void runLifecycleMutation(startUnderLease).catch(() => {
|
|
15469
|
+
console.error("[byok/client] service enrollment persisted but normal daemon startup failed");
|
|
15470
|
+
});
|
|
15471
|
+
});
|
|
15472
|
+
return { deviceId: record.deviceId };
|
|
15473
|
+
},
|
|
14580
15474
|
"toolsets.reload": (params) => {
|
|
14581
15475
|
const parsed = parseToolsetsReloadParams(params);
|
|
14582
15476
|
if (!parsed) {
|
|
@@ -14687,7 +15581,12 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
14687
15581
|
observer.noteDeviceAssertion({ result: "denied", reason: "revoked", audience: parsed.audience });
|
|
14688
15582
|
throw new ControlError("revoked", "this device has been revoked by the server; re-pair required");
|
|
14689
15583
|
}
|
|
14690
|
-
|
|
15584
|
+
let record;
|
|
15585
|
+
try {
|
|
15586
|
+
record = await auth.readCurrent();
|
|
15587
|
+
} catch (error) {
|
|
15588
|
+
if (!(error instanceof DeviceRecordRePairRequiredError)) throw error;
|
|
15589
|
+
}
|
|
14691
15590
|
if (record === void 0) {
|
|
14692
15591
|
observer.noteDeviceAssertion({ result: "denied", reason: "not_paired", audience: parsed.audience });
|
|
14693
15592
|
throw new ControlError("not_paired", "this device is not paired; nothing can be asserted about it");
|
|
@@ -15155,9 +16054,9 @@ function plistString(value) {
|
|
|
15155
16054
|
function generateLaunchdPlist(def) {
|
|
15156
16055
|
const { label, program, logDir } = def;
|
|
15157
16056
|
const args = [program.command, ...program.args];
|
|
15158
|
-
const cwd = program.cwd ??
|
|
15159
|
-
const outLog =
|
|
15160
|
-
const errLog =
|
|
16057
|
+
const cwd = program.cwd ?? os__default.homedir();
|
|
16058
|
+
const outLog = path3__default.join(logDir, `${label}.out.log`);
|
|
16059
|
+
const errLog = path3__default.join(logDir, `${label}.err.log`);
|
|
15161
16060
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
15162
16061
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
15163
16062
|
<plist version="1.0">
|
|
@@ -15189,8 +16088,8 @@ ${args.map((a) => ` ${plistString(a)}`).join("\n")}
|
|
|
15189
16088
|
}
|
|
15190
16089
|
function createLaunchdLifecycle(def, deps = {}) {
|
|
15191
16090
|
const run = deps.run ?? defaultRunner;
|
|
15192
|
-
const
|
|
15193
|
-
const homedir = deps.homedir ?? (() =>
|
|
16091
|
+
const fs28 = deps.fs ?? promises;
|
|
16092
|
+
const homedir = deps.homedir ?? (() => os__default.homedir());
|
|
15194
16093
|
const getuid = deps.getuid ?? (() => {
|
|
15195
16094
|
if (typeof process.getuid !== "function") {
|
|
15196
16095
|
throw new Error("launchd lifecycle requires a POSIX uid (process.getuid unavailable) \u2014 this module only runs on macOS");
|
|
@@ -15198,12 +16097,12 @@ function createLaunchdLifecycle(def, deps = {}) {
|
|
|
15198
16097
|
return process.getuid();
|
|
15199
16098
|
});
|
|
15200
16099
|
const label = sanitizeServiceName(def.name);
|
|
15201
|
-
const plistPath = () =>
|
|
16100
|
+
const plistPath = () => path3__default.join(homedir(), "Library", "LaunchAgents", `${label}.plist`);
|
|
15202
16101
|
const domainTarget = () => `gui/${getuid()}`;
|
|
15203
16102
|
const serviceTarget = () => `${domainTarget()}/${label}`;
|
|
15204
16103
|
async function fileExists(p) {
|
|
15205
16104
|
try {
|
|
15206
|
-
await
|
|
16105
|
+
await fs28.stat(p);
|
|
15207
16106
|
return true;
|
|
15208
16107
|
} catch {
|
|
15209
16108
|
return false;
|
|
@@ -15211,9 +16110,9 @@ function createLaunchdLifecycle(def, deps = {}) {
|
|
|
15211
16110
|
}
|
|
15212
16111
|
async function writePlist(program) {
|
|
15213
16112
|
const xml = generateLaunchdPlist({ label, program, logDir: def.logDir });
|
|
15214
|
-
await
|
|
15215
|
-
await
|
|
15216
|
-
await
|
|
16113
|
+
await fs28.mkdir(path3__default.dirname(plistPath()), { recursive: true });
|
|
16114
|
+
await fs28.mkdir(def.logDir, { recursive: true });
|
|
16115
|
+
await fs28.writeFile(plistPath(), xml, "utf8");
|
|
15217
16116
|
}
|
|
15218
16117
|
async function install(opts = {}) {
|
|
15219
16118
|
await writePlist(opts.program ?? def.program);
|
|
@@ -15224,7 +16123,7 @@ function createLaunchdLifecycle(def, deps = {}) {
|
|
|
15224
16123
|
}
|
|
15225
16124
|
async function uninstall() {
|
|
15226
16125
|
await runIdempotent(run, "launchctl", ["bootout", serviceTarget()], "launchctl bootout", LAUNCHD_NOT_LOADED);
|
|
15227
|
-
await
|
|
16126
|
+
await fs28.rm(plistPath(), { force: true });
|
|
15228
16127
|
}
|
|
15229
16128
|
async function start() {
|
|
15230
16129
|
if (!await fileExists(plistPath())) {
|
|
@@ -15283,10 +16182,10 @@ function generateSystemdUnit(def) {
|
|
|
15283
16182
|
const { name, displayName, program, logDir } = def;
|
|
15284
16183
|
assertNoControlChars(name, "name");
|
|
15285
16184
|
assertNoControlChars(displayName, "displayName");
|
|
15286
|
-
const cwd = program.cwd ??
|
|
16185
|
+
const cwd = program.cwd ?? os__default.homedir();
|
|
15287
16186
|
assertNoControlChars(cwd, "program.cwd");
|
|
15288
|
-
const outLog =
|
|
15289
|
-
const errLog =
|
|
16187
|
+
const outLog = path3__default.join(logDir, `${name}.out.log`);
|
|
16188
|
+
const errLog = path3__default.join(logDir, `${name}.err.log`);
|
|
15290
16189
|
assertNoControlChars(outLog, "logDir");
|
|
15291
16190
|
assertNoControlChars(errLog, "logDir");
|
|
15292
16191
|
const execStart = [program.command, ...program.args].map(quoteSystemdArg).join(" ");
|
|
@@ -15308,14 +16207,14 @@ WantedBy=default.target
|
|
|
15308
16207
|
}
|
|
15309
16208
|
function createSystemdLifecycle(def, deps = {}) {
|
|
15310
16209
|
const run = deps.run ?? defaultRunner;
|
|
15311
|
-
const
|
|
15312
|
-
const homedir = deps.homedir ?? (() =>
|
|
16210
|
+
const fs28 = deps.fs ?? promises;
|
|
16211
|
+
const homedir = deps.homedir ?? (() => os__default.homedir());
|
|
15313
16212
|
const name = sanitizeServiceName(def.name);
|
|
15314
16213
|
const unitName = `${name}.service`;
|
|
15315
|
-
const unitPath = () =>
|
|
16214
|
+
const unitPath = () => path3__default.join(homedir(), ".config", "systemd", "user", unitName);
|
|
15316
16215
|
async function fileExists(p) {
|
|
15317
16216
|
try {
|
|
15318
|
-
await
|
|
16217
|
+
await fs28.stat(p);
|
|
15319
16218
|
return true;
|
|
15320
16219
|
} catch {
|
|
15321
16220
|
return false;
|
|
@@ -15323,9 +16222,9 @@ function createSystemdLifecycle(def, deps = {}) {
|
|
|
15323
16222
|
}
|
|
15324
16223
|
async function writeUnit(program) {
|
|
15325
16224
|
const unit = generateSystemdUnit({ name, displayName: def.displayName ?? def.name, program, logDir: def.logDir });
|
|
15326
|
-
await
|
|
15327
|
-
await
|
|
15328
|
-
await
|
|
16225
|
+
await fs28.mkdir(path3__default.dirname(unitPath()), { recursive: true });
|
|
16226
|
+
await fs28.mkdir(def.logDir, { recursive: true });
|
|
16227
|
+
await fs28.writeFile(unitPath(), unit, "utf8");
|
|
15329
16228
|
}
|
|
15330
16229
|
async function install(opts = {}) {
|
|
15331
16230
|
await writeUnit(opts.program ?? def.program);
|
|
@@ -15334,7 +16233,7 @@ function createSystemdLifecycle(def, deps = {}) {
|
|
|
15334
16233
|
}
|
|
15335
16234
|
async function uninstall() {
|
|
15336
16235
|
await runIdempotent(run, "systemctl", ["--user", "disable", "--now", unitName], "systemctl disable --now", SYSTEMD_NOT_LOADED);
|
|
15337
|
-
await
|
|
16236
|
+
await fs28.rm(unitPath(), { force: true });
|
|
15338
16237
|
await run("systemctl", ["--user", "daemon-reload"]);
|
|
15339
16238
|
}
|
|
15340
16239
|
async function start() {
|
|
@@ -15397,7 +16296,7 @@ ${argXml}${cwdXml}
|
|
|
15397
16296
|
}
|
|
15398
16297
|
function createWinswLifecycle(def, deps = {}) {
|
|
15399
16298
|
const run = deps.run ?? defaultRunner;
|
|
15400
|
-
const
|
|
16299
|
+
const fs28 = deps.fs ?? promises;
|
|
15401
16300
|
const windows = def.windows;
|
|
15402
16301
|
if (!windows) {
|
|
15403
16302
|
throw new Error("WinSW service lifecycle requires `ServiceDefinition.windows.winswBin` (the product-bundled WinSW executable path)");
|
|
@@ -15405,11 +16304,11 @@ function createWinswLifecycle(def, deps = {}) {
|
|
|
15405
16304
|
const winswBin = windows.winswBin;
|
|
15406
16305
|
const id = sanitizeServiceName(def.name);
|
|
15407
16306
|
const installDir = windows.installDir ?? def.logDir;
|
|
15408
|
-
const exePath =
|
|
15409
|
-
const xmlPath =
|
|
16307
|
+
const exePath = path3__default.join(installDir, `${id}.exe`);
|
|
16308
|
+
const xmlPath = path3__default.join(installDir, `${id}.xml`);
|
|
15410
16309
|
async function fileExists(p) {
|
|
15411
16310
|
try {
|
|
15412
|
-
await
|
|
16311
|
+
await fs28.stat(p);
|
|
15413
16312
|
return true;
|
|
15414
16313
|
} catch {
|
|
15415
16314
|
return false;
|
|
@@ -15417,10 +16316,10 @@ function createWinswLifecycle(def, deps = {}) {
|
|
|
15417
16316
|
}
|
|
15418
16317
|
async function writeFiles(program) {
|
|
15419
16318
|
const xml = generateWinswXml({ id, displayName: def.displayName ?? def.name, program, logDir: def.logDir });
|
|
15420
|
-
await
|
|
15421
|
-
await
|
|
15422
|
-
await
|
|
15423
|
-
await
|
|
16319
|
+
await fs28.mkdir(installDir, { recursive: true });
|
|
16320
|
+
await fs28.mkdir(def.logDir, { recursive: true });
|
|
16321
|
+
await fs28.copyFile(winswBin, exePath);
|
|
16322
|
+
await fs28.writeFile(xmlPath, xml, "utf8");
|
|
15424
16323
|
}
|
|
15425
16324
|
async function install(opts = {}) {
|
|
15426
16325
|
await writeFiles(opts.program ?? def.program);
|
|
@@ -15430,8 +16329,8 @@ function createWinswLifecycle(def, deps = {}) {
|
|
|
15430
16329
|
async function uninstall() {
|
|
15431
16330
|
await runIdempotent(run, exePath, ["stop"], "winsw stop", WINSW_NOT_INSTALLED);
|
|
15432
16331
|
await runIdempotent(run, exePath, ["uninstall"], "winsw uninstall", WINSW_NOT_INSTALLED);
|
|
15433
|
-
await
|
|
15434
|
-
await
|
|
16332
|
+
await fs28.rm(exePath, { force: true });
|
|
16333
|
+
await fs28.rm(xmlPath, { force: true });
|
|
15435
16334
|
}
|
|
15436
16335
|
async function start() {
|
|
15437
16336
|
if (!await fileExists(xmlPath)) {
|
|
@@ -15476,7 +16375,7 @@ function createServiceLifecycle(def, opts = {}) {
|
|
|
15476
16375
|
|
|
15477
16376
|
// src/bin/official-release.ts
|
|
15478
16377
|
var OFFICIAL_LOCAL_AGENT_RELEASE = resolveLocalAgentReleaseIdentity({
|
|
15479
|
-
version: "0.8.
|
|
16378
|
+
version: "0.8.1"
|
|
15480
16379
|
});
|
|
15481
16380
|
|
|
15482
16381
|
// src/bin/config.ts
|
|
@@ -15871,7 +16770,23 @@ async function resolveApproval(storeDir, productId, approvalId, decision, reason
|
|
|
15871
16770
|
// src/bin/commands/pair.ts
|
|
15872
16771
|
async function runPairCommand(config, code, deps = {}) {
|
|
15873
16772
|
const log = deps.log ?? ((line) => console.log(line));
|
|
15874
|
-
|
|
16773
|
+
if (deps.daemon) {
|
|
16774
|
+
const result2 = await deps.daemon.pair(code);
|
|
16775
|
+
log(`paired: deviceId=${result2.deviceId}`);
|
|
16776
|
+
return;
|
|
16777
|
+
}
|
|
16778
|
+
const connectControl = deps.connectControl ?? connectControlClient;
|
|
16779
|
+
const conn = await connectControl({ storeDir: resolveStoreDir(config), productId: config.productId });
|
|
16780
|
+
if (conn.ok) {
|
|
16781
|
+
try {
|
|
16782
|
+
const result2 = await conn.client.request("enrollment.pair", { pairingCode: code });
|
|
16783
|
+
log(`paired: deviceId=${result2.deviceId}`);
|
|
16784
|
+
return;
|
|
16785
|
+
} finally {
|
|
16786
|
+
conn.client.close();
|
|
16787
|
+
}
|
|
16788
|
+
}
|
|
16789
|
+
const daemon = createDaemon(config);
|
|
15875
16790
|
const result = await daemon.pair(code);
|
|
15876
16791
|
log(`paired: deviceId=${result.deviceId}`);
|
|
15877
16792
|
}
|
|
@@ -15961,7 +16876,7 @@ function safeProtocol(serverUrl) {
|
|
|
15961
16876
|
}
|
|
15962
16877
|
}
|
|
15963
16878
|
async function inspectDevice(storeDir) {
|
|
15964
|
-
const filePath =
|
|
16879
|
+
const filePath = path3__default.join(storeDir, "device.json");
|
|
15965
16880
|
let pathStat;
|
|
15966
16881
|
try {
|
|
15967
16882
|
pathStat = await promises.lstat(filePath);
|
|
@@ -16041,11 +16956,11 @@ async function copyOpenFileBounded(source, expected, destinationPath) {
|
|
|
16041
16956
|
}
|
|
16042
16957
|
}
|
|
16043
16958
|
async function inspectJournal(storeDir) {
|
|
16044
|
-
const journalPath =
|
|
16959
|
+
const journalPath = path3__default.join(storeDir, JOURNAL_DB_FILENAME);
|
|
16045
16960
|
try {
|
|
16046
16961
|
const mainIdentity = await regularFileIdentity(journalPath);
|
|
16047
16962
|
if (mainIdentity === void 0) return { status: "missing" };
|
|
16048
|
-
const walIdentity = await regularFileIdentity(
|
|
16963
|
+
const walIdentity = await regularFileIdentity(path3__default.join(storeDir, `${JOURNAL_DB_FILENAME}-wal`));
|
|
16049
16964
|
let sizeBytes = Number(mainIdentity.size);
|
|
16050
16965
|
let walBytes = walIdentity === void 0 ? void 0 : Number(walIdentity.size);
|
|
16051
16966
|
if (!isSqliteAvailable()) {
|
|
@@ -16053,7 +16968,7 @@ async function inspectJournal(storeDir) {
|
|
|
16053
16968
|
}
|
|
16054
16969
|
const componentNames = [JOURNAL_DB_FILENAME, `${JOURNAL_DB_FILENAME}-wal`, `${JOURNAL_DB_FILENAME}-shm`];
|
|
16055
16970
|
const initial = /* @__PURE__ */ new Map();
|
|
16056
|
-
for (const name of componentNames) initial.set(name, await regularFileIdentity(
|
|
16971
|
+
for (const name of componentNames) initial.set(name, await regularFileIdentity(path3__default.join(storeDir, name)));
|
|
16057
16972
|
const snapshotMain = initial.get(JOURNAL_DB_FILENAME);
|
|
16058
16973
|
if (!snapshotMain) return { status: "unavailable", reason: "journal changed during diagnostics snapshot" };
|
|
16059
16974
|
sizeBytes = Number(snapshotMain.size);
|
|
@@ -16065,7 +16980,7 @@ async function inspectJournal(storeDir) {
|
|
|
16065
16980
|
let handle;
|
|
16066
16981
|
try {
|
|
16067
16982
|
handle = await promises.open(
|
|
16068
|
-
|
|
16983
|
+
path3__default.join(storeDir, name),
|
|
16069
16984
|
constants.O_RDONLY | constants.O_NONBLOCK | (constants.O_NOFOLLOW ?? 0)
|
|
16070
16985
|
);
|
|
16071
16986
|
} catch (err) {
|
|
@@ -16094,28 +17009,28 @@ async function inspectJournal(storeDir) {
|
|
|
16094
17009
|
reason: "journal exceeds the bounded diagnostics copy limit"
|
|
16095
17010
|
};
|
|
16096
17011
|
}
|
|
16097
|
-
const tempDir = await promises.mkdtemp(
|
|
17012
|
+
const tempDir = await promises.mkdtemp(path3__default.join(os__default.tmpdir(), "byok-journal-inspect-"));
|
|
16098
17013
|
const { DatabaseSync } = loadSqliteModule();
|
|
16099
17014
|
let db;
|
|
16100
17015
|
try {
|
|
16101
17016
|
for (const name of componentNames) {
|
|
16102
17017
|
const component = opened.get(name);
|
|
16103
|
-
if (component && !await copyOpenFileBounded(component.handle, component.identity,
|
|
17018
|
+
if (component && !await copyOpenFileBounded(component.handle, component.identity, path3__default.join(tempDir, name))) {
|
|
16104
17019
|
return { status: "unavailable", sizeBytes, ...walBytes === void 0 ? {} : { walBytes }, reason: "journal changed during diagnostics snapshot" };
|
|
16105
17020
|
}
|
|
16106
17021
|
}
|
|
16107
17022
|
for (const [name, component] of opened) {
|
|
16108
|
-
if (!sameIdentity(component.identity, identityFromBigIntStat(await component.handle.stat({ bigint: true }))) || !sameIdentity(component.identity, await regularFileIdentity(
|
|
17023
|
+
if (!sameIdentity(component.identity, identityFromBigIntStat(await component.handle.stat({ bigint: true }))) || !sameIdentity(component.identity, await regularFileIdentity(path3__default.join(storeDir, name)))) {
|
|
16109
17024
|
return { status: "unavailable", sizeBytes, ...walBytes === void 0 ? {} : { walBytes }, reason: "journal changed during diagnostics snapshot" };
|
|
16110
17025
|
}
|
|
16111
17026
|
}
|
|
16112
17027
|
for (const name of componentNames) {
|
|
16113
|
-
if (!opened.has(name) && await regularFileIdentity(
|
|
17028
|
+
if (!opened.has(name) && await regularFileIdentity(path3__default.join(storeDir, name)) !== void 0) {
|
|
16114
17029
|
return { status: "unavailable", sizeBytes, ...walBytes === void 0 ? {} : { walBytes }, reason: "journal changed during diagnostics snapshot" };
|
|
16115
17030
|
}
|
|
16116
17031
|
}
|
|
16117
17032
|
const header = Buffer.alloc(16);
|
|
16118
|
-
const copiedHandle = await promises.open(
|
|
17033
|
+
const copiedHandle = await promises.open(path3__default.join(tempDir, JOURNAL_DB_FILENAME), "r");
|
|
16119
17034
|
try {
|
|
16120
17035
|
const { bytesRead } = await copiedHandle.read(header, 0, header.length, 0);
|
|
16121
17036
|
if (bytesRead !== 16 || header.toString("binary") !== "SQLite format 3\0") {
|
|
@@ -16124,7 +17039,7 @@ async function inspectJournal(storeDir) {
|
|
|
16124
17039
|
} finally {
|
|
16125
17040
|
await copiedHandle.close();
|
|
16126
17041
|
}
|
|
16127
|
-
db = new DatabaseSync(
|
|
17042
|
+
db = new DatabaseSync(path3__default.join(tempDir, JOURNAL_DB_FILENAME), { readOnly: true });
|
|
16128
17043
|
const result = db.prepare("PRAGMA quick_check(1)").get();
|
|
16129
17044
|
if (result?.quick_check !== "ok") {
|
|
16130
17045
|
return { status: "corrupt", sizeBytes, ...walBytes === void 0 ? {} : { walBytes }, reason: "journal quick_check failed" };
|
|
@@ -16159,7 +17074,7 @@ async function inspectWorkspace(workspaceRoot) {
|
|
|
16159
17074
|
}
|
|
16160
17075
|
}
|
|
16161
17076
|
function readPinnedQuarantineFile(name, maxBytes, includeBytes, budget) {
|
|
16162
|
-
if (
|
|
17077
|
+
if (path3__default.basename(name) !== name || name === "." || name === "..") {
|
|
16163
17078
|
throw new Error("quarantine manifest contains an invalid evidence name");
|
|
16164
17079
|
}
|
|
16165
17080
|
const namedBefore = lstatSync(name, { bigint: true });
|
|
@@ -16263,10 +17178,10 @@ function inspectQuarantinePinned(dir, expectedDirectory) {
|
|
|
16263
17178
|
if (isJournalQuarantineManifest(parsed)) {
|
|
16264
17179
|
const manifestBase = manifestName.slice(0, -".manifest.json".length);
|
|
16265
17180
|
const boundNames = parsed.files.map((file) => {
|
|
16266
|
-
if (
|
|
17181
|
+
if (path3__default.dirname(path3__default.resolve(file)) !== path3__default.resolve(".")) {
|
|
16267
17182
|
throw new Error("journal quarantine manifest points outside quarantine");
|
|
16268
17183
|
}
|
|
16269
|
-
return
|
|
17184
|
+
return path3__default.basename(file);
|
|
16270
17185
|
});
|
|
16271
17186
|
if (!boundNames.includes(manifestBase)) {
|
|
16272
17187
|
throw new Error("journal quarantine manifest is not bound to its primary database evidence");
|
|
@@ -16306,7 +17221,7 @@ function inspectQuarantinePinned(dir, expectedDirectory) {
|
|
|
16306
17221
|
}
|
|
16307
17222
|
}
|
|
16308
17223
|
async function inspectQuarantine(storeDir) {
|
|
16309
|
-
const dir =
|
|
17224
|
+
const dir = path3__default.join(storeDir, JOURNAL_QUARANTINE_DIRNAME);
|
|
16310
17225
|
let directory;
|
|
16311
17226
|
try {
|
|
16312
17227
|
directory = await promises.lstat(dir, { bigint: true });
|
|
@@ -16377,7 +17292,7 @@ function checksFor(snapshot) {
|
|
|
16377
17292
|
];
|
|
16378
17293
|
}
|
|
16379
17294
|
async function collectDiagnostics(config, storeDir, options = {}) {
|
|
16380
|
-
const resolvedStoreDir =
|
|
17295
|
+
const resolvedStoreDir = path3__default.resolve(storeDir);
|
|
16381
17296
|
const adapters = options.adapters ?? defaultRuntimeAdapters(config.runtimeAllowlist);
|
|
16382
17297
|
const connectControl = options.connectControl ?? connectControlClient;
|
|
16383
17298
|
const [device, probedRuntimes, health, journal, workspace, quarantine, controlConnection] = await Promise.all([
|
|
@@ -16552,7 +17467,7 @@ function publishQuarantineEvidencePinned(quarantineDir, expectedDirectory, sourc
|
|
|
16552
17467
|
unlinkSync(sourcePath);
|
|
16553
17468
|
sourceRemoved = true;
|
|
16554
17469
|
if (process.platform !== "win32") {
|
|
16555
|
-
const directoryFd = openSync(
|
|
17470
|
+
const directoryFd = openSync(path3__default.dirname(sourcePath), constants.O_RDONLY);
|
|
16556
17471
|
try {
|
|
16557
17472
|
fsyncSync(directoryFd);
|
|
16558
17473
|
} finally {
|
|
@@ -16591,10 +17506,10 @@ function publishQuarantineEvidencePinned(quarantineDir, expectedDirectory, sourc
|
|
|
16591
17506
|
}
|
|
16592
17507
|
}
|
|
16593
17508
|
async function quarantineCorruptOperationalHealth(storeDir, options = {}) {
|
|
16594
|
-
const resolvedStoreDir =
|
|
17509
|
+
const resolvedStoreDir = path3__default.resolve(storeDir);
|
|
16595
17510
|
const owner = await acquireDaemonOwner(resolvedStoreDir, "doctor", options.clock);
|
|
16596
17511
|
try {
|
|
16597
|
-
const sourcePath =
|
|
17512
|
+
const sourcePath = path3__default.join(resolvedStoreDir, OPERATIONAL_HEALTH_FILENAME);
|
|
16598
17513
|
let opened;
|
|
16599
17514
|
try {
|
|
16600
17515
|
opened = await openOperationalHealthFile(resolvedStoreDir);
|
|
@@ -16611,7 +17526,7 @@ async function quarantineCorruptOperationalHealth(storeDir, options = {}) {
|
|
|
16611
17526
|
}
|
|
16612
17527
|
const sourceStat = await source.stat({ bigint: true });
|
|
16613
17528
|
if (!sourceStat.isFile()) throw new Error("operational health state is not a regular file; refusing quarantine");
|
|
16614
|
-
const quarantineDir =
|
|
17529
|
+
const quarantineDir = path3__default.join(resolvedStoreDir, JOURNAL_QUARANTINE_DIRNAME);
|
|
16615
17530
|
try {
|
|
16616
17531
|
const existing = await promises.lstat(quarantineDir);
|
|
16617
17532
|
if (!existing.isDirectory() || existing.isSymbolicLink()) {
|
|
@@ -16712,8 +17627,8 @@ function buildServiceDefinition(config, configPath, rest) {
|
|
|
16712
17627
|
const name = argValue(rest, "--name") ?? config.productId;
|
|
16713
17628
|
const agentBin = argValue(rest, "--agent-bin") ?? process.argv[1] ?? "byok-agent";
|
|
16714
17629
|
const nodeBin = argValue(rest, "--node-bin") ?? process.execPath;
|
|
16715
|
-
const absoluteConfigPath =
|
|
16716
|
-
const logDir =
|
|
17630
|
+
const absoluteConfigPath = path3__default.resolve(configPath);
|
|
17631
|
+
const logDir = path3__default.join(resolveStoreDir(config), "service-logs");
|
|
16717
17632
|
const definition = {
|
|
16718
17633
|
name,
|
|
16719
17634
|
displayName: config.branding?.displayName ?? config.productName,
|
|
@@ -16767,7 +17682,7 @@ async function runServiceStatusCommand(config, configPath, rest, deps = {}) {
|
|
|
16767
17682
|
log(`detail: ${status.detail.trim() || "(none)"}`);
|
|
16768
17683
|
}
|
|
16769
17684
|
function auditLogPath(storeDir) {
|
|
16770
|
-
return
|
|
17685
|
+
return path3__default.join(storeDir, "audit.jsonl");
|
|
16771
17686
|
}
|
|
16772
17687
|
var AUDIT_LOG_MODE = 384;
|
|
16773
17688
|
var AUDIT_STORE_DIR_MODE = 448;
|
|
@@ -17605,11 +18520,11 @@ async function createSupportBundle(config, storeDir, options = {}) {
|
|
|
17605
18520
|
};
|
|
17606
18521
|
}
|
|
17607
18522
|
async function writeSupportBundle(outputPath, bundle, secureFileOptions = {}) {
|
|
17608
|
-
const dir =
|
|
18523
|
+
const dir = path3__default.dirname(outputPath);
|
|
17609
18524
|
const parentStat = await promises.stat(dir);
|
|
17610
18525
|
if (!parentStat.isDirectory()) throw new Error("support bundle output parent is not a directory");
|
|
17611
|
-
const privateDir =
|
|
17612
|
-
const tempPath =
|
|
18526
|
+
const privateDir = path3__default.join(dir, `.${path3__default.basename(outputPath)}.${process.pid}.${randomUUID()}.private`);
|
|
18527
|
+
const tempPath = path3__default.join(privateDir, "bundle.tmp");
|
|
17613
18528
|
try {
|
|
17614
18529
|
await promises.mkdir(privateDir, { mode: 448 });
|
|
17615
18530
|
await ensureSecureDir(privateDir, secureFileOptions);
|
|
@@ -17639,7 +18554,7 @@ async function writeSupportBundle(outputPath, bundle, secureFileOptions = {}) {
|
|
|
17639
18554
|
// src/bin/commands/support-bundle.ts
|
|
17640
18555
|
async function runSupportBundleCommand(config, options) {
|
|
17641
18556
|
if (!options.outputPath) throw new Error("support-bundle requires --output <path>");
|
|
17642
|
-
const outputPath =
|
|
18557
|
+
const outputPath = path3__default.resolve(options.outputPath);
|
|
17643
18558
|
const bundle = await createSupportBundle(config, resolveStoreDir(config), options);
|
|
17644
18559
|
await writeSupportBundle(outputPath, bundle);
|
|
17645
18560
|
const log = options.log ?? ((line) => console.log(line));
|
|
@@ -17675,8 +18590,8 @@ async function runTasksFollowCommand(config, deps) {
|
|
|
17675
18590
|
});
|
|
17676
18591
|
return;
|
|
17677
18592
|
}
|
|
17678
|
-
const
|
|
17679
|
-
await followAuditLog(
|
|
18593
|
+
const path36 = auditLogPath(storeDir);
|
|
18594
|
+
await followAuditLog(path36, (event) => log(formatDaemonEventLine(event)), {
|
|
17680
18595
|
signal: deps.signal,
|
|
17681
18596
|
pollIntervalMs: deps.pollIntervalMs,
|
|
17682
18597
|
fromEnd: true
|