@brightbeamai/chap-coordinator 0.2.8 → 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/dist/index.d.cts CHANGED
@@ -1,5 +1,5 @@
1
- import { C as CoordinatorOptions, a as Coordinator } from './api-C3nkEk-B.cjs';
2
- export { b as CoordinatorApi, c as CoordinatorError, d as CredentialVerifier, E as EscalationPolicyFn, H as Handler, I as IdFactory, P as PRIVILEGED_METHODS, R as ReviewDepthPolicyFn, e as RoutingPolicyFn, S as ScittReceiptVerifier, f as ScittSubmitter, T as TokenVerifier, g as call, m as makeApi } from './api-C3nkEk-B.cjs';
1
+ import { C as CoordinatorOptions, a as Coordinator } from './api-CcT_8H5j.cjs';
2
+ export { b as CoordinatorApi, c as CoordinatorError, d as CredentialVerifier, E as EscalationPolicyFn, H as Handler, I as IdFactory, P as PRIVILEGED_METHODS, R as ReviewDepthPolicyFn, e as RoutingPolicyFn, S as ScittReceiptVerifier, f as ScittSubmitter, T as TokenVerifier, g as call, m as makeApi } from './api-CcT_8H5j.cjs';
3
3
  export { MethodName, MethodParams, MethodResult, MethodTable } from './methods.cjs';
4
4
  export { MemoryStore, Store, WorkspaceRecord } from './storage/store.cjs';
5
5
  export { ArtefactId, ArtefactRoutingHints, AuditEntry, AuditListener, Deliberation, DeliberationVote, Envelope, Handoff, HandoffTaskItem, JsonPatchOp, Jwk, KeyRecord, Member, Mode, OverrideArtefact, ParticipantType, ParticipantUri, PauseScope, ReviewDecision, RouteDecisionArtefact, SnapshotArtefact, Task, TaskId, TaskRoutingHints, TaskState, WhisperPrompt, Workspace, WorkspaceId, WorkspaceState, modeLE } from './types.cjs';
@@ -107,7 +107,7 @@ declare function makeDefaultPolicy(seniorTarget: string): Pick<CoordinatorOption
107
107
  * happened outside CHAP, wrap it so the audit log carries a faithful,
108
108
  * citable record.
109
109
  *
110
- * Spec target: CHAP 0.2. MCP 2025-11-25. A2A 1.0.
110
+ * Spec target: CHAP 0.2. MCP 2026-07-28. A2A 1.0.
111
111
  */
112
112
 
113
113
  interface WrapMcpToolCallOptions {
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { C as CoordinatorOptions, a as Coordinator } from './api-CmSvlIqO.js';
2
- export { b as CoordinatorApi, c as CoordinatorError, d as CredentialVerifier, E as EscalationPolicyFn, H as Handler, I as IdFactory, P as PRIVILEGED_METHODS, R as ReviewDepthPolicyFn, e as RoutingPolicyFn, S as ScittReceiptVerifier, f as ScittSubmitter, T as TokenVerifier, g as call, m as makeApi } from './api-CmSvlIqO.js';
1
+ import { C as CoordinatorOptions, a as Coordinator } from './api-Cvkhcalw.js';
2
+ export { b as CoordinatorApi, c as CoordinatorError, d as CredentialVerifier, E as EscalationPolicyFn, H as Handler, I as IdFactory, P as PRIVILEGED_METHODS, R as ReviewDepthPolicyFn, e as RoutingPolicyFn, S as ScittReceiptVerifier, f as ScittSubmitter, T as TokenVerifier, g as call, m as makeApi } from './api-Cvkhcalw.js';
3
3
  export { MethodName, MethodParams, MethodResult, MethodTable } from './methods.js';
4
4
  export { MemoryStore, Store, WorkspaceRecord } from './storage/store.js';
5
5
  export { ArtefactId, ArtefactRoutingHints, AuditEntry, AuditListener, Deliberation, DeliberationVote, Envelope, Handoff, HandoffTaskItem, JsonPatchOp, Jwk, KeyRecord, Member, Mode, OverrideArtefact, ParticipantType, ParticipantUri, PauseScope, ReviewDecision, RouteDecisionArtefact, SnapshotArtefact, Task, TaskId, TaskRoutingHints, TaskState, WhisperPrompt, Workspace, WorkspaceId, WorkspaceState, modeLE } from './types.js';
@@ -107,7 +107,7 @@ declare function makeDefaultPolicy(seniorTarget: string): Pick<CoordinatorOption
107
107
  * happened outside CHAP, wrap it so the audit log carries a faithful,
108
108
  * citable record.
109
109
  *
110
- * Spec target: CHAP 0.2. MCP 2025-11-25. A2A 1.0.
110
+ * Spec target: CHAP 0.2. MCP 2026-07-28. A2A 1.0.
111
111
  */
112
112
 
113
113
  interface WrapMcpToolCallOptions {
package/dist/index.js CHANGED
@@ -1,10 +1,3 @@
1
- var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
2
- get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
3
- }) : x)(function(x) {
4
- if (typeof require !== "undefined") return require.apply(this, arguments);
5
- throw Error('Dynamic require of "' + x + '" is not supported');
6
- });
7
-
8
1
  // src/canonical.ts
