Skip to content

Commit 09331a6

Browse files
committed
docs: add consumer analysis examples
1 parent 3e8555f commit 09331a6

2 files changed

Lines changed: 163 additions & 1 deletion

File tree

IMPLEMENTATION_PLAN.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,10 @@ priorities.
135135
- [x] **Consumer guide.** Added `docs/CONSUMER_GUIDE.md` with the
136136
shell-neutral security-consumer algorithm, Bash and PowerShell guidance,
137137
worked public use cases, and immutable permalinks to Netclaw's production
138-
integration. Linked it from the README and aligned stale PowerShell
138+
integration. Added compact input-to-result-to-policy examples for command
139+
occurrences, attached arguments, bounded and zero-or-more loops, cwd
140+
propagation, file and descriptor redirects, substitutions, and safe-fail
141+
results. Linked it from the README and aligned stale PowerShell
139142
prerelease/status wording in the public project docs.
140143
- [x] **Issue #52 — hyphenated PowerShell parameters/native options.**
141144
Preserve internal hyphens, apply bash-compatible native

docs/CONSUMER_GUIDE.md

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -385,6 +385,53 @@ such as `--work-tree=../repo` and `-Path:C:\repo` can produce two `Arg`
385385
records that share one source element; consumers do not need to reconstruct
386386
that normal many-to-one relationship from indexes or source spans.
387387

388+
The examples in this guide use a compact result notation rather than dumping
389+
the complete object graph. Each one shows the submitted input, the
390+
policy-relevant facts returned by the parser, and the decision those facts
391+
enable. Names such as `Exact("/work")` and `Descriptor(2)` denote the
392+
corresponding closed runtime alternatives, not strings that consumers need to
393+
parse.
394+
395+
For example, parse this with `BashParser`, `WorkingDirectory = "/work"`:
396+
397+
```bash
398+
cat file.txt | grep x && rm /tmp/stale
399+
```
400+
401+
The authorization projection is:
402+
403+
| `Commands` index | Authored command | `ImmediateRole` | `IsComplete` | `WorkingDirectory` |
404+
|---:|---|---|---|---|
405+
| 0 | `cat file.txt` | `PipelineStage` | `true` | `Exact("/work")` |
406+
| 1 | `grep x` | `PipelineStage` | `true` | `Exact("/work")` |
407+
| 2 | `rm /tmp/stale` | `Ordinary` | `true` | `Exact("/work")` |
408+
409+
The consumer evaluates all three rows. It may group the first two into one
410+
pipeline-shaped prompt for display, but that grouping does not authorize the
411+
second stage implicitly. The final `rm` occurrence is also evaluated even if
412+
an earlier occurrence already requires a prompt, because it may produce a
413+
hard deny.
414+
415+
Attached option forms demonstrate why `AnalyzedArgument` includes direct
416+
object references. For this Bash input:
417+
418+
```bash
419+
git --work-tree=../repo status
420+
```
421+
422+
`Commands[0].Arguments` contains three entries:
423+
424+
| `Argument.Raw` | `Value` | `Element.Raw` |
425+
|---|---|---|
426+
| `--work-tree` | `Exact("--work-tree")` | `--work-tree=../repo` |
427+
| `../repo` | `Exact("../repo")` | `--work-tree=../repo` |
428+
| `status` | `Exact("status")` | `status` |
429+
430+
The first two entries reference the same `ClauseElement`. A consumer can bind
431+
the option and its operand without source-span arithmetic or re-tokenizing the
432+
command. PowerShell attached parameters such as
433+
`Remove-Item -Path:C:\repo` use the same many-to-one shape.
434+
388435
Apply the executable's complete argument grammar to every value:
389436

