ever-works-node 0.2.3 → 0.2.4

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.
Files changed (2) hide show
  1. package/cli.js +869 -661
  2. package/package.json +1 -1
package/cli.js CHANGED
@@ -3767,10 +3767,14 @@ var USER_SELECTABLE_WORK_KINDS = [
3767
3767
  "repo",
3768
3768
  "app"
3769
3769
  ];
3770
- var WORK_KINDS = [...USER_SELECTABLE_WORK_KINDS, "company", "campaign", "default"];
3770
+ var WORK_KINDS = [...USER_SELECTABLE_WORK_KINDS, "company", "campaign", "project", "default"];
3771
3771
  var WORK_KIND_SET = new Set(WORK_KINDS);
3772
3772
  var GOAL_KINDS = ["metric", "delivery"];
3773
3773
  var GOAL_KIND_SET = new Set(GOAL_KINDS);
3774
+ var RECORD_VISIBILITIES = ["private", "team", "organization"];
3775
+ var RECORD_VISIBILITY_SET = new Set(RECORD_VISIBILITIES);
3776
+ var RECORD_LIST_VIEWS = ["mine", "shared", "organization"];
3777
+ var RECORD_LIST_VIEW_SET = new Set(RECORD_LIST_VIEWS);
3774
3778
  var APP_DEPENDENCY_BACKUP_OVERDUE_MS = 26 * 36e5;
