@consiliency/agent-board-cli 1.28.0 → 1.29.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/claude-marketplace/plugins/agent-board/.claude-plugin/plugin.json +2 -2
- package/dist/assets.json +4 -4
- package/dist/cli.d.ts +5 -2
- package/dist/cli.js +39 -12
- package/dist/doctor.d.ts +82 -2
- package/dist/doctor.js +170 -4
- package/dist/identity_writer.d.ts +1 -46
- package/dist/identity_writer.js +5 -174
- package/dist/keyslot_status.d.ts +17 -0
- package/dist/keyslot_status.js +108 -2
- package/dist/setup.d.ts +112 -5
- package/dist/setup.js +287 -61
- package/dist/setup_all.d.ts +51 -0
- package/dist/setup_all.js +164 -0
- package/dist/setup_storage.d.ts +39 -1
- package/dist/setup_storage.js +99 -37
- package/dist/setup_writers.d.ts +7 -0
- package/dist/setup_writers.js +11 -2
- package/keyslot/lib.sh +88 -47
- package/keyslot/status.sh +411 -246
- package/package.json +2 -2
package/dist/identity_writer.js
CHANGED
|
@@ -1,174 +1,5 @@
|
|
|
1
|
-
// CREDPATH SL-2 — the identity file
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
|
|
6
|
-
// (Consiliency/message-board#521): `resolveIdentityDeployment` runs BEFORE enrollment commits,
|
|
7
|
-
// so a missing or secret-shaped deployment value is refused while the token is still unspent.
|
|
8
|
-
// Only then does enrollment run, and `identityValuesFromEnrollment` + `writeIdentityFile` turn
|
|
9
|
-
// its outcome into the file.
|
|
10
|
-
import { createHash } from "node:crypto";
|
|
11
|
-
import { constants as FS } from "node:fs";
|
|
12
|
-
import * as fs from "node:fs/promises";
|
|
13
|
-
import { dirname, join } from "node:path";
|
|
14
|
-
import { CredentialResolutionError, DEPLOYMENT_ENV_ALIASES, describeMissingDeploymentFields, identityConfigRoot, resolveDeploymentField, identityFilePath, serializeIdentityFile, walkTrustedDirectory, } from "@consiliency/agent-board-client";
|
|
15
|
-
async function syncDirectory(path) {
|
|
16
|
-
const handle = await fs.open(path, "r");
|
|
17
|
-
try {
|
|
18
|
-
await handle.sync();
|
|
19
|
-
}
|
|
20
|
-
finally {
|
|
21
|
-
await handle.close();
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
/** Create `path` exclusively (never following a planted link) at 0600, write, fsync. */
|
|
25
|
-
export async function writeOwnerOnlyFile(path, content) {
|
|
26
|
-
const handle = await fs.open(path, FS.O_CREAT | FS.O_EXCL | FS.O_WRONLY | FS.O_NOFOLLOW, 0o600);
|
|
27
|
-
try {
|
|
28
|
-
await handle.writeFile(content, "utf8");
|
|
29
|
-
await handle.sync();
|
|
30
|
-
}
|
|
31
|
-
finally {
|
|
32
|
-
await handle.close();
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
/**
|
|
36
|
-
* Write `content` to `path` so a reader sees the old file or the whole new one, never a
|
|
37
|
-
* part: an exclusive 0600 temp beside it, fsync'd, renamed over it, then the directory
|
|
38
|
-
* fsync'd. On any throw before the rename the temp is removed.
|
|
39
|
-
*/
|
|
40
|
-
export async function writeOwnerOnlyFileAtomic(path, content, options = {}) {
|
|
41
|
-
const sync = options.syncDirectory ?? syncDirectory;
|
|
42
|
-
const temp = `${path}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
|
43
|
-
try {
|
|
44
|
-
await writeOwnerOnlyFile(temp, content);
|
|
45
|
-
await fs.rename(temp, path);
|
|
46
|
-
}
|
|
47
|
-
catch (error) {
|
|
48
|
-
await fs.unlink(temp).catch(() => { });
|
|
49
|
-
throw error;
|
|
50
|
-
}
|
|
51
|
-
await sync(dirname(path));
|
|
52
|
-
}
|
|
53
|
-
/**
|
|
54
|
-
* Where each deployment value is read, in precedence order: the client's one alias table
|
|
55
|
-
* (`DEPLOYMENT_ENV_ALIASES`), which puts the enrollment's own names first (so the file records
|
|
56
|
-
* the deployment the enrollment used) and then the identity names. The enrollment resolvers read
|
|
57
|
-
* the same table, so the two cannot disagree. The generic Supabase names are in no list: a
|
|
58
|
-
* generic key never fills an identity field, here or in the loader.
|
|
59
|
-
*/
|
|
60
|
-
export const DEPLOYMENT_ENV_NAMES = Object.freeze({
|
|
61
|
-
boardUrl: DEPLOYMENT_ENV_ALIASES.boardUrl,
|
|
62
|
-
sessionUrl: DEPLOYMENT_ENV_ALIASES.sessionUrl,
|
|
63
|
-
publishableKey: DEPLOYMENT_ENV_ALIASES.publishableKey,
|
|
64
|
-
});
|
|
65
|
-
const DEPLOYMENT_FILE_KEYS = {
|
|
66
|
-
boardUrl: "AGENT_BOARD_URL",
|
|
67
|
-
sessionUrl: "AGENT_BOARD_SESSION_URL",
|
|
68
|
-
publishableKey: "AGENT_BOARD_PUBLISHABLE_KEY",
|
|
69
|
-
};
|
|
70
|
-
// Well-shaped stand-ins for the values only the enrollment outcome supplies. The preflight
|
|
71
|
-
// substitutes ONE deployment value into this map and lets `serializeIdentityFile` judge it, so
|
|
72
|
-
// the shape rule applied here is the parser's own, never a second copy.
|
|
73
|
-
const PREFLIGHT_STAND_INS = {
|
|
74
|
-
AGENT_BOARD_URL: "https://preflight.invalid",
|
|
75
|
-
AGENT_BOARD_SESSION_URL: "https://preflight.invalid/session",
|
|
76
|
-
AGENT_BOARD_PUBLISHABLE_KEY: "sb_publishable_preflight",
|
|
77
|
-
AGENT_BOARD_AGENT_SLUG: "preflight",
|
|
78
|
-
AGENT_BOARD_EXPECTED_AGENT_HANDLE: "preflight",
|
|
79
|
-
AGENT_BOARD_AGENT_KEY_REFERENCE: "os_keychain:preflight",
|
|
80
|
-
AGENT_BOARD_SIGNING_KEY_ID: "preflight",
|
|
81
|
-
AGENT_BOARD_SIGNING_KEY_VERSION: "1",
|
|
82
|
-
AGENT_BOARD_SIGNING_PUBLIC_KEY_FINGERPRINT: `sha256:${"A".repeat(43)}`,
|
|
83
|
-
MESSAGE_BOARD_AGENT_AUTH_MODE: "keystore_session",
|
|
84
|
-
};
|
|
85
|
-
/**
|
|
86
|
-
* The deployment half of an identity, read and shape-checked before enrollment commits. Each
|
|
87
|
-
* value is the shipped default, or env under the board group's all-or-nothing rule (the
|
|
88
|
-
* client's `resolveDeploymentField`). Absent from an env-set group → `identity_incomplete`,
|
|
89
|
-
* naming the variable that switched the group off; not the non-secret shape its key requires →
|
|
90
|
-
* `identity_secret_value`. Both name the variable, never the value.
|
|
91
|
-
*/
|
|
92
|
-
export function resolveIdentityDeployment(env) {
|
|
93
|
-
const deployment = {};
|
|
94
|
-
for (const field of Object.keys(DEPLOYMENT_ENV_NAMES)) {
|
|
95
|
-
const resolved = resolveDeploymentField(env, field);
|
|
96
|
-
if (resolved.value === undefined) {
|
|
97
|
-
throw new CredentialResolutionError("identity_incomplete", `cannot write an identity file: no ${DEPLOYMENT_FILE_KEYS[field]} for it ${describeMissingDeploymentFields([field], resolved.switchedBy)}`);
|
|
98
|
-
}
|
|
99
|
-
// A default is named by the file key it fills: it came from no variable.
|
|
100
|
-
const found = { name: resolved.name ?? `the default ${DEPLOYMENT_FILE_KEYS[field]}`, value: resolved.value };
|
|
101
|
-
try {
|
|
102
|
-
serializeIdentityFile({ ...PREFLIGHT_STAND_INS, [DEPLOYMENT_FILE_KEYS[field]]: found.value });
|
|
103
|
-
}
|
|
104
|
-
catch (error) {
|
|
105
|
-
if (error.code !== "identity_secret_value")
|
|
106
|
-
throw error;
|
|
107
|
-
throw new CredentialResolutionError("identity_secret_value", `${found.name} is not the non-secret shape ${DEPLOYMENT_FILE_KEYS[field]} requires; an identity file carries public values only`);
|
|
108
|
-
}
|
|
109
|
-
deployment[field] = found.value;
|
|
110
|
-
}
|
|
111
|
-
return deployment;
|
|
112
|
-
}
|
|
113
|
-
// ---------------------------------------------------------------------------------------------
|
|
114
|
-
// Outcome → values → file
|
|
115
|
-
// ---------------------------------------------------------------------------------------------
|
|
116
|
-
/** `sha256:` + base64url, over the DECODED public key — exactly what fleet finalize recomputes. */
|
|
117
|
-
function publicKeyFingerprint(agentPublicKey) {
|
|
118
|
-
const publicKey = Buffer.from(agentPublicKey, "base64url");
|
|
119
|
-
// The same refusal finalize makes: a value that is not canonical base64url of 32 bytes would
|
|
120
|
-
// still hash to a plausible fingerprint, of a key that is not the enrolled one. The
|
|
121
|
-
// re-encode comparison is also the charset and padding check.
|
|
122
|
-
if (publicKey.length !== 32 || publicKey.toString("base64url") !== agentPublicKey) {
|
|
123
|
-
throw new CredentialResolutionError("identity_public_key_invalid", "the enrollment outcome's public key is not a 32-byte base64url key");
|
|
124
|
-
}
|
|
125
|
-
return `sha256:${createHash("sha256").update(publicKey).digest("base64url")}`;
|
|
126
|
-
}
|
|
127
|
-
/**
|
|
128
|
-
* The identity file's values for an enrolled agent. The URLs and publishable key come only
|
|
129
|
-
* from `deployment`; everything else from the outcome. Validated by the client's serializer
|
|
130
|
-
* before it is returned.
|
|
131
|
-
*/
|
|
132
|
-
export function identityValuesFromEnrollment(outcome, deployment) {
|
|
133
|
-
const values = {
|
|
134
|
-
AGENT_BOARD_URL: deployment.boardUrl,
|
|
135
|
-
AGENT_BOARD_SESSION_URL: deployment.sessionUrl,
|
|
136
|
-
AGENT_BOARD_PUBLISHABLE_KEY: deployment.publishableKey,
|
|
137
|
-
AGENT_BOARD_AGENT_SLUG: outcome.agentHandle,
|
|
138
|
-
AGENT_BOARD_EXPECTED_AGENT_HANDLE: outcome.agentHandle,
|
|
139
|
-
// The form `fleet_onboarding.ts` checks: `${resolver}:${handle}`.
|
|
140
|
-
AGENT_BOARD_AGENT_KEY_REFERENCE: `${outcome.keyReference.resolver}:${outcome.keyReference.handle}`,
|
|
141
|
-
// `keyId`, the server's key identity /session mints against — NOT `signingKeyId`, the row.
|
|
142
|
-
AGENT_BOARD_SIGNING_KEY_ID: outcome.keyId,
|
|
143
|
-
AGENT_BOARD_SIGNING_KEY_VERSION: String(outcome.keyVersion),
|
|
144
|
-
AGENT_BOARD_SIGNING_PUBLIC_KEY_FINGERPRINT: publicKeyFingerprint(outcome.agentPublicKey),
|
|
145
|
-
MESSAGE_BOARD_AGENT_AUTH_MODE: "keystore_session",
|
|
146
|
-
};
|
|
147
|
-
serializeIdentityFile(values);
|
|
148
|
-
return values;
|
|
149
|
-
}
|
|
150
|
-
async function mkdirPrivate(path) {
|
|
151
|
-
await fs.mkdir(path, { mode: 0o700 }).catch((error) => {
|
|
152
|
-
if (error.code !== "EEXIST")
|
|
153
|
-
throw error;
|
|
154
|
-
});
|
|
155
|
-
}
|
|
156
|
-
/**
|
|
157
|
-
* Write `harness`'s identity file for this host. Every refusal (bad harness, relative root,
|
|
158
|
-
* invalid host, bad value) fires before the first filesystem change. The directory is then
|
|
159
|
-
* put through the reader's own trusted walk, so the writer never produces a file the loader
|
|
160
|
-
* would refuse to open. Returns the path written.
|
|
161
|
-
*/
|
|
162
|
-
export async function writeIdentityFile(harness, values, env, options = {}) {
|
|
163
|
-
const path = identityFilePath(harness, env);
|
|
164
|
-
const content = serializeIdentityFile(values);
|
|
165
|
-
const configRoot = identityConfigRoot(env);
|
|
166
|
-
const strictFrom = join(configRoot, "agent-board");
|
|
167
|
-
await fs.mkdir(configRoot, { recursive: true, mode: 0o700 });
|
|
168
|
-
await mkdirPrivate(strictFrom);
|
|
169
|
-
await mkdirPrivate(join(strictFrom, "identities"));
|
|
170
|
-
await mkdirPrivate(dirname(path));
|
|
171
|
-
walkTrustedDirectory(dirname(path), strictFrom);
|
|
172
|
-
await writeOwnerOnlyFileAtomic(path, content, options);
|
|
173
|
-
return path;
|
|
174
|
-
}
|
|
1
|
+
// CREDPATH SL-2 — the identity file writer. ONBOARD SL-1 (IF-0-ONBOARD-9) moved its bodies to the
|
|
2
|
+
// client (`@consiliency/agent-board-client`, `identity_writer.ts`) so the MCP server's in-chat
|
|
3
|
+
// enrollment writes the same file through the same code; this module keeps every export, as a
|
|
4
|
+
// re-export, so no cli caller changes.
|
|
5
|
+
export { DEPLOYMENT_ENV_NAMES, identityValuesFromEnrollment, resolveIdentityDeployment, writeIdentityFile, writeOwnerOnlyFile, writeOwnerOnlyFileAtomic, } from "@consiliency/agent-board-client";
|
package/dist/keyslot_status.d.ts
CHANGED
|
@@ -115,3 +115,20 @@ export declare function cacheStateOf(report: Record<string, unknown> | null | un
|
|
|
115
115
|
};
|
|
116
116
|
/** IF-552-3: measure a keyslot in up to two phases. Never throws for a child's failure. */
|
|
117
117
|
export declare function measureKeyslot(options: MeasureOptions): Promise<KeyslotMeasurement>;
|
|
118
|
+
export interface MeasureSlotsOptions extends Omit<MeasureOptions, "harness" | "slot" | "notify" | "skipScan"> {
|
|
119
|
+
/** The slots to measure. Each name is ALSO its harness: `status.sh --slots` takes no `--harness`. */
|
|
120
|
+
slots: readonly string[];
|
|
121
|
+
/** Per-slot events; `progress` is the one shared walk's, so its slot is null. */
|
|
122
|
+
notify?: (slot: string | null, event: ScanEvent) => void;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* ONBOARD SL-4 (IF-0-ONBOARD-1 "a key scan at most once per host"): several keyslots measured with
|
|
126
|
+
* ONE filesystem walk, in the same two phases as `measureKeyslot`. One slot IS `measureKeyslot`
|
|
127
|
+
* (the `--harness` form, byte-identical to a single-harness setup). Two or more use
|
|
128
|
+
* `status.sh --slots a,b,… --json`: phase 1 (`--scan-cache only`) for every slot, then ONE phase 2
|
|
129
|
+
* over just the slots phase 1 left without a valid cache. That form has no `--harness`, so each
|
|
130
|
+
* name is both the harness and the slot — true for setup, which never names a slot of its own.
|
|
131
|
+
* Each slot's phase `code` is its own line's `exit`; the process exit is only the worst of them.
|
|
132
|
+
* Lines are matched by their `slot` field, never by position. Never throws for a child's failure.
|
|
133
|
+
*/
|
|
134
|
+
export declare function measureKeyslots(options: MeasureSlotsOptions): Promise<Map<string, KeyslotMeasurement>>;
|
package/dist/keyslot_status.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
// #552 IF-552-3 — the ONE place in the cli that spawns the packaged keyslot `status.sh`. Setup (the
|
|
2
2
|
// key step, the loader refresh, the re-read before enrolling) and doctor (`key_storage`) all measure
|
|
3
3
|
// a keyslot through `measureKeyslot`, so the scan cache, the two budgets and the process-group
|
|
4
|
-
// cleanup are decided once.
|
|
5
|
-
//
|
|
4
|
+
// cleanup are decided once. `setup --all` measures several slots through `measureKeyslots`
|
|
5
|
+
// (`status.sh --slots`, one walk). KSC-7 pins that no other cli source names `"status.sh"` as a
|
|
6
|
+
// call argument (by the TypeScript AST).
|
|
6
7
|
//
|
|
7
8
|
// phase 1 `status.sh --scan-cache only` under the STATUS budget (180 s): every measurement, and
|
|
8
9
|
// the scan only if a valid cache holds it. Never scans.
|
|
@@ -240,3 +241,108 @@ export async function measureKeyslot(options) {
|
|
|
240
241
|
notify({ kind: "result", phase: phase2 });
|
|
241
242
|
return result();
|
|
242
243
|
}
|
|
244
|
+
/** status.sh `--slots` output: one JSON object per line, each naming its own slot. */
|
|
245
|
+
const parseSlotReports = (stdout) => {
|
|
246
|
+
const reports = new Map();
|
|
247
|
+
for (const line of stdout.split("\n")) {
|
|
248
|
+
const report = parseReport(line);
|
|
249
|
+
if (report && typeof report.slot === "string" && !reports.has(report.slot))
|
|
250
|
+
reports.set(report.slot, report);
|
|
251
|
+
}
|
|
252
|
+
return reports;
|
|
253
|
+
};
|
|
254
|
+
/**
|
|
255
|
+
* ONBOARD SL-4 (IF-0-ONBOARD-1 "a key scan at most once per host"): several keyslots measured with
|
|
256
|
+
* ONE filesystem walk, in the same two phases as `measureKeyslot`. One slot IS `measureKeyslot`
|
|
257
|
+
* (the `--harness` form, byte-identical to a single-harness setup). Two or more use
|
|
258
|
+
* `status.sh --slots a,b,… --json`: phase 1 (`--scan-cache only`) for every slot, then ONE phase 2
|
|
259
|
+
* over just the slots phase 1 left without a valid cache. That form has no `--harness`, so each
|
|
260
|
+
* name is both the harness and the slot — true for setup, which never names a slot of its own.
|
|
261
|
+
* Each slot's phase `code` is its own line's `exit`; the process exit is only the worst of them.
|
|
262
|
+
* Lines are matched by their `slot` field, never by position. Never throws for a child's failure.
|
|
263
|
+
*/
|
|
264
|
+
export async function measureKeyslots(options) {
|
|
265
|
+
const slots = [...new Set(options.slots)];
|
|
266
|
+
const measured = new Map();
|
|
267
|
+
if (slots.length === 1) {
|
|
268
|
+
const [slot] = slots;
|
|
269
|
+
const { notify, slots: _slots, ...rest } = options;
|
|
270
|
+
void _slots;
|
|
271
|
+
measured.set(slot, await measureKeyslot({ ...rest, harness: slot, slot, ...(notify ? { notify: (event) => notify(slot, event) } : {}) }));
|
|
272
|
+
return measured;
|
|
273
|
+
}
|
|
274
|
+
if (slots.length === 0)
|
|
275
|
+
return measured;
|
|
276
|
+
const file = join(options.dir, "status.sh");
|
|
277
|
+
if (!existsSync(file)) {
|
|
278
|
+
for (const slot of slots)
|
|
279
|
+
measured.set(slot, { absent: true, code: null, report: null, phases: [], timedOut: false, folders: null });
|
|
280
|
+
return measured;
|
|
281
|
+
}
|
|
282
|
+
const run = options.run ?? defaultStorageRunner;
|
|
283
|
+
const budgets = { ...DEFAULT_BUDGETS, ...options.budgets };
|
|
284
|
+
const notify = options.notify ?? (() => { });
|
|
285
|
+
const phases = new Map(slots.map((slot) => [slot, []]));
|
|
286
|
+
const scanTimedOut = new Set();
|
|
287
|
+
let folders = null;
|
|
288
|
+
const call = async (mode, names) => {
|
|
289
|
+
const scanning = mode !== "only";
|
|
290
|
+
const args = ["--slots", names.join(","), "--json", "--scan-cache", mode, ...(scanning ? ["--scan-timeout", String(Math.max(1, Math.ceil(budgets.scanMs / 1000)))] : [])];
|
|
291
|
+
const outcome = await run(file, args, {
|
|
292
|
+
env: { ...options.env, KEYSEAL_SCAN_WATCH_PID: String(process.pid) },
|
|
293
|
+
timeoutMs: scanning ? budgets.scanMs + budgets.backstopMs : budgets.statusMs,
|
|
294
|
+
onStderrLine: scanning
|
|
295
|
+
? (line) => {
|
|
296
|
+
const match = PROGRESS_LINE.exec(line);
|
|
297
|
+
if (!match)
|
|
298
|
+
return;
|
|
299
|
+
folders = Number(match[1]);
|
|
300
|
+
notify(null, { kind: "progress", folders });
|
|
301
|
+
}
|
|
302
|
+
: undefined,
|
|
303
|
+
});
|
|
304
|
+
const reports = parseSlotReports(outcome.stdout);
|
|
305
|
+
const called = new Map();
|
|
306
|
+
for (const name of names) {
|
|
307
|
+
const report = reports.get(name) ?? null;
|
|
308
|
+
// A process that did not exit on its own judged nothing, whatever it printed first.
|
|
309
|
+
const code = outcome.code === null ? null : typeof report?.exit === "number" ? report.exit : outcome.code;
|
|
310
|
+
const phase = { mode, code, timedOut: outcome.timedOut === true, report: outcome.code === null ? null : report };
|
|
311
|
+
phases.get(name).push(phase);
|
|
312
|
+
called.set(name, phase);
|
|
313
|
+
}
|
|
314
|
+
return called;
|
|
315
|
+
};
|
|
316
|
+
let toScan = slots;
|
|
317
|
+
const phase1 = options.scan === "fresh" ? null : await call("only", slots);
|
|
318
|
+
if (phase1 !== null) {
|
|
319
|
+
if (options.scan === "only")
|
|
320
|
+
toScan = [];
|
|
321
|
+
else {
|
|
322
|
+
toScan = slots.filter((slot) => {
|
|
323
|
+
const phase = phase1.get(slot);
|
|
324
|
+
if (phase.code === null || phase.report === null)
|
|
325
|
+
return false;
|
|
326
|
+
if (scanSourceOf(phase.report) !== "none")
|
|
327
|
+
return false;
|
|
328
|
+
return !(phase.code === 1 && (options.onFailure ?? "stop") === "stop");
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
if (toScan.length > 0) {
|
|
333
|
+
for (const slot of toScan)
|
|
334
|
+
notify(slot, { kind: "before", phase1: phase1?.get(slot)?.report ?? null });
|
|
335
|
+
const phase2 = await call(options.scan === "fresh" ? "fresh" : "use", toScan);
|
|
336
|
+
for (const [slot, phase] of phase2) {
|
|
337
|
+
if (phase.timedOut)
|
|
338
|
+
scanTimedOut.add(slot);
|
|
339
|
+
notify(slot, { kind: "result", phase });
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
for (const slot of slots) {
|
|
343
|
+
const list = phases.get(slot);
|
|
344
|
+
const last = list.at(-1);
|
|
345
|
+
measured.set(slot, { absent: false, code: last ? last.code : null, report: last ? last.report : null, phases: list, timedOut: scanTimedOut.has(slot), folders });
|
|
346
|
+
}
|
|
347
|
+
return measured;
|
|
348
|
+
}
|
package/dist/setup.d.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import type { Command } from "commander";
|
|
2
|
-
import { type DeviceAuthorizationPrompt, type EnrollmentIntent, type EnrollmentOutcome } from "@consiliency/agent-board-client";
|
|
2
|
+
import { type DeviceAuthorizationPrompt, type EnrollmentIntent, type EnrollmentOutcome, type IdentityHarness, type ListHarnessProcessesOptions, type ParsedInviteInput } from "@consiliency/agent-board-client";
|
|
3
|
+
import type { SetupAllOptions } from "./setup_all.js";
|
|
3
4
|
import { type BakedAssets } from "./assets.js";
|
|
4
5
|
import { type DoctorOptions, type DoctorReport } from "./doctor.js";
|
|
5
|
-
import { type
|
|
6
|
-
import { type
|
|
6
|
+
import { type IdentityDeployment } from "./identity_writer.js";
|
|
7
|
+
import { type DepsOutcome, type KeyStorageContext, type KeyStorageSelection, type PreparedKeyStorage } from "./setup_storage.js";
|
|
8
|
+
import { type ChildRunner, type SetupScope, type WriterContext } from "./setup_writers.js";
|
|
7
9
|
export declare const SETUP_STEPS: readonly ["preflight", "deps", "key", "enroll", "identity", "board", "harness", "doctor"];
|
|
8
10
|
export type SetupStepName = (typeof SETUP_STEPS)[number];
|
|
9
11
|
export type SetupStatus = "done" | "already" | "skipped" | "failed" | "undetermined";
|
|
@@ -17,11 +19,24 @@ export interface SetupReport {
|
|
|
17
19
|
exit: 0 | 1 | 2;
|
|
18
20
|
/** Human text: per-step fixes and the next steps. Never part of `--json`'s stdout. */
|
|
19
21
|
notes: string[];
|
|
22
|
+
/**
|
|
23
|
+
* IF-0-ONBOARD-3: this harness's processes that started before setup installed or upgraded its
|
|
24
|
+
* registration, found by executable. Empty when the harness step installed nothing.
|
|
25
|
+
*/
|
|
26
|
+
staleSessions: StaleSession[];
|
|
27
|
+
}
|
|
28
|
+
export interface StaleSession {
|
|
29
|
+
pid: number;
|
|
30
|
+
/** ISO time the process started. */
|
|
31
|
+
startedAt: string;
|
|
32
|
+
cwd: string | null;
|
|
20
33
|
}
|
|
21
34
|
export interface SetupOptions {
|
|
22
35
|
harness: string;
|
|
23
36
|
slug?: string;
|
|
24
37
|
invite?: boolean;
|
|
38
|
+
/** IF-0-ONBOARD-2: a file holding the invite (a bare code, or the whole invite message). */
|
|
39
|
+
inviteFile?: string;
|
|
25
40
|
allowUnsealedKey?: boolean;
|
|
26
41
|
agentSocket?: string;
|
|
27
42
|
scope?: string;
|
|
@@ -61,7 +76,10 @@ export interface SetupDeps {
|
|
|
61
76
|
sudoPath?: string;
|
|
62
77
|
hostname?: () => string;
|
|
63
78
|
bakedAssets?: () => BakedAssets;
|
|
79
|
+
/** A whole selector, scan included; its answer is final (the pre-ONBOARD seam). */
|
|
64
80
|
selectKeyStorage?: (ctx: KeyStorageContext) => Promise<KeyStorageSelection>;
|
|
81
|
+
/** The key step's preparation (no scan); default `prepareKeyStorage`. */
|
|
82
|
+
prepareKeyStorage?: (ctx: KeyStorageContext) => Promise<PreparedKeyStorage>;
|
|
65
83
|
/** Seams passed through to the storage selector (probeKeychain, run, keyslotDir, …). */
|
|
66
84
|
storage?: Partial<KeyStorageContext>;
|
|
67
85
|
enroll?: SetupEnroller;
|
|
@@ -71,6 +89,8 @@ export interface SetupDeps {
|
|
|
71
89
|
doctorOptions?: Partial<DoctorOptions>;
|
|
72
90
|
/** Seams passed through to the harness writer (fs, claudeManagedDir). */
|
|
73
91
|
writer?: Partial<Pick<WriterContext, "fs" | "claudeManagedDir">>;
|
|
92
|
+
/** Seams passed through to the stale-session listing (`/proc` or `ps` readers, uid, platform). */
|
|
93
|
+
processes?: Omit<ListHarnessProcessesOptions, "startedBefore">;
|
|
74
94
|
}
|
|
75
95
|
export declare const DEFAULT_SUDO = "/usr/bin/sudo";
|
|
76
96
|
export declare const SECRET_SERVICE_NAME = "org.freedesktop.secrets";
|
|
@@ -90,7 +110,22 @@ export declare const KEYCHAIN_INSTALL_ARGV: ReadonlyArray<{
|
|
|
90
110
|
}>;
|
|
91
111
|
/** `<short hostname>-<harness>`, lowercased to `[a-z0-9-]`. */
|
|
92
112
|
export declare function defaultSlug(host: string, harness: string): string;
|
|
93
|
-
|
|
113
|
+
type IdentityState = {
|
|
114
|
+
kind: "present";
|
|
115
|
+
values: Record<string, string>;
|
|
116
|
+
} | {
|
|
117
|
+
kind: "absent";
|
|
118
|
+
} | {
|
|
119
|
+
kind: "unreadable";
|
|
120
|
+
reason: string;
|
|
121
|
+
};
|
|
122
|
+
declare class Run {
|
|
123
|
+
readonly steps: SetupStep[];
|
|
124
|
+
readonly notes: string[];
|
|
125
|
+
readonly staleSessions: StaleSession[];
|
|
126
|
+
record(step: SetupStep["step"], status: SetupStatus, reason: string, fix?: string): SetupStep;
|
|
127
|
+
report(): SetupReport;
|
|
128
|
+
}
|
|
94
129
|
interface DepsContext {
|
|
95
130
|
env: NodeJS.ProcessEnv;
|
|
96
131
|
platform: NodeJS.Platform;
|
|
@@ -105,6 +140,74 @@ interface DepsContext {
|
|
|
105
140
|
export declare function depsStep(ctx: DepsContext): Promise<DepsOutcome & {
|
|
106
141
|
fix?: string;
|
|
107
142
|
}>;
|
|
143
|
+
/**
|
|
144
|
+
* IF-0-ONBOARD-2: the invite, read ONCE and parsed by the shared parser. `--invite-file` and a
|
|
145
|
+
* piped stdin may hold the whole invite message; a terminal takes one hidden line that must be a
|
|
146
|
+
* bare code. The code never reaches argv, a log or any message: a refusal names only its reason.
|
|
147
|
+
*/
|
|
148
|
+
export declare function readSetupInvite(input: {
|
|
149
|
+
invite?: boolean;
|
|
150
|
+
inviteFile?: string;
|
|
151
|
+
}, deps: Pick<SetupDeps, "interactive" | "prompt" | "readStdin">): Promise<{
|
|
152
|
+
invite: ParsedInviteInput | null;
|
|
153
|
+
} | {
|
|
154
|
+
reason: string;
|
|
155
|
+
fix: string;
|
|
156
|
+
}>;
|
|
157
|
+
/** IF-0-ONBOARD-2 "Board": reported, never checked (setup has no target board). Never the code. */
|
|
158
|
+
export declare function inviteBoardNote(invite: ParsedInviteInput): string;
|
|
159
|
+
/**
|
|
160
|
+
* A pair past preflight, deps and its key PREPARATION (ONBOARD SL-4): what the rest of setup
|
|
161
|
+
* needs, plus the prepared storage, which is scanned before `finishSetupPair` may use it.
|
|
162
|
+
* `prepared` is null when the identity already exists (no key is chosen).
|
|
163
|
+
*/
|
|
164
|
+
export interface SetupPair {
|
|
165
|
+
readonly harness: IdentityHarness;
|
|
166
|
+
readonly slug: string;
|
|
167
|
+
readonly prepared: PreparedKeyStorage | null;
|
|
168
|
+
/** @internal */
|
|
169
|
+
readonly state: PairState;
|
|
170
|
+
}
|
|
171
|
+
interface PairState {
|
|
172
|
+
out: Run;
|
|
173
|
+
options: SetupOptions;
|
|
174
|
+
deps: SetupDeps;
|
|
175
|
+
env: NodeJS.ProcessEnv;
|
|
176
|
+
cwd: string;
|
|
177
|
+
scope: SetupScope;
|
|
178
|
+
assets: BakedAssets;
|
|
179
|
+
identity: IdentityState;
|
|
180
|
+
deployment: IdentityDeployment | null;
|
|
181
|
+
invite: ParsedInviteInput | null;
|
|
182
|
+
identityPath: string;
|
|
183
|
+
markerPath: string;
|
|
184
|
+
}
|
|
185
|
+
export type SetupPairStart = {
|
|
186
|
+
kind: "stopped";
|
|
187
|
+
report: SetupReport;
|
|
188
|
+
} | {
|
|
189
|
+
kind: "started";
|
|
190
|
+
pair: SetupPair;
|
|
191
|
+
};
|
|
192
|
+
/** The storage context the key step's scan reads (env, runner, budgets, notices). */
|
|
193
|
+
export declare function setupStorageContext(deps: SetupDeps): Pick<KeyStorageContext, "env" | "run" | "statusBudgets" | "notify">;
|
|
194
|
+
/**
|
|
195
|
+
* Preflight, deps and the key PREPARATION for one harness. `preset.invite` is an invite the caller
|
|
196
|
+
* already read and parsed (`setup --all` reads it once for every pair); without it, the invite is
|
|
197
|
+
* read here.
|
|
198
|
+
*/
|
|
199
|
+
export declare function startSetupPair(options: SetupOptions, deps?: SetupDeps, preset?: {
|
|
200
|
+
invite: ParsedInviteInput | null;
|
|
201
|
+
}): Promise<SetupPairStart>;
|
|
202
|
+
/**
|
|
203
|
+
* The rest of one pair, from its FINAL key storage (`finalizeKeyStorage`'s answer for this pair;
|
|
204
|
+
* null for a pair whose identity exists): enroll, identity, board, harness, doctor.
|
|
205
|
+
*/
|
|
206
|
+
export declare function finishSetupPair(pair: SetupPair, selection: KeyStorageSelection | null): Promise<SetupReport>;
|
|
207
|
+
/**
|
|
208
|
+
* One harness: start, the key step's one scan (prepare → measure → finalize, as `setup --all` does
|
|
209
|
+
* over every pair), then the rest.
|
|
210
|
+
*/
|
|
108
211
|
export declare function runSetup(options: SetupOptions, deps?: SetupDeps): Promise<SetupReport>;
|
|
109
212
|
export type SetupRunner = (options: SetupOptions) => Promise<SetupReport>;
|
|
110
213
|
interface SetupIo {
|
|
@@ -112,5 +215,9 @@ interface SetupIo {
|
|
|
112
215
|
writeStderr(message: string): void;
|
|
113
216
|
}
|
|
114
217
|
export declare function formatSetupReport(report: SetupReport): string;
|
|
115
|
-
|
|
218
|
+
/** `setup --all`: runs, prints (the table, or `--json` on stdout with notes on stderr), returns the exit. */
|
|
219
|
+
export type SetupAllCommand = (options: SetupAllOptions, output: {
|
|
220
|
+
json: boolean;
|
|
221
|
+
}) => Promise<0 | 1 | 2>;
|
|
222
|
+
export declare function addSetupCommand(program: Command, io: SetupIo, setup: SetupRunner, setupAll?: SetupAllCommand): void;
|
|
116
223
|
export {};
|