@consiliency/agent-board-cli 1.16.0 → 1.19.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/cli.d.ts CHANGED
@@ -79,6 +79,7 @@ interface ClientLike {
79
79
  agentDashboard(input: JsonObject): Promise<unknown>;
80
80
  agentInbox(input: JsonObject): Promise<unknown>;
81
81
  agentInboxCounts(input: JsonObject): Promise<unknown>;
82
+ wakeRecovery(input: JsonObject): Promise<unknown>;
82
83
  boardDirectory(input: JsonObject): Promise<unknown>;
83
84
  boardMemberships(input: JsonObject): Promise<unknown>;
84
85
  boardFeed(input: JsonObject): Promise<unknown>;
package/dist/cli.js CHANGED
@@ -2,7 +2,7 @@ import { Command, Option } from "commander";
2
2
  import { createHash, randomUUID } from "node:crypto";
3
3
  import { homedir } from "node:os";
4
4
  import { join } from "node:path";
5
- import { createAgentBoardClient, toOperationFailure, beginEnrollment, completeEnrollment, beginDeviceEnrollment, completeDeviceEnrollment, runEnrollment, runDeviceEnrollment, resolveEnrollmentDescriptor, EnrollmentDescriptorError, EnrollmentCommandError, resolveEnrollEndpoint, resolveEnrollSessionUrl, resolveEnrollPublishableKey, resolveJoinDeployment, buildFleetHarnessTemplate, finalizeFleetHarnessTemplate, normalizePublishableKeyFieldReference, normalizeFleetTupleValue, normalizeKeyReference, enrollCommitMarker, assertPresentationSafe, FLEET_TEMPLATE_FAILURE_MESSAGE, PROHIBITED_PRESENTATION_REMOVE_CLASS, buildAgentJoinInstructions, nodeFetchAdapter, buildEnrollKeystore, } from "@consiliency/agent-board-client";
5
+ import { createAgentBoardClient, toOperationFailure, beginEnrollment, completeEnrollment, beginDeviceEnrollment, completeDeviceEnrollment, runEnrollment, runDeviceEnrollment, resolveEnrollmentDescriptor, EnrollmentDescriptorError, EnrollmentCommandError, resolveEnrollEndpoint, resolveEnrollSessionUrl, resolveEnrollPublishableKey, resolveJoinDeployment, buildFleetHarnessTemplate, finalizeFleetHarnessTemplate, normalizePublishableKeyFieldReference, normalizeFleetTupleValue, normalizeKeyReference, enrollCommitMarker, assertPresentationSafe, FLEET_TEMPLATE_FAILURE_MESSAGE, PROHIBITED_PRESENTATION_REMOVE_CLASS, buildAgentJoinInstructions, nodeFetchAdapter, buildEnrollKeystore, AUTHORED_CAUSE_CODES, codeProvenance, markAuthoredCode, } from "@consiliency/agent-board-client";
6
6
  import { DevicePendingEnrollmentStore, FilePendingEnrollmentStore, FleetDeviceContinuationStore, makeBrowserOpener, prepareFleetRuntimeOutput, publishFinalizedFleetTemplate, } from "./enroll_runtime.js";
7
7
  function isRuntimeStatus(value) {
8
8
  return Boolean(value && typeof value === "object" && "runtime" in value);
@@ -29,6 +29,7 @@ class FleetSafeMessageError extends Error {
29
29
  super(message);
30
30
  this.code = code;
31
31
  this.name = "FleetSafeMessageError";
32
+ markAuthoredCode(this);
32
33
  }
33
34
  }
34
35
  /**
@@ -70,17 +71,21 @@ class FleetPreflightError extends FleetSafeMessageError {
70
71
  * blanket redaction is for.
71
72
  */
72
73
  class FleetPostCommitError extends FleetSafeMessageError {
73
- constructor(committedFacts, cause) {
74
+ constructor(committedFacts, origin, cause) {
74
75
  // Only the ids that survived. Region-1 facts are deliberately sparse — that region
75
76
  // renders what this process KNEW rather than what the response echoed — so a fixed
76
77
  // four-field list would print `agentId=,` and read as data while carrying none.
77
- const committed = renderIds(committedFacts) || "(the receipt carried no id we could verify)";
78
+ const committed = renderIds(committedFacts, origin)
79
+ || (origin === "receipt"
80
+ ? "(the receipt carried no id we could verify)"
81
+ : "(this enrollment named nothing to verify against)");
78
82
  const detail = cause instanceof FleetSafeMessageError
79
83
  ? cause.message
80
84
  : `detail withheld (a raw local failure can carry key descriptors)${namedCauseCode(cause)}`;
81
85
  super("fleet_onboarding_committed_local_finalization_failed", "fleet enrollment COMMITTED on the server; only local finalization failed. "
82
86
  + "Do NOT treat this as a failed enrollment and do NOT re-run it — the bounded invite use is spent. "
83
- + `Committed: ${committed}. Local finalization failure: ${detail}`);
87
+ + `${origin === "receipt" ? "Committed" : "Verify against the values you passed as"}: ${committed}. `
88
+ + `Local finalization failure: ${detail}`);
84
89
  this.cause = cause;
85
90
  }
86
91
  }
@@ -140,29 +145,34 @@ function throughPresentationGate(action) {
140
145
  }
141
146
  }
