@indigoai-us/hq-cli 5.108.5 → 5.108.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,97 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.108.7] — 2026-09-05
6
+
7
+ ### Fixed
8
+
9
+ - Fresh agent boxes and Outposts no longer fail `hq auth refresh` with "No
10
+ valid session: no cached session" when `HQ_MACHINE_CREDS_FILE` is unset.
11
+ Regression since 5.108.2: machine-token minting required that env (or an
12
+ explicit `{ tokenSource: "machine" }`), but hq-pro cloud-init never exports
13
+ it, so bootstrap aborted after sixty refresh failures. When the env is
14
+ unset, there is no usable person session at `~/.hq/cognito-tokens.json`, and
15
+ the default `~/.hq-agent/machine-creds.json` is readable, the CLI now mints
16
+ from that file (same daemon-state-dir cache as the env opt-in) and logs
17
+ `machine identity from default creds file`. A present person session still
18
+ wins so a human login is never flipped onto machine tokens; explicit
19
+ `tokenSource: "person"` never uses machine creds. `hq auth status` and
20
+ `hq whoami` report the resolved token source.
21
+
22
+ ## [5.108.6] — 2026-09-05
23
+
24
+ ### Added
25
+
26
+ - `hq feedback bug|feature` now uploads a full log bundle alongside the report,
27
+ so a bug report carries the submitter's whole recent log history rather than a
28
+ 16 KiB sliver of it. Measured against a real `~/.hq`: 21 MB of logs across 21
29
+ files, compressed to 1.3 MB — roughly 500x what the inline path can hold.
30
+
31
+ The inline `diagnostics.logs` summary is unchanged and still ships in the
32
+ request body. It cannot be grown: the server rejects a request over 64 KiB and
33
+ stores `diagnostics` in a DynamoDB item, which caps at 400 KB. So the bundle
34
+ goes out of band — gzipped and uploaded direct to S3 through a presigned URL,
35
+ the same path screenshots already use — and the report carries only a
36
+ reference to it.
37
+
38
+ Three properties are load-bearing and are covered by tests that fail if any is
39
+ removed:
40
+
41
+ - **Same allowlist.** Discovery is reused from the inline collector verbatim.
42
+ The bundle carries more OF the same files, never more files. Credential
43
+ material lives outside `~/.hq` (`~/.codex/auth.json`,
44
+ `~/.hq-agent/machine-creds.json`), so widening discovery — not raising the
45
+ size — is what would turn this into an exfiltration bug.
46
+ - **Same redaction.** Every chunk passes through the existing redactor before
47
+ compression. A chunk boundary is held open rather than cut through a PEM
48
+ block, and an over-long line is cut on whitespace, so a split can never
49
+ bisect a token and let the halves escape a pattern that would have matched
50
+ the whole.
51
+ - **Bounded memory.** 50 MB compressed is roughly a gigabyte of raw text.
52
+ Nothing is fully materialised: files are read in slices, redacted, and
53
+ streamed into gzip, retaining only the compressed output.
54
+
55
+ Ordering is the truncation policy — status documents first, then logs newest
56
+ first, so a capped run keeps what a triager reads first. Oversized files are
57
+ read from the tail, because the end of a log is what explains a failure.
58
+
59
+ `--no-logs` and `HQ_FEEDBACK_LOGS=0` disable the bundle and the inline logs
60
+ together. Every failure is silent and non-fatal: a missing endpoint, a
61
+ disabled bucket, a refused presign, or a failed upload all degrade to
62
+ submitting without the bundle rather than costing the user their bug report.
63
+
64
+ ### Added
65
+
66
+ - `hq feedback` debug capture now also collects three more `~/.hq` sources, all
67
+ small and all high-signal for the failures they describe:
68
+
69
+ - **`locks/`** — held operation locks and background-work claims. This is the
70
+ one place where the FILENAME is the diagnosis: a `…stale-claim…` entry names
71
+ the operation, the process that abandoned it, and the fact that the claim
72
+ went stale, which is the answer to "sync/reindex is stuck". A hung process
73
+ writes nothing to a log, so this is otherwise invisible. Zero-byte lock
74
+ files are kept for their names rather than skipped as empty.
75
+ - **`jobs/`** — scheduled-job status, last reconcile, and probe attempts,
76
+ answering "my scheduled job never ran". Nested, so this is the collector's
77
+ only directory walk; it is bounded in depth (3) and file count (20), and
78
+ refuses to traverse a symlinked directory so it cannot escape `~/.hq`.
79
+ - Three more root status documents: `outpost-session-heartbeat.json` (Outpost
80
+ liveness), `version-check.json` (why an update is not offered), and
81
+ `plan-limit-nag.json`.
82
+
83
+ Deliberately still excluded: the two multi-megabyte telemetry cursors, the
84
+ `sync-state-v3/` internals, `backups/`, and everything outside `~/.hq` —
85
+ notably Claude Code session transcripts, which are conversation content and
86
+ do not belong on a bug report.
87
+
88
+ ### Changed
89
+
90
+ - Runtime bumped to `@indigoai-us/hq-cloud` ~6.16.11: personal-vault sync no
91
+ longer downloads `workspace/.session-logs/` from other machines (push-only),
92
+ and `hq reindex` now prunes local session-log copies 7 days after the vault
93
+ confirms them (`HQ_SESSION_LOG_LOCAL_RETENTION_DAYS`, `off` to disable).
94
+
95
+
5
96
  ## [5.108.5] — 2026-09-04
