@goodandready/dsh-approval-gate 0.1.22 → 0.1.23
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 +8 -0
- package/lib/dossier.js +122 -0
- package/lib/drift.js +214 -0
- package/lib/dualtrack.js +101 -0
- package/lib/engine.js +40 -49
- package/lib/index.js +6 -10
- package/lib/ledger.js +264 -164
- package/lib/session-modes.js +146 -0
- package/package.json +55 -55
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.23
|
|
4
|
+
|
|
5
|
+
- Tech-debt: Версионирование схемы хранения сессионных вердиктов (`CURRENT_SCHEMA_VERSION = 3` в `lib/ledger.js`) с полной обратной совместимостью и бесшовной миграцией исторических форматов v1/v2/v3 (`migrateVerdictRecord`) (Refs: #73).
|
|
6
|
+
- Feature: Поддержка двухтрековых маркеров (`Dual-Track Markers` в `lib/dualtrack.js`: `TRACK_TYPES.INTERNAL` vs `TRACK_TYPES.EXTERNAL`) для четкого разделения скрытых служебных шагов агента и пользовательских мутаций проекта (Refs: #92).
|
|
7
|
+
- Feature: Сессионное переключение режимов гейта (`auto-approval` vs `manual` / `strict` / `hybrid` в `lib/session-modes.js`) на основе активного пресета сессии (`resolveSessionGateMode`, `applySessionModePolicy`, `SessionModeRegistry`) (Refs: #61).
|
|
8
|
+
- Feature: Сбор доказательного досье (`Evidence Dossier` в `lib/dossier.js`: инструмент, diff, терминал, исходный промпт юзера) с безопасной санитизацией чувствительных токенов и защитой от переполнения памяти (Refs: #64).
|
|
9
|
+
- Security: Детекция отклонения от траектории задачи (`Trajectory Drift Detection` в `lib/drift.js`: `TrajectoryTracker`) с перехватом несанкционированного сканирования сети, поиска учетных данных и системного зондирования вне рамок цели сессии (Refs: #68).
|
|
10
|
+
|
|
3
11
|
## 0.1.22
|
|
4
12
|
|
|
5
13
|
- Feature: Наследование разрешений в иерархии субагентов через `parentSession lineage authority` (`LineageManager` в `lib/lineage.js`) с безопасным обходом дерева предков и защитой от циклических ссылок (Refs: #98).
|
package/lib/dossier.js
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// lib/dossier.js
|
|
2
|
+
// Evidence Dossier Gathering (#64).
|
|
3
|
+
// Assembles compact evidence packets for tool calls, diffs, terminal output, and user prompts.
|
|
4
|
+
|
|
5
|
+
export const MAX_PROMPT_CHARS = 200;
|
|
6
|
+
export const MAX_DIFF_CHARS = 400;
|
|
7
|
+
export const MAX_OUTPUT_CHARS = 300;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Truncates a string safely with ellipsis.
|
|
11
|
+
* @param {any} str
|
|
12
|
+
* @param {number} max
|
|
13
|
+
* @returns {string}
|
|
14
|
+
*/
|
|
15
|
+
function truncate(str, max) {
|
|
16
|
+
if (typeof str !== "string") return "";
|
|
17
|
+
const trimmed = str.trim();
|
|
18
|
+
if (trimmed.length <= max) return trimmed;
|
|
19
|
+
return trimmed.slice(0, max - 3) + "...";
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Extracts a concise summary of arguments without sensitive credentials.
|
|
24
|
+
* @param {Record<string, any>} [args]
|
|
25
|
+
* @returns {Record<string, any>}
|
|
26
|
+
*/
|
|
27
|
+
function sanitizeArgs(args) {
|
|
28
|
+
if (!args || typeof args !== "object") return {};
|
|
29
|
+
const summary = {};
|
|
30
|
+
for (const [k, v] of Object.entries(args)) {
|
|
31
|
+
if (k.toLowerCase().includes("token") || k.toLowerCase().includes("secret") || k.toLowerCase().includes("password")) {
|
|
32
|
+
summary[k] = "[REDACTED]";
|
|
33
|
+
} else if (typeof v === "string") {
|
|
34
|
+
summary[k] = v.length > 80 ? v.slice(0, 77) + "..." : v;
|
|
35
|
+
} else if (typeof v === "number" || typeof v === "boolean") {
|
|
36
|
+
summary[k] = v;
|
|
37
|
+
} else if (Array.isArray(v)) {
|
|
38
|
+
summary[k] = `Array(${v.length})`;
|
|
39
|
+
} else if (typeof v === "object" && v !== null) {
|
|
40
|
+
summary[k] = `Object(${Object.keys(v).length})`;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return summary;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Builds an evidence dossier object for an execution (#64).
|
|
48
|
+
*
|
|
49
|
+
* @param {Object} params
|
|
50
|
+
* @param {Record<string, any>} [params.execution]
|
|
51
|
+
* @param {Record<string, any>} [params.decision]
|
|
52
|
+
* @param {Record<string, any>} [params.context]
|
|
53
|
+
* @param {string} [params.toolName]
|
|
54
|
+
* @param {string} [params.command]
|
|
55
|
+
* @param {Record<string, any>} [params.args]
|
|
56
|
+
* @param {string} [params.userPrompt]
|
|
57
|
+
* @param {string} [params.diffSnippet]
|
|
58
|
+
* @param {string} [params.terminalOutput]
|
|
59
|
+
* @param {string} [params.track]
|
|
60
|
+
* @param {string} [params.riskBucket]
|
|
61
|
+
* @returns {{
|
|
62
|
+
* toolName: string,
|
|
63
|
+
* command: string,
|
|
64
|
+
* argsSummary: Record<string, any>,
|
|
65
|
+
* userPrompt: string,
|
|
66
|
+
* diffSnippet: string,
|
|
67
|
+
* terminalOutput: string,
|
|
68
|
+
* track: string,
|
|
69
|
+
* riskBucket: string,
|
|
70
|
+
* timestamp: number
|
|
71
|
+
* }}
|
|
72
|
+
*/
|
|
73
|
+
export function buildEvidenceDossier({
|
|
74
|
+
execution = {},
|
|
75
|
+
decision = {},
|
|
76
|
+
context = {},
|
|
77
|
+
toolName,
|
|
78
|
+
command,
|
|
79
|
+
args,
|
|
80
|
+
userPrompt,
|
|
81
|
+
diffSnippet,
|
|
82
|
+
terminalOutput,
|
|
83
|
+
track,
|
|
84
|
+
riskBucket,
|
|
85
|
+
} = {}) {
|
|
86
|
+
const tool = String(toolName || execution?.name || execution?.toolName || "").trim();
|
|
87
|
+
const cmd = String(command || execution?.arguments?.command || execution?.command || "").trim();
|
|
88
|
+
const rawArgs = args || execution?.arguments || {};
|
|
89
|
+
const prompt = userPrompt || context?.userPrompt || context?.prompt || context?.goal || "";
|
|
90
|
+
const diff = diffSnippet || context?.diffSnippet || context?.diff || rawArgs?.diff || "";
|
|
91
|
+
const output = terminalOutput || context?.terminalOutput || context?.output || "";
|
|
92
|
+
const assignedTrack = track || decision?.track || context?.track || "external";
|
|
93
|
+
const assignedRisk = riskBucket || decision?.riskBucket || context?.riskBucket || "medium";
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
toolName: tool,
|
|
97
|
+
command: cmd,
|
|
98
|
+
argsSummary: sanitizeArgs(rawArgs),
|
|
99
|
+
userPrompt: truncate(prompt, MAX_PROMPT_CHARS),
|
|
100
|
+
diffSnippet: truncate(diff, MAX_DIFF_CHARS),
|
|
101
|
+
terminalOutput: truncate(output, MAX_OUTPUT_CHARS),
|
|
102
|
+
track: assignedTrack,
|
|
103
|
+
riskBucket: assignedRisk,
|
|
104
|
+
timestamp: Date.now(),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Formats a single-line summary of an evidence dossier.
|
|
110
|
+
* @param {ReturnType<typeof buildEvidenceDossier>} dossier
|
|
111
|
+
* @returns {string}
|
|
112
|
+
*/
|
|
113
|
+
export function formatDossierSummary(dossier) {
|
|
114
|
+
if (!dossier) return "[Dossier: empty]";
|
|
115
|
+
const parts = [];
|
|
116
|
+
if (dossier.toolName) parts.push(`tool=${dossier.toolName}`);
|
|
117
|
+
if (dossier.track) parts.push(`track=${dossier.track}`);
|
|
118
|
+
if (dossier.riskBucket) parts.push(`risk=${dossier.riskBucket}`);
|
|
119
|
+
if (dossier.userPrompt) parts.push(`prompt="${dossier.userPrompt.slice(0, 30)}"`);
|
|
120
|
+
if (dossier.diffSnippet) parts.push(`diff="${dossier.diffSnippet.slice(0, 30)}"`);
|
|
121
|
+
return `[Dossier: ${parts.join(" ")}]`;
|
|
122
|
+
}
|
package/lib/drift.js
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
// lib/drift.js
|
|
2
|
+
// Trajectory Drift Detection for dsh-approval-gate (#68).
|
|
3
|
+
// Detects when agent deviates from session goal into port scanning, credential hunting, or out-of-scope probing.
|
|
4
|
+
|
|
5
|
+
export const DRIFT_CATEGORIES = Object.freeze({
|
|
6
|
+
NETWORK_RECON: "network-recon",
|
|
7
|
+
CREDENTIAL_HUNT: "credential-hunt",
|
|
8
|
+
SYSTEM_PROBING: "system-probing",
|
|
9
|
+
OUT_OF_SCOPE: "out-of-scope",
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
const NETWORK_RECON_PATTERNS = [
|
|
13
|
+
/\b(?:nmap|masscan|zmap)\b/i,
|
|
14
|
+
/\bnetstat\s+-[antulp]+/i,
|
|
15
|
+
/\bss\s+-[antulp]+/i,
|
|
16
|
+
/\barp\s+-(?:a|n)\b/i,
|
|
17
|
+
/\b(?:tcpdump|tshark|wireshark)\b/i,
|
|
18
|
+
/\b(?:traceroute|tracepath)\b/i,
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
const NETWORK_GOAL_KEYWORDS = [
|
|
22
|
+
"network", "port", "scan", "packet", "interface", "traceroute", "traffic", "firewall", "diagnose"
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
const CREDENTIAL_HUNT_PATTERNS = [
|
|
26
|
+
/[\\/]\.ssh[\\/](?:id_rsa|id_ed25519|id_ecdsa|authorized_keys|known_hosts)/i,
|
|
27
|
+
/[\\/]\.aws[\\/]credentials/i,
|
|
28
|
+
/\/etc\/(?:shadow|gshadow|sudoers)/i,
|
|
29
|
+
/\bcat\s+[^\n]*(?:id_rsa|id_ed25519|\.ssh|\.aws\/credentials)/i,
|
|
30
|
+
/\bfind\s+[^\n]*-(?:name|iname)\s+['"]?\*?\.(?:pem|key|pfx|pkcs12)['"]?/i,
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
const CREDENTIAL_GOAL_KEYWORDS = [
|
|
34
|
+
"ssh", "key", "credential", "auth", "secret", "password", "token", "certificate", "cert", "deploy key"
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
const SYSTEM_PROBE_PATTERNS = [
|
|
38
|
+
/\bfind\s+\/(?:\s|$)[^\n]*-perm\s+-[0-9]+/i,
|
|
39
|
+
/\/proc\/(?:kallsyms|keys|sched_debug)/i,
|
|
40
|
+
/\b(?:linpeas|winpeas|chkrootkit|rkhunter)\b/i,
|
|
41
|
+
/\bls\s+-[la]*\s+\/root\b/i,
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
const SYSTEM_PROBE_GOAL_KEYWORDS = [
|
|
45
|
+
"kernel", "root", "audit", "security audit", "permission audit", "probe", "privilege"
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Checks if text contains any of the given keywords.
|
|
50
|
+
* @param {string} text
|
|
51
|
+
* @param {string[]} keywords
|
|
52
|
+
* @returns {boolean}
|
|
53
|
+
*/
|
|
54
|
+
function containsKeyword(text, keywords) {
|
|
55
|
+
if (!text || typeof text !== "string") return false;
|
|
56
|
+
const lower = text.toLowerCase();
|
|
57
|
+
return keywords.some((kw) => lower.includes(kw));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Detects trajectory drift for an execution against session goal (#68).
|
|
62
|
+
*
|
|
63
|
+
* @param {string} [sessionId]
|
|
64
|
+
* @param {Record<string, any>} [execution]
|
|
65
|
+
* @param {Record<string, any>} [context]
|
|
66
|
+
* @param {{ goal?: string, allowedScopes?: string[] }} [goalInfo]
|
|
67
|
+
* @returns {{
|
|
68
|
+
* hasDrift: boolean,
|
|
69
|
+
* driftScore: number,
|
|
70
|
+
* driftWarning: boolean,
|
|
71
|
+
* category?: string,
|
|
72
|
+
* reason?: string,
|
|
73
|
+
* detail?: string
|
|
74
|
+
* }}
|
|
75
|
+
*/
|
|
76
|
+
export function detectTrajectoryDrift(sessionId, execution = {}, context = {}, goalInfo = {}) {
|
|
77
|
+
const goal = String(
|
|
78
|
+
goalInfo?.goal ||
|
|
79
|
+
context?.goal ||
|
|
80
|
+
context?.userPrompt ||
|
|
81
|
+
context?.prompt ||
|
|
82
|
+
""
|
|
83
|
+
).trim();
|
|
84
|
+
|
|
85
|
+
const command = String(
|
|
86
|
+
execution?.arguments?.command ||
|
|
87
|
+
execution?.command ||
|
|
88
|
+
""
|
|
89
|
+
).trim();
|
|
90
|
+
|
|
91
|
+
const targetPath = String(
|
|
92
|
+
execution?.arguments?.TargetFile ||
|
|
93
|
+
execution?.arguments?.targetPath ||
|
|
94
|
+
execution?.arguments?.path ||
|
|
95
|
+
execution?.arguments?.file ||
|
|
96
|
+
""
|
|
97
|
+
).trim();
|
|
98
|
+
|
|
99
|
+
const textToScan = `${command} ${targetPath}`.trim();
|
|
100
|
+
if (!textToScan) {
|
|
101
|
+
return { hasDrift: false, driftScore: 0, driftWarning: false };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// 1. Network reconnaissance check
|
|
105
|
+
if (NETWORK_RECON_PATTERNS.some((p) => p.test(textToScan))) {
|
|
106
|
+
if (!containsKeyword(goal, NETWORK_GOAL_KEYWORDS)) {
|
|
107
|
+
return {
|
|
108
|
+
hasDrift: true,
|
|
109
|
+
driftScore: 0.85,
|
|
110
|
+
driftWarning: true,
|
|
111
|
+
category: DRIFT_CATEGORIES.NETWORK_RECON,
|
|
112
|
+
reason: "trajectory-drift-detected",
|
|
113
|
+
detail: `Operation matches network reconnaissance without matching session goal (${goal.slice(0, 50) || "none"})`,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// 2. Credential hunting check
|
|
119
|
+
if (CREDENTIAL_HUNT_PATTERNS.some((p) => p.test(textToScan))) {
|
|
120
|
+
if (!containsKeyword(goal, CREDENTIAL_GOAL_KEYWORDS)) {
|
|
121
|
+
return {
|
|
122
|
+
hasDrift: true,
|
|
123
|
+
driftScore: 0.95,
|
|
124
|
+
driftWarning: true,
|
|
125
|
+
category: DRIFT_CATEGORIES.CREDENTIAL_HUNT,
|
|
126
|
+
reason: "trajectory-drift-detected",
|
|
127
|
+
detail: `Operation matches credential inspection without matching session goal (${goal.slice(0, 50) || "none"})`,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// 3. System probing check
|
|
133
|
+
if (SYSTEM_PROBE_PATTERNS.some((p) => p.test(textToScan))) {
|
|
134
|
+
if (!containsKeyword(goal, SYSTEM_PROBE_GOAL_KEYWORDS)) {
|
|
135
|
+
return {
|
|
136
|
+
hasDrift: true,
|
|
137
|
+
driftScore: 0.9,
|
|
138
|
+
driftWarning: true,
|
|
139
|
+
category: DRIFT_CATEGORIES.SYSTEM_PROBING,
|
|
140
|
+
reason: "trajectory-drift-detected",
|
|
141
|
+
detail: `Operation matches system probing/privilege reconnaissance outside session goal`,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// 4. Allowed scopes check
|
|
147
|
+
const allowedScopes = goalInfo?.allowedScopes || context?.allowedScopes;
|
|
148
|
+
if (Array.isArray(allowedScopes) && allowedScopes.length > 0 && targetPath) {
|
|
149
|
+
const normTarget = targetPath.replace(/\\/g, "/").toLowerCase();
|
|
150
|
+
const isWithinScope = allowedScopes.some((scope) => {
|
|
151
|
+
const normScope = String(scope).replace(/\\/g, "/").toLowerCase();
|
|
152
|
+
return normTarget.startsWith(normScope);
|
|
153
|
+
});
|
|
154
|
+
if (!isWithinScope && (normTarget.startsWith("/etc") || normTarget.startsWith("/root") || normTarget.includes("/.ssh"))) {
|
|
155
|
+
return {
|
|
156
|
+
hasDrift: true,
|
|
157
|
+
driftScore: 0.8,
|
|
158
|
+
driftWarning: true,
|
|
159
|
+
category: DRIFT_CATEGORIES.OUT_OF_SCOPE,
|
|
160
|
+
reason: "trajectory-drift-detected",
|
|
161
|
+
detail: `Access to ${targetPath} is outside declared session scopes`,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return { hasDrift: false, driftScore: 0, driftWarning: false };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Trajectory Tracker for sessions (#68).
|
|
171
|
+
*/
|
|
172
|
+
export class TrajectoryTracker {
|
|
173
|
+
constructor() {
|
|
174
|
+
/** @type {Map<string, { goal: string, allowedScopes: string[], stepCount: number }>} */
|
|
175
|
+
this.sessions = new Map();
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
setGoal(sessionId, goal, allowedScopes = []) {
|
|
179
|
+
const key = sessionId || "default";
|
|
180
|
+
this.sessions.set(key, {
|
|
181
|
+
goal: String(goal || ""),
|
|
182
|
+
allowedScopes: Array.isArray(allowedScopes) ? [...allowedScopes] : [],
|
|
183
|
+
stepCount: 0,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
getGoal(sessionId) {
|
|
188
|
+
const key = sessionId || "default";
|
|
189
|
+
return this.sessions.get(key) || null;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
recordStep(sessionId) {
|
|
193
|
+
const key = sessionId || "default";
|
|
194
|
+
const entry = this.sessions.get(key);
|
|
195
|
+
if (entry) {
|
|
196
|
+
entry.stepCount += 1;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
detect(sessionId, execution, context = {}) {
|
|
201
|
+
const key = sessionId || "default";
|
|
202
|
+
const goalInfo = this.sessions.get(key) || {};
|
|
203
|
+
return detectTrajectoryDrift(key, execution, context, goalInfo);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
clearSession(sessionId) {
|
|
207
|
+
const key = sessionId || "default";
|
|
208
|
+
this.sessions.delete(key);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
reset() {
|
|
212
|
+
this.sessions.clear();
|
|
213
|
+
}
|
|
214
|
+
}
|
package/lib/dualtrack.js
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// lib/dualtrack.js
|
|
2
|
+
// Dual-Track Markers for separating internal agent chores from external project mutations (#92).
|
|
3
|
+
// Provides classification and predicates to differentiate internal sandbox actions from project changes.
|
|
4
|
+
|
|
5
|
+
export const TRACK_TYPES = Object.freeze({
|
|
6
|
+
INTERNAL: "internal",
|
|
7
|
+
EXTERNAL: "external",
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
const INTERNAL_TOOL_NAMES = new Set([
|
|
11
|
+
"read_url_content",
|
|
12
|
+
"search_web",
|
|
13
|
+
"web_search",
|
|
14
|
+
"view_file",
|
|
15
|
+
"read_file",
|
|
16
|
+
"view",
|
|
17
|
+
"inspect_file",
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
const INTERNAL_PATH_PATTERNS = [
|
|
21
|
+
/[\\/]\.dsh[\\/](?:tmp|trash|cache|sessions)[\\/]/i,
|
|
22
|
+
/[\\/]\.gemini[\\/]/i,
|
|
23
|
+
/[\\/]scratch[\\/]/i,
|
|
24
|
+
/[\\/]\.cache[\\/]/i,
|
|
25
|
+
/[\\/]tmp[\\/]/i,
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
const INTERNAL_COMMAND_REGEX = /^(?:git\s+(?:status|diff|rev-parse|branch|log)|npm\s+(?:view|help)|node\s+--version|echo\b|cat\s+[^\n]*\.(?:dsh|tmp|cache)|ls\s+-[la]*\s*(?:\.|$))/i;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Classifies an operation into internal or external track (#92).
|
|
32
|
+
*
|
|
33
|
+
* @param {string} [toolName]
|
|
34
|
+
* @param {string} [command]
|
|
35
|
+
* @param {Record<string, any>} [args]
|
|
36
|
+
* @param {Record<string, any>} [context]
|
|
37
|
+
* @returns {'internal' | 'external'}
|
|
38
|
+
*/
|
|
39
|
+
export function classifyTrack(toolName, command, args = {}, context = {}) {
|
|
40
|
+
// Explicit overrides in context or args
|
|
41
|
+
if (context.track === TRACK_TYPES.INTERNAL || args?.track === TRACK_TYPES.INTERNAL || context.internal === true || args?.internal === true) {
|
|
42
|
+
return TRACK_TYPES.INTERNAL;
|
|
43
|
+
}
|
|
44
|
+
if (context.track === TRACK_TYPES.EXTERNAL || args?.track === TRACK_TYPES.EXTERNAL || context.external === true || args?.external === true) {
|
|
45
|
+
return TRACK_TYPES.EXTERNAL;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const tool = String(toolName || "").trim();
|
|
49
|
+
const cmd = String(command || (args && args.command) || "").trim();
|
|
50
|
+
const target = String((args && (args.TargetFile || args.targetPath || args.path || args.file)) || "").trim();
|
|
51
|
+
|
|
52
|
+
// Operations targeting internal scratch/temp paths are internal
|
|
53
|
+
if (target && INTERNAL_PATH_PATTERNS.some((p) => p.test(target))) {
|
|
54
|
+
return TRACK_TYPES.INTERNAL;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Pure read-only knowledge tools are internal
|
|
58
|
+
if (INTERNAL_TOOL_NAMES.has(tool)) {
|
|
59
|
+
return TRACK_TYPES.INTERNAL;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// File mutations on project files are external
|
|
63
|
+
if (tool === "write_to_file" || tool === "replace_file_content" || tool === "create_file") {
|
|
64
|
+
return TRACK_TYPES.EXTERNAL;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Shell command classification
|
|
68
|
+
if (cmd) {
|
|
69
|
+
if (INTERNAL_PATH_PATTERNS.some((p) => p.test(cmd))) {
|
|
70
|
+
return TRACK_TYPES.INTERNAL;
|
|
71
|
+
}
|
|
72
|
+
if (INTERNAL_COMMAND_REGEX.test(cmd)) {
|
|
73
|
+
return TRACK_TYPES.INTERNAL;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Default for non-read commands or mutations
|
|
78
|
+
return TRACK_TYPES.EXTERNAL;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Checks whether track or decision is marked as internal.
|
|
83
|
+
* @param {string | { track?: string }} [trackOrDecision]
|
|
84
|
+
* @returns {boolean}
|
|
85
|
+
*/
|
|
86
|
+
export function isInternalTrack(trackOrDecision) {
|
|
87
|
+
if (!trackOrDecision) return false;
|
|
88
|
+
if (typeof trackOrDecision === "string") return trackOrDecision === TRACK_TYPES.INTERNAL;
|
|
89
|
+
return trackOrDecision.track === TRACK_TYPES.INTERNAL;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Checks whether track or decision is marked as external.
|
|
94
|
+
* @param {string | { track?: string }} [trackOrDecision]
|
|
95
|
+
* @returns {boolean}
|
|
96
|
+
*/
|
|
97
|
+
export function isExternalTrack(trackOrDecision) {
|
|
98
|
+
if (!trackOrDecision) return false;
|
|
99
|
+
if (typeof trackOrDecision === "string") return trackOrDecision === TRACK_TYPES.EXTERNAL;
|
|
100
|
+
return trackOrDecision.track === TRACK_TYPES.EXTERNAL;
|
|
101
|
+
}
|
package/lib/engine.js
CHANGED
|
@@ -24,6 +24,10 @@ import { classifyMeasureKind } from "./measure-kinds.js";
|
|
|
24
24
|
import { SnapshotManager } from "./snapshots.js";
|
|
25
25
|
import { LineageManager } from "./lineage.js";
|
|
26
26
|
import { isRedundantSandboxRequest, buildRedundantSandboxRemediation, checkEscalationJustification } from "./escalation.js";
|
|
27
|
+
import { classifyTrack } from "./dualtrack.js";
|
|
28
|
+
import { applySessionModePolicy, SessionModeRegistry } from "./session-modes.js";
|
|
29
|
+
import { buildEvidenceDossier } from "./dossier.js";
|
|
30
|
+
import { TrajectoryTracker } from "./drift.js";
|
|
27
31
|
|
|
28
32
|
/**
|
|
29
33
|
* Determine whether an operation belongs to a critical risk category requiring lockedAsk (#52).
|
|
@@ -39,29 +43,12 @@ export function isCriticalOperation(toolName, command, execution) {
|
|
|
39
43
|
const tool = String(toolName || "").trim();
|
|
40
44
|
if (!cmd && !tool) return false;
|
|
41
45
|
|
|
42
|
-
|
|
43
|
-
if (
|
|
44
|
-
if (/\b(chmod\s+(-[a-zA-Z]*\s+)?777|chown\s+(-[a-zA-Z]*\s+)?root)\b/i.test(cmd)) return true;
|
|
45
|
-
|
|
46
|
-
// 2. Recursive or bulk destructive deletion
|
|
47
|
-
if (/\brm\s+(-[a-zA-Z]*[rf][a-zA-Z]*\s+|--recursive\s+)/i.test(cmd)) return true;
|
|
48
|
-
if (/\b(shred|wipefs)\b/i.test(cmd)) return true;
|
|
49
|
-
|
|
50
|
-
// 3. Sensitive credentials and SSH access
|
|
46
|
+
if (/^(sudo|su|doas)\b/i.test(cmd) || /\b(chmod\s+(-[a-zA-Z]*\s+)?777|chown\s+(-[a-zA-Z]*\s+)?root)\b/i.test(cmd)) return true;
|
|
47
|
+
if (/\brm\s+(-[a-zA-Z]*[rf][a-zA-Z]*\s+|--recursive\s+)/i.test(cmd) || /\b(shred|wipefs)\b/i.test(cmd)) return true;
|
|
51
48
|
if (/(\.ssh\/(id_|authorized_keys|known_hosts)|\.gnupg\/|\/etc\/shadow)/i.test(cmd)) return true;
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
if (/\b(
|
|
55
|
-
if (/\bdd\s+.*of=\/dev\/(sd[a-z]|nvme|vd[a-z]|hd[a-z]|loop)/i.test(cmd)) return true;
|
|
56
|
-
|
|
57
|
-
// 5. Remote shell piping and reverse shells
|
|
58
|
-
if (/curl\s+.*\|\s*(bash|sh|python|perl)/i.test(cmd)) return true;
|
|
59
|
-
if (/\b(nc|ncat|netcat)\s+.*-e\s+/i.test(cmd)) return true;
|
|
60
|
-
if (/>&\s*\/dev\/tcp\//i.test(cmd)) return true;
|
|
61
|
-
|
|
62
|
-
// 6. System halts and service shutdowns
|
|
63
|
-
if (/\b(shutdown|reboot|poweroff|halt|init\s+0)\b/i.test(cmd)) return true;
|
|
64
|
-
if (/systemctl\s+(stop|disable|mask)\s+(sshd|systemd|ufw|iptables)/i.test(cmd)) return true;
|
|
49
|
+
if (/\b(mkfs|fdisk|parted)\b/i.test(cmd) || /\bdd\s+.*of=\/dev\/(sd[a-z]|nvme|vd[a-z]|hd[a-z]|loop)/i.test(cmd)) return true;
|
|
50
|
+
if (/curl\s+.*\|\s*(bash|sh|python|perl)/i.test(cmd) || /\b(nc|ncat|netcat)\s+.*-e\s+/i.test(cmd) || />&\s*\/dev\/tcp\//i.test(cmd)) return true;
|
|
51
|
+
if (/\b(shutdown|reboot|poweroff|halt|init\s+0)\b/i.test(cmd) || /systemctl\s+(stop|disable|mask)\s+(sshd|systemd|ufw|iptables)/i.test(cmd)) return true;
|
|
65
52
|
|
|
66
53
|
// 7. Protected file writes via tools
|
|
67
54
|
if (tool === "write_to_file" || tool === "edit" || tool === "replace_file_content" || tool === "str_replace_editor") {
|
|
@@ -104,6 +91,8 @@ export class SecurityEngine {
|
|
|
104
91
|
this.lineage = options.lineage !== undefined ? options.lineage : new LineageManager();
|
|
105
92
|
this.enableRedundantSandboxRecovery = options.enableRedundantSandboxRecovery !== false;
|
|
106
93
|
this.requireJustification = options.requireJustification === true;
|
|
94
|
+
this.sessionModes = options.sessionModes || new SessionModeRegistry(options.defaultSessionMode);
|
|
95
|
+
this.trajectoryTracker = options.trajectoryTracker !== undefined ? options.trajectoryTracker : new TrajectoryTracker();
|
|
107
96
|
}
|
|
108
97
|
|
|
109
98
|
/**
|
|
@@ -121,12 +110,7 @@ export class SecurityEngine {
|
|
|
121
110
|
|
|
122
111
|
// Check Retry Exhaustion guard (#96)
|
|
123
112
|
if (this.retryGuard && this.retryGuard.isExhausted(sessionId)) {
|
|
124
|
-
const exhHit = {
|
|
125
|
-
deny: true,
|
|
126
|
-
reason: "retry-exhaustion",
|
|
127
|
-
detail: "Retry exhaustion: consecutive blocked attempts exceeded threshold",
|
|
128
|
-
snippet: command || toolName,
|
|
129
|
-
};
|
|
113
|
+
const exhHit = { deny: true, reason: "retry-exhaustion", detail: "Retry exhaustion: consecutive blocked attempts exceeded threshold", snippet: command || toolName };
|
|
130
114
|
const exhDecision = { verdict: "deny", stage: "P0", reason: "retry-exhaustion", detail: exhHit.detail, hit: exhHit };
|
|
131
115
|
if (this.ledger) this.ledger.record(sessionId, "exhaustion", { reason: "retry-exhaustion", tool: toolName });
|
|
132
116
|
if (this.counters) this.counters.record(exhDecision, execution);
|
|
@@ -162,6 +146,10 @@ export class SecurityEngine {
|
|
|
162
146
|
|
|
163
147
|
let decision = this._evaluateWaterfall(execution, context, sessionId, toolName, command);
|
|
164
148
|
this._enrichDecision(decision, execution, context);
|
|
149
|
+
if (this.sessionModes) {
|
|
150
|
+
const mode = this.sessionModes.getMode(sessionId, context?.preset || context?.sessionMode);
|
|
151
|
+
decision = applySessionModePolicy(mode, decision, { track: decision.track, riskBucket: decision.riskBucket, lockedAsk: decision.lockedAsk });
|
|
152
|
+
}
|
|
165
153
|
|
|
166
154
|
// Post-decision hooks: ledger, retryGuard, cache
|
|
167
155
|
if (decision.verdict === "deny") {
|
|
@@ -413,19 +401,8 @@ export class SecurityEngine {
|
|
|
413
401
|
// If Circuit Breaker is tripped, force P4 Human Ask
|
|
414
402
|
if (this.breaker.isTripped(sessionId)) {
|
|
415
403
|
if (this.ledger) this.ledger.record(sessionId, "breaker", { reason: "circuit-breaker-tripped", tool: toolName });
|
|
416
|
-
const breakerHit = {
|
|
417
|
-
|
|
418
|
-
reason: "circuit-breaker-tripped",
|
|
419
|
-
detail: "Circuit breaker tripped due to repeated security denials. Manual confirmation required.",
|
|
420
|
-
snippet: command || toolName,
|
|
421
|
-
};
|
|
422
|
-
const res = {
|
|
423
|
-
verdict: "ask",
|
|
424
|
-
stage: "P4",
|
|
425
|
-
reason: "circuit-breaker-tripped",
|
|
426
|
-
detail: breakerHit.detail,
|
|
427
|
-
hit: breakerHit,
|
|
428
|
-
};
|
|
404
|
+
const breakerHit = { ask: true, reason: "circuit-breaker-tripped", detail: "Circuit breaker tripped due to repeated security denials. Manual confirmation required.", snippet: command || toolName };
|
|
405
|
+
const res = { verdict: "ask", stage: "P4", reason: "circuit-breaker-tripped", detail: breakerHit.detail, hit: breakerHit };
|
|
429
406
|
this._applyLockedAskIfCritical(res, toolName, command, execution);
|
|
430
407
|
return res;
|
|
431
408
|
}
|
|
@@ -446,6 +423,17 @@ export class SecurityEngine {
|
|
|
446
423
|
return res;
|
|
447
424
|
}
|
|
448
425
|
|
|
426
|
+
// Trajectory Drift (#68)
|
|
427
|
+
if (this.trajectoryTracker) {
|
|
428
|
+
const drift = this.trajectoryTracker.detect(sessionId, execution, context);
|
|
429
|
+
if (drift && drift.hasDrift) {
|
|
430
|
+
const driftHit = { ask: true, reason: drift.reason, detail: drift.detail, snippet: command || toolName };
|
|
431
|
+
const res = { verdict: "ask", stage: "P4", reason: drift.reason, detail: drift.detail, drift, hit: driftHit };
|
|
432
|
+
this._applyLockedAskIfCritical(res, toolName, command, execution);
|
|
433
|
+
return res;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
449
437
|
// P2: Static Rules / Safe Allow (Bands Engine)
|
|
450
438
|
if (bandVerdict === "allow" && !this.requireJustification) {
|
|
451
439
|
this.breaker.resetConsecutive(sessionId);
|
|
@@ -556,16 +544,16 @@ export class SecurityEngine {
|
|
|
556
544
|
*/
|
|
557
545
|
_enrichDecision(decision, execution, context) {
|
|
558
546
|
if (!decision) return decision;
|
|
547
|
+
const command = execution && execution.arguments && execution.arguments.command;
|
|
548
|
+
if (!decision.track) decision.track = classifyTrack(execution?.name, command, execution?.arguments, context);
|
|
549
|
+
if (!decision.dossier) decision.dossier = buildEvidenceDossier({ execution, decision, context, toolName: execution?.name, command });
|
|
559
550
|
if (!decision.riskBucket) decision.riskBucket = categorizeRiskBucket(execution, context);
|
|
560
551
|
if (!decision.riskTags || !Array.isArray(decision.riskTags)) decision.riskTags = extractRiskTags(execution, context);
|
|
561
552
|
if (!decision.measureKind) decision.measureKind = classifyMeasureKind(execution, context);
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
const callId = context?.callId || execution?.callId || execution?.id || context?.execution?.id || null;
|
|
567
|
-
const agentId = context?.agentId || context?.agent?.id || context?.session?.agentId || execution?.agentId || null;
|
|
568
|
-
const subagentId = context?.subagentId || context?.session?.subagentId || execution?.subagentId || null;
|
|
553
|
+
if (command && isRmCommand(command)) decision.trashAlternative = suggestTrashAlternative(command, this.inspectConfig?.workspaceDir, context?.sessionId);
|
|
554
|
+
const callId = context?.callId || execution?.callId || execution?.id || context?.execution?.id;
|
|
555
|
+
const agentId = context?.agentId || context?.agent?.id || context?.session?.agentId || execution?.agentId;
|
|
556
|
+
const subagentId = context?.subagentId || context?.session?.subagentId || execution?.subagentId;
|
|
569
557
|
if (callId) decision.callId = callId;
|
|
570
558
|
if (agentId) decision.agentId = agentId;
|
|
571
559
|
if (subagentId) decision.subagentId = subagentId;
|
|
@@ -583,6 +571,9 @@ export class SecurityEngine {
|
|
|
583
571
|
clearAllSnapshots() { return this.snapshotManager ? this.snapshotManager.clearAllSnapshots() : { cleared: 0 }; }
|
|
584
572
|
getLineage(sessionId) { return this.lineage ? this.lineage.getLineage(sessionId) : [sessionId]; }
|
|
585
573
|
registerLineage(child, parent, root) { return this.lineage ? this.lineage.registerLineage(child, parent, root) : false; }
|
|
574
|
+
setSessionGoal(sessionId, goal, scopes) { if (this.trajectoryTracker) this.trajectoryTracker.setGoal(sessionId, goal, scopes); }
|
|
575
|
+
getSessionGoal(sessionId) { return this.trajectoryTracker ? this.trajectoryTracker.getGoal(sessionId) : null; }
|
|
576
|
+
setSessionMode(sessionId, mode) { return this.sessionModes ? this.sessionModes.setMode(sessionId, mode) : null; }
|
|
577
|
+
getSessionMode(sessionId, preset) { return this.sessionModes ? this.sessionModes.getMode(sessionId, preset) : null; }
|
|
586
578
|
}
|
|
587
579
|
|
|
588
|
-
|
package/lib/index.js
CHANGED
|
@@ -532,12 +532,7 @@ export function apply(ctx, config) {
|
|
|
532
532
|
|
|
533
533
|
export { SecurityEngine } from "./engine.js";
|
|
534
534
|
export { BandsEngine } from "./bands.js";
|
|
535
|
-
export {
|
|
536
|
-
CircuitBreaker,
|
|
537
|
-
CIRCUIT_STATE_ACTIVE,
|
|
538
|
-
CIRCUIT_STATE_TRIPPED,
|
|
539
|
-
CIRCUIT_STATE_RESUMED,
|
|
540
|
-
} from "./breaker.js";
|
|
535
|
+
export { CircuitBreaker, CIRCUIT_STATE_ACTIVE, CIRCUIT_STATE_TRIPPED, CIRCUIT_STATE_RESUMED } from "./breaker.js";
|
|
541
536
|
export { SessionGrantStore, DEFAULT_GRANT_TTL_MS, DEFAULT_GRANT_MAX_USES, matchGrantCriteria } from "./grants.js";
|
|
542
537
|
export { canonicalizeCall, canonicalString, hashCall, sortKeys } from "./canonical.js";
|
|
543
538
|
export { expandHome, resolveSafePath, isPathTraversal, canonicalizeFsPath } from "./paths.js";
|
|
@@ -550,7 +545,7 @@ export { isLoopbackHost, isLoopbackUrl, isLoopbackCommand } from "./loopback.js"
|
|
|
550
545
|
export { computeFingerprint, computeFingerprintHash, normalizeCommandString } from "./fingerprint.js";
|
|
551
546
|
export { VerdictCache, DEFAULT_CACHE_MAX_SIZE, DEFAULT_CACHE_TTL_MS } from "./cache.js";
|
|
552
547
|
export { RetryExhaustionGuard, DEFAULT_MAX_RETRY_ATTEMPTS } from "./retry-exhaustion.js";
|
|
553
|
-
export { QualityLedger, MAX_LEDGER_EVENTS_PER_SESSION } from "./ledger.js";
|
|
548
|
+
export { QualityLedger, MAX_LEDGER_EVENTS_PER_SESSION, CURRENT_SCHEMA_VERSION, SUPPORTED_SCHEMA_VERSIONS, migrateVerdictRecord } from "./ledger.js";
|
|
554
549
|
export { AuditLogger, DEFAULT_AUDIT_LOG_RELATIVE, DEFAULT_MAX_AUDIT_BYTES, DEFAULT_MAX_AUDIT_LINES, DEFAULT_RETAIN_LINES, auditRotateContent } from "./audit-log.js";
|
|
555
550
|
export { BlockCounters, categorizeReason } from "./counters.js";
|
|
556
551
|
export { isCriticalOperation } from "./engine.js";
|
|
@@ -575,6 +570,7 @@ export { MEASURE_KINDS, classifyMeasureKind, isSystemLevelExecution, isWorkspace
|
|
|
575
570
|
export { LineageManager } from "./lineage.js";
|
|
576
571
|
export { generateAgentGuidance, injectAgentGuidance, extractGuidance, GUIDANCE_MARKER } from "./guidance.js";
|
|
577
572
|
export { isRedundantSandboxRequest, buildRedundantSandboxRemediation, checkEscalationJustification, REDUNDANT_SANDBOX_REASON, MISSING_JUSTIFICATION_REASON, INSUFFICIENT_JUSTIFICATION_REASON } from "./escalation.js";
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
573
|
+
export { TRACK_TYPES, classifyTrack, isInternalTrack, isExternalTrack } from "./dualtrack.js";
|
|
574
|
+
export { GATE_MODES, PRESET_MODE_MAP, resolveSessionGateMode, applySessionModePolicy, SessionModeRegistry } from "./session-modes.js";
|
|
575
|
+
export { buildEvidenceDossier, formatDossierSummary, MAX_PROMPT_CHARS, MAX_DIFF_CHARS, MAX_OUTPUT_CHARS } from "./dossier.js";
|
|
576
|
+
export { DRIFT_CATEGORIES, detectTrajectoryDrift, TrajectoryTracker } from "./drift.js";
|
package/lib/ledger.js
CHANGED
|
@@ -1,164 +1,264 @@
|
|
|
1
|
-
// lib/ledger.js
|
|
2
|
-
// Quality and Anomaly Ledger for dsh-approval-gate (#88).
|
|
3
|
-
// Tracks session health, consecutive denials, and execution anomalies.
|
|
4
|
-
|
|
5
|
-
export const MAX_LEDGER_EVENTS_PER_SESSION = 50;
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
* @
|
|
18
|
-
* @
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
ledger
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
ledger.
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
}
|
|
1
|
+
// lib/ledger.js
|
|
2
|
+
// Quality and Anomaly Ledger for dsh-approval-gate (#88, #73).
|
|
3
|
+
// Tracks session health, consecutive denials, and execution anomalies with schema versioning.
|
|
4
|
+
|
|
5
|
+
export const MAX_LEDGER_EVENTS_PER_SESSION = 50;
|
|
6
|
+
|
|
7
|
+
export const CURRENT_SCHEMA_VERSION = 3;
|
|
8
|
+
export const SUPPORTED_SCHEMA_VERSIONS = Object.freeze([1, 2, 3]);
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Migrates a verdict record across schema versions (v1 -> v2 -> v3) (#73).
|
|
12
|
+
* - v1: { id, verdict, ruleId, timestamp, riskBucket }
|
|
13
|
+
* - v2: + { reviewDecision, bandVerdict, mitigation, auditSummary }
|
|
14
|
+
* - v3: + { track, dossier, driftScore, driftWarning, schema_version: 3 }
|
|
15
|
+
*
|
|
16
|
+
* @param {Record<string, any>} record
|
|
17
|
+
* @param {number} [targetVersion=CURRENT_SCHEMA_VERSION]
|
|
18
|
+
* @returns {Record<string, any>}
|
|
19
|
+
*/
|
|
20
|
+
export function migrateVerdictRecord(record, targetVersion = CURRENT_SCHEMA_VERSION) {
|
|
21
|
+
if (!record || typeof record !== "object") return record;
|
|
22
|
+
|
|
23
|
+
let currentVersion = record.schema_version;
|
|
24
|
+
if (!currentVersion) {
|
|
25
|
+
// Infer version from fields
|
|
26
|
+
if (record.reviewDecision !== undefined || record.bandVerdict !== undefined) {
|
|
27
|
+
currentVersion = 2;
|
|
28
|
+
} else {
|
|
29
|
+
currentVersion = 1;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let migrated = { ...record };
|
|
34
|
+
|
|
35
|
+
// v1 -> v2 migration
|
|
36
|
+
if (currentVersion < 2 && targetVersion >= 2) {
|
|
37
|
+
migrated.reviewDecision = migrated.reviewDecision || migrated.verdict || "allow";
|
|
38
|
+
migrated.bandVerdict = migrated.bandVerdict || migrated.verdict || "allow";
|
|
39
|
+
migrated.mitigation = migrated.mitigation || null;
|
|
40
|
+
migrated.auditSummary = migrated.auditSummary || null;
|
|
41
|
+
migrated.schema_version = 2;
|
|
42
|
+
currentVersion = 2;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// v2 -> v3 migration
|
|
46
|
+
if (currentVersion < 3 && targetVersion >= 3) {
|
|
47
|
+
migrated.track = migrated.track || "external";
|
|
48
|
+
migrated.dossier = migrated.dossier || null;
|
|
49
|
+
migrated.driftScore = typeof migrated.driftScore === "number" ? migrated.driftScore : 0;
|
|
50
|
+
migrated.driftWarning = Boolean(migrated.driftWarning);
|
|
51
|
+
migrated.schema_version = 3;
|
|
52
|
+
currentVersion = 3;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
migrated.schema_version = currentVersion;
|
|
56
|
+
return migrated;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* @typedef {Object} LedgerEvent
|
|
61
|
+
* @property {number} timestamp
|
|
62
|
+
* @property {string} type - "allow" | "deny" | "ask" | "exhaustion" | "breaker" | "error"
|
|
63
|
+
* @property {string} reason
|
|
64
|
+
* @property {string} [detail]
|
|
65
|
+
* @property {string} [tool]
|
|
66
|
+
* @property {number} [schema_version]
|
|
67
|
+
*/
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* @typedef {Object} SessionLedger
|
|
71
|
+
* @property {string} sessionId
|
|
72
|
+
* @property {number} totalDecisions
|
|
73
|
+
* @property {number} allowedCount
|
|
74
|
+
* @property {number} deniedCount
|
|
75
|
+
* @property {number} askCount
|
|
76
|
+
* @property {number} anomalyCount
|
|
77
|
+
* @property {number} consecutiveDenies
|
|
78
|
+
* @property {boolean} breakerTripped
|
|
79
|
+
* @property {boolean} retryExhausted
|
|
80
|
+
* @property {LedgerEvent[]} recentEvents
|
|
81
|
+
*/
|
|
82
|
+
|
|
83
|
+
export class QualityLedger {
|
|
84
|
+
constructor() {
|
|
85
|
+
/** @type {Map<string, SessionLedger>} */
|
|
86
|
+
this.sessions = new Map();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* @private
|
|
91
|
+
*/
|
|
92
|
+
_getOrCreate(sessionId) {
|
|
93
|
+
const key = sessionId || "default";
|
|
94
|
+
let ledger = this.sessions.get(key);
|
|
95
|
+
if (!ledger) {
|
|
96
|
+
ledger = {
|
|
97
|
+
sessionId: key,
|
|
98
|
+
totalDecisions: 0,
|
|
99
|
+
allowedCount: 0,
|
|
100
|
+
deniedCount: 0,
|
|
101
|
+
askCount: 0,
|
|
102
|
+
anomalyCount: 0,
|
|
103
|
+
consecutiveDenies: 0,
|
|
104
|
+
breakerTripped: false,
|
|
105
|
+
retryExhausted: false,
|
|
106
|
+
recentEvents: [],
|
|
107
|
+
};
|
|
108
|
+
this.sessions.set(key, ledger);
|
|
109
|
+
}
|
|
110
|
+
return ledger;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Record a decision or system event in the ledger.
|
|
115
|
+
*
|
|
116
|
+
* @param {string} sessionId
|
|
117
|
+
* @param {string} type - "allow" | "deny" | "ask" | "exhaustion" | "breaker" | "error"
|
|
118
|
+
* @param {{ reason?: string, detail?: string, tool?: string, track?: string, dossier?: any }} [details]
|
|
119
|
+
*/
|
|
120
|
+
record(sessionId, type, details = {}) {
|
|
121
|
+
const ledger = this._getOrCreate(sessionId);
|
|
122
|
+
const now = Date.now();
|
|
123
|
+
|
|
124
|
+
const rawEvent = {
|
|
125
|
+
timestamp: now,
|
|
126
|
+
type,
|
|
127
|
+
reason: details.reason || "",
|
|
128
|
+
detail: details.detail ? String(details.detail).slice(0, 200) : undefined,
|
|
129
|
+
tool: details.tool || "",
|
|
130
|
+
track: details.track,
|
|
131
|
+
dossier: details.dossier,
|
|
132
|
+
schema_version: CURRENT_SCHEMA_VERSION,
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const event = migrateVerdictRecord(rawEvent, CURRENT_SCHEMA_VERSION);
|
|
136
|
+
|
|
137
|
+
ledger.recentEvents.push(event);
|
|
138
|
+
if (ledger.recentEvents.length > MAX_LEDGER_EVENTS_PER_SESSION) {
|
|
139
|
+
ledger.recentEvents.shift();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (type === "allow") {
|
|
143
|
+
ledger.totalDecisions += 1;
|
|
144
|
+
ledger.allowedCount += 1;
|
|
145
|
+
ledger.consecutiveDenies = 0;
|
|
146
|
+
} else if (type === "deny") {
|
|
147
|
+
ledger.totalDecisions += 1;
|
|
148
|
+
ledger.deniedCount += 1;
|
|
149
|
+
ledger.consecutiveDenies += 1;
|
|
150
|
+
if (ledger.consecutiveDenies > 1) {
|
|
151
|
+
ledger.anomalyCount += 1;
|
|
152
|
+
}
|
|
153
|
+
} else if (type === "ask") {
|
|
154
|
+
ledger.totalDecisions += 1;
|
|
155
|
+
ledger.askCount += 1;
|
|
156
|
+
} else if (type === "exhaustion") {
|
|
157
|
+
ledger.retryExhausted = true;
|
|
158
|
+
ledger.anomalyCount += 1;
|
|
159
|
+
} else if (type === "breaker") {
|
|
160
|
+
ledger.breakerTripped = true;
|
|
161
|
+
ledger.anomalyCount += 1;
|
|
162
|
+
} else if (type === "error") {
|
|
163
|
+
ledger.anomalyCount += 1;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Returns a quality summary for the session.
|
|
169
|
+
*
|
|
170
|
+
* @param {string} sessionId
|
|
171
|
+
* @returns {{
|
|
172
|
+
* sessionId: string,
|
|
173
|
+
* totalDecisions: number,
|
|
174
|
+
* allowedCount: number,
|
|
175
|
+
* deniedCount: number,
|
|
176
|
+
* askCount: number,
|
|
177
|
+
* anomalyCount: number,
|
|
178
|
+
* consecutiveDenies: number,
|
|
179
|
+
* qualityScore: number,
|
|
180
|
+
* degradationWarning: boolean,
|
|
181
|
+
* recentEvents: LedgerEvent[]
|
|
182
|
+
* }}
|
|
183
|
+
*/
|
|
184
|
+
getSummary(sessionId) {
|
|
185
|
+
const ledger = this._getOrCreate(sessionId);
|
|
186
|
+
const total = ledger.totalDecisions;
|
|
187
|
+
|
|
188
|
+
// Quality score: 1.0 down towards 0.0 based on denial & anomaly ratio
|
|
189
|
+
let score = 1.0;
|
|
190
|
+
if (total > 0) {
|
|
191
|
+
const penalty = (ledger.deniedCount * 0.2 + ledger.anomalyCount * 0.3) / total;
|
|
192
|
+
score = Math.max(0, Math.min(1.0, 1.0 - penalty));
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const degradationWarning = ledger.consecutiveDenies >= 2 ||
|
|
196
|
+
ledger.retryExhausted ||
|
|
197
|
+
ledger.breakerTripped ||
|
|
198
|
+
(total >= 3 && ledger.deniedCount / total >= 0.5);
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
sessionId: ledger.sessionId,
|
|
202
|
+
totalDecisions: ledger.totalDecisions,
|
|
203
|
+
allowedCount: ledger.allowedCount,
|
|
204
|
+
deniedCount: ledger.deniedCount,
|
|
205
|
+
askCount: ledger.askCount,
|
|
206
|
+
anomalyCount: ledger.anomalyCount,
|
|
207
|
+
consecutiveDenies: ledger.consecutiveDenies,
|
|
208
|
+
qualityScore: Number(score.toFixed(2)),
|
|
209
|
+
degradationWarning,
|
|
210
|
+
recentEvents: ledger.recentEvents.map((e) => ({ ...e })),
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Serializes session ledger state with current schema version (#73).
|
|
216
|
+
* @param {string} sessionId
|
|
217
|
+
* @returns {Record<string, any>}
|
|
218
|
+
*/
|
|
219
|
+
serializeSession(sessionId) {
|
|
220
|
+
const ledger = this._getOrCreate(sessionId);
|
|
221
|
+
return {
|
|
222
|
+
schema_version: CURRENT_SCHEMA_VERSION,
|
|
223
|
+
sessionId: ledger.sessionId,
|
|
224
|
+
totalDecisions: ledger.totalDecisions,
|
|
225
|
+
allowedCount: ledger.allowedCount,
|
|
226
|
+
deniedCount: ledger.deniedCount,
|
|
227
|
+
askCount: ledger.askCount,
|
|
228
|
+
anomalyCount: ledger.anomalyCount,
|
|
229
|
+
consecutiveDenies: ledger.consecutiveDenies,
|
|
230
|
+
breakerTripped: ledger.breakerTripped,
|
|
231
|
+
retryExhausted: ledger.retryExhausted,
|
|
232
|
+
recentEvents: ledger.recentEvents.map((e) => migrateVerdictRecord(e, CURRENT_SCHEMA_VERSION)),
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Restores session ledger state from serialized data with automatic migration (#73).
|
|
238
|
+
* @param {Record<string, any>} data
|
|
239
|
+
*/
|
|
240
|
+
deserializeSession(data) {
|
|
241
|
+
if (!data || !data.sessionId) return;
|
|
242
|
+
const ledger = this._getOrCreate(data.sessionId);
|
|
243
|
+
ledger.totalDecisions = data.totalDecisions || 0;
|
|
244
|
+
ledger.allowedCount = data.allowedCount || 0;
|
|
245
|
+
ledger.deniedCount = data.deniedCount || 0;
|
|
246
|
+
ledger.askCount = data.askCount || 0;
|
|
247
|
+
ledger.anomalyCount = data.anomalyCount || 0;
|
|
248
|
+
ledger.consecutiveDenies = data.consecutiveDenies || 0;
|
|
249
|
+
ledger.breakerTripped = Boolean(data.breakerTripped);
|
|
250
|
+
ledger.retryExhausted = Boolean(data.retryExhausted);
|
|
251
|
+
ledger.recentEvents = Array.isArray(data.recentEvents)
|
|
252
|
+
? data.recentEvents.map((e) => migrateVerdictRecord(e, CURRENT_SCHEMA_VERSION))
|
|
253
|
+
: [];
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
reset(sessionId) {
|
|
257
|
+
const key = sessionId || "default";
|
|
258
|
+
this.sessions.delete(key);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
clear() {
|
|
262
|
+
this.sessions.clear();
|
|
263
|
+
}
|
|
264
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
// lib/session-modes.js
|
|
2
|
+
// Session-level Gate Mode Switching (#61).
|
|
3
|
+
// Allows per-session mode switching between auto-approval, hybrid, manual, and strict modes.
|
|
4
|
+
|
|
5
|
+
export const GATE_MODES = Object.freeze({
|
|
6
|
+
AUTO: "auto-approval",
|
|
7
|
+
MANUAL: "manual",
|
|
8
|
+
HYBRID: "hybrid",
|
|
9
|
+
STRICT: "strict",
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
export const PRESET_MODE_MAP = Object.freeze({
|
|
13
|
+
sandbox: GATE_MODES.AUTO,
|
|
14
|
+
test: GATE_MODES.AUTO,
|
|
15
|
+
auto: GATE_MODES.AUTO,
|
|
16
|
+
"auto-approval": GATE_MODES.AUTO,
|
|
17
|
+
prod: GATE_MODES.STRICT,
|
|
18
|
+
production: GATE_MODES.STRICT,
|
|
19
|
+
strict: GATE_MODES.STRICT,
|
|
20
|
+
manual: GATE_MODES.MANUAL,
|
|
21
|
+
dev: GATE_MODES.HYBRID,
|
|
22
|
+
development: GATE_MODES.HYBRID,
|
|
23
|
+
hybrid: GATE_MODES.HYBRID,
|
|
24
|
+
standard: GATE_MODES.HYBRID,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Resolves active gate mode for a session based on presets, context, or defaults.
|
|
29
|
+
*
|
|
30
|
+
* @param {string} [sessionId]
|
|
31
|
+
* @param {string} [sessionPreset]
|
|
32
|
+
* @param {Record<string, any>} [options]
|
|
33
|
+
* @returns {string}
|
|
34
|
+
*/
|
|
35
|
+
export function resolveSessionGateMode(sessionId, sessionPreset, options = {}) {
|
|
36
|
+
// 1. Explicit mode in options or context
|
|
37
|
+
if (options.mode && Object.values(GATE_MODES).includes(options.mode)) {
|
|
38
|
+
return options.mode;
|
|
39
|
+
}
|
|
40
|
+
if (options.sessionMode && Object.values(GATE_MODES).includes(options.sessionMode)) {
|
|
41
|
+
return options.sessionMode;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// 2. Preset mapping
|
|
45
|
+
const presetKey = String(sessionPreset || options.preset || "").toLowerCase().trim();
|
|
46
|
+
if (presetKey && PRESET_MODE_MAP[presetKey]) {
|
|
47
|
+
return PRESET_MODE_MAP[presetKey];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// 3. Fallback default
|
|
51
|
+
return options.defaultMode || GATE_MODES.HYBRID;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Applies session gate mode policy to a verdict decision (#61).
|
|
56
|
+
*
|
|
57
|
+
* @param {string} mode - Gate mode
|
|
58
|
+
* @param {Record<string, any>} decision - Original verdict decision
|
|
59
|
+
* @param {{ track?: string, riskBucket?: string, lockedAsk?: boolean, isRead?: boolean }} [meta]
|
|
60
|
+
* @returns {Record<string, any>}
|
|
61
|
+
*/
|
|
62
|
+
export function applySessionModePolicy(mode, decision, meta = {}) {
|
|
63
|
+
if (!decision) return decision;
|
|
64
|
+
|
|
65
|
+
const currentMode = mode || GATE_MODES.HYBRID;
|
|
66
|
+
const isLocked = Boolean(decision.lockedAsk || meta.lockedAsk);
|
|
67
|
+
const track = meta.track || decision.track || "external";
|
|
68
|
+
const riskBucket = meta.riskBucket || decision.riskBucket || "medium";
|
|
69
|
+
|
|
70
|
+
// Auto-approval mode: relaxes low-risk or internal chores that would otherwise ask
|
|
71
|
+
if (currentMode === GATE_MODES.AUTO && decision.verdict === "ask" && !isLocked) {
|
|
72
|
+
const isInternal = track === "internal";
|
|
73
|
+
const isLowRisk = riskBucket === "low" || riskBucket === "minimal";
|
|
74
|
+
if (isInternal || isLowRisk) {
|
|
75
|
+
return {
|
|
76
|
+
...decision,
|
|
77
|
+
verdict: "allow",
|
|
78
|
+
autoApproved: true,
|
|
79
|
+
originalVerdict: "ask",
|
|
80
|
+
sessionMode: currentMode,
|
|
81
|
+
stage: "session-mode-auto",
|
|
82
|
+
autoReason: isInternal ? "auto-approve-internal-track" : "auto-approve-low-risk",
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Manual or strict mode: requires manual confirmation for external mutations or higher risk
|
|
88
|
+
if ((currentMode === GATE_MODES.MANUAL || currentMode === GATE_MODES.STRICT) && decision.verdict === "allow") {
|
|
89
|
+
const isSafeRead = Boolean(meta.isRead);
|
|
90
|
+
const isInternalSafe = track === "internal" && (riskBucket === "low" || riskBucket === "minimal");
|
|
91
|
+
if (!isSafeRead && !isInternalSafe) {
|
|
92
|
+
return {
|
|
93
|
+
...decision,
|
|
94
|
+
verdict: "ask",
|
|
95
|
+
manualRequired: true,
|
|
96
|
+
originalVerdict: "allow",
|
|
97
|
+
sessionMode: currentMode,
|
|
98
|
+
stage: "session-mode-manual",
|
|
99
|
+
manualReason: currentMode === GATE_MODES.STRICT ? "strict-mode-enforcement" : "manual-mode-enforcement",
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
...decision,
|
|
106
|
+
sessionMode: currentMode,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Registry for maintaining per-session gate modes (#61).
|
|
112
|
+
*/
|
|
113
|
+
export class SessionModeRegistry {
|
|
114
|
+
constructor(defaultMode = GATE_MODES.HYBRID) {
|
|
115
|
+
this.defaultMode = defaultMode;
|
|
116
|
+
/** @type {Map<string, string>} */
|
|
117
|
+
this.sessionModes = new Map();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
setMode(sessionId, mode) {
|
|
121
|
+
const key = sessionId || "default";
|
|
122
|
+
const resolved = PRESET_MODE_MAP[String(mode).toLowerCase().trim()] || mode;
|
|
123
|
+
if (Object.values(GATE_MODES).includes(resolved)) {
|
|
124
|
+
this.sessionModes.set(key, resolved);
|
|
125
|
+
return resolved;
|
|
126
|
+
}
|
|
127
|
+
return this.defaultMode;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
getMode(sessionId, preset) {
|
|
131
|
+
const key = sessionId || "default";
|
|
132
|
+
if (this.sessionModes.has(key)) {
|
|
133
|
+
return this.sessionModes.get(key);
|
|
134
|
+
}
|
|
135
|
+
return resolveSessionGateMode(sessionId, preset, { defaultMode: this.defaultMode });
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
clearSession(sessionId) {
|
|
139
|
+
const key = sessionId || "default";
|
|
140
|
+
this.sessionModes.delete(key);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
reset() {
|
|
144
|
+
this.sessionModes.clear();
|
|
145
|
+
}
|
|
146
|
+
}
|
package/package.json
CHANGED
|
@@ -1,55 +1,55 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@goodandready/dsh-approval-gate",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "Host-only command safety gate for DeepSeek Harness: blocks recognized dangerous operations and requests DSH approval when a command cannot be inspected.",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"main": "lib/index.js",
|
|
7
|
-
"exports": {
|
|
8
|
-
".": "./lib/index.js",
|
|
9
|
-
"./package.json": "./package.json",
|
|
10
|
-
"./cordis.patch.yml": "./cordis.patch.yml"
|
|
11
|
-
},
|
|
12
|
-
"files": [
|
|
13
|
-
"lib",
|
|
14
|
-
"rules",
|
|
15
|
-
"cordis.patch.yml",
|
|
16
|
-
"README.md",
|
|
17
|
-
"README.zh.md",
|
|
18
|
-
"README.ru.md",
|
|
19
|
-
"CHANGELOG.md",
|
|
20
|
-
"LICENSE"
|
|
21
|
-
],
|
|
22
|
-
"dsh": {
|
|
23
|
-
"bundle": {
|
|
24
|
-
"patch": "./cordis.patch.yml"
|
|
25
|
-
}
|
|
26
|
-
},
|
|
27
|
-
"license": "MIT",
|
|
28
|
-
"peerDependencies": {
|
|
29
|
-
"@deepseek-ai/cordis": "^4.0.1",
|
|
30
|
-
"@deepseek-ai/schemastery": "^3.18.1"
|
|
31
|
-
},
|
|
32
|
-
"repository": {
|
|
33
|
-
"type": "git",
|
|
34
|
-
"url": "git+https://github.com/GooDAnDReaDY/dsh-approval-gate.git"
|
|
35
|
-
},
|
|
36
|
-
"homepage": "https://github.com/GooDAnDReaDY/dsh-approval-gate#readme",
|
|
37
|
-
"bugs": {
|
|
38
|
-
"url": "https://github.com/GooDAnDReaDY/dsh-approval-gate/issues"
|
|
39
|
-
},
|
|
40
|
-
"scripts": {
|
|
41
|
-
"test": "node --test test/*.test.mjs"
|
|
42
|
-
},
|
|
43
|
-
"publishConfig": {
|
|
44
|
-
"registry": "https://registry.npmjs.org",
|
|
45
|
-
"access": "public"
|
|
46
|
-
},
|
|
47
|
-
"keywords": [
|
|
48
|
-
"dsh",
|
|
49
|
-
"dsh-plugin",
|
|
50
|
-
"deepseek-harness",
|
|
51
|
-
"security",
|
|
52
|
-
"approval-gate",
|
|
53
|
-
"safety"
|
|
54
|
-
]
|
|
55
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@goodandready/dsh-approval-gate",
|
|
3
|
+
"version": "0.1.23",
|
|
4
|
+
"description": "Host-only command safety gate for DeepSeek Harness: blocks recognized dangerous operations and requests DSH approval when a command cannot be inspected.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "lib/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./lib/index.js",
|
|
9
|
+
"./package.json": "./package.json",
|
|
10
|
+
"./cordis.patch.yml": "./cordis.patch.yml"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"lib",
|
|
14
|
+
"rules",
|
|
15
|
+
"cordis.patch.yml",
|
|
16
|
+
"README.md",
|
|
17
|
+
"README.zh.md",
|
|
18
|
+
"README.ru.md",
|
|
19
|
+
"CHANGELOG.md",
|
|
20
|
+
"LICENSE"
|
|
21
|
+
],
|
|
22
|
+
"dsh": {
|
|
23
|
+
"bundle": {
|
|
24
|
+
"patch": "./cordis.patch.yml"
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"peerDependencies": {
|
|
29
|
+
"@deepseek-ai/cordis": "^4.0.1",
|
|
30
|
+
"@deepseek-ai/schemastery": "^3.18.1"
|
|
31
|
+
},
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "git+https://github.com/GooDAnDReaDY/dsh-approval-gate.git"
|
|
35
|
+
},
|
|
36
|
+
"homepage": "https://github.com/GooDAnDReaDY/dsh-approval-gate#readme",
|
|
37
|
+
"bugs": {
|
|
38
|
+
"url": "https://github.com/GooDAnDReaDY/dsh-approval-gate/issues"
|
|
39
|
+
},
|
|
40
|
+
"scripts": {
|
|
41
|
+
"test": "node --test test/*.test.mjs"
|
|
42
|
+
},
|
|
43
|
+
"publishConfig": {
|
|
44
|
+
"registry": "https://registry.npmjs.org",
|
|
45
|
+
"access": "public"
|
|
46
|
+
},
|
|
47
|
+
"keywords": [
|
|
48
|
+
"dsh",
|
|
49
|
+
"dsh-plugin",
|
|
50
|
+
"deepseek-harness",
|
|
51
|
+
"security",
|
|
52
|
+
"approval-gate",
|
|
53
|
+
"safety"
|
|
54
|
+
]
|
|
55
|
+
}
|