@consiliency/agent-board-cli 1.23.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 +46 -3
- package/dist/doctor.js +242 -7
- package/dist/doctor_scopes.d.ts +162 -2
- package/dist/doctor_scopes.js +794 -16
- 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,55 @@
|
|
|
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";
|
|
13
|
-
import {
|
|
14
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
15
|
+
import { homedir, userInfo } from "node:os";
|
|
16
|
+
import { join, resolve } from "node:path";
|
|
14
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";
|
|
15
|
-
import { judgeRegistration, nodeRegistrationFs } from "./doctor_scopes.js";
|
|
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";
|
|
16
21
|
export const DOCTOR_CHECKS = Object.freeze([
|
|
17
|
-
"identity_file", "keystore_key", "whoami", "board_membership", "registration", "runtime",
|
|
22
|
+
"identity_file", "keystore_key", "key_storage", "whoami", "board_membership", "registration", "runtime",
|
|
18
23
|
]);
|
|
24
|
+
export const REPORTABLE_TOKEN = /^[A-Za-z0-9._/-]{1,64}$/;
|
|
25
|
+
const reportable = (value) => value === null ? null : REPORTABLE_TOKEN.test(value) ? value : "unrecognised";
|
|
26
|
+
/**
|
|
27
|
+
* The pin, over the live wake_producers whose cwd (realpath) is doctor's cwd, and only while our
|
|
28
|
+
* plugin's server is registered there. All at the pin (our plugin's version) → unchanged; NONE at
|
|
29
|
+
* it → `plugin_pin_mismatch`; a MIXED set → `plugin_pin_ambiguous` (which one is ours is unknown).
|
|
30
|
+
* A record elsewhere was launched by whatever registration applies THERE. An earlier reason is
|
|
31
|
+
* kept and this one appended.
|
|
32
|
+
*/
|
|
33
|
+
function pinCheck(verdict, entries, cwd, fs) {
|
|
34
|
+
// Only while our plugin's server is registered here: one turned off by `disabledMcpServers`
|
|
35
|
+
// launched nothing whose version could be judged (panel S1).
|
|
36
|
+
const pin = verdict.plugin?.installed && verdict.plugin.serverActive ? verdict.plugin.version : null;
|
|
37
|
+
if (pin === null)
|
|
38
|
+
return verdict;
|
|
39
|
+
// Real paths on both sides (panel S2): a server records its physical cwd; doctor may be in a symlink.
|
|
40
|
+
const real = (path) => fs.realpath(path) ?? resolve(path);
|
|
41
|
+
const here = entries.flatMap((entry) => entry.status === "live" && entry.record.role === "wake_producer"
|
|
42
|
+
&& real(entry.record.cwd) === real(cwd) ? [entry.record] : []);
|
|
43
|
+
const atPin = here.filter((record) => record.packageVersion === pin).length;
|
|
44
|
+
// All at the pin → nothing to say; none → a mismatch; a mixed set → which one is ours is unknown (panel B1).
|
|
45
|
+
if (atPin === here.length)
|
|
46
|
+
return verdict;
|
|
47
|
+
// An earlier reason is kept, never overwritten (panel S5).
|
|
48
|
+
const earlier = verdict.status === "pass" ? [] : [verdict.reason];
|
|
49
|
+
if (atPin === 0)
|
|
50
|
+
return { ...verdict, status: "fail", reason: [...earlier, "plugin_pin_mismatch"].join(",") };
|
|
51
|
+
return { ...verdict, status: verdict.status === "fail" ? "fail" : "undetermined", reason: [...earlier, "plugin_pin_ambiguous"].join(",") };
|
|
52
|
+
}
|
|
53
|
+
/** The binary's managed-settings directory per platform (`Zh()`, 2.1.283). */
|
|
54
|
+
function defaultClaudeManagedDir(platform) {
|
|
55
|
+
if (platform === "darwin")
|
|
56
|
+
return "/Library/Application Support/ClaudeCode";
|
|
57
|
+
if (platform === "win32")
|
|
58
|
+
return "C:\\Program Files\\ClaudeCode";
|
|
59
|
+
return "/etc/claude-code";
|
|
60
|
+
}
|
|
19
61
|
const FINGERPRINT_KEY = "AGENT_BOARD_SIGNING_PUBLIC_KEY_FINGERPRINT";
|
|
20
62
|
const HANDLE_KEY = "AGENT_BOARD_EXPECTED_AGENT_HANDLE";
|
|
21
63
|
const KEY_REFERENCE_KEY = "AGENT_BOARD_AGENT_KEY_REFERENCE";
|
|
@@ -190,6 +232,142 @@ async function keystoreCheck(values, env, probe) {
|
|
|
190
232
|
? check("pass", "fingerprint_matches")
|
|
191
233
|
: check("fail", "fingerprint_mismatch");
|
|
192
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
|
+
}
|
|
193
371
|
async function whoamiCheck(client, expectedHandle) {
|
|
194
372
|
let rows;
|
|
195
373
|
try {
|
|
@@ -304,12 +482,14 @@ export async function runDoctor(options) {
|
|
|
304
482
|
}
|
|
305
483
|
if (values === null) {
|
|
306
484
|
checks.keystore_key = check("undetermined", "no_identity_file");
|
|
485
|
+
checks.key_storage = check("undetermined", "no_identity_file");
|
|
307
486
|
checks.whoami = check("undetermined", "no_identity_file");
|
|
308
487
|
checks.board_membership = check("undetermined", "no_identity_file");
|
|
309
488
|
}
|
|
310
489
|
else {
|
|
311
490
|
const fileEnv = fileIdentityEnv(env, values);
|
|
312
491
|
checks.keystore_key = await keystoreCheck(values, fileEnv, options.probeKeystore ?? defaultKeystoreProbe);
|
|
492
|
+
checks.key_storage = await keyStorageCheck(harness, values, fileEnv, env, options);
|
|
313
493
|
let client = null;
|
|
314
494
|
try {
|
|
315
495
|
client = (options.createClient ?? defaultClient)(fileEnv);
|
|
@@ -340,21 +520,76 @@ export async function runDoctor(options) {
|
|
|
340
520
|
const cwds = [...new Set([cwd, ...entries.flatMap((entry) => entry.status === "live" ? [entry.record.cwd] : [])])];
|
|
341
521
|
const fs = options.registrationFs ?? nodeRegistrationFs;
|
|
342
522
|
const home = env.HOME && env.HOME.trim() !== "" ? env.HOME : homedir();
|
|
343
|
-
|
|
523
|
+
const platform = options.platform ?? process.platform;
|
|
524
|
+
let userName = options.userName;
|
|
525
|
+
if (userName === undefined) {
|
|
526
|
+
try {
|
|
527
|
+
userName = (options.lookupUserName ?? (() => userInfo().username))() || undefined;
|
|
528
|
+
}
|
|
529
|
+
catch {
|
|
530
|
+
userName = undefined;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
const verdicts = cwds.map((dir) => {
|
|
344
534
|
try {
|
|
345
|
-
return judgeRegistration(harness, { cwd: dir, home, env, fs, claudeManagedDir: options.claudeManagedDir ??
|
|
535
|
+
return judgeRegistration(harness, { cwd: dir, home, env, fs, claudeManagedDir: options.claudeManagedDir ?? defaultClaudeManagedDir(platform),
|
|
536
|
+
platform, userName, regQuery: options.regQuery });
|
|
346
537
|
}
|
|
347
538
|
catch (error) {
|
|
348
539
|
return check("undetermined", codeOf(error, "registration_unreadable"));
|
|
349
540
|
}
|
|
350
|
-
})
|
|
541
|
+
});
|
|
542
|
+
// `cwds[0]` is doctor's own cwd: the pin and the report are about it.
|
|
543
|
+
verdicts[0] = pinCheck(verdicts[0], entries, cwd, fs);
|
|
544
|
+
const registration = worst(verdicts);
|
|
545
|
+
checks.registration = check(registration.status, registration.reason);
|
|
351
546
|
const ordered = Object.fromEntries(DOCTOR_CHECKS.map((name) => [name, checks[name]]));
|
|
352
547
|
const statuses = Object.values(ordered).map((item) => item.status);
|
|
353
548
|
const exit = statuses.every((status) => status === "pass") ? 0 : statuses.includes("fail") ? 1 : 2;
|
|
354
|
-
|
|
549
|
+
const report = { harness, exit, checks: ordered };
|
|
550
|
+
const here = verdicts[0];
|
|
551
|
+
if (harness === "claude") {
|
|
552
|
+
const plugin = here.plugin;
|
|
553
|
+
report.plugin = {
|
|
554
|
+
installed: plugin?.installed ?? false,
|
|
555
|
+
installed_version: reportable(plugin?.version ?? null),
|
|
556
|
+
pinned_mcp_version: reportable(plugin?.pinnedMcpVersion ?? null),
|
|
557
|
+
source_ref: reportable(plugin?.sourceRef ?? null),
|
|
558
|
+
auto_update: plugin ? { effective: plugin.autoUpdate.effective, source: plugin.autoUpdate.source } : { effective: false, source: "undetermined" },
|
|
559
|
+
};
|
|
560
|
+
if (here.hookCounts)
|
|
561
|
+
report.registration = { hook_counts: { SessionStart: here.hookCounts.SessionStart, FileChanged: here.hookCounts.FileChanged } };
|
|
562
|
+
if (here.managedPolicy) {
|
|
563
|
+
report.managed_policy = {
|
|
564
|
+
checked: [...here.managedPolicy.checked], evidence: [...here.managedPolicy.evidence],
|
|
565
|
+
unreadable_sources_possible: here.managedPolicy.unreadableSourcesPossible, platform: here.managedPolicy.platformLabel,
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
return report;
|
|
355
570
|
}
|
|
356
571
|
/** Human-readable form; `--json` prints the report object itself. */
|
|
357
572
|
export function formatDoctorReport(report) {
|
|
358
573
|
const lines = DOCTOR_CHECKS.map((name) => `${report.checks[name].status.padEnd(12)} ${name}: ${report.checks[name].reason}`);
|
|
574
|
+
if (report.plugin) {
|
|
575
|
+
const { plugin } = report;
|
|
576
|
+
const counts = report.registration?.hook_counts;
|
|
577
|
+
lines.push([
|
|
578
|
+
`${"plugin".padEnd(12)} installed=${plugin.installed ? "yes" : "no"}`,
|
|
579
|
+
`version=${plugin.installed_version ?? "none"}`,
|
|
580
|
+
`pinned_mcp=${plugin.pinned_mcp_version ?? "none"}`,
|
|
581
|
+
`ref=${plugin.source_ref ?? "none"}`,
|
|
582
|
+
`auto_update=${plugin.auto_update.effective ? "on" : "off"}(${plugin.auto_update.source})`,
|
|
583
|
+
counts ? `hooks SessionStart=${counts.SessionStart} FileChanged=${counts.FileChanged}` : "hooks undetermined",
|
|
584
|
+
].join(" "));
|
|
585
|
+
}
|
|
586
|
+
if (report.managed_policy) {
|
|
587
|
+
const policy = report.managed_policy;
|
|
588
|
+
lines.push([
|
|
589
|
+
`${"admin policy".padEnd(12)} checked ${policy.checked.join(",") || "none"}`,
|
|
590
|
+
policy.evidence.length > 0 ? `; found ${policy.evidence.join(",")}` : "",
|
|
591
|
+
policy.unreadable_sources_possible ? `; ${policy.platform} MDM profiles not fully readable` : "",
|
|
592
|
+
].join(""));
|
|
593
|
+
}
|
|
359
594
|
return [`agent-board doctor --harness ${report.harness}`, ...lines, `exit ${report.exit}`].join("\n");
|
|
360
595
|
}
|
package/dist/doctor_scopes.d.ts
CHANGED
|
@@ -5,7 +5,28 @@ export declare const OPENCODE_PLUGIN_PACKAGE = "@consiliency/agent-board-opencod
|
|
|
5
5
|
export declare const PI_PACKAGE = "npm:@consiliency/agent-board-pi";
|
|
6
6
|
/** Claude Code events that each need exactly one `wake-hook.sh` hook (`docs/local-wake-signal.md`). */
|
|
7
7
|
export declare const CLAUDE_REQUIRED_HOOK_EVENTS: readonly ["SessionStart", "FileChanged"];
|
|
8
|
+
/** A wake hook is a command with a token whose BASENAME is this (see `namesWakeHook`). */
|
|
8
9
|
export declare const WAKE_HOOK_MARKER = "wake-hook.sh";
|
|
10
|
+
export declare const CLAUDE_PLUGIN_ID = "agent-board@message-board";
|
|
11
|
+
export declare const CLAUDE_PLUGIN_MARKETPLACE = "message-board";
|
|
12
|
+
/** Compared lower-cased, as Claude Code compares repos. */
|
|
13
|
+
export declare const CLAUDE_PLUGIN_REPO = "consiliency/message-board";
|
|
14
|
+
export declare const CLAUDE_PLUGIN_SERVER = "agent-board";
|
|
15
|
+
export declare const CLAUDE_PLUGIN_MCP_PACKAGE = "@consiliency/agent-board-mcp";
|
|
16
|
+
/** The SL-0 outcome: `B` = the FileChanged hook carries `asyncRewake: true`. */
|
|
17
|
+
export declare const CLAUDE_WAKE_SHAPE: "A" | "B";
|
|
18
|
+
export declare const CLAUDE_PLUGIN_HOOK_COMMANDS: Readonly<Record<string, string>>;
|
|
19
|
+
/** The stanza a plugin of version `version` must carry: the pin IS the plugin version. */
|
|
20
|
+
export declare function claudePluginStanza(version: string): {
|
|
21
|
+
command: string;
|
|
22
|
+
args: string[];
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Whether a hook command names the wake hook. The tokenizer is frozen: split on whitespace, strip
|
|
26
|
+
* quotes, no comment handling; a token matches when its basename is exactly `wake-hook.sh`, so
|
|
27
|
+
* `codex-wake-hook.sh` does not. Floor: a hook that never names the basename is invisible.
|
|
28
|
+
*/
|
|
29
|
+
export declare function namesWakeHook(command: string): boolean;
|
|
9
30
|
export interface ScopeContext {
|
|
10
31
|
cwd: string;
|
|
11
32
|
home: string;
|
|
@@ -13,6 +34,12 @@ export interface ScopeContext {
|
|
|
13
34
|
/** Claude Code's managed directory (Linux `/etc/claude-code`); injectable for tests. */
|
|
14
35
|
claudeManagedDir: string;
|
|
15
36
|
fs: RegistrationFs;
|
|
37
|
+
/** The OS, for the managed (MDM) sources doctor probes. Absent → treated as Linux. */
|
|
38
|
+
platform?: NodeJS.Platform;
|
|
39
|
+
/** The login name the macOS per-user managed plist is keyed by. */
|
|
40
|
+
userName?: string;
|
|
41
|
+
/** Registry probe for the Windows policy keys; default `nodeRegQuery`. */
|
|
42
|
+
regQuery?: RegQuery;
|
|
16
43
|
}
|
|
17
44
|
/** The filesystem reads registration makes; everything is read-only. */
|
|
18
45
|
export interface RegistrationFs {
|
|
@@ -26,6 +53,8 @@ export interface RegistrationFs {
|
|
|
26
53
|
isFile: boolean;
|
|
27
54
|
} | null;
|
|
28
55
|
exists(path: string): boolean;
|
|
56
|
+
/** The canonical path (every symlink resolved), or null when it does not exist. */
|
|
57
|
+
realpath(path: string): string | null;
|
|
29
58
|
getuid(): number;
|
|
30
59
|
}
|
|
31
60
|
export declare const nodeRegistrationFs: RegistrationFs;
|
|
@@ -58,10 +87,27 @@ export interface McpEntry {
|
|
|
58
87
|
fields: Readonly<Record<string, unknown>>;
|
|
59
88
|
shape: McpShape;
|
|
60
89
|
}
|
|
90
|
+
export interface HookEntry {
|
|
91
|
+
/** The handler's `type` (`command`, `http`, `prompt`, `agent`, `mcp_tool`); a non-string type is "". */
|
|
92
|
+
type: string;
|
|
93
|
+
/** A command handler's command; null for every other handler (its url or prompt is never kept). */
|
|
94
|
+
command: string | null;
|
|
95
|
+
/** Claude Code's `asyncRewake`: only a literal `true` counts. */
|
|
96
|
+
asyncRewake: boolean;
|
|
97
|
+
/**
|
|
98
|
+
* The group has a matcher that filters (round-2 F029): anything but absent or "". A SessionStart
|
|
99
|
+
* matcher filters on the start source, a FileChanged one on basename(file_path) (binary). The
|
|
100
|
+
* matcher text itself is never kept.
|
|
101
|
+
*/
|
|
102
|
+
hasMatcher: boolean;
|
|
103
|
+
/** The handler's and its group's own key NAMES (never values), for the exact freeze. */
|
|
104
|
+
handlerKeys: string[];
|
|
105
|
+
groupKeys: string[];
|
|
106
|
+
}
|
|
61
107
|
export interface ScopeContribution {
|
|
62
108
|
mcp: McpEntry[];
|
|
63
|
-
/** Claude hooks: event → the
|
|
64
|
-
hooks: Record<string,
|
|
109
|
+
/** Claude hooks: event → the hooks registered for it. */
|
|
110
|
+
hooks: Record<string, HookEntry[]>;
|
|
65
111
|
/** opencode `plugin` / pi `packages` specs. */
|
|
66
112
|
packages: string[];
|
|
67
113
|
}
|
|
@@ -69,6 +115,8 @@ export interface ScopeFile {
|
|
|
69
115
|
row: string;
|
|
70
116
|
path: string;
|
|
71
117
|
contribution: ScopeContribution;
|
|
118
|
+
/** A synthetic plugin file's plugin id (for the managed-plugin hook exemption); never echoed. */
|
|
119
|
+
pluginId?: string;
|
|
72
120
|
}
|
|
73
121
|
type Parser = "json" | "json_or_empty" | "jsonc" | "toml";
|
|
74
122
|
export interface ScopeRow {
|
|
@@ -103,6 +151,110 @@ export declare const HARNESS_CONFIG_SCOPES: Readonly<Record<IdentityHarness, rea
|
|
|
103
151
|
* their value is never used.
|
|
104
152
|
*/
|
|
105
153
|
export declare const MCP_LAYER_MERGE: Readonly<Record<IdentityHarness, "replace" | "fields">>;
|
|
154
|
+
/** Synthetic row names: authored, so no plugin id, marketplace name or path reaches a reason. */
|
|
155
|
+
export declare const CLAUDE_PLUGIN_ROW = "claude_plugin";
|
|
156
|
+
export declare const CLAUDE_FOREIGN_PLUGIN_ROW = "claude_plugin_foreign";
|
|
157
|
+
export type AutoUpdateSource = "env_disabled" | "config_disabled" | "settings" | "known_marketplaces" | "default" | "undetermined";
|
|
158
|
+
/** What doctor learned about OUR plugin. Values are raw; the report sanitises them. */
|
|
159
|
+
export interface ClaudePluginSummary {
|
|
160
|
+
installed: boolean;
|
|
161
|
+
/** Our plugin's board server is registered in this cwd (not turned off by `disabledMcpServers`). */
|
|
162
|
+
serverActive: boolean;
|
|
163
|
+
version: string | null;
|
|
164
|
+
pinnedMcpVersion: string | null;
|
|
165
|
+
sourceRef: string | null;
|
|
166
|
+
autoUpdate: {
|
|
167
|
+
effective: boolean;
|
|
168
|
+
source: AutoUpdateSource;
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
export interface ClaudePluginRead {
|
|
172
|
+
/** One synthetic file per enabled plugin read, ranked above user settings. */
|
|
173
|
+
files: ScopeFile[];
|
|
174
|
+
unparseable: string[];
|
|
175
|
+
/** An installPath (or a file under it) resolved outside the cache: nothing beyond it was read. */
|
|
176
|
+
untrusted: boolean;
|
|
177
|
+
failures: string[];
|
|
178
|
+
summary: ClaudePluginSummary;
|
|
179
|
+
hookPolicy: ClaudeHookPolicy;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Which hooks Claude Code runs (round-2 F027/F028; see the header's `hook policy`): settings
|
|
183
|
+
* hooks from every scope, from managed settings only, or none; plugin hooks from every plugin,
|
|
184
|
+
* only from plugins managed settings enable, or none.
|
|
185
|
+
*/
|
|
186
|
+
export interface ClaudeHookPolicy {
|
|
187
|
+
settings: "all" | "managed_only" | "none";
|
|
188
|
+
plugins: "all" | "managed_only" | "none";
|
|
189
|
+
/** Plugin ids managed settings enable (`enabledPlugins[id] === true`), for `managed_only`. */
|
|
190
|
+
managedPluginIds: ReadonlySet<string>;
|
|
191
|
+
/** The managed-plugin set comes from somewhere doctor cannot read (managed `prependPlugins`/`appendPlugins`). */
|
|
192
|
+
unjudgeable: boolean;
|
|
193
|
+
/**
|
|
194
|
+
* The managed tier may not be the file doctor reads (round-3 N1): a server-managed or MDM source
|
|
195
|
+
* replaces the file tier entirely ("first-wins"), and doctor reads neither. True when the probe
|
|
196
|
+
* found evidence of one, or could not complete.
|
|
197
|
+
*/
|
|
198
|
+
managedTierUnreadable: boolean;
|
|
199
|
+
probe: ManagedPolicyProbe;
|
|
200
|
+
}
|
|
201
|
+
export declare const CLAUDE_REMOTE_SETTINGS_FILE = "remote-settings.json";
|
|
202
|
+
/**
|
|
203
|
+
* Storage-v5 (round 4): the binary writes the server-managed payload to its state store
|
|
204
|
+
* (`Re.state("remote-settings")`, a `state` namespace in the config home) and a signed-cache
|
|
205
|
+
* sidecar beside the json path. The store's exact on-disk name is not recoverable from strings,
|
|
206
|
+
* so every candidate is probed; any one present is evidence.
|
|
207
|
+
*/
|
|
208
|
+
export declare const CLAUDE_REMOTE_SETTINGS_STATE_CANDIDATES: readonly string[];
|
|
209
|
+
export declare const CLAUDE_REMOTE_SETTINGS_SIDECARS: readonly string[];
|
|
210
|
+
export declare const MACOS_MANAGED_PREFERENCES = "/Library/Managed Preferences";
|
|
211
|
+
export declare const MACOS_POLICY_PLIST = "com.anthropic.claudecode.plist";
|
|
212
|
+
export declare const WINDOWS_POLICY_KEYS: Readonly<{
|
|
213
|
+
hklm: "HKLM\\SOFTWARE\\Policies\\ClaudeCode";
|
|
214
|
+
hkcu: "HKCU\\SOFTWARE\\Policies\\ClaudeCode";
|
|
215
|
+
}>;
|
|
216
|
+
export declare const WINDOWS_POLICY_VALUE = "Settings";
|
|
217
|
+
/** Where WSL's Windows interop puts reg.exe; the binary reads HKLM policy through this path. */
|
|
218
|
+
export declare const WSL_REG_EXE = "/mnt/c/Windows/System32/reg.exe";
|
|
219
|
+
export declare const WSL_WINDOWS_MANAGED_DIR = "/mnt/c/Program Files/ClaudeCode";
|
|
220
|
+
export type RegQueryOutcome = {
|
|
221
|
+
kind: "present";
|
|
222
|
+
} | {
|
|
223
|
+
kind: "absent";
|
|
224
|
+
} | {
|
|
225
|
+
kind: "error";
|
|
226
|
+
cause: "reg_unavailable" | "reg_timeout" | "reg_failed";
|
|
227
|
+
};
|
|
228
|
+
/** `reg query <key> /v Settings`, read-only. */
|
|
229
|
+
export type RegQuery = (regExe: string, key: string) => RegQueryOutcome;
|
|
230
|
+
/**
|
|
231
|
+
* The binary's own query: `reg query <key> /v Settings`, an argv array, no shell, 5 s. Exit 0 →
|
|
232
|
+
* the value is there; reg's "unable to find" → not there (the binary's own test); anything else —
|
|
233
|
+
* no reg.exe, a timeout, another failure — is an error, never "absent".
|
|
234
|
+
*/
|
|
235
|
+
export declare function regQueryWith(timeoutMs: number): RegQuery;
|
|
236
|
+
export declare const nodeRegQuery: RegQuery;
|
|
237
|
+
/** What doctor looked at for admin policy: authored source names only. */
|
|
238
|
+
export interface ManagedPolicyProbe {
|
|
239
|
+
checked: string[];
|
|
240
|
+
/** Sources that exist: each means a tier doctor cannot read replaces the managed file. */
|
|
241
|
+
evidence: string[];
|
|
242
|
+
/** Authored causes of probes that could not finish (`reg_unavailable`, `reg_timeout`, `reg_failed`, `login_unavailable`). */
|
|
243
|
+
errors: string[];
|
|
244
|
+
/** The platform has admin sources doctor checks only by presence (macOS, Windows, WSL). */
|
|
245
|
+
unreadableSourcesPossible: boolean;
|
|
246
|
+
platformLabel: "Linux" | "macOS" | "Windows" | "WSL";
|
|
247
|
+
}
|
|
248
|
+
export declare function probeManagedPolicy(ctx: ScopeContext): ManagedPolicyProbe;
|
|
249
|
+
/** Evidence of (or an unfinished probe for) a tier that replaces the managed settings FILE. */
|
|
250
|
+
export declare const managedFileTierReplaced: (probe: ManagedPolicyProbe) => boolean;
|
|
251
|
+
/**
|
|
252
|
+
* IF-0-CCPLUGIN-3: every plugin enabled for `ctx.cwd`, followed from `enabledPlugins` to its
|
|
253
|
+
* installed row and read from the cache. Ours is the plugin id `agent-board@message-board` whose
|
|
254
|
+
* marketplace is declared, EVERYWHERE it is declared, as the github repository
|
|
255
|
+
* Consiliency/message-board; anything else contributing a board server or a wake hook is foreign.
|
|
256
|
+
*/
|
|
257
|
+
export declare function readClaudePluginScopes(ctx: ScopeContext): ClaudePluginRead;
|
|
106
258
|
/** A package spec's identity without its version: `@scope/name@1.2.3` → `@scope/name`. */
|
|
107
259
|
export declare function packageIdentity(spec: string): string;
|
|
108
260
|
interface BoardEntryVerdict {
|
|
@@ -114,6 +266,12 @@ export declare function judgeMcpEntry(entry: McpEntry, fs: RegistrationFs): Boar
|
|
|
114
266
|
export interface RegistrationVerdict {
|
|
115
267
|
status: "pass" | "fail" | "undetermined";
|
|
116
268
|
reason: string;
|
|
269
|
+
/** claude only: what doctor learned about our plugin, for the report and the pin check. */
|
|
270
|
+
plugin?: ClaudePluginSummary;
|
|
271
|
+
/** claude only: wake hooks per required event, across settings and plugins. */
|
|
272
|
+
hookCounts?: Record<(typeof CLAUDE_REQUIRED_HOOK_EVENTS)[number], number>;
|
|
273
|
+
/** claude only: which admin-policy sources were probed, for the report. */
|
|
274
|
+
managedPolicy?: ManagedPolicyProbe;
|
|
117
275
|
}
|
|
118
276
|
/**
|
|
119
277
|
* Read every scope row of `harness` for `cwd`. A file that fails to parse is reported, not guessed.
|
|
@@ -123,6 +281,8 @@ export declare function readScopes(harness: IdentityHarness, ctx: ScopeContext):
|
|
|
123
281
|
files: ScopeFile[];
|
|
124
282
|
unparseable: string[];
|
|
125
283
|
};
|
|
284
|
+
/** The `.claude.json` `projects` key Claude Code uses for `cwd`: the git root of its realpath, else the realpath. */
|
|
285
|
+
export declare function claudeProjectKey(ctx: ScopeContext): string;
|
|
126
286
|
/** IF-0-CREDPATH-4 registration, for one working directory. */
|
|
127
287
|
export declare function judgeRegistration(harness: IdentityHarness, ctx: ScopeContext): RegistrationVerdict;
|
|
128
288
|
export {};
|