@goodandready/dsh-approval-gate 0.1.26 → 0.1.27

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.
@@ -1,5 +1,5 @@
1
1
  // lib/timeline-service.js
2
- // Session Audit Timeline and WebServer REST Service for DSH Approval Gate (#82, #60, #74, #86).
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
- // Route 1: GET /dsh-approval-gate/timeline
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
- // Route 2: GET /dsh-approval-gate/turn-card
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
- // Route 3: POST /dsh-approval-gate/diff
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
- let bodyStr = "";
218
- req.on("data", (chunk) => { bodyStr += chunk; });
219
- req.on("end", () => {
220
- try {
221
- const parsed = JSON.parse(bodyStr || "{}");
222
- const oldText = parsed.oldText || "";
223
- const newText = parsed.newText || "";
224
- const contextLines = typeof parsed.contextLines === "number" ? parsed.contextLines : 3;
225
- const diffLines = generateDiffLines(oldText, newText);
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
- if (typeof ctx.effect === "function") {
243
- ctx.effect(timelineRoute, "dsh-approval-gate: timeline route");
244
- ctx.effect(turnCardRoute, "dsh-approval-gate: turn-card route");
245
- ctx.effect(diffRoute, "dsh-approval-gate: diff route");
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.26",
3
+ "version": "0.1.27",
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",