Skip to content

Commit dcba0b4

Browse files
feat(samples): CLI explainer/auditor + Blazor WASM Mermaid visualizer (#12)
Adds two runnable samples that demonstrate consuming the ShellSyntaxTree v0.1.0-alpha API. Both samples are IsPackable=false so they don't ship in the nupkg. samples/ShellSyntaxTree.Cli.Sample (net8.0 console, System.CommandLine 2.0.7): - `explain "<cmd>"` — pretty-prints the AST with [flag]/[path]/ [cwd-attr]/[dyn-skip]/[glob] markers per arg, redirect targets, and the IsSubshell/IsBashCWrapped flags. Unparseable inputs show the reason. - `audit "<cmd>"` — runs a tiny built-in policy (deny writes under /etc/, /usr/, /bin/, /sbin/, /lib/; warn on `curl | bash`; warn on dynamic content in path-arg slots; warn on IsUnparseable). Exits 0/1/2 by severity. - AuditPolicy.cs is one file so the README can point at it as a reference implementation. samples/ShellSyntaxTree.Web.Sample (net8.0 Blazor WebAssembly): - Single page: textarea on the left, mermaid SVG render on the right. - Five preset buttons covering compound commands, subshell isolation, bash -c recursion, dynamic-cwd attribution, and unparseable inputs. - Source/AST/Unparseable tabs alongside the rendered diagram. - 300ms debounced auto-render on input; Render button available too. - mermaid.js v11 loaded from jsDelivr CDN; JS interop via shellSyntaxTreeInterop.renderMermaid(). - MermaidRenderer.cs handles label escaping, subshell/bash -c subgraphs, dotted edges for redirects, and CSS classes for unparseable (red) and dynamic-skip (yellow) nodes. - ScriptSplitter.cs folds line continuations, skips blank/comment lines, preserves heredoc bodies so the parser sees them whole. README rewritten: - Elevator pitch + install line up top - Inline mermaid example (GitHub renders directly) - Use-case bullets covering AI agent gates, CI/CD audits, sandbox policy generation, pre-commit linters, audit-log analytics, and documentation tools - Quick-start code snippet (~25 LOC of consumer code) - Pointer to both samples with copy-pasteable `dotnet run` commands - Repository layout table for contributors - Locked public API surface from SPEC §2 Eight screenshots captured via Playwright validating the Blazor sample's core scenarios (empty state, build script, subshell, bash -c, unparseable, dynamic cwd, custom input, source tab). Renderer fixes after first Playwright pass: - Empty input now returns string.Empty so the host page's "No script yet" placeholder shows instead of a phantom "(no commands)" node. - `2>&1` and `N>&M` fd-dup redirect targets are filtered out (they're not files; basename-startswith-& check handles the resolver's cwd-joined form like `/work/&1`). - Apostrophes in node labels left unescaped (mermaid 11 renders &#39; as `&'` inside `["..."]` labels). - Node labels fall back to first non-flag arg when no path args are present, so verbs like `echo done` and `set -e` show their args instead of rendering as bare verb-only nodes. - Razor textarea placeholder was Razor-encoding && to &amp;&amp;. API surface unchanged; tests still 353/353. CPM additions: - System.CommandLine 2.0.7 - $(AspNetCoreVersion) = 8.0.26 for Microsoft.AspNetCore.Components.WebAssembly[.DevServer] No new CI/CD workflow. Samples runnable via `dotnet run` only.
1 parent 782afa5 commit dcba0b4

28 files changed

Lines changed: 1644 additions & 41 deletions

Directory.Packages.props

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
<!-- Test stack -->
55
<XunitVersion>2.9.2</XunitVersion>
66
<XunitRunnerVersion>2.8.2</XunitRunnerVersion>
7+
<!-- Sample stack -->
8+
<AspNetCoreVersion>8.0.26</AspNetCoreVersion>
79
</PropertyGroup>
810
<!-- Test dependencies -->
911
<ItemGroup>
@@ -16,4 +18,10 @@
1618
<ItemGroup>
1719
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="10.0.201" />
1820
</ItemGroup>
21+
<!-- Sample dependencies -->
22+
<ItemGroup>
23+
<PackageVersion Include="System.CommandLine" Version="2.0.7" />
24+
<PackageVersion Include="Microsoft.AspNetCore.Components.WebAssembly" Version="$(AspNetCoreVersion)" />
25+
<PackageVersion Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="$(AspNetCoreVersion)" />
26+
</ItemGroup>
1927
</Project>

README.md

Lines changed: 158 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,73 +1,165 @@
11
# ShellSyntaxTree
22

3-
A focused .NET library that parses shell command strings into a structured AST.
4-
Purpose-built for **security gate evaluators** — tools that inspect agent-emitted
5-
shell commands and decide whether to allow, prompt for, or deny execution.
3+
[![NuGet](https://img.shields.io/nuget/v/ShellSyntaxTree.svg)](https://www.nuget.org/packages/ShellSyntaxTree/)
64

7-
ShellSyntaxTree is **not** a shell interpreter. It does not execute, expand,
8-
or evaluate commands. It returns an AST (verb chain, args with path
9-
classification, redirects, compound operators, `cd`-in-compound propagation,
10-
`bash -c` recursion) that consumers walk to make policy decisions.
5+
A focused .NET library that parses bash command strings into a structured
6+
AST. Purpose-built for tools that need to **reason about shell commands
7+
without running them** — approval gates for LLM-emitted commands, CI/CD
8+
script auditors, sandbox policy generators, audit-log analytics.
119

12-
The original consumer is [Netclaw](https://github.com/netclaw-dev/netclaw)'s
13-
approval policy. Any tool that needs to reason about the shape of an
14-
agent-emitted shell command — without running it — is welcome to consume it.
10+
Hand-rolled, AOT-trim friendly, zero native dependencies. Multi-targets
11+
`netstandard2.0` and `net8.0`.
1512

16-
## Status
13+
```bash
14+
dotnet add package ShellSyntaxTree --version 0.1.0-alpha
15+
```
16+
17+
## What you get
18+
19+
For an input like `cd /repo && rm /etc/passwd`, ShellSyntaxTree produces:
20+
21+
```mermaid
22+
flowchart TD
23+
classDef bad fill:#fee,stroke:#b00,stroke-width:2px
24+
A[cd /repo<br/>📁 /repo] -- "&&" --> B[rm<br/>📁 /etc/passwd<br/>cwd: /repo]
25+
class B bad
26+
```
1727

18-
**v0.1 — pre-alpha.** Public API surface and behavior are specified in
19-
[`SPEC.md`](./SPEC.md). Implementation is in progress.
28+
A two-clause AST where the second clause's `Args` includes a synthetic
29+
`/repo` attribution arg (so consumers can see "this `rm` is implicitly
30+
operating in `/repo`") *and* `/etc/passwd` is resolved and marked
31+
`IsPath = true`. Hard-deny rules over `/etc/*` fire immediately; no
32+
substring matching, no shelling out, no false positives.
33+
34+
## Things you can do with it
35+
36+
- **Approval gates for AI agents** — given a command emitted by an LLM,
37+
decide ALLOW / PROMPT / DENY before invoking the shell.
38+
- **CI/CD pipeline audits** — scan shell steps in GitHub Actions /
39+
Jenkinsfile / Azure Pipelines for writes outside the workspace,
40+
`curl | bash` from non-allowlisted hosts, hardcoded credential
41+
echoes.
42+
- **Sandbox / container policy** — derive the minimum-viable volume
43+
mount set or AppArmor profile from a build script.
44+
- **Pre-commit linters** — flag dangerous patterns (`rm -rf /`,
45+
`chmod 777 /etc/*`) in shell scripts at commit time.
46+
- **Shell history / audit-log analytics** — ingest `~/.bash_history` or
47+
`auditd` records into structured form for SIEM-style insights.
48+
- **Documentation / explainers** — convert complex one-liners into
49+
readable structure for tutorials and runbooks.
50+
51+
The original consumer is
52+
[Netclaw](https://github.com/netclaw-dev/netclaw)'s approval policy;
53+
the library is built to be reusable beyond that.
54+
55+
## Quick start
2056

21-
## Why not `tree-sitter-bash`?
57+
```csharp
58+
using ShellSyntaxTree;
59+
60+
var parser = new BashParser();
61+
var parsed = parser.Parse("cd /repo && rm /etc/passwd");
62+
63+
if (parsed.IsUnparseable)
64+
{
65+
// Safe-fail: prompt the user, deny the command, etc.
66+
Console.WriteLine($"can't model: {parsed.UnparseableReason}");
67+
return;
68+
}
69+
70+
foreach (var clause in parsed.Clauses)
71+
{
72+
Console.WriteLine($"{clause.Operator} {clause.Verb.Joined}");
73+
74+
foreach (var arg in clause.Args.Where(a => a.IsPath))
75+
{
76+
var marker = arg.IsCwdAttribution ? "↳ cwd" : " path";
77+
Console.WriteLine($" {marker}: {arg.Resolved}");
78+
}
79+
80+
foreach (var redirect in clause.Redirects.Where(r => !r.IsDynamicSkip))
81+
{
82+
Console.WriteLine($" {redirect.Direction}: {redirect.Target}");
83+
}
84+
}
85+
```
86+
87+
Run that against the example input and you get:
2288

23-
Native dependencies, AOT trim concerns, and IDE-grade fidelity we don't need.
24-
We want unsupported constructs to mark `IsUnparseable = true` so consumers
25-
route to safe-fail. See [`SPEC.md` Appendix B](./SPEC.md) for the full
26-
trade-off analysis.
89+
```
90+
None cd
91+
path: /repo
92+
AndIf rm
93+
↳ cwd: /repo
94+
path: /etc/passwd
95+
```
2796

2897
## Public API surface (locked for v0.1)
2998

3099
```csharp
31100
namespace ShellSyntaxTree;
32101

33102
public interface IShellParser { ParsedCommand Parse(string command); }
34-
public sealed class BashParser : IShellParser { /* ... */ }
103+
public sealed class BashParser : IShellParser { /* */ }
35104
public sealed record BashParserOptions { /* HomeDirectory, WorkingDirectory */ }
36105

