@klarkxy/dsh-safe-auto 0.3.0 → 0.4.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/README.md +3 -3
- package/README.zh-CN.md +3 -3
- package/dsh.plugin.json +2 -2
- package/lib/client.js +58 -31
- package/package.json +18 -10
- package/src/index.js +2 -2
- package/src/model-route.js +0 -2
- package/src/reviewer.js +11 -6
- package/src/ui-host.js +3 -0
package/README.md
CHANGED
|
@@ -18,7 +18,7 @@ Replace `web` with your profile name and restart the profile.
|
|
|
18
18
|
|
|
19
19
|
Pick **Safe Auto** from the permission menu next to the chat input. It applies only to that session; choosing any other level turns it off. Nothing happens automatically until you pick it.
|
|
20
20
|
|
|
21
|
-
Under **Plugins → Safe Auto** you can choose the reviewer model (empty follows the current session model), its reasoning effort, and extra review rules.
|
|
21
|
+
Under **Plugins → Safe Auto** you can choose the reviewer model (empty follows the current session model), its reasoning effort, and extra review rules. Changes made elsewhere update this page on their own; Model Hub is not required.
|
|
22
22
|
|
|
23
23
|
If the model list cannot load, or some providers fail, use **Retry** in the settings panel. Retrying keeps your reviewer settings and unsaved draft.
|
|
24
24
|
|
|
@@ -32,9 +32,9 @@ Reading files, searching, building, and testing need no approval; they are not i
|
|
|
32
32
|
|
|
33
33
|
## How the reviewer decides
|
|
34
34
|
|
|
35
|
-
- The reviewer sees the conversation, the working directory, and the complete arguments of the action. Only your own messages count as authorization; the agent's explanations, tool output, and file contents are treated as evidence.
|
|
35
|
+
- The reviewer sees the conversation, the current working directory, the sandbox workspace boundary, and the complete arguments of the action. Only your own messages count as authorization; the agent's explanations, tool output, and file contents are treated as evidence.
|
|
36
36
|
- Low- and medium-risk actions are approved. High-risk actions are approved only when you clearly asked for them and the scope is narrow. Critical actions, such as sending secrets to an external destination or broad irreversible deletion, are always denied.
|
|
37
|
-
- If the review times out, fails, or no model is available, the request is not approved, and the agent is told this was a review failure: it may retry once or ask you.
|
|
37
|
+
- If the review times out, returns missing or unknown verdict fields, fails, or no model is available, the request is not approved, and the agent is told this was a review failure: it may retry once or ask you.
|
|
38
38
|
- After 3 consecutive denials, or 10 denials among the last 50 reviews in one turn, automatic approval pauses until your next message.
|
|
39
39
|
|
|
40
40
|
## Limits
|
package/README.zh-CN.md
CHANGED
|
@@ -18,7 +18,7 @@ dsh plugin --profile web add @klarkxy/dsh-safe-auto
|
|
|
18
18
|
|
|
19
19
|
在输入框旁的权限菜单中选择「安全自动」。它只对当前会话生效,选回其他档位即关闭;选择之前不会有任何自动行为。
|
|
20
20
|
|
|
21
|
-
在「插件 →
|
|
21
|
+
在「插件 → 安全自动」中可以选择审核模型(留空跟随当前会话模型)、思考强度,以及额外的审核要求。设置在别处改动时,这个页面会自行更新,不需要安装模型枢纽。
|
|
22
22
|
|
|
23
23
|
模型列表加载失败或部分提供商不可用时,可在设置面板点击「重试」。重试会保留审核配置和未保存的草稿。
|
|
24
24
|
|
|
@@ -32,9 +32,9 @@ dsh plugin --profile web add @klarkxy/dsh-safe-auto
|
|
|
32
32
|
|
|
33
33
|
## 审核怎么判断
|
|
34
34
|
|
|
35
|
-
-
|
|
35
|
+
- 审核模型会看到对话记录、当前工作目录、沙箱工作区边界和这次操作的完整参数。只有你发的消息算授权,智能体的说明、工具输出和文件内容都只作参考。
|
|
36
36
|
- 低风险和中风险的操作放行;高风险只在你明确要求并且范围有限时放行;严重风险(例如把密钥发到外部、大范围不可逆删除)一律拒绝。
|
|
37
|
-
-
|
|
37
|
+
- 审核超时、返回字段缺失或含未知值、出错或模型不可用时,这次请求不放行,智能体会被告知这是审核失败,可以重试一次或者来问你。
|
|
38
38
|
- 同一轮对话里连续拒绝 3 次,或最近 50 次拒绝了 10 次,本轮就暂停自动审批,等你发下一条消息。
|
|
39
39
|
|
|
40
40
|
## 限制
|
package/dsh.plugin.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "@klarkxy/dsh-safe-auto",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"main": "src/index.js",
|
|
5
5
|
"description": "Safe Auto: a reviewer model answers native approval requests, Codex auto-review style; only a pass approves that one call.",
|
|
6
6
|
"engines": {
|
|
7
|
-
"dsh": ">=0.2.
|
|
7
|
+
"dsh": ">=0.2.1-rc.1"
|
|
8
8
|
},
|
|
9
9
|
"contributes": {
|
|
10
10
|
"tools": [],
|
package/lib/client.js
CHANGED
|
@@ -1414,25 +1414,18 @@ function createPluginReadGate(now = Date.now) {
|
|
|
1414
1414
|
}
|
|
1415
1415
|
//#endregion
|
|
1416
1416
|
//#region ../dsh-plugin-kit/lib/contracts.js
|
|
1417
|
-
/**
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
method: "watch",
|
|
1430
|
-
mode: "stream",
|
|
1431
|
-
invocation: { kind: "direct" },
|
|
1432
|
-
parameters: [],
|
|
1433
|
-
cancellation: { parameter: "signal" }
|
|
1434
|
-
}]
|
|
1435
|
-
};
|
|
1417
|
+
/** `@klarkxy/dsh-git-commit` becomes `KlarkxyDshGitCommitModelSettingsChanges`. */
|
|
1418
|
+
function modelSettingsChangesIdentity(packageName) {
|
|
1419
|
+
const scoped = /^@([^/]+)\/(.+)$/.exec(packageName);
|
|
1420
|
+
const parts = (scoped ? `${scoped[1]}/${scoped[2]}` : packageName).split(/[^A-Za-z0-9]+/).filter(Boolean);
|
|
1421
|
+
if (!parts.length) throw new Error("Model settings package name has no RPC segment");
|
|
1422
|
+
const segment = `${parts.map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("")}ModelSettingsChanges`;
|
|
1423
|
+
if (!/^[A-Z][A-Za-z0-9]*$/.test(segment)) throw new Error(`Model settings sync name is not an RPC segment: ${segment}`);
|
|
1424
|
+
return {
|
|
1425
|
+
service: segment,
|
|
1426
|
+
namespace: segment
|
|
1427
|
+
};
|
|
1428
|
+
}
|
|
1436
1429
|
//#endregion
|
|
1437
1430
|
//#region ../dsh-plugin-kit/lib/model-settings-client.js
|
|
1438
1431
|
/** This stream carries invalidations only; ready is never a settings snapshot. */
|
|
@@ -1447,17 +1440,44 @@ function parseModelSettingsChangeFrame(value) {
|
|
|
1447
1440
|
};
|
|
1448
1441
|
throw new Error("Invalid model settings change frame");
|
|
1449
1442
|
}
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1443
|
+
/** One owner's contribution. Each package mounts only its own endpoint. */
|
|
1444
|
+
function modelSettingsChangesRemote(packageName, identity = modelSettingsChangesIdentity(packageName)) {
|
|
1445
|
+
return {
|
|
1446
|
+
package: packageName,
|
|
1447
|
+
descriptors: [{
|
|
1448
|
+
id: `${identity.namespace}/watch`,
|
|
1449
|
+
service: identity.service,
|
|
1450
|
+
namespace: identity.namespace,
|
|
1451
|
+
method: "watch",
|
|
1452
|
+
mode: "stream",
|
|
1453
|
+
invocation: { kind: "direct" },
|
|
1454
|
+
parameters: [],
|
|
1455
|
+
cancellation: { parameter: "signal" },
|
|
1456
|
+
result: {
|
|
1457
|
+
mode: "strict",
|
|
1458
|
+
typeSymbol: "ModelSettings.ChangeFrame",
|
|
1459
|
+
create: () => ({ parse: parseModelSettingsChangeFrame })
|
|
1460
|
+
}
|
|
1461
|
+
}]
|
|
1462
|
+
};
|
|
1463
|
+
}
|
|
1464
|
+
/** Mount from a synchronous apply. Disposal waits for an in-flight mount and
|
|
1465
|
+
* then withdraws it, so the contribution cannot outlive the plugin. */
|
|
1466
|
+
function mountModelSettingsChanges(ctx, packageName) {
|
|
1467
|
+
const contribution = modelSettingsChangesRemote(packageName);
|
|
1468
|
+
ctx.inject(["remote"], (injected) => {
|
|
1469
|
+
const scope = injected;
|
|
1470
|
+
const mounting = scope.remote.$mount(contribution);
|
|
1471
|
+
mounting.catch(() => {});
|
|
1472
|
+
scope.effect(() => async () => {
|
|
1473
|
+
await (await mounting.catch(() => void 0))?.();
|
|
1474
|
+
});
|
|
1475
|
+
});
|
|
1476
|
+
}
|
|
1458
1477
|
/** Native RemoteStream owns carrier reconnects; no timers or browser transport.
|
|
1459
1478
|
* A factory shares one logical stream while at least one subscriber remains. */
|
|
1460
|
-
function createModelSettingsSubscriber(ctx,
|
|
1479
|
+
function createModelSettingsSubscriber(ctx, packageName, identity = modelSettingsChangesIdentity(packageName), accept = (ns) => ns === packageName) {
|
|
1480
|
+
const endpoint = identity;
|
|
1461
1481
|
const listeners = /* @__PURE__ */ new Set();
|
|
1462
1482
|
let injection;
|
|
1463
1483
|
let current;
|
|
@@ -1483,12 +1503,18 @@ function createModelSettingsSubscriber(ctx, namespace) {
|
|
|
1483
1503
|
};
|
|
1484
1504
|
const start = () => {
|
|
1485
1505
|
const life = ++lifecycle;
|
|
1486
|
-
injection = ctx.inject(["remote",
|
|
1506
|
+
injection = ctx.inject(["remote", `remote.${endpoint.namespace}`], (injected) => {
|
|
1487
1507
|
if (life !== lifecycle || !listeners.size) return;
|
|
1488
1508
|
const scope = injected;
|
|
1509
|
+
const service = scope.remote[endpoint.namespace];
|
|
1510
|
+
if (!service || !("watch" in service) || !service.watch) {
|
|
1511
|
+
report("Model settings sync service unavailable");
|
|
1512
|
+
return;
|
|
1513
|
+
}
|
|
1514
|
+
const watch = service.watch;
|
|
1489
1515
|
const stream = scope.remote.$stream({
|
|
1490
1516
|
name: "Model settings sync",
|
|
1491
|
-
open: (signal) =>
|
|
1517
|
+
open: (signal) => watch(signal),
|
|
1492
1518
|
ended: (accepted) => /* @__PURE__ */ new Error(`Model settings sync stream ended ${accepted ? "after" : "before"} ready; refresh or reopen the page`)
|
|
1493
1519
|
});
|
|
1494
1520
|
const session = {
|
|
@@ -1523,7 +1549,7 @@ function createModelSettingsSubscriber(ctx, namespace) {
|
|
|
1523
1549
|
invalidate();
|
|
1524
1550
|
} else {
|
|
1525
1551
|
if (!ready) throw new Error("Model settings sync change arrived before ready");
|
|
1526
|
-
if (
|
|
1552
|
+
if (accept(frame.ns)) invalidate();
|
|
1527
1553
|
}
|
|
1528
1554
|
}
|
|
1529
1555
|
} catch (error) {
|
|
@@ -2249,6 +2275,7 @@ function SettingsPanel({ view, api, locale, initialSettings = null, initialCatal
|
|
|
2249
2275
|
}, t("saved")));
|
|
2250
2276
|
}
|
|
2251
2277
|
function apply(ctx) {
|
|
2278
|
+
mountModelSettingsChanges(ctx, BUNDLE);
|
|
2252
2279
|
const settingsSubscriber = createModelSettingsSubscriber(ctx, BUNDLE);
|
|
2253
2280
|
const call = (endpoint, payload) => unwrapRpc((...args) => ctx.connection.rpc.call(...args), endpoint, payload);
|
|
2254
2281
|
const api = Object.freeze({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@klarkxy/dsh-safe-auto",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Safe Auto: a reviewer model answers native approval requests, Codex auto-review style; only a pass approves that one call.",
|
|
5
5
|
"icon": "./icon.svg",
|
|
6
6
|
"type": "module",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
},
|
|
29
29
|
"engines": {
|
|
30
30
|
"node": "^22.19.0 || >=24.0.0",
|
|
31
|
-
"dsh": ">=0.2.
|
|
31
|
+
"dsh": ">=0.2.1-rc.1"
|
|
32
32
|
},
|
|
33
33
|
"license": "SEE LICENSE IN LICENSE",
|
|
34
34
|
"repository": {
|
|
@@ -58,30 +58,38 @@
|
|
|
58
58
|
}
|
|
59
59
|
},
|
|
60
60
|
"dependencies": {
|
|
61
|
-
"@klarkxy/dsh-plugin-kit": "0.
|
|
61
|
+
"@klarkxy/dsh-plugin-kit": "0.4.0",
|
|
62
62
|
"zod": "^4.1.5"
|
|
63
63
|
},
|
|
64
64
|
"peerDependencies": {
|
|
65
65
|
"@deepseek-ai/cordis": ">=4.0.4",
|
|
66
|
-
"@deepseek-ai/dsh-storage-domain": ">=0.2.0-rc.2"
|
|
66
|
+
"@deepseek-ai/dsh-storage-domain": ">=0.2.0-rc.2",
|
|
67
|
+
"@deepseek-ai/dsh-working-directory": ">=0.2.1-rc.1"
|
|
67
68
|
},
|
|
68
69
|
"peerDependenciesMeta": {
|
|
69
70
|
"@deepseek-ai/dsh-storage-domain": {
|
|
70
71
|
"optional": true
|
|
72
|
+
},
|
|
73
|
+
"@deepseek-ai/cordis": {
|
|
74
|
+
"optional": true
|
|
75
|
+
},
|
|
76
|
+
"@deepseek-ai/dsh-working-directory": {
|
|
77
|
+
"optional": true
|
|
71
78
|
}
|
|
72
79
|
},
|
|
73
80
|
"devDependencies": {
|
|
74
|
-
"@deepseek-ai/cordis": "4.0.
|
|
75
|
-
"@deepseek-ai/dsh-client-ui-primitives": "0.2.
|
|
76
|
-
"@deepseek-ai/dsh-storage-domain": "0.2.
|
|
77
|
-
"@klarkxy/dsh-model-route": "0.2.
|
|
78
|
-
"jsdom": "29.1.1"
|
|
81
|
+
"@deepseek-ai/cordis": "4.0.5",
|
|
82
|
+
"@deepseek-ai/dsh-client-ui-primitives": "0.2.1-rc.1",
|
|
83
|
+
"@deepseek-ai/dsh-storage-domain": "0.2.1-rc.1",
|
|
84
|
+
"@klarkxy/dsh-model-route": "0.2.5",
|
|
85
|
+
"jsdom": "29.1.1",
|
|
86
|
+
"@deepseek-ai/dsh-working-directory": "0.2.1-rc.1"
|
|
79
87
|
},
|
|
80
88
|
"publishConfig": {
|
|
81
89
|
"access": "public",
|
|
82
90
|
"registry": "https://registry.npmjs.org/"
|
|
83
91
|
},
|
|
84
92
|
"dshRelease": {
|
|
85
|
-
"contentHash": "
|
|
93
|
+
"contentHash": "fbbaa607cab540cfc58b7d99946f5a0954f3b4c51b9ceb90e452fb3fd0522bed"
|
|
86
94
|
}
|
|
87
95
|
}
|
package/src/index.js
CHANGED
|
@@ -8,7 +8,7 @@ import { resolveReviewRoute } from './model-route.js';
|
|
|
8
8
|
export { Config } from './config.js';
|
|
9
9
|
|
|
10
10
|
export const name = 'dsh-safe-auto';
|
|
11
|
-
export const inject = ['sandboxPolicy'];
|
|
11
|
+
export const inject = ['sandboxPolicy', 'workingDirectory'];
|
|
12
12
|
|
|
13
13
|
/*
|
|
14
14
|
* Safe Auto routes native approval requests to a reviewer model, following
|
|
@@ -199,7 +199,7 @@ export function apply(ctx, raw = {}) {
|
|
|
199
199
|
const agent = req.agent, callId = String(req.callId), token = exec.token;
|
|
200
200
|
const environment = () => {
|
|
201
201
|
const sandbox = ctx.sandboxPolicy.resolve({ session });
|
|
202
|
-
return JSON.stringify({ platform: process.platform, cwd: session
|
|
202
|
+
return JSON.stringify({ platform: process.platform, cwd: ctx.workingDirectory.get(session),
|
|
203
203
|
workspaceRoot: sandbox?.workspaceRoot, sandbox: sandbox?.mode });
|
|
204
204
|
};
|
|
205
205
|
let rawAction, rawEnvironment, revision, input;
|
package/src/model-route.js
CHANGED
|
@@ -34,8 +34,6 @@ export function resolveReviewRoute(config, owner) {
|
|
|
34
34
|
...(config.reasoningEffort ? { reasoningEffort: config.reasoningEffort } : {}) });
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
export const sameRoute = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
38
|
-
|
|
39
37
|
function usageTotal(usage) {
|
|
40
38
|
if (!usage || typeof usage !== 'object') return undefined;
|
|
41
39
|
if (Number.isSafeInteger(usage.totalTokens) && usage.totalTokens >= 0) return usage.totalTokens;
|
package/src/reviewer.js
CHANGED
|
@@ -65,12 +65,17 @@ export function parseVerdict(text) {
|
|
|
65
65
|
if (end <= start) throw new Error('INVALID_REVIEW_RESPONSE');
|
|
66
66
|
let value;
|
|
67
67
|
try { value = JSON.parse(text.slice(start, end + 1)); } catch { throw new Error('INVALID_REVIEW_RESPONSE'); }
|
|
68
|
-
if (!value || typeof value !== 'object' ||
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
const
|
|
68
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)
|
|
69
|
+
|| !['allow', 'deny'].includes(value.outcome)
|
|
70
|
+
|| !RISKS.includes(value.risk_level) || !AUTHORIZATIONS.includes(value.user_authorization)
|
|
71
|
+
|| typeof value.rationale !== 'string' || !value.rationale.trim()) throw new Error('INVALID_REVIEW_RESPONSE');
|
|
72
|
+
const risk = value.risk_level;
|
|
73
|
+
const authorization = value.user_authorization;
|
|
74
|
+
const rationale = value.rationale.trim().slice(0, 600);
|
|
75
|
+
// Retain model denials (including malicious injection and broad scope), but
|
|
76
|
+
// enforce the policy's authorization floor even if the model says allow.
|
|
77
|
+
const outcome = risk === 'critical' || (risk === 'high' && !['medium', 'high'].includes(authorization))
|
|
78
|
+
? 'deny' : value.outcome;
|
|
74
79
|
return Object.freeze({ outcome, risk, authorization, rationale });
|
|
75
80
|
}
|
|
76
81
|
|
package/src/ui-host.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { createControl, CHANNEL, REVIEW_FIELDS } from './control.js';
|
|
2
2
|
import { registerHostRpc } from '@klarkxy/dsh-plugin-kit/host-rpc';
|
|
3
|
+
import { modelSettingsChangesIdentity } from '@klarkxy/dsh-plugin-kit/contracts';
|
|
4
|
+
import { createModelSettingsChanges } from '@klarkxy/dsh-plugin-kit/model-settings-changes';
|
|
3
5
|
import { registerModelSettings, notifyModelSettingsChanged } from '@klarkxy/dsh-plugin-kit/model-settings';
|
|
4
6
|
|
|
5
7
|
export const name = 'dsh-safe-auto-ui-host';
|
|
@@ -21,6 +23,7 @@ export function registerControlRoute(ctx, call) {
|
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
export async function apply(ctx) {
|
|
26
|
+
createModelSettingsChanges(ctx, modelSettingsChangesIdentity('@klarkxy/dsh-safe-auto'), '@klarkxy/dsh-safe-auto');
|
|
24
27
|
// Loaded only by the separate UI row; the review plugin keeps its minimal dependencies.
|
|
25
28
|
const [{ defineDomain, domainTable }, { z }] = await Promise.all([import('@deepseek-ai/dsh-storage-domain'), import('zod')]);
|
|
26
29
|
// No strict(): rows saved by 0.1.x carry retired keys (fastProvider, approvalReview, …),
|