Skip to content

Commit cae0019

Browse files
committed
docs: define strict and general approval matching
1 parent 870858d commit cae0019

7 files changed

Lines changed: 90 additions & 25 deletions

File tree

IMPLEMENTATION_PLAN.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,10 @@ priorities.
7676
`Invoke-Expression` recursion work: static expansion clears unmappable
7777
outer spans, while dynamic payloads retain conservative source-aligned
7878
elements. Nested and dynamic `bash -c` cases pin the equivalent Bash
79-
boundary.
79+
boundary. Consumer guidance separates strict authored-stream matching
80+
from general executable-aware normalization; Netclaw can use the latter
81+
for reusable approvals without treating parser verb roles as semantic
82+
command boundaries.
8083
- [x] **Issue #64 — path-shaped operands after native verb chains.**
8184
Stop the Bash and PowerShell native greedy passes before a token that
8285
matches the shared path-shape rules. Preserve that token as a resolved

PROJECT_CONTEXT.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,9 @@ open-source autonomous operations agent. Netclaw's POSIX approval gate consumes
4242
ShellSyntaxTree's Bash parser to decompose approval units, identify candidate
4343
verbs and directories, propagate cwd context, inspect redirects, and fail
4444
closed when parsing is uncertain. Its PowerShell integration is the remaining
45-
v0.2.0 downstream acceptance item. See
45+
v0.2.0 downstream acceptance item. Netclaw is expected to use the consumer
46+
guide's general executable-aware matching path for supported commands, with
47+
strict authored-stream matching as the fallback for unrecognized shapes. See
4648
[`docs/CONSUMER_GUIDE.md`](./docs/CONSUMER_GUIDE.md) for the public consumer
4749
algorithm and immutable Netclaw examples.
4850

SPEC.md

Lines changed: 25 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -740,25 +740,31 @@ syntactic rule disambiguates `origin` (a branch name) from `worktree`
740740
(a subcommand verb) without per-CLI semantic knowledge — and we will
741741
not bake per-CLI knowledge into the parser.
742742

743-
Consumers needing security-grade verb identification should pattern-prefix
744-
match against the source-ordered `Clause.Elements` view:
745-
746-
> A command matches an approval pattern `P` if and only if the first
747-
> `len(P.verb_prefix)` verb elements of the command equal `P.verb_prefix`.
748-
749-
This punts depth choice to the consumer (via the pattern they author)
750-
and accommodates the parser's over-extraction transparently:
751-
752-
- Pattern `git push *` (verb-prefix length 2) matches `git push origin
753-
main` because the first two command tokens are `[git, push]`.
754-
- Pattern `kubectl get pods *` (verb-prefix length 3) matches
755-
`kubectl get pods my-pod` because the first three tokens are
756-
`[kubectl, get, pods]`.
757-
- Auto-proposed patterns for unknown commands should default to
758-
the **full** extracted verb chain (greedy match), which is the
759-
security-correct default: a subsequent variation re-prompts rather
760-
than silently auto-grants. Operators wanting broader grants opt in
761-
explicitly.
743+
Consumers needing security-grade command identification choose one of two
744+
strategies over the source-ordered `Clause.Elements` view:
745+
746+
1. **Strict authored-stream matching.** Match every modeled significant
747+
element in source order. A strict matcher may define explicit operand slots
748+
or wildcards, but it SHALL NOT discard an intervening argument merely
749+
because the parser assigned it `Role=Argument`. Therefore a strict
750+
`git commit` pattern does not match `git -C /repo commit`.
751+
2. **General executable-aware matching.** Pass the complete authored stream to
752+
a grammar owned by the consumer. The grammar consumes known options and
753+
operands, identifies the executable's semantic command, and returns both a
754+
normalized approval identity and every policy-relevant operand or scope.
755+
Equivalent syntax may reuse an approval only after complete interpretation.
756+
757+
For example, a Git-aware matcher may interpret `git -C /repo commit` as the
758+
general identity `git commit` with effective directory `/repo`. It may then
759+
reuse a `git commit` approval only when that approval's directory policy covers
760+
`/repo`. Likewise, executable-aware matchers may intentionally normalize
761+
`git push origin main` to `git push` or `kubectl get pods my-pod` to
762+
`kubectl get pods` when their grammars establish which suffixes are operands.
763+
764+
There is no shell-generic rule that selects all `Role=Verb` elements and
765+
compares them as a contiguous semantic prefix. For unknown executables or an
766+
unrecognized option shape, consumers should use strict matching or prompt;
767+
they should not silently fall back to a broader general identity.
762768

763769
False-negative (re-prompt) is recoverable. False-positive (silent
764770
destructive grant) is not. Narrow-by-default favors the recoverable

