@brightbeamai/chap-coordinator 0.2.10 → 0.2.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -1,5 +1,5 @@
1
- import { C as CoordinatorOptions, a as Coordinator } from './api-CcT_8H5j.cjs';
2
- export { b as CoordinatorApi, c as CoordinatorError, d as CredentialVerifier, E as EscalationPolicyFn, H as Handler, I as IdFactory, P as PRIVILEGED_METHODS, R as ReviewDepthPolicyFn, e as RoutingPolicyFn, S as ScittReceiptVerifier, f as ScittSubmitter, T as TokenVerifier, g as call, m as makeApi } from './api-CcT_8H5j.cjs';
1
+ import { C as CoordinatorOptions, a as Coordinator } from './api-DiiL0sil.cjs';
2
+ export { b as CoordinatorApi, c as CoordinatorError, d as CredentialVerifier, E as EscalationPolicyFn, H as Handler, I as IdFactory, P as PRIVILEGED_METHODS, R as ReviewDepthPolicyFn, e as RoutingPolicyFn, S as ScittReceiptVerifier, f as ScittSubmitter, T as TokenVerifier, g as call, m as makeApi } from './api-DiiL0sil.cjs';
3
3
  export { MethodName, MethodParams, MethodResult, MethodTable } from './methods.cjs';
4
4
  export { MemoryStore, Store, WorkspaceRecord } from './storage/store.cjs';
5
5
  export { ArtefactId, ArtefactRoutingHints, AuditEntry, AuditListener, Deliberation, DeliberationVote, Envelope, Handoff, HandoffTaskItem, JsonPatchOp, Jwk, KeyRecord, Member, Mode, OverrideArtefact, ParticipantType, ParticipantUri, PauseScope, ReviewDecision, RouteDecisionArtefact, SnapshotArtefact, Task, TaskId, TaskRoutingHints, TaskState, WhisperPrompt, Workspace, WorkspaceId, WorkspaceState, modeLE } from './types.cjs';
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { C as CoordinatorOptions, a as Coordinator } from './api-Cvkhcalw.js';
2
- export { b as CoordinatorApi, c as CoordinatorError, d as CredentialVerifier, E as EscalationPolicyFn, H as Handler, I as IdFactory, P as PRIVILEGED_METHODS, R as ReviewDepthPolicyFn, e as RoutingPolicyFn, S as ScittReceiptVerifier, f as ScittSubmitter, T as TokenVerifier, g as call, m as makeApi } from './api-Cvkhcalw.js';
1
+ import { C as CoordinatorOptions, a as Coordinator } from './api-BOGEBNmX.js';
2
+ export { b as CoordinatorApi, c as CoordinatorError, d as CredentialVerifier, E as EscalationPolicyFn, H as Handler, I as IdFactory, P as PRIVILEGED_METHODS, R as ReviewDepthPolicyFn, e as RoutingPolicyFn, S as ScittReceiptVerifier, f as ScittSubmitter, T as TokenVerifier, g as call, m as makeApi } from './api-BOGEBNmX.js';
3
3
  export { MethodName, MethodParams, MethodResult, MethodTable } from './methods.js';
4
4
  export { MemoryStore, Store, WorkspaceRecord } from './storage/store.js';
5
5
  export { ArtefactId, ArtefactRoutingHints, AuditEntry, AuditListener, Deliberation, DeliberationVote, Envelope, Handoff, HandoffTaskItem, JsonPatchOp, Jwk, KeyRecord, Member, Mode, OverrideArtefact, ParticipantType, ParticipantUri, PauseScope, ReviewDecision, RouteDecisionArtefact, SnapshotArtefact, Task, TaskId, TaskRoutingHints, TaskState, WhisperPrompt, Workspace, WorkspaceId, WorkspaceState, modeLE } from './types.js';
