dsh-permission-rules 0.7.4 → 0.7.7
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/CHANGELOG.md +40 -0
- package/README-es.md +45 -43
- package/README-hi.md +46 -42
- package/README-pt.md +45 -43
- package/README-zh.md +45 -43
- package/README.md +21 -18
- package/lib/client.js +6 -6
- package/lib/client.js.map +1 -1
- package/lib/index.js +138 -49
- package/lib/typert.host.js +6 -6
- package/lib/types/client/remote.d.ts +8 -78
- package/lib/types/client/remote.d.ts.map +1 -1
- package/lib/types/config.d.ts +147 -7
- package/lib/types/config.d.ts.map +1 -1
- package/lib/types/runtime.d.ts +8 -3
- package/lib/types/runtime.d.ts.map +1 -1
- package/lib/types/settings.d.ts +21 -18
- package/lib/types/settings.d.ts.map +1 -1
- package/lib/types/typert.host.d.ts +8 -78
- package/lib/types/typert.host.d.ts.map +1 -1
- package/lib/types/wire.d.ts +16 -156
- package/lib/types/wire.d.ts.map +1 -1
- package/package.json +35 -34
- package/scripts/repair-session-logs.mjs +6 -2
- package/src/config.ts +126 -28
- package/src/runtime.ts +8 -3
- package/src/settings.ts +73 -58
- package/src/wire.ts +7 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-permission-rules",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.7",
|
|
4
4
|
"description": "Declarative Claude Code-style permission rules plus a Codex-style process-level network policy for DeepSeek Harness: ordered allow/deny/ask rules with tool-name, argument (glob/regex), workspace-path, and network-target (domain/ip/port/scheme) matching on the tools/pre-execute waterfall, a built-in local HTTP/CONNECT proxy governing shell subprocess traffic, three network modes mapped onto the official sandbox presets, full session-log audit, HMR rule reload, and a settings page with a rule editor, block counters, and recent interception lists.",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
@@ -77,7 +77,7 @@
|
|
|
77
77
|
"network:outbound"
|
|
78
78
|
],
|
|
79
79
|
"compatibility": {
|
|
80
|
-
"dshVersions": ["0.1.2-rc.1","0.1.5-rc.2"]
|
|
80
|
+
"dshVersions": ["0.1.2-rc.1","0.1.5-rc.2","0.1.7-alpha.2"]
|
|
81
81
|
},
|
|
82
82
|
"capability": {
|
|
83
83
|
"id": "rules",
|
|
@@ -116,22 +116,22 @@
|
|
|
116
116
|
},
|
|
117
117
|
"engines": {
|
|
118
118
|
"node": "^22.19.0 || >=24.0.0",
|
|
119
|
-
"dsh": ">=0.1.2-rc.1 <0.2.0 || >=0.1.5-alpha.1 <0.2.0 || >=0.1.6-0 <0.2.0"
|
|
119
|
+
"dsh": ">=0.1.2-rc.1 <0.2.0 || >=0.1.5-alpha.1 <0.2.0 || >=0.1.6-0 <0.2.0 || >=0.1.7-0 <0.2.0"
|
|
120
120
|
},
|
|
121
121
|
"peerDependencies": {
|
|
122
|
-
"@deepseek-ai/cordis": "^4.0.
|
|
123
|
-
"@deepseek-ai/dsh-agent": ">=0.1.2-rc.1 <0.2.0 || >=0.1.5-alpha.1 <0.2.0 || >=0.1.6-0 <0.2.0",
|
|
124
|
-
"@deepseek-ai/dsh-commands": ">=0.1.2-rc.1 <0.2.0 || >=0.1.5-alpha.1 <0.2.0 || >=0.1.6-0 <0.2.0",
|
|
125
|
-
"@deepseek-ai/dsh-llm": ">=0.1.2-rc.1 <0.2.0 || >=0.1.5-alpha.1 <0.2.0 || >=0.1.6-0 <0.2.0",
|
|
126
|
-
"@deepseek-ai/dsh-session": ">=0.1.2-rc.1 <0.2.0 || >=0.1.5-alpha.1 <0.2.0 || >=0.1.6-0 <0.2.0",
|
|
127
|
-
"@deepseek-ai/dsh-tools": ">=0.1.2-rc.1 <0.2.0 || >=0.1.5-alpha.1 <0.2.0 || >=0.1.6-0 <0.2.0",
|
|
128
|
-
"@deepseek-ai/dsh-typert-protocol": ">=0.1.2-rc.1 <0.2.0 || >=0.1.5-alpha.1 <0.2.0 || >=0.1.6-0 <0.2.0",
|
|
122
|
+
"@deepseek-ai/cordis": "^4.0.4",
|
|
123
|
+
"@deepseek-ai/dsh-agent": ">=0.1.2-rc.1 <0.2.0 || >=0.1.5-alpha.1 <0.2.0 || >=0.1.6-0 <0.2.0 || >=0.1.7-0 <0.2.0",
|
|
124
|
+
"@deepseek-ai/dsh-commands": ">=0.1.2-rc.1 <0.2.0 || >=0.1.5-alpha.1 <0.2.0 || >=0.1.6-0 <0.2.0 || >=0.1.7-0 <0.2.0",
|
|
125
|
+
"@deepseek-ai/dsh-llm": ">=0.1.2-rc.1 <0.2.0 || >=0.1.5-alpha.1 <0.2.0 || >=0.1.6-0 <0.2.0 || >=0.1.7-0 <0.2.0",
|
|
126
|
+
"@deepseek-ai/dsh-session": ">=0.1.2-rc.1 <0.2.0 || >=0.1.5-alpha.1 <0.2.0 || >=0.1.6-0 <0.2.0 || >=0.1.7-0 <0.2.0",
|
|
127
|
+
"@deepseek-ai/dsh-tools": ">=0.1.2-rc.1 <0.2.0 || >=0.1.5-alpha.1 <0.2.0 || >=0.1.6-0 <0.2.0 || >=0.1.7-0 <0.2.0",
|
|
128
|
+
"@deepseek-ai/dsh-typert-protocol": ">=0.1.2-rc.1 <0.2.0 || >=0.1.5-alpha.1 <0.2.0 || >=0.1.6-0 <0.2.0 || >=0.1.7-0 <0.2.0",
|
|
129
129
|
"@deepseek-ai/schemastery": "^3.18.2",
|
|
130
130
|
"react": "^18.2.0",
|
|
131
|
-
"@deepseek-ai/dsh-client-connection": ">=0.1.2-rc.1 <0.2.0 || >=0.1.5-alpha.1 <0.2.0 || >=0.1.6-0 <0.2.0",
|
|
132
|
-
"@deepseek-ai/dsh-client-locale": ">=0.1.2-rc.1 <0.2.0 || >=0.1.5-alpha.1 <0.2.0 || >=0.1.6-0 <0.2.0",
|
|
133
|
-
"@deepseek-ai/dsh-client-ui-settings": ">=0.1.2-rc.1 <0.2.0 || >=0.1.5-alpha.1 <0.2.0 || >=0.1.6-0 <0.2.0",
|
|
134
|
-
"@deepseek-ai/dsh-client-web": ">=0.1.2-rc.1 <0.2.0 || >=0.1.5-alpha.1 <0.2.0 || >=0.1.6-0 <0.2.0"
|
|
131
|
+
"@deepseek-ai/dsh-client-connection": ">=0.1.2-rc.1 <0.2.0 || >=0.1.5-alpha.1 <0.2.0 || >=0.1.6-0 <0.2.0 || >=0.1.7-0 <0.2.0",
|
|
132
|
+
"@deepseek-ai/dsh-client-locale": ">=0.1.2-rc.1 <0.2.0 || >=0.1.5-alpha.1 <0.2.0 || >=0.1.6-0 <0.2.0 || >=0.1.7-0 <0.2.0",
|
|
133
|
+
"@deepseek-ai/dsh-client-ui-settings": ">=0.1.2-rc.1 <0.2.0 || >=0.1.5-alpha.1 <0.2.0 || >=0.1.6-0 <0.2.0 || >=0.1.7-0 <0.2.0",
|
|
134
|
+
"@deepseek-ai/dsh-client-web": ">=0.1.2-rc.1 <0.2.0 || >=0.1.5-alpha.1 <0.2.0 || >=0.1.6-0 <0.2.0 || >=0.1.7-0 <0.2.0"
|
|
135
135
|
},
|
|
136
136
|
"peerDependenciesMeta": {
|
|
137
137
|
"@deepseek-ai/dsh-client-connection": {
|
|
@@ -156,27 +156,28 @@
|
|
|
156
156
|
"zod": "^4.4.3"
|
|
157
157
|
},
|
|
158
158
|
"devDependencies": {
|
|
159
|
-
"@deepseek-ai/dsh-attachment": "0.1.
|
|
160
|
-
"@deepseek-ai/cordis": "^4.0.
|
|
159
|
+
"@deepseek-ai/dsh-attachment": "0.1.7-alpha.2",
|
|
160
|
+
"@deepseek-ai/cordis": "^4.0.4",
|
|
161
161
|
"@deepseek-ai/cordis-plugin-include": "^1.0.7",
|
|
162
|
-
"@deepseek-ai/cordis-plugin-loader": "^1.0.
|
|
163
|
-
"@deepseek-ai/dsh-agent": "0.1.
|
|
164
|
-
"@deepseek-ai/dsh-brand": "0.1.
|
|
165
|
-
"@deepseek-ai/dsh-client-connection": "0.1.
|
|
166
|
-
"@deepseek-ai/dsh-client-locale": "0.1.
|
|
167
|
-
"@deepseek-ai/dsh-client-ui-settings": "0.1.
|
|
168
|
-
"@deepseek-ai/dsh-client-ui-slots": "0.1.
|
|
169
|
-
"@deepseek-ai/dsh-client-web": "0.1.
|
|
170
|
-
"@deepseek-ai/dsh-commands": "0.1.
|
|
171
|
-
"@deepseek-ai/dsh-invariants": "0.1.
|
|
172
|
-
"@deepseek-ai/dsh-llm": "0.1.
|
|
173
|
-
"@deepseek-ai/dsh-session": "0.1.
|
|
174
|
-
"@deepseek-ai/dsh-session-projection": "0.1.
|
|
175
|
-
"@deepseek-ai/dsh-subagent": "0.1.
|
|
176
|
-
"@deepseek-ai/dsh-
|
|
177
|
-
"@deepseek-ai/dsh-
|
|
178
|
-
"@deepseek-ai/dsh-
|
|
179
|
-
"@deepseek-ai/
|
|
162
|
+
"@deepseek-ai/cordis-plugin-loader": "^1.0.4",
|
|
163
|
+
"@deepseek-ai/dsh-agent": "0.1.7-alpha.2",
|
|
164
|
+
"@deepseek-ai/dsh-brand": "0.1.7-alpha.2",
|
|
165
|
+
"@deepseek-ai/dsh-client-connection": "0.1.7-alpha.2",
|
|
166
|
+
"@deepseek-ai/dsh-client-locale": "0.1.7-alpha.2",
|
|
167
|
+
"@deepseek-ai/dsh-client-ui-settings": "0.1.7-alpha.2",
|
|
168
|
+
"@deepseek-ai/dsh-client-ui-slots": "0.1.7-alpha.2",
|
|
169
|
+
"@deepseek-ai/dsh-client-web": "0.1.7-alpha.2",
|
|
170
|
+
"@deepseek-ai/dsh-commands": "0.1.7-alpha.2",
|
|
171
|
+
"@deepseek-ai/dsh-invariants": "0.1.7-alpha.2",
|
|
172
|
+
"@deepseek-ai/dsh-llm": "0.1.7-alpha.2",
|
|
173
|
+
"@deepseek-ai/dsh-session": "0.1.7-alpha.2",
|
|
174
|
+
"@deepseek-ai/dsh-session-projection": "0.1.7-alpha.2",
|
|
175
|
+
"@deepseek-ai/dsh-subagent": "0.1.7-alpha.2",
|
|
176
|
+
"@deepseek-ai/dsh-system-prompt": "0.1.7-alpha.2",
|
|
177
|
+
"@deepseek-ai/dsh-tools": "0.1.7-alpha.2",
|
|
178
|
+
"@deepseek-ai/dsh-typert-protocol": "0.1.7-alpha.2",
|
|
179
|
+
"@deepseek-ai/dsh-user-approval": "0.1.7-alpha.2",
|
|
180
|
+
"@deepseek-ai/schemastery": "^3.18.3",
|
|
180
181
|
"@eslint/js": "^10.0.1",
|
|
181
182
|
"@types/node": "^22.19.0",
|
|
182
183
|
"@vitest/coverage-v8": "^3.2.0",
|
|
@@ -82,14 +82,15 @@ const STORAGE_ROW_TYPES = new Set([
|
|
|
82
82
|
/**
|
|
83
83
|
* The harness build's generated event vocabulary
|
|
84
84
|
* (`KNOWN_SESSION_EVENT_TYPES`, `packages/core/session/src/known-event-types.ts`,
|
|
85
|
-
* synced with the `0.1.
|
|
85
|
+
* synced with the `0.1.7-alpha.1` catalog), plus the v1/v2-only spellings the
|
|
86
86
|
* older generations wrote (`assistant/chunk`, `tool/code-dispatch`,
|
|
87
87
|
* `tool/code-dispatch-start`; V3 renamed the latter two to `tool/ptc-dispatch`
|
|
88
88
|
* and `tool/ptc-dispatch-start`), so every generation scans clean. Rows whose
|
|
89
89
|
* type is outside this set are what the read path refuses when the envelope
|
|
90
90
|
* lacks `ignorable: true`; the scan reports them so nothing is missed.
|
|
91
91
|
* Regenerate this list from the harness checkout when it changes (0.1.5-rc.1
|
|
92
|
-
* added `deliverables/presented` and `subagent/catalog`
|
|
92
|
+
* added `deliverables/presented` and `subagent/catalog`; `0.1.7-alpha.1` added
|
|
93
|
+
* `developer/message`, `image/offload` and `workspace/changes`).
|
|
93
94
|
*/
|
|
94
95
|
const KNOWN_SESSION_EVENT_TYPES = new Set([
|
|
95
96
|
'agent-preset/selected',
|
|
@@ -109,12 +110,14 @@ const KNOWN_SESSION_EVENT_TYPES = new Set([
|
|
|
109
110
|
'compaction/start',
|
|
110
111
|
'compaction/summary',
|
|
111
112
|
'deliverables/presented',
|
|
113
|
+
'developer/message',
|
|
112
114
|
'feedback/message-delete',
|
|
113
115
|
'feedback/message-put',
|
|
114
116
|
'feedback/record',
|
|
115
117
|
'goal/change',
|
|
116
118
|
'hook/invoked',
|
|
117
119
|
'hook/result',
|
|
120
|
+
'image/offload',
|
|
118
121
|
'llm/retry',
|
|
119
122
|
'llm/retry-started',
|
|
120
123
|
'model/selection',
|
|
@@ -153,6 +156,7 @@ const KNOWN_SESSION_EVENT_TYPES = new Set([
|
|
|
153
156
|
'turn/start',
|
|
154
157
|
'user/message',
|
|
155
158
|
'web/deepseek-search-llm-request',
|
|
159
|
+
'workspace/changes',
|
|
156
160
|
])
|
|
157
161
|
|
|
158
162
|
/**
|
package/src/config.ts
CHANGED
|
@@ -3,6 +3,15 @@
|
|
|
3
3
|
* a validated {@link Config} field changeable from cordis.yml; the
|
|
4
4
|
* resolution step validates the numeric bounds and compiles nothing — rule
|
|
5
5
|
* files are external documents resolved per session cwd at load time.
|
|
6
|
+
*
|
|
7
|
+
* Since the `0.1.7-alpha` settings contract every field is declared
|
|
8
|
+
* `.volatile()`, so the Loader hands `apply` a live reference per field
|
|
9
|
+
* ({@link LiveConfig}) and the settings surface edits them in place. The
|
|
10
|
+
* numeric bounds live in the SCHEMA (`min`/`max`/`step`) as well as in
|
|
11
|
+
* {@link resolveConfig}: the config editor resolves the merged config
|
|
12
|
+
* against this schema before it persists anything, so an out-of-range value
|
|
13
|
+
* is still refused at write time — the tradeoff the removed
|
|
14
|
+
* `settings.register(..., { validate })` callback used to carry.
|
|
6
15
|
* @module dsh-permission-rules/config
|
|
7
16
|
*/
|
|
8
17
|
|
|
@@ -98,8 +107,10 @@ export interface Config {
|
|
|
98
107
|
/**
|
|
99
108
|
* Fallback rule file used when per-cwd discovery finds no rule file.
|
|
100
109
|
* Absolute, or relative to `process.cwd()`. Unset = an empty rule set.
|
|
110
|
+
* Declared `| undefined` because a live reference to a field the schema
|
|
111
|
+
* gives no default reports an absent value explicitly.
|
|
101
112
|
*/
|
|
102
|
-
fallbackPath?: string
|
|
113
|
+
fallbackPath?: string | undefined
|
|
103
114
|
/**
|
|
104
115
|
* How an unreadable/invalid rule file is handled at load: `'fail'` throws
|
|
105
116
|
* (the pending tool call errors loudly; HMR reloads keep the previous
|
|
@@ -146,9 +157,9 @@ export interface Config {
|
|
|
146
157
|
*/
|
|
147
158
|
allowUnmarkedAudit?: boolean
|
|
148
159
|
/** Process-level network policy (all optional; defaults inside). */
|
|
149
|
-
network?: NetworkConfig
|
|
160
|
+
network?: NetworkConfig | undefined
|
|
150
161
|
/** Built-in high-risk baseline (all optional; defaults inside). */
|
|
151
|
-
builtin?: BuiltinConfig
|
|
162
|
+
builtin?: BuiltinConfig | undefined
|
|
152
163
|
}
|
|
153
164
|
|
|
154
165
|
/** Network config after {@link resolveConfig}: every optional field has its explicit default. */
|
|
@@ -195,43 +206,125 @@ export interface ResolvedConfig {
|
|
|
195
206
|
readonly builtin: ResolvedBuiltinConfig
|
|
196
207
|
}
|
|
197
208
|
|
|
198
|
-
/**
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
209
|
+
/**
|
|
210
|
+
* Mark one schema field as a live reference, where the host can.
|
|
211
|
+
*
|
|
212
|
+
* `.volatile()` first appeared in `@deepseek-ai/schemastery` 3.18.3, and this
|
|
213
|
+
* schema is built while the plugin module is evaluated — an unguarded call
|
|
214
|
+
* would turn every host line the package's peer ranges still advertise
|
|
215
|
+
* (`0.1.2-rc`, `0.1.5-alpha`, `0.1.6-0`) into a hard mount crash. Probed
|
|
216
|
+
* instead: on a Schemastery without the method the field stays an ordinary
|
|
217
|
+
* value, the Loader hands `apply` a plain config ({@link plainConfig} passes
|
|
218
|
+
* it straight through), and the plugin runs exactly as it did before live
|
|
219
|
+
* forms existed — only the settings form is unavailable there, which is the
|
|
220
|
+
* same degradation as a host that composes no settings service at all.
|
|
221
|
+
* @param schema - the field schema.
|
|
222
|
+
* @returns the same field schema, live where supported.
|
|
223
|
+
*/
|
|
224
|
+
function live<S extends { volatile(): unknown }>(schema: S): ReturnType<S['volatile']> {
|
|
225
|
+
const builder = schema as { volatile?: () => ReturnType<S['volatile']> }
|
|
226
|
+
return typeof builder.volatile === 'function'
|
|
227
|
+
? builder.volatile()
|
|
228
|
+
: (schema as unknown as ReturnType<S['volatile']>)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Schemastery schema: the loader validates and fills defaults before
|
|
233
|
+
* `apply`. Every field is live (see {@link live}), so on a `0.1.7-alpha`
|
|
234
|
+
* host the Loader hands `apply` a reference per field and the settings
|
|
235
|
+
* surface can edit each one in place. `network` and `builtin` are marked
|
|
236
|
+
* live as WHOLE objects — Schemastery refuses a volatile inside a volatile,
|
|
237
|
+
* and the only supported shape is a fixed object path.
|
|
238
|
+
*
|
|
239
|
+
* The numeric bounds are declared here (not only in {@link resolveConfig})
|
|
240
|
+
* because the config editor resolves the merged config against THIS schema
|
|
241
|
+
* before persisting it: an out-of-range edit is refused at write time
|
|
242
|
+
* instead of disabling the plugin on the next read.
|
|
243
|
+
*/
|
|
244
|
+
export const Config = z.object({
|
|
245
|
+
rulesFile: live(z.string().default('.dsh/rules.yaml')),
|
|
246
|
+
fallbackPath: live(z.string()),
|
|
247
|
+
badFilePolicy: live(z.union(['fail', 'ignore-with-warning'] as const).default('fail')),
|
|
248
|
+
maxRules: live(z.number().step(1).min(1).default(256)),
|
|
249
|
+
maxCachedWorkspaces: live(z.number().step(1).min(1).default(512)),
|
|
250
|
+
patternMode: live(z.union(['glob', 'regex'] as const).default('glob')),
|
|
251
|
+
watch: live(z.boolean().default(true)),
|
|
252
|
+
watchStabilityThresholdMs: live(z.number().step(1).min(0).default(200)),
|
|
253
|
+
language: live(z.union(['en', 'zh', 'es', 'pt', 'hi'] as const).default('en')),
|
|
254
|
+
caseInsensitivePaths: live(z.boolean().default(process.platform === 'win32')),
|
|
255
|
+
audit: live(z.union(['all', 'hits'] as const).default('all')),
|
|
256
|
+
searchUp: live(z.boolean().default(false)),
|
|
257
|
+
maxGlobStars: live(z.number().step(1).min(1).default(2)),
|
|
258
|
+
enforce: live(z.boolean().default(true)),
|
|
259
|
+
allowUnmarkedAudit: live(z.boolean().default(false)),
|
|
260
|
+
network: live(z.object({
|
|
216
261
|
enabled: z.boolean().default(true),
|
|
217
262
|
mode: z.union(['auto', ...NETWORK_MODES] as const).default('auto'),
|
|
218
263
|
autoFallback: z.union(NETWORK_MODES as [NetworkMode, ...NetworkMode[]]).default('allow-all'),
|
|
219
264
|
unlisted: z.union(['ask', 'deny'] as const).default('ask'),
|
|
220
265
|
proxyBind: z.string().default('127.0.0.1'),
|
|
221
|
-
proxyPort: z.number().default(0),
|
|
222
|
-
proxyMaxRecent: z.number().default(100),
|
|
266
|
+
proxyPort: z.number().step(1).min(0).max(65535).default(0),
|
|
267
|
+
proxyMaxRecent: z.number().step(1).min(1).default(100),
|
|
223
268
|
loopback: z.union(['allow', 'policy'] as const).default('allow'),
|
|
224
269
|
injectEnv: z.boolean().default(true),
|
|
225
270
|
noProxy: z.union(['clear', 'preserve'] as const).default('clear'),
|
|
226
271
|
upstreamProxy: z.string().default('off'),
|
|
227
272
|
allowHostAction: z.boolean().default(true),
|
|
228
|
-
}),
|
|
229
|
-
builtin: z.object({
|
|
273
|
+
})),
|
|
274
|
+
builtin: live(z.object({
|
|
230
275
|
enabled: z.boolean().default(true),
|
|
231
276
|
path: z.string(),
|
|
232
|
-
}),
|
|
277
|
+
})),
|
|
233
278
|
})
|
|
234
279
|
|
|
280
|
+
/** The Loader-delivered config: every field is a live reference to its current value. */
|
|
281
|
+
export type LiveConfig = ReturnType<typeof Config>
|
|
282
|
+
|
|
283
|
+
/** What {@link resolveConfig} accepts: the raw config, or the Loader's live one. */
|
|
284
|
+
export type ConfigInput = Config | LiveConfig
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Whether `value` is the Loader's live config rather than a plain one.
|
|
288
|
+
* `rulesFile` is the discriminator: it always has a schema default, so the
|
|
289
|
+
* Loader always materializes its reference, plain or absent.
|
|
290
|
+
* @param value - a raw or live config.
|
|
291
|
+
* @returns true when the fields are live references.
|
|
292
|
+
*/
|
|
293
|
+
function isLiveConfig(value: ConfigInput): value is LiveConfig {
|
|
294
|
+
return typeof (value as { rulesFile?: { get?: unknown } }).rulesFile?.get === 'function'
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Unwrap the Loader's live references into the plain config
|
|
299
|
+
* {@link resolveConfig} validates. Reading each reference HERE is what keeps
|
|
300
|
+
* a caller's config source lazy: call it inside the closure, never at
|
|
301
|
+
* closure-construction time, or a live edit stops being visible.
|
|
302
|
+
* @param config - the raw config, or the Loader's live one.
|
|
303
|
+
* @returns the plain raw config (the input itself when it was already plain).
|
|
304
|
+
*/
|
|
305
|
+
export function plainConfig(config: ConfigInput): Config {
|
|
306
|
+
if (!isLiveConfig(config)) return config
|
|
307
|
+
return {
|
|
308
|
+
rulesFile: config.rulesFile.get(),
|
|
309
|
+
fallbackPath: config.fallbackPath.get(),
|
|
310
|
+
badFilePolicy: config.badFilePolicy.get(),
|
|
311
|
+
maxRules: config.maxRules.get(),
|
|
312
|
+
maxCachedWorkspaces: config.maxCachedWorkspaces.get(),
|
|
313
|
+
patternMode: config.patternMode.get(),
|
|
314
|
+
watch: config.watch.get(),
|
|
315
|
+
watchStabilityThresholdMs: config.watchStabilityThresholdMs.get(),
|
|
316
|
+
language: config.language.get(),
|
|
317
|
+
caseInsensitivePaths: config.caseInsensitivePaths.get(),
|
|
318
|
+
audit: config.audit.get(),
|
|
319
|
+
searchUp: config.searchUp.get(),
|
|
320
|
+
maxGlobStars: config.maxGlobStars.get(),
|
|
321
|
+
enforce: config.enforce.get(),
|
|
322
|
+
allowUnmarkedAudit: config.allowUnmarkedAudit.get(),
|
|
323
|
+
network: config.network.get(),
|
|
324
|
+
builtin: config.builtin.get(),
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
235
328
|
/**
|
|
236
329
|
* Validate raw values and fill explicit defaults. A `maxRules`,
|
|
237
330
|
* `maxCachedWorkspaces`, or `maxGlobStars` that is not a positive safe
|
|
@@ -239,10 +332,15 @@ export const Config: z<Config> = z.object({
|
|
|
239
332
|
* absolute `rulesFile`, a value outside a closed enum, or a non-boolean
|
|
240
333
|
* flag throws here — misconfiguration fails loud at mount even when the
|
|
241
334
|
* plugin is mounted without the Schemastery loader.
|
|
242
|
-
*
|
|
335
|
+
*
|
|
336
|
+
* Accepts the Loader's live config directly and unwraps it through
|
|
337
|
+
* {@link plainConfig}, so a caller can keep this call inside its config
|
|
338
|
+
* source and see every live edit on the next read.
|
|
339
|
+
* @param input - raw (possibly partial) plugin config, or the Loader's live one.
|
|
243
340
|
* @returns the fully resolved config.
|
|
244
341
|
*/
|
|
245
|
-
export function resolveConfig(
|
|
342
|
+
export function resolveConfig(input: ConfigInput = {}): ResolvedConfig {
|
|
343
|
+
const config = plainConfig(input)
|
|
246
344
|
const rulesFile = config.rulesFile ?? '.dsh/rules.yaml'
|
|
247
345
|
const searchUp = config.searchUp ?? false
|
|
248
346
|
if (searchUp && isAbsolute(rulesFile)) {
|
package/src/runtime.ts
CHANGED
|
@@ -21,7 +21,7 @@ import type { CommandInvocation, CommandResult } from '@deepseek-ai/dsh-commands
|
|
|
21
21
|
import type { PreToolDecision, ToolExecution } from '@deepseek-ai/dsh-tools'
|
|
22
22
|
import type { SessionEvent } from '@deepseek-ai/dsh-session'
|
|
23
23
|
import { resolveConfig } from './config.ts'
|
|
24
|
-
import type {
|
|
24
|
+
import type { LiveConfig, ResolvedConfig } from './config.ts'
|
|
25
25
|
import type { CallId } from './call-id.ts'
|
|
26
26
|
import { insertAllowRule, isWritableHost, normalizeAllowHost, resolveAllowHostTarget } from './allow-host.ts'
|
|
27
27
|
import type { AllowHostInsertion } from './allow-host.ts'
|
|
@@ -1639,10 +1639,15 @@ export class PermissionRulesRuntime {
|
|
|
1639
1639
|
/**
|
|
1640
1640
|
* Mount the plugin: resolve config, validate deployment-level rule files,
|
|
1641
1641
|
* register the pre-execute listener and the `/rules` command.
|
|
1642
|
+
*
|
|
1643
|
+
* The `config` argument is the Loader's LIVE config (every field a
|
|
1644
|
+
* `.volatile()` reference, see {@link LiveConfig}); `resolveConfig` unwraps
|
|
1645
|
+
* it, and the same object is handed to {@link attachSettingsSection} so the
|
|
1646
|
+
* runtime keeps reading the current values instead of a mount-time snapshot.
|
|
1642
1647
|
* @param ctx - the host context.
|
|
1643
|
-
* @param config -
|
|
1648
|
+
* @param config - the Loader-delivered live plugin config.
|
|
1644
1649
|
*/
|
|
1645
|
-
export async function apply(ctx: Context, config:
|
|
1650
|
+
export async function apply(ctx: Context, config: LiveConfig): Promise<void> {
|
|
1646
1651
|
const resolved = resolveConfig(config)
|
|
1647
1652
|
const runtime = new PermissionRulesRuntime(ctx, resolved)
|
|
1648
1653
|
runtime.validateDeploymentFiles()
|
package/src/settings.ts
CHANGED
|
@@ -1,74 +1,89 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Host-side
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
2
|
+
* Host-side live-configuration wiring for the `0.1.7-alpha` settings
|
|
3
|
+
* contract. The plugin's Config fields are declared `.volatile()`, the
|
|
4
|
+
* Loader hands `apply` one live reference per field, and the settings
|
|
5
|
+
* surface edits those references in place. This module therefore owns three
|
|
6
|
+
* things and nothing else: it tells the settings service that this plugin
|
|
7
|
+
* ships its OWN page (so no automatic form is generated for it), it keeps
|
|
8
|
+
* the runtime's config source pointed at the CURRENT reference values, and
|
|
9
|
+
* it rebinds the network proxy when a bind/env-relevant knob changes live.
|
|
9
10
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
11
|
+
* There is no registration any more: the namespace of a settings form is the
|
|
12
|
+
* profile entry id, chosen by the composition, not by the plugin. The
|
|
13
|
+
* service is still reached structurally (`ctx.inject(['settings'])` plus
|
|
14
|
+
* `ctx.get('settings')`) because `@deepseek-ai/dsh-settings` is not a
|
|
15
|
+
* dependency of this package — on a host without it nothing registers and
|
|
16
|
+
* the composition entry keeps serving the config, because the references
|
|
17
|
+
* ARE the entry's own values.
|
|
13
18
|
* @module dsh-permission-rules/settings
|
|
14
19
|
*/
|
|
15
20
|
|
|
16
21
|
import type { Context } from '@deepseek-ai/cordis'
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
22
|
+
// Type-only: merges the Loader's `loader/volatile-update` event into `Events`.
|
|
23
|
+
// It is a devDependency (the Loader is composed by the host), and the import is
|
|
24
|
+
// erased, so nothing is required at runtime — a host whose Loader predates the
|
|
25
|
+
// event simply never emits it, and this plugin has nothing to reconcile then.
|
|
26
|
+
import type {} from '@deepseek-ai/cordis-plugin-loader'
|
|
27
|
+
import { resolveConfig } from './config.ts'
|
|
28
|
+
import type { LiveConfig, ResolvedConfig } from './config.ts'
|
|
20
29
|
import type { PermissionRulesRuntime } from './runtime.ts'
|
|
21
30
|
|
|
22
|
-
/**
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/** The structural settings-provider shape the real `ctx.settings` satisfies. */
|
|
32
|
-
interface SettingsProviderLike {
|
|
33
|
-
register<T>(ns: string, schema: z<T>, options?: {
|
|
34
|
-
base?: Partial<T>
|
|
35
|
-
expose?: boolean
|
|
36
|
-
applies?: string
|
|
37
|
-
validate?: (value: T) => void
|
|
38
|
-
}): SettingsScopeLike<T>
|
|
31
|
+
/**
|
|
32
|
+
* The structural settings-service face this plugin uses. The real
|
|
33
|
+
* `ctx.settings` is a `SettingsForms` service; only `configure` is read, and
|
|
34
|
+
* only through this shape, so an older or replaced service is tolerated.
|
|
35
|
+
*/
|
|
36
|
+
interface SettingsLike {
|
|
37
|
+
configure(presentation: { auto?: boolean }, owner: unknown): () => void
|
|
39
38
|
}
|
|
40
39
|
|
|
41
40
|
/**
|
|
42
|
-
*
|
|
43
|
-
*
|
|
41
|
+
* Point the runtime's live config source at the Loader's references and
|
|
42
|
+
* register this instance's settings-page policy.
|
|
44
43
|
* @param ctx - the plugin context.
|
|
45
|
-
* @param runtime - the runtime whose config source follows the
|
|
46
|
-
* @param
|
|
44
|
+
* @param runtime - the runtime whose config source follows the references.
|
|
45
|
+
* @param config - the Loader-delivered live config (references to the entry's values).
|
|
47
46
|
*/
|
|
48
|
-
export function attachSettingsSection(ctx: Context, runtime: PermissionRulesRuntime,
|
|
47
|
+
export function attachSettingsSection(ctx: Context, runtime: PermissionRulesRuntime, config: LiveConfig): void {
|
|
49
48
|
ctx.inject(['settings'], (scope) => {
|
|
50
|
-
const settings = scope.get('settings') as
|
|
51
|
-
if (settings === undefined) return
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
49
|
+
const settings = scope.get('settings') as SettingsLike | undefined
|
|
50
|
+
if (settings?.configure === undefined) return
|
|
51
|
+
// The policy belongs to THIS plugin instance's fiber, and riding an effect
|
|
52
|
+
// means a late-loading or replaced Settings service still picks it up and
|
|
53
|
+
// an unload withdraws it.
|
|
54
|
+
scope.effect(() => settings.configure({ auto: false }, ctx.fiber))
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
// LAZY on purpose: `resolveConfig` unwraps the references on every call, so
|
|
58
|
+
// a live edit is visible to the very next read. Resolving once here instead
|
|
59
|
+
// would freeze the config at mount and silently break every later change —
|
|
60
|
+
// which is exactly what the rebind listener below exists to serve.
|
|
61
|
+
runtime.setConfigSource(() => resolveConfig(config))
|
|
62
|
+
|
|
63
|
+
// A volatile-only update does NOT remount the plugin (the Loader commits the
|
|
64
|
+
// references and emits), so every bind-time side effect has to be
|
|
65
|
+
// reconciled here. Only the four knobs below decide the proxy's socket, its
|
|
66
|
+
// injected environment, and the NO_PROXY policy; every other field is read
|
|
67
|
+
// live through the config source, so a mode or rule-file change needs no
|
|
68
|
+
// work at all.
|
|
69
|
+
let previous = resolveConfig(config)
|
|
70
|
+
ctx.on('loader/volatile-update', () => {
|
|
71
|
+
let next: ResolvedConfig
|
|
72
|
+
try {
|
|
73
|
+
next = resolveConfig(config)
|
|
74
|
+
} catch (error: unknown) {
|
|
75
|
+
// Every reference is already committed when this fires, and the Loader
|
|
76
|
+
// catches and warns on a listener throw anyway — reporting it in this
|
|
77
|
+
// plugin's own vocabulary is the only signal an operator gets that a
|
|
78
|
+
// live edit left the config unusable.
|
|
79
|
+
ctx.logger.warn(`permission-rules: live config update is not usable: ${error instanceof Error ? error.message : String(error)}`)
|
|
80
|
+
return
|
|
81
|
+
}
|
|
82
|
+
const rebind = next.network.proxyPort !== previous.network.proxyPort
|
|
83
|
+
|| next.network.proxyBind !== previous.network.proxyBind
|
|
84
|
+
|| next.network.injectEnv !== previous.network.injectEnv
|
|
85
|
+
|| next.network.noProxy !== previous.network.noProxy
|
|
86
|
+
previous = next
|
|
87
|
+
if (rebind) void runtime.onNetworkConfigChanged()
|
|
73
88
|
})
|
|
74
89
|
}
|
package/src/wire.ts
CHANGED
|
@@ -12,14 +12,15 @@ import { z } from 'zod'
|
|
|
12
12
|
import type { InvocationDescriptor } from '@deepseek-ai/dsh-typert-protocol'
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
|
-
* Strict wire codec
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
15
|
+
* Strict wire codec. `create` is the ONLY schema face the `0.1.7-alpha`
|
|
16
|
+
* `TypertCodec` declares — it materializes the process-realm schema on first
|
|
17
|
+
* boundary use — and the typert-loader rejects any codec without it. The
|
|
18
|
+
* older published lines also carried a literal `schema` field on the codec;
|
|
19
|
+
* that face is gone from the contract, so carrying it here would be dead
|
|
20
|
+
* weight (and only the `create` factory is ever consulted at run time).
|
|
20
21
|
*/
|
|
21
22
|
function strictWire<T>(typeSymbol: string, schema: T) {
|
|
22
|
-
return Object.freeze({
|
|
23
|
+
return Object.freeze({ mode: 'strict' as const, typeSymbol, create: () => schema })
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
/** The three network policy modes (wire vocabulary). */
|