Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .cargo/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Supply-chain soak for cargo's own dependency resolution — the same
# soak-window rule the npm side applies via minimumReleaseAge /
# min-release-age: crate versions younger than the window are skipped by
# the resolver unless already in Cargo.lock. min-publish-age is an
# [unstable] cargo feature, so these keys bite only under a nightly
# toolchain; on perry's stable toolchain they are inert and the automated
# window rides dependabot's cooldown (.github/dependabot.yml) instead.
# Managed by scripts/soak/soak.mts (`npm run soak` / `npm run soak:fix`).
[unstable]
min-publish-age = true

[registry]
global-min-publish-age = "7 days"
90 changes: 90 additions & 0 deletions .claude/skills/soak/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
---
name: soak
description: Manages the repo's supply-chain soak window (SOAK_DAYS) — checks and fixes the derived surfaces, bumps or disables the window, adds dated per-package exclusions, and bumps pinned external tools. Use when a task touches minimumReleaseAge, min-release-age, min-publish-age, dependabot cooldown, external-tools.json, sfw shims, or taze cooldowns, or when investigating why a freshly published version won't install.
---

# The soak window

One rule: a release must be at least `SOAK_DAYS` old before this repo
adopts it. The delay gives the ecosystem time to catch a malicious or
yanked release before we ever install it. The window is defined exactly
once — read the current value from `scripts/soak/constants.mts` and never
hardcode it elsewhere. Every surface derives from or is parity-checked
against it:

