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
40 changes: 36 additions & 4 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,9 @@ jobs:
needs: validate-tag
runs-on: ubuntu-latest
environment: release
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
Expand Down Expand Up @@ -123,17 +126,25 @@ jobs:
exit 1
fi

# Exchange after package verification so the 30-minute token stays fresh.
- name: Authenticate to crates.io
id: auth
uses: rust-lang/crates-io-auth-action@3268eb413133e2960fa4ae0e963ca76380c54cd9 # v1.0.6

- name: Publish bashkit-cpython-wasm to crates.io
run: cargo publish --no-verify -p bashkit-cpython-wasm
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
CARGO_REGISTRY_TOKEN: ${{ steps.auth.outputs.token }}

# Publish bashkit (core library) to crates.io
publish-bashkit:
name: Publish bashkit
runs-on: ubuntu-latest
needs: [validate-tag, publish-bashkit-cpython-wasm]
environment: release
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
Expand Down Expand Up @@ -171,17 +182,25 @@ jobs:
- name: Verify bashkit package without credentials
run: cargo publish --dry-run -p bashkit

# Exchange after package verification so the 30-minute token stays fresh.
- name: Authenticate to crates.io
id: auth
uses: rust-lang/crates-io-auth-action@3268eb413133e2960fa4ae0e963ca76380c54cd9 # v1.0.6

- name: Publish bashkit to crates.io
run: cargo publish --no-verify -p bashkit
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
CARGO_REGISTRY_TOKEN: ${{ steps.auth.outputs.token }}

# Publish bashkit-cli (depends on bashkit)
publish-bashkit-cli:
name: Publish bashkit-cli
runs-on: ubuntu-latest
needs: [validate-tag, publish-bashkit]
environment: release
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
Expand All @@ -197,17 +216,25 @@ jobs:
- name: Verify bashkit-cli package without credentials
run: cargo publish --dry-run -p bashkit-cli

# Exchange after package verification so the 30-minute token stays fresh.
- name: Authenticate to crates.io
id: auth
uses: rust-lang/crates-io-auth-action@3268eb413133e2960fa4ae0e963ca76380c54cd9 # v1.0.6

- name: Publish bashkit-cli to crates.io
run: cargo publish --no-verify -p bashkit-cli
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
CARGO_REGISTRY_TOKEN: ${{ steps.auth.outputs.token }}

# Publish bashkit-scripted-tool (depends on bashkit)
publish-bashkit-scripted-tool:
name: Publish bashkit-scripted-tool
runs-on: ubuntu-latest
needs: [validate-tag, publish-bashkit]
environment: release
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
Expand All @@ -223,10 +250,15 @@ jobs:
- name: Verify bashkit-scripted-tool package without credentials
run: cargo publish --dry-run -p bashkit-scripted-tool

# Exchange after package verification so the 30-minute token stays fresh.
- name: Authenticate to crates.io
id: auth
uses: rust-lang/crates-io-auth-action@3268eb413133e2960fa4ae0e963ca76380c54cd9 # v1.0.6

- name: Publish bashkit-scripted-tool to crates.io
run: cargo publish --no-verify -p bashkit-scripted-tool
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
CARGO_REGISTRY_TOKEN: ${{ steps.auth.outputs.token }}

