@brightbeamai/chap-coordinator 0.3.2 → 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.cjs CHANGED
@@ -22,6 +22,7 @@ var src_exports = {};
22
22
  __export(src_exports, {
23
23
  Coordinator: () => Coordinator,
24
24
  CoordinatorError: () => CoordinatorError,
25
+ DEFAULT_MAX_PATCHED_BYTES: () => DEFAULT_MAX_PATCHED_BYTES,
25
26
  E: () => E,
26
27
  IdFactory: () => IdFactory,
27
28
  MemoryStore: () => MemoryStore,
@@ -553,77 +554,116 @@ function deepEqual(a, b) {
553
554
  }
554
555
  return true;
555
556
  }
556
- function applyOne(doc, op) {
557
+ function addAt(doc, path, value, size, copied = false, movedFrom) {
558
+ const parts = splitPath(path);
559
+ if (parts.length === 0) {
560
+ if (size) {
561
+ size.reset(value);
562
+ if (copied || movedFrom !== void 0) size.carry(size);
563
+ }
564
+ return value;
565
+ }
566
+ const [parent, key] = navigate(doc, parts);
567
+ let newKey;
568
+ if (Array.isArray(parent)) {
569
+ if (key === "-") {
570
+ parent.push(value);
571
+ } else {
572
+ const idx = key;
573
+ if (idx < 0 || idx > parent.length) {
574
+ throw new PatchError(`Index out of range for add: ${path}`);
575
+ }
576
+ parent.splice(idx, 0, value);
577
+ }
578
+ } else if (parent !== null && typeof parent === "object") {
579
+ if (Object.prototype.hasOwnProperty.call(parent, key)) size?.take(parent[key]);
580
+ else newKey = key;
581
+ parent[key] = value;
582
+ } else {
583
+ throw new PatchError(`Cannot add into ${jsonType(parent)}`);
584
+ }
585
+ if (size) {
586
+ if (movedFrom === void 0) {
587
+ const inserted = size.insert(value, parts.length, newKey);
588
+ if (copied) size.carry(inserted);
589
+ } else {
590
+ if (newKey !== void 0) size.bytes += utf8Length(newKey);
591
+ if (parts.length > movedFrom) {
592
+ const moved = sizeOf(value);
593
+ size.depth = Math.max(size.depth, parts.length + moved.depth);
594
+ size.carry(moved);
595
+ }
596
+ }
597
+ }
598
+ return doc;
599
+ }
600
+ function removeAt(doc, path, size, moved = false) {
601
+ const parts = splitPath(path);
602
+ if (parts.length === 0) throw new PatchError("Cannot remove root.");
603
+ const [parent, key] = navigate(doc, parts);
604
+ if (Array.isArray(parent)) {
605
+ const idx = key === "-" ? -1 : key;
606
+ if (idx < 0 || idx >= parent.length) {
607
+ throw new PatchError(`Index out of range for remove: ${path}`);
608
+ }
609
+ if (size && !moved) size.take(parent[idx]);
610
+ parent.splice(idx, 1);
611
+ } else if (parent !== null && typeof parent === "object") {
612
+ if (!Object.prototype.hasOwnProperty.call(parent, key)) {
613
+ throw new PatchError(`Path not found for remove: ${path}`);
614
+ }
615
+ if (size) {
616
+ if (moved) size.bytes -= utf8Length(key);
617
+ else size.take(parent[key], key);
618
+ }
619
+ delete parent[key];
620
+ } else {
621
+ throw new PatchError(`Cannot remove from ${jsonType(parent)}`);
622
+ }
623
+ return doc;
624
+ }
625
+ function applyOne(doc, op, size) {
557
626
  const kind = op.op;
558
627
  const path = op.path ?? "";
559
628
  if (kind === "add") {
560
629
  if (!("value" in op)) throw new PatchError("'add' requires 'value'");
561
- const parts = splitPath(path);
562
- if (parts.length === 0) return op.value;
563
- const [parent, key] = navigate(doc, parts);
564
- if (Array.isArray(parent)) {
565
- if (key === "-") {
566
- parent.push(op.value);
567
- } else {
568
- const idx = key;
569
- if (idx < 0 || idx > parent.length) {
570
- throw new PatchError(`Index out of range for add: ${path}`);
571
- }
572
- parent.splice(idx, 0, op.value);
573
- }
574
- } else if (parent !== null && typeof parent === "object") {
575
- parent[key] = op.value;
576
- } else {
577
- throw new PatchError(`Cannot add into ${jsonType(parent)}`);
578
- }
579
- return doc;
630
+ return addAt(doc, path, clone(op.value), size);
580
631
  }
581
632
  if (kind === "replace") {
582
633
  if (!("value" in op)) throw new PatchError("'replace' requires 'value'");
583
634
  const parts = splitPath(path);
584
- if (parts.length === 0) return op.value;
635
+ const value = clone(op.value);
636
+ if (parts.length === 0) {
637
+ size?.reset(value);
638
+ return value;
639
+ }
585
640
  const [parent, key] = navigate(doc, parts);
586
641
  if (Array.isArray(parent)) {
587
642
  const idx = key === "-" ? -1 : key;
588
643
  if (idx < 0 || idx >= parent.length) {
589
644
  throw new PatchError(`Path not found for replace: ${path}`);
590
645
  }
591
- parent[idx] = op.value;
646
+ size?.take(parent[idx]);
647
+ parent[idx] = value;
592
648
  } else if (parent !== null && typeof parent === "object") {
593
649
  if (!Object.prototype.hasOwnProperty.call(parent, key)) {
594
650
  throw new PatchError(`Path not found for replace: ${path}`);
595
651
  }
596
- parent[key] = op.value;
652
+ size?.take(parent[key]);
653
+ parent[key] = value;
597
654
  } else {
598
655
  throw new PatchError(`Cannot replace in ${jsonType(parent)}`);
599
656
  }
657
+ size?.insert(value, parts.length);
600
658
  return doc;
601
659
  }
602
660
  if (kind === "remove") {
603
- const parts = splitPath(path);
604
- if (parts.length === 0) throw new PatchError("Cannot remove root.");
605
- const [parent, key] = navigate(doc, parts);
606
- if (Array.isArray(parent)) {
607
- const idx = key === "-" ? -1 : key;
608
- if (idx < 0 || idx >= parent.length) {
609
- throw new PatchError(`Index out of range for remove: ${path}`);
610
- }
611
- parent.splice(idx, 1);
612
- } else if (parent !== null && typeof parent === "object") {
613
- if (!Object.prototype.hasOwnProperty.call(parent, key)) {
614
- throw new PatchError(`Path not found for remove: ${path}`);
615
- }
616
- delete parent[key];
617
- } else {
618
- throw new PatchError(`Cannot remove from ${jsonType(parent)}`);
619
- }
620
- return doc;
661
+ return removeAt(doc, path, size);
621
662
  }
622
663
  if (kind === "copy") {
623
664
  const src = op.from;
624
665
  if (src === void 0) throw new PatchError("'copy' requires 'from'");
625
- const value = clone(getAt(doc, src));
626
- return applyOne(doc, { op: "add", path, value });
666
+ return addAt(doc, path, clone(getAt(doc, src)), size, true);
627
667
  }
628
668
  if (kind === "move") {
629
669
  const src = op.from;
@@ -631,9 +671,9 @@ function applyOne(doc, op) {
631
671
  if (path.startsWith(src + "/")) {
632
672
  throw new PatchError("'move' cannot move a location into its own child.");
633
673
  }
634
- const value = clone(getAt(doc, src));
635
- doc = applyOne(doc, { op: "remove", path: src });
636
- return applyOne(doc, { op: "add", path, value });
674
+ const value = getAt(doc, src);
675
+ doc = removeAt(doc, src, size, true);
676
+ return addAt(doc, path, value, size, false, splitPath(src).length);
637
677
  }
638
678
  if (kind === "test") {
639
679
  if (!("value" in op)) throw new PatchError("'test' requires 'value'");
@@ -645,29 +685,122 @@ function applyOne(doc, op) {
645
685
  }
646
686
  var MAX_PATCH_OPS = 1e3;
647
687
  var MAX_DOCUMENT_NODES = 1e5;
648
- function nodeCount(value) {
649
- if (Array.isArray(value)) {
650
- let n = 1;
651
- for (const v of value) n += nodeCount(v);
652
- return n;
653
- }
654
- if (value !== null && typeof value === "object") {
655
- let n = 1;
656
- for (const v of Object.values(value)) n += nodeCount(v);
657
- return n;
688
+ var MAX_DOCUMENT_DEPTH = 64;
689
+ var MAX_CARRIED_TIMES = 3;
690
+ var DEFAULT_MAX_PATCHED_BYTES = 1048576;
691
+ function utf8Length(s) {
692
+ const buffer = globalThis.Buffer;
693
+ if (typeof buffer === "function") {
694
+ return buffer.byteLength(s, "utf8");
695
+ }
696
+ let n = 0;
697
+ for (let i = 0; i < s.length; i++) {
698
+ const c = s.charCodeAt(i);
699
+ if (c < 128) n += 1;
700
+ else if (c < 2048) n += 2;
701
+ else if (c >= 55296 && c <= 56319 && i + 1 < s.length) {
702
+ const d = s.charCodeAt(i + 1);
703
+ if (d >= 56320 && d <= 57343) {
704
+ n += 4;
705
+ i++;
706
+ } else n += 3;
707
+ } else n += 3;
708
+ }
709
+ return n;
710
+ }
711
+ function sizeOf(value) {
712
+ if (value === null || typeof value !== "object") {
713
+ return { nodes: 1, bytes: typeof value === "string" ? utf8Length(value) : 0, depth: 1 };
714
+ }
715
+ let nodes = 0;
716
+ let bytes = 0;
717
+ let depth = 0;
718
+ const values = [value];
719
+ const levels = [1];
720
+ const visit = (x, level) => {
721
+ if (x !== null && typeof x === "object") {
722
+ values.push(x);
723
+ levels.push(level);
724
+ return;
725
+ }
726
+ nodes += 1;
727
+ if (level > depth) depth = level;
728
+ if (typeof x === "string") bytes += utf8Length(x);
729
+ };
730
+ while (values.length > 0) {
731
+ const v = values.pop();
732
+ const level = levels.pop();
733
+ nodes += 1;
734
+ if (level > depth) depth = level;
735
+ if (Array.isArray(v)) {
736
+ for (const x of v) visit(x, level + 1);
737
+ } else {
738
+ const o = v;
739
+ for (const k of Object.keys(o)) {
740
+ bytes += utf8Length(k);
741
+ visit(o[k], level + 1);
742
+ }
743
+ }
658
744
  }
659
- return 1;
745
+ return { nodes, bytes, depth };
660
746
  }
661
- function applyJsonPatch(doc, ops) {
747
+ var Size = class {
748
+ nodes = 0;
749
+ bytes = 0;
750
+ depth = 0;
751
+ /** What copies, and moves to a deeper place or onto the whole document, have carried. */
752
+ carriedNodes = 0;
753
+ carriedBytes = 0;
754
+ constructor(doc) {
755
+ this.reset(doc);
756
+ }
757
+ reset(doc) {
758
+ const s = sizeOf(doc);
759
+ this.nodes = s.nodes;
760
+ this.bytes = s.bytes;
761
+ this.depth = s.depth;
762
+ }
763
+ /** `value` goes in `segments` levels below the top, under a new `key`. */
764
+ insert(value, segments, key) {
765
+ const s = sizeOf(value);
766
+ this.nodes += s.nodes;
767
+ this.bytes += s.bytes + (key === void 0 ? 0 : utf8Length(key));
768
+ this.depth = Math.max(this.depth, segments + s.depth);
769
+ return s;
770
+ }
771
+ carry(s) {
772
+ this.carriedNodes += s.nodes;
773
+ this.carriedBytes += s.bytes;
774
+ }
775
+ /** `value` comes out, with its `key`. */
776
+ take(value, key) {
777
+ const s = sizeOf(value);
778
+ this.nodes -= s.nodes;
779
+ this.bytes -= s.bytes + (key === void 0 ? 0 : utf8Length(key));
780
+ }
781
+ };
782
+ function applyJsonPatch(doc, ops, limits = {}) {
783
+ const maxBytes = limits.maxStringBytes ?? DEFAULT_MAX_PATCHED_BYTES;
662
784
  if (ops.length > MAX_PATCH_OPS) {
663
785
  throw new PatchError(`Patch has too many operations (${ops.length} > ${MAX_PATCH_OPS})`);
664
786
  }
665
787
  let out = JSON.parse(JSON.stringify(doc));
788
+ const size = new Size(out);
666
789
  for (const op of ops) {
667
- out = applyOne(out, op);
668
- if (nodeCount(out) > MAX_DOCUMENT_NODES) {
790
+ out = applyOne(out, op, size);
791
+ if (size.depth > MAX_DOCUMENT_DEPTH) size.depth = sizeOf(out).depth;
792
+ if (size.nodes > MAX_DOCUMENT_NODES) {
669
793
  throw new PatchError(`Patched document exceeds the node limit (${MAX_DOCUMENT_NODES})`);
670
794
  }
795
+ if (size.bytes > maxBytes) {
796
+ throw new PatchError(`Patched document exceeds the size limit (${maxBytes} bytes of strings and keys)`);
797
+ }
798
+ if (size.depth > MAX_DOCUMENT_DEPTH) {
799
+ throw new PatchError(`Patched document nests deeper than ${MAX_DOCUMENT_DEPTH} levels`);
800
+ }
801
+ if (size.carriedNodes > MAX_CARRIED_TIMES * MAX_DOCUMENT_NODES || size.carriedBytes > MAX_CARRIED_TIMES * maxBytes) {
802
+ 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)`);
803
+ }
671
804
  }
672
805
  return out;
673
806
  }
@@ -1400,6 +1533,7 @@ function registerControl(coord) {
1400
1533
  }
1401
1534
 
1402
1535
  // src/profiles/deliberation.ts
1536
+ var RESERVED_IDS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
1403
1537
  function parseRule(rule) {
1404
1538
  if (rule.includes(":")) {
1405
1539
  const [kind, arg] = rule.split(":", 2);
@@ -1500,6 +1634,18 @@ function registerDeliberation(coord) {
1500
1634
  const msg = e instanceof Error ? e.message : String(e);
1501
1635
  return { error: rpcError(E.DELIB_UNKNOWN_RULE, msg) };
1502
1636
  }
1637
+ const suppliedDeliberationId = p.deliberation_id;
1638
+ if (suppliedDeliberationId !== void 0 && suppliedDeliberationId !== null && suppliedDeliberationId !== "") {
1639
+ if (typeof suppliedDeliberationId !== "string") {
1640
+ return { error: rpcError(E.PARAMS, "deliberation_id must be a string") };
1641
+ }
1642
+ if (RESERVED_IDS.has(suppliedDeliberationId)) {
1643
+ return { error: rpcError(E.PARAMS, `deliberation_id cannot be ${JSON.stringify(suppliedDeliberationId)}`) };
1644
+ }
1645
+ if (ws.deliberations.has(suppliedDeliberationId)) {
1646
+ return { error: rpcError(E.PARAMS, `deliberation_id already in use: ${suppliedDeliberationId}`) };
1647
+ }
1648
+ }
1503
1649
  const id = p.deliberation_id || coord.ids.deliberationId();
1504
1650
  const delib = {
1505
1651
  id,
@@ -1582,6 +1728,7 @@ function registerDeliberation(coord) {
1582
1728
  }
1583
1729
 
1584
1730
  // src/profiles/handoff.ts
1731
+ var RESERVED_IDS2 = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
1585
1732
  function registerHandoff(coord) {
1586
1733
  coord.handlers.set("handoff.propose", (p) => {
1587
1734
  const ws = coord.workspaces.get(p.workspace);
@@ -1624,6 +1771,18 @@ function registerHandoff(coord) {
1624
1771
  blockers: t.blockers ?? []
1625
1772
  });
1626
1773
  }
1774
+ const suppliedHandoffId = p.handoff_id;
1775
+ if (suppliedHandoffId !== void 0 && suppliedHandoffId !== null && suppliedHandoffId !== "") {
1776
+ if (typeof suppliedHandoffId !== "string") {
1777
+ return { error: rpcError(E.PARAMS, "handoff_id must be a string") };
1778
+ }
1779
+ if (RESERVED_IDS2.has(suppliedHandoffId)) {
1780
+ return { error: rpcError(E.PARAMS, `handoff_id cannot be ${JSON.stringify(suppliedHandoffId)}`) };
1781
+ }
1782
+ if (ws.handoffs.has(suppliedHandoffId)) {
1783
+ return { error: rpcError(E.PARAMS, `handoff_id already in use: ${suppliedHandoffId}`) };
1784
+ }
1785
+ }
1627
1786
  const id = p.handoff_id || coord.ids.handoffId();
1628
1787
  const ho = {
1629
1788
  id,
@@ -2014,6 +2173,7 @@ function registerSecuritySigned(coord) {
2014
2173
  }
2015
2174
 
2016
2175
  // src/profiles/whisper.ts
2176
+ var RESERVED_IDS3 = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
2017
2177
  function parseIso(ts) {
2018
2178
  return new Date(ts).getTime();
2019
2179
  }
@@ -2038,6 +2198,18 @@ function registerWhisper(coord) {
2038
2198
  }
2039
2199
  const to = p.to;
2040
2200
  const askee = Array.isArray(to) ? to : [to];
2201
+ const suppliedWhisperId = p.whisper_id;
2202
+ if (suppliedWhisperId !== void 0 && suppliedWhisperId !== null && suppliedWhisperId !== "") {
2203
+ if (typeof suppliedWhisperId !== "string") {
2204
+ return { error: rpcError(E.PARAMS, "whisper_id must be a string") };
2205
+ }
2206
+ if (RESERVED_IDS3.has(suppliedWhisperId)) {
2207
+ return { error: rpcError(E.PARAMS, `whisper_id cannot be ${JSON.stringify(suppliedWhisperId)}`) };
2208
+ }
2209
+ if (ws.whispers.has(suppliedWhisperId)) {
2210
+ return { error: rpcError(E.PARAMS, `whisper_id already in use: ${suppliedWhisperId}`) };
2211
+ }
2212
+ }
2041
2213
  const id = p.whisper_id || coord.ids.artefactId();
2042
2214
  const prompt = {
2043
2215
  id,
@@ -2491,7 +2663,8 @@ var Coordinator = class {
2491
2663
  let record;
2492
2664
  try {
2493
2665
  record = { id: ws.id, data: this.snapshotOf(ws), version: next, updated_at: this.now() };
2494
- } catch {
2666
+ } catch (err) {
2667
+ this.reportStoreError(err, { id: ws.id, version: next });
2495
2668
  return;
2496
2669
  }
2497
2670
  const pending = this.saveChains.get(ws.id);
@@ -3386,6 +3559,16 @@ var Coordinator = class {
3386
3559
  ) };
3387
3560
  }
3388
3561
  task.review = { requested_at: now, requested_to: eligible, rule: "any_one_approves", decisions: [] };
3562
+ } else {
3563
+ const author = p.from;
3564
+ const reviewers = task.review.requested_to.filter((r) => r !== author);
3565
+ if (reviewers.length === 0) {
3566
+ return { error: rpcError(
3567
+ E.NOT_AUTHORISED,
3568
+ "Task requires review but has no reviewer other than its completer"
3569
+ ) };
3570
+ }
3571
+ task.review = { ...task.review, requested_at: now, requested_to: reviewers, decisions: [] };
3389
3572
  }
3390
3573
  task.pending_artefact = p.output;
3391
3574
  task.state = "review_requested";
@@ -3601,7 +3784,9 @@ var Coordinator = class {
3601
3784
  if (base === void 0) return { error: rpcError(E.PARAMS, "No base artefact for override") };
3602
3785
  let applied;
3603
3786
  try {
3604
- applied = applyJsonPatch(base, p.diff);
3787
+ applied = applyJsonPatch(base, p.diff, {
3788
+ maxStringBytes: this.options.maxEnvelopeBytes ?? DEFAULT_MAX_ENVELOPE_BYTES
3789
+ });
3605
3790
  } catch (e) {
3606
3791
  const msg = e instanceof Error ? e.message : String(e);
3607
3792
  return { error: rpcError(E.PATCH_FAILED, msg) };
@@ -3938,6 +4123,7 @@ function wrapA2aMessageExchange(coord, workspace, options) {
3938
4123
  0 && (module.exports = {
3939
4124
  Coordinator,
3940
4125
  CoordinatorError,
4126
+ DEFAULT_MAX_PATCHED_BYTES,
3941
4127
  E,
3942
4128
  IdFactory,
3943
4129
  MemoryStore,