@sema-agent/server 7.117.0 → 7.118.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.
Files changed (98) hide show
  1. package/MIGRATION.md +97 -0
  2. package/USAGE.md +37 -18
  3. package/dist/approval-card.d.ts +40 -10
  4. package/dist/approval-card.js +11 -1
  5. package/dist/approval.d.ts +4 -12
  6. package/dist/approval.js +4 -26
  7. package/dist/boot/parked-revive-gate.d.ts +1 -7
  8. package/dist/boot/parked-revive-gate.js +2 -12
  9. package/dist/boot/resolve-spec.js +8 -13
  10. package/dist/boot/runner-deps.js +2 -2
  11. package/dist/boot/stage-07-capability-layer.js +2 -2
  12. package/dist/capabilities/hands-lane.d.ts +25 -0
  13. package/dist/capabilities/hands-lane.js +1 -1
  14. package/dist/capabilities/mcp-probe.d.ts +2 -2
  15. package/dist/config.d.ts +14 -18
  16. package/dist/config.js +13 -24
  17. package/dist/deployment-governance.d.ts +2 -7
  18. package/dist/deployment-governance.js +3 -5
  19. package/dist/host-decision.d.ts +83 -25
  20. package/dist/host-decision.js +31 -1
  21. package/dist/http/admission.js +7 -1
  22. package/dist/http/approval-sweeps.js +1 -1
  23. package/dist/http/mcp-dial-deadline.d.ts +35 -0
  24. package/dist/http/mcp-dial-deadline.js +21 -0
  25. package/dist/http/memory-compliance-failure-reply.d.ts +11 -5
  26. package/dist/http/memory-compliance-failure-reply.js +17 -8
  27. package/dist/http/memory-store-refusal-reply.d.ts +14 -11
  28. package/dist/http/memory-store-refusal-reply.js +16 -5
  29. package/dist/http/resume-legs.js +7 -3
  30. package/dist/http/routes/approvals-assistant.js +21 -24
  31. package/dist/http/routes/capabilities.js +2 -0
  32. package/dist/http/routes/mcp-reconnect.d.ts +17 -0
  33. package/dist/http/routes/mcp-reconnect.js +6 -1
  34. package/dist/http/routes/memory-bundle.js +1 -1
  35. package/dist/http/routes/memory-compliance.js +1 -1
  36. package/dist/http/routes/memory-origin.js +1 -1
  37. package/dist/http/routes/runs.js +41 -33
  38. package/dist/http/routes/sessions.d.ts +1 -1
  39. package/dist/http/routes/sessions.js +3 -6
  40. package/dist/http/routes/workflows.js +2 -0
  41. package/dist/http/wire-types.d.ts +6 -1
  42. package/dist/memory-store-refusal.d.ts +42 -17
  43. package/dist/memory-store-refusal.js +25 -6
  44. package/dist/model-unreachable-notice.d.ts +1 -1
  45. package/dist/namespaced-rule-name.d.ts +45 -41
  46. package/dist/namespaced-rule-name.js +36 -22
  47. package/dist/observability/corrupt-read-seat.d.ts +4 -0
  48. package/dist/observability/corrupt-read-seat.js +1 -0
  49. package/dist/observability/fail-open.d.ts +14 -2
  50. package/dist/observability/fail-open.js +14 -2
  51. package/dist/operator-ask.d.ts +4 -1
  52. package/dist/orchestration/workflow-agent-session-index.d.ts +82 -59
  53. package/dist/orchestration/workflow-agent-session-index.js +80 -14
  54. package/dist/orchestration/workflow-completion-inbox.d.ts +5 -0
  55. package/dist/orchestration/workflow-completion-inbox.js +39 -17
  56. package/dist/orchestration/workflow-notify-journal.d.ts +1 -1
  57. package/dist/orchestration/workflow-notify-journal.js +7 -2
  58. package/dist/outbound-dispatcher.d.ts +29 -20
  59. package/dist/outbound-dispatcher.js +105 -16
  60. package/dist/parked-decide.d.ts +43 -48
  61. package/dist/parked-decide.js +54 -16
  62. package/dist/permission-rule-vocab.d.ts +4 -3
  63. package/dist/permission-rule-vocab.js +1 -1
  64. package/dist/plugins/approval-ask-store-sql.d.ts +17 -20
  65. package/dist/plugins/approval-ask-store-sql.js +0 -3
  66. package/dist/plugins/one-time-migrations.d.ts +5 -0
  67. package/dist/plugins/one-time-migrations.js +110 -4
  68. package/dist/plugins/pg-pool.js +1 -1
  69. package/dist/plugins/tidb-pool.js +1 -1
  70. package/dist/plugins/web-search.d.ts +20 -1
  71. package/dist/plugins/web-search.js +39 -10
  72. package/dist/plugins/workflow-run-store-sql.d.ts +7 -6
  73. package/dist/plugins/workflow-run-store-sql.js +5 -7
  74. package/dist/request-key-closure.js +1 -1
  75. package/dist/request-value-closure.d.ts +1 -1
  76. package/dist/rule-entry-list.d.ts +35 -46
  77. package/dist/rule-entry-list.js +17 -25
  78. package/dist/rules-consent.js +3 -0
  79. package/dist/run-local.js +1 -0
  80. package/dist/runs.d.ts +1 -1
  81. package/dist/session-policy-wire.d.ts +13 -12
  82. package/dist/session-policy-wire.js +11 -6
  83. package/dist/spec-fields.d.ts +7 -9
  84. package/dist/spec-fields.js +4 -7
  85. package/dist/standing-answer-grant.d.ts +14 -2
  86. package/dist/standing-answer-grant.js +8 -1
  87. package/dist/task-settings.js +8 -4
  88. package/dist/tool-approval.d.ts +53 -112
  89. package/dist/tool-approval.js +75 -55
  90. package/dist/trace/core-keyset-guard.d.ts +5 -5
  91. package/dist/trace/self-minted-wire.d.ts +16 -2
  92. package/dist/trace/self-minted-wire.js +7 -0
  93. package/dist/user-input-ledger.d.ts +24 -12
  94. package/dist/user-input-ledger.js +33 -14
  95. package/package.json +2 -2
  96. package/scripts/drain-check-local.mjs +74 -3
  97. package/scripts/drain-check.mysql.sql +43 -17
  98. package/scripts/drain-check.pg.sql +46 -20
@@ -1,3 +1,4 @@
1
+ import { validateHeaderValue } from "node:http";
1
2
  import { createSearxngSearchBackend } from "@sema-agent/core";
2
3
  import { isHierarchicalUrl, redactSecrets } from "../trace/redact.js";
3
4
  export const WEB_SEARCH_PROVIDERS = ["brave", "tavily", "searxng"];