390437
```csharp
@@ -437,6 +484,63 @@ alternatives carry stdin data whose meaning remains receiver-specific. An
437484
occurrence can be complete while an argument, cwd, or redirect value is
438485
unknown, so test all facts separately.
439486

487+
## Evaluating loops
488+
489+
A loop body is represented once as authored syntax. ShellSyntaxTree does not
490+
pretend that it executed the loop or duplicate a command occurrence for every
491+
candidate value. Instead, it gives the loop-dependent argument a value domain.
492+
493+
With `BashInitialStateMode.IsolatedNonInteractive` and
494+
`WorkingDirectory = "/work"`, this input:
495+
496+
```bash
497+
for f in a.txt b.txt; do rm -- "$f"; done
498+
```
499+
500+
produces one loop-body occurrence:
501+
502+
```text
503+
Commands[0]
504+
Clause.Verb.Tokens: ["rm"]
505+
ImmediateRole: LoopBody
506+
IsComplete: true
507+
WorkingDirectory: Exact("/work")
508+
Arguments[0]: "--" -> Exact("--")
509+
Arguments[1]: "\"$f\"" -> FiniteSet("a.txt", "b.txt")
510+
```
511+
512+
The consumer applies the complete `rm` grammar and path policy to both
513+
`a.txt` and `b.txt`. It must not approve only the first candidate, and it must
514+
not mistake one occurrence for proof that the command runs only once.
515+
516+
PowerShell uses the same consumer shape. Under
517+
`PwshInitialStateMode.IsolatedNonInteractiveNoProfile`, this input:
518+
519+
```powershell
520+
foreach ($f in @('a.txt', 'b.txt', 'a.txt')) { Write-Output $F }
521+
```
522+
523+
produces one `LoopBody` occurrence whose `$F` argument is
524+
`FiniteSet("a.txt", "b.txt")`; PowerShell's case-insensitive variable binding
525+
and duplicate elimination have already been reflected in the domain.
526+
527+
The isolated modes are executor assertions, not parser optimizations. With
528+
the safe default initial-state modes, these ambient-variable-dependent proofs
529+
remain unknown or make the construct unparseable as specified earlier. A
530+
consumer must not select an isolated mode merely to obtain a finite set.
531+
532+
Loops also affect later state even when their body facts are static. With an
533+
incoming cwd of `/work`:
534+
535+
```bash
536+
for f in /tmp/*.txt; do cd /tmp; done; pwd
537+
```
538+
539+
the loop may execute zero times, so both the body `cd` occurrence and the
540+
later `pwd` occurrence report `WorkingDirectory = Unknown`. The reachable
541+
states are `/work` and `/tmp`; the parser does not choose whichever value
542+
would make policy easiest. A cwd-sensitive consumer prompts or denies.
543+
440544
## Choosing a command identity
441545

442546
Choose the identity from the authored syntax. Runtime command discovery is an
@@ -562,6 +666,19 @@ For `cd /repo && cat file.txt`, the `cat` clause receives a synthetic
562666
directory. PowerShell provides the same contract for `Set-Location` and its
563667
aliases.
564668

669+
With an incoming cwd of `/work`, the relevant output is:
670+
671+
| Occurrence | `WorkingDirectory` | Authored path | `Arg.Resolved` |
672+
|---|---|---|---|
673+
| `cd /repo` | `Exact("/work")` | `/repo` | `/repo` |
674+
| `cat file.txt` | `Exact("/repo")` | `file.txt` | `/repo/file.txt` |
675+
676+
The `cd` row reports the directory in which `cd` itself runs; the `cat` row
677+
reports the successful `AndIf` continuation state. This is why consumers
678+
should use the occurrence's `WorkingDirectory` for execution context and the
679+
argument's `Resolved` value for path-zone policy rather than trying to infer
680+
either from clause order.
681+
565682
The attributed argument is derived context:
566683

567684
- use it when evaluating where a clause operates;
@@ -582,6 +699,18 @@ write outside an allowed zone:
582699
echo safe > /etc/profile.d/example.sh
583700
```
584701

702+
With a Bash working directory of `/work`, representative results are:
703+
704+
| Input | Redirect alternative | Source | Relevant value | Complete? | Consumer consequence |
705+
|---|---|---|---|---:|---|
706+
| `echo safe > /etc/profile.d/example.sh` | `FileRedirectAnalysis` with `Mode = Output` | `Default` | `Target = Exact("/etc/profile.d/example.sh")` | yes | Apply write-path policy to the exact target. |
707+
| `command 2>&1` | `DescriptorDuplicateRedirectAnalysis` | `Descriptor(2)` | `TargetDescriptor = 1` | yes | Apply descriptor policy; do not treat `1` as a path. |
708+
| `command 2>&$FD` | `UnresolvedRedirectAnalysis` | `Unknown` | no proved target descriptor | no | Prompt or deny the occurrence. |
709+
710+
Those are runtime alternatives, not interpretations of a string prefix. In
711+
particular, the incomplete third row cannot accidentally pass a rule written
712+
for ordinary stderr-to-stdout duplication.
713+
585714
For a v0.2 compatibility consumer, walk `Clause.Redirects` independently of
586715
`Args`:
587716

@@ -714,6 +843,24 @@ invocation. By contrast, `& $(Write-Output Get-Date)` also retains an
714843
incomplete dynamic outer occurrence because PowerShell invokes the produced
715844
name.
716845

846+
Bash exposes the same execution-before-container ordering. For:
847+
848+
```bash
849+
rm "$(find /tmp)"
850+
```
851+
852+
the relevant projection is:
853+
854+
| `Commands` index | Command | `ImmediateRole` | Argument value |
855+
|---:|---|---|---|
856+
| 0 | `find /tmp` | `Substitution` | `/tmp` is `Exact("/tmp")` |
857+
| 1 | `rm "$(find /tmp)"` | `Ordinary` | produced filename is `Unknown` |
858+
859+
The `find` occurrence is independently authorizable, but its presence does not
860+
make the bytes it prints a statically known `rm` operand. A path-sensitive
861+
policy therefore evaluates `find` and still prompts or denies `rm`. It does not
862+
walk `Syntax` afterward and authorize `find` a second time.
863+
717864
Quoting also determines the scope of host-wrapper substitutions. In
718865
`pwsh -Command "Write-Output $(Get-Date)"`, the parent evaluates `Get-Date`, so
719866
the result contains that parent-scope occurrence plus an incomplete outer
@@ -750,6 +897,18 @@ The recoverable outcome is normally a user prompt with a one-time option, or a
750897
deny. A false-negative approval match causes another prompt; a false-positive
751898
match can silently execute something the operator did not authorize.
752899

900+
Two different result shapes reach that same safe outcome:
901+
902+
| Input and parser | Relevant output | Why reusable approval stops |
903+
|---|---|---|
904+
| PowerShell: `& $exe` | `IsUnparseable = false`; one occurrence with `IsComplete = false` and `Verb.IsDynamic = true` | The syntax is recognized, but the executable identity is not bounded. |
905+
| Bash: `if true; then echo ok; fi` | `IsUnparseable = true`; `Commands` and `Clauses` are empty | The unsupported control construct may contain execution, so partial syntax is diagnostic only. |
906+
907+
`IsUnparseable = false` is therefore not an allow signal. It means only that
908+
the whole input was not rejected as an unsupported or unsafe-to-project
909+
construct; the consumer still checks every occurrence and every
910+
policy-sensitive domain.
911+
753912
## Worked use cases
754913

755914
### AI-agent approval gate

0 commit comments

Comments
 (0)