@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/{api-Cvkhcalw.d.ts → api-BOGEBNmX.d.ts} +2 -0
- package/dist/{api-CcT_8H5j.d.cts → api-DiiL0sil.d.cts} +2 -0
- package/dist/api.d.cts +1 -1
- package/dist/api.d.ts +1 -1
- package/dist/index.cjs +99 -20
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +99 -20
- package/dist/index.js.map +1 -1
- package/dist/patch.cjs +8 -7
- package/dist/patch.cjs.map +1 -1
- package/dist/patch.js +8 -7
- package/dist/patch.js.map +1 -1
- package/package.json +1 -1
|
@@ -75,6 +75,8 @@ interface CoordinatorOptions {
|
|
|
75
75
|
requireSignatures?: boolean;
|
|
76
76
|
enforceStepUp?: boolean;
|
|
77
77
|
requireReadMembership?: boolean;
|
|
78
|
+
/** Largest envelope accepted (default 1 MiB), published in the descriptor. */
|
|
79
|
+
maxEnvelopeBytes?: number;
|
|
78
80
|
onAudit?: AuditListener;
|
|
79
81
|
onAutoEscalate?: (task: Task, to: ParticipantUri) => void;
|
|
80
82
|
verifyOidcToken?: TokenVerifier;
|
|
@@ -75,6 +75,8 @@ interface CoordinatorOptions {
|
|
|
75
75
|
requireSignatures?: boolean;
|
|
76
76
|
enforceStepUp?: boolean;
|
|
77
77
|
requireReadMembership?: boolean;
|
|
78
|
+
/** Largest envelope accepted (default 1 MiB), published in the descriptor. */
|
|
79
|
+
maxEnvelopeBytes?: number;
|
|
78
80
|
onAudit?: AuditListener;
|
|
79
81
|
onAutoEscalate?: (task: Task, to: ParticipantUri) => void;
|
|
80
82
|
verifyOidcToken?: TokenVerifier;
|
package/dist/api.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import './methods.cjs';
|
|
2
|
-
export { b as CoordinatorApi, c as CoordinatorError, g as call, m as makeApi } from './api-
|
|
2
|
+
export { b as CoordinatorApi, c as CoordinatorError, g as call, m as makeApi } from './api-DiiL0sil.cjs';
|
|
3
3
|
import './types.cjs';
|
|
4
4
|
import './storage/store.cjs';
|
package/dist/api.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import './methods.js';
|
|
2
|
-
export { b as CoordinatorApi, c as CoordinatorError, g as call, m as makeApi } from './api-
|
|
2
|
+
export { b as CoordinatorApi, c as CoordinatorError, g as call, m as makeApi } from './api-BOGEBNmX.js';
|
|
3
3
|
import './types.js';
|
|
4
4
|
import './storage/store.js';
|
package/dist/index.cjs
CHANGED
|
@@ -323,6 +323,11 @@ function isValidEnvelope(env) {
|
|
|
323
323
|
var PatchError = class extends Error {
|
|
324
324
|
};
|
|
325
325
|
var DANGEROUS_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
326
|
+
var ARRAY_INDEX_RE = /^(0|[1-9][0-9]*)$/;
|
|
327
|
+
function arrayIndex(seg) {
|
|
328
|
+
if (!ARRAY_INDEX_RE.test(seg)) throw new PatchError(`Array index expected at ${JSON.stringify(seg)}`);
|
|
329
|
+
return Number(seg);
|
|
330
|
+
}
|
|
326
331
|
function unescape(token) {
|
|
327
332
|
return token.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
328
333
|
}
|
|
@@ -347,8 +352,8 @@ function navigate(doc, parts) {
|
|
|
347
352
|
for (let i = 0; i < parts.length - 1; i++) {
|
|
348
353
|
const part = parts[i];
|
|
349
354
|
if (Array.isArray(parent)) {
|
|
350
|
-
const idx =
|
|
351
|
-
if (
|
|
355
|
+
const idx = arrayIndex(part);
|
|
356
|
+
if (idx >= parent.length) {
|
|
352
357
|
throw new PatchError(`Index out of range at /${parts.slice(0, i + 1).join("/")}`);
|
|
353
358
|
}
|
|
354
359
|
parent = parent[idx];
|
|
@@ -364,11 +369,7 @@ function navigate(doc, parts) {
|
|
|
364
369
|
const last = parts[parts.length - 1];
|
|
365
370
|
if (Array.isArray(parent)) {
|
|
366
371
|
if (last === "-") return [parent, "-"];
|
|
367
|
-
|
|
368
|
-
if (!Number.isInteger(idx)) {
|
|
369
|
-
throw new PatchError(`Array index expected at ${JSON.stringify(last)}`);
|
|
370
|
-
}
|
|
371
|
-
return [parent, idx];
|
|
372
|
+
return [parent, arrayIndex(last)];
|
|
372
373
|
}
|
|
373
374
|
return [parent, last];
|
|
374
375
|
}
|
|
@@ -819,7 +820,7 @@ function registerControl(coord) {
|
|
|
819
820
|
if (scope === "task") {
|
|
820
821
|
const task = ws.tasks.get(p.task_id);
|
|
821
822
|
if (!task) return { error: rpcError(E.PARAMS, "Unknown task") };
|
|
822
|
-
if (task.state === "completed" || task.state === "declined" || task.state === "cancelled") {
|
|
823
|
+
if (task.state === "completed" || task.state === "declined" || task.state === "cancelled" || task.state === "superseded") {
|
|
823
824
|
return { error: rpcError(E.CONTROL_NOT_AUTHORISED, `Cannot pause ${task.state} task`) };
|
|
824
825
|
}
|
|
825
826
|
task.paused = true;
|
|
@@ -876,7 +877,7 @@ function registerControl(coord) {
|
|
|
876
877
|
if (!ws) return { error: rpcError(E.PARAMS, "Unknown workspace") };
|
|
877
878
|
const task = ws.tasks.get(p.task_id);
|
|
878
879
|
if (!task) return { error: rpcError(E.PARAMS, "Unknown task") };
|
|
879
|
-
if (task.state === "completed" || task.state === "declined" || task.state === "cancelled") {
|
|
880
|
+
if (task.state === "completed" || task.state === "declined" || task.state === "cancelled" || task.state === "superseded") {
|
|
880
881
|
return { error: rpcError(E.CONTROL_NOT_AUTHORISED, `Cannot cancel ${task.state} task`) };
|
|
881
882
|
}
|
|
882
883
|
task.state = "cancelled";
|
|
@@ -898,7 +899,7 @@ function registerControl(coord) {
|
|
|
898
899
|
}));
|
|
899
900
|
}
|
|
900
901
|
if (include.includes("open_tasks")) {
|
|
901
|
-
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 }));
|
|
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 }));
|
|
902
903
|
}
|
|
903
904
|
if (include.includes("mode_ceiling")) state.mode_ceiling = ws.mode_ceiling;
|
|
904
905
|
if (include.includes("policy")) state.routing_policy_uri = ws.routing_policy_uri;
|
|
@@ -966,6 +967,13 @@ function registerControl(coord) {
|
|
|
966
967
|
if (!ws.members.has(assignee)) {
|
|
967
968
|
return { error: rpcError(E.PARAMS, "successor assignee not in workspace") };
|
|
968
969
|
}
|
|
970
|
+
const requestedMode = newSpec.mode ?? old.mode;
|
|
971
|
+
if (!modeLE(requestedMode, ws.mode_ceiling)) {
|
|
972
|
+
return { error: rpcError(
|
|
973
|
+
E.MODE_CEILING_EXCEEDED,
|
|
974
|
+
`Requested mode ${requestedMode} exceeds ceiling ${ws.mode_ceiling}`
|
|
975
|
+
) };
|
|
976
|
+
}
|
|
969
977
|
const now = coord.now();
|
|
970
978
|
const newId = coord.ids.taskId();
|
|
971
979
|
const newTask = {
|
|
@@ -977,7 +985,7 @@ function registerControl(coord) {
|
|
|
977
985
|
input: newSpec.input ?? {},
|
|
978
986
|
created_at: now,
|
|
979
987
|
updated_at: now,
|
|
980
|
-
mode:
|
|
988
|
+
mode: requestedMode,
|
|
981
989
|
supersedes: old.id,
|
|
982
990
|
history: [{
|
|
983
991
|
ts: now,
|
|
@@ -987,6 +995,8 @@ function registerControl(coord) {
|
|
|
987
995
|
}],
|
|
988
996
|
paused: false
|
|
989
997
|
};
|
|
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;
|
|
990
1000
|
ws.tasks.set(newId, newTask);
|
|
991
1001
|
old.state = "superseded";
|
|
992
1002
|
old.superseded_by = newId;
|
|
@@ -1740,6 +1750,8 @@ var READ_ONLY_METHODS = /* @__PURE__ */ new Set([
|
|
|
1740
1750
|
"audit.verify_chain",
|
|
1741
1751
|
"audit.verify_receipt"
|
|
1742
1752
|
]);
|
|
1753
|
+
var MAX_IDEMPOTENCY_KEYS = 1e4;
|
|
1754
|
+
var DEFAULT_MAX_ENVELOPE_BYTES = 1048576;
|
|
1743
1755
|
function nowIso(clockMs) {
|
|
1744
1756
|
const d = clockMs === void 0 ? /* @__PURE__ */ new Date() : new Date(clockMs);
|
|
1745
1757
|
return d.toISOString().replace(/\.(\d{3})Z$/, ".$1Z");
|
|
@@ -1758,6 +1770,25 @@ function tagsError(p) {
|
|
|
1758
1770
|
function linkHash(envelope, prev) {
|
|
1759
1771
|
return sha256Hex(Buffer.concat([canonicalize(envelope), Buffer.from(prev, "utf-8")]));
|
|
1760
1772
|
}
|
|
1773
|
+
function reviewRuleSupported(rule) {
|
|
1774
|
+
if (rule === "any_one_approves" || rule === "all_approve") return true;
|
|
1775
|
+
if (rule.startsWith("quorum:")) {
|
|
1776
|
+
const arg = rule.slice("quorum:".length);
|
|
1777
|
+
return /^[0-9]+$/.test(arg) && parseInt(arg, 10) >= 1;
|
|
1778
|
+
}
|
|
1779
|
+
return false;
|
|
1780
|
+
}
|
|
1781
|
+
function reviewSatisfied(review) {
|
|
1782
|
+
const approvers = new Set(review.decisions.filter((d) => d.kind === "approve").map((d) => d.reviewer));
|
|
1783
|
+
const rule = review.rule || "any_one_approves";
|
|
1784
|
+
if (rule === "all_approve") {
|
|
1785
|
+
const named = review.requested_to.filter((r) => !(r.startsWith("workspace:") || r.startsWith("group:")));
|
|
1786
|
+
if (named.length === 0) return approvers.size >= 1;
|
|
1787
|
+
return named.every((r) => approvers.has(r));
|
|
1788
|
+
}
|
|
1789
|
+
if (rule.startsWith("quorum:")) return approvers.size >= parseInt(rule.slice("quorum:".length), 10);
|
|
1790
|
+
return approvers.size >= 1;
|
|
1791
|
+
}
|
|
1761
1792
|
function reply(env, body) {
|
|
1762
1793
|
const out = { jsonrpc: "2.0", id: env.id };
|
|
1763
1794
|
if (body.error !== void 0) out.error = body.error;
|
|
@@ -1983,6 +2014,18 @@ var Coordinator = class {
|
|
|
1983
2014
|
if (!isValidEnvelope(envelope) || !envelope.method) {
|
|
1984
2015
|
return reply(envelope, { error: rpcError(E.REQUEST, "Invalid JSON-RPC 2.0 request") });
|
|
1985
2016
|
}
|
|
2017
|
+
const maxBytes = this.options.maxEnvelopeBytes ?? DEFAULT_MAX_ENVELOPE_BYTES;
|
|
2018
|
+
let size = 0;
|
|
2019
|
+
try {
|
|
2020
|
+
size = canonicalize(envelope).length;
|
|
2021
|
+
} catch {
|
|
2022
|
+
}
|
|
2023
|
+
if (size > maxBytes) {
|
|
2024
|
+
return reply(envelope, { error: rpcError(
|
|
2025
|
+
E.REQUEST,
|
|
2026
|
+
`Envelope exceeds max_envelope_bytes (${size} > ${maxBytes})`
|
|
2027
|
+
) });
|
|
2028
|
+
}
|
|
1986
2029
|
const method = envelope.method;
|
|
1987
2030
|
const rawParams = envelope.params;
|
|
1988
2031
|
if (rawParams !== void 0 && rawParams !== null && (typeof rawParams !== "object" || Array.isArray(rawParams))) {
|
|
@@ -2007,7 +2050,7 @@ var Coordinator = class {
|
|
|
2007
2050
|
const stale = this.checkStepUp(params);
|
|
2008
2051
|
if (stale) return reply(envelope, { error: stale });
|
|
2009
2052
|
}
|
|
2010
|
-
if (method.startsWith("control.") || method.startsWith("deliberate.") || method.startsWith("handoff.")) {
|
|
2053
|
+
if (method.startsWith("control.") || method.startsWith("deliberate.") || method.startsWith("handoff.") || method.startsWith("whisper.")) {
|
|
2011
2054
|
const wsId2 = params.workspace;
|
|
2012
2055
|
const ws = typeof wsId2 === "string" ? this.workspaces.get(wsId2) : void 0;
|
|
2013
2056
|
if (ws) {
|
|
@@ -2116,6 +2159,9 @@ var Coordinator = class {
|
|
|
2116
2159
|
if (member.keys.some((k) => k.kid === kid && k.revoked_at !== void 0 && now >= k.revoked_at)) {
|
|
2117
2160
|
return rpcError(E.SIG_KEY_REVOKED, `Key ${kid} is revoked`);
|
|
2118
2161
|
}
|
|
2162
|
+
if (member.keys.some((k) => k.kid === kid && k.valid_until !== void 0 && now >= k.valid_until)) {
|
|
2163
|
+
return rpcError(E.SIG_KEY_NOT_FOUND, `Key ${kid} is no longer valid`);
|
|
2164
|
+
}
|
|
2119
2165
|
const key = this.lookupKey(member, kid, ts);
|
|
2120
2166
|
if (!key) {
|
|
2121
2167
|
const revoked = member.keys.find((k) => k.kid === kid && k.revoked_at);
|
|
@@ -2131,9 +2177,8 @@ var Coordinator = class {
|
|
|
2131
2177
|
return rpcError(E.SIG_VERIFY_FAILED, "Signature failed verification");
|
|
2132
2178
|
}
|
|
2133
2179
|
return null;
|
|
2134
|
-
} catch
|
|
2135
|
-
|
|
2136
|
-
return rpcError(E.INTERNAL, `Signature check error: ${msg}`);
|
|
2180
|
+
} catch {
|
|
2181
|
+
return rpcError(E.SIG_VERIFY_FAILED, "Signature failed verification");
|
|
2137
2182
|
}
|
|
2138
2183
|
}
|
|
2139
2184
|
/** Lookup the key valid for a given participant at a given time. */
|
|
@@ -2257,6 +2302,7 @@ var Coordinator = class {
|
|
|
2257
2302
|
state: ws.state,
|
|
2258
2303
|
mode: ws.mode,
|
|
2259
2304
|
mode_ceiling: ws.mode_ceiling,
|
|
2305
|
+
max_envelope_bytes: this.options.maxEnvelopeBytes ?? DEFAULT_MAX_ENVELOPE_BYTES,
|
|
2260
2306
|
step_up_window_sec: ws.step_up_window_sec,
|
|
2261
2307
|
profiles: ws.profiles,
|
|
2262
2308
|
members: Array.from(ws.members.values()).map(memberToDict),
|
|
@@ -2397,10 +2443,14 @@ var Coordinator = class {
|
|
|
2397
2443
|
history: [{ ts: now, from: p.from, state: "created" }],
|
|
2398
2444
|
paused: false
|
|
2399
2445
|
};
|
|
2400
|
-
if (task.mode === "trial") task.review_required = true;
|
|
2446
|
+
if (ws.profiles.some((pr) => pr.startsWith("modes/")) && task.mode === "trial") task.review_required = true;
|
|
2401
2447
|
else if ("review_required" in p) task.review_required = !!p.review_required;
|
|
2402
2448
|
ws.tasks.set(taskId, task);
|
|
2403
|
-
if (typeof p.idempotency_key === "string")
|
|
2449
|
+
if (typeof p.idempotency_key === "string") {
|
|
2450
|
+
ws.idempotency_keys[p.idempotency_key] = taskId;
|
|
2451
|
+
const keys = Object.keys(ws.idempotency_keys);
|
|
2452
|
+
if (keys.length > MAX_IDEMPOTENCY_KEYS) delete ws.idempotency_keys[keys[0]];
|
|
2453
|
+
}
|
|
2404
2454
|
return { result: { task_id: taskId, state: "created" } };
|
|
2405
2455
|
}
|
|
2406
2456
|
opTaskUpdate(p) {
|
|
@@ -2414,7 +2464,7 @@ var Coordinator = class {
|
|
|
2414
2464
|
const legal = {
|
|
2415
2465
|
created: ["in_progress", "declined", "paused"],
|
|
2416
2466
|
in_progress: ["in_progress", "completed", "declined", "review_requested", "paused"],
|
|
2417
|
-
review_requested: ["in_progress"
|
|
2467
|
+
review_requested: ["in_progress"],
|
|
2418
2468
|
paused: ["in_progress", "cancelled"]
|
|
2419
2469
|
};
|
|
2420
2470
|
if (!legal[task.state]?.includes(newState)) {
|
|
@@ -2440,6 +2490,30 @@ var Coordinator = class {
|
|
|
2440
2490
|
if (task.state !== "created" && task.state !== "in_progress") {
|
|
2441
2491
|
return { error: rpcError(E.PARAMS, `Cannot complete task in state: ${task.state}`) };
|
|
2442
2492
|
}
|
|
2493
|
+
if (task.review_required) {
|
|
2494
|
+
const now = this.now();
|
|
2495
|
+
task.pending_artefact = p.output;
|
|
2496
|
+
if (!task.review) {
|
|
2497
|
+
const producer = p.from;
|
|
2498
|
+
const eligible = [...ws.members.keys()].filter((uri) => uri !== producer && uri !== task.assignee);
|
|
2499
|
+
if (eligible.length === 0) {
|
|
2500
|
+
return { error: rpcError(
|
|
2501
|
+
E.NOT_AUTHORISED,
|
|
2502
|
+
"Task requires review but has no eligible reviewer (needs a member other than its assignee and completer)"
|
|
2503
|
+
) };
|
|
2504
|
+
}
|
|
2505
|
+
task.review = { requested_at: now, requested_to: eligible, rule: "any_one_approves", decisions: [] };
|
|
2506
|
+
}
|
|
2507
|
+
task.state = "review_requested";
|
|
2508
|
+
task.updated_at = now;
|
|
2509
|
+
task.history.push({
|
|
2510
|
+
ts: now,
|
|
2511
|
+
from: p.from,
|
|
2512
|
+
state: "review_requested",
|
|
2513
|
+
note: "review required; opened on task.complete"
|
|
2514
|
+
});
|
|
2515
|
+
return { result: { state: "review_requested", review_id: task.id } };
|
|
2516
|
+
}
|
|
2443
2517
|
task.output = p.output;
|
|
2444
2518
|
if (typeof p.confidence === "number" || typeof p.confidence === "string") task.confidence = p.confidence;
|
|
2445
2519
|
task.state = "completed";
|
|
@@ -2491,6 +2565,9 @@ var Coordinator = class {
|
|
|
2491
2565
|
if (!reviewers.length) return { error: rpcError(E.PARAMS, "review.request needs 'to'") };
|
|
2492
2566
|
const now = this.now();
|
|
2493
2567
|
const rule = p.rule || "any_one_approves";
|
|
2568
|
+
if (!reviewRuleSupported(rule)) {
|
|
2569
|
+
return { error: rpcError(E.PARAMS, `Unsupported review rule ${JSON.stringify(rule)}: review/1.0 supports any_one_approves, all_approve, quorum:<n>`) };
|
|
2570
|
+
}
|
|
2494
2571
|
if (task.state === "review_requested" && task.review) {
|
|
2495
2572
|
if (contentHash(task.pending_artefact) !== contentHash(p.artefact)) {
|
|
2496
2573
|
return { error: rpcError(
|
|
@@ -2587,8 +2664,10 @@ var Coordinator = class {
|
|
|
2587
2664
|
tags: p.tags
|
|
2588
2665
|
});
|
|
2589
2666
|
if (kind === "approve") {
|
|
2590
|
-
task.
|
|
2591
|
-
|
|
2667
|
+
if (reviewSatisfied(task.review)) {
|
|
2668
|
+
task.output = task.pending_artefact;
|
|
2669
|
+
task.state = "completed";
|
|
2670
|
+
}
|
|
2592
2671
|
} else {
|
|
2593
2672
|
task.state = p.request_revision ? "in_progress" : "declined";
|
|
2594
2673
|
}
|