3775
3779
  var APP_DEPENDENCY_MANAGED = {
3776
3780
  pgRoleConnectionLimit: 20,
@@ -7371,269 +7375,606 @@ function readPendingComputerSessions(payload) {
7371
7375
  }
7372
7376
  __name(readPendingComputerSessions, "readPendingComputerSessions");
7373
7377
 
7374
- // src/core/api-base.ts
7375
- var API_URL_ENV = "EVER_WORKS_NODE_API_URL";
7376
- function rawPin(env) {
7377
- const value = env[API_URL_ENV];
7378
- if (typeof value !== "string") {
7378
+ // src/core/heartbeat.ts
7379
+ var systemScheduler = {
7380
+ setTimeout: /* @__PURE__ */ __name((callback, ms) => setTimeout(callback, ms), "setTimeout"),
7381
+ clearTimeout: /* @__PURE__ */ __name((handle) => clearTimeout(handle), "clearTimeout")
7382
+ };
7383
+ var OPTIONAL_DESCRIPTION_FIELD_TIERS = [
7384
+ // NEWEST first. Node lifecycle + remote node limits (self-build slices
7385
+ // AR, AS). Tiered (review): `cliVersions` goes out on EVERY beat of a
7386
+ // node with a pinned CLI, so an upgraded node talking to a platform that
7387
+ // predates it would otherwise drop the worker state and housekeeping
7388
+ // along with it — losing health signals that platform DOES understand,
7389
+ // for the life of the process. Stripping the newest tier first keeps them.
7390
+ ["cliVersions", "maxConcurrentJobs", "maxCpuPercent", "maxMemoryMb"],
7391
+ // Fleet health signals (EW-776) and node housekeeping (EW-803).
7392
+ [
7393
+ "workerState",
7394
+ "workerStateReason",
7395
+ "minFreeDiskBytes",
7396
+ "workspaceCount",
7397
+ "workspaceBytes",
7398
+ "lastReclaimAt",
7399
+ "lastReclaimFreedBytes"
7400
+ ]
7401
+ ];
7402
+ function untilHeartbeatAccepted(loop, shouldWait = () => true) {
7403
+ const verdict = /* @__PURE__ */ __name((state) => {
7404
+ if (state.state === "connected") return true;
7405
+ if (state.state === "stopped" || !shouldWait()) return false;
7379
7406
  return null;
7380
- }
7381
- const trimmed = value.trim();
7382
- return trimmed ? trimmed : null;
7407
+ }, "verdict");
7408
+ const now = verdict(loop.getState());
7409
+ if (now !== null) return Promise.resolve(now);
7410
+ return new Promise((resolve9) => {
7411
+ const unsubscribe = loop.onChange((state) => {
7412
+ const settled = verdict(state);
7413
+ if (settled === null) return;
7414
+ unsubscribe();
7415
+ resolve9(settled);
7416
+ });
7417
+ });
7383
7418
  }
7384
- __name(rawPin, "rawPin");
7385
- function readApiUrlPin(env = process.env) {
7386
- const raw = rawPin(env);
7387
- return raw === null ? null : normalizeApiUrl(raw);
7419
+ __name(untilHeartbeatAccepted, "untilHeartbeatAccepted");
7420
+ function computeBackoffDelay(intervalMs, failures, maxMs) {
7421
+ if (failures <= 0) {
7422
+ return intervalMs;
7423
+ }
7424
+ const exponent = Math.min(failures, 30);
7425
+ const delay2 = intervalMs * 2 ** exponent;
7426
+ return Math.min(delay2, maxMs);
7388
7427
  }
7389
- __name(readApiUrlPin, "readApiUrlPin");
7390
- function resolveApiBase(config, env = process.env) {
7391
- const configuredUrl = config.apiUrl;
7392
- const pinnedTo = readApiUrlPin(env);
7393
- if (pinnedTo === null) {
7394
- return { url: configuredUrl, source: "config", configuredUrl, pinnedTo: null, mismatch: false };
7428
+ __name(computeBackoffDelay, "computeBackoffDelay");
7429
+ var HeartbeatLoop = class {
7430
+ static {
7431
+ __name(this, "HeartbeatLoop");
7395
7432
  }
7396
- return {
7397
- url: pinnedTo,
7398
- source: "pin",
7399
- configuredUrl,
7400
- pinnedTo,
7401
- mismatch: pinnedTo !== configuredUrl
7433
+ options;
7434
+ scheduler;
7435
+ now;
7436
+ listeners = /* @__PURE__ */ new Set();
7437
+ timer = null;
7438
+ running = false;
7439
+ inFlight = null;
7440
+ /**
7441
+ * How many optional-field TIERS (newest first) this platform has proven
7442
+ * it predates; from then on the beat leaves those tiers out. 0 = it
7443
+ * takes everything. Process-scoped on purpose — a platform upgrade is a
7444
+ * restart-shaped event for the node, and re-probing on every beat would
7445
+ * mean one wasted round trip per beat, forever, against an API that
7446
+ * will never accept them.
7447
+ */
7448
+ strippedTiers = 0;
7449
+ state = {
7450
+ state: "idle",
7451
+ lastHeartbeatAt: null,
7452
+ consecutiveFailures: 0,
7453
+ nextAttemptInMs: null,
7454
+ lastError: null,
7455
+ lastErrorKind: null,
7456
+ node: null
7402
7457
  };
7403
- }
7404
- __name(resolveApiBase, "resolveApiBase");
7405
- function describeApiBase(base) {
7406
- if (base.source === "config") {
7407
- return `${base.url} (from the enrolled config)`;
7458
+ constructor(options) {
7459
+ this.options = {
7460
+ ...options,
7461
+ intervalMs: options.intervalMs ?? DEFAULT_HEARTBEAT_INTERVAL_MS,
7462
+ maxBackoffMs: options.maxBackoffMs ?? MAX_HEARTBEAT_BACKOFF_MS
7463
+ };
7464
+ this.scheduler = options.scheduler ?? systemScheduler;
7465
+ this.now = options.now ?? (() => Date.now());
7466
+ this.options.logger?.protect(options.secret);
7408
7467
  }
7409
- if (!base.mismatch) {
7410
- return `${base.url} (pinned via ${API_URL_ENV}; matches the enrolled origin)`;
7468
+ getState() {
7469
+ return { ...this.state };
7411
7470
  }
7412
- return `${base.url} (PINNED via ${API_URL_ENV}) \u2014 enrolled against ${base.configuredUrl}. The pin does not match the enrolled origin: this node has no credential on the pinned platform, so every call will be refused with 401 until the pin is corrected or the node is re-enrolled.`;
7413
- }
7414
- __name(describeApiBase, "describeApiBase");
7415
-
7416
- // src/core/auth-client.ts
7417
- var EVER_ID_DEVICE_SCOPE = ["openid", "email", EVER_ID_SCOPES.SESSION_EXCHANGE].join(" ");
7418
-
7419
- // src/core/telemetry-probe.ts
7420
- var AGENT_CLI_CANDIDATES = ["claude", "codex", "gemini", "opencode"];
7421
- function parseCliVersion(stdout) {
7422
- if (typeof stdout !== "string") return null;
7423
- const match = /\b(\d+\.\d+(?:\.\d+)?(?:[-+][0-9A-Za-z.-]+)?)\b/.exec(stdout);
7424
- return match ? match[1] : null;
7425
- }
7426
- __name(parseCliVersion, "parseCliVersion");
7427
- async function detectAgentCliVersion(runner, candidates = AGENT_CLI_CANDIDATES) {
7428
- for (const command of candidates) {
7429
- try {
7430
- const result = await runner.run(command, ["--version"]);
7431
- if (result.code !== 0) continue;
7432
- const version = parseCliVersion(`${result.stdout ?? ""}
7433
- ${result.stderr ?? ""}`);
7434
- if (!version) continue;
7435
- return `${command} ${version}`.slice(0, FLEET_MAX_CLI_VERSION_LENGTH);
7436
- } catch {
7437
- continue;
7471
+ onChange(listener) {
7472
+ this.listeners.add(listener);
7473
+ return () => {
7474
+ this.listeners.delete(listener);
7475
+ };
7476
+ }
7477
+ /** Start beating. Resolves once the FIRST beat has settled. */
7478
+ start() {
7479
+ if (this.running) {
7480
+ return this.inFlight ?? Promise.resolve();
7438
7481
  }
7482
+ this.running = true;
7483
+ return this.tick();
7439
7484
  }
7440
- return null;
7441
- }
7442
- __name(detectAgentCliVersion, "detectAgentCliVersion");
7443
- async function detectDiskFreeBytes(io, path2) {
7444
- try {
7445
- const free = await io.freeBytes(path2);
7446
- if (typeof free !== "number" || !Number.isFinite(free) || free < 0) return null;
7447
- return Math.floor(free);
7448
- } catch {
7449
- return null;
7485
+ /** Stop beating and cancel any pending timer. */
7486
+ stop() {
7487
+ this.running = false;
7488
+ this.cancelTimer();
7489
+ this.patch({ state: "stopped", nextAttemptInMs: null });
7450
7490
  }
7451
- }
7452
- __name(detectDiskFreeBytes, "detectDiskFreeBytes");
7453
- var MODEL_IDENTITY_CACHE_TTL_MS = 5 * 6e4;
7454
- function parseClaudeAuthStatus(stdout) {
7455
- let parsed;
7456
- try {
7457
- parsed = JSON.parse(stdout.trim());
7458
- } catch {
7459
- return null;
7491
+ /** Await the in-flight beat, if any (test/shutdown helper). */
7492
+ async settled() {
7493
+ await this.inFlight;
7460
7494
  }
7461
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
7462
- const status = parsed;
7463
- if (status.loggedIn !== true) return "claude-code: not logged in";
7464
- const email = compactString(status.email);
7465
- const org = compactString(status.orgName);
7466
- const plan = compactString(status.subscriptionType);
7467
- const method = compactString(status.authMethod);
7468
- const who = email ?? (method ? `${method} login` : "logged in");
7469
- const details = [org, plan].filter((part) => part !== null);
7470
- return `claude-code: ${who}${details.length > 0 ? ` (${details.join(", ")})` : ""}`;
7471
- }
7472
- __name(parseClaudeAuthStatus, "parseClaudeAuthStatus");
7473
- function parseCodexLoginStatus(stdout) {
7474
- const text = stdout.trim().toLowerCase();
7475
- if (!text) return null;
7476
- if (text.includes("not logged in")) return "codex: not logged in";
7477
- if (text.includes("chatgpt")) return "codex: chatgpt";
7478
- if (text.includes("api key")) return "codex: api-key";
7479
- if (text.includes("logged in")) return "codex: logged in";
7480
- return null;
7481
- }
7482
- __name(parseCodexLoginStatus, "parseCodexLoginStatus");
7483
- function compactString(value) {
7484
- if (typeof value !== "string") return null;
7485
- const cleaned = value.replace(/[\u0000-\u001F\u007F]+/g, " ").trim();
7486
- return cleaned ? cleaned : null;
7487
- }
7488
- __name(compactString, "compactString");
7489
- async function detectModelIdentity(runner, paths = {}) {
7490
- const candidates = [
7491
- { command: paths["claude-code"] || "claude", args: ["auth", "status", "--json"], parse: parseClaudeAuthStatus },
7492
- { command: paths.codex || "codex", args: ["login", "status"], parse: parseCodexLoginStatus }
7493
- ];
7494
- for (const candidate of candidates) {
7495
+ /** Run one beat now. Exposed so the UI can offer "retry" and tests can step. */
7496
+ tick() {
7497
+ const run = this.runOnce().finally(() => {
7498
+ if (this.inFlight === run) {
7499
+ this.inFlight = null;
7500
+ }
7501
+ });
7502
+ this.inFlight = run;
7503
+ return run;
7504
+ }
7505
+ async runOnce() {
7506
+ if (!this.running) {
7507
+ return;
7508
+ }
7509
+ this.cancelTimer();
7510
+ if (this.state.state !== "connected") {
7511
+ this.patch({ state: "connecting" });
7512
+ }
7495
7513
  try {
7496
- const result = await runner.run(candidate.command, candidate.args);
7497
- const stdout = result.stdout ?? "";
7498
- const stderr = result.stderr ?? "";
7499
- const label = candidate.parse(stdout) ?? candidate.parse(`${stdout}
7500
- ${stderr}`);
7501
- if (label) return label.slice(0, FLEET_MAX_MODEL_IDENTITY_LENGTH);
7502
- } catch {
7503
- continue;
7514
+ const description = await this.options.describe();
7515
+ this.onSuccess(await this.beat(description));
7516
+ } catch (error) {
7517
+ this.onFailure(error);
7504
7518
  }
7519
+ this.scheduleNext();
7505
7520
  }
7506
- return null;
7507
- }
7508
- __name(detectModelIdentity, "detectModelIdentity");
7509
- function cacheProbe(probe, ttlMs = MODEL_IDENTITY_CACHE_TTL_MS, now = Date.now) {
7510
- let value = null;
7511
- let expiresAt = Number.NEGATIVE_INFINITY;
7512
- let inFlight = null;
7513
- return async () => {
7514
- if (now() < expiresAt) return value;
7515
- if (inFlight) return inFlight;
7516
- inFlight = probe().then((fresh) => {
7517
- value = fresh;
7518
- expiresAt = now() + ttlMs;
7519
- return fresh;
7520
- }).finally(() => {
7521
- inFlight = null;
7522
- });
7523
- return inFlight;
7524
- };
7525
- }
7526
- __name(cacheProbe, "cacheProbe");
7527
-
7528
- // src/core/executors/model-cli.ts
7529
- var MODEL_CLI_EMPTY_MCP_CONFIG = `${JSON.stringify({ mcpServers: {} }, null, 2)}
7530
- `;
7531
- function modelCliMcpIsolationEnv(provider) {
7532
- return provider === "claude-code" ? { ENABLE_CLAUDEAI_MCP_SERVERS: "false" } : null;
7533
- }
7534
- __name(modelCliMcpIsolationEnv, "modelCliMcpIsolationEnv");
7535
- function claudeManagedMcpConfigPath(platform = process.platform) {
7536
- if (platform === "win32") return "C:\\Program Files\\ClaudeCode\\managed-mcp.json";
7537
- if (platform === "darwin") return "/Library/Application Support/ClaudeCode/managed-mcp.json";
7538
- return "/etc/claude-code/managed-mcp.json";
7539
- }
7540
- __name(claudeManagedMcpConfigPath, "claudeManagedMcpConfigPath");
7541
- function claudeManagedMcpConfigInEffect(raw) {
7542
- if (typeof raw !== "string" || !raw.trim()) return false;
7543
- try {
7544
- const parsed = JSON.parse(raw);
7545
- return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed);
7546
- } catch {
7547
- return false;
7521
+ /**
7522
+ * Send one beat, tolerating a platform that predates the optional
7523
+ * self-description fields (the worker state, EW-776; the housekeeping
7524
+ * report, EW-803).
7525
+ *
7526
+ * The API validates heartbeats with `whitelist + forbidNonWhitelisted`,
7527
+ * so a field an older build does not know is not ignored — it 400s the
7528
+ * whole request. A 400 is a FAILED beat, a failed beat backs off, and
7529
+ * enough of them sweep the node to `offline`. Which would mean shipping
7530
+ * a health-reporting feature whose failure mode is "every node in a
7531
+ * mixed-version fleet goes dark": the exact class of outage this slice
7532
+ * was written to end.
7533
+ *
7534
+ * So a 400 on a beat that CARRIED those fields is retried once,
7535
+ * immediately, without them. If that succeeds the platform is simply
7536
+ * older than this daemon; we latch, say so once, and keep reporting
7537
+ * liveness. The retry costs one request, once per process.
7538
+ *
7539
+ * Inferring "the field was rejected" from a bare 400 is deliberate and
7540
+ * has a cost worth stating: `FleetClient` never surfaces server bodies
7541
+ * (fixed client-authored messages only), so a 400 caused by something
7542
+ * else on such a beat also latches, and this node reports liveness only
7543
+ * until it restarts. That is the safe direction — the node stays
7544
+ * observable either way, and only the extra signal is lost.
7545
+ */
7546
+ async beat(description) {
7547
+ const credential = { nodeId: this.options.nodeId, secret: this.options.secret };
7548
+ let sent = stripTiers(description, this.strippedTiers);
7549
+ try {
7550
+ return await this.options.client.heartbeat({ ...credential, ...sent });
7551
+ } catch (error) {
7552
+ if (!isFieldRejection(error)) throw error;
7553
+ let lastError = error;
7554
+ for (let tiers = this.strippedTiers + 1; tiers <= OPTIONAL_DESCRIPTION_FIELD_TIERS.length; tiers += 1) {
7555
+ const candidate = stripTiers(description, tiers);
7556
+ if (Object.keys(candidate).length === Object.keys(sent).length) continue;
7557
+ sent = candidate;
7558
+ try {
7559
+ const result = await this.options.client.heartbeat({ ...credential, ...candidate });
7560
+ this.strippedTiers = tiers;
7561
+ this.options.logger?.warn(
7562
+ tiers >= OPTIONAL_DESCRIPTION_FIELD_TIERS.length ? "Platform rejected the optional self-description fields (worker state, housekeeping); it predates them. Reporting liveness only until this node restarts." : "Platform rejected the newest optional self-description fields (pinned CLI versions, enforced limits); it predates them. Still reporting worker state and housekeeping; the newer fields stay off until this node restarts."
7563
+ );
7564
+ return result;
7565
+ } catch (retryError) {
7566
+ if (!isFieldRejection(retryError)) throw retryError;
7567
+ lastError = retryError;
7568
+ }
7569
+ }
7570
+ throw lastError;
7571
+ }
7548
7572
  }
7549
- }
7550
- __name(claudeManagedMcpConfigInEffect, "claudeManagedMcpConfigInEffect");
7551
- var ModelCliCommandError = class extends Error {
7552
- static {
7553
- __name(this, "ModelCliCommandError");
7573
+ onSuccess(result) {
7574
+ this.patch({
7575
+ state: "connected",
7576
+ lastHeartbeatAt: this.now(),
7577
+ consecutiveFailures: 0,
7578
+ lastError: null,
7579
+ lastErrorKind: null,
7580
+ node: result.node
7581
+ });
7582
+ if (this.options.onAccepted) {
7583
+ try {
7584
+ this.options.onAccepted(result);
7585
+ } catch {
7586
+ }
7587
+ }
7554
7588
  }
7555
- constructor(message) {
7556
- super(message);
7557
- this.name = "ModelCliCommandError";
7589
+ onFailure(error) {
7590
+ const kind = error instanceof FleetClientError ? error.kind : "network";
7591
+ const raw = error instanceof Error ? error.message : String(error);
7592
+ const message = this.options.logger?.redact(raw) ?? raw;
7593
+ const failures = this.state.consecutiveFailures + 1;
7594
+ this.patch({
7595
+ // `unauthorized` is sticky and visible: the operator has to act
7596
+ // (re-enable the node in Fleet, or re-enroll). We keep retrying at
7597
+ // the backoff ceiling so a re-enabled node recovers on its own.
7598
+ state: kind === "unauthorized" ? "unauthorized" : "retrying",
7599
+ consecutiveFailures: failures,
7600
+ lastError: message,
7601
+ lastErrorKind: kind
7602
+ });
7603
+ this.options.logger?.warn(`Heartbeat failed (attempt ${failures}): ${message}`);
7558
7604
  }
7559
- };
7560
- var MODEL_CLI_STEP_ID = "model";
7561
- var MODEL_CLI_OUTPUT_TAIL_BYTES = 8 * 1024;
7562
- var MODEL_CLI_MAX_OUTPUT_BYTES = 8 * 1024 * 1024;
7563
- var ABSOLUTE_WIN32 = /^[A-Za-z]:[\\/]/;
7564
- var UNSAFE_PATH_CHARS = /[\0\r\n"'`$&|;<>%!^]/;
7565
- function quoteShellPath(path2, platform = process.platform) {
7566
- if (typeof path2 !== "string" || !path2.trim()) {
7567
- throw new ModelCliCommandError("Path must not be empty");
7605
+ scheduleNext() {
7606
+ if (!this.running) {
7607
+ return;
7608
+ }
7609
+ const delay2 = computeBackoffDelay(
7610
+ this.options.intervalMs,
7611
+ this.state.consecutiveFailures,
7612
+ this.options.maxBackoffMs
7613
+ );
7614
+ this.patch({ nextAttemptInMs: delay2 });
7615
+ this.timer = this.scheduler.setTimeout(() => {
7616
+ this.timer = null;
7617
+ void this.tick();
7618
+ }, delay2);
7568
7619
  }
7569
- if (UNSAFE_PATH_CHARS.test(path2)) {
7570
- throw new ModelCliCommandError("Path contains characters the shell could interpret");
7620
+ cancelTimer() {
7621
+ if (this.timer !== null) {
7622
+ this.scheduler.clearTimeout(this.timer);
7623
+ this.timer = null;
7624
+ }
7571
7625
  }
7572
- const absolute = platform === "win32" ? ABSOLUTE_WIN32.test(path2) || path2.startsWith("\\\\") : path2.startsWith("/");
7573
- if (!absolute) {
7574
- throw new ModelCliCommandError("Path must be absolute");
7626
+ patch(next) {
7627
+ this.state = { ...this.state, ...next };
7628
+ const snapshot = this.getState();
7629
+ for (const listener of this.listeners) {
7630
+ listener(snapshot);
7631
+ }
7575
7632
  }
7576
- return `"${path2}"`;
7577
- }
7578
- __name(quoteShellPath, "quoteShellPath");
7579
- function assertMcpServerName(name) {
7580
- if (typeof name !== "string" || !MCP_SERVER_NAME_PATTERN.test(name)) {
7581
- throw new ModelCliCommandError("MCP server name is not an opaque identifier");
7633
+ };
7634
+ function stripTiers(description, tiers) {
7635
+ const out = {};
7636
+ const dropped = new Set(OPTIONAL_DESCRIPTION_FIELD_TIERS.slice(0, tiers).flat());
7637
+ for (const [key, value] of Object.entries(description)) {
7638
+ if (value === void 0 || dropped.has(key)) continue;
7639
+ out[key] = value;
7582
7640
  }
7583
- return name;
7641
+ return out;
7584
7642
  }
7585
- __name(assertMcpServerName, "assertMcpServerName");
7586
- function assertLoopbackUrl(url) {
7587
- if (typeof url !== "string" || !MCP_LOOPBACK_URL_PATTERN.test(url)) {
7588
- throw new ModelCliCommandError("MCP bridge URL is not a loopback URL this node produced");
7589
- }
7590
- return url;
7643
+ __name(stripTiers, "stripTiers");
7644
+ function isFieldRejection(error) {
7645
+ return error instanceof FleetClientError && error.kind === "invalid-request" && error.status === 400;
7591
7646
  }
7592
- __name(assertLoopbackUrl, "assertLoopbackUrl");
7593
- var MCP_SERVER_NAME_PATTERN = /^[A-Za-z0-9_-]{1,64}$/;
7594
- var MCP_LOOPBACK_URL_PATTERN = /^http:\/\/127\.0\.0\.1:\d{1,5}\/mcp\/[a-f0-9]{8,64}$/;
7595
- function assertModelId(model) {
7596
- if (model === void 0) return null;
7597
- if (!FLEET_AGENT_EXECUTION_MODEL_PATTERN.test(model)) {
7598
- throw new ModelCliCommandError("Model id is not an opaque identifier");
7647
+ __name(isFieldRejection, "isFieldRejection");
7648
+
7649
+ // src/core/api-base.ts
7650
+ var API_URL_ENV = "EVER_WORKS_NODE_API_URL";
7651
+ function rawPin(env) {
7652
+ const value = env[API_URL_ENV];
7653
+ if (typeof value !== "string") {
7654
+ return null;
7599
7655
  }
7600
- return model;
7656
+ const trimmed = value.trim();
7657
+ return trimmed ? trimmed : null;
7601
7658
  }
7602
- __name(assertModelId, "assertModelId");
7603
- var MODEL_CLI_OPTIONAL_FLAGS = {
7604
- "claude-code": ["--effort", "--max-budget-usd"],
7605
- codex: []
7606
- };
7607
- function unsupportedOptionalModelCliFlags(execution, supportedFlags) {
7608
- if (!supportedFlags) return [];
7609
- const wanted = [];
7610
- if (execution.provider === "claude-code") {
7611
- if (execution.effort) wanted.push("--effort");
7612
- if (execution.maxBudgetUsd !== void 0) wanted.push("--max-budget-usd");
7659
+ __name(rawPin, "rawPin");
7660
+ function readApiUrlPin(env = process.env) {
7661
+ const raw = rawPin(env);
7662
+ return raw === null ? null : normalizeApiUrl(raw);
7663
+ }
7664
+ __name(readApiUrlPin, "readApiUrlPin");
7665
+ function resolveApiBase(config, env = process.env) {
7666
+ const configuredUrl = config.apiUrl;
7667
+ const pinnedTo = readApiUrlPin(env);
7668
+ if (pinnedTo === null) {
7669
+ return { url: configuredUrl, source: "config", configuredUrl, pinnedTo: null, mismatch: false };
7613
7670
  }
7614
- const droppable = new Set(
7615
- MODEL_CLI_OPTIONAL_FLAGS[execution.provider] ?? []
7616
- );
7617
- return wanted.filter((flag) => droppable.has(flag) && !supportedFlags.has(flag));
7671
+ return {
7672
+ url: pinnedTo,
7673
+ source: "pin",
7674
+ configuredUrl,
7675
+ pinnedTo,
7676
+ mismatch: pinnedTo !== configuredUrl
7677
+ };
7618
7678
  }
7619
- __name(unsupportedOptionalModelCliFlags, "unsupportedOptionalModelCliFlags");
7620
- function assertResumableSession(provider, sessionId) {
7621
- if (sessionId === void 0) return null;
7622
- if (!fleetAgentExecutionProviderSupportsSessionResume(provider)) {
7623
- throw new ModelCliCommandError(`Provider '${String(provider)}' cannot resume a CLI session on the fleet`);
7679
+ __name(resolveApiBase, "resolveApiBase");
7680
+ function describeApiBase(base) {
7681
+ if (base.source === "config") {
7682
+ return `${base.url} (from the enrolled config)`;
7624
7683
  }
7625
- if (!isFleetAgentModelSessionId(sessionId)) {
7626
- throw new ModelCliCommandError("CLI session id is not an opaque identifier");
7684
+ if (!base.mismatch) {
7685
+ return `${base.url} (pinned via ${API_URL_ENV}; matches the enrolled origin)`;
7627
7686
  }
7628
- return sessionId;
7687
+ return `${base.url} (PINNED via ${API_URL_ENV}) \u2014 enrolled against ${base.configuredUrl}. The pin does not match the enrolled origin: this node has no credential on the pinned platform, so every call will be refused with 401 until the pin is corrected or the node is re-enrolled.`;
7629
7688
  }
7630
- __name(assertResumableSession, "assertResumableSession");
7631
- function formatBudget(value) {
7632
- if (value === void 0) return null;
7633
- if (!Number.isFinite(value) || value <= 0) {
7634
- throw new ModelCliCommandError("maxBudgetUsd must be a positive number");
7635
- }
7636
- return String(Math.round(value * 100) / 100);
7689
+ __name(describeApiBase, "describeApiBase");
7690
+
7691
+ // src/core/auth-client.ts
7692
+ var EVER_ID_DEVICE_SCOPE = ["openid", "email", EVER_ID_SCOPES.SESSION_EXCHANGE].join(" ");
7693
+
7694
+ // src/core/telemetry-probe.ts
7695
+ var AGENT_CLI_CANDIDATES = ["claude", "codex", "gemini", "opencode"];
7696
+ function parseCliVersion(stdout) {
7697
+ if (typeof stdout !== "string") return null;
7698
+ const match = /\b(\d+\.\d+(?:\.\d+)?(?:[-+][0-9A-Za-z.-]+)?)\b/.exec(stdout);
7699
+ return match ? match[1] : null;
7700
+ }
7701
+ __name(parseCliVersion, "parseCliVersion");
7702
+ async function detectAgentCliVersion(runner, candidates = AGENT_CLI_CANDIDATES) {
7703
+ for (const command of candidates) {
7704
+ try {
7705
+ const result = await runner.run(command, ["--version"]);
7706
+ if (result.code !== 0) continue;
7707
+ const version = parseCliVersion(`${result.stdout ?? ""}
7708
+ ${result.stderr ?? ""}`);
7709
+ if (!version) continue;
7710
+ return `${command} ${version}`.slice(0, FLEET_MAX_CLI_VERSION_LENGTH);
7711
+ } catch {
7712
+ continue;
7713
+ }
7714
+ }
7715
+ return null;
7716
+ }
7717
+ __name(detectAgentCliVersion, "detectAgentCliVersion");
7718
+ async function detectDiskFreeBytes(io, path2) {
7719
+ try {
7720
+ const free = await io.freeBytes(path2);
7721
+ if (typeof free !== "number" || !Number.isFinite(free) || free < 0) return null;
7722
+ return Math.floor(free);
7723
+ } catch {
7724
+ return null;
7725
+ }
7726
+ }
7727
+ __name(detectDiskFreeBytes, "detectDiskFreeBytes");
7728
+ var MODEL_IDENTITY_CACHE_TTL_MS = 5 * 6e4;
7729
+ function parseClaudeAuthStatus(stdout) {
7730
+ let parsed;
7731
+ try {
7732
+ parsed = JSON.parse(stdout.trim());
7733
+ } catch {
7734
+ return null;
7735
+ }
7736
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
7737
+ const status = parsed;
7738
+ if (status.loggedIn !== true) return "claude-code: not logged in";
7739
+ const email = compactString(status.email);
7740
+ const org = compactString(status.orgName);
7741
+ const plan = compactString(status.subscriptionType);
7742
+ const method = compactString(status.authMethod);
7743
+ const who = email ?? (method ? `${method} login` : "logged in");
7744
+ const details = [org, plan].filter((part) => part !== null);
7745
+ return `claude-code: ${who}${details.length > 0 ? ` (${details.join(", ")})` : ""}`;
7746
+ }
7747
+ __name(parseClaudeAuthStatus, "parseClaudeAuthStatus");
7748
+ function parseCodexLoginStatus(stdout) {
7749
+ const text = stdout.trim().toLowerCase();
7750
+ if (!text) return null;
7751
+ if (text.includes("not logged in")) return "codex: not logged in";
7752
+ if (text.includes("chatgpt")) return "codex: chatgpt";
7753
+ if (text.includes("api key")) return "codex: api-key";
7754
+ if (text.includes("logged in")) return "codex: logged in";
7755
+ return null;
7756
+ }
7757
+ __name(parseCodexLoginStatus, "parseCodexLoginStatus");
7758
+ function compactString(value) {
7759
+ if (typeof value !== "string") return null;
7760
+ const cleaned = value.replace(/[\u0000-\u001F\u007F]+/g, " ").trim();
7761
+ return cleaned ? cleaned : null;
7762
+ }
7763
+ __name(compactString, "compactString");
7764
+ async function detectModelIdentity(runner, paths = {}) {
7765
+ const candidates = [
7766
+ { command: paths["claude-code"] || "claude", args: ["auth", "status", "--json"], parse: parseClaudeAuthStatus },
7767
+ { command: paths.codex || "codex", args: ["login", "status"], parse: parseCodexLoginStatus }
7768
+ ];
7769
+ for (const candidate of candidates) {
7770
+ try {
7771
+ const result = await runner.run(candidate.command, candidate.args);
7772
+ const stdout = result.stdout ?? "";
7773
+ const stderr = result.stderr ?? "";
7774
+ const label = candidate.parse(stdout) ?? candidate.parse(`${stdout}
7775
+ ${stderr}`);
7776
+ if (label) return label.slice(0, FLEET_MAX_MODEL_IDENTITY_LENGTH);
7777
+ } catch {
7778
+ continue;
7779
+ }
7780
+ }
7781
+ return null;
7782
+ }
7783
+ __name(detectModelIdentity, "detectModelIdentity");
7784
+ function cacheProbe(probe, ttlMs = MODEL_IDENTITY_CACHE_TTL_MS, now = Date.now) {
7785
+ let value = null;
7786
+ let expiresAt = Number.NEGATIVE_INFINITY;
7787
+ let inFlight = null;
7788
+ return async () => {
7789
+ if (now() < expiresAt) return value;
7790
+ if (inFlight) return inFlight;
7791
+ inFlight = probe().then((fresh) => {
7792
+ value = fresh;
7793
+ expiresAt = now() + ttlMs;
7794
+ return fresh;
7795
+ }).finally(() => {
7796
+ inFlight = null;
7797
+ });
7798
+ return inFlight;
7799
+ };
7800
+ }
7801
+ __name(cacheProbe, "cacheProbe");
7802
+
7803
+ // src/core/executors/model-cli.ts
7804
+ var MODEL_CLI_EMPTY_MCP_CONFIG = `${JSON.stringify({ mcpServers: {} }, null, 2)}
7805
+ `;
7806
+ var CODEX_SETTING_VALUE = /^[A-Za-z0-9._-]{1,64}$/;
7807
+ var TOML_STRING_ASSIGNMENT = /^([A-Za-z0-9_-]+)[ \t]*=[ \t]*(?:"([^"\\]*)"|'([^']*)')(?:[ \t]+#.*|#.*)?$/;
7808
+ var TOML_MULTILINE_VALUE_START = /^[^=#]*=[ \t]*("""|''')/;
7809
+ function readCodexUserSettings(configToml) {
7810
+ const settings = {};
7811
+ if (typeof configToml !== "string" || configToml.length === 0) return settings;
7812
+ let table = "";
7813
+ let openMultiline = null;
7814
+ for (const raw of configToml.split(/\r?\n/)) {
7815
+ if (openMultiline !== null) {
7816
+ if (indexOfTomlClosingDelimiter(raw, openMultiline) !== -1) openMultiline = null;
7817
+ continue;
7818
+ }
7819
+ const line = raw.trim();
7820
+ const opening = TOML_MULTILINE_VALUE_START.exec(line);
7821
+ if (opening) {
7822
+ const delimiter = opening[1];
7823
+ if (indexOfTomlClosingDelimiter(line.slice(opening[0].length), delimiter) === -1) {
7824
+ openMultiline = delimiter;
7825
+ continue;
7826
+ }
7827
+ }
7828
+ if (line.startsWith("[")) {
7829
+ if (line.startsWith("[[")) {
7830
+ table = "\0array";
7831
+ } else {
7832
+ const close = line.indexOf("]");
7833
+ table = close > 1 ? line.slice(1, close).trim() : "\0unknown";
7834
+ }
7835
+ continue;
7836
+ }
7837
+ const assignment = TOML_STRING_ASSIGNMENT.exec(line);
7838
+ if (!assignment) continue;
7839
+ const key = assignment[1];
7840
+ const value = assignment[2] ?? assignment[3] ?? "";
7841
+ if (!CODEX_SETTING_VALUE.test(value)) continue;
7842
+ if (table === "" && key === "model" && settings.model === void 0) settings.model = value;
7843
+ else if (table === "" && key === "model_reasoning_effort" && settings.reasoningEffort === void 0) {
7844
+ settings.reasoningEffort = value;
7845
+ } else if (table === "windows" && key === "sandbox" && settings.windowsSandbox === void 0) {
7846
+ settings.windowsSandbox = value;
7847
+ }
7848
+ }
7849
+ return settings;
7850
+ }
7851
+ __name(readCodexUserSettings, "readCodexUserSettings");
7852
+ function indexOfTomlClosingDelimiter(text, delimiter) {
7853
+ let from = 0;
7854
+ for (; ; ) {
7855
+ const at = text.indexOf(delimiter, from);
7856
+ if (at === -1 || delimiter === "'''") return at;
7857
+ let backslashes = 0;
7858
+ for (let i = at - 1; i >= 0 && text[i] === "\\"; i -= 1) backslashes += 1;
7859
+ if (backslashes % 2 === 0) return at;
7860
+ from = at + 1;
7861
+ }
7862
+ }
7863
+ __name(indexOfTomlClosingDelimiter, "indexOfTomlClosingDelimiter");
7864
+ function assertCodexSetting(name, value) {
7865
+ if (value === void 0) return void 0;
7866
+ if (!CODEX_SETTING_VALUE.test(value)) {
7867
+ throw new ModelCliCommandError(`Refusing to forward the Codex setting '${name}': not a bare identifier`);
7868
+ }
7869
+ return value;
7870
+ }
7871
+ __name(assertCodexSetting, "assertCodexSetting");
7872
+ function modelCliMcpIsolationEnv(provider) {
7873
+ return provider === "claude-code" ? { ENABLE_CLAUDEAI_MCP_SERVERS: "false" } : null;
7874
+ }
7875
+ __name(modelCliMcpIsolationEnv, "modelCliMcpIsolationEnv");
7876
+ function claudeManagedMcpConfigPath(platform = process.platform) {
7877
+ if (platform === "win32") return "C:\\Program Files\\ClaudeCode\\managed-mcp.json";
7878
+ if (platform === "darwin") return "/Library/Application Support/ClaudeCode/managed-mcp.json";
7879
+ return "/etc/claude-code/managed-mcp.json";
7880
+ }
7881
+ __name(claudeManagedMcpConfigPath, "claudeManagedMcpConfigPath");
7882
+ function claudeManagedMcpConfigInEffect(raw) {
7883
+ if (typeof raw !== "string" || !raw.trim()) return false;
7884
+ try {
7885
+ const parsed = JSON.parse(raw);
7886
+ return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed);
7887
+ } catch {
7888
+ return false;
7889
+ }
7890
+ }
7891
+ __name(claudeManagedMcpConfigInEffect, "claudeManagedMcpConfigInEffect");
7892
+ var ModelCliCommandError = class extends Error {
7893
+ static {
7894
+ __name(this, "ModelCliCommandError");
7895
+ }
7896
+ constructor(message) {
7897
+ super(message);
7898
+ this.name = "ModelCliCommandError";
7899
+ }
7900
+ };
7901
+ var MODEL_CLI_STEP_ID = "model";
7902
+ var MODEL_CLI_OUTPUT_TAIL_BYTES = 8 * 1024;
7903
+ var MODEL_CLI_MAX_OUTPUT_BYTES = 8 * 1024 * 1024;
7904
+ var ABSOLUTE_WIN32 = /^[A-Za-z]:[\\/]/;
7905
+ var UNSAFE_PATH_CHARS = /[\0\r\n"'`$&|;<>%!^]/;
7906
+ function quoteShellPath(path2, platform = process.platform) {
7907
+ if (typeof path2 !== "string" || !path2.trim()) {
7908
+ throw new ModelCliCommandError("Path must not be empty");
7909
+ }
7910
+ if (UNSAFE_PATH_CHARS.test(path2)) {
7911
+ throw new ModelCliCommandError("Path contains characters the shell could interpret");
7912
+ }
7913
+ const absolute = platform === "win32" ? ABSOLUTE_WIN32.test(path2) || path2.startsWith("\\\\") : path2.startsWith("/");
7914
+ if (!absolute) {
7915
+ throw new ModelCliCommandError("Path must be absolute");
7916
+ }
7917
+ return `"${path2}"`;
7918
+ }
7919
+ __name(quoteShellPath, "quoteShellPath");
7920
+ function assertMcpServerName(name) {
7921
+ if (typeof name !== "string" || !MCP_SERVER_NAME_PATTERN.test(name)) {
7922
+ throw new ModelCliCommandError("MCP server name is not an opaque identifier");
7923
+ }
7924
+ return name;
7925
+ }
7926
+ __name(assertMcpServerName, "assertMcpServerName");
7927
+ function assertLoopbackUrl(url) {
7928
+ if (typeof url !== "string" || !MCP_LOOPBACK_URL_PATTERN.test(url)) {
7929
+ throw new ModelCliCommandError("MCP bridge URL is not a loopback URL this node produced");
7930
+ }
7931
+ return url;
7932
+ }
7933
+ __name(assertLoopbackUrl, "assertLoopbackUrl");
7934
+ var MCP_SERVER_NAME_PATTERN = /^[A-Za-z0-9_-]{1,64}$/;
7935
+ var MCP_LOOPBACK_URL_PATTERN = /^http:\/\/127\.0\.0\.1:\d{1,5}\/mcp\/[a-f0-9]{8,64}$/;
7936
+ function assertModelId(model) {
7937
+ if (model === void 0) return null;
7938
+ if (!FLEET_AGENT_EXECUTION_MODEL_PATTERN.test(model)) {
7939
+ throw new ModelCliCommandError("Model id is not an opaque identifier");
7940
+ }
7941
+ return model;
7942
+ }
7943
+ __name(assertModelId, "assertModelId");
7944
+ var MODEL_CLI_OPTIONAL_FLAGS = {
7945
+ "claude-code": ["--effort", "--max-budget-usd"],
7946
+ codex: []
7947
+ };
7948
+ function unsupportedOptionalModelCliFlags(execution, supportedFlags) {
7949
+ if (!supportedFlags) return [];
7950
+ const wanted = [];
7951
+ if (execution.provider === "claude-code") {
7952
+ if (execution.effort) wanted.push("--effort");
7953
+ if (execution.maxBudgetUsd !== void 0) wanted.push("--max-budget-usd");
7954
+ }
7955
+ const droppable = new Set(
7956
+ MODEL_CLI_OPTIONAL_FLAGS[execution.provider] ?? []
7957
+ );
7958
+ return wanted.filter((flag) => droppable.has(flag) && !supportedFlags.has(flag));
7959
+ }
7960
+ __name(unsupportedOptionalModelCliFlags, "unsupportedOptionalModelCliFlags");
7961
+ function assertResumableSession(provider, sessionId) {
7962
+ if (sessionId === void 0) return null;
7963
+ if (!fleetAgentExecutionProviderSupportsSessionResume(provider)) {
7964
+ throw new ModelCliCommandError(`Provider '${String(provider)}' cannot resume a CLI session on the fleet`);
7965
+ }
7966
+ if (!isFleetAgentModelSessionId(sessionId)) {
7967
+ throw new ModelCliCommandError("CLI session id is not an opaque identifier");
7968
+ }
7969
+ return sessionId;
7970
+ }
7971
+ __name(assertResumableSession, "assertResumableSession");
7972
+ function formatBudget(value) {
7973
+ if (value === void 0) return null;
7974
+ if (!Number.isFinite(value) || value <= 0) {
7975
+ throw new ModelCliCommandError("maxBudgetUsd must be a positive number");
7976
+ }
7977
+ return String(Math.round(value * 100) / 100);
7637
7978
  }
7638
7979
  __name(formatBudget, "formatBudget");
7639
7980
  function buildModelCliCommand(input) {
@@ -7692,7 +8033,21 @@ function buildModelCliCommand(input) {
7692
8033
  if (roots.length > 0) args.push("--add-dir", ...roots);
7693
8034
  } else if (execution.provider === "codex") {
7694
8035
  const sandbox = permissionMode === "plan" ? "read-only" : "workspace-write";
7695
- args.push("exec", "--json", "--sandbox", sandbox, "-C", quoteShellPath(input.workspacePath, platform));
8036
+ args.push(
8037
+ "exec",
8038
+ "--ignore-user-config",
8039
+ "--json",
8040
+ "--sandbox",
8041
+ sandbox,
8042
+ "-C",
8043
+ quoteShellPath(input.workspacePath, platform)
8044
+ );
8045
+ const kept = input.codexUserSettings ?? {};
8046
+ const windowsSandbox = assertCodexSetting("windows.sandbox", kept.windowsSandbox);
8047
+ const reasoningEffort = assertCodexSetting("model_reasoning_effort", kept.reasoningEffort);
8048
+ const ownerModel = assertCodexSetting("model", kept.model);
8049
+ if (windowsSandbox) args.push("-c", `windows.sandbox=${windowsSandbox}`);
8050
+ if (reasoningEffort) args.push("-c", `model_reasoning_effort=${reasoningEffort}`);
7696
8051
  if (sandbox === "workspace-write") {
7697
8052
  for (const root of grantedRoots(writableMounts)) args.push("--add-dir", root);
7698
8053
  }
@@ -7702,7 +8057,8 @@ function buildModelCliCommand(input) {
7702
8057
  `mcp_servers.${assertMcpServerName(mcp.serverName)}.url=${assertLoopbackUrl(mcp.serverUrl)}`
7703
8058
  );
7704
8059
  }
7705
- if (model) args.push("-m", model);
8060
+ const codexModel = model ?? ownerModel;
8061
+ if (codexModel) args.push("-m", codexModel);
7706
8062
  if (execution.skipPermissions === true) args.push("--dangerously-bypass-approvals-and-sandbox");
7707
8063
  args.push("-");
7708
8064
  } else {
@@ -7725,7 +8081,16 @@ function assertMountGrantsInCommand(input) {
7725
8081
  }
7726
8082
  __name(assertMountGrantsInCommand, "assertMountGrantsInCommand");
7727
8083
  var STRICT_MCP_CONFIG_IN_COMMAND = / --mcp-config "[^"]+" --strict-mcp-config /;
8084
+ var CODEX_IGNORE_USER_CONFIG_IN_COMMAND = / exec --ignore-user-config /;
7728
8085
  function assertMcpIsolationInCommand(input) {
8086
+ if (input.execution.provider === "codex") {
8087
+ if (!CODEX_IGNORE_USER_CONFIG_IN_COMMAND.test(input.command)) {
8088
+ throw new ModelCliCommandError(
8089
+ "The codex command line does not carry exec --ignore-user-config, so the run would load the machine owner's own config.toml: its MCP servers, plugins and notify hook"
8090
+ );
8091
+ }
8092
+ return;
8093
+ }
7729
8094
  if (input.execution.provider !== "claude-code") return;
7730
8095
  if (!STRICT_MCP_CONFIG_IN_COMMAND.test(input.command)) {
7731
8096
  throw new ModelCliCommandError(
@@ -8053,436 +8418,183 @@ var MODEL_CLI_REQUIRED_FLAGS = {
8053
8418
  // bridge's. A CLI without them cannot run fleet work at all, so `doctor`
8054
8419
  // must say INCOMPATIBLE, not "runs that need them will fail".
8055
8420
  "claude-code": ["-p", "--output-format", "--permission-mode", "--mcp-config", "--strict-mcp-config"],
8056
- codex: ["--json", "--sandbox", "-C"]
8421
+ codex: ["--json", "--sandbox", "-C", "--ignore-user-config"]
8057
8422
  };
8058
8423
  var MODEL_CLI_CONDITIONAL_FLAGS = {
8059
- "claude-code": [
8060
- "--model",
8061
- "--add-dir",
8062
- "--allowedTools",
8063
- "--dangerously-skip-permissions",
8064
- "--verbose",
8065
- "--resume",
8066
- "--fork-session"
8067
- ],
8068
- codex: ["--add-dir", "-c", "-m", "--dangerously-bypass-approvals-and-sandbox"]
8069
- };
8070
- var MODEL_CLI_COMPAT_UNSTAMPED_TTL_MS = 10 * 6e4;
8071
- var MODEL_CLI_COMPAT_STAMPED_MAX_AGE_MS = 60 * 6e4;
8072
- var MODEL_CLI_COMPAT_INCOMPLETE_RETRY_MS = 6e4;
8073
- var ModelCliCompatibilityProbe = class {
8074
- constructor(options) {
8075
- this.options = options;
8076
- this.now = options.now ?? (() => Date.now());
8077
- this.unstampedTtlMs = options.unstampedTtlMs ?? MODEL_CLI_COMPAT_UNSTAMPED_TTL_MS;
8078
- this.incompleteRetryMs = options.incompleteRetryMs ?? MODEL_CLI_COMPAT_INCOMPLETE_RETRY_MS;
8079
- this.stampedMaxAgeMs = options.stampedMaxAgeMs ?? MODEL_CLI_COMPAT_STAMPED_MAX_AGE_MS;
8080
- }
8081
- options;
8082
- static {
8083
- __name(this, "ModelCliCompatibilityProbe");
8084
- }
8085
- cache = /* @__PURE__ */ new Map();
8086
- inFlight = /* @__PURE__ */ new Map();
8087
- now;
8088
- unstampedTtlMs;
8089
- incompleteRetryMs;
8090
- stampedMaxAgeMs;
8091
- /** One pinned binary. */
8092
- async probe(provider, executable) {
8093
- const stamp = await this.stamp(executable);
8094
- const key = `${provider}|${executable}|${stamp ? `${stamp.mtimeMs}:${stamp.size}` : "unstamped"}`;
8095
- const cached = this.cache.get(`${provider}|${executable}`);
8096
- if (cached && cached.key === key && this.now() - cached.probedAt < cached.ttlMs) {
8097
- return cached.result;
8098
- }
8099
- const pending = this.inFlight.get(key);
8100
- if (pending) return pending;
8101
- const run = this.run(provider, executable).then(({ result, answered }) => {
8102
- this.cache.set(`${provider}|${executable}`, {
8103
- key,
8104
- // A COMPLETE answer from a stamped file is kept until the file
8105
- // changes or, at the latest, for `stampedMaxAgeMs` (an upgrade
8106
- // behind a stable wrapper does not change the stamp). An
8107
- // incomplete one is retried soon, and an unstamped one on the
8108
- // ten-minute clock.
8109
- ttlMs: !answered ? this.incompleteRetryMs : stamp !== null ? this.stampedMaxAgeMs : this.unstampedTtlMs,
8110
- probedAt: this.now(),
8111
- result
8112
- });
8113
- return result;
8114
- }).finally(() => {
8115
- this.inFlight.delete(key);
8116
- });
8117
- this.inFlight.set(key, run);
8118
- return run;
8119
- }
8120
- /** Every pinned binary in `paths`, in provider order. Unpinned providers are skipped. */
8121
- async probeAll(paths) {
8122
- const out = [];
8123
- for (const provider of MODEL_CLI_PROVIDERS) {
8124
- const executable = paths?.[provider];
8125
- if (typeof executable !== "string" || !executable.trim()) continue;
8126
- out.push(await this.probe(provider, executable));
8127
- }
8128
- return out;
8129
- }
8130
- async stamp(executable) {
8131
- if (!this.options.statFile) return null;
8132
- try {
8133
- const stamp = await this.options.statFile(executable);
8134
- return stamp && Number.isFinite(stamp.mtimeMs) && Number.isFinite(stamp.size) ? stamp : null;
8135
- } catch {
8136
- return null;
8137
- }
8138
- }
8139
- async run(provider, executable) {
8140
- const command = this.commandFor(executable);
8141
- const versionOutput = await this.ask(command, ["--version"]);
8142
- const version = versionOutput === null ? null : parseCliVersion(versionOutput);
8143
- const help = await this.ask(command, [...HELP_ARGS[provider]]);
8144
- return {
8145
- result: judgeModelCliHelp(provider, executable, version, help),
8146
- // "Answered" = both invocations produced output. An UNRECOGNISED
8147
- // help text is still an answer — deterministic for that file — and
8148
- // re-asking every minute would learn nothing.
8149
- answered: versionOutput !== null && help !== null
8150
- };
8151
- }
8152
- /**
8153
- * The runner on Windows goes through `cmd.exe` (`shell: true`), which
8154
- * splits an unquoted `C:\Program Files\…` at the space.
8155
- */
8156
- commandFor(executable) {
8157
- return this.options.platform === "win32" && /\s/.test(executable) && !executable.startsWith('"') ? `"${executable}"` : executable;
8158
- }
8159
- /** stdout+stderr of one invocation, or null when it could not be run at all. */
8160
- async ask(command, args) {
8161
- try {
8162
- const result = await this.options.runner.run(command, args);
8163
- if (result.timedOut) return null;
8164
- const text = `${result.stdout ?? ""}
8165
- ${result.stderr ?? ""}`;
8166
- return result.code === 0 || text.trim() ? text : null;
8167
- } catch {
8168
- return null;
8169
- }
8170
- }
8171
- };
8172
- function parseAdvertisedFlags(help) {
8173
- const flags = /* @__PURE__ */ new Set();
8174
- const pattern = /(?:^|[\s,[(|])(--?[A-Za-z][A-Za-z0-9-]*)(?=$|[\s,=<[\])|.:])/gm;
8175
- for (const match of help.matchAll(pattern)) {
8176
- flags.add(match[1]);
8177
- }
8178
- return flags;
8179
- }
8180
- __name(parseAdvertisedFlags, "parseAdvertisedFlags");
8181
- function judgeModelCliHelp(provider, executable, version, help) {
8182
- const required = MODEL_CLI_REQUIRED_FLAGS[provider];
8183
- const base = {
8184
- provider,
8185
- executable,
8186
- version,
8187
- supportedFlags: null,
8188
- missingRequiredFlags: [],
8189
- missingConditionalFlags: [],
8190
- unsupportedOptionalFlags: [],
8191
- note: null
8192
- };
8193
- if (help === null) {
8194
- return { ...base, note: "the binary did not answer --help; nothing is dropped" };
8195
- }
8196
- const advertised = parseAdvertisedFlags(help);
8197
- if (!required.some((flag) => advertised.has(flag))) {
8198
- return {
8199
- ...base,
8200
- note: `the help text lists none of the flags this node always passes (${required.join(", ")}); its format is not recognised, so nothing is dropped`
8201
- };
8202
- }
8203
- const missing = /* @__PURE__ */ __name((list) => list.filter((flag) => !advertised.has(flag)), "missing");
8204
- return {
8205
- ...base,
8206
- supportedFlags: advertised,
8207
- missingRequiredFlags: missing(required),
8208
- missingConditionalFlags: missing(MODEL_CLI_CONDITIONAL_FLAGS[provider]),
8209
- unsupportedOptionalFlags: missing(MODEL_CLI_OPTIONAL_FLAGS[provider])
8210
- };
8211
- }
8212
- __name(judgeModelCliHelp, "judgeModelCliHelp");
8213
- function isModelCliCompatible(compat) {
8214
- return compat.missingRequiredFlags.length === 0;
8215
- }
8216
- __name(isModelCliCompatible, "isModelCliCompatible");
8217
- function describeCliVersions(results) {
8218
- return results.map((result) => `${result.provider} ${result.version ?? "unknown"}`.slice(0, FLEET_MAX_CLI_VERSION_LENGTH)).slice(0, FLEET_MAX_CLI_VERSIONS);
8219
- }
8220
- __name(describeCliVersions, "describeCliVersions");
8221
- var LEGACY_COMMAND_NAME = {
8222
- "claude-code": "claude",
8223
- codex: "codex"
8224
- };
8225
- function primaryPinnedCliVersion(results) {
8226
- const first = results.find((result) => result.version !== null);
8227
- return first ? `${LEGACY_COMMAND_NAME[first.provider]} ${first.version}`.slice(0, FLEET_MAX_CLI_VERSION_LENGTH) : null;
8228
- }
8229
- __name(primaryPinnedCliVersion, "primaryPinnedCliVersion");
8230
-
8231
- // src/core/worker-loop.ts
8232
- var import_node_perf_hooks = require("node:perf_hooks");
8233
-
8234
- // src/core/heartbeat.ts
8235
- var systemScheduler = {
8236
- setTimeout: /* @__PURE__ */ __name((callback, ms) => setTimeout(callback, ms), "setTimeout"),
8237
- clearTimeout: /* @__PURE__ */ __name((handle) => clearTimeout(handle), "clearTimeout")
8238
- };
8239
- var OPTIONAL_DESCRIPTION_FIELD_TIERS = [
8240
- // NEWEST first. Node lifecycle + remote node limits (self-build slices
8241
- // AR, AS). Tiered (review): `cliVersions` goes out on EVERY beat of a
8242
- // node with a pinned CLI, so an upgraded node talking to a platform that
8243
- // predates it would otherwise drop the worker state and housekeeping
8244
- // along with it — losing health signals that platform DOES understand,
8245
- // for the life of the process. Stripping the newest tier first keeps them.
8246
- ["cliVersions", "maxConcurrentJobs", "maxCpuPercent", "maxMemoryMb"],
8247
- // Fleet health signals (EW-776) and node housekeeping (EW-803).
8248
- [
8249
- "workerState",
8250
- "workerStateReason",
8251
- "minFreeDiskBytes",
8252
- "workspaceCount",
8253
- "workspaceBytes",
8254
- "lastReclaimAt",
8255
- "lastReclaimFreedBytes"
8256
- ]
8257
- ];
8258
- function computeBackoffDelay(intervalMs, failures, maxMs) {
8259
- if (failures <= 0) {
8260
- return intervalMs;
8261
- }
8262
- const exponent = Math.min(failures, 30);
8263
- const delay2 = intervalMs * 2 ** exponent;
8264
- return Math.min(delay2, maxMs);
8265
- }
8266
- __name(computeBackoffDelay, "computeBackoffDelay");
8267
- var HeartbeatLoop = class {
8268
- static {
8269
- __name(this, "HeartbeatLoop");
8270
- }
8271
- options;
8272
- scheduler;
8273
- now;
8274
- listeners = /* @__PURE__ */ new Set();
8275
- timer = null;
8276
- running = false;
8277
- inFlight = null;
8278
- /**
8279
- * How many optional-field TIERS (newest first) this platform has proven
8280
- * it predates; from then on the beat leaves those tiers out. 0 = it
8281
- * takes everything. Process-scoped on purpose — a platform upgrade is a
8282
- * restart-shaped event for the node, and re-probing on every beat would
8283
- * mean one wasted round trip per beat, forever, against an API that
8284
- * will never accept them.
8285
- */
8286
- strippedTiers = 0;
8287
- state = {
8288
- state: "idle",
8289
- lastHeartbeatAt: null,
8290
- consecutiveFailures: 0,
8291
- nextAttemptInMs: null,
8292
- lastError: null,
8293
- lastErrorKind: null,
8294
- node: null
8295
- };
8296
- constructor(options) {
8297
- this.options = {
8298
- ...options,
8299
- intervalMs: options.intervalMs ?? DEFAULT_HEARTBEAT_INTERVAL_MS,
8300
- maxBackoffMs: options.maxBackoffMs ?? MAX_HEARTBEAT_BACKOFF_MS
8301
- };
8302
- this.scheduler = options.scheduler ?? systemScheduler;
8303
- this.now = options.now ?? (() => Date.now());
8304
- this.options.logger?.protect(options.secret);
8305
- }
8306
- getState() {
8307
- return { ...this.state };
8308
- }
8309
- onChange(listener) {
8310
- this.listeners.add(listener);
8311
- return () => {
8312
- this.listeners.delete(listener);
8313
- };
8314
- }
8315
- /** Start beating. Resolves once the FIRST beat has settled. */
8316
- start() {
8317
- if (this.running) {
8318
- return this.inFlight ?? Promise.resolve();
8319
- }
8320
- this.running = true;
8321
- return this.tick();
8322
- }
8323
- /** Stop beating and cancel any pending timer. */
8324
- stop() {
8325
- this.running = false;
8326
- this.cancelTimer();
8327
- this.patch({ state: "stopped", nextAttemptInMs: null });
8328
- }
8329
- /** Await the in-flight beat, if any (test/shutdown helper). */
8330
- async settled() {
8331
- await this.inFlight;
8332
- }
8333
- /** Run one beat now. Exposed so the UI can offer "retry" and tests can step. */
8334
- tick() {
8335
- const run = this.runOnce().finally(() => {
8336
- if (this.inFlight === run) {
8337
- this.inFlight = null;
8338
- }
8339
- });
8340
- this.inFlight = run;
8341
- return run;
8342
- }
8343
- async runOnce() {
8344
- if (!this.running) {
8345
- return;
8346
- }
8347
- this.cancelTimer();
8348
- if (this.state.state !== "connected") {
8349
- this.patch({ state: "connecting" });
8350
- }
8351
- try {
8352
- const description = await this.options.describe();
8353
- this.onSuccess(await this.beat(description));
8354
- } catch (error) {
8355
- this.onFailure(error);
8356
- }
8357
- this.scheduleNext();
8358
- }
8359
- /**
8360
- * Send one beat, tolerating a platform that predates the optional
8361
- * self-description fields (the worker state, EW-776; the housekeeping
8362
- * report, EW-803).
8363
- *
8364
- * The API validates heartbeats with `whitelist + forbidNonWhitelisted`,
8365
- * so a field an older build does not know is not ignored — it 400s the
8366
- * whole request. A 400 is a FAILED beat, a failed beat backs off, and
8367
- * enough of them sweep the node to `offline`. Which would mean shipping
8368
- * a health-reporting feature whose failure mode is "every node in a
8369
- * mixed-version fleet goes dark": the exact class of outage this slice
8370
- * was written to end.
8371
- *
8372
- * So a 400 on a beat that CARRIED those fields is retried once,
8373
- * immediately, without them. If that succeeds the platform is simply
8374
- * older than this daemon; we latch, say so once, and keep reporting
8375
- * liveness. The retry costs one request, once per process.
8376
- *
8377
- * Inferring "the field was rejected" from a bare 400 is deliberate and
8378
- * has a cost worth stating: `FleetClient` never surfaces server bodies
8379
- * (fixed client-authored messages only), so a 400 caused by something
8380
- * else on such a beat also latches, and this node reports liveness only
8381
- * until it restarts. That is the safe direction — the node stays
8382
- * observable either way, and only the extra signal is lost.
8383
- */
8384
- async beat(description) {
8385
- const credential = { nodeId: this.options.nodeId, secret: this.options.secret };
8386
- let sent = stripTiers(description, this.strippedTiers);
8387
- try {
8388
- return await this.options.client.heartbeat({ ...credential, ...sent });
8389
- } catch (error) {
8390
- if (!isFieldRejection(error)) throw error;
8391
- let lastError = error;
8392
- for (let tiers = this.strippedTiers + 1; tiers <= OPTIONAL_DESCRIPTION_FIELD_TIERS.length; tiers += 1) {
8393
- const candidate = stripTiers(description, tiers);
8394
- if (Object.keys(candidate).length === Object.keys(sent).length) continue;
8395
- sent = candidate;
8396
- try {
8397
- const result = await this.options.client.heartbeat({ ...credential, ...candidate });
8398
- this.strippedTiers = tiers;
8399
- this.options.logger?.warn(
8400
- tiers >= OPTIONAL_DESCRIPTION_FIELD_TIERS.length ? "Platform rejected the optional self-description fields (worker state, housekeeping); it predates them. Reporting liveness only until this node restarts." : "Platform rejected the newest optional self-description fields (pinned CLI versions, enforced limits); it predates them. Still reporting worker state and housekeeping; the newer fields stay off until this node restarts."
8401
- );
8402
- return result;
8403
- } catch (retryError) {
8404
- if (!isFieldRejection(retryError)) throw retryError;
8405
- lastError = retryError;
8406
- }
8407
- }
8408
- throw lastError;
8409
- }
8424
+ "claude-code": [
8425
+ "--model",
8426
+ "--add-dir",
8427
+ "--allowedTools",
8428
+ "--dangerously-skip-permissions",
8429
+ "--verbose",
8430
+ "--resume",
8431
+ "--fork-session"
8432
+ ],
8433
+ codex: ["--add-dir", "-c", "-m", "--dangerously-bypass-approvals-and-sandbox"]
8434
+ };
8435
+ var MODEL_CLI_COMPAT_UNSTAMPED_TTL_MS = 10 * 6e4;
8436
+ var MODEL_CLI_COMPAT_STAMPED_MAX_AGE_MS = 60 * 6e4;
8437
+ var MODEL_CLI_COMPAT_INCOMPLETE_RETRY_MS = 6e4;
8438
+ var ModelCliCompatibilityProbe = class {
8439
+ constructor(options) {
8440
+ this.options = options;
8441
+ this.now = options.now ?? (() => Date.now());
8442
+ this.unstampedTtlMs = options.unstampedTtlMs ?? MODEL_CLI_COMPAT_UNSTAMPED_TTL_MS;
8443
+ this.incompleteRetryMs = options.incompleteRetryMs ?? MODEL_CLI_COMPAT_INCOMPLETE_RETRY_MS;
8444
+ this.stampedMaxAgeMs = options.stampedMaxAgeMs ?? MODEL_CLI_COMPAT_STAMPED_MAX_AGE_MS;
8410
8445
  }
8411
- onSuccess(result) {
8412
- this.patch({
8413
- state: "connected",
8414
- lastHeartbeatAt: this.now(),
8415
- consecutiveFailures: 0,
8416
- lastError: null,
8417
- lastErrorKind: null,
8418
- node: result.node
8419
- });
8420
- if (this.options.onAccepted) {
8421
- try {
8422
- this.options.onAccepted(result);
8423
- } catch {
8424
- }
8425
- }
8446
+ options;
8447
+ static {
8448
+ __name(this, "ModelCliCompatibilityProbe");
8426
8449
  }
8427
- onFailure(error) {
8428
- const kind = error instanceof FleetClientError ? error.kind : "network";
8429
- const raw = error instanceof Error ? error.message : String(error);
8430
- const message = this.options.logger?.redact(raw) ?? raw;
8431
- const failures = this.state.consecutiveFailures + 1;
8432
- this.patch({
8433
- // `unauthorized` is sticky and visible: the operator has to act
8434
- // (re-enable the node in Fleet, or re-enroll). We keep retrying at
8435
- // the backoff ceiling so a re-enabled node recovers on its own.
8436
- state: kind === "unauthorized" ? "unauthorized" : "retrying",
8437
- consecutiveFailures: failures,
8438
- lastError: message,
8439
- lastErrorKind: kind
8450
+ cache = /* @__PURE__ */ new Map();
8451
+ inFlight = /* @__PURE__ */ new Map();
8452
+ now;
8453
+ unstampedTtlMs;
8454
+ incompleteRetryMs;
8455
+ stampedMaxAgeMs;
8456
+ /** One pinned binary. */
8457
+ async probe(provider, executable) {
8458
+ const stamp = await this.stamp(executable);
8459
+ const key = `${provider}|${executable}|${stamp ? `${stamp.mtimeMs}:${stamp.size}` : "unstamped"}`;
8460
+ const cached = this.cache.get(`${provider}|${executable}`);
8461
+ if (cached && cached.key === key && this.now() - cached.probedAt < cached.ttlMs) {
8462
+ return cached.result;
8463
+ }
8464
+ const pending = this.inFlight.get(key);
8465
+ if (pending) return pending;
8466
+ const run = this.run(provider, executable).then(({ result, answered }) => {
8467
+ this.cache.set(`${provider}|${executable}`, {
8468
+ key,
8469
+ // A COMPLETE answer from a stamped file is kept until the file
8470
+ // changes or, at the latest, for `stampedMaxAgeMs` (an upgrade
8471
+ // behind a stable wrapper does not change the stamp). An
8472
+ // incomplete one is retried soon, and an unstamped one on the
8473
+ // ten-minute clock.
8474
+ ttlMs: !answered ? this.incompleteRetryMs : stamp !== null ? this.stampedMaxAgeMs : this.unstampedTtlMs,
8475
+ probedAt: this.now(),
8476
+ result
8477
+ });
8478
+ return result;
8479
+ }).finally(() => {
8480
+ this.inFlight.delete(key);
8440
8481
  });
8441
- this.options.logger?.warn(`Heartbeat failed (attempt ${failures}): ${message}`);
8482
+ this.inFlight.set(key, run);
8483
+ return run;
8442
8484
  }
8443
- scheduleNext() {
8444
- if (!this.running) {
8445
- return;
8485
+ /** Every pinned binary in `paths`, in provider order. Unpinned providers are skipped. */
8486
+ async probeAll(paths) {
8487
+ const out = [];
8488
+ for (const provider of MODEL_CLI_PROVIDERS) {
8489
+ const executable = paths?.[provider];
8490
+ if (typeof executable !== "string" || !executable.trim()) continue;
8491
+ out.push(await this.probe(provider, executable));
8446
8492
  }
8447
- const delay2 = computeBackoffDelay(
8448
- this.options.intervalMs,
8449
- this.state.consecutiveFailures,
8450
- this.options.maxBackoffMs
8451
- );
8452
- this.patch({ nextAttemptInMs: delay2 });
8453
- this.timer = this.scheduler.setTimeout(() => {
8454
- this.timer = null;
8455
- void this.tick();
8456
- }, delay2);
8493
+ return out;
8457
8494
  }
8458
- cancelTimer() {
8459
- if (this.timer !== null) {
8460
- this.scheduler.clearTimeout(this.timer);
8461
- this.timer = null;
8495
+ async stamp(executable) {
8496
+ if (!this.options.statFile) return null;
8497
+ try {
8498
+ const stamp = await this.options.statFile(executable);
8499
+ return stamp && Number.isFinite(stamp.mtimeMs) && Number.isFinite(stamp.size) ? stamp : null;
8500
+ } catch {
8501
+ return null;
8462
8502
  }
8463
8503
  }
8464
- patch(next) {
8465
- this.state = { ...this.state, ...next };
8466
- const snapshot = this.getState();
8467
- for (const listener of this.listeners) {
8468
- listener(snapshot);
8504
+ async run(provider, executable) {
8505
+ const command = this.commandFor(executable);
8506
+ const versionOutput = await this.ask(command, ["--version"]);
8507
+ const version = versionOutput === null ? null : parseCliVersion(versionOutput);
8508
+ const help = await this.ask(command, [...HELP_ARGS[provider]]);
8509
+ return {
8510
+ result: judgeModelCliHelp(provider, executable, version, help),
8511
+ // "Answered" = both invocations produced output. An UNRECOGNISED
8512
+ // help text is still an answer — deterministic for that file — and
8513
+ // re-asking every minute would learn nothing.
8514
+ answered: versionOutput !== null && help !== null
8515
+ };
8516
+ }
8517
+ /**
8518
+ * The runner on Windows goes through `cmd.exe` (`shell: true`), which
8519
+ * splits an unquoted `C:\Program Files\…` at the space.
8520
+ */
8521
+ commandFor(executable) {
8522
+ return this.options.platform === "win32" && /\s/.test(executable) && !executable.startsWith('"') ? `"${executable}"` : executable;
8523
+ }
8524
+ /** stdout+stderr of one invocation, or null when it could not be run at all. */
8525
+ async ask(command, args) {
8526
+ try {
8527
+ const result = await this.options.runner.run(command, args);
8528
+ if (result.timedOut) return null;
8529
+ const text = `${result.stdout ?? ""}
8530
+ ${result.stderr ?? ""}`;
8531
+ return result.code === 0 || text.trim() ? text : null;
8532
+ } catch {
8533
+ return null;
8469
8534
  }
8470
8535
  }
8471
8536
  };
8472
- function stripTiers(description, tiers) {
8473
- const out = {};
8474
- const dropped = new Set(OPTIONAL_DESCRIPTION_FIELD_TIERS.slice(0, tiers).flat());
8475
- for (const [key, value] of Object.entries(description)) {
8476
- if (value === void 0 || dropped.has(key)) continue;
8477
- out[key] = value;
8537
+ function parseAdvertisedFlags(help) {
8538
+ const flags = /* @__PURE__ */ new Set();
8539
+ const pattern = /(?:^|[\s,[(|])(--?[A-Za-z][A-Za-z0-9-]*)(?=$|[\s,=<[\])|.:])/gm;
8540
+ for (const match of help.matchAll(pattern)) {
8541
+ flags.add(match[1]);
8478
8542
  }
8479
- return out;
8543
+ return flags;
8480
8544
  }
8481
- __name(stripTiers, "stripTiers");
8482
- function isFieldRejection(error) {
8483
- return error instanceof FleetClientError && error.kind === "invalid-request" && error.status === 400;
8545
+ __name(parseAdvertisedFlags, "parseAdvertisedFlags");
8546
+ function judgeModelCliHelp(provider, executable, version, help) {
8547
+ const required = MODEL_CLI_REQUIRED_FLAGS[provider];
8548
+ const base = {
8549
+ provider,
8550
+ executable,
8551
+ version,
8552
+ supportedFlags: null,
8553
+ missingRequiredFlags: [],
8554
+ missingConditionalFlags: [],
8555
+ unsupportedOptionalFlags: [],
8556
+ note: null
8557
+ };
8558
+ if (help === null) {
8559
+ return { ...base, note: "the binary did not answer --help; nothing is dropped" };
8560
+ }
8561
+ const advertised = parseAdvertisedFlags(help);
8562
+ if (!required.some((flag) => advertised.has(flag))) {
8563
+ return {
8564
+ ...base,
8565
+ note: `the help text lists none of the flags this node always passes (${required.join(", ")}); its format is not recognised, so nothing is dropped`
8566
+ };
8567
+ }
8568
+ const missing = /* @__PURE__ */ __name((list) => list.filter((flag) => !advertised.has(flag)), "missing");
8569
+ return {
8570
+ ...base,
8571
+ supportedFlags: advertised,
8572
+ missingRequiredFlags: missing(required),
8573
+ missingConditionalFlags: missing(MODEL_CLI_CONDITIONAL_FLAGS[provider]),
8574
+ unsupportedOptionalFlags: missing(MODEL_CLI_OPTIONAL_FLAGS[provider])
8575
+ };
8484
8576
  }
8485
- __name(isFieldRejection, "isFieldRejection");
8577
+ __name(judgeModelCliHelp, "judgeModelCliHelp");
8578
+ function isModelCliCompatible(compat) {
8579
+ return compat.missingRequiredFlags.length === 0;
8580
+ }
8581
+ __name(isModelCliCompatible, "isModelCliCompatible");
8582
+ function describeCliVersions(results) {
8583
+ return results.map((result) => `${result.provider} ${result.version ?? "unknown"}`.slice(0, FLEET_MAX_CLI_VERSION_LENGTH)).slice(0, FLEET_MAX_CLI_VERSIONS);
8584
+ }
8585
+ __name(describeCliVersions, "describeCliVersions");
8586
+ var LEGACY_COMMAND_NAME = {
8587
+ "claude-code": "claude",
8588
+ codex: "codex"
8589
+ };
8590
+ function primaryPinnedCliVersion(results) {
8591
+ const first = results.find((result) => result.version !== null);
8592
+ return first ? `${LEGACY_COMMAND_NAME[first.provider]} ${first.version}`.slice(0, FLEET_MAX_CLI_VERSION_LENGTH) : null;
8593
+ }
8594
+ __name(primaryPinnedCliVersion, "primaryPinnedCliVersion");
8595
+
8596
+ // src/core/worker-loop.ts
8597
+ var import_node_perf_hooks = require("node:perf_hooks");
8486
8598
 
8487
8599
  // src/core/resource-limits.ts
8488
8600
  var ADMIT = { admit: true, reason: null };
@@ -13096,6 +13208,11 @@ async function runModelStep(jobId, execution, workspacePath, mounts, bridgeSpec,
13096
13208
  emptyMcpConfigPath = mcpConfigPath;
13097
13209
  }
13098
13210
  const droppedFlags = await optionalFlagsToDrop(jobId, execution, executable, io);
13211
+ const codexUserSettings = execution.provider === "codex" ? await readOwnerCodexSettings(
13212
+ jobId,
13213
+ io,
13214
+ effectiveCodexHome(io, modelEnvOverlay, execution.envPassthrough, execution.envGrants)
13215
+ ) : void 0;
13099
13216
  const invoke = /* @__PURE__ */ __name(async (instructions, resumeSessionId) => {
13100
13217
  await scratchFs.writeFile(scratch.instructionsPath, instructions);
13101
13218
  let command;
@@ -13108,6 +13225,7 @@ async function runModelStep(jobId, execution, workspacePath, mounts, bridgeSpec,
13108
13225
  ...mounts && mounts.length > 0 ? { mounts } : {},
13109
13226
  ...bridge.cli ? { mcp: bridge.cli } : {},
13110
13227
  ...emptyMcpConfigPath ? { emptyMcpConfigPath } : {},
13228
+ ...codexUserSettings ? { codexUserSettings } : {},
13111
13229
  ...resumeSessionId ? { resumeSessionId } : {},
13112
13230
  ...io.platform ? { platform: io.platform } : {},
13113
13231
  ...streamClaude ? { stream: true } : {},
@@ -13326,6 +13444,32 @@ function resolveRealHomeDir(io) {
13326
13444
  return (0, import_os2.homedir)();
13327
13445
  }
13328
13446
  __name(resolveRealHomeDir, "resolveRealHomeDir");
13447
+ async function readOwnerCodexSettings(jobId, io, codexHome) {
13448
+ const read = io.sessionConfigFs?.readFile?.bind(io.sessionConfigFs);
13449
+ if (!read) return {};
13450
+ try {
13451
+ return readCodexUserSettings(await read((0, import_path4.join)(codexHome, "config.toml")));
13452
+ } catch (error) {
13453
+ io.logger?.warn(
13454
+ `[fleet-node] job ${jobId}: could not read the Codex config to keep its sandbox and model defaults; the run uses Codex's own defaults (still under --ignore-user-config): ${describeContainmentError(error)}`
13455
+ );
13456
+ return {};
13457
+ }
13458
+ }
13459
+ __name(readOwnerCodexSettings, "readOwnerCodexSettings");
13460
+ function effectiveCodexHome(io, envOverlay, envPassthrough, envGrants) {
13461
+ const isCodexHome = /* @__PURE__ */ __name((name) => name.toUpperCase() === "CODEX_HOME", "isCodexHome");
13462
+ const childEnv = buildNodeCheckEnv(
13463
+ envPassthrough?.filter(isCodexHome) ?? null,
13464
+ io.parentEnv,
13465
+ envGrants?.filter(isCodexHome) ?? null,
13466
+ envOverlay
13467
+ );
13468
+ const key = Object.keys(childEnv).find(isCodexHome);
13469
+ const value = key === void 0 ? void 0 : childEnv[key];
13470
+ return typeof value === "string" && value.trim() ? value.trim() : (0, import_path4.join)(resolveRealHomeDir(io), ".codex");
13471
+ }
13472
+ __name(effectiveCodexHome, "effectiveCodexHome");
13329
13473
  function describeContainmentError(error) {
13330
13474
  return error instanceof Error ? error.message : String(error);
13331
13475
  }
@@ -15137,10 +15281,14 @@ var USER_SELECTABLE_WORK_KINDS2 = [
15137
15281
  "repo",
15138
15282
  "app"
15139
15283
  ];
15140
- var WORK_KINDS2 = [...USER_SELECTABLE_WORK_KINDS2, "company", "campaign", "default"];
15284
+ var WORK_KINDS2 = [...USER_SELECTABLE_WORK_KINDS2, "company", "campaign", "project", "default"];
15141
15285
  new Set(WORK_KINDS2);
15142
15286
  var GOAL_KINDS2 = ["metric", "delivery"];
15143
15287
  new Set(GOAL_KINDS2);
15288
+ var RECORD_VISIBILITIES2 = ["private", "team", "organization"];
15289
+ new Set(RECORD_VISIBILITIES2);
15290
+ var RECORD_LIST_VIEWS2 = ["mine", "shared", "organization"];
15291
+ new Set(RECORD_LIST_VIEWS2);
15144
15292
  var VERSION2 = "1.0.0";
15145
15293
  var GTM_SKILLS2 = [
15146
15294
  // ── research ────────────────────────────────────────────────────────
@@ -19451,10 +19599,14 @@ var USER_SELECTABLE_WORK_KINDS3 = [
19451
19599
  "repo",
19452
19600
  "app"
19453
19601
  ];
19454
- var WORK_KINDS3 = [...USER_SELECTABLE_WORK_KINDS3, "company", "campaign", "default"];
19602
+ var WORK_KINDS3 = [...USER_SELECTABLE_WORK_KINDS3, "company", "campaign", "project", "default"];
19455
19603
  new Set(WORK_KINDS3);
19456
19604
  var GOAL_KINDS3 = ["metric", "delivery"];
19457
19605
  new Set(GOAL_KINDS3);
19606
+ var RECORD_VISIBILITIES3 = ["private", "team", "organization"];
19607
+ new Set(RECORD_VISIBILITIES3);
19608
+ var RECORD_LIST_VIEWS3 = ["mine", "shared", "organization"];
19609
+ new Set(RECORD_LIST_VIEWS3);
19458
19610
  var VERSION3 = "1.0.0";
19459
19611
  var GTM_SKILLS3 = [
19460
19612
  // ── research ────────────────────────────────────────────────────────
@@ -23170,14 +23322,47 @@ async function runStart(deps, options) {
23170
23322
  });
23171
23323
  }
23172
23324
  await loop.start();
23173
- if (worker) {
23174
- await worker.start();
23175
- }
23176
- if (attendedWorker) {
23177
- await attendedWorker.start();
23178
- deps.out(
23179
- runtime.attended?.captureBackend ? "Live view enabled \u2014 the owner can watch this machine from the dashboard (screen and terminal)" : "Live view enabled \u2014 terminal only: no browser was found here to show a screen"
23180
- );
23325
+ let shuttingDown = false;
23326
+ const startLanes = /* @__PURE__ */ __name(async () => {
23327
+ if (worker) {
23328
+ await worker.start();
23329
+ }
23330
+ if (attendedWorker) {
23331
+ await attendedWorker.start();
23332
+ deps.out(
23333
+ runtime.attended?.captureBackend ? "Live view enabled \u2014 the owner can watch this machine from the dashboard (screen and terminal)" : "Live view enabled \u2014 terminal only: no browser was found here to show a screen"
23334
+ );
23335
+ }
23336
+ }, "startLanes");
23337
+ let lanesStarted = Promise.resolve();
23338
+ let laneStartFailed = false;
23339
+ let laneStartFailure = void 0;
23340
+ if (worker || attendedWorker) {
23341
+ if (loop.getState().state === "connected") {
23342
+ await startLanes();
23343
+ } else {
23344
+ deps.out(
23345
+ "Waiting for the platform to accept a heartbeat before leasing work (its answer carries this node\u2019s limits and version verdict)"
23346
+ );
23347
+ lanesStarted = startLanesAfterAcceptedHeartbeat(
23348
+ loop,
23349
+ async () => {
23350
+ deps.io.logger.info("Heartbeat accepted \u2014 starting the worker lanes");
23351
+ await startLanes();
23352
+ },
23353
+ () => !shuttingDown
23354
+ ).then((outcome) => {
23355
+ if (!outcome.failed) return;
23356
+ laneStartFailed = true;
23357
+ laneStartFailure = outcome.error;
23358
+ deps.io.logger.error(
23359
+ `Worker lanes failed to start after the first accepted heartbeat: ${describeError4(
23360
+ outcome.error
23361
+ )} \u2014 shutting down`
23362
+ );
23363
+ requestShutdown();
23364
+ });
23365
+ }
23181
23366
  }
23182
23367
  const reaper = worker && runtime.workspaceRoot ? startWorkspaceReaperTimer({
23183
23368
  rootPath: runtime.workspaceRoot,
@@ -23215,8 +23400,10 @@ async function runStart(deps, options) {
23215
23400
  }
23216
23401
  await (deps.waitForShutdown?.() ?? (deps.signals ? signalled : Promise.resolve()));
23217
23402
  reaper?.stop();
23403
+ shuttingDown = true;
23218
23404
  loop.stop();
23219
23405
  await loop.settled();
23406
+ await lanesStarted;
23220
23407
  if (attendedWorker) {
23221
23408
  for (const jobId of attendedWorker.getState().activeJobIds) {
23222
23409
  attendedWorker.cancelJob(jobId, "This machine is shutting down");
@@ -23229,8 +23416,29 @@ async function runStart(deps, options) {
23229
23416
  deps.out(`Worker host stopped (completed ${state.completed}, failed ${state.failed}).`);
23230
23417
  }
23231
23418
  deps.out("Stopped.");
23419
+ if (laneStartFailed) {
23420
+ throw new CliError(
23421
+ `Worker lanes failed to start after the first accepted heartbeat: ${describeError4(laneStartFailure)}`,
23422
+ EXIT_FAILURE
23423
+ );
23424
+ }
23232
23425
  }
23233
23426
  __name(runStart, "runStart");
23427
+ async function startLanesAfterAcceptedHeartbeat(loop, startLanes, shouldWait) {
23428
+ const accepted = await untilHeartbeatAccepted(loop, shouldWait);
23429
+ if (!accepted || !shouldWait()) return { started: false, failed: false };
23430
+ try {
23431
+ await startLanes();
23432
+ return { started: true, failed: false };
23433
+ } catch (error) {
23434
+ return { started: false, failed: true, error };
23435
+ }
23436
+ }
23437
+ __name(startLanesAfterAcceptedHeartbeat, "startLanesAfterAcceptedHeartbeat");
23438
+ function describeError4(error) {
23439
+ return error instanceof Error ? error.message : String(error);
23440
+ }
23441
+ __name(describeError4, "describeError");
23234
23442
  function parseConcurrency(raw) {
23235
23443
  if (raw === void 0) return void 0;
23236
23444
  const parsed = Number.parseInt(raw, 10);
@@ -23897,7 +24105,7 @@ function createLogger(options = {}) {
23897
24105
  __name(createLogger, "createLogger");
23898
24106
 
23899
24107
  // src/version.ts
23900
- var NODE_APP_VERSION = "0.2.3";
24108
+ var NODE_APP_VERSION = "0.2.4";
23901
24109
 
23902
24110
  // src/cli.ts
23903
24111
  function buildDeps() {