dsh-codex-approval 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -1
- package/index.js +126 -7
- package/judge.js +22 -5
- package/modes.js +62 -0
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -34,7 +34,29 @@ approval/request 到达(toolName + callId + reason)
|
|
|
34
34
|
└─ 4. 兜底:fallback(默认 ask → GUI 弹窗)
|
|
35
35
|
```
|
|
36
36
|
|
|
37
|
-
每次决策写入一行 JSONL 审计日志(默认 `~/.dsh/logs/approval.jsonl`):工具名、命令预览、reason、判定来源(rule / ai / ai-error / fallback
|
|
37
|
+
每次决策写入一行 JSONL 审计日志(默认 `~/.dsh/logs/approval.jsonl`):工具名、命令预览、reason、判定来源(rule / ai / ai-error / fallback)、**模式(mode)**、风险、AI 理由、耗时。
|
|
38
|
+
|
|
39
|
+
## 审批模式(v0.2.0)
|
|
40
|
+
|
|
41
|
+
插件提供一个与 dsh 沙箱模式**正交**的审批模式维度,三种模式按需切换:
|
|
42
|
+
|
|
43
|
+
| 模式 | 名称 | 行为 | 场景 |
|
|
44
|
+
|---|---|---|---|
|
|
45
|
+
| 1 | `manual` | **完全旁路**:不决策、不写日志,审批全部交回人类弹窗 | 回归未装插件的原生体验 |
|
|
46
|
+
| 2 | `ai`(默认) | 规则 → AI → ask 交人类 | 日常:低风险自动、高风险问人 |
|
|
47
|
+
| 3 | `ai-auto` | 规则 → AI → **ask 永不交人类**,按 `mode3OnAsk`(默认 deny)处理 | 全自动操作但又不放心 full access:AI 全权把关,绝不弹窗 |
|
|
48
|
+
|
|
49
|
+
**运行时切换**(GUI 斜杠命令,作用于当前会话,持久化到 settings):
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
/approval-mode 显示当前模式(覆盖值 + 生效值)
|
|
53
|
+
/approval-mode 3 切换为 ai-auto(也接受 ai-auto / 1 / 2 / manual 等)
|
|
54
|
+
/approval-mode default 清除会话覆盖,回落到配置默认
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
**ai-auto 下 ask 的归宿**(`mode3OnAsk`,默认 `deny`):规则 ask、AI 判 ask 且超容忍度、AI 故障 failOpen=ask、兜底 fallback=ask——全部按此处理,绝不弹窗。⚠️ 若设为 `allow`,AI 无法决定时也会放行高风险操作,**慎用**。
|
|
58
|
+
|
|
59
|
+
**模式持久化**:会话覆盖存 `~/.dsh/settings.yaml` 的 `dsh-codex-approval` 命名空间(settings 服务不可用时降级为纯内存,重启丢失)。默认模式由配置 `mode` 字段决定。
|
|
38
60
|
|
|
39
61
|
## 安装
|
|
40
62
|
|
|
@@ -50,6 +72,8 @@ dsh plugin --profile web add dsh-codex-approval
|
|
|
50
72
|
```yaml
|
|
51
73
|
- id: dsh-codex-approval
|
|
52
74
|
config:
|
|
75
|
+
mode: ai # manual | ai | ai-auto(默认 ai)
|
|
76
|
+
mode3OnAsk: deny # deny | allow(ai-auto 下 ask 的归宿;默认 deny 安全)
|
|
53
77
|
rules:
|
|
54
78
|
- match: 'Bash(git status*)' # 命中即自动通过(Codex approve-always)
|
|
55
79
|
action: allow
|
package/index.js
CHANGED
|
@@ -24,10 +24,12 @@ import { appendFile, mkdir } from "node:fs/promises";
|
|
|
24
24
|
import { mkdirSync } from "node:fs";
|
|
25
25
|
import { homedir } from "node:os";
|
|
26
26
|
import { join, dirname } from "node:path";
|
|
27
|
+
import z from "@deepseek-ai/schemastery";
|
|
27
28
|
|
|
28
29
|
import { evaluateRules } from "./rules.js";
|
|
29
30
|
import { findToolCallArgs, argsPreview } from "./enrich.js";
|
|
30
31
|
import { judgeWith, decideAuthorization } from "./judge.js";
|
|
32
|
+
import { MODES, parseMode, resolveMode, effectiveOnAsk } from "./modes.js";
|
|
31
33
|
|
|
32
34
|
export const name = "dsh-codex-approval";
|
|
33
35
|
|
|
@@ -43,6 +45,8 @@ export const inject = ["approval", "llm"];
|
|
|
43
45
|
/** Default configuration — tune via the profile patch id-targeted config. */
|
|
44
46
|
export const DEFAULT_CONFIG = {
|
|
45
47
|
enabled: true,
|
|
48
|
+
mode: "ai",
|
|
49
|
+
mode3OnAsk: "deny",
|
|
46
50
|
rules: [
|
|
47
51
|
// read-only / harmless commands: auto-approve
|
|
48
52
|
{ match: "Bash(git status*)", action: "allow" },
|
|
@@ -86,6 +90,8 @@ const TOLERANCES = ["low", "medium", "high"];
|
|
|
86
90
|
function assertConfig(cfg) {
|
|
87
91
|
if (typeof cfg !== "object" || cfg === null) throw new TypeError("dsh-codex-approval: config must be an object");
|
|
88
92
|
if (typeof cfg.enabled !== "boolean") throw new TypeError("dsh-codex-approval: config.enabled must be a boolean");
|
|
93
|
+
if (!MODES.includes(cfg.mode)) throw new TypeError(`dsh-codex-approval: config.mode must be one of ${MODES.join("/")}`);
|
|
94
|
+
if (!["deny", "allow"].includes(cfg.mode3OnAsk)) throw new TypeError("dsh-codex-approval: config.mode3OnAsk must be deny/allow");
|
|
89
95
|
if (!Array.isArray(cfg.rules)) throw new TypeError("dsh-codex-approval: config.rules must be an array");
|
|
90
96
|
for (const rule of cfg.rules) {
|
|
91
97
|
if (typeof rule.match !== "string" || rule.match === "") throw new TypeError("dsh-codex-approval: each rule needs a non-empty match");
|
|
@@ -141,16 +147,23 @@ export function makeLlmRunner(llm, { provider, model, timeoutMs, maxTokens }) {
|
|
|
141
147
|
/**
|
|
142
148
|
* Create the approval/request handler with injected dependencies
|
|
143
149
|
* (unit-testable without a cordis ctx).
|
|
144
|
-
* @param deps - { config, record, llmRunner }
|
|
150
|
+
* @param deps - { config, record, llmRunner, getSessionMode }
|
|
145
151
|
* @returns async (req, next) => ApprovalOutcome
|
|
146
152
|
*/
|
|
147
|
-
export function createHandler({ config, record, llmRunner }) {
|
|
153
|
+
export function createHandler({ config, record, llmRunner, getSessionMode }) {
|
|
148
154
|
const cfg = config;
|
|
149
155
|
return async (req, next) => {
|
|
150
156
|
const started = Date.now();
|
|
151
157
|
if (req.signal?.aborted === true) return "cancelled";
|
|
152
158
|
if (!cfg.enabled) return next();
|
|
153
159
|
|
|
160
|
+
const sessionId = req.agent?.session?.id ?? req.agent?.id;
|
|
161
|
+
const override = await getSessionMode?.(sessionId);
|
|
162
|
+
const mode = resolveMode(override, cfg.mode);
|
|
163
|
+
|
|
164
|
+
// mode 1: fully bypassed — the pre-plugin experience (no decision, no audit)
|
|
165
|
+
if (mode === "manual") return next();
|
|
166
|
+
|
|
154
167
|
const args = findToolCallArgs(req.agent?.session?.events, req.callId);
|
|
155
168
|
const argsText = argsPreview(args, req.toolName, cfg.ai.maxPromptChars);
|
|
156
169
|
const matchReq = { toolName: req.toolName, argsText, reason: req.reason ?? "" };
|
|
@@ -162,7 +175,8 @@ export function createHandler({ config, record, llmRunner }) {
|
|
|
162
175
|
} else if (cfg.ai.enabled) {
|
|
163
176
|
const judged = await judgeWith({
|
|
164
177
|
runner: llmRunner,
|
|
165
|
-
input: { toolName: req.toolName, argsText, reason: req.reason ?? "" }
|
|
178
|
+
input: { toolName: req.toolName, argsText, reason: req.reason ?? "" },
|
|
179
|
+
allowAsk: mode !== "ai-auto"
|
|
166
180
|
});
|
|
167
181
|
if (judged.ok) {
|
|
168
182
|
const authorization = decideAuthorization(judged.verdict, cfg.ai.riskTolerance);
|
|
@@ -186,9 +200,18 @@ export function createHandler({ config, record, llmRunner }) {
|
|
|
186
200
|
verdict = { kind: "fallback", action: cfg.fallback, outcome: outcomeFor(cfg.fallback) };
|
|
187
201
|
}
|
|
188
202
|
|
|
203
|
+
// mode 3 (ai-auto): an "ask" is never routed to a human — resolve it
|
|
204
|
+
// through mode3OnAsk (default deny), regardless of its source
|
|
205
|
+
// (rule ask, AI ask over tolerance, failOpen=ask, fallback=ask).
|
|
206
|
+
if (mode === "ai-auto" && verdict.action === "ask") {
|
|
207
|
+
const resolved = effectiveOnAsk(mode, cfg.mode3OnAsk);
|
|
208
|
+
verdict = { ...verdict, action: resolved, outcome: outcomeFor(resolved), viaAskResolution: true };
|
|
209
|
+
}
|
|
210
|
+
|
|
189
211
|
await record({
|
|
190
212
|
ts: new Date().toISOString(),
|
|
191
|
-
sessionId:
|
|
213
|
+
sessionId: sessionId ?? "?",
|
|
214
|
+
mode,
|
|
192
215
|
toolName: req.toolName,
|
|
193
216
|
callId: req.callId,
|
|
194
217
|
argsPreview: argsText.slice(0, 300),
|
|
@@ -217,12 +240,106 @@ export function makeRecorder(logFile) {
|
|
|
217
240
|
};
|
|
218
241
|
}
|
|
219
242
|
|
|
243
|
+
/**
|
|
244
|
+
* Per-session approval-mode store. Persists through the dsh settings service
|
|
245
|
+
* under the `dsh-codex-approval` namespace when available; falls back to
|
|
246
|
+
* memory only (survives nothing) otherwise. All writes go through `replace`
|
|
247
|
+
* so the whole `sessionOverrides` map stays authoritative in one place.
|
|
248
|
+
*/
|
|
249
|
+
export function makeModeStore(ctx, logger) {
|
|
250
|
+
const memory = new Map();
|
|
251
|
+
let settings = null;
|
|
252
|
+
ctx.inject(["settings"], (sctx) => {
|
|
253
|
+
settings = sctx.settings;
|
|
254
|
+
try {
|
|
255
|
+
sctx.settings.register("dsh-codex-approval", z.object({
|
|
256
|
+
sessionOverrides: z.dict(z.union(MODES)).default({})
|
|
257
|
+
}), { base: {} });
|
|
258
|
+
const resolved = sctx.settings.get("dsh-codex-approval");
|
|
259
|
+
const overrides = resolved?.sessionOverrides;
|
|
260
|
+
if (overrides !== null && typeof overrides === "object") {
|
|
261
|
+
for (const [key, value] of Object.entries(overrides)) memory.set(key, value);
|
|
262
|
+
}
|
|
263
|
+
} catch (error) {
|
|
264
|
+
logger?.warn?.("[dsh-codex-approval] settings init failed (%s) — session overrides are memory-only", String(error?.message ?? error));
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
const persist = async () => {
|
|
268
|
+
if (settings === null) return "memory-only";
|
|
269
|
+
try {
|
|
270
|
+
const next = {};
|
|
271
|
+
for (const [key, value] of memory) next[key] = value;
|
|
272
|
+
await settings.replace("dsh-codex-approval", { sessionOverrides: next });
|
|
273
|
+
return "persisted";
|
|
274
|
+
} catch {
|
|
275
|
+
return "memory-only";
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
return {
|
|
279
|
+
async get(sessionId) {
|
|
280
|
+
if (sessionId === undefined || sessionId === null) return undefined;
|
|
281
|
+
return memory.get(sessionId);
|
|
282
|
+
},
|
|
283
|
+
async set(sessionId, mode) {
|
|
284
|
+
if (sessionId === undefined || sessionId === null) return "memory-only";
|
|
285
|
+
memory.set(sessionId, mode);
|
|
286
|
+
return persist();
|
|
287
|
+
},
|
|
288
|
+
async clear(sessionId) {
|
|
289
|
+
if (sessionId !== undefined && sessionId !== null) memory.delete(sessionId);
|
|
290
|
+
return persist();
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/** Register the /approval-mode command (mirrors dsh-plan-mode's /plan). */
|
|
296
|
+
export function registerModeCommand(ctx, cfg, store) {
|
|
297
|
+
ctx.inject(["commands"], (commandCtx) => {
|
|
298
|
+
commandCtx.commands.register({
|
|
299
|
+
name: "approval-mode",
|
|
300
|
+
description: "Show or switch the approval mode (manual | ai | ai-auto, or 1/2/3)",
|
|
301
|
+
input: { hint: "[manual|ai|ai-auto|default]" },
|
|
302
|
+
handler: async ({ agent, rawInput }) => {
|
|
303
|
+
const sessionId = agent?.session?.id ?? agent?.id;
|
|
304
|
+
const input = rawInput.trim();
|
|
305
|
+
if (input === "") {
|
|
306
|
+
const override = await store.get(sessionId);
|
|
307
|
+
const effective = resolveMode(override, cfg.mode);
|
|
308
|
+
const base = override === void 0
|
|
309
|
+
? `approval mode: ${effective} (config default: ${cfg.mode}, no session override)`
|
|
310
|
+
: `approval mode: ${effective} (session override: ${override}, config default: ${cfg.mode})`;
|
|
311
|
+
return { kind: "success", text: base };
|
|
312
|
+
}
|
|
313
|
+
if (input === "default" || input === "off" || input === "reset") {
|
|
314
|
+
const persisted = await store.clear(sessionId);
|
|
315
|
+
const note = persisted === "persisted" ? "" : " (memory-only: settings unavailable)";
|
|
316
|
+
return { kind: "success", text: `approval mode: session override cleared — effective ${cfg.mode} (config default)${note}` };
|
|
317
|
+
}
|
|
318
|
+
const mode = parseMode(input);
|
|
319
|
+
if (mode === null) {
|
|
320
|
+
return { kind: "success", text: `unknown approval mode "${input}" — use manual | ai | ai-auto (or 1/2/3), or "default" to clear the override` };
|
|
321
|
+
}
|
|
322
|
+
const persisted = await store.set(sessionId, mode);
|
|
323
|
+
const note = persisted === "persisted" ? "" : " (memory-only: settings unavailable, lost on restart)";
|
|
324
|
+
return { kind: "success", text: `approval mode → ${mode} for this session${note}` };
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
|
|
220
330
|
/** Cordis plugin entry: register the answerer when approval is composed. */
|
|
221
331
|
export async function apply(ctx, userConfig) {
|
|
222
332
|
const cfg = normalizeConfig(userConfig);
|
|
333
|
+
const store = makeModeStore(ctx, ctx.logger);
|
|
223
334
|
const llmRunner = makeLlmRunner(ctx.llm, cfg.ai);
|
|
224
|
-
const handler = createHandler({
|
|
335
|
+
const handler = createHandler({
|
|
336
|
+
config: cfg,
|
|
337
|
+
record: makeRecorder(cfg.logFile),
|
|
338
|
+
llmRunner,
|
|
339
|
+
getSessionMode: (sessionId) => store.get(sessionId)
|
|
340
|
+
});
|
|
225
341
|
ctx.on("approval/request", handler);
|
|
342
|
+
registerModeCommand(ctx, cfg, store);
|
|
226
343
|
// Self-proving startup record: this line in the log after a restart proves
|
|
227
344
|
// the plugin loaded (decision records follow it). Awaited so a boot that
|
|
228
345
|
// cannot even write its own log fails loud instead of silently degrading.
|
|
@@ -230,11 +347,13 @@ export async function apply(ctx, userConfig) {
|
|
|
230
347
|
ts: new Date().toISOString(),
|
|
231
348
|
event: "plugin-loaded",
|
|
232
349
|
sessionId: "boot",
|
|
350
|
+
mode: cfg.mode,
|
|
351
|
+
mode3OnAsk: cfg.mode3OnAsk,
|
|
233
352
|
rules: cfg.rules.length,
|
|
234
353
|
ai: cfg.ai.enabled,
|
|
235
354
|
tolerance: cfg.ai.riskTolerance,
|
|
236
355
|
fallback: cfg.fallback
|
|
237
356
|
});
|
|
238
|
-
ctx.logger?.info?.("[dsh-codex-approval] answerer registered — rules=%d ai=%s tolerance=%s log=%s",
|
|
239
|
-
cfg.rules.length, cfg.ai.enabled ? "on" : "off", cfg.ai.riskTolerance, cfg.logFile);
|
|
357
|
+
ctx.logger?.info?.("[dsh-codex-approval] answerer registered — mode=%s rules=%d ai=%s tolerance=%s log=%s",
|
|
358
|
+
cfg.mode, cfg.rules.length, cfg.ai.enabled ? "on" : "off", cfg.ai.riskTolerance, cfg.logFile);
|
|
240
359
|
}
|
package/judge.js
CHANGED
|
@@ -28,16 +28,33 @@ Rules of thumb:
|
|
|
28
28
|
Reply with ONLY one JSON object, no prose, no markdown fences:
|
|
29
29
|
{"risk":"low|medium|high","authorization":"allow|ask|deny","reason":"one short sentence"}`;
|
|
30
30
|
|
|
31
|
-
/**
|
|
32
|
-
|
|
31
|
+
/** Variant used in ai-auto mode: the judge must decide itself, no human is available. */
|
|
32
|
+
const SYSTEM_PROMPT_NO_ASK = SYSTEM_PROMPT.replace(
|
|
33
|
+
'2. authorization: "allow" (proceed without asking) | "ask" (a human must confirm) | "deny" (must not run).',
|
|
34
|
+
'2. authorization: "allow" (proceed without asking) | "deny" (must not run). "ask" is NOT available — no human will review this request, you MUST decide between allow and deny yourself.'
|
|
35
|
+
).replace(
|
|
36
|
+
'- When uncertain, prefer "ask". Prefer "deny" for destructive or credential-exposing actions.',
|
|
37
|
+
'- When uncertain, prefer "deny". Prefer "deny" for destructive or credential-exposing actions.'
|
|
38
|
+
).replace(
|
|
39
|
+
'{"risk":"low|medium|high","authorization":"allow|ask|deny","reason":"one short sentence"}',
|
|
40
|
+
'{"risk":"low|medium|high","authorization":"allow|deny","reason":"one short sentence"}'
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Build the messages array for the judge call.
|
|
45
|
+
* @param allowAsk - when false (ai-auto mode), the prompt forbids "ask":
|
|
46
|
+
* the judge must commit to allow or deny.
|
|
47
|
+
*/
|
|
48
|
+
export function buildJudgeMessages({ toolName, argsText, reason }, { allowAsk = true } = {}) {
|
|
33
49
|
const user = JSON.stringify({
|
|
34
50
|
toolName,
|
|
35
51
|
command: argsText === "" ? null : argsText,
|
|
36
52
|
reason: reason ?? null
|
|
37
53
|
});
|
|
54
|
+
const system = allowAsk ? SYSTEM_PROMPT : SYSTEM_PROMPT_NO_ASK;
|
|
38
55
|
return [{
|
|
39
56
|
role: "user",
|
|
40
|
-
content: [{ type: "text", text: `${
|
|
57
|
+
content: [{ type: "text", text: `${system}\n\n${user}` }]
|
|
41
58
|
}];
|
|
42
59
|
}
|
|
43
60
|
|
|
@@ -128,8 +145,8 @@ export function decideAuthorization(verdict, tolerance) {
|
|
|
128
145
|
* @param config - { maxPromptChars } (unused here; kept for symmetry)
|
|
129
146
|
* @returns { ok: true, verdict } | { ok: false, error }
|
|
130
147
|
*/
|
|
131
|
-
export async function judgeWith({ runner, input, signal }) {
|
|
132
|
-
const messages = buildJudgeMessages(input);
|
|
148
|
+
export async function judgeWith({ runner, input, signal, allowAsk = true }) {
|
|
149
|
+
const messages = buildJudgeMessages(input, { allowAsk });
|
|
133
150
|
let result;
|
|
134
151
|
try {
|
|
135
152
|
result = await runner(messages, { signal });
|
package/modes.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-codex-approval — modes.js
|
|
3
|
+
*
|
|
4
|
+
* The approval-mode dimension, orthogonal to the dsh sandbox mode:
|
|
5
|
+
*
|
|
6
|
+
* manual — plugin fully bypassed (next() straight through, no decision,
|
|
7
|
+
* no audit): the pre-plugin experience.
|
|
8
|
+
* ai — rules, then AI judge, then human fallback for every "ask"
|
|
9
|
+
* outcome (the default / v0.1.x behavior).
|
|
10
|
+
* ai-auto — rules, then AI judge; "ask" is never routed to a human —
|
|
11
|
+
* it resolves through mode3OnAsk (default deny).
|
|
12
|
+
*
|
|
13
|
+
* Pure functions only: parse/validate mode names, resolve the effective mode
|
|
14
|
+
* (per-session override wins over the config default), and map an "ask"
|
|
15
|
+
* outcome onto its effective action under the active mode.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/** The three approval modes. */
|
|
19
|
+
export const MODES = ["manual", "ai", "ai-auto"];
|
|
20
|
+
/** Numeric aliases mirroring the user-facing 1/2/3 choice. */
|
|
21
|
+
export const MODE_ALIASES = { "1": "manual", "2": "ai", "3": "ai-auto" };
|
|
22
|
+
/** Actions an "ask" may resolve to. */
|
|
23
|
+
export const ASK_ACTIONS = ["ask", "deny", "allow"];
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Parse and validate a mode name (or numeric alias).
|
|
27
|
+
* @param input - "manual" | "ai" | "ai-auto" | "1" | "2" | "3"
|
|
28
|
+
* @returns the canonical mode name, or null when invalid.
|
|
29
|
+
*/
|
|
30
|
+
export function parseMode(input) {
|
|
31
|
+
if (typeof input !== "string") return null;
|
|
32
|
+
const trimmed = input.trim().toLowerCase();
|
|
33
|
+
if (MODES.includes(trimmed)) return trimmed;
|
|
34
|
+
if (MODE_ALIASES[trimmed] !== void 0) return MODE_ALIASES[trimmed];
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Resolve the effective mode for one request: per-session override wins,
|
|
40
|
+
* else the config default.
|
|
41
|
+
* @param sessionOverride - mode from the per-session store (or undefined)
|
|
42
|
+
* @param configDefault - the configured default mode
|
|
43
|
+
* @returns a canonical mode name (never null when configDefault is valid).
|
|
44
|
+
*/
|
|
45
|
+
export function resolveMode(sessionOverride, configDefault) {
|
|
46
|
+
return parseMode(sessionOverride) ?? parseMode(configDefault) ?? "ai";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Map an "ask" outcome onto its effective action under the active mode.
|
|
51
|
+
* - manual: unreachable (handler bypasses); defensive "ask".
|
|
52
|
+
* - ai: "ask" — route to the human (next()).
|
|
53
|
+
* - ai-auto: mode3OnAsk — the human is never asked; default deny.
|
|
54
|
+
* @param mode - effective mode
|
|
55
|
+
* @param mode3OnAsk - "deny" | "allow" (validated config; anything else
|
|
56
|
+
* falls back to "deny")
|
|
57
|
+
* @returns "ask" | "deny" | "allow"
|
|
58
|
+
*/
|
|
59
|
+
export function effectiveOnAsk(mode, mode3OnAsk) {
|
|
60
|
+
if (mode === "ai-auto") return mode3OnAsk === "allow" ? "allow" : "deny";
|
|
61
|
+
return "ask";
|
|
62
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-codex-approval",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Codex-style approval autopilot for DeepSeek Harness: ordered glob rules (allow/ask/deny) plus an AI risk judge (low/medium/high) mapped through a risk tolerance, as an approval answerer.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
"rules.js",
|
|
10
10
|
"enrich.js",
|
|
11
11
|
"judge.js",
|
|
12
|
+
"modes.js",
|
|
12
13
|
"cordis.patch.yml",
|
|
13
14
|
"README.md",
|
|
14
15
|
"LICENSE"
|
|
@@ -33,5 +34,8 @@
|
|
|
33
34
|
},
|
|
34
35
|
"engines": {
|
|
35
36
|
"node": ">=22.19"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@deepseek-ai/schemastery": "^3.18.1"
|
|
36
40
|
}
|
|
37
41
|
}
|