Skip to content

Run container remediation with Pi, without agent write access - #33

Merged
rgarcia merged 4 commits into
mainfrom
hypeship/container-remediation-pi
Nov 6, 2026
Merged

rgarcia merged 4 commits into
mainfrom
hypeship/container-remediation-pi

Conversation

@rgarcia

@rgarcia rgarcia commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Moves container-remediation.yml from Claude Code (claude -p --dangerously-skip-permissions) to Pi, and takes GitHub write credentials away from the agent.

Agent invocation

  • Triage and fix both run pi --print --no-session --approve --model "$PI_PREFERRED_MODEL" --thinking max @<prompt file>, using Node 22, ANTHROPIC_API_KEY/OPENAI_API_KEY, and the PI_PREFERRED_MODEL org variable (replaces CLAUDE_CODE_PREFERRED_MODEL). The workflow had no model input, so none was added.
  • Prompts are still fetched from security-workflows-ref. They are rendered with envsubst to a file instead of piped.
  • Prompt changes:
    • Triage: the KEV check points to CISA's JSON feed and says to fetch it with curl. Claude Code had a web-fetch tool; Pi does not.
    • Fix: scan and triage results are read from $SCAN_DIR (outside the checkout). The prompt also says the agent has no GitHub credentials and must not commit or push.

Credential boundary
Before this change, the fix job minted an App token with no permission limit, wrote it into a global git insteadOf rewrite, and then ran the agent.

  • go-modules (new job, only when the caller passes go-version-file): if the module needs private github.com/kernel/* modules, it mints a contents: read token scoped to just those repos, runs go mod download with an env-only git rewrite, and revokes the token. It checks out main and uploads only $GOMODCACHE/cache/download/github.com/kernel as a 1-day artifact. The fix job builds on that same main SHA, so the module cache matches the tree the agent edits. This is a separate job because every secret a job references stays readable in runner memory for the whole job, and runners have passwordless sudo, so revoking the minted token before Pi starts would not protect the App key.
  • fix (contents: read; references no secrets except ANTHROPIC_API_KEY and OPENAI_API_KEY): checks out with persist-credentials: false and copies the private-module artifact into its module cache. Public modules download through the proxy as usual.
    • After Pi runs, a runner step takes a snapshot of every change since the pre-agent origin/main commit, committed or not. It keeps only go.mod, go.sum, and Dockerfile/Dockerfile.*/*.Dockerfile (the old git add '*.mod' '*.sum' '*Dockerfile*' set, tightened to basenames). Then it cleans the checkout, re-applies the patch, and runs go build ./... in each changed module.
    • setup-go caching is off in both Go jobs, so neither private module sources nor a module cache the agent could have written is saved to the Actions cache.
  • publish (new job): rebuilds the branch from the recorded main SHA and applies the patch. It re-checks the allowlist, rejects non-regular files and added replace directives, and commits. Only then does it mint a token limited to the calling repo (repositories: comes from $GITHUB_REPOSITORY, because scheduled runs have no github.event.repository) with contents: write + pull-requests: write, to push (--force-with-lease against the branch SHA recorded before Pi ran) and to create or update the PR. It never runs Go, Node, or Pi.
  • PR title, body, assignee, and reviewer are unchanged.

Other

  • Every action is pinned to a commit SHA. trivy-action is pinned to the current master commit, so the Trivy version does not change.
  • Checkouts in scan and triage no longer persist the default token.
  • The triage job timeout goes from 5 to 15 min and the fix job from 10 to 30 min. The fix job now also runs a separate build.
  • The self-test passes security-workflows-ref: ${{ github.sha }} so it uses the branch's prompts and allowlist instead of main's.
  • New scripts/test_container_remediation.py and container-remediation-tests.yml add credential-boundary regression tests.

Callers

kernel/kernel calls this workflow 5 times with secrets: inherit, so OPENAI_API_KEY and the App secrets reach it without caller changes. Its contents: write / pull-requests: write grants are no longer used by this workflow and could be lowered to contents: read later. That is optional; nothing here requires it.

