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
2 changes: 1 addition & 1 deletion Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@
<PackageVersion Include="SlackNet.Extensions.DependencyInjection" Version="$(SlackNetVersion)" />
<PackageVersion Include="Cronos" Version="0.13.0" />
<PackageVersion Include="Netclaw.SkillClient" Version="0.4.1" />
<PackageVersion Include="ShellSyntaxTree" Version="0.3.3" />
<PackageVersion Include="ShellSyntaxTree" Version="0.3.4" />
<PackageVersion Include="Termina" Version="0.16.1" />
</ItemGroup>
<!-- Serialization -->
Expand Down
9 changes: 9 additions & 0 deletions IMPLEMENTATION_PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,12 @@ Done when:
each effective directory, across live, sub-agent, and redrive paths.
- [x] External paths, mismatched grants, dynamic syntax, and hard-deny rules
keep their strict behavior.
- [ ] Bash causal approval intent composes exact stored grants for an initial
exact ShellSyntaxTree 0.3.4 directory change with reviewed diagnostic tails.
Execution facts, folder grants, protected paths, headless authority, and
PowerShell remain strict. Each possible fallback passes the same symlink
check. POSIX `/tmp` aliases map through host path facts. The exact D03 fixture
must pass on Linux, macOS, and native Windows CI.
- [x] Directory operands preserve dotted directory names without weakening the
external-path or symlink checks.
- [x] Reviewed-safe policy matches canonical ShellSyntaxTree token prefixes.
Expand Down Expand Up @@ -280,6 +286,9 @@ Done when:
The Release build and all 7,138 runnable tests pass. The suite reports 15
expected platform or opt-in skips. Adversarial review and all required CI
checks passed before merge.
- [ ] Netclaw consumes public ShellSyntaxTree `0.3.4` for parser-owned
working-directory effects. The causal policy does not recognize command
names or private executable grammar. Native platform CI remains required.
- [x] The expanded 247-test matrix covers command-substitution and PowerShell
execution-region behavior. Known command-owned regions reuse independently
matched host and body grants after Netclaw accounts for the parsed body.
Expand Down
44 changes: 44 additions & 0 deletions docs/runbooks/tool-approval-gates.md
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,50 @@ names a directory that the session can declare, Netclaw can first return a
candidates require approval. The bare `echo` side effect does not become a
reusable prompt choice.

#### Example: Bash causal directory intent

Input:

```bash
cd /tmp && gh api repos/example/project/actions/jobs/123456/logs \
> slopwatch.log 2>&1; wc -c slopwatch.log; head -100 slopwatch.log
```

Assume global grants cover `cd` and `gh api`. The reviewed catalog contains
`wc` and `head`.

| Candidate | Real scope | Approval scope | Coverage |
|-----------|------------|----------------|----------|
| `cd` | `/tmp` target | Real scope | Persistent global grant |
| `gh api` | `/tmp` | Real scope | Persistent global grant |
| `wc` | Unknown after the sequence boundary | `/tmp` intent | Reviewed-safe policy |
| `head` | Unknown after the sequence boundary | `/tmp` intent | Reviewed-safe policy |

Netclaw allows the call when all four rows have coverage. It does not change
the command, its arguments, its execution directory, or model history.

ShellSyntaxTree starts intent only after an exact working-directory change on
success. The next action must be success-gated with `&&`. Both prerequisites
need one-time, session, or stored authority.

Netclaw does not inspect names such as `cd`, `command cd`, or `builtin cd`.
It consumes ShellSyntaxTree's closed working-directory effect. An unchanged
effect preserves intent. An unknown effect invalidates it.

The intent stops after an unknown directory change, an alternate branch, a
scope join, a group, a subshell, dynamic flow, or unsupported syntax.
Netclaw also validates every possible fallback directory. A prior symlink
target cannot become a later fallback. On POSIX, the policy resolves the
runtime temp root and the conventional `/tmp` alias independently. Safe alias
descendants map to their canonical host path.

Session grants can cover prerequisites. Folder grants use each prerequisite's
real scope. Intent scope cannot convert a folder near miss into coverage.

Only a reviewed diagnostic without a file-output redirect can use the intent.
Protected paths and folder grants always use real execution facts. Headless
runs and native PowerShell do not receive this reviewed-safe authority.

#### Example: a finite Bash loop over known files

Input:
Expand Down
65 changes: 41 additions & 24 deletions openspec/changes/structure-shell-approval-policy/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,33 +170,37 @@ convert deny or prompt to allow, and its output cannot enter a stored grant.
directory. Hard deny, protected paths, folder grants, noninteractive authority,
and process execution use it.

For Bash only, `Intent` can carry the exact target of a leading authored
directory transition. The transition begins on the success edge of `cd TARGET
&& ...`. It may remain the user's approval scope for later top-level diagnostic
occurrences until invalidated, even when a semicolon means runtime failure
would continue in the original directory. This is an approval-intent fact, not
a runtime cwd claim.
For Bash only, `Intent` can carry an exact target from ShellSyntaxTree 0.3.4.
The leading occurrence must publish `ChangesOnSuccess(Exact(target))`. Its next
top-level action must be success-gated with `&&`. Intent may remain the user's
approval scope for later top-level diagnostics. A semicolon can still execute a
tail in the original directory after failure. Intent is an approval fact, not a
runtime cwd claim.

Intent is invalidated by:

- a later directory mutation whose exact target is unavailable;
- a later `Unknown` working-directory effect;
- a later `ChangesOnSuccess` effect whose target is not exact;
- `||`, alternate branches, or a join whose incoming intent scopes differ;
- entry to or exit from a subshell/group boundary unless both sides retain the
same proved intent;
- dynamic identity, command substitution controlling flow, or unsupported
control flow.

An exact later success-gated `cd` replaces intent on its success edge. Relative
authored paths under intent are rebased only for safe-policy scope; protected
path evaluation also checks their real execution projection. Folder grants
never use intent.

An intent target is eligible only when it is exact, absolute, normalized,
symlink-free, and allowed by protected-path policy; the `cd` candidate and the
first non-navigation action on its success edge must already have one-time,
session, or stored-grant coverage. This existing user authority is what lets a
later reviewed diagnostic consume intent even when the target is not a normal
session/project safe root. Safe policy alone cannot manufacture causal intent.
An exact later success-gated `ChangesOnSuccess` effect replaces intent. An
`Unchanged` effect preserves it. Netclaw does not identify transition verbs.
Relative authored paths under intent are rebased only for safe-policy scope.
Protected-path evaluation also checks their real execution projection. Folder
grants never use intent.

An intent target is eligible only when it is exact, absolute, normalized, and
allowed by protected-path policy. It must contain no symlink segment. The
captured platform temporary alias can map to its canonical root. No other
symlink target is eligible. The `cd` candidate and the first non-navigation
action on its success edge must already have one-time, session, or stored-grant
coverage. This user authority lets a later reviewed diagnostic consume intent
outside a normal session or project safe root. Safe policy alone cannot create
causal intent.

Only a reviewed diagnostic entry and an occurrence without a file-writing
redirect can consume eligible intent. Native
Expand Down Expand Up @@ -325,7 +329,7 @@ filesystem resolution. This rule applies to both execution and the eligibility
probe. An invalid path returns a bounded error and cannot enter model history,
child scope, or project-instruction lookup as a successful declaration.

### 7. Consume ShellSyntaxTree 0.3.1 facts through 0.3.3 explicitly
### 7. Consume ShellSyntaxTree 0.3.1 facts through 0.3.4 explicitly

Netclaw uses effective `AnalyzedArgument.Value` for runtime-sensitive checks.
It may use `AuthoredValue` for approval matching only after the maintainer
Expand All @@ -340,6 +344,10 @@ accepts only `Exact` and `FiniteSet`. Each value enters `ToolPathPolicy` and the
approval scope check. Unknown and all other alternatives stay strict. Netclaw
does not infer the role from an executable's private grammar.

