dsh-codex-approval 0.2.0 → 0.2.2
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 +13 -0
- package/i18n.js +57 -0
- package/index.js +44 -14
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -58,6 +58,18 @@ approval/request 到达(toolName + callId + reason)
|
|
|
58
58
|
|
|
59
59
|
**模式持久化**:会话覆盖存 `~/.dsh/settings.yaml` 的 `dsh-codex-approval` 命名空间(settings 服务不可用时降级为纯内存,重启丢失)。默认模式由配置 `mode` 字段决定。
|
|
60
60
|
|
|
61
|
+
**与 dsh 沙箱模式的关系**:
|
|
62
|
+
|
|
63
|
+
| 沙箱模式 | AI 审核是否生效 |
|
|
64
|
+
|---|---|
|
|
65
|
+
| `read-only` | ✅ 生效——沙箱拒绝写操作,模型可申请升级(`WIDER_MODES` 允许),升级请求照常走审批链 |
|
|
66
|
+
| `workspace-write` | ✅ 生效(推荐组合:工作区内自由,越界 AI 把关) |
|
|
67
|
+
| `danger-full-access` | ⏸ 不触发——沙箱从不拒绝任何操作,没有升级请求,插件自然空闲 |
|
|
68
|
+
|
|
69
|
+
**命令多语言**:`/approval-mode` 的返回文案跟随 dsh 设置的语言(`locale.preference`,中/英)。命令 `description` 在启动时按当时语言注册,运行中切换语言后需重启才更新 description(返回文本每次实时跟随)。
|
|
70
|
+
|
|
71
|
+
**npm publish 默认 ask**:内置规则 `Bash(npm publish*) → ask`——agent 执行 `npm publish` 的升级请求**必定弹窗询问人类**,AI 无权自动放行(`ai` 模式下弹窗;`ai-auto` 模式下按 `mode3OnAsk` 处理,默认拒绝)。`npm unpublish` 无规则,由 AI 默认判定(通常判 high 直接拒绝)。
|
|
72
|
+
|
|
61
73
|
## 安装
|
|
62
74
|
|
|
63
75
|
```bash
|
|
@@ -74,6 +86,7 @@ dsh plugin --profile web add dsh-codex-approval
|
|
|
74
86
|
config:
|
|
75
87
|
mode: ai # manual | ai | ai-auto(默认 ai)
|
|
76
88
|
mode3OnAsk: deny # deny | allow(ai-auto 下 ask 的归宿;默认 deny 安全)
|
|
89
|
+
locale: auto # auto | zh | en(命令文案语言;auto=跟随 dsh 设置的语言偏好)
|
|
77
90
|
rules:
|
|
78
91
|
- match: 'Bash(git status*)' # 命中即自动通过(Codex approve-always)
|
|
79
92
|
action: allow
|
package/i18n.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-codex-approval — i18n.js
|
|
3
|
+
*
|
|
4
|
+
* zh/en copy for the /approval-mode command. The host reads the user's
|
|
5
|
+
* locale preference from the dsh settings service (`locale.preference`,
|
|
6
|
+
* owned by dsh-client-locale, persisted in settings.yaml); without a value
|
|
7
|
+
* (or without settings at all) the copy falls back to English.
|
|
8
|
+
*
|
|
9
|
+
* Pure functions only: pick the locale, render command texts.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export const LOCALES = ["zh", "en"];
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The full message table. Keys are identical across locales so a missing
|
|
16
|
+
* translation fails loudly in tests rather than silently at runtime.
|
|
17
|
+
*/
|
|
18
|
+
export const T = {
|
|
19
|
+
zh: {
|
|
20
|
+
showWithOverride: (effective, override, configDefault) =>
|
|
21
|
+
`当前模式:${effective}(会话覆盖:${override},配置默认:${configDefault})`,
|
|
22
|
+
showNoOverride: (effective, configDefault) =>
|
|
23
|
+
`当前模式:${effective}(配置默认:${configDefault},无会话覆盖)`,
|
|
24
|
+
cleared: (configDefault) => `已清除会话覆盖 → 回落 ${configDefault}(配置默认)`,
|
|
25
|
+
clearedMemoryOnly: (configDefault) => `已清除会话覆盖 → 回落 ${configDefault}(配置默认;settings 不可用,仅内存)`,
|
|
26
|
+
switched: (mode) => `已切换 → ${mode}(本会话)`,
|
|
27
|
+
switchedMemoryOnly: (mode) => `已切换 → ${mode}(本会话;settings 不可用,重启后丢失)`,
|
|
28
|
+
unknown: (input) => `未知模式 "${input}" — 用 manual | ai | ai-auto(或 1/2/3),或用 default 清除覆盖`
|
|
29
|
+
},
|
|
30
|
+
en: {
|
|
31
|
+
showWithOverride: (effective, override, configDefault) =>
|
|
32
|
+
`mode: ${effective} (session override: ${override}, config default: ${configDefault})`,
|
|
33
|
+
showNoOverride: (effective, configDefault) =>
|
|
34
|
+
`mode: ${effective} (config default: ${configDefault}, no session override)`,
|
|
35
|
+
cleared: (configDefault) => `override cleared → ${configDefault} (config default)`,
|
|
36
|
+
clearedMemoryOnly: (configDefault) => `override cleared → ${configDefault} (config default; memory-only: settings unavailable)`,
|
|
37
|
+
switched: (mode) => `switched → ${mode} (this session)`,
|
|
38
|
+
switchedMemoryOnly: (mode) => `switched → ${mode} (this session; memory-only: settings unavailable, lost on restart)`,
|
|
39
|
+
unknown: (input) => `unknown mode "${input}" — use manual | ai | ai-auto (or 1/2/3), or "default" to clear the override`
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Pick the command copy locale from a raw preference value.
|
|
45
|
+
* @param pref - settings `locale.preference` (e.g. "zh", "en", or undefined)
|
|
46
|
+
* @returns "zh" | "en" — valid values pass through; anything else → "en"
|
|
47
|
+
*/
|
|
48
|
+
export function pickLocale(pref) {
|
|
49
|
+
return pref === "zh" ? "zh" : "en";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** The command description for the given locale (registered once at boot). */
|
|
53
|
+
export function commandDescription(locale) {
|
|
54
|
+
return locale === "zh"
|
|
55
|
+
? "显示或切换审批模式(manual | ai | ai-auto,或 1/2/3)"
|
|
56
|
+
: "Show or switch the approval mode (manual | ai | ai-auto, or 1/2/3)";
|
|
57
|
+
}
|
package/index.js
CHANGED
|
@@ -30,6 +30,7 @@ import { evaluateRules } from "./rules.js";
|
|
|
30
30
|
import { findToolCallArgs, argsPreview } from "./enrich.js";
|
|
31
31
|
import { judgeWith, decideAuthorization } from "./judge.js";
|
|
32
32
|
import { MODES, parseMode, resolveMode, effectiveOnAsk } from "./modes.js";
|
|
33
|
+
import { T, pickLocale, commandDescription } from "./i18n.js";
|
|
33
34
|
|
|
34
35
|
export const name = "dsh-codex-approval";
|
|
35
36
|
|
|
@@ -47,6 +48,7 @@ export const DEFAULT_CONFIG = {
|
|
|
47
48
|
enabled: true,
|
|
48
49
|
mode: "ai",
|
|
49
50
|
mode3OnAsk: "deny",
|
|
51
|
+
locale: "auto",
|
|
50
52
|
rules: [
|
|
51
53
|
// read-only / harmless commands: auto-approve
|
|
52
54
|
{ match: "Bash(git status*)", action: "allow" },
|
|
@@ -68,7 +70,11 @@ export const DEFAULT_CONFIG = {
|
|
|
68
70
|
{ match: "reason:*secret*", action: "ask" },
|
|
69
71
|
{ match: "reason:*password*", action: "ask" },
|
|
70
72
|
{ match: "reason:*credential*", action: "ask" },
|
|
71
|
-
{ match: "reason:*token*", action: "ask" }
|
|
73
|
+
{ match: "reason:*token*", action: "ask" },
|
|
74
|
+
// publishing: never auto-decided — a human must confirm every publish
|
|
75
|
+
// (both bare `npm publish` and prefixed forms like `cd x && npm publish`)
|
|
76
|
+
{ match: "Bash(npm publish*)", action: "ask" },
|
|
77
|
+
{ match: "Bash(*npm publish*)", action: "ask" }
|
|
72
78
|
],
|
|
73
79
|
ai: {
|
|
74
80
|
enabled: true,
|
|
@@ -92,6 +98,7 @@ function assertConfig(cfg) {
|
|
|
92
98
|
if (typeof cfg.enabled !== "boolean") throw new TypeError("dsh-codex-approval: config.enabled must be a boolean");
|
|
93
99
|
if (!MODES.includes(cfg.mode)) throw new TypeError(`dsh-codex-approval: config.mode must be one of ${MODES.join("/")}`);
|
|
94
100
|
if (!["deny", "allow"].includes(cfg.mode3OnAsk)) throw new TypeError("dsh-codex-approval: config.mode3OnAsk must be deny/allow");
|
|
101
|
+
if (!["auto", "zh", "en"].includes(cfg.locale)) throw new TypeError("dsh-codex-approval: config.locale must be auto/zh/en");
|
|
95
102
|
if (!Array.isArray(cfg.rules)) throw new TypeError("dsh-codex-approval: config.rules must be an array");
|
|
96
103
|
for (const rule of cfg.rules) {
|
|
97
104
|
if (typeof rule.match !== "string" || rule.match === "") throw new TypeError("dsh-codex-approval: each rule needs a non-empty match");
|
|
@@ -253,7 +260,7 @@ export function makeModeStore(ctx, logger) {
|
|
|
253
260
|
settings = sctx.settings;
|
|
254
261
|
try {
|
|
255
262
|
sctx.settings.register("dsh-codex-approval", z.object({
|
|
256
|
-
sessionOverrides: z.
|
|
263
|
+
sessionOverrides: z.dict(z.union(MODES)).default({})
|
|
257
264
|
}), { base: {} });
|
|
258
265
|
const resolved = sctx.settings.get("dsh-codex-approval");
|
|
259
266
|
const overrides = resolved?.sessionOverrides;
|
|
@@ -293,40 +300,62 @@ export function makeModeStore(ctx, logger) {
|
|
|
293
300
|
}
|
|
294
301
|
|
|
295
302
|
/** Register the /approval-mode command (mirrors dsh-plan-mode's /plan). */
|
|
296
|
-
export function registerModeCommand(ctx, cfg, store) {
|
|
303
|
+
export function registerModeCommand(ctx, cfg, store, getLocale) {
|
|
304
|
+
const locale = getLocale ? getLocale() : "en";
|
|
297
305
|
ctx.inject(["commands"], (commandCtx) => {
|
|
298
306
|
commandCtx.commands.register({
|
|
299
307
|
name: "approval-mode",
|
|
300
|
-
description:
|
|
308
|
+
description: commandDescription(locale),
|
|
301
309
|
input: { hint: "[manual|ai|ai-auto|default]" },
|
|
302
310
|
handler: async ({ agent, rawInput }) => {
|
|
311
|
+
const t = T[getLocale ? getLocale() : "en"];
|
|
303
312
|
const sessionId = agent?.session?.id ?? agent?.id;
|
|
304
313
|
const input = rawInput.trim();
|
|
305
314
|
if (input === "") {
|
|
306
315
|
const override = await store.get(sessionId);
|
|
307
316
|
const effective = resolveMode(override, cfg.mode);
|
|
308
|
-
const
|
|
309
|
-
?
|
|
310
|
-
:
|
|
311
|
-
return { kind: "success", text
|
|
317
|
+
const text = override === void 0
|
|
318
|
+
? t.showNoOverride(effective, cfg.mode)
|
|
319
|
+
: t.showWithOverride(effective, override, cfg.mode);
|
|
320
|
+
return { kind: "success", text };
|
|
312
321
|
}
|
|
313
322
|
if (input === "default" || input === "off" || input === "reset") {
|
|
314
323
|
const persisted = await store.clear(sessionId);
|
|
315
|
-
const
|
|
316
|
-
|
|
324
|
+
const text = persisted === "persisted"
|
|
325
|
+
? t.cleared(cfg.mode)
|
|
326
|
+
: t.clearedMemoryOnly(cfg.mode);
|
|
327
|
+
return { kind: "success", text };
|
|
317
328
|
}
|
|
318
329
|
const mode = parseMode(input);
|
|
319
330
|
if (mode === null) {
|
|
320
|
-
return { kind: "success", text:
|
|
331
|
+
return { kind: "success", text: t.unknown(input) };
|
|
321
332
|
}
|
|
322
333
|
const persisted = await store.set(sessionId, mode);
|
|
323
|
-
const
|
|
324
|
-
|
|
334
|
+
const text = persisted === "persisted"
|
|
335
|
+
? t.switched(mode)
|
|
336
|
+
: t.switchedMemoryOnly(mode);
|
|
337
|
+
return { kind: "success", text };
|
|
325
338
|
}
|
|
326
339
|
});
|
|
327
340
|
});
|
|
328
341
|
}
|
|
329
342
|
|
|
343
|
+
/**
|
|
344
|
+
* Build the command-copy locale resolver. `auto` follows the dsh settings
|
|
345
|
+
* preference (`locale.preference`, owned by dsh-client-locale); an explicit
|
|
346
|
+
* `zh`/`en` config wins. Without settings or preference → English.
|
|
347
|
+
*/
|
|
348
|
+
export function makeGetLocale(cfg, ctx) {
|
|
349
|
+
return () => {
|
|
350
|
+
if (cfg.locale === "zh" || cfg.locale === "en") return cfg.locale;
|
|
351
|
+
try {
|
|
352
|
+
return pickLocale(ctx.get("settings", false)?.get?.("locale")?.preference);
|
|
353
|
+
} catch {
|
|
354
|
+
return "en";
|
|
355
|
+
}
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
|
|
330
359
|
/** Cordis plugin entry: register the answerer when approval is composed. */
|
|
331
360
|
export async function apply(ctx, userConfig) {
|
|
332
361
|
const cfg = normalizeConfig(userConfig);
|
|
@@ -339,7 +368,8 @@ export async function apply(ctx, userConfig) {
|
|
|
339
368
|
getSessionMode: (sessionId) => store.get(sessionId)
|
|
340
369
|
});
|
|
341
370
|
ctx.on("approval/request", handler);
|
|
342
|
-
|
|
371
|
+
// Command copy follows config.locale ("auto" → dsh locale preference)
|
|
372
|
+
registerModeCommand(ctx, cfg, store, makeGetLocale(cfg, ctx));
|
|
343
373
|
// Self-proving startup record: this line in the log after a restart proves
|
|
344
374
|
// the plugin loaded (decision records follow it). Awaited so a boot that
|
|
345
375
|
// cannot even write its own log fails loud instead of silently degrading.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-codex-approval",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
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",
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
"index.js",
|
|
9
9
|
"rules.js",
|
|
10
10
|
"enrich.js",
|
|
11
|
+
"i18n.js",
|
|
11
12
|
"judge.js",
|
|
12
13
|
"modes.js",
|
|
13
14
|
"cordis.patch.yml",
|