@consiliency/agent-board-cli 1.7.0 → 1.9.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/dist/cli.d.ts +24 -1
- package/dist/cli.js +170 -9
- package/package.json +2 -2
package/dist/cli.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
|
-
import { type EnrollKeystore, type EnrollmentIntent, type PendingEnrollmentStore, type OAuthFetchLike, type FetchLike } from "@consiliency/agent-board-client";
|
|
2
|
+
import { type EnrollKeystore, type EnrollmentIntent, type PendingEnrollmentStore, type OAuthFetchLike, type FetchLike, type AgentJoinDeployment, type AgentJoinInstructions, type DeviceAuthorizationPrompt } from "@consiliency/agent-board-client";
|
|
3
3
|
type JsonObject = Record<string, unknown>;
|
|
4
4
|
export interface CliIo {
|
|
5
5
|
writeStdout(message: string): void;
|
|
@@ -38,7 +38,15 @@ interface ClientLike {
|
|
|
38
38
|
grantBoardRole(input: JsonObject): Promise<unknown>;
|
|
39
39
|
setBoardRoleGate(input: JsonObject): Promise<unknown>;
|
|
40
40
|
createInviteLink(input: JsonObject): Promise<unknown>;
|
|
41
|
+
createInviteLinkWithInstructions(input: JsonObject, deployment: AgentJoinDeployment, board?: {
|
|
42
|
+
boardSlug?: string;
|
|
43
|
+
boardName?: string;
|
|
44
|
+
}): Promise<{
|
|
45
|
+
result: unknown;
|
|
46
|
+
instructions: AgentJoinInstructions;
|
|
47
|
+
}>;
|
|
41
48
|
revokeInviteLink(input: JsonObject): Promise<unknown>;
|
|
49
|
+
redeemInviteLink(input: JsonObject): Promise<unknown>;
|
|
42
50
|
listBoardPendingMemberships(input: JsonObject): Promise<unknown>;
|
|
43
51
|
resolveAgentHandle(input: JsonObject): Promise<unknown>;
|
|
44
52
|
updateInboxItemState(input: JsonObject): Promise<unknown>;
|
|
@@ -129,6 +137,14 @@ export interface EnrollmentCommandRunner {
|
|
|
129
137
|
redirectResponse?: string;
|
|
130
138
|
slug?: string;
|
|
131
139
|
}): Promise<unknown>;
|
|
140
|
+
device?(input: {
|
|
141
|
+
slug: string;
|
|
142
|
+
intent: EnrollmentIntent;
|
|
143
|
+
agentKind?: string;
|
|
144
|
+
requestedScopes?: string[];
|
|
145
|
+
requestedBoardScopes?: string[];
|
|
146
|
+
onDeviceAuthorization: (prompt: DeviceAuthorizationPrompt) => void | Promise<void>;
|
|
147
|
+
}): Promise<unknown>;
|
|
132
148
|
}
|
|
133
149
|
export interface DefaultEnrollmentRunnerDeps {
|
|
134
150
|
/** Pending store bridging begin→complete. Default: FilePendingEnrollmentStore(env). */
|
|
@@ -149,6 +165,13 @@ export interface DefaultEnrollmentRunnerDeps {
|
|
|
149
165
|
publishableKey?: string;
|
|
150
166
|
/** Clock (tests). Default: wall clock. */
|
|
151
167
|
now?: () => number;
|
|
168
|
+
/**
|
|
169
|
+
* Poll delay for the device-authorization grant (tests inject a no-op so the e2e
|
|
170
|
+
* does not incur a real RFC 8628 backoff sleep). Default: the real timer inside
|
|
171
|
+
* `acquireAccessTokenViaDeviceCode`. Only the `device` flow polls; run/begin/complete
|
|
172
|
+
* ignore it.
|
|
173
|
+
*/
|
|
174
|
+
sleep?: (ms: number) => Promise<void>;
|
|
152
175
|
/** Writer for the default browser opener's printed URL. Default: process std streams. */
|
|
153
176
|
io?: {
|
|
154
177
|
writeStdout(message: string): void;
|
package/dist/cli.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Command, Option } from "commander";
|
|
2
|
-
import { createAgentBoardClient, toOperationFailure, beginEnrollment, completeEnrollment, runEnrollment, resolveEnrollmentDescriptor, resolveEnrollEndpoint, resolveEnrollSessionUrl, resolveEnrollPublishableKey, nodeFetchAdapter, buildEnrollKeystore, } from "@consiliency/agent-board-client";
|
|
2
|
+
import { createAgentBoardClient, toOperationFailure, beginEnrollment, completeEnrollment, runEnrollment, runDeviceEnrollment, resolveEnrollmentDescriptor, EnrollmentDescriptorError, resolveEnrollEndpoint, resolveEnrollSessionUrl, resolveEnrollPublishableKey, resolveJoinDeployment, nodeFetchAdapter, buildEnrollKeystore, } from "@consiliency/agent-board-client";
|
|
3
3
|
import { FilePendingEnrollmentStore, makeBrowserOpener } from "./enroll_runtime.js";
|
|
4
4
|
function isRuntimeStatus(value) {
|
|
5
5
|
return Boolean(value && typeof value === "object" && "runtime" in value);
|
|
@@ -1246,6 +1246,40 @@ export function defaultEnrollmentRunner(env = process.env, deps = {}) {
|
|
|
1246
1246
|
now: deps.now,
|
|
1247
1247
|
});
|
|
1248
1248
|
},
|
|
1249
|
+
async device(input) {
|
|
1250
|
+
const descriptor = resolveEnrollmentDescriptor(env);
|
|
1251
|
+
// Fail CLEAN and actionable when the host descriptor carries no device
|
|
1252
|
+
// endpoint — otherwise the shared flow throws a bare `no_device_endpoint`.
|
|
1253
|
+
// The endpoint is optional in resolveEnrollmentDescriptor (it is not required
|
|
1254
|
+
// for the auth-code paths), so a headless operator must set it explicitly.
|
|
1255
|
+
if (!descriptor.issuer.deviceAuthorizationEndpoint) {
|
|
1256
|
+
throw new EnrollmentDescriptorError("missing_device_endpoint", "device enrollment needs a device authorization endpoint (set MESSAGE_BOARD_ENROLL_DEVICE_AUTHORIZATION_ENDPOINT, e.g. https://portal.consiliency.io/api/oauth/device_authorization)");
|
|
1257
|
+
}
|
|
1258
|
+
// Post-token deps are reconstructed EXACTLY as `run` does (rule #1): keystore,
|
|
1259
|
+
// keystoreBackends, boardFetch, enrollEndpoint, sessionUrl, publishableKey. A
|
|
1260
|
+
// dropped sessionUrl/publishableKey breaks the post-enroll board op on a raw-
|
|
1261
|
+
// Supabase board — pinned by the device analogue of the `run` e2e (drop either
|
|
1262
|
+
// and the sessionUrl-override / anon-apikey assertion fails).
|
|
1263
|
+
const enrollEndpoint = deps.enrollEndpoint ?? resolveEnrollEndpoint(env);
|
|
1264
|
+
const keystore = deps.keystore ?? buildEnrollKeystore(descriptor, env);
|
|
1265
|
+
return runDeviceEnrollment(descriptor, input.intent, {
|
|
1266
|
+
slug: input.slug,
|
|
1267
|
+
agentKind: input.agentKind,
|
|
1268
|
+
requestedScopes: input.requestedScopes,
|
|
1269
|
+
requestedBoardScopes: input.requestedBoardScopes,
|
|
1270
|
+
}, {
|
|
1271
|
+
fetch,
|
|
1272
|
+
enrollEndpoint,
|
|
1273
|
+
keystore,
|
|
1274
|
+
boardFetch,
|
|
1275
|
+
keystoreBackends: keystore.backends,
|
|
1276
|
+
sessionUrl,
|
|
1277
|
+
publishableKey,
|
|
1278
|
+
onDeviceAuthorization: input.onDeviceAuthorization,
|
|
1279
|
+
sleep: deps.sleep,
|
|
1280
|
+
now: deps.now,
|
|
1281
|
+
});
|
|
1282
|
+
},
|
|
1249
1283
|
async begin(input) {
|
|
1250
1284
|
// Force OOB: a loopback receiver cannot bridge two separate CLI processes, so
|
|
1251
1285
|
// the two-step path is always out-of-band paste (no redirectUri ⇒ OOB).
|
|
@@ -1259,7 +1293,15 @@ export function defaultEnrollmentRunner(env = process.env, deps = {}) {
|
|
|
1259
1293
|
requestedScopes: input.requestedScopes,
|
|
1260
1294
|
requestedBoardScopes: input.requestedBoardScopes,
|
|
1261
1295
|
redirectMode: "oob",
|
|
1262
|
-
}, store, {
|
|
1296
|
+
}, store, {
|
|
1297
|
+
fetch,
|
|
1298
|
+
clientId: descriptor.issuer.clientId,
|
|
1299
|
+
// #173 — pass the provisioning config so begin refuses a doomed raw-Supabase
|
|
1300
|
+
// enrollment BEFORE the human is asked to approve anything.
|
|
1301
|
+
sessionUrl,
|
|
1302
|
+
publishableKey,
|
|
1303
|
+
now: deps.now,
|
|
1304
|
+
});
|
|
1263
1305
|
},
|
|
1264
1306
|
async complete(input) {
|
|
1265
1307
|
const descriptor = resolveEnrollmentDescriptor(env);
|
|
@@ -1285,6 +1327,9 @@ export function defaultEnrollmentRunner(env = process.env, deps = {}) {
|
|
|
1285
1327
|
};
|
|
1286
1328
|
}
|
|
1287
1329
|
function intentFromOptions(options) {
|
|
1330
|
+
if (options.board === false) {
|
|
1331
|
+
return { kind: "none" };
|
|
1332
|
+
}
|
|
1288
1333
|
if (options.joinBoard) {
|
|
1289
1334
|
return { kind: "join_board", boardId: options.joinBoard };
|
|
1290
1335
|
}
|
|
@@ -1324,6 +1369,7 @@ function addEnrollCommands(program, io, enrollment) {
|
|
|
1324
1369
|
.option("--join-board <boardId>", "join-board intent board id")
|
|
1325
1370
|
.option("--requested-scopes <csv>", "requested scopes (comma-separated)")
|
|
1326
1371
|
.option("--requested-board-scopes <csv>", "requested board scopes (comma-separated)")
|
|
1372
|
+
.option("--no-board", "enroll WITHOUT provisioning any board (no create, no join)")
|
|
1327
1373
|
.action((options) => runHandler(io, "agent_board.enroll", () => enrollment.run({
|
|
1328
1374
|
slug: options.slug,
|
|
1329
1375
|
agentKind: options.agentKind,
|
|
@@ -1343,6 +1389,7 @@ function addEnrollCommands(program, io, enrollment) {
|
|
|
1343
1389
|
.option("--join-board <boardId>", "join-board intent board id")
|
|
1344
1390
|
.option("--requested-scopes <csv>", "requested scopes (comma-separated)")
|
|
1345
1391
|
.option("--requested-board-scopes <csv>", "requested board scopes (comma-separated)")
|
|
1392
|
+
.option("--no-board", "enroll WITHOUT provisioning any board (no create, no join)")
|
|
1346
1393
|
// A6: `begin` is ALWAYS out-of-band (a loopback receiver cannot bridge the two
|
|
1347
1394
|
// separate begin/complete CLI processes), so this flag is a vestigial no-op kept
|
|
1348
1395
|
// only for backward compatibility. Use `enroll run` for the loopback flow.
|
|
@@ -1376,6 +1423,66 @@ function addEnrollCommands(program, io, enrollment) {
|
|
|
1376
1423
|
slug: options.slug,
|
|
1377
1424
|
}));
|
|
1378
1425
|
});
|
|
1426
|
+
// RFC 8628 device authorization — the correct flow for a HEADLESS host. `run`'s
|
|
1427
|
+
// loopback receiver binds the SERVER's localhost (useless when the human's browser
|
|
1428
|
+
// is elsewhere) and `begin`/`complete`'s OOB redirect is not registered for the
|
|
1429
|
+
// Portal client; the device grant prints a user_code + URL the human approves on
|
|
1430
|
+
// ANY device, then polls to completion. Same options as `run` where they apply.
|
|
1431
|
+
enroll
|
|
1432
|
+
.command("device")
|
|
1433
|
+
.description("headless enrollment via the RFC 8628 device authorization grant (approve on any device)")
|
|
1434
|
+
.requiredOption("--slug <slug>", "desired agent slug")
|
|
1435
|
+
.option("--agent-kind <kind>", "agent kind (default 'agent')")
|
|
1436
|
+
.option("--create-board <slug>", "create-board intent slug")
|
|
1437
|
+
.option("--board-name <name>", "board name for create-board")
|
|
1438
|
+
.option("--board-description <description>", "board description")
|
|
1439
|
+
.option("--visibility <visibility>", "board visibility")
|
|
1440
|
+
.option("--join-board <boardId>", "join-board intent board id")
|
|
1441
|
+
.option("--requested-scopes <csv>", "requested scopes (comma-separated)")
|
|
1442
|
+
.option("--requested-board-scopes <csv>", "requested board scopes (comma-separated)")
|
|
1443
|
+
.option("--no-board", "enroll WITHOUT provisioning any board (no create, no join)")
|
|
1444
|
+
.action((options) => runHandler(io, "agent_board.enroll_device", () => {
|
|
1445
|
+
// `device` is an OPTIONAL member of EnrollmentCommandRunner (added after the
|
|
1446
|
+
// interface shipped). The default runner always provides it; a custom runner
|
|
1447
|
+
// that predates it gets a clean error instead of a `undefined is not a function`.
|
|
1448
|
+
if (!enrollment.device) {
|
|
1449
|
+
throw new Error("device-authorization enrollment is not supported by this runner (EnrollmentCommandRunner.device is not implemented)");
|
|
1450
|
+
}
|
|
1451
|
+
return enrollment.device({
|
|
1452
|
+
slug: options.slug,
|
|
1453
|
+
agentKind: options.agentKind,
|
|
1454
|
+
intent: intentFromOptions(options),
|
|
1455
|
+
requestedScopes: csv(options.requestedScopes),
|
|
1456
|
+
requestedBoardScopes: csv(options.requestedBoardScopes),
|
|
1457
|
+
// Print the human-facing prompt PROMINENTLY to stderr (stdout is reserved
|
|
1458
|
+
// for the final JSON outcome). The device_code secret is never surfaced.
|
|
1459
|
+
onDeviceAuthorization: (prompt) => io.writeStderr(renderDevicePrompt(prompt)),
|
|
1460
|
+
});
|
|
1461
|
+
}));
|
|
1462
|
+
}
|
|
1463
|
+
/**
|
|
1464
|
+
* Render the human-facing device-authorization prompt. Written to be pasted to a
|
|
1465
|
+
* human: the user_code and verification URI are the two things they act on. Never
|
|
1466
|
+
* carries the device_code (the shared flow does not expose it here).
|
|
1467
|
+
*/
|
|
1468
|
+
function renderDevicePrompt(prompt) {
|
|
1469
|
+
const lines = [
|
|
1470
|
+
"",
|
|
1471
|
+
"=== Approve this agent on any device (RFC 8628 device authorization) ===",
|
|
1472
|
+
` 1. Open: ${prompt.verificationUri}`,
|
|
1473
|
+
` 2. Enter code: ${prompt.userCode}`,
|
|
1474
|
+
];
|
|
1475
|
+
if (prompt.verificationUriComplete) {
|
|
1476
|
+
lines.push(` (or open this pre-filled link directly: ${prompt.verificationUriComplete})`);
|
|
1477
|
+
}
|
|
1478
|
+
if (prompt.expiresIn) {
|
|
1479
|
+
lines.push(` This code expires in ${prompt.expiresIn} seconds. Waiting for approval...`);
|
|
1480
|
+
}
|
|
1481
|
+
else {
|
|
1482
|
+
lines.push(" Waiting for approval...");
|
|
1483
|
+
}
|
|
1484
|
+
lines.push("");
|
|
1485
|
+
return `${lines.join("\n")}\n`;
|
|
1379
1486
|
}
|
|
1380
1487
|
/** Read a single-line auth code from a piped stdin (A5). Returns undefined on a TTY. */
|
|
1381
1488
|
async function readCodeFromStdin() {
|
|
@@ -1494,18 +1601,72 @@ function addRoomsCommands(program, client, io) {
|
|
|
1494
1601
|
.addOption(numberOption("--max-uses <n>", "maximum redemptions"))
|
|
1495
1602
|
.option("--expires <iso>")
|
|
1496
1603
|
.option("--created-by-agent-id <agentId>")
|
|
1497
|
-
.
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1604
|
+
.option("--with-instructions", "also print the canonical, paste-able agent-join instructions block (embeds the invite code — treat the output as secret)")
|
|
1605
|
+
.option("--board-slug <slug>", "board slug for the instructions label (the RPC does not return it)")
|
|
1606
|
+
.option("--board-name <name>", "board name for the instructions label (the RPC does not return it)")
|
|
1607
|
+
.action(async (o) => {
|
|
1608
|
+
const rpcInput = {
|
|
1609
|
+
p_board_id: o.boardId,
|
|
1610
|
+
p_preset_role: o.role,
|
|
1611
|
+
p_max_uses: o.maxUses,
|
|
1612
|
+
p_expires_at: o.expires,
|
|
1613
|
+
p_created_by_agent_id: o.createdByAgentId,
|
|
1614
|
+
};
|
|
1615
|
+
if (!o.withInstructions) {
|
|
1616
|
+
return runHandler(io, "agent_board.create_invite_link", () => client.createInviteLink(rpcInput));
|
|
1617
|
+
}
|
|
1618
|
+
// Opt-in instructions path. Resolve deployment facts FIRST so a config gap
|
|
1619
|
+
// fails cleanly WITHOUT minting an orphan link; the plain path above is
|
|
1620
|
+
// untouched, so a missing descriptor never breaks ordinary invite creation.
|
|
1621
|
+
try {
|
|
1622
|
+
const deployment = resolveJoinDeployment();
|
|
1623
|
+
const { result, instructions } = await client.createInviteLinkWithInstructions(rpcInput, deployment, {
|
|
1624
|
+
boardSlug: o.boardSlug,
|
|
1625
|
+
boardName: o.boardName,
|
|
1626
|
+
});
|
|
1627
|
+
io.writeStdout(`${JSON.stringify(result, null, 2)}\n`);
|
|
1628
|
+
// Rendered block printed raw (not JSON-escaped) so it is paste-able. The
|
|
1629
|
+
// invite code lives here; this is the operator's own terminal, the same
|
|
1630
|
+
// place the JSON result already prints it.
|
|
1631
|
+
io.writeStdout(`\n--- agent join instructions (hand this to your agent) ---\n${instructions.renderedText}\n`);
|
|
1632
|
+
}
|
|
1633
|
+
catch (error) {
|
|
1634
|
+
io.writeStdout(`${JSON.stringify(toOperationFailure("agent_board.create_invite_link", error), null, 2)}\n`);
|
|
1635
|
+
process.exitCode = 1;
|
|
1636
|
+
}
|
|
1637
|
+
});
|
|
1504
1638
|
rooms
|
|
1505
1639
|
.command("link-revoke")
|
|
1506
1640
|
.description("Revoke an invite link by code")
|
|
1507
1641
|
.requiredOption("--code <code>")
|
|
1508
1642
|
.action((o) => runHandler(io, "agent_board.revoke_invite_link", () => client.revokeInviteLink({ p_code: o.code })));
|
|
1643
|
+
// Joiner-side redeem: an already-enrolled agent redeems an invite AS ITSELF from
|
|
1644
|
+
// its OWN env-resolved credentials (the same resolution every other CLI command
|
|
1645
|
+
// uses) — the clean path for a host whose MCP session carries a different/shared
|
|
1646
|
+
// identity, without hand-rolling an SDK script. The code is a BEARER SECRET.
|
|
1647
|
+
//
|
|
1648
|
+
// A5 (mirrors `enroll complete`, cli.ts): PREFER stdin. A bearer code on argv leaks
|
|
1649
|
+
// via /proc/<pid>/cmdline and the shell's history file, so `--code` is optional and
|
|
1650
|
+
// documented as leaky; when it is absent and stdin is piped, the code is read from
|
|
1651
|
+
// stdin (e.g. a quoted heredoc — the form the rendered join instructions emit, which
|
|
1652
|
+
// keeps the code out of any executable shell string). The command never writes the
|
|
1653
|
+
// code to a log, and neither the success result nor the failure object echoes it.
|
|
1654
|
+
rooms
|
|
1655
|
+
.command("link-redeem")
|
|
1656
|
+
.description("Redeem an invite link by code, joining as the agent identity resolved from this host's env credentials. The code is a BEARER SECRET — prefer piping it on stdin.")
|
|
1657
|
+
.option("--code <code>", "the invite code (prefer stdin; argv leaks via /proc/<pid>/cmdline and shell history)")
|
|
1658
|
+
.action(async (o) => {
|
|
1659
|
+
let code = o.code;
|
|
1660
|
+
if (!code) {
|
|
1661
|
+
code = await readCodeFromStdin();
|
|
1662
|
+
}
|
|
1663
|
+
if (!code) {
|
|
1664
|
+
io.writeStderr("link-redeem needs an invite code: pipe it on stdin (e.g. `agent-board rooms link-redeem <<'MB_INVITE_CODE'` ... `MB_INVITE_CODE`) or pass --code (which leaks the bearer code via /proc and shell history).\n");
|
|
1665
|
+
process.exitCode = 1;
|
|
1666
|
+
return;
|
|
1667
|
+
}
|
|
1668
|
+
return runHandler(io, "agent_board.redeem_invite_link", () => client.redeemInviteLink({ p_code: code }));
|
|
1669
|
+
});
|
|
1509
1670
|
}
|
|
1510
1671
|
export function createProgram(client, io, enrollment = defaultEnrollmentRunner()) {
|
|
1511
1672
|
const program = new Command()
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@consiliency/agent-board-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.0",
|
|
4
4
|
"repository": "https://github.com/Consiliency/message-board.git",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"registry": "https://registry.npmjs.org"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@consiliency/agent-board-client": "1.
|
|
34
|
+
"@consiliency/agent-board-client": "1.15.0",
|
|
35
35
|
"commander": "^14.0.1"
|
|
36
36
|
},
|
|
37
37
|
"license": "Apache-2.0"
|