Validation

  • actionlint 1.7.12 with shellcheck 0.11.0 on the changed workflows: clean except two SC2016 infos for the single-quoted envsubst variable lists, which are intentional and also present on main.
  • python -m unittest discover -s scripts -p test_container_remediation.py: 17 tests pass. They cover:
    • pinning and checkout credentials;
    • publish token scoped to the calling repo;
    • the jobs that run Pi reference no secrets.* beyond the two model keys (toJSON(secrets) included);
    • the module download step revoking its token and exporting only the github.com/kernel cache subtree (stubbed go/gh), and the fix job copying it into the module cache;
    • the module token being read-only and minted only in go-modules;
    • step order;
    • fix resetting to the go-modules main SHA rather than a newer origin/main;
    • publish never running agent or repo code;
    • the allowlist;
    • the real snapshot and publish shell run against local git repos (agent commits, the allowlist exit, the file-mode exit for symlinks and submodules, local and remote replace).
  • Mutation checks: the tests fail when ADMIN_APP_PRIVATE_KEY or toJSON(secrets) is put back into the fix job, the publish allowlist exit 1 or file-mode exit 1 is removed, publish goes back to the default success condition, the allowlist is opened up, the token revoke is removed, the replace check is removed, the pre-agent soft reset is removed, a checkout persists credentials, or a token is passed to the agent step.
  • Live self-test on this branch (run 42112157223): all four jobs passed (before the go-modules split).
    • Triage ran on Pi with the org PI_PREFERRED_MODEL and wrote a valid triage-result.json (71 findings, 6 actionable groups).
    • Fix bumped the fixture's go-jose/v3, x/net, grpc, and otel/sdk versions, and the build passed.
    • Publish pushed security/container-remediation and opened security: container vulnerability remediation #32 with the usual assignee and reviewer. I closed security: container vulnerability remediation #32 because it was test output, as security: container vulnerability remediation #15 was.
    • After the job split: run 42124727030 passed all five jobs (scan, go-modules, triage, fix, publish), with no ADMIN_APP reference in the fix job. It opened security: container vulnerability remediation #34, which I closed as test output.
    • Neither run exercised the private-module read token, because the fixture has no github.com/kernel/* dependencies. The first kernel/kernel api run will be the first real test of that path. The four callers without go-version-file skip go-modules. fix and publish use explicit !cancelled() conditions so that skip doesn't cascade, but no live run has exercised that skipped path.

Trade-off: the private-module artifact (module zips from repos such as kernel-images-private) can be downloaded for 1 day by anyone with read access to the calling repo.

Not verified: whether the PI_PREFERRED_MODEL variable and the OPENAI_API_KEY secret are visible to kernel/kernel (no org admin access). The README.md "Required secrets/variables" section is left as-is because other workflow migrations in this repo touch it.

@rgarcia

rgarcia commented Nov 5, 2026

Copy link
Copy Markdown
Contributor Author

Picked ulziibay-kernel to review: touched the changed code in workflows; 26 pending

@cursor cursor 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.

Stale Bugbot comment from a previous run.

@rgarcia

rgarcia commented Nov 5, 2026

Copy link
Copy Markdown
Contributor Author

Pushed 323f8e6: the job that runs Pi no longer references any GitHub App secret.

Every secret a job references stays in runner memory for the whole job, and hosted runners have passwordless sudo. A prompt-injected agent in the old fix job could therefore read ADMIN_APP_PRIVATE_KEY and mint its own token, even though the module token was revoked before Pi started.

  • New go-modules job (runs only when the caller passes go-version-file): mints the read-only token scoped to the private github.com/kernel/* repos, runs go mod download, and revokes the token. It uploads only $GOMODCACHE/cache/download/github.com/kernel as a 1-day artifact. setup-go caching is off there too.
  • fix references only ANTHROPIC_API_KEY and OPENAI_API_KEY. It copies that subtree into its module cache before Pi runs. Public modules come from the proxy.
  • Job conditions: fix runs when go-modules either succeeded or was skipped. publish now uses !cancelled() && needs.fix.result == 'success', because the default success() would skip it for callers with no Go module, where go-modules is skipped.
  • Tests (16, all passing):
    • A test parses the workflow and fails if any job that runs Pi references secrets.* beyond the two model keys, including toJSON(secrets). It fails when ADMIN_APP_PRIVATE_KEY is put back into fix.
    • The module-download step is run with stubbed go/gh to check that it revokes the token and exports only the private subtree.
    • The publish gate tests run the real step against local repos. Removing either the allowlist exit 1 or the file-mode exit 1 makes them fail (symlinks and submodules are both covered).
  • Live self-test (run 42124727030): all five jobs passed. The fixture has no private deps, so the token path and the skipped-go-modules path still haven't run live.
  • Trade-off: the module artifact can be downloaded for 1 day by anyone with read access to the calling repo.

@cursor cursor 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.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 323f8e6. Configure here.

@rgarcia
rgarcia merged commit cbac8b1 into main Nov 6, 2026
2 checks passed
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