@indigoai-us/hq-cli 5.345.170 → 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 (111) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/assets/scaffold/core/scripts/hq-delegate-send.sh +1 -1
  3. package/dist/command-catalog.generated.d.ts +132 -5
  4. package/dist/command-catalog.generated.js +164 -6
  5. package/dist/commands/access.d.ts +32 -1
  6. package/dist/commands/access.js +112 -3
  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 +137 -14
  12. package/dist/commands/cloud.d.ts +58 -1
  13. package/dist/commands/cloud.js +219 -5
  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/integrations-api.d.ts +4 -0
  23. package/dist/commands/integrations-api.js +18 -2
  24. package/dist/commands/integrations-connect.d.ts +10 -3
  25. package/dist/commands/integrations-connect.js +70 -6
  26. package/dist/commands/integrations-core.d.ts +23 -0
  27. package/dist/commands/integrations-core.js +100 -0
  28. package/dist/commands/integrations-manage.js +8 -1
  29. package/dist/commands/integrations.js +49 -5
  30. package/dist/commands/lanes-loop.d.ts +20 -0
  31. package/dist/commands/lanes-loop.js +69 -11
  32. package/dist/commands/lanes.js +9 -1
  33. package/dist/commands/rescue.d.ts +9 -0
  34. package/dist/commands/rescue.js +31 -0
  35. package/dist/commands/run.js +115 -40
  36. package/dist/commands/scan.d.ts +5 -1
  37. package/dist/commands/scan.js +16 -4
  38. package/dist/commands/secrets.js +50 -9
  39. package/dist/hq-cloud-sync-runner/bin/sync-runner.js +2367 -2109
  40. package/dist/hq-cloud-sync-runner/package.json +1 -1
  41. package/dist/lib/agent-kit/inbox-state.d.ts +17 -0
  42. package/dist/lib/agent-kit/inbox-state.js +41 -20
  43. package/dist/lib/agent-kit/kit-config.d.ts +2 -0
  44. package/dist/lib/agent-kit/kit-config.js +6 -0
  45. package/dist/lib/agent-kit/mcp/tools.js +12 -5
  46. package/dist/lib/agent-kit/run/heartbeat.d.ts +6 -3
  47. package/dist/lib/agent-kit/run/heartbeat.js +6 -3
  48. package/dist/lib/agent-kit/run/inbox.d.ts +8 -3
  49. package/dist/lib/agent-kit/run/inbox.js +42 -10
  50. package/dist/lib/agent-kit/run/request-timeout.d.ts +26 -0
  51. package/dist/lib/agent-kit/run/request-timeout.js +51 -0
  52. package/dist/lib/bot/api.d.ts +1 -1
  53. package/dist/lib/bot/config.d.ts +10 -0
  54. package/dist/lib/bot/config.js +17 -1
  55. package/dist/lib/bot/fleet-local.d.ts +127 -0
  56. package/dist/lib/bot/fleet-local.js +638 -0
  57. package/dist/lib/bot/index.d.ts +1 -0
  58. package/dist/lib/bot/index.js +1 -0
  59. package/dist/lib/bot/local-config.d.ts +3 -2
  60. package/dist/lib/bot/local-config.js +5 -2
  61. package/dist/lib/bot/promote.d.ts +10 -0
  62. package/dist/lib/bot/promote.js +3 -2
  63. package/dist/lib/bot/runtime/index.js +8 -4
  64. package/dist/lib/bot/runtime/scratch-dir.d.ts +24 -0
  65. package/dist/lib/bot/runtime/scratch-dir.js +41 -0
  66. package/dist/lib/caller-principal.d.ts +4 -0
  67. package/dist/lib/caller-principal.js +40 -0
  68. package/dist/lib/capacity/collectors/grok.js +8 -2
  69. package/dist/lib/cli-flag-keys.d.ts +2 -0
  70. package/dist/lib/cli-flag-keys.js +2 -0
  71. package/dist/lib/daemon/catalog.js +4 -1
  72. package/dist/lib/daemon/sync-core-canary.d.ts +3 -0
  73. package/dist/lib/daemon/sync-core-canary.js +8 -3
  74. package/dist/lib/doctor/checks/release-links.d.ts +62 -1
  75. package/dist/lib/doctor/checks/release-links.js +188 -2
  76. package/dist/lib/doctor/fix/apply.js +54 -0
  77. package/dist/lib/doctor/fix/remediation.d.ts +8 -1
  78. package/dist/lib/doctor/fix/remediation.js +22 -0
  79. package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
  80. package/dist/lib/flag-registry-worker-source.generated.js +1 -1
  81. package/dist/lib/index-render/index.d.ts +9 -0
  82. package/dist/lib/index-render/index.js +65 -5
  83. package/dist/lib/index-render/session-scope.d.ts +69 -0
  84. package/dist/lib/index-render/session-scope.js +279 -0
  85. package/dist/lib/lanes/admission.js +7 -20
  86. package/dist/lib/lanes/reconcile.js +13 -15
  87. package/dist/lib/lanes/stale-lock.d.ts +50 -0
  88. package/dist/lib/lanes/stale-lock.js +105 -0
  89. package/dist/lib/lanes/store.js +11 -76
  90. package/dist/lib/notify-recipient.d.ts +10 -4
  91. package/dist/lib/notify-recipient.js +16 -4
  92. package/dist/lib/secret-input-link.d.ts +42 -0
  93. package/dist/lib/secret-input-link.js +93 -0
  94. package/dist/lib/secret-scan/range.d.ts +57 -0
  95. package/dist/lib/secret-scan/range.js +266 -0
  96. package/dist/lib/secret-scan/repo-config.d.ts +18 -3
  97. package/dist/lib/secret-scan/repo-config.js +135 -2
  98. package/dist/run/script-bound-schema.d.ts +27 -0
  99. package/dist/run/script-bound-schema.js +97 -0
  100. package/dist/secrets-env-action.d.ts +2 -0
  101. package/dist/secrets-env-action.js +7 -2
  102. package/dist/utils/cognito-session.d.ts +3 -30
  103. package/dist/utils/cognito-session.js +2 -100
  104. package/dist/utils/machine-principal-state.d.ts +38 -0
  105. package/dist/utils/machine-principal-state.js +110 -0
  106. package/dist/utils/safe-link.js +9 -0
  107. package/dist/utils/script-bound-exec.d.ts +158 -0
  108. package/dist/utils/script-bound-exec.js +568 -0
  109. package/dist/utils/sync-watchdog.d.ts +92 -0
  110. package/dist/utils/sync-watchdog.js +250 -0
  111. package/package.json +8 -8
