Skip to content

Fix the three box jobs, and give each one an image that is the commit it runs - #705

Merged
chhhee10 merged 6 commits into
mainfrom
feat/canary-three-images
Aug 18, 2026
Merged

Fix the three box jobs, and give each one an image that is the commit it runs#705
chhhee10 merged 6 commits into
mainfrom
feat/canary-three-images

Conversation

@chhhee10

@chhhee10 chhhee10 commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Three scheduled jobs moved onto one box in #694. Since then the canary has been crying wolf, the nightly translation has been failing every night in silence, and the weekly docs audit has been unable to file its issue. This fixes all three and changes how the box gets its code.

What was actually wrong

The canary could not explain itself. probe-cli.sh captured each agent's output, used it for two greps and threw it away; run.sh then echoed tail -20 of the probe on a non-PASS verdict — and the last 20 lines are the verdict block. Four CLIs sat yellow for three days with nothing recorded anywhere but the word INCONCLUSIVE, and re-running produced the same nothing. Each failing probe now prints the last 25 lines of what the CLI said, plus whether a hook fired at all.

canary-guard was never committed. The policy from the July workaround investigation does not exist on any ref — git log -S canary-guard returns nothing. So an agent that honoured a deny and completed the outcome through a different tool scored FAIL with the deny sitting two lines above it in the log. Copilot 1.0.80 did exactly that (denied touch, then "created the equivalent file instead" with its Create tool); goose leaked the read probe's sentinel the same way on 15 Aug. The guard splits route-around (probe still PASSes, prints a NOTE) from drift (NORMALIZATION-DRIFT-SUSPECT → FAIL), and the drift test is restricted to tools with a known canonical field so a Grep for the marker does not read as drift.

hermes has been uninstallable for weeks. Its installer runs npm install, which builds node-pty through node-gyp, which needs a Python and a C++ toolchain — and the sandbox image had none. Upstream made that step fatal, and it runs before the launcher is written, so the install aborted leaving no hermes binary and the probe drove a missing command.

"Cannot test" read as "the model didn't try." is_error() matched none of cursor's Authentication required, devin's Login canceled, or devin's high demand for this model.

The nightly translation was silent when it failed. Its output is a PR, so a run that dies leaves the same signal as a run with nothing to do: none. It failed every night from 11–17 Aug while 28 pages sat missing from 14 locales, and the weekly docs audit — not the job — is what noticed. It now posts on failure and stamps translate/last-run.json on every exit, whose age the docs audit reports.

Three images, each baking its commit

One image per job. Only the canary carries a docker client, so only its cron line needs the socket. Each bakes the checkout, dependencies and build products; the canary also bakes a compiled failproofaid instead of cross-compiling one per run in a sibling container. Clone, install, two builds and a cargo build leave the nightly path entirely.

CI publishes all three on every push to main, unfiltered — with the tree baked in, a path filter silently ships stale code. The failure that trade introduces is answered rather than prevented: FP_SHA and BUILT_AT are baked in, the entrypoint turns them into an age, and a stale image says so in every Slack report it produces.

Credentials stop being written into the checkout, which is now a build context: they move to a run-scoped dir, a repo-root .dockerignore names them anyway (including the beta variants .gitignore never covered), and each Dockerfile refuses to build if one arrives.

Four bugs only real runs found

The canary spawns sibling containers, so the host daemon resolves their mounts host-side, and a baked image's paths exist only inside itself.

  • the baked checkout mounted into probe containers → bash: /opt/canary/install-clis.sh: No such file or directory. The tree is materialised into the work dir, keyed by the baked SHA, so it is one copy per published image.
  • decoded OAuth tarballs went to a container-local dir → mounted empty; then, once host-visible, 0700 root while the sandbox reads as uid 1001. Both presented as three CLIs that were simply not logged in.
  • git fetch origin <short sha> is rejected by github — translate anchors on the full object id.
  • git init leaves an empty index and reset --soft does not touch it, so every tracked file read as a staged deletion: git add -A would have committed the repository being emptied, as an auto-PR, at 02:00. Mixed reset, plus a guard that refuses to publish when the baked tree differs from the commit it claims.

Verified live on the box

