@indigoai-us/hq-cli 5.345.169 → 5.345.171

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 (140) hide show
  1. package/CHANGELOG.md +69 -0
  2. package/assets/scaffold/core/scripts/hq-delegate-send.sh +1 -1
  3. package/dist/command-catalog.generated.d.ts +181 -9
  4. package/dist/command-catalog.generated.js +224 -10
  5. package/dist/commands/access.d.ts +32 -1
  6. package/dist/commands/access.js +119 -5
  7. package/dist/commands/agent-kit.d.ts +1 -1
  8. package/dist/commands/agent-kit.js +3 -1
  9. package/dist/commands/agents.js +2 -1
  10. package/dist/commands/bot.d.ts +3 -0
  11. package/dist/commands/bot.js +141 -14
  12. package/dist/commands/cloud.d.ts +78 -1
  13. package/dist/commands/cloud.js +274 -9
  14. package/dist/commands/core-company-index-scope-flag.d.ts +8 -1
  15. package/dist/commands/core-company-index-scope-flag.js +9 -2
  16. package/dist/commands/core-delegate-w1b.js +5 -2
  17. package/dist/commands/core-dm-bind-native.js +19 -4
  18. package/dist/commands/core-rebuild-index-fast.js +24 -6
  19. package/dist/commands/core.js +18 -6
  20. package/dist/commands/dm.d.ts +17 -3
  21. package/dist/commands/dm.js +121 -26
  22. package/dist/commands/feedback-triage.d.ts +43 -1
  23. package/dist/commands/feedback-triage.js +123 -23
  24. package/dist/commands/feedback.d.ts +40 -1
  25. package/dist/commands/feedback.js +184 -4
  26. package/dist/commands/files.d.ts +42 -0
  27. package/dist/commands/files.js +223 -30
  28. package/dist/commands/integrations-api.d.ts +4 -0
  29. package/dist/commands/integrations-api.js +18 -2
  30. package/dist/commands/integrations-connect.d.ts +10 -3
  31. package/dist/commands/integrations-connect.js +70 -6
  32. package/dist/commands/integrations-core.d.ts +23 -0
  33. package/dist/commands/integrations-core.js +100 -0
  34. package/dist/commands/integrations-manage.js +8 -1
  35. package/dist/commands/integrations.js +49 -5
  36. package/dist/commands/lanes-loop.d.ts +20 -0
  37. package/dist/commands/lanes-loop.js +69 -11
  38. package/dist/commands/lanes.js +9 -1
  39. package/dist/commands/mesh.js +6 -0
  40. package/dist/commands/rescue.d.ts +9 -0
  41. package/dist/commands/rescue.js +31 -0
  42. package/dist/commands/run.js +115 -40
  43. package/dist/commands/scan.d.ts +5 -1
  44. package/dist/commands/scan.js +16 -4
  45. package/dist/commands/secrets.js +50 -9
  46. package/dist/commands/sync-manifest.d.ts +16 -0
  47. package/dist/commands/sync-manifest.js +17 -1
  48. package/dist/hq-cloud-sync-runner/bin/sync-runner.js +2367 -2109
  49. package/dist/hq-cloud-sync-runner/package.json +1 -1
  50. package/dist/lib/agent-kit/inbox-state.d.ts +17 -0
  51. package/dist/lib/agent-kit/inbox-state.js +41 -20
  52. package/dist/lib/agent-kit/kit-config.d.ts +2 -0
  53. package/dist/lib/agent-kit/kit-config.js +6 -0
  54. package/dist/lib/agent-kit/mcp/tools.js +12 -5
  55. package/dist/lib/agent-kit/run/heartbeat.d.ts +6 -3
  56. package/dist/lib/agent-kit/run/heartbeat.js +6 -3
  57. package/dist/lib/agent-kit/run/inbox.d.ts +8 -3
  58. package/dist/lib/agent-kit/run/inbox.js +42 -10
  59. package/dist/lib/agent-kit/run/request-timeout.d.ts +26 -0
  60. package/dist/lib/agent-kit/run/request-timeout.js +51 -0
  61. package/dist/lib/bot/api.d.ts +1 -1
  62. package/dist/lib/bot/config.d.ts +10 -0
  63. package/dist/lib/bot/config.js +17 -1
  64. package/dist/lib/bot/decisions.d.ts +62 -0
  65. package/dist/lib/bot/decisions.js +228 -0
  66. package/dist/lib/bot/fleet-local.d.ts +127 -0
  67. package/dist/lib/bot/fleet-local.js +638 -0
  68. package/dist/lib/bot/index.d.ts +2 -0
  69. package/dist/lib/bot/index.js +2 -0
  70. package/dist/lib/bot/local-config.d.ts +3 -2
  71. package/dist/lib/bot/local-config.js +5 -2
  72. package/dist/lib/bot/promote.d.ts +10 -0
  73. package/dist/lib/bot/promote.js +3 -2
  74. package/dist/lib/bot/run.d.ts +7 -0
  75. package/dist/lib/bot/run.js +104 -1
  76. package/dist/lib/bot/runtime/index.js +8 -4
  77. package/dist/lib/bot/runtime/scratch-dir.d.ts +24 -0
  78. package/dist/lib/bot/runtime/scratch-dir.js +41 -0
  79. package/dist/lib/caller-principal.d.ts +4 -0
  80. package/dist/lib/caller-principal.js +40 -0
  81. package/dist/lib/capacity/collectors/grok.js +8 -2
  82. package/dist/lib/cli-flag-keys.d.ts +2 -0
  83. package/dist/lib/cli-flag-keys.js +2 -0
  84. package/dist/lib/daemon/catalog.d.ts +4 -0
  85. package/dist/lib/daemon/catalog.js +63 -29
  86. package/dist/lib/daemon/config.d.ts +7 -0
  87. package/dist/lib/daemon/config.js +32 -0
  88. package/dist/lib/daemon/run.d.ts +3 -2
  89. package/dist/lib/daemon/run.js +23 -3
  90. package/dist/lib/daemon/supervisor.d.ts +1 -0
  91. package/dist/lib/daemon/supervisor.js +4 -0
  92. package/dist/lib/daemon/sync-core-canary.d.ts +12 -0
  93. package/dist/lib/daemon/sync-core-canary.js +61 -0
  94. package/dist/lib/doctor/checks/release-links.d.ts +62 -1
  95. package/dist/lib/doctor/checks/release-links.js +188 -2
  96. package/dist/lib/doctor/checks/sync-health.d.ts +32 -0
  97. package/dist/lib/doctor/checks/sync-health.js +54 -1
  98. package/dist/lib/doctor/fix/apply.d.ts +26 -4
  99. package/dist/lib/doctor/fix/apply.js +149 -11
  100. package/dist/lib/doctor/fix/remediation.d.ts +8 -1
  101. package/dist/lib/doctor/fix/remediation.js +22 -0
  102. package/dist/lib/doctor/types.d.ts +1 -1
  103. package/dist/lib/doctor/types.js +1 -0
  104. package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
  105. package/dist/lib/flag-registry-worker-source.generated.js +1 -1
  106. package/dist/lib/index-render/index.d.ts +9 -0
  107. package/dist/lib/index-render/index.js +65 -5
  108. package/dist/lib/index-render/session-scope.d.ts +69 -0
  109. package/dist/lib/index-render/session-scope.js +279 -0
  110. package/dist/lib/lanes/admission.js +7 -20
  111. package/dist/lib/lanes/reconcile.js +13 -15
  112. package/dist/lib/lanes/stale-lock.d.ts +50 -0
  113. package/dist/lib/lanes/stale-lock.js +105 -0
  114. package/dist/lib/lanes/store.js +11 -76
  115. package/dist/lib/mesh/live/daemon/work-subscribe.js +31 -0
  116. package/dist/lib/notify-recipient.d.ts +10 -4
  117. package/dist/lib/notify-recipient.js +16 -4
  118. package/dist/lib/secret-input-link.d.ts +42 -0
  119. package/dist/lib/secret-input-link.js +93 -0
  120. package/dist/lib/secret-scan/range.d.ts +57 -0
  121. package/dist/lib/secret-scan/range.js +266 -0
  122. package/dist/lib/secret-scan/repo-config.d.ts +18 -3
  123. package/dist/lib/secret-scan/repo-config.js +135 -2
  124. package/dist/lib/work-context/organize.d.ts +7 -0
  125. package/dist/lib/work-context/organize.js +21 -1
  126. package/dist/run/script-bound-schema.d.ts +27 -0
  127. package/dist/run/script-bound-schema.js +97 -0
  128. package/dist/secrets-env-action.d.ts +2 -0
  129. package/dist/secrets-env-action.js +7 -2
  130. package/dist/sentry-before-send.js +1 -0
  131. package/dist/utils/cognito-session.d.ts +3 -30
  132. package/dist/utils/cognito-session.js +2 -100
  133. package/dist/utils/machine-principal-state.d.ts +38 -0
  134. package/dist/utils/machine-principal-state.js +110 -0
  135. package/dist/utils/safe-link.js +9 -0
  136. package/dist/utils/script-bound-exec.d.ts +158 -0
  137. package/dist/utils/script-bound-exec.js +568 -0
  138. package/dist/utils/sync-watchdog.d.ts +92 -0
  139. package/dist/utils/sync-watchdog.js +250 -0
  140. package/package.json +8 -8
