Skip to content

Canary box: move the daily suite off Actions, and make setting it up one command - #686

Open
chhhee10 wants to merge 7 commits into
mainfrom
feat/canary-one-command
Open

Canary box: move the daily suite off Actions, and make setting it up one command#686
chhhee10 wants to merge 7 commits into
mainfrom
feat/canary-one-command

Conversation

@chhhee10

@chhhee10 chhhee10 commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Supersedes #656 — same four commits, cherry-picked onto current main (that
branch was based on the old failproofaid line, so its history replayed ~160
commits main already has), plus a one-command installer.

Why

The daily integration suite runs on GitHub Actions. Runner minutes were its
entire cost — the LLM spend is identical wherever it runs — so it moves to a box
whose whole contract is Docker + one cron line + one env file.

Setting it up used to be four commands. Three fail silently for a day.

That is the wrong property for the thing whose job is noticing silent failures:

  • Work dir mounted at a different path inside than out. The runner drives the
    host's Docker through the socket, so paths under it are used both for
    in-container file ops and as sibling-container -v sources the host
    daemon resolves. Get it wrong and the sandbox mounts nothing.
  • CANARY_REF left at the shipped origin/failproofaid. That branch merged
    in [failproofaid] Split failproofai into a CLI + Rust background daemon #632, so the box tests a frozen tree forever and never says so.
  • A filled-in env file with no Slack webhook. The run works perfectly and
    reports nowhere — worse than no canary, because it looks like coverage.

integration-suite/local/install.sh refuses all three at install time, in
front of a person
, rather than at 06:17 tomorrow in front of nobody. The
webhook is required for that reason, not because the run needs it.

