cool-workflow 0.1.97 → 0.2.0
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/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/README.md +11 -2
- package/apps/architecture-review/app.json +1 -1
- package/apps/architecture-review-fast/app.json +1 -1
- package/apps/end-to-end-golden-path/app.json +1 -1
- package/apps/pr-review-fix-ci/app.json +1 -1
- package/apps/release-cut/app.json +1 -1
- package/apps/research-synthesis/app.json +1 -1
- package/dist/cli/dispatch.js +236 -0
- package/dist/cli/entry.js +120 -0
- package/dist/cli/io.js +21 -4
- package/dist/cli/parseargv.js +157 -0
- package/dist/cli.js +6 -33
- package/dist/core/capability-table.js +3534 -0
- package/dist/core/format/help.js +314 -0
- package/dist/{state-explosion/format.js → core/format/state-explosion-text.js} +10 -1
- package/dist/core/hash.js +137 -0
- package/dist/core/multi-agent/candidate-scoring.js +219 -0
- package/dist/core/multi-agent/collaboration.js +481 -0
- package/dist/core/multi-agent/coordinator.js +515 -0
- package/dist/core/multi-agent/eval-replay.js +306 -0
- package/dist/core/multi-agent/runtime.js +929 -0
- package/dist/core/multi-agent/topology.js +298 -0
- package/dist/core/multi-agent/trust-policy.js +197 -0
- package/dist/core/pipeline/commit-gate.js +320 -0
- package/dist/{pipeline-contract.js → core/pipeline/contract.js} +24 -37
- package/dist/core/pipeline/dispatch.js +103 -0
- package/dist/core/pipeline/drive-decide.js +227 -0
- package/dist/core/pipeline/error-feedback.js +161 -0
- package/dist/core/pipeline/loop-expansion.js +124 -0
- package/dist/{result-normalize.js → core/pipeline/result-normalize.js} +14 -37
- package/dist/core/pipeline/runner.js +230 -0
- package/dist/{contract-migration.js → core/state/contract-migration.js} +68 -92
- package/dist/{state-migrations.js → core/state/migrations.js} +103 -96
- package/dist/core/state/node-projection.js +95 -0
- package/dist/core/state/node-snapshot.js +230 -0
- package/dist/core/state/run-paths.js +93 -0
- package/dist/{schema-validate.js → core/state/schema-validate.js} +35 -28
- package/dist/core/state/schema.js +50 -0
- package/dist/core/state/state-explosion/digest.js +243 -0
- package/dist/core/state/state-explosion/graph.js +527 -0
- package/dist/{state-explosion → core/state/state-explosion}/helpers.js +34 -3
- package/dist/core/state/state-explosion/report.js +187 -0
- package/dist/{state-explosion → core/state/state-explosion}/size.js +25 -7
- package/dist/{state-node.js → core/state/state-node.js} +90 -113
- package/dist/core/state/types.js +19 -0
- package/dist/{validation.js → core/state/validation.js} +64 -131
- package/dist/core/trust/evidence-grounding.js +134 -0
- package/dist/core/trust/ledger.js +199 -0
- package/dist/{telemetry-attestation.js → core/trust/telemetry-attestation.js} +94 -68
- package/dist/core/trust/telemetry-ledger.js +127 -0
- package/dist/core/types.js +20 -0
- package/dist/core/version.js +16 -0
- package/dist/{workflow-app-framework.js → core/workflow-apps/app-schema.js} +337 -426
- package/dist/mcp/dispatch.js +104 -0
- package/dist/mcp/server.js +144 -0
- package/dist/mcp-server.js +6 -84
- package/dist/{agent-config.js → shell/agent-config.js} +118 -71
- package/dist/shell/app-run-cli.js +88 -0
- package/dist/shell/audit-cli.js +259 -0
- package/dist/shell/audit-provenance.js +83 -0
- package/dist/shell/candidate-scoring-io.js +459 -0
- package/dist/shell/collaboration-io.js +264 -0
- package/dist/shell/commit-summary.js +104 -0
- package/dist/shell/commit.js +290 -0
- package/dist/shell/coordinator-io.js +476 -0
- package/dist/shell/demo-cli.js +19 -0
- package/dist/shell/dispatch.js +162 -0
- package/dist/{doctor.js → shell/doctor.js} +123 -56
- package/dist/shell/drive.js +873 -0
- package/dist/shell/error-feedback-io.js +305 -0
- package/dist/shell/eval-io.js +473 -0
- package/dist/{multi-agent-eval/format.js → shell/eval-text.js} +78 -49
- package/dist/{evidence-reasoning.js → shell/evidence-reasoning.js} +124 -255
- package/dist/shell/exec-backend-cli.js +88 -0
- package/dist/shell/execution-backend/agent.js +475 -0
- package/dist/shell/execution-backend/ci.js +15 -0
- package/dist/shell/execution-backend/container.js +69 -0
- package/dist/shell/execution-backend/envelopes.js +55 -0
- package/dist/shell/execution-backend/local.js +113 -0
- package/dist/shell/execution-backend/probes.js +175 -0
- package/dist/shell/execution-backend/registry.js +402 -0
- package/dist/shell/execution-backend/remote.js +128 -0
- package/dist/shell/execution-backend/types.js +11 -0
- package/dist/shell/feedback-cli.js +81 -0
- package/dist/shell/feedback-operations.js +48 -0
- package/dist/shell/fs-atomic.js +276 -0
- package/dist/shell/harness.js +98 -0
- package/dist/shell/ledger-cli.js +212 -0
- package/dist/shell/ledger-io.js +169 -0
- package/dist/shell/man-cli.js +89 -0
- package/dist/shell/metrics-cli.js +98 -0
- package/dist/shell/multi-agent-cli.js +1002 -0
- package/dist/shell/multi-agent-host.js +563 -0
- package/dist/shell/multi-agent-io.js +387 -0
- package/dist/{multi-agent-operator-ux.js → shell/multi-agent-operator-ux.js} +234 -191
- package/dist/shell/node-store.js +124 -0
- package/dist/{observability/format.js → shell/observability-format.js} +7 -1
- package/dist/{observability/intake.js → shell/observability-intake.js} +79 -56
- package/dist/{observability.js → shell/observability.js} +159 -332
- package/dist/{onramp.js → shell/onramp.js} +11 -0
- package/dist/{operator-ux/format.js → shell/operator-ux-text.js} +250 -239
- package/dist/shell/operator-ux.js +431 -0
- package/dist/shell/orchestrator.js +231 -0
- package/dist/shell/pipeline-cli.js +667 -0
- package/dist/shell/pipeline.js +217 -0
- package/dist/shell/reclamation-io.js +1366 -0
- package/dist/shell/registry-cli.js +329 -0
- package/dist/{remote-source.js → shell/remote-source.js} +12 -5
- package/dist/shell/report-cli.js +101 -0
- package/dist/shell/report-view-cli.js +117 -0
- package/dist/{orchestrator → shell}/report.js +289 -282
- package/dist/shell/reporter.js +62 -0
- package/dist/shell/run-export-cli.js +106 -0
- package/dist/shell/run-export.js +680 -0
- package/dist/shell/run-registry-io.js +1014 -0
- package/dist/shell/run-store.js +164 -0
- package/dist/{sandbox-profile.js → shell/sandbox-profile.js} +134 -95
- package/dist/{scheduler.js → shell/scheduler-io.js} +248 -48
- package/dist/shell/scheduling-io.js +311 -0
- package/dist/shell/state-cli.js +181 -0
- package/dist/shell/state-explosion-cli.js +197 -0
- package/dist/shell/telemetry-cli.js +85 -0
- package/dist/{telemetry-demo.js → shell/telemetry-demo.js} +149 -119
- package/dist/shell/telemetry-ledger-io.js +132 -0
- package/dist/{term.js → shell/term.js} +36 -46
- package/dist/shell/topology-io.js +361 -0
- package/dist/shell/trust-audit.js +471 -0
- package/dist/{multi-agent-trust.js → shell/trust-policy-io.js} +67 -205
- package/dist/shell/verifier.js +48 -0
- package/dist/shell/workbench-host.js +250 -0
- package/dist/shell/workbench-text.js +18 -0
- package/dist/shell/workbench.js +175 -0
- package/dist/shell/worker-cli.js +124 -0
- package/dist/shell/worker-isolation.js +852 -0
- package/dist/shell/workflow-app-loader.js +650 -0
- package/docs/agent-delegation-drive.7.md +4 -0
- package/docs/cli-mcp-parity.7.md +284 -211
- package/docs/contract-migration-tooling.7.md +4 -0
- package/docs/control-plane-scheduling.7.md +4 -0
- package/docs/cross-agent-ledger.7.md +217 -0
- package/docs/designs/handoff-ledger.md +145 -0
- package/docs/durable-state-and-locking.7.md +4 -0
- package/docs/evidence-adoption-reasoning-chain.7.md +4 -0
- package/docs/execution-backends.7.md +4 -0
- package/docs/handoff-setup.md +120 -0
- package/docs/multi-agent-cli-mcp-surface.7.md +4 -0
- package/docs/multi-agent-eval-replay-harness.7.md +4 -0
- package/docs/multi-agent-operator-ux.7.md +4 -0
- package/docs/node-snapshot-diff-replay.7.md +4 -0
- package/docs/observability-cost-accounting.7.md +4 -0
- package/docs/project-index.md +143 -71
- package/docs/real-execution-backends.7.md +4 -0
- package/docs/release-and-migration.7.md +4 -0
- package/docs/release-tooling.7.md +4 -0
- package/docs/run-registry-control-plane.7.md +4 -0
- package/docs/run-retention-reclamation.7.md +25 -0
- package/docs/state-explosion-management.7.md +4 -0
- package/docs/team-collaboration.7.md +4 -0
- package/docs/web-desktop-workbench.7.md +4 -0
- package/manifest/plugin.manifest.json +1 -1
- package/manifest/source-context-profiles.json +9 -13
- package/package.json +1 -1
- package/scripts/agents/codex-agent.js +34 -4
- package/scripts/agents/cw-attest-wrap.js +2 -2
- package/scripts/bump-version.js +4 -3
- package/scripts/canonical-apps.js +4 -4
- package/scripts/children/batch-delegate-child.js +30 -10
- package/scripts/dogfood-architecture-review.js +2 -3
- package/scripts/dogfood-release.js +3 -3
- package/scripts/gen-parity-doc.js +15 -2
- package/scripts/golden-path.js +4 -4
- package/scripts/onramp-check.js +1 -1
- package/scripts/parity-check.js +38 -21
- package/scripts/release-flow.js +9 -3
- package/scripts/sync-project-index.js +51 -27
- package/scripts/validate-run-state-schema.js +2 -2
- package/scripts/version-sync-check.js +30 -30
- package/dist/candidate-scoring.js +0 -729
- package/dist/capability-core.js +0 -1186
- package/dist/capability-registry.js +0 -879
- package/dist/cli/command-surface.js +0 -490
- package/dist/cli/format.js +0 -56
- package/dist/cli/handlers/audit.js +0 -82
- package/dist/cli/handlers/blackboard.js +0 -81
- package/dist/cli/handlers/candidate.js +0 -40
- package/dist/cli/handlers/clones.js +0 -34
- package/dist/cli/handlers/collaboration.js +0 -61
- package/dist/cli/handlers/eval.js +0 -40
- package/dist/cli/handlers/maintenance.js +0 -107
- package/dist/cli/handlers/multi-agent.js +0 -165
- package/dist/cli/handlers/node.js +0 -41
- package/dist/cli/handlers/operational.js +0 -155
- package/dist/cli/handlers/operator.js +0 -146
- package/dist/cli/handlers/registry.js +0 -68
- package/dist/cli/handlers/run.js +0 -153
- package/dist/cli/handlers/scheduling.js +0 -132
- package/dist/cli/handlers/workbench.js +0 -41
- package/dist/cli/handlers/worker.js +0 -45
- package/dist/cli/run-summary.js +0 -45
- package/dist/clones.js +0 -162
- package/dist/collaboration.js +0 -726
- package/dist/commit.js +0 -592
- package/dist/compare.js +0 -18
- package/dist/coordinator/classify.js +0 -45
- package/dist/coordinator/paths.js +0 -42
- package/dist/coordinator/util.js +0 -126
- package/dist/coordinator.js +0 -990
- package/dist/daemon.js +0 -44
- package/dist/dispatch.js +0 -250
- package/dist/drive.js +0 -827
- package/dist/error-feedback.js +0 -419
- package/dist/evidence-grounding.js +0 -184
- package/dist/execution-backend/agent.js +0 -294
- package/dist/execution-backend/probes.js +0 -112
- package/dist/execution-backend/util.js +0 -47
- package/dist/execution-backend.js +0 -961
- package/dist/gates.js +0 -48
- package/dist/harness.js +0 -61
- package/dist/loop-expansion.js +0 -60
- package/dist/mcp/tool-call.js +0 -434
- package/dist/mcp/tool-definitions.js +0 -1040
- package/dist/mcp-surface.js +0 -30
- package/dist/multi-agent/graph.js +0 -84
- package/dist/multi-agent/helpers.js +0 -141
- package/dist/multi-agent/ids.js +0 -20
- package/dist/multi-agent/paths.js +0 -22
- package/dist/multi-agent-eval/normalize.js +0 -51
- package/dist/multi-agent-eval.js +0 -678
- package/dist/multi-agent-host.js +0 -777
- package/dist/multi-agent.js +0 -984
- package/dist/node-projection.js +0 -59
- package/dist/node-snapshot.js +0 -260
- package/dist/operator-ux.js +0 -631
- package/dist/orchestrator/app-operations.js +0 -211
- package/dist/orchestrator/audit-operations.js +0 -182
- package/dist/orchestrator/candidate-operations.js +0 -117
- package/dist/orchestrator/cli-options.js +0 -294
- package/dist/orchestrator/collaboration-operations.js +0 -86
- package/dist/orchestrator/feedback-operations.js +0 -81
- package/dist/orchestrator/host-operations.js +0 -78
- package/dist/orchestrator/lifecycle-operations.js +0 -626
- package/dist/orchestrator/migration-operations.js +0 -44
- package/dist/orchestrator/multi-agent-operations.js +0 -362
- package/dist/orchestrator/topology-operations.js +0 -84
- package/dist/orchestrator.js +0 -917
- package/dist/pipeline-runner.js +0 -285
- package/dist/reclamation/hash.js +0 -72
- package/dist/reclamation.js +0 -812
- package/dist/reporter.js +0 -67
- package/dist/run-export.js +0 -784
- package/dist/run-registry/derive.js +0 -175
- package/dist/run-registry/format.js +0 -124
- package/dist/run-registry/gc.js +0 -251
- package/dist/run-registry/policy.js +0 -16
- package/dist/run-registry/queue.js +0 -115
- package/dist/run-registry.js +0 -850
- package/dist/run-state-schema.js +0 -68
- package/dist/scheduling.js +0 -184
- package/dist/state-explosion.js +0 -1014
- package/dist/state.js +0 -367
- package/dist/telemetry-ledger.js +0 -196
- package/dist/topology.js +0 -565
- package/dist/triggers.js +0 -184
- package/dist/trust-audit.js +0 -644
- package/dist/types/blackboard.js +0 -2
- package/dist/types/candidate.js +0 -2
- package/dist/types/collaboration.js +0 -2
- package/dist/types/core.js +0 -2
- package/dist/types/drive.js +0 -10
- package/dist/types/error-feedback.js +0 -2
- package/dist/types/evidence-reasoning.js +0 -2
- package/dist/types/execution-backend.js +0 -2
- package/dist/types/multi-agent.js +0 -2
- package/dist/types/observability.js +0 -2
- package/dist/types/pipeline.js +0 -2
- package/dist/types/reclamation.js +0 -8
- package/dist/types/report-bundle.js +0 -6
- package/dist/types/result.js +0 -2
- package/dist/types/run-registry.js +0 -2
- package/dist/types/run.js +0 -2
- package/dist/types/sandbox.js +0 -2
- package/dist/types/schedule.js +0 -2
- package/dist/types/state-node.js +0 -2
- package/dist/types/topology.js +0 -2
- package/dist/types/trust.js +0 -2
- package/dist/types/workbench.js +0 -2
- package/dist/types/worker.js +0 -2
- package/dist/types/workflow-app.js +0 -2
- package/dist/types.js +0 -44
- package/dist/util/fingerprint.js +0 -19
- package/dist/util/fingerprint.test.js +0 -27
- package/dist/verifier.js +0 -78
- package/dist/version.js +0 -8
- package/dist/workbench-host.js +0 -182
- package/dist/workbench.js +0 -192
- package/dist/worker-accept/acceptance.js +0 -114
- package/dist/worker-accept/blackboard-fanout.js +0 -80
- package/dist/worker-accept/blackboard-linkage.js +0 -19
- package/dist/worker-accept/context.js +0 -2
- package/dist/worker-accept/telemetry-ledger.js +0 -126
- package/dist/worker-accept/validation.js +0 -77
- package/dist/worker-accept/verifier-completion.js +0 -73
- package/dist/worker-isolation/helpers.js +0 -51
- package/dist/worker-isolation/paths.js +0 -46
- package/dist/worker-isolation.js +0 -656
- package/dist/workflow-api.js +0 -131
- /package/dist/{types → core/types}/boundary.js +0 -0
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// core/multi-agent/topology.ts — OFFICIAL_TOPOLOGIES, applyTopology's
|
|
3
|
+
// decision half, role-width math, collector-exclusion, trust-authority.
|
|
4
|
+
//
|
|
5
|
+
// MILESTONE 9. Byte-exact port of the old build's src/topology.ts, minus
|
|
6
|
+
// the actual multi-agent/blackboard/coordinator record creation and audit
|
|
7
|
+
// writes (those are impure — see shell/topology-io.ts, which calls this
|
|
8
|
+
// file's pure helpers then wires createMultiAgentRun/createAgentRole/
|
|
9
|
+
// createBlackboardTopic/etc via shell/multi-agent-io.ts +
|
|
10
|
+
// shell/coordinator-io.ts, byte-identical to the old build's applyTopology
|
|
11
|
+
// call sequence).
|
|
12
|
+
//
|
|
13
|
+
// BYTE-COMPAT ITEM 3 [load-bearing, HIGH priority]: `unique` in THIS file
|
|
14
|
+
// does NOT sort (dedup only, insertion order preserved) — the opposite of
|
|
15
|
+
// core/multi-agent/runtime.ts's sorting `unique`. Never merge the two. See
|
|
16
|
+
// uniquedual-role-vs-candidate-order.case.js.
|
|
17
|
+
//
|
|
18
|
+
// Evidence: SPEC/multi-agent.md section B ("Topologies"), "Topology error
|
|
19
|
+
// strings and outputs"; plugins/cool-workflow/src/topology.ts (byte-exact
|
|
20
|
+
// source).
|
|
21
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
22
|
+
exports.OFFICIAL_TOPOLOGIES = exports.TOPOLOGY_SCHEMA_VERSION = void 0;
|
|
23
|
+
exports.unique = unique;
|
|
24
|
+
exports.registerTopology = registerTopology;
|
|
25
|
+
exports.listTopologyDefinitions = listTopologyDefinitions;
|
|
26
|
+
exports.getTopologyDefinition = getTopologyDefinition;
|
|
27
|
+
exports.validateTopologyDefinition = validateTopologyDefinition;
|
|
28
|
+
exports.withLegacyRoleCounts = withLegacyRoleCounts;
|
|
29
|
+
exports.materializedRoles = materializedRoles;
|
|
30
|
+
exports.fanoutRoleIds = fanoutRoleIds;
|
|
31
|
+
exports.selectedTaskIds = selectedTaskIds;
|
|
32
|
+
exports.topologyRunId = topologyRunId;
|
|
33
|
+
exports.nextActionsFor = nextActionsFor;
|
|
34
|
+
exports.statusToNodeStatus = statusToNodeStatus;
|
|
35
|
+
exports.buildTopologyGraphFromRuns = buildTopologyGraphFromRuns;
|
|
36
|
+
exports.TOPOLOGY_SCHEMA_VERSION = 1;
|
|
37
|
+
/** Dedup, insertion-order preserved (does NOT sort) — the topology-side
|
|
38
|
+
* `unique` twin. See file header byte-compat note. */
|
|
39
|
+
function unique(items) {
|
|
40
|
+
return [...new Set(items.filter((item) => item !== undefined && item !== null))];
|
|
41
|
+
}
|
|
42
|
+
function roleSpec(id, title, responsibilities, expectedArtifacts, faninObligations, count) {
|
|
43
|
+
return { id, title, responsibilities, requiredEvidence: expectedArtifacts, expectedArtifacts, faninObligations, ...(count !== undefined ? { count } : {}) };
|
|
44
|
+
}
|
|
45
|
+
function topicSpec(id, title, description) {
|
|
46
|
+
return { id, title, description };
|
|
47
|
+
}
|
|
48
|
+
function phaseSpec(id, title, roleIds, fanout, fanin, requiredEvidence, coordinatorDecisionKinds) {
|
|
49
|
+
return { id, title, roleIds, fanout, fanin, requiredEvidence, coordinatorDecisionKinds };
|
|
50
|
+
}
|
|
51
|
+
/** The three built-in topologies, in this exact order. Byte-exact
|
|
52
|
+
* transcription of src/topology.ts:58-137. */
|
|
53
|
+
exports.OFFICIAL_TOPOLOGIES = [
|
|
54
|
+
{
|
|
55
|
+
schemaVersion: 1,
|
|
56
|
+
id: "map-reduce",
|
|
57
|
+
title: "Map-Reduce",
|
|
58
|
+
summary: "Fan out mapper roles, index mapper evidence on the blackboard, then reduce only after required evidence is present.",
|
|
59
|
+
roles: [
|
|
60
|
+
roleSpec("mapper", "Mapper", ["Produce an independent shard result and cite evidence."], ["mapper output artifact"], ["indexed mapper artifact"], 2),
|
|
61
|
+
roleSpec("reducer", "Reducer", ["Synthesize mapper outputs only after fanin is verifier-ready."], ["reducer synthesis"], ["all mapper evidence"]),
|
|
62
|
+
],
|
|
63
|
+
groups: [{ id: "map-reduce", title: "Map-Reduce Group", roleIds: ["mapper", "reducer"] }],
|
|
64
|
+
blackboardTopics: [
|
|
65
|
+
topicSpec("mapper-outputs", "Mapper Outputs", "Indexed mapper result artifacts and evidence."),
|
|
66
|
+
topicSpec("reducer-synthesis", "Reducer Synthesis", "Reducer fanin readiness and synthesis provenance."),
|
|
67
|
+
],
|
|
68
|
+
phases: [
|
|
69
|
+
phaseSpec("map", "Map", ["mapper"], true, false, ["mapper output artifact"], ["artifact-index"]),
|
|
70
|
+
phaseSpec("reduce", "Reduce", ["reducer"], false, true, ["all mapper evidence"], ["fanin-readiness", "candidate-synthesis"]),
|
|
71
|
+
],
|
|
72
|
+
fanoutStrategy: "one membership per mapper role over selected run tasks",
|
|
73
|
+
faninStrategy: "required mapper roles must report result evidence and indexed blackboard artifacts",
|
|
74
|
+
requiredEvidence: ["mapper output artifact", "blackboard artifact ref", "reducer synthesis"],
|
|
75
|
+
coordinatorDecisions: ["artifact-index", "fanin-readiness", "candidate-synthesis"],
|
|
76
|
+
candidateExpectations: ["Reducer result becomes a candidate only with mapper provenance."],
|
|
77
|
+
verifierGates: ["Reducer fanin must be ready before completion or commit."],
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
schemaVersion: 1,
|
|
81
|
+
id: "debate",
|
|
82
|
+
title: "Debate",
|
|
83
|
+
summary: "Record opposing claims, rebuttal rounds, conflict context, coordinator decisions, and final synthesis on shared topics.",
|
|
84
|
+
roles: [
|
|
85
|
+
roleSpec("position-a", "Position A", ["Argue one supported position with evidence."], ["claim message"], ["round messages"]),
|
|
86
|
+
roleSpec("position-b", "Position B", ["Argue a contrasting position with evidence."], ["counterclaim message"], ["round messages"]),
|
|
87
|
+
roleSpec("synthesizer", "Synthesis", ["Resolve or preserve conflicts with citations."], ["debate synthesis"], ["coordinator decisions"]),
|
|
88
|
+
],
|
|
89
|
+
groups: [{ id: "debate", title: "Debate Group", roleIds: ["position-a", "position-b", "synthesizer"] }],
|
|
90
|
+
blackboardTopics: [
|
|
91
|
+
topicSpec("debate-rounds", "Debate Rounds", "Claim and rebuttal messages by round."),
|
|
92
|
+
topicSpec("debate-conflicts", "Conflict Context", "Conflicting or unresolved claims."),
|
|
93
|
+
topicSpec("debate-synthesis", "Final Synthesis", "Accepted, rejected, conflicting, and unresolved claims."),
|
|
94
|
+
],
|
|
95
|
+
phases: [
|
|
96
|
+
phaseSpec("opening", "Opening Claims", ["position-a", "position-b"], true, false, ["claim evidence"], ["message-moderation"]),
|
|
97
|
+
phaseSpec("rebuttal", "Rebuttal Rounds", ["position-a", "position-b"], true, false, ["response evidence"], ["conflict-resolution"]),
|
|
98
|
+
phaseSpec("synthesis", "Synthesis", ["synthesizer"], false, true, ["debate messages", "coordinator decisions"], ["candidate-synthesis"]),
|
|
99
|
+
],
|
|
100
|
+
fanoutStrategy: "opposing roles write blackboard messages for each round",
|
|
101
|
+
faninStrategy: "synthesis requires debate messages and coordinator claim decisions",
|
|
102
|
+
requiredEvidence: ["debate message", "conflict context", "coordinator decision", "final synthesis"],
|
|
103
|
+
coordinatorDecisions: ["message-moderation", "conflict-resolution", "candidate-synthesis"],
|
|
104
|
+
candidateExpectations: ["Final synthesis cites debate messages and decisions."],
|
|
105
|
+
verifierGates: ["Required debate rounds and synthesis evidence must be present."],
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
schemaVersion: 1,
|
|
109
|
+
id: "judge-panel",
|
|
110
|
+
title: "Judge Panel",
|
|
111
|
+
summary: "Collect independent judge outputs, aggregate scores, and select a panel decision with linked evidence.",
|
|
112
|
+
roles: [
|
|
113
|
+
roleSpec("judge", "Judge", ["Score candidates independently and cite evidence."], ["judge score artifact"], ["judge verdict"], 3),
|
|
114
|
+
roleSpec("panel-chair", "Panel Chair", ["Aggregate scores and write a panel decision."], ["panel decision"], ["judge evidence"]),
|
|
115
|
+
],
|
|
116
|
+
groups: [{ id: "judge-panel", title: "Judge Panel Group", roleIds: ["judge", "panel-chair"] }],
|
|
117
|
+
blackboardTopics: [
|
|
118
|
+
topicSpec("judge-verdicts", "Judge Verdicts", "Independent judge outputs and score evidence."),
|
|
119
|
+
topicSpec("panel-decision", "Panel Decision", "Aggregated verdict and candidate selection rationale."),
|
|
120
|
+
],
|
|
121
|
+
phases: [
|
|
122
|
+
phaseSpec("judge", "Judge", ["judge"], true, false, ["judge score artifact"], ["artifact-index"]),
|
|
123
|
+
phaseSpec("panel", "Panel", ["panel-chair"], false, true, ["judge evidence", "score records"], ["candidate-synthesis"]),
|
|
124
|
+
],
|
|
125
|
+
fanoutStrategy: "one membership per independent judge role",
|
|
126
|
+
faninStrategy: "panel decision requires fanin over judge evidence and score records",
|
|
127
|
+
requiredEvidence: ["judge output", "score record", "panel decision", "candidate selection rationale"],
|
|
128
|
+
coordinatorDecisions: ["artifact-index", "candidate-synthesis"],
|
|
129
|
+
candidateExpectations: ["No single judge is authoritative without aggregated fanin and score evidence."],
|
|
130
|
+
verifierGates: ["Panel decision requires multiple judge outputs unless explicitly configured otherwise."],
|
|
131
|
+
},
|
|
132
|
+
];
|
|
133
|
+
/** Module-level registry (mechanism); OFFICIAL_TOPOLOGIES + registrations
|
|
134
|
+
* are policy. `listTopologyDefinitions()` composes them — registered
|
|
135
|
+
* wins on id clash, last-write-wins. */
|
|
136
|
+
const topologyRegistry = new Map();
|
|
137
|
+
function registerTopology(definition) {
|
|
138
|
+
topologyRegistry.set(definition.id, clone(definition));
|
|
139
|
+
}
|
|
140
|
+
function listTopologyDefinitions() {
|
|
141
|
+
const merged = exports.OFFICIAL_TOPOLOGIES.map((definition) => clone(definition));
|
|
142
|
+
for (const registered of topologyRegistry.values()) {
|
|
143
|
+
const idx = merged.findIndex((d) => d.id === registered.id);
|
|
144
|
+
if (idx >= 0)
|
|
145
|
+
merged[idx] = clone(registered);
|
|
146
|
+
else
|
|
147
|
+
merged.push(clone(registered));
|
|
148
|
+
}
|
|
149
|
+
return merged;
|
|
150
|
+
}
|
|
151
|
+
function getTopologyDefinition(topologyId) {
|
|
152
|
+
const registered = topologyRegistry.get(topologyId);
|
|
153
|
+
if (registered)
|
|
154
|
+
return clone(registered);
|
|
155
|
+
return exports.OFFICIAL_TOPOLOGIES.find((definition) => definition.id === topologyId);
|
|
156
|
+
}
|
|
157
|
+
function validateTopologyDefinition(topologyId) {
|
|
158
|
+
const definition = getTopologyDefinition(topologyId);
|
|
159
|
+
if (!definition)
|
|
160
|
+
return { valid: false, topologyId, issues: [{ code: "unknown-topology", message: `Unknown topology id: ${topologyId}` }] };
|
|
161
|
+
const issues = [];
|
|
162
|
+
if (!definition.roles.length)
|
|
163
|
+
issues.push({ code: "missing-roles", message: "Topology must declare at least one role.", path: "roles" });
|
|
164
|
+
if (!definition.groups.length)
|
|
165
|
+
issues.push({ code: "missing-groups", message: "Topology must declare at least one group.", path: "groups" });
|
|
166
|
+
if (!definition.blackboardTopics.length)
|
|
167
|
+
issues.push({ code: "missing-topics", message: "Topology must declare blackboard topics.", path: "blackboardTopics" });
|
|
168
|
+
if (!definition.requiredEvidence.length)
|
|
169
|
+
issues.push({ code: "missing-evidence", message: "Topology must declare required evidence.", path: "requiredEvidence" });
|
|
170
|
+
const roleIds = new Set(definition.roles.map((role) => role.id));
|
|
171
|
+
for (const phase of definition.phases) {
|
|
172
|
+
for (const roleId of phase.roleIds) {
|
|
173
|
+
if (!roleIds.has(roleId))
|
|
174
|
+
issues.push({ code: "unknown-phase-role", message: `Phase ${phase.id} references unknown role ${roleId}.`, path: `phases.${phase.id}` });
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return { valid: issues.length === 0, topologyId, issues, definition };
|
|
178
|
+
}
|
|
179
|
+
/** Fold the legacy id-keyed mapperCount/judgeCount flags into the uniform
|
|
180
|
+
* roleCounts map. An explicit roleCounts entry always wins; judgeCount
|
|
181
|
+
* floors at 2 (a panel never collapses to one judge via the legacy
|
|
182
|
+
* flag), mapperCount floors at 1. */
|
|
183
|
+
function withLegacyRoleCounts(input) {
|
|
184
|
+
const legacy = {
|
|
185
|
+
mapper: input.mapperCount === undefined ? undefined : Math.max(1, input.mapperCount),
|
|
186
|
+
judge: input.judgeCount === undefined ? undefined : Math.max(2, input.judgeCount),
|
|
187
|
+
};
|
|
188
|
+
const roleCounts = { ...input.roleCounts };
|
|
189
|
+
for (const [roleId, value] of Object.entries(legacy)) {
|
|
190
|
+
if (value !== undefined && roleCounts[roleId] === undefined)
|
|
191
|
+
roleCounts[roleId] = value;
|
|
192
|
+
}
|
|
193
|
+
return Object.keys(roleCounts).length ? { ...input, roleCounts } : input;
|
|
194
|
+
}
|
|
195
|
+
/** Width = `max(1, roleCounts[role.id] ?? role.count ?? 1)`; when width >
|
|
196
|
+
* 1, instances get ids `${role.id}-${index}` and titles `${role.title}
|
|
197
|
+
* ${index}` for index 1..N. Data-driven — no topology-id/role-id
|
|
198
|
+
* branching. */
|
|
199
|
+
function materializedRoles(definition, input) {
|
|
200
|
+
const roles = [];
|
|
201
|
+
for (const role of definition.roles) {
|
|
202
|
+
const roleCount = Math.max(1, input.roleCounts?.[role.id] ?? role.count ?? 1);
|
|
203
|
+
if (roleCount > 1) {
|
|
204
|
+
for (let index = 1; index <= roleCount; index += 1) {
|
|
205
|
+
roles.push({ ...role, id: `${role.id}-${index}`, title: `${role.title} ${index}` });
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
roles.push({ ...role, id: role.id, title: role.title });
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return roles;
|
|
213
|
+
}
|
|
214
|
+
/** Collector roles are excluded from fanout by id suffix
|
|
215
|
+
* (-reducer/-synthesizer/-panel-chair) — NOT by a flag or role position.
|
|
216
|
+
* If the filter empties the list, ALL roles fan out. */
|
|
217
|
+
function fanoutRoleIds(roleIds) {
|
|
218
|
+
const filtered = roleIds.filter((roleId) => !roleId.endsWith("-reducer") && !roleId.endsWith("-synthesizer") && !roleId.endsWith("-panel-chair"));
|
|
219
|
+
return filtered.length ? filtered : roleIds;
|
|
220
|
+
}
|
|
221
|
+
function selectedTaskIds(tasks, taskIds) {
|
|
222
|
+
const ids = taskIds?.length ? taskIds : [tasks.find((task) => task.status === "pending")?.id || tasks[0]?.id].filter(Boolean);
|
|
223
|
+
for (const id of ids) {
|
|
224
|
+
if (!tasks.some((task) => task.id === id))
|
|
225
|
+
throw new Error(`Unknown task id for topology: ${id}`);
|
|
226
|
+
}
|
|
227
|
+
return ids;
|
|
228
|
+
}
|
|
229
|
+
/** Deterministic default topology-run id: `${definition.id}-${hex16}`
|
|
230
|
+
* where `hex16` is the first 16 hex chars of a content-hash over
|
|
231
|
+
* `{definitionId, roleIds sorted, taskIds sorted, runId, sequence}`.
|
|
232
|
+
* `sha256`/`stableStringify` are injected so this file stays free of a
|
|
233
|
+
* direct core/hash.ts import cycle risk (matches the old build's own
|
|
234
|
+
* use of the shared hash module). */
|
|
235
|
+
function topologyRunId(definition, taskIds, runId, sequence, hash) {
|
|
236
|
+
const digest = hash({
|
|
237
|
+
definitionId: definition.id,
|
|
238
|
+
roleIds: [...definition.roles.map((role) => role.id)].sort(),
|
|
239
|
+
taskIds: [...taskIds].sort(),
|
|
240
|
+
runId,
|
|
241
|
+
sequence,
|
|
242
|
+
});
|
|
243
|
+
return `${definition.id}-${digest.replace("sha256:", "").slice(0, 16)}`;
|
|
244
|
+
}
|
|
245
|
+
function nextActionsFor(runId, topologyRunId, fanoutId) {
|
|
246
|
+
return [
|
|
247
|
+
`node scripts/cw.js dispatch ${runId} --multi-agent-fanout ${fanoutId}`,
|
|
248
|
+
`node scripts/cw.js multi-agent fanin ${runId} ${topologyRunId}-fanin --fanout ${fanoutId}`,
|
|
249
|
+
`node scripts/cw.js topology summary ${runId}`,
|
|
250
|
+
];
|
|
251
|
+
}
|
|
252
|
+
function statusToNodeStatus(status) {
|
|
253
|
+
if (status === "completed" || status === "ready")
|
|
254
|
+
return "completed";
|
|
255
|
+
if (status === "blocked")
|
|
256
|
+
return "blocked";
|
|
257
|
+
if (status === "failed")
|
|
258
|
+
return "failed";
|
|
259
|
+
if (status === "running")
|
|
260
|
+
return "running";
|
|
261
|
+
return "pending";
|
|
262
|
+
}
|
|
263
|
+
function clone(value) {
|
|
264
|
+
return JSON.parse(JSON.stringify(value));
|
|
265
|
+
}
|
|
266
|
+
function uniqueEdges(edges) {
|
|
267
|
+
const seen = new Set();
|
|
268
|
+
return edges.filter((edge) => {
|
|
269
|
+
const key = `${edge.from}->${edge.to}:${edge.label || ""}`;
|
|
270
|
+
if (seen.has(key))
|
|
271
|
+
return false;
|
|
272
|
+
seen.add(key);
|
|
273
|
+
return true;
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
function buildTopologyGraphFromRuns(runId, records, runPath) {
|
|
277
|
+
const nodes = [];
|
|
278
|
+
const edges = [];
|
|
279
|
+
for (const record of records) {
|
|
280
|
+
nodes.push({ id: `${runId}:topology:${record.id}`, kind: "topology-run", status: record.status, label: `${record.topologyId}:${record.id}`, path: runPath(record.id) });
|
|
281
|
+
edges.push({ from: `${runId}:run`, to: `${runId}:topology:${record.id}` });
|
|
282
|
+
edges.push({ from: `${runId}:topology:${record.id}`, to: `${runId}:multi-agent:${record.multiAgentRunId}`, label: "multi-agent" });
|
|
283
|
+
edges.push({ from: `${runId}:topology:${record.id}`, to: `${runId}:blackboard:${record.blackboardId}`, label: "blackboard" });
|
|
284
|
+
for (const topicId of record.topicIds)
|
|
285
|
+
edges.push({ from: `${runId}:topology:${record.id}`, to: `${runId}:blackboard:topic:${topicId}`, label: "topic" });
|
|
286
|
+
for (const roleId of record.roleIds)
|
|
287
|
+
edges.push({ from: `${runId}:topology:${record.id}`, to: `${runId}:multi-agent:role:${roleId}`, label: "role" });
|
|
288
|
+
for (const groupId of record.groupIds)
|
|
289
|
+
edges.push({ from: `${runId}:topology:${record.id}`, to: `${runId}:multi-agent:group:${groupId}`, label: "group" });
|
|
290
|
+
for (const fanoutId of record.fanoutIds)
|
|
291
|
+
edges.push({ from: `${runId}:topology:${record.id}`, to: `${runId}:multi-agent:fanout:${fanoutId}`, label: "fanout" });
|
|
292
|
+
for (const faninId of record.faninIds)
|
|
293
|
+
edges.push({ from: `${runId}:topology:${record.id}`, to: `${runId}:multi-agent:fanin:${faninId}`, label: "fanin" });
|
|
294
|
+
for (const decisionId of record.coordinatorDecisionIds)
|
|
295
|
+
edges.push({ from: `${runId}:topology:${record.id}`, to: `${runId}:blackboard:decision:${decisionId}`, label: "decision" });
|
|
296
|
+
}
|
|
297
|
+
return { nodes, edges: uniqueEdges(edges) };
|
|
298
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// core/multi-agent/trust-policy.ts — policyForRole/Group/Membership,
|
|
3
|
+
// authorizeMultiAgentAction's PURE decision half.
|
|
4
|
+
//
|
|
5
|
+
// MILESTONE 9. Byte-exact port of the decision logic in the old build's
|
|
6
|
+
// src/multi-agent-trust.ts. Audit-event recording (recordTrustAuditEvent,
|
|
7
|
+
// which appends to disk) is the caller's job — see
|
|
8
|
+
// shell/multi-agent-io.ts's `authorizeMultiAgentAction` wrapper, which
|
|
9
|
+
// calls `evaluateMultiAgentAction` below then records the two audit
|
|
10
|
+
// events exactly like the old build did.
|
|
11
|
+
//
|
|
12
|
+
// Evidence: SPEC/multi-agent.md section D ("Trust policies"), "Trust
|
|
13
|
+
// denial reasons"; plugins/cool-workflow/src/multi-agent-trust.ts
|
|
14
|
+
// (byte-exact source for policyForRole/Group/Membership + evaluatePolicy/
|
|
15
|
+
// missingEvidence).
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.policyForRole = policyForRole;
|
|
18
|
+
exports.policyForGroup = policyForGroup;
|
|
19
|
+
exports.policyForMembership = policyForMembership;
|
|
20
|
+
exports.evaluateMultiAgentAction = evaluateMultiAgentAction;
|
|
21
|
+
exports.resolvePolicy = resolvePolicy;
|
|
22
|
+
exports.sourceForActor = sourceForActor;
|
|
23
|
+
function unique(values) {
|
|
24
|
+
return Array.from(new Set(values.filter(Boolean)));
|
|
25
|
+
}
|
|
26
|
+
/** Chair detection: lowercased `metadata.topologyRoleId` (or title)
|
|
27
|
+
* contains "chair", "reducer", or "synthesizer". Judge detection:
|
|
28
|
+
* contains "judge". Renaming a role changes its authority (v2/PLAN.md
|
|
29
|
+
* byte-compat / rebuild risk 4 — substring match, not a flag). */
|
|
30
|
+
function policyForRole(role) {
|
|
31
|
+
const topologyRole = String(role.metadata?.topologyRoleId || role.title || "").toLowerCase();
|
|
32
|
+
const isChair = topologyRole.includes("chair") || topologyRole.includes("reducer") || topologyRole.includes("synthesizer");
|
|
33
|
+
const isJudge = topologyRole.includes("judge");
|
|
34
|
+
return {
|
|
35
|
+
schemaVersion: 1,
|
|
36
|
+
id: `${role.id}-policy`,
|
|
37
|
+
policyRef: `multiAgent.roles.${role.id}.policy`,
|
|
38
|
+
subjectKind: "role",
|
|
39
|
+
subjectId: role.id,
|
|
40
|
+
allowedBlackboardTopicIds: unique(role.topicIds || ["*"]),
|
|
41
|
+
allowedWriteOperations: unique([
|
|
42
|
+
"message",
|
|
43
|
+
"context",
|
|
44
|
+
"artifact",
|
|
45
|
+
...(isChair ? ["snapshot", "coordinator-decision"] : []),
|
|
46
|
+
]),
|
|
47
|
+
allowedCandidateOperations: (isChair ? ["score", "select"] : ["score"]),
|
|
48
|
+
allowedJudgeOperations: unique([
|
|
49
|
+
...(isJudge ? ["verdict", "rationale"] : []),
|
|
50
|
+
...(isChair ? ["rationale", "panel-decision"] : []),
|
|
51
|
+
]),
|
|
52
|
+
sandboxProfileHints: unique(role.sandboxProfileHints || []),
|
|
53
|
+
requiredEvidenceRefs: unique(role.requiredEvidence || []),
|
|
54
|
+
requiredEvidenceFor: {
|
|
55
|
+
"judge.rationale": ["judge rationale evidence"],
|
|
56
|
+
"judge.verdict": ["judge verdict evidence"],
|
|
57
|
+
"judge.panel-decision": ["judge messages", "score evidence", "coordinator decision"],
|
|
58
|
+
"candidate.select": ["score evidence", "judge rationale"],
|
|
59
|
+
},
|
|
60
|
+
deniedOperations: [],
|
|
61
|
+
metadata: { title: role.title, topologyRoleId: role.metadata?.topologyRoleId },
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
/** Wide-open group policy: all write/candidate/judge ops allowed, topics
|
|
65
|
+
* from the group or `["*"]`. */
|
|
66
|
+
function policyForGroup(group) {
|
|
67
|
+
return {
|
|
68
|
+
schemaVersion: 1,
|
|
69
|
+
id: `${group.id}-policy`,
|
|
70
|
+
policyRef: `multiAgent.groups.${group.id}.policy`,
|
|
71
|
+
subjectKind: "group",
|
|
72
|
+
subjectId: group.id,
|
|
73
|
+
allowedBlackboardTopicIds: unique(group.topicIds || ["*"]),
|
|
74
|
+
allowedWriteOperations: ["message", "context", "artifact", "snapshot", "coordinator-decision"],
|
|
75
|
+
allowedCandidateOperations: ["register", "score", "select"],
|
|
76
|
+
allowedJudgeOperations: ["verdict", "rationale", "panel-decision"],
|
|
77
|
+
sandboxProfileHints: [],
|
|
78
|
+
requiredEvidenceRefs: [],
|
|
79
|
+
deniedOperations: [],
|
|
80
|
+
metadata: { phase: group.phase },
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
/** Copies the role policy (or a minimal message/context/artifact-only
|
|
84
|
+
* policy if no role) and re-subjects it to the membership. */
|
|
85
|
+
function policyForMembership(membership, role) {
|
|
86
|
+
const source = role?.policy || (role ? policyForRole(role) : undefined);
|
|
87
|
+
return {
|
|
88
|
+
...(source || {
|
|
89
|
+
schemaVersion: 1,
|
|
90
|
+
id: `${membership.id}-policy`,
|
|
91
|
+
policyRef: `multiAgent.memberships.${membership.id}.policy`,
|
|
92
|
+
subjectKind: "membership",
|
|
93
|
+
subjectId: membership.id,
|
|
94
|
+
allowedBlackboardTopicIds: unique(membership.topicIds || ["*"]),
|
|
95
|
+
allowedWriteOperations: ["message", "context", "artifact"],
|
|
96
|
+
allowedCandidateOperations: [],
|
|
97
|
+
allowedJudgeOperations: [],
|
|
98
|
+
sandboxProfileHints: [],
|
|
99
|
+
requiredEvidenceRefs: [],
|
|
100
|
+
deniedOperations: [],
|
|
101
|
+
}),
|
|
102
|
+
id: `${membership.id}-policy`,
|
|
103
|
+
policyRef: `multiAgent.memberships.${membership.id}.policy`,
|
|
104
|
+
subjectKind: "membership",
|
|
105
|
+
subjectId: membership.id,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
/** The pure decision half of `authorizeMultiAgentAction`: resolves the
|
|
109
|
+
* policy (membership -> role -> group, in that priority order) and
|
|
110
|
+
* evaluates denied ops / topic scope / op-class membership / required
|
|
111
|
+
* evidence. Audit recording is NOT done here (shell/multi-agent-io.ts's
|
|
112
|
+
* wrapper does that, injecting the resolved policy + reason into
|
|
113
|
+
* recordTrustAuditEvent calls byte-identical to the old build's). */
|
|
114
|
+
function evaluateMultiAgentAction(policy, operation, topicId, evidenceRefs) {
|
|
115
|
+
const reason = evaluatePolicy(policy, operation, topicId, evidenceRefs);
|
|
116
|
+
const allowed = !reason;
|
|
117
|
+
return {
|
|
118
|
+
allowed,
|
|
119
|
+
decision: allowed ? "allowed" : "denied",
|
|
120
|
+
reason: reason || "allowed by explicit multi-agent policy",
|
|
121
|
+
policyRef: policy?.policyRef,
|
|
122
|
+
policy,
|
|
123
|
+
missingEvidenceRefs: missingEvidence(policy, operation, evidenceRefs),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
/** Resolves the effective policy for an action: membership wins, else
|
|
127
|
+
* role, else group — same priority chain as the old build's
|
|
128
|
+
* `resolvePolicy`. Callers supply lookup functions so this stays pure
|
|
129
|
+
* (no direct WorkflowRun/state coupling beyond what's passed in). */
|
|
130
|
+
function resolvePolicy(input, lookups) {
|
|
131
|
+
const membership = input.membershipId ? lookups.membership?.(input.membershipId) : undefined;
|
|
132
|
+
if (membership?.policy)
|
|
133
|
+
return membership.policy;
|
|
134
|
+
const roleId = input.roleId || membership?.roleId;
|
|
135
|
+
const role = roleId ? lookups.role?.(roleId) : undefined;
|
|
136
|
+
if (role?.policy)
|
|
137
|
+
return role.policy;
|
|
138
|
+
if (role)
|
|
139
|
+
return policyForRole(role);
|
|
140
|
+
const group = input.groupId ? lookups.group?.(input.groupId) : undefined;
|
|
141
|
+
if (group?.policy)
|
|
142
|
+
return group.policy;
|
|
143
|
+
if (group)
|
|
144
|
+
return policyForGroup(group);
|
|
145
|
+
return undefined;
|
|
146
|
+
}
|
|
147
|
+
function evaluatePolicy(policy, operation, topicId, evidenceRefs) {
|
|
148
|
+
if (!policy)
|
|
149
|
+
return "missing role authority or policy";
|
|
150
|
+
const denied = policy.deniedOperations.find((entry) => entry.operation === operation);
|
|
151
|
+
if (denied)
|
|
152
|
+
return denied.reason;
|
|
153
|
+
if (topicId && policy.allowedBlackboardTopicIds.length && !policy.allowedBlackboardTopicIds.includes("*") && !policy.allowedBlackboardTopicIds.includes(topicId)) {
|
|
154
|
+
return `topic ${topicId} is outside policy ${policy.policyRef}`;
|
|
155
|
+
}
|
|
156
|
+
if (operation.startsWith("candidate.")) {
|
|
157
|
+
const op = operation.slice("candidate.".length);
|
|
158
|
+
if (!policy.allowedCandidateOperations.includes(op))
|
|
159
|
+
return `candidate operation ${op} is outside policy ${policy.policyRef}`;
|
|
160
|
+
}
|
|
161
|
+
else if (operation.startsWith("judge.")) {
|
|
162
|
+
const op = operation.slice("judge.".length);
|
|
163
|
+
if (!policy.allowedJudgeOperations.includes(op))
|
|
164
|
+
return `judge operation ${op} is outside policy ${policy.policyRef}`;
|
|
165
|
+
}
|
|
166
|
+
else if (!policy.allowedWriteOperations.includes(operation)) {
|
|
167
|
+
return `blackboard write operation ${operation} is outside policy ${policy.policyRef}`;
|
|
168
|
+
}
|
|
169
|
+
const missing = missingEvidence(policy, operation, evidenceRefs);
|
|
170
|
+
if (missing.length)
|
|
171
|
+
return `operation ${operation} requires evidence refs: ${missing.join(", ")}`;
|
|
172
|
+
return undefined;
|
|
173
|
+
}
|
|
174
|
+
function missingEvidence(policy, operation, evidenceRefs) {
|
|
175
|
+
if (!policy)
|
|
176
|
+
return [];
|
|
177
|
+
const required = unique([...(policy.requiredEvidenceFor?.[operation] || [])]);
|
|
178
|
+
if (!required.length)
|
|
179
|
+
return [];
|
|
180
|
+
if (evidenceRefs.length)
|
|
181
|
+
return [];
|
|
182
|
+
return required;
|
|
183
|
+
}
|
|
184
|
+
/** no actor -> operator-recorded; worker -> host-attested; operator ->
|
|
185
|
+
* operator-recorded; runtime/coordinator/verifier -> runtime-derived;
|
|
186
|
+
* else cw-validated. */
|
|
187
|
+
function sourceForActor(actor) {
|
|
188
|
+
if (!actor)
|
|
189
|
+
return "operator-recorded";
|
|
190
|
+
if (actor.kind === "worker")
|
|
191
|
+
return "host-attested";
|
|
192
|
+
if (actor.kind === "operator")
|
|
193
|
+
return "operator-recorded";
|
|
194
|
+
if (actor.kind === "runtime" || actor.kind === "coordinator" || actor.kind === "verifier")
|
|
195
|
+
return "runtime-derived";
|
|
196
|
+
return "cw-validated";
|
|
197
|
+
}
|