6
97
 
7
98
  ### Added
@@ -10,6 +101,49 @@
10
101
  through HQ's managed source pipeline, with immutable idempotent replay and
11
102
  explicit conflict detection when an external source ID is reused with
12
103
  different content.
104
+ - `hq feedback bug|feature` now attaches bounded, redacted debug evidence from
105
+ the submitter's `~/.hq` directory to the submission's `diagnostics`, so a bug
106
+ report carries the context a triager would otherwise have to ask for. Two
107
+ classes are collected:
108
+
109
+ - **Log tails** — `*.log` and `*.jsonl` directly inside `~/.hq/logs/`,
110
+ including rotated generations (`hq-sync.log.1`, `.2`, `.3`), plus
111
+ `boot-sync.log` and `boot-capture.log` at the `~/.hq` root. This covers the
112
+ hq-cloud sync engine and the desktop sync app (which share
113
+ `logs/hq-sync.log`) and the CLI's MCP registry audit trail
114
+ (`logs/mcp-registry.log`). Rotated generations matter most right after a
115
+ rotation, when the active file is nearly empty and the history a triager
116
+ needs sits in `.1`. Any future log dropped into `~/.hq/logs/` is picked up
117
+ automatically.
118
+ - **State snapshots** — the small JSON status documents that say where sync
119
+ and client health stand: `sync-progress.json` (phase, file counts, conflict
120
+ count), `sync-version.json`, `cli-client-health.json` (failure streak),
121
+ `cli-client-health.observation.json`, and the per-company
122
+ `sync-journal.<slug>.json` locators. These are collected first, since they
123
+ answer questions a log tail cannot, and the log budget takes what remains.
124
+
125
+ Eligibility is an explicit allowlist in both classes. The rest of `~/.hq`
126
+ (`cognito-tokens.json`, `deploy-passwords.json`, `secrets-cache/`) is never
127
+ read, the enormous `sync-journal.*.json.last-good` snapshots are excluded by
128
+ suffix, and eligibility is decided with `lstat` so a symlink wearing an
129
+ eligible name cannot be used to reach a credential file.
130
+
131
+ Every tail is redacted before it is measured or sent (JWTs, AWS key ids,
132
+ GitHub and Slack tokens, `Bearer` values, SigV4 presign signatures, PEM
133
+ private-key blocks, and `key=value` pairs for secret-shaped names). The total
134
+ is budgeted against the headroom left under the server's 64 KiB request-body
135
+ cap and re-measured against the real serialized envelope after attaching, so
136
+ a large report can never be turned into a 413 by its own diagnostics.
137
+
138
+ Opt out per invocation with `--no-logs`, or globally with
139
+ `HQ_FEEDBACK_LOGS=0`.
140
+
141
+ ### Fixed
142
+
143
+ - `hq` self-update no longer loops. After the CLI updates to `@latest` and a
144
+ re-run produces no effective version change, the version check records that
145
+ outcome and converges instead of re-triggering the updater, so 5.105.x (and
146
+ later) installs stop repeatedly self-updating (#494).
13
147
 
14
148
  ## [5.108.4] — 2026-09-04
15
149
 
@@ -14,8 +14,8 @@
14
14
  * by the deploy + sync skills.
15
15
  */
16
16
  import chalk from "chalk";
17
- import { browserLogin, clearCachedTokens, loadCachedTokens, isExpiring, isMachineIdentity, loadMachineCreds, CognitoAuthError, } from "@indigoai-us/hq-cloud";
18
- import { DEFAULT_COGNITO, refreshCachedSession, } from "../utils/cognito-session.js";
17
+ import { browserLogin, clearCachedTokens, loadCachedTokens, isExpiring, loadMachineCreds, CognitoAuthError, } from "@indigoai-us/hq-cloud";
18
+ import { DEFAULT_COGNITO, refreshCachedSession, resolveCognitoTokenSource, } from "../utils/cognito-session.js";
19
19
  import { cognitoConfigForLoginProvider } from "../utils/login-provider.js";
20
20
  import { callbackPortBusyGuidance, DEFAULT_CALLBACK_PORT, isCallbackPortBusy, } from "../utils/callback-port-busy.js";
21
21
  /**
@@ -56,7 +56,7 @@ export function registerAuthCommands(program) {
56
56
  .description("Sign in to HQ — opens the Cognito Hosted UI and caches tokens locally")
57
57
  .option("--provider <provider>", "OAuth provider to use: google, microsoft, or picker")
58
58
  .action(async (options) => {
59
- if (isMachineIdentity()) {
59
+ if (resolveCognitoTokenSource() === "machine") {
60
60
  console.log(chalk.green(`Running as ${machineIdentityLabel()} — sessions mint automatically; no browser login needed.`));
61
61
  return;
62
62
  }
@@ -119,16 +119,22 @@ export function registerAuthCommands(program) {
119
119
  .command("status")
120
120
  .description("Show whether a valid HQ session is cached")
121
121
  .action(() => {
122
- const machine = isMachineIdentity();
122
+ // Effective source: env opt-in, fresh-box default-creds when no usable
123
+ // person session, or after a definitive person-refresh rejection marked
124
+ // the person cache rejected (transient refresh errors still report person).
125
+ const tokenSource = resolveCognitoTokenSource();
126
+ const machine = tokenSource === "machine";
123
127
  const cached = loadCachedTokens();
124
128
  if (!cached) {
125
129
  if (machine) {
126
130
  // No cached session yet, but machine creds mint one on demand —
127
131
  // report ready, not signed-out.
128
132
  console.log(chalk.green(`${machineIdentityLabel()} — no cached session yet (mints automatically on first use)`));
133
+ console.log(chalk.dim(`token source: ${tokenSource}`));
129
134
  return;
130
135
  }
131
136
  console.log(chalk.yellow("No cached HQ session — run `hq auth login`"));
137
+ console.log(chalk.dim(`token source: ${tokenSource}`));
132
138
  process.exit(1);
133
139
  }
134
140
  const claims = peekIdToken(cached.idToken);
@@ -140,6 +146,7 @@ export function registerAuthCommands(program) {
140
146
  console.log(expiring
141
147
  ? chalk.yellow(`${label}HQ session cached but expiring (expiresAt=${cached.expiresAt})`)
142
148
  : chalk.green(`${label}HQ session valid (expiresAt=${cached.expiresAt})`));
149
+ console.log(chalk.dim(`token source: ${tokenSource}`));
143
150
  });
144
151
  }
145
152
  //# sourceMappingURL=auth.js.map
@@ -1,5 +1,17 @@
1
1
  import { Command } from "commander";
2
+ import { type DiagnosticsBlob } from "../utils/feedback-diagnostics.js";
3
+ import { collectFeedbackLogs, feedbackLogsEnabled } from "../utils/feedback-logs.js";
4
+ import { type LogBundleRef } from "../utils/feedback-log-bundle.js";
2
5
  export declare const BODY_MAX_BYTES: number;
6
+ /**
7
+ * Mirror of the server's `MAX_BODY_BYTES` in hq-pro
8
+ * `src/vault-service/handlers/feedback.ts`, which rejects the WHOLE request
9
+ * body over this size with a 413. Note this is the same number as
10
+ * `BODY_MAX_BYTES` above but a different measurement: that one bounds the
11
+ * markdown body alone, this one bounds the serialized JSON envelope. Debug
12
+ * logs are budgeted against whatever headroom is left between them.
13
+ */
14
+ export declare const REQUEST_MAX_BYTES: number;
3
15
  export interface FeedbackResult {
4
16
  id: string;
5
17
  }
@@ -11,8 +23,34 @@ export interface FeedbackSubmitOptions {
11
23
  token: string;
12
24
  /** S3 object keys of already-uploaded screenshots (see uploadScreenshots). */
13
25
  screenshots?: string[];
26
+ /**
27
+ * Attach redacted tails of the submitter's `~/.hq` logs. Defaults to true;
28
+ * `false` (from `--no-logs`) skips collection entirely. Even when true, the
29
+ * user's `HQ_FEEDBACK_LOGS` env override still applies.
30
+ */
31
+ includeLogs?: boolean;
32
+ /**
33
+ * Reference to an already-uploaded log bundle (see uploadLogBundle). Carries
34
+ * the full log history that the inline `diagnostics.logs` blob cannot — the
35
+ * inline blob is bounded by the request cap and DynamoDB's item limit, so it
36
+ * stays a summary and this carries the rest.
37
+ */
38
+ logBundle?: LogBundleRef;
14
39
  }
15
40
  export declare function readBodyFile(bodyFile: string, stdin?: NodeJS.ReadableStream): Promise<string>;
41
+ /**
42
+ * Attach redacted `~/.hq` log tails to `diagnostics` if, and only if, they fit
43
+ * inside the server's request-body cap.
44
+ *
45
+ * Mutates `diagnostics` in place (it is already referenced by `requestBody`,
46
+ * so the measurement below sees the attached logs). Best-effort throughout: a
47
+ * collection failure leaves the submission untouched rather than failing a bug
48
+ * report over its own diagnostics.
49
+ */
50
+ export declare function attachDebugLogs(requestBody: Record<string, unknown>, diagnostics: DiagnosticsBlob, enabled: boolean, deps?: {
51
+ collect?: typeof collectFeedbackLogs;
52
+ envEnabled?: typeof feedbackLogsEnabled;
53
+ }): void;
16
54
  export declare function submitFeedback(opts: FeedbackSubmitOptions): Promise<FeedbackResult>;
17
55
  export declare function registerFeedbackCommand(program: Command): void;
18
56
  //# sourceMappingURL=feedback.d.ts.map
@@ -4,7 +4,24 @@ import { ensureCognitoToken } from "../utils/cognito-session.js";
4
4
  import { vaultApiFetch } from "../utils/vault-api.js";
5
5
  import { collectDiagnostics } from "../utils/feedback-diagnostics.js";
6
6
  import { MAX_SCREENSHOTS, uploadScreenshots } from "../utils/feedback-screenshots.js";
7
+ import { FEEDBACK_LOGS_MAX_TOTAL_BYTES, FEEDBACK_LOGS_MIN_USEFUL_BYTES, collectFeedbackLogs, feedbackLogsEnabled, } from "../utils/feedback-logs.js";
8
+ import { uploadLogBundle } from "../utils/feedback-log-bundle.js";
7
9
  export const BODY_MAX_BYTES = 64 * 1024;
10
+ /**
11
+ * Mirror of the server's `MAX_BODY_BYTES` in hq-pro
12
+ * `src/vault-service/handlers/feedback.ts`, which rejects the WHOLE request
13
+ * body over this size with a 413. Note this is the same number as
14
+ * `BODY_MAX_BYTES` above but a different measurement: that one bounds the
15
+ * markdown body alone, this one bounds the serialized JSON envelope. Debug
16
+ * logs are budgeted against whatever headroom is left between them.
17
+ */
18
+ export const REQUEST_MAX_BYTES = 64 * 1024;
19
+ /**
20
+ * Headroom held back for the `"logs":{...}` JSON envelope itself — its keys,
21
+ * braces, and the escaping of the tail text. The final size is re-measured
22
+ * after attaching, so this reserve only needs to be roughly right.
23
+ */
24
+ const LOGS_ENVELOPE_RESERVE_BYTES = 1024;
8
25
  export async function readBodyFile(bodyFile, stdin) {
9
26
  if (bodyFile === "-") {
10
27
  const stream = stdin ?? process.stdin;
@@ -23,6 +40,44 @@ export async function readBodyFile(bodyFile, stdin) {
23
40
  }
24
41
  return fs.promises.readFile(bodyFile, "utf-8");
25
42
  }
43
+ /**
44
+ * Attach redacted `~/.hq` log tails to `diagnostics` if, and only if, they fit
45
+ * inside the server's request-body cap.
46
+ *
47
+ * Mutates `diagnostics` in place (it is already referenced by `requestBody`,
48
+ * so the measurement below sees the attached logs). Best-effort throughout: a
49
+ * collection failure leaves the submission untouched rather than failing a bug
50
+ * report over its own diagnostics.
51
+ */
52
+ export function attachDebugLogs(requestBody, diagnostics, enabled, deps = {}) {
53
+ const collect = deps.collect ?? collectFeedbackLogs;
54
+ const envEnabled = deps.envEnabled ?? feedbackLogsEnabled;
55
+ if (!enabled || !envEnabled())
56
+ return;
57
+ try {
58
+ const baseBytes = Buffer.byteLength(JSON.stringify(requestBody), "utf8");
59
+ const available = REQUEST_MAX_BYTES - baseBytes - LOGS_ENVELOPE_RESERVE_BYTES;
60
+ if (available < FEEDBACK_LOGS_MIN_USEFUL_BYTES)
61
+ return;
62
+ const logs = collect({
63
+ budgetBytes: Math.min(available, FEEDBACK_LOGS_MAX_TOTAL_BYTES),
64
+ });
65
+ if (!logs)
66
+ return;
67
+ diagnostics.logs = logs;
68
+ // Final authority: re-measure the ACTUAL envelope. JSON escaping of the
69
+ // tail text (quotes, newlines, control chars) can expand it well past the
70
+ // raw byte count the budget counted, so the estimate above is necessary
71
+ // but not sufficient. If we overshot, drop the logs rather than 413.
72
+ if (Buffer.byteLength(JSON.stringify(requestBody), "utf8") > REQUEST_MAX_BYTES) {
73
+ delete diagnostics.logs;
74
+ }
75
+ }
76
+ catch {
77
+ // Never let diagnostics collection break the submission.
78
+ delete diagnostics.logs;
79
+ }
80
+ }
26
81
  export async function submitFeedback(opts) {
27
82
  // Validate the title locally, symmetric with the body check below. Commander's
28
83
  // `requiredOption("--title")` only requires the flag to be PRESENT — an empty
@@ -54,6 +109,18 @@ export async function submitFeedback(opts) {
54
109
  if (opts.screenshots && opts.screenshots.length > 0) {
55
110
  requestBody.screenshots = opts.screenshots;
56
111
  }
112
+ // The bundle reference is four small fields, so unlike the inline logs below
113
+ // it needs no budgeting — but it is added BEFORE attachDebugLogs so those
114
+ // bytes are counted against the inline budget rather than overrunning it.
115
+ if (opts.logBundle) {
116
+ requestBody.logBundle = opts.logBundle;
117
+ }
118
+ // Debug logs are attached LAST and sized against the headroom the rest of
119
+ // the request left behind. The server 413s the whole body over 64 KiB, so a
120
+ // fixed log budget would turn a previously-working large-body submission
121
+ // into a hard failure. Measuring the real serialized envelope — the exact
122
+ // bytes vaultApiFetch will send — is what makes that impossible.
123
+ attachDebugLogs(requestBody, diagnostics, opts.includeLogs !== false);
57
124
  const res = await vaultApiFetch({
58
125
  token: opts.token,
59
126
  path: "/v1/feedback",
@@ -81,6 +148,9 @@ function registerSubcommand(feedbackCmd, type) {
81
148
  .requiredOption("--body-file <path>", "Path to a markdown file with the body; use - to read from stdin")
82
149
  .option("--company <slug>", "Company slug to associate with the report")
83
150
  .option("--screenshot <path>", `Attach a screenshot (repeatable, up to ${MAX_SCREENSHOTS}; .png/.jpg/.jpeg/.webp/.gif)`, (value, prev) => [...prev, value], [])
151
+ // Commander turns `--no-logs` into a boolean `logs` that defaults to true,
152
+ // so the default is "attach logs" and the flag is the opt-out.
153
+ .option("--no-logs", "Do not attach redacted tails of your ~/.hq log files (also: HQ_FEEDBACK_LOGS=0)")
84
154
  .action(async (opts) => {
85
155
  try {
86
156
  const token = await ensureCognitoToken({ interactive: false });
@@ -91,6 +161,14 @@ function registerSubcommand(feedbackCmd, type) {
91
161
  paths: opts.screenshot ?? [],
92
162
  token,
93
163
  });
164
+ // Full-history logs go direct to S3 before the submission that
165
+ // references them. Best-effort throughout: if the endpoint is absent
166
+ // (older server), the bucket is unconfigured, or the upload fails,
167
+ // this resolves undefined and the report still carries inline logs.
168
+ const logBundle = await uploadLogBundle({
169
+ token,
170
+ enabled: opts.logs !== false && feedbackLogsEnabled(),
171
+ });
94
172
  const result = await submitFeedback({
95
173
  type,
96
174
  title: opts.title,
@@ -98,6 +176,8 @@ function registerSubcommand(feedbackCmd, type) {
98
176
  company: opts.company,
99
177
  token,
100
178
  screenshots,
179
+ includeLogs: opts.logs !== false,
180
+ logBundle,
101
181
  });
102
182
  console.log(`Submitted: ${result.id}`);
103
183
  }
@@ -101,6 +101,12 @@ async function runList(options) {
101
101
  const format = options.format ?? defaultListFormat();
102
102
  const accessToken = await resolveAccessToken();
103
103
  const vaultConfig = buildVaultConfig(accessToken);
104
+ // HQ-59: every company (cmp_) read below rides the presign VaultClient
105
+ // (`usePresign` keys on the vault client + cmp_ prefix), so tell entity
106
+ // resolution to skip the company `POST /sts/vend` — the vended creds were
107
+ // never consumed here, but each call re-entered the packed FILE_ACL fit
108
+ // ladder (policyTruncated noise). Personal vaults still vend-self.
109
+ vaultConfig.companyVaultUsesPresign = true;
104
110
  const entity = await resolveEntity({ slug, vaultConfig });
105
111
  const result = await listSignals({
106
112
  entity,
@@ -134,6 +140,12 @@ async function runGet(options) {
134
140
  const format = options.format ?? defaultGetFormat();
135
141
  const accessToken = await resolveAccessToken();
136
142
  const vaultConfig = buildVaultConfig(accessToken);
143
+ // HQ-59: every company (cmp_) read below rides the presign VaultClient
144
+ // (`usePresign` keys on the vault client + cmp_ prefix), so tell entity
145
+ // resolution to skip the company `POST /sts/vend` — the vended creds were
146
+ // never consumed here, but each call re-entered the packed FILE_ACL fit
147
+ // ladder (policyTruncated noise). Personal vaults still vend-self.
148
+ vaultConfig.companyVaultUsesPresign = true;
137
149
  const entity = await resolveEntity({ slug, vaultConfig });
138
150
  const doc = await getSignal({
139
151
  entity,
@@ -109,6 +109,12 @@ async function runList(options) {
109
109
  const format = options.format ?? defaultListFormat();
110
110
  const accessToken = await resolveAccessToken();
111
111
  const vaultConfig = buildVaultConfig(accessToken);
112
+ // HQ-59: every company (cmp_) read below rides the presign VaultClient
113
+ // (`usePresign` keys on the vault client + cmp_ prefix), so tell entity
114
+ // resolution to skip the company `POST /sts/vend` — the vended creds were
115
+ // never consumed here, but each call re-entered the packed FILE_ACL fit
116
+ // ladder (policyTruncated noise). Personal vaults still vend-self.
117
+ vaultConfig.companyVaultUsesPresign = true;
112
118
  const entity = await resolveEntity({ slug, vaultConfig });
113
119
  const result = await listSources({
114
120
  entity,
@@ -142,6 +148,12 @@ async function runGet(options) {
142
148
  const format = options.format ?? defaultGetFormat();
143
149
  const accessToken = await resolveAccessToken();
144
150
  const vaultConfig = buildVaultConfig(accessToken);
151
+ // HQ-59: every company (cmp_) read below rides the presign VaultClient
152
+ // (`usePresign` keys on the vault client + cmp_ prefix), so tell entity
153
+ // resolution to skip the company `POST /sts/vend` — the vended creds were
154
+ // never consumed here, but each call re-entered the packed FILE_ACL fit
155
+ // ladder (policyTruncated noise). Personal vaults still vend-self.
156
+ vaultConfig.companyVaultUsesPresign = true;
145
157
  const entity = await resolveEntity({ slug, vaultConfig });
146
158
  const doc = await getSource({
147
159
  entity,
@@ -2,8 +2,9 @@
2
2
  * hq whoami — displays current user or 'not logged in'
3
3
  */
4
4
  import chalk from 'chalk';
5
- import { loadCachedTokens, isExpiring, isMachineIdentity, loadMachineCreds, } from '@indigoai-us/hq-cloud';
5
+ import { loadCachedTokens, isExpiring, loadMachineCreds, } from '@indigoai-us/hq-cloud';
6
6
  import { peekIdToken as decodeIdToken } from "../utils/id-token.js";
7
+ import { resolveCognitoTokenSource } from "../utils/cognito-session.js";
7
8
  function peekIdToken(idToken) {
8
9
  const decoded = decodeIdToken(idToken);
9
10
  return {
@@ -23,7 +24,10 @@ export function registerWhoamiCommand(program) {
23
24
  .description('Show the currently authenticated user')
24
25
  .action(async () => {
25
26
  try {
26
- const machine = isMachineIdentity();
27
+ // Effective source (same as auth status / ensureCognitoToken): env
28
+ // opt-in, fresh-box default-creds when no usable person session, or
29
+ // after a definitive person-refresh rejection marked the session rejected.
30
+ const machine = resolveCognitoTokenSource() === "machine";
27
31
  const cached = loadCachedTokens();
28
32
  if (machine) {
29
33
  // Machine identities (company agents) mint sessions on demand from
@@ -34,6 +38,7 @@ export function registerWhoamiCommand(program) {
34
38
  ? peekIdToken(cached.idToken).entityUid
35
39
  : undefined;
36
40
  console.log(`Machine identity ${username}${entityUid ? ` (agent ${entityUid})` : ''} — sessions mint automatically`);
41
+ console.log(`token source: machine`);
37
42
  return;
38
43
  }
39
44
  if (!cached) {
@@ -93,31 +93,64 @@ export declare function machineTokenStateDir(home?: string, env?: NodeJS.Process
93
93
  export declare function machineTokenCacheFile(home?: string, env?: NodeJS.ProcessEnv): string;
94
94
  /**
95
95
  * True when `HQ_MACHINE_CREDS_FILE` is explicitly set (non-empty) in `env`.
96
- * The mere presence of `~/.hq-agent/machine-creds.json` is NOT enough minting
97
- * and doctor "machine" reporting are opt-in via this env (systemd unit) or an
98
- * explicit `{ tokenSource: "machine" }` caller option.
96
+ * Explicit env remains the primary opt-in for daemon units. Fresh agent boxes
97
+ * without the env still mint via {@link wantsMachineCognitoTokens}'s
98
+ * default-file fallback when no usable person session exists.
99
99
  */
100
100
  export declare function isMachineCredsFileEnvSet(env?: NodeJS.ProcessEnv): boolean;
101
+ /** Default machine-creds path hq-cloud also checks when the env is unset. */
102
+ export declare function defaultMachineCredsFile(home?: string): string;
101
103
  export type EnsureCognitoTokenOptions = {
102
104
  interactive?: boolean;
103
105
  /**
104
106
  * Force machine-credential minting (`"machine"`) or the person login cache
105
- * (`"person"`). When omitted, machine minting is used only if
106
- * `HQ_MACHINE_CREDS_FILE` is explicitly set and readable.
107
+ * (`"person"`). When omitted, machine minting is used if
108
+ * `HQ_MACHINE_CREDS_FILE` is set, or (fresh-box fallback) the default
109
+ * `~/.hq-agent/machine-creds.json` is readable and no person session is
110
+ * cached. Explicit `"person"` never uses machine creds.
107
111
  */
108
112
  tokenSource?: CognitoTokenSource;
109
113
  };
114
+ /**
115
+ * On-disk person Cognito cache path for `env` (`HQ_STATE_DIR` or `~/.hq`).
116
+ * Deliberately does not use hq-cloud's `loadCachedTokens()`: after a machine
117
+ * mint in this process, that helper returns in-memory machine tokens when the
118
+ * person file is absent, which would falsely block the fresh-box fallback.
119
+ */
120
+ export declare function personTokenCacheFile(home?: string, env?: NodeJS.ProcessEnv): string;
121
+ /**
122
+ * True when a usable person Cognito cache file exists under the effective
123
+ * `HQ_STATE_DIR` (or `~/.hq`). Presence alone keeps human commands on the
124
+ * person path even when the default machine-creds file is also on disk
125
+ * (no-flip guarantee).
126
+ */
127
+ export declare function hasPersonCachedSession(env?: NodeJS.ProcessEnv): boolean;
128
+ /**
129
+ * Fresh-box / Outpost bootstrap: env unset, not forced to person, no person
130
+ * cache, and the default machine-creds file is readable. Distinct from
131
+ * explicit `HQ_MACHINE_CREDS_FILE` / `{ tokenSource: "machine" }` opt-in.
132
+ */
133
+ export declare function isDefaultMachineCredsFallback(options?: Pick<EnsureCognitoTokenOptions, "tokenSource">, env?: NodeJS.ProcessEnv): boolean;
110
134
  /**
111
135
  * Whether this call should mint/cache via machine creds (USER_PASSWORD_AUTH).
112
- * Opt-in only: explicit `{ tokenSource: "machine" }`, or `HQ_MACHINE_CREDS_FILE`
113
- * set in the environment. Default-path `~/.hq-agent/machine-creds.json` alone
114
- * never flips other CLI commands onto the machine path.
136
+ *
137
+ * Machine path when:
138
+ * - explicit `{ tokenSource: "machine" }` and creds are readable, or
139
+ * - `HQ_MACHINE_CREDS_FILE` is set and readable, or
140
+ * - fresh-box fallback: env unset, no person cache, default
141
+ * `~/.hq-agent/machine-creds.json` readable.
142
+ *
143
+ * Explicit `{ tokenSource: "person" }` never uses machine creds. A present
144
+ * person cache keeps the person path even when the default creds file exists
145
+ * (do not flip a human's commands onto machine tokens).
115
146
  */
116
147
  export declare function wantsMachineCognitoTokens(options?: Pick<EnsureCognitoTokenOptions, "tokenSource">, env?: NodeJS.ProcessEnv): boolean;
117
148
  /**
118
- * Which Cognito token source this process will use for doctor / reporting:
119
- * machine only when `HQ_MACHINE_CREDS_FILE` is explicitly set and readable;
120
- * otherwise the person login cache. Default-path machine-creds alone person.
149
+ * Which Cognito token source this process will use for doctor / status / whoami.
150
+ * Matches {@link wantsMachineCognitoTokens}: env opt-in, or default-file
151
+ * fallback when no usable person session is cached (absent, or marked rejected
152
+ * after a definitive Cognito refresh refusal). Transient refresh failures do
153
+ * not flip this to machine.
121
154
  */
122
155
  export declare function resolveCognitoTokenSource(env?: NodeJS.ProcessEnv): CognitoTokenSource;
123
156
  /** Derive actor kind from an ID token's custom:entity* claims (no verify). */
@@ -143,19 +176,28 @@ export declare function describeCognitoTokenSource(opts?: {
143
176
  * Run `fn` with `HQ_STATE_DIR` pointed at the machine/daemon token cache so
144
177
  * hq-cloud's mint writes never touch the person login file.
145
178
  *
146
- * When `HQ_STATE_DIR` is already set (tests / explicit callers), leave it alone
147
- * so a pre-seeded machine session is still found — only redirect when unset.
179
+ * Always redirects even when the caller already set `HQ_STATE_DIR` (person
180
+ * cache / tests). Override the machine cache location with
181
+ * `HQ_MACHINE_TOKEN_STATE_DIR` instead. Restores the prior env afterwards.
148
182
  */
149
183
  export declare function withMachineTokenStateDir<T>(fn: () => Promise<T>, opts?: {
150
184
  home?: string;
151
185
  env?: NodeJS.ProcessEnv;
152
186
  }): Promise<T>;
187
+ /**
188
+ * True when a person-token refresh failed with a definitive Cognito auth
189
+ * rejection (refresh token invalid/revoked). Transient network/5xx/timeouts
190
+ * must NOT trigger machine-identity fallback.
191
+ */
192
+ export declare function isDefinitivePersonAuthRejection(err: unknown): boolean;
153
193
  /**
154
194
  * Return a non-expired Cognito access token, refreshing or browser-logging-in
155
195
  * as needed. Person cache lives at ~/.hq/cognito-tokens.json. Machine minting
156
- * is opt-in: `HQ_MACHINE_CREDS_FILE` explicitly set, or
157
- * `{ tokenSource: "machine" }` then USER_PASSWORD_AUTH and (when
158
- * `HQ_STATE_DIR` is unset) a cache under the daemon state dir.
196
+ * uses `HQ_MACHINE_CREDS_FILE` / `{ tokenSource: "machine" }`, or the fresh-box
197
+ * fallback when the default creds file is readable and no person session
198
+ * exists (or person refresh fails with a definitive auth rejection).
199
+ * Machine tokens always cache under the dedicated machine/daemon state dir
200
+ * (never the caller's `HQ_STATE_DIR` person cache).
159
201
  *
160
202
  * Pass `interactive: false` from automated contexts (e.g. the `hq-auth-refresh`
161
203
  * bin invoked by the deploy skill) where failing fast is better than opening