142
147
  /**
143
- * WITHDRAWN (#402). Returns "" for every input, and is kept as the single named place that
144
- * decision is recorded rather than deleted into a diff nobody will read.
148
+ * The one-word cause discriminator, restored on a PROVENANCE gate (#403) after #402
149
+ * withdrew it. #402's two shape gates both rested on a claim about what a credential can
150
+ * look like (a snake_case alphabet, then a 42-character cap) and a fleet invite code or a
151
+ * short OAuth `device_code` fits either. There is no third shape bound, so this gate asks
152
+ * a different question: did WE write this value?
145
153
  *
146
- * It used to put a CAUSE's `code` into a message this CLI prints, behind a shape gate. Two
147
- * successive bounds were defended here and BOTH rested on a claim about what a credential
148
- * can look like: first a lowercase snake_case class (whose alphabet is a subset of
149
- * base64url's, so a fleet invite code qualifies), then a 42-character cap derived from that
150
- * code's invariant 43 — which covered exactly one credential and was then treated as
151
- * covering the channel. The OAuth `device_code` on this same flow is constrained only to be
152
- * non-empty, so a short one passes the cap.
154
+ * Property (plan §2.1): returns ` [code <cause.code>]` iff `cause.code` is a string equal
155
+ * to a member of AUTHORED_CAUSE_CODES — the set derived by
156
+ * `scripts/derive-authored-cause-codes.mjs` from every literal this repository assigns to
157
+ * an error `code` — AND the instance carries the authored value-bound brand for that exact
158
+ * value (`codeProvenance(cause) === "authored"`, see code_provenance.ts). Both halves are
159
+ * needed: the set alone would print a server body that happens to equal a member; the
160
+ * brand alone would print a value the brand was never bound to. A foreign value therefore
161
+ * reaches the operator only if it is already a published member of the set (it carries no
162
+ * foreign byte) AND arrived through an instance that was branded authored for it (R6-1:
163
+ * an equal-value overwrite of an authored code is one bit of equality, not a leak).
153
164
  *
154
- * There is no third bound. A code we did not write is not printed, which is the rule the
155
- * `--harness-kind`, `--local-key-id` and `--publishable-key-field-reference` echoes all
156
- * reached by the same road.
157
- *
158
- * THE COST, stated because it is real: a post-commit or commit-unknown message no longer
159
- * carries `[code <cause>]`, so an operator debugging one of those loses the one-word
160
- * discriminator. Everything else in both messages is unchanged, and both still name the
161
- * state and the action. Restoring it needs a bound that holds for EVERY credential able to
162
- * reach here, not one derived from a single issuer.
165
+ * Every other input returns "": a non-object, a non-string `code`, a non-member, an
166
+ * unbranded instance, a `"foreign"`-branded instance, or an instance whose `code` was
167
+ * written after it was branded.
163
168
  */