ShellSyntaxTree 0.3.4 publishes each occurrence's working-directory effect.
The causal projection consumes this closed fact directly. It never parses a
directory command name, alias, option, or operand.

`AuthoredPathShape` is lexical shape only. It may make review stricter, but it
never establishes that an executable treats an argument as a filesystem
operand and never creates filesystem authority. Repository slugs, container
Expand Down Expand Up @@ -408,13 +416,22 @@ Complete D03 example:

```text
Input: cd /tmp && gh api ... > slopwatch.log 2>&1; wc -c slopwatch.log; head -100 slopwatch.log
Preflight: unresolved approval scope; no reusable candidate rows
Trace: Completion/RequiresApproval/UncoveredCandidates
Final: RequiresApproval(IsMessy=true)
Execution: real scopes remain unchanged
Prerequisites: cd and gh api use persistent global grants
Intent: wc and head use the exact protected-path-safe /tmp target
Trace: two StoredGrantMatch rows, two ReviewedSafePolicy rows, then Completion/Allow
Final: Allow(AllCandidatesCovered)
```

This current result remains explicit evidence debt. The fixture does not claim
that a future session-scratch correction or stronger parser fact already exists.
The fixture keeps each causal role and prerequisite ID explicit. It does not
replace execution scope with approval intent.

The policy validates each runtime fallback before reviewed-safe coverage. A
prior target cannot become a later fallback through a symlink. POSIX policy
captures the conventional `/tmp` alias independently of the runtime temp root.
It maps that alias and its safe descendants to the host-resolved canonical
root. Parser-published working-directory effects identify each transition.
Netclaw does not inspect transition command names.

