@brightbeamai/chap-coordinator 0.2.10 → 0.2.12

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,6 +1722,25 @@ 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;
@@ -1935,6 +1966,18 @@ var Coordinator = class {
1935
1966
  if (!isValidEnvelope(envelope) || !envelope.method) {
1936
1967
  return reply(envelope, { error: rpcError(E.REQUEST, "Invalid JSON-RPC 2.0 request") });
1937
1968
  }
1969
+ const maxBytes = this.options.maxEnvelopeBytes ?? DEFAULT_MAX_ENVELOPE_BYTES;
1970
+ let size = 0;
1971
+ try {
1972
+ size = canonicalize(envelope).length;
1973
+ } catch {
1974
+ }
1975
+ if (size > maxBytes) {
1976
+ return reply(envelope, { error: rpcError(
1977
+ E.REQUEST,
1978
+ `Envelope exceeds max_envelope_bytes (${size} > ${maxBytes})`
1979
+ ) });
1980
+ }
1938
1981
  const method = envelope.method;
1939
1982
  const rawParams = envelope.params;
1940
1983
  if (rawParams !== void 0 && rawParams !== null && (typeof rawParams !== "object" || Array.isArray(rawParams))) {
@@ -1959,7 +2002,7 @@ var Coordinator = class {
1959
2002
  const stale = this.checkStepUp(params);
1960
2003
  if (stale) return reply(envelope, { error: stale });
1961
2004
  }
1962
- if (method.startsWith("control.") || method.startsWith("deliberate.") || method.startsWith("handoff.")) {
2005
+ if (method.startsWith("control.") || method.startsWith("deliberate.") || method.startsWith("handoff.") || method.startsWith("whisper.")) {
1963
2006
  const wsId2 = params.workspace;
1964
2007
  const ws = typeof wsId2 === "string" ? this.workspaces.get(wsId2) : void 0;
1965
2008
  if (ws) {
@@ -2068,6 +2111,9 @@ var Coordinator = class {
2068
2111
  if (member.keys.some((k) => k.kid === kid && k.revoked_at !== void 0 && now >= k.revoked_at)) {
2069
2112
  return rpcError(E.SIG_KEY_REVOKED, `Key ${kid} is revoked`);
2070
2113
  }
2114
+ if (member.keys.some((k) => k.kid === kid && k.valid_until !== void 0 && now >= k.valid_until)) {
2115
+ return rpcError(E.SIG_KEY_NOT_FOUND, `Key ${kid} is no longer valid`);
2116
+ }
2071
2117
  const key = this.lookupKey(member, kid, ts);
2072
2118
  if (!key) {
2073
2119
  const revoked = member.keys.find((k) => k.kid === kid && k.revoked_at);
@@ -2083,9 +2129,8 @@ var Coordinator = class {
2083
2129
  return rpcError(E.SIG_VERIFY_FAILED, "Signature failed verification");
2084
2130
  }
2085
2131
  return null;
2086
- } catch (e) {
2087
- const msg = e instanceof Error ? e.message : String(e);
2088
- return rpcError(E.INTERNAL, `Signature check error: ${msg}`);
2132
+ } catch {
2133
+ return rpcError(E.SIG_VERIFY_FAILED, "Signature failed verification");
2089
2134
  }
2090
2135
  }
2091
2136
  /** Lookup the key valid for a given participant at a given time. */
@@ -2209,6 +2254,7 @@ var Coordinator = class {
2209
2254
  state: ws.state,
2210
2255
  mode: ws.mode,
2211
2256
  mode_ceiling: ws.mode_ceiling,
2257
+ max_envelope_bytes: this.options.maxEnvelopeBytes ?? DEFAULT_MAX_ENVELOPE_BYTES,
2212
2258
  step_up_window_sec: ws.step_up_window_sec,
2213
2259
  profiles: ws.profiles,
2214
2260
  members: Array.from(ws.members.values()).map(memberToDict),
@@ -2349,10 +2395,14 @@ var Coordinator = class {
2349
2395
  history: [{ ts: now, from: p.from, state: "created" }],
2350
2396
  paused: false
2351
2397
  };
2352
- if (task.mode === "trial") task.review_required = true;
2398
+ if (ws.profiles.some((pr) => pr.startsWith("modes/")) && task.mode === "trial") task.review_required = true;
2353
2399
  else if ("review_required" in p) task.review_required = !!p.review_required;
2354
2400
  ws.tasks.set(taskId, task);
2355
- if (typeof p.idempotency_key === "string") ws.idempotency_keys[p.idempotency_key] = taskId;
2401
+ if (typeof p.idempotency_key === "string") {
2402
+ ws.idempotency_keys[p.idempotency_key] = taskId;
2403
+ const keys = Object.keys(ws.idempotency_keys);
2404
+ if (keys.length > MAX_IDEMPOTENCY_KEYS) delete ws.idempotency_keys[keys[0]];
2405
+ }
2356
2406
  return { result: { task_id: taskId, state: "created" } };
2357
2407
  }
2358
2408
  opTaskUpdate(p) {
@@ -2366,7 +2416,7 @@ var Coordinator = class {
2366
2416
  const legal = {
2367
2417
  created: ["in_progress", "declined", "paused"],
2368
2418
  in_progress: ["in_progress", "completed", "declined", "review_requested", "paused"],
2369
- review_requested: ["in_progress", "completed", "declined"],
2419
+ review_requested: ["in_progress"],
2370
2420
  paused: ["in_progress", "cancelled"]
2371
2421
  };
2372
2422
  if (!legal[task.state]?.includes(newState)) {
@@ -2392,6 +2442,30 @@ var Coordinator = class {
2392
2442
  if (task.state !== "created" && task.state !== "in_progress") {
2393
2443
  return { error: rpcError(E.PARAMS, `Cannot complete task in state: ${task.state}`) };
2394
2444
  }
2445
+ if (task.review_required) {
2446
+ const now = this.now();
2447
+ task.pending_artefact = p.output;
2448
+ if (!task.review) {
2449
+ const producer = p.from;
2450
+ const eligible = [...ws.members.keys()].filter((uri) => uri !== producer && uri !== task.assignee);
2451
+ if (eligible.length === 0) {
2452
+ return { error: rpcError(
2453
+ E.NOT_AUTHORISED,
2454
+ "Task requires review but has no eligible reviewer (needs a member other than its assignee and completer)"
2455
+ ) };
2456
+ }
2457
+ task.review = { requested_at: now, requested_to: eligible, rule: "any_one_approves", decisions: [] };
2458
+ }
2459
+ task.state = "review_requested";
2460
+ task.updated_at = now;
2461
+ task.history.push({
2462
+ ts: now,
2463
+ from: p.from,
2464
+ state: "review_requested",
2465
+ note: "review required; opened on task.complete"
2466
+ });
2467
+ return { result: { state: "review_requested", review_id: task.id } };
2468
+ }
2395
2469
  task.output = p.output;
2396
2470
  if (typeof p.confidence === "number" || typeof p.confidence === "string") task.confidence = p.confidence;
2397
2471
  task.state = "completed";
@@ -2443,6 +2517,9 @@ var Coordinator = class {
2443
2517
  if (!reviewers.length) return { error: rpcError(E.PARAMS, "review.request needs 'to'") };
2444
2518
  const now = this.now();
2445
2519
  const rule = p.rule || "any_one_approves";
2520
+ if (!reviewRuleSupported(rule)) {
2521
+ return { error: rpcError(E.PARAMS, `Unsupported review rule ${JSON.stringify(rule)}: review/1.0 supports any_one_approves, all_approve, quorum:<n>`) };
2522
+ }
2446
2523
  if (task.state === "review_requested" && task.review) {
2447
2524
  if (contentHash(task.pending_artefact) !== contentHash(p.artefact)) {
2448
2525
  return { error: rpcError(
@@ -2539,8 +2616,10 @@ var Coordinator = class {
2539
2616
  tags: p.tags
2540
2617
  });
2541
2618
  if (kind === "approve") {
2542
- task.output = task.pending_artefact;
2543
- task.state = "completed";
2619
+ if (reviewSatisfied(task.review)) {
2620
+ task.output = task.pending_artefact;
2621
+ task.state = "completed";
2622
+ }
2544
2623
  } else {
2545
2624
  task.state = p.request_revision ? "in_progress" : "declined";
2546
2625
  }