@indigoai-us/hq-cli 5.342.0 → 5.342.2
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/CHANGELOG.md +7 -0
- package/dist/commands/lanes.js +24 -7
- package/dist/commands/secrets-public-types.test-d.d.ts +2 -0
- package/dist/commands/secrets-public-types.test-d.js +18 -0
- package/dist/commands/secrets.d.ts +3 -80
- package/dist/commands/secrets.js +5 -458
- package/dist/lib/lanes/envelope-sequence.d.ts +3 -0
- package/dist/lib/lanes/envelope-sequence.js +13 -0
- package/dist/lib/lanes/keep-alive-audit.d.ts +9 -0
- package/dist/lib/lanes/keep-alive-audit.js +8 -3
- package/dist/lib/lanes/reconcile.d.ts +1 -0
- package/dist/lib/lanes/reconcile.js +77 -9
- package/dist/lib/lanes/spawn.js +9 -4
- package/dist/lib/lanes/store.js +10 -0
- package/dist/lib/lanes/types.d.ts +5 -1
- package/dist/lib/lanes/types.js +1 -0
- package/dist/lib/lanes/watcher.js +3 -2
- package/dist/secrets-env-action.d.ts +103 -0
- package/dist/secrets-env-action.js +459 -0
- package/dist/secrets-env-fast-path.js +1 -1
- package/package.json +1 -1
|
@@ -18,7 +18,7 @@ import { AccountResolutionError, LanesError, isLaneVersionConflict } from "./err
|
|
|
18
18
|
import { loadWorkerProfile } from "./profile.js";
|
|
19
19
|
import { terminalChildResumeHireGrantError, withHireGrantAdmission, } from "./grants.js";
|
|
20
20
|
import { classifySessionLiveness, defaultLivenessDeps, loadLivenessIndex, readDaemonPresenceConnected, unindexedClaudeSessionLiveness, } from "./liveness.js";
|
|
21
|
-
import { ANSWERED_QUESTIONS_FILE, armLaneWatcher, envelopeRepairMode, isPidRecordAlive, laneRunDir, readPidRecord, readProcStarttime, readSpawnManifest, markSpawnFailure, stopLaneProcesses, spawnEnvelopeRepair, spawnLaneWorker, retryLaneOnAlternateAccount, } from "./spawn.js";
|
|
21
|
+
import { ANSWERED_QUESTIONS_FILE, armLaneWatcher, envelopeRepairMode, isPidRecordAlive, laneRunDir, nextEnvelopePath, readPidRecord, readProcStarttime, readSpawnManifest, markSpawnFailure, stopLaneProcesses, spawnEnvelopeRepair, spawnLaneWorker, retryLaneOnAlternateAccount, } from "./spawn.js";
|
|
22
22
|
import { appendCappedHistory, lanesStoreRoot, parseLaneRecord, parseQuestionRecord, readLane, readQuestion, listQuestionIds, updateLane, updateQuestion, } from "./store.js";
|
|
23
23
|
import { applyThrottleDecision, decideAdmission, defaultThrottleDeps, emptyThrottleState, evaluateThrottle, isCpuCheckEnabled, readAvailableMemMb, readThrottleState, resolveCpuBusyFraction, sampleCpuBusyObservation, recordCpuBusyObservation, throttleStatePath, writeThrottleState, } from "./throttle.js";
|
|
24
24
|
import { readLanesRegistryFlag, readLanesRegistryFlagFailOff } from "./registry-flag.js";
|
|
@@ -87,6 +87,27 @@ function markDirty(summary, skip) {
|
|
|
87
87
|
summary.ok = false;
|
|
88
88
|
logSkip(skip);
|
|
89
89
|
}
|
|
90
|
+
function isMissingPathError(error) {
|
|
91
|
+
const code = error?.code;
|
|
92
|
+
return code === "ENOENT" || code === "ENOTDIR";
|
|
93
|
+
}
|
|
94
|
+
function recordMissingCodexHomeNote(summary, home) {
|
|
95
|
+
const note = `Codex rollout retention skipped home absent on this host: ${home}`;
|
|
96
|
+
summary.notes ??= [];
|
|
97
|
+
summary.notes.push(note);
|
|
98
|
+
process.stderr.write(`hq lanes reconcile: note: ${note}\n`);
|
|
99
|
+
}
|
|
100
|
+
function homeIsMissingAfterError(home, error) {
|
|
101
|
+
if (!isMissingPathError(error))
|
|
102
|
+
return false;
|
|
103
|
+
try {
|
|
104
|
+
fs.statSync(home);
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
catch (statError) {
|
|
108
|
+
return isMissingPathError(statError);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
90
111
|
function terminalizeModelUnsupported(hqRoot, lane, err) {
|
|
91
112
|
if (!(err instanceof LanesError) || err.error !== "model_unsupported")
|
|
92
113
|
return;
|
|
@@ -940,14 +961,28 @@ export async function runPassBody(hqRoot, deps) {
|
|
|
940
961
|
companyRefs.set(company.uid ?? company.slug, company);
|
|
941
962
|
}
|
|
942
963
|
const readFlag = deps.readRegistryFlag ?? readLanesRegistryFlag;
|
|
943
|
-
const
|
|
964
|
+
const configuredHomes = configuredCodexRolloutHomes(hqRoot, config, deps.host.hostname);
|
|
944
965
|
let retentionEnabled = false;
|
|
945
|
-
for (const company of
|
|
966
|
+
for (const company of configuredHomes.length > 0 ? companyRefs.values() : []) {
|
|
946
967
|
retentionEnabled ||= await readFlag("lanes.codex-rollout-retention", { company });
|
|
947
968
|
if (retentionEnabled)
|
|
948
969
|
break;
|
|
949
970
|
}
|
|
950
971
|
if (retentionEnabled) {
|
|
972
|
+
const presentHomes = [];
|
|
973
|
+
for (const home of configuredHomes) {
|
|
974
|
+
try {
|
|
975
|
+
fs.statSync(home);
|
|
976
|
+
presentHomes.push(home);
|
|
977
|
+
}
|
|
978
|
+
catch (err) {
|
|
979
|
+
if (isMissingPathError(err)) {
|
|
980
|
+
recordMissingCodexHomeNote(summary, home);
|
|
981
|
+
continue;
|
|
982
|
+
}
|
|
983
|
+
throw err;
|
|
984
|
+
}
|
|
985
|
+
}
|
|
951
986
|
const statePath = path.join(hqRoot, "workspace", "lanes-runs", `.rollout-retention-${deps.host.hostname.replaceAll(/[^a-zA-Z0-9_-]/g, "_")}.json`);
|
|
952
987
|
let lastRunAt;
|
|
953
988
|
try {
|
|
@@ -961,20 +996,38 @@ export async function runPassBody(hqRoot, deps) {
|
|
|
961
996
|
if (err.code !== "ENOENT")
|
|
962
997
|
throw err;
|
|
963
998
|
}
|
|
964
|
-
|
|
999
|
+
let lowDisk = false;
|
|
1000
|
+
const retentionHomes = [];
|
|
1001
|
+
for (const home of presentHomes) {
|
|
1002
|
+
try {
|
|
1003
|
+
const disk = checkLaneDiskSpace(hqRoot, home, deps.spawnLaneDeps?.statfs, config.low_disk_min_free_bytes);
|
|
1004
|
+
lowDisk = lowDisk || disk.low;
|
|
1005
|
+
retentionHomes.push(home);
|
|
1006
|
+
}
|
|
1007
|
+
catch (err) {
|
|
1008
|
+
if (homeIsMissingAfterError(home, err)) {
|
|
1009
|
+
recordMissingCodexHomeNote(summary, home);
|
|
1010
|
+
continue;
|
|
1011
|
+
}
|
|
1012
|
+
throw err;
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
965
1015
|
if (lowDisk || lastRunAt === undefined || nowMs - lastRunAt >= 60 * 60 * 1000) {
|
|
966
1016
|
const result = await retainCodexRollouts({
|
|
967
|
-
homes,
|
|
1017
|
+
homes: retentionHomes,
|
|
968
1018
|
lanes,
|
|
969
1019
|
nowMs,
|
|
1020
|
+
dryRun: deps.dryRun,
|
|
970
1021
|
compressAfterMs: config.codex_rollout_compress_after_hours * 60 * 60 * 1000,
|
|
971
1022
|
deleteAfterMs: config.codex_rollout_delete_after_days * 24 * 60 * 60 * 1000,
|
|
972
1023
|
});
|
|
973
1024
|
summary.rollout_retention = result;
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
1025
|
+
if (!deps.dryRun) {
|
|
1026
|
+
fs.mkdirSync(path.dirname(statePath), { recursive: true });
|
|
1027
|
+
const tempPath = `${statePath}.${process.pid}.tmp`;
|
|
1028
|
+
fs.writeFileSync(tempPath, `${JSON.stringify({ last_run_at: nowIso }, null, 2)}\n`, { flag: "wx" });
|
|
1029
|
+
fs.renameSync(tempPath, statePath);
|
|
1030
|
+
}
|
|
978
1031
|
}
|
|
979
1032
|
}
|
|
980
1033
|
}
|
|
@@ -1848,6 +1901,9 @@ async function admitQueued(hqRoot, lanes, summary, deps, now, onlyLaneIds, exclu
|
|
|
1848
1901
|
}
|
|
1849
1902
|
try {
|
|
1850
1903
|
if (deps.dryRun) {
|
|
1904
|
+
// Spawn reserves an envelope path. Probe it without writing so a dry
|
|
1905
|
+
// run does not promise admission when the run cannot start.
|
|
1906
|
+
nextEnvelopePath(laneRunDir(hqRoot, lane.lane_id));
|
|
1851
1907
|
summary.admitted.push({
|
|
1852
1908
|
id: lane.lane_id,
|
|
1853
1909
|
from: "queued",
|
|
@@ -1907,6 +1963,18 @@ async function admitQueued(hqRoot, lanes, summary, deps, now, onlyLaneIds, exclu
|
|
|
1907
1963
|
}
|
|
1908
1964
|
}
|
|
1909
1965
|
terminalizeModelUnsupported(hqRoot, lane, err);
|
|
1966
|
+
if (err instanceof LanesError && err.error !== "model_unsupported") {
|
|
1967
|
+
const queueRecorded = deps.dryRun || casLaneIf(hqRoot, lane.lane_id, (current) => current.state === "queued", (current) => ({
|
|
1968
|
+
...current,
|
|
1969
|
+
queued_reason: "admit_failed",
|
|
1970
|
+
queued_details: { reason: "admit_failed", error: err.error, message: err.message },
|
|
1971
|
+
admission_reserved_at: undefined,
|
|
1972
|
+
}), false).changed;
|
|
1973
|
+
if (queueRecorded) {
|
|
1974
|
+
summary.skipped.push({ kind: "lane", id: lane.lane_id, why: `admit_failed:${err.error}` });
|
|
1975
|
+
continue;
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1910
1978
|
markDirty(summary, {
|
|
1911
1979
|
kind: "lane",
|
|
1912
1980
|
id: lane.lane_id,
|
package/dist/lib/lanes/spawn.js
CHANGED
|
@@ -983,16 +983,21 @@ function proveDetachedViaPs(pid) {
|
|
|
983
983
|
}
|
|
984
984
|
export function nextEnvelopePath(runDir) {
|
|
985
985
|
const dir = path.join(runDir, "envelopes");
|
|
986
|
+
let names = new Set();
|
|
987
|
+
try {
|
|
988
|
+
names = new Set(fs.readdirSync(dir));
|
|
989
|
+
}
|
|
990
|
+
catch (err) {
|
|
991
|
+
if (err.code !== "ENOENT")
|
|
992
|
+
throw err;
|
|
993
|
+
}
|
|
986
994
|
let n = 1;
|
|
987
995
|
while (true) {
|
|
988
996
|
const name = `${String(n).padStart(3, "0")}.json`;
|
|
989
997
|
const filePath = path.join(dir, name);
|
|
990
|
-
if (!
|
|
998
|
+
if (!names.has(name))
|
|
991
999
|
return filePath;
|
|
992
1000
|
n += 1;
|
|
993
|
-
if (n > 999) {
|
|
994
|
-
throw new LanesError("envelope_sequence_exhausted", `No free envelope slot under ${dir}`, { dir });
|
|
995
|
-
}
|
|
996
1001
|
}
|
|
997
1002
|
}
|
|
998
1003
|
export function envelopeInstruction(envelopePath) {
|
package/dist/lib/lanes/store.js
CHANGED
|
@@ -623,6 +623,16 @@ export function parseLaneRecord(raw, filePath) {
|
|
|
623
623
|
}
|
|
624
624
|
requireTimestamp(detailsRecord.measured_at, "queued_details.measured_at", filePath);
|
|
625
625
|
}
|
|
626
|
+
else if (detailsRecord.reason === "admit_failed") {
|
|
627
|
+
if (raw.state !== "queued" || raw.queued_reason !== "admit_failed" ||
|
|
628
|
+
typeof detailsRecord.error !== "string" || detailsRecord.error.trim() === "" ||
|
|
629
|
+
typeof detailsRecord.message !== "string" || detailsRecord.message.trim() === "") {
|
|
630
|
+
throw new LaneSchemaError("queued_details must describe a failed lane admission", {
|
|
631
|
+
path: filePath,
|
|
632
|
+
field: "queued_details",
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
}
|
|
626
636
|
else {
|
|
627
637
|
throw new LaneSchemaError("queued_details must describe a supported queue condition", {
|
|
628
638
|
path: filePath,
|
|
@@ -177,7 +177,7 @@ export declare const TERMINAL_LANE_STATES: readonly ["done", "failed", "interrup
|
|
|
177
177
|
export type TerminalLaneState = (typeof TERMINAL_LANE_STATES)[number];
|
|
178
178
|
export declare const LANE_PROVIDERS: readonly ["claude", "codex", "grok"];
|
|
179
179
|
export type LaneProvider = (typeof LANE_PROVIDERS)[number];
|
|
180
|
-
export declare const QUEUED_REASONS: readonly ["cap", "provider_cap", "account_cap", "profile_cap", "queue_position", "throttle", "provider_binary_missing", "account_retry", "worker_profile_changed", "low_disk"];
|
|
180
|
+
export declare const QUEUED_REASONS: readonly ["cap", "provider_cap", "account_cap", "profile_cap", "queue_position", "throttle", "provider_binary_missing", "account_retry", "worker_profile_changed", "low_disk", "admit_failed"];
|
|
181
181
|
export type QueuedReason = (typeof QUEUED_REASONS)[number];
|
|
182
182
|
export type QueuedDetails = {
|
|
183
183
|
reason: "worker_profile_changed";
|
|
@@ -193,6 +193,10 @@ export type QueuedDetails = {
|
|
|
193
193
|
hq_free_bytes: number;
|
|
194
194
|
provider_free_bytes: number;
|
|
195
195
|
measured_at: string;
|
|
196
|
+
} | {
|
|
197
|
+
reason: "admit_failed";
|
|
198
|
+
error: string;
|
|
199
|
+
message: string;
|
|
196
200
|
};
|
|
197
201
|
export declare function isQueuedReason(value: unknown): value is QueuedReason;
|
|
198
202
|
export declare const HOST_KINDS: readonly ["local", "outpost"];
|
package/dist/lib/lanes/types.js
CHANGED
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
import * as fs from "node:fs";
|
|
35
35
|
import * as path from "node:path";
|
|
36
36
|
import { LanesError } from "./errors.js";
|
|
37
|
+
import { compareEnvelopeNames } from "./envelope-sequence.js";
|
|
37
38
|
import { questionRecordRefusalCode, raiseEnvelopeQuestions, resolveQuestionCompanyWithFallback, } from "./questions.js";
|
|
38
39
|
import { isTerminalLaneState, ENVELOPE_INVALID_REASON, parseLaneEnvelope, STORY_STATUSES, LANE_PROVIDERS, } from "./types.js";
|
|
39
40
|
import { laneStories, terminalEnvelopeStoryStatus } from "./stories.js";
|
|
@@ -619,8 +620,8 @@ export function readLatestEnvelopeFile(hqRoot, laneId, minName) {
|
|
|
619
620
|
names = fs
|
|
620
621
|
.readdirSync(dir)
|
|
621
622
|
.filter((name) => name.endsWith(".json"))
|
|
622
|
-
.filter((name) => minName === undefined || name >=
|
|
623
|
-
.sort();
|
|
623
|
+
.filter((name) => minName === undefined || compareEnvelopeNames(name, minName) >= 0)
|
|
624
|
+
.sort(compareEnvelopeNames);
|
|
624
625
|
}
|
|
625
626
|
catch (err) {
|
|
626
627
|
const code = err.code;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { writeSync } from "node:fs";
|
|
2
|
+
/**
|
|
3
|
+
* A browser login is safe only when a person can answer it. This mirrors the
|
|
4
|
+
* repository's existing terminal convention in `canOfferTeamUpgrade`: both
|
|
5
|
+
* stdin and stderr must be TTYs, and CI is always non-interactive.
|
|
6
|
+
*/
|
|
7
|
+
export declare function secretsCredentialOptions(): {
|
|
8
|
+
interactive: boolean;
|
|
9
|
+
};
|
|
10
|
+
export interface SecretsScopeOpts {
|
|
11
|
+
company?: string;
|
|
12
|
+
personal?: boolean;
|
|
13
|
+
}
|
|
14
|
+
export declare function scopeOpts(opts: SecretsScopeOpts): {
|
|
15
|
+
personal: boolean;
|
|
16
|
+
companySlug: string | undefined;
|
|
17
|
+
};
|
|
18
|
+
export type SecretTier = "standard" | "sensitive" | "nuclear";
|
|
19
|
+
export type SecretScriptLockMode = "off" | "enforced";
|
|
20
|
+
export type SecretUsageChannel = "run" | "exec" | "env" | "sandbox" | "reveal" | "submit-link";
|
|
21
|
+
export interface SecretScriptUsage {
|
|
22
|
+
scriptId: string;
|
|
23
|
+
path: string;
|
|
24
|
+
sha256: string;
|
|
25
|
+
attestationLevel: string;
|
|
26
|
+
}
|
|
27
|
+
export interface SecretUsage {
|
|
28
|
+
channel: SecretUsageChannel;
|
|
29
|
+
reason?: string;
|
|
30
|
+
script?: SecretScriptUsage;
|
|
31
|
+
}
|
|
32
|
+
export interface SecretMetadata {
|
|
33
|
+
tier?: SecretTier;
|
|
34
|
+
scriptLock?: {
|
|
35
|
+
mode?: SecretScriptLockMode;
|
|
36
|
+
requiredAttestation?: string;
|
|
37
|
+
};
|
|
38
|
+
cacheTtlMs?: number;
|
|
39
|
+
revokedAt?: string | null;
|
|
40
|
+
}
|
|
41
|
+
export interface SecretLoadSuccessRow extends SecretMetadata {
|
|
42
|
+
name: string;
|
|
43
|
+
value?: string;
|
|
44
|
+
version?: number;
|
|
45
|
+
}
|
|
46
|
+
export interface SecretLoadResponse {
|
|
47
|
+
secrets: SecretLoadSuccessRow[];
|
|
48
|
+
errors: Array<{
|
|
49
|
+
name: string;
|
|
50
|
+
code: string;
|
|
51
|
+
message?: string;
|
|
52
|
+
}>;
|
|
53
|
+
}
|
|
54
|
+
export declare function highSecuritySandboxOnlyMessage(secretName?: string): string;
|
|
55
|
+
export declare function normalizeSecretTier(tier?: string): SecretTier;
|
|
56
|
+
export declare function normalizeScriptLockMode(mode?: string): SecretScriptLockMode;
|
|
57
|
+
export declare function extractApiMessage(body: Record<string, unknown>, fallback: string): string;
|
|
58
|
+
export declare function buildSecretUsage(channel: SecretUsageChannel, scriptPath?: string, scriptId?: string, attestationLevel?: string): Promise<SecretUsage>;
|
|
59
|
+
export declare function parseSecretNameList(input: string[] | undefined): string[];
|
|
60
|
+
export declare function loadRevealedSecrets(token: string, companyUid: string, keys: string[], usage?: SecretUsage, options?: {
|
|
61
|
+
allowOfflineFallback?: boolean;
|
|
62
|
+
}): Promise<Map<string, string>>;
|
|
63
|
+
/**
|
|
64
|
+
* Emit `hq secrets env` output and terminate deterministically.
|
|
65
|
+
*
|
|
66
|
+
* The process MUST exit even when the consuming pipe has gone away. The
|
|
67
|
+
* orphaned-process leak (personal/projects/hq-secrets-process-leak) was caused
|
|
68
|
+
* by relying on `process.stdout.write(payload, () => process.exit(0))`: when the
|
|
69
|
+
* parent shell reading `source <(hq secrets env …)` is killed on timeout, the
|
|
70
|
+
* stdout pipe loses its reader, the write callback never fires, `process.exit(0)`
|
|
71
|
+
* is never reached, and — because `env` forces no other exit — the process parks
|
|
72
|
+
* in `epoll_pwait(-1)` forever, reparented to PID 1. Hundreds accumulated this
|
|
73
|
+
* way, exhausting swap and driving load into triple digits.
|
|
74
|
+
*
|
|
75
|
+
* Writing synchronously to fd 1 and then exiting unconditionally removes every
|
|
76
|
+
* dependency on the event loop draining and on the reader still being attached:
|
|
77
|
+
* - a live reader receives the export lines (synchronous write reaches the
|
|
78
|
+
* kernel pipe buffer before we exit, so nothing is truncated);
|
|
79
|
+
* - a dead reader surfaces as EPIPE, which we swallow — the secret is already
|
|
80
|
+
* resolved and there is nothing left to deliver, so a clean exit is correct;
|
|
81
|
+
* - EAGAIN (stdout in non-blocking mode with a momentarily full buffer) is
|
|
82
|
+
* retried a bounded number of times. `env` payloads are a few short export
|
|
83
|
+
* lines, far under the 64 KiB pipe buffer, so this effectively never spins.
|
|
84
|
+
*
|
|
85
|
+
* `exit` and `write` are injectable for tests; production uses the real ones.
|
|
86
|
+
*/
|
|
87
|
+
export declare function emitEnvExportsAndExit(payload: string, exit?: (code: number) => never, write?: typeof writeSync): never;
|
|
88
|
+
/**
|
|
89
|
+
* Shared `secrets env` action for Commander and the default-off lightweight
|
|
90
|
+
* entrypoint. The fast entrypoint still runs the same credential, usage-audit,
|
|
91
|
+
* server authorization, cache-fallback, redaction, and output code.
|
|
92
|
+
*/
|
|
93
|
+
export declare function runSecretsEnvCommand(opts: {
|
|
94
|
+
only: string[];
|
|
95
|
+
script?: string;
|
|
96
|
+
scriptId?: string;
|
|
97
|
+
}, parentScope: {
|
|
98
|
+
company?: string;
|
|
99
|
+
personal?: boolean;
|
|
100
|
+
}, options?: {
|
|
101
|
+
fallbackOnAuthFailure?: boolean;
|
|
102
|
+
}): Promise<"handled" | "fallback">;
|
|
103
|
+
//# sourceMappingURL=secrets-env-action.d.ts.map
|