37-
public sealed record ParsedCommand { /* Source, Clauses, IsUnparseable, ... */ }
38-
public sealed record Clause { /* Operator, Verb, Args, Redirects, ... */ }
39-
public sealed record VerbChain { /* Tokens */ }
40-
public sealed record Arg { /* Raw, Resolved, Kind, IsPath, ... */ }
41-
public sealed record Redirect { /* Direction, Target */ }
106+
public sealed record ParsedCommand { /* Source, Clauses, IsUnparseable, */ }
107+
public sealed record Clause { /* Operator, Verb, Args, Redirects, */ }
108+
public sealed record VerbChain { /* Tokens, Joined */ }
109+
public sealed record Arg { /* Raw, Resolved, Kind, IsPath, IsCwdAttribution, IsFlag */ }
110+
public sealed record Redirect { /* Direction, Target, IsDynamicSkip */ }
42111

43112
public enum ArgKind { Literal, EnvVar, Glob, Tilde, DynamicSkip }
44113
public enum RedirectDirection { In, Out, Append, ErrOut, ErrAppend }
45114
public enum CompoundOperator { None, AndIf, OrIf, Sequence, Pipe }
46115
```
47116

48117
PowerShell and Windows `cmd` parsers are deferred to later versions; the
49-
`IShellParser` seam is already in place so consumers don't have to refactor
50-
when they ship.
118+
`IShellParser` seam is in place so consumers don't refactor when they
119+
ship.
120+
121+
Full behavioral contract: [`SPEC.md`](./SPEC.md).
51122

52-
## Multi-targeting
123+
## Samples
53124

54-
`netstandard2.0` for broad consumer compatibility, `net8.0` for modern
55-
runtimes. Tests target `net10.0`.
125+
Two runnable samples live under [`samples/`](./samples).
56126

57-
## Repository layout
127+
### `ShellSyntaxTree.Cli.Sample` — terminal explainer + audit policy
58128

129+
```bash
130+
dotnet run --project samples/ShellSyntaxTree.Cli.Sample -- explain "cd /repo && rm /etc/passwd"
131+
dotnet run --project samples/ShellSyntaxTree.Cli.Sample -- audit "cd /repo && rm /etc/passwd"
59132
```
60-
src/ShellSyntaxTree/ # library (TBD)
61-
tests/ShellSyntaxTree.Tests/ # xunit tests + corpus runner (TBD)
62-
tests/ShellSyntaxTree.Tests/Corpus/ # JSON test corpus (acceptance contract)
63-
SPEC.md # the implementation specification
64-
PROJECT_CONTEXT.md # what this is, who it serves
65-
TOOLING.md # available tooling and how to access it
66-
AGENTS.md / CLAUDE.md # agent operating constitution
67-
IMPLEMENTATION_PLAN.md # NOW / NEXT / LATER work tracker
133+
134+
`explain` pretty-prints the AST with `[flag]` / `[path]` / `[cwd-attr]` /
135+
`[dyn-skip]` / `[glob]` markers per arg. `audit` runs a small built-in
136+
policy ("deny writes in `/etc`, `/usr`, `/bin`, `/sbin`, `/lib`",
137+
"warn on `curl | bash`", "warn on dynamic args in path slots") and
138+
exits 0 / 1 / 2 by severity. See
139+
[`samples/ShellSyntaxTree.Cli.Sample/Commands/AuditPolicy.cs`](./samples/ShellSyntaxTree.Cli.Sample/Commands/AuditPolicy.cs)
140+
for the policy code — ~50 lines.
141+
142+
### `ShellSyntaxTree.Web.Sample` — Blazor WebAssembly Mermaid visualizer
143+
144+
Paste a bash script, watch the parsed AST render as a Mermaid flowchart
145+
in your browser. Everything runs client-side — pasted scripts never
146+
leave your machine. Useful for "what does this script actually do?"
147+
moments and for understanding how the library models constructs like
148+
subshells and `bash -c` recursion.
149+
150+
```bash
151+
dotnet run --project samples/ShellSyntaxTree.Web.Sample
152+
# → http://localhost:5239
68153
```
69154

70-
## Building
155+
![Build script preset](./assets/sample-web-build-script.png)
156+
157+
The visualizer ships preset scripts demonstrating compound commands,
158+
subshell isolation, `bash -c` recursion, dynamic-cwd attribution, and
159+
unparseable inputs (control-flow, function definitions). Each preset
160+
shows what the library produces in a single click.
161+
162+
## Building from source
71163

72164
```bash
73165
dotnet tool restore
@@ -76,9 +168,34 @@ dotnet test -c Release
76168
dotnet pack -c Release -o ./bin/nuget
77169
```
78170

79-
`global.json` pins the SDK version. Targeting requires .NET 10 SDK or later
80-
(`.slnx` solution format).
171+
`global.json` pins the SDK; you need .NET 10 SDK or later for the
172+
`.slnx` solution format.
173+
174+
## Versioning
175+
176+
- **v0.1.0-alpha** — first publishable cut. Bash-only.
177+
- **v0.1.x** — additive (more verb table entries, more corpus, bug
178+
fixes).
179+
- **v0.2.0** — first PowerShell parser.
180+
- **v1.0.0** — when an external consumer beyond Netclaw ships against
181+
it without finding API gaps.
81182

82183
## License
83184

84185
[Apache-2.0](./LICENSE). Copyright © 2026 Aaron Stannard.
186+
187+
---
188+
189+
**Repository layout** — for contributors and curious agents:
190+
191+
| Path | What |
192+
|---|---|
193+
| `src/ShellSyntaxTree/` | The library |
194+
| `tests/ShellSyntaxTree.Tests/` | xUnit unit tests + corpus runner |
195+
| `tests/ShellSyntaxTree.Tests/Corpus/bash/*.json` | 115 corpus entries — the acceptance contract |
196+
| `samples/ShellSyntaxTree.Cli.Sample/` | Console explainer + audit policy |
197+
| `samples/ShellSyntaxTree.Web.Sample/` | Blazor WASM Mermaid visualizer |
198+
| `SPEC.md` | Locked v0.1 contract |
199+
| `openspec/` | Change-proposal history (rationale for v0.1 design decisions) |
200+
| `PROJECT_CONTEXT.md`, `TOOLING.md`, `AGENTS.md` | Repo governance — for autonomous agents |
201+
| `IMPLEMENTATION_PLAN.md` | NOW / NEXT / LATER work tracker |

ShellSyntaxTree.slnx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,8 @@
2121
<Folder Name="/tests/">
2222
<Project Path="tests/ShellSyntaxTree.Tests/ShellSyntaxTree.Tests.csproj" />
2323
</Folder>
24+
<Folder Name="/samples/">
25+
<Project Path="samples/ShellSyntaxTree.Cli.Sample/ShellSyntaxTree.Cli.Sample.csproj" />
26+
<Project Path="samples/ShellSyntaxTree.Web.Sample/ShellSyntaxTree.Web.Sample.csproj" />
27+
</Folder>
2428
</Solution>

assets/sample-web-bash-c.png

46.9 KB
Loading

assets/sample-web-build-script.png

49.5 KB
Loading

assets/sample-web-custom.png

45.1 KB
Loading

assets/sample-web-dynamic-cwd.png

54.2 KB
Loading

assets/sample-web-empty.png

43.1 KB
Loading

assets/sample-web-source-tab.png

56.7 KB
Loading

assets/sample-web-subshell.png

49.8 KB
Loading

0 commit comments

Comments
 (0)