164
- function namedCauseCode(_cause) {
165
- return "";
169
+ function namedCauseCode(cause) {
170
+ if (cause === null || typeof cause !== "object")
171
+ return "";
172
+ const { code } = cause;
173
+ if (typeof code !== "string" || !AUTHORED_CAUSE_CODES.has(code))
174
+ return "";
175
+ return codeProvenance(cause) === "authored" ? ` [code ${code}]` : "";
166
176
  }
167
177
  function committedFactsFromOutcome(outcome) {
168
178
  return {
@@ -227,7 +237,7 @@ async function afterFleetCommit(outcome, action) {
227
237
  return await action();
228
238
  }
229
239
  catch (error) {
230
- throw error instanceof FleetPostCommitError ? error : new FleetPostCommitError(committedFactsFromOutcome(outcome), error);
240
+ throw error instanceof FleetPostCommitError ? error : new FleetPostCommitError(committedFactsFromOutcome(outcome), "receipt", error);
231
241
  }
232
242
  }
233
243
  /**
@@ -235,60 +245,42 @@ async function afterFleetCommit(outcome, action) {
235
245
  * agentHandle=,`) is the shape both messages exist to avoid printing: it reads as
236
246
  * evidence while carrying none.
237
247
  */
238
- function renderIds(facts) {
239
- // Gated HERE, at the single render point, rather than at each producer. There are two
240
- // producers of these facts — the client's marker (already normalized) and
241
- // `committedFactsFromOutcome` below (straight from the outcome, never normalized) —
242
- // and the type carries a stated invariant that a non-id-shaped value is not rendered.
243
- // A per-producer gate makes that invariant a thing each new producer must remember;
244
- // gating the render makes it hold for producers that do not exist yet. Without it a
245
- // poisoned `agent_id` in an otherwise contract-valid receipt reaches stdout verbatim,
246
- // newlines and all.
247
- const printable = (value) => /^[A-Za-z0-9._:@/+=-]{1,128}$/.test(value);
248
- // `agentId` gets a TYPED gate, not the generic one, and the asymmetry is deliberate.
249
- //
250
- // Of the four fields, three are already tied to something this process sent: the
251
- // receipt check requires `board_id`/`machine_id`/`agent_handle` to EQUAL the
252
- // authorized tuple, so a contract-valid receipt cannot carry a surprise there — the
253
- // validation does what region 1 achieves by construction. `agent_id` is the one field
254
- // the contract constrains only as non-empty, so it is the one place a receipt can
255
- // carry an arbitrary server-chosen string into an operator's terminal, including a
256
- // value we ourselves sent (the invite code) echoed back.
257
- //
258
- // The schema declares it: `agents.id uuid primary key`. So a non-UUID `agent_id` is
259
- // off-contract, and rendering it as an agent id would be rendering something this
260
- // field cannot legitimately be. This is a type check derived from the contract, not a
261
- // guess at what "looks like an id".
262
- //
263
- // Residual, stated rather than implied: an on-path attacker who can forge a
264
- // contract-valid receipt can also supply a well-formed UUID. No client-side check
265
- // closes that; it needs a signature over the response.
266
- const isUuid = (value) => /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(value);
267
- // THREE typed gates now, not one (Consiliency/message-board#402). `boardId` and
268
- // `machineId` are OPERATOR-SUPPLIED on the indeterminate path — `localFacts` takes them
269
- // from the intent, deliberately, so the message reports what the enrollment ASKED FOR —
270
- // and the generic `printable` alphabet is a superset of base64url, which is what
271
- // `create_invite_link` mints. A bounded invite code pasted into `--board-id` was therefore
272
- // echoed here, on a path that needs no injected runner: an off-contract server answer is
273
- // all it takes.
248
+ function renderIds(facts, origin) {
249
+ // FACTS ARE RENDERED BY WHERE THEY CAME FROM, NEVER BY WHAT THEY LOOK LIKE
250
+ // (Consiliency/message-board#404).
274
251
  //
275
- // The fix is the same argument the `agentId` gate already makes, extended to the two
276
- // fields it fits: `boards.id` and `fleet_machines.id` are `uuid primary key`, so a
277
- // non-UUID value in either is off-contract and rendering it would be rendering something
278
- // the field cannot legitimately be. A type check derived from the schema, not a guess at
279
- // what a credential looks like — that guess has been wrong twice on this branch.
252
+ // This function used to hold three shape gates — a permissive `printable` alphabet and
253
+ // a UUID test — and both were the wrong instrument twice over. A shape gate on the
254
+ // INTENT arm bounds an echo of the operator's own argument instead of removing it: the
255
+ // alphabet is a superset of base64url, so a bounded invite code pasted into `--board-id`
256
+ // passed it, and `<host>-<code>` passed the handle's. A shape gate on the RECEIPT arm is
257
+ // a second, weaker copy of the `exact` check the value already passed, and it HID a
258
+ // legitimate id whose shape the schema does not actually pin.
280
259
  //
281
- // RESIDUAL, stated rather than papered over: `agentHandle` is a slug with no type to
282
- // derive, so it keeps the generic gate. Reaching it with a code requires the code in BOTH
283
- // `--harness-kind` and `--expected-agent-handle` (as `<host>-<code>`), because the argv
284
- // census refuses a single-flag paste — the handle must end with `-<harness-kind>` — before
285
- // any server call. Narrow, contrived, and real; recorded rather than closed.
286
- return [
287
- ["agentId", facts.agentId, isUuid],
288
- ["agentHandle", facts.agentHandle, printable],
289
- ["boardId", facts.boardId, isUuid],
290
- ["machineId", facts.machineId, isUuid],
291
- ].filter(([, value, ok]) => value !== "" && ok(value)).map(([key, value]) => `${key}=${value}`).join(", ");
260
+ // So there is no predicate here at all, and none can be reintroduced without failing
261
+ // B1a (a value hidden on the intent arm whatever its shape) or B2 (a value shown on the
262
+ // receipt arm whatever its shape).
263
+ const present = [
264
+ ["agentId", facts.agentId],
265
+ ["agentHandle", facts.agentHandle],
266
+ ["boardId", facts.boardId],
267
+ ["machineId", facts.machineId],
268
+ ].filter(([, value]) => value !== "");
269
+ if (origin === "receipt") {
270
+ // The server's own answer, after `exact` required it to equal the authorized tuple —
271
+ // the same strings the success path prints in full.
272
+ return present.map(([key, value]) => `${key}=${value}`).join(", ");
273
+ }
274
+ // The operator's own arguments. The value is theirs already; what the message has to
275
+ // carry is WHICH enrollment and WHAT to verify against, and the flag names carry that.
276
+ // `agentId` and `keyId` are `""` on this arm by construction (`enroll.ts` localFacts),
277
+ // so they contribute nothing and are not in the map.
278
+ const FLAG_OF_INTENT_FACT = {
279
+ agentHandle: "--expected-agent-handle",
280
+ boardId: "--board-id",
281
+ machineId: "--machine-id",
282
+ };
283
+ return present.map(([key]) => FLAG_OF_INTENT_FACT[key]).filter((flag) => flag !== undefined).join(", ");
292
284
  }
293
285
  /**
294
286
  * Raised when the server's answer was OFF-CONTRACT, so whether it committed is unknown.
@@ -303,9 +295,11 @@ class FleetIndeterminateError extends FleetSafeMessageError {
303
295
  // Only the ids that actually survived. An all-empty list (`agentId=, agentHandle=`)
304
296
  // is the shape this state exists to stop printing — it reads as data while carrying
305
297
  // none, and on the off-contract path there is frequently nothing to show.
306
- const rendered = renderIds(marker.facts);
298
+ const rendered = renderIds(marker.facts, marker.origin);
307
299
  const observed = rendered
308
- ? ` What this enrollment asked for, to verify against: ${rendered}.`
300
+ ? (marker.origin === "receipt"
301
+ ? ` Ids from the server's answer, to verify against: ${rendered}.`
302
+ : ` Verify against the values you passed as: ${rendered}.`)
309
303
  : " Nothing about this enrollment could be corroborated, so there is no id here to verify against.";
310
304
  super("fleet_onboarding_commit_indeterminate",
311
305
  // Covers both shapes: an off-contract status (201/202/204), and a 200 that carried
@@ -340,7 +334,7 @@ async function throughFleetCommit(call) {
340
334
  throw error;
341
335
  throw marker.state === "indeterminate"
342
336
  ? new FleetIndeterminateError(marker, error)
343
- : new FleetPostCommitError(marker.facts, error);
337
+ : new FleetPostCommitError(marker.facts, marker.origin, error);
344
338
  }
345
339
  }
346
340
  /**
@@ -1512,6 +1506,27 @@ function addReadCommands(program, client, io) {
1512
1506
  recipientAgentId: options.recipientAgentId,
1513
1507
  limit: options.limit,
1514
1508
  })));
1509
+ // WAKETIER0 (plans/WAKE-MULTIHARNESS-ROADMAP.md Phase 1). The terminal binding of
1510
+ // the universal baseline. `wake-recovery` is the primary name and `agent-wake-recovery`
1511
+ // the view-shaped alias, matching `agent-inbox`/`inbox` in the other direction: the
1512
+ // join text an agent is handed spells the short form, so that is the one that must
1513
+ // not drift.
1514
+ //
1515
+ // NO `--recipient-agent-id` OPTION, and its absence is the property (see the MCP
1516
+ // registration): the view self-scopes to current_agent_id() in SQL, so a
1517
+ // caller-supplied recipient could only ever contradict the session. Offering the
1518
+ // flag would invite an operator to "check another agent's" recovery set and get
1519
+ // their own rows back, silently.
1520
+ program
1521
+ .command("wake-recovery")
1522
+ .alias("agent-wake-recovery")
1523
+ .description("your OWN unhandled inbox items (Tier 0: a per-turn check, not a wake)")
1524
+ .option("--board-id <boardId>")
1525
+ .addOption(numberOption("--limit <limit>", "row limit"))
1526
+ .action((options) => runHandler(io, "agent_board.v_agent_wake_recovery", () => client.wakeRecovery({
1527
+ boardId: options.boardId,
1528
+ limit: options.limit,
1529
+ })));
1515
1530
  program
1516
1531
  .command("board-directory")
1517
1532
  .option("--board-id <boardId>")
@@ -2395,7 +2410,7 @@ function addEnrollCommands(program, io, enrollment) {
2395
2410
  // The non-fleet half of what `requiredOption` used to do. The fleet half is the
2396
2411
  // collector's; this arm keeps its own guard because `enrollmentIntentFromCli`
2397
2412
  // returns before the collector when `--fleet-onboarding` is absent.
2398
- if (!options.fleetOnboarding && !options.slug?.trim()) {
2413
+ if (!options.fleetOnboarding && !String(options.slug ?? "").trim()) {
2399
2414
  throw new FleetPreflightError("enroll_run_slug_required", "enroll run requires --slug");
2400
2415
  }
2401
2416
  // ORDER IS THE CONTRACT on this path (#254). The intent is built and the whole
@@ -2587,7 +2602,7 @@ function addEnrollCommands(program, io, enrollment) {
2587
2602
  // this file writes. Only the NAME of each comparison crosses; no operand does,
2588
2603
  // because two of the five are a key reference and a whole template.
2589
2604
  const divergent = [
2590
- ["--slug", options.slug.trim() !== pending.agentSlug],
2605
+ ["--slug", String(options.slug ?? "").trim() !== pending.agentSlug],
2591
2606
  ["--runtime-output", options.runtimeOutput !== continuation.runtimeOutput],
2592
2607
  ["the keystore key reference", continuation.keyReference !== pending.keyReference],
2593
2608
  ["--publishable-key-field-reference", continuation.publishableKeyFieldReference !== pending.publishableKeyFieldReference],
@@ -1,8 +1,9 @@
1
1
  import type { EnrollmentOutcome, FinalizedFleetHarnessTemplate, PendingFleetHarnessTemplate } from "@consiliency/agent-board-client";
2
+ import { type AuthoredCauseCode } from "@consiliency/agent-board-client";
2
3
  import { type EnrollmentIntent, type PendingEnrollment, type PendingEnrollmentStore } from "@consiliency/agent-board-client";
3
4
  export declare class EnrollRuntimeError extends Error {
4
5
  code: string;
5
- constructor(code: string, message: string);
6
+ constructor(code: AuthoredCauseCode, message: string);
6
7
  }
7
8
  /** Versioned, non-secret continuation for an interrupted fleet device flow. */
8
9
  export interface FleetDeviceContinuation {
@@ -8,7 +8,7 @@ import { constants as FS } from "node:fs";
8
8
  import * as fs from "node:fs/promises";
9
9
  import { homedir } from "node:os";
10
10
  import { dirname, join } from "node:path";
11
- import { validateFinalizedFleetHarnessTemplate } from "@consiliency/agent-board-client";
11
+ import { validateFinalizedFleetHarnessTemplate, markAuthoredCode } from "@consiliency/agent-board-client";
12
12
  import { resolveEnrollmentDescriptor, } from "@consiliency/agent-board-client";
13
13
  export class EnrollRuntimeError extends Error {
14
14
  code;
@@ -16,6 +16,7 @@ export class EnrollRuntimeError extends Error {
16
16
  super(message);
17
17
  this.name = "EnrollRuntimeError";
18
18
  this.code = code;
19
+ markAuthoredCode(this);
19
20
  }
20
21
  }
21
22
  export class FleetDeviceContinuationStore {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@consiliency/agent-board-cli",
3
- "version": "1.16.0",
3
+ "version": "1.19.0",
4
4
  "repository": "https://github.com/Consiliency/message-board.git",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -31,7 +31,7 @@
31
31
  "registry": "https://registry.npmjs.org"
32
32
  },
33
33
  "dependencies": {
34
- "@consiliency/agent-board-client": "1.26.0",
34
+ "@consiliency/agent-board-client": "1.29.0",
35
35
  "commander": "^14.0.1"
36
36
  },
37
37
  "license": "Apache-2.0"