Skip to content

feat(shell): pipelines + Spawner seam, increment 2 (#34)#39

Merged
hartsock merged 1 commit into
mainfrom
issue-34/pipelines
Jun 25, 2026
Merged

feat(shell): pipelines + Spawner seam, increment 2 (#34)#39
hartsock merged 1 commit into
mainfrom
issue-34/pipelines

Conversation

@hartsock

Copy link
Copy Markdown
Member

Increment 2 of #34 (ADR 0005 D3): the safe-subset engine now runs pipelines (a | b | c), and introduces the Spawner seam that establishes the fully-mocked testing strategy for the rest of Track A.

What

  • parse.rsclassify() returns a Pipeline (Vec<Vec<String>>). An unquoted single | splits stages; || and every other operator/construct stay refused (Unsupported/Dynamic). Empty stages → Malformed.
  • shell_tool.rs — injectable Spawner trait. Real OsSpawner wires stages with OS pipes, deadlock-free (a reader thread per stderr + the last stdout), exit = last stage (no pipefail), and kills already-spawned stages on a mid-pipeline spawn error. Atomic admission: every stage is exec-checked before any stage spawns.

Testing (fully mocked + deep)

  • Unit (no real processes): mock Spawner asserts the leash blocks before any spawn — out-of-scope exec, a refused construct, and an out-of-scope pipeline stage all leave the spawner uncalled; quoted | stays one stage; correct stages reach the spawner; timeout via a blocking mock.
  • Integration (tests/real_spawn.rs, real std::process): echo|cat passes data; pipeline exit = last stage (true|false→1, false|true→0); stderr/non-zero captured; out-of-scope rm denied and never spawns.

L3 note

The Spawner seam is also where L3 will plug in (Landlock on Linux, #35) and where it's simulated in tests — brush is not an L3 mechanism (it can't see a spawned binary's syscalls).

Test plan

just check green (fmt + clippy all-features & no-default-features + workspace tests). Part of #34.

🤖 Generated with Claude Code

WHAT: The safe-subset engine now supports pipelines (a | b | c). parse.rs classify() returns a Pipeline (Vec of stages) — an unquoted single | splits stages; || and every other operator stay refused. shell_tool.rs gains an injectable Spawner seam: the real OsSpawner wires stages with OS pipes (deadlock-free: per-pipe reader threads), captures last-stage stdout + all stderr, exit = last stage (no pipefail), and kills already-spawned stages on a mid-pipeline spawn failure. Atomic admission: EVERY stage's program is exec-checked before any stage spawns.

WHY: Pipelines are the highest-value engine increment (agents pipe constantly), and the Spawner seam establishes the fully-mocked testing strategy for all of Track A: unit tests inject a mock spawner and assert the leash blocks BEFORE any spawn (the spawner is never called on a refusal/denial — incl. atomic pipeline admission), with no real subprocesses. The real std::process path moves to tests/real_spawn.rs (integration), per the no-real-subprocess-in-unit-tests norm.

TEST: 28 unit (pure parser + mock-spawner) + 5 real-spawn integration. Keystones: quoted | stays a single stage; a dynamic stage downstream of a pipe is refused; an out-of-scope stage aborts the whole pipeline before any spawn; real echo|cat passes data; pipeline exit = last stage. just check green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Claude-Session: https://claude.ai/code/session_01HMGPEApE4XfwgMhgFbRn6c
@hartsock hartsock added the risk:low Low-risk change label Jun 25, 2026
@hartsock hartsock merged commit 39cdc0b into main Jun 25, 2026
1 check passed
@hartsock hartsock deleted the issue-34/pipelines branch June 25, 2026 00:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

risk:low Low-risk change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant