@leonhardthust/dsh-decision-engine 1.9.1 → 1.9.2

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.
@@ -26,6 +26,27 @@
26
26
  - 用户提示为可读说明,机器码留在 reasonCode / denial info。
27
27
  - 旧记录证据不足时保持原样,不把 0ms 倒推为特定调用状态。
28
28
 
29
+ ## 1.9.1 实录复查与后续修正(未发布)
30
+
31
+ 按用户指定的北京时间 2026-11-15 00:30 起读取当前日志及 `.1` 轮转备份。首次完整提取快照有 182 条时间范围内记录,其中 39 条拒绝,JSON 均可解析。真实日志仅留在 Git 忽略目录,不提交会话标识、用户正文或工具参数。
32
+
33
+ - 26 条 `unresolved-effect-needs-model-review`:primary 已运行,但非 Auto 分支把 unknown 无条件覆盖为 ask/deny,连 tolerance=delegate 也被拒绝。
34
+ - 12 条 `advisory-failed-unknown-effect`:工作区 edit 已知 safe、四个核心信号有值,但 primary 解析失效;历史未记录具体 validation errors,不能反推原始错误分布。旧解析器确实会因附加 confidence/probabilities 校验失败使整体失效。
35
+ - 1 条 `fallback-model-deny`:唯一一条实际调用独立回退模型后拒绝的记录,不能和其余 38 条混淆。
36
+ - 5 条 MCP 拒绝中,4 条的 primary 与 tolerance 均已有有效 delegate,最终仍被 unknown 分支覆盖;另 1 条 primary 解析失败。5 条全部未请求 fallback。
37
+
38
+ 修正非 Auto 分支:有效 primary 与所选 tolerance 不再被 unknown 无条件覆盖;只有最终确实 ask 且 approval=never 才拒绝。Auto 不确定性仍独立复核,真实硬约束保持不变。
39
+
40
+ 附加概率和 confidence 元数据校验失败时只保留 warning 并弃用该元数据,不归一化、不用它抬高 severity,也不使有效必需信号整体失效;risk choice 或必需 noul 无效仍失败关闭。新审计记录 validationErrors、metadataWarnings、raw_class_probabilities,避免再只剩 answer-shape-mismatch。
41
+
42
+ 服务解析依据宿主源码:先用 agentPresets.serviceFor 读取确实存在的隔离 preset 实例;正常未覆盖时使用 actor.ctx 的共享基础服务。实例存在但方法返回 undefined 不再重试更宽全局实例;解析抛错保持不可用。回退 LLM 同样按 actor 解析。日志新增 permissionPreset、approvalPolicy、automatic 和 serviceSources。历史 39 条没有 permissionPreset,不能断言它们全因 Auto 识别错误造成。
43
+
44
+ 60 项匿名实录形状回归及包含作用域的 231 项组合回归通过;不是对原请求副作用的重放。全量 996 项中 993 通过、3 项失败,均为已有 Windows POSIX 文件权限断言。
45
+
46
+ 本地另对 39 条拒绝的已记录核心信号和阈值运行修正后的监听器,分手动 / Auto 两种显式场景共 78 次;手动场景 28 allow / 11 deny,Auto 场景在固定 LLM allow stub 下为 39 allow(31 次独立复核、8 次核心信号与软摩擦已允许的有限编辑)。这些计数不代表真实模型效果:历史 preset、完整参数与可选原始分布未完整保留,不重建或伪造;固定 stub 也不推翻原先那一次真实 fallback deny。仅验证 unknown 不再无条件覆盖有效判定,以及符合条件的 Auto 请求确实进入回退。原始实录与逐行回放仍在 Git 忽略目录。
47
+
48
+ 真实主端点探测在配置前置检查退出,没有发请求,没有更改用户配置;运行中的已发布 1.9.1 不会自动获得这些源码修正。
49
+
29
50
  ## 验证边界
