cool-workflow 0.1.94 → 0.1.96
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 +4 -0
- package/apps/architecture-review/app.json +1 -1
- package/apps/architecture-review/workflow.js +3 -3
- package/apps/architecture-review-fast/app.json +1 -1
- package/apps/end-to-end-golden-path/app.json +1 -1
- package/apps/pdca-blackboard-loop/app.json +45 -0
- package/apps/pdca-blackboard-loop/workflow.js +59 -0
- 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/agent-config.js +2 -1
- package/dist/cli/command-surface.js +3 -1
- package/dist/dispatch.js +12 -6
- package/dist/evidence-grounding.js +18 -13
- package/dist/execution-backend/probes.js +22 -6
- package/dist/execution-backend.js +37 -4
- package/dist/node-snapshot.js +3 -3
- package/dist/orchestrator/lifecycle-operations.js +13 -5
- package/dist/orchestrator.js +28 -2
- package/dist/reclamation.js +8 -2
- package/dist/run-registry/derive.js +4 -1
- package/dist/scheduler.js +14 -14
- package/dist/schema-validate.js +8 -2
- package/dist/state-explosion/helpers.js +4 -21
- package/dist/state-explosion/size.js +63 -0
- package/dist/state-explosion.js +18 -71
- package/dist/state.js +47 -9
- package/dist/trust-audit.js +27 -2
- package/dist/util/fingerprint.js +19 -0
- package/dist/util/fingerprint.test.js +27 -0
- package/dist/version.js +1 -1
- package/dist/workbench-host.js +11 -0
- package/dist/workbench.js +19 -17
- package/dist/worker-isolation.js +25 -1
- package/docs/agent-delegation-drive.7.md +66 -1
- package/docs/cli-mcp-parity.7.md +4 -0
- package/docs/contract-migration-tooling.7.md +4 -0
- package/docs/control-plane-scheduling.7.md +4 -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/multi-agent-cli-mcp-surface.7.md +12 -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 +11 -4
- package/docs/real-execution-backends.7.md +4 -0
- package/docs/release-and-migration.7.md +4 -0
- package/docs/release-tooling.7.md +20 -0
- package/docs/routines.md +30 -0
- package/docs/run-registry-control-plane.7.md +4 -0
- package/docs/run-retention-reclamation.7.md +4 -0
- package/docs/sandbox-profiles.7.md +15 -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/package.json +8 -5
- package/scripts/agents/agent-adapter-core.js +22 -1
- package/scripts/agents/claude-p-agent.js +10 -2
- package/scripts/agents/codex-agent.js +22 -2
- package/scripts/agents/gemini-agent.js +10 -2
- package/scripts/agents/opencode-agent.js +10 -2
- package/scripts/bump-version.js +10 -0
- package/scripts/canonical-apps.js +4 -4
- package/scripts/dogfood-release.js +1 -1
- package/scripts/golden-path.js +4 -4
- package/scripts/release-check.js +7 -1
- package/scripts/release-flow.js +44 -3
- package/scripts/release-gate.sh +1 -1
- package/scripts/version-sync-check.js +2 -0
|
@@ -164,7 +164,10 @@ function loadReclaimedFromDir(runDir) {
|
|
|
164
164
|
return { schemaVersion: 1, runId: "", tombstones: [] };
|
|
165
165
|
try {
|
|
166
166
|
const parsed = JSON.parse(node_fs_1.default.readFileSync(file, "utf8"));
|
|
167
|
-
|
|
167
|
+
if (!parsed || typeof parsed !== "object" || parsed.schemaVersion !== 1 || !Array.isArray(parsed.tombstones)) {
|
|
168
|
+
return { schemaVersion: 1, runId: "", tombstones: [] };
|
|
169
|
+
}
|
|
170
|
+
return { schemaVersion: 1, runId: String(parsed.runId || ""), tombstones: parsed.tombstones };
|
|
168
171
|
}
|
|
169
172
|
catch {
|
|
170
173
|
return { schemaVersion: 1, runId: "", tombstones: [] };
|
package/dist/scheduler.js
CHANGED
|
@@ -274,21 +274,19 @@ function addJitter(date, jitterSeconds) {
|
|
|
274
274
|
return new Date(date.getTime() + seconds * 1000);
|
|
275
275
|
}
|
|
276
276
|
// Deterministic schedule id (replay-determinism self-audit): the stamp is an edge
|
|
277
|
-
// timestamp (recorded once)
|
|
278
|
-
//
|
|
279
|
-
// schedule
|
|
280
|
-
//
|
|
281
|
-
// at distinct instants still get distinct ids. Mirrors src/worker-isolation/paths.ts.
|
|
277
|
+
// timestamp (recorded once). Set CW_DETERMINISTIC_RUN_IDS=1 to use a
|
|
278
|
+
// content-hash-based id without wall-clock, so re-deriving the id for a recorded
|
|
279
|
+
// schedule yields the byte-identical value. Distinct instants still get distinct
|
|
280
|
+
// ids via the monotonic counter.
|
|
282
281
|
let scheduleIdSequence = 0;
|
|
283
282
|
function createScheduleId(kind) {
|
|
284
283
|
const stamp = new Date().toISOString().replace(/[-:]/g, "").replace(/\..+/, "Z");
|
|
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
284
|
scheduleIdSequence += 1;
|
|
290
|
-
const
|
|
291
|
-
|
|
285
|
+
const deterministic = /^(1|true|yes|on)$/i.test(process.env.CW_DETERMINISTIC_RUN_IDS || "");
|
|
286
|
+
const suffix = node_crypto_1.default.createHash("sha256")
|
|
287
|
+
.update(deterministic ? `${kind}:${process.pid}:${scheduleIdSequence}` : `${kind}:${stamp}:${process.pid}:${scheduleIdSequence}`)
|
|
288
|
+
.digest("hex").slice(0, 6);
|
|
289
|
+
return deterministic ? `${kind}-${suffix}` : `${kind}-${stamp}-${suffix}`;
|
|
292
290
|
}
|
|
293
291
|
// Deterministic schedule-run (history) id — same rationale as createScheduleId. The
|
|
294
292
|
// history record stamp differs from the owning schedule's, so the hashed identity
|
|
@@ -297,10 +295,12 @@ function createScheduleId(kind) {
|
|
|
297
295
|
let scheduleRunIdSequence = 0;
|
|
298
296
|
function createScheduleRunId(kind) {
|
|
299
297
|
const stamp = new Date().toISOString().replace(/[-:]/g, "").replace(/\..+/, "Z");
|
|
300
|
-
// pid + counter break same-kind/same-second collisions (see createScheduleId).
|
|
301
298
|
scheduleRunIdSequence += 1;
|
|
302
|
-
const
|
|
303
|
-
|
|
299
|
+
const deterministic = /^(1|true|yes|on)$/i.test(process.env.CW_DETERMINISTIC_RUN_IDS || "");
|
|
300
|
+
const suffix = node_crypto_1.default.createHash("sha256")
|
|
301
|
+
.update(deterministic ? `run:${kind}:${process.pid}:${scheduleRunIdSequence}` : `run:${kind}:${stamp}:${process.pid}:${scheduleRunIdSequence}`)
|
|
302
|
+
.digest("hex").slice(0, 6);
|
|
303
|
+
return deterministic ? `run-${kind}-${suffix}` : `run-${kind}-${stamp}-${suffix}`;
|
|
304
304
|
}
|
|
305
305
|
function requiredString(value, name) {
|
|
306
306
|
const text = stringOption(value);
|
package/dist/schema-validate.js
CHANGED
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
// type (object|array|string|number|integer|boolean|null, or an array of them),
|
|
13
13
|
// enum, const, required, properties, additionalProperties (false), items.
|
|
14
14
|
// Unsupported keywords ($ref, allOf/anyOf/oneOf, pattern, formats, numeric
|
|
15
|
-
// bounds) are
|
|
16
|
-
//
|
|
15
|
+
// bounds) are surfaced as WARNINGS (a constraint that wasn't checked), so the
|
|
16
|
+
// operator can see their schema reliance is incomplete.
|
|
17
17
|
// (If full JSON Schema is needed later, swap this module's impl for ajv behind
|
|
18
18
|
// the same signature.)
|
|
19
19
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
@@ -52,6 +52,12 @@ function validateAgainstSchema(value, schema, path = "$") {
|
|
|
52
52
|
const errors = [];
|
|
53
53
|
if (!schema || typeof schema !== "object")
|
|
54
54
|
return errors;
|
|
55
|
+
// Unsupported keywords — surface as diagnostics (Rule of Silence: stderr)
|
|
56
|
+
const UNSUPPORTED = new Set(["$ref", "allOf", "anyOf", "oneOf", "not", "pattern", "format", "minimum", "maximum", "minLength", "maxLength", "minItems", "maxItems", "uniqueItems", "contains", "if", "then", "else"]);
|
|
57
|
+
const unsupported = Object.keys(schema).filter((k) => UNSUPPORTED.has(k));
|
|
58
|
+
if (unsupported.length && process.stderr.isTTY) {
|
|
59
|
+
process.stderr.write(`[cw] schema at ${path}: unsupported keywords ignored: ${unsupported.join(", ")}\n`);
|
|
60
|
+
}
|
|
55
61
|
// type
|
|
56
62
|
if (schema.type !== undefined) {
|
|
57
63
|
const types = Array.isArray(schema.type) ? schema.type.map(String) : [String(schema.type)];
|
|
@@ -1,13 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.fingerprintStrings = exports.fingerprintRecords = void 0;
|
|
6
4
|
exports.isProtectedStatus = isProtectedStatus;
|
|
7
5
|
exports.dominantStatus = dominantStatus;
|
|
8
6
|
exports.parentMap = parentMap;
|
|
9
|
-
exports.fingerprintRecords = fingerprintRecords;
|
|
10
|
-
exports.fingerprintStrings = fingerprintStrings;
|
|
11
7
|
exports.stableLine = stableLine;
|
|
12
8
|
exports.sortKeys = sortKeys;
|
|
13
9
|
exports.stripRunId = stripRunId;
|
|
@@ -15,14 +11,9 @@ exports.unique = unique;
|
|
|
15
11
|
exports.byId = byId;
|
|
16
12
|
exports.truncate = truncate;
|
|
17
13
|
exports.slug = slug;
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
// so the report/graph/digest builders no longer bundle the primitive helper
|
|
22
|
-
// layer. Nothing here touches run state beyond its arguments; every function is
|
|
23
|
-
// pure (`fingerprintStrings` is re-exported from state-explosion.ts to keep the
|
|
24
|
-
// public surface byte-identical for importers).
|
|
25
|
-
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
14
|
+
const fingerprint_1 = require("../util/fingerprint");
|
|
15
|
+
Object.defineProperty(exports, "fingerprintRecords", { enumerable: true, get: function () { return fingerprint_1.fingerprintRecords; } });
|
|
16
|
+
Object.defineProperty(exports, "fingerprintStrings", { enumerable: true, get: function () { return fingerprint_1.fingerprintStrings; } });
|
|
26
17
|
function isProtectedStatus(status) {
|
|
27
18
|
return ["failed", "blocked", "rejected", "conflicting"].includes(status);
|
|
28
19
|
}
|
|
@@ -41,14 +32,6 @@ function parentMap(edges) {
|
|
|
41
32
|
}
|
|
42
33
|
return parents;
|
|
43
34
|
}
|
|
44
|
-
function fingerprintRecords(records) {
|
|
45
|
-
return fingerprintStrings(records.map((r) => `${r.id}:${r.status || ""}`).sort());
|
|
46
|
-
}
|
|
47
|
-
function fingerprintStrings(values) {
|
|
48
|
-
const hash = node_crypto_1.default.createHash("sha256");
|
|
49
|
-
hash.update(JSON.stringify([...values].sort()));
|
|
50
|
-
return `sha256:${hash.digest("hex").slice(0, 32)}`;
|
|
51
|
-
}
|
|
52
35
|
function stableLine(value) {
|
|
53
36
|
return JSON.stringify(sortKeys(value));
|
|
54
37
|
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DEFAULT_STATE_EXPLOSION_THRESHOLDS = exports.STATE_EXPLOSION_SCHEMA_VERSION = void 0;
|
|
4
|
+
exports.computeStateSize = computeStateSize;
|
|
5
|
+
exports.computeStateSizeWithGraph = computeStateSizeWithGraph;
|
|
6
|
+
const multi_agent_operator_ux_1 = require("../multi-agent-operator-ux");
|
|
7
|
+
exports.STATE_EXPLOSION_SCHEMA_VERSION = 1;
|
|
8
|
+
exports.DEFAULT_STATE_EXPLOSION_THRESHOLDS = {
|
|
9
|
+
graphNodes: 40,
|
|
10
|
+
graphEdges: 60,
|
|
11
|
+
blackboardMessages: 25,
|
|
12
|
+
blackboardRecords: 40,
|
|
13
|
+
collapseBucket: 6,
|
|
14
|
+
totalRecords: 80
|
|
15
|
+
};
|
|
16
|
+
function computeStateSize(run, thresholds = exports.DEFAULT_STATE_EXPLOSION_THRESHOLDS) {
|
|
17
|
+
return computeStateSizeWithGraph(run, thresholds, (0, multi_agent_operator_ux_1.buildMultiAgentOperatorGraph)(run));
|
|
18
|
+
}
|
|
19
|
+
function computeStateSizeWithGraph(run, thresholds, graph) {
|
|
20
|
+
const ma = run.multiAgent || { runs: [], roles: [], groups: [], memberships: [], fanouts: [], fanins: [] };
|
|
21
|
+
const bb = run.blackboard || { topics: [], messages: [], contexts: [], artifacts: [], snapshots: [], decisions: [] };
|
|
22
|
+
const counts = {
|
|
23
|
+
multiAgentRuns: (ma.runs || []).length,
|
|
24
|
+
roles: (ma.roles || []).length,
|
|
25
|
+
groups: (ma.groups || []).length,
|
|
26
|
+
memberships: (ma.memberships || []).length,
|
|
27
|
+
fanouts: (ma.fanouts || []).length,
|
|
28
|
+
fanins: (ma.fanins || []).length,
|
|
29
|
+
topics: (bb.topics || []).length,
|
|
30
|
+
messages: (bb.messages || []).length,
|
|
31
|
+
contexts: (bb.contexts || []).length,
|
|
32
|
+
artifacts: (bb.artifacts || []).length,
|
|
33
|
+
snapshots: (bb.snapshots || []).length,
|
|
34
|
+
decisions: (bb.decisions || []).length,
|
|
35
|
+
graphNodes: graph.nodes.length,
|
|
36
|
+
graphEdges: graph.edges.length
|
|
37
|
+
};
|
|
38
|
+
const total = counts.multiAgentRuns +
|
|
39
|
+
counts.roles +
|
|
40
|
+
counts.groups +
|
|
41
|
+
counts.memberships +
|
|
42
|
+
counts.fanouts +
|
|
43
|
+
counts.fanins +
|
|
44
|
+
counts.topics +
|
|
45
|
+
counts.messages +
|
|
46
|
+
counts.contexts +
|
|
47
|
+
counts.artifacts +
|
|
48
|
+
counts.snapshots +
|
|
49
|
+
counts.decisions;
|
|
50
|
+
const reasons = [];
|
|
51
|
+
if (counts.graphNodes > thresholds.graphNodes)
|
|
52
|
+
reasons.push(`graph has ${counts.graphNodes} nodes (> ${thresholds.graphNodes})`);
|
|
53
|
+
if (counts.graphEdges > thresholds.graphEdges)
|
|
54
|
+
reasons.push(`graph has ${counts.graphEdges} edges (> ${thresholds.graphEdges})`);
|
|
55
|
+
if (counts.messages > thresholds.blackboardMessages)
|
|
56
|
+
reasons.push(`blackboard has ${counts.messages} messages (> ${thresholds.blackboardMessages})`);
|
|
57
|
+
const bbRecords = counts.topics + counts.messages + counts.contexts + counts.artifacts + counts.snapshots + counts.decisions;
|
|
58
|
+
if (bbRecords > thresholds.blackboardRecords)
|
|
59
|
+
reasons.push(`blackboard has ${bbRecords} records (> ${thresholds.blackboardRecords})`);
|
|
60
|
+
if (total > thresholds.totalRecords)
|
|
61
|
+
reasons.push(`run has ${total} multi-agent records (> ${thresholds.totalRecords})`);
|
|
62
|
+
return { ...counts, total, compactionRecommended: reasons.length > 0, reasons: reasons.sort() };
|
|
63
|
+
}
|
package/dist/state-explosion.js
CHANGED
|
@@ -3,8 +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.stateExplosionReportLines = exports.formatBlackboardDigest = exports.formatCompactGraph = exports.formatStateExplosionReport = exports.fingerprintStrings = exports.GRAPH_VIEWS = exports.DEFAULT_STATE_EXPLOSION_THRESHOLDS = exports.
|
|
7
|
-
exports.computeStateSize = computeStateSize;
|
|
6
|
+
exports.stateExplosionReportLines = exports.formatBlackboardDigest = exports.formatCompactGraph = exports.formatStateExplosionReport = exports.fingerprintStrings = exports.GRAPH_VIEWS = exports.STATE_EXPLOSION_SCHEMA_VERSION = exports.DEFAULT_STATE_EXPLOSION_THRESHOLDS = exports.computeStateSizeWithGraph = exports.computeStateSize = void 0;
|
|
8
7
|
exports.summarizeBlackboardDigest = summarizeBlackboardDigest;
|
|
9
8
|
exports.buildCompactGraph = buildCompactGraph;
|
|
10
9
|
exports.buildStateExplosionReport = buildStateExplosionReport;
|
|
@@ -21,15 +20,11 @@ const multi_agent_operator_ux_1 = require("./multi-agent-operator-ux");
|
|
|
21
20
|
const trust_audit_1 = require("./trust-audit");
|
|
22
21
|
const evidence_reasoning_1 = require("./evidence-reasoning");
|
|
23
22
|
const helpers_1 = require("./state-explosion/helpers");
|
|
24
|
-
|
|
25
|
-
exports.
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
blackboardRecords: 40,
|
|
30
|
-
collapseBucket: 6,
|
|
31
|
-
totalRecords: 80
|
|
32
|
-
};
|
|
23
|
+
const size_1 = require("./state-explosion/size");
|
|
24
|
+
Object.defineProperty(exports, "computeStateSize", { enumerable: true, get: function () { return size_1.computeStateSize; } });
|
|
25
|
+
Object.defineProperty(exports, "computeStateSizeWithGraph", { enumerable: true, get: function () { return size_1.computeStateSizeWithGraph; } });
|
|
26
|
+
Object.defineProperty(exports, "DEFAULT_STATE_EXPLOSION_THRESHOLDS", { enumerable: true, get: function () { return size_1.DEFAULT_STATE_EXPLOSION_THRESHOLDS; } });
|
|
27
|
+
Object.defineProperty(exports, "STATE_EXPLOSION_SCHEMA_VERSION", { enumerable: true, get: function () { return size_1.STATE_EXPLOSION_SCHEMA_VERSION; } });
|
|
33
28
|
exports.GRAPH_VIEWS = [
|
|
34
29
|
"full",
|
|
35
30
|
"compact",
|
|
@@ -79,65 +74,17 @@ function graphKey(view, options) {
|
|
|
79
74
|
view,
|
|
80
75
|
options.focus || "",
|
|
81
76
|
options.depth === undefined ? "" : String(options.depth),
|
|
82
|
-
thresholdsKey(options.thresholds ||
|
|
77
|
+
thresholdsKey(options.thresholds || size_1.DEFAULT_STATE_EXPLOSION_THRESHOLDS)
|
|
83
78
|
].join("\0");
|
|
84
79
|
}
|
|
85
80
|
// ---------------------------------------------------------------------------
|
|
86
|
-
// State size
|
|
81
|
+
// State size (implementation in state-explosion/size.ts)
|
|
87
82
|
// ---------------------------------------------------------------------------
|
|
88
|
-
function computeStateSize(run, thresholds = exports.DEFAULT_STATE_EXPLOSION_THRESHOLDS) {
|
|
89
|
-
return computeStateSizeWithGraph(run, thresholds, (0, multi_agent_operator_ux_1.buildMultiAgentOperatorGraph)(run));
|
|
90
|
-
}
|
|
91
|
-
function computeStateSizeWithGraph(run, thresholds, graph) {
|
|
92
|
-
const ma = run.multiAgent || { runs: [], roles: [], groups: [], memberships: [], fanouts: [], fanins: [] };
|
|
93
|
-
const bb = run.blackboard || { topics: [], messages: [], contexts: [], artifacts: [], snapshots: [], decisions: [] };
|
|
94
|
-
const counts = {
|
|
95
|
-
multiAgentRuns: (ma.runs || []).length,
|
|
96
|
-
roles: (ma.roles || []).length,
|
|
97
|
-
groups: (ma.groups || []).length,
|
|
98
|
-
memberships: (ma.memberships || []).length,
|
|
99
|
-
fanouts: (ma.fanouts || []).length,
|
|
100
|
-
fanins: (ma.fanins || []).length,
|
|
101
|
-
topics: (bb.topics || []).length,
|
|
102
|
-
messages: (bb.messages || []).length,
|
|
103
|
-
contexts: (bb.contexts || []).length,
|
|
104
|
-
artifacts: (bb.artifacts || []).length,
|
|
105
|
-
snapshots: (bb.snapshots || []).length,
|
|
106
|
-
decisions: (bb.decisions || []).length,
|
|
107
|
-
graphNodes: graph.nodes.length,
|
|
108
|
-
graphEdges: graph.edges.length
|
|
109
|
-
};
|
|
110
|
-
const total = counts.multiAgentRuns +
|
|
111
|
-
counts.roles +
|
|
112
|
-
counts.groups +
|
|
113
|
-
counts.memberships +
|
|
114
|
-
counts.fanouts +
|
|
115
|
-
counts.fanins +
|
|
116
|
-
counts.topics +
|
|
117
|
-
counts.messages +
|
|
118
|
-
counts.contexts +
|
|
119
|
-
counts.artifacts +
|
|
120
|
-
counts.snapshots +
|
|
121
|
-
counts.decisions;
|
|
122
|
-
const reasons = [];
|
|
123
|
-
if (counts.graphNodes > thresholds.graphNodes)
|
|
124
|
-
reasons.push(`graph has ${counts.graphNodes} nodes (> ${thresholds.graphNodes})`);
|
|
125
|
-
if (counts.graphEdges > thresholds.graphEdges)
|
|
126
|
-
reasons.push(`graph has ${counts.graphEdges} edges (> ${thresholds.graphEdges})`);
|
|
127
|
-
if (counts.messages > thresholds.blackboardMessages)
|
|
128
|
-
reasons.push(`blackboard has ${counts.messages} messages (> ${thresholds.blackboardMessages})`);
|
|
129
|
-
const bbRecords = counts.topics + counts.messages + counts.contexts + counts.artifacts + counts.snapshots + counts.decisions;
|
|
130
|
-
if (bbRecords > thresholds.blackboardRecords)
|
|
131
|
-
reasons.push(`blackboard has ${bbRecords} records (> ${thresholds.blackboardRecords})`);
|
|
132
|
-
if (total > thresholds.totalRecords)
|
|
133
|
-
reasons.push(`run has ${total} multi-agent records (> ${thresholds.totalRecords})`);
|
|
134
|
-
return { ...counts, total, compactionRecommended: reasons.length > 0, reasons: reasons.sort() };
|
|
135
|
-
}
|
|
136
83
|
function stateSizeFor(run, thresholds, context) {
|
|
137
84
|
const key = thresholdsKey(thresholds);
|
|
138
85
|
let size = context.stateSizes.get(key);
|
|
139
86
|
if (!size) {
|
|
140
|
-
size = computeStateSizeWithGraph(run, thresholds, fullGraphFor(run, context));
|
|
87
|
+
size = (0, size_1.computeStateSizeWithGraph)(run, thresholds, fullGraphFor(run, context));
|
|
141
88
|
context.stateSizes.set(key, size);
|
|
142
89
|
}
|
|
143
90
|
return size;
|
|
@@ -321,7 +268,7 @@ function summarizeBlackboardDigest(run, blackboardId) {
|
|
|
321
268
|
]);
|
|
322
269
|
const fingerprint = (0, helpers_1.fingerprintRecords)([...topics, ...messages, ...contexts, ...artifacts, ...decisions]);
|
|
323
270
|
return {
|
|
324
|
-
schemaVersion:
|
|
271
|
+
schemaVersion: size_1.STATE_EXPLOSION_SCHEMA_VERSION,
|
|
325
272
|
runId: run.id,
|
|
326
273
|
id: `blackboard-digest${boardId ? `:${boardId}` : ""}`,
|
|
327
274
|
scope: "blackboard",
|
|
@@ -368,7 +315,7 @@ function buildCompactGraph(run, view = "compact", options = {}) {
|
|
|
368
315
|
return buildCompactGraphWithContext(run, view, options, createStateExplosionBuildContext());
|
|
369
316
|
}
|
|
370
317
|
function buildCompactGraphWithContext(run, view, options, context) {
|
|
371
|
-
const thresholds = options.thresholds ||
|
|
318
|
+
const thresholds = options.thresholds || size_1.DEFAULT_STATE_EXPLOSION_THRESHOLDS;
|
|
372
319
|
const key = graphKey(view, { ...options, thresholds });
|
|
373
320
|
const cached = context.graphRecords.get(key);
|
|
374
321
|
if (cached)
|
|
@@ -517,7 +464,7 @@ function finalizeGraphRecord(run, view, options, full, built) {
|
|
|
517
464
|
...built.syntheticNodes.filter((s) => s.blockedReason).map((s) => s.blockedReason)
|
|
518
465
|
]);
|
|
519
466
|
return {
|
|
520
|
-
schemaVersion:
|
|
467
|
+
schemaVersion: size_1.STATE_EXPLOSION_SCHEMA_VERSION,
|
|
521
468
|
runId: run.id,
|
|
522
469
|
id: `graph-${view}${options.focus ? `:focus:${(0, helpers_1.slug)(options.focus)}` : ""}`,
|
|
523
470
|
scope: "run",
|
|
@@ -754,7 +701,7 @@ function buildOperatorDigestWithContext(run, thresholds, context) {
|
|
|
754
701
|
...compact.syntheticNodes.map((syn) => syn.expansionCommand)
|
|
755
702
|
]);
|
|
756
703
|
return {
|
|
757
|
-
schemaVersion:
|
|
704
|
+
schemaVersion: size_1.STATE_EXPLOSION_SCHEMA_VERSION,
|
|
758
705
|
runId: run.id,
|
|
759
706
|
id: "operator-digest",
|
|
760
707
|
scope: "run",
|
|
@@ -817,7 +764,7 @@ function buildStateExplosionReport(run, options = {}) {
|
|
|
817
764
|
return buildStateExplosionReportWithContext(run, options, createStateExplosionBuildContext());
|
|
818
765
|
}
|
|
819
766
|
function buildStateExplosionReportWithContext(run, options, context) {
|
|
820
|
-
const thresholds = options.thresholds ||
|
|
767
|
+
const thresholds = options.thresholds || size_1.DEFAULT_STATE_EXPLOSION_THRESHOLDS;
|
|
821
768
|
const stateSize = stateSizeFor(run, thresholds, context);
|
|
822
769
|
const compactGraph = buildCompactGraphWithContext(run, "compact", { thresholds }, context);
|
|
823
770
|
const criticalPathGraph = buildCompactGraphWithContext(run, "critical-path", { thresholds }, context);
|
|
@@ -847,7 +794,7 @@ function buildStateExplosionReportWithContext(run, options, context) {
|
|
|
847
794
|
? `node scripts/cw.js summary refresh ${run.id}`
|
|
848
795
|
: operatorDigest.nextAction;
|
|
849
796
|
return {
|
|
850
|
-
schemaVersion:
|
|
797
|
+
schemaVersion: size_1.STATE_EXPLOSION_SCHEMA_VERSION,
|
|
851
798
|
runId: run.id,
|
|
852
799
|
generatedAt: new Date().toISOString(),
|
|
853
800
|
stateSize,
|
|
@@ -887,7 +834,7 @@ function currentEntryFingerprint(run, entry, records) {
|
|
|
887
834
|
* BSD: mechanism (check + refresh); policy (when to call) is at the call site. */
|
|
888
835
|
function maybeCompactRun(run) {
|
|
889
836
|
try {
|
|
890
|
-
const size = computeStateSize(run);
|
|
837
|
+
const size = (0, size_1.computeStateSize)(run);
|
|
891
838
|
if (size.compactionRecommended) {
|
|
892
839
|
refreshStateExplosionSummaries(run);
|
|
893
840
|
}
|
|
@@ -900,7 +847,7 @@ function summariesDir(run) {
|
|
|
900
847
|
return node_path_1.default.join(run.paths.runDir, "summaries");
|
|
901
848
|
}
|
|
902
849
|
function refreshStateExplosionSummaries(run, options = {}) {
|
|
903
|
-
const thresholds = options.thresholds ||
|
|
850
|
+
const thresholds = options.thresholds || size_1.DEFAULT_STATE_EXPLOSION_THRESHOLDS;
|
|
904
851
|
const context = createStateExplosionBuildContext();
|
|
905
852
|
const dir = summariesDir(run);
|
|
906
853
|
node_fs_1.default.mkdirSync(dir, { recursive: true });
|
|
@@ -923,7 +870,7 @@ function refreshStateExplosionSummaries(run, options = {}) {
|
|
|
923
870
|
const compactGraph = buildCompactGraphWithContext(run, "compact", { thresholds }, context);
|
|
924
871
|
const reportPath = node_path_1.default.join(dir, "state-explosion-report.json");
|
|
925
872
|
const index = {
|
|
926
|
-
schemaVersion:
|
|
873
|
+
schemaVersion: size_1.STATE_EXPLOSION_SCHEMA_VERSION,
|
|
927
874
|
runId: run.id,
|
|
928
875
|
id: "multi-agent-summary-index",
|
|
929
876
|
scope: "run",
|
package/dist/state.js
CHANGED
|
@@ -70,6 +70,7 @@ function ensureRunDirs(paths) {
|
|
|
70
70
|
function loadRunFromCwd(runId, cwd = process.cwd()) {
|
|
71
71
|
if (!runId)
|
|
72
72
|
throw new Error("Missing run id");
|
|
73
|
+
assertSafeRunId(runId);
|
|
73
74
|
const statePath = node_path_1.default.join(cwd, ".cw", "runs", runId, "state.json");
|
|
74
75
|
const result = loadRunStateFile(statePath, { dryRun: true });
|
|
75
76
|
if (result.report.status === "unsupported") {
|
|
@@ -98,8 +99,11 @@ function migrateRunStateFile(statePath, options = {}) {
|
|
|
98
99
|
}
|
|
99
100
|
function saveCheckpoint(run) {
|
|
100
101
|
run.updatedAt = new Date().toISOString();
|
|
101
|
-
// state.json is the single source of truth — write it DURABLY
|
|
102
|
-
|
|
102
|
+
// state.json is the single source of truth — write it DURABLY with a lock
|
|
103
|
+
// so concurrent processes never lose an update (v0.1.95).
|
|
104
|
+
withFileLock(run.paths.state, () => {
|
|
105
|
+
writeJson(run.paths.state, run, { durable: true });
|
|
106
|
+
});
|
|
103
107
|
}
|
|
104
108
|
/** Compact a run checkpoint by stripping empty optional arrays and null values
|
|
105
109
|
* that don't carry semantic meaning (v0.1.60). The normalization layer
|
|
@@ -245,6 +249,12 @@ function isContainedPath(candidate, allowed) {
|
|
|
245
249
|
// per-run reclamation chain) so a concurrent writer can never lose a record.
|
|
246
250
|
// O_EXCL (`wx`) is portable (no native flock); a stale holder is stolen so a
|
|
247
251
|
// crashed process can never wedge the store forever.
|
|
252
|
+
//
|
|
253
|
+
// v0.1.95: the lock mtime is refreshed immediately before fn() runs, and
|
|
254
|
+
// verified AFTER fn() returns — if another process stole the lock mid-operation
|
|
255
|
+
// (the stale check fired and it overwrote our lock), the holder refuses to
|
|
256
|
+
// release and throws. This guards long-running RMW operations (e.g. GC over a
|
|
257
|
+
// large run) against mid-operation theft.
|
|
248
258
|
// ---------------------------------------------------------------------------
|
|
249
259
|
const FILE_LOCK_STALE_MS = 30_000;
|
|
250
260
|
function sleepSync(ms) {
|
|
@@ -254,11 +264,12 @@ function sleepSync(ms) {
|
|
|
254
264
|
function withFileLock(targetPath, fn) {
|
|
255
265
|
const lock = `${targetPath}.lock`;
|
|
256
266
|
node_fs_1.default.mkdirSync(node_path_1.default.dirname(lock), { recursive: true });
|
|
267
|
+
const pid = String(process.pid);
|
|
257
268
|
let acquired = false;
|
|
258
269
|
for (let attempt = 0; attempt < 240 && !acquired; attempt++) {
|
|
259
270
|
try {
|
|
260
271
|
const fd = node_fs_1.default.openSync(lock, "wx");
|
|
261
|
-
node_fs_1.default.writeFileSync(fd, `${
|
|
272
|
+
node_fs_1.default.writeFileSync(fd, `${pid}@${new Date().toISOString()}\n`, "utf8");
|
|
262
273
|
node_fs_1.default.closeSync(fd);
|
|
263
274
|
acquired = true;
|
|
264
275
|
}
|
|
@@ -272,19 +283,45 @@ function withFileLock(targetPath, fn) {
|
|
|
272
283
|
}
|
|
273
284
|
}
|
|
274
285
|
catch {
|
|
275
|
-
continue;
|
|
286
|
+
continue;
|
|
276
287
|
}
|
|
277
288
|
sleepSync(25);
|
|
278
289
|
}
|
|
279
290
|
}
|
|
280
291
|
if (!acquired)
|
|
281
292
|
throw new Error(`could not acquire file lock for ${targetPath}`);
|
|
293
|
+
// Refresh mtime right before the critical section
|
|
282
294
|
try {
|
|
283
|
-
|
|
295
|
+
node_fs_1.default.utimesSync(lock, new Date(), new Date());
|
|
296
|
+
}
|
|
297
|
+
catch { /* best-effort */ }
|
|
298
|
+
try {
|
|
299
|
+
const result = fn();
|
|
300
|
+
// Verify lock was not stolen during fn(). The lock content may have changed
|
|
301
|
+
// if another process opened it with wx after stealing it. If our PID is not
|
|
302
|
+
// in the lock file, the lock was stolen — do NOT release the stolen lock
|
|
303
|
+
// (the thief owns it now, and releasing would corrupt its operation).
|
|
304
|
+
try {
|
|
305
|
+
const current = node_fs_1.default.readFileSync(lock, "utf8");
|
|
306
|
+
if (!current.startsWith(pid + "@")) {
|
|
307
|
+
throw new Error(`File lock for ${targetPath} was stolen during the critical section ` +
|
|
308
|
+
`(lock now owned by another process). The operation may have lost ` +
|
|
309
|
+
`cross-process isolation — increase FILE_LOCK_STALE_MS or split the work.`);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
catch (checkError) {
|
|
313
|
+
if (checkError instanceof Error && checkError.message.includes("stolen"))
|
|
314
|
+
throw checkError;
|
|
315
|
+
/* lock vanished — another process already released it, nothing to clean up */
|
|
316
|
+
}
|
|
317
|
+
return result;
|
|
284
318
|
}
|
|
285
319
|
finally {
|
|
286
320
|
try {
|
|
287
|
-
|
|
321
|
+
// Only release if we still own the lock
|
|
322
|
+
const current = node_fs_1.default.readFileSync(lock, "utf8");
|
|
323
|
+
if (current.startsWith(pid + "@"))
|
|
324
|
+
node_fs_1.default.rmSync(lock, { force: true });
|
|
288
325
|
}
|
|
289
326
|
catch {
|
|
290
327
|
/* releasing a missing lock is fine */
|
|
@@ -299,7 +336,8 @@ function safeFileName(value) {
|
|
|
299
336
|
* runs directory via a separator or a `..`/`.` component (path traversal). A
|
|
300
337
|
* real run id is `${workflowId}-${stamp}-${suffix}` (createRunId), and a
|
|
301
338
|
* workflow id is alnum-bounded [a-z0-9.-] (validateWorkflowId) — all within
|
|
302
|
-
* [A-Za-z0-9._
|
|
339
|
+
* [A-Za-z0-9._:-]. Sub-workflow ids also include `:` from the task id prefix.
|
|
340
|
+
* Because the charset already forbids any separator, the whole
|
|
303
341
|
* id is ONE path component, so the only values that could traverse are the
|
|
304
342
|
* components `.` and `..` themselves; an embedded `..` (e.g. a workflow id like
|
|
305
343
|
* `v1..2`) is a safe directory name and is allowed. A separator, an absolute
|
|
@@ -309,8 +347,8 @@ function assertSafeRunId(value, context = "run id") {
|
|
|
309
347
|
if (typeof value !== "string" || value.length === 0) {
|
|
310
348
|
throw new Error(`Invalid ${context}: expected a non-empty string`);
|
|
311
349
|
}
|
|
312
|
-
if (!/^[A-Za-z0-9._
|
|
313
|
-
throw new Error(`Unsafe ${context}: ${JSON.stringify(value)} must be a single path segment ([A-Za-z0-9._
|
|
350
|
+
if (!/^[A-Za-z0-9._:-]+$/.test(value) || value === "." || value === "..") {
|
|
351
|
+
throw new Error(`Unsafe ${context}: ${JSON.stringify(value)} must be a single path segment ([A-Za-z0-9._:-], not '.' or '..')`);
|
|
314
352
|
}
|
|
315
353
|
return value;
|
|
316
354
|
}
|
package/dist/trust-audit.js
CHANGED
|
@@ -11,6 +11,8 @@ exports.recordTrustAuditEvent = recordTrustAuditEvent;
|
|
|
11
11
|
exports.recordSandboxPathDecision = recordSandboxPathDecision;
|
|
12
12
|
exports.recordSandboxPolicyDecision = recordSandboxPolicyDecision;
|
|
13
13
|
exports.recordHostAttestation = recordHostAttestation;
|
|
14
|
+
exports.setAuditEventCache = setAuditEventCache;
|
|
15
|
+
exports.clearAuditEventCache = clearAuditEventCache;
|
|
14
16
|
exports.listTrustAuditEvents = listTrustAuditEvents;
|
|
15
17
|
exports.searchAuditEvents = searchAuditEvents;
|
|
16
18
|
exports.summarizeTrustAudit = summarizeTrustAudit;
|
|
@@ -300,9 +302,25 @@ function recordHostAttestation(run, input) {
|
|
|
300
302
|
source: "host-attested"
|
|
301
303
|
});
|
|
302
304
|
}
|
|
305
|
+
// Per-request event log cache (v0.1.95). When set, readEvents returns
|
|
306
|
+
// memoized results keyed by event log path. Clears after each request.
|
|
307
|
+
let _eventLogCache = null;
|
|
308
|
+
function setAuditEventCache(cache) {
|
|
309
|
+
_eventLogCache = cache;
|
|
310
|
+
}
|
|
311
|
+
function clearAuditEventCache() {
|
|
312
|
+
_eventLogCache = null;
|
|
313
|
+
}
|
|
303
314
|
function listTrustAuditEvents(run) {
|
|
304
315
|
const audit = ensureTrustAudit(run);
|
|
305
|
-
|
|
316
|
+
if (_eventLogCache) {
|
|
317
|
+
const cached = _eventLogCache.get(audit.eventLogPath);
|
|
318
|
+
if (cached)
|
|
319
|
+
return cached;
|
|
320
|
+
}
|
|
321
|
+
const events = readEventsRaw(audit.eventLogPath).events.sort(compareEvents);
|
|
322
|
+
_eventLogCache?.set(audit.eventLogPath, events);
|
|
323
|
+
return events;
|
|
306
324
|
}
|
|
307
325
|
/** Search audit events by kind, worker, or candidate (v0.1.65).
|
|
308
326
|
* Filters are AND-ed; empty filters match all. */
|
|
@@ -515,7 +533,14 @@ function auditRoot(run) {
|
|
|
515
533
|
return run.paths.auditDir || node_path_1.default.join(run.paths.runDir, "audit");
|
|
516
534
|
}
|
|
517
535
|
function readEvents(eventLogPath) {
|
|
518
|
-
|
|
536
|
+
if (_eventLogCache) {
|
|
537
|
+
const cached = _eventLogCache.get(eventLogPath);
|
|
538
|
+
if (cached)
|
|
539
|
+
return cached;
|
|
540
|
+
}
|
|
541
|
+
const events = readEventsRaw(eventLogPath).events.sort(compareEvents);
|
|
542
|
+
_eventLogCache?.set(eventLogPath, events);
|
|
543
|
+
return events;
|
|
519
544
|
}
|
|
520
545
|
function workerRows(events, run) {
|
|
521
546
|
const workerIds = unique([...(run.workers || []).map((worker) => worker.id), ...events.map((event) => event.workerId || "")]).sort();
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.fingerprintStrings = fingerprintStrings;
|
|
7
|
+
exports.fingerprintRecords = fingerprintRecords;
|
|
8
|
+
// Deterministic content fingerprint — the single canonical implementation.
|
|
9
|
+
// Replaces duplicated copies in observability.ts and run-registry.ts (v0.1.95).
|
|
10
|
+
// Pure function of its arguments; never imports run state or high-level modules.
|
|
11
|
+
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
12
|
+
function fingerprintStrings(values) {
|
|
13
|
+
const hash = node_crypto_1.default.createHash("sha256");
|
|
14
|
+
hash.update(JSON.stringify([...values].sort()));
|
|
15
|
+
return `sha256:${hash.digest("hex").slice(0, 32)}`;
|
|
16
|
+
}
|
|
17
|
+
function fingerprintRecords(records) {
|
|
18
|
+
return fingerprintStrings(records.map((r) => `${r.id}:${r.status || ""}`).sort());
|
|
19
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
// Unit test for the canonical fingerprint utility (v0.1.95).
|
|
4
|
+
// Pure function — no run state or tmpdir needed.
|
|
5
|
+
const strict_1 = require("node:assert/strict");
|
|
6
|
+
const node_test_1 = require("node:test");
|
|
7
|
+
const fingerprint_1 = require("../util/fingerprint");
|
|
8
|
+
(0, node_test_1.test)("fingerprintStrings returns a sha256: prefix", () => {
|
|
9
|
+
const fp = (0, fingerprint_1.fingerprintStrings)(["a", "b"]);
|
|
10
|
+
(0, strict_1.ok)(fp.startsWith("sha256:"), "must have sha256: prefix");
|
|
11
|
+
(0, strict_1.equal)(fp.length, 32 + "sha256:".length, "must be 32 hex chars");
|
|
12
|
+
});
|
|
13
|
+
(0, node_test_1.test)("fingerprintStrings is deterministic and order-independent", () => {
|
|
14
|
+
const a = (0, fingerprint_1.fingerprintStrings)(["b", "a", "c"]);
|
|
15
|
+
const b = (0, fingerprint_1.fingerprintStrings)(["c", "b", "a"]);
|
|
16
|
+
(0, strict_1.equal)(a, b, "same values in different order must produce same fingerprint");
|
|
17
|
+
});
|
|
18
|
+
(0, node_test_1.test)("fingerprintStrings produces distinct values for different inputs", () => {
|
|
19
|
+
const a = (0, fingerprint_1.fingerprintStrings)(["x"]);
|
|
20
|
+
const b = (0, fingerprint_1.fingerprintStrings)(["y"]);
|
|
21
|
+
(0, strict_1.ok)(a !== b, "different inputs must produce different fingerprints");
|
|
22
|
+
});
|
|
23
|
+
(0, node_test_1.test)("fingerprintRecords uses id:status sorted", () => {
|
|
24
|
+
const a = (0, fingerprint_1.fingerprintRecords)([{ id: "b", status: "ok" }, { id: "a", status: "fail" }]);
|
|
25
|
+
const b = (0, fingerprint_1.fingerprintRecords)([{ id: "a", status: "fail" }, { id: "b", status: "ok" }]);
|
|
26
|
+
(0, strict_1.equal)(a, b, "records in different order must produce same fingerprint");
|
|
27
|
+
});
|
package/dist/version.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.MIN_SUPPORTED_RUN_STATE_SCHEMA_VERSION = exports.LEGACY_RUN_STATE_SCHEMA_VERSION = exports.CURRENT_RUN_STATE_SCHEMA_VERSION = exports.WORKFLOW_APP_SCHEMA_VERSION = exports.CURRENT_COOL_WORKFLOW_VERSION = void 0;
|
|
4
|
-
exports.CURRENT_COOL_WORKFLOW_VERSION = "0.1.
|
|
4
|
+
exports.CURRENT_COOL_WORKFLOW_VERSION = "0.1.96";
|
|
5
5
|
exports.WORKFLOW_APP_SCHEMA_VERSION = 1;
|
|
6
6
|
exports.CURRENT_RUN_STATE_SCHEMA_VERSION = 1;
|
|
7
7
|
exports.LEGACY_RUN_STATE_SCHEMA_VERSION = 0;
|
package/dist/workbench-host.js
CHANGED
|
@@ -85,7 +85,18 @@ class WorkbenchHost {
|
|
|
85
85
|
if ((req.method || "GET").toUpperCase() !== "GET") {
|
|
86
86
|
return this.send(res, 405, { error: "read-only: only GET is permitted" }, { Allow: "GET" });
|
|
87
87
|
}
|
|
88
|
+
// Optional token auth (CW_WORKBENCH_TOKEN). When set, every request must
|
|
89
|
+
// carry the token as an Authorization: Bearer header or ?token= query param.
|
|
90
|
+
// Off by default — single-user loopback is already gated by the OS boundary.
|
|
88
91
|
const url = new URL(req.url || "/", `http://${this.host}`);
|
|
92
|
+
const requiredToken = (process.env.CW_WORKBENCH_TOKEN || "").trim();
|
|
93
|
+
if (requiredToken) {
|
|
94
|
+
const bearer = (req.headers.authorization || "").replace(/^Bearer\s+/i, "").trim();
|
|
95
|
+
const queryToken = url.searchParams.get("token") || "";
|
|
96
|
+
if (bearer !== requiredToken && queryToken !== requiredToken) {
|
|
97
|
+
return this.send(res, 401, { error: "unauthorized: token mismatch" });
|
|
98
|
+
}
|
|
99
|
+
}
|
|
89
100
|
const route = decodeURIComponent(url.pathname);
|
|
90
101
|
if (route === "/" || route === "/index.html")
|
|
91
102
|
return this.sendAsset(res, "index.html");
|