cool-workflow 0.1.81 → 0.1.83
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 +2 -2
- package/.codex-plugin/plugin.json +4 -4
- package/README.md +125 -121
- 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/candidate-scoring.js +20 -26
- package/dist/capability-core.js +80 -93
- package/dist/capability-registry.js +30 -3
- package/dist/cli.js +12 -1
- package/dist/commit.js +217 -204
- package/dist/coordinator/util.js +6 -9
- package/dist/dispatch.js +11 -3
- package/dist/doctor.js +153 -0
- package/dist/evidence-reasoning.js +4 -1
- package/dist/execution-backend/agent.js +11 -48
- package/dist/execution-backend.js +11 -31
- package/dist/gates.js +48 -0
- package/dist/mcp-server.js +11 -0
- package/dist/multi-agent/helpers.js +6 -10
- package/dist/multi-agent/ids.js +20 -0
- package/dist/multi-agent-eval.js +27 -1
- package/dist/multi-agent-host.js +53 -21
- package/dist/multi-agent-operator-ux.js +2 -1
- package/dist/multi-agent-trust.js +5 -5
- package/dist/node-projection.js +59 -0
- package/dist/node-snapshot.js +8 -18
- package/dist/orchestrator/lifecycle-operations.js +22 -1
- package/dist/orchestrator.js +29 -2
- package/dist/reclamation/hash.js +72 -0
- package/dist/reclamation.js +31 -112
- package/dist/run-registry/queue.js +6 -7
- package/dist/run-registry.js +35 -24
- package/dist/scheduler.js +112 -57
- package/dist/topology.js +25 -4
- package/dist/trust-audit.js +70 -38
- package/dist/validation.js +328 -0
- package/dist/version.js +1 -1
- package/dist/worker-isolation.js +163 -58
- package/docs/agent-delegation-drive.7.md +90 -85
- package/docs/agent-framework.md +33 -32
- package/docs/candidate-scoring.7.md +26 -24
- package/docs/canonical-workflow-apps.7.md +40 -40
- package/docs/capability-topology-registry.7.md +24 -24
- package/docs/cli-mcp-parity.7.md +227 -154
- package/docs/contract-migration-tooling.7.md +49 -41
- package/docs/control-plane-scheduling.7.md +45 -40
- package/docs/coordinator-blackboard.7.md +30 -30
- package/docs/dogfood-one-real-repo.7.md +44 -44
- package/docs/durable-state-and-locking.7.md +35 -30
- package/docs/end-to-end-golden-path.7.md +29 -29
- package/docs/error-feedback.7.md +27 -27
- package/docs/evidence-adoption-reasoning-chain.7.md +63 -58
- package/docs/execution-backends.7.md +84 -79
- package/docs/getting-started.md +35 -18
- package/docs/index.md +3 -3
- package/docs/mcp-app-surface.7.md +64 -64
- package/docs/multi-agent-cli-mcp-surface.7.md +83 -77
- package/docs/multi-agent-eval-replay-harness.7.md +59 -54
- package/docs/multi-agent-operator-ux.7.md +70 -65
- package/docs/multi-agent-runtime-core.7.md +39 -39
- package/docs/multi-agent-topologies.7.md +24 -24
- package/docs/multi-agent-trust-policy-audit.7.md +38 -38
- package/docs/node-snapshot-diff-replay.7.md +26 -21
- package/docs/observability-cost-accounting.7.md +50 -45
- package/docs/operator-ux.7.md +30 -30
- package/docs/pipeline-runner.7.md +31 -31
- package/docs/project-index.md +15 -6
- package/docs/real-execution-backends.7.md +47 -42
- package/docs/release-and-migration.7.md +43 -38
- package/docs/release-tooling.7.md +74 -39
- package/docs/routines.md +16 -16
- package/docs/run-registry-control-plane.7.md +120 -115
- package/docs/run-retention-reclamation.7.md +46 -41
- package/docs/sandbox-profiles.7.md +32 -32
- package/docs/scheduled-tasks.md +14 -14
- package/docs/security-trust-hardening.7.md +29 -29
- package/docs/source-context-profiles.7.md +28 -28
- package/docs/state-explosion-management.7.md +64 -59
- package/docs/state-node.7.md +8 -8
- package/docs/team-collaboration.7.md +63 -58
- package/docs/trust-model.md +126 -126
- package/docs/unix-principles.md +80 -80
- package/docs/vendor-manifest-loadability.7.md +20 -20
- package/docs/verifier-gated-commit.7.md +16 -16
- package/docs/web-desktop-workbench.7.md +70 -65
- package/docs/worker-isolation.7.md +34 -37
- package/docs/workflow-app-framework.7.md +38 -38
- package/manifest/plugin.manifest.json +4 -4
- package/manifest/source-context-profiles.json +1 -1
- package/package.json +3 -2
- package/scripts/canonical-apps.js +4 -4
- package/scripts/children/batch-delegate-child.js +58 -0
- package/scripts/children/http-delegate-child.js +39 -0
- package/scripts/dogfood-release.js +1 -1
- package/scripts/gen-parity-doc.js +106 -0
- package/scripts/golden-path.js +4 -4
- package/scripts/release-flow.js +181 -5
- package/dist/verifier-registry.js +0 -46
package/dist/run-registry.js
CHANGED
|
@@ -162,29 +162,36 @@ class RunRegistry {
|
|
|
162
162
|
return node_path_1.default.join(this.homeRoot, "registry");
|
|
163
163
|
}
|
|
164
164
|
// ---- per-repo overlays (plain files) ------------------------------------
|
|
165
|
+
// Overlay reads distinguish ABSENT (clean default) from PRESENT-but-corrupt
|
|
166
|
+
// (fail closed). readJson throws `Invalid JSON in <file>` on a present file
|
|
167
|
+
// that won't parse; we let that propagate instead of swallowing it. Swallowing
|
|
168
|
+
// is the absent-vs-corrupt conflation telemetry-ledger.ts flags as the bug that
|
|
169
|
+
// "let a corrupt overlay verify green" — here it would silently un-archive every
|
|
170
|
+
// archived run / drop every provenance link. This is authoritative durable state.
|
|
171
|
+
// A present overlay must parse to a JSON OBJECT. readJson already fails closed
|
|
172
|
+
// on unparseable bytes; this catches the next shape over: valid JSON that is
|
|
173
|
+
// `null`, an array, or a scalar. Without it `parsed.archived` throws a cryptic
|
|
174
|
+
// TypeError (null) or silently reads `undefined` (array) and the whole registry
|
|
175
|
+
// scan breaks. Fail closed with a clear message instead.
|
|
176
|
+
requireOverlayObject(parsed, file) {
|
|
177
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
178
|
+
throw new Error(`Corrupt overlay ${file}: expected a JSON object, got ${Array.isArray(parsed) ? "array" : parsed === null ? "null" : typeof parsed}`);
|
|
179
|
+
}
|
|
180
|
+
return parsed;
|
|
181
|
+
}
|
|
165
182
|
loadArchiveOverlay(repo) {
|
|
166
183
|
const file = node_path_1.default.join(this.repoRegistryDir(repo), "archive.json");
|
|
167
184
|
if (!node_fs_1.default.existsSync(file))
|
|
168
185
|
return { schemaVersion: 1, archived: {} };
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
return { schemaVersion: 1, archived: parsed.archived || {} };
|
|
172
|
-
}
|
|
173
|
-
catch {
|
|
174
|
-
return { schemaVersion: 1, archived: {} };
|
|
175
|
-
}
|
|
186
|
+
const parsed = this.requireOverlayObject((0, state_1.readJson)(file), file);
|
|
187
|
+
return { schemaVersion: 1, archived: parsed.archived || {} };
|
|
176
188
|
}
|
|
177
189
|
loadProvenanceOverlay(repo) {
|
|
178
190
|
const file = node_path_1.default.join(this.repoRegistryDir(repo), "provenance.json");
|
|
179
191
|
if (!node_fs_1.default.existsSync(file))
|
|
180
192
|
return { schemaVersion: 1, links: {} };
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
return { schemaVersion: 1, links: parsed.links || {} };
|
|
184
|
-
}
|
|
185
|
-
catch {
|
|
186
|
-
return { schemaVersion: 1, links: {} };
|
|
187
|
-
}
|
|
193
|
+
const parsed = this.requireOverlayObject((0, state_1.readJson)(file), file);
|
|
194
|
+
return { schemaVersion: 1, links: parsed.links || {} };
|
|
188
195
|
}
|
|
189
196
|
loadRepoOverlays(repo) {
|
|
190
197
|
return {
|
|
@@ -203,15 +210,14 @@ class RunRegistry {
|
|
|
203
210
|
}
|
|
204
211
|
loadRepos() {
|
|
205
212
|
const file = this.reposFilePath();
|
|
213
|
+
// Absent => no registered repos. Present-but-corrupt must fail closed: a
|
|
214
|
+
// swallowed parse error here silently drops every cross-repo root the
|
|
215
|
+
// operator registered, shrinking the home index to the current repo with no
|
|
216
|
+
// signal. Let readJson's `Invalid JSON` throw surface the corruption.
|
|
206
217
|
if (!node_fs_1.default.existsSync(file))
|
|
207
218
|
return { schemaVersion: 1, repos: [] };
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
return { schemaVersion: 1, repos: Array.isArray(parsed.repos) ? parsed.repos : [] };
|
|
211
|
-
}
|
|
212
|
-
catch {
|
|
213
|
-
return { schemaVersion: 1, repos: [] };
|
|
214
|
-
}
|
|
219
|
+
const parsed = this.requireOverlayObject((0, state_1.readJson)(file), file);
|
|
220
|
+
return { schemaVersion: 1, repos: Array.isArray(parsed.repos) ? parsed.repos : [] };
|
|
215
221
|
}
|
|
216
222
|
/** Persisted union of registered repo roots and the current repo, deduped and
|
|
217
223
|
* sorted. Read-only: does NOT write repos.json (reads stay pure). */
|
|
@@ -745,9 +751,14 @@ class RunRegistry {
|
|
|
745
751
|
// Record provenance in the per-repo overlay (derived metadata), NOT in the
|
|
746
752
|
// original run's source state — the past is never mutated.
|
|
747
753
|
const provFile = node_path_1.default.join(this.repoRegistryDir(original.repo), "provenance.json");
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
(
|
|
754
|
+
// Lock the read-modify-write: a concurrent rerun/archive on the same repo
|
|
755
|
+
// overlay would otherwise last-writer-wins and drop a provenance link. The
|
|
756
|
+
// sibling writers (registerRepo, archive) already serialize via withFileLock.
|
|
757
|
+
(0, state_1.withFileLock)(provFile, () => {
|
|
758
|
+
const provOverlay = this.loadProvenanceOverlay(original.repo);
|
|
759
|
+
provOverlay.links[newRun.id] = provenance;
|
|
760
|
+
(0, state_1.writeJson)(provFile, provOverlay, { durable: true });
|
|
761
|
+
});
|
|
751
762
|
return {
|
|
752
763
|
schemaVersion: 1,
|
|
753
764
|
originalRunId: runId,
|
package/dist/scheduler.js
CHANGED
|
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.Scheduler = void 0;
|
|
7
|
+
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
7
8
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
8
9
|
const node_path_1 = __importDefault(require("node:path"));
|
|
9
10
|
const state_1 = require("./state");
|
|
@@ -15,6 +16,16 @@ class Scheduler {
|
|
|
15
16
|
this.cwd = node_path_1.default.resolve(cwd);
|
|
16
17
|
this.storePath = node_path_1.default.join(this.cwd, ".cw", "schedules", "tasks.json");
|
|
17
18
|
}
|
|
19
|
+
// Every mutation is a cross-process read-modify-write of the one store file
|
|
20
|
+
// (the daemon polls `due` while CLI calls create/complete/delete concurrently).
|
|
21
|
+
// Without serialization two writers both load, both atomically rename their
|
|
22
|
+
// copy back, and the second silently clobbers the first's new task / status /
|
|
23
|
+
// history record. `locked` holds the same advisory lock the queue and
|
|
24
|
+
// reclamation stores already use. Reads (list/history) need no lock: the atomic
|
|
25
|
+
// rename means a reader always sees a whole old-or-new store.
|
|
26
|
+
locked(fn) {
|
|
27
|
+
return (0, state_1.withFileLock)(this.storePath, fn);
|
|
28
|
+
}
|
|
18
29
|
create(options) {
|
|
19
30
|
const kind = normalizeKind(options.kind);
|
|
20
31
|
const now = new Date();
|
|
@@ -42,23 +53,30 @@ class Scheduler {
|
|
|
42
53
|
maxRuns: numberOption(options.maxRuns),
|
|
43
54
|
runCount: 0
|
|
44
55
|
};
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
56
|
+
return this.locked(() => {
|
|
57
|
+
const store = this.load();
|
|
58
|
+
store.tasks.push(task);
|
|
59
|
+
this.save(store);
|
|
60
|
+
return task;
|
|
61
|
+
});
|
|
49
62
|
}
|
|
50
63
|
list(status) {
|
|
51
64
|
const store = this.load();
|
|
52
65
|
return status ? store.tasks.filter((task) => task.status === status) : store.tasks;
|
|
53
66
|
}
|
|
54
67
|
delete(id) {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
68
|
+
return this.locked(() => {
|
|
69
|
+
const store = this.load();
|
|
70
|
+
const before = store.tasks.length;
|
|
71
|
+
store.tasks = store.tasks.filter((task) => task.id !== id);
|
|
72
|
+
this.save(store);
|
|
73
|
+
return { deleted: store.tasks.length !== before, id };
|
|
74
|
+
});
|
|
60
75
|
}
|
|
61
76
|
due(now = new Date()) {
|
|
77
|
+
return this.locked(() => this.dueLocked(now));
|
|
78
|
+
}
|
|
79
|
+
dueLocked(now) {
|
|
62
80
|
const store = this.load();
|
|
63
81
|
let changed = false;
|
|
64
82
|
for (const task of store.tasks) {
|
|
@@ -86,63 +104,71 @@ class Scheduler {
|
|
|
86
104
|
return dueTasks;
|
|
87
105
|
}
|
|
88
106
|
complete(id, options = {}) {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
task.
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
107
|
+
return this.locked(() => {
|
|
108
|
+
const store = this.load();
|
|
109
|
+
const task = store.tasks.find((candidate) => candidate.id === id);
|
|
110
|
+
if (!task)
|
|
111
|
+
throw new Error(`Scheduled task not found: ${id}`);
|
|
112
|
+
const now = new Date();
|
|
113
|
+
task.runCount += 1;
|
|
114
|
+
task.lastRunAt = now.toISOString();
|
|
115
|
+
task.updatedAt = now.toISOString();
|
|
116
|
+
const maxRuns = numberOption(options.maxRuns) ?? task.maxRuns;
|
|
117
|
+
if (maxRuns !== undefined)
|
|
118
|
+
task.maxRuns = maxRuns;
|
|
119
|
+
if (task.kind === "reminder" || (task.maxRuns !== undefined && task.runCount >= task.maxRuns)) {
|
|
120
|
+
task.status = "completed";
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
task.nextRunAt = computeNextRunAt(task, now).toISOString();
|
|
124
|
+
}
|
|
125
|
+
this.save(store);
|
|
126
|
+
return task;
|
|
127
|
+
});
|
|
108
128
|
}
|
|
109
129
|
pause(id) {
|
|
110
130
|
return this.setStatus(id, "paused");
|
|
111
131
|
}
|
|
112
132
|
resume(id) {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
task.nextRunAt
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
133
|
+
return this.locked(() => {
|
|
134
|
+
const store = this.load();
|
|
135
|
+
const task = findTask(store, id);
|
|
136
|
+
const now = new Date();
|
|
137
|
+
task.status = "active";
|
|
138
|
+
task.updatedAt = now.toISOString();
|
|
139
|
+
if (new Date(task.nextRunAt).getTime() <= now.getTime()) {
|
|
140
|
+
task.nextRunAt = computeNextRunAt(task, now).toISOString();
|
|
141
|
+
}
|
|
142
|
+
this.save(store);
|
|
143
|
+
return task;
|
|
144
|
+
});
|
|
123
145
|
}
|
|
124
146
|
runNow(id) {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
147
|
+
return this.locked(() => {
|
|
148
|
+
const store = this.load();
|
|
149
|
+
const task = findTask(store, id);
|
|
150
|
+
const now = new Date();
|
|
151
|
+
task.lastDueAt = now.toISOString();
|
|
152
|
+
task.updatedAt = now.toISOString();
|
|
153
|
+
const record = createHistoryRecord(task, "started", this.cwd, now);
|
|
154
|
+
store.history.push(record);
|
|
155
|
+
this.save(store);
|
|
156
|
+
return record;
|
|
157
|
+
});
|
|
134
158
|
}
|
|
135
159
|
history(id) {
|
|
136
160
|
const store = this.load();
|
|
137
161
|
return id ? store.history.filter((record) => record.scheduleId === id) : store.history;
|
|
138
162
|
}
|
|
139
163
|
setStatus(id, status) {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
164
|
+
return this.locked(() => {
|
|
165
|
+
const store = this.load();
|
|
166
|
+
const task = findTask(store, id);
|
|
167
|
+
task.status = status;
|
|
168
|
+
task.updatedAt = new Date().toISOString();
|
|
169
|
+
this.save(store);
|
|
170
|
+
return task;
|
|
171
|
+
});
|
|
146
172
|
}
|
|
147
173
|
load() {
|
|
148
174
|
if (!node_fs_1.default.existsSync(this.storePath))
|
|
@@ -233,19 +259,48 @@ function matchesCron(value, expr, min, max) {
|
|
|
233
259
|
return Number.isInteger(parsed) && parsed >= min && parsed <= max && parsed === value;
|
|
234
260
|
});
|
|
235
261
|
}
|
|
262
|
+
// Deterministic jitter (replay-determinism self-audit): the jittered Date is NOT a
|
|
263
|
+
// runtime-only sleep — it lands in persisted/replayed state (task.nextRunAt and,
|
|
264
|
+
// transitively, the schedule store), so a Math.random() offset broke replay
|
|
265
|
+
// determinism. The spread (0..jitterSeconds) is now derived from a content hash of
|
|
266
|
+
// the base instant, so the same base time + jitter window always yields the same
|
|
267
|
+
// offset; distinct base times still spread out across the window. The base Date is
|
|
268
|
+
// itself an edge timestamp that is recorded once.
|
|
236
269
|
function addJitter(date, jitterSeconds) {
|
|
237
270
|
if (!jitterSeconds)
|
|
238
271
|
return date;
|
|
239
|
-
const
|
|
240
|
-
|
|
272
|
+
const digest = node_crypto_1.default.createHash("sha256").update(`${date.getTime()}`).digest();
|
|
273
|
+
const seconds = digest.readUInt32BE(0) % (jitterSeconds + 1);
|
|
274
|
+
return new Date(date.getTime() + seconds * 1000);
|
|
241
275
|
}
|
|
276
|
+
// Deterministic schedule id (replay-determinism self-audit): the stamp is an edge
|
|
277
|
+
// timestamp (recorded once), but the former Math.random() suffix made each
|
|
278
|
+
// persisted schedule id non-reproducible. The suffix is now a content hash of the
|
|
279
|
+
// schedule's deterministic identity (kind + the recorded stamp), so re-deriving the
|
|
280
|
+
// id for a recorded schedule yields the byte-identical value while schedules created
|
|
281
|
+
// at distinct instants still get distinct ids. Mirrors src/worker-isolation/paths.ts.
|
|
282
|
+
let scheduleIdSequence = 0;
|
|
242
283
|
function createScheduleId(kind) {
|
|
243
284
|
const stamp = new Date().toISOString().replace(/[-:]/g, "").replace(/\..+/, "Z");
|
|
244
|
-
|
|
285
|
+
// Second-resolution stamp: two schedules of the same kind created within one
|
|
286
|
+
// second would otherwise collide on an identical id. process.pid + a monotonic
|
|
287
|
+
// counter break the tie across concurrent processes and within one process,
|
|
288
|
+
// deterministically (not a PRNG).
|
|
289
|
+
scheduleIdSequence += 1;
|
|
290
|
+
const suffix = node_crypto_1.default.createHash("sha256").update(`${kind}:${stamp}:${process.pid}:${scheduleIdSequence}`).digest("hex").slice(0, 6);
|
|
291
|
+
return `${kind}-${stamp}-${suffix}`;
|
|
245
292
|
}
|
|
293
|
+
// Deterministic schedule-run (history) id — same rationale as createScheduleId. The
|
|
294
|
+
// history record stamp differs from the owning schedule's, so the hashed identity
|
|
295
|
+
// (kind + run stamp) stays distinct from the schedule id while remaining a pure
|
|
296
|
+
// function of already-recorded state.
|
|
297
|
+
let scheduleRunIdSequence = 0;
|
|
246
298
|
function createScheduleRunId(kind) {
|
|
247
299
|
const stamp = new Date().toISOString().replace(/[-:]/g, "").replace(/\..+/, "Z");
|
|
248
|
-
|
|
300
|
+
// pid + counter break same-kind/same-second collisions (see createScheduleId).
|
|
301
|
+
scheduleRunIdSequence += 1;
|
|
302
|
+
const suffix = node_crypto_1.default.createHash("sha256").update(`run:${kind}:${stamp}:${process.pid}:${scheduleRunIdSequence}`).digest("hex").slice(0, 6);
|
|
303
|
+
return `run-${kind}-${stamp}-${suffix}`;
|
|
249
304
|
}
|
|
250
305
|
function requiredString(value, name) {
|
|
251
306
|
const text = stringOption(value);
|
package/dist/topology.js
CHANGED
|
@@ -17,6 +17,8 @@ exports.showTopologyRun = showTopologyRun;
|
|
|
17
17
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
18
18
|
const node_path_1 = __importDefault(require("node:path"));
|
|
19
19
|
const state_1 = require("./state");
|
|
20
|
+
const execution_backend_1 = require("./execution-backend");
|
|
21
|
+
const telemetry_attestation_1 = require("./telemetry-attestation");
|
|
20
22
|
const pipeline_contract_1 = require("./pipeline-contract");
|
|
21
23
|
const state_node_1 = require("./state-node");
|
|
22
24
|
const trust_audit_1 = require("./trust-audit");
|
|
@@ -186,10 +188,16 @@ function applyTopology(run, topologyId, input = {}) {
|
|
|
186
188
|
const definition = validation.definition;
|
|
187
189
|
const state = ensureTopologyState(run);
|
|
188
190
|
(0, multi_agent_1.ensureMultiAgentState)(run);
|
|
189
|
-
const
|
|
191
|
+
const taskIds = selectedTaskIds(run, input.taskIds);
|
|
192
|
+
// Default id is a DETERMINISTIC content-hash (replay determinism): two `topology
|
|
193
|
+
// apply` invocations WITHOUT --id over the same definition/tasks/run produce a
|
|
194
|
+
// byte-identical id. Same sha256/stableStringify the kernel uses for node ids
|
|
195
|
+
// (state-node/createNodeId) and ledger record ids. `state.runs.length` is the
|
|
196
|
+
// stable sequence so repeated applies on the same run get distinct ids without a
|
|
197
|
+
// wall-clock stamp. input.id stays an explicit override.
|
|
198
|
+
const id = input.id || topologyRunId(definition, taskIds, run.id, state.runs.length);
|
|
190
199
|
if (state.runs.some((record) => record.id === id))
|
|
191
200
|
throw new Error(`Duplicate MultiAgentTopologyRun id: ${id}`);
|
|
192
|
-
const taskIds = selectedTaskIds(run, input.taskIds);
|
|
193
201
|
const board = (0, coordinator_1.resolveBlackboard)(run, {
|
|
194
202
|
id: input.blackboardId || `${id}-blackboard`,
|
|
195
203
|
title: `${definition.title} Blackboard`,
|
|
@@ -514,8 +522,21 @@ function statusToNodeStatus(status) {
|
|
|
514
522
|
function issue(code, message, path) {
|
|
515
523
|
return { code, message, path };
|
|
516
524
|
}
|
|
517
|
-
|
|
518
|
-
|
|
525
|
+
// Deterministic default topology-run id (F2, replay determinism): no wall-clock.
|
|
526
|
+
// Bound to a short sha256 of canonical content — definition id, the SORTED role and
|
|
527
|
+
// selected-task ids, the workflow run id, and a stable sequence (the count of
|
|
528
|
+
// topology runs already on this run). Same sha256/stableStringify the kernel uses
|
|
529
|
+
// for node ids and ledger record hashes, so two replays without --id reach a
|
|
530
|
+
// byte-identical fingerprint, while distinct applies on one run stay distinct.
|
|
531
|
+
function topologyRunId(definition, taskIds, runId, sequence) {
|
|
532
|
+
const digest = (0, execution_backend_1.sha256)((0, telemetry_attestation_1.stableStringify)({
|
|
533
|
+
definitionId: definition.id,
|
|
534
|
+
roleIds: [...definition.roles.map((role) => role.id)].sort(),
|
|
535
|
+
taskIds: [...taskIds].sort(),
|
|
536
|
+
runId,
|
|
537
|
+
sequence
|
|
538
|
+
}));
|
|
539
|
+
return `${definition.id}-${digest.replace("sha256:", "").slice(0, 16)}`;
|
|
519
540
|
}
|
|
520
541
|
function countBy(items, pick) {
|
|
521
542
|
const counts = {};
|
package/dist/trust-audit.js
CHANGED
|
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.TRUST_AUDIT_SCHEMA_VERSION = void 0;
|
|
6
|
+
exports.CORRELATION_ID_FIELDS = exports.TRUST_AUDIT_SCHEMA_VERSION = void 0;
|
|
7
7
|
exports.trustAuditGenesis = trustAuditGenesis;
|
|
8
8
|
exports.verifyTrustAudit = verifyTrustAudit;
|
|
9
9
|
exports.ensureTrustAudit = ensureTrustAudit;
|
|
@@ -152,6 +152,65 @@ function verifyTrustAudit(run) {
|
|
|
152
152
|
}
|
|
153
153
|
return { present: events.length > 0, verified, eventCount: events.length, chained, unchained, corruptLines, checks };
|
|
154
154
|
}
|
|
155
|
+
// ---- Correlation-id fields (single source of truth, F12) -------------------
|
|
156
|
+
// These id fields are plain pass-throughs that flow input -> persisted event ->
|
|
157
|
+
// summary index unchanged. They were previously re-listed by hand in three
|
|
158
|
+
// places (recordTrustAuditEvent, the index writer, and partially in
|
|
159
|
+
// multi-agent-trust), so adding a new correlation id meant editing every list and
|
|
160
|
+
// silently dropping it from the index when one was missed. They now live in ONE
|
|
161
|
+
// array; both spread sites pick from it via pickCorrelationIds, so a new id is
|
|
162
|
+
// added in exactly one place.
|
|
163
|
+
//
|
|
164
|
+
// SERIALIZATION-PRESERVING by design: this is a plain key-copy with NO transform,
|
|
165
|
+
// so the persisted JSON for these keys is byte-identical to the old hand-spread
|
|
166
|
+
// (compact() still drops the undefined ones). The hash chain that binds these
|
|
167
|
+
// fields is therefore unaffected. Fields needing derivation/normalization
|
|
168
|
+
// (feedbackIds, sandboxProfileId, policyRef, multiAgentPolicyRef, normalizedPath,
|
|
169
|
+
// envVars, …) are intentionally NOT here — they keep their bespoke handling at the
|
|
170
|
+
// call site.
|
|
171
|
+
exports.CORRELATION_ID_FIELDS = [
|
|
172
|
+
"candidateId",
|
|
173
|
+
"scoreId",
|
|
174
|
+
"selectionId",
|
|
175
|
+
"commitId",
|
|
176
|
+
"multiAgentRunId",
|
|
177
|
+
"agentRoleId",
|
|
178
|
+
"agentGroupId",
|
|
179
|
+
"agentMembershipId",
|
|
180
|
+
"agentFanoutId",
|
|
181
|
+
"agentFaninId",
|
|
182
|
+
"blackboardId",
|
|
183
|
+
"blackboardTopicId",
|
|
184
|
+
"blackboardMessageId",
|
|
185
|
+
"blackboardContextId",
|
|
186
|
+
"blackboardArtifactRefId",
|
|
187
|
+
"blackboardSnapshotId",
|
|
188
|
+
"coordinatorDecisionId",
|
|
189
|
+
"topologyId",
|
|
190
|
+
"topologyRunId"
|
|
191
|
+
];
|
|
192
|
+
/** Copy exactly the correlation-id keys (and no others) from `source`, preserving
|
|
193
|
+
* each value verbatim (including `undefined`, which compact()/JSON.stringify drop
|
|
194
|
+
* identically to the prior per-key spread). Spread the result; do not transform. */
|
|
195
|
+
function pickCorrelationIds(source) {
|
|
196
|
+
const picked = {};
|
|
197
|
+
for (const field of exports.CORRELATION_ID_FIELDS)
|
|
198
|
+
picked[field] = source[field];
|
|
199
|
+
return picked;
|
|
200
|
+
}
|
|
201
|
+
// The summary index has always carried every correlation id EXCEPT scoreId (the
|
|
202
|
+
// per-candidate score lives in the candidates[] rows, not the flat event index).
|
|
203
|
+
// Pin that exception in ONE place so the index stays byte-identical while still
|
|
204
|
+
// inheriting any NEW id from CORRELATION_ID_FIELDS automatically.
|
|
205
|
+
const INDEX_OMITTED_CORRELATION_IDS = new Set(["scoreId"]);
|
|
206
|
+
/** Correlation ids for the summary index: the full pick minus the keys the index
|
|
207
|
+
* has historically omitted (see INDEX_OMITTED_CORRELATION_IDS). */
|
|
208
|
+
function indexCorrelationIds(event) {
|
|
209
|
+
const picked = pickCorrelationIds(event);
|
|
210
|
+
for (const field of INDEX_OMITTED_CORRELATION_IDS)
|
|
211
|
+
delete picked[field];
|
|
212
|
+
return picked;
|
|
213
|
+
}
|
|
155
214
|
function ensureTrustAudit(run) {
|
|
156
215
|
const auditDir = auditRoot(run);
|
|
157
216
|
node_fs_1.default.mkdirSync(auditDir, { recursive: true });
|
|
@@ -177,25 +236,11 @@ function recordTrustAuditEvent(run, input) {
|
|
|
177
236
|
taskId: input.taskId,
|
|
178
237
|
nodeId: input.nodeId,
|
|
179
238
|
feedbackIds: input.feedbackIds?.filter(Boolean).sort(),
|
|
180
|
-
candidateId
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
agentRoleId: input.agentRoleId,
|
|
186
|
-
agentGroupId: input.agentGroupId,
|
|
187
|
-
agentMembershipId: input.agentMembershipId,
|
|
188
|
-
agentFanoutId: input.agentFanoutId,
|
|
189
|
-
agentFaninId: input.agentFaninId,
|
|
190
|
-
blackboardId: input.blackboardId,
|
|
191
|
-
blackboardTopicId: input.blackboardTopicId,
|
|
192
|
-
blackboardMessageId: input.blackboardMessageId,
|
|
193
|
-
blackboardContextId: input.blackboardContextId,
|
|
194
|
-
blackboardArtifactRefId: input.blackboardArtifactRefId,
|
|
195
|
-
blackboardSnapshotId: input.blackboardSnapshotId,
|
|
196
|
-
coordinatorDecisionId: input.coordinatorDecisionId,
|
|
197
|
-
topologyId: input.topologyId,
|
|
198
|
-
topologyRunId: input.topologyRunId,
|
|
239
|
+
// Plain correlation-id pass-throughs (candidateId … topologyRunId) come from the
|
|
240
|
+
// single CORRELATION_ID_FIELDS list. Key order is preserved (the list is in the
|
|
241
|
+
// same order the keys used to be hand-written), so the persisted JSON — and thus
|
|
242
|
+
// the eventHash chain — is byte-identical.
|
|
243
|
+
...pickCorrelationIds(input),
|
|
199
244
|
sandboxProfileId: input.sandboxProfileId || input.policySnapshot?.id,
|
|
200
245
|
policyRef: input.policyRef || (input.policySnapshot?.id ? `run.sandboxProfiles.${input.policySnapshot.id}` : undefined),
|
|
201
246
|
multiAgentPolicyRef: input.policyRef,
|
|
@@ -351,24 +396,11 @@ function summarizeTrustAudit(run) {
|
|
|
351
396
|
source: event.source,
|
|
352
397
|
workerId: event.workerId,
|
|
353
398
|
taskId: event.taskId,
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
agentGroupId: event.agentGroupId,
|
|
360
|
-
agentMembershipId: event.agentMembershipId,
|
|
361
|
-
agentFanoutId: event.agentFanoutId,
|
|
362
|
-
agentFaninId: event.agentFaninId,
|
|
363
|
-
blackboardId: event.blackboardId,
|
|
364
|
-
blackboardTopicId: event.blackboardTopicId,
|
|
365
|
-
blackboardMessageId: event.blackboardMessageId,
|
|
366
|
-
blackboardContextId: event.blackboardContextId,
|
|
367
|
-
blackboardArtifactRefId: event.blackboardArtifactRefId,
|
|
368
|
-
blackboardSnapshotId: event.blackboardSnapshotId,
|
|
369
|
-
coordinatorDecisionId: event.coordinatorDecisionId,
|
|
370
|
-
topologyId: event.topologyId,
|
|
371
|
-
topologyRunId: event.topologyRunId,
|
|
399
|
+
// Same single CORRELATION_ID_FIELDS list the record path uses, so the index
|
|
400
|
+
// can never silently omit a correlation id that the event carries. (The index
|
|
401
|
+
// historically omitted scoreId; that is preserved below by deleting it after
|
|
402
|
+
// the pick, keeping this writer's output byte-identical.)
|
|
403
|
+
...indexCorrelationIds(event),
|
|
372
404
|
sandboxProfileId: event.sandboxProfileId,
|
|
373
405
|
policyRef: event.policyRef,
|
|
374
406
|
multiAgentPolicyRef: event.multiAgentPolicyRef
|