@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/src/index.js ADDED
@@ -0,0 +1,396 @@
1
+ /** DCAR 1.1.0: CAutoR permission preset and deterministic-first tool gate. */
2
+ import z from "@deepseek-ai/schemastery";
3
+ import { DEFAULTS, resolveConfig, patchConfig } from "./config.js";
4
+ import { ReviewEngine } from "./engine.js";
5
+ import { attachPermissionAdapter } from "./permission-adapter.js";
6
+ import { attachDefaultPersistence } from "./preset-defaults.js";
7
+ import { setApprovalPolicy } from "@deepseek-ai/dsh-user-approval";
8
+ const handoffKey = Symbol.for("dcar.permission-handoff");
9
+ export const name = "@lytharalab/dsh-cons-auto-review";
10
+ export const inject = [
11
+ "approval",
12
+ "llm",
13
+ "permissionPresets",
14
+ "sessions",
15
+ "tools",
16
+ "fs",
17
+ "sessionProjections",
18
+ ];
19
+ export const PRESET = "CAutoR";
20
+ function schema(value) {
21
+ if (Array.isArray(value)) return z.array(z.string()).default(value);
22
+ if (value && typeof value === "object")
23
+ return z
24
+ .object(
25
+ Object.fromEntries(
26
+ Object.entries(value).map(([k, v]) => [k, schema(v)]),
27
+ ),
28
+ )
29
+ .default(value);
30
+ return (
31
+ typeof value === "boolean"
32
+ ? z.boolean()
33
+ : typeof value === "number"
34
+ ? z.number()
35
+ : z.string()
36
+ ).default(value);
37
+ }
38
+ const fields = Object.fromEntries(
39
+ Object.entries(DEFAULTS).map(([k, v]) => [k, schema(v)]),
40
+ );
41
+ fields.rules = z
42
+ .object({
43
+ ...Object.fromEntries(
44
+ Object.entries(DEFAULTS.rules)
45
+ .filter(([k]) => k !== "customTools")
46
+ .map(([k, v]) => [k, schema(v)]),
47
+ ),
48
+ customTools: z
49
+ .array(
50
+ z.object({
51
+ name: z.string().required(),
52
+ effect: z.union(["read", "write"]).required(),
53
+ pathFields: z.array(z.string()).required(),
54
+ }),
55
+ )
56
+ .default([]),
57
+ })
58
+ .default(DEFAULTS.rules);
59
+ fields.shell = z
60
+ .object({
61
+ ...Object.fromEntries(
62
+ Object.entries(DEFAULTS.shell)
63
+ .filter(([k]) => k !== "trustedCommands")
64
+ .map(([k, v]) => [k, schema(v)]),
65
+ ),
66
+ trustedCommands: z
67
+ .array(
68
+ z.object({
69
+ executable: z.string().required(),
70
+ args: z.array(z.string()).required(),
71
+ }),
72
+ )
73
+ .default([]),
74
+ })
75
+ .default(DEFAULTS.shell);
76
+ /** DSH plugin settings schema; full semantic validation runs at activation. */
77
+ export const Config = z.object(fields);
78
+ export { resolveConfig, DEFAULTS } from "./config.js";
79
+ export { ReviewEngine } from "./engine.js";
80
+ /** Install the gate before exposing any command that selects CAutoR. */
81
+ export function apply(ctx, input = {}) {
82
+ const config = resolveConfig(input),
83
+ presets = ctx.permissionPresets;
84
+ if (
85
+ typeof ctx.tools.get !== "function" ||
86
+ typeof presets.catalog !== "function" ||
87
+ typeof ctx.fs.resolve !== "function" ||
88
+ typeof ctx.llm.stream !== "function"
89
+ )
90
+ throw new Error(
91
+ "DCAR requires the DSH tools, filesystem, permission catalog and LLM extension APIs; see docs/COMPATIBILITY.md",
92
+ );
93
+ const fallback = presets.resolve(config.lifecycle.fallbackPreset);
94
+ if (fallback.sandbox === "danger-full-access")
95
+ throw new Error("DCAR disposal fallback must be a confined preset");
96
+ const engine = new ReviewEngine(ctx, config),
97
+ overrides = new WeakMap(),
98
+ inherited = new WeakSet(),
99
+ active = new Set();
100
+ let handoff = Object.getOwnPropertyDescriptor(ctx.fiber, handoffKey)?.value;
101
+ if (!handoff) {
102
+ handoff = new WeakMap();
103
+ Object.defineProperty(ctx.fiber, handoffKey, { value: handoff });
104
+ }
105
+ let accepting = true;
106
+ let stopGate, stopDefaultPersistence, stopSessionCreated, stopAgentCreated;
107
+ const admit = () => {
108
+ if (!accepting || !config.enabled) throw new Error("DCAR is disabled or closing");
109
+ };
110
+ const getConfig = (session) => overrides.get(session) ?? config;
111
+ function selected(session) {
112
+ if (presets.current(session) === PRESET) return true;
113
+ const state = ctx.sessionProjections.stateOf(session, "permissions");
114
+ // In-process subagents pin approval=never; configured custom presets otherwise derive as custom.
115
+ return state?.preset === PRESET && state.sandbox === "danger-full-access";
116
+ }
117
+ function markInherited(session) {
118
+ if (!config.inheritSubagents || !session.header.parentSession) return;
119
+ const parent = ctx.sessions.get(session.header.parentSession);
120
+ if (!parent) return;
121
+ const childState = ctx.sessionProjections.stateOf(session, "permissions");
122
+ const capturedReview =
123
+ childState?.sandbox === "danger-full-access" &&
124
+ childState.approval === "never" &&
125
+ childState.preset !== "danger-full-access" &&
126
+ childState.preset !== "auto" &&
127
+ parent
128
+ .snapshotEvents()
129
+ .some(
130
+ (event) =>
131
+ event.type === "permission/preset" && event.data.preset === PRESET,
132
+ );
133
+ // DSH captures its delegation knobs before awaiting child creation. Keep review
134
+ // if the parent switched away while a captured custom review child was pending.
135
+ if (selected(parent) || inherited.has(parent) || capturedReview)
136
+ inherited.add(session);
137
+ }
138
+ ctx.effect(function* () {
139
+ yield attachPermissionAdapter(presets);
140
+ for (const session of ctx.sessions.list()) markInherited(session);
141
+ yield stopSessionCreated = ctx.on(
142
+ "session/created",
143
+ (session) => {
144
+ if (!accepting) return;
145
+ markInherited(session);
146
+ const state = ctx.sessionProjections.stateOf(session, "permissions");
147
+ if (!session.header.parentSession && state?.preset === null &&
148
+ state.sandbox === null && state.approval === null && !state.seeded &&
149
+ presets.defaultPreset === PRESET)
150
+ presets.set(session, accepting && config.enabled ? PRESET : config.lifecycle.fallbackPreset);
151
+ },
152
+ { prepend: true },
153
+ );
154
+ yield stopAgentCreated = ctx.on("agent/created", ({ agent }) => {
155
+ if (!accepting) return;
156
+ if (inherited.has(agent.session))
157
+ agent.session.append("permission/preset", { preset: PRESET });
158
+ });
159
+ yield stopGate = ctx.on(
160
+ "tools/pre-execute",
161
+ async (exec, next) => {
162
+ if (
163
+ !exec.agent ||
164
+ (exec.parent === undefined && exec.name === "run_code")
165
+ )
166
+ return next();
167
+ if (!selected(exec.agent.session) && !inherited.has(exec.agent.session))
168
+ return next();
169
+ if (!accepting) return { kind: "cancel" };
170
+ let done;
171
+ const completed = new Promise((resolve) => {
172
+ done = resolve;
173
+ });
174
+ active.add(completed);
175
+ try {
176
+ const c = getConfig(exec.agent.session);
177
+ const decision = c.enabled
178
+ ? await engine.review(exec, c)
179
+ : engine.nonAllow(
180
+ exec,
181
+ "ask",
182
+ "DCAR is disabled; select another permission mode",
183
+ "DCAR_REVIEW_FAILED",
184
+ );
185
+ if (!accepting || exec.signal.aborted) return { kind: "cancel" };
186
+ // A DCAR allow delegates: other policy listeners and DSH tool guards retain their decisions.
187
+ if (decision.kind !== "allow") return decision;
188
+ return await next();
189
+ } finally {
190
+ active.delete(completed);
191
+ done();
192
+ }
193
+ },
194
+ { prepend: true },
195
+ );
196
+ yield presets.registerReviewPreset(
197
+ PRESET,
198
+ {
199
+ sandbox: "danger-full-access",
200
+ approval: "ask",
201
+ name: PRESET,
202
+ description: "Con's 自动审查:程序直接放行安全操作,其余交给 LLM。",
203
+ },
204
+ admit,
205
+ );
206
+ // Dependency reloads reuse this plugin fiber. Restore only the exact session
207
+ // cursor we confined on unload; a later user permission change invalidates it.
208
+ for (const session of ctx.sessions.list()) {
209
+ const pending = handoff.get(session);
210
+ handoff.delete(session);
211
+ if (!config.enabled || !pending || pending.seq !== session.seq) continue;
212
+ presets.apply(session, PRESET, (policy) => setApprovalPolicy(session, pending.approval === "never" ? "never" : policy));
213
+ if (pending.inherited) inherited.add(session);
214
+ }
215
+ // A session created while the persisted default's provider was absent starts
216
+ // confined. Restore an untouched fresh root only after the review gate is live.
217
+ if (config.enabled)
218
+ for (const session of ctx.sessions.list()) {
219
+ const state = ctx.sessionProjections.stateOf(session, "permissions");
220
+ if (session.header.parentSession || state?.preset !== PRESET ||
221
+ state.sandbox !== "read-only" || state.approval !== "never") continue;
222
+ const events = session.snapshotEvents();
223
+ if (events.some((event) => ["turn/start", "user/message", "assistant/message"].includes(event.type)) ||
224
+ events.filter((event) => event.type === "sandbox/mode").length !== 1 ||
225
+ events.filter((event) => event.type === "approval/policy").length !== 1) continue;
226
+ presets.set(session, PRESET);
227
+ }
228
+ ctx.inject(["configEditor"], (child) => {
229
+ child.effect(() => {
230
+ const dispose = attachDefaultPersistence(child.configEditor, presets, admit);
231
+ stopDefaultPersistence = dispose;
232
+ return dispose;
233
+ });
234
+ });
235
+ if (config.commands.enabled)
236
+ ctx.inject(["commands"], (child) => {
237
+ child.effect(() =>
238
+ child.commands.register({
239
+ name: config.commands.name,
240
+ description: "Con's 自动审查:切换 CAutoR、查看统计、历史和配置",
241
+ input: {
242
+ hint: "on | off | status | stats | history [N] | config [JSON] | reset | clear-cache | check <tool> <JSON>",
243
+ },
244
+ handler: async ({ agent, rawInput, signal }) => {
245
+ const text = rawInput.trim(),
246
+ space = text.indexOf(" "),
247
+ cmd = space < 0 ? text : text.slice(0, space),
248
+ rest = space < 0 ? "" : text.slice(space + 1).trim(),
249
+ session = agent.session;
250
+ try {
251
+ if (cmd === "on") {
252
+ if (!getConfig(session).enabled)
253
+ throw new Error("DCAR is disabled in configuration");
254
+ presets.set(session, PRESET);
255
+ return { kind: "success", text: "CAutoR 已启用。" };
256
+ }
257
+ if (cmd === "off") {
258
+ presets.set(session, config.lifecycle.fallbackPreset);
259
+ inherited.delete(session);
260
+ return {
261
+ kind: "success",
262
+ text: `已切换至 ${config.lifecycle.fallbackPreset}。`,
263
+ };
264
+ }
265
+ if (cmd === "config") {
266
+ if (rest) {
267
+ if (!config.commands.allowSessionConfig)
268
+ throw new Error("Session configuration is disabled");
269
+ const patch = JSON.parse(rest);
270
+ if (
271
+ Object.keys(patch).some(
272
+ (k) =>
273
+ ![
274
+ "enabled",
275
+ "rules",
276
+ "shell",
277
+ "review",
278
+ "cache",
279
+ "audit",
280
+ ].includes(k),
281
+ )
282
+ )
283
+ throw new Error(
284
+ "Change lifecycle, commands and inheritSubagents in the profile configuration and restart DSH",
285
+ );
286
+ overrides.set(
287
+ session,
288
+ patchConfig(getConfig(session), patch),
289
+ );
290
+ engine.clearCache();
291
+ }
292
+ return {
293
+ kind: "success",
294
+ text: JSON.stringify(getConfig(session), null, 2),
295
+ };
296
+ }
297
+ if (cmd === "stats")
298
+ return {
299
+ kind: "success",
300
+ text: JSON.stringify(
301
+ engine.audit.stats(session.id),
302
+ null,
303
+ 2,
304
+ ),
305
+ };
306
+ if (cmd === "history") {
307
+ const n = rest ? Number(rest) : 20;
308
+ if (
309
+ !Number.isSafeInteger(n) ||
310
+ n < 1 ||
311
+ n > getConfig(session).audit.historyLimit
312
+ )
313
+ throw new Error(
314
+ `N must be 1..${getConfig(session).audit.historyLimit}`,
315
+ );
316
+ return {
317
+ kind: "success",
318
+ text: JSON.stringify(
319
+ engine.audit.history(session.id, n),
320
+ null,
321
+ 2,
322
+ ),
323
+ };
324
+ }
325
+ if (cmd === "reset") {
326
+ engine.audit.reset(session.id);
327
+ overrides.delete(session);
328
+ engine.clearCache();
329
+ return {
330
+ kind: "success",
331
+ text: "会话配置、统计和审查缓存已重置。",
332
+ };
333
+ }
334
+ if (cmd === "clear-cache") {
335
+ engine.clearCache();
336
+ return { kind: "success", text: "审查缓存已清空。" };
337
+ }
338
+ if (cmd === "check") {
339
+ const i = rest.indexOf(" ");
340
+ if (i < 1)
341
+ throw new Error("Usage: /dcar check <tool> <JSON>");
342
+ const { deterministic } = await import("./deterministic.js");
343
+ const result = await deterministic(
344
+ {
345
+ name: rest.slice(0, i),
346
+ arguments: JSON.parse(rest.slice(i + 1)),
347
+ agent,
348
+ signal,
349
+ },
350
+ ctx,
351
+ getConfig(session),
352
+ signal,
353
+ );
354
+ return {
355
+ kind: "success",
356
+ text: JSON.stringify(result, null, 2),
357
+ };
358
+ }
359
+ if (cmd === "" || cmd === "status")
360
+ return {
361
+ kind: "success",
362
+ text: `DCAR 1.1.0 | 当前权限:${presets.current(session)} | CAutoR 审查:${selected(session) || inherited.has(session) ? "启用" : "未启用"}\n/dcar on · off · stats · history · config · reset · clear-cache · check`,
363
+ };
364
+ return {
365
+ kind: "error",
366
+ text: `未知子命令 ${cmd};使用 /${config.commands.name} 查看帮助。`,
367
+ };
368
+ } catch (error) {
369
+ return { kind: "error", text: String(error.message ?? error) };
370
+ }
371
+ },
372
+ }),
373
+ );
374
+ });
375
+ yield async () => {
376
+ accepting = false;
377
+ engine.lifecycle.abort(new Error("DCAR plugin disposed"));
378
+ try {
379
+ for (const session of ctx.sessions.list())
380
+ if (selected(session) || inherited.has(session)) {
381
+ const state = ctx.sessionProjections.stateOf(session, "permissions");
382
+ const wasInherited = inherited.has(session);
383
+ presets.set(session, config.lifecycle.fallbackPreset);
384
+ handoff.set(session, { seq: session.seq, approval: state?.approval, inherited: wasInherited });
385
+ }
386
+ } finally {
387
+ stopGate?.();
388
+ stopSessionCreated?.();
389
+ stopAgentCreated?.();
390
+ stopDefaultPersistence?.();
391
+ await Promise.allSettled([...active]);
392
+ await engine.stop();
393
+ }
394
+ };
395
+ }, "DCAR authorization lifecycle");
396
+ }
package/src/paths.js ADDED
@@ -0,0 +1,94 @@
1
+ /** DSH filesystem resolution + the upstream canonical containment check. */
2
+ import { basename, isAbsolute, resolve } from "node:path";
3
+ import { writableRoots } from "@deepseek-ai/dsh-sandbox";
4
+ import { isPathUnder } from "./containment.js";
5
+ export const allow = (code, reason) => ({ kind: "allow", code, reason });
6
+ export const escalate = (code, reason) => ({ kind: "escalate", code, reason });
7
+ const regexCache = new Map();
8
+ /** Match a slash-normalized full path against an administrator-configured glob. */
9
+ export function matchesGlob(path, pattern) {
10
+ let r = regexCache.get(pattern);
11
+ if (!r) {
12
+ let s = "";
13
+ for (let i = 0; i < pattern.length; i++) {
14
+ const ch = pattern[i];
15
+ if (ch === "*" && pattern[i + 1] === "*") {
16
+ i++;
17
+ if (pattern[i + 1] === "/") {
18
+ i++;
19
+ s += "(?:.*/)?";
20
+ } else s += ".*";
21
+ } else if (ch === "*") s += "[^/]*";
22
+ else if (ch === "?") s += "[^/]";
23
+ else s += ch.replace(/[\\^$+?.()|{}[\]]/g, "\\$&");
24
+ }
25
+ r = new RegExp(`^${s}$`, process.platform === "win32" ? "i" : "");
26
+ if (regexCache.size > 500) regexCache.clear();
27
+ regexCache.set(pattern, r);
28
+ }
29
+ return r.test(path.replaceAll("\\", "/"));
30
+ }
31
+ export function protectedPath(path, config) {
32
+ const sample = config.rules.safeEnvironmentFiles.includes(basename(path));
33
+ const normalized = path.replaceAll("\\", "/");
34
+ return config.rules.protectedPaths.some(
35
+ (p) =>
36
+ !(sample && p.startsWith("**/.env")) &&
37
+ (matchesGlob(normalized, p) || matchesGlob(`${normalized}/`, p)),
38
+ );
39
+ }
40
+ /** Check every path, including aliases and missing targets, without changing sandbox policy. */
41
+ export async function checkPaths(paths, effect, cwd, ctx, config, signal) {
42
+ if (!paths.length)
43
+ return escalate("INVALID_PATH", "No target paths supplied");
44
+ if (!isAbsolute(cwd))
45
+ return escalate("WORKSPACE_UNAVAILABLE", "No absolute session workspace");
46
+ const roots = config.rules.includeTempRoots
47
+ ? writableRoots({ mode: "workspace-write", workspaceRoot: cwd })
48
+ : [cwd];
49
+ roots.push(
50
+ ...config.rules.additionalWritableRoots.map((p) =>
51
+ isAbsolute(p) ? p : resolve(cwd, p),
52
+ ),
53
+ );
54
+ for (const path of paths) {
55
+ if (typeof path !== "string" || !path.trim())
56
+ return escalate("INVALID_PATH", "Path arguments are missing or invalid");
57
+ // Windows alternate data streams, devices, and ambiguous namespace spellings require review.
58
+ if (
59
+ process.platform === "win32" &&
60
+ (/^\\\\[?.]\\/.test(path) ||
61
+ /(^|[\\/])(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])(?:\.|$)/i.test(path) ||
62
+ /:/.test(path.replace(/^[a-z]:/i, "")))
63
+ )
64
+ return escalate(
65
+ "WINDOWS_SPECIAL_PATH",
66
+ "Device paths or alternate data streams require review",
67
+ );
68
+ const target = await ctx.fs.resolve(path, { cwd, signal });
69
+ const key = String(target.targetKey);
70
+ if (protectedPath(key, config) || protectedPath(target.displayPath, config))
71
+ return escalate(
72
+ "PROTECTED_PATH",
73
+ `Protected path: ${target.displayPath}`,
74
+ );
75
+ if (effect === "read" && config.rules.allowOutsideReads) continue;
76
+ let contained = false;
77
+ for (const root of roots) {
78
+ const canonical = await ctx.fs.resolve(root, { cwd, signal });
79
+ if (await isPathUnder(key, String(canonical.targetKey))) {
80
+ contained = true;
81
+ break;
82
+ }
83
+ }
84
+ if (!contained)
85
+ return escalate(
86
+ "OUTSIDE_WORKSPACE",
87
+ `Path is outside permitted ${effect} roots: ${target.displayPath}`,
88
+ );
89
+ }
90
+ return allow(
91
+ effect === "write" ? "WORKSPACE_WRITE" : "SAFE_READ",
92
+ `All ${effect} paths passed DSH workspace rules`,
93
+ );
94
+ }
@@ -0,0 +1,75 @@
1
+ /** Extend the live host service; no profile row replacement or second service. */
2
+ import { registerReviewPreset } from "./preset-defaults.js";
3
+ function descriptorOf(service, name) {
4
+ for (let object = service; object; object = Object.getPrototypeOf(object)) {
5
+ const descriptor = Object.getOwnPropertyDescriptor(object, name);
6
+ if (descriptor) return descriptor;
7
+ }
8
+ }
9
+
10
+ /**
11
+ * Patch only this service instance for the DCAR effect lifetime. Methods use
12
+ * their calling `this`, so Cordis caller-context proxies and the original
13
+ * service's projection/command closures all see the same registration.
14
+ */
15
+ export function attachPermissionAdapter(service) {
16
+ // Compatibility with the 1.0.0 adapter that some existing profiles retain.
17
+ if (typeof service.registerReviewPreset === "function") return () => {};
18
+ const keys = ["names", "specOf", "derive", "apply", "registerReviewPreset"];
19
+ const own = new Map(keys.map((key) => [key, Object.getOwnPropertyDescriptor(service, key)]));
20
+ const names = descriptorOf(service, "names")?.get;
21
+ const original = Object.fromEntries(
22
+ ["specOf", "derive", "apply"].map((key) => [key, descriptorOf(service, key)?.value]),
23
+ );
24
+ if (typeof names !== "function" ||
25
+ Object.values(original).some((method) => typeof method !== "function") ||
26
+ typeof service.emitCatalogChanged !== "function")
27
+ throw new Error("DCAR requires the DSH 0.1.7-rc.2 / 0.2.0-rc.2 permission service runtime APIs");
28
+ if (!Object.isExtensible(service) || [...own.values()].some((d) => d && !d.configurable))
29
+ throw new Error("DCAR cannot extend this permission service instance");
30
+ const entries = new Map();
31
+ const installed = {
32
+ names: { get() { return [...new Set([...names.call(this), ...entries.keys()])]; } },
33
+ specOf: { value: function (name) {
34
+ return entries.get(name)?.spec ?? original.specOf.call(this, name);
35
+ } },
36
+ derive: { value: function (state) {
37
+ const entry = entries.get(state.preset);
38
+ if (entry && state.sandbox === entry.spec.sandbox &&
39
+ (state.approval === entry.spec.approval || state.approval === "never"))
40
+ return state.preset;
41
+ const derived = original.derive.call(this, state);
42
+ return derived === "CAutoR" && entries.has("CAutoR") ? "custom" : derived;
43
+ } },
44
+ apply: { value: function (session, name, setApproval) {
45
+ entries.get(name)?.admit();
46
+ return original.apply.call(this, session, name, setApproval);
47
+ } },
48
+ registerReviewPreset: { value: function (name, spec, admit) {
49
+ return registerReviewPreset(this, entries, name, spec, admit);
50
+ } },
51
+ };
52
+ // Validate before changing anything; rollback an unexpected partial failure.
53
+ const changed = [];
54
+ const restore = () => {
55
+ for (const key of [...changed].reverse()) {
56
+ const prior = own.get(key);
57
+ if (prior) Object.defineProperty(service, key, prior);
58
+ else Reflect.deleteProperty(service, key);
59
+ }
60
+ };
61
+ try {
62
+ for (const key of keys) {
63
+ Object.defineProperty(service, key, { ...installed[key], configurable: true });
64
+ changed.push(key);
65
+ }
66
+ } catch (error) { restore(); throw error; }
67
+ let live = true;
68
+ return () => {
69
+ if (!live) return;
70
+ live = false;
71
+ entries.clear();
72
+ restore();
73
+ service.emitCatalogChanged();
74
+ };
75
+ }
@@ -0,0 +1,77 @@
1
+ /**
2
+ * DSH 0.1.7-rc.2 / 0.2.0-rc.2 permission service adapter.
3
+ * Adds effect-scoped review presets without changing DSH's built-in knob setters,
4
+ * /permission command, session projection, catalog Remote, or official Auto integration.
5
+ */
6
+ import PermissionPresetService from "@deepseek-ai/dsh-permission-presets";
7
+ import { registerReviewPreset } from "./preset-defaults.js";
8
+ function entries(service) {
9
+ return service.dcarReviewPresets;
10
+ }
11
+ export default class DCARPermissionPresetService extends PermissionPresetService {
12
+ constructor(ctx, config) {
13
+ for (const method of ["specOf", "derive", "apply", "emitCatalogChanged"])
14
+ if (typeof PermissionPresetService.prototype[method] !== "function")
15
+ throw new Error(
16
+ `DCAR permission adapter requires DSH 0.1.7-rc.2 or 0.2.0-rc.2 (${method} is unavailable)`,
17
+ );
18
+ super(ctx, config);
19
+ this.dcarReviewPresets = new Map();
20
+ // A restored CAutoR session cannot execute while its review provider is absent.
21
+ ctx.inject(["tools"], (child) => {
22
+ child.on(
23
+ "tools/pre-execute",
24
+ async (exec, next) => {
25
+ if (exec.agent) {
26
+ const state = ctx.sessionProjections.stateOf(
27
+ exec.agent.session,
28
+ "permissions",
29
+ );
30
+ if (
31
+ state?.preset === "CAutoR" &&
32
+ state.sandbox === "danger-full-access" &&
33
+ !entries(this)?.has("CAutoR")
34
+ )
35
+ return {
36
+ kind: "deny",
37
+ reason:
38
+ "CAutoR review provider is unavailable; select workspace-write or enable DCAR.",
39
+ info: {
40
+ name: "DCARUnavailableError",
41
+ code: "DCAR_UNAVAILABLE",
42
+ },
43
+ };
44
+ }
45
+ return next();
46
+ },
47
+ { prepend: true },
48
+ );
49
+ });
50
+ }
51
+ get names() {
52
+ return [...new Set([...super.names, ...(entries(this)?.keys() ?? [])])];
53
+ }
54
+ specOf(name) {
55
+ return entries(this)?.get(name)?.spec ?? super.specOf(name);
56
+ }
57
+ derive(state) {
58
+ const entry =
59
+ state.preset === null ? undefined : entries(this)?.get(state.preset);
60
+ if (
61
+ entry &&
62
+ state.sandbox === entry.spec.sandbox &&
63
+ (state.approval === entry.spec.approval || state.approval === "never")
64
+ )
65
+ return state.preset;
66
+ const derived = super.derive(state);
67
+ return derived === "CAutoR" && entries(this)?.has("CAutoR") ? "custom" : derived;
68
+ }
69
+ apply(session, name, setApproval) {
70
+ entries(this)?.get(name)?.admit();
71
+ return super.apply(session, name, setApproval);
72
+ }
73
+ /** Register an effect-owned custom review mode and its synchronous selection guard. */
74
+ registerReviewPreset(name, spec, admit) {
75
+ return registerReviewPreset(this, entries(this), name, spec, admit);
76
+ }
77
+ }
@@ -0,0 +1,48 @@
1
+ /** Chinese human-facing prompts; raw diagnostics stay in the audit/agent error. */
2
+ function visible(value) {
3
+ return String(value).replace(
4
+ /[\u0000-\u0008\u000b-\u001f\u007f\u202a-\u202e\u2066-\u2069]/g,
5
+ "",
6
+ );
7
+ }
8
+ export function bounded(text, limit) {
9
+ const value = visible(text);
10
+ return value.length <= limit
11
+ ? value
12
+ : `${value.slice(0, limit)}\n(内容过长,展示已截断)`;
13
+ }
14
+ export function actionText(exec, c) {
15
+ if (!c.review.showAction) return "";
16
+ const args = exec.arguments;
17
+ let body;
18
+ if (args && typeof args === "object" && typeof args.command === "string") {
19
+ const { command, ...rest } = args;
20
+ body =
21
+ `命令:${command}` +
22
+ (Object.keys(rest).length ? `\n其他参数:${JSON.stringify(rest)}` : "");
23
+ } else {
24
+ try {
25
+ body = `参数:${JSON.stringify(args) ?? "(无)"}`;
26
+ } catch {
27
+ body = "参数无法展示,请查看原生工具详情。";
28
+ }
29
+ }
30
+ return `\n\n待执行工具:${visible(exec.name)}\n${bounded(body, c.review.maxActionChars)}`;
31
+ }
32
+ export function reviewFailureMessage(error, c, trace) {
33
+ const message = String(error.message ?? error);
34
+ if (error.code === "DCAR_MAX_TOKENS")
35
+ return `自动审查达到输出 token 上限,未取得完整结论(最后一次预算 ${trace.lastMaxTokens},配置上限 ${c.review.maxTokensLimit},已扩容重试 ${trace.tokenRetries} 次)。`;
36
+ if (error.code === "DCAR_REVIEW_TIMEOUT")
37
+ return `自动审查超时(总时限 ${c.review.timeoutMs} 毫秒,包含排队、重试和中文翻译),未取得完整结论。`;
38
+ if (error.code === "DCAR_REVIEW_INPUT")
39
+ return `授权上下文超过 ${c.review.maxInputChars} 字符的配置上限,自动审查未完成。`;
40
+ if (message === "DCAR review queue is full")
41
+ return "自动审查等待队列已满,未取得审查结论。";
42
+ if (["DCAR_REVIEW_INVALID", "DCAR_REVIEW_STREAM"].includes(error.code))
43
+ return "自动审查返回的结论格式无效或响应不完整,无法据此批准。";
44
+ return "自动审查服务请求失败,未取得有效结论。";
45
+ }
46
+ export function approvalText(exec, c, explanation) {
47
+ return `Con's 自动审查:是否允许本次操作?\n${visible(explanation)}${actionText(exec, c)}\n\n允许仅针对这一次调用;拒绝则不执行。`;
48
+ }