@indigoai-us/hq-cli 5.345.47 → 5.345.49

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 (49) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +2 -1
  3. package/assets/scaffold/core/scripts/hq-session.sh +113 -7
  4. package/assets/scaffold/core/scripts/lib/session-scope-capability.sh +101 -0
  5. package/dist/command-catalog.generated.d.ts +12 -0
  6. package/dist/command-catalog.generated.js +15 -0
  7. package/dist/commands/anywhere-runtime-flag.js +5 -3
  8. package/dist/commands/core-hq-session-native.d.ts +2 -0
  9. package/dist/commands/core-hq-session-native.js +242 -52
  10. package/dist/commands/core-resolve-company-native.d.ts +1 -0
  11. package/dist/commands/core-resolve-company-native.js +41 -11
  12. package/dist/commands/feedback.d.ts +10 -0
  13. package/dist/commands/feedback.js +45 -0
  14. package/dist/commands/lanes-message.js +2 -2
  15. package/dist/commands/lanes.js +2 -2
  16. package/dist/commands/mcp-serve.js +17 -4
  17. package/dist/commands/secrets.d.ts +5 -0
  18. package/dist/commands/secrets.js +181 -2
  19. package/dist/commands/workers.d.ts +4 -1
  20. package/dist/commands/workers.js +57 -15
  21. package/dist/lib/anywhere-person-setting.d.ts +1 -0
  22. package/dist/lib/anywhere-person-setting.js +12 -0
  23. package/dist/lib/anywhere-runtime-fast-flag.js +5 -3
  24. package/dist/lib/anywhere-runtime-flag-key.d.ts +3 -1
  25. package/dist/lib/anywhere-runtime-flag-key.js +6 -1
  26. package/dist/lib/cli-flag-keys.d.ts +1 -0
  27. package/dist/lib/cli-flag-keys.js +1 -0
  28. package/dist/lib/core-utils/session-id.d.ts +6 -0
  29. package/dist/lib/core-utils/session-id.js +35 -0
  30. package/dist/lib/daemon/catalog.js +1 -1
  31. package/dist/lib/daemon/flag.d.ts +1 -1
  32. package/dist/lib/daemon/flag.js +20 -9
  33. package/dist/lib/daemon/lanes-reconcile-guard.d.ts +6 -5
  34. package/dist/lib/daemon/lanes-reconcile-guard.js +26 -7
  35. package/dist/lib/daemon/run.d.ts +6 -0
  36. package/dist/lib/daemon/run.js +36 -9
  37. package/dist/lib/daemon/server.d.ts +1 -1
  38. package/dist/lib/daemon/supervisor.d.ts +7 -1
  39. package/dist/lib/daemon/supervisor.js +50 -4
  40. package/dist/lib/lanes/reconcile.js +6 -4
  41. package/dist/lib/lanes/registry-flag.d.ts +11 -1
  42. package/dist/lib/lanes/registry-flag.js +68 -29
  43. package/dist/lib/lanes/watcher.js +63 -2
  44. package/dist/utils/resident-package-preload.js +8 -1
  45. package/dist/utils/self-update.js +6 -5
  46. package/dist/utils/version-gate.d.ts +11 -4
  47. package/dist/utils/version-gate.js +132 -41
  48. package/docs/anywhere-runtime.md +5 -3
  49. package/package.json +1 -1
