dsh-perm-gate 2.1.0 → 3.0.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.
Files changed (77) hide show
  1. package/CHANGELOG.ja.md +82 -0
  2. package/CHANGELOG.ko.md +80 -0
  3. package/CHANGELOG.md +321 -0
  4. package/INSTALL.ja.md +70 -8
  5. package/INSTALL.ko.md +68 -9
  6. package/INSTALL.md +68 -8
  7. package/INSTALL.zh.md +59 -7
  8. package/README.ja.md +103 -16
  9. package/README.ko.md +101 -17
  10. package/README.md +152 -25
  11. package/README.zh.md +99 -12
  12. package/cordis.patch.yml +34 -3
  13. package/docs/baseline-guard.md +56 -0
  14. package/docs/plan-rules-to-settings.md +217 -0
  15. package/docs/rules-format.en.md +294 -0
  16. package/docs/rules-format.md +279 -0
  17. package/docs/ui-gap-analysis-and-branch-permissions.md +338 -0
  18. package/dsh.plugin.json +22 -0
  19. package/examples/permissions.example.yaml +16 -1
  20. package/lib/agent-identity.d.ts +43 -0
  21. package/lib/agent-identity.js +95 -0
  22. package/lib/allowlist.d.ts +27 -0
  23. package/lib/allowlist.js +103 -1
  24. package/lib/cli.js +11 -14
  25. package/lib/client.js +598 -6
  26. package/lib/client.js.map +1 -1
  27. package/lib/command-dispatcher.d.ts +22 -0
  28. package/lib/command-dispatcher.js +97 -0
  29. package/lib/command-semantics.d.ts +62 -0
  30. package/lib/command-semantics.js +6 -0
  31. package/lib/compiler.d.ts +50 -0
  32. package/lib/compiler.js +122 -0
  33. package/lib/config.d.ts +161 -7
  34. package/lib/config.js +78 -11
  35. package/lib/dry-run.d.ts +87 -0
  36. package/lib/dry-run.js +110 -0
  37. package/lib/engine.js +61 -4
  38. package/lib/evaluate.d.ts +9 -0
  39. package/lib/evaluate.js +264 -1
  40. package/lib/events.d.ts +69 -2
  41. package/lib/events.js +123 -0
  42. package/lib/index.d.ts +1 -1
  43. package/lib/index.js +320 -11
  44. package/lib/network-lifecycle.d.ts +107 -0
  45. package/lib/network-lifecycle.js +180 -0
  46. package/lib/network.d.ts +109 -0
  47. package/lib/network.js +196 -0
  48. package/lib/parsers/git.d.ts +43 -0
  49. package/lib/parsers/git.js +404 -0
  50. package/lib/parsers/shell-cmds.d.ts +32 -0
  51. package/lib/parsers/shell-cmds.js +293 -0
  52. package/lib/preset.d.ts +13 -0
  53. package/lib/preset.js +17 -0
  54. package/lib/proxy.d.ts +160 -0
  55. package/lib/proxy.js +529 -0
  56. package/lib/risk.d.ts +17 -3
  57. package/lib/risk.js +19 -6
  58. package/lib/rule-chain.d.ts +39 -0
  59. package/lib/rule-chain.js +213 -0
  60. package/lib/rule-dims.d.ts +174 -0
  61. package/lib/rule-dims.js +230 -0
  62. package/lib/rule.d.ts +60 -0
  63. package/lib/rule.js +69 -7
  64. package/lib/rules-view.d.ts +52 -0
  65. package/lib/rules-view.js +126 -0
  66. package/lib/runtime.d.ts +208 -7
  67. package/lib/runtime.js +433 -27
  68. package/lib/session-sweep.d.ts +45 -0
  69. package/lib/session-sweep.js +153 -0
  70. package/lib/shadow.d.ts +34 -0
  71. package/lib/shadow.js +143 -0
  72. package/lib/shell.d.ts +14 -0
  73. package/lib/shell.js +17 -0
  74. package/lib/watch.d.ts +42 -0
  75. package/lib/watch.js +170 -0
  76. package/package.json +15 -2
  77. package/scripts/patch-permission-glyph.mjs +209 -0