@@ -1,3 +1,5 @@
1
+ import { assertInjectedNamesAllowed, assertLiteralNamesAllowed, assertScriptUnchanged, planScriptBinding, selectChildLaunch } from '../utils/script-bound-exec.js';
2
+ import { parseStrictSchemaFiles } from '../run/script-bound-schema.js';
1
3
  import { spawn } from 'node:child_process';
2
4
  import * as path from 'node:path';
3
5
  import * as fs from 'node:fs';
@@ -33,6 +35,47 @@ export async function buildRunUsage(scriptPath, scriptId) {
33
35
  },
34
36
  };
35
37
  }
38
+ /**
39
+ * `hq run --script` resolution: read the schema files strictly, fetch only the
40
+ * hq() secrets, and return literals plus secret values. No varlock graph.
41
+ */
42
+ async function resolveStrictSchema(paths, companyUid, usage, fetchBatch) {
43
+ const strict = parseStrictSchemaFiles(paths);
44
+ // Refuse proxy, CA/TLS, pager/editor, HOME/XDG and loader names before any
45
+ // fetch, so a schema literal cannot redirect what the bound script calls.
46
+ // Literals must be on the small --script allowlist; hq() names keep the
47
+ // denylist check (creating a secret needs vault write).
48
+ assertLiteralNamesAllowed(Object.keys(strict.statics));
49
+ assertInjectedNamesAllowed(strict.secrets.keys());
50
+ const names = [...new Set(strict.secrets.values())];
51
+ if (names.length > 100) {
52
+ throw new Error(`hq run supports at most 100 hq() resolvers per schema; got ${names.length}`);
53
+ }
54
+ const values = new Map();
55
+ if (names.length > 0) {
56
+ const data = await fetchBatch(companyUid, names, usage);
57
+ if (!Array.isArray(data?.secrets) || !Array.isArray(data?.errors)) {
58
+ throw new Error('Invalid secret load response from vault');
59
+ }
60
+ const firstError = data.errors[0];
61
+ if (firstError) {
62
+ throw new Error(`Failed to fetch secret '${firstError.name}': ${firstError.message ?? firstError.code}`);
63
+ }
64
+ for (const row of data.secrets) {
65
+ if (row && typeof row.name === 'string' && typeof row.value === 'string' && row.revokedAt == null) {
66
+ values.set(row.name, row.value);
67
+ }
68
+ }
69
+ }
70
+ const env = { ...strict.statics };
71
+ for (const [key, name] of strict.secrets) {
72
+ const value = values.get(name);
73
+ if (value === undefined)
74
+ throw new Error(`Failed to fetch secret '${name}': not returned by vault`);
75
+ env[key] = value;
76
+ }
77
+ return env;
78
+ }
36
79
  export function registerRunCommand(program) {
37
80
  program
38
81
  .command('run')
@@ -89,8 +132,19 @@ export function registerRunCommand(program) {
89
132
  const uid = await getCompanyUid(token, slug, undefined, {
90
133
  allowOfflineScopeFallback: true,
91
134
  });
135
+ // feedback_0e6abcb8: with --script, script-locked secrets may reach
136
+ // only the approved script itself. Resolve the command now; a refusal
137
+ // is applied once the vault says which secrets are locked.
138
+ const scriptPlan = opts.check ? undefined : await planScriptBinding(opts.script, childArgs);
92
139
  const usage = await buildRunUsage(opts.script, opts.scriptId);
140
+ if (scriptPlan?.bound) {
141
+ await assertScriptUnchanged(scriptPlan.bound, usage?.script?.sha256);
142
+ }
143
+ const requestedNames = new Set();
144
+ const lockModes = new Map();
93
145
  const fetchBatch = async (companyUid, names, requestUsage) => withRunSecretReadTelemetry(companyUid, async () => {
146
+ for (const name of names)
147
+ requestedNames.add(name);
94
148
  const res = await vaultApiFetch({
95
149
  token,
96
150
  path: `/secrets/${encodeURIComponent(companyUid)}/load`,
@@ -115,7 +169,12 @@ export function registerRunCommand(program) {
115
169
  }
116
170
  throw new Error(`Failed to batch-load secrets: ${message}`);
117
171
  }
118
- return res.json();
172
+ const data = await res.json();
173
+ for (const row of Array.isArray(data?.secrets) ? data.secrets : []) {
174
+ if (row && typeof row.name === 'string')
175
+ lockModes.set(row.name, row.scriptLock?.mode);
176
+ }
177
+ return data;
119
178
  });
120
179
  const pluginOpts = {
121
180
  companyOverride: opts.company,
@@ -127,61 +186,77 @@ export function registerRunCommand(program) {
127
186
  };
128
187
  // LAST entry = highest precedence; .env.local files trail .env.schema files so any .env.local beats any schema regardless of depth.
129
188
  const paths = [...schemaPaths, ...envLocalPaths];
130
- let state;
131
- const graph = await internal.loadEnvGraph({
132
- entryFilePaths: paths,
133
- // hq run owns its short-lived in-memory resolution. Varlock's default
134
- // cache auto-detection can write ~/.config/varlock and initialize
135
- // the native local-encryption helper, neither of which belongs on
136
- // this command path.
137
- skipCache: true,
138
- afterInit: async (g) => { state = installHqPlugin(g, pluginOpts); },
139
- });
140
- // Refuse to resolve a graph varlock never finished loading (a malformed
141
- // .env.schema / .env.local): resolving an unprocessed item whose key is
142
- // also in process.env reaches varlock's `expected dataType to be set`
143
- // invariant as a FLOATED, never-settling rejection — the CLI then exits
144
- // 0 with the command never run (HQ-CLI-W). Run this BEFORE prewarm so a
145
- // bad file can never trigger a secret fetch.
146
- assertEnvGraphLoaded(graph);
147
- await prewarmHqSecrets(graph, pluginOpts, state);
148
- // Defence in depth: convert a floated resolution rejection into a thrown
149
- // error instead of hanging on varlock's never-settling promise, in case
150
- // the shape-dependent guard above ever silently degrades.
151
- await resolveEnvValuesOrThrow(graph);
152
- // varlock's item.errors includes warning-severity entries (isWarning),
153
- // e.g. "Value is very short! Is it actually sensitive?" for a short
154
- // sensitive value. Print those and carry on; only real errors are fatal.
155
- const schemaItems = Object.entries(graph.configSchema);
156
- const itemMessages = (wantWarnings) => schemaItems.flatMap(([k, item]) => (item.errors ?? [])
157
- .filter((e) => Boolean(e?.isWarning) === wantWarnings)
158
- .map((e) => ` ${k}: ${e?.message ?? String(e)}`));
159
- const warningMsgs = itemMessages(true);
160
- if (warningMsgs.length > 0) {
161
- process.stderr.write(`Warning: env var checks reported warnings:\n${warningMsgs.join('\n')}\n`);
189
+ let resolvedEnv;
190
+ if (opts.script) {
191
+ // feedback_0e6abcb8: with --script, never build a varlock graph. Its
192
+ // exec()/plugin/import resolvers would run in this process while the
193
+ // fetched secrets are in memory, before the script binding is
194
+ // checked. The strict reader accepts only hq() items and literals.
195
+ resolvedEnv = await resolveStrictSchema(paths, uid, usage, fetchBatch);
162
196
  }
163
- const errorMsgs = itemMessages(false);
164
- if (errorMsgs.length > 0) {
165
- process.stderr.write(`Error: failed to resolve env vars:\n${errorMsgs.join('\n')}\n`);
166
- process.exit(1);
197
+ else {
198
+ let state;
199
+ const graph = await internal.loadEnvGraph({
200
+ entryFilePaths: paths,
201
+ // hq run owns its short-lived in-memory resolution. Varlock's default
202
+ // cache auto-detection can write ~/.config/varlock and initialize
203
+ // the native local-encryption helper, neither of which belongs on
204
+ // this command path.
205
+ skipCache: true,
206
+ afterInit: async (g) => { state = installHqPlugin(g, pluginOpts); },
207
+ });
208
+ // Refuse to resolve a graph varlock never finished loading (a malformed
209
+ // .env.schema / .env.local): resolving an unprocessed item whose key is
210
+ // also in process.env reaches varlock's `expected dataType to be set`
211
+ // invariant as a FLOATED, never-settling rejection — the CLI then exits
212
+ // 0 with the command never run (HQ-CLI-W). Run this BEFORE prewarm so a
213
+ // bad file can never trigger a secret fetch.
214
+ assertEnvGraphLoaded(graph);
215
+ await prewarmHqSecrets(graph, pluginOpts, state);
216
+ // Defence in depth: convert a floated resolution rejection into a thrown
217
+ // error instead of hanging on varlock's never-settling promise, in case
218
+ // the shape-dependent guard above ever silently degrades.
219
+ await resolveEnvValuesOrThrow(graph);
220
+ // varlock's item.errors includes warning-severity entries (isWarning),
221
+ // e.g. "Value is very short! Is it actually sensitive?" for a short
222
+ // sensitive value. Print those and carry on; only real errors are fatal.
223
+ const schemaItems = Object.entries(graph.configSchema);
224
+ const itemMessages = (wantWarnings) => schemaItems.flatMap(([k, item]) => (item.errors ?? [])
225
+ .filter((e) => Boolean(e?.isWarning) === wantWarnings)
226
+ .map((e) => ` ${k}: ${e?.message ?? String(e)}`));
227
+ const warningMsgs = itemMessages(true);
228
+ if (warningMsgs.length > 0) {
229
+ process.stderr.write(`Warning: env var checks reported warnings:\n${warningMsgs.join('\n')}\n`);
230
+ }
231
+ const errorMsgs = itemMessages(false);
232
+ if (errorMsgs.length > 0) {
233
+ process.stderr.write(`Error: failed to resolve env vars:\n${errorMsgs.join('\n')}\n`);
234
+ process.exit(1);
235
+ }
236
+ resolvedEnv = graph.getResolvedEnvObject();
167
237
  }
168
- const resolvedEnv = graph.getResolvedEnvObject();
169
238
  const varCount = Object.keys(resolvedEnv).length;
170
239
  process.stderr.write(`Loaded ${varCount} env vars from .env.schema (company: ${slug})\n`);
171
240
  if (opts.check) {
172
241
  process.exit(0);
173
242
  }
174
- const [childCmd, ...restArgs] = childArgs;
243
+ // Script-locked (or unknown) secrets go only to a private, re-hashed
244
+ // copy of the approved script; unlocked secrets keep the command as is.
245
+ const launch = selectChildLaunch(scriptPlan, [...requestedNames], lockModes, usage?.script?.sha256, childArgs, process.env, resolvedEnv);
246
+ const childCmd = launch.file;
247
+ const restArgs = launch.args;
175
248
  const child = spawn(childCmd, restArgs, {
176
249
  stdio: 'inherit',
177
- env: { ...process.env, ...resolvedEnv },
250
+ env: launch.env,
178
251
  });
179
252
  child.once('spawn', () => releaseChildWrapperPackageUseLeaseAfterStart("childWrapper", isHqCliChild(childCmd, restArgs)));
180
253
  child.on('error', (err) => {
181
254
  process.stderr.write(`Error: failed to start command '${childCmd}': ${err.message}\n`);
255
+ launch.cleanup();
182
256
  process.exit(1);
183
257
  });
184
258
  child.on('close', (code, signal) => {
259
+ launch.cleanup();
185
260
  if (signal) {
186
261
  process.kill(process.pid, signal);
187
262
  }
@@ -6,7 +6,10 @@
6
6
  * hq scan secrets --json findings as JSON
7
7
  * hq scan secrets --repo <dir> [--snapshot-root <dir>] [--allowlist-rev <rev>] <file...>
8
8
  * scan files with that repo's committed
9
- * .gitleaks.toml allowlists (used by pr-check)
9
+ * .gitleaks.toml allowlists
10
+ * hq scan secrets --range <base>[..<head>] [--repo <dir>]
11
+ * scan the lines a branch added since <base>,
12
+ * every commit included (used by pr-check)
10
13
  *
11
14
  * Rules: the vendored gitleaks default rule set (MIT, vendor/gitleaks/) plus
12
15
  * HQ's own token patterns. No network, no account, no quota. A staged scan also
@@ -24,6 +27,7 @@ interface ScanSecretsOptions {
24
27
  repo?: string;
25
28
  snapshotRoot?: string;
26
29
  allowlistRev?: string;
30
+ range?: string;
27
31
  }
28
32
  export declare function runScanSecrets(paths: string[], opts: ScanSecretsOptions, cwd?: string): number;
29
33
  export declare function registerScanCommand(program: Command): void;
@@ -6,7 +6,10 @@
6
6
  * hq scan secrets --json findings as JSON
7
7
  * hq scan secrets --repo <dir> [--snapshot-root <dir>] [--allowlist-rev <rev>] <file...>
8
8
  * scan files with that repo's committed
9
- * .gitleaks.toml allowlists (used by pr-check)
9
+ * .gitleaks.toml allowlists
10
+ * hq scan secrets --range <base>[..<head>] [--repo <dir>]
11
+ * scan the lines a branch added since <base>,
12
+ * every commit included (used by pr-check)
10
13
  *
11
14
  * Rules: the vendored gitleaks default rule set (MIT, vendor/gitleaks/) plus
12
15
  * HQ's own token patterns. No network, no account, no quota. A staged scan also
@@ -18,6 +21,7 @@
18
21
  * Output names the rule, file and line; secret values are never printed.
19
22
  */
20
23
  import { ScanConfigError } from "../lib/secret-scan/repo-config.js";
24
+ import { scanRange } from "../lib/secret-scan/range.js";
21
25
  import { ScanInputError, scanFiles, scanStaged } from "../lib/secret-scan/staged.js";
22
26
  function report(findings, scope) {
23
27
  if (!findings.length)
@@ -38,7 +42,14 @@ export function runScanSecrets(paths, opts, cwd = process.cwd()) {
38
42
  throw new ScanInputError("--snapshot-root needs --repo");
39
43
  if (opts.allowlistRev !== undefined && opts.repo === undefined)
40
44
  throw new ScanInputError("--allowlist-rev needs --repo");
41
- findings = paths.length && !opts.staged ? scanFiles(paths, { repo: opts.repo, snapshotRoot: opts.snapshotRoot, allowlistRev: opts.allowlistRev }) : scanStaged(cwd);
45
+ if (opts.range !== undefined) {
46
+ if (paths.length || opts.staged)
47
+ throw new ScanInputError("--range scans a commit range; do not pass files or --staged");
48
+ if (opts.snapshotRoot !== undefined || opts.allowlistRev !== undefined) {
49
+ throw new ScanInputError("--range reads .gitleaks.toml from the range base; drop --snapshot-root and --allowlist-rev");
50
+ }
51
+ }
52
+ findings = opts.range !== undefined ? scanRange(opts.repo ?? cwd, opts.range) : paths.length && !opts.staged ? scanFiles(paths, { repo: opts.repo, snapshotRoot: opts.snapshotRoot, allowlistRev: opts.allowlistRev }) : scanStaged(cwd);
42
53
  }
43
54
  catch (err) {
44
55
  const msg = err instanceof ScanInputError || err instanceof ScanConfigError ? err.message : `scan failed: ${err instanceof Error ? err.message : String(err)}`;
@@ -51,7 +62,7 @@ export function runScanSecrets(paths, opts, cwd = process.cwd()) {
51
62
  process.stdout.write(`${JSON.stringify({ ok: findings.length === 0, findings })}\n`);
52
63
  }
53
64
  else {
54
- report(findings, paths.length && !opts.staged ? "the given files" : "staged changes");
65
+ report(findings, opts.range !== undefined ? `the lines added in ${opts.range}` : paths.length && !opts.staged ? "the given files" : "staged changes");
55
66
  }
56
67
  return findings.length ? 1 : 0;
57
68
  }
@@ -63,9 +74,10 @@ export function registerScanCommand(program) {
63
74
  .argument("[files...]", "Scan these files instead of the staged changes")
64
75
  .option("--staged", "Scan the staged changes (default when no files are given)")
65
76
  .option("--json", "Print findings as JSON")
66
- .option("--repo <dir>", "With files: apply this repo's committed .gitleaks.toml allowlists, matching repo-relative paths")
77
+ .option("--repo <dir>", "With files: apply this repo's committed .gitleaks.toml allowlists, matching repo-relative paths. With --range: the repo to scan (default: the current directory)")
67
78
  .option("--snapshot-root <dir>", "With --repo: files under <dir>/<commit>/<path> are matched as <path>")
68
79
  .option("--allowlist-rev <rev>", "With --repo: read .gitleaks.toml from this commit instead of HEAD")
80
+ .option("--range <range>", "Scan the lines added in <base>..<head> (head defaults to HEAD), every commit included, with the base's .gitleaks.toml allowlists and the repo's [[rules]]")
69
81
  .action((files, opts) => {
70
82
  process.exitCode = runScanSecrets(files ?? [], opts);
71
83
  });
@@ -1,3 +1,4 @@
1
+ import { assertScriptUnchanged, planScriptBinding, selectChildLaunch } from "../utils/script-bound-exec.js";
1
2
  import * as fs from "node:fs";
2
3
  import * as path from "node:path";
3
4
  import * as yaml from "js-yaml";
@@ -77,6 +78,7 @@ import { describeSecretsScope, formatSecretSaved, formatSecretsListEmpty, format
77
78
  import { vaultApiFetch, getCompanyUid, getEntityUid, looksLikeCompanyUid, } from "../utils/vault-api.js";
78
79
  import { formatApiKeyCapabilityDenial, peekHqApiKey, requireApiKeyCapability, resolveVaultCredentialForCapability, } from "../utils/resolve-vault-credential.js";
79
80
  import { runSandboxJobAfterSubmission } from "../utils/secrets-sandbox-resident.js";
81
+ import { renderSecretInputLink, SECRET_LINK_FORMATS, } from "../lib/secret-input-link.js";
80
82
  export { vaultApiFetch, getCompanyUid, getEntityUid };
81
83
  /**
82
84
  * Mirrors `RESOURCE_SURFACES` in hq-pro
@@ -1370,6 +1372,10 @@ export function registerSecretsCommand(program) {
1370
1372
  process.exit(1);
1371
1373
  }
1372
1374
  const keys = parseSecretNameList(_opts.only);
1375
+ // feedback_0e6abcb8: with --script, a script-locked secret may reach
1376
+ // only the approved script itself. Resolve the command now; a refusal
1377
+ // is applied once the vault says which secrets are locked.
1378
+ const scriptPlan = await planScriptBinding(_opts.script, childArgs);
1373
1379
  const parentScope = { ...secrets.opts() };
1374
1380
  const inheritedCompany = parentScope.company;
1375
1381
  let requestedCompany = _opts.company ?? inheritedCompany;
@@ -1466,9 +1472,14 @@ export function registerSecretsCommand(program) {
1466
1472
  secret_names: keys,
1467
1473
  });
1468
1474
  }
1469
- const readSecrets = async () => loadRevealedSecrets(cred.token, companyUid, keys, cred.kind === "cognito"
1475
+ const execUsage = cred.kind === "cognito"
1470
1476
  ? await buildSecretUsage("exec", _opts.script, _opts.scriptId)
1471
- : undefined, { allowOfflineFallback: true });
1477
+ : undefined;
1478
+ if (scriptPlan?.bound) {
1479
+ await assertScriptUnchanged(scriptPlan.bound, execUsage?.script?.sha256);
1480
+ }
1481
+ const lockModes = new Map();
1482
+ const readSecrets = async () => loadRevealedSecrets(cred.token, companyUid, keys, execUsage, { allowOfflineFallback: true, lockModes });
1472
1483
  // API-key auth has no signed-in person identity for the CLI telemetry
1473
1484
  // endpoint. Do not attribute that read to an unrelated cached session.
1474
1485
  let revealed;
@@ -1489,18 +1500,24 @@ export function registerSecretsCommand(program) {
1489
1500
  }
1490
1501
  secretEnv[key] = value;
1491
1502
  }
1492
- const [childCmd, ...childCmdArgs] = childArgs;
1503
+ // Script-locked (or unknown) secrets go only to a private, re-hashed
1504
+ // copy of the approved script; unlocked secrets keep the command as is.
1505
+ const launch = selectChildLaunch(scriptPlan, keys, lockModes, execUsage?.script?.sha256, childArgs, process.env, secretEnv);
1506
+ const childCmd = launch.file;
1507
+ const childCmdArgs = launch.args;
1493
1508
  const { spawn } = await import("node:child_process");
1494
1509
  const child = spawn(childCmd, childCmdArgs, {
1495
1510
  stdio: "inherit",
1496
- env: { ...process.env, ...secretEnv },
1511
+ env: launch.env,
1497
1512
  });
1498
1513
  child.once("spawn", () => releaseChildWrapperPackageUseLeaseAfterStart("childWrapper", isHqCliChild(childCmd, childCmdArgs)));
1499
1514
  child.on("error", (err) => {
1500
1515
  console.error(chalk.red(`Failed to start command '${childCmd}': ${err.message}`));
1516
+ launch.cleanup();
1501
1517
  process.exit(1);
1502
1518
  });
1503
1519
  child.on("close", (code, signal) => {
1520
+ launch.cleanup();
1504
1521
  if (signal) {
1505
1522
  process.kill(process.pid, signal);
1506
1523
  }
@@ -1525,8 +1542,19 @@ export function registerSecretsCommand(program) {
1525
1542
  .command("generate-link <name>")
1526
1543
  .description("Generate a one-time link for someone to submit a secret value")
1527
1544
  .option("--expires <duration>", "Token expiry duration (e.g. 24h, 2d, 30m)", "24h")
1545
+ .option("--format <format>", "Output format: text (default; OSC-8 hyperlink on a supporting TTY, plain URL otherwise; FORCE_HYPERLINK=1/0 overrides detection on a TTY only), markdown (one Markdown link line on stdout, details on stderr), json")
1546
+ .option("--json", "Print { url, expiresAt, secretName } as JSON (same as --format json)")
1528
1547
  .action(async (name, opts) => {
1529
1548
  try {
1549
+ const format = (opts.json ? "json" : (opts.format ?? "text"));
1550
+ if (!SECRET_LINK_FORMATS.includes(format)) {
1551
+ console.error(chalk.red(`Invalid format '${opts.format}': must be one of ${SECRET_LINK_FORMATS.join(", ")}`));
1552
+ process.exit(1);
1553
+ }
1554
+ if (opts.json && opts.format !== undefined && opts.format !== "json") {
1555
+ console.error(chalk.red(`--json conflicts with --format ${opts.format}`));
1556
+ process.exit(1);
1557
+ }
1530
1558
  if (!SECRET_NAME_PATTERN.test(name)) {
1531
1559
  console.error(chalk.red(`Invalid secret name '${name}': must match ^[A-Z][A-Z0-9_]*(/[A-Z][A-Z0-9_]+)*$ (e.g. MY_API_KEY or DEV/MY_KEY)`));
1532
1560
  process.exit(1);
@@ -1556,11 +1584,24 @@ export function registerSecretsCommand(program) {
1556
1584
  process.exit(1);
1557
1585
  }
1558
1586
  const data = (await res.json());
1559
- console.log(chalk.green("Secret input link generated:"));
1560
- console.log(`\n ${data.url}\n`);
1561
- console.log(chalk.dim(` Secret: ${data.secretName}`));
1562
- console.log(chalk.dim(` Expires: ${data.expiresAt}`));
1563
- console.log(chalk.dim(" One-time use — link is invalidated after first submission."));
1587
+ const rendered = renderSecretInputLink(data, {
1588
+ format,
1589
+ isTTY: process.stdout.isTTY === true,
1590
+ env: process.env,
1591
+ });
1592
+ if (format === "text") {
1593
+ const [heading, link, ...meta] = rendered.stdout;
1594
+ console.log(chalk.green(heading));
1595
+ console.log(link);
1596
+ for (const line of meta)
1597
+ console.log(chalk.dim(line));
1598
+ }
1599
+ else {
1600
+ for (const line of rendered.stdout)
1601
+ console.log(line);
1602
+ for (const line of rendered.stderr)
1603
+ console.error(chalk.dim(line));
1604
+ }
1564
1605
  }
1565
1606
  catch (err) {
1566
1607
  // Same shared plan-limit renderer as `secrets set` (see main.ts).