30
51
 
31
52
  125 项专项回归、212 项组合回归通过;24 个独立合成情景的四档矩阵通过。组合之间有重叠,不相加成独立样本数。模型 stub 的 allow/deny/failure 仅验证路由不变量,不代表真实危险 FNR 或概率校准。
package/docs/releasing.md CHANGED
@@ -1,14 +1,15 @@
1
1
  # npm 发布
2
2
 
3
- 发布目标:`@leonhardthust/dsh-decision-engine`,当前版本 `1.9.1`,公开包。
3
+ 发布目标:`@leonhardthust/dsh-decision-engine`,当前版本 `1.9.2`,公开包。
4
4
 
5
5
  本分支基于 `zuosus/dsh-decision-engine` 的 MIT 代码,保留原作者与许可证;npm scope、源码仓库、bundle 引用和浏览器主模块 / charts chunk 标识已同步为本分支身份。内部 API、样式标识和用户数据目录保留旧名称以兼容已有配置。不要同时启用原包与本 scoped fork,它们提供相同的服务与页面。
6
6
 
7
7
  ## 工作流
8
8
 
9
9
  - `.github/workflows/ci.yml`:main push、PR、手动运行或发布工作流复用;Ubuntu 24.04 + Node.js 24 + Vite+ 1.6.0。执行 frozen install、完整测试与接线、隔离情景、实际打包和 tarball 检查。失败不会被忽略。
10
- - `.github/workflows/publish.yml`:版本 tag push 或 main 上手动运行。只有 `LeonhardtHUST/dsh-decision-engine` 可以进入发布;发布必须等待上述 CI 成功。
11
- - 手动运行必须填写与 manifest 完全一致的 `v1.9.1`。后续发版同步修改 manifest 与 tag。预发布版本不能覆盖 npm `latest`。
10
+ - `.github/workflows/publish.yml`:每次 `main` push 自动检测版本。先比较整个 push 的 `before` 与当前 manifest,拒绝错误包名、非法或倒退版本;再只读查询 npmjs。当前版本已上传则跳过;当前版本高于 npmjs latest 且未上传则进入完整验证及发布。多提交 push 比较整个 push 前后,不仅比较 HEAD 的父提交。
11
+ - 后续发版只需修改 `package.json` 版本并提交到 main;无需创建 tag 或手动点运行。发布依赖检测和本次提交的完整 CI 均成功,不跳过测试。稳定版本发布到 `latest`,预发布版本自动进入 `next`。
12
+ - 当前 `1.9.2` 包含实录 MCP 误拒、附加元数据校验和 actor/preset 服务解析修复;边界见 `docs/auto-review-hotfix.md`。
12
13
  - checkout 与 setup-vp Action 固定到已核对的 commit SHA;不通过 `pull_request_target` 运行不可信 PR。
13
14
  - 验证任务只有 `contents: read`;只有发布 job 有 `id-token: write`。发布 step 只使用 OIDC(trusted publishing),不再读取任何 npm token secret。代码 checkout 不保留 Git 凭据。此工作流不会自动修改版本、提交源码或推送 Git。
14
15
 
@@ -41,7 +42,6 @@ vp run verify
41
42
  vp run verify:guard
42
43
  vp run pack:release
43
44
  vp run verify:package
44
- $env:RELEASE_TAG = "v1.9.1"
45
45
  vp run verify:release