@@ -0,0 +1,293 @@
1
+ /**
2
+ * Shell dangerous commands parser - rm, mv, cp, chmod, dd, diskpart, etc.
3
+ * P1 priority - detects destructive filesystem operations.
4
+ *
5
+ * Handles:
6
+ * - rm -rf / recursive deletion
7
+ * - mv overwriting existing files
8
+ * - cp -r with destructive patterns
9
+ * - chmod/chown with system directories
10
+ * - dd writing to disk
11
+ * - diskpart on Windows
12
+ * - format disk operations
13
+ * - PowerShell Remove-Item with dangerous patterns
14
+ */
15
+ /** Commands that are inherently destructive (only dangerous ones trigger canParse) */
16
+ const DESTRUCTIVE_COMMANDS = {
17
+ // Level 5: Extreme (system-wide, data loss)
18
+ 'dd': 5,
19
+ 'diskpart': 5,
20
+ 'format': 5,
21
+ 'fdisk': 5,
22
+ 'mkfs': 5,
23
+ 'rm-sudo': 5,
24
+ // Level 4: High (significant file deletion/modification)
25
+ 'rm': 4,
26
+ 'rmdir': 4,
27
+ 'shred': 4,
28
+ 'wipefs': 4,
29
+ // Level 3: Medium (file operations with potential data loss)
30
+ 'mv': 3,
31
+ // Level 2: Low (potentially risky)
32
+ 'cp': 2,
33
+ 'chmod': 2,
34
+ 'chown': 2,
35
+ 'chgrp': 2,
36
+ 'install': 2,
37
+ 'rsync': 2,
38
+ 'tar': 2,
39
+ };
40
+ /** System directories that shouldn't be modified */
41
+ const SYSTEM_DIRS = /^(\/bin|\/sbin|\/usr|\/lib|\/etc|\/boot|\/dev|\/proc|\/sys|\/var|C:\\Windows|C:\\Program Files)/i;
42
+ /** Patterns that indicate destructive operations */
43
+ const DESTRUCTIVE_PATTERNS = [
44
+ /rm\s+(-\w*\s+)*[^-\w]*\//i, // rm with /
45
+ /rm\s+(-\w*\s+)*\*/i, // rm with *
46
+ /rm\s+(-\w*\s+)*~/i, // rm with ~
47
+ /rm\s+(-\w*\s+)*\/[a-z]/i, // rm /anything
48
+ /rmdir\s+/i,
49
+ /mv\s+.*\//i, // mv to absolute path
50
+ /chmod\s+[0-7]*\s*\//i, // chmod on root paths
51
+ /chown\s+.*\//i, // chown on root paths
52
+ /dd\s+.*of=/i, // dd writing
53
+ /mkfs\./i, // format filesystem
54
+ />\s*\//i, // redirect to /
55
+ />>\s*\//i, // append to /
56
+ ];
57
+ export class ShellDangerousParser {
58
+ name = 'shell-dangerous';
59
+ priority = 20;
60
+ canParse(command) {
61
+ const cmd = command.trim().toLowerCase();
62
+ const baseName = this.extractBaseName(cmd);
63
+ // Check if it's a known destructive command
64
+ if (DESTRUCTIVE_COMMANDS[baseName] !== undefined) {
65
+ return true;
66
+ }
67
+ // Check for PowerShell equivalents
68
+ if (cmd.startsWith('remove-item') || cmd.startsWith('ri ')) {
69
+ return true;
70
+ }
71
+ // Check for destructive patterns
72
+ return DESTRUCTIVE_PATTERNS.some(p => p.test(command));
73
+ }
74
+ parse(command) {
75
+ const trimmed = command.trim();
76
+ const cmd = trimmed.toLowerCase();
77
+ const baseName = this.extractBaseName(cmd);
78
+ let destructiveness = DESTRUCTIVE_COMMANDS[baseName] || 1;
79
+ let operation = baseName;
80
+ let targets = [];
81
+ let destructiveFlags = false;
82
+ // Parse based on command type
83
+ if (baseName === 'rm' || baseName === 'rmdir') {
84
+ const parsed = this.parseRm(trimmed);
85
+ targets = parsed.targets;
86
+ destructiveFlags = parsed.recursive || parsed.force;
87
+ if (parsed.recursive && parsed.force) {
88
+ destructiveness = 5;
89
+ operation = 'recursive-force-delete';
90
+ }
91
+ else if (parsed.recursive) {
92
+ destructiveness = 4;
93
+ operation = 'recursive-delete';
94
+ }
95
+ else if (parsed.force) {
96
+ destructiveness = 3;
97
+ operation = 'force-delete';
98
+ }
99
+ // Check if targeting system directories
100
+ if (targets.some(t => SYSTEM_DIRS.test(t))) {
101
+ destructiveness = 5;
102
+ }
103
+ }
104
+ else if (baseName === 'mv') {
105
+ const parsed = this.parseMv(trimmed);
106
+ targets = parsed.targets;
107
+ destructiveFlags = parsed.force;
108
+ if (parsed.force) {
109
+ destructiveness = 4;
110
+ operation = 'force-move';
111
+ }
112
+ // Check if overwriting system files
113
+ if (targets.some(t => SYSTEM_DIRS.test(t))) {
114
+ destructiveness = 4;
115
+ }
116
+ }
117
+ else if (baseName === 'chmod' || baseName === 'chown' || baseName === 'chgrp') {
118
+ const parsed = this.parseChmod(trimmed);
119
+ targets = parsed.targets;
120
+ if (targets.some(t => SYSTEM_DIRS.test(t))) {
121
+ destructiveness = 4;
122
+ }
123
+ }
124
+ else if (baseName === 'dd') {
125
+ const parsed = this.parseDd(trimmed);
126
+ targets = parsed.targets;
127
+ destructiveFlags = parsed.force;
128
+ }
129
+ else if (baseName === 'diskpart' || baseName === 'format') {
130
+ const parsed = this.parseDiskpart(trimmed);
131
+ targets = parsed.targets;
132
+ destructiveFlags = true;
133
+ }
134
+ else if (baseName.startsWith('remove-item') || baseName.startsWith('ri ')) {
135
+ const parsed = this.parsePowerShellRemove(trimmed);
136
+ targets = parsed.targets;
137
+ destructiveFlags = parsed.recursive || parsed.force;
138
+ if (parsed.recursive && parsed.force) {
139
+ destructiveness = 5;
140
+ operation = 'recursive-force-delete';
141
+ }
142
+ else if (parsed.recursive) {
143
+ destructiveness = 4;
144
+ operation = 'recursive-delete';
145
+ }
146
+ }
147
+ else if (baseName === 'cp') {
148
+ const parsed = this.parseCp(trimmed);
149
+ targets = parsed.targets;
150
+ destructiveFlags = parsed.force;
151
+ if (parsed.recursive) {
152
+ operation = 'recursive-copy';
153
+ destructiveness = 3;
154
+ }
155
+ }
156
+ else if (baseName === 'tar' || baseName === 'rsync') {
157
+ const parsed = this.parseArchive(trimmed);
158
+ targets = parsed.targets;
159
+ destructiveFlags = parsed.delete;
160
+ if (parsed.delete) {
161
+ destructiveness = 4;
162
+ operation = 'archive-delete-target';
163
+ }
164
+ }
165
+ const flags = this.extractFlags(trimmed);
166
+ return {
167
+ family: 'shell-dangerous',
168
+ subcommand: baseName,
169
+ operation,
170
+ destructiveness: destructiveness,
171
+ destructiveFlags,
172
+ targets,
173
+ flags,
174
+ requiresConfirmation: destructiveness >= 4,
175
+ compound: false,
176
+ gitOperation: false,
177
+ targetsSharedBranch: false,
178
+ rawCommand: trimmed,
179
+ };
180
+ }
181
+ extractBaseName(command) {
182
+ // Handle sh -c "..." patterns
183
+ const shMatch = command.match(/(?:sh|bash|pwsh)(?:\s+-c\s+['"]?)(.+?)(?:['"]?)$/);
184
+ const actual = shMatch ? shMatch[1] : command;
185
+ // Extract first word
186
+ const parts = actual.trim().split(/\s+/);
187
+ return parts[0]?.toLowerCase() || '';
188
+ }
189
+ parseRm(command) {
190
+ const args = this.extractArgs(command);
191
+ const recursive = args.some(a => a === '-r' || a === '-rf' || a === '-fr' || a === '--recursive');
192
+ const force = args.some(a => a === '-f' || a === '-rf' || a === '-fr' || a === '--force');
193
+ const targets = args.filter(a => !a.startsWith('-'));
194
+ return { targets, recursive, force };
195
+ }
196
+ parseMv(command) {
197
+ const args = this.extractArgs(command);
198
+ const force = args.some(a => a === '-f' || a === '--force');
199
+ const targets = args.filter(a => !a.startsWith('-'));
200
+ return { targets, force };
201
+ }
202
+ parseCp(command) {
203
+ const args = this.extractArgs(command);
204
+ const recursive = args.some(a => a === '-r' || a === '-R' || a === '--recursive');
205
+ const force = args.some(a => a === '-f' || a === '--force');
206
+ const targets = args.filter(a => !a.startsWith('-'));
207
+ return { targets, recursive, force };
208
+ }
209
+ parseChmod(command) {
210
+ const args = this.extractArgs(command);
211
+ const targets = args.filter(a => !a.startsWith('-') && !/^[0-7]+$/.test(a));
212
+ return { targets };
213
+ }
214
+ parseDd(command) {
215
+ const args = this.extractArgs(command);
216
+ const targets = [];
217
+ for (const arg of args) {
218
+ if (arg.startsWith('of=')) {
219
+ targets.push(arg.slice(3));
220
+ }
221
+ if (arg.startsWith('if=')) {
222
+ targets.push(arg.slice(3));
223
+ }
224
+ }
225
+ const force = command.includes('conv=') || command.includes('oflag=');
226
+ return { targets, force };
227
+ }
228
+ parseDiskpart(command) {
229
+ // Diskpart commands are always dangerous
230
+ const args = this.extractArgs(command);
231
+ const targets = args.filter(a => !a.startsWith('-'));
232
+ return { targets };
233
+ }
234
+ parsePowerShellRemove(command) {
235
+ const args = this.extractArgs(command);
236
+ const recursive = args.some(a => a === '-Recurse' || a === '-r');
237
+ const force = args.some(a => a === '-Force' || a === '-f');
238
+ const targets = args.filter(a => !a.startsWith('-'));
239
+ return { targets, recursive, force };
240
+ }
241
+ parseArchive(command) {
242
+ const args = this.extractArgs(command);
243
+ const deleteFlag = args.some(a => a === '--delete' || a === '-d');
244
+ const targets = args.filter(a => !a.startsWith('-'));
245
+ return { targets, delete: deleteFlag };
246
+ }
247
+ extractArgs(command) {
248
+ const args = [];
249
+ let current = '';
250
+ let inQuote = false;
251
+ let quoteChar = '';
252
+ let escape = false;
253
+ for (const char of command) {
254
+ if (escape) {
255
+ current += char;
256
+ escape = false;
257
+ continue;
258
+ }
259
+ if (char === '\\') {
260
+ escape = true;
261
+ continue;
262
+ }
263
+ if (inQuote) {
264
+ if (char === quoteChar) {
265
+ inQuote = false;
266
+ continue;
267
+ }
268
+ current += char;
269
+ continue;
270
+ }
271
+ if (char === '"' || char === "'") {
272
+ inQuote = true;
273
+ quoteChar = char;
274
+ continue;
275
+ }
276
+ if (/\s/.test(char)) {
277
+ if (current) {
278
+ args.push(current);
279
+ current = '';
280
+ }
281
+ continue;
282
+ }
283
+ current += char;
284
+ }
285
+ if (current)
286
+ args.push(current);
287
+ return args;
288
+ }
289
+ extractFlags(command) {
290
+ const args = this.extractArgs(command);
291
+ return args.filter(a => a.startsWith('-'));
292
+ }
293
+ }
package/lib/preset.d.ts CHANGED
@@ -28,3 +28,16 @@ export interface SessionEventLike {
28
28
  export declare function permissionPresetOf(events: readonly SessionEventLike[] | undefined): string | undefined;
29
29
  /** Whether one preset name is covered by a configured scope list. */
30
30
  export declare function presetInScope(preset: string | undefined, scope: readonly string[]): boolean;
31
+ /**
32
+ * Presets in which the gate is active. It owns both 自动审查 tiers — the plain
33
+ * one (file sandbox kept) and the full-access one (sandbox restriction lifted,
34
+ * same approval behaviour) — so a user can have the gate without inheriting a
35
+ * sandbox that breaks `git` / Cygwin tools. `'*'` makes it global (every
36
+ * preset, including the hard-deny layer).
37
+ *
38
+ * Lives in this dependency-free module (not `config.ts`) so the browser half can
39
+ * render the scope without pulling schemastery into the client bundle.
40
+ */
41
+ export declare const DEFAULT_GATE_PRESETS: readonly string[];
42
+ /** Normalize the gate scope: an unset/empty list means the default. */
43
+ export declare function resolveGatePresets(configured?: readonly string[]): readonly string[];
package/lib/preset.js CHANGED
@@ -39,3 +39,20 @@ export function presetInScope(preset, scope) {
39
39
  return false;
40
40
  return scope.includes(preset);
41
41
  }
42
+ /**
43
+ * Presets in which the gate is active. It owns both 自动审查 tiers — the plain
44
+ * one (file sandbox kept) and the full-access one (sandbox restriction lifted,
45
+ * same approval behaviour) — so a user can have the gate without inheriting a
46
+ * sandbox that breaks `git` / Cygwin tools. `'*'` makes it global (every
47
+ * preset, including the hard-deny layer).
48
+ *
49
+ * Lives in this dependency-free module (not `config.ts`) so the browser half can
50
+ * render the scope without pulling schemastery into the client bundle.
51
+ */
52
+ export const DEFAULT_GATE_PRESETS = ['permissive', 'permissive-full'];
53
+ /** Normalize the gate scope: an unset/empty list means the default. */
54
+ export function resolveGatePresets(configured) {
55
+ if (configured === undefined || configured.length === 0)
56
+ return DEFAULT_GATE_PRESETS;
57
+ return configured.filter((name) => typeof name === 'string' && name !== '');
58
+ }
package/lib/proxy.d.ts ADDED
@@ -0,0 +1,160 @@
1
+ import type { NetworkDecision, NetworkMode, NetworkTarget } from './network.js';
2
+ /** Proxy env var names the injector sets/restores (upper + lower for mixed-ecosystem CLIs). */
3
+ export declare const PROXY_ENV_NAMES: readonly string[];
4
+ /** NO_PROXY env var names cleared by the injector. */
5
+ export declare const NO_PROXY_ENV_NAMES: readonly string[];
6
+ /** Default DNS lookup bound (ms). A black-holed resolver must not hang a CONNECT. */
7
+ export declare const DNS_TIMEOUT_MS = 3000;
8
+ /** Default connection-setup bound (ms) — slowloris guard. */
9
+ export declare const HEADERS_TIMEOUT_MS = 60000;
10
+ /** Default cap on concurrent accepted connections. */
11
+ export declare const MAX_CONNECTIONS = 256;
12
+ /** Bound on how long close() waits for a server to finish closing (ms). */
13
+ export declare const CLOSE_TIMEOUT_MS = 2000;
14
+ /** One recorded proxy-layer block. */
15
+ export interface NetworkBlockRecord {
16
+ readonly time: number;
17
+ readonly tool: string;
18
+ readonly attributed: boolean;
19
+ readonly callId?: string;
20
+ readonly domain: string;
21
+ readonly scheme?: string;
22
+ readonly port?: number;
23
+ readonly action: 'deny' | 'ask';
24
+ readonly mode: NetworkMode;
25
+ readonly matched: boolean;
26
+ readonly source: string;
27
+ readonly ruleIndex?: number;
28
+ readonly reason?: string;
29
+ }
30
+ /** Cumulative proxy-layer block counters. */
31
+ export interface NetworkStats {
32
+ denied: number;
33
+ askBlocked: number;
34
+ }
35
+ /** Attribution the runtime supplies for one connection. */
36
+ export interface ProxyAttribution {
37
+ readonly tool: string;
38
+ readonly callId?: string;
39
+ }
40
+ /** Construction options for NetworkProxy. */
41
+ export interface NetworkProxyOptions {
42
+ /** Bind address (loopback by config). */
43
+ readonly bind: string;
44
+ /** Requested port; `0` binds an ephemeral port. */
45
+ readonly port: number;
46
+ /** Cap on recent-block records kept in memory. */
47
+ readonly maxRecent: number;
48
+ /** Decision function supplied by the runtime. */
49
+ readonly decide: (target: NetworkTarget) => NetworkDecision | Promise<NetworkDecision>;
50
+ /**
51
+ * Escalate an `ask` decision to the interactive approval seam. Called ONLY
52
+ * when {@link decide} returns `ask` — a `deny` is never escalated, so the
53
+ * rule review cannot be bypassed by approving a connection.
54
+ *
55
+ * Absent, throwing, or timing out all mean "not allowed": the connection is
56
+ * blocked exactly as it would have been.
57
+ */
58
+ readonly escalate?: (target: NetworkTarget, decision: NetworkDecision) => Promise<'allow' | 'deny'>;
59
+ /** Current attribution (newest in-flight shell execution). */
60
+ readonly attribution?: () => ProxyAttribution | undefined;
61
+ /** Called for every blocked connection. */
62
+ readonly onBlock?: (record: NetworkBlockRecord, attribution: ProxyAttribution | undefined) => void;
63
+ /** Bind a DNS lookup with this timeout (ms). Default {@link DNS_TIMEOUT_MS}. */
64
+ readonly dnsTimeoutMs?: number;
65
+ /** Connection-setup bound (ms). Default {@link HEADERS_TIMEOUT_MS}. */
66
+ readonly headersTimeoutMs?: number;
67
+ /** Cap on concurrent connections. Default {@link MAX_CONNECTIONS}. */
68
+ readonly maxConnections?: number;
69
+ /** Logger sink (proxy failures must never crash the host). */
70
+ readonly logger: {
71
+ warn(message: string): void;
72
+ };
73
+ }
74
+ /**
75
+ * The local HTTP/CONNECT policy proxy. Binds on demand; every live tunnel
76
+ * socket is tracked and destroyed on close so updates and uninstalls leave
77
+ * no orphaned connections.
78
+ */
79
+ export declare class NetworkProxy {
80
+ private readonly options;
81
+ private server;
82
+ private readonly sockets;
83
+ private readonly recent;
84
+ private readonly stats;
85
+ private actualPort;
86
+ /** In-flight start(); guards against a second concurrent bind. */
87
+ private starting;
88
+ /** Set by close(); a bind resolving afterwards tears itself down. */
89
+ private closed;
90
+ constructor(options: NetworkProxyOptions);
91
+ /**
92
+ * Logger sink that can never throw. The logger is called from inside
93
+ * 'error' handlers, so a throwing logger would itself become an
94
+ * unhandled error and defeat the whole point of the guard.
95
+ */
96
+ private safeWarn;
97
+ /** The bound port (valid after start resolves). */
98
+ get port(): number;
99
+ /** Deny/ask blocks recorded since mount, newest first. */
100
+ recentBlocks(): readonly NetworkBlockRecord[];
101
+ /** Cumulative block counters. */
102
+ blockStats(): NetworkStats;
103
+ /** Number of active sockets (tunnels + client connections). */
104
+ activeSocketCount(): number;
105
+ /**
106
+ * Bind the server and return the actual port.
107
+ * On bind failure: warns and returns -1 (degraded mode — no proxy).
108
+ * Concurrent calls share the single in-flight bind.
109
+ */
110
+ start(): Promise<number>;
111
+ private bind;
112
+ /**
113
+ * Stop the server and destroy every tunnel socket.
114
+ *
115
+ * Safe against a concurrent (in-flight) {@link start}: the `closed` flag
116
+ * makes a bind that resolves after this call tear itself down instead of
117
+ * leaving an orphaned listening server behind.
118
+ */
119
+ close(): Promise<void>;
120
+ private handleRequest;
121
+ private handleConnect;
122
+ private forwardOrBlock;
123
+ /**
124
+ * DNS-resolve a hostname so `ips`-scoped rules see real addresses, then
125
+ * decide. Resolution failure → decide on the literal name.
126
+ *
127
+ * The lookup is **time-bounded**: an unbounded `lookup()` on a black-holed
128
+ * resolver would hold the CONNECT socket open indefinitely, so a slow
129
+ * resolver degrades to literal-name evaluation instead of hanging the
130
+ * client. `ips`-scoped rules simply cannot fire in that case.
131
+ */
132
+ private decideWithResolution;
133
+ /**
134
+ * The final decision for one target: the rule/mode verdict, then — only for
135
+ * an `ask` — the interactive approval seam.
136
+ *
137
+ * Escalation is deliberately narrow:
138
+ * - A `deny` verdict is NEVER escalated. Approving a connection can widen
139
+ * reach for a target no rule allows, but it can never override a rule that
140
+ * says no. The rule review stays authoritative.
141
+ * - An escalation that throws, is absent, or returns anything but `allow`
142
+ * leaves the verdict blocked.
143
+ */
144
+ private resolveDecision;
145
+ private recordBlock;
146
+ }
147
+ /**
148
+ * Inject proxy environment variables for subprocesses and return a disposer
149
+ * restoring every previous value exactly.
150
+ *
151
+ * The snapshot pass covers EVERY name BEFORE any write: on Windows
152
+ * `process.env` is case-insensitive, so writing `HTTP_PROXY` mid-loop
153
+ * would poison the later snapshot of `http_proxy`.
154
+ *
155
+ * @param port - the bound proxy port.
156
+ * @param noProxy - `'clear'` empties NO_PROXY so policy cannot be bypassed;
157
+ * `'preserve'` keeps ambient values.
158
+ * @returns the restore disposer.
159
+ */
160
+ export declare function injectProxyEnv(port: number, noProxy: 'clear' | 'preserve'): () => void;