@goodandready/dsh-approval-gate 0.1.22 → 0.1.24

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.
@@ -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
+ }
@@ -0,0 +1,134 @@
1
+ // lib/similarity.js
2
+ // Semantic Command Similarity & Fast Validation (#85).
3
+ // Normalizes and validates syntactic variations of commands (e.g. pnpm test vs npm run test vs npm test).
4
+ // Provides canonical command normalization and optional fast Flash-model validator with deterministic fallback.
5
+
6
+ const PKG_MGR_REGEX = /^(?:pnpm|npm|yarn|bun)\b/;
7
+
8
+ /**
9
+ * Normalizes common command variants into canonical representation (#85).
10
+ *
11
+ * @param {string} command
12
+ * @returns {string}
13
+ */
14
+ export function canonicalizeCommand(command) {
15
+ if (!command || typeof command !== "string") return "";
16
+ let cmd = command.trim().replace(/\s+/g, " ");
17
+
18
+ // 1. Package manager scripts normalization
19
+ // pnpm test / npm run test / yarn test / bun test -> npm test
20
+ if (/^(?:pnpm|npm\s+run|yarn|bun)\s+test\b/.test(cmd)) {
21
+ return cmd.replace(/^(?:pnpm|npm\s+run|yarn|bun)\s+test/, "npm test");
22
+ }
23
+ // pnpm run <script> / yarn <script> / bun run <script> -> npm run <script>
24
+ if (/^(?:pnpm\s+run|yarn\s+run|bun\s+run)\s+([a-zA-Z0-9_:.-]+)/.test(cmd)) {
25
+ return cmd.replace(/^(?:pnpm\s+run|yarn\s+run|bun\s+run)/, "npm run");
26
+ }
27
+ if (/^(?:pnpm|yarn|bun)\s+(build|lint|check|start|dev)\b/.test(cmd)) {
28
+ return cmd.replace(/^(?:pnpm|yarn|bun)\s+/, "npm run ");
29
+ }
30
+
31
+ // 2. Python testing tools
32
+ // python -m pytest / python3 -m pytest -> pytest
33
+ if (/^python[23]?\s+-m\s+pytest\b/.test(cmd)) {
34
+ return cmd.replace(/^python[23]?\s+-m\s+pytest/, "pytest");
35
+ }
36
+
37
+ // 3. Git status / diff flags
38
+ if (/^git\s+status\s+(?:-s|--short)\b/.test(cmd)) {
39
+ return "git status";
40
+ }
41
+ if (/^git\s+diff\s+HEAD\b/.test(cmd)) {
42
+ return "git diff";
43
+ }
44
+
45
+ return cmd;
46
+ }
47
+
48
+ /**
49
+ * Checks whether two commands are semantically equivalent (#85).
50
+ *
51
+ * @param {string} cmdA
52
+ * @param {string} cmdB
53
+ * @returns {{ equivalent: boolean, confidence: number, method: string }}
54
+ */
55
+ export function isSemanticallyEquivalent(cmdA, cmdB) {
56
+ if (!cmdA || !cmdB) return { equivalent: false, confidence: 0, method: "none" };
57
+ const a = String(cmdA).trim();
58
+ const b = String(cmdB).trim();
59
+ if (a === b) {
60
+ return { equivalent: true, confidence: 1.0, method: "exact" };
61
+ }
62
+
63
+ const normA = canonicalizeCommand(a);
64
+ const normB = canonicalizeCommand(b);
65
+
66
+ if (normA && normA === normB) {
67
+ return { equivalent: true, confidence: 0.98, method: "canonical" };
68
+ }
69
+
70
+ return { equivalent: false, confidence: 0, method: "mismatch" };
71
+ }
72
+
73
+ /**
74
+ * Searches a list of learned rules for a semantically equivalent match (#85).
75
+ *
76
+ * @param {string} command
77
+ * @param {Array<{ command?: string, key?: string }>} learnedRules
78
+ * @returns {{ matched: boolean, matchedRule?: any, method?: string }}
79
+ */
80
+ export function findSemanticallyEquivalentRule(command, learnedRules = []) {
81
+ if (!command || !Array.isArray(learnedRules) || learnedRules.length === 0) {
82
+ return { matched: false };
83
+ }
84
+
85
+ for (const rule of learnedRules) {
86
+ const ruleCmd = rule.command || (rule.key && rule.key.includes(":") ? rule.key.slice(rule.key.indexOf(":") + 1) : "");
87
+ if (!ruleCmd) continue;
88
+
89
+ const check = isSemanticallyEquivalent(command, ruleCmd);
90
+ if (check.equivalent) {
91
+ return { matched: true, matchedRule: rule, method: check.method };
92
+ }
93
+ }
94
+
95
+ return { matched: false };
96
+ }
97
+
98
+ /**
99
+ * Validates semantic similarity with optional Flash-model LLM client (#85).
100
+ * Falls back safely to deterministic canonical matching on error or absence.
101
+ *
102
+ * @param {string} cmdA
103
+ * @param {string} cmdB
104
+ * @param {{ classify?: Function, model?: string }} [llmClient]
105
+ * @returns {Promise<{ equivalent: boolean, confidence: number, method: string }>}
106
+ */
107
+ export async function validateSemanticSimilarity(cmdA, cmdB, llmClient = null) {
108
+ // 1. Fast deterministic check
109
+ const det = isSemanticallyEquivalent(cmdA, cmdB);
110
+ if (det.equivalent) {
111
+ return det;
112
+ }
113
+
114
+ // 2. If no LLM client, return deterministic result
115
+ if (!llmClient || typeof llmClient.classify !== "function") {
116
+ return det;
117
+ }
118
+
119
+ try {
120
+ const prompt = `Are these two terminal commands semantically equivalent in purpose and safety?\nCommand 1: ${cmdA}\nCommand 2: ${cmdB}\nRespond with JSON: {"equivalent": true|false, "confidence": number}`;
121
+ const res = await llmClient.classify({ name: "semantic_check", prompt });
122
+ if (res && typeof res.equivalent === "boolean") {
123
+ return {
124
+ equivalent: res.equivalent,
125
+ confidence: typeof res.confidence === "number" ? res.confidence : 0.9,
126
+ method: "flash-model",
127
+ };
128
+ }
129
+ } catch (_err) {
130
+ // Fail closed to deterministic fallback
131
+ }
132
+
133
+ return det;
134
+ }
package/package.json CHANGED
@@ -1,55 +1,60 @@
1
- {
2
- "name": "@goodandready/dsh-approval-gate",
3
- "version": "0.1.22",
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
- "type": "module",
6
- "main": "lib/index.js",
7
- "exports": {
8
- ".": "./lib/index.js",
9
- "./package.json": "./package.json",
10
- "./cordis.patch.yml": "./cordis.patch.yml"
11
- },
12
- "files": [
13
- "lib",
14
- "rules",
15
- "cordis.patch.yml",
16
- "README.md",
17
- "README.zh.md",
18
- "README.ru.md",
19
- "CHANGELOG.md",
20
- "LICENSE"
21
- ],
22
- "dsh": {
23
- "bundle": {
24
- "patch": "./cordis.patch.yml"
25
- }
26
- },
27
- "license": "MIT",
28
- "peerDependencies": {
29
- "@deepseek-ai/cordis": "^4.0.1",
30
- "@deepseek-ai/schemastery": "^3.18.1"
31
- },
32
- "repository": {
33
- "type": "git",
34
- "url": "git+https://github.com/GooDAnDReaDY/dsh-approval-gate.git"
35
- },
36
- "homepage": "https://github.com/GooDAnDReaDY/dsh-approval-gate#readme",
37
- "bugs": {
38
- "url": "https://github.com/GooDAnDReaDY/dsh-approval-gate/issues"
39
- },
40
- "scripts": {
41
- "test": "node --test test/*.test.mjs"
42
- },
43
- "publishConfig": {
44
- "registry": "https://registry.npmjs.org",
45
- "access": "public"
46
- },
47
- "keywords": [
48
- "dsh",
49
- "dsh-plugin",
50
- "deepseek-harness",
51
- "security",
52
- "approval-gate",
53
- "safety"
54
- ]
55
- }
1
+ {
2
+ "name": "@goodandready/dsh-approval-gate",
3
+ "version": "0.1.24",
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
+ "type": "module",
6
+ "main": "lib/index.js",
7
+ "exports": {
8
+ ".": "./lib/index.js",
9
+ "./package.json": "./package.json",
10
+ "./cordis.patch.yml": "./cordis.patch.yml"
11
+ },
12
+ "files": [
13
+ "lib",
14
+ "rules",
15
+ "cordis.patch.yml",
16
+ "README.md",
17
+ "README.zh.md",
18
+ "README.ru.md",
19
+ "CHANGELOG.md",
20
+ "LICENSE",
21
+ "bin"
22
+ ],
23
+ "dsh": {
24
+ "bundle": {
25
+ "patch": "./cordis.patch.yml"
26
+ }
27
+ },
28
+ "license": "MIT",
29
+ "peerDependencies": {
30
+ "@deepseek-ai/cordis": "^4.0.1",
31
+ "@deepseek-ai/schemastery": "^3.18.1"
32
+ },
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "git+https://github.com/GooDAnDReaDY/dsh-approval-gate.git"
36
+ },
37
+ "homepage": "https://github.com/GooDAnDReaDY/dsh-approval-gate#readme",
38
+ "bugs": {
39
+ "url": "https://github.com/GooDAnDReaDY/dsh-approval-gate/issues"
40
+ },
41
+ "scripts": {
42
+ "test": "node --test test/*.test.mjs",
43
+ "eval": "node bin/eval.mjs"
44
+ },
45
+ "publishConfig": {
46
+ "registry": "https://registry.npmjs.org",
47
+ "access": "public"
48
+ },
49
+ "keywords": [
50
+ "dsh",
51
+ "dsh-plugin",
52
+ "deepseek-harness",
53
+ "security",
54
+ "approval-gate",
55
+ "safety"
56
+ ],
57
+ "bin": {
58
+ "dsh-approval-eval": "./bin/eval.mjs"
59
+ }
60
+ }