@brightbeamai/chap-coordinator 0.2.9 → 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/README.md +6 -1
- package/dist/{api-CPEk0V6i.d.ts → api-BOGEBNmX.d.ts} +15 -0
- package/dist/{api-DE78DA0o.d.cts → api-DiiL0sil.d.cts} +15 -0
- package/dist/api.d.cts +1 -1
- package/dist/api.d.ts +1 -1
- package/dist/index.cjs +190 -23
- 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 +190 -23
- package/dist/index.js.map +1 -1
- package/dist/jsonrpc.cjs +2 -0
- package/dist/jsonrpc.cjs.map +1 -1
- package/dist/jsonrpc.d.cts +2 -0
- package/dist/jsonrpc.d.ts +2 -0
- package/dist/jsonrpc.js +2 -0
- package/dist/jsonrpc.js.map +1 -1
- package/dist/methods.cjs.map +1 -1
- package/dist/methods.d.cts +22 -2
- package/dist/methods.d.ts +22 -2
- 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/dist/storage/sqlite.cjs +5 -2
- package/dist/storage/sqlite.cjs.map +1 -1
- package/dist/storage/sqlite.js +4 -9
- package/dist/storage/sqlite.js.map +1 -1
- package/dist/storage/store.cjs.map +1 -1
- package/dist/storage/store.d.cts +26 -1
- package/dist/storage/store.d.ts +26 -1
- package/dist/storage/store.js.map +1 -1
- package/package.json +79 -27
package/dist/index.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { C as CoordinatorOptions, a as Coordinator } from './api-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
@@ -211,6 +211,7 @@ var E = {
|
|
|
211
211
|
NOT_AUTHORISED: -32011,
|
|
212
212
|
PATCH_FAILED: -32012,
|
|
213
213
|
REVIEW_LAPSED: -32013,
|
|
214
|
+
REVIEW_ALREADY_OPEN: -32014,
|
|
214
215
|
// whisper/1.0 profile (profiles/whisper.md S6)
|
|
215
216
|
WHISPER_ALREADY_ANSWERED: -32020,
|
|
216
217
|
WHISPER_LAPSED: -32021,
|
|
@@ -237,6 +238,7 @@ var E = {
|
|
|
237
238
|
SIG_KEY_NOT_FOUND: -32071,
|
|
238
239
|
SIG_KEY_REVOKED: -32072,
|
|
239
240
|
SIG_ROTATION_KEY_MISMATCH: -32073,
|
|
241
|
+
SIG_ARTEFACT_DIGEST_MISMATCH: -32074,
|
|
240
242
|
// audit-scitt/1.0 profile (profiles/audit-scitt.md S8)
|
|
241
243
|
SCITT_UNREACHABLE: -32080,
|
|
242
244
|
SCITT_STATEMENT_REJECTED: -32081,
|
|
@@ -273,6 +275,11 @@ function isValidEnvelope(env) {
|
|
|
273
275
|
var PatchError = class extends Error {
|
|
274
276
|
};
|
|
275
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
|
+
}
|
|
276
283
|
function unescape(token) {
|
|
277
284
|
return token.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
278
285
|
}
|
|
@@ -297,8 +304,8 @@ function navigate(doc, parts) {
|
|
|
297
304
|
for (let i = 0; i < parts.length - 1; i++) {
|
|
298
305
|
const part = parts[i];
|
|
299
306
|
if (Array.isArray(parent)) {
|
|
300
|
-
const idx =
|
|
301
|
-
if (
|
|
307
|
+
const idx = arrayIndex(part);
|
|
308
|
+
if (idx >= parent.length) {
|
|
302
309
|
throw new PatchError(`Index out of range at /${parts.slice(0, i + 1).join("/")}`);
|
|
303
310
|
}
|
|
304
311
|
parent = parent[idx];
|
|
@@ -314,11 +321,7 @@ function navigate(doc, parts) {
|
|
|
314
321
|
const last = parts[parts.length - 1];
|
|
315
322
|
if (Array.isArray(parent)) {
|
|
316
323
|
if (last === "-") return [parent, "-"];
|
|
317
|
-
|
|
318
|
-
if (!Number.isInteger(idx)) {
|
|
319
|
-
throw new PatchError(`Array index expected at ${JSON.stringify(last)}`);
|
|
320
|
-
}
|
|
321
|
-
return [parent, idx];
|
|
324
|
+
return [parent, arrayIndex(last)];
|
|
322
325
|
}
|
|
323
326
|
return [parent, last];
|
|
324
327
|
}
|
|
@@ -704,6 +707,12 @@ function registerAuditScitt(coord) {
|
|
|
704
707
|
if (!(ws.chain_enabled || coord.options.enableChain)) {
|
|
705
708
|
return { error: rpcError(E.PARAMS, "Chain not enabled for this workspace") };
|
|
706
709
|
}
|
|
710
|
+
if (p.from_seq !== void 0 || p.to_seq !== void 0) {
|
|
711
|
+
return { error: rpcError(
|
|
712
|
+
E.PARAMS,
|
|
713
|
+
"Range verification is not implemented; omit from_seq and to_seq to verify the whole log."
|
|
714
|
+
) };
|
|
715
|
+
}
|
|
707
716
|
let start = ws.audit.findIndex((e) => e.prev_hash != null);
|
|
708
717
|
if (start < 0) start = ws.audit.length;
|
|
709
718
|
const errors = [];
|
|
@@ -715,16 +724,35 @@ function registerAuditScitt(coord) {
|
|
|
715
724
|
}
|
|
716
725
|
prev = sha256Hex(Buffer.concat([canonicalize(e.envelope), Buffer.from(expectedPrev, "utf-8")]));
|
|
717
726
|
}
|
|
718
|
-
const storedHead = ws.chain_head
|
|
727
|
+
const storedHead = ws.chain_head === void 0 || ws.chain_head === null ? ZERO_HASH : ws.chain_head;
|
|
719
728
|
if (prev !== storedHead) {
|
|
720
729
|
errors.push("chain_head mismatch: replay does not match stored head");
|
|
721
730
|
}
|
|
722
731
|
if (errors.length) {
|
|
723
732
|
return { error: rpcError(E.PARAMS, errors.join("; ")) };
|
|
724
733
|
}
|
|
734
|
+
const entriesTotal = ws.audit.length;
|
|
735
|
+
const entriesChecked = entriesTotal - start;
|
|
736
|
+
const entriesUnchecked = start;
|
|
737
|
+
if (entriesUnchecked > 0) {
|
|
738
|
+
return { result: {
|
|
739
|
+
status: "not_evaluated",
|
|
740
|
+
ok: false,
|
|
741
|
+
reason: "unchained_prefix",
|
|
742
|
+
entries_total: entriesTotal,
|
|
743
|
+
entries_checked: entriesChecked,
|
|
744
|
+
entries_unchecked: entriesUnchecked,
|
|
745
|
+
checked_from_seq: entriesChecked > 0 ? ws.audit[start].seq : null,
|
|
746
|
+
chain_head: storedHead
|
|
747
|
+
} };
|
|
748
|
+
}
|
|
725
749
|
return { result: {
|
|
750
|
+
status: "verified",
|
|
726
751
|
ok: true,
|
|
727
|
-
|
|
752
|
+
entries_total: entriesTotal,
|
|
753
|
+
entries_checked: entriesChecked,
|
|
754
|
+
entries_unchecked: 0,
|
|
755
|
+
checked_from_seq: entriesChecked > 0 ? ws.audit[start].seq : null,
|
|
728
756
|
chain_head: storedHead
|
|
729
757
|
} };
|
|
730
758
|
});
|
|
@@ -744,7 +772,7 @@ function registerControl(coord) {
|
|
|
744
772
|
if (scope === "task") {
|
|
745
773
|
const task = ws.tasks.get(p.task_id);
|
|
746
774
|
if (!task) return { error: rpcError(E.PARAMS, "Unknown task") };
|
|
747
|
-
if (task.state === "completed" || task.state === "declined" || task.state === "cancelled") {
|
|
775
|
+
if (task.state === "completed" || task.state === "declined" || task.state === "cancelled" || task.state === "superseded") {
|
|
748
776
|
return { error: rpcError(E.CONTROL_NOT_AUTHORISED, `Cannot pause ${task.state} task`) };
|
|
749
777
|
}
|
|
750
778
|
task.paused = true;
|
|
@@ -801,7 +829,7 @@ function registerControl(coord) {
|
|
|
801
829
|
if (!ws) return { error: rpcError(E.PARAMS, "Unknown workspace") };
|
|
802
830
|
const task = ws.tasks.get(p.task_id);
|
|
803
831
|
if (!task) return { error: rpcError(E.PARAMS, "Unknown task") };
|
|
804
|
-
if (task.state === "completed" || task.state === "declined" || task.state === "cancelled") {
|
|
832
|
+
if (task.state === "completed" || task.state === "declined" || task.state === "cancelled" || task.state === "superseded") {
|
|
805
833
|
return { error: rpcError(E.CONTROL_NOT_AUTHORISED, `Cannot cancel ${task.state} task`) };
|
|
806
834
|
}
|
|
807
835
|
task.state = "cancelled";
|
|
@@ -823,7 +851,7 @@ function registerControl(coord) {
|
|
|
823
851
|
}));
|
|
824
852
|
}
|
|
825
853
|
if (include.includes("open_tasks")) {
|
|
826
|
-
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 }));
|
|
827
855
|
}
|
|
828
856
|
if (include.includes("mode_ceiling")) state.mode_ceiling = ws.mode_ceiling;
|
|
829
857
|
if (include.includes("policy")) state.routing_policy_uri = ws.routing_policy_uri;
|
|
@@ -891,6 +919,13 @@ function registerControl(coord) {
|
|
|
891
919
|
if (!ws.members.has(assignee)) {
|
|
892
920
|
return { error: rpcError(E.PARAMS, "successor assignee not in workspace") };
|
|
893
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
|
+
}
|
|
894
929
|
const now = coord.now();
|
|
895
930
|
const newId = coord.ids.taskId();
|
|
896
931
|
const newTask = {
|
|
@@ -902,7 +937,7 @@ function registerControl(coord) {
|
|
|
902
937
|
input: newSpec.input ?? {},
|
|
903
938
|
created_at: now,
|
|
904
939
|
updated_at: now,
|
|
905
|
-
mode:
|
|
940
|
+
mode: requestedMode,
|
|
906
941
|
supersedes: old.id,
|
|
907
942
|
history: [{
|
|
908
943
|
ts: now,
|
|
@@ -912,6 +947,8 @@ function registerControl(coord) {
|
|
|
912
947
|
}],
|
|
913
948
|
paused: false
|
|
914
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;
|
|
915
952
|
ws.tasks.set(newId, newTask);
|
|
916
953
|
old.state = "superseded";
|
|
917
954
|
old.superseded_by = newId;
|
|
@@ -1665,6 +1702,8 @@ var READ_ONLY_METHODS = /* @__PURE__ */ new Set([
|
|
|
1665
1702
|
"audit.verify_chain",
|
|
1666
1703
|
"audit.verify_receipt"
|
|
1667
1704
|
]);
|
|
1705
|
+
var MAX_IDEMPOTENCY_KEYS = 1e4;
|
|
1706
|
+
var DEFAULT_MAX_ENVELOPE_BYTES = 1048576;
|
|
1668
1707
|
function nowIso(clockMs) {
|
|
1669
1708
|
const d = clockMs === void 0 ? /* @__PURE__ */ new Date() : new Date(clockMs);
|
|
1670
1709
|
return d.toISOString().replace(/\.(\d{3})Z$/, ".$1Z");
|
|
@@ -1683,6 +1722,25 @@ function tagsError(p) {
|
|
|
1683
1722
|
function linkHash(envelope, prev) {
|
|
1684
1723
|
return sha256Hex(Buffer.concat([canonicalize(envelope), Buffer.from(prev, "utf-8")]));
|
|
1685
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
|
+
}
|
|
1686
1744
|
function reply(env, body) {
|
|
1687
1745
|
const out = { jsonrpc: "2.0", id: env.id };
|
|
1688
1746
|
if (body.error !== void 0) out.error = body.error;
|
|
@@ -1908,6 +1966,18 @@ var Coordinator = class {
|
|
|
1908
1966
|
if (!isValidEnvelope(envelope) || !envelope.method) {
|
|
1909
1967
|
return reply(envelope, { error: rpcError(E.REQUEST, "Invalid JSON-RPC 2.0 request") });
|
|
1910
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
|
+
}
|
|
1911
1981
|
const method = envelope.method;
|
|
1912
1982
|
const rawParams = envelope.params;
|
|
1913
1983
|
if (rawParams !== void 0 && rawParams !== null && (typeof rawParams !== "object" || Array.isArray(rawParams))) {
|
|
@@ -1932,7 +2002,7 @@ var Coordinator = class {
|
|
|
1932
2002
|
const stale = this.checkStepUp(params);
|
|
1933
2003
|
if (stale) return reply(envelope, { error: stale });
|
|
1934
2004
|
}
|
|
1935
|
-
if (method.startsWith("control.") || method.startsWith("deliberate.") || method.startsWith("handoff.")) {
|
|
2005
|
+
if (method.startsWith("control.") || method.startsWith("deliberate.") || method.startsWith("handoff.") || method.startsWith("whisper.")) {
|
|
1936
2006
|
const wsId2 = params.workspace;
|
|
1937
2007
|
const ws = typeof wsId2 === "string" ? this.workspaces.get(wsId2) : void 0;
|
|
1938
2008
|
if (ws) {
|
|
@@ -2041,6 +2111,9 @@ var Coordinator = class {
|
|
|
2041
2111
|
if (member.keys.some((k) => k.kid === kid && k.revoked_at !== void 0 && now >= k.revoked_at)) {
|
|
2042
2112
|
return rpcError(E.SIG_KEY_REVOKED, `Key ${kid} is revoked`);
|
|
2043
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
|
+
}
|
|
2044
2117
|
const key = this.lookupKey(member, kid, ts);
|
|
2045
2118
|
if (!key) {
|
|
2046
2119
|
const revoked = member.keys.find((k) => k.kid === kid && k.revoked_at);
|
|
@@ -2056,9 +2129,8 @@ var Coordinator = class {
|
|
|
2056
2129
|
return rpcError(E.SIG_VERIFY_FAILED, "Signature failed verification");
|
|
2057
2130
|
}
|
|
2058
2131
|
return null;
|
|
2059
|
-
} catch
|
|
2060
|
-
|
|
2061
|
-
return rpcError(E.INTERNAL, `Signature check error: ${msg}`);
|
|
2132
|
+
} catch {
|
|
2133
|
+
return rpcError(E.SIG_VERIFY_FAILED, "Signature failed verification");
|
|
2062
2134
|
}
|
|
2063
2135
|
}
|
|
2064
2136
|
/** Lookup the key valid for a given participant at a given time. */
|
|
@@ -2182,6 +2254,7 @@ var Coordinator = class {
|
|
|
2182
2254
|
state: ws.state,
|
|
2183
2255
|
mode: ws.mode,
|
|
2184
2256
|
mode_ceiling: ws.mode_ceiling,
|
|
2257
|
+
max_envelope_bytes: this.options.maxEnvelopeBytes ?? DEFAULT_MAX_ENVELOPE_BYTES,
|
|
2185
2258
|
step_up_window_sec: ws.step_up_window_sec,
|
|
2186
2259
|
profiles: ws.profiles,
|
|
2187
2260
|
members: Array.from(ws.members.values()).map(memberToDict),
|
|
@@ -2322,10 +2395,14 @@ var Coordinator = class {
|
|
|
2322
2395
|
history: [{ ts: now, from: p.from, state: "created" }],
|
|
2323
2396
|
paused: false
|
|
2324
2397
|
};
|
|
2325
|
-
if (task.mode === "trial") task.review_required = true;
|
|
2398
|
+
if (ws.profiles.some((pr) => pr.startsWith("modes/")) && task.mode === "trial") task.review_required = true;
|
|
2326
2399
|
else if ("review_required" in p) task.review_required = !!p.review_required;
|
|
2327
2400
|
ws.tasks.set(taskId, task);
|
|
2328
|
-
if (typeof p.idempotency_key === "string")
|
|
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
|
+
}
|
|
2329
2406
|
return { result: { task_id: taskId, state: "created" } };
|
|
2330
2407
|
}
|
|
2331
2408
|
opTaskUpdate(p) {
|
|
@@ -2339,7 +2416,7 @@ var Coordinator = class {
|
|
|
2339
2416
|
const legal = {
|
|
2340
2417
|
created: ["in_progress", "declined", "paused"],
|
|
2341
2418
|
in_progress: ["in_progress", "completed", "declined", "review_requested", "paused"],
|
|
2342
|
-
review_requested: ["in_progress"
|
|
2419
|
+
review_requested: ["in_progress"],
|
|
2343
2420
|
paused: ["in_progress", "cancelled"]
|
|
2344
2421
|
};
|
|
2345
2422
|
if (!legal[task.state]?.includes(newState)) {
|
|
@@ -2365,6 +2442,30 @@ var Coordinator = class {
|
|
|
2365
2442
|
if (task.state !== "created" && task.state !== "in_progress") {
|
|
2366
2443
|
return { error: rpcError(E.PARAMS, `Cannot complete task in state: ${task.state}`) };
|
|
2367
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
|
+
}
|
|
2368
2469
|
task.output = p.output;
|
|
2369
2470
|
if (typeof p.confidence === "number" || typeof p.confidence === "string") task.confidence = p.confidence;
|
|
2370
2471
|
task.state = "completed";
|
|
@@ -2415,12 +2516,41 @@ var Coordinator = class {
|
|
|
2415
2516
|
const reviewers = Array.isArray(to) ? to : typeof to === "string" ? [to] : [];
|
|
2416
2517
|
if (!reviewers.length) return { error: rpcError(E.PARAMS, "review.request needs 'to'") };
|
|
2417
2518
|
const now = this.now();
|
|
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
|
+
}
|
|
2523
|
+
if (task.state === "review_requested" && task.review) {
|
|
2524
|
+
if (contentHash(task.pending_artefact) !== contentHash(p.artefact)) {
|
|
2525
|
+
return { error: rpcError(
|
|
2526
|
+
E.REVIEW_ALREADY_OPEN,
|
|
2527
|
+
"A review is already open on this task with different content. Decide, abstain, escalate or cancel it before requesting review of a new artefact."
|
|
2528
|
+
) };
|
|
2529
|
+
}
|
|
2530
|
+
if (rule !== task.review.rule) {
|
|
2531
|
+
return { error: rpcError(
|
|
2532
|
+
E.REVIEW_ALREADY_OPEN,
|
|
2533
|
+
`Cannot change the decision rule of an open review (currently ${task.review.rule})`
|
|
2534
|
+
) };
|
|
2535
|
+
}
|
|
2536
|
+
for (const r of reviewers) {
|
|
2537
|
+
if (!task.review.requested_to.includes(r)) task.review.requested_to.push(r);
|
|
2538
|
+
}
|
|
2539
|
+
if (p.deadline !== void 0) task.review.deadline = p.deadline;
|
|
2540
|
+
task.updated_at = now;
|
|
2541
|
+
return { result: {
|
|
2542
|
+
state: "review_requested",
|
|
2543
|
+
review_id: task.id,
|
|
2544
|
+
amended: true,
|
|
2545
|
+
requested_to: [...task.review.requested_to]
|
|
2546
|
+
} };
|
|
2547
|
+
}
|
|
2418
2548
|
task.state = "review_requested";
|
|
2419
2549
|
task.updated_at = now;
|
|
2420
2550
|
task.review = {
|
|
2421
2551
|
requested_at: now,
|
|
2422
2552
|
requested_to: reviewers,
|
|
2423
|
-
rule
|
|
2553
|
+
rule,
|
|
2424
2554
|
deadline: p.deadline,
|
|
2425
2555
|
decisions: []
|
|
2426
2556
|
};
|
|
@@ -2428,6 +2558,37 @@ var Coordinator = class {
|
|
|
2428
2558
|
task.pending_artefact = p.artefact;
|
|
2429
2559
|
return { result: { state: "review_requested", review_id: task.id } };
|
|
2430
2560
|
}
|
|
2561
|
+
/**
|
|
2562
|
+
* Verify an optional `approved_artefact_digest` against the artefact under
|
|
2563
|
+
* review (review/1.0 S3.2).
|
|
2564
|
+
*
|
|
2565
|
+
* The digest sits in params, so it is inside whatever the envelope signature
|
|
2566
|
+
* covers: the decision then attests the content the reviewer saw rather than
|
|
2567
|
+
* a task reference, and a relying party can check it without trusting the
|
|
2568
|
+
* Coordinator that produced it. Absent, behaviour is exactly as before.
|
|
2569
|
+
*
|
|
2570
|
+
* On mismatch nothing is recorded and no state changes, so a client that
|
|
2571
|
+
* computes the wrong digest refuses only its own decision.
|
|
2572
|
+
*/
|
|
2573
|
+
checkArtefactDigest(p, task) {
|
|
2574
|
+
const declared = p.approved_artefact_digest;
|
|
2575
|
+
if (declared === void 0 || declared === null) return null;
|
|
2576
|
+
if (typeof declared !== "string") {
|
|
2577
|
+
return { error: rpcError(E.PARAMS, "approved_artefact_digest must be a string") };
|
|
2578
|
+
}
|
|
2579
|
+
if (task.pending_artefact === void 0) {
|
|
2580
|
+
return { error: rpcError(E.PARAMS, "No artefact under review to bind a digest to") };
|
|
2581
|
+
}
|
|
2582
|
+
const actual = contentHash(task.pending_artefact);
|
|
2583
|
+
if (declared !== actual) {
|
|
2584
|
+
return { error: rpcError(
|
|
2585
|
+
E.SIG_ARTEFACT_DIGEST_MISMATCH,
|
|
2586
|
+
"approved_artefact_digest does not match the artefact under review",
|
|
2587
|
+
{ declared, actual }
|
|
2588
|
+
) };
|
|
2589
|
+
}
|
|
2590
|
+
return null;
|
|
2591
|
+
}
|
|
2431
2592
|
opDecide(p, kind) {
|
|
2432
2593
|
const miss = missing(p, ["workspace", "from", "task_id"]);
|
|
2433
2594
|
if (miss) return { error: rpcError(E.PARAMS, `Missing field: ${miss}`) };
|
|
@@ -2444,6 +2605,8 @@ var Coordinator = class {
|
|
|
2444
2605
|
}
|
|
2445
2606
|
const notReviewer = this.requireReviewer(task, p.from);
|
|
2446
2607
|
if (notReviewer) return notReviewer;
|
|
2608
|
+
const digestError = this.checkArtefactDigest(p, task);
|
|
2609
|
+
if (digestError) return digestError;
|
|
2447
2610
|
const now = this.now();
|
|
2448
2611
|
task.review.decisions.push({
|
|
2449
2612
|
reviewer: p.from,
|
|
@@ -2453,8 +2616,10 @@ var Coordinator = class {
|
|
|
2453
2616
|
tags: p.tags
|
|
2454
2617
|
});
|
|
2455
2618
|
if (kind === "approve") {
|
|
2456
|
-
task.
|
|
2457
|
-
|
|
2619
|
+
if (reviewSatisfied(task.review)) {
|
|
2620
|
+
task.output = task.pending_artefact;
|
|
2621
|
+
task.state = "completed";
|
|
2622
|
+
}
|
|
2458
2623
|
} else {
|
|
2459
2624
|
task.state = p.request_revision ? "in_progress" : "declined";
|
|
2460
2625
|
}
|
|
@@ -2478,6 +2643,8 @@ var Coordinator = class {
|
|
|
2478
2643
|
}
|
|
2479
2644
|
const notReviewer = this.requireReviewer(task, p.from);
|
|
2480
2645
|
if (notReviewer) return notReviewer;
|
|
2646
|
+
const digestError = this.checkArtefactDigest(p, task);
|
|
2647
|
+
if (digestError) return digestError;
|
|
2481
2648
|
const base = task.pending_artefact;
|
|
2482
2649
|
if (base === void 0) return { error: rpcError(E.PARAMS, "No base artefact for override") };
|
|
2483
2650
|
let applied;
|