@@ -129,7 +129,7 @@ export function parseSecretNameList(input) {
129
129
  // refuse a key bound to a different company. Without it the fetch route reads
130
130
  // from the key's own company, which made `--company <other>` silently return
131
131
  // the key company's same-named secret (P0 triage, 2026-10-11).
132
- async function loadRevealedSecretsViaApiKey(token, keys, companyUid) {
132
+ async function loadRevealedSecretsViaApiKey(token, keys, companyUid, lockModes) {
133
133
  const resolved = new Map();
134
134
  const requested = [...new Set(keys)];
135
135
  const cacheScope = "__api_key__";
@@ -170,6 +170,10 @@ async function loadRevealedSecretsViaApiKey(token, keys, companyUid) {
170
170
  throw new Error(`Failed to fetch secret '${name}': malformed fetch response`);
171
171
  }
172
172
  removeCacheEntry(cacheScope, name);
173
+ // The API-key fetch route returns the secret's scriptLock (the vault denies
174
+ // an enforced secret on this route, since it carries no script usage).
175
+ const mode = secret.scriptLock?.mode;
176
+ lockModes?.set(name, typeof mode === "string" ? mode : undefined);
173
177
  resolved.set(name, secret.value);
174
178
  }
175
179
  return resolved;
@@ -219,7 +223,7 @@ export async function assertApiKeyCompany(token, requested, commandLabel) {
219
223
  }
220
224
  export async function loadRevealedSecrets(token, companyUid, keys, usage, options = {}) {
221
225
  if (token.startsWith(HQ_API_KEY_PREFIX)) {
222
- return loadRevealedSecretsViaApiKey(token, keys, companyUid);
226
+ return loadRevealedSecretsViaApiKey(token, keys, companyUid, options.lockModes);
223
227
  }
224
228
  const resolved = new Map();
225
229
  const requested = [...new Set(keys)];
@@ -284,6 +288,7 @@ export async function loadRevealedSecrets(token, companyUid, keys, usage, option
284
288
  throw new Error("Invalid secret load response from vault");
285
289
  }
286
290
  seenNames.add(s.name);
291
+ options.lockModes?.set(s.name, s.scriptLock?.mode);
287
292
  if (s.revokedAt != null) {
288
293
  errorsByName.set(s.name, {
289
294
  code: "revoked",
@@ -215,6 +215,7 @@ export function beforeSend(event, _hint) {
215
215
  "no-manifest-scopes",
216
216
  "never-uploaded",
217
217
  "invalid-upload-time",
218
+ "manifest-upload-rejected",
218
219
  "unresolved-company-uid",
219
220
  ]);
220
221
  const platforms = new Set([
@@ -19,11 +19,12 @@
19
19
  * HQ_VAULT_API_URL — vault-service API Gateway URL
20
20
  */
21
21
  import { isExpiring, isMachineIdentity, type CognitoAuthConfig, type CognitoTokens, type ClientInfo, type VaultServiceConfig } from "@indigoai-us/hq-cloud";
22
+ import { hasPersonCachedSession, isMachineCredsFileEnvSet, wantsMachineCognitoTokens, type CognitoTokenSource } from "./machine-principal-state.js";
22
23
  import { loadCachedTokens, personTokenCacheFile, readPersonTokenFile } from "./person-token-cache.js";
23
24
  export { loadCachedTokens, personTokenCacheFile, readPersonTokenFile };
24
25
  export { isExpiring, isMachineIdentity };
25
- /** Person browser/login cache vs machine-creds USER_PASSWORD_AUTH mint. */
26
- export type CognitoTokenSource = "person" | "machine";
26
+ export { hasPersonCachedSession, isMachineCredsFileEnvSet, wantsMachineCognitoTokens };
27
+ export type { CognitoTokenSource };
27
28
  /** Actor kind derived from the active token / machine-creds signal. */
28
29
  export type CognitoActorKind = "human" | "agent" | "unknown";
29
30
  /**
@@ -97,13 +98,6 @@ export declare const DEFAULT_HQ_ROOT: string;
97
98
  export declare function machineTokenStateDir(home?: string, env?: NodeJS.ProcessEnv): string;
98
99
  /** Absolute path of the machine-token cache file (daemon state dir). */
99
100
  export declare function machineTokenCacheFile(home?: string, env?: NodeJS.ProcessEnv): string;
100
- /**
101
- * True when `HQ_MACHINE_CREDS_FILE` is explicitly set (non-empty) in `env`.
102
- * Explicit env remains the primary opt-in for daemon units. Fresh agent boxes
103
- * without the env still mint via {@link wantsMachineCognitoTokens}'s
104
- * default-file fallback when no usable person session exists.
105
- */
106
- export declare function isMachineCredsFileEnvSet(env?: NodeJS.ProcessEnv): boolean;
107
101
  /** Default machine-creds path hq-cloud also checks when the env is unset. */
108
102
  export declare function defaultMachineCredsFile(home?: string): string;
109
103
  export type EnsureCognitoTokenOptions = {
@@ -127,33 +121,12 @@ export type EnsureCognitoTokenOptions = {
127
121
  export declare const HQ_NO_BROWSER_LOGIN_ENV = "HQ_NO_BROWSER_LOGIN";
128
122
  /** An explicit `interactive` option wins; otherwise interactive unless the env switch is "1". */
129
123
  export declare function resolveInteractiveLogin(options: Pick<EnsureCognitoTokenOptions, "interactive">, env?: NodeJS.ProcessEnv): boolean;
130
- /**
131
- * True when a usable person Cognito cache file exists under the effective
132
- * `HQ_STATE_DIR` (or `~/.hq`). Presence alone keeps human commands on the
133
- * person path even when the default machine-creds file is also on disk
134
- * (no-flip guarantee).
135
- */
136
- export declare function hasPersonCachedSession(env?: NodeJS.ProcessEnv): boolean;
137
124
  /**
138
125
  * Fresh-box / Outpost bootstrap: env unset, not forced to person, no person
139
126
  * cache, and the default machine-creds file is readable. Distinct from
140
127
  * explicit `HQ_MACHINE_CREDS_FILE` / `{ tokenSource: "machine" }` opt-in.
141
128
  */
142
129
  export declare function isDefaultMachineCredsFallback(options?: Pick<EnsureCognitoTokenOptions, "tokenSource">, env?: NodeJS.ProcessEnv): boolean;
143
- /**
144
- * Whether this call should mint/cache via machine creds (USER_PASSWORD_AUTH).
145
- *
146
- * Machine path when:
147
- * - explicit `{ tokenSource: "machine" }` and creds are readable, or
148
- * - `HQ_MACHINE_CREDS_FILE` is set and readable, or
149
- * - fresh-box fallback: env unset, no person cache, default
150
- * `~/.hq-agent/machine-creds.json` readable.
151
- *
152
- * Explicit `{ tokenSource: "person" }` never uses machine creds. A present
153
- * person cache keeps the person path even when the default creds file exists
154
- * (do not flip a human's commands onto machine tokens).
155
- */
156
- export declare function wantsMachineCognitoTokens(options?: Pick<EnsureCognitoTokenOptions, "tokenSource">, env?: NodeJS.ProcessEnv): boolean;
157
130
  /**
158
131
  * Which Cognito token source this process will use for doctor / status / whoami.
159
132
  * Matches {@link wantsMachineCognitoTokens}: env opt-in, or default-file
@@ -26,6 +26,7 @@ import * as path from "path";
26
26
  import * as yaml from "js-yaml";
27
27
  import chalk from "chalk";
28
28
  import { isExpiring, refreshTokens, saveCachedTokens, browserLogin, detectHqCoreVersion, isMachineIdentity, getValidMachineTokens, CognitoRefreshError, accessTokenFingerprint, invalidateCachedTokensByFingerprint, } from "@indigoai-us/hq-cloud";
29
+ import { hasPersonCachedSession, isMachineCredsFileEnvSet, probeMachineIdentity, wantsMachineCognitoTokens, } from "./machine-principal-state.js";
29
30
  import { CLI_NAME, CLI_VERSION } from "../cli-version.js";
30
31
  import { daemonDir } from "../lib/mesh/live/daemon/paths.js";
31
32
  import { workMeshRoot } from "../lib/mesh/live/paths.js";
@@ -36,6 +37,7 @@ export { loadCachedTokens, personTokenCacheFile, readPersonTokenFile };
36
37
  // Re-export the cached-session primitives for callers that must deliberately
37
38
  // avoid the refresh/login behavior in the higher-level helpers below.
38
39
  export { isExpiring, isMachineIdentity };
40
+ export { hasPersonCachedSession, isMachineCredsFileEnvSet, wantsMachineCognitoTokens };
39
41
  /**
40
42
  * Override for where machine-minted Cognito tokens are cached.
41
43
  * Default: the work-mesh daemon state dir (`~/.hq/work-mesh/daemon`), so a
@@ -253,16 +255,6 @@ export function machineTokenStateDir(home = os.homedir(), env = process.env) {
253
255
  export function machineTokenCacheFile(home = os.homedir(), env = process.env) {
254
256
  return path.join(machineTokenStateDir(home, env), "cognito-tokens.json");
255
257
  }
256
- /**
257
- * True when `HQ_MACHINE_CREDS_FILE` is explicitly set (non-empty) in `env`.
258
- * Explicit env remains the primary opt-in for daemon units. Fresh agent boxes
259
- * without the env still mint via {@link wantsMachineCognitoTokens}'s
260
- * default-file fallback when no usable person session exists.
261
- */
262
- export function isMachineCredsFileEnvSet(env = process.env) {
263
- const raw = env.HQ_MACHINE_CREDS_FILE;
264
- return typeof raw === "string" && raw.trim().length > 0;
265
- }
266
258
  /** Default machine-creds path hq-cloud also checks when the env is unset. */
267
259
  export function defaultMachineCredsFile(home = os.homedir()) {
268
260
  return path.join(home, ".hq-agent", "machine-creds.json");
@@ -279,69 +271,6 @@ export function resolveInteractiveLogin(options, env = process.env) {
279
271
  return options.interactive;
280
272
  return env[HQ_NO_BROWSER_LOGIN_ENV]?.trim() !== "1";
281
273
  }
282
- /**
283
- * Align `process.env.HQ_MACHINE_CREDS_FILE` with a (possibly synthetic) `env`
284
- * for the duration of `fn`, so hq-cloud's `isMachineIdentity()` sees the same
285
- * value doctor / tests intended.
286
- */
287
- function withAlignedMachineCredsEnv(env, fn) {
288
- const prevCreds = process.env.HQ_MACHINE_CREDS_FILE;
289
- const nextCreds = env.HQ_MACHINE_CREDS_FILE;
290
- if (nextCreds !== prevCreds) {
291
- if (nextCreds === undefined || String(nextCreds).trim() === "") {
292
- delete process.env.HQ_MACHINE_CREDS_FILE;
293
- }
294
- else {
295
- process.env.HQ_MACHINE_CREDS_FILE = nextCreds;
296
- }
297
- }
298
- try {
299
- return fn();
300
- }
301
- finally {
302
- if (nextCreds !== prevCreds) {
303
- if (prevCreds === undefined)
304
- delete process.env.HQ_MACHINE_CREDS_FILE;
305
- else
306
- process.env.HQ_MACHINE_CREDS_FILE = prevCreds;
307
- }
308
- }
309
- }
310
- /**
311
- * True when a usable person Cognito cache file exists under the effective
312
- * `HQ_STATE_DIR` (or `~/.hq`). Presence alone keeps human commands on the
313
- * person path even when the default machine-creds file is also on disk
314
- * (no-flip guarantee).
315
- */
316
- export function hasPersonCachedSession(env = process.env) {
317
- try {
318
- const file = personTokenCacheFile(os.homedir(), env);
319
- const raw = readPersonTokenFile(os.homedir(), env);
320
- if (!raw ||
321
- typeof raw.accessToken !== "string" ||
322
- raw.accessToken.length === 0 ||
323
- typeof raw.idToken !== "string" ||
324
- raw.idToken.length === 0) {
325
- return false;
326
- }
327
- // Definitive refresh rejection writes `${file}.invalid.<fingerprint>`;
328
- // treat that as no usable person session so status/whoami report machine.
329
- const marker = `${file}.invalid.${accessTokenFingerprint(raw.accessToken)}`;
330
- if (fs.existsSync(marker))
331
- return false;
332
- return true;
333
- }
334
- catch {
335
- return false;
336
- }
337
- }
338
- /**
339
- * Whether hq-cloud currently sees readable machine creds (env override or
340
- * default `~/.hq-agent/machine-creds.json`), honoring a synthetic `env`.
341
- */
342
- function probeMachineIdentity(env) {
343
- return withAlignedMachineCredsEnv(env, () => isMachineIdentity());
344
- }
345
274
  /**
346
275
  * Fresh-box / Outpost bootstrap: env unset, not forced to person, no person
347
276
  * cache, and the default machine-creds file is readable. Distinct from
@@ -361,33 +290,6 @@ export function isDefaultMachineCredsFallback(options = {}, env = process.env) {
361
290
  function logDefaultMachineCredsFallback() {
362
291
  console.error(chalk.dim("machine identity from default creds file"));
363
292
  }
364
- /**
365
- * Whether this call should mint/cache via machine creds (USER_PASSWORD_AUTH).
366
- *
367
- * Machine path when:
368
- * - explicit `{ tokenSource: "machine" }` and creds are readable, or
369
- * - `HQ_MACHINE_CREDS_FILE` is set and readable, or
370
- * - fresh-box fallback: env unset, no person cache, default
371
- * `~/.hq-agent/machine-creds.json` readable.
372
- *
373
- * Explicit `{ tokenSource: "person" }` never uses machine creds. A present
374
- * person cache keeps the person path even when the default creds file exists
375
- * (do not flip a human's commands onto machine tokens).
376
- */
377
- export function wantsMachineCognitoTokens(options = {}, env = process.env) {
378
- if (options.tokenSource === "person")
379
- return false;
380
- if (options.tokenSource === "machine") {
381
- return probeMachineIdentity(env);
382
- }
383
- if (isMachineCredsFileEnvSet(env)) {
384
- return probeMachineIdentity(env);
385
- }
386
- // Fresh-box fallback — default creds file only when no person session.
387
- if (hasPersonCachedSession(env))
388
- return false;
389
- return probeMachineIdentity(env);
390
- }
391
293
  /**
392
294
  * Which Cognito token source this process will use for doctor / status / whoami.
393
295
  * Matches {@link wantsMachineCognitoTokens}: env opt-in, or default-file
@@ -0,0 +1,38 @@
1
+ /** Person browser/login cache vs machine-creds USER_PASSWORD_AUTH mint. */
2
+ export type CognitoTokenSource = "person" | "machine";
3
+ /**
4
+ * True when `HQ_MACHINE_CREDS_FILE` is explicitly set (non-empty) in `env`.
5
+ * Explicit env remains the primary opt-in for daemon units. Fresh agent boxes
6
+ * without the env still mint via {@link wantsMachineCognitoTokens}'s
7
+ * default-file fallback when no usable person session exists.
8
+ */
9
+ export declare function isMachineCredsFileEnvSet(env?: NodeJS.ProcessEnv): boolean;
10
+ /**
11
+ * True when a usable person Cognito cache file exists under the effective
12
+ * `HQ_STATE_DIR` (or `~/.hq`). Presence alone keeps human commands on the
13
+ * person path even when the default machine-creds file is also on disk
14
+ * (no-flip guarantee).
15
+ */
16
+ export declare function hasPersonCachedSession(env?: NodeJS.ProcessEnv): boolean;
17
+ /**
18
+ * Whether hq-cloud currently sees readable machine creds (env override or
19
+ * default `~/.hq-agent/machine-creds.json`), honoring a synthetic `env`.
20
+ */
21
+ export declare function probeMachineIdentity(env: NodeJS.ProcessEnv): boolean;
22
+ /**
23
+ * Whether this call should mint/cache via machine creds (USER_PASSWORD_AUTH).
24
+ *
25
+ * Machine path when:
26
+ * - explicit `{ tokenSource: "machine" }` and creds are readable, or
27
+ * - `HQ_MACHINE_CREDS_FILE` is set and readable, or
28
+ * - fresh-box fallback: env unset, no person cache, default
29
+ * `~/.hq-agent/machine-creds.json` readable.
30
+ *
31
+ * Explicit `{ tokenSource: "person" }` never uses machine creds. A present
32
+ * person cache keeps the person path even when the default creds file exists
33
+ * (do not flip a human's commands onto machine tokens).
34
+ */
35
+ export declare function wantsMachineCognitoTokens(options?: {
36
+ tokenSource?: CognitoTokenSource;
37
+ }, env?: NodeJS.ProcessEnv): boolean;
38
+ //# sourceMappingURL=machine-principal-state.d.ts.map
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Local-state checks that decide machine vs person Cognito tokens. Reads env
3
+ * and files only; no network code, so fast native entries can import it
4
+ * (e.g. via src/lib/caller-principal.ts) without pulling cognito-session.ts.
5
+ */
6
+ import * as os from "node:os";
7
+ import * as fs from "node:fs";
8
+ import { accessTokenFingerprint, isMachineIdentity } from "@indigoai-us/hq-cloud";
9
+ import { personTokenCacheFile, readPersonTokenFile } from "./person-token-cache.js";
10
+ /**
11
+ * True when `HQ_MACHINE_CREDS_FILE` is explicitly set (non-empty) in `env`.
12
+ * Explicit env remains the primary opt-in for daemon units. Fresh agent boxes
13
+ * without the env still mint via {@link wantsMachineCognitoTokens}'s
14
+ * default-file fallback when no usable person session exists.
15
+ */
16
+ export function isMachineCredsFileEnvSet(env = process.env) {
17
+ const raw = env.HQ_MACHINE_CREDS_FILE;
18
+ return typeof raw === "string" && raw.trim().length > 0;
19
+ }
20
+ /**
21
+ * Align `process.env.HQ_MACHINE_CREDS_FILE` with a (possibly synthetic) `env`
22
+ * for the duration of `fn`, so hq-cloud's `isMachineIdentity()` sees the same
23
+ * value doctor / tests intended.
24
+ */
25
+ function withAlignedMachineCredsEnv(env, fn) {
26
+ const prevCreds = process.env.HQ_MACHINE_CREDS_FILE;
27
+ const nextCreds = env.HQ_MACHINE_CREDS_FILE;
28
+ if (nextCreds !== prevCreds) {
29
+ if (nextCreds === undefined || String(nextCreds).trim() === "") {
30
+ delete process.env.HQ_MACHINE_CREDS_FILE;
31
+ }
32
+ else {
33
+ process.env.HQ_MACHINE_CREDS_FILE = nextCreds;
34
+ }
35
+ }
36
+ try {
37
+ return fn();
38
+ }
39
+ finally {
40
+ if (nextCreds !== prevCreds) {
41
+ if (prevCreds === undefined)
42
+ delete process.env.HQ_MACHINE_CREDS_FILE;
43
+ else
44
+ process.env.HQ_MACHINE_CREDS_FILE = prevCreds;
45
+ }
46
+ }
47
+ }
48
+ /**
49
+ * True when a usable person Cognito cache file exists under the effective
50
+ * `HQ_STATE_DIR` (or `~/.hq`). Presence alone keeps human commands on the
51
+ * person path even when the default machine-creds file is also on disk
52
+ * (no-flip guarantee).
53
+ */
54
+ export function hasPersonCachedSession(env = process.env) {
55
+ try {
56
+ const file = personTokenCacheFile(os.homedir(), env);
57
+ const raw = readPersonTokenFile(os.homedir(), env);
58
+ if (!raw ||
59
+ typeof raw.accessToken !== "string" ||
60
+ raw.accessToken.length === 0 ||
61
+ typeof raw.idToken !== "string" ||
62
+ raw.idToken.length === 0) {
63
+ return false;
64
+ }
65
+ // Definitive refresh rejection writes `${file}.invalid.<fingerprint>`;
66
+ // treat that as no usable person session so status/whoami report machine.
67
+ const marker = `${file}.invalid.${accessTokenFingerprint(raw.accessToken)}`;
68
+ if (fs.existsSync(marker))
69
+ return false;
70
+ return true;
71
+ }
72
+ catch {
73
+ return false;
74
+ }
75
+ }
76
+ /**
77
+ * Whether hq-cloud currently sees readable machine creds (env override or
78
+ * default `~/.hq-agent/machine-creds.json`), honoring a synthetic `env`.
79
+ */
80
+ export function probeMachineIdentity(env) {
81
+ return withAlignedMachineCredsEnv(env, () => isMachineIdentity());
82
+ }
83
+ /**
84
+ * Whether this call should mint/cache via machine creds (USER_PASSWORD_AUTH).
85
+ *
86
+ * Machine path when:
87
+ * - explicit `{ tokenSource: "machine" }` and creds are readable, or
88
+ * - `HQ_MACHINE_CREDS_FILE` is set and readable, or
89
+ * - fresh-box fallback: env unset, no person cache, default
90
+ * `~/.hq-agent/machine-creds.json` readable.
91
+ *
92
+ * Explicit `{ tokenSource: "person" }` never uses machine creds. A present
93
+ * person cache keeps the person path even when the default creds file exists
94
+ * (do not flip a human's commands onto machine tokens).
95
+ */
96
+ export function wantsMachineCognitoTokens(options = {}, env = process.env) {
97
+ if (options.tokenSource === "person")
98
+ return false;
99
+ if (options.tokenSource === "machine") {
100
+ return probeMachineIdentity(env);
101
+ }
102
+ if (isMachineCredsFileEnvSet(env)) {
103
+ return probeMachineIdentity(env);
104
+ }
105
+ // Fresh-box fallback — default creds file only when no person session.
106
+ if (hasPersonCachedSession(env))
107
+ return false;
108
+ return probeMachineIdentity(env);
109
+ }
110
+ //# sourceMappingURL=machine-principal-state.js.map
@@ -284,6 +284,9 @@ export function removeLinkOnly(linkPath) {
284
284
  if (!st.isSymbolicLink()) {
285
285
  throw new Error(`Refusing to remove ${linkPath}: it is not a link.`);
286
286
  }
287
+ // Re-check immediately before each removal call, so an entry swapped for a
288
+ // real file or folder after the first check is refused, not removed.
289
+ assertStillLink(linkPath);
287
290
  try {
288
291
  fs.unlinkSync(linkPath);
289
292
  }
@@ -293,8 +296,14 @@ export function removeLinkOnly(linkPath) {
293
296
  // unlink(). A non-recursive rmdir removes only the junction entry.
294
297
  if (code !== 'EPERM' && code !== 'EISDIR')
295
298
  throw err;
299
+ assertStillLink(linkPath);
296
300
  fs.rmdirSync(linkPath);
297
301
  }
298
302
  return true;
299
303
  }
304
+ function assertStillLink(linkPath) {
305
+ if (!fs.lstatSync(linkPath).isSymbolicLink()) {
306
+ throw new Error(`Refusing to remove ${linkPath}: it is no longer a link.`);
307
+ }
308
+ }
300
309
  //# sourceMappingURL=safe-link.js.map
@@ -0,0 +1,158 @@
1
+ /**
2
+ * Bind a script-locked secret release to the process that is actually run
3
+ * (feedback_0e6abcb8).
4
+ *
5
+ * `--script <path>` sends the script's path and hash to the vault, which
6
+ * releases a script-locked secret when that pair is approved. Before this
7
+ * module the command after `--` was never compared with that script, so any
8
+ * command that named an approved script received the secret.
9
+ *
10
+ * When any requested secret is script-locked (or its lock state is unknown),
11
+ * the command after `--` must be one of:
12
+ * 1. the approved script itself: `-- ./approved.sh [args...]` (realpath
13
+ * match). Its `#!` line is parsed and the interpreter it names goes
14
+ * through the same allowlist; `#!/usr/bin/env X` resolves X against a
15
+ * fixed system PATH, never the caller's PATH;
16
+ * 2. an allowlisted interpreter followed directly by the script:
17
+ * `-- bash approved.sh [args...]`, also resolved against the fixed PATH.
18
+ * Anything else (wrappers such as env, xargs, sudo, a shell `-c` string, an
19
+ * interpreter option before the script) is refused and nothing is injected.
20
+ *
21
+ * Allowlisted interpreters: sh, dash, ksh, bash (run with --noprofile
22
+ * --norc), zsh (run with -f), python3/python (run with -I -S), node. Ruby and
23
+ * perl are not allowlisted. The interpreter's realpath and every parent
24
+ * directory must be owned by root and not group- or world-writable, so
25
+ * Homebrew or other user-writable installs are refused.
26
+ *
27
+ * At spawn time the script bytes are read once, hashed, compared with the hash
28
+ * sent to the vault, written to a fresh 0700 private directory, re-hashed, and
29
+ * that copy is passed to the interpreter. The original path is exposed to the
30
+ * script as HQ_APPROVED_SCRIPT and its directory as HQ_APPROVED_SCRIPT_DIR.
31
+ * `$0` / `__file__` / `__dirname` name the copy, so a script that loads
32
+ * neighbour files relative to itself must use HQ_APPROVED_SCRIPT_DIR. The
33
+ * working directory is left as the caller's.
34
+ *
35
+ * The child environment gets a fixed system PATH and loses the variables that
36
+ * make an interpreter, the loader or the macOS xcrun shims run other code.
37
+ * The same names are refused when they arrive in the injected secrets or in
38
+ * an env-schema / env-local value, because those are merged last.
39
+ *
40
+ * The child environment is built from an allowlist (see scriptBoundChildEnv),
41
+ * not inherited from the caller.
42
+ *
43
+ * What the binding does and does not protect: it controls which entrypoint
44
+ * receives the secret. It does not isolate what that entrypoint runs. A
45
+ * same-user caller can still influence tools the script calls through files
46
+ * in HOME (for example ~/.gitconfig and fsmonitor hooks, ~/.curlrc,
47
+ * ~/.ssh/config, ~/.netrc) or through anything else the user can write. Real
48
+ * isolation needs the vault-side sandbox release (`hq secrets sandbox`).
49
+ *
50
+ * Remaining limits, documented on purpose:
51
+ * - Only the script file is hashed. Files it sources, requires or imports
52
+ * (neighbour files, modules) are not covered by the approval.
53
+ * - A process running as the same user can still race the private copy
54
+ * between its re-hash and the interpreter opening it. The window is short
55
+ * and the path is random, but same-user code is not a boundary this
56
+ * client-side check can enforce.
57
+ * - The check runs in the CLI. A modified CLI, or a direct call to the vault
58
+ * API with a self-asserted hash, is outside what it can stop.
59
+ */
60
+ export declare class ScriptBindingError extends Error {
61
+ constructor(message: string);
62
+ }
63
+ /** Root-owned search path used for interpreters and for the child. */
64
+ export declare const FIXED_SYSTEM_PATH = "/usr/bin:/bin:/usr/sbin:/sbin";
65
+ export interface ScriptBoundCommand {
66
+ scriptRealPath: string;
67
+ interpreterPath: string;
68
+ interpreterArgs: string[];
69
+ scriptArgs: string[];
70
+ /** sha256 of the script when the command was resolved. */
71
+ sha256: string;
72
+ }
73
+ export interface PreparedBoundSpawn {
74
+ file: string;
75
+ args: string[];
76
+ copyPath: string;
77
+ cleanup: () => void;
78
+ }
79
+ /** True for a variable a bound script's environment must not take from the caller. */
80
+ export declare function isDeniedEnvName(name: string): boolean;
81
+ /**
82
+ * Schema / local env literal names a bound `hq run --script` launch accepts
83
+ * (fifth review). Literals are caller-written, so instead of denying
84
+ * tool-config names one by one, only these app-level switches are allowed.
85
+ * Everything else must come from the vault through hq().
86
+ */
87
+ export declare const SAFE_LITERAL_NAMES: Set<string>;
88
+ /** Refuse schema literals that are not on SAFE_LITERAL_NAMES (or are denied). */
89
+ export declare function assertLiteralNamesAllowed(names: Iterable<string>): void;
90
+ /**
91
+ * Design (feedback_0e6abcb8, fourth review): an injected name is accepted in a
92
+ * bound launch only when it is not on the loader/interpreter denylist, not
93
+ * one of the inherited allowlist names (HOME, USER, LANG, LC_*, TZ, TMPDIR,
94
+ * ...), and not on the explicit dangerous list above. Ordinary application
95
+ * names (FAKE_API_KEY, DATABASE_URL, NODE_ENV) pass.
96
+ */
97
+ export declare function isDeniedInjectedName(name: string): boolean;
98
+ /** Refuse a bound launch whose injected values use a denied name. */
99
+ export declare function assertInjectedNamesAllowed(names: Iterable<string>): void;
100
+ export declare function privateCopyRoot(): string;
101
+ /**
102
+ * True when realPath and every parent directory are owned by root and not
103
+ * group- or world-writable.
104
+ */
105
+ export declare function isRootOwnedChain(realPath: string): boolean;
106
+ /**
107
+ * Resolve the command after `--` against the approved script. Throws
108
+ * ScriptBindingError when the command is not exactly the script invocation.
109
+ */
110
+ export declare function resolveScriptBoundCommand(scriptPath: string, childArgs: string[]): Promise<ScriptBoundCommand>;
111
+ /** Compare the script on disk with the hash sent to the vault. */
112
+ export declare function assertScriptUnchanged(bound: ScriptBoundCommand, attestedSha256: string | undefined): Promise<void>;
113
+ /**
114
+ * Read the script once, check its hash, copy those bytes into a fresh 0700
115
+ * directory, re-hash the copy, and return the spawn target that runs the copy.
116
+ */
117
+ export declare function prepareBoundSpawn(bound: ScriptBoundCommand, attestedSha256: string | undefined): PreparedBoundSpawn;
118
+ /**
119
+ * Remove the private copy when the parent gets SIGINT, SIGTERM or SIGHUP, or
120
+ * exits, then re-raise the signal so the default termination still happens.
121
+ * SIGKILL cannot be handled; a copy left by it stays in the 0700 temp
122
+ * directory until the OS cleans the temp dir.
123
+ */
124
+ export declare function cleanupOnParentSignals(cleanup: () => void, proc?: Pick<NodeJS.Process, "once" | "removeListener" | "kill" | "pid">): () => void;
125
+ /**
126
+ * Child environment for a bound script, built from an allowlist: HOME, USER,
127
+ * LOGNAME, LANG, LC_*, TERM, TZ and TMPDIR from the caller, a fixed PATH, and
128
+ * the HQ_APPROVED_* variables. Injected secrets and schema values are added by
129
+ * the caller after a denylist check.
130
+ */
131
+ export declare function scriptBoundChildEnv(env: NodeJS.ProcessEnv, scriptRealPath?: string): NodeJS.ProcessEnv;
132
+ /**
133
+ * Whether the release must be bound to the script. Only rows the vault marked
134
+ * `scriptLock.mode: "off"` are exempt; enforced or unknown fails closed.
135
+ */
136
+ export declare function requiresScriptBinding(keys: string[], lockModes: Map<string, string | undefined>): boolean;
137
+ export interface ScriptBindingPlan {
138
+ bound?: ScriptBoundCommand;
139
+ error?: ScriptBindingError;
140
+ }
141
+ /**
142
+ * Resolve the binding before any fetch. A refusal is held, not thrown, so a
143
+ * caller whose secrets turn out not to be script-locked keeps working.
144
+ */
145
+ export declare function planScriptBinding(scriptPath: string | undefined, childArgs: string[]): Promise<ScriptBindingPlan | undefined>;
146
+ export interface ChildLaunch {
147
+ file: string;
148
+ args: string[];
149
+ env: NodeJS.ProcessEnv;
150
+ cleanup: () => void;
151
+ }
152
+ /**
153
+ * Pick what to spawn once the vault has answered. Unlocked secrets keep the
154
+ * caller's command unchanged; enforced or unknown lock state requires the
155
+ * bound script and throws ScriptBindingError otherwise.
156
+ */
157
+ export declare function selectChildLaunch(plan: ScriptBindingPlan | undefined, keys: string[], lockModes: Map<string, string | undefined>, attestedSha256: string | undefined, childArgs: string[], env: NodeJS.ProcessEnv, injected: Record<string, string>): ChildLaunch;
158
+ //# sourceMappingURL=script-bound-exec.d.ts.map