@brightbeamai/chap-coordinator 0.2.9 → 0.2.10

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/README.md CHANGED
@@ -216,7 +216,12 @@ task's `assignee`.
216
216
 
217
217
  Audit entries are hash-linked when the profile is active.
218
218
  `audit.verify_chain` replays the chain locally and confirms
219
- `prev_hash` continuity.
219
+ `prev_hash` continuity. The verdict names the range it covered:
220
+ `status` is `verified` with `ok: true` only when every entry was
221
+ checked, and `not_evaluated` with `ok: false` when part of the log
222
+ lies outside the chain, which happens when the profile is added to a
223
+ workspace that already has entries. A broken chain is an error. Read
224
+ `status`, or read `ok` and fail closed.
220
225
 
221
226
  `audit.submit_to_scitt` builds a SCITT-shaped signed statement for
222
227
  each audit entry in the requested range and passes it to a
@@ -175,6 +175,19 @@ declare class Coordinator {
175
175
  private opTaskComplete;
176
176
  private opAuditRead;
177
177
  private opReviewRequest;
178
+ /**
179
+ * Verify an optional `approved_artefact_digest` against the artefact under
180
+ * review (review/1.0 S3.2).
181
+ *
182
+ * The digest sits in params, so it is inside whatever the envelope signature
183
+ * covers: the decision then attests the content the reviewer saw rather than
184
+ * a task reference, and a relying party can check it without trusting the
185
+ * Coordinator that produced it. Absent, behaviour is exactly as before.
186
+ *
187
+ * On mismatch nothing is recorded and no state changes, so a client that
188
+ * computes the wrong digest refuses only its own decision.
189
+ */
190
+ private checkArtefactDigest;
178
191
  private opDecide;
179
192
  private opDecideOverride;
180
193
  private opAbstainDeclare;
@@ -175,6 +175,19 @@ declare class Coordinator {
175
175
  private opTaskComplete;
176
176
  private opAuditRead;
177
177
  private opReviewRequest;
178
+ /**
179
+ * Verify an optional `approved_artefact_digest` against the artefact under
180
+ * review (review/1.0 S3.2).
181
+ *
182
+ * The digest sits in params, so it is inside whatever the envelope signature
183
+ * covers: the decision then attests the content the reviewer saw rather than
184
+ * a task reference, and a relying party can check it without trusting the
185
+ * Coordinator that produced it. Absent, behaviour is exactly as before.
186
+ *
187
+ * On mismatch nothing is recorded and no state changes, so a client that
188
+ * computes the wrong digest refuses only its own decision.
189
+ */
190
+ private checkArtefactDigest;
178
191
  private opDecide;
179
192
  private opDecideOverride;
180
193
  private opAbstainDeclare;
package/dist/api.d.cts CHANGED
@@ -1,4 +1,4 @@
1
1
  import './methods.cjs';
2
- export { b as CoordinatorApi, c as CoordinatorError, g as call, m as makeApi } from './api-DE78DA0o.cjs';
2
+ export { b as CoordinatorApi, c as CoordinatorError, g as call, m as makeApi } from './api-CcT_8H5j.cjs';
3
3
  import './types.cjs';
4
4
  import './storage/store.cjs';
package/dist/api.d.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  import './methods.js';
2
- export { b as CoordinatorApi, c as CoordinatorError, g as call, m as makeApi } from './api-CPEk0V6i.js';
2
+ export { b as CoordinatorApi, c as CoordinatorError, g as call, m as makeApi } from './api-Cvkhcalw.js';
3
3
  import './types.js';
4
4
  import './storage/store.js';
package/dist/index.cjs CHANGED
@@ -259,6 +259,7 @@ var E = {
259
259
  NOT_AUTHORISED: -32011,
260
260
  PATCH_FAILED: -32012,
261
261
  REVIEW_LAPSED: -32013,
262
+ REVIEW_ALREADY_OPEN: -32014,
262
263
  // whisper/1.0 profile (profiles/whisper.md S6)
263
264
  WHISPER_ALREADY_ANSWERED: -32020,
264
265
  WHISPER_LAPSED: -32021,
@@ -285,6 +286,7 @@ var E = {
285
286
  SIG_KEY_NOT_FOUND: -32071,
286
287
  SIG_KEY_REVOKED: -32072,
287
288
  SIG_ROTATION_KEY_MISMATCH: -32073,
289
+ SIG_ARTEFACT_DIGEST_MISMATCH: -32074,
288
290
  // audit-scitt/1.0 profile (profiles/audit-scitt.md S8)
289
291
  SCITT_UNREACHABLE: -32080,
290
292
  SCITT_STATEMENT_REJECTED: -32081,
@@ -752,6 +754,12 @@ function registerAuditScitt(coord) {
752
754
  if (!(ws.chain_enabled || coord.options.enableChain)) {
753
755
  return { error: rpcError(E.PARAMS, "Chain not enabled for this workspace") };
754
756
  }
757
+ if (p.from_seq !== void 0 || p.to_seq !== void 0) {
758
+ return { error: rpcError(
759
+ E.PARAMS,
760
+ "Range verification is not implemented; omit from_seq and to_seq to verify the whole log."
761
+ ) };
762
+ }
755
763
  let start = ws.audit.findIndex((e) => e.prev_hash != null);
756
764
  if (start < 0) start = ws.audit.length;
757
765
  const errors = [];
@@ -763,16 +771,35 @@ function registerAuditScitt(coord) {
763
771
  }
764
772
  prev = sha256Hex(Buffer.concat([canonicalize(e.envelope), Buffer.from(expectedPrev, "utf-8")]));
765
773
  }
766
- const storedHead = ws.chain_head ?? ZERO_HASH;
774
+ const storedHead = ws.chain_head === void 0 || ws.chain_head === null ? ZERO_HASH : ws.chain_head;
767
775
  if (prev !== storedHead) {
768
776
  errors.push("chain_head mismatch: replay does not match stored head");
769
777
  }
770
778
  if (errors.length) {
771
779
  return { error: rpcError(E.PARAMS, errors.join("; ")) };
772
780
  }
781
+ const entriesTotal = ws.audit.length;
782
+ const entriesChecked = entriesTotal - start;
783
+ const entriesUnchecked = start;
784
+ if (entriesUnchecked > 0) {
785
+ return { result: {
786
+ status: "not_evaluated",
787
+ ok: false,
788
+ reason: "unchained_prefix",
789
+ entries_total: entriesTotal,
790
+ entries_checked: entriesChecked,
791
+ entries_unchecked: entriesUnchecked,
792
+ checked_from_seq: entriesChecked > 0 ? ws.audit[start].seq : null,
793
+ chain_head: storedHead
794
+ } };
795
+ }
773
796
  return { result: {
797
+ status: "verified",
774
798
  ok: true,
775
- entries_checked: ws.audit.length - start,
799
+ entries_total: entriesTotal,
800
+ entries_checked: entriesChecked,
801
+ entries_unchecked: 0,
802
+ checked_from_seq: entriesChecked > 0 ? ws.audit[start].seq : null,
776
803
  chain_head: storedHead
777
804
  } };
778
805
  });
@@ -2463,12 +2490,38 @@ var Coordinator = class {
2463
2490
  const reviewers = Array.isArray(to) ? to : typeof to === "string" ? [to] : [];
2464
2491
  if (!reviewers.length) return { error: rpcError(E.PARAMS, "review.request needs 'to'") };
2465
2492
  const now = this.now();
2493
+ const rule = p.rule || "any_one_approves";
2494
+ if (task.state === "review_requested" && task.review) {
2495
+ if (contentHash(task.pending_artefact) !== contentHash(p.artefact)) {
2496
+ return { error: rpcError(
2497
+ E.REVIEW_ALREADY_OPEN,
2498
+ "A review is already open on this task with different content. Decide, abstain, escalate or cancel it before requesting review of a new artefact."
2499
+ ) };
2500
+ }
2501
+ if (rule !== task.review.rule) {
2502
+ return { error: rpcError(
2503
+ E.REVIEW_ALREADY_OPEN,
2504
+ `Cannot change the decision rule of an open review (currently ${task.review.rule})`
2505
+ ) };
2506
+ }
2507
+ for (const r of reviewers) {
2508
+ if (!task.review.requested_to.includes(r)) task.review.requested_to.push(r);
2509
+ }
2510
+ if (p.deadline !== void 0) task.review.deadline = p.deadline;
2511
+ task.updated_at = now;
2512
+ return { result: {
2513
+ state: "review_requested",
2514
+ review_id: task.id,
2515
+ amended: true,
2516
+ requested_to: [...task.review.requested_to]
2517
+ } };
2518
+ }
2466
2519
  task.state = "review_requested";
2467
2520
  task.updated_at = now;
2468
2521
  task.review = {
2469
2522
  requested_at: now,
2470
2523
  requested_to: reviewers,
2471
- rule: p.rule || "any_one_approves",
2524
+ rule,
2472
2525
  deadline: p.deadline,
2473
2526
  decisions: []
2474
2527
  };
@@ -2476,6 +2529,37 @@ var Coordinator = class {
2476
2529
  task.pending_artefact = p.artefact;
2477
2530
  return { result: { state: "review_requested", review_id: task.id } };
2478
2531
  }
2532
+ /**
2533
+ * Verify an optional `approved_artefact_digest` against the artefact under
2534
+ * review (review/1.0 S3.2).
2535
+ *
2536
+ * The digest sits in params, so it is inside whatever the envelope signature
2537
+ * covers: the decision then attests the content the reviewer saw rather than
2538
+ * a task reference, and a relying party can check it without trusting the
2539
+ * Coordinator that produced it. Absent, behaviour is exactly as before.
2540
+ *
2541
+ * On mismatch nothing is recorded and no state changes, so a client that
2542
+ * computes the wrong digest refuses only its own decision.
2543
+ */
2544
+ checkArtefactDigest(p, task) {
2545
+ const declared = p.approved_artefact_digest;
2546
+ if (declared === void 0 || declared === null) return null;
2547
+ if (typeof declared !== "string") {
2548
+ return { error: rpcError(E.PARAMS, "approved_artefact_digest must be a string") };
2549
+ }
2550
+ if (task.pending_artefact === void 0) {
2551
+ return { error: rpcError(E.PARAMS, "No artefact under review to bind a digest to") };
2552
+ }
2553
+ const actual = contentHash(task.pending_artefact);
2554
+ if (declared !== actual) {
2555
+ return { error: rpcError(
2556
+ E.SIG_ARTEFACT_DIGEST_MISMATCH,
2557
+ "approved_artefact_digest does not match the artefact under review",
2558
+ { declared, actual }
2559
+ ) };
2560
+ }
2561
+ return null;
2562
+ }
2479
2563
  opDecide(p, kind) {
2480
2564
  const miss = missing(p, ["workspace", "from", "task_id"]);
2481
2565
  if (miss) return { error: rpcError(E.PARAMS, `Missing field: ${miss}`) };
@@ -2492,6 +2576,8 @@ var Coordinator = class {
2492
2576
  }
2493
2577
  const notReviewer = this.requireReviewer(task, p.from);
2494
2578
  if (notReviewer) return notReviewer;
2579
+ const digestError = this.checkArtefactDigest(p, task);
2580
+ if (digestError) return digestError;
2495
2581
  const now = this.now();
2496
2582
  task.review.decisions.push({
2497
2583
  reviewer: p.from,
@@ -2526,6 +2612,8 @@ var Coordinator = class {
2526
2612
  }
2527
2613
  const notReviewer = this.requireReviewer(task, p.from);
2528
2614
  if (notReviewer) return notReviewer;
2615
+ const digestError = this.checkArtefactDigest(p, task);
2616
+ if (digestError) return digestError;
2529
2617
  const base = task.pending_artefact;
2530
2618
  if (base === void 0) return { error: rpcError(E.PARAMS, "No base artefact for override") };
2531
2619
  let applied;