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 .github/workflows/

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:

  1. Create a machine account for this automation only (e.g. myorg-remediation-bot). Forks accumulate under it, or under REMEDIATION_FORK_ORG.

  2. As that account, create a classic PAT with public_repo (all targets public) or repo (any target private); add workflow if fixes may modify .github/workflows/.

  3. For private targets, give the machine account read access on each target and enable the target org’s Allow forking of private repositories.

  4. 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 gh and the agent CLI. No secrets, because npm postinstall scripts are third-party code.

setup

GitHub token

Clone and create the work branch, via gh auth setup-git. No token is written to disk or embedded in the clone URL.

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.yml with a pull_request trigger, 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:

  1. A static key (SANDBOX_AGENT_API_KEY) — simplest, but a long-lived key in that sandbox is stealable. The worker logs a warning.

  2. 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 with SANDBOX_AGENT_BASE_URL to point at your gateway.

  3. 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 with SANDBOX_AGENT_BASE_URL; for opencode, set SANDBOX_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/base image’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_requirements re-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

SANDBOX_AGENT_PROVIDER

claude

Coding-agent CLI: claude (Claude Code), codex, or opencode. opencode is multi-provider — set SANDBOX_AGENT_MODEL to a provider/model id (e.g. deepseek/deepseek-v4-pro) and it uses that provider’s key, reusing the same global *_API_KEY (e.g. DEEPSEEK_API_KEY) the chat assistant uses. For opencode, an explicit SANDBOX_AGENT_API_KEY must belong to the model’s provider — an Anthropic key with a deepseek/… model is exported as DEEPSEEK_API_KEY and fails auth.

SANDBOX_AGENT_TEMPLATE

"" (official)

E2B sandbox template. Empty → the provider’s official prebuilt template (E2B ships first-party claude/codex/opencode images with the CLI installed), which removes the per-run npm install and its postinstall scripts from the flow. A template name → that template (e.g. a self-pinned copy). none → the plain base image (the run installs the CLI itself). Ignored on self-hosted backends (SANDBOX_DOMAIN set): E2B templates are a cloud feature, and the idempotent install step covers those. The template provides tools only, never credentials, so the phase isolation above is unchanged.

SANDBOX_AGENT_API_KEY

""

Static API key for the CLI, exported only to the agent phase. Empty → falls back to ANTHROPIC_API_KEY for claude. Prefer the key command below.

SANDBOX_AGENT_API_KEY_COMMAND

""

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.

SANDBOX_AGENT_BASE_URL

""

LLM gateway/proxy base URL exported to the agent phase (ANTHROPIC_BASE_URL / OPENAI_BASE_URL); typically paired with the key command so the sandbox only ever holds a short-lived gateway key. Mutually exclusive with the credential proxy below.

SANDBOX_AGENT_CREDENTIAL_PROXY_ENABLED

false

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 (opencode needs SANDBOX_AGENT_MODEL set).

SANDBOX_AGENT_CREDENTIAL_PROXY_MAX_BUDGET

5

USD spend cap (LiteLLM’s in-memory global max_budget) — bounds real-time abuse of the ephemeral key while the proxy is up.

SANDBOX_AGENT_CREDENTIAL_PROXY_REQUIREMENTS_FILE

"" (checked-in lock)

Hash-locked requirement file a templateless proxy sandbox installs. Empty → reporting/services/sandbox_proxy_requirements.txt. Point it at your own compiled lock to run a different LiteLLM, or one resolved for a different sandbox runtime; see Keeping the proxy’s LiteLLM pinned.

SANDBOX_AGENT_CREDENTIAL_PROXY_TEMPLATE

""

E2B template for the proxy sandbox, built from those requirements by make build_proxy_template. Set → runs use that image as built and install nothing. Empty → the plain base image, and every run installs LiteLLM first.

SANDBOX_AGENT_MODEL

""

Model for the CLI. For claude/codex a bare model override (empty → the CLI’s default). For opencode it is required and takes the provider/model form (e.g. deepseek/deepseek-v4-pro), which also selects the provider key and — in credential-proxy mode — the LiteLLM namespace.

REMEDIATION_TIMEOUT_SECONDS

1800

Hard cap for one remediation run (all sandbox phases). Also caps each CI-fix run.

REMEDIATION_CI_MAX_WAIT_SECONDS

3600

Total time the workflow watches one PR’s checks (including re-runs after a fix push). 0 disables the CI watch.

REMEDIATION_CI_POLL_SECONDS

120

Interval between check-status polls.

REMEDIATION_CI_QUEUED_STUCK_SECONDS

1800

A check still queued (never started) after this long is ignored by the watch instead of waited on.

REMEDIATION_CI_FIX_MAX_ATTEMPTS

1

Coding-agent CI-fix runs allowed per PR (each is a full sandbox agent session — this bounds spend). 0 → watch and record the outcome but never fix.

REMEDIATION_GH_SHA256

""

Expected SHA-256 of the pinned gh linux_amd64 tarball. Set it (out of band) for an independent supply-chain pin, or bake gh into a pinned sandbox image — since the installed gh later handles the token. Empty → verify against the release’s own checksums (integrity only).

REMEDIATION_GITHUB_HOST

github.com

GitHub host the target repositories live on — github.com or a GitHub Enterprise Server hostname. Used for the clone URL and gh (GH_HOST/GH_ENTERPRISE_TOKEN).

REMEDIATION_GITHUB_TOKEN

""

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).

REMEDIATION_USE_FORK

false

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.

REMEDIATION_FORK_ORG

""

Organization that owns the bot forks in fork mode; empty → forks live under the token user’s account.

REMEDIATION_GIT_USER / REMEDIATION_GIT_EMAIL

seizu-remediation-bot / seizu-remediation@localhost

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.