Skip to content

fix(ci): the unattended repair leaves the model no credential to copy; both workflows pin felix-home at cb3a72ce - #26

Merged
LeavesJ merged 1 commit into
mainfrom
claude/unattended-no-credential-pin
Sep 25, 2026
Merged

LeavesJ merged 1 commit into
mainfrom
claude/unattended-no-credential-pin

Conversation

@LeavesJ

@LeavesJ LeavesJ commented Sep 24, 2026 •

Copy link
Copy Markdown
Owner

A workflow file, so this one is yours to merge. Both pins move together, as #24 and the comment in gate.yml say they must.

.github/workflows/unattended.yml is now felix-home's engine/harness/templates/unattended.yml at cb3a72ce4f0d85d37253359ca73b4976e9d45201, the merge of felix-home #344 (2026-09-24, engine 0.49.194), copied whole, with its two per-repository placeholders filled in: {{FELIX_REPO}} is LeavesJ/felix-home and {{FELIX_REF}} is cb3a72ce4f0d85d37253359ca73b4976e9d45201. Nothing else differs, and anyone can check that:

diff <(git -C ~/Documents/Felix show cb3a72ce4f0d85d37253359ca73b4976e9d45201:engine/harness/templates/unattended.yml | sed -e 's#{{FELIX_REPO}}#LeavesJ/felix-home#' -e 's#{{FELIX_REF}}#cb3a72ce4f0d85d37253359ca73b4976e9d45201#') .github/workflows/unattended.yml

gate.yml moves its pin to the same commit, and the two comment lines that named the old one now name this one. Nothing else in it changes.

What was wrong with the copy on main. Both checkouts left a credential on the runner for the model step to read. actions/checkout keeps what it fetched with unless told otherwise: the job's token in repo/.git/config as an http.extraheader, and the deploy key as a file under $RUNNER_TEMP that felix/.git/config names by path. The model step runs claude -p with --allowedTools "Read,Edit", and a tool-name allow rule matches every path. felix-home measured it on the pinned 2.1.37 against a scripted Messages API: Read returned all three files, Edit put their contents in a tracked file, the gate's secret scan passed the diff (a base64 header and a ghs_ token are shapes it does not know), and the push step commits every tracked change. A model steered by a hostile failing log is the reader to plan for.

What the template does now.

  • Both checkouts set persist-credentials: false. The token stays out of repo/.git/config; the deploy key file and the core.sshCommand naming it are removed before the next step runs. Nothing between the fetch and the push needed either: felix repair and the escalation label go through gh with their own GH_TOKEN, and the gate reads refs the fetch already brought.
  • The token appears once in the job, as GITHUB_TOKEN in the Verify and push step's environment. A credential helper hands it to git at push time, written to no file and passed in no argument. An empty helper first clears any helper the runner already had, so this step's token is the one git offers.
  • The model step is handed no token, in its environment or its text. It also carries felix-home #341, which landed on main between the old pin and this one: Read(//proc/**) is denied, so the step's own ANTHROPIC_API_KEY is not a file the model can copy out of /proc/self/environ on a Linux runner. Its tools are scoped: --allowedTools "Read(./**),Edit(./**)" and --disallowedTools "Read(.git/**),Edit(.git/**)". A bare tool name matches every path, and a bare-path deny matches only under the working directory, so the allow had to be scoped too: measured on 2.1.37, Read,Edit with the deny alone still read and edited ../felix/.git/config, where a core.hooksPath would have made the verify step's own git commands run code with the token in their environment.

Checked (T1).

  • Copy equals the template. The diff above prints nothing, and the file is 292 lines. No {{FELIX_...}} placeholder is left.
  • The pinned sha. It is felix-home main (compare reports identical), engine 0.49.194, and it holds projects/elenchus/project.json with remote_match: LeavesJ/Elenchus, so felix repair and felix gate resolve this repository there.
  • Parses. 7 steps; both checkouts are actions/checkout@v5; the Felix checkout is LeavesJ/felix-home at the sha, read as a string; persist-credentials is false on both.
  • The checks felix-home #344 added to its suite, run on this copy. Every checkout drops its credential (0 of 2 keep one); the model step's text and environment name no token; its allow list is exactly Read(./**),Edit(./**) and its deny list carries Read(.git/**), Edit(.git/**) and Read(//proc/**); the Verify and push step, cut out of this file and run under bash -e against a stub git that runs the helper as git would and exits 7 on any push but push origin HEAD:$BRANCH, pushes to the repaired branch, and the helper answers password=<the step's GITHUB_TOKEN> while the token is in no argument git was given. The same checks on the copy main holds today fail six ways: 2 of 2 checkouts keep a credential, the allow list is the bare Read,Edit, none of the three denies is present, and no helper answers.
  • End to end. A workflow_dispatch of this branch's workflow: run 36034788167, success, stopped at scope with nothing to repair; and the gate workflow at the same pin, run 36034789391, success. See the comment below.