9
2
  import { createHash } from "crypto";
10
3
  var ZERO_HASH = "sha256:" + "0".repeat(64);
@@ -169,12 +162,7 @@ var IdFactory = class {
169
162
  return this.counter * 0x9E3779B97F4A7C15n & (1n << 80n) - 1n;
170
163
  }
171
164
  const buf = new Uint8Array(10);
172
- if (typeof crypto !== "undefined" && crypto.getRandomValues) {
173
- crypto.getRandomValues(buf);
174
- } else {
175
- const nodeCrypto = __require("crypto");
176
- buf.set(nodeCrypto.randomBytes(10));
177
- }
165
+ crypto.getRandomValues(buf);
178
166
  let n = 0n;
179
167
  for (const b of buf) n = n << 8n | BigInt(b);
180
168
  return n;
@@ -223,6 +211,7 @@ var E = {
223
211
  NOT_AUTHORISED: -32011,
224
212
  PATCH_FAILED: -32012,
225
213
  REVIEW_LAPSED: -32013,
214
+ REVIEW_ALREADY_OPEN: -32014,
226
215
  // whisper/1.0 profile (profiles/whisper.md S6)
227
216
  WHISPER_ALREADY_ANSWERED: -32020,
228
217
  WHISPER_LAPSED: -32021,
@@ -249,6 +238,7 @@ var E = {
249
238
  SIG_KEY_NOT_FOUND: -32071,
250
239
  SIG_KEY_REVOKED: -32072,
251
240
  SIG_ROTATION_KEY_MISMATCH: -32073,
241
+ SIG_ARTEFACT_DIGEST_MISMATCH: -32074,
252
242
  // audit-scitt/1.0 profile (profiles/audit-scitt.md S8)
253
243
  SCITT_UNREACHABLE: -32080,
254
244
  SCITT_STATEMENT_REJECTED: -32081,
@@ -716,6 +706,12 @@ function registerAuditScitt(coord) {
716
706
  if (!(ws.chain_enabled || coord.options.enableChain)) {
717
707
  return { error: rpcError(E.PARAMS, "Chain not enabled for this workspace") };
718
708
  }
709
+ if (p.from_seq !== void 0 || p.to_seq !== void 0) {
710
+ return { error: rpcError(
711
+ E.PARAMS,
712
+ "Range verification is not implemented; omit from_seq and to_seq to verify the whole log."
713
+ ) };
714
+ }
719
715
  let start = ws.audit.findIndex((e) => e.prev_hash != null);
720
716
  if (start < 0) start = ws.audit.length;
721
717
  const errors = [];
@@ -727,16 +723,35 @@ function registerAuditScitt(coord) {
727
723
  }
728
724
  prev = sha256Hex(Buffer.concat([canonicalize(e.envelope), Buffer.from(expectedPrev, "utf-8")]));
729
725
  }
730
- const storedHead = ws.chain_head ?? ZERO_HASH;
726
+ const storedHead = ws.chain_head === void 0 || ws.chain_head === null ? ZERO_HASH : ws.chain_head;
731
727
  if (prev !== storedHead) {
732
728
  errors.push("chain_head mismatch: replay does not match stored head");
733
729
  }
734
730
  if (errors.length) {
735
731
  return { error: rpcError(E.PARAMS, errors.join("; ")) };
736
732
  }
733
+ const entriesTotal = ws.audit.length;
734
+ const entriesChecked = entriesTotal - start;
735
+ const entriesUnchecked = start;
736
+ if (entriesUnchecked > 0) {
737
+ return { result: {
738
+ status: "not_evaluated",
739
+ ok: false,
740
+ reason: "unchained_prefix",
741
+ entries_total: entriesTotal,
742
+ entries_checked: entriesChecked,
743
+ entries_unchecked: entriesUnchecked,
744
+ checked_from_seq: entriesChecked > 0 ? ws.audit[start].seq : null,
745
+ chain_head: storedHead
746
+ } };
747
+ }
737
748
  return { result: {
749
+ status: "verified",
738
750
  ok: true,
739
- entries_checked: ws.audit.length - start,
751
+ entries_total: entriesTotal,
752
+ entries_checked: entriesChecked,
753
+ entries_unchecked: 0,
754
+ checked_from_seq: entriesChecked > 0 ? ws.audit[start].seq : null,
740
755
  chain_head: storedHead
741
756
  } };
742
757
  });
