@indigoai-us/hq-cli 5.117.0 → 5.117.1
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 +28 -0
- package/dist/command-catalog.generated.d.ts +1 -1
- package/dist/command-catalog.generated.js +1 -1
- package/dist/commands/agent-kit.d.ts +23 -3
- package/dist/commands/agent-kit.js +110 -13
- package/dist/commands/agent-probe.d.ts +15 -7
- package/dist/commands/agent-probe.js +59 -21
- package/dist/commands/dm.d.ts +10 -0
- package/dist/commands/dm.js +80 -0
- package/dist/lib/agent-kit/fallback.d.ts +63 -0
- package/dist/lib/agent-kit/fallback.js +129 -0
- package/dist/lib/agent-kit/run/inbox.d.ts +18 -6
- package/dist/lib/agent-kit/run/inbox.js +38 -6
- package/dist/lib/agent-kit/run/mesh-listener.d.ts +22 -6
- package/dist/lib/agent-kit/run/mesh-listener.js +44 -8
- package/dist/lib/agent-kit/run/supervisor.d.ts +35 -0
- package/dist/lib/agent-kit/run/supervisor.js +85 -0
- package/dist/lib/mesh/live/daemon/credentials.d.ts +27 -0
- package/dist/lib/mesh/live/daemon/credentials.js +95 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,34 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.117.1] — 2026-09-16
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- `hq agent probe` works for external agents. The work-mesh check no longer
|
|
10
|
+
reads the owner-only company roster (which answered "Not found" to an agent);
|
|
11
|
+
it passes when the realtime credential vend names the agent, the agent's own
|
|
12
|
+
inbox answers, and this host's heartbeat and mesh listener are live. The DM
|
|
13
|
+
check reads the probe message back from the agent's inbox instead of the
|
|
14
|
+
human-only thread view that refused agents. Output format is unchanged.
|
|
15
|
+
- The agent kit's mesh listener stays connected. It now asks for the personal
|
|
16
|
+
realtime credentials that cover the agent's own doorbell topics (the company
|
|
17
|
+
presence credentials it used made the connection drop before subscribing),
|
|
18
|
+
subscribes only to topics those credentials grant, and polls the inbox
|
|
19
|
+
immediately when an inbox doorbell rings.
|
|
20
|
+
- Inbox poll failures are diagnosable: the log names the method, full URL,
|
|
21
|
+
status and the first 300 characters of the response, and the inbox URL keeps
|
|
22
|
+
any base path from `apiBaseUrl` without doubling `/v1`.
|
|
23
|
+
- `hq dm inbox` works for agent identities by reading the agent inbox;
|
|
24
|
+
`hq dm thread` tells agents to use `hq dm inbox` instead of failing with a
|
|
25
|
+
server refusal.
|
|
26
|
+
- `hq agent kit install` works on hosts with no systemd user session or
|
|
27
|
+
launchd: it starts the services under one background `hq agent kit run all`
|
|
28
|
+
supervisor (restarting any service that exits), records its pid in
|
|
29
|
+
`~/.hq-agent/kit.pid`, and prints the `@reboot` crontab line that keeps it
|
|
30
|
+
running after a restart. `kit status` and `kit uninstall` understand this
|
|
31
|
+
mode.
|
|
32
|
+
|
|
5
33
|
## [5.117.0] — 2026-09-16
|
|
6
34
|
|
|
7
35
|
### Added
|
|
@@ -4217,7 +4217,7 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
4217
4217
|
readonly subcommands: readonly [];
|
|
4218
4218
|
}, {
|
|
4219
4219
|
readonly name: "run";
|
|
4220
|
-
readonly description: "Run one kit service in the foreground (sync|mesh|inbox|heartbeat)";
|
|
4220
|
+
readonly description: "Run one kit service in the foreground (sync|mesh|inbox|heartbeat), or `all` to supervise every service in one process";
|
|
4221
4221
|
readonly aliases: readonly [];
|
|
4222
4222
|
readonly hidden: false;
|
|
4223
4223
|
readonly usage: "[options] <service>";
|
|
@@ -5455,7 +5455,7 @@ export const COMMAND_CATALOG = [
|
|
|
5455
5455
|
},
|
|
5456
5456
|
{
|
|
5457
5457
|
"name": "run",
|
|
5458
|
-
"description": "Run one kit service in the foreground (sync|mesh|inbox|heartbeat)",
|
|
5458
|
+
"description": "Run one kit service in the foreground (sync|mesh|inbox|heartbeat), or `all` to supervise every service in one process",
|
|
5459
5459
|
"aliases": [],
|
|
5460
5460
|
"hidden": false,
|
|
5461
5461
|
"usage": "[options] <service>",
|
|
@@ -10,7 +10,10 @@
|
|
|
10
10
|
* `install` renders one user-level unit per service (LaunchAgent on macOS,
|
|
11
11
|
* systemd --user on Linux), each of which just runs
|
|
12
12
|
* `node hq agent kit run <service>`; the Docker image runs the same four
|
|
13
|
-
* commands.
|
|
13
|
+
* commands. Hosts where the service manager is unusable (no systemd user
|
|
14
|
+
* session bus, permission refused, launchd unavailable) fall back to one
|
|
15
|
+
* detached `hq agent kit run all` supervisor (pid in ~/.hq-agent/kit.pid,
|
|
16
|
+
* output in ~/.hq-agent/logs/kit.log). Logs: ~/.hq-agent/logs/<service>.log. Skills:
|
|
14
17
|
* ~/.hq-agent/skills/<name>/SKILL.md.
|
|
15
18
|
*/
|
|
16
19
|
import { Command } from "commander";
|
|
@@ -19,6 +22,7 @@ import { type KitConfig } from "../lib/agent-kit/kit-config.js";
|
|
|
19
22
|
import { type KitLogger } from "../lib/agent-kit/log.js";
|
|
20
23
|
import { type AgentKitPaths } from "../lib/agent-kit/paths.js";
|
|
21
24
|
import { type KitService } from "../lib/agent-kit/services.js";
|
|
25
|
+
import { type DetachedSpawn, type KillFn } from "../lib/agent-kit/fallback.js";
|
|
22
26
|
import { type ServiceHostPaths, type ServiceManagerDeps, type ServiceSetResult } from "../lib/service-manager/index.js";
|
|
23
27
|
export declare class KitError extends Error {
|
|
24
28
|
constructor(message: string);
|
|
@@ -32,12 +36,25 @@ export interface KitInstallOptions {
|
|
|
32
36
|
inboxAck?: boolean;
|
|
33
37
|
activate?: boolean;
|
|
34
38
|
}
|
|
39
|
+
export interface KitFallbackInfo {
|
|
40
|
+
mode: "fallback";
|
|
41
|
+
pid: number;
|
|
42
|
+
pidFile: string;
|
|
43
|
+
logPath: string;
|
|
44
|
+
/** Add to `crontab -e` so the supervisor survives a reboot. */
|
|
45
|
+
crontab: string;
|
|
46
|
+
}
|
|
35
47
|
export interface KitInstallResult {
|
|
36
48
|
config: KitConfig;
|
|
37
49
|
skills: string[];
|
|
38
50
|
services: ServiceSetResult;
|
|
51
|
+
fallback?: KitFallbackInfo;
|
|
52
|
+
}
|
|
53
|
+
export interface KitFallbackDeps {
|
|
54
|
+
spawn?: DetachedSpawn;
|
|
55
|
+
kill?: KillFn;
|
|
39
56
|
}
|
|
40
|
-
export declare function installKit(paths: AgentKitPaths, opts: KitInstallOptions, host: ServiceHostPaths, deps?: ServiceManagerDeps): KitInstallResult;
|
|
57
|
+
export declare function installKit(paths: AgentKitPaths, opts: KitInstallOptions, host: ServiceHostPaths, deps?: ServiceManagerDeps, fallbackDeps?: KitFallbackDeps): KitInstallResult;
|
|
41
58
|
export declare function formatServiceSet(result: ServiceSetResult): string[];
|
|
42
59
|
/** Shared runtime for `kit run <service>`. */
|
|
43
60
|
export interface KitRuntime {
|
|
@@ -47,7 +64,10 @@ export interface KitRuntime {
|
|
|
47
64
|
getToken: () => Promise<string>;
|
|
48
65
|
log: KitLogger;
|
|
49
66
|
}
|
|
50
|
-
export declare function buildKitRuntime(service: KitService): KitRuntime;
|
|
67
|
+
export declare function buildKitRuntime(service: KitService | "all"): KitRuntime;
|
|
51
68
|
export declare function runKitService(service: KitService, rt: KitRuntime): Promise<void>;
|
|
69
|
+
/** `kit run all`: supervise the four services as child processes with restart backoff. */
|
|
70
|
+
export declare function runAllKitServices(rt: KitRuntime, host: ServiceHostPaths): Promise<void>;
|
|
71
|
+
export declare function formatFallbackStatus(paths: AgentKitPaths, kill?: KillFn): string[] | null;
|
|
52
72
|
export declare function registerAgentKitCommand(agent: Command): void;
|
|
53
73
|
//# sourceMappingURL=agent-kit.d.ts.map
|
|
@@ -10,7 +10,10 @@
|
|
|
10
10
|
* `install` renders one user-level unit per service (LaunchAgent on macOS,
|
|
11
11
|
* systemd --user on Linux), each of which just runs
|
|
12
12
|
* `node hq agent kit run <service>`; the Docker image runs the same four
|
|
13
|
-
* commands.
|
|
13
|
+
* commands. Hosts where the service manager is unusable (no systemd user
|
|
14
|
+
* session bus, permission refused, launchd unavailable) fall back to one
|
|
15
|
+
* detached `hq agent kit run all` supervisor (pid in ~/.hq-agent/kit.pid,
|
|
16
|
+
* output in ~/.hq-agent/logs/kit.log). Logs: ~/.hq-agent/logs/<service>.log. Skills:
|
|
14
17
|
* ~/.hq-agent/skills/<name>/SKILL.md.
|
|
15
18
|
*/
|
|
16
19
|
import chalk from "chalk";
|
|
@@ -25,7 +28,11 @@ import { agentKitPaths } from "../lib/agent-kit/paths.js";
|
|
|
25
28
|
import { isKitService, KIT_SERVICES, kitServiceSpecs, } from "../lib/agent-kit/services.js";
|
|
26
29
|
import { writeKitSkills } from "../lib/agent-kit/skills.js";
|
|
27
30
|
import { runHeartbeatLoop } from "../lib/agent-kit/run/heartbeat.js";
|
|
28
|
-
import { runInboxLoop } from "../lib/agent-kit/run/inbox.js";
|
|
31
|
+
import { pollInboxOnce, runInboxLoop } from "../lib/agent-kit/run/inbox.js";
|
|
32
|
+
import { superviseKitServices } from "../lib/agent-kit/run/supervisor.js";
|
|
33
|
+
import { fallbackStatus, rebootCrontabLine, shouldUseFallback, startFallback, stopFallback, } from "../lib/agent-kit/fallback.js";
|
|
34
|
+
import { serviceLogPath } from "../lib/agent-kit/paths.js";
|
|
35
|
+
import { spawn } from "node:child_process";
|
|
29
36
|
import { startMeshListener } from "../lib/agent-kit/run/mesh-listener.js";
|
|
30
37
|
import { runSyncLoop } from "../lib/agent-kit/run/sync.js";
|
|
31
38
|
import { resolveHqBinary, resolveNodeBinary, } from "../lib/mesh/live/daemon/install.js";
|
|
@@ -57,7 +64,7 @@ export function resolveServiceHost(paths) {
|
|
|
57
64
|
hqBinary: resolveHqBinary(),
|
|
58
65
|
};
|
|
59
66
|
}
|
|
60
|
-
export function installKit(paths, opts, host, deps = {}) {
|
|
67
|
+
export function installKit(paths, opts, host, deps = {}, fallbackDeps = {}) {
|
|
61
68
|
requireExternalCreds(paths);
|
|
62
69
|
ensureKitDirs(paths);
|
|
63
70
|
const existing = fs.existsSync(paths.kitConfigPath)
|
|
@@ -72,10 +79,20 @@ export function installKit(paths, opts, host, deps = {}) {
|
|
|
72
79
|
fs.mkdirSync(config.hqRoot, { recursive: true, mode: 0o700 });
|
|
73
80
|
writeKitConfig(paths, config);
|
|
74
81
|
const skills = writeKitSkills(paths);
|
|
82
|
+
const activate = opts.activate ?? deps.activate ?? true;
|
|
75
83
|
const services = installServices(kitServiceSpecs(paths), host, {
|
|
76
84
|
...deps,
|
|
77
|
-
activate
|
|
85
|
+
activate,
|
|
78
86
|
});
|
|
87
|
+
if (shouldUseFallback(services, activate)) {
|
|
88
|
+
const started = startFallback(paths, host, fallbackDeps);
|
|
89
|
+
return {
|
|
90
|
+
config,
|
|
91
|
+
skills,
|
|
92
|
+
services,
|
|
93
|
+
fallback: { mode: "fallback", ...started, crontab: rebootCrontabLine(paths, host) },
|
|
94
|
+
};
|
|
95
|
+
}
|
|
79
96
|
return { config, skills, services };
|
|
80
97
|
}
|
|
81
98
|
export function formatServiceSet(result) {
|
|
@@ -151,6 +168,16 @@ export async function runKitService(service, rt) {
|
|
|
151
168
|
refreshMs: rt.config.meshRefreshMs,
|
|
152
169
|
getToken: rt.getToken,
|
|
153
170
|
log: rt.log,
|
|
171
|
+
// An inbox doorbell means "poll now" rather than waiting for the loop.
|
|
172
|
+
onInboxDoorbell: () => pollInboxOnce({
|
|
173
|
+
paths: rt.paths,
|
|
174
|
+
agentUid: rt.creds.entityUid,
|
|
175
|
+
apiBaseUrl: rt.creds.apiBaseUrl,
|
|
176
|
+
pollMs: rt.config.inboxPollMs,
|
|
177
|
+
ack: rt.config.inboxAck,
|
|
178
|
+
getToken: rt.getToken,
|
|
179
|
+
log: rt.log,
|
|
180
|
+
}),
|
|
154
181
|
});
|
|
155
182
|
const stop = () => {
|
|
156
183
|
void handle.stop().then(() => process.exit(0));
|
|
@@ -164,6 +191,45 @@ export async function runKitService(service, rt) {
|
|
|
164
191
|
}
|
|
165
192
|
}
|
|
166
193
|
}
|
|
194
|
+
/** `kit run all`: supervise the four services as child processes with restart backoff. */
|
|
195
|
+
export async function runAllKitServices(rt, host) {
|
|
196
|
+
rt.log("info", `supervisor starting ${KIT_SERVICES.join(",")} agent=${rt.creds.entityUid} cli=${CLI_VERSION}`);
|
|
197
|
+
const handle = superviseKitServices({
|
|
198
|
+
services: KIT_SERVICES,
|
|
199
|
+
log: rt.log,
|
|
200
|
+
spawnService: (service) => {
|
|
201
|
+
const fd = fs.openSync(serviceLogPath(rt.paths, service), "a", 0o600);
|
|
202
|
+
try {
|
|
203
|
+
return spawn(host.nodeBinary, [host.hqBinary, "agent", "kit", "run", service], {
|
|
204
|
+
stdio: ["ignore", fd, fd],
|
|
205
|
+
env: process.env,
|
|
206
|
+
cwd: rt.paths.agentDir,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
finally {
|
|
210
|
+
fs.closeSync(fd);
|
|
211
|
+
}
|
|
212
|
+
},
|
|
213
|
+
});
|
|
214
|
+
await new Promise((resolve) => {
|
|
215
|
+
const stop = (signal) => {
|
|
216
|
+
rt.log("info", `supervisor stopping (${signal})`);
|
|
217
|
+
handle.stop("SIGTERM");
|
|
218
|
+
resolve();
|
|
219
|
+
};
|
|
220
|
+
process.once("SIGTERM", () => stop("SIGTERM"));
|
|
221
|
+
process.once("SIGINT", () => stop("SIGINT"));
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
export function formatFallbackStatus(paths, kill) {
|
|
225
|
+
const st = fallbackStatus(paths, kill);
|
|
226
|
+
if (!st)
|
|
227
|
+
return null;
|
|
228
|
+
const state = st.running
|
|
229
|
+
? chalk.green(`running (pid ${st.pid})`)
|
|
230
|
+
: chalk.yellow(st.pid === null ? "pidfile unreadable" : `not running (stale pid ${st.pid})`);
|
|
231
|
+
return [` mode: fallback supervisor (hq agent kit run all) ${state}${chalk.dim(` ${st.pidFile}`)}`];
|
|
232
|
+
}
|
|
167
233
|
function fail(err) {
|
|
168
234
|
console.error(chalk.red(err instanceof Error ? err.message : String(err)));
|
|
169
235
|
process.exit(1);
|
|
@@ -191,9 +257,19 @@ export function registerAgentKitCommand(agent) {
|
|
|
191
257
|
console.log(` hq root: ${result.config.hqRoot}`);
|
|
192
258
|
console.log(` logs: ${paths.logsDir}`);
|
|
193
259
|
console.log(` skills: ${paths.skillsDir} (${result.skills.length} skills)`);
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
260
|
+
if (result.fallback) {
|
|
261
|
+
console.log(chalk.yellow(" services: service manager unavailable — running in fallback mode " +
|
|
262
|
+
`(hq agent kit run all, pid ${result.fallback.pid})`));
|
|
263
|
+
console.log(` pidfile: ${result.fallback.pidFile}`);
|
|
264
|
+
console.log(` kit log: ${result.fallback.logPath}`);
|
|
265
|
+
console.log(" To restart it after a reboot, add this line with `crontab -e`:");
|
|
266
|
+
console.log(` ${result.fallback.crontab}`);
|
|
267
|
+
}
|
|
268
|
+
else {
|
|
269
|
+
console.log(" services:");
|
|
270
|
+
for (const line of formatServiceSet(result.services))
|
|
271
|
+
console.log(line);
|
|
272
|
+
}
|
|
197
273
|
console.log("");
|
|
198
274
|
console.log("Next: hq agent probe");
|
|
199
275
|
}
|
|
@@ -208,13 +284,19 @@ export function registerAgentKitCommand(agent) {
|
|
|
208
284
|
.action((opts) => {
|
|
209
285
|
try {
|
|
210
286
|
const paths = agentKitPaths(os.homedir(), process.env);
|
|
287
|
+
const fallback = fallbackStatus(paths);
|
|
211
288
|
const result = servicesStatus(kitServiceSpecs(paths), resolveServiceHost(paths));
|
|
212
289
|
if (opts.json) {
|
|
213
|
-
console.log(JSON.stringify({ ok: true, ...result }, null, 2));
|
|
290
|
+
console.log(JSON.stringify({ ok: true, ...result, ...(fallback ? { fallback } : {}) }, null, 2));
|
|
214
291
|
return;
|
|
215
292
|
}
|
|
216
|
-
|
|
217
|
-
|
|
293
|
+
const fallbackLines = formatFallbackStatus(paths);
|
|
294
|
+
if (fallbackLines)
|
|
295
|
+
for (const line of fallbackLines)
|
|
296
|
+
console.log(line);
|
|
297
|
+
else
|
|
298
|
+
for (const line of formatServiceSet(result))
|
|
299
|
+
console.log(line);
|
|
218
300
|
}
|
|
219
301
|
catch (err) {
|
|
220
302
|
fail(err);
|
|
@@ -227,11 +309,15 @@ export function registerAgentKitCommand(agent) {
|
|
|
227
309
|
.action((opts) => {
|
|
228
310
|
try {
|
|
229
311
|
const paths = agentKitPaths(os.homedir(), process.env);
|
|
312
|
+
const fallback = stopFallback(paths);
|
|
230
313
|
const result = uninstallServices(kitServiceSpecs(paths), resolveServiceHost(paths));
|
|
231
314
|
if (opts.json) {
|
|
232
|
-
console.log(JSON.stringify({ ok: true, ...result }, null, 2));
|
|
315
|
+
console.log(JSON.stringify({ ok: true, ...result, fallback }, null, 2));
|
|
233
316
|
return;
|
|
234
317
|
}
|
|
318
|
+
if (fallback.pid !== null) {
|
|
319
|
+
console.log(` fallback supervisor pid ${fallback.pid} ${fallback.killed ? "stopped" : "was not running"}; pidfile removed`);
|
|
320
|
+
}
|
|
235
321
|
console.log(chalk.green("HQ agent kit services removed."));
|
|
236
322
|
for (const line of formatServiceSet(result))
|
|
237
323
|
console.log(line);
|
|
@@ -242,10 +328,21 @@ export function registerAgentKitCommand(agent) {
|
|
|
242
328
|
});
|
|
243
329
|
kit
|
|
244
330
|
.command("run <service>")
|
|
245
|
-
.description(`Run one kit service in the foreground (${KIT_SERVICES.join("|")})`)
|
|
331
|
+
.description(`Run one kit service in the foreground (${KIT_SERVICES.join("|")}), or \`all\` to supervise every service in one process`)
|
|
246
332
|
.action(async (service) => {
|
|
333
|
+
if (service === "all") {
|
|
334
|
+
try {
|
|
335
|
+
const paths = agentKitPaths(os.homedir(), process.env);
|
|
336
|
+
const rt = buildKitRuntime("all");
|
|
337
|
+
await runAllKitServices(rt, resolveServiceHost(paths));
|
|
338
|
+
process.exit(0);
|
|
339
|
+
}
|
|
340
|
+
catch (err) {
|
|
341
|
+
fail(err);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
247
344
|
if (!isKitService(service)) {
|
|
248
|
-
console.error(chalk.red(`Unknown kit service "${service}". Expected one of: ${KIT_SERVICES.join(", ")}`));
|
|
345
|
+
console.error(chalk.red(`Unknown kit service "${service}". Expected one of: all, ${KIT_SERVICES.join(", ")}`));
|
|
249
346
|
process.exit(1);
|
|
250
347
|
}
|
|
251
348
|
try {
|
|
@@ -4,9 +4,14 @@
|
|
|
4
4
|
*
|
|
5
5
|
* whoami mint as the machine identity; token names this agent
|
|
6
6
|
* team-sync `hq sync pull --all` succeeds and the company folder exists
|
|
7
|
-
* work-mesh realtime
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* work-mesh the personal (contract-2) realtime vend names this agent, the
|
|
8
|
+
* agent-authorized inbox route answers 200, and this host's own
|
|
9
|
+
* presence is live: a fresh last-heartbeat.json plus an ok
|
|
10
|
+
* component-mesh stamp from the doorbell listener. (The company
|
|
11
|
+
* roster route is owner/admin-only and 404s for agt_ callers.)
|
|
12
|
+
* dm send a DM to self via POST /v1/notify/dm and read it back
|
|
13
|
+
* through GET /v1/agents/{uid}/inbox (the agent read surface;
|
|
14
|
+
* /v1/notify/thread is intentionally closed to agents)
|
|
10
15
|
* secrets `GET /secrets/{companyUid}` answers 200
|
|
11
16
|
*
|
|
12
17
|
* Output is the exact shape the console recipes promise: one
|
|
@@ -18,6 +23,7 @@
|
|
|
18
23
|
* the kit's last heartbeat.
|
|
19
24
|
*/
|
|
20
25
|
import { Command } from "commander";
|
|
26
|
+
import { type InboxFetchResult } from "../lib/agent-kit/run/inbox.js";
|
|
21
27
|
import { type AgentKitPaths } from "../lib/agent-kit/paths.js";
|
|
22
28
|
import type { ExternalMachineCreds } from "../lib/agent-kit/creds.js";
|
|
23
29
|
export interface ProbeCheck {
|
|
@@ -34,6 +40,8 @@ export interface ProbeResult {
|
|
|
34
40
|
}
|
|
35
41
|
export declare const DM_ROUNDTRIP_TIMEOUT_MS = 20000;
|
|
36
42
|
export declare const DM_ROUNDTRIP_POLL_MS = 2000;
|
|
43
|
+
/** A heartbeat older than this does not count as live presence (3 missed 60 s beats). */
|
|
44
|
+
export declare const PRESENCE_FRESH_SECONDS = 180;
|
|
37
45
|
export interface ProbeDeps {
|
|
38
46
|
paths: AgentKitPaths;
|
|
39
47
|
creds: ExternalMachineCreds;
|
|
@@ -44,11 +52,9 @@ export interface ProbeDeps {
|
|
|
44
52
|
vendRealtime: (token: string) => Promise<{
|
|
45
53
|
actorUid: string;
|
|
46
54
|
}>;
|
|
47
|
-
|
|
55
|
+
/** GET /v1/agents/{self}/inbox — agent-authorized, self-scoped. */
|
|
56
|
+
readInbox: (token: string) => Promise<InboxFetchResult>;
|
|
48
57
|
sendDm: (token: string, toUid: string, body: string) => Promise<void>;
|
|
49
|
-
readThread: (token: string, withUid: string) => Promise<Array<{
|
|
50
|
-
body: string;
|
|
51
|
-
}>>;
|
|
52
58
|
listSecrets: (token: string, companyUid: string) => Promise<number>;
|
|
53
59
|
postResult: (token: string, result: ProbeResult) => Promise<number>;
|
|
54
60
|
now?: () => Date;
|
|
@@ -58,6 +64,8 @@ export interface ProbeDeps {
|
|
|
58
64
|
}
|
|
59
65
|
export declare function heartbeatAgeSeconds(paths: Pick<AgentKitPaths, "lastHeartbeatPath">, now?: () => Date): number | null;
|
|
60
66
|
export declare function runProbe(deps: ProbeDeps): Promise<ProbeResult>;
|
|
67
|
+
/** True when any inbox item (whatever its channel shape) carries the nonce. */
|
|
68
|
+
export declare function inboxContainsNonce(body: unknown, nonce: string): boolean;
|
|
61
69
|
export declare function formatHeartbeatAge(age: number | null): string;
|
|
62
70
|
/**
|
|
63
71
|
* Recipe contract: `PASS <check> <detail>` / `FAIL <check> <detail>` per
|
|
@@ -4,9 +4,14 @@
|
|
|
4
4
|
*
|
|
5
5
|
* whoami mint as the machine identity; token names this agent
|
|
6
6
|
* team-sync `hq sync pull --all` succeeds and the company folder exists
|
|
7
|
-
* work-mesh realtime
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* work-mesh the personal (contract-2) realtime vend names this agent, the
|
|
8
|
+
* agent-authorized inbox route answers 200, and this host's own
|
|
9
|
+
* presence is live: a fresh last-heartbeat.json plus an ok
|
|
10
|
+
* component-mesh stamp from the doorbell listener. (The company
|
|
11
|
+
* roster route is owner/admin-only and 404s for agt_ callers.)
|
|
12
|
+
* dm send a DM to self via POST /v1/notify/dm and read it back
|
|
13
|
+
* through GET /v1/agents/{uid}/inbox (the agent read surface;
|
|
14
|
+
* /v1/notify/thread is intentionally closed to agents)
|
|
10
15
|
* secrets `GET /secrets/{companyUid}` answers 200
|
|
11
16
|
*
|
|
12
17
|
* Output is the exact shape the console recipes promise: one
|
|
@@ -26,8 +31,11 @@ import { CLI_VERSION } from "../cli-version.js";
|
|
|
26
31
|
import { ensureCognitoToken } from "../utils/cognito-session.js";
|
|
27
32
|
import { peekIdToken } from "../utils/id-token.js";
|
|
28
33
|
import { getCompanyUid, vaultApiFetch } from "../utils/vault-api.js";
|
|
29
|
-
import {
|
|
30
|
-
import {
|
|
34
|
+
import { sendAgentDm } from "./agents.js";
|
|
35
|
+
import { createPersonalRealtimeFetcher } from "../lib/mesh/live/daemon/credentials.js";
|
|
36
|
+
import { readComponentStatus } from "../lib/agent-kit/creds.js";
|
|
37
|
+
import { COMPONENT_STALE_AFTER_MS } from "../lib/agent-kit/run/heartbeat.js";
|
|
38
|
+
import { defaultFetchInbox } from "../lib/agent-kit/run/inbox.js";
|
|
31
39
|
import { agentKitPaths } from "../lib/agent-kit/paths.js";
|
|
32
40
|
import { readKitConfig } from "../lib/agent-kit/kit-config.js";
|
|
33
41
|
import { readLastHeartbeat } from "../lib/agent-kit/run/heartbeat.js";
|
|
@@ -35,6 +43,8 @@ import { defaultRunPull, syncPullArgs } from "../lib/agent-kit/run/sync.js";
|
|
|
35
43
|
import { requireExternalCreds } from "./agent-kit.js";
|
|
36
44
|
export const DM_ROUNDTRIP_TIMEOUT_MS = 20_000;
|
|
37
45
|
export const DM_ROUNDTRIP_POLL_MS = 2_000;
|
|
46
|
+
/** A heartbeat older than this does not count as live presence (3 missed 60 s beats). */
|
|
47
|
+
export const PRESENCE_FRESH_SECONDS = 180;
|
|
38
48
|
function errText(err) {
|
|
39
49
|
return err instanceof Error ? err.message : String(err);
|
|
40
50
|
}
|
|
@@ -108,7 +118,7 @@ export async function runProbe(deps) {
|
|
|
108
118
|
else {
|
|
109
119
|
checks.push({ name: "team-sync", ok: false, detail: "skipped: no token" });
|
|
110
120
|
}
|
|
111
|
-
// 3. mesh-presence
|
|
121
|
+
// 3. work-mesh: vend + agent-authorized reachability + self presence
|
|
112
122
|
if (token && companyUid) {
|
|
113
123
|
try {
|
|
114
124
|
const vend = await deps.vendRealtime(token);
|
|
@@ -116,17 +126,23 @@ export async function runProbe(deps) {
|
|
|
116
126
|
checks.push({ name: "work-mesh", ok: false, detail: `realtime vend is for ${vend.actorUid}` });
|
|
117
127
|
}
|
|
118
128
|
else {
|
|
119
|
-
const
|
|
120
|
-
const
|
|
121
|
-
const
|
|
122
|
-
const
|
|
123
|
-
const
|
|
129
|
+
const inbox = await deps.readInbox(token);
|
|
130
|
+
const age = heartbeatAgeSeconds(deps.paths, now);
|
|
131
|
+
const mesh = readComponentStatus(deps.paths, "mesh", COMPONENT_STALE_AFTER_MS.mesh, now);
|
|
132
|
+
const reachable = inbox.status === 200;
|
|
133
|
+
const fresh = age !== null && age <= PRESENCE_FRESH_SECONDS;
|
|
134
|
+
const meshOk = mesh.status === "ok";
|
|
135
|
+
const ok = reachable && fresh && meshOk;
|
|
136
|
+
const parts = [
|
|
137
|
+
"realtime vend ok",
|
|
138
|
+
`inbox GET → ${inbox.status}`,
|
|
139
|
+
`heartbeat ${age === null ? "never" : `${age}s ago`}`,
|
|
140
|
+
`mesh listener ${mesh.status}${mesh.at ? ` @${mesh.at.toISOString()}` : ""}`,
|
|
141
|
+
];
|
|
124
142
|
checks.push({
|
|
125
143
|
name: "work-mesh",
|
|
126
144
|
ok,
|
|
127
|
-
detail:
|
|
128
|
-
? `realtime vend ok; presence=${presence} lastHeartbeatAt=${last}${ok ? "" : " — is `hq agent kit` installed and running?"}`
|
|
129
|
-
: "realtime vend ok but this agent is not on the company roster",
|
|
145
|
+
detail: `${parts.join("; ")}${ok ? "" : " — is `hq agent kit` installed and running? see ~/.hq-agent/logs/mesh.log"}`,
|
|
130
146
|
});
|
|
131
147
|
}
|
|
132
148
|
}
|
|
@@ -137,27 +153,37 @@ export async function runProbe(deps) {
|
|
|
137
153
|
else {
|
|
138
154
|
checks.push({ name: "work-mesh", ok: false, detail: "skipped: no token/company" });
|
|
139
155
|
}
|
|
140
|
-
// 4. dm-
|
|
156
|
+
// 4. dm round-trip: send via notify, read back via the agent inbox
|
|
141
157
|
if (token) {
|
|
142
158
|
try {
|
|
143
159
|
const body = `hq agent probe ${nonce}`;
|
|
144
160
|
await deps.sendDm(token, self, body);
|
|
145
161
|
const deadline = now().getTime() + (deps.dmTimeoutMs ?? DM_ROUNDTRIP_TIMEOUT_MS);
|
|
146
162
|
let found = false;
|
|
163
|
+
let lastStatus = 0;
|
|
147
164
|
for (;;) {
|
|
148
|
-
const
|
|
149
|
-
|
|
165
|
+
const res = await deps.readInbox(token);
|
|
166
|
+
lastStatus = res.status;
|
|
167
|
+
if (res.status === 200 && inboxContainsNonce(res.body, nonce)) {
|
|
150
168
|
found = true;
|
|
151
169
|
break;
|
|
152
170
|
}
|
|
171
|
+
// A 4xx will not heal by polling (auth/route refusal): stop now.
|
|
172
|
+
if (res.status >= 400 && res.status < 500)
|
|
173
|
+
break;
|
|
153
174
|
if (now().getTime() >= deadline)
|
|
154
175
|
break;
|
|
155
176
|
await sleep(DM_ROUNDTRIP_POLL_MS);
|
|
156
177
|
}
|
|
178
|
+
const secs = Math.round((deps.dmTimeoutMs ?? DM_ROUNDTRIP_TIMEOUT_MS) / 1000);
|
|
157
179
|
checks.push({
|
|
158
180
|
name: "dm",
|
|
159
181
|
ok: found,
|
|
160
|
-
detail: found
|
|
182
|
+
detail: found
|
|
183
|
+
? "sent to self via /v1/notify/dm and read back from /v1/agents/{uid}/inbox"
|
|
184
|
+
: lastStatus === 200
|
|
185
|
+
? `sent but not visible in /v1/agents/{uid}/inbox after ${secs}s`
|
|
186
|
+
: `sent but /v1/agents/{uid}/inbox answered ${lastStatus}`,
|
|
161
187
|
});
|
|
162
188
|
}
|
|
163
189
|
catch (err) {
|
|
@@ -205,6 +231,19 @@ export async function runProbe(deps) {
|
|
|
205
231
|
}
|
|
206
232
|
return result;
|
|
207
233
|
}
|
|
234
|
+
/** True when any inbox item (whatever its channel shape) carries the nonce. */
|
|
235
|
+
export function inboxContainsNonce(body, nonce) {
|
|
236
|
+
const rec = body && typeof body === "object" ? body : {};
|
|
237
|
+
const messages = Array.isArray(rec.messages) ? rec.messages : [];
|
|
238
|
+
return messages.some((m) => {
|
|
239
|
+
try {
|
|
240
|
+
return JSON.stringify(m).includes(nonce);
|
|
241
|
+
}
|
|
242
|
+
catch {
|
|
243
|
+
return false;
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
}
|
|
208
247
|
export function formatHeartbeatAge(age) {
|
|
209
248
|
if (age === null)
|
|
210
249
|
return "never (kit heartbeat has not posted yet)";
|
|
@@ -241,12 +280,11 @@ export function defaultProbeDeps(paths, creds) {
|
|
|
241
280
|
resolveCompanyUid: (token, slug) => getCompanyUid(token, slug),
|
|
242
281
|
runPull: defaultRunPull(process.execPath, process.argv[1], process.env),
|
|
243
282
|
vendRealtime: async (token) => {
|
|
244
|
-
const bundle = await
|
|
283
|
+
const bundle = await createPersonalRealtimeFetcher({ token, baseUrl: base })();
|
|
245
284
|
return { actorUid: bundle.actorUid };
|
|
246
285
|
},
|
|
247
|
-
|
|
286
|
+
readInbox: (token) => defaultFetchInbox(creds.entityUid, base)(token),
|
|
248
287
|
sendDm: (token, to, body) => sendAgentDm(token, to, body),
|
|
249
|
-
readThread: async (token, withUid) => readAgentThread(token, withUid, 20),
|
|
250
288
|
listSecrets: async (token, companyUid) => {
|
|
251
289
|
const res = await vaultApiFetch({
|
|
252
290
|
token,
|
package/dist/commands/dm.d.ts
CHANGED
|
@@ -259,5 +259,15 @@ export declare function formatInboxEvent(e: DmInboxEvent, nowMs: number): string
|
|
|
259
259
|
export declare function formatThreadMessage(m: DmThreadMessage, nowMs: number): string;
|
|
260
260
|
/** Render one channel/group message line. */
|
|
261
261
|
export declare function formatChannelMessage(m: ChannelMessageItem, nowMs: number): string;
|
|
262
|
+
/**
|
|
263
|
+
* The agent uid when the caller is an agent (machine) identity, else null.
|
|
264
|
+
* Agents may not read /v1/notify/inbox or /v1/notify/thread (the server
|
|
265
|
+
* allow-list is intentionally human-only); their read surface is
|
|
266
|
+
* GET /v1/agents/{uid}/inbox.
|
|
267
|
+
*/
|
|
268
|
+
export declare function agentCallerUid(token: string, machine?: () => boolean): string | null;
|
|
269
|
+
export declare const AGENT_THREAD_UNAVAILABLE: string;
|
|
270
|
+
/** One-line summary of an agent inbox item (DM, email, … — shapes vary by channel). Pure. */
|
|
271
|
+
export declare function formatAgentInboxItem(item: Record<string, unknown>): string;
|
|
262
272
|
export declare function registerDmCommand(program: Command): void;
|
|
263
273
|
//# sourceMappingURL=dm.d.ts.map
|
package/dist/commands/dm.js
CHANGED
|
@@ -2,6 +2,8 @@ import chalk from "chalk";
|
|
|
2
2
|
import { readFileSync } from "node:fs";
|
|
3
3
|
import { ensureCognitoToken } from "../utils/cognito-session.js";
|
|
4
4
|
import { vaultApiFetch } from "../utils/vault-api.js";
|
|
5
|
+
import { isMachineIdentity } from "@indigoai-us/hq-cloud";
|
|
6
|
+
import { peekIdToken } from "../utils/id-token.js";
|
|
5
7
|
const EMAIL_PATTERN = /^[^\s]+@[^\s]+$/;
|
|
6
8
|
// People (prs_) and agents (agt_) are both first-class DM participants; the
|
|
7
9
|
// server applies the same membership-overlap gate to either and delivers
|
|
@@ -789,9 +791,83 @@ async function ackEvents(token, eventIds) {
|
|
|
789
791
|
throw new Error(friendlyDmError(res.status, err.code, err.error ?? err.message ?? res.statusText));
|
|
790
792
|
}
|
|
791
793
|
}
|
|
794
|
+
/**
|
|
795
|
+
* The agent uid when the caller is an agent (machine) identity, else null.
|
|
796
|
+
* Agents may not read /v1/notify/inbox or /v1/notify/thread (the server
|
|
797
|
+
* allow-list is intentionally human-only); their read surface is
|
|
798
|
+
* GET /v1/agents/{uid}/inbox.
|
|
799
|
+
*/
|
|
800
|
+
export function agentCallerUid(token, machine = isMachineIdentity) {
|
|
801
|
+
const claims = peekIdToken(token);
|
|
802
|
+
const uid = claims["custom:entityUid"];
|
|
803
|
+
if (typeof uid !== "string" || !uid.startsWith("agt_"))
|
|
804
|
+
return null;
|
|
805
|
+
if (claims["custom:entityType"] === "agent")
|
|
806
|
+
return uid;
|
|
807
|
+
try {
|
|
808
|
+
return machine() ? uid : null;
|
|
809
|
+
}
|
|
810
|
+
catch {
|
|
811
|
+
return null;
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
export const AGENT_THREAD_UNAVAILABLE = "`hq dm thread` reads /v1/notify/thread, which is not available to agent identities. " +
|
|
815
|
+
"Use `hq dm inbox` — for agents it reads GET /v1/agents/{uid}/inbox.";
|
|
816
|
+
/** One-line summary of an agent inbox item (DM, email, … — shapes vary by channel). Pure. */
|
|
817
|
+
export function formatAgentInboxItem(item) {
|
|
818
|
+
const str = (k) => (typeof item[k] === "string" ? item[k] : "");
|
|
819
|
+
const id = str("messageId") || str("id") || "?";
|
|
820
|
+
const channel = str("channel") || "inbox";
|
|
821
|
+
const from = str("fromDisplayName") || str("fromEmail") || str("from") || str("fromPersonUid") || "unknown";
|
|
822
|
+
const when = str("createdAt") || str("receivedAt");
|
|
823
|
+
const text = str("text") || str("body") || str("subject");
|
|
824
|
+
return `${chalk.bold(from)} ${chalk.dim(`[${channel}] ${id}${when ? ` ${when}` : ""}`)}\n ${firstLine(text)}`;
|
|
825
|
+
}
|
|
826
|
+
async function runAgentInbox(token, agentUid, opts) {
|
|
827
|
+
const route = `/v1/agents/${encodeURIComponent(agentUid)}/inbox`;
|
|
828
|
+
const res = await vaultApiFetch({ token, path: route });
|
|
829
|
+
if (!res.ok) {
|
|
830
|
+
const text = await res.text().catch(() => "");
|
|
831
|
+
console.error(chalk.red(`GET ${route} → ${res.status}${text ? `: ${text.slice(0, 300)}` : ""}`));
|
|
832
|
+
process.exit(1);
|
|
833
|
+
}
|
|
834
|
+
const data = (await res.json().catch(() => ({})));
|
|
835
|
+
let items = Array.isArray(data.messages) ? data.messages : [];
|
|
836
|
+
if (opts.limit && Number(opts.limit) > 0)
|
|
837
|
+
items = items.slice(0, Number(opts.limit));
|
|
838
|
+
if (opts.json) {
|
|
839
|
+
console.log(JSON.stringify(items, null, 2));
|
|
840
|
+
}
|
|
841
|
+
else if (items.length === 0) {
|
|
842
|
+
console.log(chalk.dim("No pending inbox items."));
|
|
843
|
+
}
|
|
844
|
+
else {
|
|
845
|
+
console.log(chalk.green(`${items.length} pending inbox item${items.length === 1 ? "" : "s"}:`));
|
|
846
|
+
for (const item of items)
|
|
847
|
+
console.log(`\n${formatAgentInboxItem(item)}`);
|
|
848
|
+
}
|
|
849
|
+
if (opts.markRead) {
|
|
850
|
+
let acked = 0;
|
|
851
|
+
for (const item of items) {
|
|
852
|
+
const id = typeof item.messageId === "string" ? item.messageId : typeof item.id === "string" ? item.id : null;
|
|
853
|
+
if (!id)
|
|
854
|
+
continue;
|
|
855
|
+
const ack = await vaultApiFetch({ token, path: `${route}/${encodeURIComponent(id)}/ack`, method: "POST" });
|
|
856
|
+
if (ack.ok)
|
|
857
|
+
acked += 1;
|
|
858
|
+
}
|
|
859
|
+
if (!opts.json && acked > 0)
|
|
860
|
+
console.log(chalk.dim(`\nAcked ${acked}.`));
|
|
861
|
+
}
|
|
862
|
+
}
|
|
792
863
|
async function runDmInbox(opts) {
|
|
793
864
|
try {
|
|
794
865
|
const token = await ensureCognitoToken();
|
|
866
|
+
const agentUid = agentCallerUid(token);
|
|
867
|
+
if (agentUid) {
|
|
868
|
+
await runAgentInbox(token, agentUid, opts);
|
|
869
|
+
return;
|
|
870
|
+
}
|
|
795
871
|
const query = {};
|
|
796
872
|
if (opts.limit)
|
|
797
873
|
query.limit = opts.limit;
|
|
@@ -841,6 +917,10 @@ async function runDmThread(identifier, opts) {
|
|
|
841
917
|
if (opts.limit)
|
|
842
918
|
query.limit = opts.limit;
|
|
843
919
|
const token = await ensureCognitoToken();
|
|
920
|
+
if (agentCallerUid(token)) {
|
|
921
|
+
console.error(chalk.yellow(AGENT_THREAD_UNAVAILABLE));
|
|
922
|
+
process.exit(1);
|
|
923
|
+
}
|
|
844
924
|
const res = await vaultApiFetch({
|
|
845
925
|
token,
|
|
846
926
|
path: "/v1/notify/thread",
|