dsh-perm-gate 2.1.0 → 2.6.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.
- package/CHANGELOG.ja.md +108 -0
- package/CHANGELOG.ko.md +104 -0
- package/CHANGELOG.md +347 -0
- package/INSTALL.ja.md +84 -8
- package/INSTALL.ko.md +80 -9
- package/INSTALL.md +83 -8
- package/INSTALL.zh.md +70 -7
- package/README.ja.md +130 -16
- package/README.ko.md +128 -17
- package/README.md +179 -25
- package/README.zh.md +121 -12
- package/assets/allow1.png +0 -0
- package/assets/allow2.png +0 -0
- package/assets/gateoff.png +0 -0
- package/assets/llms.png +0 -0
- package/assets/permlist.png +0 -0
- package/assets/setting-head.png +0 -0
- package/assets/setting-rules.png +0 -0
- package/cordis.patch.yml +37 -3
- package/docs/baseline-guard.md +56 -0
- package/docs/plan-rules-to-settings.md +217 -0
- package/docs/rules-format.en.md +294 -0
- package/docs/rules-format.md +279 -0
- package/docs/ui-gap-analysis-and-branch-permissions.md +338 -0
- package/dsh.plugin.json +22 -0
- package/examples/permissions.example.yaml +16 -1
- package/lib/agent-identity.d.ts +43 -0
- package/lib/agent-identity.js +95 -0
- package/lib/allowlist.d.ts +27 -0
- package/lib/allowlist.js +103 -1
- package/lib/cli.js +11 -14
- package/lib/client.js +598 -6
- package/lib/client.js.map +1 -1
- package/lib/command-dispatcher.d.ts +22 -0
- package/lib/command-dispatcher.js +97 -0
- package/lib/command-semantics.d.ts +62 -0
- package/lib/command-semantics.js +6 -0
- package/lib/compiler.d.ts +50 -0
- package/lib/compiler.js +122 -0
- package/lib/config.d.ts +161 -7
- package/lib/config.js +78 -11
- package/lib/dry-run.d.ts +87 -0
- package/lib/dry-run.js +110 -0
- package/lib/engine.js +61 -4
- package/lib/evaluate.d.ts +9 -0
- package/lib/evaluate.js +264 -1
- package/lib/events.d.ts +69 -2
- package/lib/events.js +123 -0
- package/lib/index.d.ts +1 -1
- package/lib/index.js +320 -11
- package/lib/network-lifecycle.d.ts +107 -0
- package/lib/network-lifecycle.js +180 -0
- package/lib/network.d.ts +109 -0
- package/lib/network.js +196 -0
- package/lib/parsers/git.d.ts +43 -0
- package/lib/parsers/git.js +404 -0
- package/lib/parsers/shell-cmds.d.ts +32 -0
- package/lib/parsers/shell-cmds.js +293 -0
- package/lib/preset.d.ts +13 -0
- package/lib/preset.js +17 -0
- package/lib/proxy.d.ts +160 -0
- package/lib/proxy.js +529 -0
- package/lib/risk.d.ts +17 -3
- package/lib/risk.js +19 -6
- package/lib/rule-chain.d.ts +39 -0
- package/lib/rule-chain.js +213 -0
- package/lib/rule-dims.d.ts +174 -0
- package/lib/rule-dims.js +230 -0
- package/lib/rule.d.ts +60 -0
- package/lib/rule.js +69 -7
- package/lib/rules-view.d.ts +52 -0
- package/lib/rules-view.js +126 -0
- package/lib/runtime.d.ts +208 -7
- package/lib/runtime.js +433 -27
- package/lib/session-sweep.d.ts +45 -0
- package/lib/session-sweep.js +153 -0
- package/lib/shadow.d.ts +34 -0
- package/lib/shadow.js +143 -0
- package/lib/shell.d.ts +14 -0
- package/lib/shell.js +17 -0
- package/lib/watch.d.ts +42 -0
- package/lib/watch.js +170 -0
- package/package.json +26 -3
- package/scripts/patch-permission-glyph.mjs +392 -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;
|