@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.
@@ -0,0 +1,85 @@
1
+ /** A persisted CAutoR default must remain confined before DCAR activates. */
2
+ export const DORMANT_PRESET = Object.freeze({
3
+ sandbox: "read-only",
4
+ approval: "never",
5
+ name: "CAutoR(DCAR 未启用)",
6
+ description: "DCAR 未启用时使用只读权限;启用后恢复 Con's 自动审查。",
7
+ });
8
+
9
+ export function isDormantPreset(spec) {
10
+ return spec && Object.entries(DORMANT_PRESET).every(([key, value]) => spec[key] === value);
11
+ }
12
+
13
+ /** Publish a live review preset and, for CAutoR, a guarded native default entry. */
14
+ export function registerReviewPreset(service, entries, name, spec, admit) {
15
+ const table = service.presets;
16
+ const previous = name === "CAutoR" ? Object.getOwnPropertyDescriptor(table, name) : undefined;
17
+ const dormant = name === "CAutoR" && isDormantPreset(previous?.value);
18
+ if ((service.names.includes(name) && !dormant) || name === "auto" || name === "custom")
19
+ throw new Error(`DCAR preset ${name} already exists or is reserved`);
20
+ if (entries.has(name)) throw new Error(`DCAR preset ${name} already exists or is reserved`);
21
+ if (name === "CAutoR")
22
+ Object.defineProperty(table, name, { value: DORMANT_PRESET, configurable: true, enumerable: true, writable: true });
23
+ entries.set(name, { spec: Object.freeze({ ...spec }), admit });
24
+ const restore = (keepDefault) => {
25
+ if (name !== "CAutoR") return;
26
+ if (previous) Object.defineProperty(table, name, previous);
27
+ else if (!keepDefault) Reflect.deleteProperty(table, name);
28
+ };
29
+ try { service.emitCatalogChanged(); }
30
+ catch (error) { entries.delete(name); restore(false); throw error; }
31
+ let live = true;
32
+ return () => {
33
+ if (!live) return;
34
+ live = false;
35
+ const keepDefault = name === "CAutoR" && service.defaultPreset === name;
36
+ entries.delete(name);
37
+ restore(keepDefault);
38
+ service.emitCatalogChanged();
39
+ };
40
+ }
41
+
42
+ /** Persist a confined CAutoR entry through the host's normal config editor. */
43
+ const persistenceKey = Symbol.for("dcar.default-persistence");
44
+ export function attachDefaultPersistence(editor, service, admit) {
45
+ const provider = Object.getOwnPropertyDescriptor(service, "ctx")?.value?.fiber;
46
+ if (!provider) return () => {};
47
+ let registry = Object.getOwnPropertyDescriptor(editor, persistenceKey)?.value;
48
+ if (!registry) {
49
+ const previous = Object.getOwnPropertyDescriptor(editor, "edit");
50
+ let original;
51
+ for (let object = editor; object; object = Object.getPrototypeOf(object)) {
52
+ const descriptor = Object.getOwnPropertyDescriptor(object, "edit");
53
+ if (descriptor) { original = descriptor.value; break; }
54
+ }
55
+ if (typeof original !== "function") return () => {};
56
+ if (previous && !previous.configurable) throw new Error("DCAR cannot extend this config editor instance");
57
+ const handlers = new Map();
58
+ const wrapper = function (entry, change) {
59
+ const handler = handlers.get(entry.fiber);
60
+ if (!handler) return original.call(this, entry, change);
61
+ return original.call(this, entry, (current, inherited) => {
62
+ const next = change(current, inherited);
63
+ if ((next.defaultPreset ?? inherited.defaultPreset) !== "CAutoR") return next;
64
+ handler.admit();
65
+ const presets = next.presets ?? inherited.presets ?? handler.service.presets;
66
+ return { ...next, presets: { ...presets, CAutoR: { ...DORMANT_PRESET } } };
67
+ });
68
+ };
69
+ registry = { handlers, wrapper, previous };
70
+ Object.defineProperty(editor, "edit", { value: wrapper, configurable: true });
71
+ Object.defineProperty(editor, persistenceKey, { value: registry, configurable: true });
72
+ }
73
+ const handler = { service, admit };
74
+ registry.handlers.set(provider, handler);
75
+ return () => {
76
+ if (registry.handlers.get(provider) !== handler) return;
77
+ registry.handlers.delete(provider);
78
+ if (registry.handlers.size) return;
79
+ if (Object.getOwnPropertyDescriptor(editor, "edit")?.value === registry.wrapper) {
80
+ if (registry.previous) Object.defineProperty(editor, "edit", registry.previous);
81
+ else Reflect.deleteProperty(editor, "edit");
82
+ }
83
+ Reflect.deleteProperty(editor, persistenceKey);
84
+ };
85
+ }
package/src/shell.js ADDED
@@ -0,0 +1,197 @@
1
+ /** Conservative command grammar for unconfined execution. Unrecognized syntax escalates. */
2
+ import { checkPaths, allow, escalate } from "./paths.js";
3
+ /** Parse literal argv only; never evaluate a command or use a prefix permission match. */
4
+ export function literalArgv(command) {
5
+ if (
6
+ typeof command !== "string" ||
7
+ !command.trim() ||
8
+ /[\r\n\x00-\x1f$`<>|;&(){}\\*?\[\]@]/.test(command)
9
+ )
10
+ return undefined;
11
+ const args = [];
12
+ let token = "",
13
+ started = false,
14
+ quote = "";
15
+ for (const ch of command.trim()) {
16
+ if (quote) {
17
+ if (ch === quote) quote = "";
18
+ else token += ch;
19
+ started = true;
20
+ continue;
21
+ }
22
+ if (ch === '"' || ch === "'") {
23
+ quote = ch;
24
+ started = true;
25
+ continue;
26
+ }
27
+ if (/\s/.test(ch)) {
28
+ if (started) {
29
+ args.push(token);
30
+ token = "";
31
+ started = false;
32
+ }
33
+ continue;
34
+ }
35
+ if (/[\\*?!#]/.test(ch)) return undefined;
36
+ token += ch;
37
+ started = true;
38
+ }
39
+ if (quote) return undefined;
40
+ if (started) args.push(token);
41
+ if (args[0] && /^[A-Za-z_][A-Za-z0-9_]*=/.test(args[0])) return undefined;
42
+ return args.length ? args : undefined;
43
+ }
44
+ function same(a, b) {
45
+ return a.length === b.length && a.every((x, i) => x === b[i]);
46
+ }
47
+ /** Exact literal commands and checked workspace mutations can skip LLM review. */
48
+ export async function checkShell(exec, ctx, c, signal) {
49
+ const a = exec.arguments;
50
+ if (!c.shell.enabled)
51
+ return escalate("SHELL_RULES_DISABLED", "Shell rules disabled");
52
+ if (
53
+ typeof a.command !== "string" ||
54
+ a.command.length > c.shell.maxCommandChars
55
+ )
56
+ return escalate("COMMAND_SIZE", "Command missing or too long");
57
+ const argv = literalArgv(a.command);
58
+ if (!argv)
59
+ return escalate(
60
+ "SHELL_SYNTAX",
61
+ "Expansion, compound syntax or scripts require LLM review",
62
+ );
63
+ const [exe, ...args] = argv;
64
+ const cwd = exec.agent.session.header.cwd;
65
+ if (a.workdir !== undefined) {
66
+ const checked = await checkPaths([a.workdir], "write", cwd, ctx, c, signal);
67
+ if (checked.kind !== "allow") return checked;
68
+ }
69
+ const base =
70
+ a.workdir === undefined
71
+ ? cwd
72
+ : String((await ctx.fs.resolve(a.workdir, { cwd, signal })).targetKey);
73
+ if (
74
+ c.shell.trustedCommands.some(
75
+ (cmd) => cmd.executable === exe && same(cmd.args, args),
76
+ )
77
+ )
78
+ return allow("TRUSTED_COMMAND", "Administrator configured this exact argv");
79
+ const isPwsh = exec.name === "pwsh";
80
+ // PowerShell resolves aliases and case-insensitive names; only canonical cmdlets are admitted.
81
+ const readExe = c.shell.readCommands.find((x) =>
82
+ isPwsh ? x.toLowerCase() === exe.toLowerCase() : x === exe,
83
+ );
84
+ if (readExe) {
85
+ if (isPwsh) {
86
+ if (
87
+ ![
88
+ "Get-Location",
89
+ "Get-ChildItem",
90
+ "Get-Content",
91
+ "Write-Output",
92
+ ].includes(readExe)
93
+ )
94
+ return escalate("UNKNOWN_COMMAND", "Unknown PowerShell read semantics");
95
+ if (readExe === "Get-Location")
96
+ return args.length
97
+ ? escalate("SHELL_OPTIONS", "Unsupported parameters")
98
+ : allow("READ_COMMAND", "Get-Location");
99
+ if (readExe === "Write-Output")
100
+ return args.some((x) => x.startsWith("-"))
101
+ ? escalate("SHELL_OPTIONS", "Unsupported parameters")
102
+ : allow("READ_COMMAND", "Literal output");
103
+ if (args.some((x) => x.startsWith("-")))
104
+ return escalate("SHELL_OPTIONS", "PowerShell options require review");
105
+ return checkPaths(
106
+ args.length ? args : [base],
107
+ "read",
108
+ base,
109
+ ctx,
110
+ c,
111
+ signal,
112
+ );
113
+ }
114
+ if (exe === "pwd")
115
+ return args.length
116
+ ? escalate("SHELL_OPTIONS", "Unsupported pwd arguments")
117
+ : allow("READ_COMMAND", "Print current directory");
118
+ if (exe === "echo") return allow("READ_COMMAND", "Literal output");
119
+ const safeOptions = {
120
+ ls: new Set([
121
+ "-a",
122
+ "-l",
123
+ "-h",
124
+ "-la",
125
+ "-al",
126
+ "-lh",
127
+ "-lah",
128
+ "-R",
129
+ "--all",
130
+ ]),
131
+ cat: new Set(["-n", "-b", "--number"]),
132
+ head: new Set(["-n", "-c"]),
133
+ tail: new Set(["-n", "-c"]),
134
+ wc: new Set(["-l", "-w", "-c", "-m"]),
135
+ rg: new Set([
136
+ "-n",
137
+ "-l",
138
+ "-i",
139
+ "--files",
140
+ "--hidden",
141
+ "--no-ignore",
142
+ "-F",
143
+ "--fixed-strings",
144
+ "--count",
145
+ ]),
146
+ grep: new Set(["-n", "-l", "-i", "-r", "-R", "-F", "--count"]),
147
+ };
148
+ if (!safeOptions[exe])
149
+ return escalate(
150
+ "UNKNOWN_COMMAND",
151
+ "Configured command has no built-in semantics; use trustedCommands for exact argv",
152
+ );
153
+ let positional = [];
154
+ for (let i = 0; i < args.length; i++) {
155
+ const arg = args[i];
156
+ if (arg === "--") {
157
+ positional.push(...args.slice(i + 1));
158
+ break;
159
+ }
160
+ if (arg.startsWith("-")) {
161
+ if (!safeOptions[exe].has(arg))
162
+ return escalate("SHELL_OPTIONS", `Unsupported ${exe} option ${arg}`);
163
+ if (
164
+ (exe === "head" || exe === "tail") &&
165
+ (arg === "-n" || arg === "-c")
166
+ ) {
167
+ if (!/^\d+$/.test(args[++i] ?? ""))
168
+ return escalate("SHELL_OPTIONS", "Invalid count");
169
+ }
170
+ } else positional.push(arg);
171
+ }
172
+ if ((exe === "rg" && !args.includes("--files")) || exe === "grep") {
173
+ if (!positional.length)
174
+ return escalate("SHELL_OPTIONS", "Missing search pattern");
175
+ positional = positional.slice(1);
176
+ }
177
+ return checkPaths(
178
+ positional.length ? positional : [base],
179
+ "read",
180
+ base,
181
+ ctx,
182
+ c,
183
+ signal,
184
+ );
185
+ }
186
+ if (c.shell.workspaceMutationCommands.includes(exe)) {
187
+ if (isPwsh || !["mkdir", "touch"].includes(exe))
188
+ return escalate("UNKNOWN_COMMAND", "No built-in mutation semantics");
189
+ const paths = args.filter(
190
+ (x) => !(exe === "mkdir" && x === "-p") && x !== "--",
191
+ );
192
+ if (!paths.length || paths.some((x) => x.startsWith("-")))
193
+ return escalate("SHELL_OPTIONS", "Unsupported mutation options");
194
+ return checkPaths(paths, "write", base, ctx, c, signal);
195
+ }
196
+ return escalate("UNKNOWN_COMMAND", `Command ${exe} requires semantic review`);
197
+ }