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
28 changes: 14 additions & 14 deletions Cargo.lock

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

28 changes: 28 additions & 0 deletions crates/bashkit/src/builtins/jq/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2507,3 +2507,31 @@ async fn decoding_builtins_follow_jq() {
\"date \\\"2026-01-15\\\" does not match format \\\"%Y-%m-%dT%H:%M:%SZ\\\"\"\n1768478400\n"
);
}

/// A filter whose trailing comment is not followed by a newline, which had
/// no coverage: every other comment case here is followed by more filter.
///
/// jaq-core 3.1.2 fixes a lexer panic on exactly this shape (the comment
/// scanner replaced the remaining input with a `""` literal, breaking the
/// invariant that the replacement is a suffix of the previous input). That
/// panic does not reproduce through this builtin on 3.1.1 -- checked across
/// these inputs plus a dozen malformed ones, all of which returned the same
/// output on both versions -- so this pins the shape rather than guarding a
/// regression: the prepended compat prefix and the `loc`/`altpat` rewrites
/// put the filter at the end of the lexer input, where the fix applies.
#[tokio::test]
async fn filter_ending_in_a_comment_is_accepted() {
assert_eq!(run_jq(".a # trailing", r#"{"a":1}"#).await.unwrap(), "1\n");
// The comment is the whole filter, and the input ends mid-comment.
assert_eq!(run_jq(". # c", r#"1"#).await.unwrap(), "1\n");
// A comment ending in a carriage return, and one ending in a backslash
// (the odd-backslash line-continuation branch the fix touches).
assert_eq!(
run_jq(".a # trailing\r", r#"{"a":1}"#).await.unwrap(),
"1\n"
);
assert_eq!(
run_jq(".a # trailing\\", r#"{"a":1}"#).await.unwrap(),
"1\n"
);
}
6 changes: 6 additions & 0 deletions knowledge/log.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Bashkit Knowledge Update Log

## 2026-10-27

* **Supply chain**: A first-party crate reaching crates.io silently breaks `cargo vet`. cargo-vet calls a workspace member first-party by provenance -- it was not fetched from crates.io -- so once the same name and version is also published, the two descriptions collide and the command refuses to run until `policy.<name>.audit-as-crates-io` says which to believe. `bashkit-scripted-tool` went public in 0.20.0 with no entry and nothing failed, because CI runs `cargo vet --locked`, which never reaches the collision check. The Audit job stayed green while the command was broken for every developer and for cargo-vet itself: refreshing `supply-chain/imports.lock` needs the non-locked run, so a bump that relies on publisher trust could not be made to pass at all. #2790's tokio 1.54.1 was exactly that bump -- it rides `[[trusted.tokio]]` (Alice Ryhl) plus a publisher fact, not an audit -- which is how a four-release-old gap surfaced. `scripts/tests/test_ci_supply_chain.py` now derives the published set from the `release-check` recipe and fails when any of those crates lacks a first-party policy entry; a missing one cannot be caught by the gate that matters, since `--locked` is the gate. See [Dependencies](operations/dependencies.md).
* **Operations**: The `rust-dependencies` group bump (tokio 1.53.3 -> 1.54.1, jaq-core 3.1.1 -> 3.1.2, jaq-std 3.0.3 -> 3.0.4, napi 3.14.2 -> 3.15.0, napi-derive 3.6.12 -> 3.6.13, plus napi-derive-backend and napi-sys behind them) failed Audit alone with seven unvetted crates -- the same rule as #2706/#2720 and the 2026-10-18 entry: a Dependabot group bump cannot go green on its own, because nothing in the bump records the audits the new versions need. Dependabot's lockfile also carried collateral its own description does not claim: six `windows-sys` 0.61.2 -> 0.52.0 re-points and `tempfile` moving to getrandom 0.4.3. Those are opportunistic re-resolution, not part of the five updates, and are left out -- the same call #2789 made.
* **Dependencies**: Two findings worth keeping from reviewing the deltas. jaq-std 3.0.4 rewrites `scan` to jq's semantics, and it is inert here: `builtins/jq/compat.rs` prepends its own `scan` to every filter and shadows the stdlib one. Keep that override rather than deferring to upstream -- upstream's new definition is `scan_(re; "g" + flags)`, which produces `"gg"` and a double-`g` compile error for a filter that already passes `g`, the exact case the compat definition was written to avoid. jaq-core 3.1.2 fixes a lexer panic on a comment that ends the input (the scanner substituted a `""` literal, breaking the invariant that the replacement is a suffix of the previous input); it does not reproduce through the jq builtin on 3.1.1, checked across eighteen filter shapes including malformed ones, all of which returned identical output on both versions. `filter_ending_in_a_comment_is_accepted` now pins the shape, which had no coverage. napi 3.15.0 is the delta that needed real reading: it adds a `napi_run_script` call, on a constant three-line source with no interpolation and only on the ReadableStream cancel path, and its new `class_identity` module checks the recorded Rust class before casting a `napi_unwrap` result, which matters here because `bashkit-js` selects `napi6` and so has no Node-API type tag to check instead. See [Vendored jaq-json](runtimes/jaq-json-vendor.md), [Node Package](runtimes/node-package.md).

## 2026-10-26

* Preserve binary [HTTP transport](security/http-transport.md) output in curl
Expand Down
15 changes: 15 additions & 0 deletions knowledge/operations/dependencies.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,21 @@ and failed with `feature-banned` after it, while the workspace still passed.
Cargo-deny's [feature restrictions](https://embarkstudios.github.io/cargo-deny/checks/bans/cfg.html#the-features-field-optional)
enforce this audit boundary in CI.

A workspace crate that is published to crates.io needs the same `policy.*`
entry from the other direction: `audit-as-crates-io = false`. cargo-vet calls a
member first-party by provenance -- it was not fetched from crates.io -- so once
the same name and version is also published, the two descriptions collide and
`cargo vet` refuses to run until an entry says which to believe. The gap is
quiet where it matters: CI runs `cargo vet --locked`, which never reaches that
check, so the Audit job stays green while the command is broken for every
developer and for refreshing `supply-chain/imports.lock` -- the non-locked run
is the only way to record publisher facts, which `[[trusted.*]]` crates such as
`tokio` need on every bump. `bashkit-scripted-tool` went public in 0.20.0 with
no entry and nothing failed until #2792 needed that refresh.
`scripts/tests/test_ci_supply_chain.py` derives the published set from the
`release-check` recipe and fails when one of those crates has no entry; a new
published crate must be added in both places.

`get-size2` is constrained to `=0.10.1` as a Python-only direct dependency,
so downstream resolution is protected even without this workspace's lockfile.
Both 0.10.2 and 0.10.3 implement `GetSize` for `compact_str` 0.10, whereas
Expand Down
50 changes: 50 additions & 0 deletions scripts/tests/test_ci_supply_chain.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,3 +166,53 @@ def test_fetched_secrets_are_masked_before_they_travel(self):
if travels is not None:
self.assertLess(masked, travels,
f'${variable} escapes the fetch step before it is masked')


class VetFirstPartyPolicyTests(unittest.TestCase):
"""Every crate this repo publishes needs a `policy.*` entry in cargo-vet.

cargo-vet decides first-party by provenance: a workspace member is
first-party because it was not fetched from crates.io. Once that same
name and version is also *on* crates.io, the two descriptions collide
and `cargo vet` refuses to run at all -- 'Some non-crates.io-fetched
packages match published crates.io versions' -- until a
`policy.<name>.audit-as-crates-io` entry says which one to believe.

The failure is quiet in the one place that matters. CI runs
`cargo vet --locked`, which never reaches the collision check, so the
Audit job stays green while the command is broken for every developer
and, worse, for `cargo vet` itself: refreshing `supply-chain/imports.lock`
(publisher facts for `[[trusted.*]]` crates, imported audits) needs the
non-locked run. A dependency bump that relies on publisher trust then
cannot be made to pass. bashkit-scripted-tool reached crates.io in
0.20.0 with no entry, and nothing failed until #2790 needed tokio's.

The published set is derived from the `release-check` recipe rather than
listed here: that recipe is what actually stages the packages.
"""

def published_crates(self):
recipe = (ROOT / 'justfile').read_text().split('\nrelease-check:\n', 1)[1]
recipe = recipe.split('\n# Create and push', 1)[0]
# Fold shell line continuations so the whole command is one line.
publish = re.search(r'cargo publish --dry-run[^\n]*',
re.sub(r'\\\n\s*', ' ', recipe))
self.assertIsNotNone(publish, 'release-check no longer dry-run publishes')
crates = re.findall(r'-p\s+([A-Za-z0-9_-]+)', publish.group(0))
self.assertIn('bashkit', crates)
return crates

def test_every_published_crate_has_a_first_party_vet_policy(self):
config = (ROOT / 'supply-chain/config.toml').read_text()
for crate in self.published_crates():
with self.subTest(crate=crate):
entry = re.search(
rf'^\[policy\.{re.escape(crate)}\]$(.*?)(?=^\[|\Z)',
config, re.M | re.S)
self.assertIsNotNone(
entry,
f'{crate} is published to crates.io but has no [policy.{crate}] '
'entry, so `cargo vet` cannot run once that version is live')
self.assertIn(
'audit-as-crates-io = false', entry.group(1),
f'[policy.{crate}] must mark the workspace copy first-party')
41 changes: 41 additions & 0 deletions supply-chain/audits.toml
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,12 @@ criteria = "safe-to-deploy"
delta = "3.1.0 -> 3.1.1"
notes = "Reviewed complete package delta: dependency feature metadata only; interpreter source unchanged."

[[audits.jaq-core]]
who = "Mykhailo Chalyi <mike@chaliy.name>"
criteria = "safe-to-deploy"
delta = "3.1.1 -> 3.1.2"
notes = 'Fixes a lexer panic: when a comment ended the input, the comment scanner replaced the remaining input with a "" literal, breaking the invariant that the replacement is a suffix of the previous input; it now takes the empty suffix at the end of the input, and the same change writes that invariant down on the field. Remaining delta is a new example, one test and a README note. unsafe count unchanged at 1; no new ambient access.'

[[audits.jaq-json]]
who = "Mykhailo Chalyi <mike@chaliy.name>"
criteria = "safe-to-deploy"
Expand All @@ -273,6 +279,12 @@ criteria = "safe-to-deploy"
delta = "3.0.2 -> 3.0.3"
notes = "Reviewed no_std float rounding with bounded IEEE754 bit operations; std keeps native floor/round/ceil. Corrected import feature gates; no unsafe or ambient changes."

[[audits.jaq-std]]
who = "Mykhailo Chalyi <mike@chaliy.name>"
criteria = "safe-to-deploy"
delta = "3.0.3 -> 3.0.4"
notes = "jq-level definitions only (src/defs.jq): scan becomes scan_ and gains jq's semantics, applying the g flag automatically and yielding captures rather than the whole match when the regex has capture groups, with a new scan(re; flags) wrapper. No Rust change, no unsafe, no ambient access."

[[audits.jiter]]
who = "Mykhailo Chalyi <mike@chaliy.name>"
criteria = "safe-to-deploy"
Expand Down Expand Up @@ -347,6 +359,17 @@ criteria = "safe-to-deploy"
delta = "3.14.0 -> 3.14.2"
notes = "Reviewed complete published package diff across 3.14.1 and 3.14.2: no new dependencies, no build scripts, no new host filesystem, network or process access. Four upstream fixes, all hardening. NativeBorrowScope now owns its storage through NonNull instead of aliasing a reference, removing an aliasing violation. serde_json::Value conversion is bounded at 128 levels of nesting, so deeply nested JSON from a caller can no longer recurse without limit - the change that matters most for this tree, where the binding converts untrusted input. The wasm cleanup is registered through the env cleanup hook rather than ad hoc. Object creation uses napi_define_properties on every Node version. Existing unsafe in the js_values conversions is refactored in place; no new unsafe blocks."

[[audits.napi]]
who = "Mykhailo Chalyi <mike@chaliy.name>"
criteria = "safe-to-deploy"
delta = "3.14.2 -> 3.15.0"
notes = """
Reviewed the complete published delta (24 files, ~2700 lines). No new ambient capability: no std::fs, std::net or std::env appears, std::process is unchanged at 8 occurrences, build.rs is byte-identical, and the only Cargo.toml change is the napi-sys 3.4.1 bump, so no new dependency.

New here, and the reason to look twice: napi_run_script compiles FOLLOW_LIKE_AWAIT to follow a cancel result exactly as await does. The source is a three-line async arrow held in a CStr constant with no interpolation, so there is no injection surface; it runs only on the ReadableStream cancel path, and where eval is blocked the future resolves to Err.

The rest is hardening. New class_identity.rs records the wrapped Rust class in the REFERENCE_MAP entry and checks it before casting a napi_unwrap result to *mut T on builds without Node-API type tags, which is every build bashkit-js makes since it selects napi6, turning a type-confusion cast into InvalidArg. TypedArray views now scale their byte slice by element size instead of treating the element count as a byte count, so a multi-byte view is no longer truncated. native_borrow gains deferred scopes with lease counting and hands roots to the owning env's custom-GC threadsafe function instead of calling napi_delete_reference off-thread; its two new unsafe impl Send/Sync carry raw handles that are only dereferenced on the owner JavaScript thread or through that tsfn. New unsafe is N-API handle calls plus three extern "C" promise-settle trampolines, and the three added raw-pointer sites are slice/Vec reconstruction over N-API buffer data."""

[[audits.napi-build]]
who = "Mykhailo Chalyi <mike@chaliy.name>"
criteria = "safe-to-deploy"
Expand Down Expand Up @@ -389,6 +412,12 @@ criteria = "safe-to-deploy"
delta = "3.6.9 -> 3.6.12"
notes = "Reviewed syn 3 parser migration: function arguments read their type field, new AST attributes initialized and non-exhaustive path fields ignored. No new unsafe or build-time execution."

[[audits.napi-derive]]
who = "Mykhailo Chalyi <mike@chaliy.name>"
criteria = "safe-to-deploy"
delta = "3.6.12 -> 3.6.13"
notes = "No source changes; the release only bumps the napi-derive-backend dependency to 6.1.6 in Cargo.toml."

[[audits.napi-derive-backend]]
who = "Mykhailo Chalyi <mykhailo.chalyi-contractor@procore.com>"
criteria = "safe-to-deploy"
Expand All @@ -407,6 +436,12 @@ criteria = "safe-to-deploy"
delta = "6.1.4 -> 6.1.5"
notes = "Reviewed syn 3 AST adaptation and generated enum property flags changed to writable/enumerable/configurable. No new unsafe or external execution."

[[audits.napi-derive-backend]]
who = "Mykhailo Chalyi <mike@chaliy.name>"
criteria = "safe-to-deploy"
delta = "6.1.5 -> 6.1.6"
notes = "Codegen only. Borrow scopes become deferred: a synchronous callback records its borrowed values and hands the scope to whichever async sink claims it during return-value conversion, with a guard that releases it on Err, throw or panic; the zero-argument fast path emits no defer because it builds no CallbackInfo. Class instance wraps replace a 0x1 wrapped pointer with a real one-byte allocation so zero-sized classes cannot share the dangling key napi's new class-identity check keys on. No new unsafe (74 occurrences unchanged), no ambient access."

[[audits.napi-sys]]
who = "Mykhailo Chalyi <mike@chaliy.name>"
criteria = "safe-to-deploy"
Expand All @@ -419,6 +454,12 @@ criteria = "safe-to-deploy"
delta = "3.3.2 -> 3.4.0"
notes = "Reviewed exact-target build cfg, atomic shared-memory refresh and worker-crash flag modules. Checked pointer/page bounds and target gating; native code unchanged outside tests."

[[audits.napi-sys]]
who = "Mykhailo Chalyi <mike@chaliy.name>"
criteria = "safe-to-deploy"
delta = "3.4.0 -> 3.4.1"
notes = "Doc-comment only: the wasi_heap_sync design-note reference moves from rolldown's internal docs to cli/docs/wasi-heap-sync-design.md. No code, FFI declaration or build-script change."

[[audits.num-bigint]]
who = "Mykhailo Chalyi <mike@chaliy.name>"
criteria = "safe-to-deploy"
Expand Down
4 changes: 4 additions & 0 deletions supply-chain/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ criteria = "safe-to-deploy"
audit-as-crates-io = false
criteria = "safe-to-deploy"

[policy.bashkit-scripted-tool]
audit-as-crates-io = false
criteria = "safe-to-deploy"

[policy.bashkit-wasm]
audit-as-crates-io = false
criteria = "safe-to-deploy"
Expand Down
4 changes: 2 additions & 2 deletions supply-chain/imports.lock
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,8 @@ user-login = "epage"
user-name = "Ed Page"

[[publisher.tokio]]
version = "1.53.3"
when = "2026-10-13"
version = "1.54.1"
when = "2026-10-21"
user-id = 6741
user-login = "Darksonn"
user-name = "Alice Ryhl"
Expand Down
Loading