dsh-perm-gate 3.0.0 → 4.1.0-beta.1

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.
@@ -1,217 +0,0 @@
1
- # Plan: rules.yml → DSH Settings Namespace Migration
2
-
3
- ## Goal
4
-
5
- Eliminate the external `rules.yml` file dependency by migrating rules into the DSH settings namespace (`dsh-perm-gate.rules`). This solves the cold-start directory problem at the root cause: settings are managed by the framework and loaded before any plugin `apply()` runs.
6
-
7
- ## Current Architecture
8
-
9
- ```
10
- User edits rules.yml (or allowlist card writes to it)
11
- ↓
12
- resolveRulesFile() → path string
13
- ↓
14
- readFileSafe() → raw YAML text
15
- ↓
16
- parsePermissionsDocument() → structured doc
17
- ↓
18
- compileDocument() → CompiledRuleset
19
- ↓
20
- PermGateRuntime.decide() → verdict
21
- ```
22
-
23
- Read-only view:
24
- ```
25
- GET /api/dsh-perm-gate/rules
26
- → readRulesView(rulesFile)
27
- → statSync + readFileSync + parse + compile
28
- → RulesView { path, exists, bytes, hash, raw, counts, error }
29
- ```
30
-
31
- Write path (allowlist card):
32
- ```
33
- appendAllowCommand() / replaceAllowCommands()
34
- → readFileSync → parse → mutate → stringify → writeFileSync
35
- ```
36
-
37
- ## Target Architecture
38
-
39
- ```
40
- User edits via settings card (or API)
41
- ↓
42
- settings.get('dsh-perm-gate.rules') → structured object (already parsed)
43
- ↓
44
- compileDocument(rules) → CompiledRuleset
45
- ↓
46
- PermGateRuntime.decide() → verdict
47
- ```
48
-
49
- Read-only view:
50
- ```
51
- GET /api/dsh-perm-gate/rules
52
- → settings.get('dsh-perm-gate.rules')
53
- → RulesView { source: 'settings', rules: {...}, counts, hash }
54
- ```
55
-
56
- Write path (allowlist card):
57
- ```
58
- settings.update({ rules: { allow: [...] } })
59
- → framework persists to settings.yaml
60
- → settings.watch() triggers reload
61
- ```
62
-
63
- ## Schema (config.ts)
64
-
65
- ```typescript
66
- // New settings schema for rules
67
- const RuleEntrySchema = z.object({
68
- command: z.array(z.string()).optional(),
69
- args: z.array(z.string()).optional(),
70
- tools: z.array(z.string()).optional(),
71
- network: z.object({
72
- domains: z.array(z.string()),
73
- }).optional(),
74
- reason: z.string().default(''),
75
- })
76
-
77
- const RulesSchema = z.object({
78
- defaultAction: z.enum(['allow', 'deny', 'ask']).default('ask'),
79
- deny: z.array(RuleEntrySchema).default([]),
80
- allow: z.array(RuleEntrySchema).default([]),
81
- ask: z.array(RuleEntrySchema).default([]),
82
- network: z.object({
83
- domains: z.array(z.string()).default([]),
84
- }).optional(),
85
- }).default({ defaultAction: 'ask', deny: [], allow: [] })
86
- ```
87
-
88
- ## Migration Steps
89
-
90
- ### Phase 1: Dual-Source (backward compatible)
91
-
92
- 1. **Add `rules` to settings schema** — new field in `dsh-perm-gate` namespace
93
- 2. **Runtime reads from settings first, falls back to file** — `settings.get('rules') ?? loadRulesFile()`
94
- 3. **Allowlist card writes to settings** — `settings.update({ rules: { allow: [...] } })`
95
- 4. **Rules view serves from settings** — `readRulesView()` reads from settings object, not file
96
- 5. **Module-level: still create dataDir** — belt-and-suspenders for the transition period
97
-
98
- ### Phase 2: File Deprecation
99
-
100
- 6. **Log deprecation warning** — when `rulesFile` is configured, warn that it's deprecated
101
- 7. **Import button** — UI can import existing `rules.yml` into settings
102
- 8. **Export button** — UI can export settings rules as YAML for version control
103
-
104
- ### Phase 3: File Removal
105
-
106
- 9. **Remove `rulesFile` config option** — no longer accepted
107
- 10. **Remove file I/O code** — `allowlist.ts`, `rules-view.ts` file operations
108
- 11. **Remove chokidar watcher** — settings has built-in `watch()`
109
- 12. **Remove dataDir bootstrap** — no longer needed
110
-
111
- ## How the Read-Only View Changes
112
-
113
- ### Before (file-based)
114
-
115
- ```json
116
- {
117
- "path": "C:\\Users\\joshua\\.dsh\\perm-gate\\rules.yml",
118
- "exists": true,
119
- "bytes": 1513,
120
- "hash": "abc123...",
121
- "lines": 61,
122
- "raw": "permissions:\n defaultAction: ask\n deny:\n ...",
123
- "truncated": false,
124
- "defaultAction": "ask",
125
- "counts": { "allow": 11, "deny": 1, "ask": 0 },
126
- "error": null
127
- }
128
- ```
129
-
130
- ### After (settings-based)
131
-
132
- ```json
133
- {
134
- "source": "settings",
135
- "defaultAction": "ask",
136
- "counts": { "allow": 11, "deny": 1, "ask": 0 },
137
- "hash": "abc123...",
138
- "rules": {
139
- "defaultAction": "ask",
140
- "deny": [
141
- { "tools": ["shell", "pwsh"], "command": ["rm#recursive"], "reason": "destructive" }
142
- ],
143
- "allow": [
144
- { "command": ["grep"], "reason": "permissive allowlist" }
145
- ],
146
- "network": {
147
- "domains": ["github.com", "registry.npmjs.org"]
148
- }
149
- }
150
- }
151
- ```
152
-
153
- **Key differences:**
154
- - `path` / `exists` / `bytes` / `raw` / `truncated` → removed (no file)
155
- - `source: 'settings'` → added (provenance)
156
- - `rules` → the structured object (already parsed, not raw YAML)
157
- - `counts` / `defaultAction` / `hash` → kept (useful for UI)
158
-
159
- ## How the Allowlist Write Path Changes
160
-
161
- ### Before (file-based)
162
-
163
- ```typescript
164
- // appendAllowCommand reads YAML, mutates, writes back
165
- appendAllowCommand(rulesFile, 'git push', 'permissive allow-everywhere')
166
- // → readFileSync → parse → push → stringify → writeFileSync
167
- ```
168
-
169
- ### After (settings-based)
170
-
171
- ```typescript
172
- // Direct settings update
173
- const current = settings.get('dsh-perm-gate.rules')
174
- const allow = [...(current.allow ?? []), { command: ['git push'], reason: 'permissive allow-everywhere' }]
175
- settings.update({ rules: { ...current, allow } })
176
- // → framework persists → settings.watch() triggers reload
177
- ```
178
-
179
- ## Files Changed
180
-
181
- | File | Change |
182
- |---|---|
183
- | `src/config.ts` | Add `RulesSchema`, keep `resolveRulesFile` for backward compat |
184
- | `src/index.ts` | Read from settings first, fall back to file; register settings schema |
185
- | `src/runtime.ts` | Accept `rules` object directly (no file read) |
186
- | `src/rules-view.ts` | Add `readRulesViewFromSettings()` |
187
- | `src/allowlist.ts` | Add `appendAllowToSettings()`, `replaceAllowInSettings()` |
188
- | `src/events.ts` | Update `registerRulesRoute` to use settings view |
189
- | `src/watch.ts` | Remove chokidar watcher (settings has built-in watch) |
190
-
191
- ## Cold-Start Resilience (All Plugins)
192
-
193
- The same pattern should be applied to other plugins that read files at startup:
194
-
195
- | Plugin | Data | Risk | Fix |
196
- |---|---|---|---|
197
- | `dsh-perm-gate` | rules.yml | HIGH | Settings migration (this plan) |
198
- | `dsh-search-index` | index files | MEDIUM | dataDir bootstrap + try/catch |
199
- | `dsh-session-guard` | guard config | LOW | Already settings-backed |
200
- | `dsh-prime-memory` | memory files | MEDIUM | dataDir bootstrap + try/catch |
201
- | `dsh-context-compression` | config | LOW | Already settings-backed |
202
-
203
- **Universal pattern:**
204
- 1. Module-level `ensureDataDir()` — belt
205
- 2. Read paths: `dataDirReady()` check → empty defaults if missing
206
- 3. Write paths: `ensureDataDir()` before first write
207
- 4. All file reads wrapped in try/catch with graceful fallback
208
-
209
- ## Testing
210
-
211
- - [ ] Unit: `compileDocument` with settings-sourced rules (same as YAML-sourced)
212
- - [ ] Unit: `readRulesViewFromSettings` returns correct `RulesView`
213
- - [ ] Unit: `appendAllowToSettings` correctly mutates settings object
214
- - [ ] Integration: settings card reads and displays rules
215
- - [ ] Integration: allowlist card writes persist to settings
216
- - [ ] Migration: existing `rules.yml` imports into settings correctly
217
- - [ ] Cold start: plugin loads with empty settings (no rules configured)