## Risks / Trade-offs

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,16 +78,31 @@
{ "tokens": ["head"], "proof": "ReviewedDiagnostic" }
]
},
"candidates": [],
"candidates": [
{ "id": 0, "tokens": ["cd"], "realDirectory": "/tmp", "intentDirectory": null, "role": "CausalPrerequisite", "prerequisiteIds": [], "expectedCoverage": "PersistentGlobal" },
{ "id": 1, "tokens": ["gh", "api"], "realDirectory": "/tmp", "intentDirectory": null, "role": "CausalPrerequisite", "prerequisiteIds": [], "expectedCoverage": "PersistentGlobal" },
{ "id": 2, "tokens": ["wc"], "realDirectory": null, "intentDirectory": "/tmp", "role": "CausalIntentConsumer", "prerequisiteIds": [0, 1], "expectedCoverage": "ReviewedSafePolicy" },
{ "id": 3, "tokens": ["head"], "realDirectory": null, "intentDirectory": "/tmp", "role": "CausalIntentConsumer", "prerequisiteIds": [0, 1], "expectedCoverage": "ReviewedSafePolicy" }
],
"shellEffects": {
"redirects": [
{ "commandIndex": 1, "target": "/tmp/slopwatch.log", "mode": "Output", "expectedPathPolicy": "Allow" }
],
"workingDirectoryEffects": [
{ "commandIndex": 0, "kind": "ChangesOnSuccess", "targets": ["/tmp"] },
{ "commandIndex": 1, "kind": "Unchanged", "targets": [] },
{ "commandIndex": 2, "kind": "Unchanged", "targets": [] },
{ "commandIndex": 3, "kind": "Unchanged", "targets": [] }
]
},
"expectedTrace": [
{ "stage": "Completion", "candidateId": null, "executableBasename": null, "outcome": "RequiresApproval", "reason": "UncoveredCandidates", "coverage": null, "scopeRelation": "None", "grantTimestamp": null }
{ "stage": "StoredGrantMatch", "candidateId": 0, "executableBasename": "cd", "outcome": "Covered", "reason": "PersistentGlobalGrant", "coverage": "PersistentGlobal", "scopeRelation": "Global", "grantTimestamp": "2026-08-13T00:00:00.0000000+00:00" },
{ "stage": "StoredGrantMatch", "candidateId": 1, "executableBasename": "gh", "outcome": "Covered", "reason": "PersistentGlobalGrant", "coverage": "PersistentGlobal", "scopeRelation": "Global", "grantTimestamp": "2026-08-13T00:00:00.0000000+00:00" },
{ "stage": "ReviewedSafePolicy", "candidateId": 2, "executableBasename": "wc", "outcome": "Covered", "reason": "ReviewedSafePhrase", "coverage": "ReviewedSafePolicy", "scopeRelation": "UnderIntentRoot", "grantTimestamp": null },
{ "stage": "ReviewedSafePolicy", "candidateId": 3, "executableBasename": "head", "outcome": "Covered", "reason": "ReviewedSafePhrase", "coverage": "ReviewedSafePolicy", "scopeRelation": "UnderIntentRoot", "grantTimestamp": null },
{ "stage": "Completion", "candidateId": null, "executableBasename": null, "outcome": "Allow", "reason": "AllCandidatesCovered", "coverage": null, "scopeRelation": "None", "grantTimestamp": null }
],
"expectedFinal": { "outcome": "RequiresApproval", "reason": "UncoveredCandidates", "approvalCandidates": [], "isMessy": true, "agentCorrection": null }
"expectedFinal": { "outcome": "Allow", "reason": "AllCandidatesCovered" }
},
{
"evidenceId": "D07",
Expand Down
5 changes: 3 additions & 2 deletions openspec/changes/structure-shell-approval-policy/proposal.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ session and persistent grant snapshots.
- Emit a bounded, redacted decision trace that also supplies near-miss data.
- Adopt the authored/effective fact separation introduced in ShellSyntaxTree
0.3.1 and the authored filesystem fact from public 0.3.3.
- Consume ShellSyntaxTree 0.3.4 working-directory effects for causal intent.
- Use the 0.3.3 authored filesystem fact for D14. Accept only exact or finite
values. Check each path through product policy. Lexical path shape alone
cannot create file authority.
Expand Down Expand Up @@ -55,6 +56,6 @@ No production branch will parse an executable's private options or operands.
per-candidate coverage from one atomic snapshot.
- UX: covered diagnostic chains stop prompting; unresolved syntax remains
one-time-only.
- Dependencies: implementation consumes public ShellSyntaxTree 0.3.3 for the
authored, effective, and authored filesystem facts.
- Dependencies: implementation consumes public ShellSyntaxTree 0.3.4 for the
authored, effective, filesystem, and working-directory effect facts.
- Documentation/evals: operator guidance and approval behavioral evals change.
Original file line number Diff line number Diff line change
Expand Up @@ -159,20 +159,26 @@ mismatched actor result, or impossible transition SHALL produce terminal deny.
### Requirement: Causal approval intent is separate from execution scope

The system SHALL keep canonical execution facts unchanged. For Bash only, it
MAY derive an approval-intent directory from an exact leading authored
directory transition on the success edge of `cd TARGET && ...`.
MAY derive approval intent from a leading ShellSyntaxTree 0.3.4 occurrence.
That occurrence SHALL publish `ChangesOnSuccess(Exact(target))`. Its next
top-level action SHALL be success-gated with `&&`.

Intent MAY continue through later top-level diagnostic statements until a
later directory mutation, differing control-flow join, alternate branch,
subshell/group boundary, dynamic flow, or unsupported region invalidates it. An
exact later success-gated directory transition SHALL replace intent on its
success edge.

An intent target SHALL be eligible only when exact, absolute, normalized,
symlink-free, and allowed by protected-path policy. The directory-transition
candidate and first non-navigation action on its success edge SHALL already
have one-time, session, or stored-grant coverage. Safe policy alone SHALL NOT
manufacture causal intent.
exact later success-gated `ChangesOnSuccess` effect SHALL replace intent.
`Unchanged` SHALL preserve intent. `Unknown` or a non-exact change target SHALL
invalidate intent. Causal and temporary-scope policy SHALL NOT identify
directory-transition verbs.

An intent target SHALL be eligible only when exact, absolute, normalized, and
allowed by protected-path policy. It SHALL contain no symlink segment. Every
possible fallback directory SHALL meet the same rule. A captured platform
temporary alias and its descendants MAY map to its canonical root. POSIX hosts
MAY also capture the conventional `/tmp` alias. No other symlink target SHALL
be eligible. The directory-transition candidate and first non-navigation
action on its success edge SHALL already have one-time, session, or
stored-grant coverage. Safe policy alone SHALL NOT create causal intent.

Only a reviewed diagnostic candidate without a file-writing
redirect MAY consume eligible intent. Hard deny, protected paths, folder
Expand Down Expand Up @@ -200,6 +206,62 @@ scope from `Set-Location`.
- **THEN** the tail has unknown intent
- **AND** safe policy cannot use the earlier `/tmp` intent

#### Scenario: Parser-owned wrappers establish and replace intent

- **WHEN** Bash reports `ChangesOnSuccess(Exact("/tmp"))` for `command cd /tmp`
- **THEN** the effect can establish causal intent
- **AND** no Netclaw command-name rule is consulted

#### Scenario: Directory-stack effect invalidates intent

- **GIVEN** intent is `/tmp`
- **WHEN** a later `pushd` or `popd` occurrence reports `Unknown`
- **THEN** no later diagnostic receives the earlier intent

#### Scenario: Failure-only transition shape does not create intent

- **WHEN** Bash reports `Unchanged` for `cd /tmp extra`
- **THEN** no causal intent is created
- **AND** Netclaw does not reinterpret the command's private arguments

#### Scenario: Arbitrary symlink target cannot create intent

- **GIVEN** `/work/alias` is a symlink to another directory
- **WHEN** source starts with `cd /work/alias && inspect`
- **THEN** no causal approval intent is eligible
- **AND** the captured platform temporary alias remains a separate bounded
exception

#### Scenario: Earlier symlink target cannot become a fallback

- **GIVEN** an earlier exact intent target crosses a symlink
- **WHEN** a later eligible transition replaces intent
- **THEN** the earlier target fails fallback eligibility
- **AND** no later diagnostic receives reviewed-safe intent coverage

#### Scenario: Protected fallback denial stays terminal

- **GIVEN** an earlier fallback alias resolves into a protected directory
- **WHEN** a later intent candidate also fails symlink eligibility
- **THEN** protected-path policy denies before the eligibility check
- **AND** the system does not offer a one-time approval prompt

#### Scenario: Conventional macOS tmp alias remains eligible

- **GIVEN** the host runtime temp root differs from `/tmp`
- **AND** the POSIX `/tmp` alias resolves to `/private/tmp`
- **WHEN** intent targets `/tmp` or one of its safe descendants
- **THEN** causal policy validates the canonical `/private/tmp` path
- **AND** arbitrary POSIX symlink aliases remain strict

#### Scenario: Session and folder grants use real prerequisite scope

- **GIVEN** session or persistent-folder authority covers each prerequisite
- **WHEN** causal policy checks a diagnostic tail
- **THEN** prerequisite coverage can establish intent
- **AND** a folder grant matches only the prerequisite's real scope
- **AND** intent scope cannot convert a folder near miss into coverage

#### Scenario: Alternate branch does not leak intent

- **WHEN** source is `cd /tmp && inspect || recover; head result.log`
Expand Down Expand Up @@ -814,6 +876,10 @@ authored values only for the approved approval perspective. It SHALL route
ShellSyntaxTree 0.3.3 authored filesystem values through path policy. Unknown
policy-relevant values SHALL not create reusable or safe coverage.

Netclaw SHALL consume ShellSyntaxTree 0.3.4 working-directory effects for the
bounded Bash causal projection. It SHALL NOT derive equivalent effects from
command names or executable-private grammar.

Deny-only defensive scans MAY deny incomplete input but SHALL never authorize
it.

Expand Down
Loading
Loading