@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/doctor.js
CHANGED
|
@@ -9,13 +9,17 @@
|
|
|
9
9
|
// only with a live `wake_producer` at its expected tier, so 0 is never returned without runtime
|
|
10
10
|
// evidence. Output is names and codes only: no identity value, key or path content is printed,
|
|
11
11
|
// and an error's `code` is printed only when this repository authored it (`authoredCode`).
|
|
12
|
+
import { execFile } from "node:child_process";
|
|
12
13
|
import { randomBytes } from "node:crypto";
|
|
14
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
13
15
|
import { homedir, userInfo } from "node:os";
|
|
14
|
-
import { resolve } from "node:path";
|
|
16
|
+
import { join, resolve } from "node:path";
|
|
15
17
|
import { AUTHORED_CAUSE_CODES, AgentSocketSelectionError, GENERIC_SUPABASE_FIELDS, IDENTITY_FILE_KEYS, IDENTITY_TRIGGER_FIELDS, KeystoreUnavailableError, assertIdentityHarness, codeProvenance, createAgentBoardClient, createOsKeychainBackend, expectedWakeTier, normalizeKeyReference, readIdentityFile, readSessionRecords, resolveCredentials, selectAgentSocketIdentity, } from "@consiliency/agent-board-client";
|
|
16
18
|
import { judgeRegistration, nodeRegistrationFs, } from "./doctor_scopes.js";
|
|
19
|
+
import { resolveKeyslotDir } from "./assets.js";
|
|
20
|
+
import { SLOT_RE, externalDeclarationProblems, fileIdentity, keyslotSocketDir, machineIdOf, resolveAgentSocketPath, unsealedOptinProblems, } from "./setup_storage.js";
|
|
17
21
|
export const DOCTOR_CHECKS = Object.freeze([
|
|
18
|
-
"identity_file", "keystore_key", "whoami", "board_membership", "registration", "runtime",
|
|
22
|
+
"identity_file", "keystore_key", "key_storage", "whoami", "board_membership", "registration", "runtime",
|
|
19
23
|
]);
|
|
20
24
|
export const REPORTABLE_TOKEN = /^[A-Za-z0-9._/-]{1,64}$/;
|
|
21
25
|
const reportable = (value) => value === null ? null : REPORTABLE_TOKEN.test(value) ? value : "unrecognised";
|
|
@@ -228,6 +232,142 @@ async function keystoreCheck(values, env, probe) {
|
|
|
228
232
|
? check("pass", "fingerprint_matches")
|
|
229
233
|
: check("fail", "fingerprint_mismatch");
|
|
230
234
|
}
|
|
235
|
+
/**
|
|
236
|
+
* IF-0-SETUP-3 / IF-0-KEYSEAL-6 — which storage holds the identity's key, MEASURED: from the file's
|
|
237
|
+
* key reference plus the packaged `status.sh --json`, never from anything setup recorded.
|
|
238
|
+
*
|
|
239
|
+
* - an `os_keychain` reference → pass `os_keychain` (keystore_key proves the key is there);
|
|
240
|
+
* - an `agent_socket` reference: the socket keystore_key signs through (same resolution, including
|
|
241
|
+
* `MESSAGE_BOARD_AGENT_SOCKET_PATH`) is matched by DEVICE AND INODE, never spelling, against
|
|
242
|
+
* each `$XDG_RUNTIME_DIR/agent-board/<slot>.sock`;
|
|
243
|
+
* - a keyslot → `status.sh --harness <h> --slot <slot> --json`. Its measurement fields
|
|
244
|
+
* (machine_id, uid, slot, seam, mode) are compared with doctor's own values FIRST: a report
|
|
245
|
+
* that is not about this host, user and slot says nothing, so a mismatch or a missing field is
|
|
246
|
+
* 2 even beside a status exit 1 (both are non-pass). Then exit 1 → 1, exit 2 → 2, and exit 0
|
|
247
|
+
* passes as `sealed`, or as `unsealed` only when doctor's own field-by-field read of the opt-in
|
|
248
|
+
* is valid (an invalid opt-in is 1, as A1-S2 requires of status.sh);
|
|
249
|
+
* - any other socket → `external`, which passes only with a valid declaration
|
|
250
|
+
* (`<configRoot>/agent-board/external-agent`); undeclared or mismatched → 2.
|
|
251
|
+
* - status.sh absent, unrunnable, timed out or unparseable → 2.
|
|
252
|
+
*/
|
|
253
|
+
const STATUS_MODES = new Set(["sealed", "unsealed", "contradiction", "undetermined"]);
|
|
254
|
+
const STATUS_EXIT2_REASON = /^(nothing_committed|undeterminable|linger_off|upgrade_available|loader_outdated|loader_unrecorded|legacy_loader_present|capability_(error|incapable|incapable-for-keyslot))$/;
|
|
255
|
+
const STATUS_TIMEOUT_MS = 180_000;
|
|
256
|
+
function runStatus(file, args, env) {
|
|
257
|
+
return new Promise((done) => {
|
|
258
|
+
execFile(file, args, { env, timeout: STATUS_TIMEOUT_MS, maxBuffer: 16 * 1024 * 1024, encoding: "utf8" }, (error, stdout) => {
|
|
259
|
+
const failure = error;
|
|
260
|
+
const code = !failure ? 0 : typeof failure.code === "number" && !failure.killed ? failure.code : null;
|
|
261
|
+
done({ code, stdout: String(stdout ?? "") });
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
/** The keyslot whose socket is the same file as `socket`, or null. */
|
|
266
|
+
function matchKeyslot(socket, env) {
|
|
267
|
+
const dir = keyslotSocketDir(env);
|
|
268
|
+
const target = fileIdentity(socket);
|
|
269
|
+
if (!dir || !target)
|
|
270
|
+
return null;
|
|
271
|
+
let names;
|
|
272
|
+
try {
|
|
273
|
+
names = readdirSync(dir);
|
|
274
|
+
}
|
|
275
|
+
catch {
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
for (const name of names.sort()) {
|
|
279
|
+
if (!name.endsWith(".sock"))
|
|
280
|
+
continue;
|
|
281
|
+
const slot = name.slice(0, -".sock".length);
|
|
282
|
+
if (!SLOT_RE.test(slot))
|
|
283
|
+
continue;
|
|
284
|
+
const candidate = fileIdentity(join(dir, name));
|
|
285
|
+
if (candidate && candidate.dev === target.dev && candidate.ino === target.ino)
|
|
286
|
+
return slot;
|
|
287
|
+
}
|
|
288
|
+
return null;
|
|
289
|
+
}
|
|
290
|
+
async function keyStorageCheck(harness, values, fileEnv, env, options) {
|
|
291
|
+
let reference;
|
|
292
|
+
try {
|
|
293
|
+
reference = normalizeKeyReference(values[KEY_REFERENCE_KEY]);
|
|
294
|
+
}
|
|
295
|
+
catch {
|
|
296
|
+
return check("undetermined", "key_reference_invalid");
|
|
297
|
+
}
|
|
298
|
+
if (reference.resolver === "os_keychain")
|
|
299
|
+
return check("pass", "os_keychain");
|
|
300
|
+
if (reference.resolver !== "agent_socket")
|
|
301
|
+
return check("undetermined", "resolver_unsupported");
|
|
302
|
+
const socket = resolveAgentSocketPath(String(reference.handle), fileEnv);
|
|
303
|
+
if (socket === null)
|
|
304
|
+
return check("undetermined", "agent_socket_unresolved");
|
|
305
|
+
if (fileIdentity(socket) === null)
|
|
306
|
+
return check("undetermined", "agent_socket_absent");
|
|
307
|
+
const uid = options.uid ?? (typeof process.getuid === "function" ? process.getuid() : -1);
|
|
308
|
+
const machineId = machineIdOf(options.machineIdFile);
|
|
309
|
+
const slot = matchKeyslot(socket, env);
|
|
310
|
+
if (slot === null) {
|
|
311
|
+
const problems = externalDeclarationProblems(env, socket, uid, machineId);
|
|
312
|
+
if (problems.length === 0)
|
|
313
|
+
return check("pass", "external");
|
|
314
|
+
return check("undetermined", problems.includes("missing") ? "external_undeclared" : "external_declaration_mismatch");
|
|
315
|
+
}
|
|
316
|
+
let dir;
|
|
317
|
+
try {
|
|
318
|
+
dir = options.keyslotDir ?? resolveKeyslotDir();
|
|
319
|
+
}
|
|
320
|
+
catch {
|
|
321
|
+
return check("undetermined", "status_absent");
|
|
322
|
+
}
|
|
323
|
+
if (!existsSync(join(dir, "status.sh")))
|
|
324
|
+
return check("undetermined", "status_absent");
|
|
325
|
+
const run = await runStatus(join(dir, "status.sh"), ["--harness", harness, "--slot", slot, "--json"], env);
|
|
326
|
+
if (run.code === null)
|
|
327
|
+
return check("undetermined", "status_did_not_complete");
|
|
328
|
+
let report;
|
|
329
|
+
try {
|
|
330
|
+
const parsed = JSON.parse(run.stdout);
|
|
331
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
332
|
+
throw new Error("not an object");
|
|
333
|
+
report = parsed;
|
|
334
|
+
}
|
|
335
|
+
catch {
|
|
336
|
+
return check("undetermined", "status_unreadable");
|
|
337
|
+
}
|
|
338
|
+
const seam = typeof env.KEYSEAL_TEST_ROOT === "string" && env.KEYSEAL_TEST_ROOT !== "";
|
|
339
|
+
const own = {
|
|
340
|
+
machine_id: (value) => machineId !== null && value === machineId,
|
|
341
|
+
uid: (value) => value === uid,
|
|
342
|
+
slot: (value) => value === slot,
|
|
343
|
+
seam: (value) => value === seam,
|
|
344
|
+
mode: (value) => typeof value === "string" && STATUS_MODES.has(value),
|
|
345
|
+
};
|
|
346
|
+
for (const [field, matches] of Object.entries(own)) {
|
|
347
|
+
if (!(field in report) || report[field] === null)
|
|
348
|
+
return check("undetermined", `status_field_missing:${field}`);
|
|
349
|
+
if (!matches(report[field]))
|
|
350
|
+
return check("undetermined", `status_field_mismatch:${field}`);
|
|
351
|
+
}
|
|
352
|
+
if (run.code === 1)
|
|
353
|
+
return check("fail", "keyslot_status_failed");
|
|
354
|
+
if (run.code === 2) {
|
|
355
|
+
const reasons = Array.isArray(report.exit2_reasons) ? report.exit2_reasons : [];
|
|
356
|
+
const named = reasons.length > 0 && reasons.every((reason) => typeof reason === "string" && STATUS_EXIT2_REASON.test(reason));
|
|
357
|
+
return check("undetermined", named ? `keyslot_status_undetermined:${reasons.join(",")}` : "keyslot_status_undetermined");
|
|
358
|
+
}
|
|
359
|
+
if (run.code !== 0)
|
|
360
|
+
return check("undetermined", "status_unexpected_exit");
|
|
361
|
+
if (report.mode === "sealed")
|
|
362
|
+
return check("pass", "sealed");
|
|
363
|
+
if (report.mode === "unsealed") {
|
|
364
|
+
const optin = report.optin;
|
|
365
|
+
if (optin?.ok !== true)
|
|
366
|
+
return check("fail", "unsealed_optin_invalid");
|
|
367
|
+
return unsealedOptinProblems(env, slot, uid, machineId).length === 0 ? check("pass", "unsealed") : check("fail", "unsealed_optin_invalid");
|
|
368
|
+
}
|
|
369
|
+
return check("undetermined", "status_mode_unexpected");
|
|
370
|
+
}
|
|
231
371
|
async function whoamiCheck(client, expectedHandle) {
|
|
232
372
|
let rows;
|
|
233
373
|
try {
|
|
@@ -342,12 +482,14 @@ export async function runDoctor(options) {
|
|
|
342
482
|
}
|
|
343
483
|
if (values === null) {
|
|
344
484
|
checks.keystore_key = check("undetermined", "no_identity_file");
|
|
485
|
+
checks.key_storage = check("undetermined", "no_identity_file");
|
|
345
486
|
checks.whoami = check("undetermined", "no_identity_file");
|
|
346
487
|
checks.board_membership = check("undetermined", "no_identity_file");
|
|
347
488
|
}
|
|
348
489
|
else {
|
|
349
490
|
const fileEnv = fileIdentityEnv(env, values);
|
|
350
491
|
checks.keystore_key = await keystoreCheck(values, fileEnv, options.probeKeystore ?? defaultKeystoreProbe);
|
|
492
|
+
checks.key_storage = await keyStorageCheck(harness, values, fileEnv, env, options);
|
|
351
493
|
let client = null;
|
|
352
494
|
try {
|
|
353
495
|
client = (options.createClient ?? defaultClient)(fileEnv);
|
|
@@ -16,16 +16,18 @@ export interface IdentityDeployment {
|
|
|
16
16
|
publishableKey: string;
|
|
17
17
|
}
|
|
18
18
|
/**
|
|
19
|
-
* Where each deployment value is read, in precedence order: the
|
|
20
|
-
* (
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* identity field, here or in the loader.
|
|
19
|
+
* Where each deployment value is read, in precedence order: the client's one alias table
|
|
20
|
+
* (`DEPLOYMENT_ENV_ALIASES`), which puts the enrollment's own names first (so the file records
|
|
21
|
+
* the deployment the enrollment used) and then the identity names. The enrollment resolvers read
|
|
22
|
+
* the same table, so the two cannot disagree. The generic Supabase names are in no list: a
|
|
23
|
+
* generic key never fills an identity field, here or in the loader.
|
|
24
24
|
*/
|
|
25
25
|
export declare const DEPLOYMENT_ENV_NAMES: Readonly<Record<keyof IdentityDeployment, readonly string[]>>;
|
|
26
26
|
/**
|
|
27
|
-
* The deployment half of an identity, read and shape-checked before enrollment commits.
|
|
28
|
-
*
|
|
27
|
+
* The deployment half of an identity, read and shape-checked before enrollment commits. Each
|
|
28
|
+
* value is the shipped default, or env under the board group's all-or-nothing rule (the
|
|
29
|
+
* client's `resolveDeploymentField`). Absent from an env-set group → `identity_incomplete`,
|
|
30
|
+
* naming the variable that switched the group off; not the non-secret shape its key requires →
|
|
29
31
|
* `identity_secret_value`. Both name the variable, never the value.
|
|
30
32
|
*/
|
|
31
33
|
export declare function resolveIdentityDeployment(env: NodeJS.ProcessEnv): IdentityDeployment;
|
package/dist/identity_writer.js
CHANGED
|
@@ -11,7 +11,7 @@ import { createHash } from "node:crypto";
|
|
|
11
11
|
import { constants as FS } from "node:fs";
|
|
12
12
|
import * as fs from "node:fs/promises";
|
|
13
13
|
import { dirname, join } from "node:path";
|
|
14
|
-
import { CredentialResolutionError,
|
|
14
|
+
import { CredentialResolutionError, DEPLOYMENT_ENV_ALIASES, describeMissingDeploymentFields, identityConfigRoot, resolveDeploymentField, identityFilePath, serializeIdentityFile, walkTrustedDirectory, } from "@consiliency/agent-board-client";
|
|
15
15
|
async function syncDirectory(path) {
|
|
16
16
|
const handle = await fs.open(path, "r");
|
|
17
17
|
try {
|
|
@@ -50,24 +50,17 @@ export async function writeOwnerOnlyFileAtomic(path, content, options = {}) {
|
|
|
50
50
|
}
|
|
51
51
|
await sync(dirname(path));
|
|
52
52
|
}
|
|
53
|
-
/** The identity-name alias family containing `canonical`, from the client's one declaration. */
|
|
54
|
-
function identityFamily(canonical) {
|
|
55
|
-
const family = ENV_ALIAS_FAMILIES.find((members) => members.includes(canonical));
|
|
56
|
-
if (!family)
|
|
57
|
-
throw new Error(`identity_writer: no alias family declares ${canonical}`);
|
|
58
|
-
return family;
|
|
59
|
-
}
|
|
60
53
|
/**
|
|
61
|
-
* Where each deployment value is read, in precedence order: the
|
|
62
|
-
* (
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
* identity field, here or in the loader.
|
|
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.
|
|
66
59
|
*/
|
|
67
60
|
export const DEPLOYMENT_ENV_NAMES = Object.freeze({
|
|
68
|
-
boardUrl:
|
|
69
|
-
sessionUrl:
|
|
70
|
-
publishableKey:
|
|
61
|
+
boardUrl: DEPLOYMENT_ENV_ALIASES.boardUrl,
|
|
62
|
+
sessionUrl: DEPLOYMENT_ENV_ALIASES.sessionUrl,
|
|
63
|
+
publishableKey: DEPLOYMENT_ENV_ALIASES.publishableKey,
|
|
71
64
|
});
|
|
72
65
|
const DEPLOYMENT_FILE_KEYS = {
|
|
73
66
|
boardUrl: "AGENT_BOARD_URL",
|
|
@@ -89,27 +82,22 @@ const PREFLIGHT_STAND_INS = {
|
|
|
89
82
|
AGENT_BOARD_SIGNING_PUBLIC_KEY_FINGERPRINT: `sha256:${"A".repeat(43)}`,
|
|
90
83
|
MESSAGE_BOARD_AGENT_AUTH_MODE: "keystore_session",
|
|
91
84
|
};
|
|
92
|
-
function firstPresent(env, names) {
|
|
93
|
-
for (const name of names) {
|
|
94
|
-
const value = env[name];
|
|
95
|
-
if (value !== undefined && value.trim() !== "")
|
|
96
|
-
return { name, value: value.trim() };
|
|
97
|
-
}
|
|
98
|
-
return undefined;
|
|
99
|
-
}
|
|
100
85
|
/**
|
|
101
|
-
* The deployment half of an identity, read and shape-checked before enrollment commits.
|
|
102
|
-
*
|
|
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 →
|
|
103
90
|
* `identity_secret_value`. Both name the variable, never the value.
|
|
104
91
|
*/
|
|
105
92
|
export function resolveIdentityDeployment(env) {
|
|
106
93
|
const deployment = {};
|
|
107
94
|
for (const field of Object.keys(DEPLOYMENT_ENV_NAMES)) {
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
throw new CredentialResolutionError("identity_incomplete", `cannot write an identity file: no ${DEPLOYMENT_FILE_KEYS[field]} for it (set one of: ${names.join(", ")})`);
|
|
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)}`);
|
|
112
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 };
|
|
113
101
|
try {
|
|
114
102
|
serializeIdentityFile({ ...PREFLIGHT_STAND_INS, [DEPLOYMENT_FILE_KEYS[field]]: found.value });
|
|
115
103
|
}
|
package/dist/setup.d.ts
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import type { Command } from "commander";
|
|
2
|
+
import { type DeviceAuthorizationPrompt, type EnrollmentIntent, type EnrollmentOutcome } from "@consiliency/agent-board-client";
|
|
3
|
+
import { type BakedAssets } from "./assets.js";
|
|
4
|
+
import { type DoctorOptions, type DoctorReport } from "./doctor.js";
|
|
5
|
+
import { type DepsOutcome, type KeyStorageContext, type KeyStorageSelection } from "./setup_storage.js";
|
|
6
|
+
import { type ChildRunner, type WriterContext } from "./setup_writers.js";
|
|
7
|
+
export declare const SETUP_STEPS: readonly ["preflight", "deps", "key", "enroll", "identity", "board", "harness", "doctor"];
|
|
8
|
+
export type SetupStepName = (typeof SETUP_STEPS)[number];
|
|
9
|
+
export type SetupStatus = "done" | "already" | "skipped" | "failed" | "undetermined";
|
|
10
|
+
export interface SetupStep {
|
|
11
|
+
step: SetupStepName | "clear_pending";
|
|
12
|
+
status: SetupStatus;
|
|
13
|
+
reason: string;
|
|
14
|
+
}
|
|
15
|
+
export interface SetupReport {
|
|
16
|
+
steps: SetupStep[];
|
|
17
|
+
exit: 0 | 1 | 2;
|
|
18
|
+
/** Human text: per-step fixes and the next steps. Never part of `--json`'s stdout. */
|
|
19
|
+
notes: string[];
|
|
20
|
+
}
|
|
21
|
+
export interface SetupOptions {
|
|
22
|
+
harness: string;
|
|
23
|
+
slug?: string;
|
|
24
|
+
invite?: boolean;
|
|
25
|
+
allowUnsealedKey?: boolean;
|
|
26
|
+
agentSocket?: string;
|
|
27
|
+
scope?: string;
|
|
28
|
+
installDeps?: boolean;
|
|
29
|
+
clearPending?: boolean;
|
|
30
|
+
/** Default true; `--no-follow-symlinks` refuses to write through a symlinked config. */
|
|
31
|
+
followSymlinks?: boolean;
|
|
32
|
+
}
|
|
33
|
+
/** The enrollment setup runs: the device flow, the key reference injected, intent always `none`. */
|
|
34
|
+
export type SetupEnroller = (input: {
|
|
35
|
+
slug: string;
|
|
36
|
+
intent: EnrollmentIntent;
|
|
37
|
+
keyReference: string;
|
|
38
|
+
env: NodeJS.ProcessEnv;
|
|
39
|
+
onDeviceAuthorization: (prompt: DeviceAuthorizationPrompt) => void | Promise<void>;
|
|
40
|
+
}) => Promise<EnrollmentOutcome>;
|
|
41
|
+
export interface BoardClient {
|
|
42
|
+
redeemInviteLink(input: {
|
|
43
|
+
p_code: string;
|
|
44
|
+
}): Promise<unknown>;
|
|
45
|
+
}
|
|
46
|
+
export interface SetupDeps {
|
|
47
|
+
env?: NodeJS.ProcessEnv;
|
|
48
|
+
cwd?: string;
|
|
49
|
+
platform?: NodeJS.Platform;
|
|
50
|
+
/** stdin is a terminal. Default `process.stdin.isTTY === true`. */
|
|
51
|
+
interactive?: boolean;
|
|
52
|
+
/** One line from the terminal; `secret` hides the echo. */
|
|
53
|
+
prompt?: (question: string, options: {
|
|
54
|
+
secret: boolean;
|
|
55
|
+
}) => Promise<string>;
|
|
56
|
+
/** All of stdin (non-interactive). */
|
|
57
|
+
readStdin?: () => Promise<string>;
|
|
58
|
+
/** Every child (busctl, sudo, the harness CLIs) runs through this, with no stdin. */
|
|
59
|
+
run?: ChildRunner;
|
|
60
|
+
/** Default `/usr/bin/sudo`: never a PATH lookup. */
|
|
61
|
+
sudoPath?: string;
|
|
62
|
+
hostname?: () => string;
|
|
63
|
+
bakedAssets?: () => BakedAssets;
|
|
64
|
+
selectKeyStorage?: (ctx: KeyStorageContext) => Promise<KeyStorageSelection>;
|
|
65
|
+
/** Seams passed through to the storage selector (probeKeychain, run, keyslotDir, …). */
|
|
66
|
+
storage?: Partial<KeyStorageContext>;
|
|
67
|
+
enroll?: SetupEnroller;
|
|
68
|
+
createBoardClient?: (fileEnv: NodeJS.ProcessEnv) => BoardClient;
|
|
69
|
+
doctor?: (options: DoctorOptions) => Promise<DoctorReport>;
|
|
70
|
+
/** Seams passed through to doctor (createClient, probeKeystore, isAlive, …). */
|
|
71
|
+
doctorOptions?: Partial<DoctorOptions>;
|
|
72
|
+
/** Seams passed through to the harness writer (fs, claudeManagedDir). */
|
|
73
|
+
writer?: Partial<Pick<WriterContext, "fs" | "claudeManagedDir">>;
|
|
74
|
+
}
|
|
75
|
+
export declare const DEFAULT_SUDO = "/usr/bin/sudo";
|
|
76
|
+
export declare const SECRET_SERVICE_NAME = "org.freedesktop.secrets";
|
|
77
|
+
/** `busctl --user call … NameHasOwner`: asks the BUS whether the name is owned; never calls the service. */
|
|
78
|
+
export declare const NAME_HAS_OWNER_ARGS: readonly string[];
|
|
79
|
+
/**
|
|
80
|
+
* The keychain tool's install, one complete argv per package manager, first found wins. Each
|
|
81
|
+
* package name was re-checked against the distro's own index (throwaway containers, 2026-09-27):
|
|
82
|
+
* ubuntu 24.04 `libsecret-tools` ships ./usr/bin/secret-tool; fedora 44 `dnf repoquery
|
|
83
|
+
* --whatprovides /usr/bin/secret-tool` → libsecret; arch `pacman -F usr/bin/secret-tool` →
|
|
84
|
+
* core/libsecret; opensuse tumbleweed `zypper se --provides /usr/bin/secret-tool` → secret-tool
|
|
85
|
+
* (NOT libsecret-tools, which the plan text named).
|
|
86
|
+
*/
|
|
87
|
+
export declare const KEYCHAIN_INSTALL_ARGV: ReadonlyArray<{
|
|
88
|
+
manager: string;
|
|
89
|
+
argv: readonly string[];
|
|
90
|
+
}>;
|
|
91
|
+
/** `<short hostname>-<harness>`, lowercased to `[a-z0-9-]`. */
|
|
92
|
+
export declare function defaultSlug(host: string, harness: string): string;
|
|
93
|
+
export declare function pendingMarkerPath(harness: string, env: NodeJS.ProcessEnv): string;
|
|
94
|
+
interface DepsContext {
|
|
95
|
+
env: NodeJS.ProcessEnv;
|
|
96
|
+
platform: NodeJS.Platform;
|
|
97
|
+
interactive: boolean;
|
|
98
|
+
installDeps: boolean;
|
|
99
|
+
run: ChildRunner;
|
|
100
|
+
sudo: string;
|
|
101
|
+
prompt: (question: string, options: {
|
|
102
|
+
secret: boolean;
|
|
103
|
+
}) => Promise<string>;
|
|
104
|
+
}
|
|
105
|
+
export declare function depsStep(ctx: DepsContext): Promise<DepsOutcome & {
|
|
106
|
+
fix?: string;
|
|
107
|
+
}>;
|
|
108
|
+
export declare function runSetup(options: SetupOptions, deps?: SetupDeps): Promise<SetupReport>;
|
|
109
|
+
export type SetupRunner = (options: SetupOptions) => Promise<SetupReport>;
|
|
110
|
+
interface SetupIo {
|
|
111
|
+
writeStdout(message: string): void;
|
|
112
|
+
writeStderr(message: string): void;
|
|
113
|
+
}
|
|
114
|
+
export declare function formatSetupReport(report: SetupReport): string;
|
|
115
|
+
export declare function addSetupCommand(program: Command, io: SetupIo, setup: SetupRunner): void;
|
|
116
|
+
export {};
|