From 525ee97954f9f05ec5eb35120593ade9df05fac2 Mon Sep 17 00:00:00 2001 From: Mykhailo Chalyi Date: Fri, 30 Oct 2026 20:25:22 -0500 Subject: [PATCH 1/2] ci(release): use crates.io trusted publishing --- .github/workflows/publish.yml | 40 ++++++++++++++++++++++--- knowledge/operations/release-process.md | 24 +++++++++++++++ knowledge/security/threat-model.md | 2 +- scripts/tests/test_release_security.py | 21 +++++++++++++ 4 files changed, 82 insertions(+), 5 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 834368b18..e9e4a11db 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -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: @@ -123,10 +126,15 @@ 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: @@ -134,6 +142,9 @@ jobs: 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: @@ -171,10 +182,15 @@ 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: @@ -182,6 +198,9 @@ jobs: 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: @@ -197,10 +216,15 @@ 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: @@ -208,6 +232,9 @@ jobs: 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: @@ -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 diff --git a/knowledge/operations/release-process.md b/knowledge/operations/release-process.md index 194d00f7f..7cac2984f 100644 --- a/knowledge/operations/release-process.md +++ b/knowledge/operations/release-process.md @@ -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 diff --git a/knowledge/security/threat-model.md b/knowledge/security/threat-model.md index acfa4f5fd..644527271 100644 --- a/knowledge/security/threat-model.md +++ b/knowledge/security/threat-model.md @@ -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 `_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 = Command::new(...); .(...)` 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** | diff --git a/scripts/tests/test_release_security.py b/scripts/tests/test_release_security.py index 652a518d8..e15609078 100644 --- a/scripts/tests/test_release_security.py +++ b/scripts/tests/test_release_security.py @@ -16,6 +16,27 @@ def jobs(name): class ReleaseSecurityTests(unittest.TestCase): + def test_crates_oidc_is_scoped_to_publish_jobs_after_verification(self): + workflow = yaml.safe_load((ROOT / '.github/workflows/publish.yml').read_text()) + self.assertNotIn('id-token', workflow['permissions']) + for name, job in workflow['jobs'].items(): + with self.subTest(job=name): + if not name.startswith('publish-'): + self.assertNotIn('id-token', job.get('permissions', {})) + continue + self.assertEqual(job['environment'], 'release') + self.assertEqual(job['permissions'], {'contents': 'read', 'id-token': 'write'}) + steps = job['steps'] + auth = next(i for i, s in enumerate(steps) if s.get('id') == 'auth') + self.assertRegex(steps[auth]['uses'], r'^rust-lang/crates-io-auth-action@[0-9a-f]{40}$') + self.assertIn('--dry-run', steps[auth - 1]['run']) + publish = steps[auth + 1] + self.assertIn('cargo publish --no-verify', publish['run']) + self.assertEqual(publish['env']['CARGO_REGISTRY_TOKEN'], '${{ steps.auth.outputs.token }}') + for step in steps[:auth]: + self.assertNotIn('CARGO_REGISTRY_TOKEN', step.get('env', {})) + self.assertNotIn('secrets.CARGO_REGISTRY_TOKEN', (ROOT / '.github/workflows/publish.yml').read_text()) + def test_binary_builds_use_validated_immutable_commit(self): for name in ('c-api-binaries.yml', 'cli-binaries.yml', 'publish.yml'): with self.subTest(workflow=name): From 79c661b4185b9843f2712c78711e64029267e5ed Mon Sep 17 00:00:00 2001 From: Mykhailo Chalyi Date: Fri, 30 Oct 2026 20:44:51 -0500 Subject: [PATCH 2/2] fix(deps): patch JavaScript test glob vulnerabilities --- crates/bashkit-js/package.json | 2 ++ crates/bashkit-js/pnpm-lock.yaml | 32 +++++++++++++++++----------- knowledge/operations/dependencies.md | 10 +++++++++ 3 files changed, 32 insertions(+), 12 deletions(-) diff --git a/crates/bashkit-js/package.json b/crates/bashkit-js/package.json index 0570eff4a..2ad30fb4b 100644 --- a/crates/bashkit-js/package.json +++ b/crates/bashkit-js/package.json @@ -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": [ diff --git a/crates/bashkit-js/pnpm-lock.yaml b/crates/bashkit-js/pnpm-lock.yaml index fa64e42d5..1f0ed07e3 100644 --- a/crates/bashkit-js/pnpm-lock.yaml +++ b/crates/bashkit-js/pnpm-lock.yaml @@ -7,7 +7,9 @@ settings: 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 patchedDependencies: @@ -372,6 +374,9 @@ packages: resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} engines: {node: '>=18.0.0'} + '@isaacs/nonrecursive-glob-match@1.0.2': + resolution: {integrity: sha512-GJPHIgPPtFl+nLhFHyF0zZoFcOFLsZWbHFFzo+Zr+PWDXJbl9hHxb2w+9SiF2fHmLgKaXIpUzQo3SbBBZ9WObw==} + '@langchain/core@1.3.0': resolution: {integrity: sha512-ZiS8USsHdO8ZVFh0m8Rs6v2BvqBEvfFicG66zxdF4rGgXSJjk0d0sLnnFZCx3RXPUvP8ybTJgPlNfkJGs1J7BA==} engines: {node: '>=20'} @@ -1352,8 +1357,8 @@ packages: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} - glob@13.0.6: - resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==} + glob@14.0.2: + resolution: {integrity: sha512-vfMgTR2djMHPUchsJMLpZlYQvtjVL+mHpEr5wPpoCbQs2vr/FBNK+3Gnl9Ara1DSs4Aeg4eG9HiADb5/rkY/ZA==} engines: {node: 18 || 20 || >=22} globby@16.2.4: @@ -1487,8 +1492,8 @@ packages: resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} engines: {node: '>=18'} - minimatch@10.2.6: - resolution: {integrity: sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==} + minimatch@11.0.2: + resolution: {integrity: sha512-pWTgz5CN3tjqyZNSK2tePlSmu9IwM41q8yn8Usdqqwe9TIHh+9dEpLLjF4uApsfPEAZY71qtSiA2B2APEr+1Yw==} engines: {node: 18 || 20 || >=22} minipass@7.1.3: @@ -1565,8 +1570,8 @@ packages: resolution: {integrity: sha512-GYTTew2slBcYdvRHqjhwaaydVMvn/qrGC323+nKclYioNSLTDUM/lGgtGTgyHVtYcozb+XkE8CNhwcraOmZ9Mg==} engines: {node: '>=18'} - path-scurry@2.0.2: - resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} + path-scurry@3.0.0: + resolution: {integrity: sha512-1pTbQqH1wIwMi/eleQ5leFTTCkzFsPcgXNjwOA8YGGW/W4/kHT3dssE7Z6NH+zuqNLOzOB0uaD61yl7cAcVhgg==} engines: {node: 18 || 20 || >=22} picomatch@2.3.2: @@ -1982,6 +1987,8 @@ snapshots: dependencies: minipass: 7.1.3 + '@isaacs/nonrecursive-glob-match@1.0.2': {} + '@langchain/core@1.3.0': dependencies: '@cfworker/json-schema': 4.1.1 @@ -2498,7 +2505,7 @@ snapshots: async-sema: 3.1.1 bindings: 1.5.0 estree-walker: 2.0.2 - glob: 13.0.6 + glob: 14.0.2 graceful-fs: 4.2.11 node-gyp-build: 4.8.4 picomatch: 4.0.7 @@ -2774,11 +2781,11 @@ snapshots: dependencies: is-glob: 4.0.3 - glob@13.0.6: + glob@14.0.2: dependencies: - minimatch: 10.2.6 + minimatch: 11.0.2 minipass: 7.1.3 - path-scurry: 2.0.2 + path-scurry: 3.0.0 globby@16.2.4: dependencies: @@ -2872,8 +2879,9 @@ snapshots: mimic-function@5.0.1: {} - minimatch@10.2.6: + minimatch@11.0.2: dependencies: + '@isaacs/nonrecursive-glob-match': 1.0.2 brace-expansion: 2.1.7 minipass@7.1.3: {} @@ -2940,7 +2948,7 @@ snapshots: find-up-simple: 1.0.1 load-json-file: 7.0.1 - path-scurry@2.0.2: + path-scurry@3.0.0: dependencies: lru-cache: 11.5.3 minipass: 7.1.3 diff --git a/knowledge/operations/dependencies.md b/knowledge/operations/dependencies.md index 5521389c2..d63cd03b6 100644 --- a/knowledge/operations/dependencies.md +++ b/knowledge/operations/dependencies.md @@ -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