@indigoai-us/hq-cli 5.115.6 → 5.117.0
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 +169 -11
- package/dist/command-catalog.generated.d.ts +220 -2
- package/dist/command-catalog.generated.js +281 -2
- package/dist/command-registration-plan.d.ts +6 -0
- package/dist/command-registration-plan.js +1 -0
- package/dist/commands/agent-enroll.d.ts +105 -0
- package/dist/commands/agent-enroll.js +273 -0
- package/dist/commands/agent-kit.d.ts +53 -0
- package/dist/commands/agent-kit.js +260 -0
- package/dist/commands/agent-mcp.d.ts +22 -0
- package/dist/commands/agent-mcp.js +104 -0
- package/dist/commands/agent-probe.d.ts +71 -0
- package/dist/commands/agent-probe.js +294 -0
- package/dist/commands/agent.d.ts +12 -0
- package/dist/commands/agent.js +23 -0
- package/dist/commands/agents.d.ts +27 -0
- package/dist/commands/agents.js +280 -6
- package/dist/commands/bot.d.ts +140 -1
- package/dist/commands/bot.js +757 -22
- package/dist/commands/secrets.js +17 -5
- package/dist/lib/agent-kit/creds.d.ts +60 -0
- package/dist/lib/agent-kit/creds.js +123 -0
- package/dist/lib/agent-kit/kit-config.d.ts +29 -0
- package/dist/lib/agent-kit/kit-config.js +54 -0
- package/dist/lib/agent-kit/log.d.ts +17 -0
- package/dist/lib/agent-kit/log.js +46 -0
- package/dist/lib/agent-kit/mcp/jsonrpc.d.ts +84 -0
- package/dist/lib/agent-kit/mcp/jsonrpc.js +164 -0
- package/dist/lib/agent-kit/mcp/tools.d.ts +45 -0
- package/dist/lib/agent-kit/mcp/tools.js +280 -0
- package/dist/lib/agent-kit/paths.d.ts +42 -0
- package/dist/lib/agent-kit/paths.js +56 -0
- package/dist/lib/agent-kit/run/heartbeat.d.ts +52 -0
- package/dist/lib/agent-kit/run/heartbeat.js +97 -0
- package/dist/lib/agent-kit/run/inbox.d.ts +59 -0
- package/dist/lib/agent-kit/run/inbox.js +152 -0
- package/dist/lib/agent-kit/run/mesh-listener.d.ts +58 -0
- package/dist/lib/agent-kit/run/mesh-listener.js +193 -0
- package/dist/lib/agent-kit/run/sync.d.ts +33 -0
- package/dist/lib/agent-kit/run/sync.js +58 -0
- package/dist/lib/agent-kit/services.d.ts +21 -0
- package/dist/lib/agent-kit/services.js +46 -0
- package/dist/lib/agent-kit/skills.d.ts +18 -0
- package/dist/lib/agent-kit/skills.js +149 -0
- package/dist/lib/bot/api.d.ts +51 -0
- package/dist/lib/bot/api.js +32 -0
- package/dist/lib/bot/daemon.d.ts +17 -0
- package/dist/lib/bot/daemon.js +44 -3
- package/dist/lib/bot/index.d.ts +4 -0
- package/dist/lib/bot/index.js +4 -0
- package/dist/lib/bot/inflight.d.ts +14 -0
- package/dist/lib/bot/local-config.d.ts +70 -0
- package/dist/lib/bot/local-config.js +147 -0
- package/dist/lib/bot/local-name.d.ts +54 -0
- package/dist/lib/bot/local-name.js +114 -0
- package/dist/lib/bot/run.d.ts +9 -0
- package/dist/lib/bot/run.js +117 -24
- package/dist/lib/bot/runnable.d.ts +51 -0
- package/dist/lib/bot/runnable.js +65 -0
- package/dist/lib/bot/self-heal.d.ts +52 -0
- package/dist/lib/bot/self-heal.js +79 -0
- package/dist/lib/bot/split.d.ts +32 -0
- package/dist/lib/bot/split.js +241 -0
- package/dist/lib/service-manager/index.d.ts +43 -0
- package/dist/lib/service-manager/index.js +114 -0
- package/dist/lib/service-manager/launchd.d.ts +23 -0
- package/dist/lib/service-manager/launchd.js +81 -0
- package/dist/lib/service-manager/systemd.d.ts +19 -0
- package/dist/lib/service-manager/systemd.js +72 -0
- package/dist/lib/service-manager/types.d.ts +32 -0
- package/dist/lib/service-manager/types.js +26 -0
- package/dist/utils/self-update.js +2 -30
- package/dist/utils/update-command-supervisor.cjs +194 -0
- package/dist/utils/version-gate.d.ts +18 -0
- package/dist/utils/version-gate.js +126 -7
- package/package.json +2 -2
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `hq agent mcp` — stdio MCP server backed by ~/.hq-agent/machine-creds.json.
|
|
3
|
+
*
|
|
4
|
+
* The console recipes register HQ in the bot's framework as
|
|
5
|
+
* { command: "hq", args: ["agent", "mcp"], env: { HQ_MACHINE_CREDS_FILE: … } }
|
|
6
|
+
* so this command must never print anything but JSON-RPC on stdout; all
|
|
7
|
+
* diagnostics go to stderr. Tools wrap existing CLI capability (see
|
|
8
|
+
* lib/agent-kit/mcp/tools.ts); subprocess tools re-invoke the same node +
|
|
9
|
+
* hq binary with the machine-identity env pinned.
|
|
10
|
+
*/
|
|
11
|
+
import { spawn } from "node:child_process";
|
|
12
|
+
import * as os from "node:os";
|
|
13
|
+
import { CLI_VERSION } from "../cli-version.js";
|
|
14
|
+
import { ensureCognitoToken } from "../utils/cognito-session.js";
|
|
15
|
+
import { vaultApiFetch } from "../utils/vault-api.js";
|
|
16
|
+
import { McpServer } from "../lib/agent-kit/mcp/jsonrpc.js";
|
|
17
|
+
import { buildAgentMcpTools } from "../lib/agent-kit/mcp/tools.js";
|
|
18
|
+
import { agentKitPaths } from "../lib/agent-kit/paths.js";
|
|
19
|
+
import { requireExternalCreds } from "./agent-kit.js";
|
|
20
|
+
export const AGENT_MCP_SERVER_NAME = "hq-agent";
|
|
21
|
+
export const AGENT_MCP_INSTRUCTIONS = "You are acting inside HQ as an enrolled external agent. Tools run as that machine identity: " +
|
|
22
|
+
"hq_whoami (identity), hq_search / hq_files_list / hq_files_read (company vault), " +
|
|
23
|
+
"hq_secrets_list / hq_secrets_exec (secrets by NAME only — values are never returned), " +
|
|
24
|
+
"hq_dm_send / hq_inbox_read (direct messages), hq_work_mesh_status (what the team is doing). " +
|
|
25
|
+
"Never ask a user for a token or secret value; if a secret is missing, name it and ask an admin to add it.";
|
|
26
|
+
export const HQ_SUBPROCESS_TIMEOUT_MS = 120_000;
|
|
27
|
+
export function defaultRunHq(nodeBinary, hqBinary, env) {
|
|
28
|
+
return (args) => new Promise((resolve) => {
|
|
29
|
+
const child = spawn(nodeBinary, [hqBinary, ...args], {
|
|
30
|
+
env,
|
|
31
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
32
|
+
});
|
|
33
|
+
let stdout = "";
|
|
34
|
+
let stderr = "";
|
|
35
|
+
const timer = setTimeout(() => child.kill("SIGKILL"), HQ_SUBPROCESS_TIMEOUT_MS);
|
|
36
|
+
child.stdout.on("data", (d) => (stdout += d.toString("utf8")));
|
|
37
|
+
child.stderr.on("data", (d) => (stderr += d.toString("utf8")));
|
|
38
|
+
child.on("error", (err) => {
|
|
39
|
+
clearTimeout(timer);
|
|
40
|
+
resolve({ code: 127, stdout, stderr: `${stderr}\n${err.message}` });
|
|
41
|
+
});
|
|
42
|
+
child.on("close", (code) => {
|
|
43
|
+
clearTimeout(timer);
|
|
44
|
+
resolve({ code: code ?? 1, stdout, stderr });
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
export function defaultMcpClients(creds, credsPath) {
|
|
49
|
+
const env = {
|
|
50
|
+
...process.env,
|
|
51
|
+
HQ_MACHINE_CREDS_FILE: credsPath,
|
|
52
|
+
HQ_REQUIRE_MACHINE_IDENTITY: "1",
|
|
53
|
+
HQ_VAULT_API_URL: creds.apiBaseUrl,
|
|
54
|
+
NO_COLOR: "1",
|
|
55
|
+
};
|
|
56
|
+
return {
|
|
57
|
+
creds,
|
|
58
|
+
runHq: defaultRunHq(process.execPath, process.argv[1], env),
|
|
59
|
+
getToken: () => ensureCognitoToken({ tokenSource: "machine", interactive: false }),
|
|
60
|
+
apiJson: async (token, path, init) => {
|
|
61
|
+
const res = await vaultApiFetch({
|
|
62
|
+
token,
|
|
63
|
+
baseUrl: creds.apiBaseUrl,
|
|
64
|
+
path,
|
|
65
|
+
method: init?.method,
|
|
66
|
+
body: init?.body,
|
|
67
|
+
query: init?.query,
|
|
68
|
+
});
|
|
69
|
+
const body = await res.json().catch(() => ({}));
|
|
70
|
+
return { status: res.status, body };
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
export function createAgentMcpServer(clients) {
|
|
75
|
+
return new McpServer({
|
|
76
|
+
name: AGENT_MCP_SERVER_NAME,
|
|
77
|
+
version: CLI_VERSION,
|
|
78
|
+
instructions: AGENT_MCP_INSTRUCTIONS,
|
|
79
|
+
tools: buildAgentMcpTools(clients),
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
export function registerAgentMcpCommand(agent) {
|
|
83
|
+
agent
|
|
84
|
+
.command("mcp")
|
|
85
|
+
.description("Serve HQ as a stdio MCP server for the bot framework (whoami, search, files, secrets-exec, dm, inbox, work-mesh)")
|
|
86
|
+
.action(async () => {
|
|
87
|
+
const paths = agentKitPaths(os.homedir(), process.env);
|
|
88
|
+
let creds;
|
|
89
|
+
try {
|
|
90
|
+
creds = requireExternalCreds(paths);
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
|
|
94
|
+
process.exit(1);
|
|
95
|
+
}
|
|
96
|
+
process.env.HQ_MACHINE_CREDS_FILE = paths.machineCredsPath;
|
|
97
|
+
process.env.HQ_REQUIRE_MACHINE_IDENTITY = "1";
|
|
98
|
+
process.env.HQ_VAULT_API_URL = creds.apiBaseUrl;
|
|
99
|
+
const server = createAgentMcpServer(defaultMcpClients(creds, paths.machineCredsPath));
|
|
100
|
+
process.stderr.write(`hq agent mcp: serving as ${creds.entityUid} (${creds.companySlug})\n`);
|
|
101
|
+
await server.serve(process.stdin, process.stdout);
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
//# sourceMappingURL=agent-mcp.js.map
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `hq agent probe` — end-to-end check that this host is a working external
|
|
3
|
+
* team member, one line per check, non-zero exit on any failure:
|
|
4
|
+
*
|
|
5
|
+
* whoami mint as the machine identity; token names this agent
|
|
6
|
+
* team-sync `hq sync pull --all` succeeds and the company folder exists
|
|
7
|
+
* work-mesh realtime credentials vend for this agent and the roster row
|
|
8
|
+
* shows presence online/stale (heartbeat landed)
|
|
9
|
+
* dm send a DM to self via the notify surface and read it back
|
|
10
|
+
* secrets `GET /secrets/{companyUid}` answers 200
|
|
11
|
+
*
|
|
12
|
+
* Output is the exact shape the console recipes promise: one
|
|
13
|
+
* `PASS <check> <detail>` / `FAIL <check> <detail>` line per check, then
|
|
14
|
+
* `heartbeat <age>`.
|
|
15
|
+
*
|
|
16
|
+
* The result is POSTed to /v1/agents/{uid}/probe-result (api-contract v1) so
|
|
17
|
+
* the console can flip the enrollment to "Enrolled". Also reports the age of
|
|
18
|
+
* the kit's last heartbeat.
|
|
19
|
+
*/
|
|
20
|
+
import { Command } from "commander";
|
|
21
|
+
import { type AgentKitPaths } from "../lib/agent-kit/paths.js";
|
|
22
|
+
import type { ExternalMachineCreds } from "../lib/agent-kit/creds.js";
|
|
23
|
+
export interface ProbeCheck {
|
|
24
|
+
name: string;
|
|
25
|
+
ok: boolean;
|
|
26
|
+
detail: string;
|
|
27
|
+
}
|
|
28
|
+
export interface ProbeResult {
|
|
29
|
+
passed: boolean;
|
|
30
|
+
checks: ProbeCheck[];
|
|
31
|
+
at: string;
|
|
32
|
+
/** Seconds since the kit's last successful heartbeat, or null if never. */
|
|
33
|
+
heartbeatAgeSeconds: number | null;
|
|
34
|
+
}
|
|
35
|
+
export declare const DM_ROUNDTRIP_TIMEOUT_MS = 20000;
|
|
36
|
+
export declare const DM_ROUNDTRIP_POLL_MS = 2000;
|
|
37
|
+
export interface ProbeDeps {
|
|
38
|
+
paths: AgentKitPaths;
|
|
39
|
+
creds: ExternalMachineCreds;
|
|
40
|
+
hqRoot: string;
|
|
41
|
+
getToken: () => Promise<string>;
|
|
42
|
+
resolveCompanyUid: (token: string, slug: string) => Promise<string>;
|
|
43
|
+
runPull: (args: string[]) => Promise<number>;
|
|
44
|
+
vendRealtime: (token: string) => Promise<{
|
|
45
|
+
actorUid: string;
|
|
46
|
+
}>;
|
|
47
|
+
listRoster: (token: string, companyUid: string) => Promise<Array<Record<string, unknown>>>;
|
|
48
|
+
sendDm: (token: string, toUid: string, body: string) => Promise<void>;
|
|
49
|
+
readThread: (token: string, withUid: string) => Promise<Array<{
|
|
50
|
+
body: string;
|
|
51
|
+
}>>;
|
|
52
|
+
listSecrets: (token: string, companyUid: string) => Promise<number>;
|
|
53
|
+
postResult: (token: string, result: ProbeResult) => Promise<number>;
|
|
54
|
+
now?: () => Date;
|
|
55
|
+
sleep?: (ms: number) => Promise<void>;
|
|
56
|
+
nonce?: () => string;
|
|
57
|
+
dmTimeoutMs?: number;
|
|
58
|
+
}
|
|
59
|
+
export declare function heartbeatAgeSeconds(paths: Pick<AgentKitPaths, "lastHeartbeatPath">, now?: () => Date): number | null;
|
|
60
|
+
export declare function runProbe(deps: ProbeDeps): Promise<ProbeResult>;
|
|
61
|
+
export declare function formatHeartbeatAge(age: number | null): string;
|
|
62
|
+
/**
|
|
63
|
+
* Recipe contract: `PASS <check> <detail>` / `FAIL <check> <detail>` per
|
|
64
|
+
* check, then `heartbeat <age>`. Two-space separators; no trailing verdict
|
|
65
|
+
* line (the exit code carries it, and the console keys only on the POSTed
|
|
66
|
+
* probe-result).
|
|
67
|
+
*/
|
|
68
|
+
export declare function formatProbeReport(result: ProbeResult, color?: boolean): string[];
|
|
69
|
+
export declare function defaultProbeDeps(paths: AgentKitPaths, creds: ExternalMachineCreds): ProbeDeps;
|
|
70
|
+
export declare function registerAgentProbeCommand(agent: Command): void;
|
|
71
|
+
//# sourceMappingURL=agent-probe.d.ts.map
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `hq agent probe` — end-to-end check that this host is a working external
|
|
3
|
+
* team member, one line per check, non-zero exit on any failure:
|
|
4
|
+
*
|
|
5
|
+
* whoami mint as the machine identity; token names this agent
|
|
6
|
+
* team-sync `hq sync pull --all` succeeds and the company folder exists
|
|
7
|
+
* work-mesh realtime credentials vend for this agent and the roster row
|
|
8
|
+
* shows presence online/stale (heartbeat landed)
|
|
9
|
+
* dm send a DM to self via the notify surface and read it back
|
|
10
|
+
* secrets `GET /secrets/{companyUid}` answers 200
|
|
11
|
+
*
|
|
12
|
+
* Output is the exact shape the console recipes promise: one
|
|
13
|
+
* `PASS <check> <detail>` / `FAIL <check> <detail>` line per check, then
|
|
14
|
+
* `heartbeat <age>`.
|
|
15
|
+
*
|
|
16
|
+
* The result is POSTed to /v1/agents/{uid}/probe-result (api-contract v1) so
|
|
17
|
+
* the console can flip the enrollment to "Enrolled". Also reports the age of
|
|
18
|
+
* the kit's last heartbeat.
|
|
19
|
+
*/
|
|
20
|
+
import chalk from "chalk";
|
|
21
|
+
import * as fs from "node:fs";
|
|
22
|
+
import * as os from "node:os";
|
|
23
|
+
import * as path from "node:path";
|
|
24
|
+
import { randomBytes } from "node:crypto";
|
|
25
|
+
import { CLI_VERSION } from "../cli-version.js";
|
|
26
|
+
import { ensureCognitoToken } from "../utils/cognito-session.js";
|
|
27
|
+
import { peekIdToken } from "../utils/id-token.js";
|
|
28
|
+
import { getCompanyUid, vaultApiFetch } from "../utils/vault-api.js";
|
|
29
|
+
import { listAgents, readAgentThread, sendAgentDm } from "./agents.js";
|
|
30
|
+
import { createContract3Fetcher } from "../lib/mesh/live/daemon/credentials.js";
|
|
31
|
+
import { agentKitPaths } from "../lib/agent-kit/paths.js";
|
|
32
|
+
import { readKitConfig } from "../lib/agent-kit/kit-config.js";
|
|
33
|
+
import { readLastHeartbeat } from "../lib/agent-kit/run/heartbeat.js";
|
|
34
|
+
import { defaultRunPull, syncPullArgs } from "../lib/agent-kit/run/sync.js";
|
|
35
|
+
import { requireExternalCreds } from "./agent-kit.js";
|
|
36
|
+
export const DM_ROUNDTRIP_TIMEOUT_MS = 20_000;
|
|
37
|
+
export const DM_ROUNDTRIP_POLL_MS = 2_000;
|
|
38
|
+
function errText(err) {
|
|
39
|
+
return err instanceof Error ? err.message : String(err);
|
|
40
|
+
}
|
|
41
|
+
export function heartbeatAgeSeconds(paths, now = () => new Date()) {
|
|
42
|
+
const last = readLastHeartbeat(paths);
|
|
43
|
+
if (!last)
|
|
44
|
+
return null;
|
|
45
|
+
const at = new Date(last.at).getTime();
|
|
46
|
+
if (Number.isNaN(at))
|
|
47
|
+
return null;
|
|
48
|
+
return Math.max(0, Math.round((now().getTime() - at) / 1000));
|
|
49
|
+
}
|
|
50
|
+
export async function runProbe(deps) {
|
|
51
|
+
const now = deps.now ?? (() => new Date());
|
|
52
|
+
const sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
53
|
+
const nonce = (deps.nonce ?? (() => randomBytes(6).toString("hex")))();
|
|
54
|
+
const self = deps.creds.entityUid;
|
|
55
|
+
const checks = [];
|
|
56
|
+
let token = null;
|
|
57
|
+
let companyUid = null;
|
|
58
|
+
// 1. whoami
|
|
59
|
+
try {
|
|
60
|
+
token = await deps.getToken();
|
|
61
|
+
const claims = peekIdToken(token);
|
|
62
|
+
const uid = claims["custom:entityUid"];
|
|
63
|
+
const type = claims["custom:entityType"];
|
|
64
|
+
if (type === "agent" && uid === self) {
|
|
65
|
+
checks.push({ name: "whoami", ok: true, detail: `${self} (external, ${deps.creds.companySlug})` });
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
checks.push({
|
|
69
|
+
name: "whoami",
|
|
70
|
+
ok: false,
|
|
71
|
+
detail: `token names ${String(type)}:${String(uid)}, expected agent:${self}`,
|
|
72
|
+
});
|
|
73
|
+
token = null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
checks.push({ name: "whoami", ok: false, detail: `mint failed: ${errText(err)}` });
|
|
78
|
+
}
|
|
79
|
+
if (token) {
|
|
80
|
+
try {
|
|
81
|
+
companyUid = await deps.resolveCompanyUid(token, deps.creds.companySlug);
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
checks.push({ name: "company", ok: false, detail: `cannot resolve ${deps.creds.companySlug}: ${errText(err)}` });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
// 2. team-sync
|
|
88
|
+
if (token) {
|
|
89
|
+
try {
|
|
90
|
+
const code = await deps.runPull(syncPullArgs(deps.hqRoot));
|
|
91
|
+
const companyDir = path.join(deps.hqRoot, "companies", deps.creds.companySlug);
|
|
92
|
+
const present = fs.existsSync(companyDir);
|
|
93
|
+
const ok = code === 0 && present;
|
|
94
|
+
checks.push({
|
|
95
|
+
name: "team-sync",
|
|
96
|
+
ok,
|
|
97
|
+
detail: ok
|
|
98
|
+
? `synced into ${companyDir}`
|
|
99
|
+
: code !== 0
|
|
100
|
+
? `hq sync pull exited ${code}`
|
|
101
|
+
: `sync exited 0 but ${companyDir} is missing`,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
catch (err) {
|
|
105
|
+
checks.push({ name: "team-sync", ok: false, detail: errText(err) });
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
checks.push({ name: "team-sync", ok: false, detail: "skipped: no token" });
|
|
110
|
+
}
|
|
111
|
+
// 3. mesh-presence
|
|
112
|
+
if (token && companyUid) {
|
|
113
|
+
try {
|
|
114
|
+
const vend = await deps.vendRealtime(token);
|
|
115
|
+
if (vend.actorUid !== self) {
|
|
116
|
+
checks.push({ name: "work-mesh", ok: false, detail: `realtime vend is for ${vend.actorUid}` });
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
const roster = await deps.listRoster(token, companyUid);
|
|
120
|
+
const row = roster.find((r) => r.uid === self);
|
|
121
|
+
const presence = typeof row?.presence === "string" ? row.presence : "unknown";
|
|
122
|
+
const last = typeof row?.lastHeartbeatAt === "string" ? row.lastHeartbeatAt : "never";
|
|
123
|
+
const ok = presence === "online" || presence === "stale";
|
|
124
|
+
checks.push({
|
|
125
|
+
name: "work-mesh",
|
|
126
|
+
ok,
|
|
127
|
+
detail: row
|
|
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",
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
catch (err) {
|
|
134
|
+
checks.push({ name: "work-mesh", ok: false, detail: errText(err) });
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
checks.push({ name: "work-mesh", ok: false, detail: "skipped: no token/company" });
|
|
139
|
+
}
|
|
140
|
+
// 4. dm-roundtrip
|
|
141
|
+
if (token) {
|
|
142
|
+
try {
|
|
143
|
+
const body = `hq agent probe ${nonce}`;
|
|
144
|
+
await deps.sendDm(token, self, body);
|
|
145
|
+
const deadline = now().getTime() + (deps.dmTimeoutMs ?? DM_ROUNDTRIP_TIMEOUT_MS);
|
|
146
|
+
let found = false;
|
|
147
|
+
for (;;) {
|
|
148
|
+
const msgs = await deps.readThread(token, self);
|
|
149
|
+
if (msgs.some((m) => typeof m.body === "string" && m.body.includes(nonce))) {
|
|
150
|
+
found = true;
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
if (now().getTime() >= deadline)
|
|
154
|
+
break;
|
|
155
|
+
await sleep(DM_ROUNDTRIP_POLL_MS);
|
|
156
|
+
}
|
|
157
|
+
checks.push({
|
|
158
|
+
name: "dm",
|
|
159
|
+
ok: found,
|
|
160
|
+
detail: found ? "sent to self via /v1/notify/dm and read back" : `sent but not visible in thread after ${Math.round((deps.dmTimeoutMs ?? DM_ROUNDTRIP_TIMEOUT_MS) / 1000)}s`,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
catch (err) {
|
|
164
|
+
checks.push({ name: "dm", ok: false, detail: errText(err) });
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
checks.push({ name: "dm", ok: false, detail: "skipped: no token" });
|
|
169
|
+
}
|
|
170
|
+
// 5. secrets-list
|
|
171
|
+
if (token && companyUid) {
|
|
172
|
+
try {
|
|
173
|
+
const status = await deps.listSecrets(token, companyUid);
|
|
174
|
+
checks.push({
|
|
175
|
+
name: "secrets",
|
|
176
|
+
ok: status === 200,
|
|
177
|
+
detail: status === 200 ? `GET /secrets/${companyUid} → 200` : `GET /secrets/${companyUid} → ${status}`,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
catch (err) {
|
|
181
|
+
checks.push({ name: "secrets", ok: false, detail: errText(err) });
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
checks.push({ name: "secrets", ok: false, detail: "skipped: no token/company" });
|
|
186
|
+
}
|
|
187
|
+
const result = {
|
|
188
|
+
passed: checks.every((c) => c.ok),
|
|
189
|
+
checks,
|
|
190
|
+
at: now().toISOString(),
|
|
191
|
+
heartbeatAgeSeconds: heartbeatAgeSeconds(deps.paths, now),
|
|
192
|
+
};
|
|
193
|
+
if (token) {
|
|
194
|
+
try {
|
|
195
|
+
const status = await deps.postResult(token, result);
|
|
196
|
+
if (status < 200 || status >= 300) {
|
|
197
|
+
result.checks.push({ name: "report", ok: false, detail: `probe-result POST → ${status}` });
|
|
198
|
+
result.passed = false;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
catch (err) {
|
|
202
|
+
result.checks.push({ name: "report", ok: false, detail: `probe-result POST failed: ${errText(err)}` });
|
|
203
|
+
result.passed = false;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return result;
|
|
207
|
+
}
|
|
208
|
+
export function formatHeartbeatAge(age) {
|
|
209
|
+
if (age === null)
|
|
210
|
+
return "never (kit heartbeat has not posted yet)";
|
|
211
|
+
if (age < 90)
|
|
212
|
+
return `${age}s ago`;
|
|
213
|
+
if (age < 3600)
|
|
214
|
+
return `${Math.round(age / 60)}m ago`;
|
|
215
|
+
return `${(age / 3600).toFixed(1)}h ago`;
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Recipe contract: `PASS <check> <detail>` / `FAIL <check> <detail>` per
|
|
219
|
+
* check, then `heartbeat <age>`. Two-space separators; no trailing verdict
|
|
220
|
+
* line (the exit code carries it, and the console keys only on the POSTed
|
|
221
|
+
* probe-result).
|
|
222
|
+
*/
|
|
223
|
+
export function formatProbeReport(result, color = true) {
|
|
224
|
+
const pass = color ? chalk.green("PASS") : "PASS";
|
|
225
|
+
const failTag = color ? chalk.red("FAIL") : "FAIL";
|
|
226
|
+
const lines = result.checks.map((c) => `${c.ok ? pass : failTag} ${c.name} ${c.detail}`);
|
|
227
|
+
lines.push(`heartbeat ${formatHeartbeatAge(result.heartbeatAgeSeconds)}`);
|
|
228
|
+
return lines;
|
|
229
|
+
}
|
|
230
|
+
export function defaultProbeDeps(paths, creds) {
|
|
231
|
+
process.env.HQ_MACHINE_CREDS_FILE = paths.machineCredsPath;
|
|
232
|
+
process.env.HQ_REQUIRE_MACHINE_IDENTITY = "1";
|
|
233
|
+
process.env.HQ_VAULT_API_URL = creds.apiBaseUrl;
|
|
234
|
+
const config = readKitConfig(paths, CLI_VERSION);
|
|
235
|
+
const base = creds.apiBaseUrl;
|
|
236
|
+
return {
|
|
237
|
+
paths,
|
|
238
|
+
creds,
|
|
239
|
+
hqRoot: config.hqRoot,
|
|
240
|
+
getToken: () => ensureCognitoToken({ tokenSource: "machine", interactive: false }),
|
|
241
|
+
resolveCompanyUid: (token, slug) => getCompanyUid(token, slug),
|
|
242
|
+
runPull: defaultRunPull(process.execPath, process.argv[1], process.env),
|
|
243
|
+
vendRealtime: async (token) => {
|
|
244
|
+
const bundle = await createContract3Fetcher({ token, baseUrl: base })();
|
|
245
|
+
return { actorUid: bundle.actorUid };
|
|
246
|
+
},
|
|
247
|
+
listRoster: async (token, companyUid) => (await listAgents(token, companyUid)),
|
|
248
|
+
sendDm: (token, to, body) => sendAgentDm(token, to, body),
|
|
249
|
+
readThread: async (token, withUid) => readAgentThread(token, withUid, 20),
|
|
250
|
+
listSecrets: async (token, companyUid) => {
|
|
251
|
+
const res = await vaultApiFetch({
|
|
252
|
+
token,
|
|
253
|
+
baseUrl: base,
|
|
254
|
+
path: `/secrets/${encodeURIComponent(companyUid)}`,
|
|
255
|
+
});
|
|
256
|
+
return res.status;
|
|
257
|
+
},
|
|
258
|
+
postResult: async (token, result) => {
|
|
259
|
+
const res = await vaultApiFetch({
|
|
260
|
+
token,
|
|
261
|
+
baseUrl: base,
|
|
262
|
+
path: `/v1/agents/${encodeURIComponent(creds.entityUid)}/probe-result`,
|
|
263
|
+
method: "POST",
|
|
264
|
+
body: { passed: result.passed, checks: result.checks, at: result.at },
|
|
265
|
+
});
|
|
266
|
+
return res.status;
|
|
267
|
+
},
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
export function registerAgentProbeCommand(agent) {
|
|
271
|
+
agent
|
|
272
|
+
.command("probe")
|
|
273
|
+
.description("Verify identity, vault sync, work-mesh presence, DMs and secrets; report to the console")
|
|
274
|
+
.option("--json", "Print machine-readable JSON")
|
|
275
|
+
.action(async (opts) => {
|
|
276
|
+
try {
|
|
277
|
+
const paths = agentKitPaths(os.homedir(), process.env);
|
|
278
|
+
const creds = requireExternalCreds(paths);
|
|
279
|
+
const result = await runProbe(defaultProbeDeps(paths, creds));
|
|
280
|
+
if (opts.json)
|
|
281
|
+
console.log(JSON.stringify(result, null, 2));
|
|
282
|
+
else
|
|
283
|
+
for (const line of formatProbeReport(result))
|
|
284
|
+
console.log(line);
|
|
285
|
+
if (!result.passed)
|
|
286
|
+
process.exit(1);
|
|
287
|
+
}
|
|
288
|
+
catch (err) {
|
|
289
|
+
console.error(chalk.red(err instanceof Error ? err.message : String(err)));
|
|
290
|
+
process.exit(1);
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
//# sourceMappingURL=agent-probe.js.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `hq agent …` — commands a host runs to BE an external HQ agent (as opposed
|
|
3
|
+
* to `hq agents …`, which an admin runs to manage a company's agents):
|
|
4
|
+
*
|
|
5
|
+
* hq agent enroll <code> redeem a one-time enrollment code
|
|
6
|
+
* hq agent kit install|status|uninstall|run <service>
|
|
7
|
+
* hq agent probe end-to-end membership check
|
|
8
|
+
* hq agent mcp stdio MCP server for the bot framework
|
|
9
|
+
*/
|
|
10
|
+
import { Command } from "commander";
|
|
11
|
+
export declare function registerAgentCommand(program: Command): void;
|
|
12
|
+
//# sourceMappingURL=agent.d.ts.map
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `hq agent …` — commands a host runs to BE an external HQ agent (as opposed
|
|
3
|
+
* to `hq agents …`, which an admin runs to manage a company's agents):
|
|
4
|
+
*
|
|
5
|
+
* hq agent enroll <code> redeem a one-time enrollment code
|
|
6
|
+
* hq agent kit install|status|uninstall|run <service>
|
|
7
|
+
* hq agent probe end-to-end membership check
|
|
8
|
+
* hq agent mcp stdio MCP server for the bot framework
|
|
9
|
+
*/
|
|
10
|
+
import { registerAgentEnrollCommand } from "./agent-enroll.js";
|
|
11
|
+
import { registerAgentKitCommand } from "./agent-kit.js";
|
|
12
|
+
import { registerAgentMcpCommand } from "./agent-mcp.js";
|
|
13
|
+
import { registerAgentProbeCommand } from "./agent-probe.js";
|
|
14
|
+
export function registerAgentCommand(program) {
|
|
15
|
+
const agent = program
|
|
16
|
+
.command("agent")
|
|
17
|
+
.description("Enroll and run this host as an external HQ agent");
|
|
18
|
+
registerAgentEnrollCommand(agent);
|
|
19
|
+
registerAgentKitCommand(agent);
|
|
20
|
+
registerAgentProbeCommand(agent);
|
|
21
|
+
registerAgentMcpCommand(agent);
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=agent.js.map
|
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
* hq agents start|stop <uid> — EC2 start/stop
|
|
16
16
|
* hq agents retry <uid> — resume setup from first non-done step
|
|
17
17
|
* hq agents rm <uid> --yes — deprovision (destructive; flag-guarded)
|
|
18
|
+
* hq agents rotate <uid> — new enrollment code for an external agent
|
|
19
|
+
* hq agents revoke <uid> --yes — revoke an external agent (--remove deletes it)
|
|
18
20
|
* hq agents jobs list <uid> — off-box job roster (schedule + rate)
|
|
19
21
|
* hq agents jobs pause <uid> <jobId> — flip schedule State=DISABLED
|
|
20
22
|
* hq agents jobs cancel <uid> <jobId> — delete schedule + drop the job
|
|
@@ -350,5 +352,30 @@ export declare function sendAgentDm(token: string, agentUid: string, message: st
|
|
|
350
352
|
export declare function readAgentThread(token: string, agentUid: string, limit?: number): Promise<DmThreadMessage[]>;
|
|
351
353
|
/** Render one thread message as a labeled line for the terminal. */
|
|
352
354
|
export declare function formatThreadMessage(m: DmThreadMessage, agentLabel: string): string;
|
|
355
|
+
export interface EnrollmentIssue {
|
|
356
|
+
code: string;
|
|
357
|
+
expiresAt: string;
|
|
358
|
+
enrollmentId: string;
|
|
359
|
+
}
|
|
360
|
+
export interface RotateAgentResult {
|
|
361
|
+
agentUid: string;
|
|
362
|
+
enrollment: EnrollmentIssue;
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* POST /v1/agents/{uid}/rotate — invalidate the current secret and host key
|
|
366
|
+
* and issue a fresh one-time enrollment code (returned exactly once).
|
|
367
|
+
*/
|
|
368
|
+
export declare function rotateAgent(token: string, agentUid: string): Promise<RotateAgentResult>;
|
|
369
|
+
/** Display form of a 24-char code: six groups of four. */
|
|
370
|
+
export declare function groupEnrollmentCode(code: string): string;
|
|
371
|
+
/**
|
|
372
|
+
* POST /v1/agents/{uid}/revoke — disable the Cognito user, drop membership,
|
|
373
|
+
* mark the enrollment revoked; `remove` also deletes the entity.
|
|
374
|
+
*/
|
|
375
|
+
export declare function revokeAgent(token: string, agentUid: string, remove: boolean): Promise<{
|
|
376
|
+
removed: boolean;
|
|
377
|
+
}>;
|
|
378
|
+
/** Lines printed after a rotate: the code is shown once and never stored. */
|
|
379
|
+
export declare function formatRotateOutput(result: RotateAgentResult): string[];
|
|
353
380
|
export declare function registerAgentsCommand(program: Command): void;
|
|
354
381
|
//# sourceMappingURL=agents.d.ts.map
|