| Surface | Key | Units |
|---|---|---|
| `.cargo/config.toml` | `global-min-publish-age` (nightly-only feature; inert on perry's stable toolchain) | `"N days"` |
| `tools/pnpm-workspace.yaml` | `minimumReleaseAge` | minutes |
| `.npmrc` | `min-release-age` | days |
| `tools/taze.config.mts` | `maturityPeriod` | imports `SOAK_DAYS` |
| `external-tools.json` | `soakBypass` annotations | days |
| `.github/dependabot.yml` | `cooldown.default-days` per update block | days |

## Commands (package.json scripts — the code lives in `scripts/soak/`)

- `npm run soak` — parity-check every surface (CI-gated: `soak-gate` job
in security-audit.yml, always-run)
- `npm run soak:fix` — rewrite drifted windows, prune expired exclusions
- `npm run deps:update` — bump npm (taze) + cargo deps through the window
- `npm run tools:check` / `tools:fix` / `tools:install` — validate /
prune-expired-bypasses / install the SRI-pinned external tools
(`external-tools.json`); `tools:install` also writes the sfw firewall
shims into the dev-tools bin dir
- `npm run test:scripts` — the scripts' own unit tests

The gates fail closed when a bypass window clears, but nobody has to
watch for that: the scheduled `soak-autofix` workflow runs `soak:fix` +
`tools:fix` daily and commits the pruning as a bot PR.

A soak change is done when `npm run soak` and `npm run test:scripts`
both exit 0 — the same gates CI runs. Re-run them after every fix.

## Change the window (one place)

1. Edit `SOAK_DAYS` in `scripts/soak/constants.mts`.
2. `npm run soak:fix` (rewrites cargo/npmrc/yaml and drifted dependabot
values; taze follows by import). A dependabot block with NO cooldown
at all is a check finding fixed by hand — add the two lines where the
finding says.
3. `npm run soak` + `npm run test:scripts` — existing exclusion
annotations encode the old window and will be flagged; re-date or
remove them, then re-run until both pass.

**Opt out entirely**: set `SOAK_DAYS = 0` and run the same two steps —
cargo, pnpm, npm, and taze all treat zero as disabled. There is
deliberately no env-var bypass: opting out is a committed, reviewable
change, never a silent one.

## Skip the soak for ONE package (dated, temporary)

Add to `minimumReleaseAgeExclude` in `tools/pnpm-workspace.yaml` with the
annotation on the line above (block list only — flow `[..]` is rejected
because a comment line can't attach to an inline entry):

```yaml
# published: YYYY-MM-DD | removable: YYYY-MM-DD
- 'name@1.2.3'
```

`removable` = `published + SOAK_DAYS`; `published` must be the real
registry publish date (the placeholders above are schematic — copying
them verbatim is rejected). Once `removable` passes, `npm run soak`
warns until the pin is pruned (`soak:fix` or the soak-autofix workflow
does it). Bare names / `@scope/*` globs are standing trust and
need no annotation. External tools use the same shape via a `soakBypass`
object in `external-tools.json`.

## Maintaining this skill

`scripts/soak/` is the law; this file only documents it — when they
disagree, fix this file. When editing, follow Anthropic's guidance:

- [Prompting best practices](https://platform.claude.com/docs/en/build-with-claude/prompt-engineering/claude-prompting-best-practices)
- [Prompting Claude Fable 5](https://platform.claude.com/docs/en/build-with-claude/prompt-engineering/prompting-claude-fable-5)
- [Skill authoring best practices](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/best-practices)
- [Write an effective CLAUDE.md](https://code.claude.com/docs/en/best-practices#write-an-effective-claude-md)

Keep it concise (goal + constraints, not step enumeration), keep the
description in third person with explicit "use when" triggers, and keep
the window value in `constants.mts` rather than restating it here.
7 changes: 7 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ updates:
schedule:
interval: weekly
open-pull-requests-limit: 5
# Release-age soak: a bumped version must be at least this many days
# old before dependabot proposes it (SOAK_DAYS in
# scripts/soak/constants.mts; `npm run soak` gates the parity).
cooldown:
default-days: 7
groups:
cargo-minor-and-patch:
update-types: [minor, patch]
Expand Down Expand Up @@ -33,6 +38,8 @@ updates:
schedule:
interval: weekly
open-pull-requests-limit: 3
cooldown:
default-days: 7
commit-message:
prefix: "ci"
include: scope
76 changes: 74 additions & 2 deletions .github/workflows/security-audit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:
security-audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
with:
Expand Down Expand Up @@ -64,6 +64,78 @@ jobs:
--ignore RUSTSEC-2026-0119 \
--ignore RUSTSEC-2026-0187

# Soak parity gate + external-tool pin gate. Always-run (deliberately not
# path-filtered — nub hid this gate in a path-gated job and it silently
# skipped Rust-only PRs). The scripts are dep-free erasable-TS .mts run
# with the pinned Node's native type stripping; no npm install needed.
soak-gate:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version-file: .node-version
- name: Soak window parity (npm run soak)
run: node scripts/soak/soak.mts --check
- name: External-tool pins valid (npm run tools:check)
run: node scripts/soak/external-tools.mts --check
- name: Soak script unit tests (npm run test:scripts)
run: node --test scripts/soak/*.test.mts

# AgentShield — audits the operator-side Claude config (.claude/: hooks,
# permissions, MCP servers, agents) for prompt injection, leaked secrets,
# over-permissive tool grants. Installed from the SRI-pinned registry
# tarball in external-tools.json (never `npm install -g`).
agent-scan:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version-file: .node-version
- name: Install pinned agentshield
env:
# download() attaches this to github.com fetches only (release
# assets); without it private assets 404 and public ones ride
# unauthenticated rate limits.
GITHUB_TOKEN: ${{ github.token }}
run: node scripts/soak/external-tools.mts --install agentshield
- name: Scan .claude/ config
run: |
BIN_DIR="$(node scripts/soak/external-tools.mts --print-bin)"
export PATH="$BIN_DIR:$PATH"
agentshield scan

# SkillSpector (NVIDIA) — audits the repo's Claude skills (.claude/skills/)
# before they run on anyone's machine: YARA + AST static analysis (the
# --no-llm path; no API key needed in CI). Pinned to the same git SHA as
# external-tools.json tools.skillspector; python pinned because
# yara-python ships no cp314 wheels yet.
skills-scan:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
- name: Scan each skill
run: |
set -euo pipefail
for skill in .claude/skills/*/; do
echo "::group::skillspector ${skill}"
uv tool run --python 3.12 \
--from git+https://github.com/NVIDIA/skillspector@2eb84478 \
skillspector scan "${skill}" --no-llm
echo "::endgroup::"
done

# License policy + duplicate-version tracking (deny.toml at repo root).
# Advisories stay with cargo-audit above — deny.toml doesn't duplicate them.
# Pure metadata check: no compilation, runs in ~1 min once cargo-deny is
Expand All @@ -72,7 +144,7 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
with:
Expand Down
94 changes: 94 additions & 0 deletions .github/workflows/soak-autofix.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
name: soak-autofix

# The soak gates fail CLOSED by design: an expired soakBypass annotation or
# a cleared minimumReleaseAgeExclude pin turns tools:check / soak red until
# someone prunes it. This workflow does the pruning automatically — daily it
# runs the fixers, and when they change anything it commits to a bot branch
# and opens (or updates) a PR, so the gate never sits red waiting for a
# human to delete two lines.

on:
schedule:
# Daily, shortly after midnight UTC — annotations expire on date
# boundaries, so the fix lands the morning a window clears.
- cron: '17 0 * * *'
workflow_dispatch:

permissions: {}

# One run at a time: overlapping runs would race on the bot branch
# force-push and the open-PR check.
concurrency:
group: soak-autofix
cancel-in-progress: false

jobs:
autofix:
# Guard the dispatch path: run only from main, so a workflow_dispatch
# on a topic branch can't force-push bot/soak-autofix from arbitrary
# HEAD state.
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: write
pull-requests: write
steps:
# persist-credentials stays on: this job pushes its own fix branch.
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 # zizmor: ignore[artipacked]
with:
# Optional PAT: PRs opened with the default github.token do NOT
# trigger CI on the PR (GitHub drops workflow events from
# token-created refs), so checks stay pending until a human
# closes/reopens the PR. Set the SOAK_AUTOFIX_TOKEN repo secret
# (a fine-grained PAT with contents+pull-requests write) to make
# the autofix PRs run CI like any other PR.
token: ${{ secrets.SOAK_AUTOFIX_TOKEN || github.token }}
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version-file: .node-version
- name: Run the soak fixers
id: fixers
# Fixers write mechanical repairs first, then exit by post-fix
# check status. Capture that status instead of masking it: the
# commit step below still lands whatever WAS fixable, and the
# final step re-raises the failure so a crashed fixer or a
# human-only finding can never ride a green run.
run: |
status=0
node scripts/soak/soak.mts --fix || status=$?
node scripts/soak/external-tools.mts --fix || status=$?
echo "status=${status}" >> "$GITHUB_OUTPUT"
- name: Commit + PR when something was fixed
env:
GH_TOKEN: ${{ secrets.SOAK_AUTOFIX_TOKEN || github.token }}
run: |
if git diff --quiet; then
echo "soak surfaces clean — nothing to fix"
exit 0
fi
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
BRANCH="bot/soak-autofix"
git checkout -B "$BRANCH"
git add -A
git commit -m "chore(soak): prune expired soak annotations (automated)

Generated by the soak-autofix workflow: soak.mts --fix +
external-tools.mts --fix. Windows that cleared have soaked;
their bypass annotations are dead weight the gates would
otherwise fail on."
git push -f origin "$BRANCH"
if [ -z "$(gh pr list --head "$BRANCH" --state open --json number --jq '.[0].number')" ]; then
gh pr create --head "$BRANCH" \
--title "chore(soak): prune expired soak annotations (automated)" \
--body "Automated by the soak-autofix workflow. The listed soak windows have cleared, so their bypass annotations are stale. Diff is the full review: only annotation/window lines are touched."
fi
- name: Re-raise fixer findings
# After the mechanical repairs are committed, a nonzero fixer
# status means findings remain that need a human (or the fixer
# itself crashed) — fail the run so it can't read as clean.
if: steps.fixers.outputs.status != '0'
run: |
echo "soak fixers exited nonzero — human-actionable findings remain (see fixer step log)"
exit 1
53 changes: 53 additions & 0 deletions .github/workflows/zizmor.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
name: zizmor

on:
push:
branches: [main]
pull_request:
paths: ['.github/**']

permissions: {}

jobs:
zizmor:
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version-file: .node-version
# zizmor rides the same SRI-pinned rack as every other external tool
# (external-tools.json tools.zizmor) instead of a marketplace action —
# one pin source, and local `npm run tools:install` audits with the
# exact same binary CI does.
- name: Install pinned zizmor
env:
# download() attaches this to github.com fetches only (release
# assets) — avoids unauthenticated rate limits.
GITHUB_TOKEN: ${{ github.token }}
run: |
node scripts/soak/external-tools.mts --install zizmor
node scripts/soak/external-tools.mts --print-bin >> "$GITHUB_PATH"
- name: Audit GitHub Actions
env:
GH_TOKEN: ${{ github.token }}
run: |
# Pass the token only when non-empty: zizmor treats an empty
# --gh-token as a real token, then fatally errors when its online
# impostor-commit check can't authenticate.
#
# Starting gate: high only (config in .github/zizmor.yml). The 34
# medium findings (missing permissions: blocks in test.yml /
# benchmark.yml / container-tests.yml / cache-warm.yml /
# simctl-tests.yml) are a tracked ratchet — audit each job's real
# token needs, add the blocks, then gate at medium.
if [ -n "${GH_TOKEN}" ]; then
zizmor .github/ --gh-token "${GH_TOKEN}" --min-severity high
else
zizmor .github/ --min-severity high
fi
Loading
Loading