@consiliency/agent-board-cli 1.22.5 → 1.23.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/doctor.js ADDED
@@ -0,0 +1,360 @@
1
+ // CREDPATH IF-0-CREDPATH-4 — `agent-board doctor --harness <h>`, as a module. SL-6 registers it.
2
+ //
3
+ // Doctor judges ONE identity: the harness's identity FILE, read through the client's trusted
4
+ // open (`readIdentityFile`), never doctor's own shell environment. Every networked check is bound
5
+ // to that file's handle and fingerprint through a client whose credentials resolve from the
6
+ // file alone. Records written by live harness processes are judged against the same identity.
7
+ //
8
+ // Exit 0 iff every check passes; else 1 if any check fails; else 2. The `runtime` check passes
9
+ // only with a live `wake_producer` at its expected tier, so 0 is never returned without runtime
10
+ // evidence. Output is names and codes only: no identity value, key or path content is printed,
11
+ // and an error's `code` is printed only when this repository authored it (`authoredCode`).
12
+ import { randomBytes } from "node:crypto";
13
+ import { homedir } from "node:os";
14
+ 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";
16
+ export const DOCTOR_CHECKS = Object.freeze([
17
+ "identity_file", "keystore_key", "whoami", "board_membership", "registration", "runtime",
18
+ ]);
19
+ const FINGERPRINT_KEY = "AGENT_BOARD_SIGNING_PUBLIC_KEY_FINGERPRINT";
20
+ const HANDLE_KEY = "AGENT_BOARD_EXPECTED_AGENT_HANDLE";
21
+ const KEY_REFERENCE_KEY = "AGENT_BOARD_AGENT_KEY_REFERENCE";
22
+ if (![FINGERPRINT_KEY, HANDLE_KEY, KEY_REFERENCE_KEY].every((key) => IDENTITY_FILE_KEYS.includes(key))) {
23
+ throw new Error("doctor: an identity key it reads is not in IDENTITY_FILE_KEYS");
24
+ }
25
+ /**
26
+ * An error's `code`, only when this repository wrote it: a member of the derived
27
+ * AUTHORED_CAUSE_CODES that this instance carries under the authored provenance brand — the
28
+ * CLI's `namedCauseCode` gate (#403). Anything else — a code copied from a /session or PostgREST
29
+ * body, an errno, an unbranded error — is replaced by `fallback`, a fixed local classification.
30
+ * A shape test cannot do this: a server may put any well-shaped token in `code` (code review
31
+ * round 1, Astra 4: `sb_secret_…` passed the previous `^[A-Za-z0-9_.:-]{1,80}$` filter).
32
+ */
33
+ export function authoredCode(error, fallback) {
34
+ const value = error?.code;
35
+ if (typeof value === "string" && AUTHORED_CAUSE_CODES.has(value) && codeProvenance(error) === "authored")
36
+ return value;
37
+ return fallback;
38
+ }
39
+ const codeOf = authoredCode;
40
+ /** A refusal the service marks retryable (5xx, 429, …) is about the service, not the identity. */
41
+ function isRetryable(error) {
42
+ let current = error;
43
+ for (let depth = 0; current && depth < 5; depth += 1) {
44
+ if (current.retryable === true)
45
+ return true;
46
+ current = current.cause;
47
+ }
48
+ return false;
49
+ }
50
+ // Transport failures, by the errno/undici codes and error names Node's fetch surfaces. These
51
+ // say nothing about the identity, so they are undetermined; any other refusal is a fail.
52
+ const NETWORK_CODES = new Set([
53
+ "ECONNREFUSED", "ECONNRESET", "ENOTFOUND", "EAI_AGAIN", "ETIMEDOUT", "EHOSTUNREACH", "ENETUNREACH", "EPIPE",
54
+ "UND_ERR_CONNECT_TIMEOUT", "UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "UND_ERR_SOCKET", "UND_ERR_CLOSED",
55
+ ]);
56
+ export function isNetworkError(error) {
57
+ let current = error;
58
+ for (let depth = 0; current && depth < 5; depth += 1) {
59
+ const candidate = current;
60
+ if (typeof candidate.code === "string" && NETWORK_CODES.has(candidate.code))
61
+ return true;
62
+ if (candidate.name === "AbortError" || candidate.name === "TimeoutError")
63
+ return true;
64
+ if (candidate.name === "TypeError" && candidate.message === "fetch failed")
65
+ return true;
66
+ current = candidate.cause;
67
+ }
68
+ return false;
69
+ }
70
+ /**
71
+ * The session mint signs with the identity's key, so a keystore that cannot be reached fails
72
+ * `whoami` too. That says nothing about the identity: undetermined, like `keystore_key`.
73
+ */
74
+ export function isKeystoreUnreachable(error) {
75
+ let current = error;
76
+ for (let depth = 0; current && depth < 5; depth += 1) {
77
+ if (current instanceof KeystoreUnavailableError)
78
+ return true;
79
+ if (current instanceof AgentSocketSelectionError && current.reason === "unreachable")
80
+ return true;
81
+ const code = current.code;
82
+ if (code === "keystore_unavailable" || code === "agent_socket_unreachable")
83
+ return true;
84
+ current = current.cause;
85
+ }
86
+ return false;
87
+ }
88
+ function rowsOf(result) {
89
+ const data = result?.data;
90
+ return Array.isArray(data) ? data.filter((row) => !!row && typeof row === "object") : [];
91
+ }
92
+ /** The ambient env minus every identity and generic name, plus the file: the file is the identity. */
93
+ export function fileIdentityEnv(ambient, values) {
94
+ const drop = new Set([...IDENTITY_TRIGGER_FIELDS, ...GENERIC_SUPABASE_FIELDS]);
95
+ const env = {};
96
+ for (const [name, value] of Object.entries(ambient))
97
+ if (!drop.has(name))
98
+ env[name] = value;
99
+ return { ...env, ...values };
100
+ }
101
+ function defaultClient(fileEnv) {
102
+ const client = createAgentBoardClient({ resolveCredentials: () => resolveCredentials(undefined, fileEnv) });
103
+ return {
104
+ whoami: (input) => client.whoami(input),
105
+ boardMemberships: (query) => client.boardMemberships(query),
106
+ };
107
+ }
108
+ /**
109
+ * The default os_keychain probe. Doctor never holds private-key bytes, so it does not read the
110
+ * item: it asks the client's own backend to SIGN a nonce, exactly as a session mint would. A
111
+ * signature proves the key is present; it does not prove WHICH key, so the result is
112
+ * `present_unverified`.
113
+ *
114
+ * What `whoami` then proves, and what it does not (code review round 1, Opus B1 / Astra 2): the
115
+ * session mint sends `agent_slug`, `key_id`, `key_version` and a signature — never the file's
116
+ * fingerprint — and the server verifies the signature against the key enrolled under that
117
+ * key_id. So a passing `whoami` proves the keychain holds the enrolled key FOR THE FILE'S KEY_ID.
118
+ * It says nothing about the file's `AGENT_BOARD_SIGNING_PUBLIC_KEY_FINGERPRINT`, which stays
119
+ * unverified for os_keychain; the check's reason says so. By default the backend runs with the
120
+ * client's own exec; `spawn` (the client's exported exec seam) exists only so tests can stand in
121
+ * for the keychain CLI. Nothing here reads its output; doctor.test.ts K-0 pins that no
122
+ * key-decoding API is reachable.
123
+ */
124
+ export function osKeychainProbe(spawn) {
125
+ return async (keyReference) => {
126
+ const reference = normalizeKeyReference(keyReference);
127
+ try {
128
+ const capability = await createOsKeychainBackend(spawn ? { exec: spawn } : {})(reference);
129
+ await capability.sign(randomBytes(32));
130
+ return { kind: "present_unverified" };
131
+ }
132
+ catch (error) {
133
+ // The client raises one code for "no item" and "cannot reach the keychain"; only its
134
+ // message tells them apart (pinned by K-2 against the client's real backend).
135
+ if (error instanceof KeystoreUnavailableError && /no key found/.test(error.message)) {
136
+ return { kind: "absent", reason: "key_absent" };
137
+ }
138
+ return { kind: "unreachable", reason: codeOf(error, "keystore_unavailable") };
139
+ }
140
+ };
141
+ }
142
+ /** The default agent_socket probe: the loaded key's fingerprint, derived from the agent's identity list. */
143
+ export const agentSocketProbe = async (keyReference, expectedFingerprint, env) => {
144
+ const reference = normalizeKeyReference(keyReference);
145
+ // A `#<pub>` handle pins the key itself; otherwise the file's fingerprint is the only selector,
146
+ // and a match IS the comparison.
147
+ const pinned = { ...reference, publicKeyFingerprint: reference.handle.includes("#") ? null : expectedFingerprint };
148
+ try {
149
+ const selected = await selectAgentSocketIdentity(pinned, { env });
150
+ return { kind: "found", fingerprint: selected.publicKeyFingerprint };
151
+ }
152
+ catch (error) {
153
+ if (error instanceof AgentSocketSelectionError) {
154
+ return error.reason === "unreachable"
155
+ ? { kind: "unreachable", reason: codeOf(error, "agent_socket_unreachable") }
156
+ : { kind: "absent", reason: codeOf(error, "agent_socket_no_match") };
157
+ }
158
+ // No socket to ask (`SSH_AUTH_SOCK` unset): nothing was learned about the key.
159
+ if (error instanceof KeystoreUnavailableError)
160
+ return { kind: "unreachable", reason: codeOf(error, "keystore_unavailable") };
161
+ return { kind: "absent", reason: codeOf(error, "key_reference_invalid") };
162
+ }
163
+ };
164
+ const defaultKeystoreProbe = async (keyReference, expectedFingerprint, env) => {
165
+ const resolver = normalizeKeyReference(keyReference).resolver;
166
+ if (resolver === "agent_socket")
167
+ return agentSocketProbe(keyReference, expectedFingerprint, env);
168
+ if (resolver === "os_keychain")
169
+ return osKeychainProbe()(keyReference, expectedFingerprint, env);
170
+ return { kind: "unreachable", reason: `resolver_unsupported:${resolver}` };
171
+ };
172
+ const check = (status, reason) => ({ status, reason });
173
+ export const OS_KEYCHAIN_PRESENT = "present; key_id proven server-side by whoami; file fingerprint unverified for os_keychain";
174
+ async function keystoreCheck(values, env, probe) {
175
+ let result;
176
+ try {
177
+ result = await probe(values[KEY_REFERENCE_KEY], values[FINGERPRINT_KEY], env);
178
+ }
179
+ catch (error) {
180
+ return check("undetermined", codeOf(error, "keystore_probe_error"));
181
+ }
182
+ if (result.kind === "unreachable")
183
+ return check("undetermined", result.reason);
184
+ if (result.kind === "absent")
185
+ return check("fail", result.reason);
186
+ // Presence, and the key_id is proven by `whoami`; the file's fingerprint is NOT (see osKeychainProbe).
187
+ if (result.kind === "present_unverified")
188
+ return check("pass", OS_KEYCHAIN_PRESENT);
189
+ return result.fingerprint === values[FINGERPRINT_KEY]
190
+ ? check("pass", "fingerprint_matches")
191
+ : check("fail", "fingerprint_mismatch");
192
+ }
193
+ async function whoamiCheck(client, expectedHandle) {
194
+ let rows;
195
+ try {
196
+ rows = rowsOf(await client.whoami({}));
197
+ }
198
+ catch (error) {
199
+ if (isNetworkError(error))
200
+ return { check: check("undetermined", "network_error"), agentId: null };
201
+ if (isKeystoreUnreachable(error))
202
+ return { check: check("undetermined", "keystore_unreachable"), agentId: null };
203
+ if (isRetryable(error))
204
+ return { check: check("undetermined", "service_unavailable"), agentId: null };
205
+ return { check: check("fail", codeOf(error, "whoami_refused")), agentId: null };
206
+ }
207
+ const row = rows[0];
208
+ if (!row)
209
+ return { check: check("fail", "no_acting_identity"), agentId: null };
210
+ if (row.handle !== expectedHandle)
211
+ return { check: check("fail", "handle_mismatch"), agentId: null };
212
+ const agentId = typeof row.agent_id === "string" ? row.agent_id : null;
213
+ if (!agentId)
214
+ return { check: check("fail", "no_agent_id"), agentId: null };
215
+ return { check: check("pass", "expected_handle"), agentId };
216
+ }
217
+ const ACTIVE_MEMBERSHIP = new Set(["joined", "subscribed"]);
218
+ async function membershipCheck(client, agentId) {
219
+ let rows;
220
+ try {
221
+ rows = rowsOf(await client.boardMemberships({ agentId, limit: 50 }));
222
+ }
223
+ catch (error) {
224
+ if (isNetworkError(error))
225
+ return check("undetermined", "network_error");
226
+ if (isRetryable(error))
227
+ return check("undetermined", "service_unavailable");
228
+ return check("fail", codeOf(error, "membership_read_refused"));
229
+ }
230
+ const active = rows.filter((row) => row.agent_id === agentId && ACTIVE_MEMBERSHIP.has(String(row.membership_state)));
231
+ return active.length > 0 ? check("pass", "member") : check("fail", "no_membership");
232
+ }
233
+ const TIER_RANK = { tier0: 0, wake: 1 };
234
+ /** IF-0-CREDPATH-4 runtime, over already-read records. Exported for its fixtures. */
235
+ export function judgeRuntime(harness, entries, identity) {
236
+ const fails = [];
237
+ const unknowns = [];
238
+ if (entries.some((entry) => entry.status === "untrusted"))
239
+ fails.push("record_untrusted");
240
+ const live = entries.filter((entry) => entry.status === "live")
241
+ .map((entry) => entry.record);
242
+ for (const record of live) {
243
+ if (record.identitySource === "env")
244
+ unknowns.push("identity_source_env");
245
+ else if (identity === null)
246
+ unknowns.push("identity_unknown");
247
+ else if (record.identityHandle !== identity.handle || record.identityFingerprint !== identity.fingerprint)
248
+ fails.push("identity_mismatch");
249
+ }
250
+ const producers = live.filter((record) => record.role === "wake_producer");
251
+ const liveProducer = producers.length > 0;
252
+ for (const record of live) {
253
+ if (record.wake.tier === "arming") {
254
+ unknowns.push("arming");
255
+ continue;
256
+ }
257
+ if (record.role === "wake_producer") {
258
+ if (TIER_RANK[record.wake.tier] < TIER_RANK[expectedWakeTier(harness, record.flags)])
259
+ fails.push("below_expected_tier");
260
+ }
261
+ else if (record.wake.tier !== "tier0") {
262
+ fails.push(`${record.role}_above_tier0`);
263
+ }
264
+ else if (record.role === "tools_only" && !liveProducer) {
265
+ unknowns.push("tools_only_without_producer");
266
+ }
267
+ }
268
+ if (!liveProducer)
269
+ unknowns.push("no_live_wake_producer");
270
+ const byHash = new Map();
271
+ for (const record of live) {
272
+ if (record.wake.tier !== "wake" || record.sessionHash === null)
273
+ continue;
274
+ byHash.set(record.sessionHash, (byHash.get(record.sessionHash) ?? 0) + 1);
275
+ }
276
+ if ([...byHash.values()].some((count) => count > 1))
277
+ fails.push("duplicate_wake_server");
278
+ const unique = (list) => [...new Set(list)].join(",");
279
+ if (fails.length > 0)
280
+ return check("fail", unique(fails));
281
+ if (unknowns.length > 0)
282
+ return check("undetermined", unique(unknowns));
283
+ return check("pass", "wake_producer_live");
284
+ }
285
+ function worst(checks) {
286
+ return checks.find((item) => item.status === "fail")
287
+ ?? checks.find((item) => item.status === "undetermined")
288
+ ?? checks[0];
289
+ }
290
+ export async function runDoctor(options) {
291
+ assertIdentityHarness(options.harness);
292
+ const harness = options.harness;
293
+ const env = options.env ?? process.env;
294
+ const cwd = options.cwd ?? process.cwd();
295
+ const trust = options.trust ?? {};
296
+ const checks = {};
297
+ let values = null;
298
+ try {
299
+ values = (await readIdentityFile({ ...trust, harness, env })).values;
300
+ checks.identity_file = check("pass", "trusted");
301
+ }
302
+ catch (error) {
303
+ checks.identity_file = check("fail", codeOf(error, "identity_unreadable"));
304
+ }
305
+ if (values === null) {
306
+ checks.keystore_key = check("undetermined", "no_identity_file");
307
+ checks.whoami = check("undetermined", "no_identity_file");
308
+ checks.board_membership = check("undetermined", "no_identity_file");
309
+ }
310
+ else {
311
+ const fileEnv = fileIdentityEnv(env, values);
312
+ checks.keystore_key = await keystoreCheck(values, fileEnv, options.probeKeystore ?? defaultKeystoreProbe);
313
+ let client = null;
314
+ try {
315
+ client = (options.createClient ?? defaultClient)(fileEnv);
316
+ }
317
+ catch (error) {
318
+ checks.whoami = check("fail", codeOf(error, "client_unavailable"));
319
+ }
320
+ if (client) {
321
+ const who = await whoamiCheck(client, values[HANDLE_KEY]);
322
+ checks.whoami = who.check;
323
+ checks.board_membership = who.agentId === null
324
+ ? check("undetermined", "whoami_not_passed")
325
+ : await membershipCheck(client, who.agentId);
326
+ }
327
+ else {
328
+ checks.board_membership = check("undetermined", "whoami_not_passed");
329
+ }
330
+ }
331
+ let entries = [];
332
+ try {
333
+ entries = readSessionRecords(harness, { ...trust, env, isAlive: options.isAlive });
334
+ checks.runtime = judgeRuntime(harness, entries, values && { handle: values[HANDLE_KEY], fingerprint: values[FINGERPRINT_KEY] });
335
+ }
336
+ catch (error) {
337
+ checks.runtime = check("undetermined", codeOf(error, "records_unreadable"));
338
+ }
339
+ // Registration is read for this directory and for every live record's.
340
+ const cwds = [...new Set([cwd, ...entries.flatMap((entry) => entry.status === "live" ? [entry.record.cwd] : [])])];
341
+ const fs = options.registrationFs ?? nodeRegistrationFs;
342
+ const home = env.HOME && env.HOME.trim() !== "" ? env.HOME : homedir();
343
+ checks.registration = worst(cwds.map((dir) => {
344
+ try {
345
+ return judgeRegistration(harness, { cwd: dir, home, env, fs, claudeManagedDir: options.claudeManagedDir ?? "/etc/claude-code" });
346
+ }
347
+ catch (error) {
348
+ return check("undetermined", codeOf(error, "registration_unreadable"));
349
+ }
350
+ }));
351
+ const ordered = Object.fromEntries(DOCTOR_CHECKS.map((name) => [name, checks[name]]));
352
+ const statuses = Object.values(ordered).map((item) => item.status);
353
+ const exit = statuses.every((status) => status === "pass") ? 0 : statuses.includes("fail") ? 1 : 2;
354
+ return { harness, exit, checks: ordered };
355
+ }
356
+ /** Human-readable form; `--json` prints the report object itself. */
357
+ export function formatDoctorReport(report) {
358
+ const lines = DOCTOR_CHECKS.map((name) => `${report.checks[name].status.padEnd(12)} ${name}: ${report.checks[name].reason}`);
359
+ return [`agent-board doctor --harness ${report.harness}`, ...lines, `exit ${report.exit}`].join("\n");
360
+ }
@@ -0,0 +1,128 @@
1
+ import type { IdentityHarness } from "@consiliency/agent-board-client";
2
+ export declare const BOARD_MCP_MARKER = "agent-board-mcp";
3
+ export declare const METADATA_ONLY_FLAG = "--metadata-only";
4
+ export declare const OPENCODE_PLUGIN_PACKAGE = "@consiliency/agent-board-opencode";
5
+ export declare const PI_PACKAGE = "npm:@consiliency/agent-board-pi";
6
+ /** Claude Code events that each need exactly one `wake-hook.sh` hook (`docs/local-wake-signal.md`). */
7
+ export declare const CLAUDE_REQUIRED_HOOK_EVENTS: readonly ["SessionStart", "FileChanged"];
8
+ export declare const WAKE_HOOK_MARKER = "wake-hook.sh";
9
+ export interface ScopeContext {
10
+ cwd: string;
11
+ home: string;
12
+ env: Record<string, string | undefined>;
13
+ /** Claude Code's managed directory (Linux `/etc/claude-code`); injectable for tests. */
14
+ claudeManagedDir: string;
15
+ fs: RegistrationFs;
16
+ }
17
+ /** The filesystem reads registration makes; everything is read-only. */
18
+ export interface RegistrationFs {
19
+ /** File text, or null when the file does not exist. */
20
+ readFile(path: string): string | null;
21
+ /** Sorted entry names, or null when the directory does not exist. */
22
+ readdir(path: string): string[] | null;
23
+ lstat(path: string): {
24
+ uid: number;
25
+ size: number;
26
+ isFile: boolean;
27
+ } | null;
28
+ exists(path: string): boolean;
29
+ getuid(): number;
30
+ }
31
+ export declare const nodeRegistrationFs: RegistrationFs;
32
+ export declare class UnparseableConfigError extends Error {
33
+ constructor(message: string);
34
+ }
35
+ /** Keys that name a prototype slot rather than data: a file carrying one is refused. */
36
+ export declare const FORBIDDEN_CONFIG_KEYS: ReadonlySet<string>;
37
+ /** An OWN property of `value`, else undefined: nothing is read through a prototype. */
38
+ export declare function own(value: unknown, key: string): unknown;
39
+ /** JSONC: `//` and `/* *\/` comments and trailing commas removed outside strings, then JSON. */
40
+ export declare function parseJsonc(text: string): unknown;
41
+ /**
42
+ * A restricted TOML reader: enough of the grammar to read `[mcp_servers.*]` and `[projects.*]`
43
+ * tables without a new dependency, and fail-closed on anything outside it. It handles bare and
44
+ * quoted dotted keys, `[table]` and `[[array.table]]` headers, basic/literal (and multi-line)
45
+ * strings, arrays over several lines, inline tables, and bare scalars. Sub-tables nest, so
46
+ * `[mcp_servers.x.env]` is `x`'s `env`, never a server named `x.env`.
47
+ */
48
+ export declare function parseRestrictedToml(text: string): Record<string, unknown>;
49
+ type McpShape = "claude" | "toml" | "agy" | "opencode";
50
+ export interface McpEntry {
51
+ /** Server name (the harness merges same-named entries across its layers). */
52
+ name: string;
53
+ command: string | null;
54
+ args: string[];
55
+ /** The harness will not launch it (`disabled: true`, `enabled = false`). */
56
+ disabled: boolean;
57
+ /** The entry's own fields, as written: a field-merging harness overlays these across layers. */
58
+ fields: Readonly<Record<string, unknown>>;
59
+ shape: McpShape;
60
+ }
61
+ export interface ScopeContribution {
62
+ mcp: McpEntry[];
63
+ /** Claude hooks: event → the command strings registered for it. */
64
+ hooks: Record<string, string[]>;
65
+ /** opencode `plugin` / pi `packages` specs. */
66
+ packages: string[];
67
+ }
68
+ export interface ScopeFile {
69
+ row: string;
70
+ path: string;
71
+ contribution: ScopeContribution;
72
+ }
73
+ type Parser = "json" | "json_or_empty" | "jsonc" | "toml";
74
+ export interface ScopeRow {
75
+ /** Stable row name; reported, and keyed by the per-row fixture table in doctor.test.ts. */
76
+ name: string;
77
+ parser: Parser;
78
+ /** Precedence within the harness: a same-named entry in a higher-ranked row overrides. */
79
+ rank: number;
80
+ /**
81
+ * For a row whose paths walk from cwd up to the git root (cwd first), one file per directory:
82
+ * which end wins when two of its files name the same server. Absent for a single-path row, and
83
+ * for a row that already returns its paths in application order (`opencode_project`, which has
84
+ * two files per directory and must not have them swapped).
85
+ */
86
+ walkWins?: "nearest" | "farthest";
87
+ /** Every candidate file for this row (absent ones are skipped). */
88
+ paths(ctx: ScopeContext): string[];
89
+ /** Whether a present file at `path` is loaded by the harness (codex trust). Default: yes. */
90
+ loaded?(path: string, ctx: ScopeContext): boolean;
91
+ extract(document: unknown, ctx: ScopeContext): ScopeContribution;
92
+ }
93
+ /** Judge-relevant fields of one entry, from its own fields only. */
94
+ export declare function mcpEntry(name: string, fields: Readonly<Record<string, unknown>>, shape: McpShape): McpEntry;
95
+ /** Directories from `cwd` up to its git root (inclusive); just `cwd` outside a repository. */
96
+ export declare function walkToGitRoot(cwd: string, fs: RegistrationFs): string[];
97
+ /** The frozen, re-checked scope table (see the header for each amendment and its evidence). */
98
+ export declare const HARNESS_CONFIG_SCOPES: Readonly<Record<IdentityHarness, readonly ScopeRow[]>>;
99
+ /**
100
+ * How a harness combines a same-named entry across its layers (see the header): codex and
101
+ * opencode overlay the higher layer's fields (measured); claude replaces the whole entry
102
+ * (inferred); grok is assumed to behave like codex (unmeasured); agy and pi have one layer, so
103
+ * their value is never used.
104
+ */
105
+ export declare const MCP_LAYER_MERGE: Readonly<Record<IdentityHarness, "replace" | "fields">>;
106
+ /** A package spec's identity without its version: `@scope/name@1.2.3` → `@scope/name`. */
107
+ export declare function packageIdentity(spec: string): string;
108
+ interface BoardEntryVerdict {
109
+ board: boolean;
110
+ flagged: boolean;
111
+ }
112
+ /** A board MCP entry names `agent-board-mcp` in its command/args, or through ONE user-owned wrapper file. */
113
+ export declare function judgeMcpEntry(entry: McpEntry, fs: RegistrationFs): BoardEntryVerdict;
114
+ export interface RegistrationVerdict {
115
+ status: "pass" | "fail" | "undetermined";
116
+ reason: string;
117
+ }
118
+ /**
119
+ * Read every scope row of `harness` for `cwd`. A file that fails to parse is reported, not guessed.
120
+ * `files` is in ascending precedence: a later file's same-named entry overrides an earlier one's.
121
+ */
122
+ export declare function readScopes(harness: IdentityHarness, ctx: ScopeContext): {
123
+ files: ScopeFile[];
124
+ unparseable: string[];
125
+ };
126
+ /** IF-0-CREDPATH-4 registration, for one working directory. */
127
+ export declare function judgeRegistration(harness: IdentityHarness, ctx: ScopeContext): RegistrationVerdict;
128
+ export {};