CVE remediation#
Purpose#
cve_dependency_remediation is a built-in workflow
that opens pull requests upgrading vulnerable dependencies. Given rows naming a
repository and a vulnerable package, it runs a headless coding-agent CLI in an
ephemeral sandbox to make the upgrade, then pushes a branch and opens a PR.
It is the most privileged thing Seizu does: a coding agent reads untrusted repository code, and a GitHub token can push. Those two never coexist — see How a run works.
Setting it up#
You need three things: a sandbox provider, a coding-agent provider key, and a GitHub token. Configuring the GitHub token is what enables the workflow; there is no separate feature flag.
SANDBOX_API_KEY=e2b_... # see Sandbox; SANDBOX_ENABLED is not required
SANDBOX_AGENT_PROVIDER=claude # or codex, opencode
SANDBOX_AGENT_API_KEY=sk-ant-... # the CLI's provider key
REMEDIATION_GITHUB_TOKEN=... # see below
The workflow does not use the chat LLM. Add it as an activity to a workflow
whose query returns rows with repo and package keys — the seeded
New CVE dependencies requiring remediation query does this.
Always verify the token before enabling the workflow:
make remediation_smoke SMOKE_REPO=org/repo # add SMOKE_FORK=1 for fork mode
This opens real sandboxes and reproduces the clone, the credential handoff and
the push, ending in SMOKE PASS. It does not run a coding agent or open a PR.
In direct mode you can check write access first with
gh api repos/<org>/<repo> --jq .permissions — push must be true.
Important
A public target repository hides auth failures until push. Cloning a public
repo is anonymous, so the token is first exercised on git push — a successful
clone proves nothing. Always push a branch when testing.
GitHub token#
Which token you need depends on the mode. In both, the token is used to clone, push, open PRs, and by the worker-side CI watch — never while the coding agent is running.
Direct mode (default) pushes work branches into the target repositories, so the token needs write access there. Use a fine-grained PAT with resource owner set to the org that owns the targets, repository access limited to the repositories you want remediated, and:
Permission |
Why |
|---|---|
Contents: Read and write |
Clone and push the work branch |
Pull requests: Read and write |
Open PRs; read PR state for the CI watch |
Issues: Read and write |
The CI watch posts triage comments |
Checks: Read, Commit statuses: Read |
Poll check runs and legacy statuses |
Actions: Read |
Job-log tails for the fix agent (optional; degrades gracefully) |
Workflows: Read and write |
Only if fixes may touch |
Organizations may require fine-grained PAT approval under Org settings →
Third-party access. A classic PAT with repo also works but grants far more
than needed.
Fork mode (REMEDIATION_USE_FORK=true) pushes to a bot-owned fork instead,
so the token needs no write access to the targets. A fine-grained PAT cannot do
this — it has a single resource owner and so cannot both write to the bot’s
forks and open PRs on targets owned by someone else. Use a dedicated machine
account with a classic PAT:
Create a machine account for this automation only (e.g.
myorg-remediation-bot). Forks accumulate under it, or underREMEDIATION_FORK_ORG.As that account, create a classic PAT with
public_repo(all targets public) orrepo(any target private); addworkflowif fixes may modify.github/workflows/.For private targets, give the machine account read access on each target and enable the target org’s Allow forking of private repositories.
Check the target org allows classic PATs, and authorize the token for SAML SSO if enforced.
A GitHub App installation token works in either mode if you have tooling to mint
and rotate it — Seizu just reads REMEDIATION_GITHUB_TOKEN.
Keep branch protection on. Nothing should merge without human review.
Turning it off#
There is no per-user permission: the workflow is reachable only through
workflows, which are admin-managed (workflows:write). Disable the workflow,
remove REMEDIATION_GITHUB_TOKEN, or drop cve_dependency_remediation from
TEMPORAL_ENABLED_WORKFLOWS.
How a run works#
Per (repository, package) group the workflow runs four sandbox commands, each given only the environment it needs:
Phase |
Credentials |
What it does |
|---|---|---|
install |
none |
Install |
setup |
GitHub token |
Clone and create the work branch, via |
agent |
provider key only |
Run the coding agent. No GitHub token exists in the sandbox during this phase, so a prompt-injected agent has nothing to exfiltrate and cannot push. |
push |
GitHub token |
Verify the agent committed, push the branch, open or update the PR. |
The agent upgrades the dependency in every affected manifest, including compatibility code changes, and writes the PR title and body. It does not run the test suite — the sandbox usually lacks the dependencies — so CI tests the PR.
Two behaviours worth knowing:
Least-change upgrades. The agent targets the smallest released version clearing every vulnerable range, preferring the current major, then minor.
Branches are version-keyed (
seizu/dependency-update/{ecosystem}-{package}-{version}). If an open PR already exists for the branch, the run is skipped rather than force-pushing over a PR under review; a later fix needing a higher version gets its own branch. Manual re-runs are therefore safe.
Groups run sequentially to bound agent spend, and a group is never retried — a retry would repeat an expensive run and risk duplicate PRs. A failing group records an error without aborting the rest.
Fork mode caveats#
With REMEDIATION_USE_FORK=true, forks are created on demand and the PR is
opened cross-repo. Three things catch people out:
The token owner must be able to fork the target. A repository cannot be forked into the account that already owns it.
Many organizations restrict Actions on fork PRs (no secrets, or approve-first). If the CI watch keeps reporting
no_checks, check the target’s Actions fork policy.CodeQL default setup never analyzes fork PRs. A target combining default setup with a code-scanning merge rule blocks fork PRs indefinitely. Switch those repositories to CodeQL advanced setup — a committed
codeql.ymlwith apull_requesttrigger, which runs in the base-repo context.
Credential phase isolation is identical in both modes.
CI watch and fix#
An upgrade can break CI, so after pushing a PR the workflow watches its checks,
polling every REMEDIATION_CI_POLL_SECONDS until they settle or
REMEDIATION_CI_MAX_WAIT_SECONDS elapses (0 disables the watch). Checks stuck
queued past REMEDIATION_CI_QUEUED_STUCK_SECONDS are ignored rather than
waited on, as are cancelled and stale runs. A merged or closed PR ends the watch.
If any check fails, the workflow runs up to REMEDIATION_CI_FIX_MAX_ATTEMPTS
coding-agent fix runs (0 → watch and record only), reusing the same
phase-isolated flow against the PR branch. The agent triages each failure:
Caused by the upgrade → it fixes and commits; the new commits are pushed from a fresh push sandbox, re-triggering CI.
Unrelated (flaky, already failing on base, infrastructure) → it writes an explanation, which the worker posts through the GitHub API. The text is rendered into a fixed template as a block quote with
@-mentions and slash commands neutralized, so a prompt-injected agent cannot ping people or drive bots under the bot identity.
The result records ci_status: passed, fixed, failures_commented,
ci_failed, fix_failed, timed_out, no_checks, merged, pr_closed, or
error, with detail in ci_detail.
Protecting the agent’s provider key#
The provider key is the one credential present while the agent runs untrusted code. Three options, in increasing order of protection:
A static key (
SANDBOX_AGENT_API_KEY) — simplest, but a long-lived key in that sandbox is stealable. The worker logs a warning.A minted short-lived key (
SANDBOX_AGENT_API_KEY_COMMAND) — a command whose stdout becomes that run’s key. Recommended if you have a broker (Vault, a gateway’s virtual-key issuer). Pair withSANDBOX_AGENT_BASE_URLto point at your gateway.The built-in credential proxy (
SANDBOX_AGENT_CREDENTIAL_PROXY_ENABLED=true) — for Anthropic and OpenAI, whose direct APIs have no short-lived tokens. A separate sandbox runs a LiteLLM proxy holding the real key, and the agent sandbox gets only an ephemeral per-run key with a spend cap (SANDBOX_AGENT_CREDENTIAL_PROXY_MAX_BUDGET). The proxy dies with the run, so a leaked key is worthless afterwards. Mutually exclusive withSANDBOX_AGENT_BASE_URL; foropencode, setSANDBOX_AGENT_MODEL.
Warning
The proxy stands up LiteLLM inside a sandbox and depends on your agent CLI
talking to it correctly. Verify it against your CLI and LiteLLM versions
before enabling in production: make remediation_smoke SMOKE_PROXY=1 boots
the private proxy and confirms a second sandbox can reach it.
Keeping the proxy’s LiteLLM pinned#
Without a template, the proxy sandbox installs LiteLLM on every run from a hash-locked requirement file. This is pinned deliberately: LiteLLM leaves FastAPI, pydantic, aiohttp, openai and httpx on open ranges, and an incompatible release published upstream is enough to break every remediation run — in the sandbox holding your real provider key.
To move to a newer LiteLLM:
make lock_proxy_requirements REQUIREMENTS="litellm[proxy]==1.90.0"
make build_proxy_template # required if you use a template
make remediation_smoke SMOKE_PROXY=1 # prove it still boots
Keep the old pin if the smoke test fails. Three gotchas:
The FastAPI pin is a ceiling, not a floor. LiteLLM accepts a range but imports symbols newer FastAPI releases have removed. When bumping LiteLLM, check before raising the FastAPI pin.
A lock is only valid for the interpreter and architecture it was built for. The shipped lock targets python 3.13, which is what a templateless E2B sandbox runs — not the
e2bdev/baseimage’s 3.11. The install checks the sandbox against the lock’s header and fails with a re-lock instruction rather than a wall of resolution errors. For a self-hosted backend on a different runtime, compile your own:make lock_proxy_requirements PYTHON_VERSION=3.12 PLATFORM=aarch64-unknown-linux-gnu \ OUTPUT=locks/litellm-3.12-arm.txt # then point SANDBOX_AGENT_CREDENTIAL_PROXY_REQUIREMENTS_FILE at it
With no arguments,
make lock_proxy_requirementsre-locks in place, reading the target runtime from the lock’s own header, so it cannot overwrite a different lock.
Prebuilding the proxy template#
Installing that tree on every run costs time and makes each run depend on PyPI.
make build_proxy_template bakes the pins into an E2B template:
make build_proxy_template # or TEMPLATE_NAME=my-proxy
# then, on the temporal worker:
SANDBOX_AGENT_CREDENTIAL_PROXY_TEMPLATE=seizu-litellm-proxy
E2B templates are a cloud feature; with a self-hosted SANDBOX_DOMAIN there is
nothing to build and the run-time install covers it.
Warning
A configured template is used exactly as you built it — runs install nothing and check nothing. Nothing notices a template built from older requirements, or one missing LiteLLM altogether (that surfaces as a health-check failure at proxy start). Re-lock and rebuild together when you bump pins.
Configuration#
Variable |
Default |
Description |
|---|---|---|
|
|
Coding-agent CLI: |
|
|
E2B sandbox template. Empty → the provider’s official prebuilt template (E2B ships first-party |
|
|
Static API key for the CLI, exported only to the agent phase. Empty → falls back to |
|
|
Command run in the worker before each remediation; its stdout becomes that run’s agent API key. Use it to mint short-lived credentials from a broker (Vault, an LLM-gateway virtual-key issuer, …) instead of handing the sandbox a long-lived key. Takes precedence over the static key. Recommended for production: unlike the GitHub token (kept out of the agent sandbox), the agent’s provider key is present while it runs untrusted repo code with internet on, so a long-lived key is stealable — the worker logs a warning when a static key is used. |
|
|
LLM gateway/proxy base URL exported to the agent phase ( |
|
|
Run a short-lived LiteLLM proxy in its own sandbox holding the real provider key, and hand the agent sandbox only the proxy’s ephemeral per-run key with an in-memory spend cap (see below). All providers ( |
|
|
USD spend cap (LiteLLM’s in-memory global |
|
|
Hash-locked requirement file a templateless proxy sandbox installs. Empty → |
|
|
E2B template for the proxy sandbox, built from those requirements by |
|
|
Model for the CLI. For |
|
|
Hard cap for one remediation run (all sandbox phases). Also caps each CI-fix run. |
|
|
Total time the workflow watches one PR’s checks (including re-runs after a fix push). |
|
|
Interval between check-status polls. |
|
|
A check still queued (never started) after this long is ignored by the watch instead of waited on. |
|
|
Coding-agent CI-fix runs allowed per PR (each is a full sandbox agent session — this bounds spend). |
|
|
Expected SHA-256 of the pinned |
|
|
GitHub host the target repositories live on — |
|
|
GitHub token for the setup/push phases and the worker-side CI watch: a minimally-scoped fine-grained PAT in direct mode, a machine-account classic PAT in fork mode — see GitHub token. Required (configured = enabled). |
|
|
Push the work branch to a bot-owned fork (created on demand) and open cross-repo PRs instead of writing branches into the target repositories — see Fork mode caveats. |
|
|
Organization that owns the bot forks in fork mode; empty → forks live under the token user’s account. |
|
|
git author identity for the remediation commits. |
Temporal and general workflow settings are in
workflow configuration. The sandbox provider
settings (SANDBOX_API_KEY, SANDBOX_DOMAIN) are in Sandbox.
Note
The temporal worker does not hot-reload. It imports workflow code once at
startup, so after changing remediation code a run silently uses the old code
until you docker compose restart seizu-temporal-worker (dev) or rebuild the
image.