@indigoai-us/hq-cli 5.345.170 → 5.345.172

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 (142) hide show
  1. package/CHANGELOG.md +84 -0
  2. package/assets/scaffold/core/scripts/hq-delegate-send.sh +1 -1
  3. package/dist/command-catalog.generated.d.ts +206 -7
  4. package/dist/command-catalog.generated.js +252 -8
  5. package/dist/command-registration-plan.d.ts +7 -0
  6. package/dist/command-registration-plan.js +1 -0
  7. package/dist/commands/access.d.ts +32 -1
  8. package/dist/commands/access.js +112 -3
  9. package/dist/commands/agent-kit.d.ts +1 -1
  10. package/dist/commands/agent-kit.js +3 -1
  11. package/dist/commands/agents.js +2 -1
  12. package/dist/commands/bot.d.ts +3 -0
  13. package/dist/commands/bot.js +179 -18
  14. package/dist/commands/cloud.d.ts +58 -1
  15. package/dist/commands/cloud.js +228 -10
  16. package/dist/commands/core-company-index-scope-flag.d.ts +8 -1
  17. package/dist/commands/core-company-index-scope-flag.js +9 -2
  18. package/dist/commands/core-delegate-w1b.js +5 -2
  19. package/dist/commands/core-dm-bind-native.js +19 -4
  20. package/dist/commands/core-rebuild-index-fast.js +24 -6
  21. package/dist/commands/core.js +18 -6
  22. package/dist/commands/daemon.js +16 -1
  23. package/dist/commands/dm.d.ts +17 -3
  24. package/dist/commands/dm.js +121 -26
  25. package/dist/commands/files.d.ts +1 -0
  26. package/dist/commands/files.js +22 -4
  27. package/dist/commands/integrations-api.d.ts +4 -0
  28. package/dist/commands/integrations-api.js +18 -2
  29. package/dist/commands/integrations-connect.d.ts +10 -3
  30. package/dist/commands/integrations-connect.js +70 -6
  31. package/dist/commands/integrations-core.d.ts +23 -0
  32. package/dist/commands/integrations-core.js +100 -0
  33. package/dist/commands/integrations-manage.js +8 -1
  34. package/dist/commands/integrations.js +49 -5
  35. package/dist/commands/lanes-loop.d.ts +20 -0
  36. package/dist/commands/lanes-loop.js +69 -11
  37. package/dist/commands/lanes.js +9 -1
  38. package/dist/commands/rescue.d.ts +9 -0
  39. package/dist/commands/rescue.js +31 -0
  40. package/dist/commands/run.js +115 -40
  41. package/dist/commands/scan.d.ts +5 -1
  42. package/dist/commands/scan.js +16 -4
  43. package/dist/commands/secrets.js +50 -9
  44. package/dist/commands/sync-core.d.ts +22 -0
  45. package/dist/commands/sync-core.js +124 -0
  46. package/dist/hq-cloud-sync-runner/bin/sync-runner.js +2447 -2135
  47. package/dist/hq-cloud-sync-runner/package.json +1 -1
  48. package/dist/lib/agent-kit/inbox-state.d.ts +17 -0
  49. package/dist/lib/agent-kit/inbox-state.js +41 -20
  50. package/dist/lib/agent-kit/kit-config.d.ts +2 -0
  51. package/dist/lib/agent-kit/kit-config.js +6 -0
  52. package/dist/lib/agent-kit/mcp/tools.js +12 -5
  53. package/dist/lib/agent-kit/run/heartbeat.d.ts +6 -3
  54. package/dist/lib/agent-kit/run/heartbeat.js +6 -3
  55. package/dist/lib/agent-kit/run/inbox.d.ts +8 -3
  56. package/dist/lib/agent-kit/run/inbox.js +42 -10
  57. package/dist/lib/agent-kit/run/request-timeout.d.ts +26 -0
  58. package/dist/lib/agent-kit/run/request-timeout.js +51 -0
  59. package/dist/lib/bot/api.d.ts +1 -1
  60. package/dist/lib/bot/config.d.ts +10 -0
  61. package/dist/lib/bot/config.js +17 -1
  62. package/dist/lib/bot/fleet-local.d.ts +142 -0
  63. package/dist/lib/bot/fleet-local.js +689 -0
  64. package/dist/lib/bot/fleet-python.d.ts +47 -0
  65. package/dist/lib/bot/fleet-python.js +218 -0
  66. package/dist/lib/bot/index.d.ts +2 -0
  67. package/dist/lib/bot/index.js +2 -0
  68. package/dist/lib/bot/local-config.d.ts +3 -2
  69. package/dist/lib/bot/local-config.js +5 -2
  70. package/dist/lib/bot/promote.d.ts +10 -0
  71. package/dist/lib/bot/promote.js +3 -2
  72. package/dist/lib/bot/prompt.d.ts +6 -1
  73. package/dist/lib/bot/prompt.js +8 -3
  74. package/dist/lib/bot/runtime/index.js +8 -4
  75. package/dist/lib/bot/runtime/scratch-dir.d.ts +24 -0
  76. package/dist/lib/bot/runtime/scratch-dir.js +41 -0
  77. package/dist/lib/caller-principal.d.ts +4 -0
  78. package/dist/lib/caller-principal.js +40 -0
  79. package/dist/lib/capacity/collectors/grok.js +8 -2
  80. package/dist/lib/cli-flag-keys.d.ts +2 -0
  81. package/dist/lib/cli-flag-keys.js +2 -0
  82. package/dist/lib/daemon/catalog.d.ts +12 -4
  83. package/dist/lib/daemon/catalog.js +70 -24
  84. package/dist/lib/daemon/config.d.ts +0 -7
  85. package/dist/lib/daemon/config.js +0 -32
  86. package/dist/lib/daemon/run.d.ts +5 -1
  87. package/dist/lib/daemon/run.js +48 -11
  88. package/dist/lib/daemon/supervisor.js +3 -1
  89. package/dist/lib/doctor/checks/release-links.d.ts +62 -1
  90. package/dist/lib/doctor/checks/release-links.js +188 -2
  91. package/dist/lib/doctor/fix/apply.js +54 -0
  92. package/dist/lib/doctor/fix/remediation.d.ts +8 -1
  93. package/dist/lib/doctor/fix/remediation.js +22 -0
  94. package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
  95. package/dist/lib/flag-registry-worker-source.generated.js +1 -1
  96. package/dist/lib/index-render/index.d.ts +9 -0
  97. package/dist/lib/index-render/index.js +65 -5
  98. package/dist/lib/index-render/session-scope.d.ts +69 -0
  99. package/dist/lib/index-render/session-scope.js +279 -0
  100. package/dist/lib/lanes/admission.js +7 -20
  101. package/dist/lib/lanes/reconcile.js +13 -15
  102. package/dist/lib/lanes/stale-lock.d.ts +50 -0
  103. package/dist/lib/lanes/stale-lock.js +105 -0
  104. package/dist/lib/lanes/store.js +11 -76
  105. package/dist/lib/notify-recipient.d.ts +10 -4
  106. package/dist/lib/notify-recipient.js +16 -4
  107. package/dist/lib/secret-input-link.d.ts +42 -0
  108. package/dist/lib/secret-input-link.js +93 -0
  109. package/dist/lib/secret-scan/range.d.ts +57 -0
  110. package/dist/lib/secret-scan/range.js +266 -0
  111. package/dist/lib/secret-scan/repo-config.d.ts +18 -3
  112. package/dist/lib/secret-scan/repo-config.js +135 -2
  113. package/dist/lib/sync-core/engine-home.d.ts +44 -0
  114. package/dist/lib/sync-core/engine-home.js +193 -0
  115. package/dist/lib/sync-core/flag.d.ts +17 -0
  116. package/dist/lib/sync-core/flag.js +47 -0
  117. package/dist/lib/sync-core/reconcile.d.ts +31 -0
  118. package/dist/lib/sync-core/reconcile.js +196 -0
  119. package/dist/lib/sync-core/release.d.ts +32 -0
  120. package/dist/lib/sync-core/release.js +140 -0
  121. package/dist/lib/sync-core/rollout-state.d.ts +24 -0
  122. package/dist/lib/sync-core/rollout-state.js +94 -0
  123. package/dist/lib/vault-sync-switches.d.ts +5 -0
  124. package/dist/lib/vault-sync-switches.js +43 -0
  125. package/dist/run/script-bound-schema.d.ts +27 -0
  126. package/dist/run/script-bound-schema.js +97 -0
  127. package/dist/secrets-env-action.d.ts +2 -0
  128. package/dist/secrets-env-action.js +7 -2
  129. package/dist/utils/cognito-session.d.ts +3 -30
  130. package/dist/utils/cognito-session.js +2 -100
  131. package/dist/utils/machine-principal-state.d.ts +38 -0
  132. package/dist/utils/machine-principal-state.js +110 -0
  133. package/dist/utils/remote-list-not-converging.d.ts +21 -0
  134. package/dist/utils/remote-list-not-converging.js +40 -0
  135. package/dist/utils/safe-link.js +9 -0
  136. package/dist/utils/script-bound-exec.d.ts +158 -0
  137. package/dist/utils/script-bound-exec.js +568 -0
  138. package/dist/utils/sync-watchdog.d.ts +92 -0
  139. package/dist/utils/sync-watchdog.js +250 -0
  140. package/package.json +8 -8
  141. package/dist/lib/daemon/sync-core-canary.d.ts +0 -9
  142. package/dist/lib/daemon/sync-core-canary.js +0 -56
