Skip to content

Resolve #95, #97, #98 — toolchain floor, CodeRabbit findings, gate guidance - #110

Merged
hyperpolymath merged 5 commits into
mainfrom
arena/01a0edde-feedback-o-tron
Sep 29, 2026
Merged

hyperpolymath merged 5 commits into
mainfrom
arena/01a0edde-feedback-o-tron

Conversation

@arena-ai-coding-agent

Copy link
Copy Markdown
Contributor

Work in progress. Landing in reviewable slices; each acceptance criterion is met by a named commit and backed by a green cell.

Closes

Slices

  1. fix(build) — raise the floor to Elixir 1.17 / OTP 27 (main does not build on OTP 26 while README and the AI install guide promise it #97).
  2. fix(harness) — repair consent_bypass_check.sh and give it a positive control (F3), which gates F1.
  3. fix(security) — replace the forgeable consent atom with an unforgeable capability (F1).
  4. fix(doors) — stop silently dropping the HTTP door when the port is taken (F2).
  5. fix(config+docs) — F4, F5, F6 and the four minors.
  6. chore(ci) — align the repo with the canonical rsr-template-repo.

Out of scope

📛 Hands off .a2ml grammar (standing ruling). No bulk rename of manifests; manifest edits are value strings only. No --yes flag is introduced anywhere.

⚠ No agent runs the owner-only stranger rehearsal; where #95 touches that document, the change is to the plan/step text only.

Closes #97 (resolution A).

`main` could not build on OTP 26: `mix.lock` resolves `cowlib` 2.20.0,
required by `cowboy` 2.19.0, and `cow_capsule.erl` calls `parse/1`, which
OTP 26 does not provide. `README.adoc` and `docs/AI_INSTALLATION_GUIDE.adoc`
both promised "OTP 26 or newer", so an AI or a stranger following the
documented prerequisites got a compile failure instead of a working install
— which bears directly on the "install hyperpolymath's FoT please" goal.

This is the support-policy call the issue asked for, resolved as option A:
raise the floor rather than pin `cowlib` back to 2.16.0. The toolchain the
repo already pins (mise.toml: erlang 27.0, elixir 1.19.5-otp-27) is OTP 27,
and holding `cowlib` at 2.16.0 to keep a floor nothing else targets would
have frozen out modern cowboy/bandit fixes.

Changed together in one commit, so no reader sees a stale floor:

* .github/workflows/elixir-ci.yml — matrix is now 1.17, 1.18 and 1.19 on
  OTP 27. The three OTP-26 cells are gone; every remaining cell is a
  measurement of a version the documents promise. Dialyzer moves from
  1.17/26 to 1.19/27 for the same reason.
* elixir-mcp/mix.exs — `elixir: "~> 1.15"` to `"~> 1.17"`, matching the
  floor the matrix measures.
* README.adoc, docs/AI_INSTALLATION_GUIDE.adoc, elixir-mcp/README.adoc —
  the floor, and every "older than" fallback sentence that quoted it.
* 0-AI-MANIFEST.a2ml — value strings only: "Elixir 1.15+"/"OTP 26+" to
  "Elixir 1.17+"/"OTP 27+". Grammar untouched, per the standing ruling.
* Justfile — `just doctor` reports and enforces the new floor.
* mise.toml — the comment no longer claims the matrix covers OTP 26.

Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
@coderabbitai

coderabbitai Bot commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

Important

Review skipped

Bot user detected.

To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Advanced

Run ID: f70efe14-3570-416f-95b7-05a2216f1f22

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

Two findings from #95, in dependency order: F3 gates F1, because a security
fix verified by a miscounting harness is unverified.

## F3 — the harness counted the wrong thing, and had never been seen to fail

`scripts/consent_bypass_check.sh` counted *every* `gh` invocation, so its
verdict depended on whether the engine happened to probe `gh auth token`
rather than on whether it sent anything. A harness cannot pass or fail for a
reason unrelated to the property it measures.

* Only `gh issue create` is counted now. A credential probe is not a send.
* New `--self-test` positive control: the script rewrites the consent check
  to `true`, rebuilds, and requires itself to FAIL against that build —
  then restores the tree and re-runs against the real gate. If a mutant with
  a defeated gate walks through unchallenged, the harness fails loudly. The
  mutation is anchored to an exact marker string, and the self-test refuses
  to run if that marker has moved, so it cannot quietly assert nothing.
  Verified locally with a stubbed builder: clean build PASSes, mutant FAILs,
  tree restored.
* Wired into CI (`--self-test`) and into `just consent`.

## F1 — `:human_confirmed` was a forgeable atom

`Submitter` selected the credentialed branch on
`Keyword.get(opts, :consent) == :human_confirmed`. Any code in the VM could
name that atom, and the check ran on data the caller supplied — the estate's
own doctrine, a default is not a gate, applies verbatim: a claim is not a
proof.

New `FeedbackATron.Consent` mints what the send path now requires:

* **Unguessable** — 256 random bits, so no wire client can mint or guess one.
  The MCP door and the HTTP door are now structurally incapable of asserting
  consent, which is what the pre-ledger rule always claimed.
* **One-time** — redeeming deletes it, so it cannot be replayed.
* **Payload-bound** — issued over a digest of the exact report, destinations
  and labels that were shown, so a leaked value cannot send something else.
* **Expiring** — unspent capabilities die inside five minutes.

`CLI` mints the capability at the terminal, in the process that asked the
question, and carries no claim about it into the opts. Fails closed: if the
service is unreachable the answer is an error, never consent.

**No `--yes` flag is added.** Non-tty still means no.

**Stated residual:** code already executing inside the VM can call
`Consent.issue/2` directly — and such code can equally call `Channel`
straight. This removes *data* as a way to assert consent, which is the way
that mattered, because it is the one a remote client could reach.

## Tests

* `consent_test.exs` (new): one-time spend, payload/destination/label
  binding, expiry (backdated via `:sys.replace_state`, so no sleeping),
  refusal of every value a caller could invent, and fail-closed behaviour.
* `submitter_test.exs`: four forgery tests — the bare atom, a fabricated
  token, a real capability minted for a different payload, and one minted
  for other destinations. **Each fails against today's `main`.** Plus: a real
  capability opens the send path exactly once, and cannot carry a batch.
* "Opened the send path" is measured without a network call: the destination
  is a platform no credential can resolve, so the attempt dies at
  `Credentials.get/2` and reports `:no_credentials` — a different outcome
  from drafting, and the only one that proves the gate opened. This matters
  in CI, where `GITHUB_TOKEN` is present in the environment.
* `cli_test.exs`: a yes now attaches a capability that redeems once and does
  not redeem twice, and a yes with the service unreachable sends nothing.

Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
@arena-ai-coding-agent
arena-ai-coding-agent Bot force-pushed the arena/01a0edde-feedback-o-tron branch 2 times, most recently from ede91d1 to 332233b Compare September 29, 2026 16:27
Arena Agent and others added 3 commits September 29, 2026 16:33
…F2)

