@brightbeamai/chap-coordinator 0.2.12 → 0.3.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/index.cjs CHANGED
@@ -35,8 +35,12 @@ __export(src_exports, {
35
35
  deriveKeypair: () => deriveKeypair,
36
36
  deriveSeed: () => deriveSeed,
37
37
  diffJsonPatch: () => diffJsonPatch,
38
+ entryCall: () => entryCall,
39
+ entryRecord: () => entryRecord,
40
+ isRefusal: () => isRefusal,
38
41
  isValidEnvelope: () => isValidEnvelope,
39
42
  jwkFromPrivateKey: () => jwkFromPrivateKey,
43
+ linkHash: () => linkHash,
40
44
  makeApi: () => makeApi,
41
45
  makeDefaultPolicy: () => makeDefaultPolicy,
42
46
  modeLE: () => modeLE,
@@ -57,7 +61,21 @@ var import_node_crypto = require("crypto");
57
61
  var ZERO_HASH = "sha256:" + "0".repeat(64);
58
62
  var NON_INTEGER_ERROR = 'CHAP canonical numbers must be integers; represent decimals as strings (e.g. "8.2") so the hash is deterministic across implementations.';
59
63
  var NUMBER_RANGE_ERROR = "CHAP canonical integers must be within the safe-integer range (abs value <= Number.MAX_SAFE_INTEGER); represent larger numbers as strings.";
64
+ var LONE_SURROGATE_ERROR = "CHAP canonical strings must be valid Unicode (I-JSON / RFC 7493); unpaired UTF-16 surrogates are not permitted.";
65
+ function assertNoLoneSurrogate(s) {
66
+ for (let i = 0; i < s.length; i++) {
67
+ const c = s.charCodeAt(i);
68
+ if (c >= 55296 && c <= 56319) {
69
+ const next = i + 1 < s.length ? s.charCodeAt(i + 1) : 0;
70
+ if (next < 56320 || next > 57343) throw new Error(LONE_SURROGATE_ERROR);
71
+ i++;
72
+ } else if (c >= 56320 && c <= 57343) {
73
+ throw new Error(LONE_SURROGATE_ERROR);
74
+ }
75
+ }
76
+ }
60
77
  function canonicalString(s) {
78
+ assertNoLoneSurrogate(s);
61
79
  return JSON.stringify(s);
62
80
  }
63
81
  function canonicalNumber(n) {
@@ -106,6 +124,55 @@ function contentHash(content) {
106
124
  return sha256Hex(canonicalize(content));
107
125
  }
108
126
 
127
+ // src/catalogue.ts
128
+ var OWNING_PROFILE = Object.freeze({
129
+ "abstain.declare": "review/1.0",
130
+ "audit.read": "core/1.0",
131
+ "audit.submit_to_scitt": "audit-scitt/1.0",
132
+ "audit.verify_chain": "audit-scitt/1.0",
133
+ "audit.verify_receipt": "audit-scitt/1.0",
134
+ "control.cancel": "control/1.0",
135
+ "control.pause": "control/1.0",
136
+ "control.resume": "control/1.0",
137
+ "control.rollback": "control/1.0",
138
+ "control.set_mode_ceiling": "control/1.0",
139
+ "control.snapshot": "control/1.0",
140
+ "control.supersede": "control/1.0",
141
+ "decide.approve": "review/1.0",
142
+ "decide.override": "review/1.0",
143
+ "decide.reject": "review/1.0",
144
+ "deliberate.close": "deliberation/1.0",
145
+ "deliberate.comment": "deliberation/1.0",
146
+ "deliberate.open": "deliberation/1.0",
147
+ "deliberate.vote": "deliberation/1.0",
148
+ "escalate.auto": "routing/1.0",
149
+ "escalate.raise": "review/1.0",
150
+ "handoff.accept": "handoff/1.0",
151
+ "handoff.decline": "handoff/1.0",
152
+ "handoff.propose": "handoff/1.0",
153
+ "participant.join": "core/1.0",
154
+ "participant.leave": "core/1.0",
155
+ "participant.revoke_key": "core/1.0",
156
+ "participant.rotate_key": "core/1.0",
157
+ "review.depth": "routing/1.0",
158
+ "review.request": "review/1.0",
159
+ "task.complete": "core/1.0",
160
+ "task.create": "core/1.0",
161
+ "task.route": "routing/1.0",
162
+ "task.update": "core/1.0",
163
+ "whisper.answer": "whisper/1.0",
164
+ "whisper.ask": "whisper/1.0",
165
+ "workspace.create": "core/1.0",
166
+ "workspace.describe": "core/1.0",
167
+ "workspace.set_profiles": "core/1.0"
168
+ });
169
+ var ALWAYS_AVAILABLE = /* @__PURE__ */ new Set([
170
+ "workspace.describe",
171
+ "audit.read",
172
+ "audit.verify_chain",
173
+ "audit.verify_receipt"
174
+ ]);
175
+
109
176
  // src/crypto.ts
110
177
  var import_node_crypto2 = require("crypto");
111
178
  var ED25519_OID = Buffer.from("302a300506032b6570032100", "hex");
@@ -319,11 +386,84 @@ function isValidEnvelope(env) {
319
386
  return e.jsonrpc === "2.0";
320
387
  }
321
388
 
389
+ // src/audit.ts
390
+ function entryRecord(entry) {
391
+ if (entry.outcome != null) return { outcome: entry.outcome, request: entry.request };
392
+ return entry.envelope;
393
+ }
394
+ function entryCall(entry) {
395
+ return entry.envelope ?? entry.request ?? void 0;
396
+ }
397
+ function isRefusal(entry) {
398
+ return entry.outcome != null;
399
+ }
400
+ function isCall(value) {
401
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
402
+ const v = value;
403
+ return v.jsonrpc === "2.0" && typeof v.method === "string";
404
+ }
405
+ function entryIsWellFormed(entry) {
406
+ const accepted = entry.envelope != null;
407
+ const refused = entry.request != null;
408
+ if (accepted === refused) return false;
409
+ if (accepted) return isCall(entry.envelope) && entry.outcome == null;
410
+ const o = entry.outcome;
411
+ return isCall(entry.request) && o != null && typeof o === "object" && o.status === "refused" && Number.isInteger(o.code);
412
+ }
413
+ function isSigned(call2) {
414
+ return call2 !== null && typeof call2 === "object" && typeof call2.sig === "string";
415
+ }
416
+ function signedDigest(call2) {
417
+ const signed = { ...call2 };
418
+ delete signed.sig;
419
+ return sha256Hex(canonicalize(signed));
420
+ }
421
+ function linkHash(record, prev) {
422
+ return sha256Hex(Buffer.concat([canonicalize(record), Buffer.from(prev, "utf-8")]));
423
+ }
424
+ var UNRECORDED_CODES = /* @__PURE__ */ new Set([
425
+ E.PARSE,
426
+ E.REQUEST,
427
+ E.PARAMS,
428
+ E.INTERNAL,
429
+ E.SIG_VERIFY_FAILED,
430
+ E.SIG_KEY_NOT_FOUND,
431
+ E.SIG_KEY_REVOKED,
432
+ E.SIG_ROTATION_KEY_MISMATCH
433
+ ]);
434
+ var UNRECORDED_METHODS = /* @__PURE__ */ new Set(["workspace.create", "participant.join"]);
435
+ function refusalIsRecorded(method, error, privileged) {
436
+ if (!Number.isInteger(error.code)) return false;
437
+ if (UNRECORDED_METHODS.has(method)) return false;
438
+ if (UNRECORDED_CODES.has(error.code)) return false;
439
+ if (error.code === E.METHOD) {
440
+ const data = error.data;
441
+ return typeof data?.profile === "string" && privileged.has(method);
442
+ }
443
+ return true;
444
+ }
445
+
322
446
  // src/patch.ts
323
447
  var PatchError = class extends Error {
324
448
  };
325
449
  var DANGEROUS_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
326
450
  var ARRAY_INDEX_RE = /^(0|[1-9][0-9]*)$/;
451
+ function jsonType(value) {
452
+ if (value === null) return "null";
453
+ if (Array.isArray(value)) return "array";
454
+ switch (typeof value) {
455
+ case "object":
456
+ return "object";
457
+ case "string":
458
+ return "string";
459
+ case "number":
460
+ return "number";
461
+ case "boolean":
462
+ return "boolean";
463
+ default:
464
+ return "null";
465
+ }
466
+ }
327
467
  function arrayIndex(seg) {
328
468
  if (!ARRAY_INDEX_RE.test(seg)) throw new PatchError(`Array index expected at ${JSON.stringify(seg)}`);
329
469
  return Number(seg);
@@ -363,7 +503,7 @@ function navigate(doc, parts) {
363
503
  }
364
504
  parent = parent[part];
365
505
  } else {
366
- throw new PatchError(`Cannot traverse into ${typeof parent} at ${JSON.stringify(part)}`);
506
+ throw new PatchError(`Cannot traverse into ${jsonType(parent)} at ${JSON.stringify(part)}`);
367
507
  }
368
508
  }
369
509
  const last = parts[parts.length - 1];
@@ -433,7 +573,7 @@ function applyOne(doc, op) {
433
573
  } else if (parent !== null && typeof parent === "object") {
434
574
  parent[key] = op.value;
435
575
  } else {
436
- throw new PatchError(`Cannot add into ${typeof parent}`);
576
+ throw new PatchError(`Cannot add into ${jsonType(parent)}`);
437
577
  }
438
578
  return doc;
439
579
  }
@@ -443,7 +583,7 @@ function applyOne(doc, op) {
443
583
  if (parts.length === 0) return op.value;
444
584
  const [parent, key] = navigate(doc, parts);
445
585
  if (Array.isArray(parent)) {
446
- const idx = key;
586
+ const idx = key === "-" ? -1 : key;
447
587
  if (idx < 0 || idx >= parent.length) {
448
588
  throw new PatchError(`Path not found for replace: ${path}`);
449
589
  }
@@ -454,7 +594,7 @@ function applyOne(doc, op) {
454
594
  }
455
595
  parent[key] = op.value;
456
596
  } else {
457
- throw new PatchError(`Cannot replace in ${typeof parent}`);
597
+ throw new PatchError(`Cannot replace in ${jsonType(parent)}`);
458
598
  }
459
599
  return doc;
460
600
  }
@@ -463,7 +603,7 @@ function applyOne(doc, op) {
463
603
  if (parts.length === 0) throw new PatchError("Cannot remove root.");
464
604
  const [parent, key] = navigate(doc, parts);
465
605
  if (Array.isArray(parent)) {
466
- const idx = key;
606
+ const idx = key === "-" ? -1 : key;
467
607
  if (idx < 0 || idx >= parent.length) {
468
608
  throw new PatchError(`Index out of range for remove: ${path}`);
469
609
  }
@@ -474,7 +614,7 @@ function applyOne(doc, op) {
474
614
  }
475
615
  delete parent[key];
476
616
  } else {
477
- throw new PatchError(`Cannot remove from ${typeof parent}`);
617
+ throw new PatchError(`Cannot remove from ${jsonType(parent)}`);
478
618
  }
479
619
  return doc;
480
620
  }
@@ -682,7 +822,8 @@ function modeLE(a, ceiling) {
682
822
  }
683
823
 
684
824
  // src/profiles/audit_scitt.ts
685
- function buildStatement(workspaceId, envelope, issuer) {
825
+ var MALFORMED = "sha256:malformed";
826
+ function buildStatement(workspaceId, record, issuer) {
686
827
  return {
687
828
  protected: {
688
829
  alg: -8,
@@ -690,9 +831,9 @@ function buildStatement(workspaceId, envelope, issuer) {
690
831
  iss: issuer,
691
832
  kid: "scitt-issuer",
692
833
  cwt_claims: { sub: workspaceId, iat: null },
693
- "content-type": "application/chap+json;version=0.2"
834
+ "content-type": "application/chap+json;version=0.3"
694
835
  },
695
- payload: canonicalize(envelope).toString("utf-8"),
836
+ payload: canonicalize(record).toString("utf-8"),
696
837
  signature: "<deployment-supplied>"
697
838
  };
698
839
  }
@@ -705,7 +846,7 @@ function registerAuditScitt(coord) {
705
846
  const toSeq = range.to_seq ?? ws.audit.length;
706
847
  const issuer = p.issuer ?? "service:coordinator";
707
848
  if (!coord.options.scittSubmitter) {
708
- const statements = ws.audit.slice(fromSeq, toSeq).map((e) => buildStatement(ws.id, e.envelope, issuer));
849
+ const statements = ws.audit.slice(fromSeq, toSeq).map((e) => buildStatement(ws.id, entryRecord(e), issuer));
709
850
  return { result: {
710
851
  statements,
711
852
  note: "No scittSubmitter configured; submit these out-of-band"
@@ -713,7 +854,7 @@ function registerAuditScitt(coord) {
713
854
  }
714
855
  const receipts = [];
715
856
  for (const entry of ws.audit.slice(fromSeq, toSeq)) {
716
- const statement = buildStatement(ws.id, entry.envelope, issuer);
857
+ const statement = buildStatement(ws.id, entryRecord(entry), issuer);
717
858
  let receipt;
718
859
  try {
719
860
  receipt = coord.options.scittSubmitter(statement);
@@ -765,12 +906,20 @@ function registerAuditScitt(coord) {
765
906
  if (start < 0) start = ws.audit.length;
766
907
  const errors = [];
767
908
  let prev = ZERO_HASH;
909
+ let resync = false;
768
910
  for (const e of ws.audit.slice(start)) {
769
- const expectedPrev = prev;
911
+ const expectedPrev = resync && typeof e.prev_hash === "string" ? e.prev_hash : prev;
912
+ resync = false;
770
913
  if (e.prev_hash !== expectedPrev) {
771
914
  errors.push(`seq ${e.seq}: prev_hash mismatch`);
772
915
  }
773
- prev = sha256Hex(Buffer.concat([canonicalize(e.envelope), Buffer.from(expectedPrev, "utf-8")]));
916
+ if (!entryIsWellFormed(e)) {
917
+ errors.push(`seq ${e.seq}: malformed entry`);
918
+ prev = MALFORMED;
919
+ resync = true;
920
+ continue;
921
+ }
922
+ prev = linkHash(entryRecord(e), expectedPrev);
774
923
  }
775
924
  const storedHead = ws.chain_head === void 0 || ws.chain_head === null ? ZERO_HASH : ws.chain_head;
776
925
  if (prev !== storedHead) {
@@ -825,6 +974,7 @@ function registerControl(coord) {
825
974
  }
826
975
  task.paused = true;
827
976
  const prior = task.state;
977
+ if (prior !== "paused") task.paused_from = prior;
828
978
  task.state = "paused";
829
979
  task.updated_at = coord.now();
830
980
  task.history.push({ ts: task.updated_at, from: p.from, state: "paused", note: `was ${prior}` });
@@ -858,10 +1008,12 @@ function registerControl(coord) {
858
1008
  return { error: rpcError(E.CONTROL_NOT_AUTHORISED, `Cannot resume ${task.state} task`) };
859
1009
  }
860
1010
  task.paused = false;
861
- task.state = "in_progress";
1011
+ const restored = task.paused_from ?? "in_progress";
1012
+ task.paused_from = void 0;
1013
+ task.state = restored;
862
1014
  task.updated_at = coord.now();
863
- task.history.push({ ts: task.updated_at, from: p.from, state: "in_progress" });
864
- return { result: { scope: "task", task_id: task.id, state: "in_progress" } };
1015
+ task.history.push({ ts: task.updated_at, from: p.from, state: restored });
1016
+ return { result: { scope: "task", task_id: task.id, state: restored } };
865
1017
  }
866
1018
  if (scope === "participant") {
867
1019
  const uri = p.participant_uri || p.uri;
@@ -888,36 +1040,56 @@ function registerControl(coord) {
888
1040
  coord.handlers.set("control.snapshot", (p) => {
889
1041
  const ws = coord.workspaces.get(p.workspace);
890
1042
  if (!ws) return { error: rpcError(E.PARAMS, "Unknown workspace") };
891
- const include = p.include ?? ["members", "open_tasks", "mode_ceiling"];
1043
+ if (Array.isArray(p.include) && p.include.length === 0) {
1044
+ return { error: rpcError(E.PARAMS, "include must not be empty") };
1045
+ }
1046
+ const include = Array.isArray(p.include) ? [...p.include] : ["members", "open_tasks", "mode_ceiling"];
892
1047
  const state = {};
893
1048
  if (include.includes("members")) {
894
- state.members = Array.from(ws.members.values()).map((m) => ({
1049
+ const members = Array.from(ws.members.values()).map((m) => ({
895
1050
  uri: m.uri,
896
1051
  type: m.type,
897
- role: m.role,
898
- scopes: m.scopes
1052
+ ...m.role ? { role: m.role } : {},
1053
+ ...m.scopes?.length ? { scopes: m.scopes } : {}
899
1054
  }));
1055
+ if (members.length) state.members = members;
900
1056
  }
901
1057
  if (include.includes("open_tasks")) {
902
- state.open_tasks = Array.from(ws.tasks.values()).filter((t) => t.state !== "completed" && t.state !== "declined" && t.state !== "cancelled" && t.state !== "superseded").map((t) => ({ id: t.id, kind: t.kind, state: t.state, assignee: t.assignee }));
1058
+ const tasks = Array.from(ws.tasks.values()).filter((t) => t.state !== "completed" && t.state !== "declined" && t.state !== "cancelled" && t.state !== "superseded").map((t) => ({
1059
+ id: t.id,
1060
+ kind: t.kind,
1061
+ state: t.state,
1062
+ ...t.assignee ? { assignee: t.assignee } : {}
1063
+ }));
1064
+ if (tasks.length) state.open_tasks = tasks;
903
1065
  }
904
1066
  if (include.includes("mode_ceiling")) state.mode_ceiling = ws.mode_ceiling;
905
- if (include.includes("policy")) state.routing_policy_uri = ws.routing_policy_uri;
1067
+ if (include.includes("policy") && ws.routing_policy_uri) state.routing_policy_uri = ws.routing_policy_uri;
906
1068
  if (include.includes("audit") || include.includes("all")) state.audit_seq = ws.audit.length;
1069
+ const content = structuredClone({
1070
+ workspace: ws.id,
1071
+ audit_seq: ws.audit.length,
1072
+ include,
1073
+ state,
1074
+ ...p.label ? { label: p.label } : {}
1075
+ });
907
1076
  const snapId = coord.ids.artefactId();
908
1077
  const snap = {
909
1078
  id: snapId,
910
1079
  kind: "snapshot",
911
- ts: coord.now(),
912
- by: p.from,
913
- workspace: ws.id,
914
- audit_seq: ws.audit.length,
915
- label: p.label,
916
- include,
917
- state
1080
+ produced_at: coord.now(),
1081
+ produced_by: p.from,
1082
+ content_hash: contentHash(content),
1083
+ content
918
1084
  };
919
1085
  ws.snapshots.set(snapId, snap);
920
- return { result: { snapshot_artefact_id: snapId, audit_seq: snap.audit_seq, artefact: snap } };
1086
+ return {
1087
+ result: {
1088
+ snapshot_artefact_id: snapId,
1089
+ audit_seq: snap.content.audit_seq,
1090
+ artefact: structuredClone(snap)
1091
+ }
1092
+ };
921
1093
  });
922
1094
  coord.handlers.set("control.rollback", (p) => {
923
1095
  const ws = coord.workspaces.get(p.workspace);
@@ -926,31 +1098,34 @@ function registerControl(coord) {
926
1098
  if (!snapId) return { error: rpcError(E.PARAMS, "to_snapshot_artefact_id is required") };
927
1099
  const snap = ws.snapshots.get(snapId);
928
1100
  if (!snap) return { error: rpcError(E.CONTROL_SNAPSHOT_NOT_FOUND, `Unknown snapshot: ${snapId}`) };
929
- const what = p.what_to_restore ?? snap.include;
1101
+ if (Array.isArray(p.what_to_restore) && p.what_to_restore.length === 0) {
1102
+ return { error: rpcError(E.PARAMS, "what_to_restore must not be empty") };
1103
+ }
1104
+ const what = p.what_to_restore ?? snap.content.include;
930
1105
  const restored = [];
931
- if (what.includes("mode_ceiling") && snap.state.mode_ceiling) {
932
- ws.mode_ceiling = snap.state.mode_ceiling;
1106
+ if (what.includes("mode_ceiling") && snap.content.state.mode_ceiling) {
1107
+ ws.mode_ceiling = snap.content.state.mode_ceiling;
933
1108
  restored.push("mode_ceiling");
934
1109
  }
935
- if (what.includes("members") && Array.isArray(snap.state.members)) {
1110
+ if (what.includes("members") && Array.isArray(snap.content.state.members)) {
936
1111
  const snapByUri = /* @__PURE__ */ new Map();
937
- for (const m of snap.state.members) {
1112
+ for (const m of snap.content.state.members) {
938
1113
  snapByUri.set(m.uri, m);
939
1114
  }
940
1115
  for (const [uri, snapM] of snapByUri) {
941
1116
  if (ws.members.has(uri)) {
942
1117
  const cur = ws.members.get(uri);
943
1118
  cur.role = snapM.role ?? cur.role;
944
- cur.scopes = snapM.scopes;
1119
+ cur.scopes = Array.isArray(snapM.scopes) ? [...snapM.scopes] : void 0;
945
1120
  }
946
1121
  }
947
1122
  restored.push("members");
948
1123
  }
949
1124
  return { result: {
950
1125
  rolled_back_to: snapId,
951
- audit_seq: snap.audit_seq,
1126
+ audit_seq: snap.content.audit_seq,
952
1127
  restored,
953
- reason: p.reason
1128
+ ...p.reason ? { reason: p.reason } : {}
954
1129
  } };
955
1130
  });
956
1131
  coord.handlers.set("control.supersede", (p) => {
@@ -964,16 +1139,25 @@ function registerControl(coord) {
964
1139
  return { error: rpcError(E.PARAMS, "successor_task must include kind") };
965
1140
  }
966
1141
  const assignee = newSpec.assignee || old.assignee;
967
- if (!ws.members.has(assignee)) {
1142
+ if (typeof assignee !== "string" || !ws.members.has(assignee)) {
968
1143
  return { error: rpcError(E.PARAMS, "successor assignee not in workspace") };
969
1144
  }
970
- const requestedMode = newSpec.mode ?? old.mode;
1145
+ if (ws.members.get(assignee).paused) {
1146
+ return { error: rpcError(E.CONTROL_WORKSPACE_PAUSED, `Assignee ${assignee} is paused`) };
1147
+ }
1148
+ if (newSpec.mode !== void 0 && newSpec.mode !== null && typeof newSpec.mode !== "string") {
1149
+ return { error: rpcError(E.PARAMS, "successor_task.mode must be a string") };
1150
+ }
1151
+ const requestedMode = newSpec.mode || old.mode;
971
1152
  if (!modeLE(requestedMode, ws.mode_ceiling)) {
972
1153
  return { error: rpcError(
973
1154
  E.MODE_CEILING_EXCEEDED,
974
1155
  `Requested mode ${requestedMode} exceeds ceiling ${ws.mode_ceiling}`
975
1156
  ) };
976
1157
  }
1158
+ if (newSpec.review_required !== void 0 && newSpec.review_required !== null && typeof newSpec.review_required !== "boolean") {
1159
+ return { error: rpcError(E.PARAMS, "successor_task.review_required must be a boolean") };
1160
+ }
977
1161
  const now = coord.now();
978
1162
  const newId = coord.ids.taskId();
979
1163
  const newTask = {
@@ -995,8 +1179,11 @@ function registerControl(coord) {
995
1179
  }],
996
1180
  paused: false
997
1181
  };
998
- if (ws.profiles.some((pr) => pr.startsWith("modes/")) && newTask.mode === "trial") newTask.review_required = true;
999
- else if ("review_required" in newSpec) newTask.review_required = !!newSpec.review_required;
1182
+ if (old.review_required || ws.profiles.some((pr) => pr.startsWith("modes/")) && newTask.mode === "trial") {
1183
+ newTask.review_required = true;
1184
+ } else if ("review_required" in newSpec) {
1185
+ newTask.review_required = !!newSpec.review_required;
1186
+ }
1000
1187
  ws.tasks.set(newId, newTask);
1001
1188
  old.state = "superseded";
1002
1189
  old.superseded_by = newId;
@@ -1293,8 +1480,14 @@ function registerHandoff(coord) {
1293
1480
  ) };
1294
1481
  }
1295
1482
  }
1483
+ if (ws.members.get(acceptor).paused) {
1484
+ return { error: rpcError(E.CONTROL_WORKSPACE_PAUSED, `Acceptor ${acceptor} is paused`) };
1485
+ }
1486
+ if (Array.isArray(p.accepted_task_ids) && p.accepted_task_ids.length === 0) {
1487
+ return { error: rpcError(E.PARAMS, "accepted_task_ids must not be empty") };
1488
+ }
1296
1489
  const validIds = new Set(ho.tasks.map((t) => t.task_id));
1297
- const acceptedIds = Array.isArray(p.accepted_task_ids) ? p.accepted_task_ids : ho.tasks.map((t) => t.task_id);
1490
+ const acceptedIds = Array.isArray(p.accepted_task_ids) ? [...p.accepted_task_ids] : ho.tasks.map((t) => t.task_id);
1298
1491
  for (const tid of acceptedIds) {
1299
1492
  if (!validIds.has(tid)) {
1300
1493
  return { error: rpcError(E.PARAMS, `Task ${tid} not in this handoff`) };
@@ -1365,6 +1558,9 @@ function registerRouting(coord) {
1365
1558
  if (!ws) return { error: rpcError(E.PARAMS, "Unknown workspace") };
1366
1559
  const task = ws.tasks.get(p.task_id);
1367
1560
  if (!task) return { error: rpcError(E.PARAMS, "Unknown task") };
1561
+ if (p.candidates !== void 0 && p.candidates !== null && !Array.isArray(p.candidates)) {
1562
+ return { error: rpcError(E.PARAMS, "candidates must be a list") };
1563
+ }
1368
1564
  const candidates = p.candidates ?? [];
1369
1565
  if (!candidates.length) {
1370
1566
  return { error: rpcError(E.ROUTING_CANDIDATES_EMPTY, "candidates array was empty") };
@@ -1381,19 +1577,29 @@ function registerRouting(coord) {
1381
1577
  return { error: rpcError(E.ROUTING_POLICY_UNREACHABLE, `routing policy error: ${msg}`) };
1382
1578
  }
1383
1579
  } else {
1384
- const eligible = candidates.filter((c) => ws.members.has(c));
1580
+ const memberOf = (c) => typeof c === "string" ? ws.members.get(c) : void 0;
1581
+ const eligible = candidates.filter((c) => {
1582
+ const m = memberOf(c);
1583
+ return !!m && !m.paused;
1584
+ });
1385
1585
  if (!eligible.length) {
1386
- return { error: rpcError(E.ROUTING_NO_ELIGIBLE_ASSIGNEE, "No candidate is a workspace member") };
1586
+ return { error: rpcError(E.ROUTING_NO_ELIGIBLE_ASSIGNEE, "No candidate is a workspace member who is not paused") };
1387
1587
  }
1388
1588
  selected = eligible[0];
1589
+ const alternatives = [];
1590
+ let chosen = false;
1591
+ for (const c of candidates) {
1592
+ const m = memberOf(c);
1593
+ if (!m) continue;
1594
+ if (m.paused) alternatives.push({ candidate: c, reason_excluded: "paused" });
1595
+ else if (!chosen) chosen = true;
1596
+ else alternatives.push({ candidate: c, reason_excluded: "not first eligible" });
1597
+ }
1389
1598
  rationale = {
1390
1599
  policy_id: "default",
1391
1600
  hints_used: Object.keys(task.routing_hints ?? {}),
1392
1601
  summary: "default policy: first eligible candidate",
1393
- alternatives_considered: eligible.slice(1).map((c) => ({
1394
- candidate: c,
1395
- reason_excluded: "not first eligible"
1396
- }))
1602
+ alternatives_considered: alternatives
1397
1603
  };
1398
1604
  }
1399
1605
  if (!ws.members.has(selected)) {
@@ -1402,6 +1608,9 @@ function registerRouting(coord) {
1402
1608
  `Selected ${JSON.stringify(selected)} is not a member`
1403
1609
  ) };
1404
1610
  }
1611
+ if (ws.members.get(selected).paused) {
1612
+ return { error: rpcError(E.CONTROL_WORKSPACE_PAUSED, `Assignee ${selected} is paused`) };
1613
+ }
1405
1614
  task.assignee = selected;
1406
1615
  task.updated_at = coord.now();
1407
1616
  task.history.push({
@@ -1519,6 +1728,18 @@ function registerRouting(coord) {
1519
1728
  hints_used: ["criticality", "confidence"]
1520
1729
  };
1521
1730
  }
1731
+ if (escalate) {
1732
+ if (!to) return { error: rpcError(
1733
+ E.ROUTING_ESC_TARGET_UNAVAILABLE,
1734
+ "Escalation triggered but no target available"
1735
+ ) };
1736
+ if (!to.startsWith("group:") && !ws.members.has(to)) {
1737
+ return { error: rpcError(
1738
+ E.ROUTING_ESC_TARGET_UNAVAILABLE,
1739
+ `Escalation target ${to} is not a member or group`
1740
+ ) };
1741
+ }
1742
+ }
1522
1743
  const artId = coord.ids.artefactId();
1523
1744
  const artefact = {
1524
1745
  id: artId,
@@ -1535,17 +1756,7 @@ function registerRouting(coord) {
1535
1756
  };
1536
1757
  ws.route_decisions.set(artId, artefact);
1537
1758
  if (escalate) {
1538
- if (!to) return { error: rpcError(
1539
- E.ROUTING_ESC_TARGET_UNAVAILABLE,
1540
- "Escalation triggered but no target available"
1541
- ) };
1542
- if (!to.startsWith("group:") && !ws.members.has(to)) {
1543
- return { error: rpcError(
1544
- E.ROUTING_ESC_TARGET_UNAVAILABLE,
1545
- `Escalation target ${to} is not a member or group`
1546
- ) };
1547
- }
1548
- if (coord.options.onAutoEscalate) {
1759
+ if (coord.options.onAutoEscalate && to) {
1549
1760
  try {
1550
1761
  coord.options.onAutoEscalate(task, to);
1551
1762
  } catch {
@@ -1615,6 +1826,12 @@ function registerSecuritySigned(coord) {
1615
1826
  function parseIso(ts) {
1616
1827
  return new Date(ts).getTime();
1617
1828
  }
1829
+ function present(v) {
1830
+ if (v === void 0 || v === null || v === false || v === 0 || v === "") return false;
1831
+ if (Array.isArray(v)) return v.length > 0;
1832
+ if (typeof v === "object") return Object.keys(v).length > 0;
1833
+ return true;
1834
+ }
1618
1835
  function registerWhisper(coord) {
1619
1836
  coord.handlers.set("whisper.ask", (p) => {
1620
1837
  const ws = coord.workspaces.get(p.workspace);
@@ -1625,6 +1842,9 @@ function registerWhisper(coord) {
1625
1842
  if (!ws.tasks.has(p.task_id)) {
1626
1843
  return { error: rpcError(E.PARAMS, "Unknown task") };
1627
1844
  }
1845
+ if (p.options !== void 0 && p.options !== null && !Array.isArray(p.options)) {
1846
+ return { error: rpcError(E.PARAMS, "options must be a list") };
1847
+ }
1628
1848
  const to = p.to;
1629
1849
  const askee = Array.isArray(to) ? to : [to];
1630
1850
  const id = p.whisper_id || coord.ids.artefactId();
@@ -1668,22 +1888,21 @@ function registerWhisper(coord) {
1668
1888
  return { error: rpcError(E.WHISPER_LAPSED, "Whisper already lapsed") };
1669
1889
  }
1670
1890
  const answerOption = p.answer_option;
1671
- const answerText = p.answer ?? p.answer_text;
1891
+ const hasOption = answerOption !== void 0 && answerOption !== null;
1892
+ const answerText = present(p.answer) ? p.answer : p.answer_text;
1672
1893
  if (prompt.options && prompt.options.length) {
1673
- if (answerOption === void 0) {
1894
+ if (!hasOption) {
1674
1895
  return { error: rpcError(E.PARAMS, "answer_option is required when options are defined") };
1675
1896
  }
1676
1897
  const valid = new Set(prompt.options.map((o) => o.id));
1677
1898
  if (!valid.has(answerOption)) {
1678
1899
  return { error: rpcError(
1679
1900
  E.WHISPER_OPTION_NOT_IN_SET,
1680
- `Answer option ${JSON.stringify(answerOption)} not in option set`
1901
+ `Answer option ${canonicalize(answerOption).toString("utf-8")} not in option set`
1681
1902
  ) };
1682
1903
  }
1683
- } else {
1684
- if (answerText === void 0 && answerOption === void 0) {
1685
- return { error: rpcError(E.PARAMS, "answer or answer_option required") };
1686
- }
1904
+ } else if (!present(answerText) && !hasOption) {
1905
+ return { error: rpcError(E.PARAMS, "answer or answer_option required") };
1687
1906
  }
1688
1907
  prompt.state = "answered";
1689
1908
  prompt.answered_at = coord.now();
@@ -1691,7 +1910,6 @@ function registerWhisper(coord) {
1691
1910
  prompt.answer_option = answerOption;
1692
1911
  prompt.answer_text = answerText;
1693
1912
  prompt.comment = p.comment;
1694
- p.task_id = prompt.task_id;
1695
1913
  return { result: { answered: true, whisper_id: prompt.id, task_id: prompt.task_id } };
1696
1914
  });
1697
1915
  coord.checkWhisperLapses = (workspaceId, now) => {
@@ -1744,14 +1962,38 @@ var PRIVILEGED_METHODS = /* @__PURE__ */ new Set([
1744
1962
  "participant.rotate_key",
1745
1963
  "participant.revoke_key"
1746
1964
  ]);
1965
+ var MEMBER_ONLY_METHODS = /* @__PURE__ */ new Set([
1966
+ "task.route",
1967
+ "review.depth",
1968
+ "escalate.auto",
1969
+ "participant.leave"
1970
+ ]);
1747
1971
  var READ_ONLY_METHODS = /* @__PURE__ */ new Set([
1748
1972
  "workspace.describe",
1749
1973
  "audit.read",
1750
1974
  "audit.verify_chain",
1751
- "audit.verify_receipt"
1975
+ "audit.verify_receipt",
1976
+ // Submitting the chain to a transparency service reads it and sends it
1977
+ // onward. Recording the submission would append to the very log being
1978
+ // submitted, so the receipt would attest a chain one entry shorter.
1979
+ "audit.submit_to_scitt"
1752
1980
  ]);
1753
1981
  var MAX_IDEMPOTENCY_KEYS = 1e4;
1754
1982
  var DEFAULT_MAX_ENVELOPE_BYTES = 1048576;
1983
+ var MAX_NESTING = 64;
1984
+ function withinNesting(value, limit) {
1985
+ const stack = [[value, 1]];
1986
+ while (stack.length) {
1987
+ const [node, depth] = stack.pop();
1988
+ if (depth > limit) return false;
1989
+ if (Array.isArray(node)) {
1990
+ for (const v of node) stack.push([v, depth + 1]);
1991
+ } else if (node !== null && typeof node === "object") {
1992
+ for (const v of Object.values(node)) stack.push([v, depth + 1]);
1993
+ }
1994
+ }
1995
+ return true;
1996
+ }
1755
1997
  function nowIso(clockMs) {
1756
1998
  const d = clockMs === void 0 ? /* @__PURE__ */ new Date() : new Date(clockMs);
1757
1999
  return d.toISOString().replace(/\.(\d{3})Z$/, ".$1Z");
@@ -1767,9 +2009,6 @@ function tagsError(p) {
1767
2009
  }
1768
2010
  return null;
1769
2011
  }
1770
- function linkHash(envelope, prev) {
1771
- return sha256Hex(Buffer.concat([canonicalize(envelope), Buffer.from(prev, "utf-8")]));
1772
- }
1773
2012
  function reviewRuleSupported(rule) {
1774
2013
  if (rule === "any_one_approves" || rule === "all_approve") return true;
1775
2014
  if (rule.startsWith("quorum:")) {
@@ -1795,14 +2034,35 @@ function reply(env, body) {
1795
2034
  else out.result = body.result;
1796
2035
  return out;
1797
2036
  }
2037
+ function indexSigned(index, entry) {
2038
+ const call2 = entryCall(entry);
2039
+ if (!isSigned(call2)) return;
2040
+ let digest;
2041
+ try {
2042
+ digest = signedDigest(call2);
2043
+ } catch {
2044
+ return;
2045
+ }
2046
+ if (entry.outcome != null) index.refused.set(digest, { seq: entry.seq, code: entry.outcome.code });
2047
+ else index.accepted.add(digest);
2048
+ }
2049
+ var REVIEW_REQUEST_STOPPED = ["cancelled", "superseded", "paused"];
1798
2050
  var Coordinator = class {
1799
2051
  workspaces = /* @__PURE__ */ new Map();
1800
2052
  options;
1801
2053
  ids;
1802
2054
  store;
1803
2055
  wsVersions = /* @__PURE__ */ new Map();
2056
+ saveChains = /* @__PURE__ */ new Map();
1804
2057
  clockMs;
2058
+ frozenNow;
1805
2059
  auditListeners = [];
2060
+ /**
2061
+ * The signed calls on each workspace's log, by what their senders signed:
2062
+ * recorded refusals with their seq and code, and accepted calls. Rebuilt
2063
+ * from the log when missing, so a snapshot never carries it.
2064
+ */
2065
+ signedIndexes = /* @__PURE__ */ new WeakMap();
1806
2066
  /** Method handler registry; profiles plug into this. */
1807
2067
  handlers = /* @__PURE__ */ new Map();
1808
2068
  /** Custom lapse-check function (whisper/1.0). Set by the whisper profile. */
@@ -1844,42 +2104,72 @@ var Coordinator = class {
1844
2104
  this.applyRecords(records);
1845
2105
  }
1846
2106
  applyRecords(records) {
1847
- this.workspaces.clear();
1848
2107
  this.wsVersions.clear();
1849
- for (const r of records) {
1850
- this.restore([r.data]);
1851
- this.wsVersions.set(r.id, r.version);
1852
- }
2108
+ this.restore(records.map((r) => r.data));
2109
+ for (const r of records) this.wsVersions.set(r.id, r.version);
1853
2110
  }
1854
2111
  /** Persist the current snapshot of one workspace to the store. */
1855
2112
  persist(ws) {
1856
2113
  const next = (this.wsVersions.get(ws.id) ?? 0) + 1;
1857
2114
  this.wsVersions.set(ws.id, next);
2115
+ let record;
1858
2116
  try {
1859
2117
  const all = this.snapshot();
1860
2118
  const data = all.find((w) => w.id === ws.id);
1861
2119
  if (!data) return;
1862
- const result = this.store.save({
1863
- id: ws.id,
1864
- data,
1865
- version: next,
1866
- updated_at: this.now()
1867
- });
1868
- if (result instanceof Promise) {
1869
- result.catch(() => {
1870
- });
1871
- }
2120
+ record = { id: ws.id, data, version: next, updated_at: this.now() };
1872
2121
  } catch {
2122
+ return;
2123
+ }
2124
+ const pending = this.saveChains.get(ws.id);
2125
+ if (pending) {
2126
+ this.trackSave(pending.then(() => this.store.save(record)), record);
2127
+ return;
2128
+ }
2129
+ let result;
2130
+ try {
2131
+ result = this.store.save(record);
2132
+ } catch (err) {
2133
+ this.reportStoreError(err, record);
2134
+ return;
2135
+ }
2136
+ if (result instanceof Promise) {
2137
+ this.trackSave(result, record);
2138
+ }
2139
+ }
2140
+ trackSave(save, record) {
2141
+ const guarded = save.catch((err) => this.reportStoreError(err, record));
2142
+ this.saveChains.set(record.id, guarded);
2143
+ void guarded.then(() => {
2144
+ if (this.saveChains.get(record.id) === guarded) {
2145
+ this.saveChains.delete(record.id);
2146
+ }
2147
+ });
2148
+ }
2149
+ reportStoreError(err, record) {
2150
+ if (this.options.onStoreError) {
2151
+ try {
2152
+ this.options.onStoreError(err, { id: record.id, version: record.version });
2153
+ } catch {
2154
+ }
2155
+ }
2156
+ }
2157
+ async drainSaves() {
2158
+ while (this.saveChains.size) {
2159
+ await Promise.allSettled([...this.saveChains.values()]);
1873
2160
  }
1874
2161
  }
1875
2162
  // -- lifecycle -----------------------------------------------------
1876
- now() {
2163
+ advanceClock() {
1877
2164
  if (this.clockMs !== void 0) {
1878
2165
  this.clockMs += 1e3;
1879
2166
  return nowIso(this.clockMs);
1880
2167
  }
1881
2168
  return nowIso();
1882
2169
  }
2170
+ now() {
2171
+ return this.frozenNow ?? this.advanceClock();
2172
+ }
1883
2173
  onAudit(listener) {
1884
2174
  this.auditListeners.push(listener);
1885
2175
  return () => {
@@ -1904,8 +2194,11 @@ var Coordinator = class {
1904
2194
  * error-table divergence is tracked separately.)
1905
2195
  */
1906
2196
  requireMember(ws, sender) {
1907
- if (typeof sender !== "string" || !ws.members.has(sender)) {
1908
- return { error: rpcError(E.NOT_AUTHORISED, `Not a workspace member: ${String(sender)}`) };
2197
+ if (typeof sender !== "string") {
2198
+ return { error: rpcError(E.NOT_AUTHORISED, "Not a workspace member: from is not a participant URI") };
2199
+ }
2200
+ if (!ws.members.has(sender)) {
2201
+ return { error: rpcError(E.NOT_AUTHORISED, `Not a workspace member: ${sender}`) };
1909
2202
  }
1910
2203
  return null;
1911
2204
  }
@@ -2002,8 +2295,40 @@ var Coordinator = class {
2002
2295
  ws.deliberations.set(x.id, x);
2003
2296
  for (const x of w.handoffs ?? [])
2004
2297
  ws.handoffs.set(x.id, x);
2005
- for (const x of w.snapshots ?? [])
2006
- ws.snapshots.set(x.id, x);
2298
+ for (const x of w.snapshots ?? []) {
2299
+ const content = structuredClone(x.content ?? {
2300
+ workspace: x.workspace,
2301
+ audit_seq: x.audit_seq,
2302
+ include: x.include,
2303
+ state: x.state,
2304
+ ...x.label ? { label: x.label } : {}
2305
+ });
2306
+ if (!x.content) {
2307
+ const oldState = content.state;
2308
+ const state = {};
2309
+ for (const key of ["mode_ceiling", "routing_policy_uri", "audit_seq"]) {
2310
+ if (oldState[key] != null && oldState[key] !== "") state[key] = oldState[key];
2311
+ }
2312
+ for (const [key, fields] of [
2313
+ ["members", ["uri", "type", "role", "scopes"]],
2314
+ ["open_tasks", ["id", "kind", "state", "assignee"]]
2315
+ ]) {
2316
+ const items = oldState[key];
2317
+ if (Array.isArray(items) && items.length) {
2318
+ state[key] = items.map((item) => Object.fromEntries(fields.filter((field) => item[field] != null && item[field] !== "" && (!Array.isArray(item[field]) || item[field].length > 0)).map((field) => [field, item[field]])));
2319
+ }
2320
+ }
2321
+ content.state = state;
2322
+ }
2323
+ ws.snapshots.set(x.id, {
2324
+ id: x.id,
2325
+ kind: "snapshot",
2326
+ produced_by: x.produced_by ?? x.by,
2327
+ produced_at: x.produced_at ?? x.ts,
2328
+ content_hash: x.content_hash ?? contentHash(content),
2329
+ content
2330
+ });
2331
+ }
2007
2332
  for (const x of w.route_decisions ?? [])
2008
2333
  ws.route_decisions.set(x.id, x);
2009
2334
  this.workspaces.set(ws.id, ws);
@@ -2011,14 +2336,29 @@ var Coordinator = class {
2011
2336
  }
2012
2337
  // -- dispatch ------------------------------------------------------
2013
2338
  dispatch(envelope) {
2014
- if (!isValidEnvelope(envelope) || !envelope.method) {
2339
+ this.frozenNow = this.advanceClock();
2340
+ try {
2341
+ const response = this._dispatch(envelope);
2342
+ if (response.error) this.recordRefusal(envelope, response.error);
2343
+ return response;
2344
+ } finally {
2345
+ this.frozenNow = void 0;
2346
+ }
2347
+ }
2348
+ _dispatch(envelope) {
2349
+ if (!isValidEnvelope(envelope) || typeof envelope.method !== "string" || !envelope.method) {
2015
2350
  return reply(envelope, { error: rpcError(E.REQUEST, "Invalid JSON-RPC 2.0 request") });
2016
2351
  }
2352
+ if (!withinNesting(envelope, MAX_NESTING)) {
2353
+ return reply(envelope, { error: rpcError(E.REQUEST, `Envelope nests deeper than ${MAX_NESTING} levels`) });
2354
+ }
2017
2355
  const maxBytes = this.options.maxEnvelopeBytes ?? DEFAULT_MAX_ENVELOPE_BYTES;
2018
2356
  let size = 0;
2357
+ let canonicalError;
2019
2358
  try {
2020
2359
  size = canonicalize(envelope).length;
2021
- } catch {
2360
+ } catch (e) {
2361
+ canonicalError = e;
2022
2362
  }
2023
2363
  if (size > maxBytes) {
2024
2364
  return reply(envelope, { error: rpcError(
@@ -2032,6 +2372,28 @@ var Coordinator = class {
2032
2372
  return reply(envelope, { error: rpcError(E.PARAMS, "Invalid params: expected an object") });
2033
2373
  }
2034
2374
  const params = rawParams ?? {};
2375
+ const handler = this.handlers.get(method);
2376
+ if (!handler) {
2377
+ return reply(envelope, { error: rpcError(E.METHOD, `Unknown method: ${method}`) });
2378
+ }
2379
+ if (canonicalError !== void 0) {
2380
+ const msg = canonicalError instanceof Error ? canonicalError.message : String(canonicalError);
2381
+ return reply(envelope, { error: rpcError(E.PARAMS, msg) });
2382
+ }
2383
+ if (isSigned(envelope)) {
2384
+ const wsId2 = params.workspace;
2385
+ const ws = typeof wsId2 === "string" ? this.workspaces.get(wsId2) : void 0;
2386
+ if (ws) {
2387
+ const prior = this.signedIndex(ws).refused.get(signedDigest(envelope));
2388
+ if (prior) {
2389
+ return reply(envelope, { error: rpcError(
2390
+ prior.code,
2391
+ `Refused at seq ${prior.seq}; a refused request is not evaluated again`,
2392
+ { refused_at_seq: prior.seq }
2393
+ ) });
2394
+ }
2395
+ }
2396
+ }
2035
2397
  if (this.options.requireSignatures && method !== "participant.join" && method !== "workspace.create") {
2036
2398
  const sigErr = this.verifySignature(envelope);
2037
2399
  if (sigErr) return reply(envelope, { error: sigErr });
@@ -2050,7 +2412,11 @@ var Coordinator = class {
2050
2412
  const stale = this.checkStepUp(params);
2051
2413
  if (stale) return reply(envelope, { error: stale });
2052
2414
  }
2053
- if (method.startsWith("control.") || method.startsWith("deliberate.") || method.startsWith("handoff.") || method.startsWith("whisper.")) {
2415
+ {
2416
+ const gate = this.profileGate(method, params);
2417
+ if (gate) return reply(envelope, { error: gate });
2418
+ }
2419
+ if (method.startsWith("control.") || method.startsWith("deliberate.") || method.startsWith("handoff.") || method.startsWith("whisper.") || MEMBER_ONLY_METHODS.has(method)) {
2054
2420
  const wsId2 = params.workspace;
2055
2421
  const ws = typeof wsId2 === "string" ? this.workspaces.get(wsId2) : void 0;
2056
2422
  if (ws) {
@@ -2085,16 +2451,6 @@ var Coordinator = class {
2085
2451
  if (task) return reply(envelope, { result: { task_id: existingId, state: task.state } });
2086
2452
  }
2087
2453
  }
2088
- const handler = this.handlers.get(method);
2089
- if (!handler) {
2090
- return reply(envelope, { error: rpcError(E.METHOD, `Unknown method: ${method}`) });
2091
- }
2092
- try {
2093
- canonicalize(envelope);
2094
- } catch (e) {
2095
- const msg = e instanceof Error ? e.message : String(e);
2096
- return reply(envelope, { error: rpcError(E.PARAMS, msg) });
2097
- }
2098
2454
  let out;
2099
2455
  try {
2100
2456
  out = handler(params);
@@ -2112,17 +2468,56 @@ var Coordinator = class {
2112
2468
  }
2113
2469
  // -- audit ---------------------------------------------------------
2114
2470
  recordAudit(ws, envelope) {
2115
- const entry = {
2116
- seq: ws.audit.length,
2117
- arrived: this.now(),
2118
- envelope: JSON.parse(JSON.stringify(envelope))
2119
- };
2471
+ this.appendEntry(ws, { envelope: JSON.parse(JSON.stringify(envelope)) });
2472
+ }
2473
+ /**
2474
+ * SPECIFICATION 10.1: record a refused call when it is a governed attempt.
2475
+ * The caller must be a member of an existing workspace, the call must be a
2476
+ * JSON-RPC call for a method that is not a read, and the refusal must be one
2477
+ * `refusalIsRecorded` names. The request is kept as it arrived, so a
2478
+ * signature on it still verifies. Every call that reaches a recorded refusal
2479
+ * has passed the canonical check, so it can be hashed into the chain.
2480
+ */
2481
+ recordRefusal(envelope, error) {
2482
+ if (!isValidEnvelope(envelope) || typeof envelope.method !== "string") return;
2483
+ const method = envelope.method;
2484
+ if (READ_ONLY_METHODS.has(method)) return;
2485
+ if (!refusalIsRecorded(method, error, PRIVILEGED_METHODS)) return;
2486
+ const raw = envelope.params;
2487
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return;
2488
+ const params = raw;
2489
+ const ws = typeof params.workspace === "string" ? this.workspaces.get(params.workspace) : void 0;
2490
+ if (!ws || typeof params.from !== "string" || !ws.members.has(params.from)) return;
2491
+ if (isSigned(envelope)) {
2492
+ const index = this.signedIndex(ws);
2493
+ const digest = signedDigest(envelope);
2494
+ if (index.refused.has(digest) || index.accepted.has(digest)) return;
2495
+ }
2496
+ this.appendEntry(ws, {
2497
+ request: JSON.parse(JSON.stringify(envelope)),
2498
+ outcome: { status: "refused", code: error.code }
2499
+ });
2500
+ }
2501
+ /** The signed calls on this workspace's log, built from the log when missing. */
2502
+ signedIndex(ws) {
2503
+ let index = this.signedIndexes.get(ws);
2504
+ if (!index) {
2505
+ index = { refused: /* @__PURE__ */ new Map(), accepted: /* @__PURE__ */ new Set() };
2506
+ for (const entry of ws.audit) indexSigned(index, entry);
2507
+ this.signedIndexes.set(ws, index);
2508
+ }
2509
+ return index;
2510
+ }
2511
+ appendEntry(ws, record) {
2512
+ const entry = { seq: ws.audit.length, arrived: this.now(), ...record };
2120
2513
  if (ws.chain_enabled || this.options.enableChain) {
2121
2514
  const prev = ws.chain_head ?? ZERO_HASH;
2122
2515
  entry.prev_hash = prev;
2123
- ws.chain_head = linkHash(entry.envelope, prev);
2516
+ ws.chain_head = linkHash(entryRecord(entry), prev);
2124
2517
  }
2125
2518
  ws.audit.push(entry);
2519
+ const index = this.signedIndexes.get(ws);
2520
+ if (index) indexSigned(index, entry);
2126
2521
  for (const l of this.auditListeners) {
2127
2522
  try {
2128
2523
  l(ws, entry);
@@ -2145,10 +2540,14 @@ var Coordinator = class {
2145
2540
  const params = envelope.params ?? {};
2146
2541
  const sender = params.from;
2147
2542
  const wsId = params.workspace;
2148
- const ts = params.ts ?? this.now();
2149
- if (!sender || !wsId) {
2543
+ const rawTs = params.ts;
2544
+ if (typeof sender !== "string" || !sender || typeof wsId !== "string" || !wsId) {
2150
2545
  return rpcError(E.SIG_VERIFY_FAILED, "Cannot verify signature: missing from/workspace");
2151
2546
  }
2547
+ if (rawTs !== void 0 && rawTs !== null && typeof rawTs !== "string") {
2548
+ return rpcError(E.SIG_VERIFY_FAILED, "Cannot verify signature: ts must be a string");
2549
+ }
2550
+ const ts = rawTs ?? this.now();
2152
2551
  const ws = this.workspaces.get(wsId);
2153
2552
  if (!ws) {
2154
2553
  return rpcError(E.SIG_VERIFY_FAILED, "Cannot verify signature: unknown workspace");
@@ -2193,10 +2592,34 @@ var Coordinator = class {
2193
2592
  return null;
2194
2593
  }
2195
2594
  // -- step-up (identity-oidc/1.0) -----------------------------------
2595
+ /**
2596
+ * The SPECIFICATION 15.4 check, per method rather than per namespace.
2597
+ *
2598
+ * Core is always present, so a core method is never gated. The reads in
2599
+ * ALWAYS_AVAILABLE are never gated either: a workspace must be able to ask
2600
+ * what it is and to check its own chain, and `audit.verify_chain` belongs to
2601
+ * `audit-scitt/1.0` while chaining also turns on through an option, so
2602
+ * gating it would let a workspace write a hash-linked chain it is refused
2603
+ * permission to verify.
2604
+ */
2605
+ profileGate(method, params) {
2606
+ const owner = OWNING_PROFILE[method];
2607
+ if (owner === void 0 || owner === "core/1.0" || ALWAYS_AVAILABLE.has(method)) return void 0;
2608
+ const wsId = params.workspace;
2609
+ const ws = typeof wsId === "string" ? this.workspaces.get(wsId) : void 0;
2610
+ if (!ws) return void 0;
2611
+ const name = owner.split("/")[0];
2612
+ if (ws.profiles.some((p) => p === name || p.startsWith(name + "/"))) return void 0;
2613
+ return rpcError(
2614
+ E.METHOD,
2615
+ `Unknown method: ${method}`,
2616
+ { profile: owner, advertised: [...ws.profiles] }
2617
+ );
2618
+ }
2196
2619
  checkStepUp(params) {
2197
2620
  const wsId = params.workspace;
2198
2621
  const sender = params.from;
2199
- if (!wsId || !sender) return null;
2622
+ if (typeof wsId !== "string" || typeof sender !== "string" || !wsId || !sender) return null;
2200
2623
  const ws = this.workspaces.get(wsId);
2201
2624
  if (!ws) return null;
2202
2625
  const member = ws.members.get(sender);
@@ -2260,6 +2683,20 @@ var Coordinator = class {
2260
2683
  if (typeof id !== "string") return { error: rpcError(E.PARAMS, "workspace must be a string id") };
2261
2684
  if (this.workspaces.has(id)) return { error: rpcError(E.PARAMS, `workspace already exists: ${id}`) };
2262
2685
  const profiles = Array.isArray(p.profiles) ? [...p.profiles] : [...this.options.defaultProfiles ?? ["core/1.0", "review/1.0"]];
2686
+ for (const [profile, enforced, option] of [
2687
+ ["security-signed/1.0", !!this.options.requireSignatures, "requireSignatures"],
2688
+ ["identity-oidc/1.0", this.options.verifyOidcToken !== void 0, "verifyOidcToken"]
2689
+ ]) {
2690
+ const name = profile.split("/")[0];
2691
+ const advertised = profiles.some((x) => x === name || x.startsWith(name + "/"));
2692
+ if (enforced && !advertised) profiles.push(profile);
2693
+ else if (advertised && !enforced) {
2694
+ return { error: rpcError(
2695
+ E.PARAMS,
2696
+ `${profile} is advertised but ${option} is not configured. A workspace that advertises it and does not enforce it describes a guarantee it does not give.`
2697
+ ) };
2698
+ }
2699
+ }
2263
2700
  const chainEnabled = profiles.includes("audit-scitt/1.0") || !!this.options.enableChain;
2264
2701
  const ws = {
2265
2702
  id,
@@ -2309,7 +2746,10 @@ var Coordinator = class {
2309
2746
  audit_count: ws.audit.length,
2310
2747
  task_count: ws.tasks.size,
2311
2748
  override_count: ws.overrides.size,
2312
- evidence_head: ws.chain_head,
2749
+ // Omitted where the workspace has no chain, which is what the Python
2750
+ // reference now sends too: JSON.stringify drops an undefined value, so
2751
+ // the two answered the same call differently on the wire.
2752
+ ...ws.chain_head ? { evidence_head: ws.chain_head } : {},
2313
2753
  ...ws.routing_policy_uri ? { routing_policy_uri: ws.routing_policy_uri } : {}
2314
2754
  } };
2315
2755
  }
@@ -2343,6 +2783,8 @@ var Coordinator = class {
2343
2783
  }
2344
2784
  const uri = p.from;
2345
2785
  const now = this.now();
2786
+ const existing = ws.members.get(uri);
2787
+ let tokenVerified = false;
2346
2788
  const member = {
2347
2789
  uri,
2348
2790
  type: p.type,
@@ -2357,20 +2799,41 @@ var Coordinator = class {
2357
2799
  if (this.options.verifyOidcToken && typeof p.oidc_token === "string") {
2358
2800
  const claims = this.options.verifyOidcToken(p.oidc_token);
2359
2801
  if (claims === null) return { error: rpcError(E.OIDC_TOKEN_INVALID, "OIDC token invalid") };
2360
- member.oidc_sub = claims.sub;
2802
+ const boundUri = claims.chap_participant_uri;
2803
+ const sub = typeof claims.sub === "string" && claims.sub !== "" ? claims.sub : void 0;
2804
+ if (boundUri != null && boundUri !== uri) {
2805
+ return { error: rpcError(E.OIDC_CNF_MISMATCH, "OIDC token is bound to another participant") };
2806
+ }
2807
+ if (existing) {
2808
+ if (existing.oidc_sub != null) {
2809
+ if (sub !== existing.oidc_sub) {
2810
+ return { error: rpcError(E.OIDC_CNF_MISMATCH, "OIDC token subject does not match the member") };
2811
+ }
2812
+ } else if (boundUri !== uri) {
2813
+ return { error: rpcError(E.OIDC_CNF_MISMATCH, "OIDC token does not name this member") };
2814
+ }
2815
+ }
2816
+ member.oidc_sub = sub;
2817
+ tokenVerified = true;
2361
2818
  const at = claims.auth_time;
2362
2819
  if (typeof at === "number") member.oidc_auth_time = at;
2363
- member.oidc_acr = claims.acr;
2820
+ member.oidc_acr = typeof claims.acr === "string" ? claims.acr : void 0;
2364
2821
  const cnf = claims.cnf;
2365
2822
  const cnfJwk = cnf?.jwk;
2366
2823
  if (cnfJwk && typeof cnfJwk.kid === "string") {
2367
2824
  member.keys.push({ jwk: cnfJwk, kid: cnfJwk.kid, valid_from: now });
2368
2825
  }
2369
2826
  }
2370
- if (this.options.verifyVc && typeof p.vc_presentation === "object" && p.vc_presentation !== null) {
2827
+ if (this.options.verifyVc && typeof p.vc_presentation === "object" && p.vc_presentation !== null && !Array.isArray(p.vc_presentation)) {
2371
2828
  const subject = this.options.verifyVc(p.vc_presentation);
2372
2829
  if (subject === null) return { error: rpcError(E.VC_VP_INVALID, "VC presentation invalid") };
2373
- member.vc_holder = subject.holder ?? subject.id;
2830
+ const holder = [subject.holder, subject.id].find(
2831
+ (h) => typeof h === "string" && h !== ""
2832
+ );
2833
+ if (existing && (existing.vc_holder == null || holder !== existing.vc_holder)) {
2834
+ return { error: rpcError(E.VC_HOLDER_BINDING_INVALID, "Presentation holder does not match the member") };
2835
+ }
2836
+ member.vc_holder = holder;
2374
2837
  const vpJwk = subject.cnf_jwk;
2375
2838
  if (vpJwk && typeof vpJwk.kid === "string") {
2376
2839
  member.keys.push({ jwk: vpJwk, kid: vpJwk.kid, valid_from: now });
@@ -2385,11 +2848,11 @@ var Coordinator = class {
2385
2848
  }
2386
2849
  }
2387
2850
  }
2388
- const existing = ws.members.get(uri);
2389
2851
  if (existing) {
2390
2852
  if (member.oidc_sub !== void 0) existing.oidc_sub = member.oidc_sub;
2391
2853
  if (member.oidc_auth_time !== void 0) existing.oidc_auth_time = member.oidc_auth_time;
2392
2854
  if (member.oidc_acr !== void 0) existing.oidc_acr = member.oidc_acr;
2855
+ if (tokenVerified) existing.oidc_acr = member.oidc_acr;
2393
2856
  if (member.vc_holder !== void 0) existing.vc_holder = member.vc_holder;
2394
2857
  for (const k of attested) {
2395
2858
  if (!existing.keys.some((x) => x.kid === k.kid)) existing.keys.push(k);
@@ -2413,12 +2876,15 @@ var Coordinator = class {
2413
2876
  const notMember = this.requireMember(ws, p.from);
2414
2877
  if (notMember) return notMember;
2415
2878
  const assignee = p.assignee || p.to;
2416
- if (!assignee || !ws.members.has(assignee)) {
2879
+ if (typeof assignee !== "string" || !ws.members.has(assignee)) {
2417
2880
  return { error: rpcError(E.PARAMS, "Assignee not in workspace") };
2418
2881
  }
2419
2882
  if (ws.members.get(assignee).paused) {
2420
2883
  return { error: rpcError(E.CONTROL_WORKSPACE_PAUSED, `Assignee ${assignee} is paused`) };
2421
2884
  }
2885
+ if (p.mode !== void 0 && p.mode !== null && typeof p.mode !== "string") {
2886
+ return { error: rpcError(E.PARAMS, "mode must be a string") };
2887
+ }
2422
2888
  const requestedMode = p.mode || ws.mode;
2423
2889
  if (!modeLE(requestedMode, ws.mode_ceiling)) {
2424
2890
  return { error: rpcError(
@@ -2426,6 +2892,9 @@ var Coordinator = class {
2426
2892
  `Requested mode ${requestedMode} exceeds ceiling ${ws.mode_ceiling}`
2427
2893
  ) };
2428
2894
  }
2895
+ if (p.review_required !== void 0 && p.review_required !== null && typeof p.review_required !== "boolean") {
2896
+ return { error: rpcError(E.PARAMS, "review_required must be a boolean") };
2897
+ }
2429
2898
  const taskId = this.ids.taskId();
2430
2899
  const now = this.now();
2431
2900
  const task = {
@@ -2462,14 +2931,20 @@ var Coordinator = class {
2462
2931
  if (!task) return { error: rpcError(E.PARAMS, "Unknown task") };
2463
2932
  const newState = p.state;
2464
2933
  const legal = {
2465
- created: ["in_progress", "declined", "paused"],
2466
- in_progress: ["in_progress", "completed", "declined", "review_requested", "paused"],
2934
+ created: ["in_progress", "declined"],
2935
+ in_progress: ["in_progress", "completed", "declined"],
2467
2936
  review_requested: ["in_progress"],
2468
- paused: ["in_progress", "cancelled"]
2937
+ paused: ["cancelled"]
2469
2938
  };
2470
2939
  if (!legal[task.state]?.includes(newState)) {
2471
2940
  return { error: rpcError(E.PARAMS, `Illegal transition ${task.state} -> ${newState}`) };
2472
2941
  }
2942
+ if (newState === "completed" && task.review_required) {
2943
+ return { error: rpcError(
2944
+ E.PARAMS,
2945
+ "This task requires review. Submit the output with task.complete, which opens the review; a reviewer decision completes it."
2946
+ ) };
2947
+ }
2473
2948
  task.state = newState;
2474
2949
  task.updated_at = this.now();
2475
2950
  task.history.push({
@@ -2492,18 +2967,18 @@ var Coordinator = class {
2492
2967
  }
2493
2968
  if (task.review_required) {
2494
2969
  const now = this.now();
2495
- task.pending_artefact = p.output;
2496
2970
  if (!task.review) {
2497
2971
  const producer = p.from;
2498
- const eligible = [...ws.members.keys()].filter((uri) => uri !== producer && uri !== task.assignee);
2972
+ const eligible = [...ws.members.entries()].filter(([uri, member]) => uri !== producer && uri !== task.assignee && member.type === "human").map(([uri]) => uri);
2499
2973
  if (eligible.length === 0) {
2500
2974
  return { error: rpcError(
2501
2975
  E.NOT_AUTHORISED,
2502
- "Task requires review but has no eligible reviewer (needs a member other than its assignee and completer)"
2976
+ "Task requires review but has no eligible human reviewer (needs a human member other than its assignee and completer)"
2503
2977
  ) };
2504
2978
  }
2505
2979
  task.review = { requested_at: now, requested_to: eligible, rule: "any_one_approves", decisions: [] };
2506
2980
  }
2981
+ task.pending_artefact = p.output;
2507
2982
  task.state = "review_requested";
2508
2983
  task.updated_at = now;
2509
2984
  task.history.push({
@@ -2530,23 +3005,39 @@ var Coordinator = class {
2530
3005
  }
2531
3006
  const range = p.range ?? {};
2532
3007
  const filter = p.filter ?? {};
3008
+ if (filter.outcome != null && filter.outcome !== "accepted" && filter.outcome !== "refused") {
3009
+ return { error: rpcError(E.PARAMS, "filter.outcome must be 'accepted' or 'refused'") };
3010
+ }
2533
3011
  const fromSeq = range.from_seq ?? 0;
2534
3012
  const toSeq = range.to_seq ?? ws.audit.length;
2535
3013
  const out = [];
2536
3014
  for (const entry of ws.audit.slice(fromSeq, toSeq)) {
2537
- const ep = entry.envelope.params ?? {};
2538
- if (filter.method && entry.envelope.method !== filter.method) continue;
3015
+ const refused = entry.outcome != null;
3016
+ if (filter.outcome === "accepted" && refused) continue;
3017
+ if (filter.outcome === "refused" && !refused) continue;
3018
+ const call2 = entryCall(entry) ?? {};
3019
+ const ep = call2.params ?? {};
3020
+ if (filter.method && call2.method !== filter.method) continue;
2539
3021
  if (filter.from && ep.from !== filter.from) continue;
2540
- if (filter.task_id && ep.task_id !== filter.task_id) continue;
2541
- const item = {
2542
- seq: entry.seq,
2543
- arrived: entry.arrived,
2544
- envelope: entry.envelope
2545
- };
3022
+ if (filter.task_id) {
3023
+ let taskId = ep.task_id;
3024
+ if (call2.method === "whisper.answer") {
3025
+ const prompt = ws.whispers.get(ep.whisper_id);
3026
+ taskId = prompt ? prompt.task_id : void 0;
3027
+ }
3028
+ if (taskId !== filter.task_id) continue;
3029
+ }
3030
+ const item = { seq: entry.seq, arrived: entry.arrived };
3031
+ if (refused) {
3032
+ item.request = entry.request;
3033
+ item.outcome = entry.outcome;
3034
+ } else {
3035
+ item.envelope = entry.envelope;
3036
+ }
2546
3037
  if (entry.prev_hash) item.prev_hash = entry.prev_hash;
2547
3038
  out.push(item);
2548
3039
  }
2549
- return { result: { entries: out, next_seq: toSeq } };
3040
+ return { result: { entries: out, next_seq: Math.min(toSeq, ws.audit.length) } };
2550
3041
  }
2551
3042
  // ==========================================================
2552
3043
  // review/1.0 handlers
@@ -2560,11 +3051,15 @@ var Coordinator = class {
2560
3051
  if (notMember) return notMember;
2561
3052
  const task = ws.tasks.get(p.task_id);
2562
3053
  if (!task) return { error: rpcError(E.PARAMS, "Unknown task") };
3054
+ if (REVIEW_REQUEST_STOPPED.includes(task.state)) {
3055
+ return { error: rpcError(E.NOT_REVIEWABLE, `Cannot request review for task in state: ${task.state}`) };
3056
+ }
2563
3057
  const to = p.to;
2564
3058
  const reviewers = Array.isArray(to) ? to : typeof to === "string" ? [to] : [];
2565
3059
  if (!reviewers.length) return { error: rpcError(E.PARAMS, "review.request needs 'to'") };
2566
3060
  const now = this.now();
2567
- const rule = p.rule || "any_one_approves";
3061
+ const declaredRule = p.rule;
3062
+ const rule = declaredRule || "any_one_approves";
2568
3063
  if (!reviewRuleSupported(rule)) {
2569
3064
  return { error: rpcError(E.PARAMS, `Unsupported review rule ${JSON.stringify(rule)}: review/1.0 supports any_one_approves, all_approve, quorum:<n>`) };
2570
3065
  }
@@ -2575,7 +3070,7 @@ var Coordinator = class {
2575
3070
  "A review is already open on this task with different content. Decide, abstain, escalate or cancel it before requesting review of a new artefact."
2576
3071
  ) };
2577
3072
  }
2578
- if (rule !== task.review.rule) {
3073
+ if (declaredRule !== void 0 && declaredRule !== task.review.rule) {
2579
3074
  return { error: rpcError(
2580
3075
  E.REVIEW_ALREADY_OPEN,
2581
3076
  `Cannot change the decision rule of an open review (currently ${task.review.rule})`
@@ -2767,16 +3262,34 @@ var Coordinator = class {
2767
3262
  if (!ws) return { error: rpcError(E.PARAMS, "Unknown workspace") };
2768
3263
  const notMember = this.requireMember(ws, p.from);
2769
3264
  if (notMember) return notMember;
2770
- const orig = ws.tasks.get(p.original_task_id);
3265
+ const origId = p.original_task_id;
3266
+ const orig = typeof origId === "string" ? ws.tasks.get(origId) : void 0;
2771
3267
  if (!orig) return { error: rpcError(E.PARAMS, "Unknown original task") };
2772
3268
  if (orig.state === "completed" || orig.state === "cancelled" || orig.state === "superseded") {
2773
3269
  return { error: rpcError(E.PARAMS, `Cannot escalate a terminal task: ${orig.state}`) };
2774
3270
  }
3271
+ if (p.new_task === null || typeof p.new_task !== "object" || Array.isArray(p.new_task)) {
3272
+ return { error: rpcError(E.PARAMS, "new_task must be an object") };
3273
+ }
2775
3274
  const nt = p.new_task;
2776
3275
  const assignee = nt.assignee;
2777
- if (!assignee || !ws.members.has(assignee)) {
3276
+ if (typeof assignee !== "string" || !ws.members.has(assignee)) {
2778
3277
  return { error: rpcError(E.PARAMS, "Escalation assignee not in workspace") };
2779
3278
  }
3279
+ const named = nt.mode;
3280
+ if (named !== void 0 && named !== null && typeof named !== "string") {
3281
+ return { error: rpcError(E.PARAMS, "new_task.mode must be a string") };
3282
+ }
3283
+ if (ws.members.get(assignee).paused) {
3284
+ return { error: rpcError(E.CONTROL_WORKSPACE_PAUSED, `Assignee ${assignee} is paused`) };
3285
+ }
3286
+ const mode = named || orig.mode;
3287
+ if (!modeLE(mode, ws.mode_ceiling)) {
3288
+ return { error: rpcError(
3289
+ E.MODE_CEILING_EXCEEDED,
3290
+ `Requested mode ${mode} exceeds ceiling ${ws.mode_ceiling}`
3291
+ ) };
3292
+ }
2780
3293
  const now = this.now();
2781
3294
  const newId = this.ids.taskId();
2782
3295
  const newTask = {
@@ -2788,11 +3301,14 @@ var Coordinator = class {
2788
3301
  input: nt.input ?? {},
2789
3302
  created_at: now,
2790
3303
  updated_at: now,
2791
- mode: orig.mode,
3304
+ mode,
2792
3305
  supersedes: orig.id,
2793
3306
  history: [{ ts: now, from: p.from, state: "created", note: `escalated from ${orig.id}` }],
2794
3307
  paused: false
2795
3308
  };
3309
+ if (orig.review_required || ws.profiles.some((pr) => pr.startsWith("modes/")) && newTask.mode === "trial") {
3310
+ newTask.review_required = true;
3311
+ }
2796
3312
  ws.tasks.set(newId, newTask);
2797
3313
  orig.state = "escalated";
2798
3314
  orig.updated_at = now;
@@ -2933,11 +3449,13 @@ function wrapMcpToolCall(coord, workspace, options) {
2933
3449
  method: "task.update",
2934
3450
  params: { workspace, from: options.caller, task_id: taskId, state: "in_progress" }
2935
3451
  });
3452
+ const output = { result: options.result, citations };
3453
+ if (options.fulfils) output.fulfils = options.fulfils;
2936
3454
  const completeParams = {
2937
3455
  workspace,
2938
3456
  from: options.caller,
2939
3457
  task_id: taskId,
2940
- output: { result: options.result, citations }
3458
+ output
2941
3459
  };
2942
3460
  if (options.confidence !== void 0) completeParams.confidence = options.confidence;
2943
3461
  const completeResp = coord.dispatch({
@@ -2989,11 +3507,13 @@ function wrapA2aMessageExchange(coord, workspace, options) {
2989
3507
  state: "in_progress"
2990
3508
  }
2991
3509
  });
3510
+ const output = { received: options.received, citations: [citation] };
3511
+ if (options.fulfils) output.fulfils = options.fulfils;
2992
3512
  const completeParams = {
2993
3513
  workspace,
2994
3514
  from: options.bridgeUri,
2995
3515
  task_id: taskId,
2996
- output: { received: options.received, citations: [citation] }
3516
+ output
2997
3517
  };
2998
3518
  if (options.confidence !== void 0) completeParams.confidence = options.confidence;
2999
3519
  const completeResp = coord.dispatch({
@@ -3022,8 +3542,12 @@ function wrapA2aMessageExchange(coord, workspace, options) {
3022
3542
  deriveKeypair,
3023
3543
  deriveSeed,
3024
3544
  diffJsonPatch,
3545
+ entryCall,
3546
+ entryRecord,
3547
+ isRefusal,
3025
3548
  isValidEnvelope,
3026
3549
  jwkFromPrivateKey,
3550
+ linkHash,
3027
3551
  makeApi,
3028
3552
  makeDefaultPolicy,
3029
3553
  modeLE,