All three jobs run end to end on the canary box:

  • docs-audit → Slack report + tracking issue [auto] docs audit #703
  • translate → PR [auto] update translations #704 (28 pages × 14 locales + docs.json)
  • canary11/12 green, copilot flagged recovered, hermes installed (v0.20.2) and probed green for the first time in weeks. antigravity reports ⚠️ ERROR with the reason now printed: "Individual quota reached … Resets in 66h" — an account limit, not enforcement.

A separate credential fix was needed and is not in this PR: the box's PAT was read-only, which is why translate's push and docs-audit's issue both 403'd.

After merge

Nothing on the box changes by itself — its crontab keeps pointing at the published failproofai-canary-runner image until someone re-runs install.sh. Two things to watch: the "make package public" step is continue-on-error, and a private package means docker pull fails before the container starts, which is silent; and the first canary run on a new image re-probes all twelve CLIs, since the version gate keys on the failproofai SHA.

🤖 Generated with Claude Code

Hermes review

Field Value
Status Approved
Reviewed commit f1c6e97b7bc2087878dd7b6c4e832f0e8c81a8d7
Policy revision 1d8f31d926828f3bae215c58f5b35baa44acbff0
Model gpt-5.6-terra
Duration 281s
Updated 2026-08-18T06:12:51.572798974+00:00

Summary

The three-image scheduling migration is coherently wired, but translation jobs terminated by the configured timeout remain silent and leave no health stamp, undermining the PR’s failure-reporting goal.

Changes

  • Replaced the shared runtime-cloned runner with separate baked canary, translation, and docs-audit images.
  • Added GHCR image publishing on every main push and updated local cron/image selection.
  • Materialized the baked canary tree for sibling probe containers and moved transient credentials out of the checkout.
  • Added canary policy/diagnostic improvements and translation health reporting.

Validation

  • Passed docker run --rm --network none -v /review/input/workspace:/workspace:ro -w /workspace bash:5.2 bash -lc 'bash -n integration-suite/local/job-entrypoint.sh integration-suite/local/run-job.sh integration-suite/local/jobs/canary.sh integration-suite/local/jobs/docs-audit.sh integration-suite/local/jobs/translate.sh integration-suite/local/install.sh integration-suite/ci-entrypoint.sh integration-suite/probe-cli.sh integration-suite/run.sh' — All modified shell entrypoints and job scripts passed Bash syntax validation in an isolated container. (8s)

Findings

No blocking findings.

1 advisory finding
  • Medium/High Timed-out translation runs still produce neither a failure notification nor a run stamp — run-job.sh launches translation through timeout "$TMO" docker run with TMO=16200 (lines 32 and 54). The translation script says its stamp is written on every exit (line 84), but it has no EXIT, TERM, or INT trap; stamp failed and slack_note are reached only through die() (lines 91-100), while success stamps are explicit end-path calls. A timeout/hard stop therefore terminates the container before either path runs, leaving the previous last-run.json and no Slack failure report. (integration-suite/local/jobs/translate.sh:84)

Open questions

None.

Policy overrides

None.

chhhee10 and others added 5 commits August 17, 2026 16:28
Four CLIs sat yellow on the box for three days with nothing in the log but the
word INCONCLUSIVE, because probe-cli.sh captured each agent's output, used it
for two greps and discarded it — so run.sh's tail echoed the verdict block back
instead of the cause. It now prints what the CLI actually said, plus whether a
hook fired at all, and distinguishes a real daemon evaluation from a run where
the daemon was never asked anything.

canary-guard closes the false-red class: a model denied on one tool completes
the same outcome through another, and copilot 1.0.80 and goose both scored FAIL
with the honoured deny two lines above. A route-around leaves the probe's own
deny to decide; a payload whose canonical fields arrived empty is drift and
still FAILs, because that is the case where nothing would have matched in
production.

Also: is_error learns the three live "cannot test" texts that were reading as
"the model never tried", the sandbox image gets the toolchain hermes's installer
needs (it has been uninstallable there for weeks), and the nightly translation
reports its failures instead of being indistinguishable from an idle night.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The box ran one toolchain image and cloned, installed and built everything at
02:00 and 11:00 in front of nobody. Each job now has its own image carrying the
checkout, its dependencies and its build products — the canary also carries a
compiled failproofaid instead of cross-compiling one per run in a sibling
container. Only the canary gets a docker client, so only its cron line needs the
socket.

