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.
Files changed (4) hide show
  1. package/README.md +13 -0
  2. package/i18n.js +57 -0
  3. package/index.js +44 -14
  4. 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.record(z.string(), z.enum(MODES)).default({})
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: "Show or switch the approval mode (manual | ai | ai-auto, or 1/2/3)",
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 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 };
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 note = persisted === "persisted" ? "" : " (memory-only: settings unavailable)";
316
- return { kind: "success", text: `approval mode: session override cleared — effective ${cfg.mode} (config default)${note}` };
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: `unknown approval mode "${input}" — use manual | ai | ai-auto (or 1/2/3), or "default" to clear the override` };
331
+ return { kind: "success", text: t.unknown(input) };
321
332
  }
322
333
  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}` };
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
- registerModeCommand(ctx, cfg, store);
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.0",
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",