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.0-alpha" />
<PackageVersion Include="ShellSyntaxTree" Version="0.3.0-alpha.1" />
<PackageVersion Include="Termina" Version="0.16.1" />
</ItemGroup>
<!-- Serialization -->
Expand Down
11 changes: 9 additions & 2 deletions IMPLEMENTATION_PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,8 +141,15 @@ Done when:
explicit Bash redirect facts for the existing grammar.
- [x] Unknown occurrences, cwd facts, wrappers, and redirects stay prompt-only.
Static descriptor redirects no longer appear dynamic.
- [ ] Netclaw interprets bounded loop arguments and promotes the new mutation
cases after the next ShellSyntaxTree prerelease.
- [x] Netclaw consumes ShellSyntaxTree `0.3.0-alpha.1` and promotes Bash
command-resolution mutation and reserved execution forms into the strict
181-case review matrix.
- [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.
- [ ] Netclaw interprets bounded loop arguments only after the executor can
prove the Bash initial variable state. The inherited shell state remains
fail closed because an ambient nameref can change assignment semantics.

### Priority: Simplify Tool Execution Context Architecture

Expand Down
2 changes: 2 additions & 0 deletions openspec/changes/adopt-shellsyntax-alpha1/.openspec.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-08-09
105 changes: 105 additions & 0 deletions openspec/changes/adopt-shellsyntax-alpha1/design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
## Context

Netclaw uses ShellSyntaxTree facts before it reuses a shell approval. The first
v0.3 alpha lacks the later Bash redirect and command-resolution behavior.

The current consumer marks all heredocs and here strings unresolved. This rule
is safe, but it causes prompts for complete data sent to a non-interpreter.

The shell gate runs before actor dispatch. This change does not alter actor
messages, actor ownership, persisted grants, or recovery behavior.

## Goals / Non-Goals

**Goals:**

- Adopt ShellSyntaxTree `0.3.0-alpha.1`.
- Allow complete bounded stdin data for one constrained receiver grammar.
- Keep unknown and unsupported shell forms strict.
- Preserve complete command text in approval displays.
- Pin each decision in the review matrix.

**Non-Goals:**

- Add a general stdin receiver catalog.
- Change grant storage or approval button behavior.
- Migrate PowerShell shell analysis.
- Add a safe `sed` grammar.

## Decisions

### Use the typed redirect operation

The consumer will use `RedirectOperation.HereDocument` and `HereString`. It will
not infer these forms from a compatibility redirect target.

The display formatter will encode raw line breaks with a visible `⏎` marker
for both operations. This choice preserves `<<` or `<<<`, the data, and each
authored boundary in a single-line prompt.

Alternative: reconstruct these redirects from `Clause.Redirects`. That model
cannot preserve the v0.3 redirect operation and can misstate `<<<` as `<`.

### Start with one data-only receiver grammar

The safe grammar will require all these facts:

- The occurrence is complete.
- The verb chain contains only `cat`.
- The command has no authored arguments.
- The redirect uses the default stdin source or explicit descriptor zero.
- The redirect is complete and is not path-relevant.
- A heredoc is complete, literal, and has complete body provenance.
- A here-string target domain is `Exact` or `FiniteSet`.

A heredoc uses its expansion mode and authored body facts. A here string uses
its target domain. Every expanding heredoc, other receiver, or unproved value
stays unresolved and prompts.

Netclaw will keep its established transparent analysis for a direct shell
dispatch such as `bash -c`. A receiver wrapper such as `command cat` does not
match the one-token receiver grammar and stays unresolved. The parser clears
outer source spans after a safe command-string decode. Netclaw accepts paired
unavailable spans only on those decoded occurrences and still requires the raw
delimiter, raw body, literal mode, and complete facts.

Alternative: allow all safe verbs. That choice is unsafe because some programs
interpret stdin as code, options, or a policy-sensitive language.

### Keep every other redirect decision unchanged

The consumer will still evaluate file redirects, descriptor redirects, hard
deny rules, protected paths, and every command occurrence. An output redirect
on the same `cat` command still receives its independent path decision.

### Use the review matrix as the downstream contract

The matrix will cover exact data, unknown data, interpreter receivers, stored
grants, command-resolution mutation, and reserved execution syntax. Focused
tests will pin display text and parser integration.

## Risks / Trade-offs

- [Risk] The `cat` grammar excludes useful receivers. -> The narrow boundary
prevents an unsafe receiver classification. Later slices can add proved
receivers.
- [Risk] A future package changes an enum or value-domain shape. -> Unknown
enum values and incomplete facts stay strict.
- [Risk] A parser failure removes approval candidates. -> Netclaw offers only
one-shot approval and deny. It does not persist a grant.
- [Risk] A data-only stdin redirect also has a file output. -> Netclaw evaluates
the file redirect separately.

## Migration Plan

1. Update the central package version.
2. Add focused analysis and display tests.
3. Add the review matrix cases.
4. Run the security and actor test suites.

Rollback restores the previous package and consumer rule. No persisted state
or configuration needs conversion.

## Open Questions

None.
45 changes: 45 additions & 0 deletions openspec/changes/adopt-shellsyntax-alpha1/proposal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
## Why

PRD-002 and PRD-006 require default-deny shell approval with useful approval
reuse. Netclaw must adopt ShellSyntaxTree `0.3.0-alpha.1` because the first alpha
predates Bash command-resolution hardening, heredoc facts, and here-string facts.

## What Changes

- Update Netclaw from ShellSyntaxTree `0.3.0-alpha` to `0.3.0-alpha.1`.
- Correct the approval display contract for the v0.3 occurrence and redirect
model. Raw command text remains the fallback when facts are incomplete.
- Add strict matrix cases for Bash command-resolution mutation and reserved
execution forms.
- Add a narrow data-only stdin grammar for argument-free `cat` with a complete
literal heredoc or exact and finite here-string data.
- Add allow and prompt cases for complete and unknown Bash stdin data.
- Keep hard-deny, protected-path, dynamic-value, and incomplete-parse behavior
fail closed.

Transparent direct shell dispatch remains subject to complete recursive
analysis. Receiver wrappers such as `command cat` stay strict.

In scope: Bash approval analysis, the package reference, the approval matrix,
and the canonical approval specification.

Out of scope: PowerShell migration, stable ShellSyntaxTree v0.3, new grant
shapes, broad stdin interpreter grammars, and the deferred safe `sed` grammar.

## Capabilities

### New Capabilities

- None.

### Modified Capabilities

- `tool-approval-gates`: Align approval display and reuse rules with complete
v0.3 command-occurrence and redirect facts.

## Impact

- Code: Bash shell analysis and its package dependency.
- Tests: the review matrix and focused security tests.
- Security: unknown, incomplete, mutating, and unsupported forms stay strict.
- Operations: no configuration or migration changes are required.
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
## ADDED Requirements

### Requirement: Bounded Bash stdin data has a constrained receiver grammar

Netclaw SHALL treat Bash heredoc and here-string data as resolved only when all
required receiver and data facts are complete. The initial receiver grammar
SHALL accept only argument-free `cat`. It SHALL require a complete literal
heredoc or an exact or finite here-string target.

The grammar SHALL use the heredoc expansion mode and authored body provenance.
It SHALL use `RedirectAnalysis.Target` for here strings. It SHALL reject
expanding heredocs, unknown domains, incomplete redirects, path-relevant
redirects, non-stdin source descriptors, authored arguments, receiver wrappers,
and every other receiver. A complete direct shell dispatch MAY expose its inner
receiver through Netclaw's established recursive analysis.

Netclaw SHALL evaluate every other redirect on the occurrence independently.
Stored approval SHALL NOT bypass an unresolved stdin redirect.

#### Scenario: Exact here string to cat can use the trusted scope

- **GIVEN** an argument-free `cat` command in a trusted project directory
- **WHEN** its complete here string has exact data
- **THEN** the stdin redirect does not require a separate approval
- **AND** the normal safe-verb and path rules decide the command

#### Scenario: Literal heredoc to cat can use the trusted scope

- **GIVEN** an argument-free `cat` command in a trusted project directory
- **WHEN** its complete literal heredoc has complete authored body provenance
- **THEN** the stdin redirect does not require a separate approval
- **AND** the normal safe-verb and path rules decide the command

#### Scenario: Unknown here-string data stays strict

- **GIVEN** `cat <<< "$value"` in a trusted project directory
- **WHEN** the parser cannot prove the data value
- **THEN** Netclaw requires one-shot approval or deny
- **AND** Netclaw offers no persistent approval candidate

#### Scenario: Interpreter stdin stays strict

- **GIVEN** an interpreter receives a complete literal heredoc or here string
- **WHEN** Netclaw evaluates the redirect
- **THEN** Netclaw requires one-shot approval or deny
- **AND** an existing interpreter grant does not bypass the stdin decision

### Requirement: Bash command-resolution mutation stays strict

Netclaw SHALL use the pinned ShellSyntaxTree result as the structural authority.
An unparseable command-resolution mutation or reserved execution form SHALL
produce no persistent approval candidate.

This rule SHALL cover unsupported `exec`, mutating `hash`, alias changes,
shell-option changes, builtin-enable changes, `time`, negation, coprocesses, and
current-shell brace groups.

#### Scenario: Command-resolution mutation cannot reuse a grant

- **GIVEN** a command changes command resolution before another occurrence
- **AND** stored grants cover each visible command name
- **WHEN** ShellSyntaxTree marks the full command unparseable
- **THEN** Netclaw requires one-shot approval or deny
- **AND** Netclaw offers no persistent approval candidate

#### Scenario: Reserved execution form cannot flatten into a safe command

- **GIVEN** an unsupported reserved execution form contains a safe verb
- **WHEN** ShellSyntaxTree marks the full command unparseable
- **THEN** Netclaw does not authorize the visible safe verb
- **AND** Netclaw offers no persistent approval candidate

## MODIFIED Requirements

### Requirement: Five-button approval prompt with verb-and-directory framing

When the approval gate prompts the user, the prompt SHALL render five
buttons in one row: `Once`, `This chat`, `Always here`, `Always anywhere`,
`Deny`. The buttons `Always anywhere` and `Deny` SHALL be styled as
danger (Slack `style: "danger"`, Discord `ButtonStyle.Danger`). All
button labels SHALL fit within Slack's 76-character and Discord's
80-character button-text caps.

The prompt body SHALL show the cwd in the header
(`Approve in <cwd> ?`) and the extracted verb chains as a bulleted list.
Single-verb commands MAY collapse the list into the header
(`Approve <verb> in <cwd> ?`). The body SHALL NOT render separate
"Patterns" or "Directory Roots" sections.

The display text for a shell command SHALL be single-line. A command
containing embedded line breaks (LF or CR) SHALL be reconstructed from
its parse tree: statement separators render as explicit operators (`;`,
`&&`, `||`, `|`) and each multi-line argument or redirect target is
replaced with a `(N lines, M chars)` size summary instead of its
verbatim content (issue #1402) — channel renderers embed the display
text in single-line code fences, and dumping a multi-line quoted blob
verbatim corrupts the prompt layout. When the parser cannot decompose
the command, line breaks SHALL be flattened to spaces.

Commands with heredocs, here strings, or subshell groups SHALL NOT use the
compatibility-clause reconstruction. The formatter SHALL detect heredocs and
here strings from their typed v0.3 redirect operations. It SHALL encode each
raw line break as a visible `⏎` marker so the display keeps each redirect
operator, data body, and execution boundary. Subshell groups SHALL use the same
fallback because a flat clause sequence cannot preserve their grouping.

Button semantics:

- `Once` SHALL run the command this one time and persist nothing.
- `This chat` SHALL allow the extracted verbs in the prompt's directory
for the rest of the session, stored in session-scoped memory only.
- `Always here` SHALL persist `(verb, prompt's directory)` entries to
`tool-approvals.json` for each extracted verb.
- `Always anywhere` SHALL persist `(verb, null)` entries for each extracted
verb — the global wildcard.
- `Deny` SHALL refuse this call only. Denying a verb SHALL NOT ban it
for future invocations.

#### Scenario: Compound command shows verbs as bullets

- **GIVEN** the agent invokes `shell_execute` with command
`cd ~/repos/foo && git remote -v && git rev-parse HEAD`
and cwd `~/repos/foo/`
- **WHEN** the approval prompt is rendered on Slack
- **THEN** the body header reads `Approve in ~/repos/foo/ ?`
- **AND** the verbs `cd`, `git remote`, `git rev-parse` appear as bullets
- **AND** the action row contains five buttons
- **AND** `Always anywhere` and `Deny` are styled as danger

#### Scenario: Always here persists folder-scoped entries

- **GIVEN** an approval prompt for verbs `git remote`, `git rev-parse`
in cwd `~/repos/foo/`
- **WHEN** the user clicks `Always here`
- **THEN** `tool-approvals.json` gains entries
`{"verb": "git remote", "directory": "~/repos/foo/"}` and
`{"verb": "git rev-parse", "directory": "~/repos/foo/"}`
- **AND** the resolution message reads
`Saved: git remote, git rev-parse in ~/repos/foo/`

#### Scenario: Always anywhere persists global entries

- **GIVEN** an approval prompt for verb `freshdesk` in cwd `~/.netclaw/sessions/<id>/`
- **WHEN** the user clicks `Always anywhere`
- **THEN** `tool-approvals.json` gains entry
`{"verb": "freshdesk", "directory": null}`
- **AND** the resolution message reads `Saved: freshdesk anywhere`

#### Scenario: This chat persists session-scoped only

- **GIVEN** an approval prompt for verb `jsonlint` in cwd `~/repos/foo/`
- **WHEN** the user clicks `This chat`
- **THEN** session-scoped memory records `(jsonlint, ~/repos/foo/)`
- **AND** `tool-approvals.json` is NOT modified
- **AND** a new session prompts again

#### Scenario: Deny refuses only the current call

- **GIVEN** an approval prompt for verb `git push`
- **WHEN** the user clicks `Deny`
- **THEN** the current call is refused
- **AND** `tool-approvals.json` is NOT modified
- **AND** a later `git push` call still prompts

#### Scenario: Multi-line quoted argument summarized in display text

- **GIVEN** the agent invokes `shell_execute` with command
`freshdesk ticket reply 605 --message "Hi,⏎We've rolled out a fix. Please verify."`
where the quoted argument spans two lines
- **WHEN** the approval prompt is rendered
- **THEN** the display text reads
`freshdesk ticket reply 605 --message (2 lines, 42 chars)`
- **AND** the display text contains no newline characters

#### Scenario: Heredoc display keeps the full raw command

- **GIVEN** a multi-line command contains a complete heredoc
- **WHEN** the approval prompt formats the command
- **THEN** the single-line display keeps the `<<` operator and body text
- **AND** the formatter does not rebuild the command from compatibility clauses

#### Scenario: Here-string display keeps the authored operator

- **GIVEN** a multi-line command contains a complete `<<<` redirect
- **WHEN** the approval prompt formats the command
- **THEN** the single-line display keeps the `<<<` operator and data text
- **AND** each authored line break renders as a visible `⏎` marker
- **AND** the formatter does not replace `<<<` with `<`

#### Scenario: Heredoc display keeps a following command boundary

- **GIVEN** a complete heredoc is followed by another command after its terminator
- **WHEN** the approval prompt formats the command
- **THEN** the single-line display places a visible `⏎` marker between the terminator and the following command
- **AND** the following command does not appear to be part of the heredoc body
Loading
Loading