@brightbeamai/chap-coordinator 0.3.3 → 0.3.4

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
@@ -5,7 +5,7 @@ import { AuditEntry, Envelope } from './types.cjs';
5
5
  export { ArtefactId, ArtefactRoutingHints, AuditListener, Deliberation, DeliberationVote, Handoff, HandoffTaskItem, JsonPatchOp, Jwk, KeyRecord, Member, Mode, OverrideArtefact, ParticipantType, ParticipantUri, PauseScope, RefusalOutcome, ReviewDecision, RouteDecisionArtefact, SnapshotArtefact, Task, TaskId, TaskRoutingHints, TaskState, WhisperPrompt, Workspace, WorkspaceId, WorkspaceState, modeLE } from './types.cjs';
6
6
  export { MemoryStore, Store, StoreChange, StoreConflictError, WorkspaceRecord } from './storage/store.cjs';
7
7
  export { E, isValidEnvelope, rpcError } from './jsonrpc.cjs';
8
- export { PatchError, applyJsonPatch, diffJsonPatch } from './patch.cjs';
8
+ export { DEFAULT_MAX_PATCHED_BYTES, PatchError, PatchLimits, applyJsonPatch, diffJsonPatch } from './patch.cjs';
9
9
  import { KeyObject } from 'node:crypto';
10
10
 
11
11
  /** The value an entry's chain link hashes. */
package/dist/index.d.ts CHANGED
@@ -5,7 +5,7 @@ import { AuditEntry, Envelope } from './types.js';
5
5
  export { ArtefactId, ArtefactRoutingHints, AuditListener, Deliberation, DeliberationVote, Handoff, HandoffTaskItem, JsonPatchOp, Jwk, KeyRecord, Member, Mode, OverrideArtefact, ParticipantType, ParticipantUri, PauseScope, RefusalOutcome, ReviewDecision, RouteDecisionArtefact, SnapshotArtefact, Task, TaskId, TaskRoutingHints, TaskState, WhisperPrompt, Workspace, WorkspaceId, WorkspaceState, modeLE } from './types.js';
6
6
  export { MemoryStore, Store, StoreChange, StoreConflictError, WorkspaceRecord } from './storage/store.js';
7
7
  export { E, isValidEnvelope, rpcError } from './jsonrpc.js';
8
- export { PatchError, applyJsonPatch, diffJsonPatch } from './patch.js';
8
+ export { DEFAULT_MAX_PATCHED_BYTES, PatchError, PatchLimits, applyJsonPatch, diffJsonPatch } from './patch.js';
9
9
  import { KeyObject } from 'node:crypto';
10
10
 
11
11
  /** The value an entry's chain link hashes. */
package/dist/index.js CHANGED
@@ -500,77 +500,116 @@ function deepEqual(a, b) {
500
500
  }
501
501
  return true;
502
502
  }