@@ -0,0 +1,47 @@
1
+ type Exec = (executable: string, args: string[], options: Record<string, unknown>) => Buffer | string;
2
+ export interface FleetPythonToolAsset {
3
+ name: string;
4
+ sha256: string;
5
+ }
6
+ export interface FleetPythonToolPin {
7
+ repository: string;
8
+ version: string;
9
+ assets: Record<string, FleetPythonToolAsset>;
10
+ }
11
+ export interface FleetPythonDeps {
12
+ fleetHome?: string;
13
+ platform?: NodeJS.Platform;
14
+ arch?: NodeJS.Architecture;
15
+ fetch?: typeof fetch;
16
+ run?: Exec;
17
+ tool?: FleetPythonToolPin;
18
+ pythonVersion?: string;
19
+ env?: NodeJS.ProcessEnv;
20
+ }
21
+ export interface FleetPython {
22
+ uv: string;
23
+ python: string;
24
+ pythonDir: string;
25
+ }
26
+ export declare const HQ_FLEET_PYTHON_VERSION = "3.13.17";
27
+ export declare const HQ_FLEET_PYTHON_TOOL: FleetPythonToolPin;
28
+ export declare const FLEET_PYTHON_TOOL_DOWNLOAD_TIMEOUT_MS: number;
29
+ /** One lock for the whole machine: the installer and the private Python are shared by every bot. */
30
+ export declare const FLEET_PYTHON_LOCK_NAME = "hq-fleet-python";
31
+ /**
32
+ * The environment uv runs with: the user's own uv settings (index mirrors,
33
+ * cache, insecure hosts, config files) are dropped so the private install only
34
+ * ever sees the pinned sources.
35
+ */
36
+ export declare function fleetUvEnv(pythonDir: string, base?: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
37
+ /** The operating system's own tar when it is where it always is, so a `tar` on PATH or in the current folder is never run. */
38
+ export declare function tarExecutable(platform: NodeJS.Platform, env?: NodeJS.ProcessEnv): string;
39
+ export declare function fleetPythonToolPath(deps?: FleetPythonDeps): string;
40
+ /**
41
+ * The pinned installer and private Python, installed once per machine under
42
+ * the fleet folder and reused by every bot. Concurrent creates wait for each
43
+ * other on one machine-wide lock instead of racing over the shared folder.
44
+ */
45
+ export declare function ensureFleetPython(deps?: FleetPythonDeps): Promise<FleetPython>;
46
+ export {};
47
+ //# sourceMappingURL=fleet-python.d.ts.map
@@ -0,0 +1,218 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import * as crypto from "node:crypto";
3
+ import * as fs from "node:fs";
4
+ import * as path from "node:path";
5
+ import { fleetHome } from "./fleet-local.js";
6
+ import { withBotLock } from "./lock.js";
7
+ export const HQ_FLEET_PYTHON_VERSION = "3.13.17";
8
+ export const HQ_FLEET_PYTHON_TOOL = {
9
+ repository: "astral-sh/uv",
10
+ version: "0.13.5",
11
+ assets: {
12
+ "darwin-arm64": { name: "uv-aarch64-apple-darwin.tar.gz", sha256: "743f85035a1996ecb350659e01cd9faea9709829be09b1b4b03c73eaf3c9cd73" },
13
+ "darwin-x64": { name: "uv-x86_64-apple-darwin.tar.gz", sha256: "b0f52b0e3945b4107d89024ea38e625683011fbfc05a0d98f9fa2d6dcee28c66" },
14
+ "linux-arm64": { name: "uv-aarch64-unknown-linux-gnu.tar.gz", sha256: "4bb69cf95ed13f23713bb7bc09f241896ea0c75b7c557e615a20604b82f9c403" },
15
+ "linux-x64": { name: "uv-x86_64-unknown-linux-gnu.tar.gz", sha256: "f68d291c75783122dad58ab77def0e13f3233f358731db5d6d7e8715603b928f" },
16
+ "win32-arm64": { name: "uv-aarch64-pc-windows-msvc.zip", sha256: "09f47edf90726084a70f9426f6e3ca4f46e9bf7cbf23b1716ec64d60b055fae3" },
17
+ "win32-x64": { name: "uv-x86_64-pc-windows-msvc.zip", sha256: "a38b8d1902b7cfbc76b423ba1bb456f623f42064cdc72628b6075b89afd1019a" },
18
+ },
19
+ };
20
+ export const FLEET_PYTHON_TOOL_DOWNLOAD_TIMEOUT_MS = 5 * 60_000;
21
+ /** One lock for the whole machine: the installer and the private Python are shared by every bot. */
22
+ export const FLEET_PYTHON_LOCK_NAME = "hq-fleet-python";
23
+ function expectedError(message) {
24
+ return Object.assign(new Error(message), { expected: true });
25
+ }
26
+ function toolBinaryName(platform) {
27
+ return platform === "win32" ? "uv.exe" : "uv";
28
+ }
29
+ /** The last lines a failed child process wrote, or its exit code, so the user sees why. */
30
+ function causeOf(error) {
31
+ if (error && typeof error === "object") {
32
+ const failure = error;
33
+ const stderr = failure.stderr === undefined ? "" : (Buffer.isBuffer(failure.stderr) ? failure.stderr.toString("utf8") : String(failure.stderr));
34
+ const tail = stderr.split(/\r?\n/).map((line) => line.trim()).filter(Boolean).slice(-3).join(" ");
35
+ if (tail)
36
+ return tail;
37
+ if (typeof failure.status === "number")
38
+ return `exit code ${failure.status}`;
39
+ if (failure.message)
40
+ return failure.message;
41
+ }
42
+ return String(error);
43
+ }
44
+ /**
45
+ * The environment uv runs with: the user's own uv settings (index mirrors,
46
+ * cache, insecure hosts, config files) are dropped so the private install only
47
+ * ever sees the pinned sources.
48
+ */
49
+ export function fleetUvEnv(pythonDir, base = process.env) {
50
+ const env = {};
51
+ for (const [key, value] of Object.entries(base)) {
52
+ if (!key.toUpperCase().startsWith("UV_"))
53
+ env[key] = value;
54
+ }
55
+ env.UV_PYTHON_INSTALL_DIR = pythonDir;
56
+ env.UV_NO_CONFIG = "1";
57
+ return env;
58
+ }
59
+ /** The operating system's own tar when it is where it always is, so a `tar` on PATH or in the current folder is never run. */
60
+ export function tarExecutable(platform, env = process.env) {
61
+ const candidates = platform === "win32"
62
+ ? [path.join(env.SystemRoot ?? env.windir ?? "C:\\Windows", "System32", "tar.exe")]
63
+ : ["/usr/bin/tar", "/bin/tar"];
64
+ for (const candidate of candidates) {
65
+ if (fs.existsSync(candidate))
66
+ return candidate;
67
+ }
68
+ return "tar";
69
+ }
70
+ export function fleetPythonToolPath(deps = {}) {
71
+ const root = deps.fleetHome ?? fleetHome();
72
+ const tool = deps.tool ?? HQ_FLEET_PYTHON_TOOL;
73
+ return path.join(root, "tools", "uv", tool.version, toolBinaryName(deps.platform ?? process.platform));
74
+ }
75
+ function findFile(dir, name) {
76
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
77
+ const child = path.join(dir, entry.name);
78
+ if (entry.isDirectory()) {
79
+ const found = findFile(child, name);
80
+ if (found)
81
+ return found;
82
+ }
83
+ else if (entry.isFile() && entry.name === name) {
84
+ return child;
85
+ }
86
+ }
87
+ return null;
88
+ }
89
+ function installedTool(binary) {
90
+ try {
91
+ const stat = fs.statSync(binary);
92
+ return stat.isFile() && stat.size > 0;
93
+ }
94
+ catch {
95
+ return false;
96
+ }
97
+ }
98
+ async function installTool(deps) {
99
+ const platform = deps.platform ?? process.platform;
100
+ const arch = deps.arch ?? process.arch;
101
+ const tool = deps.tool ?? HQ_FLEET_PYTHON_TOOL;
102
+ const binary = fleetPythonToolPath(deps);
103
+ if (installedTool(binary))
104
+ return binary;
105
+ const asset = tool.assets[`${platform}-${arch}`];
106
+ if (!asset) {
107
+ throw expectedError(`HQ Fleet Local does not support ${platform} on ${arch} yet.`);
108
+ }
109
+ const fetcher = deps.fetch ?? fetch;
110
+ const run = deps.run ?? execFileSync;
111
+ const url = `https://github.com/${tool.repository}/releases/download/${tool.version}/${asset.name}`;
112
+ let response;
113
+ try {
114
+ response = await fetcher(url, { signal: AbortSignal.timeout(FLEET_PYTHON_TOOL_DOWNLOAD_TIMEOUT_MS) });
115
+ }
116
+ catch {
117
+ throw expectedError("Could not download the HQ Fleet Local runtime installer. Check your internet connection and retry.");
118
+ }
119
+ if (!response.ok) {
120
+ throw expectedError(`Could not download the HQ Fleet Local runtime installer: GitHub returned HTTP ${response.status}.`);
121
+ }
122
+ const bytes = Buffer.from(await response.arrayBuffer());
123
+ if (crypto.createHash("sha256").update(bytes).digest("hex") !== asset.sha256) {
124
+ throw expectedError("The HQ Fleet Local runtime installer download did not match its pinned checksum.");
125
+ }
126
+ const toolDir = path.dirname(binary);
127
+ const staging = `${toolDir}.staging-${process.pid}-${crypto.randomUUID()}`;
128
+ fs.mkdirSync(staging, { recursive: true, mode: 0o700 });
129
+ try {
130
+ const archive = path.join(staging, asset.name);
131
+ fs.writeFileSync(archive, bytes, { mode: 0o600 });
132
+ const extracted = path.join(staging, "extracted");
133
+ fs.mkdirSync(extracted, { mode: 0o700 });
134
+ try {
135
+ run(tarExecutable(platform, deps.env ?? process.env), ["-xf", archive, "-C", extracted], { stdio: ["ignore", "ignore", "pipe"], windowsHide: true });
136
+ }
137
+ catch (error) {
138
+ throw expectedError(`HQ Fleet Local could not unpack its runtime installer (${causeOf(error)}).`);
139
+ }
140
+ const unpacked = findFile(extracted, toolBinaryName(platform));
141
+ if (!unpacked)
142
+ throw expectedError("The HQ Fleet Local runtime installer archive did not contain the installer.");
143
+ const prepared = path.join(staging, "tool");
144
+ fs.mkdirSync(prepared, { mode: 0o700 });
145
+ fs.copyFileSync(unpacked, path.join(prepared, path.basename(binary)));
146
+ fs.chmodSync(path.join(prepared, path.basename(binary)), 0o700);
147
+ fs.mkdirSync(path.dirname(toolDir), { recursive: true, mode: 0o700 });
148
+ fs.rmSync(toolDir, { recursive: true, force: true });
149
+ fs.renameSync(prepared, toolDir);
150
+ }
151
+ finally {
152
+ fs.rmSync(staging, { recursive: true, force: true });
153
+ }
154
+ return binary;
155
+ }
156
+ function realpathOrSelf(target) {
157
+ try {
158
+ return fs.realpathSync.native(target);
159
+ }
160
+ catch {
161
+ return path.resolve(target);
162
+ }
163
+ }
164
+ function isInside(parent, child, platform) {
165
+ const fold = (value) => (platform === "win32" ? value.toLowerCase() : value);
166
+ const relative = path.relative(fold(realpathOrSelf(parent)), fold(realpathOrSelf(child)));
167
+ return relative !== "" && !relative.startsWith("..") && !path.isAbsolute(relative);
168
+ }
169
+ async function ensureFleetPythonLocked(root, deps) {
170
+ const platform = deps.platform ?? process.platform;
171
+ const run = deps.run ?? execFileSync;
172
+ const version = deps.pythonVersion ?? HQ_FLEET_PYTHON_VERSION;
173
+ const pythonDir = path.join(root, "python");
174
+ const uv = await installTool(deps);
175
+ const env = fleetUvEnv(pythonDir, deps.env ?? process.env);
176
+ fs.mkdirSync(pythonDir, { recursive: true, mode: 0o700 });
177
+ try {
178
+ run(uv, ["python", "install", version, "--no-config", "--no-bin", "--no-registry", "--install-dir", pythonDir], {
179
+ stdio: ["ignore", "ignore", "pipe"],
180
+ env,
181
+ windowsHide: true,
182
+ });
183
+ }
184
+ catch (error) {
185
+ throw expectedError(`HQ Fleet Local could not install its private Python ${version} (${causeOf(error)}). If this mentions a download, check your internet connection and retry.`);
186
+ }
187
+ let found;
188
+ try {
189
+ const output = run(uv, ["python", "find", version, "--managed-python", "--no-config"], {
190
+ stdio: ["ignore", "pipe", "pipe"],
191
+ env,
192
+ encoding: "utf8",
193
+ windowsHide: true,
194
+ });
195
+ found = (Buffer.isBuffer(output) ? output.toString("utf8") : output).trim();
196
+ }
197
+ catch (error) {
198
+ throw expectedError(`HQ Fleet Local installed its private Python but could not locate it (${causeOf(error)}).`);
199
+ }
200
+ const python = path.resolve(found);
201
+ if (!found || !fs.existsSync(python) || !isInside(pythonDir, python, platform)) {
202
+ throw expectedError("HQ Fleet Local found a Python outside its private install and refused to use it.");
203
+ }
204
+ return { uv, python, pythonDir };
205
+ }
206
+ /**
207
+ * The pinned installer and private Python, installed once per machine under
208
+ * the fleet folder and reused by every bot. Concurrent creates wait for each
209
+ * other on one machine-wide lock instead of racing over the shared folder.
210
+ */
211
+ export async function ensureFleetPython(deps = {}) {
212
+ const root = deps.fleetHome ?? fleetHome();
213
+ return withBotLock(FLEET_PYTHON_LOCK_NAME, () => ensureFleetPythonLocked(root, deps), {
214
+ command: "hq bot create",
215
+ root,
216
+ });
217
+ }
218
+ //# sourceMappingURL=fleet-python.js.map
@@ -25,4 +25,6 @@ export * from "./pack-link.js";
25
25
  export * from "./lock.js";