The trade is deliberate and its failure mode is answered rather than prevented:
job scripts no longer reach the box through a run-time clone, so CI publishes all
three on every push to main and the box tracks main by pulling. A broken image
build would otherwise leave it testing week-old code silently, so the baked SHA
and its age are printed at the top of every run and carried into every Slack
report, and an unreadable build date counts as stale.

Baking the tree also made the checkout a place credentials must never be: the
decoded OAuth tarballs and the gateway env-file move to a run-scoped dir, a
repo-root .dockerignore names them anyway (including the beta variants
.gitignore never covered), and each Dockerfile refuses to build if one arrives.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Four bugs, none of which a unit test would have found, all from the same root:
the canary spawns SIBLING containers, so the host daemon resolves their mounts
against the HOST — and a baked image's paths exist only inside itself.

  * the baked checkout was mounted into probe containers as a host path, so the
    run died on `bash: /opt/canary/install-clis.sh: No such file or directory`.
    The tree is materialised into the work dir — the one path that means the
    same thing on both sides — keyed by the baked SHA, so it is a copy once per
    published image rather than once per run.
  * the decoded OAuth tarballs went to a container-local dir, which mounted
    EMPTY into the injector; then, once host-visible, they were 0700 root while
    the sandbox reads them as uid 1001. Both presented downstream as three CLIs
    that were simply not logged in. chown to that uid, rather than widening the
    mode, because the box has other users.
  * `git fetch origin <short sha>` is rejected outright by github, so translate
    now anchors on the full object id.
  * `git init` leaves an EMPTY index and `reset --soft` does not touch it, so
    every tracked file read as a staged deletion — `git add -A` would have
    committed the repository being emptied, as an auto-PR, at 02:00. A mixed
    reset gives 0 changed paths, and a drift guard now refuses to publish when
    the baked tree differs from the commit it claims.

Verified on the box: docs-audit runs clean against the real cache, and translate
stops at the anchor and WRITES ITS FAILURE STAMP — the silence this set of
changes exists to end.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…the box

getModelForTier reads TRANSLATE_MODEL_TIER1 / TRANSLATE_MODEL_TIER23, so the
seven languages most readers arrive in can keep a strong model while the long
tail runs on something cheap. The CLI's --model flag flattens every tier to one
model, which is the opposite of what the tier split exists for.

install.sh now strips the pre-marker cron form as well as its own marker. A box
set up before the marker existed carries a long-form inline `docker run … -e
CANARY_JOB=<job>` line; matching only the marker left it in place, giving six
entries with every job scheduled twice — one on the old image, one on the new.
The per-job flock stops that doing damage and turns it into something worse to
diagnose: which image runs becomes a coin toss. Found on the real box, whose
crontab is exactly that shape.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 17, 2026

Copy link
Copy Markdown

Caution

Review failed

An error occurred during the review process. Please try again later.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@hermes-exosphere

hermes-exosphere commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Hermes

Status Reviewed
Verdict Approved
Head f1c6e97b7bc2
Rounds 0 of 5

The three-image scheduling migration is coherently wired, but translation jobs terminated by the configured timeout remain silent and leave no health stamp, undermining the PR’s failure-reporting goal.

What this changes

flowchart LR
    n0Canaryimagepublisher["+ Canary image publisher"]
    n1Jobimageruntime["+ Job image runtime"]
    n2Boxscheduler["~ Box scheduler"]
    n3Canaryprobeharness["~ Canary probe harness"]
    n4Translationautomation["~ Translation automation"]
    n5Documentationaudit["~ Documentation audit"]
    n0Canaryimagepublisher -- "publishes job images" --> n1Jobimageruntime
    n2Boxscheduler -- "runs image with secrets" --> n1Jobimageruntime
    n1Jobimageruntime -- "provides baked checkout" --> n3Canaryprobeharness
    n3Canaryprobeharness -- "uses sibling containers" --> n1Jobimageruntime
    n4Translationautomation -- "writes run-health stamp" --> n5Documentationaudit
    n2Boxscheduler -- "enforces 4.5h timeout" --> n4Translationautomation
