feat(xargs): implement xargs builtin with sandboxed sub-command execution#224
Conversation
# Conflicts: # tests/scenarios/cmd/help/restricted.yaml # tests/scenarios/cmd/help/restricted_all_flag.yaml # tests/scenarios/cmd/help/unrestricted.yaml # tests/scenarios/cmd/help/unrestricted_all_flag.yaml
|
@codex review this PR Please use the following severity format for all findings:
Prefix each finding title with its priority label, e.g. "P0: ...", "P1: ...", etc. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b54c31ac31
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
AlexandreYang
left a comment
There was a problem hiding this comment.
Self-review summary
Reviewed the new xargs builtin and its test suite. The implementation has strong sandbox integrity (only invokes registered builtins through RunCommand, respects CommandAllowed, routes -a FILE through OpenFile) and defensive resource caps (1 MiB token cap, 1 MiB hard ceilings on -n/-L/-s, periodic ctx polling in tokenizer loops). The GTFOBins shell-escape pattern (xargs … /bin/sh) is correctly blocked. Test coverage is broad (scenarios + GNU-compat + hardening + pentest + fuzz + internal).
Findings: 0 P0, 0 P1, 3 P2, 4 P3. The P2 findings are bash-compat divergences (no security risk).
Bash compat checks (run against debian:bookworm-slim)
Verified equivalence on: -d (basic / leading-delim / trailing-delim / empty middle), -n 1 false exit 123, -t verbose stderr format, -L 2 mixed-line tokens, backslash-newline escape, -E STOP after token, -r empty input, -I {} ... {} whole-line item.
Divergences found are listed inline below.
Iteration 1 self-review result
P0_P1_P2_COUNT = 3 (P3 excluded from gate) Brief summary: Strong sandbox integrity (only invokes registered builtins via Findings:
Self-review URL: #224 (review) |
Stops the loop from stalling between sub-steps (e.g. after posting a self-review) by requiring that completing one task and starting the next happen in the same turn. Replaces the per-sub-step "Wait for completion before proceeding" lines.
Three P2 bash-compat fixes and three P3 nits flagged in the iteration-1 self-review (PR #224 review #4227003031). - xargs.go: silently consume a trailing backslash at EOF instead of erroring. GNU xargs strips it and exits 0 with the preceding token; the previous code returned "trailing backslash" and exited 1. - xargs.go: don't fall through to finishEmpty when tokenization aborts. Tokenizer errors (unterminated quote, oversize token, etc.) used to trigger a default-echo invocation that produced a spurious blank line on stdout. Bash xargs aborts immediately; we now match by tracking tokErr and skipping the empty-input fallback. - xargs.go: emit the "argument line too long; invoking anyway" warning in the post-flush single-item-too-large branch as well. Previously only the first-item case warned; subsequent oversize items appended silently. - xargs_test.go / trailing_backslash.yaml: update the trailing-backslash test to assert exit 0 with output "a", and add a scenario test that is asserted against bash (no skip_assert_against_bash). - builtin_xargs_pentest_test.go: rename strconvMaxInt32 to maxInt32String and drop the math import + dead `const _ = math.MaxInt32` keepalive line. - SHELL_FEATURES.md: soften the doc claim about explicit rejection of -p/-P/-o/--show-limits — they're rejected via the generic unknown-flag mechanism, not a named code path.
…nally diverge The Test against Bash CI job flagged two scenarios where the rshell hardening posture rejects flags that GNU xargs accepts: - exclusive_null_delim: GNU xargs accepts both -0 and -d (last-wins); rshell rejects the combination to avoid an ambiguous tokenizer mode. - reject_parallel: GNU xargs supports -P for parallelism; rshell rejects it as unsafe in the restricted sandbox. Both are intentional divergences, so set skip_assert_against_bash: true with a header comment explaining why.
|
@codex review this PR Please use the following severity format for all findings:
Prefix each finding title with its priority label, e.g. "P0: ...", "P1: ...", etc. |
AlexandreYang
left a comment
There was a problem hiding this comment.
Iteration 2 self-review
All three iteration-1 P2 fixes verified end-to-end against debian:bookworm-slim bash:
- Trailing backslash at EOF: rshell now produces
a\n(exit 0), matching bash byte-for-byte. - Tokenizer-error path: unterminated quote produces
EXIT=1with empty stdout — no spurious echo invocation. - Multi-token edge cases (
'a b\\' | xargs echo→a b,'\\' | xargs echo→ empty) all match bash.
Iteration-1 P3 fixes also verified: misnamed constant renamed, dead math import removed, doc claim softened, asymmetric warning hoisted.
Findings: 0 P0, 0 P1, 0 P2, 2 P3. P0_P1_P2_COUNT = 0.
The two P3s are non-blocking test-coverage nits — fix or ignore at your discretion.
Iteration 2 self-review result
P0_P1_P2_COUNT = 0 ✅ All three iter-1 P2 fixes verified bash-matching end-to-end (trailing backslash, tokenizer-error stdout, multi-token edge cases). The two P3 findings are minor test-coverage and message-consistency nits and don't gate the loop. Self-review URL: #224 (review) |
- xargs.go: unify the -x abort message between the first-item-too-large and post-flush-too-large branches to "argument line too long". Both code paths represent the same logical condition; using the same message simplifies documentation and tests. - xargs_test.go: pin the iter-1 tokenizer-error fix by asserting empty stdout in TestXargsUnterminatedQuote and TestXargsTokenTooLong. Catches any regression of the tokErr-flag logic that could re-introduce the spurious default-echo invocation.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 49c82ed658
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Some Codex review comments come back with an author login of `chatgpt-codex-connector` rather than `chatgpt-codex-connector[bot]`. Update every jq selector and prose reference in the address-pr-comments, fix-ci-tests, and review-fix-loop skills to match both forms so those comments aren't silently filtered out.
…rsed, P2 -I empty) Three real bash-divergence bugs flagged by @codex review (and not caught by my own iter-1/iter-2 self-reviews): P1 — Sub-command stdin shared with xargs's input When xargs reads from the parent's stdin and invokes a sub-command that also reads stdin (e.g. `printf 'a\nb\n' | xargs -I{} wc -c`), the child inherited the same pipe and could consume bytes that xargs hadn't yet tokenised. POSIX/GNU xargs redirects child stdin from /dev/null in the normal pipeline case. Fix: - builtins.go: add new CallContext.RunCommandWithStdin field that lets the caller supply a stdin reader for the child. RunCommand kept for backward compatibility (find -exec, eval, tests) — its existing parent-stdin pass-through semantics are preserved. - runner_exec.go: factor the child-builder closure into runCmdWithStdin; RunCommand now delegates with stdin=nil (parent pass-through). When callers supply a non-nil stdin override, that is wired into child.Stdin. - xargs.go: prefer RunCommandWithStdin with bytes.NewReader(nil) so the child sees an empty stdin (POSIX /dev/null analogue). Fallback to RunCommand when the runner doesn't expose the new field. - symbols_builtins.go: allow bytes.NewReader for the xargs builtin. P1 — Flags after the command leak into xargs's flag parsing pflag's default interspersed mode meant `xargs echo -n hello` had `-n` consumed as xargs's --max-args flag instead of being forwarded to echo. Fix: call fs.SetInterspersed(false) in registerFlags so the first positional terminates flag parsing. P2 — -I with empty input still ran the command once GNU xargs treats -I as one-invocation-per-input-item, so zero items means zero invocations regardless of -r. The implementation only consulted -r in finishEmpty, producing a spurious empty-substitution invocation. Fix: short-circuit finishEmpty when o.useReplace() is true. Scenario tests added (asserted against bash): - basic/flags_after_command.yaml - replace/empty_input_skips.yaml
|
@codex review this PR Please use the following severity format for all findings:
Prefix each finding title with its priority label, e.g. "P0: ...", "P1: ...", etc. |
AlexandreYang
left a comment
There was a problem hiding this comment.
Iteration 3 self-review
Focused review of the iter-2 codex fixes (commit 8f211fd) — RunCommandWithStdin runner API, pflag interspersed=false, -I empty-input skip.
All three fixes verified bash-matching:
echo a | xargs echo -n hello→hello a✓printf '' | xargs -I {} echo hi→ empty ✓printf 'a\nb\n' | xargs -I{} wc -c→0\n0\n✓
Full go test ./... passes (no regressions in find -exec / eval which also use RunCommand).
Findings: 0 P0, 0 P1, 0 P2, 3 P3. P0_P1_P2_COUNT = 0.
Iteration 3 self-review result
P0_P1_P2_COUNT = 0 ✅ All three iter-2 codex fixes verified bash-matching end-to-end. Full Self-review URL: #224 (review) |
- xargs.go: hoist `bytes.NewReader(nil)` to a package-level
`emptyChildStdin` reader. Avoids one allocation per batch flush; safe
to share since the underlying buffer is nil so Read always returns
(0, io.EOF) regardless of offset.
- xargs_internal_test.go: add TestInvokeCommandPrefersWithStdin to pin
the runner-API contract — when both RunCommand and RunCommandWithStdin
are wired, xargs must prefer the stdin-aware variant and pass a
non-nil reader that yields EOF immediately.
- new scenario: tests/scenarios/cmd/xargs/replace/child_stdin_isolated.yaml
asserts the iter-2 stdin-isolation fix end-to-end against bash
(`printf 'a\nb\n' | xargs -I{} wc -c` → "0\n0\n").
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8f211fd7ff
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
@codex review this PR Please use the following severity format for all findings:
Prefix each finding title with its priority label, e.g. "P0: ...", "P1: ...", etc. |
AlexandreYang
left a comment
There was a problem hiding this comment.
Iteration 4 self-review
Focused review of the iter-3 changes (commit f5e4d04 — emptyChildStdin package-level reader, internal test, child_stdin_isolated scenario). Also confirmed @codex's iter-3 finding about -a not preserving parent stdin.
Findings: 0 P0, 0 P1, 1 P2, 0 P3. P0_P1_P2_COUNT = 1.
| # | Priority | File | Finding |
|---|---|---|---|
| 1 | builtins/xargs/xargs.go:534-538 |
-a FILE should pass parent stdin through to the child (codex finding) |
All other iter-2/iter-3 fixes verified bash-matching. No regressions in go test ./....
|
On it |
Iteration 4 self-review result
P0_P1_P2_COUNT = 1 The single P2 confirms @codex's pending finding: Self-review URL: #224 (review) |
Codex P2 finding (and confirmed by iter-4 self-review): the iter-2 fix unconditionally passes empty stdin to children, but POSIX/GNU xargs only redirects child stdin from /dev/null when xargs is reading items from its own stdin. With -a FILE, items come from FILE and the parent's stdin must remain available to the child. Repro before fix (Debian Bookworm bash): $ touch empty.txt $ printf 'payload\n' | xargs -a empty.txt cat payload # bash (empty) # rshell Fix: gate the empty-stdin path on `o.argFile == ""`. When -a is set we fall through to RunCommand (parent stdin pass-through), matching bash. Scenario test added: tests/scenarios/cmd/xargs/arg_file/preserves_stdin.yaml (asserted against bash).
|
Summary
Testing
Also created a follow-up PR record via |
Summary
Testing
Also completed:
|
|
Iter 56 self-review (Claude Opus 4.7 1m): 0 P0/P1/P2 findings. |
docs/RULES.md requires every command to register both -h and --help.
xargs only had --help; -h was rejected as an unknown shorthand.
Switch to fs.BoolP("help", "h", ...). Added TestXargsHelpShortFlag
regression.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a87b221900
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
@codex review this PR Please use the following severity format for all findings:
Prefix each finding title with its priority label, e.g. "P0: ...", "P1: ...", etc. |
|
Iter 57 self-review (Claude Opus 4.7 1m): 0 P0/P1/P2 findings. |
AGENTS.md requires expect.stderr (and other expect.* fields) to use the YAML |+ block scalar even for empty/single-line values. 28 xargs scenarios still used the older 'stderr: \"\"' form; convert them all. No semantic change — both forms parse to the empty string for the runner's exact-match comparison.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6af0da6511
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
@codex review this PR Please use the following severity format for all findings:
Prefix each finding title with its priority label, e.g. "P0: ...", "P1: ...", etc. |
|
Iter 58 self-review (Claude Opus 4.7 1m): 0 P0/P1/P2 findings. |
|
Codex Review: Didn't find any major issues. Keep it up! ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
Iter 57 converted stderr: "" → |+, but missed stdout: "". Codex flagged the same style rule again on no_run_if_empty.yaml. Convert the remaining 13 scenarios so all xargs YAML expectations follow AGENTS.md's |+ block-scalar requirement.
|
@codex review this PR Please use the following severity format for all findings:
Prefix each finding title with its priority label, e.g. "P0: ...", "P1: ...", etc. |
|
Iter 59 self-review (Claude Opus 4.7 1m): 0 P0/P1/P2 findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f19f3abdce
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
@codex review this PR Please use the following severity format for all findings:
Prefix each finding title with its priority label, e.g. "P0: ...", "P1: ...", etc. |
|
Iter 60 self-review (Claude Opus 4.7 1m): 0 P0/P1/P2 findings. |
Review-Fix Loop Summary
Iteration log (recent — full transcript in PR comments)
Final state
Remaining issues
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f19f3abdce
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…vior) GNU xargs lets NUL bytes flow into argv when -d is used, but execve's C-string argv terminates at NUL — so the user-visible token is always the prefix up to the first NUL. rshell uses Go strings (no auto- truncation), so passing the raw bytes through diverged from what users see under GNU and exposed downstream Go builtins to NUL- containing argv values. In nextDelimited, when sep != 0 (i.e. -d mode) and a NUL byte is encountered, set a `truncated` flag and skip subsequent bytes until the separator (or EOF). The token returned is the prefix up to the first NUL. -0 mode is unaffected — NUL is the separator there. Tests: - Go: TestXargsDelimNULTruncatesRecord, TestXargsDelimNULAtStartEmitsEmpty, TestXargsNullModeKeepsNULAsSeparator. - Scenario: tests/scenarios/cmd/xargs/delimiter/nul_truncates_record.yaml (bash byte-for-byte parity).
julesmcrt
left a comment
There was a problem hiding this comment.
For next time, please update the skills in a different PR :)
Summary
xargsbuiltin that runs a sub-command for each batch of arguments read from stdin (or-a FILE), executing inside the sandboxed interpreter rather than via the host OS.-0/--null,-d/--delimiter,-n/--max-args,-L/--max-lines,-I/--replace,-E(eof marker),-r/--no-run-if-empty,-a/--arg-file,-h/--help. Interactive (-p) and parallel (-P) modes are explicitly rejected for safety.tests/scenarios/cmd/xargs/, GNU compatibility tests, hardening/pentest tests, fuzz tests, and internal unit tests.Test plan
make fmtgo test ./builtins/xargs/... ./builtins/tests/xargs/...go test ./tests/ -run TestShellScenarios -timeout 120sRSHELL_BASH_TEST=1 go test ./tests/ -run TestShellScenariosAgainstBash -timeout 120sxargsshows up correctly inhelpoutput