@indigoai-us/hq-cli 5.345.154 → 5.345.156

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 (57) hide show
  1. package/CHANGELOG.md +138 -0
  2. package/dist/command-catalog.generated.d.ts +59 -7
  3. package/dist/command-catalog.generated.js +75 -7
  4. package/dist/commands/agent-inbox.js +2 -2
  5. package/dist/commands/auth.d.ts +2 -0
  6. package/dist/commands/auth.js +92 -3
  7. package/dist/commands/core.js +1 -1
  8. package/dist/commands/files-browse.d.ts +16 -0
  9. package/dist/commands/files-browse.js +52 -1
  10. package/dist/commands/integrations-core.d.ts +13 -1
  11. package/dist/commands/integrations-core.js +49 -4
  12. package/dist/commands/integrations.js +4 -2
  13. package/dist/commands/library-add.d.ts +6 -2
  14. package/dist/commands/library-add.js +45 -6
  15. package/dist/commands/library-caption.d.ts +38 -1
  16. package/dist/commands/library-caption.js +304 -3
  17. package/dist/commands/library-link.d.ts +7 -3
  18. package/dist/commands/library-link.js +10 -4
  19. package/dist/commands/library-remove.d.ts +122 -0
  20. package/dist/commands/library-remove.js +681 -0
  21. package/dist/commands/library.d.ts +26 -1
  22. package/dist/commands/library.js +101 -14
  23. package/dist/commands/secrets.d.ts +16 -0
  24. package/dist/commands/secrets.js +144 -8
  25. package/dist/commands/storage-offload.d.ts +22 -4
  26. package/dist/commands/storage-offload.js +196 -2
  27. package/dist/commands/storage.js +5 -0
  28. package/dist/lib/agent-kit/inbox-state.d.ts +7 -0
  29. package/dist/lib/agent-kit/inbox-state.js +5 -1
  30. package/dist/lib/agent-kit/mcp/tools.js +2 -1
  31. package/dist/lib/daemon/machine-login.d.ts +20 -0
  32. package/dist/lib/daemon/machine-login.js +53 -8
  33. package/dist/lib/daemon/machine-token-identity.d.ts +34 -0
  34. package/dist/lib/daemon/machine-token-identity.js +123 -0
  35. package/dist/lib/doctor/checks/sync-health.d.ts +9 -0
  36. package/dist/lib/doctor/checks/sync-health.js +66 -1
  37. package/dist/lib/doctor/company-resolution-state.d.ts +30 -0
  38. package/dist/lib/doctor/company-resolution-state.js +89 -0
  39. package/dist/lib/doctor/fix/apply.d.ts +36 -6
  40. package/dist/lib/doctor/fix/apply.js +128 -12
  41. package/dist/lib/doctor/types.d.ts +1 -1
  42. package/dist/lib/doctor/types.js +1 -0
  43. package/dist/lib/hook-engine/resolve.js +6 -2
  44. package/dist/lib/index-render/company-knowledge.d.ts +1 -1
  45. package/dist/lib/index-render/company-knowledge.js +3 -2
  46. package/dist/lib/index-render/index.js +44 -3
  47. package/dist/lib/index-render/projects.d.ts +1 -1
  48. package/dist/lib/index-render/projects.js +4 -2
  49. package/dist/lib/lanes/registry-flag.d.ts +23 -2
  50. package/dist/lib/lanes/registry-flag.js +256 -36
  51. package/dist/lib/personal-secrets-audit.d.ts +31 -0
  52. package/dist/lib/personal-secrets-audit.js +45 -0
  53. package/dist/sentry-before-send.js +1 -0
  54. package/dist/utils/access-denied-hint.js +6 -0
  55. package/dist/utils/client-health.d.ts +34 -0
  56. package/dist/utils/client-health.js +96 -3
  57. package/package.json +7 -7
@@ -1,4 +1,6 @@
1
- import { renderCompanies } from "./companies.js";
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { CompanyIndexUserError, renderCompanies } from "./companies.js";
2
4
  import { renderCompanyKnowledge } from "./company-knowledge.js";
3
5
  import { renderOrchestrator } from "./orchestrator.js";
4
6
  import { renderProjects } from "./projects.js";
@@ -8,12 +10,51 @@ import { renderSocialDrafts } from "./social-drafts.js";
8
10
  import { renderThreads } from "./threads.js";
9
11
  import { renderWorkers } from "./workers.js";
10
12
  const allOrder = ["threads", "orchestrator", "companies", "projects", "company-knowledge", "public-knowledge", "workers", "reports", "social-drafts"];
