@sema-agent/server 7.116.1 → 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 +195 -0
- package/USAGE.md +65 -41
- package/dist/approval-card.d.ts +82 -18
- package/dist/approval-card.js +46 -4
- package/dist/approval.d.ts +4 -12
- package/dist/approval.js +4 -26
- package/dist/boot/budget-tracing.js +2 -3
- package/dist/boot/config-center.d.ts +2 -2
- package/dist/boot/config-center.js +5 -7
- 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 +18 -16
- package/dist/boot/runner-deps.d.ts +5 -3
- package/dist/boot/runner-deps.js +6 -4
- package/dist/boot/stage-07-capability-layer.js +2 -2
- package/dist/brain.d.ts +17 -8
- package/dist/brain.js +10 -7
- 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-catalog.js +3 -2
- package/dist/config-center/apply-effective.d.ts +6 -16
- package/dist/config-center/apply-effective.js +0 -6
- package/dist/config-center/facade.d.ts +1 -1
- package/dist/config-center/facade.js +1 -1
- package/dist/config-center/restart-signal.d.ts +1 -1
- package/dist/config-types.d.ts +12 -16
- package/dist/config.d.ts +32 -29
- package/dist/config.js +20 -27
- package/dist/deployment-governance.d.ts +13 -19
- package/dist/deployment-governance.js +7 -14
- package/dist/governance-overrides.d.ts +20 -0
- package/dist/governance-overrides.js +8 -0
- package/dist/host-decision.d.ts +83 -25
- package/dist/host-decision.js +31 -1
- package/dist/http/admission.js +13 -7
- package/dist/http/approval-sweeps.js +16 -13
- package/dist/http/checkpoint-ctx.d.ts +83 -24
- package/dist/http/checkpoint-ctx.js +68 -8
- 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 +18 -18
- package/dist/http/retired-skill-manifest.d.ts +1 -1
- package/dist/http/route-ctx.d.ts +3 -3
- package/dist/http/routes/approvals-assistant.js +21 -24
- package/dist/http/routes/capabilities.js +5 -3
- 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 +53 -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-select.d.ts +16 -5
- package/dist/model-select.js +7 -3
- package/dist/model-unreachable-notice.d.ts +1 -1
- package/dist/namespaced-rule-name.d.ts +45 -39
- 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 +15 -3
- package/dist/observability/fail-open.js +15 -3
- package/dist/observability/permission-rule-events.d.ts +3 -0
- package/dist/observability/permission-rule-events.js +4 -0
- package/dist/observability/run-terminal.js +21 -1
- 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 +47 -20
- package/dist/outbound-dispatcher.js +198 -23
- package/dist/parked-decide.d.ts +43 -48
- package/dist/parked-decide.js +54 -16
- package/dist/permission-rule-list.d.ts +20 -31
- 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 +25 -0
- package/dist/plugins/one-time-migrations.js +192 -6
- 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/router/route-orchestration.d.ts +2 -23
- package/dist/rule-entry-list.d.ts +88 -0
- package/dist/rule-entry-list.js +102 -0
- package/dist/rules-consent.js +3 -0
- package/dist/run-local.d.ts +4 -2
- package/dist/run-local.js +4 -3
- package/dist/runs.d.ts +1 -1
- package/dist/runs.js +1 -0
- package/dist/runtime-governance.d.ts +5 -21
- package/dist/runtime-governance.js +4 -8
- package/dist/session-policy-wire.d.ts +13 -12
- package/dist/session-policy-wire.js +11 -6
- package/dist/spec-fields.d.ts +13 -24
- package/dist/spec-fields.js +13 -17
- package/dist/standing-answer-grant.d.ts +14 -2
- package/dist/standing-answer-grant.js +8 -1
- package/dist/task-settings.d.ts +18 -18
- package/dist/task-settings.js +63 -32
- package/dist/tool-approval.d.ts +77 -145
- package/dist/tool-approval.js +80 -62
- package/dist/trace/core-keyset-guard.d.ts +6 -6
- package/dist/trace/engine-notice-wire.d.ts +4 -6
- package/dist/trace/engine-notice-wire.js +14 -1
- 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 +103 -7
- package/scripts/drain-check.mysql.sql +59 -8
- package/scripts/drain-check.pg.sql +66 -10
|
@@ -106,7 +106,7 @@ export interface WebSearchCapability {
|
|
|
106
106
|
export declare function projectWebSearchCapability(provider: WebSearchProvider | undefined): WebSearchCapability;
|
|
107
107
|
export interface WebSearchBackendConfig {
|
|
108
108
|
readonly provider: WebSearchProvider;
|
|
109
|
-
/** API key (brave/tavily). From `WEB_SEARCH_API_KEY` — never reaches the model. */
|
|
109
|
+
/** API key (brave/tavily). From `WEB_SEARCH_API_KEY` — never reaches the model. Must be usable as an HTTP header value ({@link isHttpHeaderValue}); the judges refuse one that is not, and the brave / tavily legs refuse it again before fetch. */
|
|
110
110
|
readonly apiKey?: string;
|
|
111
111
|
/** SearXNG 实例特定查询参数(`engines=` / `language=` 等)——透传给 core adapter 的 `extraParams`。
|
|
112
112
|
* 一键装 SearXNG 的向导用它把「选了哪些上游引擎」落到引擎侧。仅 searxng 腿消费。 */
|
|
@@ -136,6 +136,25 @@ export interface WebSearchBackend {
|
|
|
136
136
|
search: WebSearchFn;
|
|
137
137
|
maxResults: number;
|
|
138
138
|
}
|
|
139
|
+
/**
|
|
140
|
+
* 🔴 「这个串能不能当 HTTP 头值」的**唯一**判据 —— 受理面(请求腿 `settings.webSearch.apiKey`)、部署 env 腿
|
|
141
|
+
* (`WEB_SEARCH_API_KEY`)与后端腿(brave / tavily 交给 fetch 之前)**同问这一只**。
|
|
142
|
+
*
|
|
143
|
+
* ## 病
|
|
144
|
+
* `apiKey` 以前只判「是串」。带换行 / NUL / 其它控制字符 / U+00FF 以上字符的 key 两条载体都采纳,到 fetch 构造或发出请求头的那一刻运行时当场抛;
|
|
145
|
+
* 构造腿那一抛是 `Headers.append: "<整串 key>" is an invalid header value.` —— 凭据明文就在 message 里,经 WebSearch 工具失败卡进模型与转写面。
|
|
146
|
+
*
|
|
147
|
+
* ## 判据就是运行时的那条流水线,问的是 Node 自带的两只校验
|
|
148
|
+
* fetch 对一个头值做两步:① `Headers` 构造 —— 首尾 HTTP 空白(含 CR / LF)归一化剥掉,其余位置的 CR / LF / NUL 与 U+00FF 以上字符抛;
|
|
149
|
+
* ② 发送层(undici)对**归一化后的值**再过一遍字符表(HT、U+0020..U+007E、U+0080..U+00FF 可发;其余控制字符与 U+007F 抛 `invalid <name> header`)。
|
|
150
|
+
* 本判据逐步问同样两只:`new Headers(...)` 取归一化值,再交 `node:http` 的 `validateHeaderValue`(与发送层同一张字符表)。**不自写字符表**
|
|
151
|
+
* (第二份表迟早与运行时漂开);两步合起来与真 fetch 的对拍钉在 `test/web-search.test.ts`(本地受控端点,U+0000..U+017F 逐字符全扫)—— 运行时改了头值律,那一格先红。
|
|
152
|
+
* 只问 `Headers` 构造是不够的:它接受 U+0001 / U+007F,发送层拒绝(两步缺一步 ⇒ 判官收下、每次搜索必失败)。
|
|
153
|
+
* 镜像运行时的后果如实:首尾空白(含 CR / LF)归一化后不剩,所以 `"key\n"` 这种从密钥文件带出尾随换行的串**照收**、发出去的是剥后的值 ——
|
|
154
|
+
* 与 {@link judgeEndpoint} 对 `https://@host` 同一条姿势:不比运行时多拒。
|
|
155
|
+
* 抛出的错误对象在这里**当场丢弃**(构造腿那条的 message 就是那串明文),调用方拿到的只有布尔。
|
|
156
|
+
*/
|
|
157
|
+
export declare function isHttpHeaderValue(value: string): boolean;
|
|
139
158
|
export declare function setWebSearchBadPayloadObserver(fn: ((provider: string) => void) | undefined): void;
|
|
140
159
|
/**
|
|
141
160
|
* Build the `WebSearchConfig` (a `search` fn + `maxResults`) for `assembleFullBodyTools({ webSearch })`. The API key
|
|
@@ -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,26 +1,5 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The A value router — cash out the S1 supervisor value verdict (sema-internal server/docs/S1-VALUE-VERDICT.md §6.3) into a per-task
|
|
3
|
-
* orchestration decision: run a task as `single` / `supervisor` / `team`.
|
|
4
|
-
*
|
|
5
|
-
* The verdict, in one line: **competent tasks have SUP/TEAM dominated (pure overhead) ⇒ default single; the real
|
|
6
|
-
* supervisor value is (a) DANGER backstop (SUP prevention gate-deny > TEAM fan-out which AMPLIFIES danger ×K) and
|
|
7
|
-
* (b) STRUCTURAL view-gap (goal-keeping / ask-aggregation, universally useful).** So this router is CONSERVATIVE
|
|
8
|
-
* by construction:
|
|
9
|
-
* - default **single** (escalation is never free — the verdict measured it as pure overhead on competent work);
|
|
10
|
-
* - escalate to **supervisor** ONLY on a positive (a)-danger or (b)-structural signal;
|
|
11
|
-
* - NEVER auto-select **team** (fan-out is dominated everywhere tested AND amplifies danger; team stays
|
|
12
|
-
* caller-explicit — a deliberate `council`/`debate`/`team` request, or the `/v1/leader` endpoint).
|
|
13
|
-
*
|
|
14
|
-
* 🔴 ALL signals are DETERMINISTIC (config / mounted tools / exec-env isolation / autonomy / delegation). There is
|
|
15
|
-
* NO LLM "difficulty" judge in the routing decision — the verdict's measurement-integrity rule ([ref] §2.2.1):
|
|
16
|
-
* an LLM judge is a GATE inside an arm, NEVER the routing metric (it would make the router un-auditable + gameable).
|
|
17
|
-
*
|
|
18
|
-
* Pure + unit-testable. The decision is VALIDATED empirically against the S1 harness (a ROUTER arm that runs the
|
|
19
|
-
* router's chosen arm per trap; the objective hidden-test oracle confirms danger→SUP prevents + competent→single
|
|
20
|
-
* is not-worse) BEFORE it is wired into the live task path — never ship an unvalidated router.
|
|
21
|
-
*/
|
|
22
|
-
import { type TaskSpec } from "@sema-agent/core";
|
|
23
1
|
import type { Autonomy } from "../runtime-governance.js";
|
|
2
|
+
import type { GovernanceOverrides } from "../governance-overrides.js";
|
|
24
3
|
export type OrchestrationMode = "single" | "supervisor" | "team";
|
|
25
4
|
/** The DETERMINISTIC signals the routing decision reads (extracted by {@link deriveRoutingSignals} from the
|
|
26
5
|
* task's resolved config — no task-content LLM judgement). */
|
|
@@ -116,5 +95,5 @@ export declare function isIsolatedExecEnv(provider?: string, runtimeClass?: stri
|
|
|
116
95
|
* ⚠️ 连带(产品面,如实):写工具的 ask 从此也是强制卡 ⇒ 用户的「本会话放行」不再退役 SUP 路由的写门(shell 那一问修前就退不掉)。
|
|
117
96
|
* 不改路由判据本身(`routeServiceTask` 一字不动)。
|
|
118
97
|
*/
|
|
119
|
-
export declare function supPostureOverrides():
|
|
98
|
+
export declare function supPostureOverrides(): GovernanceOverrides;
|
|
120
99
|
//# sourceMappingURL=route-orchestration.d.ts.map
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* **规则条目表在本仓受理口的判** —— 一份,四张表同调(core 7.42.0 起「这个调用不用问」只有一个去处:引擎的 allow 层;条目的文法、匹配、
|
|
3
|
+
* 名字、读成空的通告都归引擎,本仓不写第二套):
|
|
4
|
+
* · 请求自带技能的 `allowedTools` / `disallowedTools`(顶层 `skills[]` 与子代 `agents[].skills[]` 两处受理口,`spec-fields.ts` / `http/admission.ts`);
|
|
5
|
+
* · 请求的 `settings.permissions.allow`(整份原样投 `TaskSpec.allowedTools`,`task-settings.ts`);
|
|
6
|
+
* · 运维的 `APPROVAL_GRANT`(整份投 `RunnerDeps.allowedTools`,`config.ts` 启动校验)。
|
|
7
|
+
*
|
|
8
|
+
* 本仓在这些表上只剩下面几问(内容形、`*`、写路径形、名字引擎怎么读就怎么算):
|
|
9
|
+
* ① **形**({@link ruleEntryListShapeOk}):字符串数组。引擎逐条迭代;一个裸字符串会被逐字符读成几条(deny 侧 = 意图静默落空)。
|
|
10
|
+
* 本谓词不带条数帽(引擎的 `screenAllowedToolsValue` 只判形);条数帽是另一只函数 {@link allowLayerEntryCountIssue}(`MAX_ALLOW_LAYER_ENTRIES`,
|
|
11
|
+
* 请求 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
|
+
* (重估读数与退役条件在下面那只函数头注)。
|
|
21
|
+
*/
|
|
22
|
+
import { compileAllowList } from "@sema-agent/core";
|
|
23
|
+
import { emptyToolSegmentOf } from "./namespaced-rule-name.js";
|
|
24
|
+
export declare function presentRuleEntry(entry: string): string;
|
|
25
|
+
/** ① 形:字符串数组(空串也是字符串 —— 引擎把它读成 `grammar` 并通告 / 拒,见 ③)。 */
|
|
26
|
+
export declare const ruleEntryListShapeOk: (v: unknown) => v is readonly string[];
|
|
27
|
+
/**
|
|
28
|
+
* ①b **条数上限** —— 引擎要逐条播种的条目表(allow 层的三个本仓成员:请求的 `settings.permissions.allow`、请求自带技能的两只清单、运维的
|
|
29
|
+
* `APPROVAL_GRANT`)**一只常量、一只函数**。理由是引擎的准备开销,不是本仓的语义判据:引擎每次准备都逐条播种这些表(`AllowEntryTable.seed`
|
|
30
|
+
* 与技能授予表的 `hold` 都对已有条目线性查重 ⇒ 整表平方级、同步执行;core 7.42.0 上 1 万条约 2 s、2 万条约 6 s,本仓亲测)。不设帽 = 一份
|
|
31
|
+
* 8 MiB 以内的请求体能把整个进程的事件循环卡住几十秒。与 deny / ask 两桶的帽(`permission-rule-list.ts` `MAX_PERMISSION_RULES_PER_LIST`,
|
|
32
|
+
* 编译口的整表帽)是两件事、两个名字,数字恰好相同。
|
|
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
|
+
*/
|
|
37
|
+
export declare const MAX_ALLOW_LAYER_ENTRIES = 1000;
|
|
38
|
+
/** 超帽的拒句(不截:截掉的尾项读起来像「在生效」)—— 句子的唯一铸点(请求 allow 桶的 wire 拒体按字段重铸也调它)。 */
|
|
39
|
+
export declare function describeAllowLayerOverCap(field: string, count: number): string;
|
|
40
|
+
/** 一张条目表超了 {@link MAX_ALLOW_LAYER_ENTRIES} ⇒ 拒句 + 机读两数;没超 ⇒ `undefined`。四张表同调这一只。 */
|
|
41
|
+
export declare function allowLayerEntryCountIssue(field: string, list: readonly unknown[]): {
|
|
42
|
+
readonly message: string;
|
|
43
|
+
readonly count: number;
|
|
44
|
+
readonly cap: number;
|
|
45
|
+
} | undefined;
|
|
46
|
+
/** 一张表里第一条「引擎读不到、却永远匹配不到」的条目(工具段为空的协议名)。 */
|
|
47
|
+
export interface EmptyToolSegmentEntry {
|
|
48
|
+
readonly entry: string;
|
|
49
|
+
readonly ns: NonNullable<ReturnType<typeof emptyToolSegmentOf>>;
|
|
50
|
+
}
|
|
51
|
+
/** ② 第一条工具段为空的协议名条目(没有 ⇒ `undefined`)。纯字符串运算。 */
|
|
52
|
+
export declare function emptyToolSegmentEntryOf(list: readonly string[]): EmptyToolSegmentEntry | undefined;
|
|
53
|
+
/** ② 的句子(`<field> names "<entry>", which can never match a tool (…)`;后半与配置表 / 会话规则写口同一只)。 */
|
|
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 表里的 `*` 只读空它自己);条目名过引擎判官(名册 = 空集)。 */
|
|
58
|
+
export declare function inertRuleEntriesOf(list: readonly string[], behavior: "allow" | "deny"): readonly InertRuleEntry[];
|
|
59
|
+
/**
|
|
60
|
+
* 原因词 → 「怎么写」(闭集穷举:引擎加一个原因词 ⇒ 编译红)。句子只说这一条为什么什么也不做、换成哪种写法;原因词本身原样带在句子里。
|
|
61
|
+
* 名字层两词带引擎给的判词 / 1:1 现名(有就点名)。
|
|
62
|
+
*/
|
|
63
|
+
export declare function inertRuleEntryGuidance(i: InertRuleEntry): string;
|
|
64
|
+
/** ③ 的句子(`<field> entry "<e>" reads as nothing (<reason>): <怎么写>`;多条用分号连)。 */
|
|
65
|
+
export declare function describeInertRuleEntries(field: string, inert: readonly InertRuleEntry[]): string;
|
|
66
|
+
/** 提交体里技能所在的层(命中句与排空读数点名用):顶层 `skills[]` / 子代 `agents[].skills[]`。 */
|
|
67
|
+
export type SkillListLayer = "skills" | "agents.skills";
|
|
68
|
+
/** 一份提交体里第一处「技能的拒侧清单里有引擎读成空的条目」:在哪一层、拒句是什么。 */
|
|
69
|
+
export interface SkillDenyInertEntry {
|
|
70
|
+
readonly layer: SkillListLayer;
|
|
71
|
+
readonly message: string;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* ③ 的拒侧在**提交体**上的判(顶层 `skills[]` 与子代 `agents[].skills[]` 两层;第一处命中即答)。
|
|
75
|
+
*
|
|
76
|
+
* 🔴 **调用点两处**:`resolveSpec`(`boot/resolve-spec.ts`,**拒点** —— 新提交与五条续跑腿都经过它,所以「新提交会被拒的体,续跑重放时同样拒」
|
|
77
|
+
* 是结构性的)与 `http/checkpoint-ctx.ts` `storedBodyReplayRefusal`(存量体重放前的**预判**,五个续跑问点同调 —— 含 SLA 拒扫的跳过臂与终态 steer
|
|
78
|
+
* 铸唤醒卡之前,这两处不经 `resolveSpec`)。提交腿 ⇒ 400 `request.field_invalid`;续跑腿 ⇒ 409 `resume_blocked_by_policy`(`blockedBy: request.field_invalid`,
|
|
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)之后,本判与本仓的预判一起退,排空判据随之重估。
|
|
86
|
+
*/
|
|
87
|
+
export declare function skillDenyInertEntryOf(body: unknown): SkillDenyInertEntry | undefined;
|
|
88
|
+
//# sourceMappingURL=rule-entry-list.d.ts.map
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { compileAllowList } from "@sema-agent/core";
|
|
2
|
+
import { describeEmptyToolSegment, emptyToolSegmentOf, NO_DECLARED_TOOL_NAMES, ruleEntryToolName } from "./namespaced-rule-name.js";
|
|
3
|
+
import { CONTROL_AND_BIDI_STRIP_RE } from "./text-bidi.js";
|
|
4
|
+
const PRESENTED_ENTRY_MAX = 160;
|
|
5
|
+
export function presentRuleEntry(entry) {
|
|
6
|
+
const clean = entry.replace(CONTROL_AND_BIDI_STRIP_RE, "");
|
|
7
|
+
return clean.length <= PRESENTED_ENTRY_MAX ? clean : `${clean.slice(0, PRESENTED_ENTRY_MAX)}…(${entry.length} chars)`;
|
|
8
|
+
}
|
|
9
|
+
export const ruleEntryListShapeOk = (v) => Array.isArray(v) && v.every((x) => typeof x === "string");
|
|
10
|
+
export const MAX_ALLOW_LAYER_ENTRIES = 1_000;
|
|
11
|
+
export function describeAllowLayerOverCap(field, count) {
|
|
12
|
+
return (`${field} has ${count} entries — at most ${MAX_ALLOW_LAYER_ENTRIES} are accepted. Refused rather than truncated: a truncated list silently drops the entries past the cap. ` +
|
|
13
|
+
`Merge entries into broader ones (mcp__<server>__*, Bash(<prefix>:*), Read(//<dir>/**)) or trim the list`);
|
|
14
|
+
}
|
|
15
|
+
export function allowLayerEntryCountIssue(field, list) {
|
|
16
|
+
if (list.length <= MAX_ALLOW_LAYER_ENTRIES)
|
|
17
|
+
return undefined;
|
|
18
|
+
return { message: describeAllowLayerOverCap(field, list.length), count: list.length, cap: MAX_ALLOW_LAYER_ENTRIES };
|
|
19
|
+
}
|
|
20
|
+
export function emptyToolSegmentEntryOf(list) {
|
|
21
|
+
for (const entry of list) {
|
|
22
|
+
const ns = emptyToolSegmentOf(ruleEntryToolName(entry));
|
|
23
|
+
if (ns !== undefined)
|
|
24
|
+
return { entry, ns };
|
|
25
|
+
}
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
export function describeEmptyToolSegmentEntry(field, u) {
|
|
29
|
+
return `${field} names "${presentRuleEntry(u.entry)}", which can never match a tool (${describeEmptyToolSegment(u.ns)})`;
|
|
30
|
+
}
|
|
31
|
+
export function inertRuleEntriesOf(list, behavior) {
|
|
32
|
+
return compileAllowList(list, behavior, NO_DECLARED_TOOL_NAMES).inert;
|
|
33
|
+
}
|
|
34
|
+
export function inertRuleEntryGuidance(i) {
|
|
35
|
+
const reason = i.reason;
|
|
36
|
+
const write = i.currentName !== undefined ? ` — write "${presentRuleEntry(i.currentName)}"` : "";
|
|
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}`;
|
|
42
|
+
case "wildcard_all":
|
|
43
|
+
return "`*` names no tool (on an allow list it voids the whole list) — name the tools instead";
|
|
44
|
+
case "namespace_wildcard":
|
|
45
|
+
return "a server segment does not take a wildcard (`mcp__*`) — name the server: mcp__<server> or mcp__<server>__*";
|
|
46
|
+
case "path_pattern":
|
|
47
|
+
return "a write-path or file-pattern entry has no reading on this side — name the tool, or use Read(//<dir>/**) for a directory a Read may cover";
|
|
48
|
+
case "domain":
|
|
49
|
+
return "a domain entry has no reading on this side — name the tool";
|
|
50
|
+
case "content_unsupported":
|
|
51
|
+
return "this tool takes no content form here — name the tool itself, or use Bash(<command>) / Bash(<prefix>:*) for a shell command";
|
|
52
|
+
case "grammar":
|
|
53
|
+
return "the entry is not read as written (surrounding whitespace, an empty or unbalanced tool name) — write the tool name exactly, with no padding";
|
|
54
|
+
default: {
|
|
55
|
+
const unhandled = reason;
|
|
56
|
+
throw new Error(`unhandled allow-entry inert reason: ${String(unhandled)}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
export function describeInertRuleEntries(field, inert) {
|
|
61
|
+
return inert.map((i) => `${field} entry "${presentRuleEntry(i.entry)}" reads as nothing (${i.reason}): ${inertRuleEntryGuidance(i)}`).join("; ");
|
|
62
|
+
}
|
|
63
|
+
const isPlainRecord = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
|
|
64
|
+
function skillDenyInertSentence(skill) {
|
|
65
|
+
if (!isPlainRecord(skill))
|
|
66
|
+
return undefined;
|
|
67
|
+
const deny = skill["disallowedTools"];
|
|
68
|
+
if (!ruleEntryListShapeOk(deny))
|
|
69
|
+
return undefined;
|
|
70
|
+
const inert = inertRuleEntriesOf(deny, "deny");
|
|
71
|
+
if (inert.length === 0)
|
|
72
|
+
return undefined;
|
|
73
|
+
return `${describeInertRuleEntries("skill.disallowedTools", inert)} — a deny entry that reads as nothing refuses nothing, so it is refused here rather than taken as in effect`;
|
|
74
|
+
}
|
|
75
|
+
export function skillDenyInertEntryOf(body) {
|
|
76
|
+
if (!isPlainRecord(body))
|
|
77
|
+
return undefined;
|
|
78
|
+
const skills = body["skills"];
|
|
79
|
+
if (Array.isArray(skills)) {
|
|
80
|
+
for (const skill of skills) {
|
|
81
|
+
const sentence = skillDenyInertSentence(skill);
|
|
82
|
+
if (sentence !== undefined)
|
|
83
|
+
return { layer: "skills", message: sentence };
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
const agents = body["agents"];
|
|
87
|
+
if (Array.isArray(agents)) {
|
|
88
|
+
for (let i = 0; i < agents.length; i++) {
|
|
89
|
+
const agent = agents[i];
|
|
90
|
+
const agentSkills = isPlainRecord(agent) ? agent["skills"] : undefined;
|
|
91
|
+
if (!Array.isArray(agentSkills))
|
|
92
|
+
continue;
|
|
93
|
+
for (let j = 0; j < agentSkills.length; j++) {
|
|
94
|
+
const sentence = skillDenyInertSentence(agentSkills[j]);
|
|
95
|
+
if (sentence !== undefined)
|
|
96
|
+
return { layer: "agents.skills", message: `agents[${i}]: skills[${j}]: ${sentence}` };
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return undefined;
|
|
101
|
+
}
|
|
102
|
+
//# sourceMappingURL=rule-entry-list.js.map
|
package/dist/rules-consent.js
CHANGED
package/dist/run-local.d.ts
CHANGED
|
@@ -132,7 +132,7 @@ export interface LocalApproverIo {
|
|
|
132
132
|
* 🔴 S-694 起本席也服**三张审批名单**产的 ask(审批基线取 live 席,见 {@link RUN_LOCAL_APPROVAL_SEAT}):
|
|
133
133
|
* `APPROVAL_REQUIRE` / `APPROVAL_NEVER_AUTO` 命中 ⇒ 策略 `ask` ⇒ 落到这里。`AskRequest` 不带是哪条声明产的
|
|
134
134
|
* ask(core 把 `matchedAskRule` 刻意留在闸内、不上请求),但带这一问的**类**(`classes`,core 7.37.0 起)。
|
|
135
|
-
* 🔴 无 TTY 拒句的出路**按这一问的类说话**({@link NO_TTY_ASK_CLASS_ROW} 一张闭集表:每类 ⇒ 一种出路)
|
|
135
|
+
* 🔴 无 TTY 拒句的出路**按这一问的类说话**({@link NO_TTY_ASK_CLASS_ROW} 一张闭集表:每类 ⇒ 一种出路):普通问与没人表态的服务器工具调用指 `APPROVAL_GRANT`(allow 层的部署成员清的就是这两类),
|
|
136
136
|
* 写下的 ask 名单指名单旋钮(`APPROVAL_DENY` 也点名 —— 它当场拒、不经本席),部署强制要人答的问(`operator_forced`)列「one of」能铸它的来源、shell 地板(`shell_floor`)另一句只列铸它的旋钮,其余各类**不点任何旋钮**、只说去终端答;
|
|
137
137
|
* 类缺席 ⇒ 说实话(「引擎没有说明这一问的类」),不回落成全量旋钮清单(那句对多数类是错的出路)。
|
|
138
138
|
*
|
|
@@ -161,7 +161,9 @@ type NoTtyRemedy = NoTtySpokenRemedy | "terminal_only";
|
|
|
161
161
|
/**
|
|
162
162
|
* 一类问 ⇒ 这条腿上它的出路。**穷尽表**:core 的 `ASK_CLASSES` 加一类 ⇒ 这里 tsc 红(`Record<AskClass, …>` 少一键),新类必须在这里表态,
|
|
163
163
|
* 不会悄悄落成「没有出路」。本仓知道而 core 不知道的只有一件:**哪只部署旋钮会产哪一类问**(逐类的产地见各行):
|
|
164
|
-
* · `unstated`:没有任何层表态的服务器工具调用(core 7.40.0)——
|
|
164
|
+
* · `unstated`:没有任何层表态的服务器工具调用(core 7.40.0)—— 运维写一次的地方是 `APPROVAL_GRANT`(`run-local` 没有请求体,写不了 `settings.permissions.allow`;
|
|
165
|
+
* 7.117.0 / core 7.42.0 起它是 allow 层的部署成员,经部署姿态座席族落在本腿的每只 Runner 上);
|
|
166
|
+
* · `ordinary`:普通问(内置工具的写门、shell 的普通检查等)—— 7.117.0 起同样由 `APPROVAL_GRANT` 清(allow 层只清这两类),出路同上;
|
|
165
167
|
* · `ask_rule`:写下的 ask 规则 —— `APPROVAL_REQUIRE` / `APPROVAL_NEVER_AUTO`(`operatorAsk`)与治理层的 commandPolicy ask 名单(同一只铸点);请求体的
|
|
166
168
|
* `settings.permissions.ask` 到不了这条腿(本文件不 import 任何请求受理 / `task-settings`,`run-local` 的 spec 自铸);
|
|
167
169
|
* · `operator_forced`:部署**强制**要人答的问 —— 能铸它的来源(真 Runner × `applyLocalGovernance` 亲读类集,读数在 `test/run-local.test.ts` S-1100 ⑨ 各格):`APPROVAL_NEVER_AUTO`
|
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 },
|
|
@@ -157,12 +158,12 @@ export const NO_TTY_ASK_CLASS_ROW = {
|
|
|
157
158
|
artifact_shared_read: { remedy: "terminal_only", autoBudgetReleases: false },
|
|
158
159
|
mode: { remedy: "terminal_only", autoBudgetReleases: false },
|
|
159
160
|
unstated: { remedy: "grant", autoBudgetReleases: false },
|
|
160
|
-
ordinary: { remedy: "
|
|
161
|
+
ordinary: { remedy: "grant", autoBudgetReleases: false },
|
|
161
162
|
requires_user_interaction: { remedy: "terminal_only", autoBudgetReleases: false },
|
|
162
163
|
};
|
|
163
164
|
const NO_TTY_REMEDY_SENTENCE = {
|
|
164
|
-
grant: () => `
|
|
165
|
-
`(
|
|
165
|
+
grant: () => `Nothing on the allow layer covers this call: the operator can clear this kind of question once with APPROVAL_GRANT ` +
|
|
166
|
+
`(a tool name, mcp__<server>__* / a2a__<peer>__*, Bash(<prefix>:*), or Read(//<dir>/**)).`,
|
|
166
167
|
approval_lists: (budget) => `The gate comes from the approval lists: APPROVAL_REQUIRE / APPROVAL_NEVER_AUTO (APPROVAL_DENY refuses outright without asking); ` +
|
|
167
168
|
`a separate .env for this CLI keeps a server's lists off it.` +
|
|
168
169
|
(budget ? ` APPROVAL_AUTO_BUDGET auto-approves that many APPROVAL_REQUIRE calls.` : ""),
|
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>;
|
package/dist/runs.js
CHANGED
|
@@ -82,6 +82,7 @@ const PREPARE_REFUSAL_HTTP_STATUS = {
|
|
|
82
82
|
"config.invalid_checkpoint_store": 400,
|
|
83
83
|
"config.skill_listing_budget_invalid": 400,
|
|
84
84
|
"config.user_invoked_skill_unknown": 400,
|
|
85
|
+
"config.image_invalid": 400,
|
|
85
86
|
"config.memory_capture_unsupported": 409,
|
|
86
87
|
};
|
|
87
88
|
export function resumeAtHttpStatus(result) {
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
import { type ShellGateDoctrine, type TaskSpec, type ToolPolicy } from "@sema-agent/core";
|
|
27
27
|
import { GovernanceConfig } from "@sema-agent/settings-schema";
|
|
28
28
|
import { type GovernanceAskMarks } from "./governance-ask-marks.js";
|
|
29
|
+
import { type GovernanceOverrides } from "./governance-overrides.js";
|
|
29
30
|
/** 运维 autonomy 四档的**闭集**(同上)。`config.ts` 的 `parseAutonomy` 也从这里读 —— 四档词表全仓一份。 */
|
|
30
31
|
export declare const AUTONOMY_MODES: ["read-only", "ask", "plan", "auto"];
|
|
31
32
|
/** A per-command governance rule (registry `runtime.commandPolicy[]`) —— **型也从契约派生**:
|
|
@@ -97,19 +98,6 @@ export declare function validateCommandRules(rules: CommandRule[]): string[];
|
|
|
97
98
|
* 闭集本身则由契约 zod 派生 ⇒ 契约加一个判词,`RANK` 与桶分配的穷尽 switch 双双**编译红**。
|
|
98
99
|
*/
|
|
99
100
|
export declare function compileCommandPolicyLayers(rules: CommandRule[] | undefined): ToolPolicy[];
|
|
100
|
-
/** 运维 allow 规则表的出处名 = 它的 env 名(盖进每条规则;编不出来 / 不合文法时拒启句的主语)。 */
|
|
101
|
-
export declare const APPROVAL_GRANT_SOURCE = "APPROVAL_GRANT";
|
|
102
|
-
/**
|
|
103
|
-
* 7.115.0 —— 运维的 allow 规则(`APPROVAL_GRANT`)编成治理层的**叶**(空 / 缺席 ⇒ `[]`,本层一片叶都不铸,折叠逐字与修前同)。
|
|
104
|
-
*
|
|
105
|
-
* 编译口是**请求腿 `settings.permissions.allow` 的同一只**(`permission-rule-list.ts` `createRuleListPolicy`,`behavior: "allow"`):命中 = core 带
|
|
106
|
-
* `decisionReason: "rule"` 的放行(折叠把这句表态带到终局,引擎据此不再为没人表态的服务器工具调用出 `unstated` 卡);未命中 = 裸 allow =
|
|
107
|
-
* 不表态。它是一片**不收紧**的叶 —— 与 commandPolicy / 守卫集同住一个折叠点,只因为三者是同一个「部署说的话」:折叠 deny > ask > allow,
|
|
108
|
-
* 同折叠里任何一片叶的 deny / ask、`tightenTaskSpec` 之下的审批基线、之上的请求 settings 与会话规则,照旧压过它。
|
|
109
|
-
*
|
|
110
|
-
* 启动校验(`config.ts` `approvalGrantRefusal`)试编的也是本函数 —— 拒启判据与运行期编译是同一只,不会出现「启动收下、任务里编不出来」。
|
|
111
|
-
*/
|
|
112
|
-
export declare function compileApprovalGrantLayers(grant: readonly string[] | undefined): ToolPolicy[];
|
|
113
101
|
/**
|
|
114
102
|
* Expand an autonomy mode into TIGHTEN-ONLY `TaskSpec` safety overrides (the primitives — TRAP #2):
|
|
115
103
|
* - `read-only` / `plan` → `handsReadOnly: true` **+ `writeFace: "roots"`** (read + propose, never mutate; the
|
|
@@ -133,7 +121,7 @@ export declare function compileApprovalGrantLayers(grant: readonly string[] | un
|
|
|
133
121
|
* 与 `ask → shellGate:"always"` 同姿势:这一档**自己就是**一次「围栏在」的表态,所以无表态的请求上
|
|
134
122
|
* 它照样把键写出来(不是「只在有 open 时才压」——那又是一条判据)。
|
|
135
123
|
*/
|
|
136
|
-
export declare function autonomyOverrides(autonomy: Autonomy | undefined):
|
|
124
|
+
export declare function autonomyOverrides(autonomy: Autonomy | undefined): GovernanceOverrides;
|
|
137
125
|
/**
|
|
138
126
|
* [ref]:**部署级**判据 —— 本部署的治理层自己是否把 shellGate 抬到 `"always"`。
|
|
139
127
|
*
|
|
@@ -216,17 +204,15 @@ export declare function governanceOriginOf(gate: {
|
|
|
216
204
|
};
|
|
217
205
|
} | undefined, governance: GovernancePosture | undefined): true | undefined;
|
|
218
206
|
/**
|
|
219
|
-
* Apply the operator's runtime governance (autonomy + commandPolicy + manualModeShellGate + the guard set
|
|
220
|
-
* `APPROVAL_GRANT` allow rules) onto a base
|
|
207
|
+
* Apply the operator's runtime governance (autonomy + commandPolicy + manualModeShellGate + the guard set) onto a base
|
|
221
208
|
* `TaskSpec`, TIGHTEN-ONLY, in a SINGLE {@link tightenTaskSpec} call: commandPolicy compiles to a `toolPolicy`
|
|
222
209
|
* override (combined onto the baseline by `tightenTaskSpec` — TRAP #1), and autonomy expands to
|
|
223
210
|
* `handsReadOnly` / `shellGate` overrides (TRAP #2). `tightenTaskSpec` THROWS `TaskSpecTightenError` if any
|
|
224
211
|
* override would LOOSEN a stricter base safety field — surfacing a misconfiguration loudly instead of silently
|
|
225
212
|
* weakening the gate. Returns `base` untouched when there is nothing to apply.
|
|
226
213
|
*
|
|
227
|
-
* 7.
|
|
228
|
-
*
|
|
229
|
-
* 编译口与请求腿的 allow 桶同一只({@link compileApprovalGrantLayers})。
|
|
214
|
+
* 7.117.0(core 7.42.0):运维的 allow 表 `APPROVAL_GRANT` 不再是这里的一片叶 —— 策略只说 deny / ask,allow 规则住在引擎 allow 层的部署席
|
|
215
|
+
* `RunnerDeps.allowedTools`(`boot/runner-deps.ts` 的部署姿态座席族,每只 Runner 同一席)。本函数因此只剩收紧。
|
|
230
216
|
*
|
|
231
217
|
* [ref]([ref] server 分单②):`MANUAL_MODE_SHELL_GATE` 从 settings 折叠搬到这里**无条件施加**。旧家
|
|
232
218
|
* (task-settings deriveSettingsPolicy 的 default/auto/acceptEdits 三臂)让一个**部署级 tighten-only 旋钮
|
|
@@ -260,8 +246,6 @@ export declare function applyRuntimeGovernance(base: TaskSpec, governance: {
|
|
|
260
246
|
/** 守卫集的**叶**(`deployment-governance.ts` `createDeploymentGovernanceInputs` 预铸;S-958 起是叶的列表,不是一只预折的策略)。
|
|
261
247
|
* 空列表与缺席同义。 */
|
|
262
248
|
sensitivePathLayers?: readonly ToolPolicy[];
|
|
263
|
-
/** 7.115.0:运维的 allow 规则(`APPROVAL_GRANT` 原串;本函数经 {@link compileApprovalGrantLayers} 编成叶)。空列表与缺席同义。 */
|
|
264
|
-
approvalGrant?: readonly string[];
|
|
265
249
|
/** [ref]/[ref] `governanceForced` 的标记表(缺省 = **当前 run 腿的 ALS 表**,见
|
|
266
250
|
* `governance-ask-marks.ts` 顶注「作用域」段)。注入口只为测试与将来的多实例形。 */
|
|
267
251
|
askMarks?: GovernanceAskMarks;
|