46
46
  ```
47
47
 
@@ -49,7 +49,7 @@ vp run verify:release
49
49
 
50
50
  `verify:package` 读取真正的 gzip/tar,检查 manifest、scoped 模块 id、bundle 引用、全部安全模块、locale、图表资源和相对 import。配置、凭据、日志、测试目录等不能进入包。输出文件数、大小和 SHA-512 integrity。
51
51
 
52
- 当前 Windows 完整测试的 5 个基线失败及安全边界见 `bench/guard-revision/validation.md`(仅在源码仓库保留完整证据)。发布 CI 选择 Linux,依然必须运行完整测试,不跳过或忽略它们;本机专测通过不等同于 CI 发布成功。
52
+ Windows 全量测试最近仍有 3 个既有 POSIX 文件权限断言失败;旧验证检查点及安全边界见 `bench/guard-revision/validation.md` 与 `docs/auto-review-hotfix.md`(完整证据仅在源码仓库保留)。发布 CI 选择 Linux,依然必须运行完整测试,不跳过或忽略它们;本机专测通过不等同于 CI 发布成功。
53
53
 
54
54
  ## 手动发布相同的已验证 tarball
55
55
 
@@ -58,12 +58,12 @@ vp run verify:release
58
58
  授权及完整验证均满足后:
59
59
 
60
60
  ```powershell
61
- npm publish ./release-artifacts/leonhardthust-dsh-decision-engine-1.9.1.tgz --registry "https://registry.npmjs.org/" --access public --tag latest
61
+ npm publish ./release-artifacts/leonhardthust-dsh-decision-engine-1.9.2.tgz --registry "https://registry.npmjs.org/" --access public --tag latest
62
62
  npm view @leonhardthust/dsh-decision-engine version --registry "https://registry.npmjs.org/" --json