13
+ function companyScope(target, context, args) {
14
+ let company;
15
+ for (let i = 0; i < args.length; i += 1) {
16
+ let value = args[i];
17
+ if (value === "--company")
18
+ value = args[++i] ?? "";
19
+ else if (value.startsWith("--company="))
20
+ value = value.slice("--company=".length);
21
+ if (!value || value.startsWith("-"))
22
+ throw new CompanyIndexUserError(`rebuild-index ${target}: expected a company slug; unknown or incomplete option`);
23
+ if (company !== undefined)
24
+ throw new CompanyIndexUserError(`rebuild-index ${target}: supply only one company slug`);
25
+ if (!/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/.test(value))
26
+ throw new CompanyIndexUserError(`rebuild-index ${target}: invalid company slug`);
27
+ company = value;
28
+ }
29
+ if (company !== undefined) {
30
+ const directory = path.join(context.root, "companies", company);
31
+ try {
32
+ if (!fs.lstatSync(directory).isDirectory())
33
+ throw new CompanyIndexUserError(`rebuild-index ${target}: company must be a real directory`);
34
+ const contentDirectory = path.join(directory, target === "projects" ? "projects" : "knowledge");
35
+ if (fs.existsSync(contentDirectory) && !fs.lstatSync(contentDirectory).isDirectory())
36
+ throw new CompanyIndexUserError(`rebuild-index ${target}: content must be a real directory`);
37
+ }
38
+ catch (error) {
39
+ if (error.code === "ENOENT")
40
+ throw new CompanyIndexUserError(`rebuild-index ${target}: no company directory named "${company}"`);
41
+ throw error;
42
+ }
43
+ }
44
+ return company;
45
+ }
11
46
  export function renderIndexTarget(target, context, args = []) {
47
+ if (target === "company-knowledge" || target === "projects") {
48
+ if (args.some((arg) => arg === "--help" || arg === "-h")) {
49
+ (context.output ?? ((message) => process.stdout.write(message)))(`Usage: hq core rebuild-index ${target} [<company> | --company <company>] [--accept]\n\nWith a company, rebuild only that company's index. Without a company, retain the all-company rebuild. Help performs no writes.\n`);
50
+ return { written: [] };
51
+ }
52
+ const company = companyScope(target, context, args);
53
+ return target === "projects" ? renderProjects(context, company) : renderCompanyKnowledge(context, company);
54
+ }
12
55
  switch (target) {
13
56
  case "companies": return renderCompanies(context, args);
14
- case "company-knowledge": return renderCompanyKnowledge(context);
15
57
  case "orchestrator": return renderOrchestrator(context);
16
- case "projects": return renderProjects(context);
17
58
  case "public-knowledge": return renderPublicKnowledge(context);
18
59
  case "reports": return renderReports(context);
19
60
  case "social-drafts": return renderSocialDrafts(context);
@@ -1,3 +1,3 @@
1
1
  import { RenderContext, RenderResult } from "./shared.js";
2
- export declare function renderProjects(context: RenderContext): RenderResult;
2
+ export declare function renderProjects(context: RenderContext, selectedCompany?: string): RenderResult;
3
3
  //# sourceMappingURL=projects.d.ts.map
@@ -64,9 +64,9 @@ function renderCompanyProjects(context, company, projectRoot) {
64
64
  log(context, `rebuild-projects-index: wrote ${output}`);
65
65
  return written;
66
66
  }
67
- export function renderProjects(context) {
67
+ export function renderProjects(context, selectedCompany) {
68
68
  const written = [];
69
- const companyDirectories = immediateEntries(context.root, "companies", "dir");
69
+ const companyDirectories = selectedCompany === undefined ? immediateEntries(context.root, "companies", "dir") : [at(context.root, `companies/${selectedCompany}`)];
70
70
  for (const companyDirectory of companyDirectories) {
71
71
  const company = basename(companyDirectory);
72
72
  if (company.startsWith("_") || isHidden(company))
@@ -76,6 +76,8 @@ export function renderProjects(context) {
76
76
  written.push(...renderCompanyProjects(context, company, projectRoot));
77
77
  }
78
78
  }
79
+ if (selectedCompany !== undefined)
80
+ return { written };
79
81
  const base = "personal/projects";
80
82
  if (!exists(context.root, base)) {
81
83
  log(context, "rebuild-projects-index: personal/projects/ missing, skipping");
@@ -18,7 +18,15 @@ export interface ReadLanesFlagOptions {
18
18
  env?: NodeJS.ProcessEnv;
19
19
  createClient?: LanesFlagClientFactory;
20
20
  getToken?: () => string | Promise<string>;
21
+ /** Second token source used once after a 401. Defaults to `getToken`, else a forced session refresh. */
22
+ refreshToken?: () => string | Promise<string>;
21
23
  reportFailure?: (error: unknown) => void;
24
+ /** Fetch used by the default flag client; tests inject this to drive HTTP outcomes. */
25
+ fetch?: typeof fetch;
26
+ /** Last-known-good cache directory; null disables it. Defaults to the HQ state dir. */
27
+ cacheDir?: string | null;
28
+ retryBackoffMs?: number;
29
+ sleep?: (ms: number) => Promise<void>;
22
30
  }
23
31
  export declare function readLanesFlagLocalOverride(flagKey: string, env?: NodeJS.ProcessEnv): boolean | null;
24
32
  export type LanesRegistryFlagResult = {
@@ -27,10 +35,23 @@ export type LanesRegistryFlagResult = {
27
35
  } | {
28
36
  state: "unknown";
29
37
  };
38
+ export declare const LANES_FLAG_RETRY_BACKOFF_MS = 250;
39
+ export declare const LANES_FLAG_CACHE_FILE = "lanes-flag-cache.json";
40
+ /**
41
+ * Oldest last-known-good entry a failed read may serve. Every successful
42
+ * snapshot read restamps resolvedAt, so this bounds how long a value can
43
+ * outlive its last confirmation by the registry.
44
+ */
45
+ export declare const LANES_FLAG_CACHE_MAX_AGE_MS: number;
46
+ /** Error message safe for stderr: token-like substrings removed, length bounded. */
47
+ export declare function sanitizeFlagErrorMessage(error: unknown): string;
30
48
  /**
31
49
  * Read a lanes flag once for each company identity in this CLI process. The
32
- * cache contains only an in-flight or settled decision result; it is never
33
- * written to disk. A local override is checked on every call and wins first.
50
+ * in-process cache holds the in-flight or settled result. The last registry
51
+ * snapshot value per flag and company is also kept in the HQ state dir and
52
+ * used when a later read fails transiently (timeout, network error, 5xx) and
53
+ * the value was confirmed within LANES_FLAG_CACHE_MAX_AGE_MS. A local override
54
+ * is checked on every call and wins first.
34
55
  */
35
56
  export declare function readLanesRegistryFlag(flagKey: string, options?: ReadLanesFlagOptions): Promise<boolean>;
36
57
  /** Read a lanes flag while preserving registry failure as an explicit unknown state. */
@@ -1,5 +1,7 @@
1
1
  import { createFlagClient, readLocalOverride, } from "@indigoai-us/hq-flags-client";
2
- import { ensureCognitoIdToken } from "../../utils/cognito-session.js";
2
+ import * as fs from "node:fs";
3
+ import * as path from "node:path";
4
+ import { ensureCognitoIdToken, loadCachedTokens, refreshActivePersonTokens, wantsMachineCognitoTokens, } from "../../utils/cognito-session.js";
3
5
  import { FLAG_REGISTRY_DEFAULT_ENDPOINT, resolveFlagRegistryEndpoint, } from "../flag-registry-endpoint.js";
4
6
  export const LANES_FLAG_API_DEFAULT_ENDPOINT = FLAG_REGISTRY_DEFAULT_ENDPOINT;
5
7
  export const LANES_FLAG_READ_TIMEOUT_MS = 5_000;
@@ -15,15 +17,118 @@ class MissingFlagTokenError extends Error {
15
17
  this.name = "MissingFlagTokenError";
16
18
  }
17
19
  }
20
+ export const LANES_FLAG_RETRY_BACKOFF_MS = 250;
21
+ export const LANES_FLAG_CACHE_FILE = "lanes-flag-cache.json";
22
+ const FLAG_CACHE_MAX_ENTRIES = 256;
23
+ /**
24
+ * Oldest last-known-good entry a failed read may serve. Every successful
25
+ * snapshot read restamps resolvedAt, so this bounds how long a value can
26
+ * outlive its last confirmation by the registry.
27
+ */
28
+ export const LANES_FLAG_CACHE_MAX_AGE_MS = 24 * 60 * 60 * 1000;
29
+ /**
30
+ * HQ state dir from the caller's env (`HQ_STATE_DIR`, else `<home>/.hq`).
31
+ * Returns null when the env names neither, so a hermetic env never touches
32
+ * the real home directory.
33
+ */
34
+ function flagCacheDir(options) {
35
+ if (options.cacheDir !== undefined)
36
+ return options.cacheDir;
37
+ const env = options.env ?? process.env;
38
+ const stateDir = env.HQ_STATE_DIR?.trim();
39
+ if (stateDir)
40
+ return stateDir;
41
+ const home = env.HOME?.trim() || env.USERPROFILE?.trim();
42
+ return home ? path.join(home, ".hq") : null;
43
+ }
44
+ function readFlagCacheFile(dir) {
45
+ try {
46
+ const parsed = JSON.parse(fs.readFileSync(path.join(dir, LANES_FLAG_CACHE_FILE), "utf8"));
47
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
48
+ return {};
49
+ const entries = parsed.entries;
50
+ if (!entries || typeof entries !== "object" || Array.isArray(entries))
51
+ return {};
52
+ return entries;
53
+ }
54
+ catch {
55
+ return {};
56
+ }
57
+ }
58
+ function readCachedFlagValue(dir, key, now = Date.now()) {
59
+ const entry = readFlagCacheFile(dir)[key];
60
+ if (!entry || typeof entry.value !== "boolean" || typeof entry.resolvedAt !== "string")
61
+ return null;
62
+ const resolvedAt = Date.parse(entry.resolvedAt);
63
+ if (!Number.isFinite(resolvedAt))
64
+ return null;
65
+ const age = now - resolvedAt;
66
+ // A future timestamp (clock skew or a hand-edited file) is not trusted either.
67
+ if (age < 0 || age > LANES_FLAG_CACHE_MAX_AGE_MS)
68
+ return null;
69
+ return entry.value;
70
+ }
71
+ function writeCachedFlagValue(dir, key, value) {
72
+ let tmp;
73
+ try {
74
+ const entries = readFlagCacheFile(dir);
75
+ // Always restamp resolvedAt, even for an unchanged value: the max-age check
76
+ // measures time since the registry last confirmed it. Delete first so the
77
+ // key moves to the end and eviction drops the least recently confirmed.
78
+ delete entries[key];
79
+ entries[key] = { value, resolvedAt: new Date().toISOString() };
80
+ const keys = Object.keys(entries);
81
+ for (const stale of keys.slice(0, Math.max(0, keys.length - FLAG_CACHE_MAX_ENTRIES)))
82
+ delete entries[stale];
83
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
84
+ const target = path.join(dir, LANES_FLAG_CACHE_FILE);
85
+ tmp = `${target}.${process.pid}.${Date.now()}.tmp`;
86
+ fs.writeFileSync(tmp, `${JSON.stringify({ version: 1, entries })}\n`, { mode: 0o600 });
87
+ fs.renameSync(tmp, target);
88
+ }
89
+ catch {
90
+ if (tmp) {
91
+ try {
92
+ fs.unlinkSync(tmp);
93
+ }
94
+ catch {
95
+ // Nothing to clean up.
96
+ }
97
+ }
98
+ // The cache is best-effort; a read must never fail because it cannot be saved.
99
+ }
100
+ }
101
+ const TOKENISH_PATTERNS = [
102
+ /bearer\s+\S+/gi,
103
+ /eyJ[A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+)*/g,
104
+ /\b(?:hq|sk|pk|ghp|gho|xox[a-z])[_-][A-Za-z0-9_-]{8,}/gi,
105
+ /[A-Za-z0-9+/_-]{32,}={0,2}/g,
106
+ ];
107
+ /** Error message safe for stderr: token-like substrings removed, length bounded. */
108
+ export function sanitizeFlagErrorMessage(error) {
109
+ const raw = error instanceof Error ? error.message : typeof error === "string" ? error : "";
110
+ let message = raw;
111
+ for (const pattern of TOKENISH_PATTERNS)
112
+ message = message.replace(pattern, "[redacted]");
113
+ return message.length > 200 ? `${message.slice(0, 200)}...` : message;
114
+ }
18
115
  function errorClass(error) {
19
116
  return error instanceof Error ? error.name : "UnknownError";
20
117
  }
21
- function reportReadFailure(flagKey, error, fallback) {
118
+ function reportReadFailure(flagKey, error, fallback, diagnostics) {
22
119
  const suffix = flagKey.replace(/^lanes\./, "").replaceAll(/[^a-zA-Z0-9]+/g, "_");
23
120
  process.stderr.write(`${JSON.stringify({
24
121
  event: `lanes.${suffix}.flag_read_failed`,
25
122
  flag: flagKey,
26
123
  error: errorClass(error),
124
+ error_message: sanitizeFlagErrorMessage(error),
125
+ ...(diagnostics?.status !== undefined ? { status: diagnostics.status } : {}),
126
+ ...(diagnostics ? {
127
+ duration_ms: diagnostics.durationMs,
128
+ attempts: diagnostics.attempts,
129
+ token_refreshed: diagnostics.tokenRefreshed,
130
+ cache_used: diagnostics.cacheUsed,
131
+ } : {}),
27
132
  fallback,
28
133
  })}\n`);
29
134
  }
@@ -47,8 +152,11 @@ function cacheKey(flagKey, company, endpoint) {
47
152
  }
48
153
  /**
49
154
  * Read a lanes flag once for each company identity in this CLI process. The
50
- * cache contains only an in-flight or settled decision result; it is never
51
- * written to disk. A local override is checked on every call and wins first.
155
+ * in-process cache holds the in-flight or settled result. The last registry
156
+ * snapshot value per flag and company is also kept in the HQ state dir and
157
+ * used when a later read fails transiently (timeout, network error, 5xx) and
158
+ * the value was confirmed within LANES_FLAG_CACHE_MAX_AGE_MS. A local override
159
+ * is checked on every call and wins first.
52
160
  */
53
161
  export async function readLanesRegistryFlag(flagKey, options = {}) {
54
162
  const result = await readLanesRegistryFlagResult(flagKey, options);
@@ -72,7 +180,7 @@ async function readRegistryFlagOutcome(flagKey, options, failOpen) {
72
180
  const endpoint = resolveFlagRegistryEndpoint(undefined, env);
73
181
  const key = cacheKey(flagKey, options.company, endpoint);
74
182
  const cached = registryReads.get(key);
75
- const pending = cached ?? readRegistryValue(flagKey, endpoint, options);
183
+ const pending = cached ?? readRegistryValue(flagKey, endpoint, key, options);
76
184
  if (!cached)
77
185
  registryReads.set(key, pending);
78
186
  const outcome = await pending;
@@ -108,42 +216,89 @@ export async function readLanesRegistryFlagFailOff(flagKey, options = {}, reader
108
216
  export function _resetThrownFlagReadWarningsForTests() {
109
217
  reportedThrownFlagReads.clear();
110
218
  }
111
- async function readRegistryValue(flagKey, endpoint, options) {
219
+ function isTimeoutError(error) {
220
+ if (!(error instanceof Error))
221
+ return false;
222
+ return error.name === "AbortError" || error.name === "TimeoutError" ||
223
+ error.message.startsWith("hq-flags: request timed out");
224
+ }
225
+ function classifyAttempt(attempt) {
226
+ if (!attempt.failure)
227
+ return "none";
228
+ const status = attempt.status;
229
+ if (status !== undefined && status >= 400) {
230
+ if (status === 401)
231
+ return "unauthorized";
232
+ if (status >= 500)
233
+ return "transient";
234
+ return "permanent";
235
+ }
236
+ if (attempt.networkError || isTimeoutError(attempt.failure.error))
237
+ return "transient";
238
+ return "permanent";
239
+ }
240
+ async function defaultRefreshToken() {
241
+ if (wantsMachineCognitoTokens()) {
242
+ return ensureCognitoIdToken({ interactive: false });
243
+ }
244
+ const cached = loadCachedTokens();
245
+ if (!cached)
246
+ throw new MissingFlagTokenError();
247
+ const refreshed = await refreshActivePersonTokens(cached);
248
+ return refreshed.idToken;
249
+ }
250
+ function defaultAcquireToken() {
251
+ return async () => {
252
+ try {
253
+ return await ensureCognitoIdToken({ interactive: false });
254
+ }
255
+ catch (error) {
256
+ if (error instanceof Error &&
257
+ error.message.startsWith("No valid HQ session")) {
258
+ throw new MissingFlagTokenError();
259
+ }
260
+ throw error;
261
+ }
262
+ };
263
+ }
264
+ function requireToken(token) {
265
+ if (typeof token !== "string" || token.trim().length === 0) {
266
+ throw new MissingFlagTokenError();
267
+ }
268
+ return token;
269
+ }
270
+ async function readRegistryAttempt(flagKey, endpoint, options, acquireToken) {
112
271
  const company = options.company;
113
272
  const companyUid = company?.uid?.trim();
114
273
  const companySlug = company?.slug?.trim();
274
+ const baseFetch = options.fetch ?? globalThis.fetch;
115
275
  let client;
116
- let resolution;
117
- let failure;
276
+ const attempt = { networkError: false };
118
277
  const rememberFailure = (error) => {
119
- failure ??= { error };
278
+ attempt.failure ??= { error };
120
279
  };
121
280
  try {
122
- const acquireToken = options.getToken ?? (async () => {
123
- try {
124
- return await ensureCognitoIdToken({ interactive: false });
125
- }
126
- catch (error) {
127
- if (error instanceof Error &&
128
- error.message.startsWith("No valid HQ session")) {
129
- throw new MissingFlagTokenError();
130
- }
131
- throw error;
132
- }
133
- });
134
- const getToken = async () => {
135
- const token = await acquireToken();
136
- if (typeof token !== "string" || token.trim().length === 0) {
137
- throw new MissingFlagTokenError();
138
- }
139
- return token;
140
- };
141
281
  client = (options.createClient ?? createFlagClient)({
142
282
  endpoint,
143
- getToken,
283
+ getToken: async () => requireToken(await acquireToken()),
144
284
  env: {},
145
285
  refreshIntervalMs: 0,
146
286
  requestTimeoutMs: LANES_FLAG_READ_TIMEOUT_MS,
287
+ ...(baseFetch
288
+ ? {
289
+ fetch: async (input, init) => {
290
+ try {
291
+ const res = await baseFetch(input, init);
292
+ attempt.status = res.status;
293
+ return res;
294
+ }
295
+ catch (error) {
296
+ attempt.networkError = true;
297
+ throw error;
298
+ }
299
+ },
300
+ }
301
+ : {}),
147
302
  ...(companyUid ? { companyUid } : {}),
148
303
  ...(companySlug ? { companyIdentifiers: [companySlug] } : {}),
149
304
  onError: rememberFailure,
@@ -155,7 +310,7 @@ async function readRegistryValue(flagKey, endpoint, options) {
155
310
  rememberFailure(error);
156
311
  }
157
312
  try {
158
- resolution = client.explain(flagKey, { fallback: options.defaultValue ?? false });
313
+ attempt.resolution = client.explain(flagKey, { fallback: options.defaultValue ?? false });
159
314
  }
160
315
  catch (error) {
161
316
  rememberFailure(error);
@@ -172,12 +327,77 @@ async function readRegistryValue(flagKey, endpoint, options) {
172
327
  rememberFailure(error);
173
328
  }
174
329
  }
175
- if (resolution?.source === "snapshot" || resolution?.source === "local-override") {
176
- return { result: { state: "resolved", value: resolution.value }, ...(failure ? { failure } : {}) };
330
+ return attempt;
331
+ }
332
+ function isSnapshotResolution(resolution) {
333
+ return resolution?.source === "snapshot" || resolution?.source === "local-override";
334
+ }
335
+ async function readRegistryValue(flagKey, endpoint, key, options) {
336
+ const startedAt = Date.now();
337
+ const sleep = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
338
+ let acquireToken = options.getToken ?? defaultAcquireToken();
339
+ let attempt = await readRegistryAttempt(flagKey, endpoint, options, acquireToken);
340
+ let attempts = 1;
341
+ let tokenRefreshed = false;
342
+ if (!isSnapshotResolution(attempt.resolution)) {
343
+ const kind = classifyAttempt(attempt);
344
+ if (kind === "transient") {
345
+ await sleep(options.retryBackoffMs ?? LANES_FLAG_RETRY_BACKOFF_MS);
346
+ attempt = await readRegistryAttempt(flagKey, endpoint, options, acquireToken);
347
+ attempts += 1;
348
+ }
349
+ else if (kind === "unauthorized") {
350
+ const refresh = options.refreshToken ?? (options.getToken ? options.getToken : defaultRefreshToken);
351
+ let refreshedToken;
352
+ try {
353
+ refreshedToken = requireToken(await refresh());
354
+ tokenRefreshed = true;
355
+ }
356
+ catch {
357
+ // Keep the original 401 as the reported failure.
358
+ }
359
+ if (refreshedToken !== undefined) {
360
+ const token = refreshedToken;
361
+ acquireToken = () => token;
362
+ attempt = await readRegistryAttempt(flagKey, endpoint, options, acquireToken);
363
+ attempts += 1;
364
+ }
365
+ }
366
+ }
367
+ const durationMs = Date.now() - startedAt;
368
+ const diagnostics = {
369
+ ...(attempt.status !== undefined ? { status: attempt.status } : {}),
370
+ durationMs,
371
+ attempts,
372
+ tokenRefreshed,
373
+ cacheUsed: false,
374
+ };
375
+ const cacheDir = flagCacheDir(options);
376
+ if (isSnapshotResolution(attempt.resolution)) {
377
+ const value = attempt.resolution.value;
378
+ if (attempt.resolution.source === "snapshot" && cacheDir)
379
+ writeCachedFlagValue(cacheDir, key, value);
380
+ return {
381
+ result: { state: "resolved", value },
382
+ ...(attempt.failure ? { failure: { ...attempt.failure, diagnostics } } : {}),
383
+ };
384
+ }
385
+ if (attempt.failure) {
386
+ // Only a transient failure (timeout, network error, 5xx) may serve the
387
+ // last-known-good value. A 401 that survived the refresh, a 403 or other
388
+ // 4xx, and a missing token mean the registry (or the session) refused the
389
+ // read, so it fails closed to the caller's default instead.
390
+ const transient = classifyAttempt(attempt) === "transient";
391
+ const cachedValue = transient && cacheDir ? readCachedFlagValue(cacheDir, key) : null;
392
+ if (cachedValue !== null) {
393
+ return {
394
+ result: { state: "resolved", value: cachedValue },
395
+ failure: { ...attempt.failure, diagnostics: { ...diagnostics, cacheUsed: true } },
396
+ };
397
+ }
398
+ return { result: { state: "unknown" }, failure: { ...attempt.failure, diagnostics } };
177
399
  }
178
- if (failure)
179
- return { result: { state: "unknown" }, failure };
180
- return { result: { state: "resolved", value: resolution?.value ?? false } };
400
+ return { result: { state: "resolved", value: attempt.resolution?.value ?? false } };
181
401
  }
182
402
  function reportRegistryFailure(flagKey, key, outcome, options, failOpen) {
183
403
  if (!outcome.failure)
@@ -194,7 +414,7 @@ function reportRegistryFailure(flagKey, key, outcome, options, failOpen) {
194
414
  reportFailure(outcome.failure.error);
195
415
  return;
196
416
  }
197
- reportReadFailure(flagKey, outcome.failure.error, fallback);
417
+ reportReadFailure(flagKey, outcome.failure.error, fallback, outcome.failure.diagnostics);
198
418
  }
199
419
  export function _resetLanesRegistryFlagCacheForTests() {
200
420
  registryReads.clear();
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Personal vault use from a company-locked session.
3
+ *
4
+ * Owner decision dq_20261020234444_f1b1ea4185: a session locked to one company
5
+ * may read the caller's own personal-vault secrets through `hq secrets exec
6
+ * --personal`, read-only, and every use is audit-logged. All other personal
7
+ * vault operations stay refused in such a session.
8
+ */
9
+ /** True for a person (prs_) or agent-self (agt_) vault uid used as a scope. */
10
+ export declare function isEntityScopeRef(ref: string): boolean;
11
+ /** Relative to the HQ root. One JSON object per line. */
12
+ export declare const PERSONAL_SECRETS_AUDIT_RELATIVE_PATH: string;
13
+ export declare const PERSONAL_SCOPE_READ_ONLY_MESSAGE = "Personal scope in a company-locked session is read-only: only `hq secrets exec --personal --only <NAMES> -- <command>` is allowed.";
14
+ export interface PersonalSecretsAuditEntry {
15
+ ts: string;
16
+ session_id: string;
17
+ locked_company: string;
18
+ command: "secrets exec";
19
+ scope: "personal";
20
+ secret_names: string[];
21
+ }
22
+ export declare function personalSecretsAuditPath(root: string): string;
23
+ /**
24
+ * Append one audit line before any value is read. Throws when the line cannot
25
+ * be written, so the caller fails closed instead of reading unlogged.
26
+ * Records secret names only, never values.
27
+ */
28
+ export declare function appendPersonalSecretsAudit(root: string, entry: Omit<PersonalSecretsAuditEntry, "ts" | "command" | "scope"> & {
29
+ ts?: string;
30
+ }): PersonalSecretsAuditEntry;
31
+ //# sourceMappingURL=personal-secrets-audit.d.ts.map
@@ -0,0 +1,45 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ /**
4
+ * Personal vault use from a company-locked session.
5
+ *
6
+ * Owner decision dq_20261020234444_f1b1ea4185: a session locked to one company
7
+ * may read the caller's own personal-vault secrets through `hq secrets exec
8
+ * --personal`, read-only, and every use is audit-logged. All other personal
9
+ * vault operations stay refused in such a session.
10
+ */
11
+ /** True for a person (prs_) or agent-self (agt_) vault uid used as a scope. */
12
+ export function isEntityScopeRef(ref) {
13
+ return /^(prs|agt)_/.test(ref.trim());
14
+ }
15
+ /** Relative to the HQ root. One JSON object per line. */
16
+ export const PERSONAL_SECRETS_AUDIT_RELATIVE_PATH = path.join("workspace", "audit", "personal-secrets-use.jsonl");
17
+ export const PERSONAL_SCOPE_READ_ONLY_MESSAGE = "Personal scope in a company-locked session is read-only: only `hq secrets exec --personal --only <NAMES> -- <command>` is allowed.";
18
+ export function personalSecretsAuditPath(root) {
19
+ return path.join(root, PERSONAL_SECRETS_AUDIT_RELATIVE_PATH);
20
+ }
21
+ /**
22
+ * Append one audit line before any value is read. Throws when the line cannot
23
+ * be written, so the caller fails closed instead of reading unlogged.
24
+ * Records secret names only, never values.
25
+ */
26
+ export function appendPersonalSecretsAudit(root, entry) {
27
+ const line = {
28
+ ts: entry.ts ?? new Date().toISOString(),
29
+ session_id: entry.session_id,
30
+ locked_company: entry.locked_company,
31
+ command: "secrets exec",
32
+ scope: "personal",
33
+ secret_names: [...entry.secret_names],
34
+ };
35
+ const file = personalSecretsAuditPath(root);
36
+ try {
37
+ fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
38
+ fs.appendFileSync(file, `${JSON.stringify(line)}\n`, { encoding: "utf8", mode: 0o600 });
39
+ }
40
+ catch (error) {
41
+ throw new Error(`Cannot write the personal secrets audit log (${PERSONAL_SECRETS_AUDIT_RELATIVE_PATH}); refusing to read personal secrets: ${error instanceof Error ? error.message : String(error)}`);
42
+ }
43
+ return line;
44
+ }
45
+ //# sourceMappingURL=personal-secrets-audit.js.map
@@ -192,6 +192,7 @@ export function beforeSend(event, _hint) {
192
192
  "core-version-unavailable",
193
193
  "core-update-available",
194
194
  "no-local-tree",
195
+ "company-not-resolved",
195
196
  "legacy-personal-journal",
196
197
  "never-synced",
197
198
  "invalid-last-sync",
@@ -31,6 +31,12 @@ function errMessage(err) {
31
31
  export function isAccessDeniedError(err) {
32
32
  const rec = errRecord(err);
33
33
  if (rec) {
34
+ // A presigned storage download that failed AFTER the API authorized the
35
+ // key (createCompanyPresignClient). The ACL already passed, so pointing
36
+ // the caller at `hq access` would loop. Checked before the message regex
37
+ // because the message names the HTTP status.
38
+ if (rec.code === "FILES_STORAGE_DOWNLOAD_FAILED")
39
+ return false;
34
40
  if (rec.status === 403)
35
41
  return true;
36
42
  const meta = rec.$metadata;
@@ -64,6 +64,17 @@ export declare const CLIENT_HEALTH_OBSERVATION_FILE = "cli-client-health.observa
64
64
  * always re-observe — they have opened the store anyway, so it costs nothing.
65
65
  */
66
66
  export declare const JOURNAL_OBSERVATION_TTL_MS: number;
67
+ /**
68
+ * V8 old-space cap, in MB, for the child process that enumerates journals on
69
+ * behalf of a plain invocation. `listJournals()` materializes every area of
70
+ * the local sync state store; on a large store that took a short-lived
71
+ * command such as `hq secrets exec` to 2.5-4 GB RSS (feedback_d70e9d63). The
72
+ * child either finishes inside this cap or dies, and the command never holds
73
+ * that memory itself.
74
+ */
75
+ export declare const JOURNAL_OBSERVER_HEAP_MB = 512;
76
+ /** Wall-clock bound for the isolated journal observer. */
77
+ export declare const JOURNAL_OBSERVER_TIMEOUT_MS = 20000;
67
78
  export interface CliClientHealthState {
68
79
  /** Stable random installation identity — regenerated only if invalid. */
69
80
  installationId: string;
@@ -120,6 +131,24 @@ interface JournalLike {
120
131
  * contract ISO-UTC. Null when nothing has ever synced.
121
132
  */
122
133
  export declare function latestJournalSyncTime(journals: readonly JournalLike[]): string | null;
134
+ /**
135
+ * The isolated observer could not produce a value (heap cap, timeout, crash or
136
+ * unreadable output). The caller stamps the observation so the next attempt
137
+ * waits a full TTL instead of re-running a read already known to fail here.
138
+ */
139
+ export declare class JournalObservationUnavailableError extends Error {
140
+ constructor(message: string);
141
+ }
142
+ /**
143
+ * Run `listJournals()` in a heap-capped child process and return only the
144
+ * `lastSync` values. Used for plain invocations so the full store
145
+ * materialization never lands in the invoking command's own memory.
146
+ */
147
+ export declare function observeJournalsIsolated(opts?: {
148
+ engineUrl?: string;
149
+ heapMb?: number;
150
+ timeoutMs?: number;
151
+ }): JournalLike[];
123
152
  /**
124
153
  * True when this event kind must pay for a fresh `listJournals()` read.
125
154
  *
@@ -167,6 +196,11 @@ export interface ClientHealthDeps {
167
196
  expiring?: typeof isExpiring;
168
197
  versions?: () => VersionInfo;
169
198
  journals?: () => readonly JournalLike[];
199
+ /**
200
+ * Journal source for plain invocations when `journals` is not set. Defaults
201
+ * to {@link observeJournalsIsolated}; sync events read in-process.
202
+ */
203
+ isolatedJournals?: () => readonly JournalLike[];
170
204
  post?: HeartbeatPoster;
171
205
  timeoutMs?: number;
172
206
  /** US-016: overridable for tests; defaults to {@link collectLocalFilesOverview}. */