`Doors.http_children/1` probed the port with `:gen_tcp.listen` + close, and
dropped the Bandit child when the probe failed. Two defects in one shape:

* **A check-then-bind race.** The probe closed its socket before Bandit
  bound, so whatever passed the probe could still be taken — and whatever
  took it afterwards made Bandit fail on a port the probe had approved.
* **A silent, permanent failure.** `serve --no-stdio` with the port taken
  lost the only door it had and then slept forever: healthy-looking, serving
  nothing, with one warning on stderr nobody reads.

Now: the child asked for is the child handed back, and a bind failure
propagates out of the supervision tree to `CLI`, which reports it and exits
non-zero. The check-then-bind race is gone because the probe is gone —
Bandit binds exactly once.

* `Doors.children/1` always returns the Bandit child when `http: true`.
* `CLI.run/2` handles `{:error, {app, reason}}` from
  `Application.ensure_all_started/1` instead of crashing on a failed match
  (the old `{:ok, _} =` produced an opaque `CaseClauseError`). It names the
  cause and adds a hint for `:eaddrinuse` and `:eacces`, suggesting
  `serve --http-port N` — without ever dropping the term itself.

Tests, replacing the one that asserted the old silent behaviour:

* `doors_test.exs` — the child is handed back whether the port is free or
  taken, and starting it against an occupied port returns `{:error, reason}`
  naming `Bandit` and `eaddrinuse`.