bash <(curl -fsSL https://raw.githubusercontent.com/FailproofAI/failproofai/main/integration-suite/local/install.sh) ~/secrets.env

Whoever holds the credentials fills in secrets.env and sends it; the person
with the machine runs one line and needs to know nothing about the canary.

Design points worth reviewing:

  • No clone on the box. Docker takes <repo>#<ref>:<subdir> as a build
    context, so the image builds straight from the git URL. The runner re-clones
    the repo itself each night anyway, so a checkout here would only go stale.
  • The cron line is rewritten, not appended. It carries a
    # failproofai-canary marker and a re-install strips any previous line first,
    so running the installer twice upgrades the schedule instead of scheduling two
    jobs. The marker is a comment rather than a match on the command, because the
    command changes.
  • The stale default is fixed at the source too (secrets.env.example), so
    the installer's check is a backstop rather than the only thing between a wrong
    default and a year of green runs against a dead ref.
  • --dry-run separates CHECKED from CHANGED. The preflight genuinely runs in
    a dry run so it keeps its ✓; mutations print "would". A script that reports
    success for work it did not do is the same defect class this canary exists to
    find.

Testing

Not just "it parses" — the mechanisms were exercised:

Case Result
docker build from the git URL, no clone builds — image exported, docker --version OK inside
Runner with no socket mounted refuses: ✗ docker socket not mounted — add: -v …
Runner with socket, no work dir refuses: ✗ no work dir found — mount one at an identical path
Env file missing the webhook refuses, exit 1, names the variable
CANARY_REF=origin/failproofaid refuses, exit 1, says to use origin/main
No secrets file at all fetches the template, refuses, says what to fill in
Re-install over an existing crontab 1 canary line, unrelated jobs preserved
--dry-run checks report ✓, mutations report "would"
  • bun run test:run on __tests__/integration-suite/ — 49 pass
  • bash -n on the installer

Note

The one-command form only works once this is on main — the installer curls
install.sh and secrets.env.example from the default branch, and neither
exists there yet. Verified against this branch's ref in the meantime.

🤖 Generated with Claude Code

https://claude.ai/code/session_01HEwcerc9jE7ZBkfBiYRbep

Hermes review

Field Value
Status Approved
Reviewed commit e8de2272dc32ebce80ccb20e2092406ecab22074
Policy revision 1d8f31d926828f3bae215c58f5b35baa44acbff0
Model gpt-5.6-terra
Duration 453s
Updated 2026-08-12T13:39:09.863743332+00:00

Summary

The canary migration and installer are substantially covered, but the README retains obsolete systemd-runner instructions and an obsolete secrets path that can mislead canary operators.

Changes

  • Moves the daily integration suite from scheduled GitHub Actions execution to a Docker-driven local canary box.
  • Adds a one-command installer, runner image, cron scheduling, persistent state/log handling, and Slack crash notifications.
  • Adds daemon-path and fail-closed integration probing with dedicated state handling.
  • Adds structural tests for the local runner and updates canary documentation.

Validation

  • Passed docker run --rm -v /review/input/workspace:/repo:ro oven/bun:latest bash -lc 'bash -n /repo/integration-suite/local/install.sh /repo/integration-suite/local/runner-entrypoint.sh /repo/integration-suite/local/runner-daily.sh /repo/integration-suite/ci-entrypoint.sh /repo/integration-suite/probe-cli.sh /repo/integration-suite/run.sh' — All changed shell scripts passed Bash syntax validation in a nested container. (10s)

Findings

No blocking findings.

1 advisory finding
  • Medium/High Update stale local-runner documentation — The README describes the new runner as a “systemd user timer” at line 7, names nonexistent local/run-local.sh at line 109, points operators to ~/.config/failproofai-canary/secrets.env at lines 187-189, and says local/ contains systemd units at line 218. The added local runner instead consists of install.sh, runner-entrypoint.sh, and runner-daily.sh; install.sh defaults its active env file to $HOME/fp-canary/secrets.env. (integration-suite/README.md:7)

Open questions

None.

Policy overrides

None.

Summary by CodeRabbit

  • New Features

    • Added a Docker-based local canary runner for daily stable and beta integration checks.
    • Added setup validation, dry-run support, scheduled execution, log retention, failure aggregation, and optional Slack crash reporting.
    • Added optional daemon-mode testing with fail-closed behavior and isolated run state.
    • Added support for manual cloud-based integration runs as a fallback.
  • Documentation

    • Updated setup, configuration, execution, troubleshooting, and release documentation.
  • Bug Fixes

    • Improved validation for credentials, references, Docker access, paths, locks, and daemon failures.

chhhee10 and others added 5 commits August 12, 2026 18:44
… daemon path

Daily runs leave GH Actions (runner minutes were the entire cost; the LLM
spend is identical either way) for a local canary box driven by a systemd
user timer in the retired cron's 06:17 UTC slot. integration-suite/local/
ships the box side: run-local.sh (checkout CANARY_REF → stable leg → beta
leg, flock-serialized, with a crash-guard Slack note for a leg that dies
BEFORE reporting — the replacement for GHA's red-job email), install.sh
(installed copy OUTSIDE the clone the wrapper hard-resets, systemd units,
secrets template), and the service/timer units. The workflow keeps
workflow_dispatch as the cloud fallback and loses its cron.

The stable leg now probes the daemon-configured path (CANARY_DAEMON=1) —
the configuration `failproofai config` gives users going forward:
ci-entrypoint cross-compiles failproofaid in rust:1-bookworm (glibc-matched
to the node:22-bookworm-slim sandbox; a host build can link newer symbols
and fail to load inside it), run.sh bind-mounts it into the probe
container, and probe-cli.sh sets the daemon.configured fail-closed marker
through the real fp-config updateConfig path — shell-appending TOML could
produce a duplicate [daemon] table, which parses as NOT configured and
silently falls back to in-process.

The daemon restarts per probe, not per CLI: the wire protocol forwards
{hookEvent, cli, stdin, cwd} and never env, so the warm worker's
FAILPROOFAI_HOOK_LOG_FILE is fixed at daemon start — one daemon across
both probes would share one oracle dir, and probe A's incidental
read-denies would satisfy probe B's grep (false PASS). A dead daemon
cannot false-PASS either: its deny is shaped by the synthetic
failproofai/daemon-unreachable policy, which the probes' greps never
match — those probes go INCONCLUSIVE and re-probe until the daemon path
recovers.

Verified without LLM or secrets in the real sandbox image: live daemon →
canary-bash deny through the socket → warm worker writes the per-probe
oracle; killed daemon → fail-closed deny logged as daemon-unreachable,
matching neither probe grep; marker cleared → in-process evaluation
restored. Tripwires in __tests__/integration-suite/local-runner.test.ts
pin the workflow staying cron-free, the unit↔installer paths, the
secrets-template↔workflow-env parity, the per-probe daemon restarts, the
marker hygiene, and the fail-closed/oracle non-overlap (both sides
extracted from the real sources).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ero-touch hosts

The box story shrinks to Docker + one cron line + one env file: the systemd
units, install.sh and host-toolchain requirements are gone. A self-contained
runner image (local/Dockerfile.runner — node+bun+git+docker CLIENT) drives
the HOST's Docker through the mounted socket, so the sandbox image, the
per-channel volumes and every probe container are exactly the ones CI runs,
as siblings.

Two decisions carry the design:

- Path parity. The one work dir is mounted at an IDENTICAL path inside and
  out (-v "$HOME/fp-canary:$HOME/fp-canary") because paths under it serve
  both as in-container file paths and as sibling-container -v sources, which
  the host daemon resolves against the host filesystem. The entrypoint
  auto-detects the parity mount from its own container's mount table and
  names the exact flag to add when it is missing. runner-daily.sh pins the
  daemon build's cargo cache under the work dir — the only harness default
  rooted outside it ($HOME), where the rust sibling's mount would silently
  create an empty root-owned host dir and cache nothing.

- A thin baked entrypoint, everything else from the checkout. The image
  carries only runner-entrypoint.sh (preflight, work-dir detection, host-side
  flock so overlapping cron fires share one lock across containers, clone/
  fetch/checkout of $CANARY_REF, Slack crash-note for the checkout phase);
  it then execs integration-suite/local/runner-daily.sh FROM THE CHECKOUT.
  Harness changes reach the box through git — nobody rebuilds the boss's
  image for a leg tweak.

runner-daily.sh keeps the leg contract from the systemd iteration verbatim:
stable leg daemon-configured (CANARY_DAEMON=1) then beta in-process, per-leg
90-min timeout, crash-guard keyed on the absence of run.sh's own
posted-to-Slack line, 14-day log prune. secrets.env.example documents every
variable the GHA Environment supplied, in docker --env-file's literal
KEY=value format. Tripwires updated in local-runner.test.ts: the image must
never bake the daily driver, the crash-guard grep must match run.sh's actual
wording, the example must offer every secret-fed env var the workflow maps
and must contain no shell expansion on value lines.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…aemon test session

Ports the daemon-leg findings from the parallel host-run session that drove
all three legs against 10 real, locally-installed CLIs (2026-08-07): the
daemon does not regress enforcement on any CLI, denies land in 2-3ms warm
versus 7-8ms cold — and the fail-closed pass surfaced an availability
defect (factory fired 202 denied hook calls and antigravity 1,002, retrying
a deny that can never succeed until the harness killed them at ten minutes)
that only a fail-closed leg keeps visible.

CANARY_DAEMON_DEAD=1 is that leg: configure the machine for the daemon
exactly as CANARY_DAEMON=1 does, then never start it. Every CLI must DENY;
the benign probe command executing anyway means the machine believed it was
fail-closed and was not. The deny is scored through the existing
daemon-unreachable detector, which also now breaks the probe retry loops
early in live-daemon mode (a dead daemon denies everything — further LLM
attempts can only reproduce the same deny) and prints a triage note so a
mid-probe daemon death reads as DAEMON FAILED CLOSED instead of a quiet
INCONCLUSIVE.

Two hazards closed on the way in:

- The DEAD leg gets its own state lane ($STATE.dead). Its PASS means
  "denied while dead" — recorded in the enforcement gate it would skip the
  next REAL probe of the same (CLI, failproofai) pair as already-green.
- The daemon.configured marker is now cleared before wire() in EVERY mode
  and set only after it. wire() runs vendor CLIs whose hooks route through
  the marker (openclaw onboard), and a marker with no daemon up yet — set
  too early today, or surviving from yesterday in the persistent volume —
  would fail-close the wiring itself.

Also carried from that session's debugging: the SUN_LEN (108-byte) Unix
socket path cap is documented on the socket-path choice. All of it pinned
in __tests__/integration-suite/local-runner.test.ts (49 tests).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Setting the box up was four commands. Three of them have a failure mode that is
silent for a full day, which is the wrong property for the thing whose whole job
is to notice silent failures:

  - the work dir mounted at a different path inside the container than out, so
    the sibling-container `-v` sources resolve against the host to nothing;
  - `CANARY_REF` left at the shipped `origin/failproofaid`, a branch that merged
    in #632 — the box would test a frozen tree forever and never say so;
  - a filled-in env file with no Slack webhook: a run that works perfectly and
    reports nowhere, which is worse than no canary because it looks like cover.

`install.sh` refuses each at install time, in front of a person, rather than at
06:17 tomorrow in front of nobody. The webhook is required for exactly that
reason and not because the run needs it.

It builds the image straight from the git URL — Docker takes
`<repo>#<ref>:<subdir>` as a build context — so the box never clones anything.
The runner re-clones the repo itself on every run, so a checkout here would only
go stale.

The cron line is rewritten, not appended: it carries a `# failproofai-canary`
marker and a re-install strips any previous line first, so running the installer
twice upgrades the schedule instead of scheduling two jobs. The marker is a
comment rather than a match on the command, because the command changes.

The stale `CANARY_REF` default is fixed in `secrets.env.example` too. Catching it
in the installer only would leave the wrong value shipping, with a guard as the
sole thing standing between it and a year of green runs against a dead ref.

`--dry-run` distinguishes what was CHECKED from what would be CHANGED. The
preflight really does run in a dry run, so it keeps its ✓; the mutations print
"would". A script that reports success for work it did not do is the same defect
class this canary exists to find, and it would be a poor advertisement.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@chhhee10
chhhee10 force-pushed the feat/canary-one-command branch from d48406d to 913a958 Compare August 12, 2026 13:20
@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The integration suite adds a Docker-based local canary with cron scheduling, repository checkout, stable and beta legs, Slack crash reporting, and daemon-mode probes. GitHub Actions now runs only through manual dispatch. Tests validate workflow, runner, environment, daemon, state, and oracle contracts.

Changes

Local canary execution

Layer / File(s) Summary
Runner provisioning and checkout
integration-suite/local/*, integration-suite/README.md, __tests__/integration-suite/local-runner.test.ts
The installer validates Docker, storage, mounts, credentials, environment values, and CANARY_REF. The runner image provides Git, Bun, Docker CLI, and the checkout entrypoint. The entrypoint locks the workspace, checks out the requested ref, and starts the daily runner.
Daily leg orchestration
integration-suite/local/runner-daily.sh, integration-suite/ci-entrypoint.sh, integration-suite/run.sh, __tests__/integration-suite/local-runner.test.ts
The daily runner executes stable and beta legs with separate state and cache settings. The CI entrypoint optionally builds failproofaid and forwards daemon settings to the probe container.
Daemon probe lifecycle
integration-suite/probe-cli.sh, integration-suite/run.sh, __tests__/integration-suite/local-runner.test.ts
Daemon probes validate socket readiness, restart the daemon per probe, manage configuration markers, isolate dead-daemon state, and classify fail-closed denials without creating false passes.
Fallback documentation and regression contract
.github/workflows/integration-suite.yml, integration-suite/README.md, CHANGELOG.md, __tests__/integration-suite/local-runner.test.ts
The workflow removes its schedule and retains manual dispatch. Documentation and changelog entries describe local canary operation, cloud fallback, daemon legs, and installation behavior. Source-contract tests cover the updated flow.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Cron
  participant runner-entrypoint.sh
  participant runner-daily.sh
  participant ci-entrypoint.sh
  participant probe-cli.sh
  Cron->>runner-entrypoint.sh: start local canary
  runner-entrypoint.sh->>runner-daily.sh: run checked-out repository
  runner-daily.sh->>ci-entrypoint.sh: execute stable or beta leg
  ci-entrypoint.sh->>probe-cli.sh: pass daemon and state settings
  probe-cli.sh->>probe-cli.sh: start probes and classify results
Loading

Possibly related PRs

Poem

I’m a rabbit guarding the canary run,
Stable and beta hop beneath the sun.
Daemons wake, then fail closed tight,
Slack sends the tail when things lose sight.
Manual clouds wait, while local paws take flight.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 13.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly summarizes moving the daily suite off GitHub Actions and adding one-command setup.
Description check ✅ Passed The description clearly explains the migration, installer behavior, design decisions, and validation results, but omits the template checkboxes.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch

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

Copy link
Copy Markdown
Contributor

Hermes

Status Reviewing
Verdict Not reviewed yet
Head 913a95887fe1
Rounds 0 of 5

No summary yet.

What this changes

No component map for this revision.

Rounds

No review has finished on this pull request yet.

Findings

Nothing raised yet.


@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

hermes-exosphere commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Hermes

Status Reviewed
Verdict Approved
Head e8de2272dc32
Rounds 0 of 5

The canary migration and installer are substantially covered, but the README retains obsolete systemd-runner instructions and an obsolete secrets path that can mislead canary operators.

What this changes

flowchart LR
    n0Actionsfallbackworkflow["~ Actions fallback workflow"]
    n1Canaryboxinstaller["+ Canary box installer"]
    n2Runnercontainer["+ Runner container"]
    n3Dailycanaryorchestrator["+ Daily canary orchestrator"]
    n4Integrationharness["~ Integration harness"]
    n5Daemonenforcementprobe["~ Daemon enforcement probe"]
    n6Localrunnertests["+ Local runner tests"]
    n7Canarydocumentation["~ Canary documentation"]
    n0Actionsfallbackworkflow -- "manual dispatch invokes" --> n4Integrationharness
    n1Canaryboxinstaller -- "builds and schedules" --> n2Runnercontainer
    n2Runnercontainer -- "checks out then executes" --> n3Dailycanaryorchestrator
    n3Dailycanaryorchestrator -- "runs stable and beta legs" --> n4Integrationharness
    n4Integrationharness -- "starts probe containers" --> n5Daemonenforcementprobe
    n5Daemonenforcementprobe -- "routes hooks via daemon" --> n5Daemonenforcementprobe
    n6Localrunnertests -- "checks runner contract" --> n2Runnercontainer
    n7Canarydocumentation -- "documents setup" --> n1Canaryboxinstaller
Loading

Rounds

Round Reviewed Commits in this round Verdict
0 913a95887fe1 f36d9acdfede 2eb2d644193a 46a5145f4ab1 8838fff0a979 913a95887fe1 Approved
0 e8de2272dc32 43d9b8265271 e8de2272dc32 Approved

Findings

Open

  • F1 Update stale local-runner documentation (integration-suite/README.md) — 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 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.

The build step announces "dist/index.js + dist/cli.mjs — no dashboard" and then
builds exactly those two. But the `bun install --frozen-lockfile` above it fires
the package `prepare` hook, which is `bun run build` — the FULL build, ending in
`bun --bun next build`. So each leg compiled the entire Next.js application
first, then built the two artifacts it actually wanted.

Found by running the box end to end rather than reading it: the run log shows
`Creating an optimized production build` and `Generating static pages (3/3)`
underneath a step whose own text says it does not do that.

`translate-docs.yml` already carries this guard, with the same reasoning written
next to it — the trap is the hook, and every entry point that installs for
tooling has to opt out of it individually.

Costs two full Next builds a day here (stable + beta), on a box whose entire
reason for existing is that runner time was too expensive to keep buying.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The runner is root inside the container, so everything it creates under the work
dir — the clone, logs/, state/, the cargo cache — is root-owned on the host.
Only secrets.env, written by the installer, belongs to the user.

That is harmless: the next run is root too, and nothing in the pipeline cares.
But the first person to `tail` a log or `rm -rf` the clone gets a permission
error with no explanation, on a box they were told needs nothing but Docker and
a cron line. Found the same way — by doing it.

Documented in both places someone would look, with the sudo form of the command
they were about to run.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

@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 Update stale local-runner documentation — The README describes the new runner as a “systemd user timer” at line 7, names nonexistent local/run-local.sh at line 109, points operators to ~/.config/failproofai-canary/secrets.env at lines 187-189, and says local/ contains systemd units at line 218. The added local runner instead consists of install.sh, runner-entrypoint.sh, and runner-daily.sh; install.sh defaults its active env file to $HOME/fp-canary/secrets.env. (integration-suite/README.md:7)

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (2)
__tests__/integration-suite/local-runner.test.ts (2)

205-212: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

The scored-PASS regex does not pin the probe-to-log pairing.

The pattern allows "$LOG[AB]" with V[AB]=PASS in any combination. A future edit that scores probe B from probe A's log would still yield two matches and pass. Pin each pairing.

♻️ Suggested change
-    const scored = probeSh.match(
-      /\[ "\$\{CANARY_DAEMON_DEAD:-0\}" = 1 \] && daemon_failed_closed "\$LOG[AB]\/hooks\.log"; then V[AB]=PASS/g,
-    );
-    expect(scored?.length).toBe(2);
+    for (const leg of ["A", "B"] as const) {
+      expect(probeSh, `probe ${leg} must score its OWN log`).toMatch(
+        new RegExp(
+          `\\[ "\\$\\{CANARY_DAEMON_DEAD:-0\\}" = 1 \\] && daemon_failed_closed "\\$LOG${leg}/hooks\\.log"; then V${leg}=PASS`,
+        ),
+      );
+    }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@__tests__/integration-suite/local-runner.test.ts` around lines 205 - 212,
Strengthen the scored-PASS assertion in the test case “the DEAD leg scores the
fail-closed deny as PASS on both probes” so each probe variable is paired with
its corresponding log variable, explicitly matching A with LOGA and B with LOGB.
Preserve the existing requirement for exactly two matches and the
daemon_failed_closed detector assertion.

51-87: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add tripwires for install.sh.

This PR adds integration-suite/local/install.sh and the CHANGELOG states four guarantees for it: marker-based cron rewrite, required Slack webhook, rejection of origin/failproofaid, and a dry run that never claims a ✓ for unperformed work. No test parses install.sh, so each guarantee can regress silently. The file has no importable surface, so the same parse-the-real-file approach used here applies.

💚 Suggested additions
 const secretsExample = readFileSync(path.join(LOCAL, "secrets.env.example"), "utf8");
+const installSh = readFileSync(path.join(LOCAL, "install.sh"), "utf8");
describe("install.sh (the one-command box setup)", () => {
  it("rewrites its cron line instead of stacking a second one", () => {
    expect(installSh).toMatch(/CRON_MARKER="# failproofai-canary"/);
    expect(installSh).toMatch(/crontab -l[^\n]*grep -vF "\$CRON_MARKER"/);
  });

  it("refuses to schedule a run that cannot report", () => {
    expect(installSh).toContain("CANARY_SLACK_WEBHOOK");
  });

  it("rejects the merged origin/failproofaid ref", () => {
    expect(installSh).toMatch(/origin\/failproofaid\)/);
    expect(secretsExample).toMatch(/^CANARY_REF=origin\/main$/m);
  });

  it("never claims a ✓ for work a dry run did not do", () => {
    expect(installSh).toMatch(/did\(\)[\s\S]{0,120}DRY[^\n]*= 1[\s\S]{0,60}would:/);
  });
});

As per coding guidelines: "When you add or change logic, add a corresponding test in __tests__/."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@__tests__/integration-suite/local-runner.test.ts` around lines 51 - 87, Add
integration tests for the real integration-suite/local/install.sh file,
following the existing local-runner test style and available fixtures such as
installSh and secretsExample. Cover marker-based cron replacement via
CRON_MARKER and crontab filtering, requiring CANARY_SLACK_WEBHOOK, rejecting
origin/failproofaid while requiring CANARY_REF=origin/main in the secrets
example, and ensuring the dry-run did() path reports “would:” without claiming a
✓.

Source: Coding guidelines

🔇 Additional comments (17)
integration-suite/local/Dockerfile.runner (2)

23-24: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

⚠️ Unverified finding
Sandbox verification was unavailable.

Pin the bun image tag.

oven/bun:latest is mutable. A bun release that breaks the harness reaches the box on the next image rebuild with no record of what changed. DOCKER_VERSION is already pinned in this same file, so the intent is version discipline. Pin bun the same way.

♻️ Suggested change
-# bun from the official image — same trick as the sandbox Dockerfile.
-COPY --from=oven/bun:latest /usr/local/bin/bun /usr/local/bin/bun
+# bun from the official image — same trick as the sandbox Dockerfile.
+ARG BUN_VERSION=1.2.21
+COPY --from=oven/bun:${BUN_VERSION} /usr/local/bin/bun /usr/local/bin/bun

Confirm the sandbox integration-suite/Dockerfile pin strategy so both images stay aligned.


26-41: LGTM!

integration-suite/local/secrets.env.example (1)

1-45: LGTM!

integration-suite/local/install.sh (1)

64-147: LGTM!

Also applies to: 149-211

integration-suite/local/runner-entrypoint.sh (2)

42-43: 🩺 Stability & Availability | ⚡ Quick win

⚠️ Unverified finding
Sandbox verification was unavailable.

Verify flock exists in the runner image, and do not treat its absence as "already locked".

If flock is missing from the image, flock -n 9 fails with exit 127. Line 43 then treats that as a held lock, prints a benign message, and exits 0. The canary never runs and never reports — the silent-coverage failure this suite exists to prevent. Dockerfile.runner installs only git, ca-certificates and curl, so flock must come from the node:22-bookworm-slim base.

Add util-linux to the apt-get install list in Dockerfile.runner, or distinguish exit 127 from exit 1 here.

🛡️ Proposed fix
 exec 9>"$CANARY_WORK/.lock"
-flock -n 9 || { echo "another canary run holds $CANARY_WORK/.lock — exiting"; exit 0; }
+command -v flock >/dev/null 2>&1 \
+  || { echo "✗ flock is missing from this image — cannot serialize runs" >&2; exit 1; }
+flock -n 9 || { echo "another canary run holds $CANARY_WORK/.lock — exiting"; exit 0; }

9-41: LGTM!

Also applies to: 56-67

__tests__/integration-suite/local-runner.test.ts (1)

18-49: LGTM!

Also applies to: 88-204, 213-263

integration-suite/README.md (1)

17-23: LGTM!

Also applies to: 27-105, 113-114

integration-suite/local/runner-daily.sh (1)

17-88: LGTM!

integration-suite/ci-entrypoint.sh (2)

124-124: 🩺 Stability & Availability | ⚡ Quick win

⚠️ Unverified finding
Sandbox verification was unavailable.

A bare test && assignment line returns 1 on the normal path — confirm this script does not use set -e.

Line 124 is a standalone && list. When CANARY_DAEMON_DEAD is not 1 — the normal case for both daily legs — the line's exit status is 1. If ci-entrypoint.sh sets -e, the script exits there before any probe runs. run.sh line 72 uses the if … then … fi form for the same normalization, so the two files are inconsistent. Lines 1-40 of this file are not in the review context, so the set options cannot be read here.

The if form is unconditionally safe and matches run.sh.

🛡️ Proposed fix
-[ "${CANARY_DAEMON_DEAD:-0}" = 1 ] && CANARY_DAEMON=1
+if [ "${CANARY_DAEMON_DEAD:-0}" = 1 ]; then CANARY_DAEMON=1; fi

The same shape exists at integration-suite/probe-cli.sh line 88.


41-48: LGTM!

Also applies to: 102-108, 125-140, 217-219

integration-suite/run.sh (1)

64-89: LGTM!

integration-suite/probe-cli.sh (2)

134-134: 🩺 Stability & Availability

⚠️ Unverified finding
Sandbox verification was unavailable.

Confirm no other trap … EXIT exists in this script.

Line 134 installs trap daemon_stop EXIT. A trap on the same signal replaces the previous handler; it does not chain. If probe-cli.sh already registers an EXIT handler for cleanup — temp dirs, the marker, the probe workspace — daemon mode silently drops it, and the effect appears only on the daemon leg. Lines 142-152 and 272-295 are outside the review context.


89-135: LGTM!

Also applies to: 250-271, 306-346, 355-364

.github/workflows/integration-suite.yml (1)

3-11: LGTM!

Also applies to: 15-22

CHANGELOG.md (2)

3-8: 📐 Maintainability & Code Quality

⚠️ Unverified finding
Sandbox verification was unavailable.

Confirm package.json carries the matching version.

The heading declares 1.0.1-beta.0. dist/cli.mjs inlines package.json's version, and daemon-download.ts derives the release URL from it — per the release-pipeline entry at line 231 of this file. A CHANGELOG heading ahead of package.json makes the released tag and the changelog disagree.


221-223: LGTM!

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/integration-suite.yml:
- Around line 12-14: Update the documentation to describe the daily canary as
cron-based rather than systemd-based: in .github/workflows/integration-suite.yml
lines 12-14, mention the single cron line, runner image, and 06:17 UTC slot; in
integration-suite/README.md lines 7-8, replace the systemd user timer wording
with one cron line on the canary box.

In `@integration-suite/local/install.sh`:
- Line 44: Replace the --help branch’s self-reading sed pipeline with an
embedded usage function near the argument loop, such as usage(), containing the
documented invocation and all supported flags. Have the --help branch call usage
and exit successfully, without reading from $0.

In `@integration-suite/local/runner-entrypoint.sh`:
- Around line 53-55: Update the CANARY_REF validation message in the shell
entrypoint so it no longer recommends the stale origin/failproofaid branch or
references the obsolete merge condition; align the guidance with the installer
and current secrets.env.example by directing operators to origin/main.

In `@integration-suite/probe-cli.sh`:
- Around line 136-140: Update the early marker-clear command near updateConfig
so it continues after failure while recording the failure details instead of
suppressing stderr and ignoring the status. Preserve the existing marker reset
behavior and align its failure reporting with the hard-failing marker-set path
around line 260.

In `@integration-suite/README.md`:
- Around line 186-189: Update the canary secrets path in the README text to
`~/fp-canary/secrets.env`, matching install.sh’s `$WORK/secrets.env` location
and the existing `~/fp-canary` documentation; leave the surrounding workflow and
permissions guidance unchanged.

---

Nitpick comments:
In `@__tests__/integration-suite/local-runner.test.ts`:
- Around line 205-212: Strengthen the scored-PASS assertion in the test case
“the DEAD leg scores the fail-closed deny as PASS on both probes” so each probe
variable is paired with its corresponding log variable, explicitly matching A
with LOGA and B with LOGB. Preserve the existing requirement for exactly two
matches and the daemon_failed_closed detector assertion.
- Around line 51-87: Add integration tests for the real
integration-suite/local/install.sh file, following the existing local-runner
test style and available fixtures such as installSh and secretsExample. Cover
marker-based cron replacement via CRON_MARKER and crontab filtering, requiring
CANARY_SLACK_WEBHOOK, rejecting origin/failproofaid while requiring
CANARY_REF=origin/main in the secrets example, and ensuring the dry-run did()
path reports “would:” without claiming a ✓.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: ae207ed3-931d-4f5a-b2cb-5553ea205a53

📥 Commits

Reviewing files that changed from the base of the PR and between e022752 and e8de227.

📒 Files selected for processing (12)
  • .github/workflows/integration-suite.yml
  • CHANGELOG.md
  • __tests__/integration-suite/local-runner.test.ts
  • integration-suite/README.md
  • integration-suite/ci-entrypoint.sh
  • integration-suite/local/Dockerfile.runner
  • integration-suite/local/install.sh
  • integration-suite/local/runner-daily.sh
  • integration-suite/local/runner-entrypoint.sh
  • integration-suite/local/secrets.env.example
  • integration-suite/probe-cli.sh
  • integration-suite/run.sh

Comment on lines +12 to +14
# The DAILY runs moved off Actions to a local canary box for cost —
# integration-suite/local/ carries the systemd timer + wrapper that replaced
# the cron that used to live here (same 06:17 UTC slot). This workflow stays

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Documentation still describes the replaced systemd-timer design. This PR schedules the daily canary with a single crontab line written by integration-suite/local/install.sh, and integration-suite/local/ contains no systemd unit and no wrapper script. Two documents still name systemd as the box's scheduler.

  • .github/workflows/integration-suite.yml#L12-L14: replace "carries the systemd timer + wrapper that replaced the cron that used to live here" with a description of the cron line plus the runner image, keeping the 06:17 UTC slot.
  • integration-suite/README.md#L7-L8: replace "a systemd user timer on the canary box" with "one cron line on the canary box", which agrees with line 35 of the same file.
📍 Affects 2 files
  • .github/workflows/integration-suite.yml#L12-L14 (this comment)
  • integration-suite/README.md#L7-L8
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/integration-suite.yml around lines 12 - 14, Update the
documentation to describe the daily canary as cron-based rather than
systemd-based: in .github/workflows/integration-suite.yml lines 12-14, mention
the single cron line, runner image, and 06:17 UTC slot; in
integration-suite/README.md lines 7-8, replace the systemd user timer wording
with one cron line on the canary box.

--no-cron) DO_CRON=0 ;;
--dry-run) DRY=1 ;;
--at) CRON_AT="${2:?--at needs a value, e.g. --at \"17 6\"}"; shift ;;
-h|--help) sed -n '2,26p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

--help prints nothing on the documented install path.

Line 44 reads the help text from "$0". On the advertised invocation (line 5), bash <(curl -fsSL …) sets $0 to a process-substitution file descriptor such as /dev/fd/63. That descriptor is a pipe, not a seekable file, and bash has already consumed it. sed -n '2,26p' "$0" then reads nothing, so --help exits 0 with no output.

Embed the usage text instead of re-reading the script.

🐛 Proposed fix
-    -h|--help)  sed -n '2,26p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
+    -h|--help)  usage; exit 0 ;;

Add above the argument loop:

usage() {
  cat <<'USAGE'
install.sh — set the canary box up in ONE command.

  bash <(curl -fsSL .../integration-suite/local/install.sh) ~/secrets.env

Flags:
  --now        run one canary immediately after installing (foreground)
  --no-cron    set everything up but do not touch the crontab
  --dry-run    print what would happen; touch nothing
  --at "M H"   cron minute and hour (default "17 6")
USAGE
}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
-h|--help) sed -n '2,26p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
usage() {
cat <<'USAGE'
install.sh — set the canary box up in ONE command.
bash <(curl -fsSL .../integration-suite/local/install.sh) ~/secrets.env
Flags:
--now run one canary immediately after installing (foreground)
--no-cron set everything up but do not touch the crontab
--dry-run print what would happen; touch nothing
--at "M H" cron minute and hour (default "17 6")
USAGE
}
-h|--help) usage; exit 0 ;;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@integration-suite/local/install.sh` at line 44, Replace the --help branch’s
self-reading sed pipeline with an embedded usage function near the argument
loop, such as usage(), containing the documented invocation and all supported
flags. Have the --help branch call usage and exit successfully, without reading
from $0.

Comment on lines +53 to +55
# Required, no default ON PURPOSE: a baked-in ref would silently keep probing a
# stale branch after the daemon branch merges to main. The env file states it.
: "${CANARY_REF:?CANARY_REF missing from --env-file (origin/failproofaid until #632 merges, then origin/main)}"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

The CANARY_REF error text is stale and contradicts the installer.

The message tells the operator to use origin/failproofaid "until #632 merges". install.sh line 141 now refuses that exact value because the branch merged, and secrets.env.example line 11 ships origin/main. A person who hits this error follows the message and is then rejected by the installer.

🐛 Proposed fix
-: "${CANARY_REF:?CANARY_REF missing from --env-file (origin/failproofaid until `#632` merges, then origin/main)}"
+: "${CANARY_REF:?CANARY_REF missing from --env-file (set CANARY_REF=origin/main in secrets.env)}"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# Required, no default ON PURPOSE: a baked-in ref would silently keep probing a
# stale branch after the daemon branch merges to main. The env file states it.
: "${CANARY_REF:?CANARY_REF missing from --env-file (origin/failproofaid until #632 merges, then origin/main)}"
# Required, no default ON PURPOSE: a baked-in ref would silently keep probing a
# stale branch after the daemon branch merges to main. The env file states it.
: "${CANARY_REF:?CANARY_REF missing from --env-file (set CANARY_REF=origin/main in secrets.env)}"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@integration-suite/local/runner-entrypoint.sh` around lines 53 - 55, Update
the CANARY_REF validation message in the shell entrypoint so it no longer
recommends the stale origin/failproofaid branch or references the obsolete merge
condition; align the guidance with the installer and current secrets.env.example
by directing operators to origin/main.

Comment on lines +136 to +140
# The HOME volume persists across runs, so YESTERDAY's marker survives into
# today. Clear it EARLY in every mode — before install/wire — because wire()
# runs vendor CLIs (openclaw onboard fires plugin hooks) that would fail closed
# against a marker with no daemon up yet. Daemon mode re-sets it after wire.
bun -e 'const m=await import("/repo/src/hooks/fp-config.ts");m.updateConfig({daemon:{configured:false}})' 2>/dev/null || true

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

The marker clear swallows its own failure, while the marker set does not.

Line 140 ends with 2>/dev/null || true. The comment above it states the clear is required: a marker left by yesterday's daemon-mode run fail-closes the vendor CLI calls that wire() makes. If updateConfig fails — a corrupted config.toml in the persistent volume, a missing /repo/src/hooks/fp-config.ts on an older ref — the marker survives and every probe in an in-process leg denies. The result reads as INCONCLUSIVE with no cause recorded. Line 260 hard-fails for the opposite operation, so the two directions are inconsistent.

Keep the run going, but record the failure.

🛡️ Proposed fix
-bun -e 'const m=await import("/repo/src/hooks/fp-config.ts");m.updateConfig({daemon:{configured:false}})' 2>/dev/null || true
+bun -e 'const m=await import("/repo/src/hooks/fp-config.ts");m.updateConfig({daemon:{configured:false}})' \
+  || echo "⚠️  could not clear the daemon.configured marker — an inherited marker will fail-close wire() and every probe" >&2
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# The HOME volume persists across runs, so YESTERDAY's marker survives into
# today. Clear it EARLY in every mode — before install/wire — because wire()
# runs vendor CLIs (openclaw onboard fires plugin hooks) that would fail closed
# against a marker with no daemon up yet. Daemon mode re-sets it after wire.
bun -e 'const m=await import("/repo/src/hooks/fp-config.ts");m.updateConfig({daemon:{configured:false}})' 2>/dev/null || true
# The HOME volume persists across runs, so YESTERDAY's marker survives into
# today. Clear it EARLY in every mode — before install/wire — because wire()
# runs vendor CLIs (openclaw onboard fires plugin hooks) that would fail closed
# against a marker with no daemon up yet. Daemon mode re-sets it after wire.
bun -e 'const m=await import("/repo/src/hooks/fp-config.ts");m.updateConfig({daemon:{configured:false}})' \
|| echo "⚠️ could not clear the daemon.configured marker — an inherited marker will fail-close wire() and every probe" >&2
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@integration-suite/probe-cli.sh` around lines 136 - 140, Update the early
marker-clear command near updateConfig so it continues after failure while
recording the failure details instead of suppressing stderr and ignoring the
status. Preserve the existing marker reset behavior and align its failure
reporting with the hard-failing marker-set path around line 260.

Comment on lines +186 to +189
the workflow triggers on `workflow_dispatch` **only**, so fork PRs can never
reach them. (The canary box keeps its own copy of the same variables in
`~/.config/failproofai-canary/secrets.env`, chmod 600 — updating one does not
update the other.)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Correct the box secrets path.

Line 188 states the box keeps its variables in ~/.config/failproofai-canary/secrets.env. install.sh installs the file at $WORK/secrets.env, where WORK defaults to $HOME/fp-canary (lines 30 and 101), and line 49 of this README already says ~/fp-canary. An operator who follows line 188 edits a file no run ever reads.

🐛 Proposed fix
-reach them. (The canary box keeps its own copy of the same variables in
-`~/.config/failproofai-canary/secrets.env`, chmod 600 — updating one does not
-update the other.)
+reach them. (The canary box keeps its own copy of the same variables in
+`~/fp-canary/secrets.env`, chmod 600 — updating one does not
+update the other.)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
the workflow triggers on `workflow_dispatch` **only**, so fork PRs can never
reach them. (The canary box keeps its own copy of the same variables in
`~/.config/failproofai-canary/secrets.env`, chmod 600 — updating one does not
update the other.)
the workflow triggers on `workflow_dispatch` **only**, so fork PRs can never
reach them. (The canary box keeps its own copy of the same variables in
`~/fp-canary/secrets.env`, chmod 600 — updating one does not
update the other.)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@integration-suite/README.md` around lines 186 - 189, Update the canary
secrets path in the README text to `~/fp-canary/secrets.env`, matching
install.sh’s `$WORK/secrets.env` location and the existing `~/fp-canary`
documentation; leave the surrounding workflow and permissions guidance
unchanged.

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