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
11 changes: 11 additions & 0 deletions .agents/skills/maintain/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,17 @@ upstream version cannot preserve a required security contract, establish the
incompatibility, retain the newest safe version with a tested/documented pin,
and complete the rest of the pass; do not replace security checks with weaker ones.

Audit CI, not just its results. A green run says nothing about what the workflow
definition hands to the code it executes, so each pass reviews `.github/workflows/`
against the Workflow Credential Audit in `knowledge/operations/maintenance.md`:
checkout credentials persisted only where a `git`/`gh` call authenticates with them
and never in a pull-request-triggered job, a top-level `permissions` block on every
workflow, runtime-fetched secrets masked before they travel and never routed through
`$GITHUB_ENV`, broad tokens confined to steps that run no repository code, no
`pull_request_target`/`workflow_run`/`issue_comment` triggers, and no untrusted
`${{ github.event.* }}` interpolation inside a `run:` block. Treat a finding here
as a fix in this pass; `scripts/tests/test_ci_supply_chain.py` holds the regressions.

Do not create deferral issues instead of completing maintenance unless the user
explicitly requests a deferred scope or a genuine external blocker requires it.
Only stop for an actual missing permission, unavailable dependency/service, or
Expand Down
4 changes: 3 additions & 1 deletion .github/workflows/cli-binaries.yml
Original file line number Diff line number Diff line change
Expand Up @@ -229,9 +229,11 @@ jobs:
# fine-grained PAT scoped to `everruns/homebrew-tap` only
# (Contents: read+write, Metadata: read), so a leak cannot
# touch the main bashkit repo.
# Register the mask before the value can reach any log line: an
# `set -x` or error trace between fetch and mask would print it.
GH_PAT=$(doppler secrets get HOMEBREW_TAP_GITHUB_TOKEN --plain)
export GH_PAT
echo "::add-mask::$GH_PAT"
export GH_PAT

# Use askpass to avoid embedding token in clone/push URLs or logs
ASKPASS_SCRIPT=$(mktemp)
Expand Down
6 changes: 6 additions & 0 deletions .github/workflows/coverage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ jobs:
timeout-minutes: 60
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0
Expand Down Expand Up @@ -76,6 +78,8 @@ jobs:
timeout-minutes: 30
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0
Expand Down Expand Up @@ -125,6 +129,8 @@ jobs:
timeout-minutes: 30
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/fuzz.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ jobs:

steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Install Rust nightly
uses: dtolnay/rust-toolchain@5b842231ba77f5c045dba54ac5560fed2db780e2 # nightly
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/js.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0
Expand Down Expand Up @@ -110,6 +112,8 @@ jobs:

steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0
Expand Down
6 changes: 6 additions & 0 deletions .github/workflows/nightly.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Install Rust nightly with Miri
uses: dtolnay/rust-toolchain@5b842231ba77f5c045dba54ac5560fed2db780e2 # nightly
Expand Down Expand Up @@ -72,6 +74,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0
Expand Down Expand Up @@ -106,6 +110,8 @@ jobs:
timeout-minutes: 90
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

# `-Z build-std` rebuilds std + every dep + every bashkit test with
# ASAN instrumentation. Target dir grows past the default runner's
Expand Down
6 changes: 6 additions & 0 deletions .github/workflows/publish-js.yml
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ jobs:

steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Setup pnpm
uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
Expand Down Expand Up @@ -172,6 +174,8 @@ jobs:
runs-on: ${{ matrix.settings.host }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Setup pnpm
uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
Expand Down Expand Up @@ -293,6 +297,8 @@ jobs:
runs-on: ${{ contains(matrix.target, 'aarch64') && 'ubuntu-24.04-arm' || 'ubuntu-latest' }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Setup pnpm
uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
Expand Down
6 changes: 6 additions & 0 deletions .github/workflows/publish-python.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
Expand Down Expand Up @@ -94,6 +96,8 @@ jobs:
runs-on: ${{ matrix.runs-on }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
Expand Down Expand Up @@ -131,6 +135,8 @@ jobs:
PYODIDE_BUILD_VERSION: "0.34.4"
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

# Python 3.13 -> pyodide-build's modern config (Pyodide 0.29.x, Emscripten
# 4.0.9), whose binaryen understands modern LLVM's wasm target-features and
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/publish-wasm.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ jobs:
timeout-minutes: 30
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Install Rust
uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0
Expand Down
10 changes: 10 additions & 0 deletions .github/workflows/python.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0

Expand Down Expand Up @@ -74,6 +76,8 @@ jobs:
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
Expand Down Expand Up @@ -121,6 +125,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
Expand Down Expand Up @@ -173,6 +179,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
Expand Down Expand Up @@ -212,6 +220,8 @@ jobs:
PYODIDE_BUILD_VERSION: "0.34.4"
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

# Python 3.13 selects pyodide-build's modern config (Pyodide 0.29.x,
# Emscripten 4.0.9), whose binaryen understands the wasm target-features
Expand Down
9 changes: 9 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ on:
- main
workflow_dispatch:

# Least privilege by default: the single `release` job below opts into the
# write scopes it needs. Any job added later starts read-only instead of
# inheriting the repository default.
permissions:
contents: read

jobs:
release:
name: Create Release
Expand All @@ -16,6 +22,9 @@ jobs:
# Only run if manually triggered OR commit message starts with "chore(release): prepare v"
if: "${{ github.event_name == 'workflow_dispatch' || startsWith(github.event.head_commit.message, 'chore(release): prepare v') }}"
steps:
# Checkout credentials are deliberately persisted here: `git push origin
# "$TAG"` below authenticates through them. This job runs only from
# protected `main` and executes no pull-request-authored code.
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/site.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Setup Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v6
with:
Expand All @@ -55,6 +57,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Setup pnpm
uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
with:
Expand Down
3 changes: 2 additions & 1 deletion crates/bashkit/docs/threat-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,7 @@ Scripts may attempt to leak sensitive information.

| Threat | Attack Example | Mitigation | Status |
|--------|---------------|------------|--------|
| Fork-PR secret exfil (TM-INF-026) | Fork PR edits `examples/*.rs` or `build.rs` to read `$DOPPLER_TOKEN` / `$ANTHROPIC_API_KEY` from the runner env and exfiltrate; first-time-contributor approval gate runs the workflow from PR head, and `DOPPLER_TOKEN` is the master key to every other secret in the Doppler config | Trusted-run gates exclude PR execution from secret-backed examples. Dedicated fetch steps in `.github/workflows/{ci,js,publish-js}.yml` request one API key, mask it, and exit before build/example execution. Execution steps receive only that scoped key; Docker inherits it by name, without a secret value in argv. Neither the execution shell nor a live Doppler parent retains the broad service token. This protects process environments on a trusted runner; it does not sandbox dependencies persisting across steps. Workflow-script regression tests check child/parent environments. CI has contents-read permission only and does not persist checkout credentials; release examples install the reviewed lockfile without lifecycle scripts before linking the built artifact | **FIXED** |
| Fork-PR secret exfil (TM-INF-026) | Fork PR edits `examples/*.rs` or `build.rs` to read `$DOPPLER_TOKEN` / `$ANTHROPIC_API_KEY` from the runner env and exfiltrate; first-time-contributor approval gate runs the workflow from PR head, and `DOPPLER_TOKEN` is the master key to every other secret in the Doppler config | Trusted-run gates exclude PR execution from secret-backed examples. Dedicated fetch steps in `.github/workflows/{ci,js,publish-js}.yml` request one API key, mask it, and exit before build/example execution. Execution steps receive only that scoped key; Docker inherits it by name, without a secret value in argv. Neither the execution shell nor a live Doppler parent retains the broad service token. This protects process environments on a trusted runner; it does not sandbox dependencies persisting across steps. Workflow-script regression tests check child/parent environments. CI has contents-read permission only and does not persist checkout credentials anywhere they could reach untrusted code (TM-INF-034); release examples install the reviewed lockfile without lifecycle scripts before linking the built artifact | **FIXED** |

**Additional information-disclosure hardening:**

Expand All @@ -287,6 +287,7 @@ Scripts may attempt to leak sensitive information.
| jq `halt`/`halt_error` exits the host process (TM-INF-023) | Filter calls `halt(N)` → `std::process::exit` | Upstream `halt` native stripped; safe replacement returns a jq error | MITIGATED |
| Host env side-channel via clap `Arg::env` (TM-INF-024) | `ls` resolved `TABSIZE`/`TIME_STYLE` from host env | Codegen strips `.env(...)`; builtins read `ctx.env` only | MITIGATED |
| Untrusted generated Rust in drift CI (TM-INF-025) | Malicious upstream `uu_app()` runs with a write token | Generator validates the emitted shape; drift workflow splits read/write privilege | FIXED |
| Checkout token readable by untrusted CI code (TM-INF-034) | `actions/checkout` leaves `GITHUB_TOKEN` in `.git/config`, where a dependency build script or test in the same job can read it | `persist-credentials: false` on every checkout that does not back an authenticated `git`/`gh` call, and on every job of a pull-request-triggered workflow; every workflow declares a read-only default `permissions` block | FIXED |
| Publish from unverified release refs (TM-INF-027) | Dispatch publish from a branch or unprotected tag | Dispatch requires `refs/heads/main`; publish gated on real `vX.Y.Z` tags | FIXED |
| JS `onOutput` errors expose host stack traces (TM-INF-028) | Callback throws, leaking `error.stack` | Propagate `error.message` only; strip absolute/`file://` paths | FIXED |
| Raw callback errors leak host internals (TM-INF-030) | Tool callback throws API keys/connection strings/stack traces | `sanitize_errors` defaults on for `ScriptedTool`/`ToolImpl`/`ToolRegistry` across shell/Python/TypeScript | MITIGATED |
Expand Down
28 changes: 28 additions & 0 deletions knowledge/operations/maintenance.md
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,34 @@ See [Coreutils Argument Port](../runtimes/coreutils-args-port.md).
- Git-sourced dependencies still resolve
- No orphaned CI quarantines - `grep -rn continue-on-error .github/workflows/`; each hit must cite its tracking issue (bun-canary quarantine: #2417, revert when the canary `bun-linux-x64.zip` asset is back)

#### Workflow Credential Audit

Run-level green is not the same as a safe workflow definition. Audit
`.github/workflows/` itself each pass; the enforced invariants live in
`scripts/tests/test_ci_supply_chain.py` and `scripts/tests/test_maintenance_security.py`
(TM-INF-026, TM-INF-027, TM-INF-034), so a new workflow that violates one fails
`just test` rather than silently shipping.

- **Checkout credentials**: every `actions/checkout` sets
`persist-credentials: false` unless the job authenticates a `git`/`gh` call.
Without it the job's `GITHUB_TOKEN` sits in `.git/config` where any
`build.rs`, npm lifecycle script, or test in the same job can read it. No job
in a `pull_request`-triggered workflow may keep it.
- **Default permissions**: every workflow declares a top-level `permissions`
block; write scopes are opt-in per job, never inherited.
- **Secret transport**: a secret fetched at runtime is invisible to GitHub's log
masker until `::add-mask::` announces it. Fetch, mask, then pass it on through
`$GITHUB_OUTPUT` and step `env`. Never `$GITHUB_ENV` - that republishes the
value in the environment block of every later step. Only a
`secrets.X != ''` presence boolean may reach `$GITHUB_ENV`.
- **Credential lifetime**: the step holding a broad token (`DOPPLER_TOKEN`)
fetches one key and exits before any repository code runs. No `cargo`, `node`,
`bun`, `docker`, or `examples/` invocation shares a step with it.
- **Triggers**: no `pull_request_target`, `workflow_run`, or `issue_comment`
trigger; these run with a writable token in an attacker-influenced context.
- **Untrusted interpolation**: no `${{ github.event.* }}` or `${{ github.head_ref }}`
expanded directly into a `run:` block. Pass it through `env:` instead.

#### Escalation Policy

Failures persisting **>2 consecutive days** on any workflow (CI, nightly, fuzz)
Expand Down
Loading
Loading