@dsh-cc/permission-rules 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.i18n.yaml +6 -0
- package/README.md +86 -0
- package/README.zh.md +86 -0
- package/lib/approval-listener.d.ts +50 -0
- package/lib/approval-listener.d.ts.map +1 -0
- package/lib/approval-listener.js +58 -0
- package/lib/approval-listener.js.map +1 -0
- package/lib/auto-stage.d.ts +138 -0
- package/lib/auto-stage.d.ts.map +1 -0
- package/lib/auto-stage.js +284 -0
- package/lib/auto-stage.js.map +1 -0
- package/lib/classifier.d.ts +57 -0
- package/lib/classifier.d.ts.map +1 -0
- package/lib/classifier.js +129 -0
- package/lib/classifier.js.map +1 -0
- package/lib/decide.d.ts +80 -0
- package/lib/decide.d.ts.map +1 -0
- package/lib/decide.js +127 -0
- package/lib/decide.js.map +1 -0
- package/lib/domain.d.ts +46 -0
- package/lib/domain.d.ts.map +1 -0
- package/lib/domain.js +103 -0
- package/lib/domain.js.map +1 -0
- package/lib/evaluate.d.ts +32 -0
- package/lib/evaluate.d.ts.map +1 -0
- package/lib/evaluate.js +176 -0
- package/lib/evaluate.js.map +1 -0
- package/lib/index.d.ts +123 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +380 -0
- package/lib/index.js.map +1 -0
- package/lib/invariant.d.ts +28 -0
- package/lib/invariant.d.ts.map +1 -0
- package/lib/invariant.js +54 -0
- package/lib/invariant.js.map +1 -0
- package/lib/llm-classifier.d.ts +107 -0
- package/lib/llm-classifier.d.ts.map +1 -0
- package/lib/llm-classifier.js +231 -0
- package/lib/llm-classifier.js.map +1 -0
- package/lib/matchers.d.ts +18 -0
- package/lib/matchers.d.ts.map +1 -0
- package/lib/matchers.js +43 -0
- package/lib/matchers.js.map +1 -0
- package/lib/mode.d.ts +91 -0
- package/lib/mode.d.ts.map +1 -0
- package/lib/mode.js +133 -0
- package/lib/mode.js.map +1 -0
- package/lib/parser.d.ts +91 -0
- package/lib/parser.d.ts.map +1 -0
- package/lib/parser.js +282 -0
- package/lib/parser.js.map +1 -0
- package/lib/session-allowlist.d.ts +76 -0
- package/lib/session-allowlist.d.ts.map +1 -0
- package/lib/session-allowlist.js +122 -0
- package/lib/session-allowlist.js.map +1 -0
- package/lib/settings-schema.d.ts +99 -0
- package/lib/settings-schema.d.ts.map +1 -0
- package/lib/settings-schema.js +64 -0
- package/lib/settings-schema.js.map +1 -0
- package/lib/types.d.ts +150 -0
- package/lib/types.d.ts.map +1 -0
- package/lib/types.js +33 -0
- package/lib/types.js.map +1 -0
- package/package.json +71 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
6
|
+
|
|
7
|
+
1. Definitions.
|
|
8
|
+
|
|
9
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
10
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
11
|
+
|
|
12
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
13
|
+
the copyright owner that is granting the License.
|
|
14
|
+
|
|
15
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
16
|
+
other entities that control, are controlled by, or are under common
|
|
17
|
+
control with that entity. For the purposes of this definition,
|
|
18
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
19
|
+
direction or management of such entity, whether by contract or
|
|
20
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
21
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
22
|
+
|
|
23
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
24
|
+
exercising permissions granted by this License.
|
|
25
|
+
|
|
26
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
27
|
+
including but not limited to software source code, documentation
|
|
28
|
+
source, and configuration files.
|
|
29
|
+
|
|
30
|
+
"Object" form shall mean any form resulting from mechanical
|
|
31
|
+
transformation or translation of a Source form, including but
|
|
32
|
+
not limited to compiled object code, generated documentation,
|
|
33
|
+
and conversions to other media types.
|
|
34
|
+
|
|
35
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
36
|
+
Object form, made available under the License, as indicated by a
|
|
37
|
+
copyright notice that is included in or attached to the work
|
|
38
|
+
(an example is provided in the Appendix below).
|
|
39
|
+
|
|
40
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
41
|
+
form, that is based on (or derived from) the Work and for which the
|
|
42
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
43
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
44
|
+
of this License, Derivative Works shall not include works that remain
|
|
45
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
46
|
+
the Work and Derivative Works thereof.
|
|
47
|
+
|
|
48
|
+
"Contribution" shall mean any work of authorship, including
|
|
49
|
+
the original version of the Work and any modifications or additions
|
|
50
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
51
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
52
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
53
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
54
|
+
means any form of electronic, verbal, or written communication sent
|
|
55
|
+
to the Licensor or its representatives, including but not limited to
|
|
56
|
+
communication on electronic mailing lists, source code control systems,
|
|
57
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
58
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
59
|
+
excluding communication that is conspicuously marked or otherwise
|
|
60
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
61
|
+
|
|
62
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
63
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
64
|
+
subsequently incorporated within the Work.
|
|
65
|
+
|
|
66
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
67
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
68
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
69
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
70
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
71
|
+
Work and such Derivative Works in Source or Object form.
|
|
72
|
+
|
|
73
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
74
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
75
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
76
|
+
(except as stated in this section) patent license to make, have made,
|
|
77
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
78
|
+
where such license applies only to those patent claims licensable
|
|
79
|
+
by such Contributor that are necessarily infringed by their
|
|
80
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
81
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
82
|
+
institute patent litigation against any entity (including a
|
|
83
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
84
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
85
|
+
or contributory patent infringement, then any patent licenses
|
|
86
|
+
granted to You under this License for that Work shall terminate
|
|
87
|
+
as of the date such litigation is filed.
|
|
88
|
+
|
|
89
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
90
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
91
|
+
modifications, and in Source or Object form, provided that You
|
|
92
|
+
meet the following conditions:
|
|
93
|
+
|
|
94
|
+
(a) You must give any other recipients of the Work or
|
|
95
|
+
Derivative Works a copy of this License; and
|
|
96
|
+
|
|
97
|
+
(b) You must cause any modified files to carry prominent notices
|
|
98
|
+
stating that You changed the files; and
|
|
99
|
+
|
|
100
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
101
|
+
that You distribute, all copyright, patent, trademark, and
|
|
102
|
+
attribution notices from the Source form of the Work,
|
|
103
|
+
excluding those notices that do not pertain to any part of
|
|
104
|
+
the Derivative Works; and
|
|
105
|
+
|
|
106
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
107
|
+
distribution, then any Derivative Works that You distribute must
|
|
108
|
+
include a readable copy of the attribution notices contained
|
|
109
|
+
within such NOTICE file, excluding those notices that do not
|
|
110
|
+
pertain to any part of the Derivative Works, in at least one
|
|
111
|
+
of the following places: within a NOTICE text file distributed
|
|
112
|
+
as part of the Derivative Works; within the Source form or
|
|
113
|
+
documentation, if provided along with the Derivative Works; or,
|
|
114
|
+
within a display generated by the Derivative Works, if and
|
|
115
|
+
wherever such third-party notices normally appear. The contents
|
|
116
|
+
of the NOTICE file are for informational purposes only and
|
|
117
|
+
do not modify the License. You may add Your own attribution
|
|
118
|
+
notices within Derivative Works that You distribute, alongside
|
|
119
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
120
|
+
that such additional attribution notices cannot be construed
|
|
121
|
+
as modifying the License.
|
|
122
|
+
|
|
123
|
+
You may add Your own copyright statement to Your modifications and
|
|
124
|
+
may provide additional or different license terms and conditions
|
|
125
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
126
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
127
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
128
|
+
the conditions stated in this License.
|
|
129
|
+
|
|
130
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
131
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
132
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
133
|
+
this License, without any additional terms or conditions.
|
|
134
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
135
|
+
the terms of any separate license agreement you may have executed
|
|
136
|
+
with Licensor regarding such Contributions.
|
|
137
|
+
|
|
138
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
139
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
140
|
+
except as required for reasonable and customary use in describing the
|
|
141
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
142
|
+
|
|
143
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
144
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
145
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
146
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
147
|
+
implied, including, without limitation, any warranties or conditions
|
|
148
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
149
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
150
|
+
appropriateness of using or redistributing the Work and assume any
|
|
151
|
+
risks associated with Your exercise of permissions under this License.
|
|
152
|
+
|
|
153
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
154
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
155
|
+
unless required by applicable law (such as deliberate and grossly
|
|
156
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
157
|
+
liable to You for damages, including any direct, indirect, special,
|
|
158
|
+
incidental, or consequential damages of any character arising as a
|
|
159
|
+
result of this License or out of the use or inability to use the
|
|
160
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
161
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
162
|
+
other commercial damages or losses), even if such Contributor
|
|
163
|
+
has been advised of the possibility of such damages.
|
|
164
|
+
|
|
165
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
166
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
167
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
168
|
+
or other liability obligations and/or rights consistent with this
|
|
169
|
+
License. However, in accepting such obligations, You may act only
|
|
170
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
171
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
172
|
+
defend, and hold each Contributor harmless for any liability
|
|
173
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
174
|
+
of your accepting any such warranty or additional liability.
|
|
175
|
+
|
|
176
|
+
END OF TERMS AND CONDITIONS
|
|
177
|
+
|
|
178
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
179
|
+
|
|
180
|
+
To apply the Apache License to your work, attach the following
|
|
181
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
182
|
+
replaced with your own identifying information. (Don't include
|
|
183
|
+
the brackets!) The text should be enclosed in the appropriate
|
|
184
|
+
comment syntax for the file format. We also recommend that a
|
|
185
|
+
file or class name and description of purpose be included on the
|
|
186
|
+
same "printed page" as the copyright notice for easier
|
|
187
|
+
identification within third-party archives.
|
|
188
|
+
|
|
189
|
+
Copyright [yyyy] [name of copyright owner]
|
|
190
|
+
|
|
191
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
192
|
+
you may not use this file except in compliance with the License.
|
|
193
|
+
You may obtain a copy of the License at
|
|
194
|
+
|
|
195
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
196
|
+
|
|
197
|
+
Unless required by applicable law or agreed to in writing, software
|
|
198
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
199
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
200
|
+
See the License for the specific language governing permissions and
|
|
201
|
+
limitations under the License.
|
package/README.i18n.yaml
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
|
2
|
+
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
|
3
|
+
# after editing either side, bring the other along and re-record with:
|
|
4
|
+
# pnpm run verify-translation-pairing --write packages/interaction/permission-rules/README.md
|
|
5
|
+
README.md: 61fa514bd8c7b7a45c9c7f2502c173fcb83a9a2a
|
|
6
|
+
README.zh.md: cd5e4eed56f8e29aa72e5ee811b9a304b6a04031
|
package/README.md
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# @dsh-cc/permission-rules
|
|
2
|
+
|
|
3
|
+
English | [中文](README.zh.md)
|
|
4
|
+
|
|
5
|
+
Claude Code-compatible permission-rule engine. Parses `ToolName` and `ToolName(content)` rules, folds a mode-aware decision on the `tools/pre-execute` waterfall, and enforces bypass-immune content rules through the monotonic `guard()` layer so neither a mode switch nor `bypassPermissions` can override them. Rules fail loud at load; settings hot-reload by rebuilding merged state and re-registering guards.
|
|
6
|
+
|
|
7
|
+
## Rule syntax
|
|
8
|
+
|
|
9
|
+
A rule is `ToolName` (whole tool) or `ToolName(content)` (content-scoped). `content` may escape `(`/`)`/`\` with a backslash, use `*` as a wildcard, or end in `:*` to declare a prefix rule.
|
|
10
|
+
|
|
11
|
+
| Rule | Meaning |
|
|
12
|
+
|---|---|
|
|
13
|
+
| `Bash` | whole-tool rule for every `Bash` call |
|
|
14
|
+
| `Bash(npm install)` | prefix rule: any command starting with `npm install` |
|
|
15
|
+
| `Bash(npm publish:*)` | prefix rule on the stem `npm publish:` |
|
|
16
|
+
| `Edit(foo/*.json)` | wildcard: commands/paths matching `foo/*.json` (a `*` matches any run) |
|
|
17
|
+
| `Bash(python -c "print\(1\)")` | literal parens inside content |
|
|
18
|
+
|
|
19
|
+
Malformed rules (unclosed paren, content after the closing paren, content with no tool name) throw a `TypeError` at load — fail loud. `escapeRuleContent`/`unescapeRuleContent` round-trip content safely (`\` first, then parens).
|
|
20
|
+
|
|
21
|
+
## Evaluation order
|
|
22
|
+
|
|
23
|
+
The plugin registers a `tools/pre-execute` listener and folds one decision per call:
|
|
24
|
+
|
|
25
|
+
1. **Bypass-immune** content rules (e.g. `.git` internals, shell-config paths) always deny — registered as monotonic **guards**, never overridable by a mode switch or `bypassPermissions`.
|
|
26
|
+
2. **Risk classifier** (when `classifierEnabled`, default on): catastrophic shell commands (`rm -rf /`, `sudo`, `dd of=/dev`, `kill -9 1`, piping curl/wget into sh, redirecting into system paths) are a hard **deny in every mode**; writes to protected files (`.bashrc`, `.ssh/**`, credentials) are also hard denies; writes that escape the working directory scope are **ask** outside `bypassPermissions` (allowed under it).
|
|
27
|
+
3. **whole-tool deny** → deny.
|
|
28
|
+
4. **whole-tool ask** → ask (a sandboxed, confining `Bash` is exempt and allowed instead when `exemptSandboxedBashFromToolAsk` is set).
|
|
29
|
+
5. **content-level allow/deny/ask** rules by source priority (highest source first; first rule to match decides).
|
|
30
|
+
6. **mode** short-circuits: `bypassPermissions` allows everything (unless `disableBypassPermissionsMode`); `acceptEdits` auto-allows file-edit tools; `plan` auto-allows read-only tools. `auto` is not an evaluate short-circuit — it evaluates identically to `default`, with the risk classifier proxying asks at the plugin layer.
|
|
31
|
+
7. **whole-tool allow** is the coarse default for that tool when nothing more specific matched.
|
|
32
|
+
8. **no match** → passthrough to downstream listeners (ultimately the approval seam), which may still `ask`.
|
|
33
|
+
9. **plan wrap**: leftover `ask`/`passthrough` on a non-read-only call becomes a deny with `plan mode is read-only; submit via exit_plan_mode`. Matching allow/deny rules still stand.
|
|
34
|
+
|
|
35
|
+
## Config
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
import PermissionRules from '@dsh-cc/permission-rules'
|
|
39
|
+
|
|
40
|
+
await ctx.plugin(PermissionRules, {
|
|
41
|
+
rules: {
|
|
42
|
+
deny: ['Bash(rm -rf)', 'Edit(.git*)'],
|
|
43
|
+
bypassImmune: ['Edit(~/.bashrc)', 'Edit(~/.zshrc)'],
|
|
44
|
+
},
|
|
45
|
+
bashToolName: 'Bash', // default
|
|
46
|
+
fileEditTools: ['edit'], // auto-allowed under acceptEdits
|
|
47
|
+
readOnlyTools: ['read'], // auto-allowed under plan
|
|
48
|
+
exemptSandboxedBashFromToolAsk: false,
|
|
49
|
+
defaultMode: 'default',
|
|
50
|
+
classifierEnabled: true, // risk-classifier escalation stage
|
|
51
|
+
})
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
All fields are optional; the service schema applies the illustrated defaults. Rule strings are parsed with source `config`.
|
|
55
|
+
|
|
56
|
+
## Settings and hot reload
|
|
57
|
+
|
|
58
|
+
When `ctx.settings` is mounted, the plugin registers the `permissions` namespace (`permissions.allow` / `permissions.deny` / `permissions.ask` / `permissions.defaultMode`, plus `additionalDirectories` / `protectedFiles` / `dangerousPatterns` feeding the risk classifier, and the optional `autoMode` section — `autoMode.soft_deny` prose rules (with `$defaults` expansion) and `autoMode.classifier` (`enabled` / `route` / `timeoutMs` / `cacheMaxEntries`) arming the opt-in LLM risk-classifier stage for `auto` mode; an absent `autoMode` key stays absent, so the stage remains disarmed). Settings rules carry the `settingsSource` label (default `userSettings`) and merge with Config `rules` by source priority — settings rules win. A stored change re-runs the merge and re-registers guards immediately (hot reload); a malformed settings rule fails loud at the settings boundary. When `ctx.settings` is absent, only the Config `rules` are in force (the classifier uses its curated defaults).
|
|
59
|
+
|
|
60
|
+
## Sources and modes
|
|
61
|
+
|
|
62
|
+
Every rule carries a `PermissionRuleSource` (`session` > `cliArg` > `policySettings` > `flagSettings` > `localSettings` > `projectSettings` > `userSettings` > `config`) used for content-rule priority. The engine resolves the effective mode at call time: plan activation (from `@deepseek-ai/dsh-plan-mode`) overlays first, then the session's recorded `permission/mode` override (`foldPermissionMode`), falling back to `defaultMode`.
|
|
63
|
+
|
|
64
|
+
Modes are **durable** — `setMode(agent, mode)` appends a last-wins `permission/mode` session event (registered into `KNOWN_SESSION_EVENT_TYPES` at plugin load so persistence resumes it). `plan` is owned by plan-mode and throws here. Entering `bypassPermissions` pins the session sandbox to `danger-full-access` and records `resumeSandbox`; leaving restores the recorded (or `workspace-write` fallback) confinement. Under `auto`, the risk classifier proxies every `ask`: classifier-LOW calls auto-allow, classifier-MEDIUM still asks. When the LLM classifier stage is armed, read-only tool calls are exempt — they never reach the model (zero added latency on read traffic). Verdict parsing is strict and fail-closed: a malformed model output yields the constant reason `classifier output unparseable` (model output is never shown; audit records are digest-only). A per-route consecutive-failure circuit breaker (threshold 3, keyed `provider/model`) opens the stage for a failing lane — no further classifier calls on that route, one warn per process, one `breaker` audit event per session; `rebuild()` (a settings change) resets the breaker state and re-arms.
|
|
65
|
+
|
|
66
|
+
## Switching modes
|
|
67
|
+
|
|
68
|
+
`permissionRules.setMode(agent, mode)` switches durably (see above); the `/permissions <mode>` command (in `@dsh-cc/command-permissions`) drives it for `default | acceptEdits | plan | auto | bypassPermissions`. A human-facing notice is injected into the session's model transcript on each switch.
|
|
69
|
+
|
|
70
|
+
## Pure exports for host UI
|
|
71
|
+
|
|
72
|
+
- `parseRuleString(rule)`, `parseRule(rule, behavior, source)`, `escapeRuleContent`/`unescapeRuleContent` — parse rules to `PermissionRule`.
|
|
73
|
+
- `evaluatePermission(input)` — fold a `PermissionDecision` for a call (`allow` / `deny` / `ask` / `passthrough`) given tool, subject, rule set, mode, and exemption flags. Use it to preview what a rule hits without mounting the plugin.
|
|
74
|
+
- `mergeRuleSets(...sets)` — merge rule sets by source priority.
|
|
75
|
+
- `foldPermissionMode(events)`, `foldResumeSandbox(events)`, `setPermissionMode(session, mode, resumeSandbox?)` — read/write the durable `permission/mode` override. `setPermissionMode` rejects `plan` and unknown modes; other plugins can fold a session's recorded mode via `foldPermissionMode`.
|
|
76
|
+
- `assessBashCommand(command, patterns?)` — risk-classify a shell command (`LOW`/`HIGH`).
|
|
77
|
+
- `assessFilePath(filePath, opts)` — risk-classify a file write (`LOW`/`MEDIUM`/`HIGH`).
|
|
78
|
+
- `PERMISSION_MODES`, `SOURCE_PRIORITY` — closed vocabularies.
|
|
79
|
+
|
|
80
|
+
Rule parsing and evaluation are browser-safe (pure string logic), so the type/parser/evaluate modules import cleanly into UI previews.
|
|
81
|
+
|
|
82
|
+
## Invariant companion
|
|
83
|
+
|
|
84
|
+
`@dsh-cc/permission-rules/invariant` validates `permission/mode` session events at the session boundary: `mode` must be switchable (never `plan`), and `resumeSandbox` — when present — must be a known sandbox mode (`read-only` | `workspace-write` | `danger-full-access`).
|
|
85
|
+
|
|
86
|
+
See the [Agent Note](../../../.agents/notes/implemented/feature/2026-08-14-cc-permission-rules.md).
|
package/README.zh.md
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# @dsh-cc/permission-rules
|
|
2
|
+
|
|
3
|
+
[English](README.md) | 中文
|
|
4
|
+
|
|
5
|
+
Claude Code 兼容的权限规则引擎。解析 `ToolName` 与 `ToolName(content)` 规则,在 `tools/pre-execute` waterfall 上收敛出感知模式的判定,并通过单调的 `guard()` 层强制执行 bypass-immune 内容规则——任何模式切换或 `bypassPermissions` 都无法翻盘。规则在加载期 fail loud;settings 通过重建合并状态并重注册 guard 实现热更新。
|
|
6
|
+
|
|
7
|
+
## 规则语法
|
|
8
|
+
|
|
9
|
+
规则形如 `ToolName`(整工具)或 `ToolName(content)`(内容级)。`content` 可用反斜杠转义 `(`/`)`/`\`,可用 `*` 作为通配符,也可用 `:*` 结尾声明前缀规则。
|
|
10
|
+
|
|
11
|
+
| 规则 | 含义 |
|
|
12
|
+
|---|---|
|
|
13
|
+
| `Bash` | 覆盖所有 `Bash` 调用的整工具规则 |
|
|
14
|
+
| `Bash(npm install)` | 前缀规则:任何以 `npm install` 开头的命令 |
|
|
15
|
+
| `Bash(npm publish:*)` | 对主干 `npm publish:` 的前缀规则 |
|
|
16
|
+
| `Edit(foo/*.json)` | 通配符:匹配 `foo/*.json` 的命令/路径(`*` 匹配任意片段) |
|
|
17
|
+
| `Bash(python -c "print\(1\)")` | 内容中的字面括号 |
|
|
18
|
+
|
|
19
|
+
畸形规则(括号未闭合、结束括号后有内容、只有内容没有工具名)在加载期抛出 `TypeError`——fail loud。`escapeRuleContent`/`unescapeRuleContent` 可安全往返内容(先 `\`,再括号)。
|
|
20
|
+
|
|
21
|
+
## 评估顺序
|
|
22
|
+
|
|
23
|
+
插件注册一个 `tools/pre-execute` 监听器,为每次调用收敛一个判定:
|
|
24
|
+
|
|
25
|
+
1. **Bypass-immune** 内容规则(例如 `.git` 内部、shell 配置文件路径)始终 deny——注册为单调 **guard**,模式切换或 `bypassPermissions` 都不能覆盖。
|
|
26
|
+
2. **风险分类器**(`classifierEnabled` 开启时,默认开):灾难性 shell 命令(`rm -rf /`、`sudo`、`dd of=/dev`、`kill -9 1`、将 curl/wget 管道接入 sh、重定向到系统路径)在所有模式下**硬 deny**;写入受保护文件(`.bashrc`、`.ssh/**`、凭据)同样硬 deny;写文件逃逸出工作目录作用域时在 `bypassPermissions` 之外 **ask**(在它之下放行)。
|
|
27
|
+
3. **整工具 deny** → deny。
|
|
28
|
+
4. **整工具 ask** → ask(当设置了 `exemptSandboxedBashFromToolAsk` 时,被沙箱限制的 `Bash` 豁免并直接 allow)。
|
|
29
|
+
5. **内容级 allow/deny/ask** 规则按来源优先级评估(最高优先级先;首个命中规则决定)。
|
|
30
|
+
6. **模式**短路:`bypassPermissions` 放行一切(除非 `disableBypassPermissionsMode`);`acceptEdits` 自动放行文件编辑工具;`plan` 自动放行只读工具。`auto` 不是 evaluate 短路——它与 `default` 评估一致,审批由插件层的风险分类器代理。
|
|
31
|
+
7. **整工具 allow** 是该工具的粗略默认——没有更具体的规则命中时放行。
|
|
32
|
+
8. **无命中** → passthrough 给下游监听器(最终到审批缝),后者仍可能 `ask`。
|
|
33
|
+
9. **plan 收口**:非只读调用上残留的 `ask`/`passthrough` 变成 deny,理由是 `plan mode is read-only; submit via exit_plan_mode`。命中的 allow/deny 规则仍然生效。
|
|
34
|
+
|
|
35
|
+
## 配置
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
import PermissionRules from '@dsh-cc/permission-rules'
|
|
39
|
+
|
|
40
|
+
await ctx.plugin(PermissionRules, {
|
|
41
|
+
rules: {
|
|
42
|
+
deny: ['Bash(rm -rf)', 'Edit(.git*)'],
|
|
43
|
+
bypassImmune: ['Edit(~/.bashrc)', 'Edit(~/.zshrc)'],
|
|
44
|
+
},
|
|
45
|
+
bashToolName: 'Bash', // default
|
|
46
|
+
fileEditTools: ['edit'], // auto-allowed under acceptEdits
|
|
47
|
+
readOnlyTools: ['read'], // auto-allowed under plan
|
|
48
|
+
exemptSandboxedBashFromToolAsk: false,
|
|
49
|
+
defaultMode: 'default',
|
|
50
|
+
classifierEnabled: true, // 风险分类器升级阶段
|
|
51
|
+
})
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
所有字段可选;服务 schema 应用图示默认值。规则字符串以 `config` 来源解析。
|
|
55
|
+
|
|
56
|
+
## settings 与热更新
|
|
57
|
+
|
|
58
|
+
当 `ctx.settings` 挂载时,插件注册 `permissions` 命名空间(`permissions.allow` / `permissions.deny` / `permissions.ask` / `permissions.defaultMode`,另加供风险分类器使用的 `additionalDirectories` / `protectedFiles` / `dangerousPatterns`,以及可选的 `autoMode` 小节——`autoMode.soft_deny` 文字规则(支持 `$defaults` 展开)与 `autoMode.classifier`(`enabled` / `route` / `timeoutMs` / `cacheMaxEntries`)共同驱动 `auto` 模式下可选择性开启的 LLM 风险分类器阶段;`autoMode` 键缺席即保持缺席,该阶段保持解除武装)。settings 规则携带 `settingsSource` 标签(默认 `userSettings`),并按来源优先级与 Config `rules` 合并——settings 规则优先。存储变更会立即重跑合并并重注册 guard(热更新);畸形 settings 规则在 settings 边界 fail loud。当 `ctx.settings` 缺席时,仅 Config `rules` 生效(分类器使用其精选默认值)。
|
|
59
|
+
|
|
60
|
+
## 来源与模式
|
|
61
|
+
|
|
62
|
+
每条规则携带 `PermissionRuleSource`(`session` > `cliArg` > `policySettings` > `flagSettings` > `localSettings` > `projectSettings` > `userSettings` > `config`),用于内容规则的优先级。引擎在调用时解析生效模式:plan 激活(来自 `@deepseek-ai/dsh-plan-mode`)最先覆盖,然后是会话记录的 `permission/mode` 覆盖(`foldPermissionMode`),否则回退到 `defaultMode`。
|
|
63
|
+
|
|
64
|
+
模式是**持久的**——`setMode(agent, mode)` 追加一条 last-wins 的 `permission/mode` 会话事件(插件加载时注册进 `KNOWN_SESSION_EVENT_TYPES`,持久化可恢复它)。`plan` 归 plan-mode 所有,在这里会抛错。进入 `bypassPermissions` 会把会话沙箱钉到 `danger-full-access` 并记录 `resumeSandbox`;离开时恢复记录(或回退 `workspace-write`)的约束。在 `auto` 下,风险分类器代理每次 `ask`:LOW 风险调用自动放行,MEDIUM 风险仍会询问。LLM 分类器阶段武装时,只读工具调用豁免——完全不经过模型(读流量零额外延迟)。判定解析保持严格并 fail-closed:模型输出畸形时返回常量原因 `classifier output unparseable`(模型输出永不展示;审计记录只含摘要)。按路由的连续失败熔断器(阈值 3,按 `provider/model` 键控)会为故障车道打开断路——该路由不再调用分类器,每进程一次 warn、每会话一条 `breaker` 审计事件;`rebuild()`(设置变更)重置熔断状态并重新武装。
|
|
65
|
+
|
|
66
|
+
## 切换模式
|
|
67
|
+
|
|
68
|
+
`permissionRules.setMode(agent, mode)` 持久切换(见上);`/permissions <mode>` 命令(在 `@dsh-cc/command-permissions`)为 `default | acceptEdits | plan | auto | bypassPermissions` 驱动它。每次切换都会向会话的模型转录注入一条面向人的提示。
|
|
69
|
+
|
|
70
|
+
## 供宿主 UI 使用的纯导出
|
|
71
|
+
|
|
72
|
+
- `parseRuleString(rule)`、`parseRule(rule, behavior, source)`、`escapeRuleContent`/`unescapeRuleContent`——解析规则为 `PermissionRule`。
|
|
73
|
+
- `evaluatePermission(input)`——为一次调用收敛 `PermissionDecision`(`allow` / `deny` / `ask` / `passthrough`),给定工具、subject、规则集、模式与豁免标志。无需挂载插件即可预览某规则会命中什么。
|
|
74
|
+
- `mergeRuleSets(...sets)`——按来源优先级合并规则集。
|
|
75
|
+
- `foldPermissionMode(events)`、`foldResumeSandbox(events)`、`setPermissionMode(session, mode, resumeSandbox?)`——读写持久的 `permission/mode` 覆盖。`setPermissionMode` 拒绝 `plan` 与未知模式;其他插件可用 `foldPermissionMode` 折叠某会话的记录模式。
|
|
76
|
+
- `assessBashCommand(command, patterns?)`——对 shell 命令做风险分级(`LOW`/`HIGH`)。
|
|
77
|
+
- `assessFilePath(filePath, opts)`——对文件写入做风险分级(`LOW`/`MEDIUM`/`HIGH`)。
|
|
78
|
+
- `PERMISSION_MODES`、`SOURCE_PRIORITY`——封闭词汇表。
|
|
79
|
+
|
|
80
|
+
规则解析与评估是浏览器安全的(纯字符串逻辑),因此类型/解析/评估模块可干净地导入 UI 预览。
|
|
81
|
+
|
|
82
|
+
## Invariant 伴生插件
|
|
83
|
+
|
|
84
|
+
`@dsh-cc/permission-rules/invariant` 在会话边界校验 `permission/mode` 会话事件:`mode` 必须是可切换的(绝不能是 `plan`),且 `resumeSandbox`——若存在——必须是已知沙箱模式(`read-only` | `workspace-write` | `danger-full-access`)。
|
|
85
|
+
|
|
86
|
+
参见 [Agent Note](../../../.agents/notes/implemented/feature/2026-08-14-cc-permission-rules.md)。
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WS3 approval-seam listener: auto-approves sandbox escalations in `auto`
|
|
3
|
+
* mode when the operation is attributable to the session workspace.
|
|
4
|
+
*
|
|
5
|
+
* WS0 verified that sandbox escalation prompts travel a separate
|
|
6
|
+
* `approval/request` seam, independent of permission modes — so a mode-based
|
|
7
|
+
* override cannot suppress them. This listener registers on that seam's
|
|
8
|
+
* waterfall (before any UI listener) and resolves the request itself when
|
|
9
|
+
* ALL of the following hold:
|
|
10
|
+
*
|
|
11
|
+
* 1. the request carries a reason identifying it as a sandbox escalation
|
|
12
|
+
* ({@link isSandboxEscalation});
|
|
13
|
+
* 2. the effective permission mode for the requesting session is `auto`;
|
|
14
|
+
* 3. the session has a resolvable workspace root (the per-call sandbox policy
|
|
15
|
+
* is derived from the session cwd — WS3 — so a known workspace root is
|
|
16
|
+
* exactly the boundary the escalation was evaluated against).
|
|
17
|
+
*
|
|
18
|
+
* Otherwise the request falls through to `next()` and the normal approval
|
|
19
|
+
* flow proceeds. Every auto-approval is audit-logged to the session log as a
|
|
20
|
+
* `permission/session-allow` record with `scope: 'sandbox-auto'`.
|
|
21
|
+
*
|
|
22
|
+
* @module @dsh-cc/permission-rules/approval-listener
|
|
23
|
+
*/
|
|
24
|
+
import type { Agent } from '@deepseek-ai/dsh-agent';
|
|
25
|
+
import type { ApprovalOutcome, ApprovalRequest } from '@deepseek-ai/dsh-user-approval';
|
|
26
|
+
import type { PermissionMode } from './types.ts';
|
|
27
|
+
/** Seams the listener reads off its configuring context; all injectable for tests. */
|
|
28
|
+
export interface SandboxApprovalListenerConfig {
|
|
29
|
+
/** The effective permission mode for the requesting session. */
|
|
30
|
+
modeOf(agent: Agent): PermissionMode;
|
|
31
|
+
/**
|
|
32
|
+
* The session's workspace root, or undefined when none is known. An
|
|
33
|
+
* undefined workspace root can never be auto-approved (the listener cannot
|
|
34
|
+
* verify the operation is in scope) and falls through to the next provider.
|
|
35
|
+
*/
|
|
36
|
+
workspaceOf(agent: Agent): string | undefined;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Whether an approval request's reason identifies a sandbox escalation.
|
|
40
|
+
* Case-insensitive substring match on `sandbox` — the escalation reasons are
|
|
41
|
+
* produced by the sandbox runtime and always name the mechanism.
|
|
42
|
+
*/
|
|
43
|
+
export declare function isSandboxEscalation(reason: string | undefined): boolean;
|
|
44
|
+
/**
|
|
45
|
+
* Build the `approval/request` waterfall listener. Register the returned
|
|
46
|
+
* listener ahead of the UI provider so an eligible sandbox escalation never
|
|
47
|
+
* reaches the modal queue.
|
|
48
|
+
*/
|
|
49
|
+
export declare function createSandboxApprovalListener(config: SandboxApprovalListenerConfig): (req: ApprovalRequest, next: () => Promise<ApprovalOutcome>) => Promise<ApprovalOutcome>;
|
|
50
|
+
//# sourceMappingURL=approval-listener.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"approval-listener.d.ts","sourceRoot":"","sources":["../src/approval-listener.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,wBAAwB,CAAA;AACnD,OAAO,KAAK,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,gCAAgC,CAAA;AAEtF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAEhD,sFAAsF;AACtF,MAAM,WAAW,6BAA6B;IAC5C,gEAAgE;IAChE,MAAM,CAAC,KAAK,EAAE,KAAK,GAAG,cAAc,CAAA;IACpC;;;;OAIG;IACH,WAAW,CAAC,KAAK,EAAE,KAAK,GAAG,MAAM,GAAG,SAAS,CAAA;CAC9C;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAEvE;AAED;;;;GAIG;AACH,wBAAgB,6BAA6B,CAAC,MAAM,EAAE,6BAA6B,GAAG,CACpF,GAAG,EAAE,eAAe,EACpB,IAAI,EAAE,MAAM,OAAO,CAAC,eAAe,CAAC,KACjC,OAAO,CAAC,eAAe,CAAC,CAe5B"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WS3 approval-seam listener: auto-approves sandbox escalations in `auto`
|
|
3
|
+
* mode when the operation is attributable to the session workspace.
|
|
4
|
+
*
|
|
5
|
+
* WS0 verified that sandbox escalation prompts travel a separate
|
|
6
|
+
* `approval/request` seam, independent of permission modes — so a mode-based
|
|
7
|
+
* override cannot suppress them. This listener registers on that seam's
|
|
8
|
+
* waterfall (before any UI listener) and resolves the request itself when
|
|
9
|
+
* ALL of the following hold:
|
|
10
|
+
*
|
|
11
|
+
* 1. the request carries a reason identifying it as a sandbox escalation
|
|
12
|
+
* ({@link isSandboxEscalation});
|
|
13
|
+
* 2. the effective permission mode for the requesting session is `auto`;
|
|
14
|
+
* 3. the session has a resolvable workspace root (the per-call sandbox policy
|
|
15
|
+
* is derived from the session cwd — WS3 — so a known workspace root is
|
|
16
|
+
* exactly the boundary the escalation was evaluated against).
|
|
17
|
+
*
|
|
18
|
+
* Otherwise the request falls through to `next()` and the normal approval
|
|
19
|
+
* flow proceeds. Every auto-approval is audit-logged to the session log as a
|
|
20
|
+
* `permission/session-allow` record with `scope: 'sandbox-auto'`.
|
|
21
|
+
*
|
|
22
|
+
* @module @dsh-cc/permission-rules/approval-listener
|
|
23
|
+
*/
|
|
24
|
+
import { appendSessionAllow } from "./session-allowlist.js";
|
|
25
|
+
/**
|
|
26
|
+
* Whether an approval request's reason identifies a sandbox escalation.
|
|
27
|
+
* Case-insensitive substring match on `sandbox` — the escalation reasons are
|
|
28
|
+
* produced by the sandbox runtime and always name the mechanism.
|
|
29
|
+
*/
|
|
30
|
+
export function isSandboxEscalation(reason) {
|
|
31
|
+
return reason !== undefined && /sandbox/i.test(reason);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Build the `approval/request` waterfall listener. Register the returned
|
|
35
|
+
* listener ahead of the UI provider so an eligible sandbox escalation never
|
|
36
|
+
* reaches the modal queue.
|
|
37
|
+
*/
|
|
38
|
+
export function createSandboxApprovalListener(config) {
|
|
39
|
+
return async (req, next) => {
|
|
40
|
+
if (!isSandboxEscalation(req.reason))
|
|
41
|
+
return next();
|
|
42
|
+
const agent = req.agent;
|
|
43
|
+
if (agent === undefined)
|
|
44
|
+
return next();
|
|
45
|
+
if (config.modeOf(agent) !== 'auto')
|
|
46
|
+
return next();
|
|
47
|
+
if (config.workspaceOf(agent) === undefined)
|
|
48
|
+
return next();
|
|
49
|
+
appendSessionAllow(agent.session, {
|
|
50
|
+
scope: 'sandbox-auto',
|
|
51
|
+
toolName: req.toolName,
|
|
52
|
+
timestamp: Date.now(),
|
|
53
|
+
...req.reason === undefined ? {} : { reason: req.reason },
|
|
54
|
+
});
|
|
55
|
+
return 'allowed-once';
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=approval-listener.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"approval-listener.js","sourceRoot":"","sources":["../src/approval-listener.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAIH,OAAO,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAA;AAe3D;;;;GAIG;AACH,MAAM,UAAU,mBAAmB,CAAC,MAA0B;IAC5D,OAAO,MAAM,KAAK,SAAS,IAAI,UAAU,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;AACxD,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,6BAA6B,CAAC,MAAqC;IAIjF,OAAO,KAAK,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE;QACzB,IAAI,CAAC,mBAAmB,CAAC,GAAG,CAAC,MAAM,CAAC;YAAE,OAAO,IAAI,EAAE,CAAA;QACnD,MAAM,KAAK,GAAG,GAAG,CAAC,KAAK,CAAA;QACvB,IAAI,KAAK,KAAK,SAAS;YAAE,OAAO,IAAI,EAAE,CAAA;QACtC,IAAI,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,MAAM;YAAE,OAAO,IAAI,EAAE,CAAA;QAClD,IAAI,MAAM,CAAC,WAAW,CAAC,KAAK,CAAC,KAAK,SAAS;YAAE,OAAO,IAAI,EAAE,CAAA;QAC1D,kBAAkB,CAAC,KAAK,CAAC,OAAO,EAAE;YAChC,KAAK,EAAE,cAAc;YACrB,QAAQ,EAAE,GAAG,CAAC,QAAQ;YACtB,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE;YACrB,GAAG,GAAG,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE;SAC1D,CAAC,CAAA;QACF,OAAO,cAAc,CAAA;IACvB,CAAC,CAAA;AACH,CAAC"}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The async LLM-classifier stage for `auto` mode: owns arming (per call, from
|
|
3
|
+
* the live `permissions.autoMode` settings slice), the memoized
|
|
4
|
+
* `createLlmClassifier` instance, and the `permission/classifier` session
|
|
5
|
+
* audit event. The plugin's `tools/pre-execute` listener (index.ts) only
|
|
6
|
+
* wires this stage — the escalate-only decision flow lives here.
|
|
7
|
+
*
|
|
8
|
+
* Arming predicate (§4.4): `autoMode.classifier.enabled === true` AND an llm
|
|
9
|
+
* stream capability is wired AND the alias route resolves. Enabled but
|
|
10
|
+
* unarmable ⇒ disarm with ONE warning per process (plus an `unarmed` audit
|
|
11
|
+
* event when a session is available) and the legacy decision path runs.
|
|
12
|
+
*
|
|
13
|
+
* @module @dsh-cc/permission-rules/auto-stage
|
|
14
|
+
*/
|
|
15
|
+
import type { Session, SessionEvent } from '@deepseek-ai/dsh-session';
|
|
16
|
+
import type { ToolExecution } from '@dsh-cc/tools';
|
|
17
|
+
import type { DecidedCall } from './decide.ts';
|
|
18
|
+
/** `permissions.autoMode.classifier` — the plugin-local hand-mirror of the shared AutoModeClassifierSchema. */
|
|
19
|
+
export interface AutoModeClassifierSettings {
|
|
20
|
+
/** Master switch for the LLM risk classifier stage (default `false`). */
|
|
21
|
+
enabled?: boolean;
|
|
22
|
+
/** Model route used for classification (default `'haiku'`). */
|
|
23
|
+
route?: string;
|
|
24
|
+
/** Per-call timeout in milliseconds (default `8000`). */
|
|
25
|
+
timeoutMs?: number;
|
|
26
|
+
/** Verdict cache size in entries (default `256`). */
|
|
27
|
+
cacheMaxEntries?: number;
|
|
28
|
+
}
|
|
29
|
+
/** `permissions.autoMode` — the plugin-local hand-mirror of the shared AutoModeSchema. */
|
|
30
|
+
export interface AutoModeSettings {
|
|
31
|
+
/**
|
|
32
|
+
* Soft-deny hints evaluated by the classifier, in CC's snake_case spelling.
|
|
33
|
+
* `$defaults` expansion happens at consumption time — the schema never
|
|
34
|
+
* expands it.
|
|
35
|
+
*/
|
|
36
|
+
soft_deny?: string[];
|
|
37
|
+
/** LLM risk classifier configuration; absent when the section omits it. */
|
|
38
|
+
classifier?: AutoModeClassifierSettings;
|
|
39
|
+
}
|
|
40
|
+
/** The session event type carrying one classifier verdict audit record. */
|
|
41
|
+
export declare const CLASSIFIER_EVENT = "permission/classifier";
|
|
42
|
+
/** Consecutive per-route classifier failures before that route's breaker opens (module constant — no settings knob by design). */
|
|
43
|
+
export declare const CLASSIFIER_BREAKER_THRESHOLD = 3;
|
|
44
|
+
/** The `permission/classifier` payload. The raw classifier input NEVER appears — only its digest. */
|
|
45
|
+
export interface ClassifierAuditEventData {
|
|
46
|
+
/** The tool the verdict is about. */
|
|
47
|
+
tool: string;
|
|
48
|
+
/** sha256 of the rendered classifier input (absent on the arming `unarmed` record). */
|
|
49
|
+
digest?: string;
|
|
50
|
+
verdict: 'allow' | 'ask';
|
|
51
|
+
failure?: 'timeout' | 'error' | 'malformed' | 'unarmed' | 'breaker' | 'cancelled';
|
|
52
|
+
route?: string;
|
|
53
|
+
provider?: string;
|
|
54
|
+
model?: string;
|
|
55
|
+
latencyMs: number;
|
|
56
|
+
cacheHit: boolean;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Append one `permission/classifier` audit record through the widened session
|
|
60
|
+
* append face (same cross-pin strategy as `./mode.ts` and
|
|
61
|
+
* `./session-allowlist.ts`).
|
|
62
|
+
*/
|
|
63
|
+
export declare function appendSessionClassifier(session: Session, data: ClassifierAuditEventData): void;
|
|
64
|
+
/**
|
|
65
|
+
* Fold a session log into the classifier verdict records it carries, in log
|
|
66
|
+
* order. Foreign event types are skipped; resume/replay reconstructs why a
|
|
67
|
+
* call did or did not prompt.
|
|
68
|
+
*/
|
|
69
|
+
export declare function foldClassifiers(events: readonly SessionEvent[]): ClassifierAuditEventData[];
|
|
70
|
+
/**
|
|
71
|
+
* Structural dependency face the service supplies. `stream` is the llm
|
|
72
|
+
* adapter (undefined when the llm service is not mounted); `resolveRoute`
|
|
73
|
+
* resolves the configured alias route per call (undefined when unresolvable).
|
|
74
|
+
*/
|
|
75
|
+
export type AutoStageDeps = {
|
|
76
|
+
/** The live `permissions` settings section (re-read on every call). */
|
|
77
|
+
settingsRead(): {
|
|
78
|
+
autoMode?: AutoModeSettings;
|
|
79
|
+
};
|
|
80
|
+
/**
|
|
81
|
+
* One-shot text completion over the auxiliary lane; `undefined` when no llm
|
|
82
|
+
* service is mounted (the stage then disarms).
|
|
83
|
+
*/
|
|
84
|
+
stream: ((opts: {
|
|
85
|
+
provider: string;
|
|
86
|
+
model: string;
|
|
87
|
+
system: string;
|
|
88
|
+
prompt: string;
|
|
89
|
+
maxTokens: number;
|
|
90
|
+
signal?: AbortSignal;
|
|
91
|
+
}) => Promise<string>) | undefined;
|
|
92
|
+
/** Resolve the configured classifier route for this call's session. */
|
|
93
|
+
resolveRoute(exec: ToolExecution): {
|
|
94
|
+
provider: string;
|
|
95
|
+
model: string;
|
|
96
|
+
} | undefined;
|
|
97
|
+
/** Process logger for the one-time disarm warning. */
|
|
98
|
+
warn(message: string): void;
|
|
99
|
+
/** Durable audit sink (session append face, listener-owned). */
|
|
100
|
+
audit(session: Session, event: ClassifierAuditEventData): void;
|
|
101
|
+
/**
|
|
102
|
+
* Optional env-gated process-log sink for raw classifier output (R5) —
|
|
103
|
+
* never session events; wired only when DSH_PERMISSION_CLASSIFIER_DEBUG=1.
|
|
104
|
+
*/
|
|
105
|
+
debug?: (message: string) => void;
|
|
106
|
+
};
|
|
107
|
+
/** The stage's contribution to one pre-execute decision: allow, an escalated ask, or nothing (legacy path). */
|
|
108
|
+
export type StageOutcome = 'allow' | {
|
|
109
|
+
kind: 'ask';
|
|
110
|
+
reason: string;
|
|
111
|
+
};
|
|
112
|
+
export type AutoStage = {
|
|
113
|
+
/** Drop the memoized classifier so the next armed call rebuilds it (settings onChange). */
|
|
114
|
+
rebuild(): void;
|
|
115
|
+
/**
|
|
116
|
+
* Maybe escalate one verbose decision. Returns a final decision only for
|
|
117
|
+
* the armed + `auto` + LOW + `ask`/`passthrough` slice (§4.1); every other
|
|
118
|
+
* shape returns undefined and the listener applies the legacy mapping
|
|
119
|
+
* unchanged — the LLM is then never invoked (I1–I3, I5).
|
|
120
|
+
*/
|
|
121
|
+
maybeEscalate(decided: DecidedCall, exec: ToolExecution): Promise<StageOutcome | undefined>;
|
|
122
|
+
};
|
|
123
|
+
/**
|
|
124
|
+
* Trailing consecutive per-route failure streak over attributed classifier
|
|
125
|
+
* audit records (R3, pure fold — unit-testable). Only events carrying
|
|
126
|
+
* `provider`/`model` attribution matching `routeKey` count (unattributed
|
|
127
|
+
* legacy events predate route keying — skipped entirely); a parsed verdict or
|
|
128
|
+
* cache hit resets the streak; malformed/error/timeout increment it; other
|
|
129
|
+
* tags (`cancelled`, `breaker`, `unarmed`) are neutral. Capped at `threshold`.
|
|
130
|
+
*/
|
|
131
|
+
export declare function trailingRouteFailureStreak(events: readonly ClassifierAuditEventData[], routeKey: string, threshold: number): number;
|
|
132
|
+
/**
|
|
133
|
+
* Build the stage. The classifier instance is memoized per autoMode slice:
|
|
134
|
+
* `rebuild()` (wired to the plugin's settings onChange/reload) drops it, and
|
|
135
|
+
* the next armed call rebuilds from the fresh slice — never per call.
|
|
136
|
+
*/
|
|
137
|
+
export declare function createAutoStage(deps: AutoStageDeps): AutoStage;
|
|
138
|
+
//# sourceMappingURL=auto-stage.d.ts.map
|