@@ -28,6 +29,34 @@ function withTimeout(signal, ms) {
28
29
  async function errorText(res) {
29
30
  return (await res.text().catch(() => "")).slice(0, 200);
30
31
  }
32
+ export function isHttpHeaderValue(value) {
33
+ try {
34
+ const sent = new Headers([["x-probe", value]]).get("x-probe") ?? "";
35
+ validateHeaderValue("x-probe", sent);
36
+ return true;
37
+ }
38
+ catch {
39
+ return false;
40
+ }
41
+ }
42
+ const API_KEY_FORM = "must be usable as an HTTP header value (it contains a character an HTTP header cannot carry, such as a line break)";
43
+ function apiKeyHeaderValue(provider, key) {
44
+ switch (provider) {
45
+ case "brave":
46
+ case "searxng":
47
+ return key;
48
+ case "tavily":
49
+ return `Bearer ${key}`;
50
+ }
51
+ }
52
+ function authHeaderValue(provider, cfg) {
53
+ if (!cfg.apiKey)
54
+ throw new Error(`WEB_SEARCH_API_KEY is required for the ${provider} provider`);
55
+ const value = apiKeyHeaderValue(provider, cfg.apiKey);
56
+ if (!isHttpHeaderValue(value))
57
+ throw new Error(`WEB_SEARCH_API_KEY ${API_KEY_FORM}`);
58
+ return value;
59
+ }
31
60
  async function httpFailure(provider, res) {
32
61
  const body = await errorText(res);
33
62
  return new Error(`${provider} search failed: HTTP ${res.status}${body ? ` — ${body}` : ""}`);
@@ -61,20 +90,18 @@ function normalize(raw, pick, max, provider) {
61
90
  return out;
62
91
  }
63
92
  async function braveSearch(fetchImpl, cfg, query, max, signal) {
64
- if (!cfg.apiKey)
65
- throw new Error("WEB_SEARCH_API_KEY is required for the brave provider");
93
+ const token = authHeaderValue("brave", cfg);
66
94
  const url = new URL(cfg.endpoint || BRAVE_BASE);
67
95
  url.searchParams.set("q", query);
68
96
  url.searchParams.set("count", String(Math.min(20, max)));
69
- const res = await fetchImpl(url, { headers: { Accept: "application/json", "X-Subscription-Token": cfg.apiKey }, signal });
97
+ const res = await fetchImpl(url, { headers: { Accept: "application/json", "X-Subscription-Token": token }, signal });
70
98
  if (!res.ok)
71
99
  throw await httpFailure("brave", res);
72
100
  const data = (await res.json());
73
101
  return normalize(data.web?.results, (r) => ({ title: r.title, url: r.url, snippet: r.description }), max, "brave");
74
102
  }
75
103
  async function tavilySearch(fetchImpl, cfg, query, max, signal, opts) {
76
- if (!cfg.apiKey)
77
- throw new Error("WEB_SEARCH_API_KEY is required for the tavily provider");
104
+ const authorization = authHeaderValue("tavily", cfg);
78
105
  const body = { query, max_results: Math.min(20, max) };
79
106
  if (opts?.allowedDomains?.length)
80
107
  body.include_domains = opts.allowedDomains.slice(0, 300);
@@ -82,7 +109,7 @@ async function tavilySearch(fetchImpl, cfg, query, max, signal, opts) {
82
109
  body.exclude_domains = opts.blockedDomains.slice(0, 150);
83
110
  const res = await fetchImpl(new URL(cfg.endpoint || TAVILY_BASE), {
84
111
  method: "POST",
85
- headers: { "Content-Type": "application/json", Authorization: `Bearer ${cfg.apiKey}` },
112
+ headers: { "Content-Type": "application/json", Authorization: authorization },
86
113
  body: JSON.stringify(body),
87
114
  signal,
88
115
  });
@@ -153,12 +180,14 @@ export function shouldProbeWebSearchOnBoot(env = process.env) {
153
180
  const ABSENT = { ok: true };
154
181
  const valueOf = (j) => (j.ok ? j.value : undefined);
155
182
  const isBlank = (s) => s.trim() === "";
156
- function judgeText(raw) {
183
+ function judgeApiKey(provider, raw) {
157
184
  if (raw === undefined || raw === null)
158
185
  return ABSENT;
159
186
  if (typeof raw !== "string")
160
187
  return { ok: false, issue: "must be a string" };
161
- return isBlank(raw) ? ABSENT : { ok: true, value: raw };
188
+ if (isBlank(raw))
189
+ return ABSENT;
190
+ return isHttpHeaderValue(apiKeyHeaderValue(provider, raw)) ? { ok: true, value: raw } : { ok: false, issue: API_KEY_FORM };
162
191
  }
163
192
  const ENDPOINT_FORM = "must be an absolute URL with a scheme and a host, such as https://search.example.com (a bare host or host:port is not one)";
164
193
  const ENDPOINT_USERINFO = "must not carry userinfo (user or user:password before the host)";
@@ -254,7 +283,7 @@ export function webSearchConfigFromEnv(env = process.env) {
254
283
  if (provider === undefined)
255
284
  return undefined;
256
285
  return assembleBackendConfig(provider, {
257
- apiKey: envValue("WEB_SEARCH_API_KEY", judgeText(env.WEB_SEARCH_API_KEY)),
286
+ apiKey: envValue("WEB_SEARCH_API_KEY", judgeApiKey(provider, env.WEB_SEARCH_API_KEY)),
258
287
  endpoint: envValue("WEB_SEARCH_ENDPOINT", judgeEndpoint(env.WEB_SEARCH_ENDPOINT)),
259
288
  searxngParams: envValue("WEB_SEARCH_SEARXNG_PARAMS", judgeSearxngParams(env.WEB_SEARCH_SEARXNG_PARAMS)),
260
289
  maxResults: envValue("WEB_SEARCH_MAX_RESULTS", judgeCount(envNumber(env.WEB_SEARCH_MAX_RESULTS))),
@@ -284,7 +313,7 @@ export function judgeRequestWebSearch(raw) {
284
313
  if (!provider.ok)
285
314
  return { kind: "malformed", issue: `settings.webSearch.provider ${provider.issue}` };
286
315
  const fields = {
287
- apiKey: judgeText(s.apiKey),
316
+ apiKey: judgeApiKey(provider.value, s.apiKey),
288
317
  endpoint: judgeEndpoint(s.endpoint),
289
318
  searxngParams: judgeSearxngParams(s.searxngParams),
290
319
  maxResults: judgeCount(s.maxResults),
@@ -183,25 +183,26 @@ export declare class SqlWorkflowNotifyJournalStore implements WorkflowNotifyJour
183
183
  /**
184
184
  * S-185 车CM —— **workflow 出身 park 的有界 join 索引** SQL 双生(契约与纪律的唯一属主 =
185
185
  * {@link import("../orchestration/workflow-agent-session-index.js").WorkflowAgentSessionIndex} 的文件头,
186
- * 这里只记方言差)。行是 `WorkflowRun` 的派生投影,写者只有一个(Journaling 装饰器的写观察点)。
186
+ * 这里只记方言差)。行是 `WorkflowRun` 的派生投影(一行一枚指针),写者只有一个(Journaling 装饰器的写观察点);
187
+ * 收割在删 run 行的同一个方法里按同一张待删 id 表删它们的指针({@link SqlWorkflowRunStore.reap})。
187
188
  *
188
189
  * ── 方言差,保持显式 ──────────────────────────────────────────────────────────────────────────
189
190
  * - `?` 占位 vs `$n`,且 `syncRun` 的删除腿在 PG 上要按 `params.length` 记位(与 `listByScope` 同因)。
190
191
  * - 「删掉这条 run 里已不在 rows 里的旧行」:TiDB `session_id NOT IN (?,?,…)`(动态占位数)vs
191
192
  * PG `session_id <> ALL($n::text[])`(数组绑定)—— 两边同一件事,语句结构不同(reap 的批删同先例)。
192
- * - upsert:TiDB `REPLACE INTO`(整行替换)vs PG `INSERT … ON CONFLICT (session_id) DO UPDATE`。
193
+ * - upsert(主键 `(session_id, run_id)`):TiDB `REPLACE INTO`(整行替换)vs PG `INSERT … ON CONFLICT (session_id, run_id) DO UPDATE`。
193
194
  * - schema 属主同店内其余表:TiDB DDL 在 tidb-pool.ts,PG DDL 在 pg-pool.ts(店不建表)。
194
195
  *
195
- * 🔴 **写序**:先 upsert 再删。反过来(先删后 upsert)会在两条语句之间留出一个**索引行不存在**的窗口,
196
- * 而这期间进来的 `/decide` 会诚实 miss ⇒ 一次本可受理的批准被退回。先 upsert 的最坏形是窗口内多一条
197
- * 旧行(读口按 session_id 精确查,取 run 后还要逐字比对赎回键 ⇒ 多余的行只会 miss,不会错配)。
196
+ * 🔴 **写序**:先 upsert 再删。反过来(先删后 upsert)会在两条语句之间留出一个**指针不存在**的窗口,
197
+ * 而这期间进来的 `/decide` 会诚实 miss ⇒ 一次本可受理的批准被退回。先 upsert 的最坏形是窗口内多一枚
198
+ * 旧指针(判决从记录读 ⇒ 多读一条记录,不会错配)。
198
199
  */
199
200
  export declare class SqlWorkflowAgentSessionIndex implements WorkflowAgentSessionIndex {
200
201
  private readonly db;
201
202
  constructor(db: SqlDriver);
202
203
  private q;
203
204
  syncRun(runId: string, rows: readonly WorkflowAgentSessionRow[]): Promise<void>;
204
- findByAgentSession(sessionId: string): Promise<WorkflowAgentSessionRow | null>;
205
+ listByAgentSession(sessionId: string): Promise<WorkflowAgentSessionRow[]>;
205
206
  }
206
207
  /** MySQL-protocol (TiDB) bindings — historical class names preserved; `onWarn` is an ADDITIVE optional 2nd
207
208
  * ctor arg (existing 1-arg call sites are unaffected — see {@link SqlWorkflowRunStore}'s C5 trace channel). */
@@ -160,6 +160,7 @@ export class SqlWorkflowRunStore {
160
160
  if (doomed.length === 0)
161
161
  return 0;
162
162
  const res = await this.db.query(this.q(`DELETE FROM workflow_run WHERE scope = ? AND id IN (${doomed.map(() => "?").join(",")})`, "DELETE FROM workflow_run WHERE scope = $1 AND id = ANY($2::text[])"), this.db.dialect === "tidb" ? [scope, ...doomed] : [scope, doomed]);
163
+ await this.db.query(this.q(`DELETE FROM workflow_agent_session WHERE scope = ? AND run_id IN (${doomed.map(() => "?").join(",")})`, "DELETE FROM workflow_agent_session WHERE scope = $1 AND run_id = ANY($2::text[])"), this.db.dialect === "tidb" ? [scope, ...doomed] : [scope, doomed]);
163
164
  return res.affected;
164
165
  }
165
166
  }
@@ -311,7 +312,7 @@ export class SqlWorkflowAgentSessionIndex {
311
312
  async syncRun(runId, rows) {
312
313
  for (const r of rows) {
313
314
  await this.db.query(this.q("REPLACE INTO workflow_agent_session (session_id, run_id, agent_key, scope) VALUES (?,?,?,?)", `INSERT INTO workflow_agent_session (session_id, run_id, agent_key, scope) VALUES ($1,$2,$3,$4)
314
- ON CONFLICT (session_id) DO UPDATE SET run_id = EXCLUDED.run_id, agent_key = EXCLUDED.agent_key, scope = EXCLUDED.scope`), [r.sessionId, r.runId, r.agentKey, r.scope]);
315
+ ON CONFLICT (session_id, run_id) DO UPDATE SET agent_key = EXCLUDED.agent_key, scope = EXCLUDED.scope`), [r.sessionId, r.runId, r.agentKey, r.scope]);
315
316
  }
316
317
  const keep = rows.map((r) => r.sessionId);
317
318
  if (this.db.dialect === "tidb") {
@@ -322,12 +323,9 @@ export class SqlWorkflowAgentSessionIndex {
322
323
  await this.db.query(`DELETE FROM workflow_agent_session WHERE run_id = $1${keep.length > 0 ? " AND session_id <> ALL($2::text[])" : ""}`, keep.length > 0 ? [runId, keep] : [runId]);
323
324
  }
324
325
  }
325
- async findByAgentSession(sessionId) {
326
- const { rows } = await this.db.query(this.q("SELECT session_id, run_id, agent_key, scope FROM workflow_agent_session WHERE session_id = ?", "SELECT session_id, run_id, agent_key, scope FROM workflow_agent_session WHERE session_id = $1"), [sessionId]);
327
- const r = rows[0];
328
- if (!r)
329
- return null;
330
- return { sessionId: String(r.session_id), runId: String(r.run_id), agentKey: String(r.agent_key), scope: String(r.scope) };
326
+ async listByAgentSession(sessionId) {
327
+ const { rows } = await this.db.query(this.q("SELECT session_id, run_id, agent_key, scope FROM workflow_agent_session WHERE session_id = ? ORDER BY run_id", "SELECT session_id, run_id, agent_key, scope FROM workflow_agent_session WHERE session_id = $1 ORDER BY run_id"), [sessionId]);
328
+ return rows.map((r) => ({ sessionId: String(r.session_id), runId: String(r.run_id), agentKey: String(r.agent_key), scope: String(r.scope) }));
331
329
  }
332
330
  }
333
331
  export class TiDBWorkflowRunStore extends SqlWorkflowRunStore {
@@ -19,7 +19,7 @@ export function keyClosureClause(names, label) {
19
19
  }
20
20
  const TASK_BODY_DECLARED_KEYS = [
21
21
  "a2aPeers", "acceptPartialRestore", "additionalDirectories", "additionalReadDirectories", "agents",
22
- "appendSystemPrompt", "attachmentIds", "attachments", "capabilitiesNeeded", "cascade", "clientContext",
22
+ "appendSystemPrompt", "approvalReach", "attachmentIds", "attachments", "capabilitiesNeeded", "cascade", "clientContext",
23
23
  "compaction", "compactionModel", "council", "cwd", "debate", "deferTools", "deviceId", "disableSkills", "enableFork",
24
24
  "excludeAllTools", "excludeTools", "fileHistory",
25
25
  "finalVerification", "forwardSubagentEvents", "images", "inheritEnv", "interactiveTools", "jobId", "limits", "maxCostUsd",
@@ -52,7 +52,7 @@ export declare const MEMORY_OFF_SPELLING_MESSAGE = "memory must be the string 'o
52
52
  export declare const FILE_HISTORY_OFF_SPELLING_MESSAGE = "fileHistory must be the string 'disabled' when present (the request-level declaration that this run records no file history \u2014 no backup of an edited file, and no file restore in the same request; absent = the deployment's setting \u2014 there is no 'enabled' spelling)";
53
53
  /**
54
54
  * S-724 b′(主会话裁,车PW codex r1 [high] 复现后)—— `fileHistory: "disabled"` 与还原**同体**的拒句(`request.field_conflict`)。
55
- * 与 `tool-approval.ts` 的 `SETTLED_BY_DENY_ONLY` / `READ_ROOT_GRANT_ALLOW_ONLY` **同一族**:「只看体就能判的矛盾,在裁决之前响亮说」。
55
+ * 与 `tool-approval.ts` 的 `READ_ROOT_GRANT_ALLOW_ONLY` **同一族**:「只看体就能判的矛盾,在裁决之前响亮说」。
56
56
  * 这一对是调用方自己写出的矛盾(关掉这条 run 的文件历史,又要从文件历史还原);core 也拒(`rewind.capture_disabled`)。立门时(7.104.0)
57
57
  * core 的拒落在 prepare 的**会话分叉之后**(`prepare-acquire-reconcile` 先按 `resumeAt` 把活动分支移过去,`prepare-file-history` 才判),
58
58
  * 且拒后不回滚 —— 所以本层在任何副作用之前拒。**这不是替 core 再判一次「捕获关了能不能还原」**:判的是体里两个键
@@ -1,28 +1,26 @@
1
1
  /**
2
- * **规则条目表在本仓受理口的判** —— 一份,三张表同调(core 7.42.0 起「这个调用不用问」只有一个去处:引擎的 allow 层;条目的文法、匹配、
3
- * auto 剔除、读成空的通告都归引擎,本仓不写第二套):
2
+ * **规则条目表在本仓受理口的判** —— 一份,四张表同调(core 7.42.0 起「这个调用不用问」只有一个去处:引擎的 allow 层;条目的文法、匹配、
3
+ * 名字、读成空的通告都归引擎,本仓不写第二套):
4
4
  * · 请求自带技能的 `allowedTools` / `disallowedTools`(顶层 `skills[]` 与子代 `agents[].skills[]` 两处受理口,`spec-fields.ts` / `http/admission.ts`);
5
5
  * · 请求的 `settings.permissions.allow`(整份原样投 `TaskSpec.allowedTools`,`task-settings.ts`);
6
6
  * · 运维的 `APPROVAL_GRANT`(整份投 `RunnerDeps.allowedTools`,`config.ts` 启动校验)。
7
7
  *
8
- * 本仓在这些表上只剩下面三道判加名字层一问(内容形、`*`、写路径形引擎怎么读就怎么算)。**名字层四张表同一只判官**(core `judgeRuleToolName`):请求的
9
- * `settings.permissions.allow` 与 deny / ask 两桶经 `config.ts` `unmatchableToolNamesRefusal`,技能两只清单经 `spec-fields.ts` `skillToolListRefusal`,运维表经
10
- * `config.ts` `approvalGrantRefusal` —— 三只包装调的都是本文件 {@link retiredRuleEntryOf}(退役名 / 短名 / 协议名;不问文法),句子一组。
8
+ * 本仓在这些表上只剩下面几问(内容形、`*`、写路径形、名字引擎怎么读就怎么算):
11
9
  * ① **形**({@link ruleEntryListShapeOk}):字符串数组。引擎逐条迭代;一个裸字符串会被逐字符读成几条(deny 侧 = 意图静默落空)。
12
10
  * 本谓词不带条数帽(引擎的 `screenAllowedToolsValue` 只判形);条数帽是另一只函数 {@link allowLayerEntryCountIssue}(`MAX_ALLOW_LAYER_ENTRIES`,
13
11
  * 请求 allow 桶、技能两只清单、运维表四张表同一只、同一句)。
14
- * ④ **退役名 / 无协议前缀的短名**({@link retiredRuleEntryOf}):判官是 core 导出的 `judgeRuleToolName`(请求三桶、会话规则写口、准备阶段的名审计
15
- * 同一只、同一份空名册)。引擎的 allow 层对这一族今天照编一个永不命中的名字、不通告 —— 7.116.0 的运维表对它拒启,本仓不许在同一张表上
16
- * 由响亮变静默;翻转条件与 `config.ts` 的 `allow-layer` 消费者同一句(引擎对非现行名出通告之后退役)。
17
- * ② **协议名的对端段铸不出 / 缺段 / 对端段写通配**({@link unreachableRuleEntryOf}):判官是 `namespaced-rule-name.ts` 的那一只、取名口是
18
- * `ruleEntryToolName`(配置表与会话规则写口同读)。引擎今天对这一族照编、一条都不命中、不通告 —— 等引擎的判官自己给这个判词时整只退役。
19
- * ③ **引擎读成空的条目**({@link inertRuleEntriesOf},判据 = 引擎包根 `compileAllowList(entries, behavior).inert`,原因词是引擎的
20
- * `ALLOW_ENTRY_INERT_REASONS`):deny 侧(技能的 `disallowedTools`)⇒ 拒 —— 一条以为拒了其实什么也不拒的条目是安全轴上的静默放宽;
21
- * 运维的 allow 表 ⇒ 拒启(运维自己的配置,写错了当场改)。**请求的 allow 侧不拒**:引擎在开跑时发
22
- * `permission.allowed_tool_not_granted`(受众 = 用户)—— 终端会把用户设置里的 allow 整份投过来,一条今天读成空的写法不能把整条请求打回。
12
+ * ② **引擎读不到的一形**({@link emptyToolSegmentEntryOf}):工具段为空的协议名(`mcp__srv__`)—— 引擎的判官答 `current`、条目读成一条精确名行、永不命中;
13
+ * 判据与句子在 `namespaced-rule-name.ts`(配置表、会话规则写口同一只)。四张表都拒。
14
+ * ③ **引擎读成空的条目**({@link inertRuleEntriesOf},判据 = 引擎包根 `compileAllowList(entries, behavior, 名册)`,名册 = 空集 `NO_DECLARED_TOOL_NAMES`
15
+ * —— 条目的工具名也过引擎判官:退役名 / 无前缀短名 / 对端段铸不出读成 `tool_name`,带判词与 1:1 现名;原因词是引擎的 `ALLOW_ENTRY_INERT_REASONS`):
16
+ * 运维的 allow 表拒启(运维自己的配置,写错了当场改);技能的拒侧(`disallowedTools`)拒 —— 提交 400、存量体重放 409(下面的 {@link skillDenyInertEntryOf});
17
+ * 请求的 allow 侧(`settings.permissions.allow`、技能 `allowedTools`)不拒:引擎在开跑时发 `permission.allowed_tool_not_granted`(受众 = 用户)。
18
+ * 技能拒侧那一拒与引擎 #1372(core 7.43.0:那只技能不激活)是同一件事的两处 —— 7.118.0 重估过、本版**不退**:本仓的续跑腿把引擎的
19
+ * `checkpoint.config_refused`(`config.skill_deny_unreadable`)答得不一(资源停车续跑腿 500、`/decide` 先结了卡、`/wake` 先铸了 run 行),先统一那一层再退
20
+ * (重估读数与退役条件在下面那只函数头注)。
23
21
  */
24
- import { type AllowEntryInertReason, type RuleToolNameVerdict } from "@sema-agent/core";
25
- import { type UnreachableNamespacedRuleName } from "./namespaced-rule-name.js";
22
+ import { compileAllowList } from "@sema-agent/core";
23
+ import { emptyToolSegmentOf } from "./namespaced-rule-name.js";
26
24
  export declare function presentRuleEntry(entry: string): string;
27
25
  /** ① 形:字符串数组(空串也是字符串 —— 引擎把它读成 `grammar` 并通告 / 拒,见 ③)。 */
28
26
  export declare const ruleEntryListShapeOk: (v: unknown) => v is readonly string[];
@@ -32,7 +30,9 @@ export declare const ruleEntryListShapeOk: (v: unknown) => v is readonly string[
32
30
  * 与技能授予表的 `hold` 都对已有条目线性查重 ⇒ 整表平方级、同步执行;core 7.42.0 上 1 万条约 2 s、2 万条约 6 s,本仓亲测)。不设帽 = 一份
33
31
  * 8 MiB 以内的请求体能把整个进程的事件循环卡住几十秒。与 deny / ask 两桶的帽(`permission-rule-list.ts` `MAX_PERMISSION_RULES_PER_LIST`,
34
32
  * 编译口的整表帽)是两件事、两个名字,数字恰好相同。
35
- * 翻转条件:上游把播种改成集合查重之后,按新的开销重估这个数(或整只退役)。
33
+ * 7.118.0 重估(core 7.43.0 #1370 把播种改成集合查重,本仓同一只探针亲测 `AllowEntryTable.seed`:7.42.1 上 1 千条 127 ms、1 万条 4.9 s;7.44.0 上 1 千条 8 ms、
34
+ * 1 万条 160 ms、2 万条 226 ms、10 万条 1.08 s —— 线性了,但仍是同步工作,8 MiB 的体装得下几十万条)⇒ 帽留着、数不动:理由由「平方级」换成「线性但同步、
35
+ * 没有真实用法需要上千条」。抬不抬这个数是产品判断,不随提货改。
36
36
  */
37
37
  export declare const MAX_ALLOW_LAYER_ENTRIES = 1000;
38
38
  /** 超帽的拒句(不截:截掉的尾项读起来像「在生效」)—— 句子的唯一铸点(请求 allow 桶的 wire 拒体按字段重铸也调它)。 */
@@ -43,36 +43,24 @@ export declare function allowLayerEntryCountIssue(field: string, list: readonly
43
43
  readonly count: number;
44
44
  readonly cap: number;
45
45
  } | undefined;
46
- /** 一张表里第一条「协议名写得出、却永远匹配不到」的条目。 */
47
- export interface UnreachableRuleEntry {
46
+ /** 一张表里第一条「引擎读不到、却永远匹配不到」的条目(工具段为空的协议名)。 */
47
+ export interface EmptyToolSegmentEntry {
48
48
  readonly entry: string;
49
- readonly verdict: UnreachableNamespacedRuleName;
49
+ readonly ns: NonNullable<ReturnType<typeof emptyToolSegmentOf>>;
50
50
  }
51
- /** ② 第一条够不着任何挂载工具的协议名条目(都够得着 ⇒ `undefined`)。纯字符串运算。 */
52
- export declare function unreachableRuleEntryOf(list: readonly string[]): UnreachableRuleEntry | undefined;
51
+ /** ② 第一条工具段为空的协议名条目(没有 ⇒ `undefined`)。纯字符串运算。 */
52
+ export declare function emptyToolSegmentEntryOf(list: readonly string[]): EmptyToolSegmentEntry | undefined;
53
53
  /** ② 的句子(`<field> names "<entry>", which can never match a tool (…)`;后半与配置表 / 会话规则写口同一只)。 */
54
- export declare function describeUnreachableRuleEntry(field: string, u: UnreachableRuleEntry): string;
55
- /** ④ 一张表里第一条退役名 / 无协议前缀短名的条目(都现行 ⇒ `undefined`)。名册恒空:本仓不在退役名下挂自有工具(与 `config.ts` 同注)。 */
56
- export interface RetiredRuleEntry {
57
- readonly entry: string;
58
- readonly verdict: Exclude<RuleToolNameVerdict, {
59
- kind: "current";
60
- }>;
61
- }
62
- export declare function retiredRuleEntryOf(list: readonly string[]): RetiredRuleEntry | undefined;
63
- /** ④ 的句子:退役名给 core 的现役指引;短名枚举协议表前缀(与 `config.ts` 对请求三桶的说法同口径)。 */
64
- export declare function describeRetiredRuleEntry(field: string, r: RetiredRuleEntry): string;
65
- /** 一条引擎读成空的条目与引擎给的原因词。 */
66
- export interface InertRuleEntry {
67
- readonly entry: string;
68
- readonly reason: AllowEntryInertReason;
69
- }
70
- /** ③ 引擎按 `behavior` 读这张表时读成空的条目(引擎的整表读法:allow 表里的 `*` 让整表读成空;deny 表里的 `*` 只读空它自己)。 */
54
+ export declare function describeEmptyToolSegmentEntry(field: string, u: EmptyToolSegmentEntry): string;
55
+ /** 一条引擎读成空的条目(引擎的形:原因词,名字层另带判词 / 1:1 现名;包根不导出这只具名型 ⇒ 从 `compileAllowList` 的返回型取,不手抄)。 */
56
+ export type InertRuleEntry = ReturnType<typeof compileAllowList>["inert"][number];
57
+ /** ③ 引擎按 `behavior` 读这张表时读成空的条目(引擎的整表读法:allow 表里的 `*` 让整表读成空;deny 表里的 `*` 只读空它自己);条目名过引擎判官(名册 = 空集)。 */
71
58
  export declare function inertRuleEntriesOf(list: readonly string[], behavior: "allow" | "deny"): readonly InertRuleEntry[];
72
59
  /**
73
60
  * 原因词 → 「怎么写」(闭集穷举:引擎加一个原因词 ⇒ 编译红)。句子只说这一条为什么什么也不做、换成哪种写法;原因词本身原样带在句子里。
61
+ * 名字层两词带引擎给的判词 / 1:1 现名(有就点名)。
74
62
  */
75
- export declare function inertRuleEntryGuidance(reason: AllowEntryInertReason): string;
63
+ export declare function inertRuleEntryGuidance(i: InertRuleEntry): string;
76
64
  /** ③ 的句子(`<field> entry "<e>" reads as nothing (<reason>): <怎么写>`;多条用分号连)。 */
77
65
  export declare function describeInertRuleEntries(field: string, inert: readonly InertRuleEntry[]): string;
78
66
  /** 提交体里技能所在的层(命中句与排空读数点名用):顶层 `skills[]` / 子代 `agents[].skills[]`。 */
@@ -88,12 +76,13 @@ export interface SkillDenyInertEntry {
88
76
  * 🔴 **调用点两处**:`resolveSpec`(`boot/resolve-spec.ts`,**拒点** —— 新提交与五条续跑腿都经过它,所以「新提交会被拒的体,续跑重放时同样拒」
89
77
  * 是结构性的)与 `http/checkpoint-ctx.ts` `storedBodyReplayRefusal`(存量体重放前的**预判**,五个续跑问点同调 —— 含 SLA 拒扫的跳过臂与终态 steer
90
78
  * 铸唤醒卡之前,这两处不经 `resolveSpec`)。提交腿 ⇒ 400 `request.field_invalid`;续跑腿 ⇒ 409 `resume_blocked_by_policy`(`blockedBy: request.field_invalid`,
91
- * 句子同一句,铸点 `checkpoint-ctx.ts` `resumeBlockedByPolicyAnswer`),拒在认领之前,行 / 卡 / ctx 不动。
92
- * 为什么续跑也要判:引擎 7.41 的规则解析先去首尾空白(`"Bash "` 是一条有效的拒),7.42 逐字读(同一条读成 `grammar`,什么也不拒)——
93
- * 7.116.x 上停驻、体里带这种条目的行,不判就是升级后那条拒无声消失。随包排空判据 `skill-deny-inert`(`scripts/drain-check-local.mjs` 调本函数)。
94
- * 判据就是引擎的读法(`compileAllowList(entries, "deny").inert`),本函数只负责走提交体的两层、给出句子与层。
95
- * 翻转条件:引擎今天对读成空的拒侧条目只发通告、照常激活技能;上游已认改成 fail-closed(读不出的拒侧条目所守的那次激活不发生)。
96
- * 那一版到货后,本仓这一处与引擎的拒是同一件事的两处 —— 届时重估是否退役本判(连同随包排空判据 `skill-deny-inert`)。
79
+ * 句子同一句,铸点 `checkpoint-ctx.ts` `resumeBlockedByPolicyAnswer`),拒在认领之前,行 / 卡 / ctx 不动。随包排空判据 `skill-deny-inert`
80
+ * (`scripts/drain-check-local.mjs` 调本函数)。判据就是引擎的读法(`compileAllowList(entries, "deny", 名册 = 空集)`,与 {@link inertRuleEntriesOf} 同一只;
81
+ * 7.118.0 起条目名也过引擎判官,退役名 / 对端段铸不出读成 `tool_name` —— 技能清单的名字层在受理口不再另判)。
82
+ * 7.118.0 重估(core 7.43.0 #1372 到货:读成空的拒侧条目让那只技能不激活)—— **本版不退**。实测(本判退掉之后跑同一批夹具):引擎的拒到本仓各续跑腿的
83
+ * 答法不一 —— 资源停车续跑腿把 `checkpoint.config_refused` 答成 500 `internal.error`;工具审批 `/decide` 在引擎拒之前已把卡结掉;`/wake` 在引擎拒之前已铸
84
+ * run 行;子代 `agents[].skills[]` 不在引擎的认领前屏里(续跑放行、技能在调用时被拒)。退役条件:本仓续跑腿对引擎 `checkpoint.config_refused` 的答法
85
+ * 收成一只(拒在认领前、行 / 卡不动、同一个 409)之后,本判与本仓的预判一起退,排空判据随之重估。
97
86
  */
98
87
  export declare function skillDenyInertEntryOf(body: unknown): SkillDenyInertEntry | undefined;
99
88
  //# sourceMappingURL=rule-entry-list.d.ts.map
@@ -1,5 +1,5 @@
1
- import { compileAllowList, judgeRuleToolName, PROTOCOL_TABLE } from "@sema-agent/core";
2
- import { describeUnreachableNamespacedRuleName, ruleEntryToolName, unreachableNamespacedRuleNameOf } from "./namespaced-rule-name.js";
1
+ import { compileAllowList } from "@sema-agent/core";
2
+ import { describeEmptyToolSegment, emptyToolSegmentOf, NO_DECLARED_TOOL_NAMES, ruleEntryToolName } from "./namespaced-rule-name.js";
3
3
  import { CONTROL_AND_BIDI_STRIP_RE } from "./text-bidi.js";
4
4
  const PRESENTED_ENTRY_MAX = 160;
5
5
  export function presentRuleEntry(entry) {
@@ -17,36 +17,28 @@ export function allowLayerEntryCountIssue(field, list) {
17
17
  return undefined;
18
18
  return { message: describeAllowLayerOverCap(field, list.length), count: list.length, cap: MAX_ALLOW_LAYER_ENTRIES };
19
19
  }
20
- export function unreachableRuleEntryOf(list) {
20
+ export function emptyToolSegmentEntryOf(list) {
21
21
  for (const entry of list) {
22
- const verdict = unreachableNamespacedRuleNameOf(ruleEntryToolName(entry));
23
- if (verdict !== undefined)
24
- return { entry, verdict };
22
+ const ns = emptyToolSegmentOf(ruleEntryToolName(entry));
23
+ if (ns !== undefined)
24
+ return { entry, ns };
25
25
  }
26
26
  return undefined;
27
27
  }
28
- export function describeUnreachableRuleEntry(field, u) {
29
- return `${field} names "${presentRuleEntry(u.entry)}", which can never match a tool (${describeUnreachableNamespacedRuleName(u.verdict)})`;
30
- }
31
- export function retiredRuleEntryOf(list) {
32
- for (const entry of list) {
33
- const verdict = judgeRuleToolName(ruleEntryToolName(entry), new Set());
34
- if (verdict.kind !== "current")
35
- return { entry, verdict };
36
- }
37
- return undefined;
38
- }
39
- export function describeRetiredRuleEntry(field, r) {
40
- const why = r.verdict.kind === "retired"
41
- ? r.verdict.guidance
42
- : `pre-prefix short form — the live name carries a protocol prefix: ${PROTOCOL_TABLE.map((e) => `${e.prefix}${presentRuleEntry(r.entry)}`).join(" or ")}`;
43
- return `${field} names "${presentRuleEntry(r.entry)}", which can never match a tool (${why})`;
28
+ export function describeEmptyToolSegmentEntry(field, u) {
29
+ return `${field} names "${presentRuleEntry(u.entry)}", which can never match a tool (${describeEmptyToolSegment(u.ns)})`;
44
30
  }
45
31
  export function inertRuleEntriesOf(list, behavior) {
46
- return compileAllowList(list, behavior).inert;
32
+ return compileAllowList(list, behavior, NO_DECLARED_TOOL_NAMES).inert;
47
33
  }
48
- export function inertRuleEntryGuidance(reason) {
34
+ export function inertRuleEntryGuidance(i) {
35
+ const reason = i.reason;
36
+ const write = i.currentName !== undefined ? ` — write "${presentRuleEntry(i.currentName)}"` : "";
49
37
  switch (reason) {
38
+ case "tool_name":
39
+ return `the tool name can never match a mounted tool (${i.verdict ?? "stale"})${write}`;
40
+ case "near_miss":
41
+ return `the tool name differs from a mounted tool only by case${write}`;
50
42
  case "wildcard_all":
51
43
  return "`*` names no tool (on an allow list it voids the whole list) — name the tools instead";
52
44
  case "namespace_wildcard":
@@ -66,7 +58,7 @@ export function inertRuleEntryGuidance(reason) {
66
58
  }
67
59
  }
68
60
  export function describeInertRuleEntries(field, inert) {
69
- return inert.map((i) => `${field} entry "${presentRuleEntry(i.entry)}" reads as nothing (${i.reason}): ${inertRuleEntryGuidance(i.reason)}`).join("; ");
61
+ return inert.map((i) => `${field} entry "${presentRuleEntry(i.entry)}" reads as nothing (${i.reason}): ${inertRuleEntryGuidance(i)}`).join("; ");
70
62
  }
71
63
  const isPlainRecord = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
72
64
  function skillDenyInertSentence(skill) {
@@ -89,6 +89,9 @@ function isSupportedRuleMatch(match) {
89
89
  case "subpath":
90
90
  case "path":
91
91
  return false;
92
+ case "name":
93
+ case "name_prefix":
94
+ return false;
92
95
  default: {
93
96
  const unknown = match;
94
97
  void unknown;
package/dist/run-local.js CHANGED
@@ -146,6 +146,7 @@ export const NO_TTY_ASK_CLASS_ROW = {
146
146
  rule_unreadable: { remedy: "terminal_only", autoBudgetReleases: false },
147
147
  ask_rule: { remedy: "approval_lists", autoBudgetReleases: true },
148
148
  denial_limit_fallback: { remedy: "terminal_only", autoBudgetReleases: false },
149
+ classifier_no_ruling: { remedy: "terminal_only", autoBudgetReleases: false },
149
150
  operator_forced: { remedy: "deployment_mandate", autoBudgetReleases: false },
150
151
  shell_floor: { remedy: "shell_gate", autoBudgetReleases: false },
151
152
  write_protection_unresolved: { remedy: "terminal_only", autoBudgetReleases: false },
package/dist/runs.d.ts CHANGED
@@ -316,7 +316,7 @@ pendingEngineNotices?: readonly {
316
316
  trustedInternals?: Pick<RunInternals, "workflowParkedResume">,
317
317
  /**
318
318
  * S-928 用户输入受理账本(`src/user-input-ledger.ts`;路由层 `deps.userInputLedger`)。两手:开通告口那一拍(模型调用之前)把本会话
319
- * 已收敛、未告知的崩溃丢失铸成 `task.user_steer_undrained{cause:"crashed"}`;腿收场时把本腿上的 steer 受理 settle 掉。
319
+ * 已收敛、未告知的崩溃丢失铸成 `task.user_steer_undrained` / `task.user_followup_undrained`(`cause:"crashed"`,码按受理时的车道选);腿收场时把本腿上的受理 settle 掉。
320
320
  * 缺席 ⇒ 两手都不做(与本参数存在之前逐字相同)。
321
321
  */
322
322
  userInputLedger?: UserInputLedger): Promise<void>;
@@ -29,12 +29,13 @@
29
29
  * 里同点同调它;准备阶段的名审计与本仓配置表读的是同一只 `judgeRuleToolName`)—— 本仓手写的那一只(`legacyToolNameOf`)删除,零双读;
30
30
  * 「哪几桶装工具名」归 core(它的 `HOLDS_TOOL_NAMES`)。
31
31
  *
32
- * ── 7.116.0:写口读者再多判一问 —— 够不着挂载工具的协议名 ─────────────────────────────────────
33
- * 缺段(`mcp__srv__` / `mcp__`)与对端段铸不出(`mcp__github.com__*`)的协议名 core 的判官判 `current`(它只看退役名与无前缀 `__` 名),
34
- * 而配置表一直拒缺段名 —— 一个名字两张表两种答案,对端段那一半则两张表都照收、永不命中。判官与句子是 `namespaced-rule-name.ts` 的那一只
35
- * (配置表同调),码 `400 request.field_invalid`(会话规则写口既有的字段值码;不另铸新码,也不往 core 的判词闭集里塞本仓的词)。
36
- * 这一问要逐桶判,而 core 不导出 `HOLDS_TOOL_NAMES` 的运行期值 ⇒ 桶表 {@link TOOL_NAME_FIELDS} 的键集从 core 判官交回的 `field` 型派生(两向 tsc 配对),
37
- * 随这一问在 core 自己给出判词那天整只退役。SQL 店写口经 {@link assertSessionRuleNamesReachable} 同判。
32
+ * ── 7.116.0:写口读者再多判一问 —— 够不着挂载工具的协议名;7.118.0 收窄到引擎读不到的那一形 ─────────────────────────
33
+ * 对端段铸不出 / 对端段为空(`mcp__github.com__*`、`mcp__`)自 core 7.43.0 #1355 (b) 起是引擎判官自己的判词 `unmintable_segment` ——
34
+ * `assertSessionRuleNamesCurrent` 拒(`legacy_tool_name`,`detail.verdict`)、本仓映射 `400 rules.legacy_tool_name`,与退役名同码同句族;本仓 7.116.0 起
35
+ * 那只自比对端段的对照随之退役。引擎判官放行、却一只工具都够不着的只剩**工具段为空**(`mcp__srv__`):判据与句子是 `namespaced-rule-name.ts` 那一只
36
+ * (配置表同调),码 `400 request.field_invalid`(会话规则写口既有的字段值码;不往 core 的判词闭集里塞本仓的词)。这一问要逐桶判,而 core 不导出
37
+ * `HOLDS_TOOL_NAMES` 的运行期值 ⇒ 桶表 {@link TOOL_NAME_FIELDS} 的键集从 core 判官交回的 `field` 型派生(两向 tsc 配对),随引擎判官给这一形判词那天整只退役。
38
+ * SQL 店写口经 {@link assertSessionRuleNamesReachable} 同判。
38
39
  *
39
40
  * ── 文案也在这里(codex 一轮追加:两条腿各铸一份句子 = 第二份手抄)───────────────────────────────
40
41
  * 初版把 wire 文案留在两个路由各自铸,理由是错误文案冻结门(`test/api-error-text-freeze.test.ts`)锚的是
@@ -45,16 +46,16 @@
45
46
  */
46
47
  import { SessionPolicyError } from "@sema-agent/core";
47
48
  import type { SessionPermissionRules, SessionRuleNameRefusal, SessionRulesRecord } from "@sema-agent/core";
48
- import { type UnreachableNamespacedRuleName } from "./namespaced-rule-name.js";
49
+ import { emptyToolSegmentOf } from "./namespaced-rule-name.js";
49
50
  /** core `SessionPermissionRules` 的桶名全集(派生,不是手抄)。 */
50
51
  export declare const SESSION_PERMISSION_RULE_FIELDS: readonly (keyof SessionPermissionRules)[];
51
- /** 工具名桶里一条够不着任何挂载工具的协议名(缺段 / 对端段铸不出);`verdict` = 判官的判词。 */
52
+ /** 工具名桶里一条引擎判官放行、却一只工具都够不着的协议名(工具段为空);`ns` = 它的协议。 */
52
53
  export interface SessionRuleUnreachableName {
53
54
  readonly field: SessionRuleNameRefusal["field"];
54
55
  readonly name: string;
55
- readonly verdict: UnreachableNamespacedRuleName;
56
+ readonly ns: NonNullable<ReturnType<typeof emptyToolSegmentOf>>;
56
57
  }
57
- /** 逐桶逐条过协议规则名判官;第一条够不着的交回,全部够得着 ⇒ `undefined`。 */
58
+ /** 逐桶逐条问引擎读不到的那一形;第一条交回,全部够得着 ⇒ `undefined`。调用方先过引擎判官({@link legacyToolNameRefusalOf} / `assertSessionRuleNamesCurrent`)。 */
58
59
  export declare function unreachableSessionRuleNameOf(rules: SessionPermissionRules): SessionRuleUnreachableName | undefined;
59
60
  /**
60
61
  * SQL 会话规则店写口的同一问(core 判官之后、收紧门之前,与 `assertSessionRuleNamesCurrent` 同点):够不着 ⇒ 抛,一条写语句都不发。
@@ -80,7 +81,7 @@ export type SessionPermissionRulesProblem = SessionPermissionRulesShapeProblem
80
81
  kind: "legacy-tool-name";
81
82
  refusal: SessionRuleNameRefusal;
82
83
  }
83
- /** 形对、core 判官也放行,但工具名桶里有一条够不着任何挂载工具的协议名(缺段 / 对端段铸不出)。 */
84
+ /** 形对、core 判官也放行,但工具名桶里有一条一只工具都够不着的协议名(工具段为空 —— 引擎读不到的那一形)。 */
84
85
  | {
85
86
  kind: "unreachable-tool-name";
86
87
  unreachable: SessionRuleUnreachableName;
@@ -109,7 +110,7 @@ export declare function readSessionPermissionRules(raw: unknown): SessionPermiss
109
110
  * 面经 {@link readSessionRulesRecords})往会话规则店写之前的**唯一**判据。两问,按序:
110
111
  * ① 形(同 {@link readSessionPermissionRules};一份坏形的体答形的 400);
111
112
  * ② 工具名桶里的每一条过 core 的判官({@link legacyToolNameRefusalOf})—— 写进去就让会话之后每一跑都起不来的名字,在写口拒;
112
- * ③ 工具名桶里的每一条过协议规则名判官({@link unreachableSessionRuleNameOf})—— 写进去永远匹配不到任何挂载工具的协议名,在写口拒。
113
+ * ③ 工具名桶里的每一条问引擎读不到的那一形({@link unreachableSessionRuleNameOf}:工具段为空的协议名)—— 写进去永远匹配不到任何挂载工具,在写口拒。
113
114
  * 🔴 **只在写口,不在读回**:SQL 店读回同一行时只判形(形读者)。一条早先写进去的退役名行(或够不着的协议名行)必须**读得出来** ——
114
115
  * 那是运营方清掉它的唯一路径(operator 的 PUT 先读写前快照),也是 core 准备阶段报 `config.legacy_tool_name`
115
116
  * 而不是一只 500 `corrupt` 的前提。把判官放进形读者会让读回把它判坏(7.105.0 车QQ 双库套件实抓,修在同批)。
@@ -1,5 +1,5 @@
1
1
  import { PROTOCOL_TABLE, SessionPolicyError, assertSessionRuleNamesCurrent, judgeRuleToolName } from "@sema-agent/core";
2
- import { describeUnreachableNamespacedRuleName, unreachableNamespacedRuleNameOf } from "./namespaced-rule-name.js";
2
+ import { describeEmptyToolSegment, emptyToolSegmentOf, NO_DECLARED_TOOL_NAMES } from "./namespaced-rule-name.js";
3
3
  const RULE_FIELDS = {
4
4
  toolAllow: true,
5
5
  toolDeny: true,
@@ -24,9 +24,9 @@ const TOOL_NAME_FIELDS = Object.values(TOOL_NAME_FIELD_TABLE);
24
24
  export function unreachableSessionRuleNameOf(rules) {
25
25
  for (const field of TOOL_NAME_FIELDS) {
26
26
  for (const name of rules[field] ?? []) {
27
- const verdict = unreachableNamespacedRuleNameOf(name);
28
- if (verdict !== undefined)
29
- return { field, name, verdict };
27
+ const ns = emptyToolSegmentOf(name);
28
+ if (ns !== undefined)
29
+ return { field, name, ns };
30
30
  }
31
31
  }
32
32
  return undefined;
@@ -99,17 +99,22 @@ export function describeSessionPermissionRulesProblem(p) {
99
99
  }
100
100
  }
101
101
  function describeUnreachableSessionRuleName(u) {
102
- return `${u.field} names "${u.name}", which can never match a tool (${describeUnreachableNamespacedRuleName(u.verdict)}), so nothing was written`;
102
+ return `${u.field} names "${u.name}", which can never match a tool (${describeEmptyToolSegment(u.ns)}), so nothing was written`;
103
103
  }
104
104
  function describeLegacyToolName(r) {
105
105
  switch (r.verdict) {
106
106
  case "retired": {
107
- const v = judgeRuleToolName(r.name, new Set());
107
+ const v = judgeRuleToolName(r.name, NO_DECLARED_TOOL_NAMES);
108
108
  const guidance = v.kind === "retired" ? v.guidance : `renamed "${r.currentName ?? "?"}"`;
109
109
  return `${r.field} names "${r.name}", a retired tool name (${guidance}); with that rule in place this session could not start another run, so nothing was written — use the current tool name${r.currentName !== undefined ? ` ("${r.currentName}")` : ""}`;
110
110
  }
111
111
  case "unprefixed_namespace":
112
112
  return `${r.field} names "${r.name}", which contains "__" but no protocol prefix; with that rule in place this session could not start another run, so nothing was written — a protocol tool is named ${protocolToolNameShapes()}`;
113
+ case "unmintable_segment": {
114
+ const v = judgeRuleToolName(r.name, NO_DECLARED_TOOL_NAMES);
115
+ const guidance = v.kind === "unmintable_segment" ? v.guidance : "a protocol name whose peer segment the engine never mints";
116
+ return `${r.field} names "${r.name}", which can never match a tool (${guidance}), so nothing was written`;
117
+ }
113
118
  default: {
114
119
  const unhandled = r.verdict;
115
120
  throw new Error(`unhandled legacy tool name verdict: ${JSON.stringify(unhandled)}`);
@@ -49,20 +49,18 @@ export declare const MAX_TASK_AGENTS = 32;
49
49
  *
50
50
  * 本谓词只判「字符串数组」(core 7.42.0 起与请求的 `settings.permissions.allow`、运维的 `APPROVAL_GRANT` **同一只**:`rule-entry-list.ts` 的
51
51
  * `ruleEntryListShapeOk`):引擎直接 `for (const entry of list)` —— 一个裸字符串 `"Bash"` 会被逐字符迭代成 `B` / `a` / `s` / `h` 四条
52
- * (deny 侧 = 意图静默落空,安全轴上的放宽方向)。条目的**文法**归引擎:允侧读不出的条目(`*` / 写路径 / 域名)引擎在开跑时发
53
- * `permission.allowed_tool_not_granted`(受众 = 用户);拒侧读成空的条目在 `resolveSpec` 拒(提交 400 / 续跑 409,`rule-entry-list.ts` 的
54
- * `skillDenyInertEntryOf`,两处调用:提交口拒点 + 存量体重放预判)。形对之后两道门再问 {@link skillToolListRefusal}(协议名的服务器段铸不出 / 缺段 ⇒ 400)——
55
- * 不是文法,是「写得出却永远不生效」,判据与请求的 allow 桶、运维的 allow 表同一份(`rule-entry-list.ts`)。
56
- * 条数帽 = 条目表那一只(`rule-entry-list.ts` `allowLayerEntryCountIssue`,准备开销的界 —— 引擎逐条播种是平方级;与请求 allow 桶、运维表同一只),
52
+ * (deny 侧 = 意图静默落空,安全轴上的放宽方向)。条目的**文法与名字**是引擎的读法:允侧读不出的条目(`*` / 写路径 / 域名 / 退役名 …)引擎在开跑时发
53
+ * `permission.allowed_tool_not_granted`(受众 = 用户);拒侧读成空的条目(文法或名字层)在 `resolveSpec` 拒(提交 400 / 续跑 409,`rule-entry-list.ts` 的
54
+ * `skillDenyInertEntryOf`,判据 = 引擎的条目读法)。形对之后两道门再问 {@link skillToolListRefusal}(条数帽 + 引擎读不到的那一形 ⇒ 400)。
55
+ * 条数帽 = 条目表那一只(`rule-entry-list.ts` `allowLayerEntryCountIssue`,准备开销的界;与请求 allow 桶、运维表同一只),
57
56
  * 在 {@link skillToolListRefusal} 里判;无长度帽([ref])。
58
57
  */
59
58
  export declare const skillToolListOk: (v: unknown) => v is readonly string[];
60
59
  /**
61
60
  * 一条技能(形已过两道门的字符串数组判)的两只清单在受理口的拒因 —— 都过 ⇒ `undefined`。两处受理口同调:顶层 `skills[]`
62
- * (`http/admission.ts` 的 `validateUserSkills`)、子代 `agents[].skills[]`({@link validateTaskAgents})。判据是 `rule-entry-list.ts` 那一份:
63
- * 任一只清单里「协议名写得出、却永远匹配不到」的条目(与请求的 allow 桶、配置表、会话规则写口同一只判官)。
64
- * 拒侧读成空的条目**不在这里判**:那一问在 `resolveSpec`(`rule-entry-list.ts` 的 `skillDenyInertEntryOf`;新提交与每条续跑腿都经过那一处)与存量体重放前的
65
- * 预判(`http/checkpoint-ctx.ts` `storedBodyReplayRefusal`)—— 一只判、两处调用。
61
+ * (`http/admission.ts` 的 `validateUserSkills`)、子代 `agents[].skills[]`({@link validateTaskAgents})。两问:条数帽;引擎读不到、却永远匹配不到的
62
+ * 条目(工具段为空的协议名,`rule-entry-list.ts` 那一份,与配置表、会话规则写口同一只)。退役名 / 对端段铸不出 / 读成空的条目是引擎的读法(见上):
63
+ * 允侧交引擎通告,拒侧由 `resolveSpec` 拒。
66
64
  */
67
65
  export declare function skillToolListRefusal(skill: Record<string, unknown>): string | undefined;
68
66
  /** [ref] 单条 agent 定义的形状裁决:null=合法;否则一句可 400 的英文错误(不带索引,调用方自己加)。
@@ -1,7 +1,7 @@
1
1
  import { isThinkingLevel } from "@sema-agent/core";
2
2
  import { taskWallClockSec } from "./task-workflow.js";
3
3
  import { isValidCwd } from "./task-cwd.js";
4
- import { allowLayerEntryCountIssue, describeRetiredRuleEntry, describeUnreachableRuleEntry, retiredRuleEntryOf, ruleEntryListShapeOk, unreachableRuleEntryOf } from "./rule-entry-list.js";
4
+ import { allowLayerEntryCountIssue, describeEmptyToolSegmentEntry, emptyToolSegmentEntryOf, ruleEntryListShapeOk } from "./rule-entry-list.js";
5
5
  export function normalizeSuggestNextPrompts(v) {
6
6
  if (v === true)
7
7
  return true;
@@ -76,12 +76,9 @@ export function skillToolListRefusal(skill) {
76
76
  const over = allowLayerEntryCountIssue(`skill.${key}`, list);
77
77
  if (over !== undefined)
78
78
  return over.message;
79
- const retired = retiredRuleEntryOf(list);
80
- if (retired !== undefined)
81
- return describeRetiredRuleEntry(`skill.${key}`, retired);
82
- const unreachable = unreachableRuleEntryOf(list);
83
- if (unreachable !== undefined)
84
- return describeUnreachableRuleEntry(`skill.${key}`, unreachable);
79
+ const empty = emptyToolSegmentEntryOf(list);
80
+ if (empty !== undefined)
81
+ return describeEmptyToolSegmentEntry(`skill.${key}`, empty);
85
82
  }
86
83
  return undefined;
87
84
  }