# ============================================================================
# Verify all packages were published successfully
Expand Down
2 changes: 2 additions & 0 deletions crates/bashkit-js/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,9 @@
"overrides": {
"brace-expansion": "^2.1.7",
"esbuild": ">=0.28.1",
"glob": "^14.0.2",
"js-yaml": "^4.3.1",
"minimatch": "^11.0.1",
"tar": "^7.5.18"
},
"onlyBuiltDependencies": [
Expand Down
32 changes: 20 additions & 12 deletions crates/bashkit-js/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions knowledge/operations/dependencies.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,16 @@ the existing Python graph. Remove it when Monty's AST upgrades its string
type. The Python binding's `num-bigint` dependency must still match Monty's
0.4 line.

## JavaScript test-tool audit boundary

The native JavaScript package constrains AVA's transitive file-tracing tools to
patched `glob` and `minimatch` versions. The upstream `@vercel/nft` constraint
still selects vulnerable glob 13, so the package's pnpm overrides carry the
security floor until upstream catches up. These dependencies belong to test
instrumentation, not the published runtime. Keep the normal release cooldown
and audit gate; never suppress the cancellation-bypass or pattern-amplification
advisories. See [package overrides](../../crates/bashkit-js/package.json).

## YAML maintenance boundary

The `jq` feature's `yq` builtin uses `serde_norway` and
Expand Down
24 changes: 24 additions & 0 deletions knowledge/operations/release-process.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,30 @@ fetch process exits before examples execute. See the
`publish-wasm.yml` / `publish-python.yml` publish to crates.io, npm, PyPI;
each includes a published-version verification step.

## crates.io trusted publishing

The publishing workflow uses GitHub OIDC instead of a stored registry token.
Each publish job obtains a masked, short-lived token only after the package has
been verified without registry credentials. The pinned official authentication
action revokes the token when the job finishes; only the upload step receives it.
Token issuance alone does not prove a crate was published: retain the existing
registry verification and record the first successful OIDC release separately.

Before enabling the workflow, configure every published crate on crates.io to
trust this repository, the publishing workflow filename, and the `release`
environment. The workflow is [`publish.yml`](../../.github/workflows/publish.yml).
The GitHub `release` environment must allow only the `main` branch and `v*`
tags, preserving the existing main-based recovery path. The validated immutable
release source and dependency order remain mandatory. Check all four crates:
`bashkit-cpython-wasm`, `bashkit`, `bashkit-cli`, and `bashkit-scripted-tool`.

A new crate needs its first version published by an owner with a suitably scoped
API token before crates.io permits a trusted publisher. Complete that bootstrap
and configure trust before adding a new name to automated publication. Trusted
publishing cannot yank versions. Preserve any token needed for separate yank or
bootstrap operations; remove obsolete publishing credentials only after a real
OIDC publication succeeds and their other consumers have been checked.

## Pre-Release Checklist

`AGENTS.md` Pre-PR Checklist applies, plus: CI green on main, CHANGELOG.md
Expand Down
2 changes: 1 addition & 1 deletion knowledge/security/threat-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -469,7 +469,7 @@ registered non-shell builtin runs that builtin on the script path (same sandbox
| TM-INF-024 | Host env side-channel via clap `Arg::env(...)` | uutils-ported clap builtins (currently `ls`: `.env("TABSIZE")`/`.env("TIME_STYLE")`) resolved defaults from `std::env`, not `ctx.env`, (1) presence-probe of host env vars via `ls` behaviour; (2) a host-set `TIME_STYLE` became a value source for an unimplemented option, tripping the unsupported-option gate on every plain `ls` for unrelated tenants | Four-layer fix: (1) codegen, `bashkit-coreutils-port` strips runtime `.env(...)` calls and emits a sidecar `<UTIL>_ENV_DEFAULTS: &[EnvDefault]` table per stripped annotation; (2) virtual-env shim, `crate::builtins::clap_env::apply_env_defaults` rewrites argv from `ctx.env` (never `std::env`) before `try_get_matches_from`, emulating clap's "argv > env > default" precedence; (3) static guards, `no_clap_env_in_generated_parsers` forbids runtime `.env(` in `generated/*.rs`; `every_generated_parser_emits_env_defaults_table` enforces the sidecar on every util; `no_host_env_reads_in_generated_code` recursively forbids `std::env::var*` reads in `generated/**`, covering vendored uucore module bodies (which have no `ctx` and so cannot reach the virtual env) — `bashkit-coreutils-port`'s `fold_host_env` folds names allow-listed in `vendored.toml`'s `host_env` to "unset" at port time and aborts the port on any other env read, so upstream adding one (as it did with a `POSIXLY_CORRECT` probe in `format/argument.rs`) fails drift loudly; (4) defence-in-depth, workspace `clap` drops the `env` cargo feature, so a re-introduced `.env(...)` fails to compile. Coverage: `ls_ignores_host_time_style_and_tabsize` (host env ignored) and `ls_honors_virtual_env_time_style` (virtual env honoured) | **FIXED** |
| TM-INF-025 | Untrusted generated Rust runs in drift CI with repository write token | The coreutils argument-drift workflow regenerates Rust from third-party `uutils/coreutils` `uu_app()` then builds/tests bashkit; if the generator preserved arbitrary upstream statements and the job held `contents: write`/`pull-requests: write`, malicious upstream code could execute with repository write impact | Two-layer fix: (1) `bashkit-coreutils-port` validates args-mode `uu_app()` before emission, accepts only a single tail clap `Command` builder chain, or `let <ident> = Command::new(...)<chain>; <ident>.<method>(...)<chain>` with the tail chained off the let-bound ident; both shapes pass a disallowed-method/closure/macro/block visitor. Regression tests `rejects_executable_statements_in_uu_app`, `rejects_let_with_non_command_initializer`, `rejects_tail_not_chained_off_let_binding`, `rejects_three_statement_body`. (2) `.github/workflows/coreutils-args-drift.yml` separates privilege: regeneration/build/test runs with `contents: read` + `persist-credentials: false`; the `open-pr` job has write permission but never builds or executes generated Rust, only commits the tested artifact | **FIXED** |
| TM-INF-026 | Fork-PR secret exfiltration via approved CI run | GitHub's first-time-contributor gate runs the workflow **from the PR head**, and approved contributors auto-run thereafter. The `examples` job in `ci.yml` set `DOPPLER_TOKEN` at job level and `ANTHROPIC_API_KEY` at step level, so a fork PR could edit any `examples/*.rs`/`build.rs`/script the job runs and exfiltrate. `DOPPLER_TOKEN` is the master key, it fetches every other secret in the Doppler config, so one leak compromises all. Every `doppler run --` also injected the entire config into the step env | Trusted-run gates exclude PR execution from secret-backed examples. Dedicated credential-fetch steps receive `DOPPLER_TOKEN`, request one API key, mask it, and exit before build/example processes run. Execution steps receive only the requested API key through step output/env; Docker inherits it by name, without putting its value in argv. No broad token remains in the execution shell or a live Doppler parent. This is process-lifetime isolation on a trusted runner, not a sandbox against compromised dependencies persisting across steps. Regression tests execute the workflow scripts and inspect child/parent environments: `scripts/tests/test_maintenance_security.py`. CI has contents-read permission only and checkout credentials are not persisted anywhere they could reach untrusted code (TM-INF-034). Release examples install the committed dependency lockfile with lifecycle scripts disabled before linking the built artifact; `scripts/tests/test_ci_supply_chain.py` enforces both boundaries | **FIXED** |
| TM-INF-027 | Package publish from unverified release refs | A user with Actions/write privileges could dispatch release/publish workflows from a branch or unprotected `vX.Y.Z` tag; `release.yml` dispatches downstream workflows with `--ref "$TAG"`, and `publish-js.yml` treated stable `package.json` semver as `latest` even on non-tag refs, an attacker-controlled ref could source official crates.io/npm/PyPI/CLI/Homebrew artifacts | Release dispatches require full ref `refs/heads/main`; `release.yml` verifies the source is reachable from `origin/main` and the created tag resolves to the current `GITHUB_SHA` before dispatching; `publish.yml` rejects manual runs unless `GITHUB_REF` is a stable release tag matching `Cargo.toml` on a main-reachable commit; `publish-js.yml` requires main-reachable commit, assigns npm `latest` only for stable `vX.Y.Z` tag refs matching `package.json`; `publish-python.yml` applies the same main-reachable stable-tag + Cargo version gates; `cli-binaries.yml` accepts only stable `vX.Y.Z` inputs and verifies workflow ref = tag = `Cargo.toml` version on a main-reachable commit before building/uploading or updating Homebrew; CLI and C ABI validation resolve the fetched tag to an immutable commit, read its checked-out manifest, and export that SHA to all build jobs, preventing tag movement from substituting unvalidated source. Real Git tag-movement regressions: `scripts/tests/test_release_security.py` | **FIXED** |
| TM-INF-027 | Package publish from unverified release refs | A user with Actions/write privileges could dispatch release/publish workflows from a branch or unprotected `vX.Y.Z` tag; `release.yml` dispatches downstream workflows with `--ref "$TAG"`, and `publish-js.yml` treated stable `package.json` semver as `latest` even on non-tag refs, an attacker-controlled ref could source official crates.io/npm/PyPI/CLI/Homebrew artifacts | Release dispatches require full ref `refs/heads/main`; `release.yml` verifies the source is reachable from `origin/main` and the created tag resolves to the current `GITHUB_SHA` before dispatching; `publish.yml` rejects manual runs unless `GITHUB_REF` is a stable release tag matching `Cargo.toml` on a main-reachable commit; `publish-js.yml` requires main-reachable commit, assigns npm `latest` only for stable `vX.Y.Z` tag refs matching `package.json`; `publish-python.yml` applies the same main-reachable stable-tag + Cargo version gates; `cli-binaries.yml` accepts only stable `vX.Y.Z` inputs and verifies workflow ref = tag = `Cargo.toml` version on a main-reachable commit before building/uploading or updating Homebrew; CLI and C ABI validation resolve the fetched tag to an immutable commit, read its checked-out manifest, and export that SHA to all build jobs, preventing tag movement from substituting unvalidated source. Real Git tag-movement regressions: `scripts/tests/test_release_security.py`. Crates.io upload jobs obtain masked short-lived OIDC tokens after credential-free verification and revoke them at job completion; registry trust binds the repository, `publish.yml` and `release` environment. That environment must allow only main and release tags; its policy and per-crate trust require external configuration | **FIXED** |
| TM-INF-028 | JS callback errors expose host stack traces | When a caller-supplied output, custom-builtin or embedded-runtime host callback throws, propagating `error.stack` or an unsanitized message can expose host file paths and internal function names across the sandbox boundary | Native and wasm bindings propagate `error.message` only, strip absolute-path and `file://` segments, and cap diagnostics at 256 characters. Regressions: `onOutput error does not leak stack trace or host paths` in `crates/bashkit-js/__test__/streaming-output.spec.ts` and `callback errors do not leak host paths or stacks` in `crates/bashkit-wasm/__test__/bashkit-wasm.test.mjs`; mitigations annotated `THREAT[TM-INF-028]` in the binding implementations; embedded-runtime host callbacks also reject unsupported/deep JSON values and same-instance reentry | **FIXED** |
| TM-INF-030 | Raw callback or custom-builtin errors leak host internals through ScriptedTool/ToolImpl/ToolRegistry | Tool callbacks and operator-provided ScriptedTool builtins may return errors containing API keys, connection strings, or stack traces; output is attacker-influenced and agent-visible | `sanitize_errors` defaults to true on `ScriptedTool`, `ToolImpl`, and the cross-runtime `ToolRegistry`; ScriptedTool wraps custom builtins so fatal errors use the same generic message while cancellation/resource-limit errors remain actionable; every shell/Python/TypeScript registry surface applies the boundary (`bashkit-scripted-tool`: `scripted_tool/execute.rs`, `scripted_tool/mod.rs`, `tool_def.rs`, `tool_registry.rs`) | **MITIGATED** |
| TM-INF-031 | `final_env` capture bypasses output filtering and size caps | When `capture_final_env` is enabled, env contents are a user-visible output channel that could leak internal markers or exceed output limits | Visibility filtering + output-byte cap applied when building `final_env` (`interpreter/mod.rs`) | **MITIGATED** |
Expand Down
Loading
Loading