@goodandready/dsh-approval-gate 0.1.21 → 0.1.23

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/lib/messages.js CHANGED
@@ -82,6 +82,12 @@ export const MESSAGES = Object.freeze({
82
82
  ruleAgentsMdProhibitedPath: "File modification prohibited by rules in AGENTS.md",
83
83
  askMajorVersionBump: "Package.json modification contains major dependency bump; review before allowing",
84
84
  ruleMajorVersionBump: "Unauthorized major dependency version bump",
85
+ askRedundantSandbox: "Redundant sandbox elevation requested for safe workspace operation; review or execute in sandboxed mode",
86
+ ruleRedundantSandbox: "Unnecessary sandbox elevation requested for workspace-internal task",
87
+ askMissingJustification: "Privilege escalation requires an explicit justification; review before allowing",
88
+ ruleMissingJustification: "System privilege escalation requested without justification",
89
+ askInsufficientJustification: "Privilege escalation justification is too brief or generic; review before allowing",
90
+ ruleInsufficientJustification: "Insufficient privilege escalation justification",
85
91
  hintRecursiveRm: "Specify exact target files instead of recursive deletion",
86
92
  hintPathTraversal: "Keep operations strictly within the project directory",
87
93
  hintSecretWrite: "Modify configuration via settings or environment variables, not raw file overwrite",
@@ -109,6 +115,9 @@ export const MESSAGES = Object.freeze({
109
115
  hintAgentsMdProhibited: "Review project-specific guidelines and constraints defined in AGENTS.md",
110
116
  hintMajorVersionBump: "Verify compatibility before bumping major dependency versions in package.json",
111
117
  hintUnmatchedAsk: "Unrecognized operations default to fail-closed approval under defaultVerdict='ask'",
118
+ hintRedundantSandbox: "Execute the command in standard sandbox mode without danger-full-access",
119
+ hintMissingJustification: "Provide a justification field with at least 10 non-placeholder characters",
120
+ hintInsufficientJustification: "Describe the specific operational reason for privilege escalation",
112
121
  },
113
122
  zh: {
114
123
  blockedPrefix: "dsh-approval-gate 规则已阻止",
@@ -190,6 +199,12 @@ export const MESSAGES = Object.freeze({
190
199
  ruleAgentsMdProhibitedPath: "文件修改被 AGENTS.md 中的规则明确禁止",
191
200
  askMajorVersionBump: "package.json 包含破坏性依赖主版本升级;请在允许前进行审查",
192
201
  ruleMajorVersionBump: "未授权的依赖包主版本升级",
202
+ askRedundantSandbox: "对安全的内部操作请求了冗余的沙箱提权;请在允许前审查或以标准沙箱模式执行",
203
+ ruleRedundantSandbox: "工作区内部任务请求了不必要的沙箱提权",
204
+ askMissingJustification: "特权提升需要明确的理由说明;请在允许前进行审查",
205
+ ruleMissingJustification: "未提供理由即请求系统提权",
206
+ askInsufficientJustification: "特权提升理由过短或为占位符;请在允许前进行审查",
207
+ ruleInsufficientJustification: "特权提升理由不充分",
193
208
  hintRecursiveRm: "请指定具体目标文件,避免递归删除",
194
209
  hintPathTraversal: "请将操作限制在项目根目录内",
195
210
  hintSecretWrite: "请通过设置或环境变量修改配置,不要直接覆盖敏感配置文件",
@@ -217,6 +232,9 @@ export const MESSAGES = Object.freeze({
217
232
  hintAgentsMdProhibited: "请核对项目根目录 AGENTS.md 中定义的安全约束和禁止指令",
218
233
  hintMajorVersionBump: "在升级 package.json 依赖主版本前请确认破坏性变更与兼容性",
219
234
  hintUnmatchedAsk: "在 defaultVerdict='ask' 模式下,未识别的操作默认需要人工审核确认",
235
+ hintRedundantSandbox: "在标准沙箱模式下重新执行该命令,无需申请 danger-full-access",
236
+ hintMissingJustification: "请提供至少10个字符的有效理由说明字段",
237
+ hintInsufficientJustification: "请详细说明提权操作的具体业务或技术原因",
220
238
  },
221
239
  });
222
240
 
@@ -291,6 +309,9 @@ const RULE_KEYS = Object.freeze({
291
309
  "agents-md-prohibited-command": "ruleAgentsMdProhibitedCommand",
292
310
  "agents-md-prohibited-path": "ruleAgentsMdProhibitedPath",
293
311
  "major-version-bump": "askMajorVersionBump",
312
+ "redundant-sandbox-privilege": "ruleRedundantSandbox",
313
+ "missing-escalation-justification": "ruleMissingJustification",
314
+ "insufficient-escalation-justification": "ruleInsufficientJustification",
294
315
  });
295
316
 
296
317
  const HINT_KEYS = Object.freeze({
@@ -322,6 +343,9 @@ const HINT_KEYS = Object.freeze({
322
343
  "agents-md-prohibited-path": "hintAgentsMdProhibited",
323
344
  "major-version-bump": "hintMajorVersionBump",
324
345
  "unmatched-ask": "hintUnmatchedAsk",
346
+ "redundant-sandbox-privilege": "hintRedundantSandbox",
347
+ "missing-escalation-justification": "hintMissingJustification",
348
+ "insufficient-escalation-justification": "hintInsufficientJustification",
325
349
  });
326
350
 
327
351
  const ASK_KEYS = Object.freeze({
@@ -329,6 +353,9 @@ const ASK_KEYS = Object.freeze({
329
353
  "unknown-format": "askUnknownFormat",
330
354
  "unknown-write-target": "askUnknownWriteTarget",
331
355
  "unmatched-ask": "askUnmatched",
356
+ "redundant-sandbox-privilege": "askRedundantSandbox",
357
+ "missing-escalation-justification": "askMissingJustification",
358
+ "insufficient-escalation-justification": "askInsufficientJustification",
332
359
  "dynamic-command": "askDynamicCommand",
333
360
  "dynamic-arguments": "askDynamicArguments",
334
361
  "dynamic-redirect": "askDynamicRedirect",
@@ -0,0 +1,146 @@
1
+ // lib/session-modes.js
2
+ // Session-level Gate Mode Switching (#61).
3
+ // Allows per-session mode switching between auto-approval, hybrid, manual, and strict modes.
4
+
5
+ export const GATE_MODES = Object.freeze({
6
+ AUTO: "auto-approval",
7
+ MANUAL: "manual",
8
+ HYBRID: "hybrid",
9
+ STRICT: "strict",
10
+ });
11
+
12
+ export const PRESET_MODE_MAP = Object.freeze({
13
+ sandbox: GATE_MODES.AUTO,
14
+ test: GATE_MODES.AUTO,
15
+ auto: GATE_MODES.AUTO,
16
+ "auto-approval": GATE_MODES.AUTO,
17
+ prod: GATE_MODES.STRICT,
18
+ production: GATE_MODES.STRICT,
19
+ strict: GATE_MODES.STRICT,
20
+ manual: GATE_MODES.MANUAL,
21
+ dev: GATE_MODES.HYBRID,
22
+ development: GATE_MODES.HYBRID,
23
+ hybrid: GATE_MODES.HYBRID,
24
+ standard: GATE_MODES.HYBRID,
25
+ });
26
+
27
+ /**
28
+ * Resolves active gate mode for a session based on presets, context, or defaults.
29
+ *
30
+ * @param {string} [sessionId]
31
+ * @param {string} [sessionPreset]
32
+ * @param {Record<string, any>} [options]
33
+ * @returns {string}
34
+ */
35
+ export function resolveSessionGateMode(sessionId, sessionPreset, options = {}) {
36
+ // 1. Explicit mode in options or context
37
+ if (options.mode && Object.values(GATE_MODES).includes(options.mode)) {
38
+ return options.mode;
39
+ }
40
+ if (options.sessionMode && Object.values(GATE_MODES).includes(options.sessionMode)) {
41
+ return options.sessionMode;
42
+ }
43
+
44
+ // 2. Preset mapping
45
+ const presetKey = String(sessionPreset || options.preset || "").toLowerCase().trim();
46
+ if (presetKey && PRESET_MODE_MAP[presetKey]) {
47
+ return PRESET_MODE_MAP[presetKey];
48
+ }
49
+
50
+ // 3. Fallback default
51
+ return options.defaultMode || GATE_MODES.HYBRID;
52
+ }
53
+
54
+ /**
55
+ * Applies session gate mode policy to a verdict decision (#61).
56
+ *
57
+ * @param {string} mode - Gate mode
58
+ * @param {Record<string, any>} decision - Original verdict decision
59
+ * @param {{ track?: string, riskBucket?: string, lockedAsk?: boolean, isRead?: boolean }} [meta]
60
+ * @returns {Record<string, any>}
61
+ */
62
+ export function applySessionModePolicy(mode, decision, meta = {}) {
63
+ if (!decision) return decision;
64
+
65
+ const currentMode = mode || GATE_MODES.HYBRID;
66
+ const isLocked = Boolean(decision.lockedAsk || meta.lockedAsk);
67
+ const track = meta.track || decision.track || "external";
68
+ const riskBucket = meta.riskBucket || decision.riskBucket || "medium";
69
+
70
+ // Auto-approval mode: relaxes low-risk or internal chores that would otherwise ask
71
+ if (currentMode === GATE_MODES.AUTO && decision.verdict === "ask" && !isLocked) {
72
+ const isInternal = track === "internal";
73
+ const isLowRisk = riskBucket === "low" || riskBucket === "minimal";
74
+ if (isInternal || isLowRisk) {
75
+ return {
76
+ ...decision,
77
+ verdict: "allow",
78
+ autoApproved: true,
79
+ originalVerdict: "ask",
80
+ sessionMode: currentMode,
81
+ stage: "session-mode-auto",
82
+ autoReason: isInternal ? "auto-approve-internal-track" : "auto-approve-low-risk",
83
+ };
84
+ }
85
+ }
86
+
87
+ // Manual or strict mode: requires manual confirmation for external mutations or higher risk
88
+ if ((currentMode === GATE_MODES.MANUAL || currentMode === GATE_MODES.STRICT) && decision.verdict === "allow") {
89
+ const isSafeRead = Boolean(meta.isRead);
90
+ const isInternalSafe = track === "internal" && (riskBucket === "low" || riskBucket === "minimal");
91
+ if (!isSafeRead && !isInternalSafe) {
92
+ return {
93
+ ...decision,
94
+ verdict: "ask",
95
+ manualRequired: true,
96
+ originalVerdict: "allow",
97
+ sessionMode: currentMode,
98
+ stage: "session-mode-manual",
99
+ manualReason: currentMode === GATE_MODES.STRICT ? "strict-mode-enforcement" : "manual-mode-enforcement",
100
+ };
101
+ }
102
+ }
103
+
104
+ return {
105
+ ...decision,
106
+ sessionMode: currentMode,
107
+ };
108
+ }
109
+
110
+ /**
111
+ * Registry for maintaining per-session gate modes (#61).
112
+ */
113
+ export class SessionModeRegistry {
114
+ constructor(defaultMode = GATE_MODES.HYBRID) {
115
+ this.defaultMode = defaultMode;
116
+ /** @type {Map<string, string>} */
117
+ this.sessionModes = new Map();
118
+ }
119
+
120
+ setMode(sessionId, mode) {
121
+ const key = sessionId || "default";
122
+ const resolved = PRESET_MODE_MAP[String(mode).toLowerCase().trim()] || mode;
123
+ if (Object.values(GATE_MODES).includes(resolved)) {
124
+ this.sessionModes.set(key, resolved);
125
+ return resolved;
126
+ }
127
+ return this.defaultMode;
128
+ }
129
+
130
+ getMode(sessionId, preset) {
131
+ const key = sessionId || "default";
132
+ if (this.sessionModes.has(key)) {
133
+ return this.sessionModes.get(key);
134
+ }
135
+ return resolveSessionGateMode(sessionId, preset, { defaultMode: this.defaultMode });
136
+ }
137
+
138
+ clearSession(sessionId) {
139
+ const key = sessionId || "default";
140
+ this.sessionModes.delete(key);
141
+ }
142
+
143
+ reset() {
144
+ this.sessionModes.clear();
145
+ }
146
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-approval-gate",
3
- "version": "0.1.21",
3
+ "version": "0.1.23",
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",
@@ -52,4 +52,4 @@
52
52
  "approval-gate",
53
53
  "safety"
54
54
  ]
55
- }
55
+ }