Loading

Rounds

Round Reviewed Commits in this round Verdict
0 f1c6e97b7bc2 nothing new Approved

Findings

Open

  • F1 Timed-out translation runs still produce neither a failure notification nor a run stamp (integration-suite/local/jobs/translate.sh) — round 1

@hermes-exosphere help lists every command. This comment is maintained in place — I rewrite it after each review rather than posting a new one.

@hermes-exosphere

Copy link
Copy Markdown
Contributor

I could not complete the review of aee696d3da60. No approval was submitted. Retry with @hermes-exosphere review after addressing the operational error.

HTTP status client error (429 Too Many Requests) for url (https://codeload.github.com/FailproofAI/failproofai/legacy.tar.gz/aee696d3da60239da6463280b340d776ac2bb56f)

TS1501 — this repo's tsc target predates /s, and vitest does not typecheck, so
the flag ran green locally and failed quality, build, docs, all three test
configs and e2e in CI. [\s\S]* does the same job on every target.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@hermes-exosphere

Copy link
Copy Markdown
Contributor

I could not complete the review of f1c6e97b7bc2. No approval was submitted. Retry with @hermes-exosphere review after addressing the operational error.

HTTP status client error (429 Too Many Requests) for url (https://codeload.github.com/FailproofAI/failproofai/legacy.tar.gz/f1c6e97b7bc2087878dd7b6c4e832f0e8c81a8d7)

@hermes-exosphere hermes-exosphere left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hermes found no blocking issues in this revision.

1 advisory finding
  • Medium/High Timed-out translation runs still produce neither a failure notification nor a run stamp — run-job.sh launches translation through timeout "$TMO" docker run with TMO=16200 (lines 32 and 54). The translation script says its stamp is written on every exit (line 84), but it has no EXIT, TERM, or INT trap; stamp failed and slack_note are reached only through die() (lines 91-100), while success stamps are explicit end-path calls. A timeout/hard stop therefore terminates the container before either path runs, leaving the previous last-run.json and no Slack failure report. (integration-suite/local/jobs/translate.sh:84)

@chhhee10
chhhee10 merged commit 17238f6 into main Aug 18, 2026
12 checks passed
SiddarthAA added a commit that referenced this pull request Aug 18, 2026
Unblocks CI. The PR had drifted 13 commits behind main and reached a
conflicting state, and GitHub cannot build refs/pull/702/merge for a
conflicting PR — so the `pull_request` trigger never fired and the last two
commits on this branch were never tested. `gh pr checks` showed CodeRabbit
and Socket passing, so the absence of the CI run read as "no news" rather
than "blocked".

Merged rather than rebased: another session is committing to this branch, and
a rebase means a force-push that rewrites history under it.

Two conflicts, both in files each side appended to:

.gitignore — main added /blog/ (#717), this branch added the Python build
and test artefacts. Kept both; they do not overlap.

CHANGELOG.md — both sides created a `## 1.0.1-beta.2 — 2026-08-17` heading in
the same place. Resolved to one section holding the union, filed by
subsection, and `## 1.0.1-beta.1 — 2026-08-16` restored above beta.0.

That last part corrects main rather than merely reconciling with it. At the
merge base the top section was beta.1; main RENAMED that heading to beta.2
and prepended its own entries, which moved four already-shipped entries into
an unreleased section — 1.0.1-beta.1 is published on npm. The tell is that
main's beta.2 carries two `### Fixes` subsections, the second being the
orphaned beta.1 block, byte-identical to this branch's. Propagating that
would leave shipped work permanently misfiled.

Also dropped one duplicate of main's canary entry, the copy ending `(#PR)` —
an unreplaced placeholder. The `(#705)` copy is kept.

Verified nothing was lost: every bullet from both sides is present, none
invented, and everything from `## 1.0.1-beta.0` down is byte-identical to
main's.

Checks: SDK 261 passed; SDK spool contract passes strict; fp-cli 786 passed;
TS 3822 passed; tsc clean; lint 0 errors; build ok. Two tests in
__tests__/hooks/fp-reset.test.ts time out here and fail identically on a
clean origin/main worktree — this box runs a real failproofaid, which CI does
not. Pre-existing and environmental, not from this merge.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants