@indigoai-us/hq-cli 5.115.6 → 5.116.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 +72 -11
- package/dist/command-catalog.generated.d.ts +162 -2
- package/dist/command-catalog.generated.js +205 -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/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/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
package/dist/commands/agents.js
CHANGED
|
@@ -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
|
|
@@ -387,6 +389,143 @@ export async function getAgentStatus(token, agentUid) {
|
|
|
387
389
|
path: `/v1/agents/${encodeURIComponent(agentUid)}/status`,
|
|
388
390
|
});
|
|
389
391
|
}
|
|
392
|
+
function recordValue(value) {
|
|
393
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
394
|
+
? value
|
|
395
|
+
: null;
|
|
396
|
+
}
|
|
397
|
+
function nonEmptyString(value) {
|
|
398
|
+
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
399
|
+
}
|
|
400
|
+
function safeActionUrl(value) {
|
|
401
|
+
const raw = nonEmptyString(value);
|
|
402
|
+
if (!raw)
|
|
403
|
+
return null;
|
|
404
|
+
try {
|
|
405
|
+
const url = new URL(raw);
|
|
406
|
+
return url.protocol === "https:" && !url.username && !url.password ? raw : null;
|
|
407
|
+
}
|
|
408
|
+
catch {
|
|
409
|
+
return null;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
function signInProviderName(url) {
|
|
413
|
+
const parsed = new URL(url);
|
|
414
|
+
if (parsed.hostname === "auth.openai.com" &&
|
|
415
|
+
parsed.pathname.startsWith("/codex/")) {
|
|
416
|
+
return "Codex";
|
|
417
|
+
}
|
|
418
|
+
if (parsed.hostname === "accounts.x.ai")
|
|
419
|
+
return "Grok";
|
|
420
|
+
if (parsed.hostname.endsWith("anthropic.com"))
|
|
421
|
+
return "Claude";
|
|
422
|
+
return "your provider";
|
|
423
|
+
}
|
|
424
|
+
function currentDeviceSignInAction(status) {
|
|
425
|
+
const pairing = recordValue(status.pairing);
|
|
426
|
+
const url = safeActionUrl(pairing?.url);
|
|
427
|
+
if (!url)
|
|
428
|
+
return null;
|
|
429
|
+
const rawCode = nonEmptyString(pairing?.code);
|
|
430
|
+
const code = rawCode && /^[A-Z0-9]{4,8}-[A-Z0-9]{4,8}$/i.test(rawCode)
|
|
431
|
+
? rawCode
|
|
432
|
+
: undefined;
|
|
433
|
+
const provider = signInProviderName(url);
|
|
434
|
+
return {
|
|
435
|
+
type: provider === "Codex" ? "codex-device-sign-in" : "provider-sign-in",
|
|
436
|
+
title: `Sign in to ${provider}`,
|
|
437
|
+
summary: `sign in to ${provider}`,
|
|
438
|
+
instruction: code
|
|
439
|
+
? "Open this page and enter the code."
|
|
440
|
+
: "Open this page and follow the sign-in instructions.",
|
|
441
|
+
url,
|
|
442
|
+
urlLabel: "Open",
|
|
443
|
+
code,
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
function pendingSlackAction(status) {
|
|
447
|
+
const agent = recordValue(status.agent);
|
|
448
|
+
const diagnostics = recordValue(agent?.channelDiagnostics);
|
|
449
|
+
const slack = recordValue(diagnostics?.slack);
|
|
450
|
+
const url = safeActionUrl(slack?.pendingInstallUrl);
|
|
451
|
+
if (!url)
|
|
452
|
+
return null;
|
|
453
|
+
if (slack?.inboundCapability === "socket-mode-degraded") {
|
|
454
|
+
return {
|
|
455
|
+
type: "slack-app-token",
|
|
456
|
+
title: "Finish setting up the Slack app",
|
|
457
|
+
summary: "finish setting up the Slack app",
|
|
458
|
+
instruction: "Open this page, create the app-level token, then provide it in the agent's Slack setup.",
|
|
459
|
+
url,
|
|
460
|
+
urlLabel: "Open",
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
if (slack?.inboundCapability !== "pending-install")
|
|
464
|
+
return null;
|
|
465
|
+
return {
|
|
466
|
+
type: "slack-install",
|
|
467
|
+
title: "Install the Slack app",
|
|
468
|
+
summary: "install the Slack app so it can talk to your workspace",
|
|
469
|
+
instruction: "Open this link and install the app in your workspace.",
|
|
470
|
+
url,
|
|
471
|
+
urlLabel: "Install",
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* The server owns action detection. This client converts its safe, current
|
|
476
|
+
* status fields into a list so another operator action can be added without
|
|
477
|
+
* changing the renderers or command control flow.
|
|
478
|
+
*/
|
|
479
|
+
function pendingOperatorActions(status) {
|
|
480
|
+
return [
|
|
481
|
+
currentDeviceSignInAction(status),
|
|
482
|
+
pendingSlackAction(status),
|
|
483
|
+
].filter((action) => action !== null);
|
|
484
|
+
}
|
|
485
|
+
function pendingActionsJson(actions) {
|
|
486
|
+
return actions.map(({ type, title, instruction, url, code }) => ({
|
|
487
|
+
type,
|
|
488
|
+
title,
|
|
489
|
+
instruction,
|
|
490
|
+
...(url ? { url } : {}),
|
|
491
|
+
...(code ? { code } : {}),
|
|
492
|
+
}));
|
|
493
|
+
}
|
|
494
|
+
function statusAgentSlug(status) {
|
|
495
|
+
return nonEmptyString(recordValue(status.agent)?.slug);
|
|
496
|
+
}
|
|
497
|
+
/** The provision endpoint returns this nested identity; old response shapes omit it. */
|
|
498
|
+
function provisionedAgentIdentity(result) {
|
|
499
|
+
const agent = recordValue(result.agent);
|
|
500
|
+
const uid = nonEmptyString(agent?.uid) ?? nonEmptyString(agent?.agentUid);
|
|
501
|
+
const slug = nonEmptyString(agent?.slug);
|
|
502
|
+
return uid && slug ? { uid, slug } : null;
|
|
503
|
+
}
|
|
504
|
+
function printPendingOperatorActions(slug, actions) {
|
|
505
|
+
if (actions.length === 0)
|
|
506
|
+
return;
|
|
507
|
+
const first = actions[0];
|
|
508
|
+
console.log(chalk.yellow(actions.length === 1
|
|
509
|
+
? `Your agent's box is up. One step left: ${first.summary}.`
|
|
510
|
+
: "Your agent's box is up. A few steps remain before it can talk to your workspace."));
|
|
511
|
+
for (const action of actions) {
|
|
512
|
+
console.log("");
|
|
513
|
+
console.log(chalk.bold(`${action.title}:`));
|
|
514
|
+
console.log(action.instruction);
|
|
515
|
+
if (action.url)
|
|
516
|
+
console.log(chalk.cyan(`${action.urlLabel ?? "Open"}: ${action.url}`));
|
|
517
|
+
if (action.code)
|
|
518
|
+
console.log(chalk.bold(`Code: ${action.code}`));
|
|
519
|
+
}
|
|
520
|
+
const currentCodeHint = actions.some((action) => action.code)
|
|
521
|
+
? "current sign-in code and instructions"
|
|
522
|
+
: "these instructions";
|
|
523
|
+
const statusCommand = slug
|
|
524
|
+
? `Run \`hq agents status ${slug}\` any time to get ${currentCodeHint} again.`
|
|
525
|
+
: `Run \`hq agents status\` again any time to get ${currentCodeHint}.`;
|
|
526
|
+
console.log("");
|
|
527
|
+
console.log(chalk.dim(`Until that's done, the agent will show as still setting up. ${statusCommand}`));
|
|
528
|
+
}
|
|
390
529
|
export async function patchAgentProfile(token, agentUid, patch) {
|
|
391
530
|
return agentsRequest({
|
|
392
531
|
token,
|
|
@@ -656,6 +795,59 @@ function shortTime(iso) {
|
|
|
656
795
|
function sleep(ms) {
|
|
657
796
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
658
797
|
}
|
|
798
|
+
/**
|
|
799
|
+
* POST /v1/agents/{uid}/rotate — invalidate the current secret and host key
|
|
800
|
+
* and issue a fresh one-time enrollment code (returned exactly once).
|
|
801
|
+
*/
|
|
802
|
+
export async function rotateAgent(token, agentUid) {
|
|
803
|
+
const data = await agentsRequest({
|
|
804
|
+
token,
|
|
805
|
+
path: `/v1/agents/${encodeURIComponent(agentUid)}/rotate`,
|
|
806
|
+
method: "POST",
|
|
807
|
+
});
|
|
808
|
+
const e = data.enrollment ?? {};
|
|
809
|
+
if (typeof e.code !== "string" || !e.code) {
|
|
810
|
+
throw new AgentsHttpError(502, "rotate succeeded but the response carried no enrollment code");
|
|
811
|
+
}
|
|
812
|
+
return {
|
|
813
|
+
agentUid: data.agentUid ?? agentUid,
|
|
814
|
+
enrollment: {
|
|
815
|
+
code: e.code,
|
|
816
|
+
expiresAt: typeof e.expiresAt === "string" ? e.expiresAt : "",
|
|
817
|
+
enrollmentId: typeof e.enrollmentId === "string" ? e.enrollmentId : "",
|
|
818
|
+
},
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
/** Display form of a 24-char code: six groups of four. */
|
|
822
|
+
export function groupEnrollmentCode(code) {
|
|
823
|
+
const flat = code.replace(/[\s-]+/g, "").toUpperCase();
|
|
824
|
+
return flat.match(/.{1,4}/g)?.join("-") ?? flat;
|
|
825
|
+
}
|
|
826
|
+
/**
|
|
827
|
+
* POST /v1/agents/{uid}/revoke — disable the Cognito user, drop membership,
|
|
828
|
+
* mark the enrollment revoked; `remove` also deletes the entity.
|
|
829
|
+
*/
|
|
830
|
+
export async function revokeAgent(token, agentUid, remove) {
|
|
831
|
+
const data = await agentsRequest({
|
|
832
|
+
token,
|
|
833
|
+
path: `/v1/agents/${encodeURIComponent(agentUid)}/revoke`,
|
|
834
|
+
method: "POST",
|
|
835
|
+
body: { remove },
|
|
836
|
+
});
|
|
837
|
+
return { removed: data.removed === true || remove };
|
|
838
|
+
}
|
|
839
|
+
/** Lines printed after a rotate: the code is shown once and never stored. */
|
|
840
|
+
export function formatRotateOutput(result) {
|
|
841
|
+
const lines = [
|
|
842
|
+
`Rotated ${result.agentUid}. The old secret and host key no longer mint.`,
|
|
843
|
+
"",
|
|
844
|
+
` enrollment code: ${groupEnrollmentCode(result.enrollment.code)}`,
|
|
845
|
+
];
|
|
846
|
+
if (result.enrollment.expiresAt)
|
|
847
|
+
lines.push(` expires: ${result.enrollment.expiresAt} (15 minutes)`);
|
|
848
|
+
lines.push("", "Shown once. On the agent's host run:", ` hq agent enroll ${groupEnrollmentCode(result.enrollment.code)} --replace`, " hq agent probe");
|
|
849
|
+
return lines;
|
|
850
|
+
}
|
|
659
851
|
// ---------------------------------------------------------------------------
|
|
660
852
|
// Command registration
|
|
661
853
|
// ---------------------------------------------------------------------------
|
|
@@ -711,6 +903,11 @@ export function registerAgentsCommand(program) {
|
|
|
711
903
|
// Interactive terminals are for humans: hq-pro serves no keyed
|
|
712
904
|
// `/v1/keys/agents/{uid}/terminal` route, so machine keys fail closed.
|
|
713
905
|
terminal: { capability: "agents:use", routeAvailable: false },
|
|
906
|
+
// External-agent lifecycle is an admin action on the JWT-only
|
|
907
|
+
// /v1/agents/{uid}/rotate|revoke routes (api-contract v1); no keyed
|
|
908
|
+
// parallel exists, so machine keys fail closed here too.
|
|
909
|
+
rotate: { capability: "agents:use", routeAvailable: false },
|
|
910
|
+
revoke: { capability: "agents:use", routeAvailable: false },
|
|
714
911
|
},
|
|
715
912
|
});
|
|
716
913
|
// `--company` may live on the group or the subcommand; the subcommand value
|
|
@@ -955,13 +1152,34 @@ export function registerAgentsCommand(program) {
|
|
|
955
1152
|
quoteCatalogVersion: createOptions.catalogVersion,
|
|
956
1153
|
surface: CLI_AGENT_CREATE_SURFACE,
|
|
957
1154
|
});
|
|
958
|
-
const
|
|
1155
|
+
const identity = provisionedAgentIdentity(result);
|
|
1156
|
+
const status = identity
|
|
1157
|
+
? await getAgentStatus(token, identity.uid)
|
|
1158
|
+
: null;
|
|
1159
|
+
const actions = status ? pendingOperatorActions(status) : [];
|
|
1160
|
+
const slackInstall = actions.find((action) => action.type === "slack-install");
|
|
959
1161
|
if (opts.json) {
|
|
960
|
-
console.log(JSON.stringify(
|
|
1162
|
+
console.log(JSON.stringify({
|
|
1163
|
+
...result,
|
|
1164
|
+
...(actions.length > 0
|
|
1165
|
+
? { pendingActions: pendingActionsJson(actions) }
|
|
1166
|
+
: {}),
|
|
1167
|
+
...(slackInstall?.url
|
|
1168
|
+
? {
|
|
1169
|
+
slackInstallPending: true,
|
|
1170
|
+
slackInstallUrl: slackInstall.url,
|
|
1171
|
+
}
|
|
1172
|
+
: {}),
|
|
1173
|
+
}));
|
|
961
1174
|
}
|
|
962
1175
|
else {
|
|
963
1176
|
console.log(chalk.green(`Provisioning started for agent "${name}".`));
|
|
964
|
-
|
|
1177
|
+
if (actions.length > 0) {
|
|
1178
|
+
printPendingOperatorActions(identity?.slug ?? null, actions);
|
|
1179
|
+
}
|
|
1180
|
+
else {
|
|
1181
|
+
console.log(chalk.dim(`Track setup: hq agents status ${identity?.slug ?? slug} --company <slug>`));
|
|
1182
|
+
}
|
|
965
1183
|
}
|
|
966
1184
|
}
|
|
967
1185
|
catch (err) {
|
|
@@ -1050,16 +1268,28 @@ export function registerAgentsCommand(program) {
|
|
|
1050
1268
|
}
|
|
1051
1269
|
});
|
|
1052
1270
|
agents
|
|
1053
|
-
.command("status <
|
|
1271
|
+
.command("status <agent>")
|
|
1054
1272
|
.description("Show an agent's setup state and runtime detail")
|
|
1055
1273
|
.option("--company <slug>", "Company slug (resolves to companyUid)")
|
|
1056
1274
|
.option("--json", "Emit raw JSON")
|
|
1057
|
-
.action(async function (
|
|
1275
|
+
.action(async function (agent, opts) {
|
|
1058
1276
|
try {
|
|
1059
1277
|
const token = (await resolveVaultCredential()).token;
|
|
1278
|
+
const agentUid = await resolveAgentUid(token, agent, companyOf(this));
|
|
1060
1279
|
const status = await getAgentStatus(token, agentUid);
|
|
1280
|
+
const actions = pendingOperatorActions(status);
|
|
1061
1281
|
if (opts.json) {
|
|
1062
|
-
process.stdout.write(JSON.stringify(
|
|
1282
|
+
process.stdout.write(JSON.stringify({
|
|
1283
|
+
...status,
|
|
1284
|
+
...(actions.length > 0
|
|
1285
|
+
? { pendingActions: pendingActionsJson(actions) }
|
|
1286
|
+
: {}),
|
|
1287
|
+
}, null, 2) + "\n");
|
|
1288
|
+
return;
|
|
1289
|
+
}
|
|
1290
|
+
if (actions.length > 0) {
|
|
1291
|
+
printPendingOperatorActions(statusAgentSlug(status) ??
|
|
1292
|
+
(AGENT_UID_PATTERN.test(agent.trim()) ? null : agent.trim()), actions);
|
|
1063
1293
|
return;
|
|
1064
1294
|
}
|
|
1065
1295
|
// Pretty but faithful: dump the top-level fields as key: value.
|
|
@@ -1268,6 +1498,50 @@ export function registerAgentsCommand(program) {
|
|
|
1268
1498
|
fail(err);
|
|
1269
1499
|
}
|
|
1270
1500
|
});
|
|
1501
|
+
agents
|
|
1502
|
+
.command("rotate <agentUid>")
|
|
1503
|
+
.description("Issue a fresh enrollment code for an external agent (old secret + host key stop minting)")
|
|
1504
|
+
.option("--company <slug>", "Company slug (resolves to companyUid)")
|
|
1505
|
+
.option("--json", "Emit raw JSON (code included — pipe carefully)")
|
|
1506
|
+
.action(async function (agentUid, opts) {
|
|
1507
|
+
try {
|
|
1508
|
+
const token = (await resolveVaultCredential()).token;
|
|
1509
|
+
const result = await rotateAgent(token, agentUid);
|
|
1510
|
+
if (opts.json) {
|
|
1511
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1512
|
+
return;
|
|
1513
|
+
}
|
|
1514
|
+
for (const line of formatRotateOutput(result))
|
|
1515
|
+
console.log(line);
|
|
1516
|
+
}
|
|
1517
|
+
catch (err) {
|
|
1518
|
+
fail(err);
|
|
1519
|
+
}
|
|
1520
|
+
});
|
|
1521
|
+
agents
|
|
1522
|
+
.command("revoke <agentUid>")
|
|
1523
|
+
.description("Revoke an external agent: disable its login and drop it from the team")
|
|
1524
|
+
.option("--company <slug>", "Company slug (resolves to companyUid)")
|
|
1525
|
+
.option("--remove", "Also delete the agent entity (it disappears from the roster)")
|
|
1526
|
+
.option("--yes", "Confirm the revoke (required)")
|
|
1527
|
+
.action(async function (agentUid, opts) {
|
|
1528
|
+
if (!opts.yes) {
|
|
1529
|
+
console.error(chalk.yellow(`This will revoke agent ${agentUid}: its credentials stop working immediately and it ` +
|
|
1530
|
+
`leaves the team${opts.remove ? ", and the agent record is deleted" : ""}. ` +
|
|
1531
|
+
`Re-run with --yes to confirm: hq agents revoke ${agentUid}${opts.remove ? " --remove" : ""} --yes`));
|
|
1532
|
+
process.exit(1);
|
|
1533
|
+
}
|
|
1534
|
+
try {
|
|
1535
|
+
const token = (await resolveVaultCredential()).token;
|
|
1536
|
+
const result = await revokeAgent(token, agentUid, opts.remove === true);
|
|
1537
|
+
console.log(chalk.green(result.removed
|
|
1538
|
+
? `Revoked and removed agent ${agentUid}.`
|
|
1539
|
+
: `Revoked agent ${agentUid}. Its next mint will be refused (401).`));
|
|
1540
|
+
}
|
|
1541
|
+
catch (err) {
|
|
1542
|
+
fail(err);
|
|
1543
|
+
}
|
|
1544
|
+
});
|
|
1271
1545
|
// Off-box job control (US-003). Nested group so `hq agents jobs --help`
|
|
1272
1546
|
// lists list|pause|cancel. Same vaultApiFetch + person JWT as the rest of
|
|
1273
1547
|
// this file; keyed HQ_API_KEY rewrites /v1/agents → /v1/keys/agents.
|
package/dist/commands/secrets.js
CHANGED
|
@@ -43,7 +43,19 @@ export const CLI_SECRETS_SET_SURFACE = "cli_secrets_set";
|
|
|
43
43
|
*/
|
|
44
44
|
async function requireCognitoTokenForSecrets(commandLabel, capability = null) {
|
|
45
45
|
requireApiKeyCapability(capability, commandLabel, { routeAvailable: false });
|
|
46
|
-
return ensureCognitoToken();
|
|
46
|
+
return ensureCognitoToken(secretsCredentialOptions());
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* A browser login is safe only when a person can answer it. This mirrors the
|
|
50
|
+
* repository's existing terminal convention in `canOfferTeamUpgrade`: both
|
|
51
|
+
* stdin and stderr must be TTYs, and CI is always non-interactive.
|
|
52
|
+
*/
|
|
53
|
+
function secretsCredentialOptions() {
|
|
54
|
+
return {
|
|
55
|
+
interactive: process.stdin.isTTY === true &&
|
|
56
|
+
process.stderr.isTTY === true &&
|
|
57
|
+
!process.env.CI,
|
|
58
|
+
};
|
|
47
59
|
}
|
|
48
60
|
function scopeOpts(opts) {
|
|
49
61
|
if (opts.personal && opts.company) {
|
|
@@ -96,7 +108,7 @@ function resolveApiKeyCompanyUid(scope, commandLabel) {
|
|
|
96
108
|
return ref;
|
|
97
109
|
}
|
|
98
110
|
async function resolveSecretsTarget(capability, commandLabel, scopeOptions) {
|
|
99
|
-
const cred = await resolveVaultCredentialForCapability(capability, commandLabel);
|
|
111
|
+
const cred = await resolveVaultCredentialForCapability(capability, commandLabel, secretsCredentialOptions());
|
|
100
112
|
const scope = scopeOpts(scopeOptions);
|
|
101
113
|
const isApiKey = cred.kind === "api-key";
|
|
102
114
|
const companyUid = isApiKey
|
|
@@ -883,7 +895,7 @@ export function registerSecretsCommand(program) {
|
|
|
883
895
|
.option("--reveal", "Include the decrypted secret value")
|
|
884
896
|
.action(async (name, opts) => {
|
|
885
897
|
try {
|
|
886
|
-
const cred = await resolveVaultCredentialForCapability("secrets:read", "secrets get");
|
|
898
|
+
const cred = await resolveVaultCredentialForCapability("secrets:read", "secrets get", secretsCredentialOptions());
|
|
887
899
|
if (cred.kind === "api-key") {
|
|
888
900
|
if (!opts.reveal) {
|
|
889
901
|
console.error(chalk.red("HQ_API_KEY is set; `hq secrets get` without --reveal is not supported for API keys. " +
|
|
@@ -1434,7 +1446,7 @@ export function registerSecretsCommand(program) {
|
|
|
1434
1446
|
process.exit(1);
|
|
1435
1447
|
}
|
|
1436
1448
|
const keys = parseSecretNameList(_opts.only);
|
|
1437
|
-
const cred = await resolveVaultCredentialForCapability("secrets:read", "secrets exec");
|
|
1449
|
+
const cred = await resolveVaultCredentialForCapability("secrets:read", "secrets exec", secretsCredentialOptions());
|
|
1438
1450
|
const companyUid = cred.kind === "api-key"
|
|
1439
1451
|
? "__api_key__"
|
|
1440
1452
|
: await getEntityUid(cred.token, scopeOpts(secrets.opts()));
|
|
@@ -1485,7 +1497,7 @@ export function registerSecretsCommand(program) {
|
|
|
1485
1497
|
console.error(chalk.yellow("stdout is a terminal — values redacted. Use: source <(hq secrets env --only KEY1,KEY2)"));
|
|
1486
1498
|
}
|
|
1487
1499
|
const keys = parseSecretNameList(opts.only);
|
|
1488
|
-
const cred = await resolveVaultCredentialForCapability("secrets:read", "secrets env");
|
|
1500
|
+
const cred = await resolveVaultCredentialForCapability("secrets:read", "secrets env", secretsCredentialOptions());
|
|
1489
1501
|
const companyUid = cred.kind === "api-key"
|
|
1490
1502
|
? "__api_key__"
|
|
1491
1503
|
: await getEntityUid(cred.token, scopeOpts(secrets.opts()));
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host key + machine-creds file handling for external agents.
|
|
3
|
+
*
|
|
4
|
+
* The private key and the creds file are the whole credential: a stolen
|
|
5
|
+
* creds file without the key mints nothing (hq-cloud signs every mint with
|
|
6
|
+
* the key), so both are written 0600 and never printed. Nothing in this
|
|
7
|
+
* module reads `~/.hq/cognito-tokens.json` — a human's session is never
|
|
8
|
+
* consulted, copied, or reused by an agent identity (policy
|
|
9
|
+
* never-extract-stored-session-tokens-to-forge-privileged-calls).
|
|
10
|
+
*/
|
|
11
|
+
import type { AgentKitPaths, KitComponent } from "./paths.js";
|
|
12
|
+
/** Exact `machine-creds.json` shape for an external agent (api-contract v1). */
|
|
13
|
+
export interface ExternalMachineCreds {
|
|
14
|
+
username: string;
|
|
15
|
+
secret: string;
|
|
16
|
+
/** Cognito user pool the machine user lives in (informational for the host). */
|
|
17
|
+
userPoolId?: string;
|
|
18
|
+
clientId: string;
|
|
19
|
+
region: string;
|
|
20
|
+
entityType: "agent";
|
|
21
|
+
entityUid: string;
|
|
22
|
+
runtime: "external";
|
|
23
|
+
hostKeyPath: string;
|
|
24
|
+
companySlug: string;
|
|
25
|
+
apiBaseUrl: string;
|
|
26
|
+
}
|
|
27
|
+
export interface HostKeyPair {
|
|
28
|
+
privatePem: string;
|
|
29
|
+
publicPem: string;
|
|
30
|
+
/** SHA-256 of the DER (SPKI) public key, hex, first 16 chars. */
|
|
31
|
+
fingerprint: string;
|
|
32
|
+
}
|
|
33
|
+
/** Contract fingerprint: sha256(DER SPKI public key) hex, first 16 chars. */
|
|
34
|
+
export declare function hostFingerprint(publicPem: string): string;
|
|
35
|
+
export declare function generateHostKeyPair(): HostKeyPair;
|
|
36
|
+
export declare function writeHostKeyPair(paths: Pick<AgentKitPaths, "hostKeyPath" | "hostKeyPubPath">, pair: HostKeyPair): void;
|
|
37
|
+
export declare function writeMachineCreds(paths: Pick<AgentKitPaths, "machineCredsPath">, creds: ExternalMachineCreds): void;
|
|
38
|
+
/**
|
|
39
|
+
* Read the creds file without trusting it: returns null unless it is an
|
|
40
|
+
* external-agent file with every contract field present.
|
|
41
|
+
*/
|
|
42
|
+
export declare function readExternalMachineCreds(paths: Pick<AgentKitPaths, "machineCredsPath">): ExternalMachineCreds | null;
|
|
43
|
+
/** Whether ANY machine-creds file exists (external or hosted) at the kit path. */
|
|
44
|
+
export declare function machineCredsFileExists(paths: Pick<AgentKitPaths, "machineCredsPath">): boolean;
|
|
45
|
+
/**
|
|
46
|
+
* Component health stamp the kit services write and the heartbeat reads:
|
|
47
|
+
* `<status> <iso8601>` — same line shape as the hosted box's component files.
|
|
48
|
+
*/
|
|
49
|
+
export type ComponentStatus = "ok" | "error";
|
|
50
|
+
export declare function writeComponentStatus(paths: Pick<AgentKitPaths, "stateDir">, component: KitComponent, status: ComponentStatus, now?: () => Date): void;
|
|
51
|
+
export interface ComponentReading {
|
|
52
|
+
status: ComponentStatus;
|
|
53
|
+
at: Date | null;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Read a component stamp. A missing file, an unparseable line, or a stamp
|
|
57
|
+
* older than `staleAfterMs` reads as `error` — silence is never health.
|
|
58
|
+
*/
|
|
59
|
+
export declare function readComponentStatus(paths: Pick<AgentKitPaths, "stateDir">, component: KitComponent, staleAfterMs: number, now?: () => Date): ComponentReading;
|
|
60
|
+
//# sourceMappingURL=creds.d.ts.map
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host key + machine-creds file handling for external agents.
|
|
3
|
+
*
|
|
4
|
+
* The private key and the creds file are the whole credential: a stolen
|
|
5
|
+
* creds file without the key mints nothing (hq-cloud signs every mint with
|
|
6
|
+
* the key), so both are written 0600 and never printed. Nothing in this
|
|
7
|
+
* module reads `~/.hq/cognito-tokens.json` — a human's session is never
|
|
8
|
+
* consulted, copied, or reused by an agent identity (policy
|
|
9
|
+
* never-extract-stored-session-tokens-to-forge-privileged-calls).
|
|
10
|
+
*/
|
|
11
|
+
import { createHash, createPublicKey, generateKeyPairSync } from "node:crypto";
|
|
12
|
+
import * as fs from "node:fs";
|
|
13
|
+
import * as path from "node:path";
|
|
14
|
+
import { componentStatePath } from "./paths.js";
|
|
15
|
+
/** Contract fingerprint: sha256(DER SPKI public key) hex, first 16 chars. */
|
|
16
|
+
export function hostFingerprint(publicPem) {
|
|
17
|
+
const der = createPublicKey(publicPem).export({ type: "spki", format: "der" });
|
|
18
|
+
return createHash("sha256").update(der).digest("hex").slice(0, 16);
|
|
19
|
+
}
|
|
20
|
+
export function generateHostKeyPair() {
|
|
21
|
+
const { privateKey, publicKey } = generateKeyPairSync("ed25519");
|
|
22
|
+
const privatePem = privateKey.export({ type: "pkcs8", format: "pem" }).toString();
|
|
23
|
+
const publicPem = publicKey.export({ type: "spki", format: "pem" }).toString();
|
|
24
|
+
return { privatePem, publicPem, fingerprint: hostFingerprint(publicPem) };
|
|
25
|
+
}
|
|
26
|
+
/** Write a file atomically with the given mode, replacing any existing one. */
|
|
27
|
+
function writePrivateFile(dest, contents, mode) {
|
|
28
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true, mode: 0o700 });
|
|
29
|
+
const tmp = `${dest}.tmp.${process.pid}`;
|
|
30
|
+
fs.writeFileSync(tmp, contents, { mode });
|
|
31
|
+
fs.chmodSync(tmp, mode);
|
|
32
|
+
fs.renameSync(tmp, dest);
|
|
33
|
+
}
|
|
34
|
+
export function writeHostKeyPair(paths, pair) {
|
|
35
|
+
writePrivateFile(paths.hostKeyPath, pair.privatePem, 0o600);
|
|
36
|
+
writePrivateFile(paths.hostKeyPubPath, pair.publicPem, 0o644);
|
|
37
|
+
}
|
|
38
|
+
export function writeMachineCreds(paths, creds) {
|
|
39
|
+
writePrivateFile(paths.machineCredsPath, `${JSON.stringify(creds, null, 2)}\n`, 0o600);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Read the creds file without trusting it: returns null unless it is an
|
|
43
|
+
* external-agent file with every contract field present.
|
|
44
|
+
*/
|
|
45
|
+
export function readExternalMachineCreds(paths) {
|
|
46
|
+
let raw;
|
|
47
|
+
try {
|
|
48
|
+
raw = JSON.parse(fs.readFileSync(paths.machineCredsPath, "utf8"));
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
if (!raw || typeof raw !== "object")
|
|
54
|
+
return null;
|
|
55
|
+
const r = raw;
|
|
56
|
+
const str = (k) => typeof r[k] === "string" && r[k].length > 0 ? r[k] : null;
|
|
57
|
+
const username = str("username");
|
|
58
|
+
const secret = str("secret");
|
|
59
|
+
const clientId = str("clientId");
|
|
60
|
+
const region = str("region");
|
|
61
|
+
const entityUid = str("entityUid");
|
|
62
|
+
const userPoolId = str("userPoolId");
|
|
63
|
+
const hostKeyPath = str("hostKeyPath");
|
|
64
|
+
const companySlug = str("companySlug");
|
|
65
|
+
const apiBaseUrl = str("apiBaseUrl");
|
|
66
|
+
if (r.runtime !== "external" ||
|
|
67
|
+
r.entityType !== "agent" ||
|
|
68
|
+
!username ||
|
|
69
|
+
!secret ||
|
|
70
|
+
!clientId ||
|
|
71
|
+
!region ||
|
|
72
|
+
!entityUid ||
|
|
73
|
+
!entityUid.startsWith("agt_") ||
|
|
74
|
+
!hostKeyPath ||
|
|
75
|
+
!companySlug ||
|
|
76
|
+
!apiBaseUrl) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
username,
|
|
81
|
+
secret,
|
|
82
|
+
...(userPoolId ? { userPoolId } : {}),
|
|
83
|
+
clientId,
|
|
84
|
+
region,
|
|
85
|
+
entityType: "agent",
|
|
86
|
+
entityUid,
|
|
87
|
+
runtime: "external",
|
|
88
|
+
hostKeyPath,
|
|
89
|
+
companySlug,
|
|
90
|
+
apiBaseUrl,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
/** Whether ANY machine-creds file exists (external or hosted) at the kit path. */
|
|
94
|
+
export function machineCredsFileExists(paths) {
|
|
95
|
+
return fs.existsSync(paths.machineCredsPath);
|
|
96
|
+
}
|
|
97
|
+
export function writeComponentStatus(paths, component, status, now = () => new Date()) {
|
|
98
|
+
fs.mkdirSync(paths.stateDir, { recursive: true, mode: 0o700 });
|
|
99
|
+
fs.writeFileSync(componentStatePath(paths, component), `${status} ${now().toISOString()}\n`, { mode: 0o600 });
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Read a component stamp. A missing file, an unparseable line, or a stamp
|
|
103
|
+
* older than `staleAfterMs` reads as `error` — silence is never health.
|
|
104
|
+
*/
|
|
105
|
+
export function readComponentStatus(paths, component, staleAfterMs, now = () => new Date()) {
|
|
106
|
+
let line;
|
|
107
|
+
try {
|
|
108
|
+
line = fs.readFileSync(componentStatePath(paths, component), "utf8").trim();
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return { status: "error", at: null };
|
|
112
|
+
}
|
|
113
|
+
const [status, iso] = line.split(/\s+/, 2);
|
|
114
|
+
const at = iso ? new Date(iso) : null;
|
|
115
|
+
if (status !== "ok" || !at || Number.isNaN(at.getTime())) {
|
|
116
|
+
return { status: "error", at: at && !Number.isNaN(at.getTime()) ? at : null };
|
|
117
|
+
}
|
|
118
|
+
if (now().getTime() - at.getTime() > staleAfterMs) {
|
|
119
|
+
return { status: "error", at };
|
|
120
|
+
}
|
|
121
|
+
return { status: "ok", at };
|
|
122
|
+
}
|
|
123
|
+
//# sourceMappingURL=creds.js.map
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ~/.hq-agent/kit.json — settings `hq agent kit install` records so the
|
|
3
|
+
* services (which only receive `hq agent kit run <service>`) know where to
|
|
4
|
+
* sync and how often. Kept separate from machine-creds.json, which is the
|
|
5
|
+
* contract-shaped credential file and nothing else.
|
|
6
|
+
*/
|
|
7
|
+
import type { AgentKitPaths } from "./paths.js";
|
|
8
|
+
export declare const DEFAULT_SYNC_INTERVAL_MS: number;
|
|
9
|
+
export declare const DEFAULT_INBOX_POLL_MS = 15000;
|
|
10
|
+
export declare const DEFAULT_HEARTBEAT_INTERVAL_MS = 60000;
|
|
11
|
+
export declare const DEFAULT_MESH_REFRESH_MS: number;
|
|
12
|
+
export interface KitConfig {
|
|
13
|
+
/** Local HQ tree the company vault syncs into (companies/<slug>/ …). */
|
|
14
|
+
hqRoot: string;
|
|
15
|
+
syncIntervalMs: number;
|
|
16
|
+
inboxPollMs: number;
|
|
17
|
+
heartbeatIntervalMs: number;
|
|
18
|
+
meshRefreshMs: number;
|
|
19
|
+
/** Ack mirrored inbox items on the server after writing them locally. */
|
|
20
|
+
inboxAck: boolean;
|
|
21
|
+
installedAt: string;
|
|
22
|
+
cliVersion: string;
|
|
23
|
+
}
|
|
24
|
+
export declare function defaultHqRoot(paths: Pick<AgentKitPaths, "agentDir">): string;
|
|
25
|
+
export declare function defaultKitConfig(paths: Pick<AgentKitPaths, "agentDir">, cliVersion: string, now?: () => Date): KitConfig;
|
|
26
|
+
export declare function writeKitConfig(paths: Pick<AgentKitPaths, "kitConfigPath">, config: KitConfig): void;
|
|
27
|
+
/** Read kit.json, falling back to defaults for any missing/invalid field. */
|
|
28
|
+
export declare function readKitConfig(paths: Pick<AgentKitPaths, "kitConfigPath" | "agentDir">, cliVersion: string): KitConfig;
|
|
29
|
+
//# sourceMappingURL=kit-config.d.ts.map
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ~/.hq-agent/kit.json — settings `hq agent kit install` records so the
|
|
3
|
+
* services (which only receive `hq agent kit run <service>`) know where to
|
|
4
|
+
* sync and how often. Kept separate from machine-creds.json, which is the
|
|
5
|
+
* contract-shaped credential file and nothing else.
|
|
6
|
+
*/
|
|
7
|
+
import * as fs from "node:fs";
|
|
8
|
+
import * as path from "node:path";
|
|
9
|
+
export const DEFAULT_SYNC_INTERVAL_MS = 5 * 60_000;
|
|
10
|
+
export const DEFAULT_INBOX_POLL_MS = 15_000;
|
|
11
|
+
export const DEFAULT_HEARTBEAT_INTERVAL_MS = 60_000;
|
|
12
|
+
export const DEFAULT_MESH_REFRESH_MS = 5 * 60_000;
|
|
13
|
+
export function defaultHqRoot(paths) {
|
|
14
|
+
return path.join(paths.agentDir, "hq");
|
|
15
|
+
}
|
|
16
|
+
export function defaultKitConfig(paths, cliVersion, now = () => new Date()) {
|
|
17
|
+
return {
|
|
18
|
+
hqRoot: defaultHqRoot(paths),
|
|
19
|
+
syncIntervalMs: DEFAULT_SYNC_INTERVAL_MS,
|
|
20
|
+
inboxPollMs: DEFAULT_INBOX_POLL_MS,
|
|
21
|
+
heartbeatIntervalMs: DEFAULT_HEARTBEAT_INTERVAL_MS,
|
|
22
|
+
meshRefreshMs: DEFAULT_MESH_REFRESH_MS,
|
|
23
|
+
inboxAck: false,
|
|
24
|
+
installedAt: now().toISOString(),
|
|
25
|
+
cliVersion,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
export function writeKitConfig(paths, config) {
|
|
29
|
+
fs.mkdirSync(path.dirname(paths.kitConfigPath), { recursive: true, mode: 0o700 });
|
|
30
|
+
fs.writeFileSync(paths.kitConfigPath, `${JSON.stringify(config, null, 2)}\n`, { mode: 0o600 });
|
|
31
|
+
}
|
|
32
|
+
/** Read kit.json, falling back to defaults for any missing/invalid field. */
|
|
33
|
+
export function readKitConfig(paths, cliVersion) {
|
|
34
|
+
const base = defaultKitConfig(paths, cliVersion);
|
|
35
|
+
let raw;
|
|
36
|
+
try {
|
|
37
|
+
raw = JSON.parse(fs.readFileSync(paths.kitConfigPath, "utf8"));
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return base;
|
|
41
|
+
}
|
|
42
|
+
const num = (k) => typeof raw[k] === "number" && raw[k] > 0 ? raw[k] : base[k];
|
|
43
|
+
return {
|
|
44
|
+
hqRoot: typeof raw.hqRoot === "string" && raw.hqRoot ? raw.hqRoot : base.hqRoot,
|
|
45
|
+
syncIntervalMs: num("syncIntervalMs"),
|
|
46
|
+
inboxPollMs: num("inboxPollMs"),
|
|
47
|
+
heartbeatIntervalMs: num("heartbeatIntervalMs"),
|
|
48
|
+
meshRefreshMs: num("meshRefreshMs"),
|
|
49
|
+
inboxAck: raw.inboxAck === true,
|
|
50
|
+
installedAt: typeof raw.installedAt === "string" ? raw.installedAt : base.installedAt,
|
|
51
|
+
cliVersion: typeof raw.cliVersion === "string" ? raw.cliVersion : base.cliVersion,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=kit-config.js.map
|