Skip to content

Commit fc53a12

Browse files
authored
feat(yq): add jq-backed YAML processor (#2266)
## What changed Replaces the bespoke dotted-path `yaml` helper with a feature-gated `yq` builtin backed by the existing jq/jaq evaluator. Agents can process YAML or JSON from stdin/files with identity, nested selection, iteration, `select`, `map`, assignment, multi-document streams, YAML/JSON conversion, raw/compact/exit-status/slurp/null-input modes, combined short flags, and atomic in-place updates. Adds parser/document/depth/output controls, deterministic bounded diagnostics, no-leak property coverage, generated builtin metadata, user docs, limitations, and threat-model entries. TOML/CSV/XML and mikefarah/yq node/style operators remain explicit unsupported boundaries. ## Why The old `yaml` command implemented a narrow second query language that did not match the jq-style yq surface commonly generated by agents. Sharing jq/jaq avoids duplicate evaluator semantics and inherits its work, deadline, and output controls. ## Before / After Before: ```console $ printf "items:\n - type: fruit\n name: apple\n" | yq ".items[] | select(.type == \"fruit\") | .name" bash: yq: command not found ``` After: ```console $ printf "%s\n" "---" "type: fruit" "name: apple" "---" "type: veg" "name: kale" "---" "type: fruit" "name: pear" | yq -s -o=json -I=0 "[.[] | select(.type == \"fruit\") | .name]" ["apple","pear"] ``` Proof: 14/14 portable yq specs, 9 yq integration tests, arbitrary-YAML no-leak property, conditional mikefarah/yq differential, WASM feature check, real-`rg` regression, and post-rebase `just pre-pr`. ## Risk - Medium - Removes the nonstandard `yaml` helper and makes `yq` available with the `jq` feature. YAML mapping keys sort deterministically at the JSON-value boundary; source order, comments, styles, and anchors are not preserved. Custom tags and non-string keys fail closed. In-place writes depend on VFS sibling rename semantics and are covered for success, parse/filter/exit-status failure, stdout suppression, and mode preservation. ## Checklist - [x] Tests added or updated - [x] Backward compatibility considered
1 parent 275bf2c commit fc53a12

26 files changed

Lines changed: 1114 additions & 862 deletions

File tree

Cargo.lock

Lines changed: 20 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ anyhow = "1"
3232
# Serialization
3333
serde = { version = "1", features = ["derive"] }
3434
serde_json = "1"
35+
serde_yaml_ng = "0.10"
3536

3637
# JSON processing (jq) - verified embeddable
3738
jaq-core = "3.0"

crates/bashkit/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ anyhow = { workspace = true }
3030
# Serialization
3131
serde = { workspace = true }
3232
serde_json = { workspace = true }
33+
serde_yaml_ng = { workspace = true, optional = true }
3334

3435
# Regex
3536
regex = { workspace = true }
@@ -123,7 +124,7 @@ default = ["bash_tool"]
123124
bash_tool = ["dep:tower", "dep:futures-core"]
124125
# Enable jq builtin via embedded jaq interpreter
125126
# Usage: cargo build --features jq
126-
jq = ["dep:jaq-core", "dep:jaq-std", "dep:jaq-json"]
127+
jq = ["dep:jaq-core", "dep:jaq-std", "dep:jaq-json", "dep:serde_yaml_ng"]
127128
http_client = ["reqwest", "rustls"]
128129
# Enable Ed25519 request signing per RFC 9421 / web-bot-auth profile
129130
bot-auth = ["http_client", "dep:ed25519-dalek", "dep:rand", "dep:zeroize"]

crates/bashkit/docs/threat-model.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ through configurable limits.
9999
| Compound assign overflow (TM-DOS-043) | `((x+=1))` with x=i64::MAX | `wrapping_*` ops | **MITIGATED** |
100100
| Lexer stack overflow (TM-DOS-044) | ~50 nested `$()` in quotes | Depth tracking | **MITIGATED** |
101101
| parse_word_string limits (TM-DOS-050) | Parameter expansion ignores limits | Propagate limits | **MITIGATED** |
102-
| YAML parser recursion (TM-DOS-051) | Deeply nested YAML stack overflow | Add depth limit | **MITIGATED** |
102+
| Removed YAML helper parser (TM-DOS-051) | Former custom parser recursed over indentation | Parser deleted; yq uses TM-DOS-101 controls | **REMOVED** |
103103
| Template engine recursion (TM-DOS-052) | Nested `{{#if}}`/`{{#each}}` overflow | Add depth limit | **MITIGATED** |
104104
| Template output explosion (TM-DOS-053) | `{{#each}}` on large array | Bounded by `max_file_size` | MITIGATED |
105105
| glob ExtGlob blowup (TM-DOS-054) | `glob --files "+(a\|aa)"` | Same as TM-DOS-031 | **MITIGATED** |
@@ -158,6 +158,7 @@ let bash = Bash::builder()
158158
| Suspended host-call retention (TM-DOS-098) | Script repeats event-backed calls or host never resumes one | Capacity-one channel, normal execution limits, and handle-owned session released on drop | MITIGATED |
159159
| `time` report amplification (TM-DOS-099) | Attacker-controlled `-f` format expands repeatedly or targets the VFS with `-o` | Incremental rendering is capped by the stderr limit before emission or file replacement | MITIGATED |
160160
| jq control normalization amplification (TM-DOS-100) | Literal controls expand sixfold as `\u00XX` | Charge single-pass work and lease live bytes before allocation growth | MITIGATED |
161+
| yq structured-data amplification (TM-DOS-101) | Deep/multi-document YAML or JSON, runaway filters, expanded output | Parser depth and 4096-document caps, aggregate budgets, shared jaq work/deadline/output limits, final render cap | MITIGATED |
161162

162163
### Sandbox Escape (TM-ESC-*)
163164

@@ -846,6 +847,7 @@ read-only by default and gated by an allowlist.
846847
| Permissive RealFs mount (TM-FS-013) | `mount_real_readonly_at("/", …)` exposes the whole host | Allowlist-first: broad roots (`/`, `/etc`, `/root`, `/home`, …) and any path component matching `.ssh`, `.aws`, `.kube`, `.docker`, `.gnupg`, `.gcloud` are refused unless explicitly allowlisted | MITIGATED |
847848
| Partial filesystem mutation (TM-FS-014) | Failed write/copy or cross-mount move leaves corruption, duplication, or retained quota | Failure-atomic `FileSystem` contract; RealFs sibling staging; MountableFs destination rollback; NamespaceFs cross-device rejection; shared conformance + failpoint tests | MITIGATED |
848849
| Partial tar extraction (TM-FS-015) | A late unsafe or malformed entry leaves earlier files behind | Validate the complete archive and file limits before the first VFS mutation | MITIGATED |
850+
| yq in-place partial update (TM-FS-016) | A failed transform or write truncates the source file | Evaluate and serialize before writing; random sibling temporary file, mode preservation, and rename-on-success | MITIGATED |
849851

850852
### Unicode Security (TM-UNI-*)
851853

crates/bashkit/docs/yq.md

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# yq builtin
2+
3+
Bashkit ships a `yq` structured-data processor for the command shape agents
4+
commonly use with mikefarah/yq. It parses YAML or JSON, evaluates the existing
5+
`jq`/jaq expression engine, then emits YAML or JSON. Bashkit deliberately does
6+
not maintain a second YAML-specific query language.
7+
8+
Enable the Cargo `jq` feature to register both `jq` and `yq`.
9+
10+
## Examples
11+
12+
```bash
13+
yq '.server.port' config.yml
14+
yq '.items[] | select(.enabled) | .name' config.yml
15+
yq '.values | map(. * 2)' config.yml
16+
yq -o=json -I=0 '.' config.yml
17+
yq -p=json -o=yaml '.' data.json
18+
yq -i '.server.port = 8080' config.yml
19+
```
20+
21+
With no expression, `.` is used. Input comes from the listed VFS files or
22+
stdin. YAML streams containing `---` are processed one document at a time;
23+
`-s` presents all input documents to the filter as one array. The optional
24+
`e` / `eval` subcommand alias is accepted for common generated invocations.
25+
26+
## Flags
27+
28+
| Flag | Behaviour |
29+
|------|-----------|
30+
| `-p`, `--input-format` | `auto`, `yaml`, or `json` |
31+
| `-o`, `--output-format` | `yaml` or `json` |
32+
| `-r`, `--raw-output` | Unwrap string results |
33+
| `-c`, `--compact-output` | Compact JSON output |
34+
| `-e`, `--exit-status` | Nonzero for no output, `null`, or `false` |
35+
| `-s`, `--slurp` | Read all documents into an array |
36+
| `-n`, `--null-input` | Evaluate once with `null` input |
37+
| `-i`, `--inplace` | Atomically replace exactly one input file |
38+
| `-I`, `--indent` | Set JSON indentation; `0` is compact |
39+
| `-N`, `--no-doc` | Omit separators between YAML results |
40+
| `--expression` | Force an otherwise ambiguous argument to be the expression |
41+
42+
Short boolean flags combine (`-rce`, `-sn`). Attached value forms such as
43+
`-o=json`, `-p=json`, and `-I=0` are accepted.
44+
45+
In-place evaluation and serialization finish before a sibling temporary file
46+
is written and renamed over the source. A parse, filter, output-limit, write,
47+
or rename failure leaves the source unchanged.
48+
49+
## Compatibility boundary
50+
51+
The expression language is jq, not mikefarah/yq's node/style language. Common
52+
selection, iteration, `select`, `map`, construction, reduction, and assignment
53+
filters work. mikefarah/yq-only operators for comments, styles, anchors, tags,
54+
file metadata, and cross-file evaluation are not implemented.
55+
56+
YAML custom tags and non-string mapping keys are rejected rather than silently
57+
losing information. Mapping keys are sorted deterministically at the JSON-value
58+
boundary. Comments, scalar style, and anchors are not retained after
59+
conversion. The parser follows YAML 1.1. TOML, CSV, and XML conversion are not
60+
part of this builtin; Bashkit's separate `tomlq` and `csv` helpers remain
61+
available for their existing narrow command surfaces.
62+
63+
## See also
64+
65+
- [`jq_guide`](crate::jq_guide) — the shared expression engine and its jq compatibility notes.
66+
- [`threat_model`](crate::threat_model) — structured-input resource and information-disclosure controls.

crates/bashkit/examples/dump_builtins.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ use std::collections::BTreeMap;
1717
/// Families registered by compile feature alone (present in a default-built
1818
/// `Bash` whenever the feature is on).
1919
const CFG_REGISTERED: &[(&str, &[&str])] = &[
20-
("jq", &["jq"]),
20+
("jq", &["jq", "yq"]),
2121
("git", &["git"]),
2222
("ssh", &["ssh", "scp", "sftp"]),
2323
];
@@ -63,16 +63,16 @@ fn main() {
6363
.iter()
6464
.map(|name| {
6565
serde_json::json!({
66-
"name": name,
6766
"feature": feature_of.get(name),
67+
"name": name,
6868
})
6969
})
7070
.collect();
7171

7272
let doc = serde_json::json!({
7373
"_generated": "just regen-builtins — do not edit by hand",
74-
"count": builtins.len(),
7574
"builtins": builtins,
75+
"count": builtins.len(),
7676
});
7777
println!("{}", serde_json::to_string_pretty(&doc).expect("serialize"));
7878
}

crates/bashkit/src/builtins/limits.rs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -83,9 +83,6 @@ pub(crate) const TEMPLATE_MAX_DEPTH: usize = 100;
8383
/// timeout: max timeout duration in seconds (5 minutes).
8484
pub(crate) const TIMEOUT_MAX_SECONDS: u64 = 300;
8585

86-
/// yaml: max nesting depth.
87-
pub(crate) const YAML_MAX_DEPTH: usize = 100;
88-
8986
/// yes: max lines and total output bytes per invocation.
9087
pub(crate) const YES_MAX_LINES: usize = 10_000;
9188
pub(crate) const YES_MAX_OUTPUT_BYTES: usize = 1_048_576;

crates/bashkit/src/builtins/mod.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -108,8 +108,9 @@ mod vars;
108108
mod verify;
109109
mod wait;
110110
mod wc;
111-
mod yaml;
112111
mod yes;
112+
#[cfg(feature = "jq")]
113+
mod yq;
113114
mod zip_cmd;
114115

115116
mod helpers;
@@ -218,8 +219,9 @@ pub use vars::{Eval, Local, Readonly, Set, Shift, Shopt, Times, Unset};
218219
pub use verify::Verify;
219220
pub use wait::Wait;
220221
pub use wc::Wc;
221-
pub use yaml::Yaml;
222222
pub use yes::Yes;
223+
#[cfg(feature = "jq")]
224+
pub use yq::Yq;
223225
pub use zip_cmd::{Unzip, Zip};
224226

225227
#[cfg(feature = "git")]
@@ -1596,7 +1598,7 @@ mod tests {
15961598
"tee",
15971599
"csv",
15981600
"json",
1599-
"yaml",
1601+
"yq",
16001602
"tomlq",
16011603
"jq",
16021604
"semver",

0 commit comments

Comments
 (0)