docs/CONSUMER_GUIDE.md

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -165,9 +165,41 @@ audit UI can still show `gci`.
165165
`VerbChain` is a best-effort syntactic hint, not a complete executable grammar.
166166
The greedy native-command walk can include bare lowercase values because a
167167
generic parser cannot know whether `origin` is a Git remote or a subcommand.
168-
Unknown commands should therefore default to the full extracted chain, which
169-
produces narrower approvals and recoverable re-prompts. A consumer may shorten
170-
the chain only when it owns command-specific knowledge that justifies doing so.
168+
Unknown commands should therefore retain the complete authored shape through a
169+
strict pattern, producing narrower approvals and recoverable re-prompts. A
170+
consumer may normalize or shorten that shape only when it owns command-specific
171+
knowledge that justifies doing so.
172+
173+
### Choosing strict or general matching
174+
175+
`Clause.Elements` supports two security-conscious consumer strategies. The
176+
choice belongs to the approval product, not the parser.
177+
178+
**Strict matching** evaluates the significant authored stream in order. A
179+
pattern may contain explicit operand slots, but unexpected or intervening
180+
elements prevent a match. For example, a strict `git commit` pattern does not
181+
match `git -C /repo commit`, because `-C /repo` appears between the executable
182+
and subcommand. This mode is easy to audit and fail-closed, but syntactic
183+
variations can produce more prompts.
184+
185+
**General matching** uses an executable-aware interpreter. The interpreter
186+
consumes the complete element stream according to that executable's option
187+
grammar and returns a normalized approval identity plus the policy-relevant
188+
operands and scopes. A Git interpreter can normalize `git -C /repo commit` to
189+
`git commit` while retaining `/repo` as its effective-directory constraint.
190+
This preserves reusable approvals without treating the option as irrelevant.
191+
192+
General matching does not mean filtering to `Role=Verb` or trusting
193+
`PrecedingVerbElementCount` as a semantic boundary. Both fields describe the
194+
generic parser's projection. If the executable-aware interpreter encounters an
195+
unknown option, missing operand, dynamic value, or otherwise incomplete shape,
196+
it should fall back to strict matching or prompt rather than broaden the
197+
approval.
198+
199+
Netclaw is expected to use general matching for supported high-frequency
200+
commands so ordinary option placement does not create approval fatigue. Strict
201+
matching remains the safe fallback for commands whose grammar Netclaw does not
202+
yet understand.
171203

172204
## Evaluating arguments and paths
173205

@@ -347,7 +379,7 @@ The consumer can collect the attributed cwd and both path operands to propose
347379
read/write mounts. It should still apply its own cmdlet policy and access-mode
348380
rules; ShellSyntaxTree reports paths, not filesystem permissions.
349381

350-
### Command-aware policy
382+
### General command-aware policy
351383

352384
Input:
353385

openspec/changes/preserve-clause-element-order/design.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,19 @@ count alone, to interpret the command.
5858
This avoids redundant `VerbIndex` and `AfterVerbIndex` fields whose invariants
5959
could drift.
6060

61+
### Support strict and general consumer matching without defining either
62+
63+
A strict consumer matches the significant authored stream in order and rejects
64+
unexpected intervening elements. A general consumer may normalize equivalent
65+
forms only through an executable-aware grammar that completely consumes the
66+
stream and returns policy-relevant operands and scope alongside the normalized
67+
identity. Selecting every `Role=Verb` element is not a general matcher: it can
68+
silently skip options authored between an executable and subcommand.
69+
70+
This division keeps approval fatigue under consumer control. High-frequency
71+
executables can receive reusable command-aware approvals, while unknown or
72+
partially understood shapes retain a narrow strict fallback.
73+
6174
### Match native option spelling case-sensitively
6275

6376
Native executables receive option spelling unchanged under both Bash and

openspec/changes/preserve-clause-element-order/specs/clause-element-provenance/spec.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,12 @@ collection.
3232
- **THEN** `-C` has `PrecedingVerbElementCount` equal to `1`
3333
- **THEN** consumers SHALL NOT interpret that count alone as Git's semantic option scope
3434

35+
#### Scenario: Intervening arguments are not a generic verb prefix
36+
- **WHEN** either parser parses `git -C /repo commit`
37+
- **THEN** a strict consumer sees `git`, `-C`, `/repo`, and `commit` in that order
38+
- **THEN** it does not discard the two argument elements and generically match `git commit`
39+
- **THEN** a consumer may derive `git commit` only through a Git-aware interpretation that retains `/repo` as policy-relevant scope
40+
3541
#### Scenario: Verb-relative counts reset per clause
3642
- **WHEN** either parser parses `git -C /repo status | git commit -C HEAD~1`
3743
- **THEN** the first clause reports `-C` after one verb token

openspec/changes/preserve-clause-element-order/tasks.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@
4141
this implementation change release-shaped.
4242
- [x] 4.3 Rewrite the Git consumer example to enumerate every occurrence,
4343
bind operands, and derive Git semantics from all authored elements.
44+
- [x] 4.4 Document strict authored-stream and general executable-aware
45+
matching; forbid generic verb-role filtering and identify general
46+
matching as Netclaw's approval-fatigue mitigation.
4447

4548
## 5. Completion
4649

0 commit comments

Comments
 (0)