@goodandready/dsh-approval-gate 0.1.26 → 0.1.28
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 +229 -218
- package/lib/client.js +252 -67
- package/lib/compat.js +55 -22
- package/lib/index.js +3 -5
- package/lib/learning.js +5 -0
- package/lib/timeline-service.js +114 -33
- package/package.json +1 -1
package/lib/timeline-service.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// lib/timeline-service.js
|
|
2
|
-
// Session Audit Timeline
|
|
2
|
+
// Session Audit Timeline, Learned Rules & WebServer REST Service for DSH Approval Gate (#82, #60, #74, #86, #84, #49, #116).
|
|
3
3
|
|
|
4
4
|
import { generateDiffLines, groupHunks, formatUnifiedDiff } from "./diff-viewer.js";
|
|
5
5
|
|
|
@@ -26,14 +26,18 @@ import { generateDiffLines, groupHunks, formatUnifiedDiff } from "./diff-viewer.
|
|
|
26
26
|
*/
|
|
27
27
|
export class TimelineService {
|
|
28
28
|
/**
|
|
29
|
-
* @param {{ maxEventsPerSession?: number, turnCardTracker?: any }} [options]
|
|
29
|
+
* @param {{ maxEventsPerSession?: number, turnCardTracker?: any, learner?: any, engine?: any }} [options]
|
|
30
30
|
*/
|
|
31
31
|
constructor(options = {}) {
|
|
32
32
|
this.maxEventsPerSession = options.maxEventsPerSession || 500;
|
|
33
33
|
this.turnCardTracker = options.turnCardTracker || null;
|
|
34
|
+
this.learner = options.learner || null;
|
|
35
|
+
this.engine = options.engine || null;
|
|
34
36
|
/** @type {Map<string, TimelineEvent[]>} */
|
|
35
37
|
this._sessions = new Map();
|
|
36
38
|
this._nextId = 1;
|
|
39
|
+
this.customRules = [];
|
|
40
|
+
this.whitelistPaths = [];
|
|
37
41
|
}
|
|
38
42
|
|
|
39
43
|
/**
|
|
@@ -128,7 +132,6 @@ export class TimelineService {
|
|
|
128
132
|
}
|
|
129
133
|
|
|
130
134
|
const total = filtered.length;
|
|
131
|
-
// Return newest first
|
|
132
135
|
const reversed = filtered.slice().reverse();
|
|
133
136
|
const paginated = reversed.slice(offset, offset + limit);
|
|
134
137
|
|
|
@@ -146,7 +149,7 @@ export class TimelineService {
|
|
|
146
149
|
}
|
|
147
150
|
|
|
148
151
|
/**
|
|
149
|
-
* Registers REST HTTP routes on ctx.webServer for browser UI interaction (#82, #74, #86).
|
|
152
|
+
* Registers REST HTTP routes on ctx.webServer for browser UI interaction (#82, #74, #86, #84, #49, #116).
|
|
150
153
|
*
|
|
151
154
|
* @param {any} ctx Cordis context
|
|
152
155
|
* @param {Object} [options]
|
|
@@ -166,7 +169,17 @@ export class TimelineService {
|
|
|
166
169
|
res.end(JSON.stringify(data));
|
|
167
170
|
};
|
|
168
171
|
|
|
169
|
-
|
|
172
|
+
const parseBody = (req) =>
|
|
173
|
+
new Promise((resolve) => {
|
|
174
|
+
let body = "";
|
|
175
|
+
req.on("data", (c) => { body += c; });
|
|
176
|
+
req.on("end", () => {
|
|
177
|
+
try { resolve(JSON.parse(body || "{}")); }
|
|
178
|
+
catch { resolve({}); }
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// 1. GET /timeline
|
|
170
183
|
const timelineRoute = () =>
|
|
171
184
|
ctx.webServer.register({
|
|
172
185
|
path: `${prefix}/timeline`,
|
|
@@ -185,7 +198,7 @@ export class TimelineService {
|
|
|
185
198
|
},
|
|
186
199
|
});
|
|
187
200
|
|
|
188
|
-
//
|
|
201
|
+
// 2. GET /turn-card
|
|
189
202
|
const turnCardRoute = () =>
|
|
190
203
|
ctx.webServer.register({
|
|
191
204
|
path: `${prefix}/turn-card`,
|
|
@@ -207,46 +220,114 @@ export class TimelineService {
|
|
|
207
220
|
},
|
|
208
221
|
});
|
|
209
222
|
|
|
210
|
-
//
|
|
223
|
+
// 3. POST /diff
|
|
211
224
|
const diffRoute = () =>
|
|
212
225
|
ctx.webServer.register({
|
|
213
226
|
path: `${prefix}/diff`,
|
|
214
227
|
method: "POST",
|
|
215
228
|
handler: async (req, res) => {
|
|
216
229
|
try {
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
const hunks = groupHunks(diffLines, { contextLines });
|
|
227
|
-
const diffText = formatUnifiedDiff(hunks, {
|
|
228
|
-
oldFile: parsed.filename ? `a/${parsed.filename}` : "a/original",
|
|
229
|
-
newFile: parsed.filename ? `b/${parsed.filename}` : "b/modified",
|
|
230
|
-
});
|
|
231
|
-
jsonResponse(res, 200, { ok: true, hunks, diffText, linesCount: diffLines.length });
|
|
232
|
-
} catch (parseErr) {
|
|
233
|
-
jsonResponse(res, 400, { ok: false, error: "invalid-json" });
|
|
234
|
-
}
|
|
230
|
+
const parsed = await parseBody(req);
|
|
231
|
+
const oldText = parsed.oldText || "";
|
|
232
|
+
const newText = parsed.newText || "";
|
|
233
|
+
const contextLines = typeof parsed.contextLines === "number" ? parsed.contextLines : 3;
|
|
234
|
+
const diffLines = generateDiffLines(oldText, newText);
|
|
235
|
+
const hunks = groupHunks(diffLines, { contextLines });
|
|
236
|
+
const diffText = formatUnifiedDiff(hunks, {
|
|
237
|
+
oldFile: parsed.filename ? `a/${parsed.filename}` : "a/original",
|
|
238
|
+
newFile: parsed.filename ? `b/${parsed.filename}` : "b/modified",
|
|
235
239
|
});
|
|
240
|
+
jsonResponse(res, 200, { ok: true, hunks, diffText, linesCount: diffLines.length });
|
|
241
|
+
} catch (err) {
|
|
242
|
+
jsonResponse(res, 500, { ok: false, error: String(err && err.message || err) });
|
|
243
|
+
}
|
|
244
|
+
},
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
// 4. GET /learned-rules (#84)
|
|
248
|
+
const learnedRulesRoute = () =>
|
|
249
|
+
ctx.webServer.register({
|
|
250
|
+
path: `${prefix}/learned-rules`,
|
|
251
|
+
method: "GET",
|
|
252
|
+
handler: async (req, res) => {
|
|
253
|
+
try {
|
|
254
|
+
const url = new URL(req.url, "http://127.0.0.1");
|
|
255
|
+
const sid = url.searchParams.get("sessionId") || "default";
|
|
256
|
+
const rules = (this.learner && typeof this.learner.getLearnedRules === "function")
|
|
257
|
+
? this.learner.getLearnedRules(sid)
|
|
258
|
+
: [];
|
|
259
|
+
jsonResponse(res, 200, { ok: true, sessionId: sid, rules });
|
|
260
|
+
} catch (err) {
|
|
261
|
+
jsonResponse(res, 500, { ok: false, error: String(err && err.message || err) });
|
|
262
|
+
}
|
|
263
|
+
},
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
// 5. POST /learned-rules/revoke (#84)
|
|
267
|
+
const revokeLearnedRoute = () =>
|
|
268
|
+
ctx.webServer.register({
|
|
269
|
+
path: `${prefix}/learned-rules/revoke`,
|
|
270
|
+
method: "POST",
|
|
271
|
+
handler: async (req, res) => {
|
|
272
|
+
try {
|
|
273
|
+
const body = await parseBody(req);
|
|
274
|
+
const sid = body.sessionId || "default";
|
|
275
|
+
const key = body.key || "";
|
|
276
|
+
let revoked = false;
|
|
277
|
+
if (this.learner && typeof this.learner.revoke === "function" && key) {
|
|
278
|
+
revoked = this.learner.revoke(sid, key);
|
|
279
|
+
}
|
|
280
|
+
jsonResponse(res, 200, { ok: true, revoked, key });
|
|
281
|
+
} catch (err) {
|
|
282
|
+
jsonResponse(res, 500, { ok: false, error: String(err && err.message || err) });
|
|
283
|
+
}
|
|
284
|
+
},
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
// 6. GET /mode (#116)
|
|
288
|
+
const modeRoute = () =>
|
|
289
|
+
ctx.webServer.register({
|
|
290
|
+
path: `${prefix}/mode`,
|
|
291
|
+
method: "GET",
|
|
292
|
+
handler: async (req, res) => {
|
|
293
|
+
try {
|
|
294
|
+
const mode = this.engine?.mode || "hybrid";
|
|
295
|
+
jsonResponse(res, 200, { ok: true, mode });
|
|
296
|
+
} catch (err) {
|
|
297
|
+
jsonResponse(res, 500, { ok: false, error: String(err && err.message || err) });
|
|
298
|
+
}
|
|
299
|
+
},
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
// 7. GET & POST /rules (#49)
|
|
303
|
+
const rulesRoute = () =>
|
|
304
|
+
ctx.webServer.register({
|
|
305
|
+
path: `${prefix}/rules`,
|
|
306
|
+
method: "GET",
|
|
307
|
+
handler: async (req, res) => {
|
|
308
|
+
jsonResponse(res, 200, { ok: true, rules: this.customRules, whitelistPaths: this.whitelistPaths });
|
|
309
|
+
},
|
|
310
|
+
});
|
|
311
|
+
const updateRulesRoute = () =>
|
|
312
|
+
ctx.webServer.register({
|
|
313
|
+
path: `${prefix}/rules`,
|
|
314
|
+
method: "POST",
|
|
315
|
+
handler: async (req, res) => {
|
|
316
|
+
try {
|
|
317
|
+
const body = await parseBody(req);
|
|
318
|
+
if (Array.isArray(body.rules)) this.customRules = body.rules;
|
|
319
|
+
if (Array.isArray(body.whitelistPaths)) this.whitelistPaths = body.whitelistPaths;
|
|
320
|
+
jsonResponse(res, 200, { ok: true, rules: this.customRules, whitelistPaths: this.whitelistPaths });
|
|
236
321
|
} catch (err) {
|
|
237
322
|
jsonResponse(res, 500, { ok: false, error: String(err && err.message || err) });
|
|
238
323
|
}
|
|
239
324
|
},
|
|
240
325
|
});
|
|
241
326
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
ctx.effect(
|
|
245
|
-
|
|
246
|
-
} else {
|
|
247
|
-
timelineRoute();
|
|
248
|
-
turnCardRoute();
|
|
249
|
-
diffRoute();
|
|
327
|
+
const routes = [timelineRoute, turnCardRoute, diffRoute, learnedRulesRoute, revokeLearnedRoute, modeRoute, rulesRoute, updateRulesRoute];
|
|
328
|
+
for (const r of routes) {
|
|
329
|
+
if (typeof ctx.effect === "function") ctx.effect(r, `dsh-approval-gate: ${r.name}`);
|
|
330
|
+
else r();
|
|
250
331
|
}
|
|
251
332
|
}
|
|
252
333
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-approval-gate",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.28",
|
|
4
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
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|