Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 24 additions & 16 deletions IMPLEMENTATION_PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,13 +145,12 @@ Done when:
command-resolution mutation and reserved execution forms into the strict
181-case review matrix.
- [x] ShellSyntaxTree `0.3.0-alpha.2` introduced one temporary POSIX PowerShell
child wrapper. The current runtime keeps that transitional behavior until
activation task 3.1 removes it. The accepted target contract supersedes the
design: Bash treats `pwsh` as an external command, and only a native
child wrapper. Native-host activation removed that transitional consumer
behavior: Bash treats `pwsh` as an external command, and only a native
PowerShell host uses `PwshParser`.
- [x] The current 204-case shell approval review table records the transitional
child-host behavior. Activation replaces those rows with native PowerShell
cases; they do not define the accepted host-language boundary.
- [x] The shell approval review table separates Bash, PowerShell 7, and Windows
PowerShell 5.1 rows. Cross-language payloads remain ordinary external-command
arguments; same-language static children use parser-returned occurrences.
- [x] A constrained stdin grammar allows a complete literal heredoc or bounded
here string only for argument-free `cat`. Unknown data, expanding heredocs,
arguments, wrappers, interpreters, and stored grants stay strict.
Expand All @@ -170,25 +169,34 @@ This work replaces `cmd.exe` with a native PowerShell host on Windows. Netclaw
prefers a compatible PowerShell 7.6 host and falls back to Windows PowerShell
5.1. It keeps Bash and PowerShell as separate host languages.

The additive foundation now pins ShellSyntaxTree `0.3.0-alpha.5` and defines
the immutable environment, strict host probe, and process arguments. The
current runtime remains transitional until the activation tasks route every
executor and security consumer through that environment.
The additive foundation pins ShellSyntaxTree `0.3.0-alpha.5` and defines the
immutable environment, strict host probe, and process arguments. Runtime
activation now routes execution, policy, approval, background jobs, and model
context through the same resolved environment. Native Windows CI and final
OpenSpec delivery remain before this priority is complete.

Local validation on 2026-08-10 passed restore, the zero-warning Release build,
the full solution test suite, changed-file format verification, headers,
Slopwatch, `git diff --check`, and strict OpenSpec validation. The shell-platform
behavioral evaluation was unavailable because the required
`NETCLAW_EVAL_PROVIDER_TYPE`, `NETCLAW_EVAL_PROVIDER_ENDPOINT`, and
`NETCLAW_EVAL_MODEL_ID` settings were absent. This result is blocked evidence,
not an evaluation pass.

Done when:

- [ ] One immutable shell environment selects the absolute executable path,
- [x] One immutable shell environment selects the absolute executable path,
grammar, path style, process arguments, and PowerShell dialect for the daemon
lifetime.
- [ ] Windows selects `pwsh.exe` only for versions from 7.6.4 through 7.6.x. It
- [x] Windows selects `pwsh.exe` only for versions from 7.6.4 through 7.6.x. It
falls back to `powershell.exe` 5.1 and fails clearly if neither host matches.
- [ ] Execution, parsing, hard deny, approval matching, prompt display, and
- [x] Execution, parsing, hard deny, approval matching, prompt display, and
model context use the same selected environment.
- [ ] Bash treats `pwsh` as an external command. PowerShell treats `bash` as an
- [x] Bash treats `pwsh` as an external command. PowerShell treats `bash` as an
external command. Only same-language child hosts can recurse.
- [ ] Unknown or incomplete facts cannot produce a stored approval candidate
- [x] Unknown or incomplete facts cannot produce a stored approval candidate
or a safe-verb pass. Stored approval cannot bypass hard deny.
- [ ] Personal sessions state the platform, executable, grammar, and dialect,
- [x] Personal sessions state the platform, executable, grammar, and dialect,
including sessions that have no project directory.
- [ ] Native Windows tests cover PowerShell 7.6 and Windows PowerShell 5.1.
The security review table covers direct, child, retry, and background paths.
Expand Down
29 changes: 24 additions & 5 deletions docs/runbooks/tool-approval-gates.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,8 +123,11 @@ When the agent calls a tool in `Approval` mode:

### Command patterns

For `shell_execute`, patterns are verb-chain prefixes extracted by tokenizing
the command:
For `shell_execute`, patterns come from the parser for the daemon's selected
native shell environment. Linux and macOS use Bash. Windows uses a probed
native PowerShell host: compatible PowerShell 7.6 is preferred, with Windows
PowerShell 5.1 as the fallback. The exact executable, grammar, and dialect are
shown in Personal session working context.