26
26
  export * from "./display-name.js";
27
27
  export * from "./decisions.js";
28
+ export * from "./fleet-local.js";
29
+ export * from "./fleet-python.js";
28
30
  //# sourceMappingURL=index.d.ts.map
@@ -25,4 +25,6 @@ export * from "./pack-link.js";
25
25
  export * from "./lock.js";
26
26
  export * from "./display-name.js";
27
27
  export * from "./decisions.js";
28
+ export * from "./fleet-local.js";
29
+ export * from "./fleet-python.js";
28
30
  //# sourceMappingURL=index.js.map
@@ -9,7 +9,7 @@
9
9
  * checks the result before it leaves this machine: a bot's machine credentials
10
10
  * live only in ~/.hq/bots/<name>/machine-creds.json and never travel here.
11
11
  */
12
- import { type BotConfig, type BotKind, type BotMemoryMode, type BotRuntimeId } from "./config.js";
12
+ import { type BotConfig, type BotEngineMetadata, type BotKind, type BotMemoryMode, type BotRuntimeId } from "./config.js";
13
13
  /**
14
14
  * Everything needed to rebuild a bot here, minus anything secret. Written on
15
15
  * create, returned by `GET /v1/agents/mine?local=1` and by the credential
@@ -18,6 +18,7 @@ import { type BotConfig, type BotKind, type BotMemoryMode, type BotRuntimeId } f
18
18
  export interface BotLocalConfig {
19
19
  v: 1;
20
20
  runtime?: BotRuntimeId;
21
+ engine?: BotEngineMetadata;
21
22
  /** "scaffold" (its own personal/workers/<name>) or "worker" (an HQ worker). */