@@ -1517,6 +1532,16 @@ function registerSecuritySigned(coord) {
1517
1532
  for (const f of ["target_uri", "kid"]) {
1518
1533
  if (!(f in p)) return { error: rpcError(E.PARAMS, `Missing field: ${f}`) };
1519
1534
  }
1535
+ const sender = p.from;
1536
+ if (sender !== p.target_uri) {
1537
+ const caller = sender ? ws.members.get(sender) : void 0;
1538
+ if (!caller || caller.role !== "admin") {
1539
+ return { error: rpcError(
1540
+ E.NOT_AUTHORISED,
1541
+ "Revoking another member's key requires the admin role"
1542
+ ) };
1543
+ }
1544
+ }
1520
1545
  const member = ws.members.get(p.target_uri);
1521
1546
  if (!member) return { error: rpcError(E.SIG_KEY_NOT_FOUND, `Unknown target: ${p.target_uri}`) };
1522
1547
  const key = member.keys.find((k) => k.kid === p.kid);
@@ -1608,7 +1633,8 @@ function registerWhisper(coord) {
1608
1633
  prompt.answer_option = answerOption;
1609
1634
  prompt.answer_text = answerText;
1610
1635
  prompt.comment = p.comment;
1611
- return { result: { answered: true, whisper_id: prompt.id } };
1636
+ p.task_id = prompt.task_id;
1637
+ return { result: { answered: true, whisper_id: prompt.id, task_id: prompt.task_id } };
1612
1638
  });
1613
1639
  coord.checkWhisperLapses = (workspaceId, now) => {
1614
1640
  const ws = coord.workspaces.get(workspaceId);
@@ -1632,6 +1658,11 @@ function registerWhisper(coord) {
1632
1658
  ts: coord.now(),
1633
1659
  kind: "whisper_lapsed",
1634
1660
  whisper_id: prompt.id,
1661
+ // Carry the task so a task-filtered audit.read surfaces the
1662
+ // lapse. A lapsed whisper applies its default with no human
1663
+ // input, so it is the case most in need of being visible on
1664
+ // the task's thread.
1665
+ task_id: prompt.task_id,
1635
1666
  default_applied: prompt.default_if_lapsed
1636
1667
  }
1637
1668
  };