63
63
  ```
64
64
 
65
- 本地发布不伪造 GitHub provenance;在 GitHub Actions 中由工作流增加 `--provenance`。发布操作不可覆盖已存在的同版本;版本冲突必须停止,不自动递增重试。
65
+ 本地发布不伪造 GitHub provenance;在 GitHub Actions 中由工作流增加 `--provenance`。发布操作不可覆盖已存在的同版本;已发布版本的后续提交会跳过发布。若升版后的 CI 失败,修复代码并推送 main 会自动重试仍未发布的版本,无需再升版或手动触发。npmjs 查询错误会停止,不能把网络或权限错误当成未发布。自动检测不自动修改版本或源码。
66
66
 
67
67
  ## 安装
68
68
 
69
- DSH 插件管理中使用 `@leonhardthust/dsh-decision-engine` 或指定 `@leonhardthust/dsh-decision-engine@1.9.1`。安装和启用是另外的用户动作,发布工作流不会修改本机 DSH profile。
69
+ DSH 插件管理中使用 `@leonhardthust/dsh-decision-engine` 或指定 `@leonhardthust/dsh-decision-engine@1.9.2`。安装和启用是另外的用户动作,发布工作流不会修改本机 DSH profile。
@@ -2,6 +2,7 @@ export const GUARD_QUESTIONS_VERSION = "guard-questions-v2";
2
2
  export const CONFIDENCE_KINDS = ["normalized", "provider_raw", "unavailable"];
3
3
 
4
4
  const probability = (value) => typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 1;
5
+ const finite = (value) => typeof value === "number" && Number.isFinite(value);
5
6
 
6
7
  export function guardQuestions() {
7
8
  return {
@@ -31,26 +32,42 @@ export function guardQuestions() {
31
32
 
32
33
  export function parseModelAdvisory(answers, options = {}) {
33
34
  const errors = [];
35
+ const warnings = [];
34
36
  const source = answers && typeof answers === "object" && !Array.isArray(answers) ? answers : {};
35
37
  const risk = source.risk;
36
38
  const choice = risk?.type === "choice" && ["low", "medium", "high"].includes(risk.choice) ? risk.choice : "unknown";
37
39
  if (choice === "unknown") errors.push("risk-choice-unavailable");
38
40
  const declaredKind = risk?.confidenceKind ?? options.confidenceKind ?? "unavailable";
39
- const confidenceKind = CONFIDENCE_KINDS.includes(declaredKind) ? declaredKind : "unavailable";
40
- if (!CONFIDENCE_KINDS.includes(declaredKind)) errors.push("confidence-kind-invalid");
41
- const confidence = typeof risk?.confidence === "number" && Number.isFinite(risk.confidence) ? risk.confidence : null;
42
- if (risk?.confidence !== undefined && confidence === null) errors.push("confidence-invalid");
43
- if (confidenceKind === "normalized" && !probability(confidence)) errors.push("normalized-confidence-out-of-range");
44
- if (confidenceKind === "provider_raw" && confidence === null) errors.push("provider-confidence-unavailable");
45
- let probabilities = null;
46
- if (risk?.probabilities !== undefined) {
47
- const values = risk.probabilities;
48
- if (values && typeof values === "object" && !Array.isArray(values)
49
- && ["low", "medium", "high"].every((key) => probability(values[key]))
50
- && Math.abs(values.low + values.medium + values.high - 1) <= 1e-6) {
51
- probabilities = { low: values.low, medium: values.medium, high: values.high };
52
- } else errors.push("class-probabilities-invalid");
41
+ let confidenceKind = CONFIDENCE_KINDS.includes(declaredKind) ? declaredKind : "unavailable";
42
+ if (!CONFIDENCE_KINDS.includes(declaredKind)) warnings.push("confidence-kind-invalid");
43
+ const confidence = finite(risk?.confidence) ? risk.confidence : null;
44
+ if (risk?.confidence !== undefined && confidence === null) warnings.push("confidence-invalid");
45
+ if (confidenceKind === "normalized" && !probability(confidence)) {
46
+ warnings.push("normalized-confidence-out-of-range");
47
+ confidenceKind = "unavailable";
53
48
  }
49
+ if (confidenceKind === "provider_raw" && confidence === null) {
50
+ warnings.push("provider-confidence-unavailable");
51
+ confidenceKind = "unavailable";
52
+ }
53
+ let distribution = null;
54
+ let probabilityStatus = "unavailable";
55
+ const supplied = risk?.probabilities;
56
+ if (supplied !== undefined) {
57
+ if (supplied && typeof supplied === "object" && !Array.isArray(supplied)
58
+ && ["low", "medium", "high"].every((key) => probability(supplied[key]))
59
+ && Math.abs(supplied.low + supplied.medium + supplied.high - 1) <= 1e-6) {
60
+ distribution = { low: supplied.low, medium: supplied.medium, high: supplied.high };
61
+ probabilityStatus = "validated-distribution";
62
+ } else {
63
+ warnings.push("class-probabilities-invalid");
64
+ probabilityStatus = "invalid-distribution";
65
+ }
66
+ }
67
+ const rawProbabilities = supplied && typeof supplied === "object" && !Array.isArray(supplied)
68
+ ? Object.fromEntries(["low", "medium", "high"].map((key) => [key, finite(supplied[key]) ? supplied[key] : null])) : null;
69
+ const probabilities = warnings.length ? null : distribution;
70
+ if (distribution && warnings.length) probabilityStatus = "ignored-metadata-warning";
54
71
  const scalar = (name) => {
55
72
  const answer = source[name];
56
73
  if (answer?.type !== "noul" || !probability(answer.noul)) {
@@ -67,18 +84,16 @@ export function parseModelAdvisory(answers, options = {}) {
67
84
  risk: { choice, confidence, confidenceKind, ...(probabilities ? { probabilities } : {}) },
68
85
  irreversibleP, taskP, injectionP,
69
86
  metadata: {
70
- questionsVersion: GUARD_QUESTIONS_VERSION,
71
- confidenceKind,
87
+ questionsVersion: GUARD_QUESTIONS_VERSION, confidenceKind,
72
88
  raw_decision_confidence: confidence,
89
+ raw_class_probabilities: rawProbabilities,
73
90
  class_probabilities: probabilities ?? "unknown",
74
91
  p_high: probabilities?.high ?? null,
75
92
  p_non_low: probabilities ? probabilities.medium + probabilities.high : null,
76
- calibrated_p_unsafe: "unknown",
77
- authorization_status: "unknown",
78
- source_trust: "unknown",
79
- probabilityStatus: probabilities ? "validated-distribution" : "unavailable",
93
+ calibrated_p_unsafe: "unknown", authorization_status: "unknown", source_trust: "unknown",
94
+ probabilityStatus, warnings, validationErrors: errors,
80
95
  },
81
- errors,
96
+ errors, warnings,
82
97
  };
83
98
  }
84
99
 
@@ -55,10 +55,14 @@ export function createGuardAudit(options = {}) {
55
55
  p_non_low: finite(advisory.metadata?.p_non_low),
56
56
  irreversibleP: finite(advisory.irreversibleP), taskP: finite(advisory.taskP), injectionP: finite(advisory.injectionP),
57
57
  calibrated_p_unsafe: "unknown",
58
+ validationErrors: advisory.errors ?? [],
59
+ metadataWarnings: advisory.metadata?.warnings ?? [],
60
+ probabilityStatus: advisory.metadata?.probabilityStatus ?? "unavailable",
61
+ raw_class_probabilities: advisory.metadata?.raw_class_probabilities ?? null,
58
62
  },
59
63
  mandatory: { status: ["safe", "requires_approval", "deny", "unknown"].includes(mandatory.status) ? mandatory.status : "unknown", reasonCodes: mandatory.reasonCodes ?? [], evidence_summary: hashSummary(mandatory.evidence) },
60
64
  effect: { kind: effect.kind ?? "unknown", highImpact: effect.highImpact ?? "unknown", summary: hashSummary(effect) },
61
- permission: { approvalPolicy: context.approvalPolicy ?? "unknown", approvalAvailable: context.approvalAvailable ?? "unknown", sandboxMode: context.sandboxMode ?? "unknown" },
65
+ permission: { permissionPreset: context.permissionPreset ?? "unknown", serviceSources: context.serviceSources ?? {}, approvalPolicy: context.approvalPolicy ?? "unknown", approvalAvailable: context.approvalAvailable ?? "unknown", sandboxMode: context.sandboxMode ?? "unknown" },
62
66
  baseAction: actionOf(options.baseAction),
63
67
  tolerance: { level: options.tolerance?.level ?? "unknown", changed: options.tolerance?.changed ?? "unknown", action: actionOf(options.tolerance?.kind), severity: finite(options.tolerance?.severity), severityMeaning: "ranking-score-not-calibrated-probability" },
64
68
  pluginReturnedAction: actionOf(options.pluginReturnedAction),
@@ -2,12 +2,29 @@ import { createHash } from "node:crypto";
2
2
 
3
3
  export const TRUSTED_CONTEXT_VERSION = "guard-context-v1";
4
4
 
5
- function service(ctx, name) {
6
- try {
7
- return ctx?.get?.(name);
8
- } catch {
5
+ function rootService(ctx, name) {
6
+ try { return ctx?.get?.(name); } catch { return undefined; }
7
+ }
8
+
9
+ export function guardService(ctx, agent, name, sources = {}) {
10
+ const actor = agent?.ctx;
11
+ if (actor) {
12
+ const presets = rootService(ctx, "agentPresets");
13
+ if (typeof presets?.serviceFor === "function") {
14
+ try {
15
+ const scoped = presets.serviceFor(agent, name);
16
+ if (scoped !== undefined) { sources[name] = "preset"; return scoped; }
17
+ } catch { sources[name] = "preset-error"; return undefined; }
18
+ }
19
+ if (typeof actor.get === "function") {
20
+ try { sources[name] = "actor-host"; return actor.get(name); }
21
+ catch { sources[name] = "actor-error"; return undefined; }
22
+ }
23
+ sources[name] = "actor-unavailable";
9
24
  return undefined;
10
25
  }
26
+ sources[name] = "legacy-host";
27
+ return rootService(ctx, name);
11
28
  }
12
29
 
13
30
  function textOf(message) {
@@ -40,7 +57,9 @@ export async function buildTrustedContext({ agent, ctx, exec, host = {}, downstr
40
57
  messageTarget: { id: "unknown", membership: "unknown", continuable: "unknown", permissions: "unknown", downstreamIndependentApproval: "unknown" },
41
58
  availability,
42
59
  evidence,
60
+ serviceSources: {},
43
61
  };
62
+ const scopedService = (name) => guardService(ctx, agent, name, context.serviceSources);
44
63
  const read = async (field, operation) => {
45
64
  try {
46
65
  const value = await operation();
@@ -62,32 +81,32 @@ export async function buildTrustedContext({ agent, ctx, exec, host = {}, downstr
62
81
  availability.originalUserGoal = { available: false, reason: "no-host-user-message" };
63
82
  }
64
83
  }
65
- const presets = service(ctx, "permissionPresets");
84
+ const presets = scopedService("permissionPresets");
66
85
  const presetName = await read("permissionPreset", () => session && presets?.current?.(session));
67
86
  if (typeof presetName === "string") context.permissionPreset = presetName;
68
87
  const preset = await read("permissionBundle", () => presetName && presets?.resolve?.(presetName));
69
- const approval = service(ctx, "approval");
88
+ const approval = scopedService("approval");
70
89
  const override = await read("approvalOverride", () => session && approval?.overrideOf?.(session));
71
90
  const policy = override ?? preset?.approval;
72
91
  if (policy === "ask" || policy === "never") context.approvalPolicy = policy;
73
92
  context.approvalAvailable = policy === "never" ? false
74
93
  : policy === "ask" && typeof approval?.request === "function" ? true : "unknown";
75
94
  availability.approvalPolicy = { available: context.approvalPolicy !== "unknown", reason: override !== undefined ? "host-session-override" : preset?.approval ? "host-preset" : "host-default-unavailable" };
76
- const sandboxPolicy = service(ctx, "sandboxPolicy");
95
+ const sandboxPolicy = scopedService("sandboxPolicy");
77
96
  const sandbox = await read("sandboxPolicy", () => session && sandboxPolicy?.resolve?.({ session }));
78
97
  const sandboxMode = sandbox?.mode ?? preset?.sandbox;
79
98
  if (["read-only", "workspace-write", "danger-full-access"].includes(sandboxMode)) context.sandboxMode = sandboxMode;
80
99
  if (typeof sandbox?.workspaceRoot === "string") {
81
100
  context.workspaceRoots = [sandbox.workspaceRoot, ...(sandbox.additionalWritableRoots ?? []).filter((root) => typeof root === "string")];
82
101
  }
83
- const tools = service(ctx, "tools");
102
+ const tools = scopedService("tools");
84
103
  if (typeof tools?.executable === "function" && exec?.name) {
85
104
  const definition = await read("toolAccess", () => tools.executable(exec.name, agent, exec.toolSet));
86
105
  context.toolAccess = availability.toolAccess.available ? "available" : availability.toolAccess.reason === "host-field-unavailable" ? "denied" : "unknown";
87
106
  if (definition) evidence.push({ source: "tools.executable", field: "toolAccess", value: "available" });
88
107
  }
89
108
  const args = exec?.arguments;
90
- const fs = service(ctx, "fs");
109
+ const fs = scopedService("fs");
91
110
  if (["write", "edit"].includes(exec?.name) && typeof args?.file_path === "string" && context.workspaceRoots.length && typeof fs?.resolve === "function" && typeof fs?.contains === "function") {
92
111
  const contained = await read("fileTargetScope", async () => {
93
112
  const target = await fs.resolve(args.file_path, { cwd: context.workspaceRoots[0], signal: exec.signal });
@@ -100,7 +119,7 @@ export async function buildTrustedContext({ agent, ctx, exec, host = {}, downstr
100
119
  }
101
120
  }
102
121
  if (exec?.name === "send_message" && typeof args?.agent === "string") {
103
- const subagents = service(ctx, "subagents");
122
+ const subagents = scopedService("subagents");
104
123
  const target = await read("messageTarget", async () => {
105
124
  if (!agent || typeof subagents?.rootOf !== "function" || typeof subagents?.resolveName !== "function" || typeof subagents?.listDescendants !== "function") return undefined;
106
125
  const root = subagents.rootOf(agent);
@@ -110,7 +129,7 @@ export async function buildTrustedContext({ agent, ctx, exec, host = {}, downstr
110
129
  const descendants = await subagents.listDescendants(rootId, exec.signal);
111
130
  const member = descendants.find((entry) => entry.kind === "child" && entry.id === targetId && entry.mode === "continuable");
112
131
  const verified = targetId === rootId || Boolean(member);
113
- const resident = targetId === rootId ? root : service(ctx, "agents")?.get?.(targetId);
132
+ const resident = targetId === rootId ? root : scopedService("agents")?.get?.(targetId);
114
133
  const hostLocal = (resident?.id ?? resident?.session?.id) === targetId;
115
134
  return { id: targetId, membership: verified ? "same-tree" : "unknown", continuable: verified ? true : "unknown", hostLocal, permissions: "unknown", downstreamIndependentApproval: verified && hostLocal && downstreamPolicyEnforced === true ? true : "unknown" };
116
135
  });
@@ -1,4 +1,4 @@
1
- import { buildTrustedContext } from "./context.js";
1
+ import { buildTrustedContext, guardService } from "./context.js";
2
2
  import { classifyToolEffect } from "./effects.js";
3
3
  import { evaluateMandatoryPolicy } from "./policy.js";
4
4
  import { GUARD_QUESTIONS_VERSION, guardQuestions, parseModelAdvisory, decideModelAdvisory } from "./advisory.js";
@@ -85,13 +85,14 @@ export function mountGuard(ctx, deps) {
85
85
  const runFallback = async () => {
86
86
  const started = Date.now();
87
87
  let llm;
88
- try { llm = ctx.get?.("llm"); } catch { llm = undefined; }
88
+ const serviceSources = {};
89
+ llm = guardService(ctx, exec.agent, "llm", serviceSources);
89
90
  fallback = await reviewWithFallback({ config, context, mandatory: config.guardPolicyMode === "observe" ? undefined : mandatory, state, advisory, session, llm, signal: exec.signal });
90
91
  decisionSource = ["allow", "deny"].includes(fallback.fallback) ? "fallback_policy" : "fallback_model";
91
92
  secondary = {
92
93
  status: fallback.selection ? (fallback.reason === "fallback-review-failed" || fallback.kind === "cancel" ? "failed" : "completed") : "not_requested",
93
94
  provider: identityOrNull(fallback.selection?.provider), model: identityOrNull(fallback.selection?.model),
94
- latencyMs: fallback.selection ? Date.now() - started : null, reason: fallback.reason,
95
+ latencyMs: fallback.selection ? Date.now() - started : null, reason: fallback.reason, serviceScope: serviceSources.llm ?? "unavailable",
95
96
  };
96
97
  return fallback;
97
98
  };
@@ -122,7 +123,9 @@ export function mountGuard(ctx, deps) {
122
123
  try { client.recordDecision({ facts: result.statsFacts, action: action.kind, detail: action.reason, degraded: Boolean(degradedReason) }); }
123
124
  catch { ctx.logger?.warn?.("decision-engine: guard statistics unavailable"); }
124
125
  }
126
+ const reviewMode = { automatic: automaticReview(context, config), permissionPreset: context?.permissionPreset ?? "unknown", approvalPolicy: context?.approvalPolicy ?? "unknown", approvalAvailable: context?.approvalAvailable ?? "unknown", sandboxMode: context?.sandboxMode ?? "unknown", serviceSources: context?.serviceSources ?? {} };
125
127
  const scores = {
128
+ reviewMode,
126
129
  risk_p: advisory?.risk?.confidence ?? null, raw_decision_confidence: advisory?.risk?.confidence ?? null,
127
130
  confidenceKind: advisory?.risk?.confidenceKind ?? "unavailable", p_high: advisory?.risk?.probabilities?.high ?? null,
128
131
  p_non_low: advisory?.risk?.probabilities ? advisory.risk.probabilities.high + advisory.risk.probabilities.medium : null,
@@ -182,7 +185,7 @@ export function mountGuard(ctx, deps) {
182
185
  try {
183
186
  result = await client.classify({ point: "authorize", tag: "guard", mode: config.mode, pointMode, tool: exec.name, state, questions, signal: exec.signal });
184
187
  advisory = result && !result.degraded ? parseModelAdvisory(result.answers) : undefined;
185
- primary = { status: advisory?.status === "valid" ? (result.cached ? "cache_hit" : "completed") : "failed", provider: identityOrNull(result?.provider), model: identityOrNull(result?.model), latencyMs: numberOrNull(result?.latencyMs) ?? Date.now() - primaryStarted, reason: result?.degraded ?? (advisory?.status === "valid" ? null : "answer-shape-mismatch") };
188
+ primary = { status: advisory?.status === "valid" ? (result.cached ? "cache_hit" : "completed") : "failed", provider: identityOrNull(result?.provider), model: identityOrNull(result?.model), latencyMs: numberOrNull(result?.latencyMs) ?? Date.now() - primaryStarted, reason: result?.degraded ?? (advisory?.status === "valid" ? null : "answer-shape-mismatch"), validationErrors: advisory?.errors ?? [], metadataWarnings: advisory?.metadata?.warnings ?? [], probabilityStatus: advisory?.metadata?.probabilityStatus ?? "unavailable" };
186
189
  } catch {
187
190
  primary = { ...idleEvaluation(), status: "failed", latencyMs: Date.now() - primaryStarted, reason: "primary-review-exception" };
188
191
  }
@@ -198,8 +201,10 @@ export function mountGuard(ctx, deps) {
198
201
  const unresolved = mandatory.status === "unknown";
199
202
  if (automaticReview(context, config) && (unresolved || baseAction.kind === "ask" || action.kind === "ask" || primary.status === "failed")) {
200
203
  action = await runFallback();
201
- } else if (unresolved || primary.status === "failed") {
202
- action = { kind: context.approvalAvailable === true ? "ask" : "deny", reason: unresolved ? "unresolved-effect-needs-model-review" : "advisory-failed-unknown-effect" };
204
+ } else if (primary.status === "failed") {
205
+ action = { kind: context.approvalAvailable === true ? "ask" : "deny", reason: "advisory-failed-unknown-effect" };
206
+ } else if (action.kind === "ask" && context.approvalPolicy === "never") {
207
+ action = { ...action, kind: "deny" };
203
208
  }
204
209
  return finish(action, degradedReason);
205
210
  } catch {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leonhardthust/dsh-decision-engine",
3
- "version": "1.9.1",
3
+ "version": "1.9.2",
4
4
  "description": "dsh-decision-engine: a cheap judgement layer for DeepSeek Harness — a small decision model answers structured questions before the main model acts, so calls can be allowed, escalated, rerouted to a cheaper model, or skipped. All eight decision points (tool authorization, model routing, delegation, compaction, turn continuation, retrieval, skill pick, COT translation) are implemented and individually switchable. Works against any service exposing a decision endpoint: built-in presets (system one, bamboonode, SiliconFlow, Silicon Flow) or a custom base URL.",
5
5
  "icon": "./icon.svg",
6
6
  "type": "module",