From 5cc3e8e7f4cc89d87c60889505f13caaf605afc1 Mon Sep 17 00:00:00 2001 From: mikeypetrillo <22065635+MikeyPetrillo@users.noreply.github.com> Date: Fri, 7 Aug 2026 08:05:30 -0400 Subject: [PATCH 1/7] [test][deploy] The write path must not answer what the read path refuses to Submitting a wish returned { recorded: true, cluster: { count: N } } - the number of signals that cluster now holds. That is precisely the field the public read withholds: GET /api/wishes is a beacon (totals, and how many clusters qualify, never which or how hot), and the itemized board sits behind the operator token. Two surfaces, same data, opposite answers. Concretely it let anyone confirm how many others asked for an exact phrase, and since the threshold is public, how close that phrase is to being built. The clustering key is only lowercase plus collapsed whitespace, so this confirms a phrase you already guessed rather than enumerating the board. That makes it narrow, not harmless, and it costs nothing to close: the caller asked us to record a gap, and { recorded: true } says we did. The regression guard for this was already written and had never run. It sat below process.exit(), called a __resetWishes that does not exist, and used a positional recordWish signature that has never existed - it would have thrown on its first line if it had ever executed. The confidentiality lockdown was guarded by a comment. Moved above the summary, rewritten against the real API, and extended to the write response: no cluster object, and no digit anywhere in the body. The last assertion checks the token-gated board still knows the real count, so the assertion above it has something to hide - otherwise both would pass against a board that simply lost the data. Mutation-tested: restoring the count fails 2 assertions. Suite goes 44 -> 54, the difference being the block that never ran. Co-Authored-By: Claude Opus 5 (1M context) --- scripts/test-wish.js | 44 ++++++++++++++++++++++++++++++++------------ src/wish.js | 19 +++++++++++++++++-- 2 files changed, 49 insertions(+), 14 deletions(-) diff --git a/scripts/test-wish.js b/scripts/test-wish.js index 794bac09..87d9a2dd 100644 --- a/scripts/test-wish.js +++ b/scripts/test-wish.js @@ -28,7 +28,8 @@ function freshFile(tag) { { freshFile("basic"); const r = recordWish({ need: "convert stl files to obj", source: "api", ip: "10.0.0.1" }); - ok(r.recorded === true && r.cluster.count === 1, `new need → recorded, cluster.count=1 (got ${JSON.stringify(r)})`); + ok(r.recorded === true, `new need → recorded (got ${JSON.stringify(r)})`); + ok(getWishesAggregate({ detailed: true }).clusters[0].count === 1, "the cluster holds one signal (read from the token-gated board, not the response)"); } // --- validation: 400s --- @@ -55,9 +56,9 @@ function freshFile(tag) { { freshFile("dedup"); recordWish({ need: " Convert STL to OBJ ", source: "api", ip: "10.0.0.4" }); - const r2 = recordWish({ need: "convert stl to obj", source: "mcp", ip: "10.0.0.5" }); - ok(r2.cluster.count === 2, `case/whitespace variants collapse into one cluster (got count=${r2.cluster.count})`); + recordWish({ need: "convert stl to obj", source: "mcp", ip: "10.0.0.5" }); const agg = getWishesAggregate({ detailed: true }); + ok(agg.clusters[0].count === 2, `case/whitespace variants collapse into one cluster (got count=${agg.clusters[0].count})`); ok(agg.distinctClusters === 1, `dedup: exactly one distinct cluster (got ${agg.distinctClusters})`); ok(agg.clusters[0].sources.api === 1 && agg.clusters[0].sources.mcp === 1, `sources breakdown attributes each call correctly (got ${JSON.stringify(agg.clusters[0].sources)})`); } @@ -241,20 +242,39 @@ for (const f of tmpFiles) { } -console.log(`\n${pass} passed, ${fail} failed`); -process.exit(fail ? 1 : 0); - -// --- confidentiality: the PUBLIC (default) aggregate must be a beacon only, -// never the itemized board. Regression guard for the 2026-07-21 lockdown. +// --- confidentiality: what the public may learn about the demand board ------- +// +// THIS BLOCK NEVER RAN. It sat below `process.exit()`, so it was unreachable, +// and it called a `__resetWishes` that does not exist with a positional +// signature recordWish has never had - it would have thrown on its first line +// if it had ever executed. The regression guard for the 2026-07-21 lockdown +// was, in effect, a comment. Moved above the summary and rewritten against the +// real API. { - __resetWishes(); - for (let i = 0; i < 6; i++) recordWish(`secret sauce tool ${i % 2}`, "find-miss"); + freshFile("confidentiality"); + for (let i = 0; i < 6; i++) { + recordWish({ need: `secret sauce tool ${i % 2}`, source: "find-miss", ip: `10.0.9.${i}` }); + } const pub = getWishesAggregate(); // default detailed:false ok(pub.clusters === undefined, "public aggregate exposes NO per-cluster array"); ok(typeof pub.qualifiedClusters === "number", "public aggregate exposes qualified COUNT (beacon)"); ok(typeof pub.totalWishes === "number" && typeof pub.distinctClusters === "number", "public aggregate keeps headline totals"); - const raw = JSON.stringify(pub); - ok(!raw.includes("secret sauce"), "public aggregate leaks no wish text"); + ok(!JSON.stringify(pub).includes("secret sauce"), "public aggregate leaks no wish text"); const det = getWishesAggregate({ detailed: true }); ok(Array.isArray(det.clusters) && det.clusters.length > 0, "detailed aggregate still returns the itemized board"); + + // The WRITE path must not answer what the read path refuses to answer. The + // response is an acknowledgement: no count, no text, nothing that says how + // hot the cluster is. Asserted on a cluster with a known non-trivial count, + // so a leak would have something to leak. + const hot = recordWish({ need: "secret sauce tool 0", source: "api", ip: "10.0.9.99" }); + ok(hot.recorded === true, "an explicit wish on an existing cluster is still recorded"); + const raw = JSON.stringify(hot); + ok(!/\d/.test(raw), `the write response carries no number at all (got ${raw})`); + ok(hot.cluster === undefined, "the write response exposes no cluster object"); + ok(getWishesAggregate({ detailed: true }).clusters.some((c) => c.count >= 4), + "…while the token-gated board still knows the real count (so the assertion above had something to hide)"); } + +console.log(`\n${pass} passed, ${fail} failed`); +process.exit(fail ? 1 : 0); diff --git a/src/wish.js b/src/wish.js index 382f5187..558be9d8 100644 --- a/src/wish.js +++ b/src/wish.js @@ -318,7 +318,7 @@ export function recordWish({ need, context, source, ip } = {}) { // Never throw here: this path runs inside /api/find and the MCP find_tool, // and a search must not fail because the demand board is busy. Stop // RECORDING instead, and say so in the return value. - return { recorded: false, reason: "find-miss volume limit for this source", cluster: null }; + return { recorded: false, reason: "find-miss volume limit for this source" }; } const now = Date.now(); @@ -331,7 +331,22 @@ export function recordWish({ need, context, source, ip } = {}) { appendLine({ type: "threshold", key, ts: now }); } - return { recorded: true, cluster: { count: cluster.count } }; + // ACKNOWLEDGEMENT ONLY - never the cluster's count. + // + // This used to return { cluster: { count } }, the number of signals the + // cluster now holds. That is exactly the field the PUBLIC read deliberately + // withholds: getWishesAggregate({detailed:false}) is a beacon (totals and how + // many clusters qualify, never which or how hot), and the itemized board sits + // behind the operator token. The write path was answering the question the + // read path refuses to answer, about the same data. + // + // Concretely: submit a phrase, learn how many others asked for that exact + // phrase, and since WISH_THRESHOLD is public, learn how close it is to being + // built. The clustering key is only lowercase + collapsed whitespace, so this + // confirms a phrase you already guessed rather than enumerating the board - + // narrow, but free to close and inconsistent to keep. The submitter loses + // nothing they need: the caller asked us to record a gap, and we did. + return { recorded: true }; } /** From 9bb9539c92fa44586347529fa78d7b8eb8281af2 Mon Sep 17 00:00:00 2001 From: mikeypetrillo <22065635+MikeyPetrillo@users.noreply.github.com> Date: Fri, 7 Aug 2026 08:05:45 -0400 Subject: [PATCH 2/7] [test][deploy] The canary gate ratcheted itself shut The daily proof that buying works has not run on schedule since 2026-08-02, and reported success every time. The gate asked GitHub for the last SUCCESSFUL RUN of this workflow. A run whose gate skips the buy also concludes success, so every skip refreshed the timestamp the next gate reads. Three attempts a day, each one landing inside the 20h window opened by the previous skip, means the window never closes. Measured over the last 40 runs: the gate shipped 2026-08-02, and not one scheduled run has bought since. Every real purchase after it came from a manual dispatch, which bypasses the gate entirely because the step is conditioned on github.event_name == 'schedule'. The last of those was 2026-08-05 04:16 UTC. The commit that introduced this said it would make the workflow "run on the days it claims to". It stopped it running at all. Nothing paged, because skipping is not a failure. The only surface that noticed was /status, which reported the settlement component stale and put "Partially measured" on the public page - working exactly as designed, since a day with no observation is no data rather than uptime. So the gate now asks production when a canary last actually BOUGHT, reading the settlement observation from /api/status. That observation is written only by a canary that ran, and it is the same fact the public page reports, so the gate and the page can no longer disagree - and if this class of bug returns, the page goes amber again, which is how it was caught. Two conditions, not one: fresh AND operational. A recent observation of a FAILED purchase is not proof that buying works, and suppressing the next attempt on the strength of it would turn three attempts to succeed into one attempt to fail. Unreachable status, or no observation on record, proceeds with the buy - a gate that cannot check must not block, the same fail-toward-running direction the job already documents. Verified against live production before commit: with the settlement observation 2.3 days old and state unknown, the old gate skips and the new gate buys. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/paid-canary.yml | 44 ++++++++++++++++++++++++------- 1 file changed, 34 insertions(+), 10 deletions(-) diff --git a/.github/workflows/paid-canary.yml b/.github/workflows/paid-canary.yml index ca209eb7..6eacf60c 100644 --- a/.github/workflows/paid-canary.yml +++ b/.github/workflows/paid-canary.yml @@ -42,25 +42,49 @@ jobs: id: fresh if: github.event_name == 'schedule' env: - GH_TOKEN: ${{ github.token }} + TARGET_URL: https://agent402.tools run: | set -euo pipefail - LAST=$(gh run list --repo "$GITHUB_REPOSITORY" --workflow paid-canary.yml \ - --status success --limit 1 --json createdAt --jq '.[0].createdAt // empty' || true) - if [ -z "$LAST" ]; then - echo "no successful run on record - proceeding with the buy" + # Ask production when a canary last actually BOUGHT. The previous + # version asked GitHub when this workflow last concluded green, and a + # run whose gate SKIPS the buy also concludes green - so every skip + # refreshed the timestamp the next gate reads, and the gate ratcheted + # itself shut. Measured: the gate shipped 2026-08-02 and not one + # SCHEDULED run bought after it. Every real purchase from then on came + # from a manual dispatch, which bypasses this step entirely. The + # workflow reported success the whole time, because skipping is a + # success; only /status noticed, by reporting the settlement component + # stale, which is the alarm that survives this class of bug. + # + # The settlement observation is the honest signal: it is written ONLY + # by a canary that ran, in the "Record settlement on /status" step + # below, and it is the same fact the public page reports. Gate and + # page can no longer disagree. + SNAP=$(curl -sf --max-time 20 "$TARGET_URL/api/status" || true) + if [ -z "$SNAP" ]; then + echo "status unreachable - proceeding with the buy (a gate that cannot check must not block)" exit 0 fi - AGE=$(( $(date -u +%s) - $(date -u -d "$LAST" +%s) )) - echo "last successful canary: $LAST (${AGE}s ago)" + AGE=$(printf '%s' "$SNAP" | jq -r '(.components[]? | select(.key=="settlement") | .current.ageMs) // "none"') + STATE=$(printf '%s' "$SNAP" | jq -r '(.components[]? | select(.key=="settlement") | .current.state) // "none"') + if [ "$AGE" = "none" ] || [ "$AGE" = "null" ]; then + echo "no settlement observation on record - proceeding with the buy" + exit 0 + fi + echo "last proven purchase: ${AGE}ms ago (state=$STATE)" # 20h, not 24h: deliveries drift by hours, and a 24h window would let # a late run suppress the next day's on-time one, quietly halving the # cadence instead of protecting it. - if [ "$AGE" -lt 72000 ]; then - echo "already settled within 20h - skipping this redundant attempt (not a failure)" + # + # state must be operational, not merely fresh: a recent observation + # that recorded a FAILED purchase is not proof that buying works, and + # suppressing the next attempt on the strength of it would turn the + # three-attempt design into one attempt to fail per day. + if [ "$STATE" = "operational" ] && [ "$AGE" -lt 72000000 ]; then + echo "a real purchase settled within 20h - skipping this redundant attempt (not a failure)" echo "skip=true" >> "$GITHUB_OUTPUT" else - echo "stale (>20h) - this attempt will buy" + echo "no proven purchase in the last 20h - this attempt will buy" fi canary: From 01696ec84995d997243bdeb51cba787137c6ff56 Mon Sep 17 00:00:00 2001 From: mikeypetrillo <22065635+MikeyPetrillo@users.noreply.github.com> Date: Fri, 7 Aug 2026 08:05:45 -0400 Subject: [PATCH 3/7] [test][deploy] Trigger CI for the wish and canary-gate fixes Co-Authored-By: Claude Opus 5 (1M context) --- .github/trigger-deploy | 2 +- .github/trigger-test | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/trigger-deploy b/.github/trigger-deploy index 95b7d23c..1012d205 100644 --- a/.github/trigger-deploy +++ b/.github/trigger-deploy @@ -1 +1 @@ -1786099674 +1786104345 diff --git a/.github/trigger-test b/.github/trigger-test index 95b7d23c..1012d205 100644 --- a/.github/trigger-test +++ b/.github/trigger-test @@ -1 +1 @@ -1786099674 +1786104345 From 84e2694d47dfbea561fd8f20788aa478a01877ef Mon Sep 17 00:00:00 2001 From: mikeypetrillo <22065635+MikeyPetrillo@users.noreply.github.com> Date: Fri, 7 Aug 2026 08:09:06 -0400 Subject: [PATCH 4/7] [test][deploy] Make the gate's fail-safe true instead of intended Two ways the new gate could have stopped the monitor it protects. jq exits 5 on a body that is not JSON - an error page, an interstitial, a truncated response - and under `set -euo pipefail` that fails the step, which fails the gate job. Both reads now fall back to "none", so every parse failure lands on the "no observation, so buy" path. The step cannot fail. And the canary job's own comment was wrong. It said an errored gate leaves skip empty and the canary RUNS, but a job-level `if` with no status check function still carries the implicit success() on `needs`, so a failed gate would have SKIPPED the canary - a monitor stopping because its guard broke, which is the exact failure the comment claims to prevent. `!cancelled()` overrides that implicit check and makes the sentence true. Not always(): a cancelled run should stay cancelled rather than go spend money. Simulated all six paths against the real jq filters: empty body, HTML error page, valid JSON with no settlement component, today's genuinely stale observation, fresh+operational, and fresh-but-failed. Only fresh+operational skips; every uncertainty buys. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/paid-canary.yml | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/.github/workflows/paid-canary.yml b/.github/workflows/paid-canary.yml index 6eacf60c..3377317a 100644 --- a/.github/workflows/paid-canary.yml +++ b/.github/workflows/paid-canary.yml @@ -65,8 +65,14 @@ jobs: echo "status unreachable - proceeding with the buy (a gate that cannot check must not block)" exit 0 fi - AGE=$(printf '%s' "$SNAP" | jq -r '(.components[]? | select(.key=="settlement") | .current.ageMs) // "none"') - STATE=$(printf '%s' "$SNAP" | jq -r '(.components[]? | select(.key=="settlement") | .current.state) // "none"') + # `|| echo none` on both: jq exits 5 on a body that is not JSON (an + # error page, an interstitial, a truncated response), and under + # `set -e` that would fail this step, fail the gate job, and - see the + # canary job's `if` below - stop the monitor because its own guard + # broke. Every parse failure lands on the "no observation, so buy" + # path instead. This step cannot fail. + AGE=$(printf '%s' "$SNAP" | jq -r '(.components[]? | select(.key=="settlement") | .current.ageMs) // "none"' 2>/dev/null || echo none) + STATE=$(printf '%s' "$SNAP" | jq -r '(.components[]? | select(.key=="settlement") | .current.state) // "none"' 2>/dev/null || echo none) if [ "$AGE" = "none" ] || [ "$AGE" = "null" ]; then echo "no settlement observation on record - proceeding with the buy" exit 0 @@ -93,7 +99,14 @@ jobs: # leaves `skip` empty and the canary RUNS - a monitor that silently stops # monitoring because its own guard broke is the failure this whole change # exists to remove. - if: needs.gate.outputs.skip != 'true' + # + # `!cancelled()` is what makes that comment TRUE rather than merely + # intended. A job-level `if` with no status check function still carries + # the implicit success() on `needs`, so a FAILED gate would have skipped + # this job - the opposite of what the comment claimed, and never verified. + # A status function overrides that implicit check. Not always(): a + # cancelled run should stay cancelled, not go spend money. + if: ${{ !cancelled() && needs.gate.outputs.skip != 'true' }} runs-on: ubuntu-latest timeout-minutes: 10 # Wallet/pow secrets are NOT at job scope (audit R-07): job-scope env is in From 63c17a9c66eb764f581d220cf8cda5c2d013bf89 Mon Sep 17 00:00:00 2001 From: mikeypetrillo <22065635+MikeyPetrillo@users.noreply.github.com> Date: Fri, 7 Aug 2026 08:09:06 -0400 Subject: [PATCH 5/7] [test][deploy] Trigger CI for the gate fail-safe hardening Co-Authored-By: Claude Opus 5 (1M context) --- .github/trigger-test | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/trigger-test b/.github/trigger-test index 1012d205..67edf636 100644 --- a/.github/trigger-test +++ b/.github/trigger-test @@ -1 +1 @@ -1786104345 +1786104546 From 83577dc37b40beaae7ea2466596c0a8e5a2ba206 Mon Sep 17 00:00:00 2001 From: mikeypetrillo <22065635+MikeyPetrillo@users.noreply.github.com> Date: Fri, 7 Aug 2026 08:13:29 -0400 Subject: [PATCH 6/7] [test][deploy] The gate's own test asserted a property it could not see CI caught the workflow change, which is the system working. But the assertion it caught it with was checking the wrong thing, and one beside it was worse. "an errored or skipped gate leaves the canary RUNNING, never silently disabled" tested only that the comparison reads `!= 'true'` rather than `== 'false'`. That is about the polarity of an output, and says nothing about a gate that FAILED - which is governed by the implicit success() on `needs`, not by the comparison. It passed for weeks against code where a failed gate would have skipped the buy: the exact opposite of the sentence it asserts. Same shape as the wish confidentiality guard in this branch, and the same shape as a green suite hiding a dead fix - a test that supplies the answer it is supposed to check. It now reads the canary job's actual `if` line and requires a status function, which is the thing that makes the claim true, and reports the line it found so a failure names the real text. Two new locks on the regression that started this. The gate must read /api/status and must NOT read `gh run list`: keying on this workflow's own run history is what let every skip refresh the window the next gate reads. And every jq read in the gate must carry a fallback, since jq exits non-zero on a body that is not JSON and `set -e` would turn that into a failed gate. Mutation-tested one at a time: dropping !cancelled() fails 1, restoring the `gh run list` gate fails 1, removing the jq fallbacks fails 1. Each mutation is killed by its own assertion and no other, so none of the three is carrying the others. Co-Authored-By: Claude Opus 5 (1M context) --- scripts/test-canary-coverage.js | 32 +++++++++++++++++++++++++++----- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/scripts/test-canary-coverage.js b/scripts/test-canary-coverage.js index 5f8e8c87..2883b61b 100644 --- a/scripts/test-canary-coverage.js +++ b/scripts/test-canary-coverage.js @@ -146,13 +146,35 @@ if (rn) { // step-level condition would have to be repeated on every step, and the one // that got forgotten would spend ~$1.50 of real USDC anyway. ok(/\n\s{2}gate:/.test(wf), "a gate JOB decides whether this attempt spends anything"); - ok(/needs:\s*gate/.test(wf) && /if:\s*needs\.gate\.outputs\.skip\s*!=\s*'true'/.test(wf), + const canaryIf = wf.split("\n").find((l) => l.includes("if:") && l.includes("needs.gate.outputs.skip")) || ""; + ok(/needs:\s*gate/.test(wf) && canaryIf !== "", "the buying job is gated at JOB level, not per step"); - // Fail toward RUNNING. A monitor that stops monitoring because its own guard - // broke is the exact failure this change removes. - ok(/!=\s*'true'/.test(wf) && !/==\s*'false'/.test(wf), - "an errored or skipped gate leaves the canary RUNNING, never silently disabled"); + // Fail toward RUNNING, asserted STRUCTURALLY rather than by polarity. + // + // The previous version checked only that the comparison reads `!= 'true'` + // rather than `== 'false'`. That says nothing about a gate that FAILED: a + // job-level `if` with no status check function still carries the implicit + // success() on `needs`, so a failed gate SKIPS the canary. The assertion + // passed for weeks against exactly that code, claiming a property it had no + // way to see. What actually makes the claim true is a status function + // overriding the implicit check. + ok(/!=\s*'true'/.test(canaryIf) && !/==\s*'false'/.test(canaryIf), + "the gate's skip is opt-IN: an empty or missing output leaves the canary running"); + ok(/!\s*cancelled\(\)|always\(\)/.test(canaryIf), + `a FAILED gate cannot silently disable the canary - the if carries a status function (got: ${canaryIf.trim()})`); + + // The gate must ask PRODUCTION when a canary last BOUGHT, never GitHub when + // this workflow last concluded green. A run whose gate skips the buy also + // concludes green, so keying on run history makes every skip refresh the + // window the next gate reads, and the gate ratchets itself permanently shut. + // Measured: shipped 2026-08-02, and not one scheduled run bought afterwards. + const gateJob = wf.slice(wf.indexOf("\n gate:"), wf.indexOf("\n canary:")); + ok(/\/api\/status/.test(gateJob) && !/gh run list/.test(gateJob), + "the gate keys on a real settlement observation, not on this workflow's own run history"); + const jqReads = gateJob.split("\n").filter((l) => l.includes("jq -r")); + ok(jqReads.length > 0 && jqReads.every((l) => /\|\|\s*echo\s+none/.test(l)), + `every jq read in the gate falls back instead of failing the step (jq exits non-zero on a non-JSON body) (${jqReads.length} read${jqReads.length === 1 ? "" : "s"})`); // A human asking for a buy - usually right after a deploy - must never be // suppressed by the freshness window. From cf37868ae45891077c66ad0903f224afa52e3b27 Mon Sep 17 00:00:00 2001 From: mikeypetrillo <22065635+MikeyPetrillo@users.noreply.github.com> Date: Fri, 7 Aug 2026 08:13:29 -0400 Subject: [PATCH 7/7] [test][deploy] Trigger CI for the canary gate test fix Co-Authored-By: Claude Opus 5 (1M context) --- .github/trigger-test | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/trigger-test b/.github/trigger-test index 67edf636..81821101 100644 --- a/.github/trigger-test +++ b/.github/trigger-test @@ -1 +1 @@ -1786104546 +1786104809