@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.
- package/MIGRATION.md +97 -0
- package/USAGE.md +37 -18
- package/dist/approval-card.d.ts +40 -10
- package/dist/approval-card.js +11 -1
- package/dist/approval.d.ts +4 -12
- package/dist/approval.js +4 -26
- package/dist/boot/parked-revive-gate.d.ts +1 -7
- package/dist/boot/parked-revive-gate.js +2 -12
- package/dist/boot/resolve-spec.js +8 -13
- package/dist/boot/runner-deps.js +2 -2
- package/dist/boot/stage-07-capability-layer.js +2 -2
- package/dist/capabilities/hands-lane.d.ts +25 -0
- package/dist/capabilities/hands-lane.js +1 -1
- package/dist/capabilities/mcp-probe.d.ts +2 -2
- package/dist/config.d.ts +14 -18
- package/dist/config.js +13 -24
- package/dist/deployment-governance.d.ts +2 -7
- package/dist/deployment-governance.js +3 -5
- package/dist/host-decision.d.ts +83 -25
- package/dist/host-decision.js +31 -1
- package/dist/http/admission.js +7 -1
- package/dist/http/approval-sweeps.js +1 -1
- package/dist/http/mcp-dial-deadline.d.ts +35 -0
- package/dist/http/mcp-dial-deadline.js +21 -0
- package/dist/http/memory-compliance-failure-reply.d.ts +11 -5
- package/dist/http/memory-compliance-failure-reply.js +17 -8
- package/dist/http/memory-store-refusal-reply.d.ts +14 -11
- package/dist/http/memory-store-refusal-reply.js +16 -5
- package/dist/http/resume-legs.js +7 -3
- package/dist/http/routes/approvals-assistant.js +21 -24
- package/dist/http/routes/capabilities.js +2 -0
- package/dist/http/routes/mcp-reconnect.d.ts +17 -0
- package/dist/http/routes/mcp-reconnect.js +6 -1
- package/dist/http/routes/memory-bundle.js +1 -1
- package/dist/http/routes/memory-compliance.js +1 -1
- package/dist/http/routes/memory-origin.js +1 -1
- package/dist/http/routes/runs.js +41 -33
- package/dist/http/routes/sessions.d.ts +1 -1
- package/dist/http/routes/sessions.js +3 -6
- package/dist/http/routes/workflows.js +2 -0
- package/dist/http/wire-types.d.ts +6 -1
- package/dist/memory-store-refusal.d.ts +42 -17
- package/dist/memory-store-refusal.js +25 -6
- package/dist/model-unreachable-notice.d.ts +1 -1
- package/dist/namespaced-rule-name.d.ts +45 -41
- package/dist/namespaced-rule-name.js +36 -22
- package/dist/observability/corrupt-read-seat.d.ts +4 -0
- package/dist/observability/corrupt-read-seat.js +1 -0
- package/dist/observability/fail-open.d.ts +14 -2
- package/dist/observability/fail-open.js +14 -2
- package/dist/operator-ask.d.ts +4 -1
- package/dist/orchestration/workflow-agent-session-index.d.ts +82 -59
- package/dist/orchestration/workflow-agent-session-index.js +80 -14
- package/dist/orchestration/workflow-completion-inbox.d.ts +5 -0
- package/dist/orchestration/workflow-completion-inbox.js +39 -17
- package/dist/orchestration/workflow-notify-journal.d.ts +1 -1
- package/dist/orchestration/workflow-notify-journal.js +7 -2
- package/dist/outbound-dispatcher.d.ts +29 -20
- package/dist/outbound-dispatcher.js +105 -16
- package/dist/parked-decide.d.ts +43 -48
- package/dist/parked-decide.js +54 -16
- package/dist/permission-rule-vocab.d.ts +4 -3
- package/dist/permission-rule-vocab.js +1 -1
- package/dist/plugins/approval-ask-store-sql.d.ts +17 -20
- package/dist/plugins/approval-ask-store-sql.js +0 -3
- package/dist/plugins/one-time-migrations.d.ts +5 -0
- package/dist/plugins/one-time-migrations.js +110 -4
- package/dist/plugins/pg-pool.js +1 -1
- package/dist/plugins/tidb-pool.js +1 -1
- package/dist/plugins/web-search.d.ts +20 -1
- package/dist/plugins/web-search.js +39 -10
- package/dist/plugins/workflow-run-store-sql.d.ts +7 -6
- package/dist/plugins/workflow-run-store-sql.js +5 -7
- package/dist/request-key-closure.js +1 -1
- package/dist/request-value-closure.d.ts +1 -1
- package/dist/rule-entry-list.d.ts +35 -46
- package/dist/rule-entry-list.js +17 -25
- package/dist/rules-consent.js +3 -0
- package/dist/run-local.js +1 -0
- package/dist/runs.d.ts +1 -1
- package/dist/session-policy-wire.d.ts +13 -12
- package/dist/session-policy-wire.js +11 -6
- package/dist/spec-fields.d.ts +7 -9
- package/dist/spec-fields.js +4 -7
- package/dist/standing-answer-grant.d.ts +14 -2
- package/dist/standing-answer-grant.js +8 -1
- package/dist/task-settings.js +8 -4
- package/dist/tool-approval.d.ts +53 -112
- package/dist/tool-approval.js +75 -55
- package/dist/trace/core-keyset-guard.d.ts +5 -5
- package/dist/trace/self-minted-wire.d.ts +16 -2
- package/dist/trace/self-minted-wire.js +7 -0
- package/dist/user-input-ledger.d.ts +24 -12
- package/dist/user-input-ledger.js +33 -14
- package/package.json +2 -2
- package/scripts/drain-check-local.mjs +74 -3
- package/scripts/drain-check.mysql.sql +43 -17
- 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
|
-
|
|
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":
|
|
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
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
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",
|
|
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:
|
|
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`
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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` 的 `
|
|
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
|
-
* **规则条目表在本仓受理口的判** ——
|
|
3
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* `
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
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 {
|
|
25
|
-
import {
|
|
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
|
|
46
|
+
/** 一张表里第一条「引擎读不到、却永远匹配不到」的条目(工具段为空的协议名)。 */
|
|
47
|
+
export interface EmptyToolSegmentEntry {
|
|
48
48
|
readonly entry: string;
|
|
49
|
-
readonly
|
|
49
|
+
readonly ns: NonNullable<ReturnType<typeof emptyToolSegmentOf>>;
|
|
50
50
|
}
|
|
51
|
-
/** ②
|
|
52
|
-
export declare function
|
|
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
|
|
55
|
-
/**
|
|
56
|
-
export
|
|
57
|
-
|
|
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(
|
|
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
|
-
*
|
|
93
|
-
* 7.
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
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
|
package/dist/rule-entry-list.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { compileAllowList
|
|
2
|
-
import {
|
|
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
|
|
20
|
+
export function emptyToolSegmentEntryOf(list) {
|
|
21
21
|
for (const entry of list) {
|
|
22
|
-
const
|
|
23
|
-
if (
|
|
24
|
-
return { entry,
|
|
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
|
|
29
|
-
return `${field} names "${presentRuleEntry(u.entry)}", which can never match a tool (${
|
|
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(
|
|
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
|
|
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) {
|
package/dist/rules-consent.js
CHANGED
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
|
|
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
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* (
|
|
36
|
-
*
|
|
37
|
-
*
|
|
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 {
|
|
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
|
-
/**
|
|
52
|
+
/** 工具名桶里一条引擎判官放行、却一只工具都够不着的协议名(工具段为空);`ns` = 它的协议。 */
|
|
52
53
|
export interface SessionRuleUnreachableName {
|
|
53
54
|
readonly field: SessionRuleNameRefusal["field"];
|
|
54
55
|
readonly name: string;
|
|
55
|
-
readonly
|
|
56
|
+
readonly ns: NonNullable<ReturnType<typeof emptyToolSegmentOf>>;
|
|
56
57
|
}
|
|
57
|
-
/**
|
|
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
|
-
* ③
|
|
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 {
|
|
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
|
|
28
|
-
if (
|
|
29
|
-
return { field, name,
|
|
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 (${
|
|
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,
|
|
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)}`);
|
package/dist/spec-fields.d.ts
CHANGED
|
@@ -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`(受众 = 用户)
|
|
54
|
-
* `skillDenyInertEntryOf
|
|
55
|
-
*
|
|
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})
|
|
63
|
-
*
|
|
64
|
-
*
|
|
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 的英文错误(不带索引,调用方自己加)。
|
package/dist/spec-fields.js
CHANGED
|
@@ -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,
|
|
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
|
|
80
|
-
if (
|
|
81
|
-
return
|
|
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
|
}
|