@@ -13,7 +13,7 @@ import { artifactIdentityKeys, rollbackStorySwitchAttribution, storySwitchNotice
13
13
  import { laneRunDir, probePidRecord, readPidRecord, readProcStarttime, spawnLaneWorker } from "../lib/lanes/spawn.js";
14
14
  import { findRetiredLaneEntry, keepAliveExclusion, keepAliveSupersededBy, LANE_RUNNING_SUPERSEDED_MESSAGE_DELIVERY_FLAG, LANE_SUPERSESSION_LINEAGE_AWARE_FLAG, loadRetiredLaneIds } from "../lib/lanes/keep-alive.js";
15
15
  import { isTerminalLaneState } from "../lib/lanes/types.js";
16
- import { readLanesRegistryFlag } from "../lib/lanes/registry-flag.js";
16
+ import { readLanesRegistryFlag, readLanesRegistryFlagFailOpen } from "../lib/lanes/registry-flag.js";
17
17
  import { appendLaneFocusOverride, focusGuardRefusal, makeFocusOverride } from "../lib/lanes/focus-guard.js";
18
18
  import { appendProjectFocusOverride, removeProjectFocusOverride } from "../lib/lanes/story-ledger.js";
19
19
  import { resolveLaneCompany } from "../lib/lanes/lane-company.js";
@@ -192,7 +192,7 @@ export function createLaneMessageHandlers(deps) {
192
192
  : "superseded";
193
193
  }
194
194
  if (exclusion === "superseded" && currentLane.state !== "done" && currentLane.state !== "interrupted") {
195
- const runningSupersededMessageDeliveryEnabled = await readLanesRegistryFlag(LANE_RUNNING_SUPERSEDED_MESSAGE_DELIVERY_FLAG, { company: companyForLane(currentLane), env: ctx.env });
195
+ const runningSupersededMessageDeliveryEnabled = await readLanesRegistryFlagFailOpen(LANE_RUNNING_SUPERSEDED_MESSAGE_DELIVERY_FLAG, { company: companyForLane(currentLane), env: ctx.env });
196
196
  if (runningSupersededMessageDeliveryEnabled) {
197
197
  const latestLane = readLane(ctx.hqRoot(), currentLane.lane_id);
198
198
  if (latestLane.state === "killed" || latestLane.state === "cancelled") {
@@ -36,7 +36,7 @@ import { resolveLanesHqRoot } from "../lib/lanes/hq-root.js";
36
36
  import { laneDepthCap } from "../lib/lanes/depth-cap.js";
37
37
  import { isSsmRunCommandProcess } from "../lib/lanes/ssm-command-context.js";
38
38
  import { runResidentCommandWithPackageLease } from "../utils/resident-package-preload.js";
39
- import { readLanesFlagLocalOverride, readLanesRegistryFlag, readLanesRegistryFlagFailOff } from "../lib/lanes/registry-flag.js";
39
+ import { readLanesFlagLocalOverride, readLanesRegistryFlag, readLanesRegistryFlagFailOff, readLanesRegistryFlagFailOpen } from "../lib/lanes/registry-flag.js";
40
40
  import { appendProjectFocusOverride, readProjectStoryFocusData, removeProjectFocusOverride } from "../lib/lanes/story-ledger.js";
41
41
  import { appendLaneFocusOverride, assertFocusGuardAllows, makeFocusOverride } from "../lib/lanes/focus-guard.js";
42
42
  import { LANE_FOCUS_GUARD_FLAG, LANE_FOCUS_DASHBOARD_FLAG, LANE_HIRE_GRANT_FINISHED_LANES_FLAG, LANE_LIST_SKIP_REMOTE_REFRESH_FLAG, LANE_MESSAGE_ROUTE_OWNER_HOST_FLAG, LANE_ORPHANED_BARE_ID_DROPBOX_REPAIR_FLAG, LANE_PROFILE_PIN_EXHAUSTED_FALLBACK_FLAG, LANE_REUSE_REFRESHES_PROFILE_FLAG, } from "../lib/lanes/flag-keys.js";
@@ -1898,7 +1898,7 @@ async function resolveMessageLaneTarget(ctx, laneId) {
1898
1898
  const localOverride = readLanesFlagLocalOverride(LANE_MESSAGE_ROUTE_OWNER_HOST_FLAG, ctx.env);
1899
1899
  return localOverride ?? "check-company";
1900
1900
  }
1901
- return readLanesRegistryFlag(LANE_MESSAGE_ROUTE_OWNER_HOST_FLAG, {
1901
+ return readLanesRegistryFlagFailOpen(LANE_MESSAGE_ROUTE_OWNER_HOST_FLAG, {
1902
1902
  company,
1903
1903
  env: ctx.env,
1904
1904
  });
@@ -16,12 +16,16 @@ import * as os from "node:os";
16
16
  import * as path from "node:path";
17
17
  import { CLI_VERSION } from "../cli-version.js";
18
18
  import { HQ_ANYWHERE_RUNTIME_FLAG } from "../lib/anywhere-runtime-flag-key.js";
19
- import { anywhereRuntimeAllowed } from "../lib/anywhere-person-setting.js";
20
19
  import { createLineSplitter, HqdClientError, hqdRequest, hqdSocketPath } from "../lib/daemon/protocol.js";
21
20
  import { releaseCurrentPackageUseLease, releasePackageUseLeaseAfterResidentPreload } from "../utils/package-use-lease.js";
22
21
  import { preloadResidentPackageModules, reportResidentPackagePreloadFailure, RESIDENT_PACKAGE_MODULES } from "../utils/resident-package-preload.js";
23
22
  export const MCP_PROTOCOL_VERSIONS = ["2025-06-18", "2025-03-26", "2024-11-05"];
24
23
  const UNREACHABLE_CODES = new Set(["ENOENT", "ECONNREFUSED", "ECONNRESET", "EPIPE", "ETIMEDOUT", "ESOCKET", "EACCES"]);
24
+ function hasPersonTokenCache(env, home) {
25
+ // Mirrors personTokenCacheFile() in utils/cognito-session.ts; duplicated so this path does not load that module.
26
+ const stateDir = env.HQ_STATE_DIR?.trim() || path.join(home, ".hq");
27
+ return fs.existsSync(path.join(stateDir, "cognito-tokens.json"));
28
+ }
25
29
  /** HQ root for prompts: HQ_ROOT, else the ~/.hq/root pointer written by `hq install --global`. */
26
30
  export function readHqRoot(env, home) {
27
31
  const fromEnv = env.HQ_ROOT?.trim();
@@ -54,9 +58,18 @@ export async function runMcpServe(opts = {}) {
54
58
  const cwd = opts.cwd ?? process.cwd();
55
59
  const socketPath = hqdSocketPath(home, env);
56
60
  const timeoutMs = opts.timeoutMs ?? 120_000;
57
- if (!(await (opts.runtimeAllowed ?? (() => anywhereRuntimeAllowed(env, {
58
- onError: (name) => process.stderr.write(`HQ Anywhere person setting unavailable (${name}); access remains off.\n`),
59
- })))())) {
61
+ const runtimeAllowed = opts.runtimeAllowed ?? (async () => {
62
+ // A person token is required to read the opt-in setting. Deny a fresh
63
+ // install before loading the hq-flags and Cognito readers.
64
+ if (!hasPersonTokenCache(env, home))
65
+ return false;
66
+ const { anywhereRuntimeAllowed } = await import("../lib/anywhere-person-setting.js");
67
+ return anywhereRuntimeAllowed(env, {
68
+ home,
69
+ onError: (name) => process.stderr.write(`HQ Anywhere person setting unavailable (${name}); access remains off.\n`),
70
+ });
71
+ });
72
+ if (!(await runtimeAllowed())) {
60
73
  process.stderr.write(`HQ Anywhere is not enabled. The ${HQ_ANYWHERE_RUNTIME_FLAG} flag and your HQ Anywhere person setting must both be on.\n`);
61
74
  process.exitCode = 1;
62
75
  return;
@@ -2,6 +2,11 @@ import type { Command } from "commander";
2
2
  export { buildSecretUsage, emitEnvExportsAndExit, loadRevealedSecrets, parseSecretNameList, runSecretsEnvCommand, } from "../secrets-env-action.js";
3
3
  export type { SecretLoadResponse, SecretScriptLockMode, SecretTier, SecretUsage, SecretUsageChannel } from "../secrets-env-action.js";
4
4
  export type { SecretScriptUsage, SecretMetadata, SecretLoadSuccessRow } from "../secrets-env-action.js";
5
+ type SessionCompanyLock = {
6
+ sessionId: string;
7
+ companies: string[];
8
+ } | undefined;
9
+ export declare function readSessionCompanyLock(env?: NodeJS.ProcessEnv): SessionCompanyLock;
5
10
  import { vaultApiFetch, getCompanyUid, getEntityUid } from "../utils/vault-api.js";
6
11
  export type { VaultApiOptions } from "../utils/vault-api.js";
7
12
  export { vaultApiFetch, getCompanyUid, getEntityUid };
@@ -1,9 +1,153 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import * as yaml from "js-yaml";
1
4
  import { buildSecretUsage, extractApiMessage, highSecuritySandboxOnlyMessage, loadRevealedSecrets, normalizeScriptLockMode, normalizeSecretTier, parseSecretNameList, runSecretsEnvCommand, scopeOpts, secretsCredentialOptions, } from "../secrets-env-action.js";
2
5
  export { buildSecretUsage, emitEnvExportsAndExit, loadRevealedSecrets, parseSecretNameList, runSecretsEnvCommand, } from "../secrets-env-action.js";
6
+ export function readSessionCompanyLock(env = process.env) {
7
+ const root = resolveDefaultHqRoot({ env });
8
+ const sessionId = resolveSessionId(root, env);
9
+ if (!sessionId)
10
+ return undefined;
11
+ const sessionDir = path.join(root, "workspace/sessions", sessionId);
12
+ const capabilityPath = path.join(sessionDir, "scope-capability.json");
13
+ const metaPath = path.join(sessionDir, "meta.yaml");
14
+ let primary;
15
+ let companies;
16
+ let hasCapability = false;
17
+ try {
18
+ fs.lstatSync(capabilityPath);
19
+ hasCapability = true;
20
+ }
21
+ catch (error) {
22
+ const code = error.code;
23
+ if (code !== "ENOENT" && code !== "ENOTDIR")
24
+ throw new Error("Cannot verify this session's company lock because its scope capability is unreadable.");
25
+ }
26
+ if (hasCapability) {
27
+ let value;
28
+ try {
29
+ value = JSON.parse(fs.readFileSync(capabilityPath, "utf8"));
30
+ }
31
+ catch {
32
+ throw new Error("Cannot verify this session's company lock because its scope capability is unreadable.");
33
+ }
34
+ if (!value || typeof value !== "object" || Array.isArray(value))
35
+ throw new Error("Cannot verify this session's company lock because its scope capability is invalid.");
36
+ const cap = value;
37
+ if (cap.session_id !== sessionId)
38
+ throw new Error("Cannot verify this session's company lock because its scope capability does not match the active session.");
39
+ if (typeof cap.company_slug !== "string" || !/^[A-Za-z0-9_-]+$/.test(cap.company_slug))
40
+ throw new Error("Cannot verify this session's company lock because its scope capability is invalid.");
41
+ primary = cap.company_slug;
42
+ if (cap.company_slugs === undefined)
43
+ companies = [primary];
44
+ else if (Array.isArray(cap.company_slugs)
45
+ && cap.company_slugs.length > 0
46
+ && cap.company_slugs.every((slug) => typeof slug === "string" && /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/.test(slug))
47
+ && cap.company_slugs[0] === primary
48
+ && new Set(cap.company_slugs).size === cap.company_slugs.length
49
+ && ((primary === "personal" && cap.company_slugs.length === 1)
50
+ || (primary !== "personal" && !cap.company_slugs.includes("personal"))))
51
+ companies = [...cap.company_slugs];
52
+ else
53
+ throw new Error("Cannot verify this session's company lock because its scope capability is invalid.");
54
+ }
55
+ else {
56
+ try {
57
+ const meta = fs.readFileSync(metaPath, "utf8");
58
+ const read = (key) => meta.split(/\r?\n/).find((line) => new RegExp(`^\\s*${key}:`).test(line))?.replace(/^\s*[^:]+:\s*/, "").replace(/^"|"$/g, "") ?? "";
59
+ primary = read("company_slug");
60
+ const hasList = meta.split(/\r?\n/).some((line) => /^\s*company_slugs:/.test(line));
61
+ const listed = hasList ? read("company_slugs") : "";
62
+ if (hasList && (!listed || !primary))
63
+ throw new Error("invalid company lock");
64
+ companies = hasList ? listed.split(",") : primary ? [primary] : [];
65
+ if (companies.some((slug) => !/^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/.test(slug)))
66
+ throw new Error("invalid company lock");
67
+ if (new Set(companies).size !== companies.length)
68
+ throw new Error("invalid company lock");
69
+ if (primary && companies[0] !== primary)
70
+ throw new Error("invalid company lock");
71
+ if (companies.length > 1 && companies.includes("personal"))
72
+ throw new Error("invalid company lock");
73
+ }
74
+ catch (error) {
75
+ const code = error.code;
76
+ if (code === "ENOENT" || code === "ENOTDIR" || code === "EACCES")
77
+ return { sessionId, companies: [] };
78
+ throw new Error("Cannot verify this session's company lock because its metadata is invalid.");
79
+ }
80
+ }
81
+ const real = companies.filter((slug) => {
82
+ if (slug === "personal")
83
+ return true;
84
+ try {
85
+ const companyPath = path.join(root, "companies", slug);
86
+ const stat = fs.lstatSync(companyPath);
87
+ return stat.isDirectory() && !stat.isSymbolicLink();
88
+ }
89
+ catch {
90
+ return false;
91
+ }
92
+ });
93
+ if (primary && real[0] !== primary)
94
+ return { sessionId, companies: [] };
95
+ return { sessionId, companies: real };
96
+ }
97
+ function companyUidForSlug(root, slug) {
98
+ try {
99
+ const doc = yaml.load(fs.readFileSync(path.join(root, "companies", "manifest.yaml"), "utf8"));
100
+ const uid = doc.companies?.[slug]?.cloud_uid;
101
+ return typeof uid === "string" && looksLikeCompanyUid(uid) ? uid : undefined;
102
+ }
103
+ catch {
104
+ return undefined;
105
+ }
106
+ }
107
+ function companySlugForUid(root, companies, uid) {
108
+ if (!looksLikeCompanyUid(uid))
109
+ return undefined;
110
+ try {
111
+ const doc = yaml.load(fs.readFileSync(path.join(root, "companies", "manifest.yaml"), "utf8"));
112
+ return companies.find((slug) => doc.companies?.[slug]?.cloud_uid === uid);
113
+ }
114
+ catch {
115
+ return undefined;
116
+ }
117
+ }
118
+ async function resolveApiKeyCompanyWithinLock(input) {
119
+ let candidates = input.companies;
120
+ if (input.requestedCompany) {
121
+ const companySlug = input.companies.includes(input.requestedCompany)
122
+ ? input.requestedCompany
123
+ : companySlugForUid(input.root, input.companies, input.requestedCompany);
124
+ if (!companySlug) {
125
+ throw new Error("HQ_API_KEY is not bound to a company in this session's locked company set.");
126
+ }
127
+ candidates = [companySlug];
128
+ }
129
+ for (const companySlug of candidates) {
130
+ const companyUid = companyUidForSlug(input.root, companySlug);
131
+ if (!companyUid)
132
+ continue;
133
+ const response = await vaultApiFetch({ token: input.token, path: `/v1/keys/secrets/${encodeURIComponent(companyUid)}` });
134
+ if (response.ok)
135
+ return { companyUid, companySlug };
136
+ if (response.status === 401)
137
+ throw new Error("HQ_API_KEY is invalid or no longer active.");
138
+ if (response.status !== 403)
139
+ throw new Error(`Could not verify HQ_API_KEY company scope (HTTP ${response.status}).`);
140
+ }
141
+ throw new Error("HQ_API_KEY is not bound to a company in this session's locked company set.");
142
+ }
3
143
  import chalk from "chalk";
4
144
  import { ensureCognitoToken } from "../utils/cognito-session.js";
5
145
  import { removeCacheEntry, clearAllCache } from "../utils/secrets-cache.js";
6
146
  import { isHqCliChild, releaseChildWrapperPackageUseLeaseAfterStart } from "../utils/child-wrapper-lease.js";
147
+ import { resolveDefaultHqRoot } from "../utils/hq-root-resolver.js";
148
+ import { fastFlagEnabled } from "../native-fast-route.js";
149
+ import { MULTI_COMPANY_SESSION_LOCK_FLAG } from "../lib/cli-flag-keys.js";
150
+ import { resolveSessionId } from "../lib/core-utils/session-id.js";
7
151
  import { secretsExecChildExitCode } from "../utils/child-wrapper-exit.js";
8
152
  import { isPlanGateError } from "../utils/plan-gate-error.js";
9
153
  import { SECRET_NAME_PATTERN, GROUP_ID_PATTERN, EMAIL_PATTERN } from "./_patterns.js";
@@ -1085,6 +1229,7 @@ export function registerSecretsCommand(program) {
1085
1229
  .command("exec")
1086
1230
  .description("Run a command with secrets injected as env vars")
1087
1231
  .requiredOption("--only <keys>", "Secret names to inject (comma-separated; may be repeated) (required)", collectSecretNames)
1232
+ .option("--company <slug>", "Company slug (resolves to companyUid)")
1088
1233
  .option("--script <path>", "Attach local script identity for script-locked secrets")
1089
1234
  .option("--script-id <id>", "Stable script identifier approved by policy")
1090
1235
  .allowUnknownOption(true)
@@ -1104,10 +1249,44 @@ export function registerSecretsCommand(program) {
1104
1249
  process.exit(1);
1105
1250
  }
1106
1251
  const keys = parseSecretNameList(_opts.only);
1252
+ const multiCompanyLockEnabled = await fastFlagEnabled(MULTI_COMPANY_SESSION_LOCK_FLAG);
1253
+ const sessionLock = multiCompanyLockEnabled ? readSessionCompanyLock(process.env) : undefined;
1254
+ const parentScope = secrets.opts();
1255
+ const inheritedCompany = parentScope.company;
1256
+ const requestedCompany = _opts.company ?? inheritedCompany;
1257
+ const hasLockedCompanies = (sessionLock?.companies.length ?? 0) > 0;
1258
+ const lockRoot = hasLockedCompanies ? resolveDefaultHqRoot({ env: process.env }) : undefined;
1259
+ const selectedCompany = (hasLockedCompanies && requestedCompany && looksLikeCompanyUid(requestedCompany)
1260
+ ? companySlugForUid(lockRoot, sessionLock.companies, requestedCompany) ?? requestedCompany
1261
+ : requestedCompany)
1262
+ ?? (multiCompanyLockEnabled && !parentScope.personal ? sessionLock?.companies[0] : undefined);
1263
+ if (hasLockedCompanies && selectedCompany && !sessionLock.companies.includes(selectedCompany)) {
1264
+ throw new Error(`Company '${selectedCompany}' is outside this session's locked company set.`);
1265
+ }
1266
+ if (hasLockedCompanies && parentScope.personal && !sessionLock.companies.includes("personal")) {
1267
+ throw new Error("Personal scope is outside this session's locked company set.");
1268
+ }
1269
+ if (hasLockedCompanies && !selectedCompany && !parentScope.personal) {
1270
+ throw new Error("This session has no locked company scope for secrets exec.");
1271
+ }
1272
+ if (hasLockedCompanies && !requestedCompany && sessionLock.companies.length > 1) {
1273
+ console.error(`Using primary company ${sessionLock.companies[0]} from this session's lock; pass --company <slug> to choose another locked company.`);
1274
+ }
1275
+ const secretScope = selectedCompany === "personal"
1276
+ ? scopeOpts({ personal: true })
1277
+ : scopeOpts({ ...parentScope, ...(selectedCompany ? { company: selectedCompany } : {}) });
1107
1278
  const cred = await resolveVaultCredentialForCapability("secrets:read", "secrets exec", secretsCredentialOptions());
1279
+ const apiKeyCompany = cred.kind === "api-key" && hasLockedCompanies
1280
+ ? await resolveApiKeyCompanyWithinLock({
1281
+ token: cred.token,
1282
+ root: lockRoot,
1283
+ companies: sessionLock.companies,
1284
+ ...(requestedCompany ? { requestedCompany } : {}),
1285
+ })
1286
+ : undefined;
1108
1287
  const companyUid = cred.kind === "api-key"
1109
- ? "__api_key__"
1110
- : await getEntityUid(cred.token, scopeOpts(secrets.opts()), { allowOfflineScopeFallback: true });
1288
+ ? (apiKeyCompany?.companyUid ?? "__api_key__")
1289
+ : await getEntityUid(cred.token, secretScope, { allowOfflineScopeFallback: true });
1111
1290
  const readSecrets = async () => loadRevealedSecrets(cred.token, companyUid, keys, cred.kind === "cognito"
1112
1291
  ? await buildSecretUsage("exec", _opts.script, _opts.scriptId)
1113
1292
  : undefined, { allowOfflineFallback: true });
@@ -6,12 +6,15 @@ import { type RegistryWorker } from "../lib/workers-registry/read.js";
6
6
  * company_slug. Undefined when no session context is set. Pure/read-only.
7
7
  */
8
8
  export declare function resolveActiveCompany(hqRoot: string): string | undefined;
9
+ /** Resolve the active ordered lock set; default-off preserves primary-only access. */
10
+ export declare function resolveActiveCompanies(hqRoot: string, multiCompanyLockEnabled: boolean): string[] | undefined;
11
+ export declare function denyWorkerCompanyOutsideLock(company: string, lockedCompanies: string[] | undefined): boolean;
9
12
  /**
10
13
  * Membership-aware access filter for worker discovery: public workers always;
11
14
  * company workers only for the active company. Mirrors the /run skill and the
12
15
  * inject-worker-suggestion hook so all three surfaces agree.
13
16
  */
14
- export declare function filterAccessibleWorkers(workers: RegistryWorker[], activeCompany: string | undefined): RegistryWorker[];
17
+ export declare function filterAccessibleWorkers(workers: RegistryWorker[], activeCompany: string | string[] | undefined): RegistryWorker[];
15
18
  /**
16
19
  * The company-relative vault prefix for a worker, e.g.
17
20
  * companies/indigo/workers/deal-brain/ -> workers/deal-brain/. Returns null for
@@ -7,6 +7,9 @@ import { ensureCognitoToken } from "../utils/cognito-session.js";
7
7
  import { vaultApiFetch, getCompanyUid } from "./secrets.js";
8
8
  import { GROUP_ID_PATTERN, EMAIL_PATTERN, toSharedFolderGlob } from "./_patterns.js";
9
9
  import { findHqRoot } from "../utils/manifest.js";
10
+ import { fastFlagEnabled } from "../native-fast-route.js";
11
+ import { MULTI_COMPANY_SESSION_LOCK_FLAG } from "../lib/cli-flag-keys.js";
12
+ import { resolveSessionId } from "../lib/core-utils/session-id.js";
10
13
  export { readWorkerRegistry } from "../lib/workers-registry/read.js";
11
14
  import { readWorkerRegistry } from "../lib/workers-registry/read.js";
12
15
  /**
@@ -14,34 +17,68 @@ import { readWorkerRegistry } from "../lib/workers-registry/read.js";
14
17
  * company_slug. Undefined when no session context is set. Pure/read-only.
15
18
  */
16
19
  export function resolveActiveCompany(hqRoot) {
20
+ return resolveActiveCompanies(hqRoot, false)?.[0];
21
+ }
22
+ /** Resolve the active ordered lock set; default-off preserves primary-only access. */
23
+ export function resolveActiveCompanies(hqRoot, multiCompanyLockEnabled) {
24
+ const current = resolveSessionId(hqRoot);
25
+ if (!current)
26
+ return undefined;
17
27
  try {
18
- const currentFile = path.join(hqRoot, "workspace/sessions/.current");
19
- const current = fs.readFileSync(currentFile, "utf8").trim();
20
- if (!current)
21
- return undefined;
22
28
  const metaPath = path.join(hqRoot, "workspace/sessions", current, "meta.yaml");
23
29
  if (!fs.existsSync(metaPath))
24
- return undefined;
30
+ return multiCompanyLockEnabled ? [] : undefined;
25
31
  const meta = yaml.load(fs.readFileSync(metaPath, "utf8"));
26
- const co = meta?.company_slug;
27
- return typeof co === "string" && co ? co : undefined;
32
+ const primary = meta?.company_slug;
33
+ if (typeof primary !== "string" || !/^[A-Za-z0-9_-]+$/.test(primary))
34
+ return multiCompanyLockEnabled ? [] : undefined;
35
+ const rawListed = meta?.company_slugs;
36
+ const listed = multiCompanyLockEnabled && typeof rawListed === "string"
37
+ ? rawListed.split(",")
38
+ : [primary];
39
+ if (!multiCompanyLockEnabled)
40
+ return [primary];
41
+ const companies = [...new Set(listed.filter((slug) => {
42
+ if (typeof slug !== "string" || !/^[A-Za-z0-9_-]+$/.test(slug))
43
+ return false;
44
+ if (slug === "personal")
45
+ return true;
46
+ try {
47
+ const stat = fs.lstatSync(path.join(hqRoot, "companies", slug));
48
+ return stat.isDirectory() && !stat.isSymbolicLink();
49
+ }
50
+ catch {
51
+ return false;
52
+ }
53
+ }))];
54
+ if (companies.length > 1 && companies.includes("personal"))
55
+ return [primary];
56
+ return companies[0] === primary ? companies : undefined;
28
57
  }
29
58
  catch {
30
- return undefined;
59
+ return multiCompanyLockEnabled ? [] : undefined;
31
60
  }
32
61
  }
62
+ export function denyWorkerCompanyOutsideLock(company, lockedCompanies) {
63
+ if (lockedCompanies === undefined || lockedCompanies.includes(company))
64
+ return false;
65
+ console.error(`Company '${company}' is outside this session's locked company set.`);
66
+ process.exitCode = 1;
67
+ return true;
68
+ }
33
69
  /**
34
70
  * Membership-aware access filter for worker discovery: public workers always;
35
71
  * company workers only for the active company. Mirrors the /run skill and the
36
72
  * inject-worker-suggestion hook so all three surfaces agree.
37
73
  */
38
74
  export function filterAccessibleWorkers(workers, activeCompany) {
75
+ const allowed = new Set(Array.isArray(activeCompany) ? activeCompany : activeCompany ? [activeCompany] : []);
39
76
  return workers.filter((w) => {
40
77
  if (w.visibility === "public")
41
78
  return true;
42
79
  if (!w.company)
43
80
  return false;
44
- return activeCompany !== undefined && w.company === activeCompany;
81
+ return allowed.has(w.company);
45
82
  });
46
83
  }
47
84
  /**
@@ -159,14 +196,18 @@ async function runWorkersShare(workerId, opts) {
159
196
  console.log(chalk.green("✓"), `Shared worker '${workerId}' with ${classified.label} (${permission}).`);
160
197
  console.log(chalk.dim(` Vault prefix ${prefix} granted in company '${companySlug}'. It will sync to granted members and appear in their /run list.`));
161
198
  }
162
- function runWorkersList(opts) {
199
+ async function runWorkersList(opts) {
163
200
  const hqRoot = findHqRoot();
164
- const activeCompany = opts.company ?? resolveActiveCompany(hqRoot);
165
- let workers = filterAccessibleWorkers(readWorkerRegistry(hqRoot), activeCompany);
201
+ const enabled = await fastFlagEnabled(MULTI_COMPANY_SESSION_LOCK_FLAG);
202
+ const lockedCompanies = resolveActiveCompanies(hqRoot, enabled);
203
+ if (enabled && opts.company && denyWorkerCompanyOutsideLock(opts.company, lockedCompanies))
204
+ return;
205
+ const activeCompanies = opts.company ? [opts.company] : lockedCompanies;
206
+ let workers = filterAccessibleWorkers(readWorkerRegistry(hqRoot), activeCompanies);
166
207
  if (opts.shared)
167
208
  workers = workers.filter((w) => Boolean(w.grants && w.grants.trim()));
168
209
  if (opts.mine)
169
- workers = workers.filter((w) => Boolean(w.company) && w.company === activeCompany);
210
+ workers = workers.filter((w) => Boolean(w.company) && activeCompanies?.includes(w.company) === true);
170
211
  if (workers.length === 0) {
171
212
  console.log("No accessible workers.");
172
213
  return;
@@ -185,8 +226,9 @@ function runWorkersList(opts) {
185
226
  }
186
227
  };
187
228
  printGroup("Public", publicWorkers);
188
- if (activeCompany)
189
- printGroup(activeCompany, companyWorkers);
229
+ for (const company of activeCompanies ?? []) {
230
+ printGroup(company, companyWorkers.filter((worker) => worker.company === company));
231
+ }
190
232
  console.log(chalk.dim("\nUsage: hq run {worker-id} [skill] [args]"));
191
233
  console.log(chalk.dim("Share a company worker: hq workers share {worker-id} --with {grp_<name>|@all} --permission read"));
192
234
  }
@@ -6,6 +6,7 @@ export interface ReadHqAnywherePersonSettingOptions {
6
6
  fetchImpl?: typeof fetch;
7
7
  apiBaseUrl?: string;
8
8
  env?: NodeJS.ProcessEnv;
9
+ home?: string;
9
10
  onError?: (errorName: string) => void;
10
11
  }
11
12
  /** Returns null when the person preference cannot be read; callers must fail closed. */
@@ -1,8 +1,20 @@
1
+ import * as fs from "node:fs";
2
+ import * as os from "node:os";
3
+ import * as path from "node:path";
1
4
  import { anywhereRuntimeEnabledFromSnapshot } from "./anywhere-runtime-fast-flag.js";
5
+ function hasPersonTokenCache(env, home) {
6
+ // Mirrors personTokenCacheFile() in utils/cognito-session.ts; duplicated so this path does not load that module.
7
+ const stateDir = env.HQ_STATE_DIR?.trim() || path.join(home, ".hq");
8
+ return fs.existsSync(path.join(stateDir, "cognito-tokens.json"));
9
+ }
2
10
  /** Returns null when the person preference cannot be read; callers must fail closed. */
3
11
  export async function readHqAnywherePersonSetting(options = {}) {
4
12
  const env = options.env ?? process.env;
5
13
  try {
14
+ // Avoid loading the Cognito SDK on the common fresh-install path. There is
15
+ // no person token to refresh or use until a cache exists.
16
+ if (!options.getPersonToken && !hasPersonTokenCache(env, options.home ?? env.HOME ?? os.homedir()))
17
+ return null;
6
18
  const getPersonToken = options.getPersonToken ?? (async () => {
7
19
  const { ensureCognitoToken } = await import("../utils/cognito-session.js");
8
20
  return ensureCognitoToken({ interactive: false, tokenSource: "person" });
@@ -1,9 +1,11 @@
1
1
  import { readLocalOverride, resolveFlag } from "@indigoai-us/hq-flags-client";
2
2
  import { resolveProcessFlagCompanyContext } from "./process-flag-company-context.js";
3
3
  import { readFlagSnapshotFile, readPersonFlagSnapshotFile } from "./flag-snapshot-file.js";
4
- import { HQ_ANYWHERE_RUNTIME_FLAG } from "./anywhere-runtime-flag-key.js";
4
+ import { HQ_ANYWHERE_RUNTIME_FLAG, hqAnywhereRuntimeLocallyDisabled } from "./anywhere-runtime-flag-key.js";
5
5
  /** Resolve the Anywhere gate from hq-flags without loading the CLI's large worker bundle. */
6
6
  export function anywhereRuntimeEnabledFromSnapshot(env = process.env) {
7
+ if (hqAnywhereRuntimeLocallyDisabled(env))
8
+ return false;
7
9
  const local = readLocalOverride(HQ_ANYWHERE_RUNTIME_FLAG, env);
8
10
  if (local !== null)
9
11
  return local;
@@ -17,13 +19,13 @@ export function anywhereRuntimeEnabledFromSnapshot(env = process.env) {
17
19
  const snapshot = persisted
18
20
  ? { version: 0, flags: persisted.flags }
19
21
  : null;
20
- // HQ Anywhere authorizes a capability, so a missing or stale snapshot fails closed.
22
+ // The flag is a fail-safe kill switch: unknown registry state keeps HQ Anywhere enabled.
21
23
  return resolveFlag({
22
24
  flagKey: HQ_ANYWHERE_RUNTIME_FLAG,
23
25
  env,
24
26
  snapshot,
25
27
  companyIdentifiers: [company.companyUid, company.companySlug].filter((value) => Boolean(value)),
26
- lookup: { fallback: false },
28
+ lookup: { fallback: true },
27
29
  }).value;
28
30
  }
29
31
  //# sourceMappingURL=anywhere-runtime-fast-flag.js.map
@@ -1,3 +1,5 @@
1
- /** hq-flags key for the staged HQ Anywhere runtime. The registry default is off. */
1
+ /** hq-flags key for the staged HQ Anywhere runtime. The registry default is on. */
2
2
  export declare const HQ_ANYWHERE_RUNTIME_FLAG = "hq-anywhere-runtime";
3
+ /** The HQ Anywhere environment kill switch stays compatible with managed provider wrappers. */
4
+ export declare function hqAnywhereRuntimeLocallyDisabled(env?: NodeJS.ProcessEnv): boolean;
3
5
  //# sourceMappingURL=anywhere-runtime-flag-key.d.ts.map
@@ -1,3 +1,8 @@
1
- /** hq-flags key for the staged HQ Anywhere runtime. The registry default is off. */
1
+ /** hq-flags key for the staged HQ Anywhere runtime. The registry default is on. */
2
2
  export const HQ_ANYWHERE_RUNTIME_FLAG = "hq-anywhere-runtime";
3
+ /** The HQ Anywhere environment kill switch stays compatible with managed provider wrappers. */
4
+ export function hqAnywhereRuntimeLocallyDisabled(env = process.env) {
5
+ const value = env.HQ_FLAG_HQ_ANYWHERE_RUNTIME;
6
+ return value === "false" || value === "0";
7
+ }
3
8
  //# sourceMappingURL=anywhere-runtime-flag-key.js.map
@@ -1,3 +1,4 @@
1
1
  /** hq-flags keys used by CLI-wide routes. */
2
2
  export declare const CLI_COMMAND_HELP_CATALOG_FLAG = "cli.command-help-catalog";
3
+ export declare const MULTI_COMPANY_SESSION_LOCK_FLAG = "hooks.multi-company-session-lock";
3
4
  //# sourceMappingURL=cli-flag-keys.d.ts.map
@@ -1,3 +1,4 @@
1
1
  /** hq-flags keys used by CLI-wide routes. */
2
2
  export const CLI_COMMAND_HELP_CATALOG_FLAG = "cli.command-help-catalog";
3
+ export const MULTI_COMPANY_SESSION_LOCK_FLAG = "hooks.multi-company-session-lock";
3
4
  //# sourceMappingURL=cli-flag-keys.js.map
@@ -0,0 +1,6 @@
1
+ export declare const SESSION_ID_ENV_KEYS: readonly ["HQ_SESSION_ID", "CLAUDE_CODE_SESSION_ID", "CLAUDE_SESSION_ID", "CODEX_SESSION_ID", "CODEX_THREAD_ID", "GROK_SESSION_ID"];
2
+ export declare function validSessionId(value: string): boolean;
3
+ export declare function sessionIdFromEnv(env: NodeJS.ProcessEnv): string;
4
+ /** Resolve the caller's session before using the global, last-writer-wins .current fallback. */
5
+ export declare function resolveSessionId(root: string, env?: NodeJS.ProcessEnv, readFile?: (filePath: string, encoding: BufferEncoding) => string): string;
6
+ //# sourceMappingURL=session-id.d.ts.map
@@ -0,0 +1,35 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ export const SESSION_ID_ENV_KEYS = [
4
+ "HQ_SESSION_ID",
5
+ "CLAUDE_CODE_SESSION_ID",
6
+ "CLAUDE_SESSION_ID",
7
+ "CODEX_SESSION_ID",
8
+ "CODEX_THREAD_ID",
9
+ "GROK_SESSION_ID",
10
+ ];
11
+ export function validSessionId(value) {
12
+ return Boolean(value) && value !== "." && value !== ".." && /^[A-Za-z0-9._-]+$/.test(value);
13
+ }
14
+ export function sessionIdFromEnv(env) {
15
+ for (const key of SESSION_ID_ENV_KEYS) {
16
+ const value = (env[key] ?? "").replace(/[\t\n\v\f\r ]/g, "");
17
+ if (validSessionId(value))
18
+ return value;
19
+ }
20
+ return "";
21
+ }
22
+ /** Resolve the caller's session before using the global, last-writer-wins .current fallback. */
23
+ export function resolveSessionId(root, env = process.env, readFile = (filePath, encoding) => fs.readFileSync(filePath, encoding)) {
24
+ const fromEnv = sessionIdFromEnv(env);
25
+ if (fromEnv)
26
+ return fromEnv;
27
+ try {
28
+ const current = readFile(path.join(root, "workspace", "sessions", ".current"), "utf8").replace(/[\t\n\v\f\r ]/g, "");
29
+ return validSessionId(current) ? current : "";
30
+ }
31
+ catch {
32
+ return "";
33
+ }
34
+ }
35
+ //# sourceMappingURL=session-id.js.map
@@ -230,7 +230,7 @@ function planService(service, ctx) {
230
230
  return [
231
231
  base(service, {
232
232
  kind: "scheduled",
233
- // Polls every 15 s so keep-alive resumes are admitted on time (#1396).
233
+ // Polls every 15 s so local keep-alive resumes are admitted on time (#1396).
234
234
  // Each pass is a fresh `hq` node process (~1.8 s CPU, ~200 MB RSS), so
235
235
  // desktops first check the lane store in-process
236
236
  // (lanes-reconcile-guard.ts) and fall back to the pre-#1396 60 s
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Feature flags read by the daemon. The daemon runs for weeks, so each read
3
3
  * asks the registry again instead of holding a per-process answer, and it
4
- * reads as off unless the registry itself says on (or a local override does).
4
+ * reads as each flag's fallback unless the registry or a local override decides it.
5
5
  */
6
6
  import { type FlagClientOptions, type FlagResolution } from "@indigoai-us/hq-flags-client";
7
7
  /** Outposts hand running services to the next daemon on a CLI update instead of restarting them all. */