@goodandready/dsh-approval-gate 0.1.12 → 0.1.14
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 +23 -7
- package/lib/audit-log.js +76 -0
- package/lib/breaker.js +26 -2
- package/lib/cache.js +146 -0
- package/lib/dispatcher.js +208 -0
- package/lib/engine.js +129 -17
- package/lib/fingerprint.js +100 -0
- package/lib/index.js +80 -0
- package/lib/inspect.js +103 -160
- package/lib/ledger.js +164 -0
- package/lib/loopback.js +85 -0
- package/lib/messages.js +49 -1
- package/lib/parsers/git.js +321 -0
- package/lib/parsers/shell-cmds.js +267 -0
- package/lib/retry-exhaustion.js +171 -0
- package/package.json +1 -1
package/lib/engine.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// lib/engine.js
|
|
2
2
|
// 5-tier Security Decision Engine for dsh-approval-gate.
|
|
3
3
|
// Waterfall:
|
|
4
|
-
// P0: Hard Deny (Bands Band 0 + inspectExecution hard denials + protected files)
|
|
4
|
+
// P0: Hard Deny (Bands Band 0 + inspectExecution hard denials + protected files + retry exhaustion)
|
|
5
5
|
// P1: Session Grants (scoped temporary allowances)
|
|
6
6
|
// P2: Static Rules / Safe Allow (Bands Band 1 + safe read-only commands)
|
|
7
7
|
// P3: Optional Semantic Classifier
|
|
@@ -11,15 +11,26 @@ import { BandsEngine } from "./bands.js";
|
|
|
11
11
|
import { CircuitBreaker } from "./breaker.js";
|
|
12
12
|
import { SessionGrantStore } from "./grants.js";
|
|
13
13
|
import { inspectExecution } from "./inspect.js";
|
|
14
|
+
import { VerdictCache } from "./cache.js";
|
|
15
|
+
import { RetryExhaustionGuard } from "./retry-exhaustion.js";
|
|
16
|
+
import { QualityLedger } from "./ledger.js";
|
|
17
|
+
import { AuditLogger } from "./audit-log.js";
|
|
14
18
|
|
|
15
19
|
export class SecurityEngine {
|
|
16
20
|
/**
|
|
17
21
|
* @param {{
|
|
18
22
|
* bands?: BandsEngine,
|
|
19
23
|
* breaker?: CircuitBreaker,
|
|
20
|
-
*
|
|
24
|
+
* cache?: VerdictCache,
|
|
25
|
+
* retryGuard?: RetryExhaustionGuard,
|
|
26
|
+
* ledger?: QualityLedger,
|
|
27
|
+
* auditLogger?: AuditLogger,
|
|
28
|
+
* inspectConfig?: { toolName?: string, fileWriteTools?: string[], workspaceDir?: string },
|
|
21
29
|
* classifier?: ((execution: any, context?: any) => { verdict: "allow"|"deny", reason?: string }),
|
|
22
|
-
* grantRegistry?: { decide: (tool: string, args: any, context?: any) => "allow"|"no-match" }
|
|
30
|
+
* grantRegistry?: { decide: (tool: string, args: any, context?: any) => "allow"|"no-match" },
|
|
31
|
+
* dryRun?: boolean,
|
|
32
|
+
* shadowMode?: boolean,
|
|
33
|
+
* defaultVerdict?: "allow" | "ask"
|
|
23
34
|
* }} [options]
|
|
24
35
|
*/
|
|
25
36
|
constructor(options = {}) {
|
|
@@ -29,21 +40,115 @@ export class SecurityEngine {
|
|
|
29
40
|
this.classifier = options.classifier || null;
|
|
30
41
|
this.grantRegistry = options.grantRegistry || new SessionGrantStore(options);
|
|
31
42
|
this.defaultVerdict = options.defaultVerdict || "allow";
|
|
43
|
+
|
|
44
|
+
this.dryRun = Boolean(options.dryRun || options.shadowMode);
|
|
45
|
+
this.cache = options.cache !== undefined ? options.cache : new VerdictCache();
|
|
46
|
+
this.retryGuard = options.retryGuard !== undefined ? options.retryGuard : new RetryExhaustionGuard();
|
|
47
|
+
this.ledger = options.ledger !== undefined ? options.ledger : new QualityLedger();
|
|
48
|
+
this.auditLogger = options.auditLogger !== undefined ? options.auditLogger : new AuditLogger();
|
|
32
49
|
}
|
|
33
50
|
|
|
34
51
|
/**
|
|
35
52
|
* Evaluates an execution request through the P0-P4 waterfall.
|
|
36
53
|
*
|
|
37
54
|
* @param {{ name: string, arguments?: any }} execution - Tool call
|
|
38
|
-
* @param {{ sessionId?: string, knobs?: any }} [context] - Context options
|
|
39
|
-
* @returns {{ verdict: "allow" | "deny" | "ask", stage: "P0" | "P1" | "P2" | "P3" | "P4", reason: string, detail?: string, hit?: any }}
|
|
55
|
+
* @param {{ sessionId?: string, knobs?: any, cwd?: string }} [context] - Context options
|
|
56
|
+
* @returns {{ verdict: "allow" | "deny" | "ask", stage: "P0" | "P1" | "P2" | "P3" | "P4", reason: string, detail?: string, hit?: any, dryRun?: boolean, cached?: boolean }}
|
|
40
57
|
*/
|
|
41
58
|
decide(execution, context = {}) {
|
|
42
59
|
try {
|
|
43
60
|
const sessionId = context.sessionId || "default";
|
|
44
|
-
|
|
45
|
-
|
|
61
|
+
const toolName = execution && execution.name;
|
|
62
|
+
const command = execution && execution.arguments && execution.arguments.command;
|
|
63
|
+
|
|
64
|
+
// Check Retry Exhaustion guard (#96)
|
|
65
|
+
if (this.retryGuard && this.retryGuard.isExhausted(sessionId)) {
|
|
66
|
+
const exhHit = {
|
|
67
|
+
deny: true,
|
|
68
|
+
reason: "retry-exhaustion",
|
|
69
|
+
detail: "Retry exhaustion: consecutive blocked attempts exceeded threshold",
|
|
70
|
+
snippet: command || toolName,
|
|
71
|
+
};
|
|
72
|
+
const exhDecision = { verdict: "deny", stage: "P0", reason: "retry-exhaustion", detail: exhHit.detail, hit: exhHit };
|
|
73
|
+
if (this.ledger) this.ledger.record(sessionId, "exhaustion", { reason: "retry-exhaustion", tool: toolName });
|
|
74
|
+
if (this.auditLogger) this.auditLogger.log({ sessionId, execution, decision: exhDecision, dryRun: this.dryRun });
|
|
75
|
+
|
|
76
|
+
if (this.dryRun) {
|
|
77
|
+
return { verdict: "allow", stage: "P0", dryRun: true, originalVerdict: "deny", reason: "retry-exhaustion", detail: exhHit.detail, hit: exhHit };
|
|
78
|
+
}
|
|
79
|
+
return exhDecision;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Check Verdict Cache (#63, #119)
|
|
83
|
+
const cacheKey = this.cache ? this.cache.makeKey(execution, context) : null;
|
|
84
|
+
if (this.cache && cacheKey) {
|
|
85
|
+
const cached = this.cache.get(cacheKey);
|
|
86
|
+
if (cached) {
|
|
87
|
+
if (this.dryRun && (cached.verdict === "deny" || cached.verdict === "ask")) {
|
|
88
|
+
return {
|
|
89
|
+
verdict: "allow",
|
|
90
|
+
stage: cached.stage,
|
|
91
|
+
dryRun: true,
|
|
92
|
+
originalVerdict: cached.verdict,
|
|
93
|
+
reason: cached.reason,
|
|
94
|
+
detail: cached.detail,
|
|
95
|
+
hit: cached.hit,
|
|
96
|
+
cached: true,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
return cached;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const decision = this._evaluateWaterfall(execution, context, sessionId, toolName, command);
|
|
104
|
+
|
|
105
|
+
// Post-decision hooks: ledger, retryGuard, cache, audit
|
|
106
|
+
if (decision.verdict === "deny") {
|
|
107
|
+
if (this.retryGuard) this.retryGuard.recordVerdict(sessionId, execution, "deny");
|
|
108
|
+
if (this.ledger) this.ledger.record(sessionId, "deny", { reason: decision.reason, detail: decision.detail, tool: toolName });
|
|
109
|
+
if (this.cache && cacheKey) this.cache.set(cacheKey, decision, { sessionId });
|
|
110
|
+
} else if (decision.verdict === "allow") {
|
|
111
|
+
if (this.retryGuard) this.retryGuard.recordVerdict(sessionId, execution, "allow");
|
|
112
|
+
if (this.ledger) this.ledger.record(sessionId, "allow", { reason: decision.reason, detail: decision.detail, tool: toolName });
|
|
113
|
+
if (this.cache && cacheKey) this.cache.set(cacheKey, decision, { sessionId });
|
|
114
|
+
} else if (decision.verdict === "ask") {
|
|
115
|
+
if (this.ledger) this.ledger.record(sessionId, "ask", { reason: decision.reason, detail: decision.detail, tool: toolName });
|
|
116
|
+
}
|
|
46
117
|
|
|
118
|
+
if (this.auditLogger) {
|
|
119
|
+
this.auditLogger.log({ sessionId, execution, decision, dryRun: this.dryRun });
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Shadow Mode (#128)
|
|
123
|
+
if (this.dryRun && (decision.verdict === "deny" || decision.verdict === "ask")) {
|
|
124
|
+
return {
|
|
125
|
+
verdict: "allow",
|
|
126
|
+
stage: decision.stage,
|
|
127
|
+
dryRun: true,
|
|
128
|
+
originalVerdict: decision.verdict,
|
|
129
|
+
reason: decision.reason,
|
|
130
|
+
detail: decision.detail,
|
|
131
|
+
hit: decision.hit,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return decision;
|
|
136
|
+
} catch (err) {
|
|
137
|
+
return {
|
|
138
|
+
verdict: "ask",
|
|
139
|
+
stage: "P4",
|
|
140
|
+
reason: "inspect-error",
|
|
141
|
+
detail: String(err && err.message || err),
|
|
142
|
+
hit: { ask: true, reason: "inspect-error", detail: String(err && err.message || err) },
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Internal evaluation of P0-P4 waterfall.
|
|
149
|
+
* @private
|
|
150
|
+
*/
|
|
151
|
+
_evaluateWaterfall(execution, context, sessionId, toolName, command) {
|
|
47
152
|
// Fast-path Band 0 Hard Deny
|
|
48
153
|
const bandVerdict = this.bands.evaluate(toolName, command);
|
|
49
154
|
if (bandVerdict === "deny") {
|
|
@@ -70,6 +175,7 @@ export class SecurityEngine {
|
|
|
70
175
|
|
|
71
176
|
// If Circuit Breaker is tripped, suspend auto-mode and force P4 Human Ask
|
|
72
177
|
if (this.breaker.isTripped(sessionId)) {
|
|
178
|
+
if (this.ledger) this.ledger.record(sessionId, "breaker", { reason: "circuit-breaker-tripped", tool: toolName });
|
|
73
179
|
const breakerHit = {
|
|
74
180
|
ask: true,
|
|
75
181
|
reason: "circuit-breaker-tripped",
|
|
@@ -110,7 +216,8 @@ export class SecurityEngine {
|
|
|
110
216
|
return { verdict: "deny", stage: "P3", reason: cls.reason || "classifier-deny", detail: cls.reason, hit: clsHit };
|
|
111
217
|
}
|
|
112
218
|
} catch (err) {
|
|
113
|
-
// Classifier error fails closed to P4 Ask
|
|
219
|
+
// Classifier error fails closed to P4 Ask (#111: does not trip breaker)
|
|
220
|
+
if (this.ledger) this.ledger.record(sessionId, "error", { reason: "classifier-error", detail: String(err && err.message || err), tool: toolName });
|
|
114
221
|
return { verdict: "ask", stage: "P4", reason: "classifier-error", detail: String(err && err.message || err), hit: null };
|
|
115
222
|
}
|
|
116
223
|
}
|
|
@@ -121,15 +228,6 @@ export class SecurityEngine {
|
|
|
121
228
|
return { verdict: "ask", stage: "P4", reason: "unmatched-ask", hit: askHit };
|
|
122
229
|
}
|
|
123
230
|
return { verdict: "allow", stage: "P2", reason: "unmatched-allow", hit };
|
|
124
|
-
} catch (err) {
|
|
125
|
-
return {
|
|
126
|
-
verdict: "ask",
|
|
127
|
-
stage: "P4",
|
|
128
|
-
reason: "inspect-error",
|
|
129
|
-
detail: String(err && err.message || err),
|
|
130
|
-
hit: { ask: true, reason: "inspect-error", detail: String(err && err.message || err) },
|
|
131
|
-
};
|
|
132
|
-
}
|
|
133
231
|
}
|
|
134
232
|
|
|
135
233
|
addGrant(options) {
|
|
@@ -146,7 +244,21 @@ export class SecurityEngine {
|
|
|
146
244
|
return [];
|
|
147
245
|
}
|
|
148
246
|
|
|
247
|
+
getLedgerSummary(sessionId) {
|
|
248
|
+
return this.ledger ? this.ledger.getSummary(sessionId) : null;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
isRetryExhausted(sessionId) {
|
|
252
|
+
return this.retryGuard ? this.retryGuard.isExhausted(sessionId) : false;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
clearCache() {
|
|
256
|
+
if (this.cache) this.cache.clear();
|
|
257
|
+
}
|
|
258
|
+
|
|
149
259
|
resume(sessionId, reason) {
|
|
260
|
+
if (this.retryGuard) this.retryGuard.reset(sessionId);
|
|
261
|
+
if (this.cache) this.cache.clearSession(sessionId);
|
|
150
262
|
return this.breaker.resume(sessionId, reason);
|
|
151
263
|
}
|
|
152
264
|
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// lib/fingerprint.js
|
|
2
|
+
// Canonical operation fingerprint generator for dsh-approval-gate (#80).
|
|
3
|
+
// Format: tool:normalize(cmd):target_path
|
|
4
|
+
// Strict: flag or argument variations produce distinct fingerprints.
|
|
5
|
+
|
|
6
|
+
import { createHash } from "node:crypto";
|
|
7
|
+
import { extractFilePath } from "./dispatcher.js";
|
|
8
|
+
import { canonicalizeFsPath, normalizeSeparators } from "./paths.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Normalizes command whitespace while preserving quotes.
|
|
12
|
+
*
|
|
13
|
+
* @param {string} cmd
|
|
14
|
+
* @returns {string}
|
|
15
|
+
*/
|
|
16
|
+
export function normalizeCommandString(cmd) {
|
|
17
|
+
if (typeof cmd !== "string") return "";
|
|
18
|
+
const trimmed = cmd.trim();
|
|
19
|
+
if (!trimmed) return "";
|
|
20
|
+
|
|
21
|
+
let result = "";
|
|
22
|
+
let inSingle = false;
|
|
23
|
+
let inDouble = false;
|
|
24
|
+
let lastWasSpace = false;
|
|
25
|
+
|
|
26
|
+
for (let i = 0; i < trimmed.length; i += 1) {
|
|
27
|
+
const ch = trimmed[i];
|
|
28
|
+
if (ch === "'" && !inDouble) {
|
|
29
|
+
inSingle = !inSingle;
|
|
30
|
+
result += ch;
|
|
31
|
+
lastWasSpace = false;
|
|
32
|
+
} else if (ch === '"' && !inSingle) {
|
|
33
|
+
inDouble = !inDouble;
|
|
34
|
+
result += ch;
|
|
35
|
+
lastWasSpace = false;
|
|
36
|
+
} else if (/\s/.test(ch) && !inSingle && !inDouble) {
|
|
37
|
+
if (!lastWasSpace) {
|
|
38
|
+
result += " ";
|
|
39
|
+
lastWasSpace = true;
|
|
40
|
+
}
|
|
41
|
+
} else {
|
|
42
|
+
result += ch;
|
|
43
|
+
lastWasSpace = false;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return result.trim();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Generates a canonical fingerprint for an operation.
|
|
52
|
+
* Format: tool:normalized_cmd:target_path
|
|
53
|
+
*
|
|
54
|
+
* @param {{ name?: string, tool?: string, arguments?: any } | string} execution
|
|
55
|
+
* @param {{ cwd?: string }} [options]
|
|
56
|
+
* @returns {string}
|
|
57
|
+
*/
|
|
58
|
+
export function computeFingerprint(execution, options = {}) {
|
|
59
|
+
let tool = "";
|
|
60
|
+
let args = {};
|
|
61
|
+
|
|
62
|
+
if (typeof execution === "string") {
|
|
63
|
+
tool = execution.trim().toLowerCase();
|
|
64
|
+
} else if (execution && typeof execution === "object") {
|
|
65
|
+
tool = String(execution.name || execution.tool || "").trim().toLowerCase();
|
|
66
|
+
args = execution.arguments || {};
|
|
67
|
+
if (typeof args === "string") {
|
|
68
|
+
try {
|
|
69
|
+
args = JSON.parse(args);
|
|
70
|
+
} catch {
|
|
71
|
+
args = { raw: args };
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const rawCmd = args && typeof args.command === "string" ? args.command : "";
|
|
77
|
+
const normalizedCmd = normalizeCommandString(rawCmd);
|
|
78
|
+
|
|
79
|
+
let targetPath = (args && (args.cwd || extractFilePath(args))) || options.cwd || "";
|
|
80
|
+
if (typeof targetPath === "string" && targetPath.trim()) {
|
|
81
|
+
targetPath = canonicalizeFsPath(normalizeSeparators(targetPath.trim()));
|
|
82
|
+
} else {
|
|
83
|
+
targetPath = "";
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return `${tool}:${normalizedCmd}:${targetPath}`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Computes sha256 hash of the canonical operation fingerprint.
|
|
91
|
+
*
|
|
92
|
+
* @param {{ name?: string, tool?: string, arguments?: any } | string} execution
|
|
93
|
+
* @param {{ cwd?: string, algorithm?: string }} [options]
|
|
94
|
+
* @returns {string}
|
|
95
|
+
*/
|
|
96
|
+
export function computeFingerprintHash(execution, options = {}) {
|
|
97
|
+
const fp = computeFingerprint(execution, options);
|
|
98
|
+
const algo = options.algorithm || "sha256";
|
|
99
|
+
return createHash(algo).update(fp, "utf8").digest("hex");
|
|
100
|
+
}
|
package/lib/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { SecurityEngine } from "./engine.js";
|
|
2
2
|
import { BandsEngine } from "./bands.js";
|
|
3
|
+
import { AuditLogger } from "./audit-log.js";
|
|
3
4
|
import Schema from "@deepseek-ai/schemastery";
|
|
4
5
|
|
|
5
6
|
if (typeof Schema.prototype?.volatile !== "function") {
|
|
@@ -205,6 +206,12 @@ export const Config = Schema.object({
|
|
|
205
206
|
workspaceDir: Schema.string().default("").volatile(),
|
|
206
207
|
/** Default verdict for unclassified commands: 'allow' (default) or 'ask' (fail-closed) (#163). */
|
|
207
208
|
defaultVerdict: Schema.union(["allow", "ask"]).default("allow").volatile(),
|
|
209
|
+
/** Dry-run / shadow mode for safe testing of rules on production (#128). */
|
|
210
|
+
dryRun: Schema.boolean().default(false).volatile(),
|
|
211
|
+
/** Shadow mode alias for dryRun (#128). */
|
|
212
|
+
shadowMode: Schema.boolean().default(false).volatile(),
|
|
213
|
+
/** Path to append-only audit log file (#56). */
|
|
214
|
+
auditLogPath: Schema.string().default("").volatile(),
|
|
208
215
|
/** Security engine instance (internal/testing). */
|
|
209
216
|
engine: Schema.any().hidden(),
|
|
210
217
|
});
|
|
@@ -248,12 +255,18 @@ export function apply(ctx, config) {
|
|
|
248
255
|
inspectConfig: { toolName: toolName, fileWriteTools: fileWriteTools, workspaceDir: cfg.workspaceDir || "" },
|
|
249
256
|
bands: new BandsEngine({ toolRegistry: ctx && ctx.tools }),
|
|
250
257
|
defaultVerdict: cfg.defaultVerdict || "allow",
|
|
258
|
+
dryRun: Boolean(cfg.dryRun || cfg.shadowMode),
|
|
259
|
+
auditLogger: new AuditLogger({ logPath: cfg.auditLogPath || undefined }),
|
|
251
260
|
});
|
|
252
261
|
|
|
253
262
|
const gateService = {
|
|
254
263
|
engine,
|
|
255
264
|
addGrant: (options) => engine.addGrant(options),
|
|
256
265
|
resume: (sessionId, reason) => engine.resume(sessionId, reason),
|
|
266
|
+
cache: engine.cache,
|
|
267
|
+
retryGuard: engine.retryGuard,
|
|
268
|
+
ledger: engine.ledger,
|
|
269
|
+
getLedgerSummary: (sessionId) => engine.getLedgerSummary(sessionId),
|
|
257
270
|
};
|
|
258
271
|
if (ctx && typeof ctx === "object") {
|
|
259
272
|
if (typeof ctx.provide === "function") {
|
|
@@ -282,6 +295,13 @@ export function apply(ctx, config) {
|
|
|
282
295
|
return { kind: "ask", reason: askMessage(hit, who, translate, ctx && ctx.logger) };
|
|
283
296
|
}
|
|
284
297
|
|
|
298
|
+
if (decision.dryRun) {
|
|
299
|
+
if (ctx && ctx.logger && typeof ctx.logger.info === "function") {
|
|
300
|
+
ctx.logger.info(`[dsh-approval-gate:DRY-RUN] would block rule=${decision.reason} (${decision.originalVerdict})${who}`);
|
|
301
|
+
}
|
|
302
|
+
return next();
|
|
303
|
+
}
|
|
304
|
+
|
|
285
305
|
if (decision.verdict === "ask") {
|
|
286
306
|
if (decision.reason === "circuit-breaker-tripped") {
|
|
287
307
|
const hit = decision.hit || {
|
|
@@ -325,6 +345,13 @@ export function apply(ctx, config) {
|
|
|
325
345
|
return denyMessage(hit, who, translate, ctx && ctx.logger);
|
|
326
346
|
}
|
|
327
347
|
|
|
348
|
+
if (decision.dryRun) {
|
|
349
|
+
if (ctx && ctx.logger && typeof ctx.logger.info === "function") {
|
|
350
|
+
ctx.logger.info(`[dsh-approval-gate:DRY-RUN] would block rule=${decision.reason} (${decision.originalVerdict})${who}`);
|
|
351
|
+
}
|
|
352
|
+
return undefined;
|
|
353
|
+
}
|
|
354
|
+
|
|
328
355
|
if (decision.verdict === "deny" || (decision.verdict === "ask" && !hasPreExecute)) {
|
|
329
356
|
const hit = decision.hit || {
|
|
330
357
|
deny: true,
|
|
@@ -370,3 +397,56 @@ export {
|
|
|
370
397
|
export { expandHome, resolveSafePath, isPathTraversal, canonicalizeFsPath } from "./paths.js";
|
|
371
398
|
export { splitSubcommands } from "./tokenizer.js";
|
|
372
399
|
export { redactCredentials, redactUrl } from "./redact.js";
|
|
400
|
+
export {
|
|
401
|
+
CommandDispatcher,
|
|
402
|
+
isReviewerAgent,
|
|
403
|
+
extractFilePath,
|
|
404
|
+
TOOL_KIND_SHELL,
|
|
405
|
+
TOOL_KIND_FILE_WRITE,
|
|
406
|
+
TOOL_KIND_FILE_READ,
|
|
407
|
+
TOOL_KIND_NETWORK,
|
|
408
|
+
TOOL_KIND_GENERIC,
|
|
409
|
+
} from "./dispatcher.js";
|
|
410
|
+
export {
|
|
411
|
+
parseGitArgv,
|
|
412
|
+
classifyGitOperation,
|
|
413
|
+
inspectGitCommand,
|
|
414
|
+
GIT_CATEGORY_READ,
|
|
415
|
+
GIT_CATEGORY_MUTATE_LOCAL,
|
|
416
|
+
GIT_CATEGORY_REMOTE,
|
|
417
|
+
GIT_CATEGORY_DESTRUCTIVE,
|
|
418
|
+
} from "./parsers/git.js";
|
|
419
|
+
export {
|
|
420
|
+
isDevicePath,
|
|
421
|
+
extractRedirects,
|
|
422
|
+
inspectRedirectSecurity,
|
|
423
|
+
inspectSilentErrors,
|
|
424
|
+
inspectPipelines,
|
|
425
|
+
} from "./parsers/shell-cmds.js";
|
|
426
|
+
export {
|
|
427
|
+
isLoopbackHost,
|
|
428
|
+
isLoopbackUrl,
|
|
429
|
+
isLoopbackCommand,
|
|
430
|
+
} from "./loopback.js";
|
|
431
|
+
export {
|
|
432
|
+
computeFingerprint,
|
|
433
|
+
computeFingerprintHash,
|
|
434
|
+
normalizeCommandString,
|
|
435
|
+
} from "./fingerprint.js";
|
|
436
|
+
export {
|
|
437
|
+
VerdictCache,
|
|
438
|
+
DEFAULT_CACHE_MAX_SIZE,
|
|
439
|
+
DEFAULT_CACHE_TTL_MS,
|
|
440
|
+
} from "./cache.js";
|
|
441
|
+
export {
|
|
442
|
+
RetryExhaustionGuard,
|
|
443
|
+
DEFAULT_MAX_RETRY_ATTEMPTS,
|
|
444
|
+
} from "./retry-exhaustion.js";
|
|
445
|
+
export {
|
|
446
|
+
QualityLedger,
|
|
447
|
+
MAX_LEDGER_EVENTS_PER_SESSION,
|
|
448
|
+
} from "./ledger.js";
|
|
449
|
+
export {
|
|
450
|
+
AuditLogger,
|
|
451
|
+
DEFAULT_AUDIT_LOG_RELATIVE,
|
|
452
|
+
} from "./audit-log.js";
|