@indigoai-us/hq-cli 5.203.0 → 5.204.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 CHANGED
@@ -2,6 +2,14 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.204.0] — 2026-09-26
6
+
7
+ ### Added
8
+
9
+ - `hq lanes accounts set-home` adds a host-specific home to the synced lane
10
+ account registry. Account status and Claude balancing now distinguish a
11
+ missing home from a signed-out account.
12
+
5
13
  ## [5.203.0] — 2026-09-26
6
14
 
7
15
  ### Added
@@ -8135,6 +8135,33 @@ export declare const COMMAND_CATALOG: readonly [{
8135
8135
  readonly description: "JSON on stdout (always)";
8136
8136
  }];
8137
8137
  readonly subcommands: readonly [];
8138
+ }, {
8139
+ readonly name: "set-home";
8140
+ readonly description: "Set the host-specific home for an existing provider account";
8141
+ readonly aliases: readonly [];
8142
+ readonly hidden: false;
8143
+ readonly usage: "[options] <provider> <account-id> <path>";
8144
+ readonly arguments: readonly [{
8145
+ readonly name: "provider";
8146
+ readonly required: true;
8147
+ readonly variadic: false;
8148
+ }, {
8149
+ readonly name: "account-id";
8150
+ readonly required: true;
8151
+ readonly variadic: false;
8152
+ }, {
8153
+ readonly name: "path";
8154
+ readonly required: true;
8155
+ readonly variadic: false;
8156
+ }];
8157
+ readonly options: readonly [{
8158
+ readonly flags: "--host <hostname>";
8159
+ readonly description: "Host key (defaults to this host's hostname)";
8160
+ }, {
8161
+ readonly flags: "--json";
8162
+ readonly description: "JSON on stdout (always)";
8163
+ }];
8164
+ readonly subcommands: readonly [];
8138
8165
  }, {
8139
8166
  readonly name: "login";
8140
8167
  readonly description: "Create a home, run the vendor login, and register the account";
@@ -10523,6 +10523,41 @@ export const COMMAND_CATALOG = [
10523
10523
  ],
10524
10524
  "subcommands": []
10525
10525
  },