package/dist/index.js CHANGED
@@ -275,6 +275,11 @@ function isValidEnvelope(env) {
275
275
  var PatchError = class extends Error {
276
276
  };
277
277
  var DANGEROUS_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
278
+ var ARRAY_INDEX_RE = /^(0|[1-9][0-9]*)$/;
279
+ function arrayIndex(seg) {
280
+ if (!ARRAY_INDEX_RE.test(seg)) throw new PatchError(`Array index expected at ${JSON.stringify(seg)}`);
281
+ return Number(seg);
282
+ }
278
283
  function unescape(token) {
279
284
  return token.replace(/~1/g, "/").replace(/~0/g, "~");
280
285
  }
@@ -299,8 +304,8 @@ function navigate(doc, parts) {
299
304
  for (let i = 0; i < parts.length - 1; i++) {
300
305
  const part = parts[i];
301
306
  if (Array.isArray(parent)) {
302
- const idx = Number(part);
303
- if (!Number.isInteger(idx) || idx < 0 || idx >= parent.length) {
307
+ const idx = arrayIndex(part);
308
+ if (idx >= parent.length) {
304
309
  throw new PatchError(`Index out of range at /${parts.slice(0, i + 1).join("/")}`);
305
310
  }
306
311
  parent = parent[idx];
@@ -316,11 +321,7 @@ function navigate(doc, parts) {
316
321
  const last = parts[parts.length - 1];
317
322
  if (Array.isArray(parent)) {
318
323
  if (last === "-") return [parent, "-"];
319
- const idx = Number(last);
320
- if (!Number.isInteger(idx)) {
321
- throw new PatchError(`Array index expected at ${JSON.stringify(last)}`);
322
- }
323
- return [parent, idx];
324
+ return [parent, arrayIndex(last)];
324
325
  }
325
326
  return [parent, last];
326
327
  }
@@ -771,7 +772,7 @@ function registerControl(coord) {
771
772
  if (scope === "task") {
772
773
  const task = ws.tasks.get(p.task_id);
773
774
  if (!task) return { error: rpcError(E.PARAMS, "Unknown task") };
774
- if (task.state === "completed" || task.state === "declined" || task.state === "cancelled") {
775
+ if (task.state === "completed" || task.state === "declined" || task.state === "cancelled" || task.state === "superseded") {
775
776
  return { error: rpcError(E.CONTROL_NOT_AUTHORISED, `Cannot pause ${task.state} task`) };
776
777
  }
777
778
  task.paused = true;
@@ -828,7 +829,7 @@ function registerControl(coord) {
828
829
  if (!ws) return { error: rpcError(E.PARAMS, "Unknown workspace") };
829
830
  const task = ws.tasks.get(p.task_id);
830
831
  if (!task) return { error: rpcError(E.PARAMS, "Unknown task") };
831
- if (task.state === "completed" || task.state === "declined" || task.state === "cancelled") {
832
+ if (task.state === "completed" || task.state === "declined" || task.state === "cancelled" || task.state === "superseded") {
832
833
  return { error: rpcError(E.CONTROL_NOT_AUTHORISED, `Cannot cancel ${task.state} task`) };
833
834
  }
834
835
  task.state = "cancelled";
@@ -850,7 +851,7 @@ function registerControl(coord) {
850
851
  }));
851
852
  }
852
853
  if (include.includes("open_tasks")) {
853
- state.open_tasks = Array.from(ws.tasks.values()).filter((t) => t.state !== "completed" && t.state !== "declined" && t.state !== "cancelled").map((t) => ({ id: t.id, kind: t.kind, state: t.state, assignee: t.assignee }));
854
+ 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 }));
854
855
  }
855
856
  if (include.includes("mode_ceiling")) state.mode_ceiling = ws.mode_ceiling;
856
857
  if (include.includes("policy")) state.routing_policy_uri = ws.routing_policy_uri;
@@ -918,6 +919,13 @@ function registerControl(coord) {
918
919
  if (!ws.members.has(assignee)) {
919
920
  return { error: rpcError(E.PARAMS, "successor assignee not in workspace") };
920
921
  }
922
+ const requestedMode = newSpec.mode ?? old.mode;
923
+ if (!modeLE(requestedMode, ws.mode_ceiling)) {
924
+ return { error: rpcError(
925
+ E.MODE_CEILING_EXCEEDED,
926
+ `Requested mode ${requestedMode} exceeds ceiling ${ws.mode_ceiling}`
927
+ ) };
928
+ }
921
929
  const now = coord.now();
922
930
  const newId = coord.ids.taskId();
923
931
  const newTask = {
@@ -929,7 +937,7 @@ function registerControl(coord) {
929
937
  input: newSpec.input ?? {},
930
938
  created_at: now,
931
939
  updated_at: now,
932
- mode: newSpec.mode ?? old.mode,
940
+ mode: requestedMode,
933
941
  supersedes: old.id,
934
942
  history: [{
935
943
  ts: now,
@@ -939,6 +947,8 @@ function registerControl(coord) {
939
947
  }],
940
948
  paused: false
941
949
  };
950
+ if (ws.profiles.some((pr) => pr.startsWith("modes/")) && newTask.mode === "trial") newTask.review_required = true;
951
+ else if ("review_required" in newSpec) newTask.review_required = !!newSpec.review_required;
942
952
  ws.tasks.set(newId, newTask);
943
953
  old.state = "superseded";
944
954
  old.superseded_by = newId;
@@ -1692,6 +1702,8 @@ var READ_ONLY_METHODS = /* @__PURE__ */ new Set([
1692
1702
  "audit.verify_chain",
1693
1703
  "audit.verify_receipt"
1694
1704
  ]);
1705
+ var MAX_IDEMPOTENCY_KEYS = 1e4;
1706
+ var DEFAULT_MAX_ENVELOPE_BYTES = 1048576;
1695
1707
  function nowIso(clockMs) {
1696
1708
  const d = clockMs === void 0 ? /* @__PURE__ */ new Date() : new Date(clockMs);
1697
1709
  return d.toISOString().replace(/\.(\d{3})Z$/, ".$1Z");
@@ -1710,12 +1722,32 @@ function tagsError(p) {
1710
1722
  function linkHash(envelope, prev) {
1711
1723
  return sha256Hex(Buffer.concat([canonicalize(envelope), Buffer.from(prev, "utf-8")]));
1712
1724
  }
1725
+ function reviewRuleSupported(rule) {
1726
+ if (rule === "any_one_approves" || rule === "all_approve") return true;
1727
+ if (rule.startsWith("quorum:")) {
1728
+ const arg = rule.slice("quorum:".length);
1729
+ return /^[0-9]+$/.test(arg) && parseInt(arg, 10) >= 1;
1730
+ }
1731
+ return false;
1732
+ }
1733
+ function reviewSatisfied(review) {
1734
+ const approvers = new Set(review.decisions.filter((d) => d.kind === "approve").map((d) => d.reviewer));
1735
+ const rule = review.rule || "any_one_approves";
1736
+ if (rule === "all_approve") {
1737
+ const named = review.requested_to.filter((r) => !(r.startsWith("workspace:") || r.startsWith("group:")));
1738
+ if (named.length === 0) return approvers.size >= 1;
1739
+ return named.every((r) => approvers.has(r));
1740
+ }
1741
+ if (rule.startsWith("quorum:")) return approvers.size >= parseInt(rule.slice("quorum:".length), 10);
1742
+ return approvers.size >= 1;
1743
+ }
1713
1744
  function reply(env, body) {
1714
1745
  const out = { jsonrpc: "2.0", id: env.id };
1715
1746
  if (body.error !== void 0) out.error = body.error;
1716
1747
  else out.result = body.result;
1717
1748
  return out;
1718
1749
  }
1750
+ var REVIEW_REQUEST_STOPPED = ["cancelled", "superseded", "paused"];
1719
1751
  var Coordinator = class {
1720
1752
  workspaces = /* @__PURE__ */ new Map();
1721
1753
  options;
@@ -1935,6 +1967,18 @@ var Coordinator = class {
1935
1967
  if (!isValidEnvelope(envelope) || !envelope.method) {
1936
1968
  return reply(envelope, { error: rpcError(E.REQUEST, "Invalid JSON-RPC 2.0 request") });
1937
1969
  }
1970
+ const maxBytes = this.options.maxEnvelopeBytes ?? DEFAULT_MAX_ENVELOPE_BYTES;
1971
+ let size = 0;
1972
+ try {
1973
+ size = canonicalize(envelope).length;
1974
+ } catch {
1975
+ }
1976
+ if (size > maxBytes) {
1977
+ return reply(envelope, { error: rpcError(
1978
+ E.REQUEST,
1979
+ `Envelope exceeds max_envelope_bytes (${size} > ${maxBytes})`
1980
+ ) });
1981
+ }
1938
1982
  const method = envelope.method;
1939
1983
  const rawParams = envelope.params;
1940
1984
  if (rawParams !== void 0 && rawParams !== null && (typeof rawParams !== "object" || Array.isArray(rawParams))) {
@@ -1959,7 +2003,7 @@ var Coordinator = class {
1959
2003
  const stale = this.checkStepUp(params);
1960
2004
  if (stale) return reply(envelope, { error: stale });
1961
2005
  }
1962
- if (method.startsWith("control.") || method.startsWith("deliberate.") || method.startsWith("handoff.")) {
2006
+ if (method.startsWith("control.") || method.startsWith("deliberate.") || method.startsWith("handoff.") || method.startsWith("whisper.")) {
1963
2007
  const wsId2 = params.workspace;
1964
2008
  const ws = typeof wsId2 === "string" ? this.workspaces.get(wsId2) : void 0;
1965
2009
  if (ws) {
@@ -2068,6 +2112,9 @@ var Coordinator = class {
2068
2112
  if (member.keys.some((k) => k.kid === kid && k.revoked_at !== void 0 && now >= k.revoked_at)) {
2069
2113
  return rpcError(E.SIG_KEY_REVOKED, `Key ${kid} is revoked`);
2070
2114
  }
2115
+ if (member.keys.some((k) => k.kid === kid && k.valid_until !== void 0 && now >= k.valid_until)) {
2116
+ return rpcError(E.SIG_KEY_NOT_FOUND, `Key ${kid} is no longer valid`);
2117
+ }
2071
2118
  const key = this.lookupKey(member, kid, ts);
2072
2119
  if (!key) {
2073
2120
  const revoked = member.keys.find((k) => k.kid === kid && k.revoked_at);
@@ -2083,9 +2130,8 @@ var Coordinator = class {
2083
2130
  return rpcError(E.SIG_VERIFY_FAILED, "Signature failed verification");
2084
2131
  }
2085
2132
  return null;
2086
- } catch (e) {
2087
- const msg = e instanceof Error ? e.message : String(e);
2088
- return rpcError(E.INTERNAL, `Signature check error: ${msg}`);
2133
+ } catch {
2134
+ return rpcError(E.SIG_VERIFY_FAILED, "Signature failed verification");
2089
2135
  }
2090
2136
  }
2091
2137
  /** Lookup the key valid for a given participant at a given time. */
@@ -2209,6 +2255,7 @@ var Coordinator = class {
2209
2255
  state: ws.state,
2210
2256
  mode: ws.mode,
2211
2257
  mode_ceiling: ws.mode_ceiling,
2258
+ max_envelope_bytes: this.options.maxEnvelopeBytes ?? DEFAULT_MAX_ENVELOPE_BYTES,
2212
2259
  step_up_window_sec: ws.step_up_window_sec,
2213
2260
  profiles: ws.profiles,
2214
2261
  members: Array.from(ws.members.values()).map(memberToDict),
@@ -2349,10 +2396,14 @@ var Coordinator = class {
2349
2396
  history: [{ ts: now, from: p.from, state: "created" }],
2350
2397
  paused: false
2351
2398
  };
2352
- if (task.mode === "trial") task.review_required = true;
2399
+ if (ws.profiles.some((pr) => pr.startsWith("modes/")) && task.mode === "trial") task.review_required = true;
2353
2400
  else if ("review_required" in p) task.review_required = !!p.review_required;
2354
2401
  ws.tasks.set(taskId, task);
2355
- if (typeof p.idempotency_key === "string") ws.idempotency_keys[p.idempotency_key] = taskId;
2402
+ if (typeof p.idempotency_key === "string") {
2403
+ ws.idempotency_keys[p.idempotency_key] = taskId;
2404
+ const keys = Object.keys(ws.idempotency_keys);
2405
+ if (keys.length > MAX_IDEMPOTENCY_KEYS) delete ws.idempotency_keys[keys[0]];
2406
+ }
2356
2407
  return { result: { task_id: taskId, state: "created" } };
2357
2408
  }
2358
2409
  opTaskUpdate(p) {
@@ -2366,12 +2417,18 @@ var Coordinator = class {
2366
2417
  const legal = {
2367
2418
  created: ["in_progress", "declined", "paused"],
2368
2419
  in_progress: ["in_progress", "completed", "declined", "review_requested", "paused"],
2369
- review_requested: ["in_progress", "completed", "declined"],
2420
+ review_requested: ["in_progress"],
2370
2421
  paused: ["in_progress", "cancelled"]
2371
2422
  };
2372
2423
  if (!legal[task.state]?.includes(newState)) {
2373
2424
  return { error: rpcError(E.PARAMS, `Illegal transition ${task.state} -> ${newState}`) };
2374
2425
  }
2426
+ if (newState === "completed" && task.review_required) {
2427
+ return { error: rpcError(
2428
+ E.PARAMS,
2429
+ "This task requires review. Submit the output with task.complete, which opens the review; a reviewer decision completes it."
2430
+ ) };
2431
+ }
2375
2432
  task.state = newState;
2376
2433
  task.updated_at = this.now();
2377
2434
  task.history.push({
@@ -2392,6 +2449,30 @@ var Coordinator = class {
2392
2449
  if (task.state !== "created" && task.state !== "in_progress") {
2393
2450
  return { error: rpcError(E.PARAMS, `Cannot complete task in state: ${task.state}`) };
2394
2451
  }
2452
+ if (task.review_required) {
2453
+ const now = this.now();
2454
+ task.pending_artefact = p.output;
2455
+ if (!task.review) {
2456
+ const producer = p.from;
2457
+ const eligible = [...ws.members.entries()].filter(([uri, member]) => uri !== producer && uri !== task.assignee && member.type === "human").map(([uri]) => uri);
2458
+ if (eligible.length === 0) {
2459
+ return { error: rpcError(
2460
+ E.NOT_AUTHORISED,
2461
+ "Task requires review but has no eligible human reviewer (needs a human member other than its assignee and completer)"
2462
+ ) };
2463
+ }
2464
+ task.review = { requested_at: now, requested_to: eligible, rule: "any_one_approves", decisions: [] };
2465
+ }
2466
+ task.state = "review_requested";
2467
+ task.updated_at = now;
2468
+ task.history.push({
2469
+ ts: now,
2470
+ from: p.from,
2471
+ state: "review_requested",
2472
+ note: "review required; opened on task.complete"
2473
+ });
2474
+ return { result: { state: "review_requested", review_id: task.id } };
2475
+ }
2395
2476
  task.output = p.output;
2396
2477
  if (typeof p.confidence === "number" || typeof p.confidence === "string") task.confidence = p.confidence;
2397
2478
  task.state = "completed";
@@ -2438,11 +2519,18 @@ var Coordinator = class {
2438
2519
  if (notMember) return notMember;
2439
2520
  const task = ws.tasks.get(p.task_id);
2440
2521
  if (!task) return { error: rpcError(E.PARAMS, "Unknown task") };
2522
+ if (REVIEW_REQUEST_STOPPED.includes(task.state)) {
2523
+ return { error: rpcError(E.NOT_REVIEWABLE, `Cannot request review for task in state: ${task.state}`) };
2524
+ }
2441
2525
  const to = p.to;
2442
2526
  const reviewers = Array.isArray(to) ? to : typeof to === "string" ? [to] : [];
2443
2527
  if (!reviewers.length) return { error: rpcError(E.PARAMS, "review.request needs 'to'") };
2444
2528
  const now = this.now();
2445
- const rule = p.rule || "any_one_approves";
2529
+ const declaredRule = p.rule;
2530
+ const rule = declaredRule || "any_one_approves";
2531
+ if (!reviewRuleSupported(rule)) {
2532
+ return { error: rpcError(E.PARAMS, `Unsupported review rule ${JSON.stringify(rule)}: review/1.0 supports any_one_approves, all_approve, quorum:<n>`) };
2533
+ }
2446
2534
  if (task.state === "review_requested" && task.review) {
2447
2535
  if (contentHash(task.pending_artefact) !== contentHash(p.artefact)) {
2448
2536
  return { error: rpcError(
@@ -2450,7 +2538,7 @@ var Coordinator = class {
2450
2538
  "A review is already open on this task with different content. Decide, abstain, escalate or cancel it before requesting review of a new artefact."
2451
2539
  ) };
2452
2540
  }
2453
- if (rule !== task.review.rule) {
2541
+ if (declaredRule !== void 0 && declaredRule !== task.review.rule) {
2454
2542
  return { error: rpcError(
2455
2543
  E.REVIEW_ALREADY_OPEN,
2456
2544
  `Cannot change the decision rule of an open review (currently ${task.review.rule})`
@@ -2539,8 +2627,10 @@ var Coordinator = class {
2539
2627
  tags: p.tags
2540
2628
  });
2541
2629
  if (kind === "approve") {
2542
- task.output = task.pending_artefact;
2543
- task.state = "completed";
2630
+ if (reviewSatisfied(task.review)) {
2631
+ task.output = task.pending_artefact;
2632
+ task.state = "completed";
2633
+ }
2544
2634
  } else {
2545
2635
  task.state = p.request_revision ? "in_progress" : "declined";
2546
2636
  }