* `escript_smoke_test.exs` — end to end against the real binary:
  `serve --no-stdio` on an occupied port **exits non-zero within 30 s** and
  says why. It failed by sleeping before; the test now fails by timing out,
  so the regression cannot come back quietly.

Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
…o fail (#95 F4, F5, F6, minors)

## F4 — `stapeln.toml`: the production target declared no HTTP settings

A deploy of the production target started a release with every door shut:
nothing listening, and nothing saying so. It now declares the intake door
open on the engine's own port, 7722 — deliberately not an 8080-class port.

The bind stays on `127.0.0.1`, and the comment says why: the HTTP intake
carries no authentication of its own, it is a loopback wire for a cartridge
or sidecar in the same network namespace. Widening the bind to make wiring
easier would publish an unauthenticated door, so the comment names the
supported way to reach it from outside (an authenticating proxy in front).

## F5 — Step 20.7 could not fail

`docs/superpowers/plans/…adoc` ended the credential step with a `--dry-run`
submit. A dry run returns the preview before `Credentials.get/2` is reached,
so on a host with no `oauth_token` in `hosts.yml` a broken `gh auth token`
fallback produced exactly the same green output as a working one.

The step now reads the credential the send path would use, via `mix run`,
and requires `{:ok, _}` — never `{:error, :no_credentials}` — which reaches
`gh_auth_token/1` and files nothing. It must be run in both states (with
`hosts.yml`, and with it moved aside), because each state measures a
different branch. The amendment is marked with its date and reason.

⚠ Owner-only rehearsal: this is an edit to the plan text. No agent runs it.

## F6 — the privacy watch-list omitted the owner's own example

The section's prose forbids sending "a detailed build number, a MAC address
or an IP address", and the `Look for` table beneath it had no MAC-address
row — so a rehearser following the table as instructed would have passed a
payload the prose forbids. MAC row added, with the same "network identity"
reasoning as the IP row. The credential row now covers `github_pat_…`
alongside `ghp_…` and `gho_…`; fine-grained tokens match neither old prefix.

## Minors

* `0-AI-MANIFEST.a2ml:118` (and the same class of error at :163) — the
  executable path is `elixir-mcp/feedback-o-tron`, not `./feedback-o-tron`.
  An AI reading the manifest was told the wrong binary, which bears directly
  on "install hyperpolymath's FoT please". Value strings only; grammar
  untouched, per the standing ruling.
* `README.adoc` — the 401 row now names the stale-environment-token case:
  an exported `GITHUB_TOKEN`/`GH_TOKEN` beats the gh CLI, so `gh auth login`
  can succeed while the stale variable keeps being sent. The "After Install"
  commands now use the installed name on PATH rather than a `./` path that
  only works inside `elixir-mcp/`.
* `.github/CONTRIBUTING.md` — the verification commands are now commands
  that exist: `just build`, `just lint`, `just test`. `just check` was not a
  recipe, and the "cargo check" comment belongs to a Rust project.
* `llm-warmup-{dev,user}.adoc` — `just deps` fetches dependencies; it does
  not compile them. Both guides said "fetch and compile".

Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
@hyperpolymath
hyperpolymath marked this pull request as ready for review September 29, 2026 16:52
@hyperpolymath
hyperpolymath merged commit b50d1d9 into main Sep 29, 2026
5 checks passed
@hyperpolymath
hyperpolymath deleted the arena/01a0edde-feedback-o-tron branch September 29, 2026 16:53
arena-ai-coding-agent Bot added a commit that referenced this pull request Sep 29, 2026
PR #110 merged and CI ran for real on `main`. Result:

* `Compile (warnings as errors)` — green on all three cells
* `Dialyzer` — green
* `Run tests` — **red on all three cells**, one failure:
`test run/2 confirm gate a yes with the consent service down sends
nothing and exits non-zero (FeedbackATron.CLITest)`

Exit code 2 means failures **and** compile warnings, so there are two
things to read.

This PR adds the relay so the failure is readable at all: log archives
are hosted on `results-receiver.actions.githubusercontent.com`, which is
not reachable from the sandbox, so a red cell cannot be diagnosed from
the log the way a human would. The step re-emits the failing block (and
the compile warnings) as a check annotation, which the Checks API
exposes.

No behaviour is changed by this PR. Next: read the annotation, fix the
test, then decide whether the relay stays.

Co-authored-by: Arena Agent <agent@arena.ai>
Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.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.

1 participant