10526
+ {
10527
+ "name": "set-home",
10528
+ "description": "Set the host-specific home for an existing provider account",
10529
+ "aliases": [],
10530
+ "hidden": false,
10531
+ "usage": "[options] <provider> <account-id> <path>",
10532
+ "arguments": [
10533
+ {
10534
+ "name": "provider",
10535
+ "required": true,
10536
+ "variadic": false
10537
+ },
10538
+ {
10539
+ "name": "account-id",
10540
+ "required": true,
10541
+ "variadic": false
10542
+ },
10543
+ {
10544
+ "name": "path",
10545
+ "required": true,
10546
+ "variadic": false
10547
+ }
10548
+ ],
10549
+ "options": [
10550
+ {
10551
+ "flags": "--host <hostname>",
10552
+ "description": "Host key (defaults to this host's hostname)"
10553
+ },
10554
+ {
10555
+ "flags": "--json",
10556
+ "description": "JSON on stdout (always)"
10557
+ }
10558
+ ],
10559
+ "subcommands": []
10560
+ },
10526
10561
  {
10527
10562
  "name": "login",
10528
10563
  "description": "Create a home, run the vendor login, and register the account",
@@ -95,11 +95,14 @@ export interface LanesCommandDeps {
95
95
  laneTrackerEnabled?: (company: LaneCompanyRef) => boolean | Promise<boolean>;
96
96
  /** The local-only PR 2 implementation refuses if mesh storage is enabled. */
97
97
  laneTrackerMeshEnabled?: (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>;
98
100
  /** Test seam for recording approvals against the active identity. */
99
101
  trackerActor?: () => string | null;
100
102
  }
101
103
  export declare const LANE_OUTPOST_LOAD_BALANCING_FLAG = "lanes.outpost-load-balancing";
102
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";
103
106
  export declare function parseSeniorRef(raw: string): SeniorRef;
104
107
  export declare function resolveAnswerChoice(choice: string, options: string[], freeform: boolean): string;
105
108
  /**
@@ -10,6 +10,7 @@ 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";
13
14
  import { Option } from "commander";
14
15
  import { registerOutpostPrunerCommand } from "./outpost-pruner.js";
15
16
  import { ensureCognitoToken } from "../utils/cognito-session.js";
@@ -32,11 +33,12 @@ import { vaultApiFetch } from "../utils/vault-api.js";
32
33
  import { openMeshTransport } from "../lib/mesh/client.js";
33
34
  import { createLaneMesh, emitLaneDecisionEvents, emitLaneTerminalStateEvent, laneMeshEventIdentity, } from "../lib/lanes/mesh-events.js";
34
35
  import { sessionIdFromEnv } from "../lib/mesh/live/session-identity.js";
36
+ import { effectiveAccountHome } from "../lib/lanes/account-home.js";
35
37
  import { AccountResolutionError, compareLaneQueueOrder, coveredLaneIds, DEFAULT_ACCOUNT_HOMES, LANE_PROVIDERS, LANE_STATES, PROVIDER_HOME_ENV, QUESTION_STATUSES, SENIOR_KINDS, accountStatus, appendCappedHistory, canonicalizeHome, createLane, createQuestion, decideAdmission, defaultLivenessDeps, defaultSystemdUserDir, defaultThrottleDeps, installDrainHook, installReconcileTimer, providerPathEnv, probeDrainHookRegistration, isLanesError, isLaneVersionConflict, isTerminalLaneState, aliveDirectChildren, assignedChildHireGrants, LANE_HIRE_GRANTS_FLAG, planHireGrantBackfill, withHireGrantAdmission, laneStoryLedgerEnabled, validateProjectStories, buildLaneStatusSnapshot, formatLaneStatusHuman, listAccountStatuses, listLaneIds, listQuestionIds, listSessionLiveness, loadLanesConfig, laneRunsHere, MAX_LANE_DEPTH, MONITOR_ERROR_EXIT_CODE, MONITOR_ENGINES, MONITOR_MAX_TIMEOUT_MS, MONITOR_UNCOVERED_EXIT_CODE, MAX_WATCHED_LANES, monitorReminderText, readLiveMonitorEvidence, resolveMachineIdentity as lookupMachineIdentity, loadLivenessIndex, parseLaneProvider, parseLaneRecord, parseLaneState, parseLivenessState, parseQuestionRecord, parseQuestionStatus, parseSeniorKind, readDaemonPresenceConnected, readLane, retryUpdateLane, syncLaneRecordFromOwner, readQuestion, resolveAccount, resolveLaneWorker, ignoredWorkerProfileFields, loadWorkerProfile, runReconcilePass, wakeAnsweredLaneNow, runLaneWatch, sessionLiveness, tryReadLane, uninstallReconcileTimer, updateLane, updateQuestion, writeLanesConfig, appendDropboxMessage, answerDropboxMessage, deliveryRefusalReason, resumeCommandRefusal, dropboxDir, dropboxInboxPath, laneAcceptsDropboxDelivery, laneAcceptsOperatorMessage, laneDeliveryMechanism, hasActiveAdmissionReservation, hasActivePendingResume, hasRecordedSessionId, hasEarlierEligibleQueuedLane, laneAdmissionCapReason, providerSupportsSessionResume, providerSessionPortable, sessionNotPortableMessage, withLaneAdmissionLock, withLaneHireLocks, laneIdForAnswerDelivery, operatorMessageRefusalReason, raiseLaneQuestion, sha256Text, writeQuestionBody, } from "../lib/lanes/index.js";
36
38
  import { activeLaneStories, laneStories, laneStory, storyWarning, } from "../lib/lanes/stories.js";
37
39
  import { questionBodyAbsPath } from "../lib/lanes/questions.js";
38
40
  import { compactionAfterUsage, } from "../lib/lanes/compaction.js";
39
- import { providerCompactAfterMs } from "../lib/lanes/config.js";
41
+ import { assertUniqueAccountHomes, providerCompactAfterMs } from "../lib/lanes/config.js";
40
42
  import { parseCliEffort, resolveLaneEffort, } from "../lib/lanes/effort.js";
41
43
  import { LanesError } from "../lib/lanes/errors.js";
42
44
  import { dispatchLaneDelivery } from "../lib/lanes/delivery-core.js";
@@ -59,6 +61,7 @@ const VENDOR_LOGIN = {
59
61
  export { emitLaneDecisionEvents };
60
62
  export const LANE_OUTPOST_LOAD_BALANCING_FLAG = "lanes.outpost-load-balancing";
61
63
  export const LANE_OUTPOST_QUESTION_VISIBILITY_FLAG = "lanes.outpost-question-visibility";
64
+ export const LANE_HOST_SCOPED_ACCOUNT_HOMES_FLAG = "lanes.host-scoped-account-homes";
62
65
  function defaultLanePlacementEnabled() {
63
66
  return resolveProcessFlagGate(LANE_OUTPOST_LOAD_BALANCING_FLAG, { fallback: false }, () => false);
64
67
  }
@@ -82,6 +85,15 @@ function defaultLaneOutpostQuestionVisibilityEnabled(company, env) {
82
85
  env,
83
86
  });
84
87
  }
88
+ function defaultLaneHostScopedAccountHomesEnabled(company, env) {
89
+ return readLanesRegistryFlag(LANE_HOST_SCOPED_ACCOUNT_HOMES_FLAG, { company, env });
90
+ }
91
+ async function hostScopedAccountHomesEnabled(ctx) {
92
+ const localOverride = readLocalOverride(LANE_HOST_SCOPED_ACCOUNT_HOMES_FLAG, ctx.env);
93
+ if (localOverride !== null)
94
+ return localOverride;
95
+ return ctx.laneHostScopedAccountHomesEnabled(await ctx.resolveCompany());
96
+ }
85
97
  function withoutProviderHome(value) {
86
98
  if (Array.isArray(value))
87
99
  return value.map(withoutProviderHome);
@@ -286,6 +298,8 @@ function resolveDeps(deps = {}) {
286
298
  ((company) => defaultLaneOutpostQuestionVisibilityEnabled(company, env)),
287
299
  laneTrackerEnabled: deps.laneTrackerEnabled ?? defaultLaneTrackerEnabled,
288
300
  laneTrackerMeshEnabled: deps.laneTrackerMeshEnabled ?? defaultLaneTrackerMeshEnabled,
301
+ laneHostScopedAccountHomesEnabled: deps.laneHostScopedAccountHomesEnabled ??
302
+ ((company) => defaultLaneHostScopedAccountHomesEnabled(company, env)),
289
303
  trackerActor: deps.trackerActor ?? defaultTrackerActor,
290
304
  };
291
305
  }
@@ -298,6 +312,7 @@ function livenessDepsFromCtx(ctx) {
298
312
  env: ctx.env,
299
313
  existsSync: ctx.existsSync,
300
314
  readFileSync: ctx.readFileSync,
315
+ hostname: ctx.hostname,
301
316
  };
302
317
  }
303
318
  function reconcileHostFromCtx(ctx) {
@@ -1004,6 +1019,7 @@ async function loadHostCapacitySnapshots(ctx, hqRoot, config = loadLanesConfig(h
1004
1019
  lanes: loadAllLanes(hqRoot),
1005
1020
  config,
1006
1021
  existsSync: ctx.existsSync,
1022
+ hostname: ctx.hostname(),
1007
1023
  now: ctx.now(),
1008
1024
  });
1009
1025
  const remote = await Promise.all(remoteIds.map(async (id) => {
@@ -1118,6 +1134,7 @@ async function loadClaudeBalanceHostSnapshots(ctx, hqRoot, config) {
1118
1134
  existsSync: ctx.existsSync,
1119
1135
  probe: ctx.probeProvider,
1120
1136
  env: ctx.env,
1137
+ hostname: ctx.hostname(),
1121
1138
  now: ctx.now(),
1122
1139
  });
1123
1140
  const snapshots = [{
@@ -1140,6 +1157,7 @@ async function loadClaudeBalanceHostSnapshots(ctx, hqRoot, config) {
1140
1157
  existsSync: ctx.existsSync,
1141
1158
  probe: ctx.probeProvider,
1142
1159
  env: ctx.env,
1160
+ hostname: ctx.hostname(),
1143
1161
  now: ctx.now(),
1144
1162
  }),
1145
1163
  lanes: actualLocalLanes,
@@ -2196,6 +2214,7 @@ export function applyOutpostAccountPickup(hqRoot, laneId, modelDeps = {}) {
2196
2214
  try {
2197
2215
  const resolved = resolveAccount(lane.provider, lane.account, config, {
2198
2216
  hqRoot,
2217
+ hostname: os.hostname(),
2199
2218
  accountDeps: {
2200
2219
  probe: modelDeps.probe,
2201
2220
  env: modelDeps.env,
@@ -2203,6 +2222,7 @@ export function applyOutpostAccountPickup(hqRoot, laneId, modelDeps = {}) {
2203
2222
  });
2204
2223
  const status = accountStatus(lane.provider, resolved.id, config, {
2205
2224
  existsSync: (p) => fs.existsSync(p),
2225
+ hostname: os.hostname(),
2206
2226
  probe: modelDeps.probe,
2207
2227
  env: modelDeps.env,
2208
2228
  });
@@ -2589,6 +2609,7 @@ async function runCreate(ctx, opts) {
2589
2609
  }
2590
2610
  const resolved = resolveAccount(provider, requested, config, {
2591
2611
  hqRoot,
2612
+ hostname: ctx.hostname(),
2592
2613
  cliModel,
2593
2614
  model: cliModel ?? profileModel,
2594
2615
  });
@@ -2597,6 +2618,7 @@ async function runCreate(ctx, opts) {
2597
2618
  else if (profileAccount !== undefined && profileAccounts === undefined) {
2598
2619
  const resolved = resolveAccount(provider, profileAccount, config, {
2599
2620
  hqRoot,
2621
+ hostname: ctx.hostname(),
2600
2622
  cliModel,
2601
2623
  model: cliModel ?? profileModel,
2602
2624
  });
@@ -2650,6 +2672,7 @@ async function runCreate(ctx, opts) {
2650
2672
  const candidate = pinnedAccount ??
2651
2673
  resolveAccount(provider, undefined, config, {
2652
2674
  hqRoot,
2675
+ hostname: ctx.hostname(),
2653
2676
  existsSync: ctx.existsSync,
2654
2677
  accountDeps: { probe: ctx.probeProvider, env: ctx.env },
2655
2678
  cliModel,
@@ -2669,6 +2692,7 @@ async function runCreate(ctx, opts) {
2669
2692
  (profileAccounts === undefined || profileAccounts.includes(id)) &&
2670
2693
  accountStatus(provider, id, config, {
2671
2694
  existsSync: ctx.existsSync,
2695
+ hostname: ctx.hostname(),
2672
2696
  probe: ctx.probeProvider,
2673
2697
  env: ctx.env,
2674
2698
  }).logged_in === true)
@@ -2679,7 +2703,10 @@ async function runCreate(ctx, opts) {
2679
2703
  const quotaBlocked = [];
2680
2704
  const quota = loadQuotaState(hqRoot);
2681
2705
  for (const id of toValidate) {
2682
- const home = accounts[id]?.home;
2706
+ const account = accounts[id];
2707
+ const home = account === undefined
2708
+ ? undefined
2709
+ : effectiveAccountHome(account, ctx.hostname()).home;
2683
2710
  if (home === undefined)
2684
2711
  continue;
2685
2712
  checkedAccounts.push(id);
@@ -2782,6 +2809,7 @@ async function runCreate(ctx, opts) {
2782
2809
  existsSync: ctx.existsSync,
2783
2810
  probe: ctx.probeProvider,
2784
2811
  env: ctx.env,
2812
+ hostname: ctx.hostname(),
2785
2813
  now: ctx.now(),
2786
2814
  }),
2787
2815
  lanes: localLanes,
@@ -2805,6 +2833,7 @@ async function runCreate(ctx, opts) {
2805
2833
  pinnedAccount = resolveAccount(provider, undefined, config, {
2806
2834
  hqRoot,
2807
2835
  existsSync: ctx.existsSync,
2836
+ hostname: ctx.hostname(),
2808
2837
  accountDeps: { probe: ctx.probeProvider, env: ctx.env },
2809
2838
  cliModel,
2810
2839
  model: cliModel ?? profileModel,
@@ -4029,6 +4058,7 @@ async function runCapacity(ctx) {
4029
4058
  existsSync: ctx.existsSync,
4030
4059
  probe: ctx.probeProvider,
4031
4060
  env: ctx.env,
4061
+ hostname: ctx.hostname(),
4032
4062
  now: ctx.now(),
4033
4063
  });
4034
4064
  printJson(snapshot);
@@ -4873,6 +4903,7 @@ function validateResumeProfileModel(ctx, lane, profile) {
4873
4903
  const account = resolveAccount(lane.provider, lane.account, config, {
4874
4904
  hqRoot,
4875
4905
  existsSync: ctx.existsSync,
4906
+ hostname: ctx.hostname(),
4876
4907
  model: profile.meta.model,
4877
4908
  });
4878
4909
  validateProfileLaneModel(lane.provider, profile.meta.model, account.home, profile.id, {
@@ -4919,7 +4950,7 @@ function recordResumeSetup(ctx, lane, input) {
4919
4950
  if (account === undefined) {
4920
4951
  throw new AccountResolutionError("unknown_account", `Lane ${lane.lane_id} records account ${JSON.stringify(lane.account)} which is not configured for ${lane.provider}`, { provider: lane.provider, account: lane.account ?? "", enabled: [] });
4921
4952
  }
4922
- validateLaneModel(lane.provider, model, account.home, {
4953
+ validateLaneModel(lane.provider, model, effectiveAccountHome(account, ctx.hostname()).home, {
4923
4954
  readFileSync: ctx.readFileSync,
4924
4955
  probe: ctx.probeProvider,
4925
4956
  env: ctx.env,
@@ -5039,11 +5070,13 @@ async function prepareAccountMove(ctx, lane, input) {
5039
5070
  if (targetCfg === undefined) {
5040
5071
  throw new AccountResolutionError("unknown_account", `Unknown account ${JSON.stringify(target)} for ${lane.provider}. Configured: ${Object.keys(providerCfg.accounts).join(", ") || "(none)"}`, { provider: lane.provider, account: target, enabled: Object.keys(providerCfg.accounts).filter((id) => providerCfg.accounts[id]?.enabled) });
5041
5072
  }
5073
+ const targetHome = effectiveAccountHome(targetCfg, ctx.hostname()).home;
5042
5074
  if (!targetCfg.enabled) {
5043
5075
  throw new AccountResolutionError("account_disabled", `Account ${JSON.stringify(target)} is disabled${targetCfg.disabled_reason ? `: ${targetCfg.disabled_reason}` : ""}`, { provider: lane.provider, account: target, reason: targetCfg.disabled_reason ?? "" });
5044
5076
  }
5045
5077
  if (accountStatus(lane.provider, target, config, {
5046
5078
  existsSync: ctx.existsSync,
5079
+ hostname: ctx.hostname(),
5047
5080
  probe: ctx.probeProvider,
5048
5081
  env: ctx.env,
5049
5082
  }).logged_in !== true) {
@@ -5060,7 +5093,7 @@ async function prepareAccountMove(ctx, lane, input) {
5060
5093
  : undefined;
5061
5094
  const profileModel = profileDefaultsApply ? profile?.meta.model : undefined;
5062
5095
  if (input.cliModel === undefined && profileDefaultsApply && profileModel !== undefined && pinnedModel === undefined) {
5063
- validateProfileLaneModel(lane.provider, profileModel, targetCfg.home, profile.id, {
5096
+ validateProfileLaneModel(lane.provider, profileModel, targetHome, profile.id, {
5064
5097
  readFileSync: ctx.readFileSync,
5065
5098
  probe: ctx.probeProvider,
5066
5099
  env: ctx.env,
@@ -5086,7 +5119,7 @@ async function prepareAccountMove(ctx, lane, input) {
5086
5119
  throw new LanesError("account_quota_held", `Account ${target} for ${lane.provider} is held${block.quota_reset_at ? ` until ${block.quota_reset_at}` : ""}${block.marker ? ` (${block.marker})` : ""}`, { provider: lane.provider, account: target, quota_reset_at: block.quota_reset_at ?? null, marker: block.marker ?? null, model: model ?? null });
5087
5120
  }
5088
5121
  if (model !== undefined) {
5089
- validateLaneModel(lane.provider, model, targetCfg.home, {
5122
+ validateLaneModel(lane.provider, model, targetHome, {
5090
5123
  readFileSync: ctx.readFileSync,
5091
5124
  probe: ctx.probeProvider,
5092
5125
  env: ctx.env,
@@ -5096,14 +5129,15 @@ async function prepareAccountMove(ctx, lane, input) {
5096
5129
  if (sourceCfg === undefined) {
5097
5130
  throw new AccountResolutionError("unknown_account", `Lane ${laneId} records account ${JSON.stringify(lane.account)} which is not configured for ${lane.provider}`, { provider: lane.provider, account: lane.account ?? "", enabled: [] });
5098
5131
  }
5132
+ const sourceHome = effectiveAccountHome(sourceCfg, ctx.hostname()).home;
5099
5133
  // Checks done. Copy the session files, then write the record.
5100
5134
  const copy = ensureSessionHome({
5101
5135
  provider: lane.provider,
5102
5136
  sessionId,
5103
5137
  cwd: hqRoot,
5104
- targetHome: targetCfg.home,
5105
- registeredHomes: Object.values(providerCfg.accounts).map((account) => account.home),
5106
- providerHomeAtSpawn: lane.provider_home_at_spawn ?? sourceCfg.home,
5138
+ targetHome,
5139
+ registeredHomes: Object.values(providerCfg.accounts).map((account) => effectiveAccountHome(account, ctx.hostname()).home),
5140
+ providerHomeAtSpawn: lane.provider_home_at_spawn ?? sourceHome,
5107
5141
  });
5108
5142
  const at = now.toISOString();
5109
5143
  const from = lane.account ?? null;
@@ -5186,6 +5220,7 @@ async function selectAutomaticClaudeResumeAccount(ctx, lane, workerProfile) {
5186
5220
  }
5187
5221
  if (accountStatus("claude", lane.account, config, {
5188
5222
  existsSync: ctx.existsSync,
5223
+ hostname: ctx.hostname(),
5189
5224
  probe: ctx.probeProvider,
5190
5225
  env: ctx.env,
5191
5226
  }).logged_in === false) {
@@ -5212,6 +5247,7 @@ async function selectAutomaticClaudeResumeAccount(ctx, lane, workerProfile) {
5212
5247
  ? false
5213
5248
  : accountStatus("claude", lane.account, config, {
5214
5249
  existsSync: ctx.existsSync,
5250
+ hostname: ctx.hostname(),
5215
5251
  probe: ctx.probeProvider,
5216
5252
  env: ctx.env,
5217
5253
  }).logged_in;
@@ -5274,6 +5310,7 @@ function assertCurrentResumeAccountAvailable(ctx, lane) {
5274
5310
  }
5275
5311
  if (accountStatus(lane.provider, lane.account, config, {
5276
5312
  existsSync: ctx.existsSync,
5313
+ hostname: ctx.hostname(),
5277
5314
  probe: ctx.probeProvider,
5278
5315
  env: ctx.env,
5279
5316
  }).logged_in !== true) {
@@ -6419,7 +6456,7 @@ function drainHookStatusFor(ctx, lane) {
6419
6456
  const account = config.providers[lane.provider].accounts[lane.account];
6420
6457
  if (account === undefined)
6421
6458
  return null;
6422
- const home = canonicalizeHome(account.home, {
6459
+ const home = canonicalizeHome(effectiveAccountHome(account, ctx.hostname()).home, {
6423
6460
  homedir: ctx.homedir,
6424
6461
  realpathSync: (p) => fs.realpathSync(p),
6425
6462
  existsSync: (p) => fs.existsSync(p),
@@ -7671,6 +7708,7 @@ async function runAccountsList(ctx, opts) {
7671
7708
  const quota = loadQuotaState(hqRoot);
7672
7709
  const accounts = providers.flatMap((provider) => listAccountStatuses(provider, config, {
7673
7710
  existsSync: ctx.existsSync,
7711
+ hostname: ctx.hostname(),
7674
7712
  probe: ctx.probeProvider,
7675
7713
  env: ctx.env,
7676
7714
  }).map((status) => {
@@ -7686,6 +7724,7 @@ async function runAccountsList(ctx, opts) {
7686
7724
  });
7687
7725
  return {
7688
7726
  ...status,
7727
+ effective_home: status.home,
7689
7728
  model: accountCfg?.model ?? null,
7690
7729
  effort: accountCfg?.effort ?? null,
7691
7730
  effective_model: model ?? null,
@@ -7849,9 +7888,23 @@ async function runAccountsLogin(ctx, providerRaw, accountId, opts) {
7849
7888
  throw new LanesError("device_auth_unsupported", "--device-auth is supported only for codex account login", { provider });
7850
7889
  }
7851
7890
  const registeredAccount = config.providers[provider].accounts[id];
7852
- const homeRaw = opts.home?.trim() ||
7853
- registeredAccount?.home ||
7854
- (id === "a" ? DEFAULT_ACCOUNT_HOMES[provider] : `~/.${provider}-${id}`);
7891
+ const hostname = ctx.hostname().trim();
7892
+ if (!hostname) {
7893
+ throw new LanesError("host_identity_unknown", "Cannot resolve this host's hostname for account login", {});
7894
+ }
7895
+ const requestedHome = opts.home?.trim();
7896
+ if (opts.home !== undefined && !requestedHome) {
7897
+ throw new LanesError("invalid_account_home", "--home must be a non-empty path", { provider, account: id });
7898
+ }
7899
+ const hostScopedWriterEnabled = registeredAccount !== undefined && requestedHome !== undefined
7900
+ ? await hostScopedAccountHomesEnabled(ctx)
7901
+ : false;
7902
+ const homeRaw = requestedHome ??
7903
+ (registeredAccount !== undefined
7904
+ ? effectiveAccountHome(registeredAccount, hostname).home
7905
+ : id === "a"
7906
+ ? DEFAULT_ACCOUNT_HOMES[provider]
7907
+ : `~/.${provider}-${id}`);
7855
7908
  const homeResolved = homeRaw.startsWith("~") || path.isAbsolute(homeRaw)
7856
7909
  ? homeRaw
7857
7910
  : resolveCallerPath(callerCwd, homeRaw);
@@ -7860,20 +7913,43 @@ async function runAccountsLogin(ctx, providerRaw, accountId, opts) {
7860
7913
  realpathSync: (p) => fs.realpathSync(p),
7861
7914
  existsSync: (p) => fs.existsSync(p),
7862
7915
  });
7863
- for (const [existingId, account] of Object.entries(config.providers[provider].accounts)) {
7864
- if (existingId === id)
7865
- continue;
7866
- if (account.home === home) {
7867
- throw new LanesError("duplicate_account_home", `Home ${home} is already registered as ${provider} account ${existingId}`, { provider, accounts: [existingId, id], home });
7916
+ const nextAccount = registeredAccount === undefined
7917
+ ? { home, enabled: true }
7918
+ : { ...registeredAccount, enabled: true };
7919
+ if (registeredAccount !== undefined) {
7920
+ if (hostScopedWriterEnabled && requestedHome !== undefined) {
7921
+ if (home === registeredAccount.home) {
7922
+ if (nextAccount.hosts !== undefined) {
7923
+ delete nextAccount.hosts[hostname];
7924
+ if (Object.keys(nextAccount.hosts).length === 0)
7925
+ delete nextAccount.hosts;
7926
+ }
7927
+ }
7928
+ else {
7929
+ nextAccount.hosts = {
7930
+ ...(registeredAccount.hosts ?? {}),
7931
+ [hostname]: { home },
7932
+ };
7933
+ }
7934
+ }
7935
+ else if (requestedHome !== undefined) {
7936
+ // With the rollout off, retain the legacy shared-home write shape.
7937
+ nextAccount.home = home;
7868
7938
  }
7869
7939
  }
7940
+ delete nextAccount.disabled_reason;
7941
+ assertUniqueAccountHomes(provider, {
7942
+ ...config.providers[provider].accounts,
7943
+ [id]: nextAccount,
7944
+ });
7870
7945
  ensureDir(home);
7871
7946
  const defaultId = config.providers[provider].default_account;
7872
- const defaultHome = (defaultId !== undefined
7873
- ? config.providers[provider].accounts[defaultId]?.home
7947
+ const defaultAccount = defaultId !== undefined
7948
+ ? config.providers[provider].accounts[defaultId]
7949
+ : Object.values(config.providers[provider].accounts).find((account) => account.enabled);
7950
+ const defaultHome = (defaultAccount !== undefined
7951
+ ? effectiveAccountHome(defaultAccount, hostname).home
7874
7952
  : undefined) ??
7875
- Object.values(config.providers[provider].accounts).find((account) => account.enabled)
7876
- ?.home ??
7877
7953
  canonicalizeHome(DEFAULT_ACCOUNT_HOMES[provider], {
7878
7954
  homedir: ctx.homedir,
7879
7955
  realpathSync: (p) => fs.realpathSync(p),
@@ -7899,12 +7975,10 @@ async function runAccountsLogin(ctx, providerRaw, accountId, opts) {
7899
7975
  if ((grokNeedsRails || copied) && !railsPresent(home)) {
7900
7976
  throw new LanesError("rails_missing", `Hook rails are missing under ${home}; account not enabled`, { provider, account: id, home });
7901
7977
  }
7902
- config.providers[provider].accounts[id] = {
7903
- home,
7904
- enabled: true,
7905
- };
7978
+ config.providers[provider].accounts[id] = nextAccount;
7906
7979
  const status = accountStatus(provider, id, config, {
7907
7980
  existsSync: ctx.existsSync,
7981
+ hostname,
7908
7982
  probe: ctx.probeProvider,
7909
7983
  env: ctx.env,
7910
7984
  });
@@ -7916,6 +7990,93 @@ async function runAccountsLogin(ctx, providerRaw, accountId, opts) {
7916
7990
  writeLanesConfig(hqRoot, config);
7917
7991
  printJson({ ok: true, action: "accounts.login", account: status });
7918
7992
  }
7993
+ async function runAccountsSetHome(ctx, providerRaw, accountIdRaw, homeRaw, opts) {
7994
+ if (!(await hostScopedAccountHomesEnabled(ctx))) {
7995
+ 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 });
7996
+ }
7997
+ const callerCwd = ctx.callerCwd();
7998
+ const hqRoot = ctx.hqRoot();
7999
+ const provider = parseLaneProvider(providerRaw);
8000
+ const id = accountIdRaw.trim();
8001
+ if (!id) {
8002
+ throw new AccountResolutionError("unknown_account", "account-id must be non-empty", {
8003
+ provider,
8004
+ account: accountIdRaw,
8005
+ enabled: [],
8006
+ });
8007
+ }
8008
+ const hostname = opts.host === undefined ? ctx.hostname().trim() : opts.host.trim();
8009
+ if (!hostname || (opts.host !== undefined && hostname !== opts.host)) {
8010
+ throw new LanesError("invalid_host", "--host must be a non-empty hostname without surrounding whitespace", {
8011
+ host: opts.host ?? "",
8012
+ });
8013
+ }
8014
+ if (!homeRaw.trim()) {
8015
+ throw new LanesError("invalid_account_home", "home path must be a non-empty path", {
8016
+ provider,
8017
+ account: id,
8018
+ });
8019
+ }
8020
+ const config = loadLanesConfig(hqRoot);
8021
+ const account = config.providers[provider].accounts[id];
8022
+ if (account === undefined) {
8023
+ throw new AccountResolutionError("unknown_account", `Unknown account ${JSON.stringify(id)} for ${provider}`, {
8024
+ provider,
8025
+ account: id,
8026
+ enabled: Object.keys(config.providers[provider].accounts).filter((key) => config.providers[provider].accounts[key]?.enabled),
8027
+ });
8028
+ }
8029
+ const homePath = homeRaw.trim();
8030
+ const isCurrentHost = hostname === ctx.hostname().trim();
8031
+ let home;
8032
+ if (isCurrentHost) {
8033
+ const homeResolved = homePath.startsWith("~") || path.isAbsolute(homePath)
8034
+ ? homePath
8035
+ : resolveCallerPath(callerCwd, homePath);
8036
+ home = canonicalizeHome(homeResolved, {
8037
+ homedir: ctx.homedir,
8038
+ realpathSync: (p) => fs.realpathSync(p),
8039
+ existsSync: (p) => fs.existsSync(p),
8040
+ });
8041
+ }
8042
+ else if (homePath === "~" || homePath.startsWith("~/")) {
8043
+ home = homePath;
8044
+ }
8045
+ else if (path.isAbsolute(homePath)) {
8046
+ home = path.normalize(homePath);
8047
+ }
8048
+ else {
8049
+ throw new LanesError("invalid_remote_account_home", "A home for another host must be an absolute path or start with ~/", { provider, account: id, hostname, home: homePath });
8050
+ }
8051
+ for (const [otherId, other] of Object.entries(config.providers[provider].accounts)) {
8052
+ if (otherId !== id && effectiveAccountHome(other, hostname).home === home) {
8053
+ throw new LanesError("duplicate_account_home", `Home ${home} is already registered as ${provider} account ${otherId} on host ${hostname}`, { provider, accounts: [otherId, id], home, hostname });
8054
+ }
8055
+ }
8056
+ if (home === account.home) {
8057
+ if (account.hosts !== undefined) {
8058
+ delete account.hosts[hostname];
8059
+ if (Object.keys(account.hosts).length === 0)
8060
+ delete account.hosts;
8061
+ }
8062
+ }
8063
+ else {
8064
+ const hosts = Object.assign(Object.create(null), account.hosts ?? {});
8065
+ hosts[hostname] = { home };
8066
+ account.hosts = hosts;
8067
+ }
8068
+ assertUniqueAccountHomes(provider, config.providers[provider].accounts);
8069
+ writeLanesConfig(hqRoot, config);
8070
+ printJson({
8071
+ ok: true,
8072
+ action: "accounts.set-home",
8073
+ provider,
8074
+ account: id,
8075
+ hostname,
8076
+ home,
8077
+ home_source: home === account.home ? "default" : "host",
8078
+ });
8079
+ }
7919
8080
  async function runAccountsEnable(ctx, providerRaw, accountId) {
7920
8081
  const hqRoot = ctx.hqRoot();
7921
8082
  const provider = parseLaneProvider(providerRaw);
@@ -7932,6 +8093,7 @@ async function runAccountsEnable(ctx, providerRaw, accountId) {
7932
8093
  }
7933
8094
  const status = accountStatus(provider, accountId, config, {
7934
8095
  existsSync: ctx.existsSync,
8096
+ hostname: ctx.hostname(),
7935
8097
  probe: ctx.probeProvider,
7936
8098
  env: ctx.env,
7937
8099
  });
@@ -7948,6 +8110,7 @@ async function runAccountsEnable(ctx, providerRaw, accountId) {
7948
8110
  action: "accounts.enable",
7949
8111
  account: accountStatus(provider, accountId, loadLanesConfig(hqRoot), {
7950
8112
  existsSync: ctx.existsSync,
8113
+ hostname: ctx.hostname(),
7951
8114
  probe: ctx.probeProvider,
7952
8115
  env: ctx.env,
7953
8116
  }),
@@ -7976,6 +8139,7 @@ async function runAccountsDisable(ctx, providerRaw, accountId, opts) {
7976
8139
  action: "accounts.disable",
7977
8140
  account: accountStatus(provider, accountId, loadLanesConfig(hqRoot), {
7978
8141
  existsSync: ctx.existsSync,
8142
+ hostname: ctx.hostname(),
7979
8143
  probe: ctx.probeProvider,
7980
8144
  env: ctx.env,
7981
8145
  }),
@@ -8490,6 +8654,17 @@ export function registerLanesCommand(parent, deps = {}) {
8490
8654
  .action(wrap(async (opts) => {
8491
8655
  await runAccountsBalance(ctx, opts);
8492
8656
  }));
8657
+ accounts
8658
+ .command("set-home")
8659
+ .description("Set the host-specific home for an existing provider account")
8660
+ .argument("<provider>", "claude | codex | grok")
8661
+ .argument("<account-id>", "Existing account id")
8662
+ .argument("<path>", "Home directory for this host")
8663
+ .option("--host <hostname>", "Host key (defaults to this host's hostname)")
8664
+ .option("--json", "JSON on stdout (always)")
8665
+ .action(wrap(async (provider, accountId, home, opts) => {
8666
+ await runAccountsSetHome(ctx, provider, accountId, home, opts);
8667
+ }));
8493
8668
  accounts
8494
8669
  .command("login")
8495
8670
  .description("Create a home, run the vendor login, and register the account")
@@ -0,0 +1,8 @@
1
+ import type { AccountConfig } from "./types.js";
2
+ export type EffectiveAccountHome = {
3
+ home: string;
4
+ source: "host" | "default";
5
+ };
6
+ /** Resolve the account home for this host; an empty host identity fails back to the default. */
7
+ export declare function effectiveAccountHome(account: AccountConfig, hostname?: string): EffectiveAccountHome;
8
+ //# sourceMappingURL=account-home.d.ts.map
@@ -0,0 +1,10 @@
1
+ import * as os from "node:os";
2
+ /** Resolve the account home for this host; an empty host identity fails back to the default. */
3
+ export function effectiveAccountHome(account, hostname = os.hostname()) {
4
+ const host = hostname.trim();
5
+ const hostHome = host ? account.hosts?.[host]?.home : undefined;
6
+ return hostHome === undefined
7
+ ? { home: account.home, source: "default" }
8
+ : { home: hostHome, source: "host" };
9
+ }
10
+ //# sourceMappingURL=account-home.js.map