ci: add scheduled cargo-deny AI auto-fix with shared root deny.toml - #4899
Open
deniallugo-ml wants to merge 26 commits into
Open
ci: add scheduled cargo-deny AI auto-fix with shared root deny.toml#4899deniallugo-ml wants to merge 26 commits into
deniallugo-ml wants to merge 26 commits into
Conversation
Adds a proactive, scheduled cargo-deny system covering all four Cargo workspaces (core, prover, zkstack_cli, airbender_prover_server) instead of only core on pull_request: - Move core/deny.toml to the repo root as a single shared config for all workspaces (plus CDDL-1.0, required by `inferno` via zksync-airbender's riscv_transpiler in airbender_prover_server). cargo-deny does not auto-discover a root config from nested workspaces, so every invocation passes `--config deny.toml` explicitly (before the `check` subcommand, i.e. via cargo-deny-action's `arguments` input). - cargo-deny-ai-fix-reusable.yml: reusable detect -> ai-fix workflow that runs cargo-deny on a schedule and, when red with no open fix PR for that workspace, hands the failure to Claude to open a fix PR (never merges; policy decisions are left for humans and labeled needs-*-review). - cargo-deny-ai-fix.yml: caller matrix over the four workspaces, with arguments matching cargo-license.yaml so both checks agree on what is red. - cargo-license.yaml: point the existing PR-time gate at the shared root config, keeping the action's --all-features default explicitly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
deniallugo-ml
requested review from
alexandrst88,
artmakh,
asmeirs,
hatemosphere,
iluwaa,
onyxet and
shxdxwraze
July 16, 2026 12:19
PRs opened with the default GITHUB_TOKEN don't trigger on:pull_request workflows, so the ai-fix job now pushes branches and opens PRs/issues with the same bot PAT release-please uses. The GH_PAT secret is optional in the reusable workflow and falls back to GITHUB_TOKEN, so repos without a PAT still work (their fix PRs just need a manual CI kick). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Drop the GITHUB_TOKEN fallback - callers must always pass a bot PAT so fix PRs reliably trigger CI. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
cargo-deny-action v2.0.11 bundles cargo-deny 0.18.6, where --config is a `check` argument (it only became a top-level argument in later releases) and a relative path resolves from the workspace root directory, not the cwd. Pass `--config ../deny.toml` via command-arguments instead of the action's `arguments` input, and pin the interactively-installed CLI in the ai-fix job to the same 0.18.6 so Claude's re-runs accept the same syntax and produce identical results. Validated locally with the actual 0.18.6 binary against all four workspaces: core green; prover/zkstack_cli/airbender red only on real fixable advisories (8/14/3). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fixes the CodeQL actions/missing-workflow-permissions alert. The caller grants the maximum the reusable workflow's ai-fix job needs; the reusable workflow downscopes its detect job to read-only itself. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Isolate the publishing credential from untrusted code: Claude now works offline (no gh in allowedTools, checkout with persist-credentials: false, no token in its environment - cargo build scripts and proc macros execute arbitrary code and inherit the job env). It commits locally and writes report files; a deterministic publish step pushes and opens the PR/issue. - Prefer a short-lived repository-scoped GitHub App installation token (GH_APP_ID + GH_APP_PRIVATE_KEY, minted in-workflow) over the bot PAT; GH_PAT remains as fallback, validated up front. - Run all cargo commands from the workspace directory and pre-install its pinned toolchain: rustup resolves rust-toolchain files from the cwd, not --manifest-path (zkstack_cli and airbender_prover_server pin different nightlies than the repo root). - Break the paid retry loop for human-only findings: when nothing is safely fixable there are no commits, so the publish step opens or updates a cargo-deny-diagnosis issue instead of a PR, and the detect gate now dedups against open diagnosis issues as well as fix PRs. - Pin all third-party actions to full commit SHAs; document that the future central copy (matter-labs/zksync-ci-common) must be referenced by full SHA, never @main. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Second security-review round: - Same-job credential separation is not isolation: code that ran on the runner can plant git hooks or persist processes that observe a later step's token. The ai-fix job is now fully read-only (contents: read, no publishing credential anywhere on the runner) and hands its results (format-patch series + report) to a publish job on a fresh runner via an artifact. The publish job validates that patches stay inside the workspace (or its deny.toml), applies them with git hooks disabled, and is the only place the App token is minted / the PAT is read. - Diagnosis-issue dedup is now fingerprint-based: detect runs the pinned cargo-deny 0.18.6 CLI with --format json and hashes the sorted set of (code, advisory id, message) error tuples; an open issue only suppresses re-runs while its embedded fingerprint matches, so a months-open human decision cannot mask newly introduced fixable findings. Green checks close stale diagnosis issues. - Caller GITHUB_TOKEN grant reduced to contents/PR read + issues write. - Matrix serialized with max-parallel: 1 - all four workspaces share the root deny.toml and concurrent AI edits to it could conflict. (A shared concurrency group would silently drop queued runs instead.) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Third security-review round: - Fingerprint hashes the complete normalized error diagnostics (jq -cS on .fields) with multiplicity preserved instead of deduplicated (code, advisory, message) tuples: license rejections all share one message in cargo-deny 0.18.x, so a newly added disallowed dependency could previously hide behind an open diagnosis issue. - Shield ANTHROPIC_API_KEY (and other credential env vars) from cargo subprocesses via a PATH-shadowing wrapper that execs the real cargo through `env -u ...` - build scripts and proc macros no longer inherit the key. Documented residual /proc/<pid>/environ exposure with the recommendation to use a spend-capped workspace key and workload identity federation when available. - Patch path boundary uses git diff --no-renames: rename detection reported only the destination path, letting a rename smuggle a deletion outside the workspace boundary. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fourth security-review round: - Remove rustup from allowedTools (`rustup run <tc> cargo` would skip the credential-scrubbing PATH wrapper; the workspace toolchain is pre-installed so Claude never needs rustup) and scope git to explicit subcommands (`git -c alias.x='!cmd' x` would exec arbitrary commands with the original environment). - Neutralize core.hooksPath in the ai-fix job: a build script could plant .git/hooks/* during cargo build and have it fire on Claude's own `git commit` with the API key present. - Diagnosis-issue updates now remove both needs-*-review labels before re-adding what the new report warrants, so an issue whose finding set changed category no longer sits in the wrong review queue. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…rface Fifth security-review round: - Claude can no longer invoke git at all: a build script can plant executable git config (core.fsmonitor, diff.external, gpg.program, hooks) that would run inside Claude's environment - where the Anthropic credential lives - on its next git command. Claude now only edits files; a workflow step AFTER the Claude process exits (key gone from the runner) captures the tree as a raw binary diff with --no-ext-diff --no-textconv --no-renames and -c overrides. The publish job applies the diff on a fresh runner, validates the path boundary from NUL-separated git status, and creates a single bot commit. - Fix PRs are now fingerprint-gated like diagnosis issues: an open PR suppresses re-runs only while its embedded fingerprint matches; a new finding set force-updates the existing PR branch, replaces its body and review labels, and leaves a comment - so a PR awaiting review cannot mask a newly introduced advisory. - Caller gets workflow-level concurrency with queue: max, serializing overlapping schedule/push/manual runs (max-parallel only covers one run). Note: actionlint's schema doesn't know `queue` yet; GitHub docs confirm it (queues up to 100 runs vs the replace-pending default). - Interface genericized for zksync-ci-common: ANTHROPIC_API_KEY or CLAUDE_CODE_OAUTH_TOKEN accepted (validated up front), and a policy-path input replaces the hardcoded root deny.toml in the publish path boundary. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sixth review round (merge-ready checklist): - Explicitly deny Bash(git:*) to Claude: read-only git commands like `git status`/`git diff` are part of Claude Code's default read-only toolset even when git is absent from allowedTools, and they invoke attacker-plantable core.fsmonitor / diff.external inside Claude's credentialed environment. Deny rules take precedence. - Route gate expressions through step env vars to clear the CodeQL command-injection alert (false positive - fingerprints are 64 hex chars - but red CI is red CI). - Capture the diff against HEAD so staged changes can't vanish from the artifact if anything touches the index. - Parse publish-side git status with --no-renames explicitly. - Enforce exactly one of ANTHROPIC_API_KEY / CLAUDE_CODE_OAUTH_TOKEN; header now scopes WIF to future hardening instead of implying support. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Post-approval operational tweak: queue: max could accumulate up to 100 queued runs of four AI jobs each on a busy main, and stale queued runs would regenerate the bot PR against old commits. Since the concurrency group covers the entire caller run, the default single-pending replacement loses no matrix entries - the newest pending run supersedes everything an older one would have done, and the detect gate re-arms on the next tick regardless. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment-only pass plus three leftovers with no behavior impact: - Drop the unused concurrency-key input (callers serialize shared-policy workspaces with workflow-level concurrency + max-parallel instead); the reusable group is keyed by manifest path. - Drop the unused pr_number step output. - Rename the fingerprint-extraction step to say what it does. Comments now state the current design instead of narrating review iterations, and the header is restructured into placement / pipeline / credential isolation / design notes / setup sections. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Trigger the caller on pushes to this branch (TEMPORARY, remove before merge) so the pipeline can be exercised without merging. - Fix PRs target the branch the run executed on (GITHUB_REF_NAME) - the default branch for scheduled runs, unchanged behavior there. - Remove the RUSTSEC-2026-0097 (rand 0.8, no upstream fix) ignore from deny.toml to turn core red with a deliberately unfixable finding: the expected outcome is a diagnosis issue with needs-security-review, NOT a deny.toml edit, verifying the policy guardrail. Restored after the test. The other three workspaces are genuinely red and should produce fix PRs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The ai-fix job fails with "Unsupported event type: push" on push-driven runs (claude-code-action supports entity events, workflow_dispatch, repository_dispatch, schedule and workflow_run - not push). Rely on the daily schedule plus workflow_dispatch, which can target any ref the workflow file exists on. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
First live run burned 11 of 26 turns on permission denials and hit the max-turns ceiling in 104s: the allowlist lacked the read-only utilities Claude habitually uses (ls/cat/grep/...), and each denied command silently costs a turn. Allow read-only utilities that expose nothing beyond the Read tool, raise the default max-turns to 50, and enable show_full_output so the session transcript is reviewable in the run log. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Second live run: Claude fixed all core findings (including rand via the 0.8.6 backport) and cargo deny went green, but then started `cargo build --workspace --all-features`, backgrounded it, and ended its turn "waiting" - the session terminates on end of turn, so the report files were never written and packaging failed. Full workspace builds don't fit inside an agent session on a repo this size and duplicate what the fix PR's own CI does. The prompt now forbids cargo build/test and background/long-running commands, and requires the report files to be written before the session ends. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Per-workspace PRs don't make sense to review and merge separately, especially with a shared root deny.toml. The reusable workflow now takes a whitespace-separated `manifest-paths` list: detect checks every workspace and fingerprints the combined failing set, one Claude session fixes all red workspaces, and publish opens (or force-refreshes) a single PR - or a single diagnosis issue when nothing is safely fixable. This drops the matrix, the per-workspace workspace:<slug> labels, and the max-parallel serialization (a single job cannot race itself); caller-level concurrency still serializes overlapping runs. Default max-turns raised to 80 for the larger combined session. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ines
${{ inputs.manifest-paths }} is textual substitution, so the multiline
list injected literal newlines into the for-statement (bash syntax
error). Iterate over an env var instead.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Seventh review round: - No transcript in logs and no shell read utilities for Claude: the credential-bearing process could print /proc-derived secrets, and partial output bypasses GitHub's exact-value masking. File exploration goes through Read/Glob/Grep tools only. - The cargo wrapper now enforces the subcommand policy in tooling (deny/update/tree/metadata only) instead of prompt-only - none of these compile build scripts or proc macros, so no untrusted code runs in the ai-fix job at all. - Path policy tightened to Cargo.toml/Cargo.lock inside failing workspaces + policy-path + caller-declared extra-allowed-paths; a repo-root workspace no longer grants the whole repository (workflow, source and script changes are rejected even for dir == "."). - PR/issue dedup and refresh are scoped per base branch: PR queries use --base, diagnosis issues carry a cargo-deny-base marker, branch names embed the ref - a fix PR for one branch can no longer suppress or be hijacked by runs on another. - New credential-free validate job applies the diff and runs caller-provided bounded validation commands (era: cargo check of zkstack_cli, whose compile break in the previous generated fix motivated this); on failure the PR is published as a DRAFT with an explicit unverified warning. The prompt also steers toward minimal --precise bumps to reduce breakage risk. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The generated fixes kept resolving cargo-deny while breaking compilation (alloy-dyn-abi 1.4.1 pulls winnow 1.0.4, 139 E0277s caught by the validate job and again by PR CI). Builds stay out of the credentialed Claude job - instead, real compiler errors from the credential-free validate job feed a second bounded Claude session: - validate captures per-command error tails as an artifact on failure; - repair applies the previous diff, reads the error log, and adjusts versions using cargo tree/update reasoning (same restricted wrapper, no compilation, no credentials beyond Anthropic); - revalidate re-runs the caller's validation on the repaired set; - publish picks the repaired artifact when available and only marks the PR draft if the final validation still failed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Final review round: - File tools scoped to the checkout (Read/Edit/Write/Glob/Grep with ./** specifiers): the model must not be able to read outside the repository (e.g. /proc) and surface secrets into published output. - Repair input sanitized against prompt injection: only filtered compiler diagnostics (structured cargo JSON messages, falling back to rustc error/location lines) reach the repair session - never raw build-script stdout. Both prompts instruct treating all read content strictly as data. - State markers hardened: publish strips any marker lines the untrusted AI report may contain before appending the trusted ones, and detect reads the LAST marker. - Validation state made consistent: anything except a clean pass (including skipped - no validation configured) publishes as a draft, and regenerated PRs have their draft/ready state synchronized with the current validation outcome. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
# cargo-deny fix report Ran `cargo deny --all-features check --config ../deny.toml --allow unmaintained --hide-inclusion-graph` from each workspace directory (the exact CI invocation) and fixed what was safely fixable via `cargo update`. Only `Cargo.lock` files were touched — no `Cargo.toml` or `deny.toml` changes were needed anywhere. All four workspaces failed on their first run (findings below), so this is not a transient/already-green case. ## core **Before:** `advisories FAILED` — 1 finding. - `RUSTSEC-2026-0097` (unsound): `rand 0.8.5` — `ThreadRng` aliasing UB under a custom logger. Fixed: `cargo update -p rand@0.8.5 --precise 0.8.6` (patch bump, smallest fix per the advisory's own solution range `>=0.8.6`). **After:** `cargo deny check` passes (advisories/bans/licenses/sources all `ok`). Remaining output is only pre-existing warnings (yanked `core2`/`spin`, a couple of `license-not-encountered` notices for allow-list entries not hit in this workspace) — not check failures. ## prover **Before:** `advisories FAILED` — 8 findings. - `RUSTSEC-2026-0190` (unsound): `anyhow 1.0.98` — UB in `Error::downcast_mut()`. Fixed: `cargo update -p anyhow --precise 1.0.103` (solution range `>=1.0.103`). - `RUSTSEC-2026-0045`, `-0046`, `-0047`, `-0048` (vulnerabilities): `aws-lc-sys 0.28.2` — AES-CCM timing side-channel, two PKCS7 validation bypasses, and a CRL scope-check logic error in AWS-LC. Fixed: a plain `cargo update -p aws-lc-rs` (no `--precise`) picked up the latest compatible `aws-lc-rs` (`1.13.0 -> 1.17.1`), which pulled `aws-lc-sys` to `0.42.0` — well past the `>=0.39.0` the strictest of these advisories required. - `RUSTSEC-2026-0204` (vulnerability): `crossbeam-epoch 0.9.18` — invalid pointer deref in `fmt::Pointer`. Fixed: `cargo update -p crossbeam-epoch --precise 0.9.20`. - `RUSTSEC-2026-0012` (unsound): `keccak 0.1.5` — yanked, ARMv8 asm operand-type UB. Fixed: `cargo update -p keccak --precise 0.1.6`. - `RUSTSEC-2026-0097` (unsound): `rand 0.8.5` and `rand 0.9.1` both present. Fixed: `cargo update -p rand@0.8.5 --precise 0.8.6` and `cargo update -p rand@0.9.1 --precise 0.9.3`. - `RUSTSEC-2025-0055` (vulnerability): `tracing-subscriber 0.3.19` — ANSI escape injection into logs. Fixed: `cargo update -p tracing-subscriber --precise 0.3.20`. **After:** `cargo deny check` passes (advisories/bans/licenses/sources all `ok`). ## zkstack_cli **Before:** `advisories FAILED` — 14 findings. - `RUSTSEC-2025-0073` (vulnerability): `alloy-dyn-abi 1.3.1` — panic/DoS in `TypedData` hashing. Fixed: `cargo update -p alloy-dyn-abi --precise 1.4.1` (solution range `>=1.4.1` for the 1.x line). This pulled a real transitive shift (bumped `alloy-sol-types`/`alloy-primitives` family to 1.6.1 and swapped in `sha3`/`secp256k1`/`winnow`/etc. in place of older crates) — flagging this as a larger-than-usual transitive move for reviewers to double check in CI, even though it's a minor-version bump within semver. - `RUSTSEC-2026-0190` (unsound): `anyhow 1.0.93` — same as prover. Fixed: `cargo update -p anyhow --precise 1.0.103`. - `RUSTSEC-2026-0045`..`-0048` (vulnerabilities): `aws-lc-sys 0.24.0`. Fixed: `cargo update -p aws-lc-rs` (`1.12.0 -> 1.17.1`), bringing `aws-lc-sys` to `0.42.0`. - `RUSTSEC-2026-0007` (vulnerability): `bytes 1.8.0` — integer overflow in `BytesMut::reserve`. Fixed: `cargo update -p bytes --precise 1.11.1`. - `RUSTSEC-2026-0204` (vulnerability): `crossbeam-epoch 0.9.18`. Fixed: `cargo update -p crossbeam-epoch --precise 0.9.20`. - `RUSTSEC-2026-0012` (unsound): `keccak 0.1.5`. Fixed: `cargo update -p keccak --precise 0.1.6`. - `RUSTSEC-2026-0097` (unsound): `rand 0.8.5` and `rand 0.9.1`. Fixed: precise updates to `0.8.6` and `0.9.3` respectively. - `RUSTSEC-2025-0137` (vulnerability): `ruint 1.15.0` — unsound safe `reciprocal_mg10` relying on `debug_assert!`. Fixed: `cargo update -p ruint --precise 1.17.1`. Note: this added an *unreachable* `ark-ff 0.5.0` (and friends) resolution candidate to `Cargo.lock` — confirmed with `cargo tree -i ark-ff@0.5.0` that nothing in the graph actually activates it, so it doesn't affect the build. - `RUSTSEC-2025-0023` (unsound): `tokio 1.42.0` — broadcast channel `Sync` unsoundness. Fixed: `cargo update -p tokio --precise 1.42.1` (solution range `>=1.42.1, <1.43.0`). - `RUSTSEC-2025-0055` (vulnerability): `tracing-subscriber 0.3.18`. Fixed: `cargo update -p tracing-subscriber --precise 0.3.20`. - `RUSTSEC-2025-0009` (vulnerability): `ring 0.17.8` (a second, separate `ring` instance in the graph, via `rustls 0.21.12`). Fixed: `cargo update -p ring@0.17.8 --precise 0.17.14`. **After:** `cargo deny check` still has **1 unresolved finding** — see "Needs human review" below. ## airbender_prover_server **Before:** `advisories FAILED` — 3 findings. - `RUSTSEC-2026-0190` (unsound): `anyhow 1.0.102`. Fixed: `cargo update -p anyhow --precise 1.0.103`. - `RUSTSEC-2026-0204` (vulnerability): `crossbeam-epoch 0.9.18`. Fixed: `cargo update -p crossbeam-epoch --precise 0.9.20`. - `RUSTSEC-2026-0186` (unsound): `memmap2 0.9.10` — unchecked pointer offset in `advise_range`/`flush_range`. Fixed: `cargo update -p memmap2 --precise 0.9.11`. **After:** `cargo deny check` passes (advisories/bans/licenses/sources all `ok`). ## Needs human review - **`zkstack_cli`: `RUSTSEC-2025-0009`** — AES functions in `ring 0.16.20` may panic when overflow checking is enabled (`ring::aead::quic::HeaderProtectionKey::new_mask()`, or `AES_128_GCM`/`AES_256_GCM` after ~64GB in one chunk). The advisory's solution is "Upgrade to >=0.17.12", but this specific `ring 0.16.20` is pulled in by `jsonwebtoken 8.3.0` (`ring = "^0.16"`), which is pulled in by `ethers-providers 2.0.14` (part of the `ethers 2.0.14` crate family used throughout `zkstack_cli`). `jsonwebtoken` 8.x has no release compatible with `ring >=0.17` — that requires `jsonwebtoken` 9.x, which `ethers-providers` doesn't accept (`cargo update -p jsonwebtoken` / `-p ethers-providers` / `-p ethers` all reported "Locking 0 packages to latest compatible versions", i.e. no compatible bump exists in the current dependency graph). Actually fixing this needs either a semver-major `ethers` upgrade (risky, unverifiable here, and `ethers-rs` is a largely unmaintained project people are migrating off of in favor of `alloy`) or vendoring/patching around `ethers-providers`'s JWT usage — both are policy/architecture decisions, not something to force through an autonomous `cargo update`. Left unresolved; `cargo deny check` in `zkstack_cli` will still report this one finding. <!-- cargo-deny-base: use-common-cargo-deny --> <!-- cargo-deny-fingerprint: 08566c1acc885d8d1605ecf8a8faf71c4eeb457b86a9ef3182792cb433a96b75 --> Co-authored-by: cargo-deny-bot <cargo-deny-bot@users.noreply.github.com>
The merged AI fix (#4910) shipped a non-compiling zkstack_cli lockfile: alloy-sol-type-parser 1.6.1 is source-incompatible with its own winnow 1.0.4 dependency (139 E0277s), breaking every CI job at zkstack build. It was published as validated because the validate step's `| tee` runs under the default `bash -e` shell WITHOUT pipefail - tee's exit code masked the failing cargo check. - zkstack_cli/Cargo.lock: pin the alloy sol-family coherently to 1.4.1 (alloy-dyn-abi stays 1.4.1, satisfying RUSTSEC-2025-0073; winnow 1.0.x drops out of the graph entirely). Verified locally: zero dependency compile errors and cargo-deny reports only the known ring finding. All other #4910 fixes (core/prover/airbender lockfiles) are unaffected. - Reusable workflow: set -o pipefail in the validate step so a failing validation can never again report success. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Adds a proactive, scheduled cargo-deny system covering all four Cargo workspaces (
core,prover,zkstack_cli,airbender_prover_server) instead of onlycoreonpull_request, and consolidates cargo-deny policy into a single shareddeny.tomlat the repo root.deny.toml(moved fromcore/deny.toml): shared config for all workspaces. One policy addition vs. core's config: CDDL-1.0 is now allowed — required byinferno(flamegraph rendering), a mandatory transitive dependency of zksync-airbender'sriscv_transpilerinairbender_prover_server. It's weak file-level copyleft, similar in spirit to the already-allowed MPL-2.0. This is the one policy call in this PR — please review it explicitly..github/workflows/cargo-deny-ai-fix-reusable.yml: reusabledetect → ai-fix → publishworkflow.detectruns cargo-deny; if red and nothing is already open for that workspace (open fix PR or open diagnosis issue, deduped viacargo-deny-fix/cargo-deny-diagnosis+workspace:<slug>labels),ai-fixhands the failure to Claude. It never merges, and policy decisions (disallowed license with no alternative, advisory with no upstream fix, unapproved source) are deliberately left unresolved and labeledneeds-security-review/needs-license-reviewfor a human. Supportsdry-run(diagnosis issue instead of PR). Long-term this file moves tomatter-labs/zksync-ci-common, referenced by full commit SHA (never@main)..github/workflows/cargo-deny-ai-fix.yml: caller — daily cron + push-to-main + manual dispatch, matrix over the four workspaces, arguments matchingcargo-license.yamlexactly so both checks agree on what is red..github/workflows/cargo-license.yaml: minimal change pointing the existing PR-time gate at the shared root config (--config ../deny.toml— in cargo-deny 0.18.x, bundled by the action,--configis acheckargument whose relative path resolves from the workspace root directory).Security design (after two review rounds)
cargo build/cargo test) runs only in the read-onlyai-fixjob —contents: readGITHUB_TOKEN, no publishing credential anywhere on that runner,persist-credentials: false, noghinallowedTools. Claude only edits files (no git at all — plantedcore.fsmonitor/diff.external/hooks would otherwise execute in its credentialed environment); a step running after the Claude process exits captures the tree as a raw binary diff (--no-ext-diff --no-textconv --no-renames) and exports diff + report as an artifact. A separatepublishjob on a fresh runner (where nothing untrusted ever executed) validates the diff touches only the workspace (or the declaredpolicy-path), applies it, creates a single bot commit, pushes, and opens the PR/issue. Only that job mints the App token / reads the PAT.GH_APP_ID+GH_APP_PRIVATE_KEY, short-lived), release-please bot PAT (RELEASE_TOKEN) as validated fallback. Bot-authored PRs trigger CI.--format jsonand hashes the complete normalized error diagnostics (multiplicity preserved — license rejections share one message in 0.18.x, so a deduplicated summary could hide a newly added disallowed dependency). Both fix PRs andcargo-deny-diagnosisissues embed that fingerprint; the gate skips re-runs only while the fingerprint still matches, so neither a PR awaiting review nor a long-open human decision can mask newly introduced findings — a changed set force-updates the existing PR branch (with a comment) or refreshes the issue. Green checks auto-close stale diagnosis issues.rust-toolchainfiles from the cwd, not--manifest-path.matter-labs/zksync-ci-commonmust be referenced by full SHA, never@main.ANTHROPIC_API_KEY(and other credential vars) from every cargo invocation, so build scripts / proc macros don't inherit it. Wrapper bypasses are closed: norustup(skips the wrapper) and git explicitly denied via--disallowedTools "Bash(git:*)"(read-only git is otherwise part of Claude Code's default toolset; deny rules take precedence). Residual same-UID/proc/<pid>/environexposure is documented — use a spend-capped workspace key; move to workload identity federation when available.--no-renamesand the publish job validates NUL-separatedgit statuspaths against the workspace +policy-path, so a rename can't smuggle a deletion outside the boundary.max-parallel: 1serializes workspaces within a run, and a caller-level repo-wideconcurrencygroup serializes overlapping schedule/push/manual runs. Default single-pending replacement is intentional: the group spans the whole run, so nothing is lost when a newer pending run supersedes an older one, and busy-main pushes can't queue up dozens of expensive AI runs.contents: read,pull-requests: read,issues: write.Validation
Ran all four workspaces locally with the exact cargo-deny 0.18.6 binary the action bundles and CI's exact flags:
The remaining advisory errors are real,
cargo update-fixable findings — fixing them is exactly the new workflow's job: on its first scheduled run it should open one fix PR per red workspace. Workflows pass actionlint and prettier.Setup needed before the first run
ANTHROPIC_API_KEY(ideally a dedicated spend-capped workspace key) orCLAUDE_CODE_OAUTH_TOKEN— validated up front. WIF is documented as future hardening.GH_APP_ID/GH_APP_PRIVATE_KEY; until then the caller passesRELEASE_TOKENasGH_PAT.workflow_dispatchand sanity-check thedetectgate output.🤖 Generated with Claude Code