Skip to content
Merged
67 changes: 63 additions & 4 deletions .github/workflows/pr_validation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,19 @@ jobs:
name: Test-${{matrix.os}}
runs-on: ${{matrix.os}}

defaults:
run:
shell: bash

strategy:
matrix:
os: [ubuntu-latest, windows-latest]
include:
- os: ubuntu-latest
pwsh_asset: powershell-7.6.4-linux-x64-fxdependent.tar.gz
pwsh_sha256: e5c58d325a52200c37e6161b52e12141eb9a9d2685e2c8835c95018a614fd286
- os: windows-latest
pwsh_asset: PowerShell-7.6.4-win-fxdependent.zip
pwsh_sha256: 2a4036b4a0c4d1d69ed9069fa97deaf4a8cd81a0eacd1a30e6e4109fdc359796

steps:
- name: "Checkout"
Expand All @@ -36,18 +46,67 @@ jobs:
- name: "Restore .NET tools"
run: dotnet tool restore

- name: "Verify copyright headers"
- name: "Install pinned PowerShell 7.6.4 oracle"
shell: pwsh
run: |
$asset = '${{ matrix.pwsh_asset }}'
$archive = Join-Path $env:RUNNER_TEMP $asset
$installDirectory = Join-Path $env:RUNNER_TEMP 'pwsh-7.6.4'
$uri = "https://github.com/PowerShell/PowerShell/releases/download/v7.6.4/$asset"

Invoke-WebRequest -Uri $uri -OutFile $archive
$actualHash = (Get-FileHash -Algorithm SHA256 -LiteralPath $archive).Hash.ToLowerInvariant()
if ($actualHash -ne '${{ matrix.pwsh_sha256 }}') {
throw "PowerShell archive hash mismatch for ${asset}: $actualHash"
}

[void](New-Item -ItemType Directory -Force -Path $installDirectory)
if ($asset.EndsWith('.zip', [StringComparison]::OrdinalIgnoreCase)) {
Expand-Archive -LiteralPath $archive -DestinationPath $installDirectory -Force
}
else {
& tar -xzf $archive -C $installDirectory
if ($LASTEXITCODE -ne 0) {
throw "tar failed with exit code $LASTEXITCODE"
}
}

$installDirectory | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append

- name: "Verify copyright headers"
shell: pwsh -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -Command ". '{0}'"
run: ./scripts/Add-FileHeaders.ps1 -Verify

- name: "Verify pwsh is available (SPEC.POWERSHELL.md §13 oracle gate)"
- name: "Verify compatible PowerShell 7.6 is available (SPEC.POWERSHELL.md §13 oracle gate)"
shell: bash
run: |
if ! command -v pwsh >/dev/null 2>&1; then
echo "::error::pwsh is not on PATH — the PowerShell corpus oracle gate (PwshOracleTests) would silently skip."
exit 1
fi
pwsh --version
pwsh -NoProfile -NoLogo -NonInteractive -Command '
$v = $PSVersionTable.PSVersion
if ($v -lt [version]"7.6.4" -or $v -ge [version]"7.7") {
throw "Expected PowerShell >= 7.6.4 and < 7.7, found $v"
}
$v.ToString()
'

- name: "Verify Windows PowerShell 5.1 is available"
if: runner.os == 'Windows'
shell: bash
run: |
if ! command -v powershell.exe >/dev/null 2>&1; then
echo "::error::powershell.exe is not on the Bash PATH used by dotnet test — the Windows PowerShell 5.1 oracle would silently skip."
exit 1
fi
powershell.exe -NoProfile -NoLogo -NonInteractive -Command '
$v = $PSVersionTable.PSVersion
if ($v.Major -ne 5 -or $v.Minor -ne 1) {
throw "Expected Windows PowerShell 5.1, found $v"
}
$v.ToString()
'

- name: "dotnet restore"
run: dotnet restore
Expand Down
36 changes: 29 additions & 7 deletions IMPLEMENTATION_PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,28 @@ priorities.

---

## NOW (0.2.0 downstream acceptance / 0.3.0 contract design)
## NOW (0.3.0 host integration and release acceptance)