@@ -1833,6 +1864,7 @@ var Coordinator = class {
1833
1864
  mode_ceiling: ws.mode_ceiling,
1834
1865
  routing_policy_uri: ws.routing_policy_uri,
1835
1866
  step_up_window_sec: ws.step_up_window_sec,
1867
+ min_acr: ws.min_acr,
1836
1868
  members: Array.from(ws.members.values()),
1837
1869
  tasks: Array.from(ws.tasks.values()),
1838
1870
  overrides: Array.from(ws.overrides.values()),
@@ -1841,6 +1873,7 @@ var Coordinator = class {
1841
1873
  handoffs: Array.from(ws.handoffs.values()),
1842
1874
  snapshots: Array.from(ws.snapshots.values()),
1843
1875
  route_decisions: Array.from(ws.route_decisions.values()),
1876
+ idempotency_keys: ws.idempotency_keys,
1844
1877
  audit: ws.audit,
1845
1878
  chain_head: ws.chain_head,
1846
1879
  chain_enabled: ws.chain_enabled
@@ -1863,6 +1896,7 @@ var Coordinator = class {
1863
1896
  mode_ceiling: w.mode_ceiling ?? "production",
1864
1897
  routing_policy_uri: w.routing_policy_uri,
1865
1898
  step_up_window_sec: w.step_up_window_sec ?? 300,
1899
+ min_acr: w.min_acr,
1866
1900
  members: /* @__PURE__ */ new Map(),
1867
1901
  tasks: /* @__PURE__ */ new Map(),
1868
1902
  overrides: /* @__PURE__ */ new Map(),
@@ -1871,6 +1905,7 @@ var Coordinator = class {
1871
1905
  handoffs: /* @__PURE__ */ new Map(),
1872
1906
  snapshots: /* @__PURE__ */ new Map(),
1873
1907
  route_decisions: /* @__PURE__ */ new Map(),
1908
+ idempotency_keys: w.idempotency_keys ?? {},
1874
1909
  audit: w.audit ?? [],
1875
1910
  chain_head: w.chain_head,
1876
1911
  chain_enabled: !!w.chain_enabled
@@ -1909,6 +1944,16 @@ var Coordinator = class {
1909
1944
  if (this.options.requireSignatures && method !== "participant.join" && method !== "workspace.create") {
1910
1945
  const sigErr = this.verifySignature(envelope);
1911
1946
  if (sigErr) return reply(envelope, { error: sigErr });
1947
+ if (method === "participant.rotate_key") {
1948
+ const signingKid = envelope.sig.split(":")[1];
1949
+ const oldKid = params.old_kid;
1950
+ if (oldKid !== void 0 && signingKid !== oldKid) {
1951
+ return reply(envelope, { error: rpcError(
1952
+ E.SIG_ROTATION_KEY_MISMATCH,
1953
+ "Rotation must be signed with the old key"
1954
+ ) });
1955
+ }
1956
+ }
1912
1957
  }
1913
1958
  if (this.options.enforceStepUp && PRIVILEGED_METHODS.has(method)) {
1914
1959
  const stale = this.checkStepUp(params);
@@ -1939,6 +1984,16 @@ var Coordinator = class {
1939
1984
  }
1940
1985
  }
1941
1986
  }
1987
+ if (method === "task.create") {
1988
+ const wsId2 = params.workspace;
1989
+ const ws = typeof wsId2 === "string" ? this.workspaces.get(wsId2) : void 0;
1990
+ const key = params.idempotency_key;
1991
+ if (ws && typeof key === "string") {
1992
+ const existingId = ws.idempotency_keys[key];
1993
+ const task = existingId ? ws.tasks.get(existingId) : void 0;
1994
+ if (task) return reply(envelope, { result: { task_id: existingId, state: task.state } });
1995
+ }
1996
+ }
1942
1997
  const handler = this.handlers.get(method);
1943
1998
  if (!handler) {
1944
1999
  return reply(envelope, { error: rpcError(E.METHOD, `Unknown method: ${method}`) });
@@ -2052,7 +2107,13 @@ var Coordinator = class {
2052
2107
  const ws = this.workspaces.get(wsId);
2053
2108
  if (!ws) return null;
2054
2109
  const member = ws.members.get(sender);
2055
- if (!member || member.oidc_auth_time === void 0) return null;
2110
+ if (!member) return null;
2111
+ if (member.type !== "human" && member.oidc_sub === void 0) return null;
2112
+ if (member.oidc_auth_time === void 0) {
2113
+ return rpcError(E.OIDC_STEP_UP_REQUIRED, "Step-up authentication required", {
2114
+ window_sec: ws.step_up_window_sec
2115
+ });
2116
+ }
2056
2117
  const nowSec = Math.floor(Date.now() / 1e3);
2057
2118
  const age = nowSec - member.oidc_auth_time;
2058
2119
  if (age > ws.step_up_window_sec) {
@@ -2061,6 +2122,12 @@ var Coordinator = class {
2061
2122
  age_sec: age
2062
2123
  });
2063
2124
  }
2125
+ if (ws.min_acr !== void 0 && member.oidc_acr !== ws.min_acr) {
2126
+ return rpcError(E.OIDC_STEP_UP_REQUIRED, "Step-up required: acr below the workspace minimum", {
2127
+ required_acr: ws.min_acr,
2128
+ acr: member.oidc_acr
2129
+ });
2130
+ }
2064
2131
  return null;
2065
2132
  }
2066
2133
  // ==========================================================
@@ -2110,6 +2177,7 @@ var Coordinator = class {
2110
2177
  mode_ceiling: p.mode_ceiling ?? "production",
2111
2178
  routing_policy_uri: p.routing_policy_uri,
2112
2179
  step_up_window_sec: typeof p.step_up_window_sec === "number" ? p.step_up_window_sec : 300,
2180
+ min_acr: p.min_acr,
2113
2181
  members: /* @__PURE__ */ new Map(),
2114
2182
  tasks: /* @__PURE__ */ new Map(),
2115
2183
  overrides: /* @__PURE__ */ new Map(),
@@ -2118,6 +2186,7 @@ var Coordinator = class {
2118
2186
  handoffs: /* @__PURE__ */ new Map(),
2119
2187
  snapshots: /* @__PURE__ */ new Map(),
2120
2188
  route_decisions: /* @__PURE__ */ new Map(),
2189
+ idempotency_keys: {},
2121
2190
  audit: [],
2122
2191
  chain_enabled: chainEnabled,
2123
2192
  chain_head: chainEnabled ? ZERO_HASH : void 0
@@ -2130,6 +2199,10 @@ var Coordinator = class {
2130
2199
  if (miss) return { error: rpcError(E.PARAMS, `Missing field: ${miss}`) };
2131
2200
  const ws = this.workspaces.get(p.workspace);
2132
2201
  if (!ws) return { error: rpcError(E.PARAMS, `Unknown workspace: ${p.workspace}`) };
2202
+ if (this.options.requireReadMembership) {
2203
+ const notMember = this.requireMember(ws, p.from);
2204
+ if (notMember) return notMember;
2205
+ }
2133
2206
  return { result: {
2134
2207
  id: ws.id,
2135
2208
  created: ws.created,
@@ -2147,10 +2220,15 @@ var Coordinator = class {
2147
2220
  } };
2148
2221
  }
2149
2222
  opWorkspaceSetProfiles(p) {
2150
- const miss = missing(p, ["workspace", "profiles"]);
2223
+ const miss = missing(p, ["workspace", "from", "profiles"]);
2151
2224
  if (miss) return { error: rpcError(E.PARAMS, `Missing field: ${miss}`) };
2152
2225
  const ws = this.workspaces.get(p.workspace);
2153
2226
  if (!ws) return { error: rpcError(E.PARAMS, "Unknown workspace") };
2227
+ const notMember = this.requireMember(ws, p.from);
2228
+ if (notMember) return notMember;
2229
+ if (ws.members.get(p.from).role !== "admin") {
2230
+ return { error: rpcError(E.NOT_AUTHORISED, "workspace.set_profiles requires the admin role") };
2231
+ }
2154
2232
  const newProfiles = [...p.profiles];
2155
2233
  if (!newProfiles.some((pr) => pr.startsWith("core/"))) newProfiles.push("core/1.0");
2156
2234
  ws.profiles = newProfiles;
@@ -2188,6 +2266,7 @@ var Coordinator = class {
2188
2266
  member.oidc_sub = claims.sub;
2189
2267
  const at = claims.auth_time;
2190
2268
  if (typeof at === "number") member.oidc_auth_time = at;
2269
+ member.oidc_acr = claims.acr;
2191
2270
  const cnf = claims.cnf;
2192
2271
  const cnfJwk = cnf?.jwk;
2193
2272
  if (cnfJwk && typeof cnfJwk.kid === "string") {
@@ -2216,6 +2295,7 @@ var Coordinator = class {
2216
2295
  if (existing) {
2217
2296
  if (member.oidc_sub !== void 0) existing.oidc_sub = member.oidc_sub;
2218
2297
  if (member.oidc_auth_time !== void 0) existing.oidc_auth_time = member.oidc_auth_time;
2298
+ if (member.oidc_acr !== void 0) existing.oidc_acr = member.oidc_acr;
2219
2299
  if (member.vc_holder !== void 0) existing.vc_holder = member.vc_holder;
2220
2300
  for (const k of attested) {
2221
2301
  if (!existing.keys.some((x) => x.kid === k.kid)) existing.keys.push(k);
@@ -2236,6 +2316,8 @@ var Coordinator = class {
2236
2316
  if (miss) return { error: rpcError(E.PARAMS, `Missing field: ${miss}`) };
2237
2317
  const ws = this.workspaces.get(p.workspace);
2238
2318
  if (!ws) return { error: rpcError(E.PARAMS, "Unknown workspace") };
2319
+ const notMember = this.requireMember(ws, p.from);
2320
+ if (notMember) return notMember;
2239
2321
  const assignee = p.assignee || p.to;
2240
2322
  if (!assignee || !ws.members.has(assignee)) {
2241
2323
  return { error: rpcError(E.PARAMS, "Assignee not in workspace") };
@@ -2270,11 +2352,14 @@ var Coordinator = class {
2270
2352
  if (task.mode === "trial") task.review_required = true;
2271
2353
  else if ("review_required" in p) task.review_required = !!p.review_required;
2272
2354
  ws.tasks.set(taskId, task);
2355
+ if (typeof p.idempotency_key === "string") ws.idempotency_keys[p.idempotency_key] = taskId;
2273
2356
  return { result: { task_id: taskId, state: "created" } };
2274
2357
  }
2275
2358
  opTaskUpdate(p) {
2276
2359
  const ws = this.workspaces.get(p.workspace);
2277
2360
  if (!ws) return { error: rpcError(E.PARAMS, "Unknown workspace") };
2361
+ const notMember = this.requireMember(ws, p.from);
2362
+ if (notMember) return notMember;
2278
2363
  const task = ws.tasks.get(p.task_id);
2279
2364
  if (!task) return { error: rpcError(E.PARAMS, "Unknown task") };
2280
2365
  const newState = p.state;
@@ -2317,6 +2402,10 @@ var Coordinator = class {
2317
2402
  opAuditRead(p) {
2318
2403
  const ws = this.workspaces.get(p.workspace);
2319
2404
  if (!ws) return { error: rpcError(E.PARAMS, "Unknown workspace") };
2405
+ if (this.options.requireReadMembership) {
2406
+ const notMember = this.requireMember(ws, p.from);
2407
+ if (notMember) return notMember;
2408
+ }
2320
2409
  const range = p.range ?? {};
2321
2410
  const filter = p.filter ?? {};
2322
2411
  const fromSeq = range.from_seq ?? 0;
@@ -2353,12 +2442,38 @@ var Coordinator = class {
2353
2442
  const reviewers = Array.isArray(to) ? to : typeof to === "string" ? [to] : [];
2354
2443
  if (!reviewers.length) return { error: rpcError(E.PARAMS, "review.request needs 'to'") };
2355
2444
  const now = this.now();
2445
+ const rule = p.rule || "any_one_approves";
2446
+ if (task.state === "review_requested" && task.review) {
2447
+ if (contentHash(task.pending_artefact) !== contentHash(p.artefact)) {
2448
+ return { error: rpcError(
2449
+ E.REVIEW_ALREADY_OPEN,
2450
+ "A review is already open on this task with different content. Decide, abstain, escalate or cancel it before requesting review of a new artefact."
2451
+ ) };
2452
+ }
2453
+ if (rule !== task.review.rule) {
2454
+ return { error: rpcError(
2455
+ E.REVIEW_ALREADY_OPEN,
2456
+ `Cannot change the decision rule of an open review (currently ${task.review.rule})`
2457
+ ) };
2458
+ }
2459
+ for (const r of reviewers) {
2460
+ if (!task.review.requested_to.includes(r)) task.review.requested_to.push(r);
2461
+ }
2462
+ if (p.deadline !== void 0) task.review.deadline = p.deadline;
2463
+ task.updated_at = now;
2464
+ return { result: {
2465
+ state: "review_requested",
2466
+ review_id: task.id,
2467
+ amended: true,
2468
+ requested_to: [...task.review.requested_to]
2469
+ } };
2470
+ }
2356
2471
  task.state = "review_requested";
2357
2472
  task.updated_at = now;
2358
2473
  task.review = {
2359
2474
  requested_at: now,
2360
2475
  requested_to: reviewers,
2361
- rule: p.rule || "any_one_approves",
2476
+ rule,
2362
2477
  deadline: p.deadline,
2363
2478
  decisions: []
2364
2479
  };
@@ -2366,6 +2481,37 @@ var Coordinator = class {
2366
2481
  task.pending_artefact = p.artefact;
2367
2482
  return { result: { state: "review_requested", review_id: task.id } };
2368
2483
  }
2484
+ /**
2485
+ * Verify an optional `approved_artefact_digest` against the artefact under
2486
+ * review (review/1.0 S3.2).
2487
+ *
2488
+ * The digest sits in params, so it is inside whatever the envelope signature
2489
+ * covers: the decision then attests the content the reviewer saw rather than
2490
+ * a task reference, and a relying party can check it without trusting the
2491
+ * Coordinator that produced it. Absent, behaviour is exactly as before.
2492
+ *
2493
+ * On mismatch nothing is recorded and no state changes, so a client that
2494
+ * computes the wrong digest refuses only its own decision.
2495
+ */
2496
+ checkArtefactDigest(p, task) {
2497
+ const declared = p.approved_artefact_digest;
2498
+ if (declared === void 0 || declared === null) return null;
2499
+ if (typeof declared !== "string") {
2500
+ return { error: rpcError(E.PARAMS, "approved_artefact_digest must be a string") };
2501
+ }
2502
+ if (task.pending_artefact === void 0) {
2503
+ return { error: rpcError(E.PARAMS, "No artefact under review to bind a digest to") };
2504
+ }
2505
+ const actual = contentHash(task.pending_artefact);
2506
+ if (declared !== actual) {
2507
+ return { error: rpcError(
2508
+ E.SIG_ARTEFACT_DIGEST_MISMATCH,
2509
+ "approved_artefact_digest does not match the artefact under review",
2510
+ { declared, actual }
2511
+ ) };
2512
+ }
2513
+ return null;
2514
+ }
2369
2515
  opDecide(p, kind) {
2370
2516
  const miss = missing(p, ["workspace", "from", "task_id"]);
2371
2517
  if (miss) return { error: rpcError(E.PARAMS, `Missing field: ${miss}`) };
@@ -2382,6 +2528,8 @@ var Coordinator = class {
2382
2528
  }
2383
2529
  const notReviewer = this.requireReviewer(task, p.from);
2384
2530
  if (notReviewer) return notReviewer;
2531
+ const digestError = this.checkArtefactDigest(p, task);
2532
+ if (digestError) return digestError;
2385
2533
  const now = this.now();
2386
2534
  task.review.decisions.push({
2387
2535
  reviewer: p.from,
@@ -2416,6 +2564,8 @@ var Coordinator = class {
2416
2564
  }
2417
2565
  const notReviewer = this.requireReviewer(task, p.from);
2418
2566
  if (notReviewer) return notReviewer;
2567
+ const digestError = this.checkArtefactDigest(p, task);
2568
+ if (digestError) return digestError;
2419
2569
  const base = task.pending_artefact;
2420
2570
  if (base === void 0) return { error: rpcError(E.PARAMS, "No base artefact for override") };
2421
2571
  let applied;