@indigoai-us/hq-cli 5.240.0 → 5.242.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +38 -0
- package/dist/command-catalog.generated.d.ts +2 -2
- package/dist/command-catalog.generated.js +2 -2
- package/dist/commands/agent-enroll.d.ts +8 -0
- package/dist/commands/agent-enroll.js +19 -2
- package/dist/commands/lanes.d.ts +0 -6
- package/dist/commands/lanes.js +30 -63
- package/dist/lib/agent-kit/creds.d.ts +30 -2
- package/dist/lib/agent-kit/creds.js +75 -6
- package/dist/lib/lanes/claude-balance.d.ts +2 -0
- package/dist/lib/lanes/claude-balance.js +2 -1
- package/dist/lib/lanes/dropbox.d.ts +1 -6
- package/dist/lib/lanes/dropbox.js +6 -11
- package/dist/lib/lanes/message-replay.d.ts +0 -1
- package/dist/lib/lanes/message-replay.js +0 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,44 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.242.0] — 2026-09-27
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- Removed the completed `lanes.message-compaction-replay`,
|
|
10
|
+
`lanes.outpost-question-visibility`, and
|
|
11
|
+
`lanes.host-scoped-account-homes` gates. Their shipped behaviors now run
|
|
12
|
+
unconditionally.
|
|
13
|
+
- Account-home login and `set-home` no longer require an unrelated active
|
|
14
|
+
company selection when `--company` is omitted; explicit `--company` values
|
|
15
|
+
remain validated.
|
|
16
|
+
|
|
17
|
+
## [5.241.0] — 2026-09-27
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- A bot whose only persistent storage is a network share can now enroll.
|
|
22
|
+
Enrollment wrote its credentials and then locked them to the owner, and on
|
|
23
|
+
a CIFS/SMB share mounted with a fixed file mode — Azure Files is the one
|
|
24
|
+
that found this — that last step fails outright. Enrollment died there,
|
|
25
|
+
left a temp file holding the private key behind, and offered no way
|
|
26
|
+
forward: the only writable alternative was storage a container rebuild
|
|
27
|
+
erases.
|
|
28
|
+
|
|
29
|
+
The write now reports whether owner-only permissions actually took, rather
|
|
30
|
+
than assuming they did, and `hq agent enroll` says plainly when they did
|
|
31
|
+
not: the credential is readable by anything else with access to that
|
|
32
|
+
share. The file is still requested owner-only, the result is read back
|
|
33
|
+
from the filesystem instead of trusted, a refused chmod on ordinary
|
|
34
|
+
storage still fails loudly, and a failed write no longer leaves the secret
|
|
35
|
+
behind in a temp file.
|
|
36
|
+
- Resume can keep a lane on its recorded account when it uses a CLI-selected
|
|
37
|
+
model, even if that account's configured default differs. A lane without an
|
|
38
|
+
account allowlist may use any otherwise eligible account.
|
|
39
|
+
- With a worker-profile account pin, resume keeps a healthy recorded account
|
|
40
|
+
that is allowed by the pin even when the pin includes other accounts and the
|
|
41
|
+
recorded account's default differs from the lane's CLI-selected model.
|
|
42
|
+
|
|
5
43
|
## [5.240.0] — 2026-09-27
|
|
6
44
|
|
|
7
45
|
### Added
|
|
@@ -8447,7 +8447,7 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
8447
8447
|
readonly description: "Host key (defaults to this host's hostname)";
|
|
8448
8448
|
}, {
|
|
8449
8449
|
readonly flags: "--company <slug|uid>";
|
|
8450
|
-
readonly description: "Company scope for
|
|
8450
|
+
readonly description: "Company scope for account-home validation";
|
|
8451
8451
|
}, {
|
|
8452
8452
|
readonly flags: "--json";
|
|
8453
8453
|
readonly description: "JSON on stdout (always)";
|
|
@@ -8473,7 +8473,7 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
8473
8473
|
readonly description: "Account home directory";
|
|
8474
8474
|
}, {
|
|
8475
8475
|
readonly flags: "--company <slug|uid>";
|
|
8476
|
-
readonly description: "Company scope for
|
|
8476
|
+
readonly description: "Company scope for account-home validation";
|
|
8477
8477
|
}, {
|
|
8478
8478
|
readonly flags: "--device-auth";
|
|
8479
8479
|
readonly description: "Use Codex device authentication instead of localhost callback";
|
|
@@ -10929,7 +10929,7 @@ export const COMMAND_CATALOG = [
|
|
|
10929
10929
|
},
|
|
10930
10930
|
{
|
|
10931
10931
|
"flags": "--company <slug|uid>",
|
|
10932
|
-
"description": "Company scope for
|
|
10932
|
+
"description": "Company scope for account-home validation"
|
|
10933
10933
|
},
|
|
10934
10934
|
{
|
|
10935
10935
|
"flags": "--json",
|
|
@@ -10963,7 +10963,7 @@ export const COMMAND_CATALOG = [
|
|
|
10963
10963
|
},
|
|
10964
10964
|
{
|
|
10965
10965
|
"flags": "--company <slug|uid>",
|
|
10966
|
-
"description": "Company scope for
|
|
10966
|
+
"description": "Company scope for account-home validation"
|
|
10967
10967
|
},
|
|
10968
10968
|
{
|
|
10969
10969
|
"flags": "--device-auth",
|
|
@@ -129,6 +129,14 @@ export interface EnrollResult {
|
|
|
129
129
|
companyMismatch?: string;
|
|
130
130
|
/** True when this enrolled beside a person's session in its own directory. */
|
|
131
131
|
local: boolean;
|
|
132
|
+
/**
|
|
133
|
+
* False when the filesystem could not make the credential owner-only — a
|
|
134
|
+
* share mounted with a fixed file_mode. The identity works; anything else
|
|
135
|
+
* with access to that share can read it.
|
|
136
|
+
*/
|
|
137
|
+
permissionsEnforced: boolean;
|
|
138
|
+
/** errno of the refused chmod, when there was one (EPERM on Azure Files). */
|
|
139
|
+
permissionsError?: string;
|
|
132
140
|
/** The tree this identity lives in. */
|
|
133
141
|
agentDir: string;
|
|
134
142
|
}
|
|
@@ -252,7 +252,7 @@ export async function enrollHost(opts, deps = {}) {
|
|
|
252
252
|
// server can bind the code to it, and a failed call leaves a harmless
|
|
253
253
|
// unenrolled key behind (regenerated on the next attempt).
|
|
254
254
|
const pair = (deps.generateKeyPair ?? generateHostKeyPair)();
|
|
255
|
-
writeHostKeyPair(paths, pair);
|
|
255
|
+
const keyWrite = writeHostKeyPair(paths, pair);
|
|
256
256
|
let res;
|
|
257
257
|
try {
|
|
258
258
|
res = await doFetch(`${apiBaseUrl}${ENROLL_PATH}`, {
|
|
@@ -290,8 +290,12 @@ export async function enrollHost(opts, deps = {}) {
|
|
|
290
290
|
companySlug: parsed.companySlug,
|
|
291
291
|
apiBaseUrl,
|
|
292
292
|
};
|
|
293
|
-
writeMachineCreds(paths, creds);
|
|
293
|
+
const credsWrite = writeMachineCreds(paths, creds);
|
|
294
|
+
const enforced = keyWrite.enforced && credsWrite.enforced;
|
|
295
|
+
const permissionsError = keyWrite.chmodError ?? credsWrite.chmodError;
|
|
294
296
|
return {
|
|
297
|
+
permissionsEnforced: enforced,
|
|
298
|
+
...(permissionsError ? { permissionsError } : {}),
|
|
295
299
|
local: target.local,
|
|
296
300
|
agentDir: paths.agentDir,
|
|
297
301
|
agentUid: parsed.agentUid,
|
|
@@ -305,6 +309,10 @@ export async function enrollHost(opts, deps = {}) {
|
|
|
305
309
|
: {}),
|
|
306
310
|
};
|
|
307
311
|
}
|
|
312
|
+
/** Directory label for the permissions note — never the credential itself. */
|
|
313
|
+
function paths_agentDirLabel() {
|
|
314
|
+
return agentKitPaths(os.homedir(), process.env).agentDir;
|
|
315
|
+
}
|
|
308
316
|
export function registerAgentEnrollCommand(agent) {
|
|
309
317
|
agent
|
|
310
318
|
.command("enroll <code>")
|
|
@@ -330,6 +338,15 @@ export function registerAgentEnrollCommand(agent) {
|
|
|
330
338
|
` directory beside them: ${result.agentDir}. Their session was not read or changed.\n` +
|
|
331
339
|
` Every hq agent command finds this tree on its own — no environment to set.`));
|
|
332
340
|
}
|
|
341
|
+
if (!result.permissionsEnforced) {
|
|
342
|
+
// Not a warning to bury: the credential is on disk and working,
|
|
343
|
+
// but this filesystem cannot keep it to one owner.
|
|
344
|
+
console.log(chalk.yellow(` note: this filesystem could not make the credential owner-only` +
|
|
345
|
+
`${result.permissionsError ? ` (chmod: ${result.permissionsError})` : ""}.\n` +
|
|
346
|
+
` Anything with access to ${paths_agentDirLabel()} can read it. That is\n` +
|
|
347
|
+
` usually a network share mounted with a fixed file_mode. Keep the\n` +
|
|
348
|
+
` share itself restricted, or move the identity to local storage.`));
|
|
349
|
+
}
|
|
333
350
|
console.log(` host fingerprint: ${result.hostFingerprint}`);
|
|
334
351
|
console.log(` credentials: ${result.credsPath} (0600)`);
|
|
335
352
|
console.log(` host key: ${result.hostKeyPath} (0600)`);
|
package/dist/commands/lanes.d.ts
CHANGED
|
@@ -91,18 +91,12 @@ export interface LanesCommandDeps {
|
|
|
91
91
|
isSsmOwnerDispatch?: () => boolean;
|
|
92
92
|
/** Test seam for changing a lane between pre-lock and locked grant reads. */
|
|
93
93
|
withGrantHireLocks?: typeof withLaneHireLocks;
|
|
94
|
-
/** Test seam for the default-off Outpost question visibility gate. */
|
|
95
|
-
laneOutpostQuestionVisibilityEnabled?: (company?: LaneCompanyRef | null) => boolean | Promise<boolean>;
|
|
96
94
|
/** Test seam for the hq-flags gated tracker commands. */
|
|
97
95
|
laneTrackerEnabled?: (company: LaneCompanyRef) => boolean | Promise<boolean>;
|
|
98
|
-
/** Test seam for the default-off host-scoped account-home writer gate. */
|
|
99
|
-
laneHostScopedAccountHomesEnabled?: (company?: LaneCompanyRef | null) => boolean | Promise<boolean>;
|
|
100
96
|
/** Test seam for recording approvals against the active identity. */
|
|
101
97
|
trackerActor?: () => string | null;
|
|
102
98
|
}
|
|
103
99
|
export declare const LANE_OUTPOST_LOAD_BALANCING_FLAG = "lanes.outpost-load-balancing";
|
|
104
|
-
export declare const LANE_OUTPOST_QUESTION_VISIBILITY_FLAG = "lanes.outpost-question-visibility";
|
|
105
|
-
export declare const LANE_HOST_SCOPED_ACCOUNT_HOMES_FLAG = "lanes.host-scoped-account-homes";
|
|
106
100
|
export declare function parseSeniorRef(raw: string): SeniorRef;
|
|
107
101
|
export declare function resolveAnswerChoice(choice: string, options: string[], freeform: boolean): string;
|
|
108
102
|
export declare function parseQuestionBody(raw: unknown): QuestionBody | null;
|
package/dist/commands/lanes.js
CHANGED
|
@@ -10,7 +10,6 @@ import { randomBytes } from "node:crypto";
|
|
|
10
10
|
import * as fs from "node:fs";
|
|
11
11
|
import * as os from "node:os";
|
|
12
12
|
import * as path from "node:path";
|
|
13
|
-
import { readLocalOverride } from "@indigoai-us/hq-flags-client";
|
|
14
13
|
import { Option } from "commander";
|
|
15
14
|
import { registerOutpostPrunerCommand } from "./outpost-pruner.js";
|
|
16
15
|
import { registerLaneChecksCommand } from "./lanes-checks.js";
|
|
@@ -72,29 +71,12 @@ const VENDOR_LOGIN = {
|
|
|
72
71
|
};
|
|
73
72
|
export { emitLaneDecisionEvents };
|
|
74
73
|
export const LANE_OUTPOST_LOAD_BALANCING_FLAG = "lanes.outpost-load-balancing";
|
|
75
|
-
export const LANE_OUTPOST_QUESTION_VISIBILITY_FLAG = "lanes.outpost-question-visibility";
|
|
76
|
-
export const LANE_HOST_SCOPED_ACCOUNT_HOMES_FLAG = "lanes.host-scoped-account-homes";
|
|
77
74
|
function defaultLanePlacementEnabled() {
|
|
78
75
|
return resolveProcessFlagGate(LANE_OUTPOST_LOAD_BALANCING_FLAG, { fallback: false }, () => false);
|
|
79
76
|
}
|
|
80
77
|
function defaultLaneTrackerEnabled(company) {
|
|
81
78
|
return readLanesRegistryFlag(LANES_TRACKER_FLAG, { company });
|
|
82
79
|
}
|
|
83
|
-
function defaultLaneOutpostQuestionVisibilityEnabled(company, env) {
|
|
84
|
-
return readLanesRegistryFlag(LANE_OUTPOST_QUESTION_VISIBILITY_FLAG, {
|
|
85
|
-
company,
|
|
86
|
-
env,
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
|
-
function defaultLaneHostScopedAccountHomesEnabled(company, env) {
|
|
90
|
-
return readLanesRegistryFlag(LANE_HOST_SCOPED_ACCOUNT_HOMES_FLAG, { company, env });
|
|
91
|
-
}
|
|
92
|
-
async function hostScopedAccountHomesEnabled(ctx, company) {
|
|
93
|
-
const localOverride = readLocalOverride(LANE_HOST_SCOPED_ACCOUNT_HOMES_FLAG, ctx.env);
|
|
94
|
-
if (localOverride !== null)
|
|
95
|
-
return localOverride;
|
|
96
|
-
return ctx.laneHostScopedAccountHomesEnabled(company ?? await ctx.resolveCompany());
|
|
97
|
-
}
|
|
98
80
|
async function resolveSelectedAccountHomeCompany(ctx, selector) {
|
|
99
81
|
const company = await ctx.resolveActiveCompany(selector);
|
|
100
82
|
if (!company) {
|
|
@@ -312,11 +294,7 @@ function resolveDeps(deps = {}) {
|
|
|
312
294
|
storyLedgerEnabled: deps.storyLedgerEnabled ?? laneStoryLedgerEnabled,
|
|
313
295
|
isSsmOwnerDispatch: deps.isSsmOwnerDispatch ?? (() => isSsmRunCommandProcess()),
|
|
314
296
|
withGrantHireLocks: deps.withGrantHireLocks ?? withLaneHireLocks,
|
|
315
|
-
laneOutpostQuestionVisibilityEnabled: deps.laneOutpostQuestionVisibilityEnabled ??
|
|
316
|
-
((company) => defaultLaneOutpostQuestionVisibilityEnabled(company, env)),
|
|
317
297
|
laneTrackerEnabled: deps.laneTrackerEnabled ?? defaultLaneTrackerEnabled,
|
|
318
|
-
laneHostScopedAccountHomesEnabled: deps.laneHostScopedAccountHomesEnabled ??
|
|
319
|
-
((company) => defaultLaneHostScopedAccountHomesEnabled(company, env)),
|
|
320
298
|
trackerActor: deps.trackerActor ?? defaultTrackerActor,
|
|
321
299
|
};
|
|
322
300
|
}
|
|
@@ -5921,6 +5899,10 @@ async function selectAutomaticClaudeResumeAccount(ctx, lane, workerProfile, opti
|
|
|
5921
5899
|
: lane.model_source === undefined || lane.model_source === "cli" || lane.model_source === "preserved"
|
|
5922
5900
|
? lane.model
|
|
5923
5901
|
: undefined;
|
|
5902
|
+
const recordedModelAccount = lane.account !== undefined &&
|
|
5903
|
+
(lane.model_source === "cli" || lane.model_source === "preserved")
|
|
5904
|
+
? lane.account
|
|
5905
|
+
: undefined;
|
|
5924
5906
|
const preservedModel = repairUnavailableAccount && lane.model_source === "account"
|
|
5925
5907
|
? lane.model ?? currentConfig?.model
|
|
5926
5908
|
: undefined;
|
|
@@ -5944,7 +5926,8 @@ async function selectAutomaticClaudeResumeAccount(ctx, lane, workerProfile, opti
|
|
|
5944
5926
|
currentConfig?.enabled === true &&
|
|
5945
5927
|
currentTargetAccount?.enabled === true &&
|
|
5946
5928
|
currentTargetAccount.logged_in === true &&
|
|
5947
|
-
(model === undefined || currentConfig.model === undefined || currentConfig.model === model
|
|
5929
|
+
(model === undefined || currentConfig.model === undefined || currentConfig.model === model ||
|
|
5930
|
+
lane.account === recordedModelAccount))
|
|
5948
5931
|
return undefined;
|
|
5949
5932
|
const selected = selectClaudeAccount({
|
|
5950
5933
|
hosts: balanceHosts.snapshots,
|
|
@@ -5952,6 +5935,7 @@ async function selectAutomaticClaudeResumeAccount(ctx, lane, workerProfile, opti
|
|
|
5952
5935
|
config,
|
|
5953
5936
|
...(!repairUnavailableAccount && allowedAccounts !== undefined ? { allowedAccounts } : {}),
|
|
5954
5937
|
...(model !== undefined ? { model } : {}),
|
|
5938
|
+
...(recordedModelAccount !== undefined ? { recordedModelAccount } : {}),
|
|
5955
5939
|
unavailableHosts: balanceHosts.unavailableHosts,
|
|
5956
5940
|
});
|
|
5957
5941
|
const historyReason = lane.account === undefined
|
|
@@ -8254,19 +8238,15 @@ async function loadPrimaryOwnedOutpostQuestions(ctx, hqRoot, companyFilter) {
|
|
|
8254
8238
|
for (const [outpostId, lanes] of [...lanesByOutpost.entries()].sort(([a], [b]) => a.localeCompare(b))) {
|
|
8255
8239
|
for (const group of groupQuestionLanesByCompany(hqRoot, lanes, companyFilter)) {
|
|
8256
8240
|
if (group.company === null) {
|
|
8257
|
-
|
|
8258
|
-
|
|
8259
|
-
|
|
8260
|
-
|
|
8261
|
-
});
|
|
8262
|
-
}
|
|
8241
|
+
errors.push({
|
|
8242
|
+
host: `outpost:${outpostId}`,
|
|
8243
|
+
error: "outpost_question_company_unavailable",
|
|
8244
|
+
});
|
|
8263
8245
|
continue;
|
|
8264
8246
|
}
|
|
8265
8247
|
if (companyFilter && !questionCompanyRefMatches(group.company, companyFilter))
|
|
8266
8248
|
continue;
|
|
8267
|
-
|
|
8268
|
-
eligibleGroups.push({ outpostId, group });
|
|
8269
|
-
}
|
|
8249
|
+
eligibleGroups.push({ outpostId, group });
|
|
8270
8250
|
}
|
|
8271
8251
|
}
|
|
8272
8252
|
if (eligibleGroups.length === 0) {
|
|
@@ -8972,12 +8952,9 @@ async function runAccountsLogin(ctx, providerRaw, accountId, opts) {
|
|
|
8972
8952
|
if (opts.home !== undefined && !requestedHome) {
|
|
8973
8953
|
throw new LanesError("invalid_account_home", "--home must be a non-empty path", { provider, account: id });
|
|
8974
8954
|
}
|
|
8975
|
-
|
|
8976
|
-
|
|
8977
|
-
|
|
8978
|
-
const hostScopedWriterEnabled = registeredAccount !== undefined && requestedHome !== undefined
|
|
8979
|
-
? await hostScopedAccountHomesEnabled(ctx, company)
|
|
8980
|
-
: false;
|
|
8955
|
+
if (opts.company !== undefined) {
|
|
8956
|
+
await resolveSelectedAccountHomeCompany(ctx, opts.company);
|
|
8957
|
+
}
|
|
8981
8958
|
const homeRaw = requestedHome ??
|
|
8982
8959
|
(registeredAccount !== undefined
|
|
8983
8960
|
? effectiveAccountHome(registeredAccount, hostname).home
|
|
@@ -8995,25 +8972,19 @@ async function runAccountsLogin(ctx, providerRaw, accountId, opts) {
|
|
|
8995
8972
|
const nextAccount = registeredAccount === undefined
|
|
8996
8973
|
? { home, enabled: true }
|
|
8997
8974
|
: { ...registeredAccount, enabled: true };
|
|
8998
|
-
if (registeredAccount !== undefined) {
|
|
8999
|
-
if (
|
|
9000
|
-
if (
|
|
9001
|
-
|
|
9002
|
-
|
|
9003
|
-
|
|
9004
|
-
delete nextAccount.hosts;
|
|
9005
|
-
}
|
|
9006
|
-
}
|
|
9007
|
-
else {
|
|
9008
|
-
nextAccount.hosts = {
|
|
9009
|
-
...(registeredAccount.hosts ?? {}),
|
|
9010
|
-
[hostname]: { home },
|
|
9011
|
-
};
|
|
8975
|
+
if (registeredAccount !== undefined && requestedHome !== undefined) {
|
|
8976
|
+
if (home === registeredAccount.home) {
|
|
8977
|
+
if (nextAccount.hosts !== undefined) {
|
|
8978
|
+
delete nextAccount.hosts[hostname];
|
|
8979
|
+
if (Object.keys(nextAccount.hosts).length === 0)
|
|
8980
|
+
delete nextAccount.hosts;
|
|
9012
8981
|
}
|
|
9013
8982
|
}
|
|
9014
|
-
else
|
|
9015
|
-
|
|
9016
|
-
|
|
8983
|
+
else {
|
|
8984
|
+
nextAccount.hosts = {
|
|
8985
|
+
...(registeredAccount.hosts ?? {}),
|
|
8986
|
+
[hostname]: { home },
|
|
8987
|
+
};
|
|
9017
8988
|
}
|
|
9018
8989
|
}
|
|
9019
8990
|
delete nextAccount.disabled_reason;
|
|
@@ -9070,12 +9041,8 @@ async function runAccountsLogin(ctx, providerRaw, accountId, opts) {
|
|
|
9070
9041
|
printJson({ ok: true, action: "accounts.login", account: status });
|
|
9071
9042
|
}
|
|
9072
9043
|
async function runAccountsSetHome(ctx, providerRaw, accountIdRaw, homeRaw, opts) {
|
|
9073
|
-
|
|
9074
|
-
|
|
9075
|
-
: undefined;
|
|
9076
|
-
if (!(await hostScopedAccountHomesEnabled(ctx, company))) {
|
|
9077
|
-
throw new LanesError("host_scoped_account_homes_disabled", "Setting a host-specific account home requires the lanes.host-scoped-account-homes flag", { flag: LANE_HOST_SCOPED_ACCOUNT_HOMES_FLAG });
|
|
9078
|
-
}
|
|
9044
|
+
if (opts.company !== undefined)
|
|
9045
|
+
await resolveSelectedAccountHomeCompany(ctx, opts.company);
|
|
9079
9046
|
const callerCwd = ctx.callerCwd();
|
|
9080
9047
|
const hqRoot = ctx.hqRoot();
|
|
9081
9048
|
const provider = parseLaneProvider(providerRaw);
|
|
@@ -9838,7 +9805,7 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
9838
9805
|
.argument("<account-id>", "Existing account id")
|
|
9839
9806
|
.argument("<path>", "Home directory for this host")
|
|
9840
9807
|
.option("--host <hostname>", "Host key (defaults to this host's hostname)")
|
|
9841
|
-
.option("--company <slug|uid>", "Company scope for
|
|
9808
|
+
.option("--company <slug|uid>", "Company scope for account-home validation")
|
|
9842
9809
|
.option("--json", "JSON on stdout (always)")
|
|
9843
9810
|
.action(wrap(async (provider, accountId, home, opts) => {
|
|
9844
9811
|
await runAccountsSetHome(ctx, provider, accountId, home, opts);
|
|
@@ -9849,7 +9816,7 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
9849
9816
|
.argument("<provider>", "claude | codex | grok")
|
|
9850
9817
|
.argument("<account-id>", "Open-ended account id")
|
|
9851
9818
|
.option("--home <path>", "Account home directory")
|
|
9852
|
-
.option("--company <slug|uid>", "Company scope for
|
|
9819
|
+
.option("--company <slug|uid>", "Company scope for account-home validation")
|
|
9853
9820
|
.option("--device-auth", "Use Codex device authentication instead of localhost callback")
|
|
9854
9821
|
.action(wrap(async (provider, accountId, opts) => {
|
|
9855
9822
|
await runAccountsLogin(ctx, provider, accountId, opts);
|
|
@@ -7,6 +7,22 @@
|
|
|
7
7
|
* module reads `~/.hq/cognito-tokens.json` — a human's session is never
|
|
8
8
|
* consulted, copied, or reused by an agent identity (policy
|
|
9
9
|
* never-extract-stored-session-tokens-to-forge-privileged-calls).
|
|
10
|
+
*
|
|
11
|
+
* NOT EVERY FILESYSTEM CAN KEEP THAT PROMISE. A CIFS/SMB mount (Azure Files
|
|
12
|
+
* is the one that found this) is mounted with a fixed `file_mode` and fails
|
|
13
|
+
* `chmod` outright with EPERM; an NFS export or a Docker volume may accept
|
|
14
|
+
* the call and ignore it. A bot whose only persistent storage is such a
|
|
15
|
+
* share used to be unable to enroll at all — enrollment died on the chmod,
|
|
16
|
+
* leaving a stray temp file behind, and the only alternative was to keep the
|
|
17
|
+
* identity on disposable storage where a rebuild erases it.
|
|
18
|
+
*
|
|
19
|
+
* So the write no longer fails there: it reports whether owner-only
|
|
20
|
+
* permissions actually took, and the caller says so out loud. The file is
|
|
21
|
+
* still requested 0600 at creation, the outcome is read back from the
|
|
22
|
+
* filesystem rather than assumed, and a host that cannot honour it learns
|
|
23
|
+
* that its credential is readable by anything else with access to the same
|
|
24
|
+
* share. Refusing to enroll would not have made that credential safer; it
|
|
25
|
+
* would only have moved it somewhere it silently disappears.
|
|
10
26
|
*/
|
|
11
27
|
import type { AgentKitPaths, KitComponent } from "./paths.js";
|
|
12
28
|
/** Exact `machine-creds.json` shape for an external agent (api-contract v1). */
|
|
@@ -33,8 +49,20 @@ export interface HostKeyPair {
|
|
|
33
49
|
/** Contract fingerprint: sha256(DER SPKI public key) hex, first 16 chars. */
|
|
34
50
|
export declare function hostFingerprint(publicPem: string): string;
|
|
35
51
|
export declare function generateHostKeyPair(): HostKeyPair;
|
|
36
|
-
|
|
37
|
-
|
|
52
|
+
/**
|
|
53
|
+
* What a write could actually guarantee about who can read the file.
|
|
54
|
+
* `enforced: false` means the bytes are on disk but readable beyond the
|
|
55
|
+
* owner — true of a share mounted with a fixed `file_mode`.
|
|
56
|
+
*/
|
|
57
|
+
export interface PrivateFileOutcome {
|
|
58
|
+
enforced: boolean;
|
|
59
|
+
/** Mode observed after the write, when it could be read back. */
|
|
60
|
+
observedMode?: number;
|
|
61
|
+
/** errno of a refused chmod (EPERM on Azure Files), when there was one. */
|
|
62
|
+
chmodError?: string;
|
|
63
|
+
}
|
|
64
|
+
export declare function writeHostKeyPair(paths: Pick<AgentKitPaths, "hostKeyPath" | "hostKeyPubPath">, pair: HostKeyPair): PrivateFileOutcome;
|
|
65
|
+
export declare function writeMachineCreds(paths: Pick<AgentKitPaths, "machineCredsPath">, creds: ExternalMachineCreds): PrivateFileOutcome;
|
|
38
66
|
/**
|
|
39
67
|
* Read the creds file without trusting it: returns null unless it is an
|
|
40
68
|
* external-agent file with every contract field present.
|
|
@@ -7,6 +7,22 @@
|
|
|
7
7
|
* module reads `~/.hq/cognito-tokens.json` — a human's session is never
|
|
8
8
|
* consulted, copied, or reused by an agent identity (policy
|
|
9
9
|
* never-extract-stored-session-tokens-to-forge-privileged-calls).
|
|
10
|
+
*
|
|
11
|
+
* NOT EVERY FILESYSTEM CAN KEEP THAT PROMISE. A CIFS/SMB mount (Azure Files
|
|
12
|
+
* is the one that found this) is mounted with a fixed `file_mode` and fails
|
|
13
|
+
* `chmod` outright with EPERM; an NFS export or a Docker volume may accept
|
|
14
|
+
* the call and ignore it. A bot whose only persistent storage is such a
|
|
15
|
+
* share used to be unable to enroll at all — enrollment died on the chmod,
|
|
16
|
+
* leaving a stray temp file behind, and the only alternative was to keep the
|
|
17
|
+
* identity on disposable storage where a rebuild erases it.
|
|
18
|
+
*
|
|
19
|
+
* So the write no longer fails there: it reports whether owner-only
|
|
20
|
+
* permissions actually took, and the caller says so out loud. The file is
|
|
21
|
+
* still requested 0600 at creation, the outcome is read back from the
|
|
22
|
+
* filesystem rather than assumed, and a host that cannot honour it learns
|
|
23
|
+
* that its credential is readable by anything else with access to the same
|
|
24
|
+
* share. Refusing to enroll would not have made that credential safer; it
|
|
25
|
+
* would only have moved it somewhere it silently disappears.
|
|
10
26
|
*/
|
|
11
27
|
import { createHash, createPublicKey, generateKeyPairSync } from "node:crypto";
|
|
12
28
|
import * as fs from "node:fs";
|
|
@@ -23,20 +39,73 @@ export function generateHostKeyPair() {
|
|
|
23
39
|
const publicPem = publicKey.export({ type: "spki", format: "pem" }).toString();
|
|
24
40
|
return { privatePem, publicPem, fingerprint: hostFingerprint(publicPem) };
|
|
25
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* chmod failures that mean "this filesystem does not do modes" rather than
|
|
44
|
+
* "something is wrong". Anything else still throws: a refused chmod on a
|
|
45
|
+
* local disk is a real fault and must not be swallowed.
|
|
46
|
+
*/
|
|
47
|
+
const MODE_UNSUPPORTED_CODES = new Set([
|
|
48
|
+
"EPERM",
|
|
49
|
+
"EACCES",
|
|
50
|
+
"EINVAL",
|
|
51
|
+
"ENOTSUP",
|
|
52
|
+
"EOPNOTSUPP",
|
|
53
|
+
]);
|
|
26
54
|
/** Write a file atomically with the given mode, replacing any existing one. */
|
|
27
55
|
function writePrivateFile(dest, contents, mode) {
|
|
28
56
|
fs.mkdirSync(path.dirname(dest), { recursive: true, mode: 0o700 });
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
57
|
+
// Date.now() as well as the pid: two writes from one process must not
|
|
58
|
+
// collide on a temp name, and a leftover from a crashed run must not be
|
|
59
|
+
// adopted by the next one.
|
|
60
|
+
const tmp = `${dest}.tmp.${process.pid}.${Date.now()}`;
|
|
61
|
+
let chmodError;
|
|
62
|
+
try {
|
|
63
|
+
fs.writeFileSync(tmp, contents, { mode });
|
|
64
|
+
try {
|
|
65
|
+
fs.chmodSync(tmp, mode);
|
|
66
|
+
}
|
|
67
|
+
catch (err) {
|
|
68
|
+
const code = err.code ?? "";
|
|
69
|
+
if (!MODE_UNSUPPORTED_CODES.has(code))
|
|
70
|
+
throw err;
|
|
71
|
+
chmodError = code;
|
|
72
|
+
}
|
|
73
|
+
// Read the truth back rather than trusting either call: a mount can
|
|
74
|
+
// accept chmod and ignore it just as easily as it can refuse.
|
|
75
|
+
let observedMode;
|
|
76
|
+
try {
|
|
77
|
+
observedMode = fs.statSync(tmp).mode & 0o777;
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
/* stat is not worth failing a write over */
|
|
81
|
+
}
|
|
82
|
+
fs.renameSync(tmp, dest);
|
|
83
|
+
return {
|
|
84
|
+
enforced: observedMode === undefined ? !chmodError : (observedMode & 0o077) === 0,
|
|
85
|
+
...(observedMode === undefined ? {} : { observedMode }),
|
|
86
|
+
...(chmodError ? { chmodError } : {}),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
catch (err) {
|
|
90
|
+
// Never leave the temp file behind — it holds the same secret as the
|
|
91
|
+
// file that failed to land.
|
|
92
|
+
try {
|
|
93
|
+
fs.unlinkSync(tmp);
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
/* already gone */
|
|
97
|
+
}
|
|
98
|
+
throw err;
|
|
99
|
+
}
|
|
33
100
|
}
|
|
34
101
|
export function writeHostKeyPair(paths, pair) {
|
|
35
|
-
writePrivateFile(paths.hostKeyPath, pair.privatePem, 0o600);
|
|
102
|
+
const key = writePrivateFile(paths.hostKeyPath, pair.privatePem, 0o600);
|
|
103
|
+
// The public half is public; its mode is not a promise worth reporting.
|
|
36
104
|
writePrivateFile(paths.hostKeyPubPath, pair.publicPem, 0o644);
|
|
105
|
+
return key;
|
|
37
106
|
}
|
|
38
107
|
export function writeMachineCreds(paths, creds) {
|
|
39
|
-
writePrivateFile(paths.machineCredsPath, `${JSON.stringify(creds, null, 2)}\n`, 0o600);
|
|
108
|
+
return writePrivateFile(paths.machineCredsPath, `${JSON.stringify(creds, null, 2)}\n`, 0o600);
|
|
40
109
|
}
|
|
41
110
|
/**
|
|
42
111
|
* Read the creds file without trusting it: returns null unless it is an
|
|
@@ -70,6 +70,8 @@ export declare function selectClaudeAccount(input: {
|
|
|
70
70
|
config: LanesConfig;
|
|
71
71
|
allowedAccounts?: readonly string[];
|
|
72
72
|
model?: string;
|
|
73
|
+
/** Keep the recorded account eligible for its already-running CLI model. */
|
|
74
|
+
recordedModelAccount?: string;
|
|
73
75
|
explicitAccount?: string;
|
|
74
76
|
unavailableHosts?: readonly BalanceHostGap[];
|
|
75
77
|
}): ClaudeAccountSelection;
|
|
@@ -104,7 +104,8 @@ function buildLoads(input) {
|
|
|
104
104
|
const hosts = input.hosts.map((host) => accountHostLoad(host, account));
|
|
105
105
|
const targetLoad = hosts.find((host) => sameHost(host.host, targetHost));
|
|
106
106
|
const configuredModel = configAccounts[account]?.model;
|
|
107
|
-
const ineligible = targetIneligibilityReason(account, configAccounts[account]?.enabled === true, input.allowedAccounts, targetLoad) ?? (
|
|
107
|
+
const ineligible = targetIneligibilityReason(account, configAccounts[account]?.enabled === true, input.allowedAccounts, targetLoad) ?? (account !== input.recordedModelAccount &&
|
|
108
|
+
input.model !== undefined && configuredModel !== undefined && configuredModel !== input.model
|
|
108
109
|
? "model_mismatch"
|
|
109
110
|
: undefined);
|
|
110
111
|
if (ineligible !== undefined)
|
|
@@ -10,13 +10,12 @@
|
|
|
10
10
|
import * as fs from "node:fs";
|
|
11
11
|
import { probeProviderCompactionsSince } from "./compaction.js";
|
|
12
12
|
import { tryReadLane } from "./store.js";
|
|
13
|
-
import { type
|
|
13
|
+
import { type LaneRecord, type LaneState, type QuestionRecord } from "./types.js";
|
|
14
14
|
export declare const DROPBOX_DIR = "dropbox";
|
|
15
15
|
export declare const INBOX_NAME = "inbox.jsonl";
|
|
16
16
|
export declare const DRAINING_DIR = "draining";
|
|
17
17
|
export declare const DELIVERED_DIR = "delivered";
|
|
18
18
|
export declare const INFLIGHT_DIR = "inflight";
|
|
19
|
-
export { LANE_MESSAGE_COMPACTION_REPLAY_FLAG } from "./message-replay.js";
|
|
20
19
|
export interface DropboxMessage {
|
|
21
20
|
at?: string;
|
|
22
21
|
message_id?: string;
|
|
@@ -59,10 +58,6 @@ export interface LaneDrainHookInput {
|
|
|
59
58
|
hook_event_name?: string;
|
|
60
59
|
}
|
|
61
60
|
export interface RunDrainHookDeps {
|
|
62
|
-
readFlag?: (flag: string, options: {
|
|
63
|
-
company?: LaneCompanyRef | null;
|
|
64
|
-
env: NodeJS.ProcessEnv;
|
|
65
|
-
}) => Promise<boolean>;
|
|
66
61
|
readLane?: typeof tryReadLane;
|
|
67
62
|
probeCompactionsSince?: typeof probeProviderCompactionsSince;
|
|
68
63
|
}
|
|
@@ -12,8 +12,7 @@ import * as fs from "node:fs";
|
|
|
12
12
|
import * as path from "node:path";
|
|
13
13
|
import { probeProviderCompactionsSince, providerTranscriptSize, } from "./compaction.js";
|
|
14
14
|
import { LanesError } from "./errors.js";
|
|
15
|
-
import {
|
|
16
|
-
import { LANE_MESSAGE_COMPACTION_REPLAY_FLAG, MESSAGE_COMPACTION_REPLAY_NOTE, MESSAGE_REPLAY_STATE_NAME, } from "./message-replay.js";
|
|
15
|
+
import { MESSAGE_COMPACTION_REPLAY_NOTE, MESSAGE_REPLAY_STATE_NAME, } from "./message-replay.js";
|
|
17
16
|
import { LANES_RUNS_REL, readProcStarttime } from "./spawn.js";
|
|
18
17
|
import { tryReadLane } from "./store.js";
|
|
19
18
|
export const DROPBOX_DIR = "dropbox";
|
|
@@ -21,7 +20,6 @@ export const INBOX_NAME = "inbox.jsonl";
|
|
|
21
20
|
export const DRAINING_DIR = "draining";
|
|
22
21
|
export const DELIVERED_DIR = "delivered";
|
|
23
22
|
export const INFLIGHT_DIR = "inflight";
|
|
24
|
-
export { LANE_MESSAGE_COMPACTION_REPLAY_FLAG } from "./message-replay.js";
|
|
25
23
|
function defaultDrainDeps() {
|
|
26
24
|
return {
|
|
27
25
|
mkdirSync: fs.mkdirSync.bind(fs),
|
|
@@ -492,8 +490,8 @@ function readReplayState(filePath, turnId) {
|
|
|
492
490
|
(state.flag_enabled !== undefined && typeof state.flag_enabled !== "boolean")) {
|
|
493
491
|
throw new LanesError("message_replay_state_invalid", `Replay state ${filePath} does not match the current provider turn`);
|
|
494
492
|
}
|
|
495
|
-
// Older state files persisted
|
|
496
|
-
// before the next write so the
|
|
493
|
+
// Older state files persisted the removed per-turn flag. Accept it, but
|
|
494
|
+
// remove it before the next write so the stale cache does not live on.
|
|
497
495
|
delete state.flag_enabled;
|
|
498
496
|
return state;
|
|
499
497
|
}
|
|
@@ -602,12 +600,9 @@ export async function runDrainHook(env, input = {}, deps = {}) {
|
|
|
602
600
|
}
|
|
603
601
|
else if (delta.count > 0) {
|
|
604
602
|
state.last_compaction_marker += delta.count;
|
|
605
|
-
|
|
606
|
-
if (
|
|
607
|
-
|
|
608
|
-
if (replayed.length > 0)
|
|
609
|
-
replayNote = MESSAGE_COMPACTION_REPLAY_NOTE;
|
|
610
|
-
}
|
|
603
|
+
replayed = deliveredMessagesForTurn(hqRoot, laneId, turnId);
|
|
604
|
+
if (replayed.length > 0)
|
|
605
|
+
replayNote = MESSAGE_COMPACTION_REPLAY_NOTE;
|
|
611
606
|
}
|
|
612
607
|
}
|
|
613
608
|
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
export declare const LANE_MESSAGE_COMPACTION_REPLAY_FLAG = "lanes.message-compaction-replay";
|
|
2
1
|
export declare const MESSAGE_REPLAY_STATE_NAME = "message-compaction-replay.json";
|
|
3
2
|
export declare const MESSAGE_COMPACTION_REPLAY_NOTE = "These messages were delivered before a context compaction and still apply.";
|
|
4
3
|
export interface MessageReplayState {
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
export const LANE_MESSAGE_COMPACTION_REPLAY_FLAG = "lanes.message-compaction-replay";
|
|
2
1
|
export const MESSAGE_REPLAY_STATE_NAME = "message-compaction-replay.json";
|
|
3
2
|
export const MESSAGE_COMPACTION_REPLAY_NOTE = "These messages were delivered before a context compaction and still apply.";
|
|
4
3
|
//# sourceMappingURL=message-replay.js.map
|