Not changed. Nothing runs a model while FELIX_UNATTENDED is unset (gh variable list --repo LeavesJ/Elenchus shows none, checked 2026-09-24T17:30Z). Of the 48 runs of this workflow on record, 46 are skipped and 2 are dispatches that stopped at the scope step with nothing to repair; the budget and model steps have never run here, so no model has read a credential from the old copy. The deploy key and the API key secrets are untouched.

…; both workflows pin felix-home at cb3a72ce

.github/workflows/unattended.yml is felix-home's engine/harness/templates/unattended.yml
at cb3a72ce4f0d85d37253359ca73b4976e9d45201 (the merge of felix-home #344, engine
0.49.194), copied whole, with {{FELIX_REPO}} = LeavesJ/felix-home and {{FELIX_REF}} =
that sha. Both checkouts set persist-credentials: false, the push step hands git the
job's token itself through a credential helper, and the model's tools are scoped to
the checkout with .git and /proc denied (felix-home #341 and #344).

gate.yml moves its pin to the same commit, so the repair loop's pre-push gate and the
gate workflow run one engine.
@LeavesJ

LeavesJ commented Sep 24, 2026 •

Copy link
Copy Markdown
Owner Author

End to end: workflow_dispatch of this branch's workflow, run 36034788167, success.

  • Both checkouts print persist-credentials: false, and each one runs Removing auth inside its own step, before the next step starts, not only in its post step. The Felix checkout reports HEAD is now at cb3a72c fix(unattended): neither checkout leaves a credential for the model step to read, and the model's tools are scoped to the checkout with its .git denied.
  • Scope the failure ran as shell: /usr/bin/bash -e {0}. The real felix repair at the pin read this branch's gate run as still running (exit 5). The step printed nothing to repair on claude/unattended-no-credential-pin and exited 0.
  • Spend from the budget, Attempt a repair and Verify and push: all skipped. Report printed "Nothing to repair: the gate is green, still running, ended without...". No budget was spent, no model ran, no label was applied.

The gate workflow on this branch, run 36034789391, with Felix checked out at the same pin: success. So both workflows have graded this repository with cb3a72ce before that commit defines green here.

FELIX_UNATTENDED is still unset (gh variable list --repo LeavesJ/Elenchus returns nothing), checked after the run. With this run the workflow's record here is 47 skipped (the 47th is the workflow_run after this branch's green gate, skipped as designed) and 3 dispatches that stopped at scope.

@LeavesJ
LeavesJ merged commit b29c355 into main Sep 25, 2026
2 checks passed
LeavesJ added a commit that referenced this pull request Oct 2, 2026
`python -m elenchus.web` served the unauthenticated surface with no ceiling on
paid model calls. `spend.from_env` returns None, meaning unbounded, when nothing
is exported. That is right for the CLI, the probes and the suite, and only
scripts/serve_invitee.py exported a value, so the documented `python -m` path
was not bounded at all. web/__main__.py now does the same setdefault, after
_load_dotenv and before create_app, so precedence is export, then .env, then
the default floor.

The ordering is the fix. model_factory_for_this_process samples the environment
at create_app time and __main__ builds the app at import, so the same line one
place lower satisfies every grep for the variable and leaves the process
exactly as unbounded. The test is behavioural: it seizes control inside
create_app and reads the factory the entrypoint actually arrived with, and it
enumerates entrypoints off the tree so a third one is covered the day it lands.
It was watched red first and mutation-tested twice (the line moved below
create_app; setdefault swapped for assignment).

This is e625a88 of 2026-09-03 without its CI-pin hunk: main already pins
felix-home at cb3a72ce (#26), and a workflow change is the founder's to merge.
The founder chose this commit over PR #16 on 2026-10-01 ("whichever is the most
recent"). #16 arms the choke point in app.py, touches no file this change
touches, and its own author called it the stronger on the property; it stays
open for the founder to close or land.

Co-authored-by: Jarron Deng <jarrondeng@berkeley.edu>
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