@consiliency/agent-board-cli 1.24.0 → 1.25.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/assets.d.ts +22 -0
- package/dist/assets.js +61 -0
- package/dist/assets.json +6 -0
- package/dist/cli.d.ts +37 -3
- package/dist/cli.js +68 -26
- package/dist/config_edit.d.ts +37 -0
- package/dist/config_edit.js +549 -0
- package/dist/doctor.d.ts +7 -1
- package/dist/doctor.js +144 -2
- package/dist/identity_writer.d.ts +9 -7
- package/dist/identity_writer.js +18 -30
- package/dist/setup.d.ts +116 -0
- package/dist/setup.js +603 -0
- package/dist/setup_storage.d.ts +117 -0
- package/dist/setup_storage.js +447 -0
- package/dist/setup_writers.d.ts +128 -0
- package/dist/setup_writers.js +653 -0
- package/keyslot/agent-board-keyslot-load@.service +20 -0
- package/keyslot/agent-board-keyslot@.service +20 -0
- package/keyslot/constants.sh +73 -0
- package/keyslot/keygen.mjs +67 -0
- package/keyslot/keyslot-load.sh +78 -0
- package/keyslot/lib.sh +599 -0
- package/keyslot/optin.sh +185 -0
- package/keyslot/probe.sh +135 -0
- package/keyslot/provision.sh +530 -0
- package/keyslot/seam.sh +48 -0
- package/keyslot/status.sh +254 -0
- package/package.json +8 -4
package/dist/assets.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export interface BakedAssets {
|
|
2
|
+
/** `@consiliency/agent-board-mcp`'s version: the codex, grok and agy stanza pins it. */
|
|
3
|
+
mcpVersion: string;
|
|
4
|
+
piVersion: string;
|
|
5
|
+
opencodeVersion: string;
|
|
6
|
+
/** The train tag this build is released as (`v<x.y.z>`), from CHANGELOG.md's first heading. */
|
|
7
|
+
releaseTag: string;
|
|
8
|
+
}
|
|
9
|
+
export declare const BAKED_ASSET_KEYS: readonly ["mcpVersion", "piVersion", "opencodeVersion", "releaseTag"];
|
|
10
|
+
/**
|
|
11
|
+
* A missing or malformed bake. Never defaulted: an unpinned version is a different install. It
|
|
12
|
+
* carries no `code`: it is not a cause-code site (fleet_diagnosability #403), and setup names it.
|
|
13
|
+
*/
|
|
14
|
+
export declare class BakedAssetsError extends Error {
|
|
15
|
+
constructor(message: string);
|
|
16
|
+
}
|
|
17
|
+
export declare const ASSETS_URL: URL;
|
|
18
|
+
export declare const KEYSLOT_URL: URL;
|
|
19
|
+
/** The build's baked versions and release tag, validated field by field. */
|
|
20
|
+
export declare function bakedAssets(file?: string | URL): BakedAssets;
|
|
21
|
+
/** The packaged keyslot directory (no trailing slash). Throws when the build did not copy it. */
|
|
22
|
+
export declare function resolveKeyslotDir(url?: URL): string;
|
package/dist/assets.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// SETUP IF-0-SETUP-4 — what the cli build baked (`dist/assets.json`) and where the packaged keyslot
|
|
2
|
+
// scripts are. Both are written by `scripts/lib/cli-assets.mjs --write`, which the cli `build` runs;
|
|
3
|
+
// `cli-assets.mjs --check` holds them equal to the tree.
|
|
4
|
+
//
|
|
5
|
+
// Paths are resolved from this module's own URL, one level up, so the same spelling lands on the
|
|
6
|
+
// package's `dist/` and `keyslot/` whether this runs as `dist/assets.js` (installed) or as
|
|
7
|
+
// `src/assets.ts` (the test suite, after a build).
|
|
8
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
10
|
+
export const BAKED_ASSET_KEYS = Object.freeze(["mcpVersion", "piVersion", "opencodeVersion", "releaseTag"]);
|
|
11
|
+
const VERSION = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
|
|
12
|
+
const RELEASE_TAG = /^v\d+\.\d+\.\d+$/;
|
|
13
|
+
/**
|
|
14
|
+
* A missing or malformed bake. Never defaulted: an unpinned version is a different install. It
|
|
15
|
+
* carries no `code`: it is not a cause-code site (fleet_diagnosability #403), and setup names it.
|
|
16
|
+
*/
|
|
17
|
+
export class BakedAssetsError extends Error {
|
|
18
|
+
constructor(message) {
|
|
19
|
+
super(message);
|
|
20
|
+
this.name = "BakedAssetsError";
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export const ASSETS_URL = new URL("../dist/assets.json", import.meta.url);
|
|
24
|
+
export const KEYSLOT_URL = new URL("../keyslot/", import.meta.url);
|
|
25
|
+
/** The build's baked versions and release tag, validated field by field. */
|
|
26
|
+
export function bakedAssets(file = ASSETS_URL) {
|
|
27
|
+
let raw;
|
|
28
|
+
try {
|
|
29
|
+
raw = JSON.parse(readFileSync(file, "utf8"));
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
throw new BakedAssetsError("dist/assets.json is absent or unreadable: the cli build did not run");
|
|
33
|
+
}
|
|
34
|
+
const record = raw;
|
|
35
|
+
if (!record || typeof record !== "object" || Array.isArray(record))
|
|
36
|
+
throw new BakedAssetsError("dist/assets.json is not an object");
|
|
37
|
+
const keys = Object.keys(record);
|
|
38
|
+
if (keys.length !== BAKED_ASSET_KEYS.length || !BAKED_ASSET_KEYS.every((key) => keys.includes(key))) {
|
|
39
|
+
throw new BakedAssetsError(`dist/assets.json must hold exactly ${BAKED_ASSET_KEYS.join(", ")}`);
|
|
40
|
+
}
|
|
41
|
+
for (const key of ["mcpVersion", "piVersion", "opencodeVersion"]) {
|
|
42
|
+
if (typeof record[key] !== "string" || !VERSION.test(record[key]))
|
|
43
|
+
throw new BakedAssetsError(`dist/assets.json ${key} is not a version`);
|
|
44
|
+
}
|
|
45
|
+
if (typeof record.releaseTag !== "string" || !RELEASE_TAG.test(record.releaseTag)) {
|
|
46
|
+
throw new BakedAssetsError("dist/assets.json releaseTag is not v<x.y.z>");
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
mcpVersion: record.mcpVersion,
|
|
50
|
+
piVersion: record.piVersion,
|
|
51
|
+
opencodeVersion: record.opencodeVersion,
|
|
52
|
+
releaseTag: record.releaseTag,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/** The packaged keyslot directory (no trailing slash). Throws when the build did not copy it. */
|
|
56
|
+
export function resolveKeyslotDir(url = KEYSLOT_URL) {
|
|
57
|
+
const dir = fileURLToPath(url).replace(/\/+$/, "");
|
|
58
|
+
if (!existsSync(`${dir}/status.sh`))
|
|
59
|
+
throw new BakedAssetsError("keyslot/ is absent: the cli build did not copy the keyslot scripts");
|
|
60
|
+
return dir;
|
|
61
|
+
}
|
package/dist/assets.json
ADDED
package/dist/cli.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
|
-
import { type EnrollKeystore, type EnrollmentIntent, type FleetOnboardingIntent, type PendingEnrollmentStore, type OAuthFetchLike, type FetchLike, type AgentJoinDeployment, type AgentJoinInstructions, type DeviceAuthorizationPrompt } from "@consiliency/agent-board-client";
|
|
2
|
+
import { createAgentBoardClient, type EnrollKeystore, type EnrollmentIntent, type FleetOnboardingIntent, type PendingEnrollmentStore, type OAuthFetchLike, type FetchLike, type AgentJoinDeployment, type AgentJoinInstructions, type DeviceAuthorizationPrompt, resolveCredentials } from "@consiliency/agent-board-client";
|
|
3
3
|
import { type DoctorOptions, type DoctorReport } from "./doctor.js";
|
|
4
|
+
import { type SetupEnroller, type SetupRunner } from "./setup.js";
|
|
4
5
|
import { DevicePendingEnrollmentStore } from "./enroll_runtime.js";
|
|
5
6
|
type JsonObject = Record<string, unknown>;
|
|
6
7
|
export interface CliIo {
|
|
@@ -208,6 +209,11 @@ export interface DefaultEnrollmentRunnerDeps {
|
|
|
208
209
|
sessionUrl?: string;
|
|
209
210
|
/** Anon apikey for the post-enroll board RPCs. Default: resolveEnrollPublishableKey(env). */
|
|
210
211
|
publishableKey?: string;
|
|
212
|
+
/**
|
|
213
|
+
* SETUP: the key reference the `key` step produced, handed over directly (never an env var the
|
|
214
|
+
* user must set). Wins over `MESSAGE_BOARD_ENROLL_KEY_REFERENCE`. Default: env.
|
|
215
|
+
*/
|
|
216
|
+
keyReference?: string;
|
|
211
217
|
/** Clock (tests). Default: wall clock. */
|
|
212
218
|
now?: () => number;
|
|
213
219
|
/**
|
|
@@ -224,6 +230,11 @@ export interface DefaultEnrollmentRunnerDeps {
|
|
|
224
230
|
};
|
|
225
231
|
}
|
|
226
232
|
export declare function defaultEnrollmentRunner(env?: NodeJS.ProcessEnv, deps?: DefaultEnrollmentRunnerDeps): EnrollmentCommandRunner;
|
|
233
|
+
/**
|
|
234
|
+
* SETUP — the enrollment `agent-board setup` runs: this runner's device flow, built from setup's
|
|
235
|
+
* env with the `key` step's reference injected (never an env var the user must set).
|
|
236
|
+
*/
|
|
237
|
+
export declare const setupEnroller: SetupEnroller;
|
|
227
238
|
/**
|
|
228
239
|
* How each field of a `FleetOnboardingIntent` is turned from an argv string into the value
|
|
229
240
|
* that goes on the wire and into the template (Consiliency/message-board#254).
|
|
@@ -306,6 +317,29 @@ type FleetTupleField = StringFieldsOf<FleetOnboardingIntent>;
|
|
|
306
317
|
export declare const FLEET_TUPLE_DISPOSITION: Readonly<Record<FleetTupleField, FleetTupleDisposition>>;
|
|
307
318
|
/** `runDoctor`'s shape; a seam so the exit-code wiring is testable without a live board. */
|
|
308
319
|
export type DoctorRunner = (options: DoctorOptions) => Promise<DoctorReport>;
|
|
309
|
-
|
|
310
|
-
|
|
320
|
+
/**
|
|
321
|
+
* SETUP IF-0-SETUP-1 — the global `--as <harness>`: which harness's identity FILE every command's
|
|
322
|
+
* credentials come from. The client is constructed before argv is parsed, so the choice is a
|
|
323
|
+
* holder the parse fills and the client's credential resolver reads at FIRST USE (see
|
|
324
|
+
* `fileAwareCredentials`). Unset, the resolver is today's env chain, unchanged.
|
|
325
|
+
*/
|
|
326
|
+
export interface CredentialSelection {
|
|
327
|
+
as?: string;
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* The lazy credential source. With `--as <h>`: `<h>`'s identity file through the client's trusted
|
|
331
|
+
* open, and ONLY the file — `fileIdentityEnv` strips every ambient identity and generic name, as
|
|
332
|
+
* doctor does, because `--as` is an explicit choice of the file. The runtime descriptor is ignored
|
|
333
|
+
* for the same reason. Without it: `resolveCredentials(descriptor, env)`, exactly as before.
|
|
334
|
+
*/
|
|
335
|
+
export declare function fileAwareCredentials(selection: CredentialSelection, env?: NodeJS.ProcessEnv): (descriptor?: Parameters<typeof resolveCredentials>[0]) => Promise<import("@consiliency/agent-board-client").ResolvedCredentials>;
|
|
336
|
+
export declare function createProgram(client: ClientLike, io: CliIo, enrollment?: EnrollmentCommandRunner, doctor?: DoctorRunner, selection?: CredentialSelection, setup?: SetupRunner): Command;
|
|
337
|
+
export interface RunCliOptions {
|
|
338
|
+
/** The env the default client's credential resolver reads. Default `process.env`. */
|
|
339
|
+
env?: NodeJS.ProcessEnv;
|
|
340
|
+
/** Extra options for the DEFAULT client (tests: a `dataSourceFactory`); never its resolver. */
|
|
341
|
+
clientOptions?: Omit<NonNullable<Parameters<typeof createAgentBoardClient>[0]>, "resolveCredentials">;
|
|
342
|
+
setup?: SetupRunner;
|
|
343
|
+
}
|
|
344
|
+
export declare function runCli(argv: string[], io?: CliIo, client?: ClientLike, enrollment?: EnrollmentCommandRunner, doctor?: DoctorRunner, options?: RunCliOptions): Promise<void>;
|
|
311
345
|
export {};
|
package/dist/cli.js
CHANGED
|
@@ -3,8 +3,9 @@ import { createHash, randomUUID } from "node:crypto";
|
|
|
3
3
|
import * as nodeFs from "node:fs";
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
import { dirname, join } from "node:path";
|
|
6
|
-
import { createAgentBoardClient, toOperationFailure, beginEnrollment, completeEnrollment, beginDeviceEnrollment, completeDeviceEnrollment, runEnrollment, runDeviceEnrollment, resolveEnrollmentDescriptor, EnrollmentDescriptorError, EnrollmentCommandError, resolveEnrollEndpoint, resolveEnrollSessionUrl, resolveEnrollPublishableKey, resolveJoinDeployment, buildFleetHarnessTemplate, finalizeFleetHarnessTemplate, normalizePublishableKeyFieldReference, normalizeFleetTupleValue, normalizeKeyReference, enrollCommitMarker, assertPresentationSafe, FLEET_TEMPLATE_FAILURE_MESSAGE, PROHIBITED_PRESENTATION_REMOVE_CLASS, buildAgentJoinInstructions, nodeFetchAdapter, buildEnrollKeystore, AUTHORED_CAUSE_CODES, codeProvenance, markAuthoredCode, assertIdentityHarness, identityFilePath, resolveHostSegment, identityConfigRoot, walkTrustedDirectory, IDENTITY_HARNESSES, CredentialResolutionError, } from "@consiliency/agent-board-client";
|
|
7
|
-
import { formatDoctorReport, runDoctor } from "./doctor.js";
|
|
6
|
+
import { createAgentBoardClient, toOperationFailure, beginEnrollment, completeEnrollment, beginDeviceEnrollment, completeDeviceEnrollment, runEnrollment, runDeviceEnrollment, resolveEnrollmentDescriptor, EnrollmentDescriptorError, EnrollmentCommandError, resolveEnrollEndpoint, resolveEnrollSessionUrl, resolveEnrollPublishableKey, resolveJoinDeployment, buildFleetHarnessTemplate, finalizeFleetHarnessTemplate, normalizePublishableKeyFieldReference, normalizeFleetTupleValue, normalizeKeyReference, enrollCommitMarker, assertPresentationSafe, FLEET_TEMPLATE_FAILURE_MESSAGE, PROHIBITED_PRESENTATION_REMOVE_CLASS, buildAgentJoinInstructions, nodeFetchAdapter, buildEnrollKeystore, AUTHORED_CAUSE_CODES, codeProvenance, markAuthoredCode, assertIdentityHarness, identityFilePath, resolveHostSegment, identityConfigRoot, walkTrustedDirectory, IDENTITY_HARNESSES, CredentialResolutionError, readIdentityFile, resolveCredentials, } from "@consiliency/agent-board-client";
|
|
7
|
+
import { fileIdentityEnv, formatDoctorReport, runDoctor } from "./doctor.js";
|
|
8
|
+
import { addSetupCommand, runSetup } from "./setup.js";
|
|
8
9
|
import { identityValuesFromEnrollment, resolveIdentityDeployment, writeIdentityFile, } from "./identity_writer.js";
|
|
9
10
|
import { DevicePendingEnrollmentStore, FilePendingEnrollmentStore, FleetDeviceContinuationStore, makeBrowserOpener, prepareFleetRuntimeOutput, publishFinalizedFleetTemplate, } from "./enroll_runtime.js";
|
|
10
11
|
function isRuntimeStatus(value) {
|
|
@@ -1628,11 +1629,15 @@ export function defaultEnrollmentRunner(env = process.env, deps = {}) {
|
|
|
1628
1629
|
const boardFetch = deps.boardFetch ?? nodeFetchAdapter;
|
|
1629
1630
|
// Post-enroll board-op wiring (raw Supabase domain): explicit /session URL + anon
|
|
1630
1631
|
// apikey. Optional — absent preserves the ${boardUrl}/session default + no apikey.
|
|
1631
|
-
|
|
1632
|
-
|
|
1632
|
+
// LAZY (SETUP): the runner is built before parsing, for every command, so constructing it
|
|
1633
|
+
// reads nothing from env; each flow method resolves these when enrollment actually runs.
|
|
1634
|
+
const sessionUrl = () => deps.sessionUrl ?? resolveEnrollSessionUrl(env);
|
|
1635
|
+
const publishableKey = () => deps.publishableKey ?? resolveEnrollPublishableKey(env);
|
|
1636
|
+
// SETUP: the `key` step's reference is injected, and wins over env.
|
|
1637
|
+
const descriptorOf = () => resolveEnrollmentDescriptor(env, deps.keyReference === undefined ? {} : { keyReference: deps.keyReference });
|
|
1633
1638
|
return {
|
|
1634
1639
|
async run(input) {
|
|
1635
|
-
const descriptor =
|
|
1640
|
+
const descriptor = descriptorOf();
|
|
1636
1641
|
const enrollEndpoint = deps.enrollEndpoint ?? resolveEnrollEndpoint(env);
|
|
1637
1642
|
const keystore = deps.keystore ?? buildEnrollKeystore(descriptor, env, {
|
|
1638
1643
|
...(input.intent.kind === "fleet_onboarding" ? { fleetOnboarding: input.intent } : {}),
|
|
@@ -1649,14 +1654,14 @@ export function defaultEnrollmentRunner(env = process.env, deps = {}) {
|
|
|
1649
1654
|
keystore,
|
|
1650
1655
|
boardFetch,
|
|
1651
1656
|
keystoreBackends: keystore.backends,
|
|
1652
|
-
sessionUrl,
|
|
1653
|
-
publishableKey,
|
|
1657
|
+
sessionUrl: sessionUrl(),
|
|
1658
|
+
publishableKey: publishableKey(),
|
|
1654
1659
|
openAuthorization,
|
|
1655
1660
|
now: deps.now,
|
|
1656
1661
|
});
|
|
1657
1662
|
},
|
|
1658
1663
|
async device(input) {
|
|
1659
|
-
const descriptor =
|
|
1664
|
+
const descriptor = descriptorOf();
|
|
1660
1665
|
// Fail CLEAN and actionable when the host descriptor carries no device
|
|
1661
1666
|
// endpoint — otherwise the shared flow throws a bare `no_device_endpoint`.
|
|
1662
1667
|
// The endpoint is optional in resolveEnrollmentDescriptor (it is not required
|
|
@@ -1684,15 +1689,15 @@ export function defaultEnrollmentRunner(env = process.env, deps = {}) {
|
|
|
1684
1689
|
keystore,
|
|
1685
1690
|
boardFetch,
|
|
1686
1691
|
keystoreBackends: keystore.backends,
|
|
1687
|
-
sessionUrl,
|
|
1688
|
-
publishableKey,
|
|
1692
|
+
sessionUrl: sessionUrl(),
|
|
1693
|
+
publishableKey: publishableKey(),
|
|
1689
1694
|
onDeviceAuthorization: input.onDeviceAuthorization,
|
|
1690
1695
|
sleep: deps.sleep,
|
|
1691
1696
|
now: deps.now,
|
|
1692
1697
|
});
|
|
1693
1698
|
},
|
|
1694
1699
|
async deviceBegin(input) {
|
|
1695
|
-
const descriptor =
|
|
1700
|
+
const descriptor = descriptorOf();
|
|
1696
1701
|
if (!descriptor.issuer.deviceAuthorizationEndpoint) {
|
|
1697
1702
|
throw new EnrollmentDescriptorError("missing_device_endpoint", "device enrollment needs a device authorization endpoint (set MESSAGE_BOARD_ENROLL_DEVICE_AUTHORIZATION_ENDPOINT)");
|
|
1698
1703
|
}
|
|
@@ -1703,8 +1708,8 @@ export function defaultEnrollmentRunner(env = process.env, deps = {}) {
|
|
|
1703
1708
|
...(input.intent.kind === "fleet_onboarding" ? { fleetOnboarding: input.intent } : {}),
|
|
1704
1709
|
}),
|
|
1705
1710
|
boardFetch,
|
|
1706
|
-
sessionUrl,
|
|
1707
|
-
publishableKey,
|
|
1711
|
+
sessionUrl: sessionUrl(),
|
|
1712
|
+
publishableKey: publishableKey(),
|
|
1708
1713
|
onDeviceAuthorization: async () => { },
|
|
1709
1714
|
now: deps.now,
|
|
1710
1715
|
});
|
|
@@ -1776,12 +1781,12 @@ export function defaultEnrollmentRunner(env = process.env, deps = {}) {
|
|
|
1776
1781
|
if (pending.slug !== input.expectedFleet.slug) {
|
|
1777
1782
|
throw new EnrollmentCommandError("fleet_device_slug_mismatch", "device enrollment agent slug does not match");
|
|
1778
1783
|
}
|
|
1779
|
-
const currentReference = normalizeKeyReference(
|
|
1784
|
+
const currentReference = normalizeKeyReference(descriptorOf().keyReference);
|
|
1780
1785
|
if (`${currentReference.resolver}:${currentReference.handle}` !== input.expectedFleet.keyReference) {
|
|
1781
1786
|
throw new EnrollmentCommandError("fleet_device_key_reference_mismatch", "device enrollment key reference does not match current configuration");
|
|
1782
1787
|
}
|
|
1783
1788
|
}
|
|
1784
|
-
const descriptor =
|
|
1789
|
+
const descriptor = descriptorOf();
|
|
1785
1790
|
if (!descriptor.issuer.deviceAuthorizationEndpoint) {
|
|
1786
1791
|
throw new EnrollmentDescriptorError("missing_device_endpoint", "device enrollment needs a device authorization endpoint (set MESSAGE_BOARD_ENROLL_DEVICE_AUTHORIZATION_ENDPOINT)");
|
|
1787
1792
|
}
|
|
@@ -1809,8 +1814,8 @@ export function defaultEnrollmentRunner(env = process.env, deps = {}) {
|
|
|
1809
1814
|
keystore,
|
|
1810
1815
|
boardFetch,
|
|
1811
1816
|
keystoreBackends: keystore.backends,
|
|
1812
|
-
sessionUrl,
|
|
1813
|
-
publishableKey,
|
|
1817
|
+
sessionUrl: sessionUrl(),
|
|
1818
|
+
publishableKey: publishableKey(),
|
|
1814
1819
|
onDeviceAuthorization: async () => { },
|
|
1815
1820
|
sleep: deps.sleep,
|
|
1816
1821
|
now: deps.now,
|
|
@@ -1846,7 +1851,7 @@ export function defaultEnrollmentRunner(env = process.env, deps = {}) {
|
|
|
1846
1851
|
async begin(input) {
|
|
1847
1852
|
// Force OOB: a loopback receiver cannot bridge two separate CLI processes, so
|
|
1848
1853
|
// the two-step path is always out-of-band paste (no redirectUri ⇒ OOB).
|
|
1849
|
-
const descriptor =
|
|
1854
|
+
const descriptor = descriptorOf();
|
|
1850
1855
|
const store = deps.store ?? new FilePendingEnrollmentStore(env);
|
|
1851
1856
|
return beginEnrollment({
|
|
1852
1857
|
descriptor,
|
|
@@ -1861,13 +1866,13 @@ export function defaultEnrollmentRunner(env = process.env, deps = {}) {
|
|
|
1861
1866
|
clientId: descriptor.issuer.clientId,
|
|
1862
1867
|
// #173 — pass the provisioning config so begin refuses a doomed raw-Supabase
|
|
1863
1868
|
// enrollment BEFORE the human is asked to approve anything.
|
|
1864
|
-
sessionUrl,
|
|
1865
|
-
publishableKey,
|
|
1869
|
+
sessionUrl: sessionUrl(),
|
|
1870
|
+
publishableKey: publishableKey(),
|
|
1866
1871
|
now: deps.now,
|
|
1867
1872
|
});
|
|
1868
1873
|
},
|
|
1869
1874
|
async complete(input) {
|
|
1870
|
-
const descriptor =
|
|
1875
|
+
const descriptor = descriptorOf();
|
|
1871
1876
|
const enrollEndpoint = deps.enrollEndpoint ?? resolveEnrollEndpoint(env);
|
|
1872
1877
|
const store = deps.store ?? new FilePendingEnrollmentStore(env);
|
|
1873
1878
|
const pending = await store.get(input.enrollmentId);
|
|
@@ -1885,13 +1890,21 @@ export function defaultEnrollmentRunner(env = process.env, deps = {}) {
|
|
|
1885
1890
|
keystore,
|
|
1886
1891
|
boardFetch,
|
|
1887
1892
|
keystoreBackends: keystore.backends,
|
|
1888
|
-
sessionUrl,
|
|
1889
|
-
publishableKey,
|
|
1893
|
+
sessionUrl: sessionUrl(),
|
|
1894
|
+
publishableKey: publishableKey(),
|
|
1890
1895
|
now: deps.now,
|
|
1891
1896
|
});
|
|
1892
1897
|
},
|
|
1893
1898
|
};
|
|
1894
1899
|
}
|
|
1900
|
+
/**
|
|
1901
|
+
* SETUP — the enrollment `agent-board setup` runs: this runner's device flow, built from setup's
|
|
1902
|
+
* env with the `key` step's reference injected (never an env var the user must set).
|
|
1903
|
+
*/
|
|
1904
|
+
export const setupEnroller = async (input) => {
|
|
1905
|
+
const runner = defaultEnrollmentRunner(input.env, { keyReference: input.keyReference });
|
|
1906
|
+
return (await runner.device({ slug: input.slug, intent: input.intent, onDeviceAuthorization: input.onDeviceAuthorization }));
|
|
1907
|
+
};
|
|
1895
1908
|
function intentFromOptions(options) {
|
|
1896
1909
|
if (options.board === false) {
|
|
1897
1910
|
return { kind: "none" };
|
|
@@ -3199,11 +3212,33 @@ function addDoctorCommand(program, io, doctor) {
|
|
|
3199
3212
|
process.exitCode = report.exit;
|
|
3200
3213
|
});
|
|
3201
3214
|
}
|
|
3202
|
-
|
|
3215
|
+
/**
|
|
3216
|
+
* The lazy credential source. With `--as <h>`: `<h>`'s identity file through the client's trusted
|
|
3217
|
+
* open, and ONLY the file — `fileIdentityEnv` strips every ambient identity and generic name, as
|
|
3218
|
+
* doctor does, because `--as` is an explicit choice of the file. The runtime descriptor is ignored
|
|
3219
|
+
* for the same reason. Without it: `resolveCredentials(descriptor, env)`, exactly as before.
|
|
3220
|
+
*/
|
|
3221
|
+
export function fileAwareCredentials(selection, env = process.env) {
|
|
3222
|
+
return async (descriptor) => {
|
|
3223
|
+
const harness = selection.as;
|
|
3224
|
+
if (harness === undefined)
|
|
3225
|
+
return resolveCredentials(descriptor, env);
|
|
3226
|
+
assertIdentityHarness(harness);
|
|
3227
|
+
const { values } = await readIdentityFile({ harness, env });
|
|
3228
|
+
return resolveCredentials(undefined, fileIdentityEnv(env, values));
|
|
3229
|
+
};
|
|
3230
|
+
}
|
|
3231
|
+
export function createProgram(client, io, enrollment = defaultEnrollmentRunner(), doctor = runDoctor, selection = {}, setup) {
|
|
3203
3232
|
const program = new Command()
|
|
3204
3233
|
.name("agent-board")
|
|
3205
3234
|
.description("Thin CLI surface for the agent_board RPC and canonical read contracts")
|
|
3235
|
+
.option("--as <harness>", "take credentials from this harness's identity file (agy, claude, codex, grok, opencode, pi), ignoring any identity in the environment")
|
|
3206
3236
|
.addHelpText("after", "\nAuth: user-scoped agent sessions are the default. Service-role execution is reserved for callback workers and vetted operational scripts through explicit client configuration, not the normal agent CLI path. Signing configuration is inherited from metadata-safe descriptors; raw private-key arguments are not accepted.\n");
|
|
3237
|
+
// Filled at parse time, read by the client's resolver at first use.
|
|
3238
|
+
program.hook("preAction", () => {
|
|
3239
|
+
const chosen = program.opts().as;
|
|
3240
|
+
selection.as = chosen;
|
|
3241
|
+
});
|
|
3207
3242
|
program
|
|
3208
3243
|
.command("signing-status")
|
|
3209
3244
|
.description("Print metadata-safe signing descriptor status; raw private-key arguments are not accepted")
|
|
@@ -3224,12 +3259,19 @@ export function createProgram(client, io, enrollment = defaultEnrollmentRunner()
|
|
|
3224
3259
|
addInviteCommands(program, client, io);
|
|
3225
3260
|
addEnrollCommands(program, io, enrollment);
|
|
3226
3261
|
addDoctorCommand(program, io, doctor);
|
|
3262
|
+
addSetupCommand(program, io, setup ?? ((options) => runSetup(options, { enroll: setupEnroller })));
|
|
3227
3263
|
return program;
|
|
3228
3264
|
}
|
|
3229
3265
|
export async function runCli(argv, io = {
|
|
3230
3266
|
writeStdout: (message) => process.stdout.write(message),
|
|
3231
3267
|
writeStderr: (message) => process.stderr.write(message),
|
|
3232
|
-
}, client
|
|
3233
|
-
const
|
|
3268
|
+
}, client, enrollment = defaultEnrollmentRunner(), doctor = runDoctor, options = {}) {
|
|
3269
|
+
const selection = {};
|
|
3270
|
+
// Built BEFORE parsing, as always; `--as` reaches it through `selection` at first use.
|
|
3271
|
+
const resolved = client ?? createAgentBoardClient({
|
|
3272
|
+
...options.clientOptions,
|
|
3273
|
+
resolveCredentials: fileAwareCredentials(selection, options.env ?? process.env),
|
|
3274
|
+
});
|
|
3275
|
+
const program = createProgram(resolved, io, enrollment, doctor, selection, options.setup);
|
|
3234
3276
|
await program.parseAsync(argv, { from: "user" });
|
|
3235
3277
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/** What an edit found and produced. `text` equals the input when `prior` is `exact`. */
|
|
2
|
+
export interface ConfigEdit {
|
|
3
|
+
prior: "absent" | "exact" | "differs";
|
|
4
|
+
text: string;
|
|
5
|
+
}
|
|
6
|
+
/** A refusal: the reason is an authored code, never file content. */
|
|
7
|
+
export declare class ConfigEditRefused extends Error {
|
|
8
|
+
readonly reason: "config_unparseable" | "config_layout_unrecognised" | "config_not_an_object" | "config_inline_table" | "config_array_table";
|
|
9
|
+
constructor(reason: "config_unparseable" | "config_layout_unrecognised" | "config_not_an_object" | "config_inline_table" | "config_array_table");
|
|
10
|
+
}
|
|
11
|
+
/** A value an entry may carry: what setup writes, plus whatever a user kept in the entry. */
|
|
12
|
+
export type ConfigValue = string | number | boolean | readonly ConfigValue[] | {
|
|
13
|
+
readonly [key: string]: ConfigValue;
|
|
14
|
+
};
|
|
15
|
+
export type ConfigEntry = {
|
|
16
|
+
readonly [key: string]: ConfigValue;
|
|
17
|
+
};
|
|
18
|
+
export declare function deepEqual(a: unknown, b: unknown): boolean;
|
|
19
|
+
/** The block setup writes for `path` (a table header plus one line per key, in the entry's order). */
|
|
20
|
+
export declare function renderTomlTable(path: readonly string[], entry: ConfigEntry): string;
|
|
21
|
+
/**
|
|
22
|
+
* Put `entry` at the table `path` (e.g. `["mcp_servers", "agent-board"]`) of a TOML document.
|
|
23
|
+
* Absent → appended as its own block; equal → the text unchanged; different → the table's own
|
|
24
|
+
* block(s) (its header, its sub-tables' headers, and their keys) replaced in place by the rendered
|
|
25
|
+
* block. Keys defined for it any other way (a dotted key under a parent table, an inline table)
|
|
26
|
+
* are a layout this edit does not own: refused, never appended beside.
|
|
27
|
+
*/
|
|
28
|
+
export declare function upsertTomlTable(text: string, path: readonly string[], entry: ConfigEntry): ConfigEdit;
|
|
29
|
+
/**
|
|
30
|
+
* Put `entry` at `path` of a JSON/JSONC object document. Empty text is an empty object. Missing
|
|
31
|
+
* intermediate objects are created; an existing member is replaced in its span; a new member is
|
|
32
|
+
* inserted before its object's closing brace. `strict` also requires the input to be plain JSON,
|
|
33
|
+
* for a harness whose reader is strict (an edit never makes a file unreadable to its harness).
|
|
34
|
+
*/
|
|
35
|
+
export declare function upsertJsonMember(text: string, path: readonly string[], entry: ConfigEntry, options?: {
|
|
36
|
+
strict?: boolean;
|
|
37
|
+
}): ConfigEdit;
|