503
- function applyOne(doc, op) {
503
+ function addAt(doc, path, value, size, copied = false, movedFrom) {
504
+ const parts = splitPath(path);
505
+ if (parts.length === 0) {
506
+ if (size) {
507
+ size.reset(value);
508
+ if (copied || movedFrom !== void 0) size.carry(size);
509
+ }
510
+ return value;
511
+ }
512
+ const [parent, key] = navigate(doc, parts);
513
+ let newKey;
514
+ if (Array.isArray(parent)) {
515
+ if (key === "-") {
516
+ parent.push(value);
517
+ } else {
518
+ const idx = key;
519
+ if (idx < 0 || idx > parent.length) {
520
+ throw new PatchError(`Index out of range for add: ${path}`);
521
+ }
522
+ parent.splice(idx, 0, value);
523
+ }
524
+ } else if (parent !== null && typeof parent === "object") {
525
+ if (Object.prototype.hasOwnProperty.call(parent, key)) size?.take(parent[key]);
526
+ else newKey = key;
527
+ parent[key] = value;
528
+ } else {
529
+ throw new PatchError(`Cannot add into ${jsonType(parent)}`);
530
+ }
531
+ if (size) {
532
+ if (movedFrom === void 0) {
533
+ const inserted = size.insert(value, parts.length, newKey);
534
+ if (copied) size.carry(inserted);
535
+ } else {
536
+ if (newKey !== void 0) size.bytes += utf8Length(newKey);
537
+ if (parts.length > movedFrom) {
538
+ const moved = sizeOf(value);
539
+ size.depth = Math.max(size.depth, parts.length + moved.depth);
540
+ size.carry(moved);
541
+ }
542
+ }
543
+ }
544
+ return doc;
545
+ }
546
+ function removeAt(doc, path, size, moved = false) {
547
+ const parts = splitPath(path);
548
+ if (parts.length === 0) throw new PatchError("Cannot remove root.");
549
+ const [parent, key] = navigate(doc, parts);
550
+ if (Array.isArray(parent)) {
551
+ const idx = key === "-" ? -1 : key;
552
+ if (idx < 0 || idx >= parent.length) {
553
+ throw new PatchError(`Index out of range for remove: ${path}`);
554
+ }
555
+ if (size && !moved) size.take(parent[idx]);
556
+ parent.splice(idx, 1);
557
+ } else if (parent !== null && typeof parent === "object") {
558
+ if (!Object.prototype.hasOwnProperty.call(parent, key)) {
559
+ throw new PatchError(`Path not found for remove: ${path}`);
560
+ }
561
+ if (size) {
562
+ if (moved) size.bytes -= utf8Length(key);
563
+ else size.take(parent[key], key);
564
+ }
565
+ delete parent[key];
566
+ } else {
567
+ throw new PatchError(`Cannot remove from ${jsonType(parent)}`);
568
+ }
569
+ return doc;
570
+ }
571
+ function applyOne(doc, op, size) {
504
572
  const kind = op.op;
505
573
  const path = op.path ?? "";
506
574
  if (kind === "add") {
507
575
  if (!("value" in op)) throw new PatchError("'add' requires 'value'");
508
- const parts = splitPath(path);
509
- if (parts.length === 0) return op.value;
510
- const [parent, key] = navigate(doc, parts);
511
- if (Array.isArray(parent)) {
512
- if (key === "-") {
513
- parent.push(op.value);
514
- } else {
515
- const idx = key;
516
- if (idx < 0 || idx > parent.length) {
517
- throw new PatchError(`Index out of range for add: ${path}`);
518
- }
519
- parent.splice(idx, 0, op.value);
520
- }
521
- } else if (parent !== null && typeof parent === "object") {
522
- parent[key] = op.value;
523
- } else {
524
- throw new PatchError(`Cannot add into ${jsonType(parent)}`);
525
- }
526
- return doc;
576
+ return addAt(doc, path, clone(op.value), size);
527
577
  }
528
578
  if (kind === "replace") {
529
579
  if (!("value" in op)) throw new PatchError("'replace' requires 'value'");
530
580
  const parts = splitPath(path);
531
- if (parts.length === 0) return op.value;
581
+ const value = clone(op.value);
582
+ if (parts.length === 0) {
583
+ size?.reset(value);
584
+ return value;
585
+ }
532
586
  const [parent, key] = navigate(doc, parts);
533
587
  if (Array.isArray(parent)) {
534
588
  const idx = key === "-" ? -1 : key;
535
589
  if (idx < 0 || idx >= parent.length) {
536
590
  throw new PatchError(`Path not found for replace: ${path}`);
537
591
  }
538
- parent[idx] = op.value;
592
+ size?.take(parent[idx]);
593
+ parent[idx] = value;
539
594
  } else if (parent !== null && typeof parent === "object") {
540
595
  if (!Object.prototype.hasOwnProperty.call(parent, key)) {
541
596
  throw new PatchError(`Path not found for replace: ${path}`);
542
597
  }
543
- parent[key] = op.value;
598
+ size?.take(parent[key]);
599
+ parent[key] = value;
544
600
  } else {
545
601
  throw new PatchError(`Cannot replace in ${jsonType(parent)}`);
546
602
  }
603
+ size?.insert(value, parts.length);
547
604
  return doc;
548
605
  }
549
606
  if (kind === "remove") {
550
- const parts = splitPath(path);
551
- if (parts.length === 0) throw new PatchError("Cannot remove root.");
552
- const [parent, key] = navigate(doc, parts);
553
- if (Array.isArray(parent)) {
554
- const idx = key === "-" ? -1 : key;
555
- if (idx < 0 || idx >= parent.length) {
556
- throw new PatchError(`Index out of range for remove: ${path}`);
557
- }
558
- parent.splice(idx, 1);
559
- } else if (parent !== null && typeof parent === "object") {
560
- if (!Object.prototype.hasOwnProperty.call(parent, key)) {
561
- throw new PatchError(`Path not found for remove: ${path}`);
562
- }
563
- delete parent[key];
564
- } else {
565
- throw new PatchError(`Cannot remove from ${jsonType(parent)}`);
566
- }
567
- return doc;
607
+ return removeAt(doc, path, size);
568
608
  }
569
609
  if (kind === "copy") {
570
610
  const src = op.from;
571
611
  if (src === void 0) throw new PatchError("'copy' requires 'from'");
572
- const value = clone(getAt(doc, src));
573
- return applyOne(doc, { op: "add", path, value });
612
+ return addAt(doc, path, clone(getAt(doc, src)), size, true);
574
613
  }
575
614
  if (kind === "move") {
576
615
  const src = op.from;
@@ -578,9 +617,9 @@ function applyOne(doc, op) {
578
617
  if (path.startsWith(src + "/")) {
579
618
  throw new PatchError("'move' cannot move a location into its own child.");
580
619
  }
581
- const value = clone(getAt(doc, src));
582
- doc = applyOne(doc, { op: "remove", path: src });
583
- return applyOne(doc, { op: "add", path, value });
620
+ const value = getAt(doc, src);
621
+ doc = removeAt(doc, src, size, true);
622
+ return addAt(doc, path, value, size, false, splitPath(src).length);
584
623
  }
585
624
  if (kind === "test") {
586
625
  if (!("value" in op)) throw new PatchError("'test' requires 'value'");
@@ -592,29 +631,122 @@ function applyOne(doc, op) {
592
631
  }
593
632
  var MAX_PATCH_OPS = 1e3;
594
633
  var MAX_DOCUMENT_NODES = 1e5;
595
- function nodeCount(value) {
596
- if (Array.isArray(value)) {
597
- let n = 1;
598
- for (const v of value) n += nodeCount(v);
599
- return n;
600
- }
601
- if (value !== null && typeof value === "object") {
602
- let n = 1;
603
- for (const v of Object.values(value)) n += nodeCount(v);
604
- return n;
634
+ var MAX_DOCUMENT_DEPTH = 64;
635
+ var MAX_CARRIED_TIMES = 3;
636
+ var DEFAULT_MAX_PATCHED_BYTES = 1048576;
637
+ function utf8Length(s) {
638
+ const buffer = globalThis.Buffer;
639
+ if (typeof buffer === "function") {
640
+ return buffer.byteLength(s, "utf8");
641
+ }
642
+ let n = 0;
643
+ for (let i = 0; i < s.length; i++) {
644
+ const c = s.charCodeAt(i);
645
+ if (c < 128) n += 1;
646
+ else if (c < 2048) n += 2;
647
+ else if (c >= 55296 && c <= 56319 && i + 1 < s.length) {
648
+ const d = s.charCodeAt(i + 1);
649
+ if (d >= 56320 && d <= 57343) {
650
+ n += 4;
651
+ i++;
652
+ } else n += 3;
653
+ } else n += 3;
654
+ }
655
+ return n;
656
+ }
657
+ function sizeOf(value) {
658
+ if (value === null || typeof value !== "object") {
659
+ return { nodes: 1, bytes: typeof value === "string" ? utf8Length(value) : 0, depth: 1 };
660
+ }
661
+ let nodes = 0;
662
+ let bytes = 0;
663
+ let depth = 0;
664
+ const values = [value];
665
+ const levels = [1];
666
+ const visit = (x, level) => {
667
+ if (x !== null && typeof x === "object") {
668
+ values.push(x);
669
+ levels.push(level);
670
+ return;
671
+ }
672
+ nodes += 1;
673
+ if (level > depth) depth = level;
674
+ if (typeof x === "string") bytes += utf8Length(x);
675
+ };
676
+ while (values.length > 0) {
677
+ const v = values.pop();
678
+ const level = levels.pop();
679
+ nodes += 1;
680
+ if (level > depth) depth = level;
681
+ if (Array.isArray(v)) {
682
+ for (const x of v) visit(x, level + 1);
683
+ } else {
684
+ const o = v;
685
+ for (const k of Object.keys(o)) {
686
+ bytes += utf8Length(k);
687
+ visit(o[k], level + 1);
688
+ }
689
+ }
605
690
  }
606
- return 1;
691
+ return { nodes, bytes, depth };
607
692
  }
608
- function applyJsonPatch(doc, ops) {
693
+ var Size = class {
694
+ nodes = 0;
695
+ bytes = 0;
696
+ depth = 0;
697
+ /** What copies, and moves to a deeper place or onto the whole document, have carried. */
698
+ carriedNodes = 0;
699
+ carriedBytes = 0;
700
+ constructor(doc) {
701
+ this.reset(doc);
702
+ }
703
+ reset(doc) {
704
+ const s = sizeOf(doc);
705
+ this.nodes = s.nodes;
706
+ this.bytes = s.bytes;
707
+ this.depth = s.depth;
708
+ }
709
+ /** `value` goes in `segments` levels below the top, under a new `key`. */
710
+ insert(value, segments, key) {
711
+ const s = sizeOf(value);
712
+ this.nodes += s.nodes;
713
+ this.bytes += s.bytes + (key === void 0 ? 0 : utf8Length(key));
714
+ this.depth = Math.max(this.depth, segments + s.depth);
715
+ return s;
716
+ }
717
+ carry(s) {
718
+ this.carriedNodes += s.nodes;
719
+ this.carriedBytes += s.bytes;
720
+ }
721
+ /** `value` comes out, with its `key`. */
722
+ take(value, key) {
723
+ const s = sizeOf(value);
724
+ this.nodes -= s.nodes;
725
+ this.bytes -= s.bytes + (key === void 0 ? 0 : utf8Length(key));
726
+ }
727
+ };
728
+ function applyJsonPatch(doc, ops, limits = {}) {
729
+ const maxBytes = limits.maxStringBytes ?? DEFAULT_MAX_PATCHED_BYTES;
609
730
  if (ops.length > MAX_PATCH_OPS) {
610
731
  throw new PatchError(`Patch has too many operations (${ops.length} > ${MAX_PATCH_OPS})`);
611
732
  }
612
733
  let out = JSON.parse(JSON.stringify(doc));
734
+ const size = new Size(out);
613
735
  for (const op of ops) {
614
- out = applyOne(out, op);
615
- if (nodeCount(out) > MAX_DOCUMENT_NODES) {
736
+ out = applyOne(out, op, size);
737
+ if (size.depth > MAX_DOCUMENT_DEPTH) size.depth = sizeOf(out).depth;
738
+ if (size.nodes > MAX_DOCUMENT_NODES) {
616
739
  throw new PatchError(`Patched document exceeds the node limit (${MAX_DOCUMENT_NODES})`);
617
740
  }
741
+ if (size.bytes > maxBytes) {
742
+ throw new PatchError(`Patched document exceeds the size limit (${maxBytes} bytes of strings and keys)`);
743
+ }
744
+ if (size.depth > MAX_DOCUMENT_DEPTH) {
745
+ throw new PatchError(`Patched document nests deeper than ${MAX_DOCUMENT_DEPTH} levels`);
746
+ }
747
+ if (size.carriedNodes > MAX_CARRIED_TIMES * MAX_DOCUMENT_NODES || size.carriedBytes > MAX_CARRIED_TIMES * maxBytes) {
748
+ throw new PatchError(`Patch copies or moves more than its limit (${MAX_CARRIED_TIMES * MAX_DOCUMENT_NODES} values or ${MAX_CARRIED_TIMES * maxBytes} bytes of strings and keys)`);
749
+ }
618
750
  }
619
751
  return out;
620
752
  }
@@ -1347,6 +1479,7 @@ function registerControl(coord) {
1347
1479
  }
1348
1480
 
1349
1481
  // src/profiles/deliberation.ts
1482
+ var RESERVED_IDS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
1350
1483
  function parseRule(rule) {
1351
1484
  if (rule.includes(":")) {
1352
1485
  const [kind, arg] = rule.split(":", 2);
@@ -1447,6 +1580,18 @@ function registerDeliberation(coord) {
1447
1580
  const msg = e instanceof Error ? e.message : String(e);
1448
1581
  return { error: rpcError(E.DELIB_UNKNOWN_RULE, msg) };
1449
1582
  }
1583
+ const suppliedDeliberationId = p.deliberation_id;
1584
+ if (suppliedDeliberationId !== void 0 && suppliedDeliberationId !== null && suppliedDeliberationId !== "") {
1585
+ if (typeof suppliedDeliberationId !== "string") {
1586
+ return { error: rpcError(E.PARAMS, "deliberation_id must be a string") };
1587
+ }
1588
+ if (RESERVED_IDS.has(suppliedDeliberationId)) {
1589
+ return { error: rpcError(E.PARAMS, `deliberation_id cannot be ${JSON.stringify(suppliedDeliberationId)}`) };
1590
+ }
1591
+ if (ws.deliberations.has(suppliedDeliberationId)) {
1592
+ return { error: rpcError(E.PARAMS, `deliberation_id already in use: ${suppliedDeliberationId}`) };
1593
+ }
1594
+ }
1450
1595
  const id = p.deliberation_id || coord.ids.deliberationId();
1451
1596
  const delib = {
1452
1597
  id,
@@ -1529,6 +1674,7 @@ function registerDeliberation(coord) {
1529
1674
  }
1530
1675
 
1531
1676
  // src/profiles/handoff.ts
1677
+ var RESERVED_IDS2 = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
1532
1678
  function registerHandoff(coord) {
1533
1679
  coord.handlers.set("handoff.propose", (p) => {
1534
1680
  const ws = coord.workspaces.get(p.workspace);
@@ -1571,6 +1717,18 @@ function registerHandoff(coord) {
1571
1717
  blockers: t.blockers ?? []
1572
1718
  });
1573
1719
  }
1720
+ const suppliedHandoffId = p.handoff_id;
1721
+ if (suppliedHandoffId !== void 0 && suppliedHandoffId !== null && suppliedHandoffId !== "") {
1722
+ if (typeof suppliedHandoffId !== "string") {
1723
+ return { error: rpcError(E.PARAMS, "handoff_id must be a string") };
1724
+ }
1725
+ if (RESERVED_IDS2.has(suppliedHandoffId)) {
1726
+ return { error: rpcError(E.PARAMS, `handoff_id cannot be ${JSON.stringify(suppliedHandoffId)}`) };
1727
+ }
1728
+ if (ws.handoffs.has(suppliedHandoffId)) {
1729
+ return { error: rpcError(E.PARAMS, `handoff_id already in use: ${suppliedHandoffId}`) };
1730
+ }
1731
+ }
1574
1732
  const id = p.handoff_id || coord.ids.handoffId();
1575
1733
  const ho = {
1576
1734
  id,
@@ -1961,6 +2119,7 @@ function registerSecuritySigned(coord) {
1961
2119
  }
1962
2120
 
1963
2121
  // src/profiles/whisper.ts
2122
+ var RESERVED_IDS3 = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
1964
2123
  function parseIso(ts) {
1965
2124
  return new Date(ts).getTime();
1966
2125
  }
@@ -1985,6 +2144,18 @@ function registerWhisper(coord) {
1985
2144
  }
1986
2145
  const to = p.to;
1987
2146
  const askee = Array.isArray(to) ? to : [to];
2147
+ const suppliedWhisperId = p.whisper_id;
2148
+ if (suppliedWhisperId !== void 0 && suppliedWhisperId !== null && suppliedWhisperId !== "") {
2149
+ if (typeof suppliedWhisperId !== "string") {
2150
+ return { error: rpcError(E.PARAMS, "whisper_id must be a string") };
2151
+ }
2152
+ if (RESERVED_IDS3.has(suppliedWhisperId)) {
2153
+ return { error: rpcError(E.PARAMS, `whisper_id cannot be ${JSON.stringify(suppliedWhisperId)}`) };
2154
+ }
2155
+ if (ws.whispers.has(suppliedWhisperId)) {
2156
+ return { error: rpcError(E.PARAMS, `whisper_id already in use: ${suppliedWhisperId}`) };
2157
+ }
2158
+ }
1988
2159
  const id = p.whisper_id || coord.ids.artefactId();
1989
2160
  const prompt = {
1990
2161
  id,
@@ -3334,6 +3505,16 @@ var Coordinator = class {
3334
3505
  ) };
3335
3506
  }
3336
3507
  task.review = { requested_at: now, requested_to: eligible, rule: "any_one_approves", decisions: [] };
3508
+ } else {
3509
+ const author = p.from;
3510
+ const reviewers = task.review.requested_to.filter((r) => r !== author);
3511
+ if (reviewers.length === 0) {
3512
+ return { error: rpcError(
3513
+ E.NOT_AUTHORISED,
3514
+ "Task requires review but has no reviewer other than its completer"
3515
+ ) };
3516
+ }
3517
+ task.review = { ...task.review, requested_at: now, requested_to: reviewers, decisions: [] };
3337
3518
  }
3338
3519
  task.pending_artefact = p.output;
3339
3520
  task.state = "review_requested";
@@ -3549,7 +3730,9 @@ var Coordinator = class {
3549
3730
  if (base === void 0) return { error: rpcError(E.PARAMS, "No base artefact for override") };
3550
3731
  let applied;
3551
3732
  try {
3552
- applied = applyJsonPatch(base, p.diff);
3733
+ applied = applyJsonPatch(base, p.diff, {
3734
+ maxStringBytes: this.options.maxEnvelopeBytes ?? DEFAULT_MAX_ENVELOPE_BYTES
3735
+ });
3553
3736
  } catch (e) {
3554
3737
  const msg = e instanceof Error ? e.message : String(e);
3555
3738
  return { error: rpcError(E.PATCH_FAILED, msg) };
@@ -3885,6 +4068,7 @@ function wrapA2aMessageExchange(coord, workspace, options) {
3885
4068
  export {
3886
4069
  Coordinator,
3887
4070
  CoordinatorError,
4071
+ DEFAULT_MAX_PATCHED_BYTES,
3888
4072
  E,
3889
4073
  IdFactory,
3890
4074
  MemoryStore,