@rudderhq/agent-runtime-codex-local 0.7.21-canary.8 → 0.7.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/server/codex-home.d.ts.map +1 -1
- package/dist/server/codex-home.js +44 -40
- package/dist/server/codex-home.js.map +1 -1
- package/dist/server/codex-home.test.js +99 -7
- package/dist/server/codex-home.test.js.map +1 -1
- package/dist/server/execute.d.ts.map +1 -1
- package/dist/server/execute.js +144 -78
- package/dist/server/execute.js.map +1 -1
- package/dist/server/readiness-gate.d.ts +17 -2
- package/dist/server/readiness-gate.d.ts.map +1 -1
- package/dist/server/readiness-gate.js +604 -29
- package/dist/server/readiness-gate.js.map +1 -1
- package/dist/server/readiness-gate.test.js +302 -3
- package/dist/server/readiness-gate.test.js.map +1 -1
- package/package.json +3 -3
|
@@ -1,8 +1,19 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
1
2
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
3
|
import fs from "node:fs/promises";
|
|
3
4
|
import path from "node:path";
|
|
5
|
+
import { promisify } from "node:util";
|
|
4
6
|
const STATE_VERSION = 1;
|
|
5
7
|
const STATE_RELATIVE_DIRECTORY = path.join(".rudder", "provider-readiness", "codex");
|
|
8
|
+
const AUTH_FAILURE_COOLDOWN_MS = 60_000;
|
|
9
|
+
const PROBE_LEASE_MS = 120_000;
|
|
10
|
+
const STATE_LOCK_STALE_MS = 30_000;
|
|
11
|
+
const STATE_LOCK_WAIT_MS = 2_000;
|
|
12
|
+
const STATE_LOCK_RETRY_MS = 10;
|
|
13
|
+
const STATE_LOCK_COORDINATION_SUFFIX = ".coordination";
|
|
14
|
+
const PROCESS_START_LOOKUP_TIMEOUT_MS = 500;
|
|
15
|
+
const execFileAsync = promisify(execFile);
|
|
16
|
+
const activeProbeLeases = new Map();
|
|
6
17
|
function digestParts(parts) {
|
|
7
18
|
const hash = createHash("sha256");
|
|
8
19
|
for (const part of parts) {
|
|
@@ -14,16 +25,124 @@ function digestParts(parts) {
|
|
|
14
25
|
}
|
|
15
26
|
return hash.digest("hex");
|
|
16
27
|
}
|
|
28
|
+
async function readProcessStartIdentity(pid) {
|
|
29
|
+
if (!Number.isSafeInteger(pid) || pid <= 0)
|
|
30
|
+
return null;
|
|
31
|
+
if (process.platform === "linux") {
|
|
32
|
+
const raw = await fs.readFile(`/proc/${pid}/stat`, "utf8").catch(() => null);
|
|
33
|
+
if (raw) {
|
|
34
|
+
const closingCommand = raw.lastIndexOf(") ");
|
|
35
|
+
const fields = closingCommand >= 0 ? raw.slice(closingCommand + 2).trim().split(/\s+/u) : [];
|
|
36
|
+
// /proc fields start at field 3 after the command name. Field 22 is the
|
|
37
|
+
// process start time in clock ticks, which survives PID reuse.
|
|
38
|
+
const startTime = fields[19];
|
|
39
|
+
if (startTime)
|
|
40
|
+
return `linux:${startTime}`;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
const { stdout } = await execFileAsync("ps", ["-p", String(pid), "-o", "lstart="], { timeout: PROCESS_START_LOOKUP_TIMEOUT_MS, windowsHide: true });
|
|
45
|
+
const startTime = stdout.trim();
|
|
46
|
+
return startTime ? `ps:${startTime}` : null;
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function activeProbeKey(agentHome, fingerprint) {
|
|
53
|
+
return statePath(agentHome, fingerprint);
|
|
54
|
+
}
|
|
55
|
+
function rememberActiveProbe(agentHome, fingerprint, probeId) {
|
|
56
|
+
activeProbeLeases.set(activeProbeKey(agentHome, fingerprint), {
|
|
57
|
+
probeId,
|
|
58
|
+
renewedAt: process.hrtime.bigint(),
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
function forgetActiveProbe(agentHome, fingerprint, probeId) {
|
|
62
|
+
const key = activeProbeKey(agentHome, fingerprint);
|
|
63
|
+
const current = activeProbeLeases.get(key);
|
|
64
|
+
if (!current || probeId === undefined || current.probeId === probeId) {
|
|
65
|
+
activeProbeLeases.delete(key);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function hasFreshLocalProbeLease(agentHome, fingerprint, probeId) {
|
|
69
|
+
const key = activeProbeKey(agentHome, fingerprint);
|
|
70
|
+
const current = activeProbeLeases.get(key);
|
|
71
|
+
if (!current || current.probeId !== probeId)
|
|
72
|
+
return false;
|
|
73
|
+
if (process.hrtime.bigint() - current.renewedAt >= BigInt(PROBE_LEASE_MS) * 1000000n) {
|
|
74
|
+
activeProbeLeases.delete(key);
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
17
79
|
async function readFingerprintInput(candidate) {
|
|
18
|
-
return fs.readFile(candidate).catch(() => Buffer.from("
|
|
80
|
+
return fs.readFile(candidate).catch(() => Buffer.from("", "utf8"));
|
|
81
|
+
}
|
|
82
|
+
function isTomlTableBoundary(trimmedLine) {
|
|
83
|
+
return /^\[\[.+\]\]$/.test(trimmedLine) || /^\[(?!\[).+\]$/.test(trimmedLine);
|
|
84
|
+
}
|
|
85
|
+
function isManagedCodexConfigTable(trimmedLine) {
|
|
86
|
+
return /^\[mcp_servers(?:\..+)?\]$/.test(trimmedLine)
|
|
87
|
+
|| /^\[plugins\..+\]$/.test(trimmedLine)
|
|
88
|
+
|| trimmedLine === "[[skills.config]]";
|
|
89
|
+
}
|
|
90
|
+
function isUnsupportedServiceTierLine(trimmedLine) {
|
|
91
|
+
const match = trimmedLine.match(/^service_tier\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s#]+))/i);
|
|
92
|
+
if (!match)
|
|
93
|
+
return false;
|
|
94
|
+
const value = (match[1] ?? match[2] ?? match[3] ?? "").trim().toLowerCase();
|
|
95
|
+
return value !== "fast" && value !== "flex";
|
|
96
|
+
}
|
|
97
|
+
function normalizeProviderConfig(content) {
|
|
98
|
+
const output = [];
|
|
99
|
+
let blockLines = null;
|
|
100
|
+
let blockName = "";
|
|
101
|
+
const flushBlock = () => {
|
|
102
|
+
if (!blockLines)
|
|
103
|
+
return;
|
|
104
|
+
if (!isManagedCodexConfigTable(blockName)) {
|
|
105
|
+
const filtered = blockName === "[features]"
|
|
106
|
+
? blockLines.filter((line, index) => index === 0 || !/^\s*plugins\s*=/.test(line))
|
|
107
|
+
: blockName === "[skills.bundled]"
|
|
108
|
+
? blockLines.filter((line, index) => index === 0 || !/^\s*enabled\s*=/.test(line))
|
|
109
|
+
: blockLines;
|
|
110
|
+
if (filtered.slice(1).some((line) => line.trim().length > 0))
|
|
111
|
+
output.push(...filtered);
|
|
112
|
+
}
|
|
113
|
+
blockLines = null;
|
|
114
|
+
blockName = "";
|
|
115
|
+
};
|
|
116
|
+
for (const line of content.split(/\r?\n/)) {
|
|
117
|
+
const trimmedLine = line.trim();
|
|
118
|
+
if (isTomlTableBoundary(trimmedLine)) {
|
|
119
|
+
flushBlock();
|
|
120
|
+
blockLines = [line];
|
|
121
|
+
blockName = trimmedLine;
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (blockLines) {
|
|
125
|
+
blockLines.push(line);
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (trimmedLine === "# rudder-managed-skills:start" || trimmedLine === "# rudder-managed-skills:end")
|
|
129
|
+
continue;
|
|
130
|
+
if (/^\s*notify\s*=/.test(trimmedLine) || isUnsupportedServiceTierLine(trimmedLine))
|
|
131
|
+
continue;
|
|
132
|
+
output.push(line);
|
|
133
|
+
}
|
|
134
|
+
flushBlock();
|
|
135
|
+
return output.join("\n").trim().replace(/\n{3,}/g, "\n\n");
|
|
19
136
|
}
|
|
20
137
|
export async function buildCodexReadinessFingerprint(input) {
|
|
21
138
|
const apiKey = input.env.OPENAI_API_KEY?.trim() ?? "";
|
|
22
139
|
const authSource = apiKey ? "api_key" : "subscription";
|
|
140
|
+
const snapshotHome = input.codexHome ?? input.sharedCodexHome;
|
|
23
141
|
const authMaterial = apiKey
|
|
24
142
|
? Buffer.from(apiKey, "utf8")
|
|
25
|
-
: await readFingerprintInput(path.join(
|
|
26
|
-
const
|
|
143
|
+
: await readFingerprintInput(path.join(snapshotHome, "auth.json"));
|
|
144
|
+
const rawProviderConfig = await readFingerprintInput(path.join(snapshotHome, "config.toml"));
|
|
145
|
+
const providerConfig = Buffer.from(normalizeProviderConfig(rawProviderConfig.toString("utf8")), "utf8");
|
|
27
146
|
return digestParts([
|
|
28
147
|
"rudder.codex.readiness.v1",
|
|
29
148
|
authSource,
|
|
@@ -36,6 +155,9 @@ export async function buildCodexReadinessFingerprint(input) {
|
|
|
36
155
|
function statePath(agentHome, fingerprint) {
|
|
37
156
|
return path.join(agentHome, STATE_RELATIVE_DIRECTORY, `${fingerprint}.json`);
|
|
38
157
|
}
|
|
158
|
+
function stateLockPath(agentHome, fingerprint) {
|
|
159
|
+
return `${statePath(agentHome, fingerprint)}.lock`;
|
|
160
|
+
}
|
|
39
161
|
async function readState(agentHome, fingerprint) {
|
|
40
162
|
const raw = await fs.readFile(statePath(agentHome, fingerprint), "utf8").catch(() => null);
|
|
41
163
|
if (!raw)
|
|
@@ -43,49 +165,502 @@ async function readState(agentHome, fingerprint) {
|
|
|
43
165
|
try {
|
|
44
166
|
const parsed = JSON.parse(raw);
|
|
45
167
|
if (parsed.version !== STATE_VERSION
|
|
46
|
-
||
|
|
168
|
+
|| parsed.fingerprint !== fingerprint
|
|
47
169
|
|| parsed.classification !== "authentication"
|
|
48
|
-
|| parsed.errorCode !== "codex_provider_auth_required"
|
|
49
|
-
|| typeof parsed.failedAt !== "string") {
|
|
170
|
+
|| parsed.errorCode !== "codex_provider_auth_required") {
|
|
50
171
|
return null;
|
|
51
172
|
}
|
|
52
|
-
|
|
173
|
+
const state = parsed.state === undefined ? "failed" : parsed.state;
|
|
174
|
+
if (state !== "failed" && state !== "probing")
|
|
175
|
+
return null;
|
|
176
|
+
const generation = typeof parsed.generation === "number" && Number.isSafeInteger(parsed.generation)
|
|
177
|
+
&& parsed.generation >= 0
|
|
178
|
+
? parsed.generation
|
|
179
|
+
: 0;
|
|
180
|
+
if (state === "probing" && (typeof parsed.probeId !== "string" || typeof parsed.probeStartedAt !== "string")) {
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
if (state === "failed" && typeof parsed.failedAt !== "string")
|
|
184
|
+
return null;
|
|
185
|
+
return {
|
|
186
|
+
version: STATE_VERSION,
|
|
187
|
+
fingerprint: parsed.fingerprint,
|
|
188
|
+
classification: "authentication",
|
|
189
|
+
errorCode: "codex_provider_auth_required",
|
|
190
|
+
state,
|
|
191
|
+
generation,
|
|
192
|
+
...(typeof parsed.failedAt === "string" ? { failedAt: parsed.failedAt } : {}),
|
|
193
|
+
...(typeof parsed.probeId === "string" ? { probeId: parsed.probeId } : {}),
|
|
194
|
+
...(typeof parsed.probeStartedAt === "string" ? { probeStartedAt: parsed.probeStartedAt } : {}),
|
|
195
|
+
...(typeof parsed.probeOwnerPid === "number" && Number.isSafeInteger(parsed.probeOwnerPid) && parsed.probeOwnerPid > 0
|
|
196
|
+
? { probeOwnerPid: parsed.probeOwnerPid }
|
|
197
|
+
: {}),
|
|
198
|
+
};
|
|
53
199
|
}
|
|
54
200
|
catch {
|
|
55
201
|
return null;
|
|
56
202
|
}
|
|
57
203
|
}
|
|
58
|
-
|
|
59
|
-
const
|
|
60
|
-
|
|
204
|
+
function isWithinDuration(timestamp, durationMs, nowMs) {
|
|
205
|
+
const timestampMs = typeof timestamp === "string" ? Date.parse(timestamp) : Number.NaN;
|
|
206
|
+
if (!Number.isFinite(timestampMs))
|
|
207
|
+
return false;
|
|
208
|
+
const elapsedMs = nowMs - timestampMs;
|
|
209
|
+
// A future timestamp is treated as expired. Clock skew must not turn a
|
|
210
|
+
// provider failure or abandoned probe into an indefinite block.
|
|
211
|
+
return elapsedMs >= 0 && elapsedMs < durationMs;
|
|
61
212
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
213
|
+
function isActiveState(agentHome, fingerprint, state, nowMs) {
|
|
214
|
+
return state.state === "failed"
|
|
215
|
+
? isWithinDuration(state.failedAt, AUTH_FAILURE_COOLDOWN_MS, nowMs)
|
|
216
|
+
: isProbeLeaseActive(agentHome, fingerprint, state, nowMs);
|
|
217
|
+
}
|
|
218
|
+
function isProbeLeaseActive(agentHome, fingerprint, state, nowMs) {
|
|
219
|
+
const probeStartedAtMs = typeof state.probeStartedAt === "string"
|
|
220
|
+
? Date.parse(state.probeStartedAt)
|
|
221
|
+
: Number.NaN;
|
|
222
|
+
if (!Number.isFinite(probeStartedAtMs))
|
|
223
|
+
return false;
|
|
224
|
+
if (probeStartedAtMs > nowMs) {
|
|
225
|
+
// A wall-clock rollback cannot prove that a long-lived Rudder server still
|
|
226
|
+
// owns the probe. Only a bounded in-process renewal can keep this state
|
|
227
|
+
// active; an abandoned future-dated record is therefore recoverable.
|
|
228
|
+
return hasFreshLocalProbeLease(agentHome, fingerprint, state.probeId ?? "");
|
|
229
|
+
}
|
|
230
|
+
return nowMs - probeStartedAtMs < PROBE_LEASE_MS;
|
|
231
|
+
}
|
|
232
|
+
function parseStateLock(raw) {
|
|
233
|
+
try {
|
|
234
|
+
const parsed = JSON.parse(raw);
|
|
235
|
+
if (typeof parsed.ownerToken !== "string"
|
|
236
|
+
|| parsed.ownerToken.length === 0
|
|
237
|
+
|| typeof parsed.ownerPid !== "number"
|
|
238
|
+
|| !Number.isSafeInteger(parsed.ownerPid)
|
|
239
|
+
|| parsed.ownerPid <= 0) {
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
return {
|
|
243
|
+
ownerToken: parsed.ownerToken,
|
|
244
|
+
ownerPid: parsed.ownerPid,
|
|
245
|
+
ownerStartIdentity: typeof parsed.ownerStartIdentity === "string" ? parsed.ownerStartIdentity : null,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
catch {
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
async function readStateLock(lockPath) {
|
|
253
|
+
const stat = await fs.stat(lockPath).catch(() => null);
|
|
254
|
+
if (!stat)
|
|
255
|
+
return null;
|
|
256
|
+
const raw = await fs.readFile(lockPath, "utf8").catch(() => "");
|
|
257
|
+
const parsed = parseStateLock(raw);
|
|
258
|
+
return {
|
|
259
|
+
raw,
|
|
260
|
+
ownerToken: parsed?.ownerToken ?? null,
|
|
261
|
+
ownerPid: parsed?.ownerPid ?? null,
|
|
262
|
+
ownerStartIdentity: parsed?.ownerStartIdentity ?? null,
|
|
263
|
+
stat,
|
|
73
264
|
};
|
|
74
|
-
|
|
265
|
+
}
|
|
266
|
+
function isProcessAlive(pid) {
|
|
267
|
+
if (pid === null)
|
|
268
|
+
return false;
|
|
75
269
|
try {
|
|
76
|
-
|
|
270
|
+
process.kill(pid, 0);
|
|
271
|
+
return true;
|
|
77
272
|
}
|
|
78
273
|
catch (error) {
|
|
79
|
-
|
|
80
|
-
|
|
274
|
+
return error.code !== "ESRCH";
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
function isSameFileIdentity(left, right) {
|
|
278
|
+
if (left.dev !== 0 || left.ino !== 0 || right.dev !== 0 || right.ino !== 0) {
|
|
279
|
+
return left.dev === right.dev && left.ino === right.ino;
|
|
280
|
+
}
|
|
281
|
+
return left.size === right.size && left.mtimeMs === right.mtimeMs;
|
|
282
|
+
}
|
|
283
|
+
function isSameStateLockSnapshot(left, right) {
|
|
284
|
+
return left.raw === right.raw && isSameFileIdentity(left.stat, right.stat);
|
|
285
|
+
}
|
|
286
|
+
function isSameStateLockCoordinationSnapshot(left, right) {
|
|
287
|
+
return left.raw === right.raw && isSameFileIdentity(left.stat, right.stat);
|
|
288
|
+
}
|
|
289
|
+
async function isStateLockStale(snapshot, nowMs) {
|
|
290
|
+
// A live owner is allowed to renew its descriptor even when a filesystem
|
|
291
|
+
// clock jumps backwards or the operation is temporarily slow.
|
|
292
|
+
if (isProcessAlive(snapshot.ownerPid)) {
|
|
293
|
+
if (!snapshot.ownerStartIdentity)
|
|
294
|
+
return false;
|
|
295
|
+
const currentStartIdentity = await readProcessStartIdentity(snapshot.ownerPid);
|
|
296
|
+
// An unavailable process inspection is fail-closed for the lock: retain
|
|
297
|
+
// the lock rather than reclaiming a possibly live owner.
|
|
298
|
+
return currentStartIdentity !== null && currentStartIdentity !== snapshot.ownerStartIdentity;
|
|
299
|
+
}
|
|
300
|
+
const birthtimeMs = Number(snapshot.stat.birthtimeMs);
|
|
301
|
+
const ctimeMs = Number(snapshot.stat.ctimeMs);
|
|
302
|
+
const createdAtMs = Number.isFinite(birthtimeMs) && birthtimeMs > 0 ? birthtimeMs : ctimeMs;
|
|
303
|
+
const ageMs = nowMs - createdAtMs;
|
|
304
|
+
// A dead, identified owner cannot make progress regardless of the
|
|
305
|
+
// filesystem timestamp. An unidentified lock uses creation time rather than
|
|
306
|
+
// mtime so a future-dated timestamp cannot turn stale contention indefinite.
|
|
307
|
+
if (snapshot.ownerPid !== null)
|
|
308
|
+
return true;
|
|
309
|
+
return ageMs >= STATE_LOCK_STALE_MS;
|
|
310
|
+
}
|
|
311
|
+
function stateLockCoordinationPath(lockPath) {
|
|
312
|
+
return `${lockPath}${STATE_LOCK_COORDINATION_SUFFIX}`;
|
|
313
|
+
}
|
|
314
|
+
function parseCoordinationOwner(raw) {
|
|
315
|
+
try {
|
|
316
|
+
const parsed = JSON.parse(raw);
|
|
317
|
+
if (typeof parsed.ownerToken !== "string"
|
|
318
|
+
|| parsed.ownerToken.length === 0
|
|
319
|
+
|| typeof parsed.ownerPid !== "number"
|
|
320
|
+
|| !Number.isSafeInteger(parsed.ownerPid)
|
|
321
|
+
|| parsed.ownerPid <= 0) {
|
|
322
|
+
return null;
|
|
323
|
+
}
|
|
324
|
+
return {
|
|
325
|
+
ownerToken: parsed.ownerToken,
|
|
326
|
+
ownerPid: parsed.ownerPid,
|
|
327
|
+
ownerStartIdentity: typeof parsed.ownerStartIdentity === "string" ? parsed.ownerStartIdentity : null,
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
catch {
|
|
331
|
+
return null;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
async function readStateLockCoordination(coordinationPath) {
|
|
335
|
+
const stat = await fs.stat(coordinationPath).catch(() => null);
|
|
336
|
+
if (!stat || !stat.isDirectory())
|
|
337
|
+
return null;
|
|
338
|
+
const ownerPath = path.join(coordinationPath, "owner.json");
|
|
339
|
+
const raw = await fs.readFile(ownerPath, "utf8").catch(() => "");
|
|
340
|
+
const parsed = parseCoordinationOwner(raw);
|
|
341
|
+
return {
|
|
342
|
+
raw,
|
|
343
|
+
ownerToken: parsed?.ownerToken ?? null,
|
|
344
|
+
ownerPid: parsed?.ownerPid ?? null,
|
|
345
|
+
ownerStartIdentity: parsed?.ownerStartIdentity ?? null,
|
|
346
|
+
stat,
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
async function isStateLockCoordinationStale(snapshot, nowMs) {
|
|
350
|
+
if (isProcessAlive(snapshot.ownerPid)) {
|
|
351
|
+
if (!snapshot.ownerStartIdentity)
|
|
352
|
+
return false;
|
|
353
|
+
const currentStartIdentity = await readProcessStartIdentity(snapshot.ownerPid);
|
|
354
|
+
return currentStartIdentity !== null && currentStartIdentity !== snapshot.ownerStartIdentity;
|
|
355
|
+
}
|
|
356
|
+
const birthtimeMs = Number(snapshot.stat.birthtimeMs);
|
|
357
|
+
const ctimeMs = Number(snapshot.stat.ctimeMs);
|
|
358
|
+
const createdAtMs = Number.isFinite(birthtimeMs) && birthtimeMs > 0 ? birthtimeMs : ctimeMs;
|
|
359
|
+
if (snapshot.ownerPid !== null)
|
|
360
|
+
return true;
|
|
361
|
+
return nowMs - createdAtMs >= STATE_LOCK_STALE_MS;
|
|
362
|
+
}
|
|
363
|
+
async function releaseStateLockCoordination(coordination) {
|
|
364
|
+
const current = await readStateLockCoordination(coordination.coordinationPath);
|
|
365
|
+
if (!current
|
|
366
|
+
|| current.ownerToken !== coordination.ownerToken
|
|
367
|
+
|| current.stat.dev !== coordination.stat.dev
|
|
368
|
+
|| current.stat.ino !== coordination.stat.ino) {
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
await fs.unlink(path.join(coordination.coordinationPath, "owner.json")).catch(() => undefined);
|
|
372
|
+
await fs.rmdir(coordination.coordinationPath).catch(() => undefined);
|
|
373
|
+
}
|
|
374
|
+
async function acquireStateLockCoordination(lockPath) {
|
|
375
|
+
const coordinationPath = stateLockCoordinationPath(lockPath);
|
|
376
|
+
const deadline = Date.now() + STATE_LOCK_WAIT_MS;
|
|
377
|
+
while (Date.now() <= deadline) {
|
|
378
|
+
try {
|
|
379
|
+
await fs.mkdir(coordinationPath, { recursive: false, mode: 0o700 });
|
|
380
|
+
const ownerToken = randomUUID();
|
|
381
|
+
try {
|
|
382
|
+
const ownerStartIdentity = await readProcessStartIdentity(process.pid);
|
|
383
|
+
await fs.writeFile(path.join(coordinationPath, "owner.json"), `${JSON.stringify({ ownerToken, ownerPid: process.pid, ownerStartIdentity })}\n`, { encoding: "utf8", mode: 0o600 });
|
|
384
|
+
return {
|
|
385
|
+
coordinationPath,
|
|
386
|
+
ownerToken,
|
|
387
|
+
ownerStartIdentity,
|
|
388
|
+
stat: await fs.stat(coordinationPath),
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
catch (error) {
|
|
392
|
+
await fs.unlink(path.join(coordinationPath, "owner.json")).catch(() => undefined);
|
|
393
|
+
await fs.rmdir(coordinationPath).catch(() => undefined);
|
|
394
|
+
throw error;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
catch (error) {
|
|
398
|
+
if (error.code !== "EEXIST")
|
|
399
|
+
throw error;
|
|
400
|
+
const existing = await readStateLockCoordination(coordinationPath);
|
|
401
|
+
if (existing && await isStateLockCoordinationStale(existing, Date.now())) {
|
|
402
|
+
// Rename the exact stale directory into a unique tombstone before
|
|
403
|
+
// removing it. A second reclaimer cannot remove a replacement owner
|
|
404
|
+
// that has already recreated the original coordination path.
|
|
405
|
+
const reclaimPath = `${coordinationPath}.${process.pid}.${randomUUID()}.reclaim`;
|
|
406
|
+
try {
|
|
407
|
+
const current = await readStateLockCoordination(coordinationPath);
|
|
408
|
+
if (current && isSameStateLockCoordinationSnapshot(current, existing)) {
|
|
409
|
+
await fs.rename(coordinationPath, reclaimPath);
|
|
410
|
+
await fs.rm(reclaimPath, { recursive: true, force: true });
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
catch (reclaimError) {
|
|
414
|
+
const code = reclaimError.code;
|
|
415
|
+
if (code !== "ENOENT" && code !== "EEXIST" && code !== "ENOTEMPTY")
|
|
416
|
+
throw reclaimError;
|
|
417
|
+
}
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
420
|
+
await new Promise((resolve) => setTimeout(resolve, STATE_LOCK_RETRY_MS));
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
return null;
|
|
424
|
+
}
|
|
425
|
+
async function releaseStateLock(lock) {
|
|
426
|
+
await lock.handle.close().catch(() => undefined);
|
|
427
|
+
const current = await readStateLock(lock.lockPath);
|
|
428
|
+
if (!current
|
|
429
|
+
|| current.ownerToken !== lock.ownerToken
|
|
430
|
+
|| !isSameFileIdentity(current.stat, lock.stat)) {
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
await fs.unlink(lock.lockPath).catch(() => undefined);
|
|
434
|
+
}
|
|
435
|
+
async function createStateLock(lockPath) {
|
|
436
|
+
const handle = await fs.open(lockPath, "wx", 0o600);
|
|
437
|
+
const ownerToken = randomUUID();
|
|
438
|
+
const ownerStartIdentity = await readProcessStartIdentity(process.pid);
|
|
439
|
+
try {
|
|
440
|
+
await handle.writeFile(`${JSON.stringify({ ownerToken, ownerPid: process.pid, ownerStartIdentity })}\n`, "utf8");
|
|
441
|
+
return {
|
|
442
|
+
handle,
|
|
443
|
+
lockPath,
|
|
444
|
+
ownerToken,
|
|
445
|
+
ownerStartIdentity,
|
|
446
|
+
stat: await handle.stat(),
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
catch (error) {
|
|
450
|
+
await handle.close().catch(() => undefined);
|
|
451
|
+
await fs.unlink(lockPath).catch(() => undefined);
|
|
452
|
+
throw error;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
async function acquireStateLock(agentHome, fingerprint) {
|
|
456
|
+
const lockPath = stateLockPath(agentHome, fingerprint);
|
|
457
|
+
await fs.mkdir(path.dirname(lockPath), { recursive: true });
|
|
458
|
+
const deadline = Date.now() + STATE_LOCK_WAIT_MS;
|
|
459
|
+
while (Date.now() <= deadline) {
|
|
460
|
+
const coordination = await acquireStateLockCoordination(lockPath);
|
|
461
|
+
if (!coordination)
|
|
462
|
+
return null;
|
|
463
|
+
let lock = null;
|
|
464
|
+
try {
|
|
465
|
+
try {
|
|
466
|
+
lock = await createStateLock(lockPath);
|
|
467
|
+
}
|
|
468
|
+
catch (error) {
|
|
469
|
+
if (error.code !== "EEXIST")
|
|
470
|
+
throw error;
|
|
471
|
+
const existing = await readStateLock(lockPath);
|
|
472
|
+
if (existing && await isStateLockStale(existing, Date.now())) {
|
|
473
|
+
// Every claimant holds the coordination directory while creating or
|
|
474
|
+
// reclaiming the state lock, so stale deletion cannot race a normal
|
|
475
|
+
// create or another stale takeover.
|
|
476
|
+
await fs.unlink(lockPath).catch((unlinkError) => {
|
|
477
|
+
if (unlinkError.code !== "ENOENT")
|
|
478
|
+
throw unlinkError;
|
|
479
|
+
});
|
|
480
|
+
try {
|
|
481
|
+
lock = await createStateLock(lockPath);
|
|
482
|
+
}
|
|
483
|
+
catch (retryError) {
|
|
484
|
+
if (retryError.code !== "EEXIST")
|
|
485
|
+
throw retryError;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
finally {
|
|
491
|
+
await releaseStateLockCoordination(coordination);
|
|
492
|
+
}
|
|
493
|
+
if (lock)
|
|
494
|
+
return lock;
|
|
495
|
+
await new Promise((resolve) => setTimeout(resolve, STATE_LOCK_RETRY_MS));
|
|
496
|
+
}
|
|
497
|
+
return null;
|
|
498
|
+
}
|
|
499
|
+
async function withStateLock(agentHome, fingerprint, fn) {
|
|
500
|
+
const lockPath = stateLockPath(agentHome, fingerprint);
|
|
501
|
+
const lock = await acquireStateLock(agentHome, fingerprint);
|
|
502
|
+
if (!lock)
|
|
503
|
+
return null;
|
|
504
|
+
const refreshInterval = setInterval(() => {
|
|
505
|
+
void lock.handle.utimes(new Date(), new Date()).catch(() => undefined);
|
|
506
|
+
}, Math.max(1_000, Math.floor(STATE_LOCK_STALE_MS / 3)));
|
|
507
|
+
refreshInterval.unref?.();
|
|
508
|
+
try {
|
|
509
|
+
return await fn();
|
|
510
|
+
}
|
|
511
|
+
finally {
|
|
512
|
+
clearInterval(refreshInterval);
|
|
513
|
+
await releaseStateLock(lock);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
function isStateRenameCollision(error) {
|
|
517
|
+
if (!(error instanceof Error))
|
|
518
|
+
return false;
|
|
519
|
+
const code = error.code;
|
|
520
|
+
return code === "EEXIST" || code === "ENOTEMPTY" || code === "EPERM";
|
|
521
|
+
}
|
|
522
|
+
async function writeState(agentHome, fingerprint, state) {
|
|
523
|
+
const target = statePath(agentHome, fingerprint);
|
|
524
|
+
const temporary = `${target}.${process.pid}.${randomUUID()}.tmp`;
|
|
525
|
+
await fs.writeFile(temporary, `${JSON.stringify(state)}\n`, { encoding: "utf8", mode: 0o600 });
|
|
526
|
+
try {
|
|
527
|
+
try {
|
|
528
|
+
await fs.rename(temporary, target);
|
|
529
|
+
}
|
|
530
|
+
catch (error) {
|
|
531
|
+
if (!isStateRenameCollision(error))
|
|
532
|
+
throw error;
|
|
533
|
+
await fs.unlink(target).catch(() => undefined);
|
|
534
|
+
await fs.rename(temporary, target);
|
|
535
|
+
}
|
|
81
536
|
}
|
|
82
537
|
finally {
|
|
83
538
|
await fs.unlink(temporary).catch(() => undefined);
|
|
84
539
|
}
|
|
85
540
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
541
|
+
function nextGeneration(state) {
|
|
542
|
+
return (state?.generation ?? 0) + 1;
|
|
543
|
+
}
|
|
544
|
+
function matchesProbe(state, lease) {
|
|
545
|
+
return state?.state === "probing"
|
|
546
|
+
&& state.probeId === lease.probeId
|
|
547
|
+
&& state.generation === lease.generation;
|
|
548
|
+
}
|
|
549
|
+
export async function claimCodexAuthProbe(agentHome, fingerprint) {
|
|
550
|
+
const claim = await withStateLock(agentHome, fingerprint, async () => {
|
|
551
|
+
const current = await readState(agentHome, fingerprint);
|
|
552
|
+
const now = Date.now();
|
|
553
|
+
if (current && isActiveState(agentHome, fingerprint, current, now)) {
|
|
554
|
+
return { claimed: false, readinessState: "unchanged" };
|
|
555
|
+
}
|
|
556
|
+
const lease = {
|
|
557
|
+
probeId: randomUUID(),
|
|
558
|
+
generation: nextGeneration(current),
|
|
559
|
+
};
|
|
560
|
+
await writeState(agentHome, fingerprint, {
|
|
561
|
+
version: STATE_VERSION,
|
|
562
|
+
fingerprint,
|
|
563
|
+
classification: "authentication",
|
|
564
|
+
errorCode: "codex_provider_auth_required",
|
|
565
|
+
state: "probing",
|
|
566
|
+
generation: lease.generation,
|
|
567
|
+
probeId: lease.probeId,
|
|
568
|
+
probeStartedAt: new Date(now).toISOString(),
|
|
569
|
+
probeOwnerPid: process.pid,
|
|
570
|
+
});
|
|
571
|
+
rememberActiveProbe(agentHome, fingerprint, lease.probeId);
|
|
572
|
+
return { claimed: true, lease };
|
|
573
|
+
});
|
|
574
|
+
return claim ?? { claimed: false, readinessState: "busy" };
|
|
575
|
+
}
|
|
576
|
+
export async function hasMatchingCodexAuthFailure(agentHome, fingerprint) {
|
|
577
|
+
const state = await readState(agentHome, fingerprint);
|
|
578
|
+
return state?.fingerprint === fingerprint
|
|
579
|
+
&& isActiveState(agentHome, fingerprint, state, Date.now());
|
|
580
|
+
}
|
|
581
|
+
export async function recordCodexAuthFailure(agentHome, fingerprint, lease) {
|
|
582
|
+
const recorded = await withStateLock(agentHome, fingerprint, async () => {
|
|
583
|
+
const current = await readState(agentHome, fingerprint);
|
|
584
|
+
const now = Date.now();
|
|
585
|
+
if (lease) {
|
|
586
|
+
if (!matchesProbe(current, lease)) {
|
|
587
|
+
forgetActiveProbe(agentHome, fingerprint, lease.probeId);
|
|
588
|
+
return false;
|
|
589
|
+
}
|
|
590
|
+
await writeState(agentHome, fingerprint, {
|
|
591
|
+
version: STATE_VERSION,
|
|
592
|
+
fingerprint,
|
|
593
|
+
classification: "authentication",
|
|
594
|
+
errorCode: "codex_provider_auth_required",
|
|
595
|
+
state: "failed",
|
|
596
|
+
generation: lease.generation,
|
|
597
|
+
failedAt: new Date(now).toISOString(),
|
|
598
|
+
});
|
|
599
|
+
forgetActiveProbe(agentHome, fingerprint, lease.probeId);
|
|
600
|
+
return true;
|
|
601
|
+
}
|
|
602
|
+
// Keep the compatibility API fail-closed: an unowned caller cannot steal
|
|
603
|
+
// an active probe belonging to another run.
|
|
604
|
+
if (current && isActiveState(agentHome, fingerprint, current, now))
|
|
605
|
+
return false;
|
|
606
|
+
await writeState(agentHome, fingerprint, {
|
|
607
|
+
version: STATE_VERSION,
|
|
608
|
+
fingerprint,
|
|
609
|
+
classification: "authentication",
|
|
610
|
+
errorCode: "codex_provider_auth_required",
|
|
611
|
+
state: "failed",
|
|
612
|
+
generation: nextGeneration(current),
|
|
613
|
+
failedAt: new Date(now).toISOString(),
|
|
614
|
+
});
|
|
615
|
+
return true;
|
|
616
|
+
});
|
|
617
|
+
return recorded ?? false;
|
|
618
|
+
}
|
|
619
|
+
export async function renewCodexAuthProbe(agentHome, fingerprint, lease) {
|
|
620
|
+
const renewed = await withStateLock(agentHome, fingerprint, async () => {
|
|
621
|
+
const current = await readState(agentHome, fingerprint);
|
|
622
|
+
if (!matchesProbe(current, lease)) {
|
|
623
|
+
forgetActiveProbe(agentHome, fingerprint, lease.probeId);
|
|
624
|
+
return false;
|
|
625
|
+
}
|
|
626
|
+
await writeState(agentHome, fingerprint, {
|
|
627
|
+
version: STATE_VERSION,
|
|
628
|
+
fingerprint,
|
|
629
|
+
classification: "authentication",
|
|
630
|
+
errorCode: "codex_provider_auth_required",
|
|
631
|
+
state: "probing",
|
|
632
|
+
generation: lease.generation,
|
|
633
|
+
probeId: lease.probeId,
|
|
634
|
+
probeStartedAt: new Date().toISOString(),
|
|
635
|
+
probeOwnerPid: process.pid,
|
|
636
|
+
});
|
|
637
|
+
rememberActiveProbe(agentHome, fingerprint, lease.probeId);
|
|
638
|
+
return true;
|
|
639
|
+
});
|
|
640
|
+
return renewed ?? false;
|
|
641
|
+
}
|
|
642
|
+
export async function clearMatchingCodexAuthFailure(agentHome, fingerprint, lease) {
|
|
643
|
+
const cleared = await withStateLock(agentHome, fingerprint, async () => {
|
|
644
|
+
const current = await readState(agentHome, fingerprint);
|
|
645
|
+
if (lease) {
|
|
646
|
+
if (!matchesProbe(current, lease)) {
|
|
647
|
+
forgetActiveProbe(agentHome, fingerprint, lease.probeId);
|
|
648
|
+
return false;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
else if (current?.state !== "failed" || current.fingerprint !== fingerprint) {
|
|
652
|
+
return false;
|
|
653
|
+
}
|
|
654
|
+
const target = statePath(agentHome, fingerprint);
|
|
655
|
+
await fs.unlink(target).catch(() => undefined);
|
|
656
|
+
await fs.rmdir(path.dirname(target)).catch(() => undefined);
|
|
657
|
+
forgetActiveProbe(agentHome, fingerprint, lease?.probeId);
|
|
658
|
+
return true;
|
|
659
|
+
});
|
|
660
|
+
if (cleared !== null) {
|
|
661
|
+
await fs.rmdir(path.dirname(statePath(agentHome, fingerprint)))
|
|
662
|
+
.catch(() => undefined);
|
|
663
|
+
}
|
|
664
|
+
return cleared ?? false;
|
|
90
665
|
}
|
|
91
666
|
//# sourceMappingURL=readiness-gate.js.map
|