22
23
  workerSource?: "scaffold" | "worker";
23
24
  workerId?: string;
@@ -37,7 +38,7 @@ export interface BotLocalConfig {
37
38
  /** Walks a value and throws when any key looks like it could carry a secret. */
38
39
  export declare function assertNoSecretKeys(value: unknown, path?: string): void;
39
40
  /** The bot.json fields a localConfig is built from — nothing else is read. */
40
- export type BotLocalConfigSource = Pick<BotConfig, "runtime" | "workerSource" | "workerId" | "companySlug" | "model" | "effort" | "kind" | "companies" | "autoApprove" | "memoryDir" | "displayName" | "intro" | "kickoff">;
41
+ export type BotLocalConfigSource = Pick<BotConfig, "runtime" | "engine" | "workerSource" | "workerId" | "companySlug" | "model" | "effort" | "kind" | "companies" | "autoApprove" | "memoryDir" | "displayName" | "intro" | "kickoff">;
41
42
  /**
42
43
  * The non-secret settings of a bot, for the cloud to keep. Absolute paths
43
44
  * (hqRoot, memoryDir, workerDir) are deliberately left out: they belong to the
@@ -9,7 +9,7 @@
9
9
  * checks the result before it leaves this machine: a bot's machine credentials
10
10
  * live only in ~/.hq/bots/<name>/machine-creds.json and never travel here.
11
11
  */
12
- import { botMemoryMode, effectiveBotCompanies, effectiveBotKind, isBotKind, isBotMemoryMode, isBotRuntimeId, } from "./config.js";
12
+ import { botMemoryMode, effectiveBotCompanies, effectiveBotKind, isBotKind, isBotEngineMetadata, isBotMemoryMode, isBotRuntimeId, } from "./config.js";
13
13
  /** Same limit the server enforces on the stored value. */
14
14
  const BOT_LOCAL_CONFIG_MAX_BYTES = 8 * 1024;
15
15
  /** Key names the server rejects outright (case-insensitive, substring). */
@@ -41,7 +41,8 @@ export function buildBotLocalConfig(config) {
41
41
  const companies = effectiveBotCompanies(config);
42
42
  const local = {
43
43
  v: 1,
44
- runtime: config.runtime,
44
+ ...(config.engine ? {} : { runtime: config.runtime }),
45
+ ...(config.engine ? { engine: config.engine } : {}),
45
46
  workerSource: config.workerSource ?? "scaffold",
46
47
  ...(config.workerId ? { workerId: config.workerId } : {}),
47
48
  ...(config.companySlug ? { companySlug: config.companySlug } : {}),
@@ -80,6 +81,8 @@ export function parseBotLocalConfig(value) {
80
81
  const runtime = str("runtime");
81
82
  if (isBotRuntimeId(runtime))
82
83
  out.runtime = runtime;
84
+ if (isBotEngineMetadata(raw.engine))
85
+ out.engine = raw.engine;
83
86
  const workerSource = str("workerSource");
84
87
  if (workerSource === "worker" || workerSource === "scaffold")
85
88
  out.workerSource = workerSource;
@@ -1,5 +1,14 @@
1
1
  import type { BotApi, BotPromotionState } from "./api.js";
2
2
  import type { BotConfig } from "./config.js";
3
+ import type { BotMachineCreds } from "./creds.js";
4
+ /** A bot whose process and HQ machine credential live outside its state folder,
5
+ * such as an HQ Fleet Runtime bot: promotion stops its service through the
6
+ * runtime and reads the credential from the runtime's secure store.
7
+ */
8
+ export interface LocalBotPromotionRuntime {
9
+ stop(): void;
10
+ machineCredential(): BotMachineCreds;
11
+ }
3
12
  /** One recoverable local-to-cloud step, used by CLI and desktop. Never kills the
4
13
  * local runtime, recopies provider login files, or changes another bot.
5
14
  */
@@ -9,6 +18,7 @@ export declare function promoteLocalBot(input: {
9
18
  companyUid: string;
10
19
  api: BotApi;
11
20
  drainTimeoutMs?: number;
21
+ runtime?: LocalBotPromotionRuntime;
12
22
  }): Promise<{
13
23
  promotion: BotPromotionState;
14
24
  pending: boolean;
@@ -41,7 +41,7 @@ function readPrivateJson(file, limit) {
41
41
  * local runtime, recopies provider login files, or changes another bot.
42
42
  */
43
43
  export async function promoteLocalBot(input) {
44
- const { dir, config, companyUid, api } = input;
44
+ const { dir, config, companyUid, api, runtime } = input;
45
45
  const existing = await api.promotionStatus(config.agentUid, companyUid);
46
46
  const holdFile = path.join(dir, "promotion-hold.json");
47
47
  const localHold = fs.existsSync(holdFile) ? readPrivateJson(holdFile, 2048) : null;
@@ -58,6 +58,7 @@ export async function promoteLocalBot(input) {
58
58
  recordActivePromotion(dir, hold, existing);
59
59
  return { promotion: existing, pending: false };
60
60
  }
61
+ runtime?.stop();
61
62
  await waitForPromotionDrain(dir, input.drainTimeoutMs ?? 60_000);
62
63
  const file = path.join(dir, `promotion-snapshot-${operationId}.json`);
63
64
  if (!fs.existsSync(file)) {
@@ -83,7 +84,7 @@ export async function promoteLocalBot(input) {
83
84
  throw new Error("Frozen snapshot does not match the cloud operation");
84
85
  let state = existing;
85
86
  if (!state || state.phase === "preparing") {
86
- const machine = readPrivateJson(botCredsPath(dir), 8192);
87
+ const machine = runtime ? runtime.machineCredential() : readPrivateJson(botCredsPath(dir), 8192);
87
88
  if (!machine || typeof machine !== "object" || typeof machine.secret !== "string")
88
89
  throw new Error("Invalid bot machine credential");
89
90
  try {
@@ -103,6 +103,11 @@ export interface BuildSystemPromptInput {
103
103
  } | null;
104
104
  /** hqRoot-relative (default `${workerDir}/memory`) or absolute. */
105
105
  memoryDir?: string;
106
+ /**
107
+ * The bot runs on the HQ Fleet Runtime, which labels every message with the
108
+ * sender's HQ identity and prepends no "Owner context" block.
109
+ */
110
+ fleetRuntime?: boolean;
106
111
  }
107
112
  /**
108
113
  * One paragraph saying which kind of bot this is and what that means for the
@@ -110,7 +115,7 @@ export interface BuildSystemPromptInput {
110
115
  * acts as its owner everywhere; a company bot acts as itself and cannot
111
116
  * create anything for the owner.
112
117
  */
113
- export declare function botKindParagraph(input: Pick<BuildSystemPromptInput, "agentUid" | "ownerUid" | "kind" | "companies">): string;
118
+ export declare function botKindParagraph(input: Pick<BuildSystemPromptInput, "agentUid" | "ownerUid" | "kind" | "companies" | "fleetRuntime">): string;
114
119
  /**
115
120
  * The standing instructions the bot runs under. The DM body is delivered as
116
121
  * the user turn; this text is the system prompt (claude/codex) or the prompt
@@ -278,12 +278,17 @@ export function botKindParagraph(input) {
278
278
  `If asked to, say plainly that you are a company bot and cannot do that, and suggest your owner ask one of their personal bots (or do it themselves in HQ). ` +
279
279
  `Nobody's messages carry an "Owner context" block; never state which companies your owner belongs to.`);
280
280
  }
281
+ const ownerContext = input.fleetRuntime
282
+ ? `The runtime labels every message with the sender's HQ identity: messages from ${input.ownerUid} are your owner's, and only those carry your owner's authority. ` +
283
+ `When you need to know which companies your owner belongs to, run \`hq whoami\` yourself; never guess. ` +
284
+ `Other people read your answers in rooms: in a room, never state which companies your owner belongs to, and never repeat anything from their files or your DMs.`
285
+ : `Direct messages from your owner start with an "Owner context" block checked with your owner's own sign-in; that is the only source for your owner's companies. ` +
286
+ `If it says the check failed, say you could not check; never say your owner has no company unless that block says so. ` +
287
+ `Room messages do not carry it, and other people read your answers there: in a room, never state which companies your owner belongs to, and never repeat anything from their files or your DMs.`;
281
288
  return (`You are a PERSONAL bot: you act as your owner, ${input.ownerUid}. ` +
282
289
  `Every \`hq\` command you run — in direct messages, in rooms, and on your first task — runs with your owner's own sign-in, so it reaches everything they can: all of their companies and files. ` +
283
290
  `Creating a company, a bot, an invite or a share for them works directly, so do it yourself when they ask — never tell them to open a terminal or run a command. ` +
284
- `Direct messages from your owner start with an "Owner context" block checked with your owner's own sign-in; that is the only source for your owner's companies. ` +
285
- `If it says the check failed, say you could not check; never say your owner has no company unless that block says so. ` +
286
- `Room messages do not carry it, and other people read your answers there: in a room, never state which companies your owner belongs to, and never repeat anything from their files or your DMs.`);
291
+ ownerContext);
287
292
  }
288
293
  /**
289
294
  * The standing instructions the bot runs under. The DM body is delivered as
@@ -1,7 +1,6 @@
1
1
  import { execFileSync, spawn } from "node:child_process";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import * as fs from "node:fs";
4
- import * as os from "node:os";
5
4
  import * as path from "node:path";
6
5
  import { planBotRuntimeSpawn } from "./windows.js";
7
6
  import { GrokAcpClient } from "./grok-acp.js";
@@ -9,6 +8,7 @@ import { createClaudeRuntime } from "./claude.js";
9
8
  import { createCodexRuntime } from "./codex.js";
10
9
  import { createGrokRuntime, grokRetryArgsAfterInvalidOutputFormat, probeGrokOutputFormat } from "./grok.js";
11
10
  import { stripControlTokens } from "./sanitize.js";
11
+ import { createTurnScratchDir } from "./scratch-dir.js";
12
12
  import { isRuntimeVersionTooOld, versionTooOldModel } from "../runtime-failure.js";
13
13
  import { parseJsonLine, RuntimeError, turnTimeoutMs, stderrExcerpt, MAX_MODEL_FALLBACK_RETRIES, } from "./types.js";
14
14
  export * from "./types.js";
@@ -345,10 +345,13 @@ export function forgetModelFallbacks() {
345
345
  */
346
346
  export async function runRuntimeTurn(runtime, input, deps = {}) {
347
347
  const spawnImpl = deps.spawnImpl ?? defaultSpawn;
348
- const scratchDir = deps.scratchDir ?? fs.mkdtempSync(path.join(os.tmpdir(), "hq-bot-turn-"));
348
+ const turnEnv = input.env ?? deps.env ?? process.env;
349
+ const turnPlatform = deps.platform ?? input.platform ?? process.platform;
350
+ const scratch = deps.scratchDir ? { dir: deps.scratchDir } : createTurnScratchDir("hq-bot-turn-", { platform: turnPlatform });
351
+ const scratchDir = scratch.dir;
349
352
  const outputFile = path.join(scratchDir, "last-message.txt");
350
353
  const newSessionId = input.newSessionId ?? (deps.newSessionId ?? randomUUID)();
351
- const turnInput = { ...input, platform: deps.platform ?? input.platform ?? process.platform };
354
+ const turnInput = { ...input, platform: turnPlatform };
352
355
  const nowMs = deps.now ?? Date.now;
353
356
  const memoKey = fallbackKey(runtime, input.model);
354
357
  const memo = rememberedModelFallbacks.get(memoKey);
@@ -358,7 +361,8 @@ export async function runRuntimeTurn(runtime, input, deps = {}) {
358
361
  const args = runtime.buildArgs(remembered ? { ...turnInput, model: remembered.to } : turnInput, { outputFile, newSessionId });
359
362
  // Enforce at the model boundary too: old plists and direct `bot run` callers
360
363
  // may lack the guard. A bot prompt must never quit the user's desktop app.
361
- const baseEnv = { ...(input.env ?? deps.env ?? process.env), HQ_NO_ENSURE_HQ_DESKTOP: "1" };
364
+ // When TMPDIR was unwritable, hand the runtime CLI the working temp root too.
365
+ const baseEnv = { ...turnEnv, ...(scratch.fallbackRoot ? { TMPDIR: scratch.fallbackRoot } : {}), HQ_NO_ENSURE_HQ_DESKTOP: "1" };
362
366
  const env = { ...(runtime.runtimeEnv?.(baseEnv) ?? baseEnv), HQ_NO_ENSURE_HQ_DESKTOP: "1" };
363
367
  const binary = runtime.resolveBinary?.(env, input.cwd, turnInput.platform) ?? runtime.binary;
364
368
  const stdin = runtime.stdinInput?.(turnInput);
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Per-turn scratch folder for the local bot runtime (feedback_f0131909).
3
+ *
4
+ * A bot started by launchd/systemd at login can inherit a TMPDIR it cannot
5
+ * write yet (an external volume that is not mounted or not yet readable by the
6
+ * agent), so `mkdtemp` failed with EPERM on every turn until `hq bot restart`
7
+ * re-launched it with a working TMPDIR. Try the configured folder first, then
8
+ * /tmp, then ~/.hq/tmp. When TMPDIR works nothing changes.
9
+ */
10
+ export interface TurnScratchDir {
11
+ /** The created scratch folder. */
12
+ dir: string;
13
+ /** The temp root used, when it is not the configured one. */
14
+ fallbackRoot?: string;
15
+ }
16
+ export interface TurnScratchDeps {
17
+ /** The configured temp root. Defaults to os.tmpdir(), exactly as before. */
18
+ tmpdir?: string;
19
+ platform?: NodeJS.Platform;
20
+ home?: string;
21
+ warn?: (line: string) => void;
22
+ }
23
+ export declare function createTurnScratchDir(prefix: string, deps?: TurnScratchDeps): TurnScratchDir;
24
+ //# sourceMappingURL=scratch-dir.d.ts.map
@@ -0,0 +1,41 @@
1
+ import * as fs from "node:fs";
2
+ import * as os from "node:os";
3
+ import * as path from "node:path";
4
+ /** Warn once per unwritable folder per bot process, not on every turn. */
5
+ const warned = new Set();
6
+ /** Errors that mean "this folder cannot be used"; anything else is rethrown. */
7
+ const UNUSABLE = new Set(["EPERM", "EACCES", "ENOENT", "ENOTDIR", "EROFS"]);
8
+ export function createTurnScratchDir(prefix, deps = {}) {
9
+ const platform = deps.platform ?? process.platform;
10
+ const home = deps.home ?? os.homedir();
11
+ const warn = deps.warn ?? ((line) => process.stderr.write(`${line}\n`));
12
+ const configured = deps.tmpdir ?? os.tmpdir();
13
+ const fallbacks = [];
14
+ if (platform !== "win32")
15
+ fallbacks.push("/tmp");
16
+ fallbacks.push(path.join(home, ".hq", "tmp"));
17
+ let firstError;
18
+ for (const root of [configured, ...fallbacks.filter((f) => f !== configured)]) {
19
+ try {
20
+ if (root !== configured)
21
+ fs.mkdirSync(root, { recursive: true, mode: 0o700 });
22
+ const dir = fs.mkdtempSync(path.join(root, prefix));
23
+ if (root === configured) {
24
+ warned.delete(configured);
25
+ return { dir };
26
+ }
27
+ if (!warned.has(configured))
28
+ warn(`hq bot: temp folder ${configured} is not writable (${firstError?.code ?? "error"}); using ${root} instead. Fix TMPDIR or run hq bot restart.`);
29
+ warned.add(configured);
30
+ return { dir, fallbackRoot: root };
31
+ }
32
+ catch (err) {
33
+ const e = err;
34
+ if (!e.code || !UNUSABLE.has(e.code))
35
+ throw err;
36
+ firstError ??= e;
37
+ }
38
+ }
39
+ throw new Error(`hq bot cannot create a temporary folder: ${configured} is not writable (${firstError?.code ?? "error"}), and ${fallbacks.join(" and ")} did not work either. Fix TMPDIR or run hq bot restart.`);
40
+ }
41
+ //# sourceMappingURL=scratch-dir.js.map
@@ -0,0 +1,4 @@
1
+ export declare function callerIsMachinePrincipal(env?: NodeJS.ProcessEnv): boolean;
2
+ /** True when the token names an agent (agt_) or outpost (out_) identity. */
3
+ export declare function tokenIsMachinePrincipal(token: string): boolean;
4
+ //# sourceMappingURL=caller-principal.d.ts.map
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Whether the current caller is a machine principal (a local bot, an
3
+ * agent-kit agent, a hosted agent or an outpost) or a person login.
4
+ *
5
+ * The answer comes from the credentials the request will actually use, never
6
+ * from a user flag:
7
+ * - CLI sends go through `ensureCognitoToken()`, which picks machine tokens
8
+ * exactly when `wantsMachineCognitoTokens()` is true (HQ_MACHINE_CREDS_FILE
9
+ * set and valid, or the default machine creds file on a box with no person
10
+ * session). This check reads local state only and makes no network call.
11
+ * - Callers that already hold a token (the agent-kit MCP tools) use
12
+ * `tokenIsMachinePrincipal`, which reads the token's identity claims.
13
+ */
14
+ import { wantsMachineCognitoTokens } from "../utils/machine-principal-state.js";
15
+ import { peekIdToken } from "../utils/id-token.js";
16
+ export function callerIsMachinePrincipal(env = process.env) {
17
+ try {
18
+ return wantsMachineCognitoTokens({}, env);
19
+ }
20
+ catch {
21
+ return false;
22
+ }
23
+ }
24
+ const MACHINE_ENTITY_TYPES = new Set(["agent", "outpost", "company-outpost"]);
25
+ /** True when the token names an agent (agt_) or outpost (out_) identity. */
26
+ export function tokenIsMachinePrincipal(token) {
27
+ let claims;
28
+ try {
29
+ claims = peekIdToken(token);
30
+ }
31
+ catch {
32
+ return false;
33
+ }
34
+ const type = claims["custom:entityType"];
35
+ if (typeof type === "string" && MACHINE_ENTITY_TYPES.has(type))
36
+ return true;
37
+ const uid = claims["custom:entityUid"];
38
+ return typeof uid === "string" && /^(agt|out)_/.test(uid);
39
+ }
40
+ //# sourceMappingURL=caller-principal.js.map
@@ -192,7 +192,10 @@ export function mapGrokBillingBody(body, options) {
192
192
  export async function collectGrok(options = {}) {
193
193
  const ledger = ledgerOptions(options);
194
194
  const root = ledger.root ?? capacityRoot(ledger.env, ledger.home);
195
- const scoped = { ...ledger, root };
195
+ // Forward the injected clock so ledger retention and now.json use the same
196
+ // instant as observedAt; otherwise retention runs on the wall clock and can
197
+ // delete readings this call just wrote.
198
+ const scoped = { ...ledger, root, now: options.now ?? ledger.now };
196
199
  if (readCollectorMark(scoped)?.enabled === false && !options.allowDisabled) {
197
200
  return { readings: [], failures: [], view: null, skipped: "disabled", accountId: null };
198
201
  }
@@ -474,7 +477,10 @@ function stampAccount(row, options, planType) {
474
477
  async function collectEnumeratedGrok(options) {
475
478
  const ledger = ledgerOptions(options);
476
479
  const root = ledger.root ?? capacityRoot(ledger.env, ledger.home);
477
- const scoped = { ...ledger, root };
480
+ // Forward the injected clock so ledger retention and now.json use the same
481
+ // instant as observedAt; otherwise retention runs on the wall clock and can
482
+ // delete readings this call just wrote.
483
+ const scoped = { ...ledger, root, now: options.now ?? ledger.now };
478
484
  const clock = options.now ?? ledger.now ?? (() => new Date());
479
485
  const cliVersion = options.cliVersion ?? readInstalledGrokVersion(options.readVersion);
480
486
  const groups = new Map();
@@ -1,6 +1,8 @@
1
1
  /** hq-flags keys used by CLI-wide routes. */
2
2
  export declare const CLI_COMMAND_HELP_CATALOG_FLAG = "cli.command-help-catalog";
3
3
  export declare const MULTI_COMPANY_SESSION_LOCK_FLAG = "hooks.multi-company-session-lock";
4
+ /** Kill switch (default on) for company-scoped `hq core rebuild-index` root indexes. */
5
+ export declare const CORE_COMPANY_INDEX_SCOPE_FLAG = "cli.rebuild-index-company-scope";
4
6
  /**
5
7
  * hq-flags key for HQ Storage (`hq storage` and Settings > Storage in the
6
8
  * desktop app). Shared with hq-desktop-app, so it carries no `cli.` prefix,
@@ -1,6 +1,8 @@
1
1
  /** hq-flags keys used by CLI-wide routes. */
2
2
  export const CLI_COMMAND_HELP_CATALOG_FLAG = "cli.command-help-catalog";
3
3
  export const MULTI_COMPANY_SESSION_LOCK_FLAG = "hooks.multi-company-session-lock";
4
+ /** Kill switch (default on) for company-scoped `hq core rebuild-index` root indexes. */
5
+ export const CORE_COMPANY_INDEX_SCOPE_FLAG = "cli.rebuild-index-company-scope";
4
6
  /**
5
7
  * hq-flags key for HQ Storage (`hq storage` and Settings > Storage in the
6
8
  * desktop app). Shared with hq-desktop-app, so it carries no `cli.` prefix,