cool-workflow 0.1.96 → 0.1.97
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/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 +3 -3
- package/dist/capability-core.js +6 -1
- package/dist/cli/handlers/scheduling.js +7 -1
- package/dist/drive.js +10 -0
- package/dist/execution-backend/agent.js +1 -1
- package/dist/execution-backend.js +25 -5
- package/dist/mcp-server.js +4 -0
- package/dist/onramp.js +2 -0
- package/dist/orchestrator/app-operations.js +6 -0
- package/dist/orchestrator/cli-options.js +8 -2
- package/dist/orchestrator/lifecycle-operations.js +3 -0
- package/dist/orchestrator/migration-operations.js +1 -1
- package/dist/run-export.js +10 -1
- package/dist/sandbox-profile.js +6 -1
- package/dist/triggers.js +7 -1
- package/dist/version.js +1 -1
- package/dist/workbench-host.js +7 -1
- package/docs/agent-delegation-drive.7.md +2 -0
- package/docs/cli-mcp-parity.7.md +2 -0
- package/docs/contract-migration-tooling.7.md +2 -0
- package/docs/control-plane-scheduling.7.md +2 -0
- package/docs/demo.7.md +80 -0
- package/docs/doctor.7.md +97 -0
- package/docs/durable-state-and-locking.7.md +2 -0
- package/docs/evidence-adoption-reasoning-chain.7.md +2 -0
- package/docs/execution-backends.7.md +2 -0
- package/docs/fix.7.md +44 -0
- package/docs/init.7.md +62 -0
- package/docs/multi-agent-cli-mcp-surface.7.md +2 -0
- package/docs/multi-agent-eval-replay-harness.7.md +2 -0
- package/docs/multi-agent-operator-ux.7.md +2 -0
- package/docs/node-snapshot-diff-replay.7.md +2 -0
- package/docs/observability-cost-accounting.7.md +2 -0
- package/docs/pipeline-verbs.7.md +93 -0
- package/docs/project-index.md +16 -4
- package/docs/real-execution-backends.7.md +2 -0
- package/docs/release-and-migration.7.md +2 -0
- package/docs/release-tooling.7.md +2 -0
- package/docs/routine.7.md +73 -0
- package/docs/run-registry-control-plane.7.md +2 -0
- package/docs/run-retention-reclamation.7.md +2 -0
- package/docs/state-explosion-management.7.md +2 -0
- package/docs/team-collaboration.7.md +2 -0
- package/docs/web-desktop-workbench.7.md +2 -0
- package/manifest/README.md +16 -10
- package/manifest/plugin.manifest.json +1 -1
- package/package.json +1 -1
- package/scripts/agents/agent-adapter-core.js +4 -1
- package/scripts/canonical-apps.js +4 -4
- package/scripts/children/batch-delegate-child.js +10 -3
- package/scripts/children/http-delegate-child.js +2 -1
- package/scripts/dogfood-release.js +1 -1
- package/scripts/golden-path.js +4 -4
- package/scripts/release-flow.js +24 -16
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cool-workflow",
|
|
3
3
|
"description": "A workflow control plane and run-time you are able to check: it sends out jobs in TypeScript, makes certain of work against facts before it goes through, puts state into fixed records, orders jobs by time, runs jobs again and again, gets a group of agents to do their parts together, and talks MCP. It gives the doing of the work to outside agents — it never runs the models itself.",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.97",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "COOLWHITE LLC"
|
|
7
7
|
},
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cool-workflow",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.97",
|
|
4
4
|
"description": "A workflow control plane and run-time you are able to check: it sends out jobs in TypeScript, makes certain of work against facts before it goes through, puts state into fixed records, orders jobs by time, runs jobs again and again, gets a group of agents to do their parts together, and talks MCP. It gives the doing of the work to outside agents — it never runs the models itself.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "COOLWHITE LLC"
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"id": "architecture-review",
|
|
4
4
|
"title": "Architecture Review",
|
|
5
5
|
"summary": "Map a repository architecture, assess risks, verify important findings, and synthesize an evidence-backed verdict.",
|
|
6
|
-
"version": "0.1.
|
|
6
|
+
"version": "0.1.97",
|
|
7
7
|
"author": "COOLWHITE LLC",
|
|
8
8
|
"inputs": [
|
|
9
9
|
{
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"id": "architecture-review-fast",
|
|
4
4
|
"title": "Architecture Review Fast",
|
|
5
5
|
"summary": "Run a shorter architecture review with parallel map and assess phases for faster first results.",
|
|
6
|
-
"version": "0.1.
|
|
6
|
+
"version": "0.1.97",
|
|
7
7
|
"author": "COOLWHITE LLC",
|
|
8
8
|
"inputs": [
|
|
9
9
|
{
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"id": "pr-review-fix-ci",
|
|
4
4
|
"title": "PR Review Fix CI",
|
|
5
5
|
"summary": "Review a pull request or branch, inspect CI failures, diagnose actionable issues, optionally patch, verify, and summarize with evidence.",
|
|
6
|
-
"version": "0.1.
|
|
6
|
+
"version": "0.1.97",
|
|
7
7
|
"author": "COOLWHITE LLC",
|
|
8
8
|
"inputs": [
|
|
9
9
|
{
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"id": "release-cut",
|
|
4
4
|
"title": "Release Cut",
|
|
5
5
|
"summary": "Prepare a release with checklist discipline: version checks, changelog, tests, packaging, release notes, and final verification.",
|
|
6
|
-
"version": "0.1.
|
|
6
|
+
"version": "0.1.97",
|
|
7
7
|
"author": "COOLWHITE LLC",
|
|
8
8
|
"inputs": [
|
|
9
9
|
{
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"id": "research-synthesis",
|
|
4
4
|
"title": "Research Synthesis",
|
|
5
5
|
"summary": "Split a research question into claims, investigate sources, cross-check evidence, verify claims, and synthesize a concise answer.",
|
|
6
|
-
"version": "0.1.
|
|
6
|
+
"version": "0.1.97",
|
|
7
7
|
"author": "COOLWHITE LLC",
|
|
8
8
|
"inputs": [
|
|
9
9
|
{
|
|
@@ -115,7 +115,7 @@ function getCandidate(run, candidateId) {
|
|
|
115
115
|
// Fail-closed integrity boundary (F4/F5): validate the parsed record against
|
|
116
116
|
// its type def BEFORE upserting it as a trusted CandidateRecord. A corrupt or
|
|
117
117
|
// forged candidate.json must throw here rather than flow into the run.
|
|
118
|
-
const candidate = (0, validation_1.validateCandidateRecord)(
|
|
118
|
+
const candidate = (0, validation_1.validateCandidateRecord)((0, state_1.readJson)(file));
|
|
119
119
|
upsertCandidate(run, candidate);
|
|
120
120
|
return candidate;
|
|
121
121
|
}
|
|
@@ -537,7 +537,7 @@ function loadCandidatesFromDisk(run) {
|
|
|
537
537
|
// Fail-closed integrity boundary (F4/F5): each candidate.json is validated
|
|
538
538
|
// against CandidateRecord before it merges into the run; a corrupt record
|
|
539
539
|
// throws rather than entering the candidate set as a trusted cast.
|
|
540
|
-
.map((file) => (0, validation_1.validateCandidateRecord)(
|
|
540
|
+
.map((file) => (0, validation_1.validateCandidateRecord)((0, state_1.readJson)(file)));
|
|
541
541
|
}
|
|
542
542
|
function readScores(run, candidateId) {
|
|
543
543
|
const dir = node_path_1.default.join(candidateDir(run, candidateId), "scores");
|
|
@@ -550,7 +550,7 @@ function readScores(run, candidateId) {
|
|
|
550
550
|
// Fail-closed integrity boundary (F4/F5): a score file is validated against
|
|
551
551
|
// CandidateScore before it can feed ranking/selection. A corrupt score must
|
|
552
552
|
// throw, not silently widen the normalized/verdict surface the gate reads.
|
|
553
|
-
.map((file) => (0, validation_1.validateCandidateScore)(
|
|
553
|
+
.map((file) => (0, validation_1.validateCandidateScore)((0, state_1.readJson)(node_path_1.default.join(dir, file))));
|
|
554
554
|
}
|
|
555
555
|
function candidateArtifacts(run, candidate) {
|
|
556
556
|
return [
|
package/dist/capability-core.js
CHANGED
|
@@ -288,7 +288,12 @@ function runExportArchive(runner, runId, args) {
|
|
|
288
288
|
// offline. Default falls back to the same env the verify gate reads, so a single
|
|
289
289
|
// configured key both attests at record-time and travels with the export.
|
|
290
290
|
const trustPublicKey = optionalString(args["with-trust-key"] || args.withTrustKey || args.trustKey || args.pubkey) || process.env.CW_AGENT_ATTEST_PUBKEY;
|
|
291
|
-
|
|
291
|
+
const resolvedOutput = node_path_1.default.resolve(base, output);
|
|
292
|
+
const sysDirs = /^\/(etc|bin|sbin|usr|Library|System|Applications|boot|dev|proc|sys|root|var\/log|var\/run)\//;
|
|
293
|
+
if (sysDirs.test(resolvedOutput)) {
|
|
294
|
+
throw new Error(`Refusing to write archive to a system directory: ${output}`);
|
|
295
|
+
}
|
|
296
|
+
return (0, run_export_1.exportRun)(runner.withBaseDir(optionalString(args.cwd)).loadRun(runId), resolvedOutput, { trustPublicKey });
|
|
292
297
|
}
|
|
293
298
|
function runImportArchive(runner, args) {
|
|
294
299
|
const base = invocationCwd(args);
|
|
@@ -77,7 +77,13 @@ function handleRoutine(args, triggers) {
|
|
|
77
77
|
return;
|
|
78
78
|
case "fire": {
|
|
79
79
|
const kind = (0, io_1.required)(idOrKind, "trigger kind");
|
|
80
|
-
|
|
80
|
+
let payload;
|
|
81
|
+
try {
|
|
82
|
+
payload = payloadPath ? JSON.parse(node_fs_1.default.readFileSync(payloadPath, "utf8")) : args.options;
|
|
83
|
+
}
|
|
84
|
+
catch (e) {
|
|
85
|
+
throw new Error(`Failed to parse payload${payloadPath ? ` file "${payloadPath}"` : ""}: ${String(e && e.message || e)}`);
|
|
86
|
+
}
|
|
81
87
|
(0, io_1.printJson)(triggers.fire(kind, payload));
|
|
82
88
|
return;
|
|
83
89
|
}
|
package/dist/drive.js
CHANGED
|
@@ -475,6 +475,16 @@ function prepareConcurrentOutcomes(ctx, batch) {
|
|
|
475
475
|
continue;
|
|
476
476
|
const job = (0, execution_backend_1.prepareAgentSpawn)(buildAgentRequest(ctx, run, task, manifest));
|
|
477
477
|
if (job) {
|
|
478
|
+
const sandboxPolicy = manifest.sandboxPolicy;
|
|
479
|
+
if (sandboxPolicy) {
|
|
480
|
+
const filteredEnv = (0, execution_backend_1.buildChildEnv)(sandboxPolicy);
|
|
481
|
+
for (const key of Object.keys(process.env)) {
|
|
482
|
+
if (/^(CW_|ANTHROPIC_|OPENAI_|GEMINI_|DEEPSEEK_|CODEX_|GOOGLE_|COHERE_|MISTRAL_|OLLAMA_|AZURE_|AWS_)/i.test(key)) {
|
|
483
|
+
filteredEnv[key] = process.env[key];
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
job.env = filteredEnv;
|
|
487
|
+
}
|
|
478
488
|
jobs.push(job);
|
|
479
489
|
jobTaskIds.push(taskId);
|
|
480
490
|
}
|
|
@@ -276,7 +276,7 @@ function runAgentBatchOutcomes(jobs) {
|
|
|
276
276
|
const child = (0, node_child_process_1.spawnSync)(process.execPath, [BATCH_DELEGATE_CHILD_SCRIPT], {
|
|
277
277
|
input: JSON.stringify(jobs),
|
|
278
278
|
encoding: "utf8",
|
|
279
|
-
maxBuffer: 33 * 1024 * 1024 * jobs.length,
|
|
279
|
+
maxBuffer: Math.min(33 * 1024 * 1024 * jobs.length, 512 * 1024 * 1024),
|
|
280
280
|
timeout: maxTimeout + 30000
|
|
281
281
|
});
|
|
282
282
|
if (!child.error && typeof child.status === "number" && child.status === 0) {
|
|
@@ -39,6 +39,7 @@ exports.attestSandbox = attestSandbox;
|
|
|
39
39
|
exports.probeBackend = probeBackend;
|
|
40
40
|
exports.runBackend = runBackend;
|
|
41
41
|
exports.delegateChildScript = delegateChildScript;
|
|
42
|
+
exports.shouldStreamAgentStderr = shouldStreamAgentStderr;
|
|
42
43
|
exports.createExecutionBackend = createExecutionBackend;
|
|
43
44
|
exports.backendListPayload = backendListPayload;
|
|
44
45
|
exports.backendShowPayload = backendShowPayload;
|
|
@@ -410,7 +411,7 @@ function executeLocal(descriptor, policy, request, label, attestation) {
|
|
|
410
411
|
// Shell injection guard: reject args that contain shell control characters
|
|
411
412
|
// (beyond template placeholders). The command itself is operator-configured.
|
|
412
413
|
const shellArg = [command, ...args].join(" ").replace(/\{\{[a-zA-Z0-9_.-]+\}\}/g, "");
|
|
413
|
-
if (/[;&|`$(){}<>!\n\r]/.test(shellArg)) {
|
|
414
|
+
if (/[;&|`$(){}<>!\n\r#*?~]/.test(shellArg)) {
|
|
414
415
|
throw new Error(`Shell backend refused: args contain shell control characters. ` +
|
|
415
416
|
`Use the node, bun, or agent backend instead for untrusted inputs.`);
|
|
416
417
|
}
|
|
@@ -675,6 +676,25 @@ function runHttpDelegation(descriptor, policy, request, label, handle, attestati
|
|
|
675
676
|
// agentSubstitutions, substituteAgentArg, recordedAgentHandle, extractEndpointResult,
|
|
676
677
|
// agentHandle) and the concurrent batch live in ./execution-backend/agent.ts; the
|
|
677
678
|
// stateful runners below build the refusal/delegated envelopes and stay here.
|
|
679
|
+
/** Decide whether cw FORWARDS the agent wrapper's live stderr view (stdio
|
|
680
|
+
* "inherit") or captures it ("pipe"). Default follows isTTY — interactive shows
|
|
681
|
+
* the live view, a pipe/CI stays silent (Rule of Silence). The two env knobs are
|
|
682
|
+
* explicit opt-out/opt-in, honored regardless of isTTY so a piped/CI run can still
|
|
683
|
+
* opt into the wrapper's plain append-only trace, exactly as the man page promises
|
|
684
|
+
* (agent-delegation-drive.7.md "Live output"):
|
|
685
|
+
* CW_NO_STREAM=1 — master off (wins over everything)
|
|
686
|
+
* CW_AGENT_STREAM=0 — off
|
|
687
|
+
* CW_AGENT_STREAM=1 — on, even without a TTY
|
|
688
|
+
* With no env set this returns isTTY — byte-identical to the prior inline gate (POLA). */
|
|
689
|
+
function shouldStreamAgentStderr(env, isTTY) {
|
|
690
|
+
if (env.CW_NO_STREAM === "1")
|
|
691
|
+
return false;
|
|
692
|
+
if (env.CW_AGENT_STREAM === "0")
|
|
693
|
+
return false;
|
|
694
|
+
if (env.CW_AGENT_STREAM === "1")
|
|
695
|
+
return true;
|
|
696
|
+
return isTTY;
|
|
697
|
+
}
|
|
678
698
|
function runAgentProcess(descriptor, policy, request, label, handle, attestation) {
|
|
679
699
|
const resolved = (0, agent_1.resolveAgentInvocation)(request);
|
|
680
700
|
const subst = (0, agent_1.agentSubstitutions)(request, resolved.model);
|
|
@@ -692,10 +712,10 @@ function runAgentProcess(descriptor, policy, request, label, handle, attestation
|
|
|
692
712
|
outcome = request.preparedAgentOutcome;
|
|
693
713
|
}
|
|
694
714
|
else {
|
|
695
|
-
// Live output on by default when stderr is a TTY. stdout is always
|
|
696
|
-
//
|
|
697
|
-
//
|
|
698
|
-
const streamStderr = process.env
|
|
715
|
+
// Live output on by default when stderr is a TTY. stdout is always captured
|
|
716
|
+
// as data. CI/pipes stay silent unless CW_AGENT_STREAM=1 opts them into the
|
|
717
|
+
// wrapper's plain append-only trace; CW_AGENT_STREAM=0 / CW_NO_STREAM=1 force off.
|
|
718
|
+
const streamStderr = shouldStreamAgentStderr(process.env, Boolean(process.stderr.isTTY));
|
|
699
719
|
// Build child env from sandbox policy as baseline (respects env.inherit/expose/deny),
|
|
700
720
|
// then re-allow CW_* + well-known API key env vars the agent needs.
|
|
701
721
|
const childEnv = buildChildEnv(policy);
|
package/dist/mcp-server.js
CHANGED
|
@@ -35,6 +35,10 @@ function handleLine(line) {
|
|
|
35
35
|
sendError(null, -32700, `Parse error: ${messageOf(error)}`);
|
|
36
36
|
return;
|
|
37
37
|
}
|
|
38
|
+
if (message === null || typeof message !== "object" || Array.isArray(message)) {
|
|
39
|
+
sendError(null, -32600, "Invalid Request: not a JSON-RPC object");
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
38
42
|
try {
|
|
39
43
|
if (message.method === "initialize") {
|
|
40
44
|
sendResult(message.id, {
|
package/dist/onramp.js
CHANGED
|
@@ -386,6 +386,8 @@ function resolveBaseRef(root, changedFrom, env) {
|
|
|
386
386
|
return verifyRef(root, "HEAD");
|
|
387
387
|
}
|
|
388
388
|
function verifyRef(root, ref) {
|
|
389
|
+
if (ref.startsWith("-"))
|
|
390
|
+
throw new Error(`Invalid onramp base ref (must not start with '-'): ${ref}`);
|
|
389
391
|
const resolved = gitOne(root, ["rev-parse", "--verify", `${ref}^{commit}`]);
|
|
390
392
|
if (!resolved)
|
|
391
393
|
throw new Error(`Unknown onramp base ref: ${ref}`);
|
|
@@ -169,6 +169,12 @@ function initApp(appsDir, appId, options, resolveFromBase, validateApp) {
|
|
|
169
169
|
throw new Error("App id must include at least one letter or digit");
|
|
170
170
|
const title = String(options.title || titleize(id));
|
|
171
171
|
const destinationDir = resolveFromBase(String(options.directory || options.output || node_path_1.default.join(appsDir, id)));
|
|
172
|
+
// Reject writes to system-owned directories. The operator may provide any
|
|
173
|
+
// output path, but writing to /etc, /bin, /usr etc. is never valid.
|
|
174
|
+
const sysDirs = /^\/(etc|bin|sbin|usr|Library|System|Applications|boot|dev|proc|sys|root|var\/log|var\/run)\//;
|
|
175
|
+
if (sysDirs.test(node_path_1.default.resolve(destinationDir))) {
|
|
176
|
+
throw new Error(`Refusing to create app in a system directory: ${destinationDir}`);
|
|
177
|
+
}
|
|
172
178
|
const manifestPath = node_path_1.default.join(destinationDir, "app.json");
|
|
173
179
|
const entrypointPath = node_path_1.default.join(destinationDir, "workflow.js");
|
|
174
180
|
if (!options.force && (node_fs_1.default.existsSync(manifestPath) || node_fs_1.default.existsSync(entrypointPath))) {
|
|
@@ -112,8 +112,14 @@ function metadataOption(options) {
|
|
|
112
112
|
const raw = options.metadata;
|
|
113
113
|
if (raw && typeof raw === "object" && !Array.isArray(raw))
|
|
114
114
|
return raw;
|
|
115
|
-
if (typeof raw === "string")
|
|
116
|
-
|
|
115
|
+
if (typeof raw === "string") {
|
|
116
|
+
try {
|
|
117
|
+
return JSON.parse(raw);
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
throw new Error(`Invalid JSON in --metadata: ${String(raw).slice(0, 80)}`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
117
123
|
return undefined;
|
|
118
124
|
}
|
|
119
125
|
function withoutHostRunKeys(args) {
|
|
@@ -228,6 +228,9 @@ function recordResult(run, taskId, resultPath, options = {}) {
|
|
|
228
228
|
try {
|
|
229
229
|
(0, verifier_1.assertTaskCanComplete)(run, task);
|
|
230
230
|
const absoluteResultPath = node_path_1.default.resolve(resultPath);
|
|
231
|
+
if (/^\/(etc|bin|sbin|usr|Library|System|Applications|boot|dev|proc|sys|root|var\/log|var\/run)\//.test(absoluteResultPath)) {
|
|
232
|
+
throw new Error(`Result path must not be a system directory: ${resultPath}`);
|
|
233
|
+
}
|
|
231
234
|
if (!node_fs_1.default.existsSync(absoluteResultPath)) {
|
|
232
235
|
throw new Error(`Result file does not exist: ${absoluteResultPath}`);
|
|
233
236
|
}
|
|
@@ -40,5 +40,5 @@ function loadMigrationSnapshot(target, options) {
|
|
|
40
40
|
: node_path_1.default.join(process.cwd(), ".cw", "runs", target, "state.json");
|
|
41
41
|
if (!node_fs_1.default.existsSync(file))
|
|
42
42
|
throw new Error(`Migration target not found: ${target}`);
|
|
43
|
-
return { snapshot:
|
|
43
|
+
return { snapshot: (0, state_1.readJson)(file), contract, dir: node_path_1.default.dirname(file) };
|
|
44
44
|
}
|
package/dist/run-export.js
CHANGED
|
@@ -453,7 +453,16 @@ function verifyReportBundle(archivePath, options = {}) {
|
|
|
453
453
|
}
|
|
454
454
|
if (options.extractReportTo && reportContent !== undefined) {
|
|
455
455
|
reportExtractedTo = node_path_1.default.resolve(options.extractReportTo);
|
|
456
|
-
|
|
456
|
+
if (options.cwd) {
|
|
457
|
+
const baseCwd = node_path_1.default.resolve(options.cwd);
|
|
458
|
+
if (!(0, state_1.isContainedPath)(reportExtractedTo, baseCwd)) {
|
|
459
|
+
failedChecks.push({ name: "extract-report", code: "path-outside-working-directory" });
|
|
460
|
+
reportExtractedTo = undefined;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
if (reportExtractedTo) {
|
|
464
|
+
node_fs_1.default.writeFileSync(reportExtractedTo, reportContent);
|
|
465
|
+
}
|
|
457
466
|
}
|
|
458
467
|
}
|
|
459
468
|
catch (error) {
|
package/dist/sandbox-profile.js
CHANGED
|
@@ -132,7 +132,12 @@ function resolveSandboxProfileById(id, context = defaultSandboxContext()) {
|
|
|
132
132
|
// This closes the gap where `sandbox validate` accepted a custom profile that
|
|
133
133
|
// dispatch/worker-isolation then refused — validated but never enforceable.
|
|
134
134
|
// A non-bundled, non-file id still fails closed via showBundledSandboxProfile.
|
|
135
|
-
const absolute = node_path_1.default.resolve(requested);
|
|
135
|
+
const absolute = node_path_1.default.resolve(context.cwd, requested);
|
|
136
|
+
if (!absolute.startsWith(node_path_1.default.resolve(context.cwd) + node_path_1.default.sep) && absolute !== node_path_1.default.resolve(context.cwd)) {
|
|
137
|
+
throw new SandboxProfileError("sandbox-profile-path-escape", `Custom profile path traversal denied: ${requested}`, {
|
|
138
|
+
details: { requested }
|
|
139
|
+
});
|
|
140
|
+
}
|
|
136
141
|
if (node_fs_1.default.existsSync(absolute) && node_fs_1.default.statSync(absolute).isFile()) {
|
|
137
142
|
const result = validateSandboxProfileFile(requested, context);
|
|
138
143
|
if (!result.valid || !result.profile) {
|
package/dist/triggers.js
CHANGED
|
@@ -147,7 +147,13 @@ function parseJsonObject(value) {
|
|
|
147
147
|
return undefined;
|
|
148
148
|
if (typeof value === "object")
|
|
149
149
|
return value;
|
|
150
|
-
|
|
150
|
+
let parsed;
|
|
151
|
+
try {
|
|
152
|
+
parsed = JSON.parse(String(value));
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
throw new Error("Expected a JSON object, got invalid JSON");
|
|
156
|
+
}
|
|
151
157
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
152
158
|
throw new Error("Expected JSON object");
|
|
153
159
|
}
|
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.97";
|
|
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
|
@@ -24,6 +24,7 @@ exports.WorkbenchHost = void 0;
|
|
|
24
24
|
const node_http_1 = __importDefault(require("node:http"));
|
|
25
25
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
26
26
|
const node_path_1 = __importDefault(require("node:path"));
|
|
27
|
+
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
27
28
|
const workbench_1 = require("./workbench");
|
|
28
29
|
const ALLOWED_HOSTNAMES = new Set(["127.0.0.1", "localhost", "::1", "[::1]"]);
|
|
29
30
|
const CONTENT_TYPES = {
|
|
@@ -93,7 +94,12 @@ class WorkbenchHost {
|
|
|
93
94
|
if (requiredToken) {
|
|
94
95
|
const bearer = (req.headers.authorization || "").replace(/^Bearer\s+/i, "").trim();
|
|
95
96
|
const queryToken = url.searchParams.get("token") || "";
|
|
96
|
-
|
|
97
|
+
const tokenBuf = Buffer.from(requiredToken);
|
|
98
|
+
const bearerBuf = Buffer.from(bearer);
|
|
99
|
+
const queryBuf = Buffer.from(queryToken);
|
|
100
|
+
const tokenOk = bearerBuf.length === tokenBuf.length && node_crypto_1.default.timingSafeEqual(bearerBuf, tokenBuf);
|
|
101
|
+
const queryOk = queryBuf.length === tokenBuf.length && node_crypto_1.default.timingSafeEqual(queryBuf, tokenBuf);
|
|
102
|
+
if (!tokenOk && !queryOk) {
|
|
97
103
|
return this.send(res, 401, { error: "unauthorized: token mismatch" });
|
|
98
104
|
}
|
|
99
105
|
}
|
package/docs/cli-mcp-parity.7.md
CHANGED
package/docs/demo.7.md
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# DEMO(7)
|
|
2
|
+
|
|
3
|
+
## NAME
|
|
4
|
+
|
|
5
|
+
`cw demo` — prove CW trust guarantees with one command
|
|
6
|
+
|
|
7
|
+
## SYNOPSIS
|
|
8
|
+
|
|
9
|
+
```text
|
|
10
|
+
node dist/cli.js demo tamper [--json]
|
|
11
|
+
node dist/cli.js demo bundle [--json]
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## DESCRIPTION
|
|
15
|
+
|
|
16
|
+
`cw demo` is a self-contained proof of CW's central trust claims. It works
|
|
17
|
+
without an agent and without a network connection. Every run is hermetic
|
|
18
|
+
(fully self-contained) — it builds its own state, tampers with it in known
|
|
19
|
+
ways, and checks that the tampering is caught. Nothing is read from or
|
|
20
|
+
written to the real file system outside a short-term temp directory.
|
|
21
|
+
|
|
22
|
+
No agent is needed; both demos work when the setup has no agent at all.
|
|
23
|
+
|
|
24
|
+
## DEMO TAMPER
|
|
25
|
+
|
|
26
|
+
`cw demo tamper` proves that CW catches forged records offline — with only a
|
|
27
|
+
public key, no server. It:
|
|
28
|
+
|
|
29
|
+
1. Builds a signed telemetry ledger with three hops.
|
|
30
|
+
2. Tampers with it in three layers:
|
|
31
|
+
- **Hashes**: Changes a record's data and recomputes the record hash to hide
|
|
32
|
+
it. The hash chain breaks — the next record's `previousHash` does not
|
|
33
|
+
match, so the chain is no longer valid.
|
|
34
|
+
- **Signatures**: Inflates token counts and keeps the old signature. The
|
|
35
|
+
signature does not match the new data — the verifier catches it.
|
|
36
|
+
- **Findings**: Edits a signed finding (severity HIGH → LOW) after it was
|
|
37
|
+
signed by the agent. The signature check on the ed25519 envelope fails
|
|
38
|
+
because the signed bytes changed.
|
|
39
|
+
3. Verifies each tampered ledger with only the public key.
|
|
40
|
+
|
|
41
|
+
If all three forgeries are caught, the proof holds and the demo exits 0.
|
|
42
|
+
If any tamper goes undetected, the demo exits 1 — this is a regression in
|
|
43
|
+
the integrity guarantee.
|
|
44
|
+
|
|
45
|
+
## DEMO BUNDLE
|
|
46
|
+
|
|
47
|
+
`cw demo bundle` proves that exported report bundles are verifiable offline. It:
|
|
48
|
+
|
|
49
|
+
1. Builds a full telemetry chain, signs it, and exports a sealed portable
|
|
50
|
+
bundle (archive bytes + telemetry chain + trust-audit chain + embedded
|
|
51
|
+
public key).
|
|
52
|
+
2. Tampers with the bundle in two ways:
|
|
53
|
+
- **Telemetry chain**: Forges a record in the chain. The archive's file
|
|
54
|
+
digests stay valid (the archive was built from the tampered bytes), but
|
|
55
|
+
`report verify-bundle` re-checks the chain and catches it.
|
|
56
|
+
- **Signature + usage**: Inflates token counts and reseals. The signature
|
|
57
|
+
check and hash chain both break.
|
|
58
|
+
3. Verifies each tampered bundle with `report verify-bundle`.
|
|
59
|
+
|
|
60
|
+
If all forgeries are caught with only the bundle's own public key, the proof
|
|
61
|
+
holds. No repo, no server, no key handed over.
|
|
62
|
+
|
|
63
|
+
## EXIT CODES
|
|
64
|
+
|
|
65
|
+
| Exit | Meaning |
|
|
66
|
+
| --- | --- |
|
|
67
|
+
| 0 | All tampering was caught — trust guarantees hold |
|
|
68
|
+
| 1 | A tamper went undetected — integrity guarantee regression |
|
|
69
|
+
|
|
70
|
+
## FILES
|
|
71
|
+
|
|
72
|
+
```text
|
|
73
|
+
src/telemetry-demo.ts
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## SEE ALSO
|
|
77
|
+
|
|
78
|
+
report-verifiable-bundle.7.md — offline bundle verification in detail
|
|
79
|
+
trust-model.md — the trust model and its limits
|
|
80
|
+
security-trust-hardening.7.md — security and trust hardening
|
package/docs/doctor.7.md
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# DOCTOR(7)
|
|
2
|
+
|
|
3
|
+
## NAME
|
|
4
|
+
|
|
5
|
+
`cw doctor` — check the setup and name all problems with their fixes
|
|
6
|
+
|
|
7
|
+
## SYNOPSIS
|
|
8
|
+
|
|
9
|
+
```text
|
|
10
|
+
node dist/cli.js doctor
|
|
11
|
+
node dist/cli.js doctor --json
|
|
12
|
+
node dist/cli.js doctor --fix
|
|
13
|
+
node dist/cli.js doctor --onramp
|
|
14
|
+
node dist/cli.js doctor --onramp --changed-from origin/main
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## DESCRIPTION
|
|
18
|
+
|
|
19
|
+
`cw doctor` is a read-only check of your CW setup, based on `brew doctor`. It
|
|
20
|
+
probes your machine and says what is wrong and what to do about it — before a
|
|
21
|
+
run fails with a strange error.
|
|
22
|
+
|
|
23
|
+
The command never makes any file; it only reads. Running it changes nothing on
|
|
24
|
+
disk.
|
|
25
|
+
|
|
26
|
+
It gives back a report with one line for every check. Each check has a status
|
|
27
|
+
(`ok`, `warn`, or `fail`) and a clear note. Checks that are not `ok` carry a
|
|
28
|
+
`fix` line with the right command or step to put things right.
|
|
29
|
+
|
|
30
|
+
If any check has status `fail`, the command exits with code 1 (non-zero). A
|
|
31
|
+
`warn` (for example, no agent yet — demos and previews still work) does not
|
|
32
|
+
make the exit fail.
|
|
33
|
+
|
|
34
|
+
## CHECKS
|
|
35
|
+
|
|
36
|
+
The command runs six checks in order:
|
|
37
|
+
|
|
38
|
+
**node**
|
|
39
|
+
: The Node.js version. CW needs v18 or higher. A `fail` here stops everything.
|
|
40
|
+
|
|
41
|
+
**agent**
|
|
42
|
+
: The AI agent backend. CW can auto-detect agents (Claude, Codex, Gemini,
|
|
43
|
+
OpenCode) or take one from `CW_AGENT_COMMAND` / `--agent-command`. Without one,
|
|
44
|
+
real runs report `status: blocked`, but `demo` and `--preview` still work.
|
|
45
|
+
|
|
46
|
+
**agent-binary**
|
|
47
|
+
: When the agent is set by a command name (not auto or HTTP), this check sees if
|
|
48
|
+
the binary is on `$PATH`. Missing here gives a `warn` — the run will get a clear
|
|
49
|
+
error later, but CW will not guess at a different agent.
|
|
50
|
+
|
|
51
|
+
**git**
|
|
52
|
+
: The `git` command. CW uses it for commit place of origin. A `warn` here means
|
|
53
|
+
commit roots will be recorded as absent; no other part of a run needs git.
|
|
54
|
+
|
|
55
|
+
**home-registry**
|
|
56
|
+
: The cross-repo run index at `$CW_HOME` (default `$HOME/.local/state/cool-workflow`).
|
|
57
|
+
This location must be writable. A `fail` here blocks discovery across repos.
|
|
58
|
+
|
|
59
|
+
**repo-state**
|
|
60
|
+
: The per-repo run store under `<cwd>/.cw`. Must be writable. A `warn` here
|
|
61
|
+
means runs stay in-memory only — you can use `--cwd PATH` to point at another
|
|
62
|
+
writable root.
|
|
63
|
+
|
|
64
|
+
## OPTIONS
|
|
65
|
+
|
|
66
|
+
`--json`
|
|
67
|
+
: Give back the full report as a stable JSON object. Good for scripts.
|
|
68
|
+
|
|
69
|
+
`--fix`
|
|
70
|
+
: Give back only the fix commands for every non-ok check. Same as running `cw fix`
|
|
71
|
+
by itself.
|
|
72
|
+
|
|
73
|
+
`--onramp`
|
|
74
|
+
: Add a quick-start guide to the human output, with recommended checks and a
|
|
75
|
+
three-step path to your first report.
|
|
76
|
+
|
|
77
|
+
`--changed-from <ref>`
|
|
78
|
+
: When used with `--onramp`, make the quick-start checks cover only files changed
|
|
79
|
+
since `<ref>` (a Git branch, tag, or commit). Good for CI and code reading.
|
|
80
|
+
|
|
81
|
+
## FILES
|
|
82
|
+
|
|
83
|
+
```text
|
|
84
|
+
src/doctor.ts
|
|
85
|
+
dist/doctor.js
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## EXIT CODES
|
|
89
|
+
|
|
90
|
+
| Exit | Meaning |
|
|
91
|
+
| --- | --- |
|
|
92
|
+
| 0 | All checks ok (may have warnings) |
|
|
93
|
+
| 1 | One or more checks have status `fail` |
|
|
94
|
+
|
|
95
|
+
## SEE ALSO
|
|
96
|
+
|
|
97
|
+
cw fix — the same checks, but gives back only the fix commands
|