> **Spec:** `SPEC.POWERSHELL.md` (v0.2.0). The PowerShell parser is
> implemented — phases 1–14 of `SPEC.POWERSHELL.md` §16 are complete (see
> below). What remains is the downstream Netclaw integration, which needs
> actions outside this repository.

- [x] **v0.3 host-selected grammar and PowerShell dialect — library slice.** The executor
selects one top-level parser; Bash never cross-parses `pwsh` payloads and
PowerShell never cross-parses `bash -c` payloads. Add the extend-only
`PwshDialect` option with PowerShell 7 as the compatibility default and
Windows PowerShell 5.1 as an explicit native-Windows fallback. Dialect-
local syntax/catalog behavior and paired direct/corpus coverage are
implemented. GitHub Actions run 31357084413 proved the hash-pinned
PowerShell 7.6.4 oracle on Ubuntu and Windows, plus native Windows
PowerShell 5.1 discovery and its dialect-routed oracle on Windows.

- [ ] **v0.3 native-Windows Netclaw integration.** Pass the exact selected
shell through Netclaw's executor, approval policy, and model context;
prefer a compatible `pwsh.exe`, fall back to `powershell.exe`, and
reparse and reauthorize if executable selection changes.

- [ ] **v0.3 authored-command approval correction.** Treat PowerShell and Bash
approval completeness consistently: prove every authored executable
region, but do not require proof of ambient aliases, functions, modules,
Expand Down Expand Up @@ -599,25 +614,32 @@ priorities.
[NuGet package](https://www.nuget.org/packages/ShellSyntaxTree/0.3.0-alpha.3)
and [GitHub prerelease](https://github.com/Aaronontheweb/ShellSyntaxTree/releases/tag/0.3.0-alpha.3)
preserve the v0.2 projection and the existing public v0.3 API.
- [ ] Publish `0.3.0-alpha.4` with the reviewed authored-command completeness
correction. Netclaw must validate default-mode static PowerShell commands
without requiring ambient profile, module, alias, function, `PATH`,
inherited-variable, or prior-runspace proofs. Unknown and source-mutated
policy facts must remain strict.
- [x] Published `0.3.0-alpha.4` with the reviewed authored-command completeness
correction. The
[NuGet package](https://www.nuget.org/packages/ShellSyntaxTree/0.3.0-alpha.4)
and [GitHub prerelease](https://github.com/Aaronontheweb/ShellSyntaxTree/releases/tag/0.3.0-alpha.4)
preserve strict unknown and source-mutated policy facts while default-mode
static PowerShell commands no longer require ambient resolution proof.
- [x] Replace the pre-alpha consumer preview with the v0.3 occurrence-based
authorization loop and separate syntax-display guidance. Document exact,
finite, pattern, unknown, joined-cwd, redirect, incomplete-result,
equality, hashing, `ToString()`, serialization, and `Clauses` migration
behavior in the guide and release notes; direct the README quick start
to `Commands` and the full guide.
- [x] Close the v0.3 public-API compatibility gate. Existing reflection
- [x] Re-closed the v0.3 public-API compatibility gate for the additive
`PwshDialect` enum and options property. Existing reflection
snapshots pin the exact exported types, members, enum ordering,
reference nullability, defaults, parser constructors and entry points,
and fixed limits against the shared and PowerShell specifications.
Additional tests pin generated equality and `ToString()` participation
plus equal-record hash consistency, demonstrate that default JSON is not
a polymorphic round-trip contract, and make every policy-sensitive
unknown numeric enum value detectable so consumers can reject it.
The dialect slice adds explicit default, unknown-value, record equality,
hash, `ToString()`, propagation, and parser-behavior coverage. A
`dotnet-inspect` assembly diff against the published 0.3.0-alpha.4 package
reports exactly two additive changes on both `net8.0` and
`netstandard2.0`: the enum and one options member, with no breaking change.
- [x] Expose public Bash heredoc body, delimiter, expansion, tab-stripping, and
completeness facts from the delivered bounded grammar. Direct tests pin
literal and expanding delimiters, every supported substitution command,
Expand Down
12 changes: 9 additions & 3 deletions PROJECT_CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,9 @@ The output is a `ParsedCommand` containing:
elements; executable-specific semantics remain consumer-owned
- Bash `cd <dir> && cmd` and PowerShell `Set-Location <dir>; cmd`
propagation — the target is attributed to subsequent clauses
- recursion into `bash -c`, `pwsh -Command`, and `pwsh -EncodedCommand` so
wrapped commands surface as clauses
- parser-local recursion into Bash `bash -c` or PowerShell `pwsh -Command` /
`pwsh -EncodedCommand`; an external shell invoked from the other language
remains an ordinary command and its payload is not cross-parsed
- PowerShell alias canonicalization and explicit dynamic-command identity
- Bash subshell isolation and PowerShell grouping semantics for cwd attribution
- safe-fail flag `IsUnparseable` for unsupported constructs (control flow,
Expand Down Expand Up @@ -70,10 +71,15 @@ zero-native-deps .NET parser sized to what security gates actually need.

- Bash and PowerShell 7 pipeline parsing ship behind the shared
`IShellParser` seam. Windows `cmd` remains deferred.
- Stable v0.3 keeps PowerShell 7 as the compatibility default and adds an
explicit Windows PowerShell 5.1 dialect for native-Windows fallback. The
executor, parser, approval policy, and model context must agree on the exact
selected shell; ShellSyntaxTree does not auto-detect it.
- Public API surface in SPEC §2 is **locked**. Internal changes are free.
- Acceptance is the multi-shell corpus contract: every Bash and PowerShell
JSON entry parses to its expected AST, and the PowerShell corpus also passes
the live `pwsh` oracle matrix.
the dialect-matched live oracle matrix (`pwsh` for PowerShell 7 and
`powershell.exe` for Windows PowerShell 5.1 on Windows CI).

### v0.3 (contract design)

Expand Down
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,8 @@ public sealed class PwshParser : IShellParser { /* … */ } // v0.2.0

public abstract record ShellParserOptions { /* HomeDirectory, WorkingDirectory */ }
public sealed record BashParserOptions : ShellParserOptions; // InitialStateMode
public sealed record PwshParserOptions : ShellParserOptions; // InitialStateMode
public sealed record PwshParserOptions : ShellParserOptions; // InitialStateMode, Dialect
public enum PwshDialect { Unknown, PowerShell7, WindowsPowerShell51 }

public sealed record ParsedCommand { /* Source, Syntax, Commands, Clauses, IsUnparseable, … */ }
public abstract record ShellSyntaxNode;
Expand All @@ -151,6 +152,14 @@ v0.2 consumers can migrate from the conservative `Clauses` projection. The
shell-specific parsers retain different grammar and analysis rules. A Windows
`cmd` parser remains deferred.

Select the parser from the shell that will actually execute the source. The
library does not auto-detect or cross-parse languages: `pwsh -Command ...`
under `BashParser` is an ordinary external command. `PwshParserOptions.Dialect`
defaults to PowerShell 7 for compatibility; select `WindowsPowerShell51`
explicitly when the executor falls back to `powershell.exe`. The
`PowerShell7` currently denotes the proved PowerShell 7.6 servicing line:
version 7.6.4 or newer, but earlier than 7.7.

Behavioral contract: [`SPEC.md`](./SPEC.md) (bash + shared surface) and
[`SPEC.POWERSHELL.md`](./SPEC.POWERSHELL.md) (PowerShell).

Expand Down
7 changes: 7 additions & 0 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,13 @@
ShellSyntaxTree does not define a stable serialized wire format. Persisted
results require a consumer-owned, versioned DTO or explicit serializer
mapping that fails closed on unknown node and enum values.
- `PwshParserOptions.Dialect` is additive and defaults to `PowerShell7` for
compatibility. Native Windows consumers select it only for a compatible
PowerShell 7.6 host (`>=7.6.4` and `<7.7`) and select
`WindowsPowerShell51` when falling back to `powershell.exe`; the host,
parser dialect, approval policy, and executor identity must agree. The new
property also participates in options-record equality, hashing, `ToString()`,
reflection, and default serialization shape.

#### 0.3.0-alpha.4 2026-08-09 ####

Expand Down
Loading