| Command | Pattern |
|---------|---------|
Expand All @@ -143,6 +146,21 @@ For **compound commands** (`&&`, `||`, `;`, `|`), each segment is checked
independently. If any segment is unapproved, all unapproved patterns are
batched into one prompt.

The selected host grammar is also the language boundary. Under Bash,
`pwsh -Command 'Get-Content ./a.txt'` is an ordinary external `pwsh` command;
the payload is not separately parsed as PowerShell. Under native PowerShell,
`bash -c 'cat ./a.txt'` is likewise an ordinary external `bash` command.
Same-language static child hosts can expose nested command occurrences when
ShellSyntaxTree proves them.

PowerShell 7 and Windows PowerShell 5.1 are analyzed as distinct dialects.
In particular, `&&` and `||` are unresolved under 5.1 and cannot create a
persistent approval candidate or receive the read-only safe-verb shortcut.
Incomplete commands, dynamic command identities, and non-filesystem provider
drives also remain one-time-only. Netclaw does not claim knowledge of ambient
profiles, modules, inherited variables, executable lookup, or external script
contents.

For most **non-shell tools** (MCP tools, `file_read`, etc.), approval is at the
tool-name level.

Expand All @@ -157,9 +175,10 @@ path-scoped patterns (for example,
Demonstrably read-only verbs auto-run with no prompt when invoked inside a
trusted zone (`session_dir`, or `project_dir` for Personal/Team). The bundled
safe-verb lists (`safe-verbs.linux.json`, `safe-verbs.windows.json`) cover file
readers (`ls`, `grep`, `cat`), system/info verbs (`date`, `whoami`, `uname`,
`uptime`), and read-only `git`/`gh` queries (`git status`, `git log`,
`gh pr view`, `gh run list`). Mutating verbs (`git push`, `git fetch`, `rm`),
readers (for example `ls`, `grep`, and `cat` on Bash; `Get-ChildItem`,
`Get-Content`, and `Select-String` on PowerShell), system/info verbs (`date`,
`whoami`, `uname`, `uptime`), and read-only `git`/`gh` queries (`git status`,
`git log`, `gh pr view`, `gh run list`). Mutating verbs (`git push`, `git fetch`, `rm`),
command-prefixing verbs (`env`, `xargs`, `sudo`), network-writing verbs
(`gh api`, `curl`), and environment/process-inspection verbs (`printenv`,
`ps`) are never auto-allowed — the trusted-zone gate scopes verbs that act on
Expand Down
36 changes: 18 additions & 18 deletions openspec/changes/native-windows-powershell-host/tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,63 +18,63 @@
fails when neither host matches.
- [x] 2.3 Add version, priority, probe-failure, startup-failure, parser-options,
and process-argument tests for Bash, PowerShell 7.6, and PowerShell 5.1.
- [ ] 2.4 Deliver the additive foundation as an adversarially reviewed PR with
- [x] 2.4 Deliver the additive foundation as an adversarially reviewed PR with
green CI and auto-merge before activation work starts.

## 3. Atomic Runtime Activation

- [ ] 3.1 Remove Netclaw's POSIX `pwsh -Command` child-payload parser and pin
- [x] 3.1 Remove Netclaw's POSIX `pwsh -Command` child-payload parser and pin
both cross-language non-delegation directions.
- [ ] 3.2 Route shell analysis and approval matching through the environment's
- [x] 3.2 Route shell analysis and approval matching through the environment's
parser, working directory, PowerShell dialect, and unknown initial-state mode.
- [ ] 3.3 Route hard deny, protected paths, trust zones, safe verbs, approval
- [x] 3.3 Route hard deny, protected paths, trust zones, safe verbs, approval
candidates, and approval display through the environment-bound analysis.
- [ ] 3.4 Add PowerShell hard-deny coverage for process termination, recursive
- [x] 3.4 Add PowerShell hard-deny coverage for process termination, recursive
root removal, and `Start-Process -Verb RunAs` before approval evaluation.
- [ ] 3.5 Make buffered and streaming `ShellTool` execution use one shared
- [x] 3.5 Make buffered and streaming `ShellTool` execution use one shared
process-start builder. It must use only the selected absolute host path and
fixed non-interactive arguments. It must fail visibly and must not use a
per-call fallback.
- [ ] 3.6 Register one environment instance for the parser, policy, matcher,
- [x] 3.6 Register one environment instance for the parser, policy, matcher,
executor, context provider, direct calls, and background-job calls.

## 4. Model Context and Guidance

- [ ] 4.1 Add platform, executable, grammar, and dialect to the Personal
- [x] 4.1 Add platform, executable, grammar, and dialect to the Personal
working-context tail, including sessions without a project directory.
- [ ] 4.2 Prove that parent and child runs receive the same shell identity and
- [x] 4.2 Prove that parent and child runs receive the same shell identity and
that Team or Public sessions gain no shell capability.
- [ ] 4.3 Update the embedded operations guidance with native Bash and
- [x] 4.3 Update the embedded operations guidance with native Bash and
PowerShell examples. State that ambient profiles, modules, and lookup remain
outside parser proof.

## 5. Security and Approval Evidence

- [ ] 5.1 Replace `cmd.exe` host rows with reviewed PowerShell 7.6 and Windows
- [x] 5.1 Replace `cmd.exe` host rows with reviewed PowerShell 7.6 and Windows
PowerShell 5.1 allow, prompt, deny, safe-verb, and stored-grant rows.
- [ ] 5.2 Add parser-boundary cases for ordinary Bash `pwsh` arguments,
- [x] 5.2 Add parser-boundary cases for ordinary Bash `pwsh` arguments,
ordinary PowerShell `bash` arguments, and same-language child recursion.
- [ ] 5.3 Add incomplete, dynamic, unknown-dialect, 5.1 pipeline-chain,
- [x] 5.3 Add incomplete, dynamic, unknown-dialect, 5.1 pipeline-chain,
protected-path, redirect, provider-drive, alias, and hard-deny cases.
- [ ] 5.4 Prove that a dialect change reparses before grant matching and that a
- [x] 5.4 Prove that a dialect change reparses before grant matching and that a
stored approval cannot bypass a changed canonical candidate.
- [ ] 5.5 Prove buffered, streaming, direct, sub-agent, background, retry, and
- [x] 5.5 Prove buffered, streaming, direct, sub-agent, background, retry, and
redrive paths use the same selected environment and approval decision.

## 6. Verification and Delivery

- [ ] 6.1 Run focused security, actor, context, buffered-executor,
streaming-executor, resolver, and approval matrix tests on Linux and native
Windows.
- [ ] 6.2 Run restore, Release build, the full test suite, format verification,
- [x] 6.2 Run restore, Release build, the full test suite, format verification,
header verification, Slopwatch, `git diff --check`, and strict OpenSpec
validation.
- [ ] 6.3 Run the shell-platform behavioral evaluation when provider
- [x] 6.3 Run the shell-platform behavioral evaluation when provider
credentials are available. Record an unavailable provider as blocked, not
passed.
- [ ] 6.4 Run an adversarial review for every implementation PR, enable
auto-merge only after the review passes, and follow required CI to merge.
- [ ] 6.5 Update `IMPLEMENTATION_PLAN.md`, user guidance, and review-table
- [x] 6.5 Update `IMPLEMENTATION_PLAN.md`, user guidance, and review-table
evidence with observed results.
- [ ] 6.6 Run `openspec-verify-change`, sync the capability deltas, and archive
this change only after all runtime and downstream acceptance gates pass.
43 changes: 36 additions & 7 deletions src/Netclaw.Actors.Tests/Jobs/BackgroundJobExecutionActorTests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// -----------------------------------------------------------------------
// -----------------------------------------------------------------------
// <copyright file="BackgroundJobExecutionActorTests.cs" company="Petabridge, LLC">
// Copyright (C) 2026 - 2026 Petabridge, LLC <https://petabridge.com>
// </copyright>
Expand All @@ -9,6 +9,7 @@
using Netclaw.Actors.Channels;
using Netclaw.Actors.Jobs;
using Netclaw.Configuration;
using Netclaw.Security;
using Netclaw.Tests.Utilities;
using Xunit;
using static Netclaw.Actors.Jobs.BackgroundJobProtocol;
Expand All @@ -18,6 +19,7 @@ namespace Netclaw.Actors.Tests.Jobs;
[Collection(BackgroundJobProcessCollection.Name)]
public class BackgroundJobExecutionActorTests : TestKit
{
private static readonly ShellExecutionEnvironment ShellEnvironment = TestShellEnvironment.Current;
private readonly DisposableTempDir _dir = new();
private BackgroundJobDefinitionStore _store = null!;

Expand All @@ -36,8 +38,7 @@ protected override async Task AfterAllAsync()
await base.AfterAllAsync();
}

private static string LongRunningCommand =>
OperatingSystem.IsWindows() ? "ping -n 300 127.0.0.1" : "sleep 300";
private static string LongRunningCommand => TestShellEnvironment.LongRunningCommand;

private BackgroundJobDefinition MakeDefinition(string command, int timeoutSeconds = 600) => new()
{
Expand All @@ -53,13 +54,41 @@ protected override async Task AfterAllAsync()
TimeoutSeconds = timeoutSeconds
};

private IActorRef SpawnExecution(BackgroundJobDefinition definition, IActorRef probe)
private IActorRef SpawnExecution(
BackgroundJobDefinition definition,
IActorRef probe,
ShellExecutionEnvironment? environment = null)
{
var outputPath = _store.GetOutputLogPath(definition.Id);
var props = Props.Create(() => new BackgroundJobExecutionActor(definition, outputPath, TimeProvider.System));
var props = Props.Create(() => new BackgroundJobExecutionActor(
definition,
outputPath,
TimeProvider.System,
environment ?? ShellEnvironment));
return Sys.ActorOf(ForwardingParent.Props(props, probe), $"exec-{definition.Id}");
}

[Fact]
public async Task Missing_selected_executable_reports_exact_host_without_fallback()
{
const string missingExecutable = @"C:\missing\pwsh.exe";
var environment = ShellExecutionEnvironment.CreatePowerShell(
missingExecutable,
ShellSyntaxTree.PwshDialect.PowerShell7);
var definition = MakeDefinition("Get-ChildItem");
var probe = CreateTestProbe("parent");
SpawnExecution(definition, probe, environment);

var completed = await probe.ExpectMsgAsync<BackgroundJobCompleted>(
TimeSpan.FromSeconds(10),
cancellationToken: TestContext.Current.CancellationToken);

Assert.Equal(BackgroundJobStatus.Failed, completed.Status);
Assert.Contains(missingExecutable, completed.OutputTail);
Assert.DoesNotContain("powershell.exe", completed.OutputTail, StringComparison.OrdinalIgnoreCase);
Assert.DoesNotContain("cmd.exe", completed.OutputTail, StringComparison.OrdinalIgnoreCase);
}

[Fact]
public async Task SuccessfulCompletion_ReportsCompletedToParent()
{
Expand Down Expand Up @@ -118,8 +147,8 @@ public async Task RunningJob_OutputIsObservableOnDiskBeforeExit()
{
// The detached-process contract: a job that never exits (dev server)
// must still have its output readable from the log while it runs.
var command = OperatingSystem.IsWindows()
? "echo server-is-up && ping -n 300 127.0.0.1"
var command = ShellEnvironment.Grammar == ShellGrammar.PowerShell
? "Write-Output server-is-up; Start-Sleep -Seconds 300"
: "echo server-is-up && sleep 300";
var definition = MakeDefinition(command);
var probe = CreateTestProbe("parent");
Expand Down
11 changes: 7 additions & 4 deletions src/Netclaw.Actors.Tests/Jobs/BackgroundJobIntegrationTests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// -----------------------------------------------------------------------
// -----------------------------------------------------------------------
// <copyright file="BackgroundJobIntegrationTests.cs" company="Petabridge, LLC">
// Copyright (C) 2026 - 2026 Petabridge, LLC <https://petabridge.com>
// </copyright>
Expand Down Expand Up @@ -42,7 +42,10 @@ protected override void ConfigureAkka(AkkaConfigurationBuilder builder, IService
builder.StartActors((system, registry, _) =>
{
var manager = system.ActorOf(
Props.Create(() => new BackgroundJobManagerActor(_store, TimeProvider.System)),
Props.Create(() => new BackgroundJobManagerActor(
_store,
TimeProvider.System,
TestShellEnvironment.Current)),
"background-job-manager");
registry.Register<BackgroundJobManagerActorKey>(manager);
});
Expand Down Expand Up @@ -134,7 +137,7 @@ public async Task BackgroundJob_WithMissingWorkingDirectory_FailsWithHelpfulErro
TimeSpan.FromSeconds(15), cancellationToken: TestContext.Current.CancellationToken);

Assert.Contains("does not exist", delivered.Content);
Assert.Contains("mkdir", delivered.Content);
Assert.Contains(TestShellEnvironment.CreateDirectoryCommandName, delivered.Content);
Assert.Contains("failed", delivered.Content.ToLowerInvariant());

await AwaitAssertAsync(() =>
Expand Down Expand Up @@ -183,7 +186,7 @@ public async Task CancelRunningJob_ViaCheckBackgroundJobTool()
ActorRegistry.For(Sys).Register<SlackGatewayActorKey>(autoAckRef);

var started = await manager.Ask<BackgroundJobStarted>(
MakeStartCommand("sleep 300"),
MakeStartCommand(TestShellEnvironment.LongRunningCommand),
TimeSpan.FromSeconds(5),
TestContext.Current.CancellationToken);

Expand Down
Loading
Loading