@lytharalab/dsh-cons-auto-review 1.1.0
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/CHANGELOG.md +38 -0
- package/LICENSE +22 -0
- package/README.md +126 -0
- package/THIRD_PARTY_LICENSE.txt +21 -0
- package/THIRD_PARTY_NOTICES.md +10 -0
- package/cordis.patch.yml +5 -0
- package/docs/COMPATIBILITY.md +43 -0
- package/docs/CONFIGURATION.md +167 -0
- package/docs/RELEASING.md +108 -0
- package/examples/defaults.json +121 -0
- package/examples/desktop-config.patch.yml +30 -0
- package/examples/separate-review-model.patch.yml +12 -0
- package/examples/strict.patch.yml +13 -0
- package/icon.svg +1 -0
- package/locale/en.json +1 -0
- package/locale/zh.json +1 -0
- package/package.json +108 -0
- package/src/async.js +66 -0
- package/src/audit.js +125 -0
- package/src/config.js +240 -0
- package/src/containment.js +49 -0
- package/src/deterministic.js +58 -0
- package/src/engine.js +365 -0
- package/src/index.d.ts +110 -0
- package/src/index.js +396 -0
- package/src/paths.js +94 -0
- package/src/permission-adapter.js +75 -0
- package/src/permission-presets.js +77 -0
- package/src/presentation.js +48 -0
- package/src/preset-defaults.js +85 -0
- package/src/shell.js +197 -0
- package/src/upstream-reviewer.js +578 -0
package/src/config.js
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/** Validated, immutable runtime configuration. Only allow/escalate exist in layer one. */
|
|
2
|
+
export const DEFAULTS = freeze({
|
|
3
|
+
enabled: true,
|
|
4
|
+
inheritSubagents: true,
|
|
5
|
+
rules: {
|
|
6
|
+
enabled: true,
|
|
7
|
+
allowOutsideReads: true,
|
|
8
|
+
includeTempRoots: true,
|
|
9
|
+
additionalWritableRoots: [],
|
|
10
|
+
protectedPaths: [
|
|
11
|
+
"**/.git/**",
|
|
12
|
+
"**/.dsh/**",
|
|
13
|
+
"**/.dcar/**",
|
|
14
|
+
"**/.ssh/**",
|
|
15
|
+
"**/.aws/**",
|
|
16
|
+
"**/.gnupg/**",
|
|
17
|
+
"**/.env",
|
|
18
|
+
"**/.env.*",
|
|
19
|
+
"**/.npmrc",
|
|
20
|
+
"**/credentials.json",
|
|
21
|
+
],
|
|
22
|
+
safeEnvironmentFiles: [".env.example", ".env.sample", ".env.template"],
|
|
23
|
+
readTools: ["read", "read_image"],
|
|
24
|
+
searchTools: ["glob", "grep", "ls"],
|
|
25
|
+
writeTools: ["write", "edit"],
|
|
26
|
+
harmlessTools: ["todo_write"],
|
|
27
|
+
alwaysReviewTools: [],
|
|
28
|
+
trustedTools: [],
|
|
29
|
+
customTools: [],
|
|
30
|
+
maxArgumentBytes: 2097152,
|
|
31
|
+
},
|
|
32
|
+
shell: {
|
|
33
|
+
enabled: true,
|
|
34
|
+
tools: ["bash", "pwsh"],
|
|
35
|
+
readCommands: [
|
|
36
|
+
"pwd",
|
|
37
|
+
"ls",
|
|
38
|
+
"cat",
|
|
39
|
+
"head",
|
|
40
|
+
"tail",
|
|
41
|
+
"wc",
|
|
42
|
+
"rg",
|
|
43
|
+
"grep",
|
|
44
|
+
"echo",
|
|
45
|
+
"Get-Location",
|
|
46
|
+
"Get-ChildItem",
|
|
47
|
+
"Get-Content",
|
|
48
|
+
"Write-Output",
|
|
49
|
+
],
|
|
50
|
+
workspaceMutationCommands: ["mkdir", "touch"],
|
|
51
|
+
trustedCommands: [],
|
|
52
|
+
maxCommandChars: 8192,
|
|
53
|
+
},
|
|
54
|
+
review: {
|
|
55
|
+
provider: "",
|
|
56
|
+
model: "",
|
|
57
|
+
reasoningEffort: "",
|
|
58
|
+
preferLowReasoning: true,
|
|
59
|
+
timeoutMs: 120000,
|
|
60
|
+
maxTokens: 8192,
|
|
61
|
+
maxTokensLimit: 32768,
|
|
62
|
+
tokenLimitRetries: 2,
|
|
63
|
+
tokenGrowthFactor: 2,
|
|
64
|
+
chineseReasonRetries: 1,
|
|
65
|
+
maxReasonChars: 600,
|
|
66
|
+
showAction: true,
|
|
67
|
+
maxActionChars: 3000,
|
|
68
|
+
temperature: 0,
|
|
69
|
+
retries: 0,
|
|
70
|
+
retryDelayMs: 300,
|
|
71
|
+
concurrency: 2,
|
|
72
|
+
maxQueued: 128,
|
|
73
|
+
onError: "ask",
|
|
74
|
+
onDeny: "ask",
|
|
75
|
+
policyAppend: "",
|
|
76
|
+
maxInputChars: 180000,
|
|
77
|
+
allowAsk: true,
|
|
78
|
+
},
|
|
79
|
+
cache: { enabled: false, ttlMs: 30000, maxEntries: 128 },
|
|
80
|
+
audit: {
|
|
81
|
+
enabled: true,
|
|
82
|
+
file: "",
|
|
83
|
+
maxBytes: 5242880,
|
|
84
|
+
historyLimit: 200,
|
|
85
|
+
includeArguments: false,
|
|
86
|
+
includeReasons: true,
|
|
87
|
+
},
|
|
88
|
+
commands: { enabled: true, name: "dcar", allowSessionConfig: true },
|
|
89
|
+
lifecycle: { fallbackPreset: "workspace-write" },
|
|
90
|
+
});
|
|
91
|
+
const object = (x) => x !== null && typeof x === "object" && !Array.isArray(x);
|
|
92
|
+
const enums = {
|
|
93
|
+
"review.onError": ["ask", "deny"],
|
|
94
|
+
"review.onDeny": ["ask", "deny"],
|
|
95
|
+
};
|
|
96
|
+
const ranges = {
|
|
97
|
+
"rules.maxArgumentBytes": [1024, 16777216],
|
|
98
|
+
"shell.maxCommandChars": [64, 1048576],
|
|
99
|
+
"review.timeoutMs": [10, 600000],
|
|
100
|
+
"review.maxTokens": [64, 65536],
|
|
101
|
+
"review.maxTokensLimit": [64, 65536],
|
|
102
|
+
"review.tokenLimitRetries": [0, 5],
|
|
103
|
+
"review.tokenGrowthFactor": [2, 4],
|
|
104
|
+
"review.chineseReasonRetries": [0, 2],
|
|
105
|
+
"review.maxReasonChars": [100, 4000],
|
|
106
|
+
"review.maxActionChars": [200, 32768],
|
|
107
|
+
"review.temperature": [0, 2],
|
|
108
|
+
"review.retries": [0, 5],
|
|
109
|
+
"review.retryDelayMs": [0, 10000],
|
|
110
|
+
"review.concurrency": [1, 32],
|
|
111
|
+
"review.maxQueued": [1, 4096],
|
|
112
|
+
"review.maxInputChars": [1024, 4000000],
|
|
113
|
+
"cache.ttlMs": [1, 3600000],
|
|
114
|
+
"cache.maxEntries": [1, 10000],
|
|
115
|
+
"audit.maxBytes": [1024, 1073741824],
|
|
116
|
+
"audit.historyLimit": [1, 10000],
|
|
117
|
+
};
|
|
118
|
+
function merge(defaults, input, prefix = "") {
|
|
119
|
+
if (!object(input))
|
|
120
|
+
throw new Error(`DCAR config ${prefix || "<root>"} must be an object`);
|
|
121
|
+
for (const key of Object.keys(input))
|
|
122
|
+
if (!Object.hasOwn(defaults, key))
|
|
123
|
+
throw new Error(`Unknown DCAR config: ${prefix}${key}`);
|
|
124
|
+
const output = {};
|
|
125
|
+
for (const [key, fallback] of Object.entries(defaults)) {
|
|
126
|
+
const name = `${prefix}${key}`,
|
|
127
|
+
value = Object.hasOwn(input, key)
|
|
128
|
+
? input[key]
|
|
129
|
+
: structuredClone(fallback);
|
|
130
|
+
if (object(fallback)) {
|
|
131
|
+
output[key] = merge(fallback, value, `${name}.`);
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
if (Array.isArray(fallback)) {
|
|
135
|
+
if (!Array.isArray(value)) throw new Error(`${name} must be an array`);
|
|
136
|
+
if (
|
|
137
|
+
!["rules.customTools", "shell.trustedCommands"].includes(name) &&
|
|
138
|
+
value.some((x) => typeof x !== "string" || !x.trim())
|
|
139
|
+
)
|
|
140
|
+
throw new Error(`${name} must contain non-empty strings`);
|
|
141
|
+
output[key] = structuredClone(value);
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (
|
|
145
|
+
typeof value !== typeof fallback ||
|
|
146
|
+
(typeof value === "number" && !Number.isFinite(value))
|
|
147
|
+
)
|
|
148
|
+
throw new Error(`${name} has an invalid type`);
|
|
149
|
+
if (enums[name] && !enums[name].includes(value))
|
|
150
|
+
throw new Error(`${name} must be ${enums[name].join(" or ")}`);
|
|
151
|
+
if (
|
|
152
|
+
ranges[name] &&
|
|
153
|
+
(value < ranges[name][0] ||
|
|
154
|
+
value > ranges[name][1] ||
|
|
155
|
+
(name !== "review.temperature" && !Number.isSafeInteger(value)))
|
|
156
|
+
)
|
|
157
|
+
throw new Error(`${name} is outside the supported range`);
|
|
158
|
+
output[key] = value;
|
|
159
|
+
}
|
|
160
|
+
return output;
|
|
161
|
+
}
|
|
162
|
+
function freeze(value) {
|
|
163
|
+
if (value && typeof value === "object") {
|
|
164
|
+
Object.values(value).forEach(freeze);
|
|
165
|
+
Object.freeze(value);
|
|
166
|
+
}
|
|
167
|
+
return value;
|
|
168
|
+
}
|
|
169
|
+
/** Merge with defaults, reject unknown fields and invalid security rules, then freeze. */
|
|
170
|
+
export function resolveConfig(input = {}) {
|
|
171
|
+
const c = merge(DEFAULTS, input);
|
|
172
|
+
if (!!c.review.provider !== !!c.review.model)
|
|
173
|
+
throw new Error(
|
|
174
|
+
"review.provider and review.model must both be set, or both be empty",
|
|
175
|
+
);
|
|
176
|
+
if (!/^[a-z][a-z0-9_-]*$/.test(c.commands.name))
|
|
177
|
+
throw new Error("commands.name must be a lowercase slash-command name");
|
|
178
|
+
if (
|
|
179
|
+
!c.lifecycle.fallbackPreset ||
|
|
180
|
+
c.lifecycle.fallbackPreset === "CAutoR" ||
|
|
181
|
+
c.lifecycle.fallbackPreset === "danger-full-access"
|
|
182
|
+
)
|
|
183
|
+
throw new Error("lifecycle.fallbackPreset must name a confined preset");
|
|
184
|
+
for (const rule of c.rules.customTools) {
|
|
185
|
+
if (
|
|
186
|
+
!object(rule) ||
|
|
187
|
+
Object.keys(rule).some(
|
|
188
|
+
(k) => !["name", "effect", "pathFields"].includes(k),
|
|
189
|
+
) ||
|
|
190
|
+
typeof rule.name !== "string" ||
|
|
191
|
+
!rule.name ||
|
|
192
|
+
!["read", "write"].includes(rule.effect) ||
|
|
193
|
+
!Array.isArray(rule.pathFields) ||
|
|
194
|
+
!rule.pathFields.length ||
|
|
195
|
+
rule.pathFields.some(
|
|
196
|
+
(p) => typeof p !== "string" || !/^[a-zA-Z0-9_.]+$/.test(p),
|
|
197
|
+
)
|
|
198
|
+
)
|
|
199
|
+
throw new Error(
|
|
200
|
+
"rules.customTools entries require name, effect (read/write), and non-empty pathFields",
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
for (const command of c.shell.trustedCommands) {
|
|
204
|
+
if (
|
|
205
|
+
!object(command) ||
|
|
206
|
+
Object.keys(command).some((k) => !["executable", "args"].includes(k)) ||
|
|
207
|
+
typeof command.executable !== "string" ||
|
|
208
|
+
!command.executable ||
|
|
209
|
+
!Array.isArray(command.args) ||
|
|
210
|
+
command.args.some((a) => typeof a !== "string")
|
|
211
|
+
)
|
|
212
|
+
throw new Error(
|
|
213
|
+
"shell.trustedCommands entries require an executable and exact args array",
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
const toolSets = [
|
|
217
|
+
c.rules.readTools,
|
|
218
|
+
c.rules.searchTools,
|
|
219
|
+
c.rules.writeTools,
|
|
220
|
+
c.rules.harmlessTools,
|
|
221
|
+
c.rules.customTools.map((r) => r.name),
|
|
222
|
+
];
|
|
223
|
+
const all = toolSets.flat();
|
|
224
|
+
if (new Set(all).size !== all.length)
|
|
225
|
+
throw new Error(
|
|
226
|
+
"A tool cannot occur in multiple deterministic tool classes",
|
|
227
|
+
);
|
|
228
|
+
return freeze(c);
|
|
229
|
+
}
|
|
230
|
+
/** Apply a validated user-originated session configuration patch. */
|
|
231
|
+
export function patchConfig(current, patch) {
|
|
232
|
+
function overlay(a, b) {
|
|
233
|
+
if (!object(b)) return b;
|
|
234
|
+
const result = structuredClone(a);
|
|
235
|
+
for (const [k, v] of Object.entries(b))
|
|
236
|
+
result[k] = object(v) && object(a[k]) ? overlay(a[k], v) : v;
|
|
237
|
+
return result;
|
|
238
|
+
}
|
|
239
|
+
return resolveConfig(overlay(current, patch));
|
|
240
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/** DeepSeek workspace-write containment rules, MIT; see THIRD_PARTY_NOTICES.md. */
|
|
2
|
+
import { stat } from "node:fs/promises";
|
|
3
|
+
import { dirname, sep } from "node:path";
|
|
4
|
+
const MISSING_CODES = /* @__PURE__ */ new Set(["ENOENT", "ENOTDIR"]);
|
|
5
|
+
function isMissing(error) {
|
|
6
|
+
const code = error.code;
|
|
7
|
+
return MISSING_CODES.has(code);
|
|
8
|
+
}
|
|
9
|
+
function comparablePath(path, caseSensitive) {
|
|
10
|
+
return caseSensitive ? path : path.toLowerCase();
|
|
11
|
+
}
|
|
12
|
+
function isLexicallyUnder(path, root, caseSensitive) {
|
|
13
|
+
const comparableTarget = comparablePath(path, caseSensitive);
|
|
14
|
+
const comparableRoot = comparablePath(root, caseSensitive);
|
|
15
|
+
if (comparableTarget === comparableRoot) return true;
|
|
16
|
+
const prefix = comparableRoot.endsWith(sep)
|
|
17
|
+
? comparableRoot
|
|
18
|
+
: comparableRoot + sep;
|
|
19
|
+
return comparableTarget.startsWith(prefix);
|
|
20
|
+
}
|
|
21
|
+
async function statIfPresent(path) {
|
|
22
|
+
try {
|
|
23
|
+
return await stat(path, { bigint: true });
|
|
24
|
+
} catch (error) {
|
|
25
|
+
if (isMissing(error)) return void 0;
|
|
26
|
+
throw error;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function sameIdentity(left, right) {
|
|
30
|
+
return left.dev === right.dev && left.ino === right.ino;
|
|
31
|
+
}
|
|
32
|
+
async function isPathUnder(
|
|
33
|
+
path,
|
|
34
|
+
root,
|
|
35
|
+
caseSensitive = process.platform !== "win32",
|
|
36
|
+
) {
|
|
37
|
+
if (isLexicallyUnder(path, root, caseSensitive)) return true;
|
|
38
|
+
const rootInfo = await statIfPresent(root);
|
|
39
|
+
if (!rootInfo) return false;
|
|
40
|
+
let ancestor = path;
|
|
41
|
+
while (true) {
|
|
42
|
+
const ancestorInfo = await statIfPresent(ancestor);
|
|
43
|
+
if (ancestorInfo && sameIdentity(ancestorInfo, rootInfo)) return true;
|
|
44
|
+
const parent = dirname(ancestor);
|
|
45
|
+
if (parent === ancestor) return false;
|
|
46
|
+
ancestor = parent;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
export { isPathUnder };
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/** Layer one: a positive authorization rule or escalation; never a final deny. */
|
|
2
|
+
import { checkPaths, allow, escalate } from "./paths.js";
|
|
3
|
+
import { checkShell } from "./shell.js";
|
|
4
|
+
function field(a, path) {
|
|
5
|
+
return path
|
|
6
|
+
.split(".")
|
|
7
|
+
.reduce((v, k) => (v && typeof v === "object" ? v[k] : undefined), a);
|
|
8
|
+
}
|
|
9
|
+
export async function deterministic(exec, ctx, c, signal = exec.signal) {
|
|
10
|
+
if (!c.enabled || !c.rules.enabled)
|
|
11
|
+
return escalate("RULES_DISABLED", "Deterministic rules disabled; use LLM");
|
|
12
|
+
signal.throwIfAborted();
|
|
13
|
+
const name = exec.name,
|
|
14
|
+
a = exec.arguments;
|
|
15
|
+
if (c.rules.alwaysReviewTools.includes(name))
|
|
16
|
+
return escalate("ALWAYS_REVIEW", "Tool configured for LLM review");
|
|
17
|
+
const encoded = JSON.stringify(a);
|
|
18
|
+
if (
|
|
19
|
+
encoded === undefined ||
|
|
20
|
+
Buffer.byteLength(encoded) > c.rules.maxArgumentBytes
|
|
21
|
+
)
|
|
22
|
+
return escalate("ARGUMENT_SIZE", "Arguments exceed deterministic limit");
|
|
23
|
+
if (c.rules.trustedTools.includes(name))
|
|
24
|
+
return allow("TRUSTED_TOOL", "Administrator explicitly trusted this tool");
|
|
25
|
+
if (c.rules.harmlessTools.includes(name))
|
|
26
|
+
return allow("HARMLESS_TOOL", "No host file or external side effects");
|
|
27
|
+
if (!a || typeof a !== "object" || Array.isArray(a))
|
|
28
|
+
return escalate("INVALID_ARGUMENTS", "Expected object arguments");
|
|
29
|
+
if (c.shell.tools.includes(name)) return checkShell(exec, ctx, c, signal);
|
|
30
|
+
const custom = c.rules.customTools.find((r) => r.name === name);
|
|
31
|
+
const cwd = exec.agent.session.header.cwd;
|
|
32
|
+
if (custom) {
|
|
33
|
+
const paths = custom.pathFields.flatMap((p) => {
|
|
34
|
+
const v = field(a, p);
|
|
35
|
+
return Array.isArray(v) ? v : [v];
|
|
36
|
+
});
|
|
37
|
+
return checkPaths(paths, custom.effect, cwd, ctx, c, signal);
|
|
38
|
+
}
|
|
39
|
+
if (c.rules.readTools.includes(name))
|
|
40
|
+
return checkPaths([a.file_path], "read", cwd, ctx, c, signal);
|
|
41
|
+
if (c.rules.writeTools.includes(name))
|
|
42
|
+
return checkPaths([a.file_path], "write", cwd, ctx, c, signal);
|
|
43
|
+
if (c.rules.searchTools.includes(name)) {
|
|
44
|
+
const path = a.path ?? a.cwd ?? cwd;
|
|
45
|
+
// An absolute/traversing glob can search beyond its supplied root; do not approve it by its cwd.
|
|
46
|
+
if (
|
|
47
|
+
name === "glob" &&
|
|
48
|
+
(typeof a.pattern !== "string" ||
|
|
49
|
+
/(^|[\\/])\.\.([\\/]|$)|^[\\/]|^[a-z]:/i.test(a.pattern))
|
|
50
|
+
)
|
|
51
|
+
return escalate("SEARCH_SCOPE", "Search pattern needs review");
|
|
52
|
+
return checkPaths([path], "read", cwd, ctx, c, signal);
|
|
53
|
+
}
|
|
54
|
+
return escalate(
|
|
55
|
+
"UNKNOWN_TOOL",
|
|
56
|
+
"No deterministic authorization rule for this tool",
|
|
57
|
+
);
|
|
58
|
+
}
|