@sema-agent/server 7.130.0 → 7.131.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 +87 -0
- package/README.md +8 -5
- package/README.zh-CN.md +6 -4
- package/USAGE.md +36 -8
- package/dist/audit.js +4 -1
- package/dist/bench/s1/live-deps.js +2 -0
- package/dist/boot/resolve-spec.js +38 -15
- package/dist/boot/stage-02-web-admin-pull.js +9 -1
- package/dist/boot/stage-10-http-server.js +6 -3
- package/dist/capabilities/builtin-tools.js +2 -1
- package/dist/capabilities/composite-members.d.ts +1 -0
- package/dist/capabilities/composite-members.js +1 -0
- package/dist/capabilities/hands-lane.js +2 -2
- package/dist/capabilities/host-tool-names.d.ts +17 -0
- package/dist/capabilities/host-tool-names.js +15 -0
- package/dist/capabilities/mcp-probe.js +3 -3
- package/dist/capabilities/repo-tools.js +4 -3
- package/dist/capabilities/scenarios.d.ts +8 -7
- package/dist/capabilities/scenarios.js +5 -4
- package/dist/capabilities/select-environment-tool.js +2 -1
- package/dist/capabilities/send-user-file-tool.d.ts +0 -1
- package/dist/capabilities/send-user-file-tool.js +2 -2
- package/dist/capabilities/tool-defer.js +2 -1
- package/dist/config-catalog.js +16 -5
- package/dist/config-invariants.js +2 -2
- package/dist/config-provider.js +2 -2
- package/dist/config-types.d.ts +6 -0
- package/dist/config.d.ts +2 -2
- package/dist/config.js +18 -17
- package/dist/elicitation.d.ts +7 -0
- package/dist/elicitation.js +20 -5
- package/dist/hooks/hook-llm.js +2 -2
- package/dist/hooks/hook-runner.d.ts +54 -8
- package/dist/hooks/hook-runner.js +86 -16
- package/dist/http/admission.js +2 -2
- package/dist/http/approval-sweeps.js +2 -2
- package/dist/http/checkpoint-ctx.d.ts +12 -11
- package/dist/http/checkpoint-ctx.js +4 -4
- package/dist/http/dispatch.js +1 -1
- package/dist/http/resume-legs.js +6 -5
- package/dist/http/routes/approvals-assistant.js +1 -1
- package/dist/http/routes/capabilities.js +6 -16
- package/dist/http/routes/runs.js +6 -10
- package/dist/http/routes/sessions.js +22 -10
- package/dist/http/routes/tasks.js +16 -11
- package/dist/http/scenario-read-face.d.ts +14 -0
- package/dist/http/scenario-read-face.js +5 -0
- package/dist/http/send.d.ts +10 -0
- package/dist/http/send.js +6 -0
- package/dist/http/server.d.ts +16 -12
- package/dist/http/server.js +2 -2
- package/dist/http/stored-body-replay.d.ts +10 -3
- package/dist/http/stored-body-replay.js +27 -6
- package/dist/http/wire-types.d.ts +18 -5
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/mcp-assertion-header.d.ts +2 -2
- package/dist/mcp-assertion-header.js +3 -3
- package/dist/model-entry-refusal.d.ts +1 -7
- package/dist/model-route-endpoint.d.ts +51 -11
- package/dist/model-route-endpoint.js +45 -23
- package/dist/model-route-gap.d.ts +5 -4
- package/dist/model-route-gap.js +1 -1
- package/dist/model-select.d.ts +58 -0
- package/dist/model-select.js +31 -0
- package/dist/namespaced-rule-name.d.ts +7 -4
- package/dist/namespaced-rule-name.js +4 -3
- package/dist/observability/fail-open.d.ts +11 -6
- package/dist/observability/fail-open.js +5 -1
- package/dist/plugins/local-session-store.js +2 -2
- package/dist/plugins/one-time-migrations.d.ts +10 -1
- package/dist/plugins/one-time-migrations.js +16 -1
- package/dist/question.d.ts +18 -9
- package/dist/question.js +21 -13
- package/dist/request-key-closure.d.ts +2 -2
- package/dist/request-key-closure.js +16 -9
- package/dist/rule-entry-list.d.ts +124 -17
- package/dist/rule-entry-list.js +140 -24
- package/dist/run-local.d.ts +3 -3
- package/dist/run-local.js +8 -7
- package/dist/runs.d.ts +42 -2
- package/dist/runs.js +45 -2
- package/dist/spec-fields.d.ts +8 -4
- package/dist/stored-request-hooks-m20.d.ts +23 -0
- package/dist/stored-request-hooks-m20.js +124 -0
- package/dist/stored-request-tool-axes-m19.d.ts +1 -1
- package/dist/task-mcp.d.ts +41 -77
- package/dist/task-mcp.js +143 -75
- package/dist/task-settings.d.ts +41 -5
- package/dist/task-settings.js +39 -25
- package/dist/trace/core-keyset-guard.d.ts +2 -2
- package/dist/trace/ledger-sink.d.ts +29 -0
- package/dist/trace/ledger-sink.js +11 -0
- package/dist/trace/project.d.ts +6 -0
- package/dist/trace/project.js +11 -0
- package/dist/trace/wire-projection-faces.d.ts +1 -1
- package/dist/trace/wire-projection-faces.js +1 -1
- package/dist/user-authored-text.d.ts +7 -0
- package/dist/user-authored-text.js +3 -0
- package/dist/web-admin-client/apply-effective.js +10 -5
- package/dist/web-admin-client/types.d.ts +1 -1
- package/package.json +2 -2
- package/scripts/drain-check-local.mjs +3 -2
package/dist/runs.d.ts
CHANGED
|
@@ -137,6 +137,38 @@ export declare function releaseKeyIfZeroWorkRefusal(runStore: {
|
|
|
137
137
|
* surfaces the degrade (a metric) so a SYSTEMATIC failure — e.g. an un-migrated DB missing `resume_anchor` — is
|
|
138
138
|
* observable instead of a silent feature outage.
|
|
139
139
|
*/
|
|
140
|
+
/**
|
|
141
|
+
* 「这一次用户消息提交是不是一句**提问**」的唯一读法 —— 三处用户回退锚(后台腿与续跑腿的 {@link TurnAnchorCapture}、`/v1/tasks/stream`
|
|
142
|
+
* 腿的 `userMsgEntryId`)同调这一只。
|
|
143
|
+
*
|
|
144
|
+
* 目标声明了附件时,引擎把每只附件铸成**一条自己的用户消息**,紧跟在目标后面提交(core `human_input.attachments` 顶注:
|
|
145
|
+
* 「each attachment is delivered as its own message after the objective; its `message_committed` is the delivery」)。
|
|
146
|
+
* 所以一次带 N 只附件的提交会出 1 + N 次 `message_committed{role:"user"}`,而「回到这条提问」的锚要的是**人的那条**
|
|
147
|
+
* (core 的 `resumeAt` 认它:「a `resumeAt` that keeps the message keeps its group」;以附件帧作 `resumeAt` 引擎拒 `resume_at.not_a_message`),
|
|
148
|
+
* 不是排在最后的附件帧。目标**之前**还可能有引擎排队的注入消息同批提交(亲读 core `engine/harness/agent-harness.js` 的 `takePrompts`:
|
|
149
|
+
* 排队消息 ⇒ 目标 ⇒ 附件帧;真案 = 工作区是 git 仓且带图时,git 状态帧单独排在目标前面),所以「窗口里第一次用户提交」不是判据。
|
|
150
|
+
*
|
|
151
|
+
* **为什么是「窗口 + 严格吻合」而不是读标记**:`message_committed` 上只有 `entryId / role / toolCallId?`,附件帧与用户真提交在事件上**不可辨认**;
|
|
152
|
+
* 落盘的行上有标记(`attachment` 键;引擎收尾的中断标记带 `provenance: "engine-note"`),但事件到手的那一刻本仓读不到那一行
|
|
153
|
+
* (会话店面没有按条目读的口,只有整日志导出),所以只能数。数的前提是「目标之后紧接恰好 N 只帧、其后才是别的」—— 前提成立才落锚,
|
|
154
|
+
* **不成立就不猜**:猜错的锚会落在附件帧上(引擎拒)或引擎的中断标记上(回到的不是用户那句)。
|
|
155
|
+
*
|
|
156
|
+
* 读法:
|
|
157
|
+
* · `human_input{delivery:"applied"}` 带 N 只附件 ⇒ 开窗;窗内的用户提交只攒、不落锚(此刻还不知道它们是不是『目标 + N 只帧』)。
|
|
158
|
+
* · 窗被**非用户提交**(第一次助手消息 / 工具结果)关上 ⇒ 窗内共 k 次用户提交,k ≥ N+1 时提问 = 倒数第 N+1 次(严格吻合);k < N+1(帧数不符)⇒ 不猜。
|
|
159
|
+
* · 窗被 `turn_end` 先切断 ⇒ 第一次模型调用没出任何提交就收了(halt / interrupt / 空失败):窗里可能夹着引擎收尾的中断标记,数 N 会落在帧或标记上 ⇒ 不猜。
|
|
160
|
+
* · 窗内又应用了一条别的输入(`human_input{applied}`,如第一次模型调用前排进来的 steer;事件先于它的正文提交到手)⇒ 窗里多了一条别人的消息,数 N 会落在帧上 ⇒ 窗作废、不猜,
|
|
161
|
+
* 之后的用户提交(含那条 steer 正文)是窗口之外的提交,最后一次赢。排队未应用(`delivery: "queued"`)的输入不动窗。
|
|
162
|
+
* · 「不猜」= 返回 `undefined`、这次提交不落锚;拿这次 run 的 taskId 去 `resumeAt` 得 404 `resume_at.unknown_event`(文案点名这一类成因)。
|
|
163
|
+
* 窗口之外的用户提交(如 interrupt 带话的 steer 正文,在 `turn_end` 之后提交)照修前读法(是提问,最后一次赢)。
|
|
164
|
+
* 返回这次提交之后的提问 `entryId`(调用方照旧「最后一次赢」地落锚),不是提问相关的提交返回 `undefined`。
|
|
165
|
+
*
|
|
166
|
+
* 退役条件:引擎在落盘的帧上(`message_committed`)带上附件标记之日,本读法整体换成「按标记判一次」(零计数、无窗口、无拒臂),
|
|
167
|
+
* 窗口 / 严格吻合 / 不猜三臂与 `resume_at.unknown_event` 文案里对应的那一句一并删除。
|
|
168
|
+
*/
|
|
169
|
+
export declare function createPromptCommitReader(): (ev: {
|
|
170
|
+
readonly type: string;
|
|
171
|
+
}) => string | undefined;
|
|
140
172
|
export declare class TurnAnchorCapture {
|
|
141
173
|
private readonly capture;
|
|
142
174
|
private readonly onCaptureFail?;
|
|
@@ -151,6 +183,8 @@ export declare class TurnAnchorCapture {
|
|
|
151
183
|
/** The turn's FIRST text_delta eventId = the message's stable resume handle (also stamped on the durable text event). */
|
|
152
184
|
firstTextEventId: string | undefined;
|
|
153
185
|
private turnHadTool;
|
|
186
|
+
/** 用户提交是不是提问的唯一读法({@link createPromptCommitReader};附件帧不是提问)。 */
|
|
187
|
+
private readonly promptCommit;
|
|
154
188
|
constructor(capture: ((eventId: string) => Promise<void>) | undefined, onCaptureFail?: (() => void) | undefined,
|
|
155
189
|
/** R8 (CC-parity rewind): capture a USER-message anchor straight from the
|
|
156
190
|
* `message_committed{role:"user"}` event — its `entryId` is the persisted user-message `SessionTreeEntry.id`,
|
|
@@ -160,9 +194,15 @@ export declare class TurnAnchorCapture {
|
|
|
160
194
|
* Keyed by the run's taskId (the handle the shell already holds from the POST response — no eventId→entryId
|
|
161
195
|
* side map / no `getLeafId` inference needed). The assistant-turn capture above stays as an additive bonus. */
|
|
162
196
|
captureUser?: ((entryId: string) => Promise<void>) | undefined);
|
|
197
|
+
/** A `human_input` arrived — the objective's declared attachments open the frame window of {@link createPromptCommitReader}. */
|
|
198
|
+
onHumanInput(ev: {
|
|
199
|
+
readonly type: string;
|
|
200
|
+
}): void;
|
|
163
201
|
/** A `message_committed` arrived — capture a USER-message rewind anchor (CC parity). Best-effort (a failure only
|
|
164
|
-
* makes that one prompt non-rewindable). Only
|
|
165
|
-
*
|
|
202
|
+
* makes that one prompt non-rewindable). Only a user commit that is a PROMPT (not an attachment frame trailing the
|
|
203
|
+
* objective — {@link createPromptCommitReader}; an attachment window is decided when a non-user commit closes it, so
|
|
204
|
+
* the anchor of an attachment-carrying prompt lands on THAT call); assistant/toolResult committed entries are handled by
|
|
205
|
+
* the turn-text path (assistant) or deliberately not anchored (toolResult — `resumeAt` rejects them). */
|
|
166
206
|
onMessageCommitted(role: string | undefined, entryId: string | undefined): Promise<void>;
|
|
167
207
|
/** A text_delta arrived — latch the first DEFINED eventId as the message handle (eventId is optional on the stream). */
|
|
168
208
|
onText(eventId: string | undefined): void;
|
package/dist/runs.js
CHANGED
|
@@ -76,6 +76,7 @@ const PREPARE_REFUSAL_HTTP_STATUS = {
|
|
|
76
76
|
"config.skill_listing_budget_invalid": 400,
|
|
77
77
|
"config.user_invoked_skill_unknown": 400,
|
|
78
78
|
"config.image_invalid": 400,
|
|
79
|
+
"config.user_attachment_invalid": 400,
|
|
79
80
|
"config.tool_face_control": 400,
|
|
80
81
|
"config.memory_capture_unsupported": 409,
|
|
81
82
|
};
|
|
@@ -109,20 +110,58 @@ export async function releaseKeyIfZeroWorkRefusal(runStore, taskId, result) {
|
|
|
109
110
|
if (syncRefusalHttpStatus(result) !== undefined)
|
|
110
111
|
await runStore.releaseIdemKey(taskId);
|
|
111
112
|
}
|
|
113
|
+
export function createPromptCommitReader() {
|
|
114
|
+
let attachmentWindow;
|
|
115
|
+
return (ev) => {
|
|
116
|
+
if (ev.type === "human_input") {
|
|
117
|
+
const h = ev;
|
|
118
|
+
if (h.delivery !== "applied")
|
|
119
|
+
return undefined;
|
|
120
|
+
attachmentWindow = Array.isArray(h.attachments) && h.attachments.length > 0 ? { frames: h.attachments.length, commits: [] } : undefined;
|
|
121
|
+
return undefined;
|
|
122
|
+
}
|
|
123
|
+
if (ev.type === "turn_end") {
|
|
124
|
+
attachmentWindow = undefined;
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
if (ev.type !== "message_committed")
|
|
128
|
+
return undefined;
|
|
129
|
+
const c = ev;
|
|
130
|
+
const entryId = typeof c.entryId === "string" && c.entryId.length > 0 ? c.entryId : undefined;
|
|
131
|
+
if (c.role === "user") {
|
|
132
|
+
if (attachmentWindow !== undefined) {
|
|
133
|
+
attachmentWindow.commits.push(entryId);
|
|
134
|
+
return undefined;
|
|
135
|
+
}
|
|
136
|
+
return entryId;
|
|
137
|
+
}
|
|
138
|
+
const closed = attachmentWindow;
|
|
139
|
+
attachmentWindow = undefined;
|
|
140
|
+
if (closed === undefined || closed.commits.length < closed.frames + 1)
|
|
141
|
+
return undefined;
|
|
142
|
+
return closed.commits[closed.commits.length - 1 - closed.frames];
|
|
143
|
+
};
|
|
144
|
+
}
|
|
112
145
|
export class TurnAnchorCapture {
|
|
113
146
|
capture;
|
|
114
147
|
onCaptureFail;
|
|
115
148
|
captureUser;
|
|
116
149
|
firstTextEventId;
|
|
117
150
|
turnHadTool = false;
|
|
151
|
+
promptCommit = createPromptCommitReader();
|
|
118
152
|
constructor(capture, onCaptureFail, captureUser) {
|
|
119
153
|
this.capture = capture;
|
|
120
154
|
this.onCaptureFail = onCaptureFail;
|
|
121
155
|
this.captureUser = captureUser;
|
|
122
156
|
}
|
|
157
|
+
onHumanInput(ev) {
|
|
158
|
+
this.promptCommit(ev);
|
|
159
|
+
}
|
|
123
160
|
async onMessageCommitted(role, entryId) {
|
|
124
|
-
|
|
125
|
-
|
|
161
|
+
const committed = { type: "message_committed", role, entryId };
|
|
162
|
+
const prompt = this.promptCommit(committed);
|
|
163
|
+
if (prompt !== undefined && this.captureUser) {
|
|
164
|
+
await this.captureUser(prompt).catch(() => this.onCaptureFail?.());
|
|
126
165
|
}
|
|
127
166
|
}
|
|
128
167
|
onText(eventId) {
|
|
@@ -135,6 +174,7 @@ export class TurnAnchorCapture {
|
|
|
135
174
|
this.turnHadTool = true;
|
|
136
175
|
}
|
|
137
176
|
async onTurnEnd() {
|
|
177
|
+
this.promptCommit({ type: "turn_end" });
|
|
138
178
|
if (this.firstTextEventId && !this.turnHadTool && this.capture) {
|
|
139
179
|
await this.capture(this.firstTextEventId).catch(() => this.onCaptureFail?.());
|
|
140
180
|
}
|
|
@@ -373,6 +413,9 @@ export async function runInBackground(runner, spec, runStore, taskId, metrics, p
|
|
|
373
413
|
case "status":
|
|
374
414
|
metrics?.inc("brain_retry_total", { phase: String(ev.phase) });
|
|
375
415
|
break;
|
|
416
|
+
case "human_input":
|
|
417
|
+
anchor.onHumanInput(ev);
|
|
418
|
+
break;
|
|
376
419
|
case "message_committed":
|
|
377
420
|
await anchor.onMessageCommitted(ev.role, ev.entryId);
|
|
378
421
|
break;
|
package/dist/spec-fields.d.ts
CHANGED
|
@@ -51,7 +51,7 @@ export declare const MAX_TASK_AGENTS = 32;
|
|
|
51
51
|
* `ruleEntryListShapeOk`):引擎直接 `for (const entry of list)` —— 一个裸字符串 `"Bash"` 会被逐字符迭代成 `B` / `a` / `s` / `h` 四条
|
|
52
52
|
* (deny 侧 = 意图静默落空,安全轴上的放宽方向)。条目的**文法与名字**是引擎的读法:允侧读不出的条目(`*` / 写路径 / 域名 / 退役名 …)引擎在开跑时发
|
|
53
53
|
* `permission.allowed_tool_not_granted`(受众 = 用户);拒侧读成空的条目(文法或名字层)在 `resolveSpec` 拒(提交 400 / 续跑 409,`rule-entry-list.ts` 的
|
|
54
|
-
* `
|
|
54
|
+
* `inertToolListEntryOf`,判据 = 引擎的条目读法;同一只函数也拒请求的工具名选择器清单里的近似名 / 退役名)。形对之后两道门再问 {@link skillToolListRefusal}(条数帽 ⇒ 400)。
|
|
55
55
|
* 条数帽 = 条目表那一只(`rule-entry-list.ts` `allowLayerEntryCountIssue`,准备开销的界;与请求 allow 桶、运维表同一只),
|
|
56
56
|
* 在 {@link skillToolListRefusal} 里判;无长度帽([ref])。
|
|
57
57
|
*/
|
|
@@ -67,12 +67,16 @@ export declare function skillToolListRefusal(skill: Record<string, unknown>): st
|
|
|
67
67
|
* 白名单外键 fail-loud —— 与 resilience/limits 的「容忍未知键」相反:agents 是能力定义,静默丢字段 =
|
|
68
68
|
* 定义被悄悄削(一个丢掉的 isolation 就是放宽),比 400 更危险,故整条拒。`allowTools` / `denyTools` 本门只判形与条数(`toolList`),
|
|
69
69
|
* **名字归引擎一只判**(core 7.49.0 #1623 / #1433 第二步起与根 run 的 `allowTools` / `excludeTools` 是同一只选择器,开跑前,三来源同判):core 7.51.0 #1736 起
|
|
70
|
-
* 没有任何工具答应的名字(无人挂的名、大小写孪生 / 退役名 `bash` / `BASH`)
|
|
71
|
-
*
|
|
70
|
+
* 没有任何工具答应的名字(无人挂的名、大小写孪生 / 退役名 `bash` / `BASH`)**引擎不拒 run** —— 条目原样保留、经 onError(config)告知一次(带该写的拼法);
|
|
71
|
+
* 其中**写错现名的**(`denyTools:["BASH"]` 想禁的照用 = 静默放行;`allowTools:["BASH"]` 想放的什么也没放 = 静默收窄)由 `resolveSpec` 前置拒:**限制侧**(`denyTools`)四类全拒(近似名 · 退役名 · 文法读不出 · `*`),
|
|
72
|
+
* **放行侧**(`allowTools`)只拒近似名 —— 放行侧的退役名 / 内容形 / 文法读不出照 7.130.0 受理、交引擎既有通告(文法读不出的放行条目引擎在子代 prepare 整 run 拒 `config.agent.unknown_tool`,根 `allowTools` 同步腿 400 `config.tool_face_control`;`rule-entry-list.ts` `inertToolListEntryOf`:近似名提交 400 `config.agent.unknown_tool` 点名现名、续跑按方向分流;
|
|
73
|
+
* 判官仍是引擎的条目读法,不是第二个判官);
|
|
74
|
+
* **真未知名**(没有近似也没有退役对应:懒挂载的合法形)照旧只告运维、不拒。
|
|
75
|
+
* 任何工具名都不可能有的拼法(`grammar`)与非字符串条目引擎照旧整 run 拒 `config.agent.unknown_tool` —— 主车道的委派工具带 extraTools 工厂,本仓在同一处
|
|
72
76
|
* 声明它的名册(core 7.46.0 `extraToolNames`;工厂与名册读同一批工具对象,`capabilities/builtin-tools.ts` `createSubagentExtraTools`),引擎因此列得出子代工具池。
|
|
73
77
|
* 两张表管子代可挂载的**全部**工具:委派池、hands 内建、引擎自己的工具(`Monitor` / `Artifact` / `Skill` / 记忆工具 …)与父的 MCP 挂载 ——
|
|
74
78
|
* `denyTools:["Monitor"]` ⇒ 子代没有 `Monitor`;`allowTools` 只点了调用方工具 ⇒ 子代只剩那几只与它这条腿的协议工具。`denyTools` 的 `Tool(content)` ⇒ 整只工具。
|
|
75
|
-
*
|
|
79
|
+
* 本门不做名字对照(拒侧的那一判在 `resolveSpec`,见上;本门只判形与条数)。
|
|
76
80
|
* ⚠️ 键判断必须 Object.hasOwn:`in` 走原型链,JSON 里的 "toString"/"constructor"/"__proto__" 键会被
|
|
77
81
|
* 误判为白名单字段,随后 SHAPES[k].ok 不是函数 ⇒ TypeError(fresh 500 / resume 砖;cross-review 复现)。 */
|
|
78
82
|
export declare function taskAgentIssue(item: unknown): string | null;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type ParseHooksConfigResult, type RunnableHooksConfig } from "./hooks/hook-runner.js";
|
|
2
|
+
/** 一次重读的产物:读者对剥键之后的文档的答案,与被剥掉的每个坐标(出门形,经契约包的脱敏出门)。 */
|
|
3
|
+
export interface StoredHooksRereadM20Result {
|
|
4
|
+
readonly config: RunnableHooksConfig;
|
|
5
|
+
readonly stripped: readonly string[];
|
|
6
|
+
}
|
|
7
|
+
/** 重读座:存下的 `settings.hooks` 原值 → 迁移产物(`undefined` = 不迁移,调用方走原臂)。 */
|
|
8
|
+
export type StoredHooksRereadM20 = (raw: unknown) => StoredHooksRereadM20Result | undefined;
|
|
9
|
+
/** 读者(= `parseHooksConfig`);作形参只为让「读者交回什么拒因」可被替身喂(默认值就是生产读者)。 */
|
|
10
|
+
export type HooksReader = (raw: unknown) => ParseHooksConfigResult;
|
|
11
|
+
/**
|
|
12
|
+
* 重读一份存体 hooks:读者拒因全部属于两形 ⇒ 剥掉被点名的块里按类型确属未知的键、再问读者,**剥到读者受理为止**,过了就交回产物 + 被剥坐标;否则 `undefined`(原臂:整份不采纳)。
|
|
13
|
+
* 调用方只在读者**已经拒过**这份原值之后才来问;读者一开始就受理的值不进这里(进来也返回 `undefined`:没有可迁移的东西)。
|
|
14
|
+
*
|
|
15
|
+
* 终止:循环体以一次读开头(最后一次剥键之后必有一次读);继续下一轮的条件是本轮真从文档里剥掉了至少一个对象键,文档只减不增 ⇒ 必停;同一块只会被点名一次 ⇒ 轮数 ≤ 被点名的块数 + `__proto__` 键数。
|
|
16
|
+
*/
|
|
17
|
+
export declare function rereadStoredHooksM20(raw: unknown, read?: HooksReader): StoredHooksRereadM20Result | undefined;
|
|
18
|
+
/**
|
|
19
|
+
* 此刻(`today` = UTC 日历日 YYYY-MM-DD)M20 的窗开着 ⇒ 交回重读座;关了 ⇒ `undefined`(今天的读法)。窗只读登记表 M20 那一行
|
|
20
|
+
* (`migrationWindowOpen`,与 M10 / M19 同一只判据)。登记表里没有 M20 ⇒ 响亮抛:重读臂活过了它的登记行,该随那一行一起删。
|
|
21
|
+
*/
|
|
22
|
+
export declare function storedHooksRereadM20(today: string): StoredHooksRereadM20 | undefined;
|
|
23
|
+
//# sourceMappingURL=stored-request-hooks-m20.d.ts.map
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { HOOK_ENTRY_TYPES } from "@sema-agent/settings-schema/hooks";
|
|
2
|
+
import { hooksCoordinate, parseHooksConfig } from "./hooks/hook-runner.js";
|
|
3
|
+
import { ONE_TIME_MIGRATIONS, migrationWindowOpen } from "./plugins/one-time-migrations.js";
|
|
4
|
+
const ENTRY_FIELD_NAMES = {
|
|
5
|
+
command: { type: true, if: true, timeout: true, statusMessage: true, once: true, command: true, shell: true, async: true, asyncRewake: true },
|
|
6
|
+
prompt: { type: true, if: true, timeout: true, statusMessage: true, once: true, prompt: true, model: true },
|
|
7
|
+
agent: { type: true, if: true, timeout: true, statusMessage: true, once: true, prompt: true, model: true },
|
|
8
|
+
http: { type: true, if: true, timeout: true, statusMessage: true, once: true, url: true, headers: true, allowedEnvVars: true },
|
|
9
|
+
mcp_tool: { type: true, if: true, timeout: true, statusMessage: true, once: true, server: true, tool: true, input: true },
|
|
10
|
+
};
|
|
11
|
+
const MATCHER_FIELD_NAMES = { matcher: true, hooks: true };
|
|
12
|
+
function isRecord(v) {
|
|
13
|
+
return v !== null && typeof v === "object" && !Array.isArray(v);
|
|
14
|
+
}
|
|
15
|
+
function isEntryType(v) {
|
|
16
|
+
return typeof v === "string" && HOOK_ENTRY_TYPES.includes(v);
|
|
17
|
+
}
|
|
18
|
+
function nodeAt(node, path) {
|
|
19
|
+
let cur = node;
|
|
20
|
+
for (const seg of path) {
|
|
21
|
+
if (Array.isArray(cur))
|
|
22
|
+
cur = typeof seg === "number" ? cur[seg] : undefined;
|
|
23
|
+
else if (isRecord(cur) && typeof seg === "string")
|
|
24
|
+
cur = Object.hasOwn(cur, seg) ? cur[seg] : undefined;
|
|
25
|
+
else
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
return cur;
|
|
29
|
+
}
|
|
30
|
+
function contractFieldsOf(code, block) {
|
|
31
|
+
if (!isRecord(block))
|
|
32
|
+
return undefined;
|
|
33
|
+
if (code === "guard-matcher-invalid")
|
|
34
|
+
return MATCHER_FIELD_NAMES;
|
|
35
|
+
const type = Object.hasOwn(block, "type") ? block.type : undefined;
|
|
36
|
+
return isEntryType(type) ? ENTRY_FIELD_NAMES[type] : undefined;
|
|
37
|
+
}
|
|
38
|
+
function stripTargetsOf(piece, doc) {
|
|
39
|
+
const [root, ...rel] = piece.path;
|
|
40
|
+
if (root !== "hooks")
|
|
41
|
+
return undefined;
|
|
42
|
+
if ((piece.code === "unknown-event" || piece.code === "guard-under-unknown-event") && rel.length === 1 && rel[0] === "__proto__")
|
|
43
|
+
return [rel];
|
|
44
|
+
if ((piece.code === "guard-entry-invalid" || piece.code === "guard-matcher-invalid") && typeof piece.field === "string") {
|
|
45
|
+
const block = nodeAt(doc, rel);
|
|
46
|
+
const known = contractFieldsOf(piece.code, block);
|
|
47
|
+
if (known === undefined || !isRecord(block) || Object.hasOwn(known, piece.field) || !Object.hasOwn(block, piece.field))
|
|
48
|
+
return undefined;
|
|
49
|
+
return Object.keys(block)
|
|
50
|
+
.filter((key) => !Object.hasOwn(known, key))
|
|
51
|
+
.map((key) => [...rel, key]);
|
|
52
|
+
}
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
function cloneDoc(root) {
|
|
56
|
+
const isContainer = (v) => Array.isArray(v) || isRecord(v);
|
|
57
|
+
const shell = (v) => (Array.isArray(v) ? [] : isRecord(v) ? {} : v);
|
|
58
|
+
const copy = shell(root);
|
|
59
|
+
const pending = isContainer(root) ? [[root, copy]] : [];
|
|
60
|
+
for (let next = pending.pop(); next !== undefined; next = pending.pop()) {
|
|
61
|
+
const [source, target] = next;
|
|
62
|
+
if (Array.isArray(source)) {
|
|
63
|
+
for (const value of source) {
|
|
64
|
+
const child = shell(value);
|
|
65
|
+
target.push(child);
|
|
66
|
+
if (isContainer(value))
|
|
67
|
+
pending.push([value, child]);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
else if (isRecord(source)) {
|
|
71
|
+
for (const [key, value] of Object.entries(source)) {
|
|
72
|
+
const child = shell(value);
|
|
73
|
+
Object.defineProperty(target, key, { value: child, writable: true, enumerable: true, configurable: true });
|
|
74
|
+
if (isContainer(value))
|
|
75
|
+
pending.push([value, child]);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return copy;
|
|
80
|
+
}
|
|
81
|
+
function removeKeyAt(doc, path) {
|
|
82
|
+
const key = path[path.length - 1];
|
|
83
|
+
const parent = nodeAt(doc, path.slice(0, -1));
|
|
84
|
+
if (!isRecord(parent) || typeof key !== "string" || !Object.hasOwn(parent, key))
|
|
85
|
+
return false;
|
|
86
|
+
delete parent[key];
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
export function rereadStoredHooksM20(raw, read = parseHooksConfig) {
|
|
90
|
+
const doc = cloneDoc(raw);
|
|
91
|
+
const stripped = [];
|
|
92
|
+
for (;;) {
|
|
93
|
+
const r = read(doc);
|
|
94
|
+
if (r.config !== undefined)
|
|
95
|
+
return stripped.length > 0 ? { config: r.config, stripped } : undefined;
|
|
96
|
+
const pieces = r.pieces;
|
|
97
|
+
if (pieces === undefined || pieces.length === 0)
|
|
98
|
+
return undefined;
|
|
99
|
+
const targets = [];
|
|
100
|
+
for (const piece of pieces) {
|
|
101
|
+
const pieceTargets = stripTargetsOf(piece, doc);
|
|
102
|
+
if (pieceTargets === undefined)
|
|
103
|
+
return undefined;
|
|
104
|
+
for (const target of pieceTargets)
|
|
105
|
+
targets.push(target);
|
|
106
|
+
}
|
|
107
|
+
let removedAny = false;
|
|
108
|
+
for (const target of targets) {
|
|
109
|
+
if (!removeKeyAt(doc, target))
|
|
110
|
+
continue;
|
|
111
|
+
stripped.push(hooksCoordinate(["hooks", ...target]));
|
|
112
|
+
removedAny = true;
|
|
113
|
+
}
|
|
114
|
+
if (!removedAny)
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
export function storedHooksRereadM20(today) {
|
|
119
|
+
const window = ONE_TIME_MIGRATIONS.find((w) => w.id === "M20");
|
|
120
|
+
if (window === undefined)
|
|
121
|
+
throw new Error("stored-request-hooks-m20: the one-time migration registry has no M20 row (the replay-leg stored-hooks re-read window) — the re-read arm outlived its registry row; delete it with that row");
|
|
122
|
+
return migrationWindowOpen(window, today) ? (raw) => rereadStoredHooksM20(raw) : undefined;
|
|
123
|
+
}
|
|
124
|
+
//# sourceMappingURL=stored-request-hooks-m20.js.map
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*
|
|
12
12
|
* ## 可达面(结构上只在续跑重放腿)
|
|
13
13
|
* · 本模块的值只被 `boot/resolve-spec.ts` 引用一处,挂在 `opts.leg` 不是 `"fresh"` 的那一臂(续跑家族经 `http/resume-legs.ts` 的 `resolveResumeSpec`
|
|
14
|
-
* 递 `leg: "resume"`);新提交腿递 `"fresh"` 拿不到重读座,且在腿首已经被同一只判官 400 掉(`
|
|
14
|
+
* 递 `leg: "resume"`);新提交腿递 `"fresh"` 拿不到重读座,且在腿首已经被同一只判官 400 掉(`assertRequestMcpEntries`),读器根本见不到坏形;
|
|
15
15
|
* · 请求腿(`task-mcp.ts`)只 import 本模块的**型**:重读座是 `resolveRequestMcp` 的一个形参,没人递就没有;探测口(`validateRequestMcp`)没有这个形参;
|
|
16
16
|
* · 部署腿(`web-admin-client/skills-mcp.ts` 的 `resolveMcpServers`)是另一只铸 spec 处,不读本模块。
|
|
17
17
|
* 机器锚:`test/mcp-tool-axes-m19.test.ts` 的结构格。
|
package/dist/task-mcp.d.ts
CHANGED
|
@@ -64,6 +64,10 @@ export declare const MAX_REQUEST_MCP_DROP_NAMES = 32;
|
|
|
64
64
|
* · 探测口**响亮拒**(400,点名 `mcpServers[i].<键>`)—— 它问的是「这台到底能不能用」,把一个拼错的键
|
|
65
65
|
* 悄悄忽略再回一份好看的名单,正是那条面要根除的假答案。
|
|
66
66
|
* 两个门,一个词表。表在这里,门在各自的腿上。
|
|
67
|
+
*
|
|
68
|
+
* ## 受理键的判官表也钉在这张表上
|
|
69
|
+
* fresh 提交腿对**受理键的值**同样响亮拒({@link assertRequestMcpEntries}):逐键判官表 {@link ENTRY_KEY_JUDGES} 是 `{ [K in 本表的键]: 判官 }` 的映射型 ——
|
|
70
|
+
* 本表多一个键而没写判官 ⇒ 编译红,不会出现「这个键写坏了却被悄悄受理」的新键。词表外的键仍由读器宽容、由探测口闭集拒(上面两条不变)。
|
|
67
71
|
*/
|
|
68
72
|
export declare const MCP_SERVER_ENTRY_KEYS: readonly ["allowTools", "contentOrigin", "elicitation", "name", "source", "toolAxes", "toolFaces", "transport"];
|
|
69
73
|
/** `transport.kind:"stdio"` 的键闭集(两向钉在 core 那一支上)。 */
|
|
@@ -145,92 +149,52 @@ type ValidatedRequestMcp = {
|
|
|
145
149
|
* 任务。同样是失败,同步 400 至少是**立刻可诊断**的那一种,而且拒得出理由(锁在哪、怎么改)。
|
|
146
150
|
*/
|
|
147
151
|
export declare function assertRequestMcpUnlocked(bodyMcp: unknown, lockedKeys: ReadonlySet<LockedKey>): void;
|
|
148
|
-
/**
|
|
149
|
-
* [ref](core 5.60.0 提货批件②)—— 请求体里 `mcpServers[].contentOrigin` 的**值域门**,腿首同步拒。
|
|
150
|
-
*
|
|
151
|
-
* ## 先把 core 的硬条款抄下来,因为本腿的全部争议都在它上面
|
|
152
|
-
* `McpServerSpec.contentOrigin` 顶注逐字:「TRUST SOURCE — a DEPLOYMENT-plane key … any assembly layer
|
|
153
|
-
* that accepts REQUEST-side MCP entries **must reject or strip this key** from them … A single-tenant
|
|
154
|
-
* superuser surface (a host reading its own `--mcp-config` file) **IS the deployment plane and needs no
|
|
155
|
-
* such gate**.」
|
|
156
|
-
*
|
|
157
|
-
* ## 本仓怎么兑现这条义务(**结构性**,不是靠纪律)
|
|
158
|
-
* 请求腿的条目只有在 {@link mcpInjectionHonored} 为真时才被读进 `TaskSpec.mcp`,而那只谓词的语义正是
|
|
159
|
-
* 「本部署是单用户 ⇒ 请求者就是这台 worker 的超管」—— core 点名豁免的那一格。多租户部署
|
|
160
|
-
* (`requirePrincipal===true`)整只 `mcpServers` 被忽略,租户**结构上**够不着这个键(钉见
|
|
161
|
-
* `test/task-mcp.test.ts` 的多租户格)。同一条腿今天已经透传 `toolAxes`(喂**审批门**,权限比本键**高**
|
|
162
|
-
* 一档 —— core 明写 contentOrigin「no gate/policy/roster effect」)与 stdio 的**任意命令**(worker 宿主
|
|
163
|
-
* 上的真无限制执行);在那两样之后,「我声明我自己这台 server 的内容不算外来」不构成任何新的暴露面。
|
|
164
|
-
* 名字撞车时 baseline 恒赢(`mergeRequestMcp`)⇒ 调用方也改写不了一台**已配置** server 的声明。
|
|
165
|
-
*
|
|
166
|
-
* ## 为什么 fresh 腿是 400 而不是静默丢键
|
|
167
|
-
* 静默丢键会把一次**信任边界声明**变成探针 —— 调用方以为声明生效了,其实没有。本仓 `opts.leg` 的成文
|
|
168
|
-
* 契约(`ServiceDeps.resolveSpec` 顶注)逐字写着 fresh 腿「may fail-loud 400 on combos that would
|
|
169
|
-
* silently discard caller payload」,这正是那种 combo。
|
|
170
|
-
*
|
|
171
|
-
* ## 🔴 但只在 fresh 腿(与 {@link assertRequestMcpUnlocked} **刻意分歧**)
|
|
172
|
-
* 锁那条敢在 resume 腿同拒,是因为它有一个本门**没有**的前提:锁在场 ⇒ core 的 preflight 无论如何都会
|
|
173
|
-
* 拒这条腿,放进去只换来一个跑起来又终态失败的任务。而坏 `contentOrigin` 在本仓两个平面都被**降级**掉了
|
|
174
|
-
* (`normalizeServer` / `resolveMcpServers`),core 根本看不到它,也就永远不会拒。
|
|
175
|
-
* 与此同时 `checkpoint_ctx` 存的是**原始** `TaskRequestBody`,耐久 resume 逐字重放它,从本门逃出去的
|
|
176
|
-
* `HttpError` 会被 `src/http/server.ts` 折成 `409 resume_blocked_by_policy` ⇒ 一条**升级前** park、body 里
|
|
177
|
-
* 带着当时被静默忽略的坏值的任务,升级后**永久**恢复不了(codex 交叉复审 R1-[high],验真后采纳)。
|
|
178
|
-
* ⇒ 照本仓成文的三处 `opts.leg` 先例(`appendSystemPrompt` 案 / `@model` 名单 [ref] 终裁 (a) /
|
|
179
|
-
* `compactionModel` 案,都逐字写着「砖死 resume 比降级糟」):fresh fail-loud,resume 降级 + 留声
|
|
180
|
-
* (留声点 = {@link resolveRequestMcp} 的 `mcp_content_class_dropped`)。挂点判据在 `boot/resolve-spec.ts`。
|
|
181
|
-
*
|
|
182
|
-
* ## 位置与射程
|
|
183
|
-
* 排在腿首、`assertRequestMcpUnlocked` 旁边 —— **任何副作用之前**(附件绑定 / per-session cwd·env 写入;
|
|
184
|
-
* 理由逐字见 `boot/resolve-spec.ts` 那两行的注)。判据是**纯值域**、与**租户车道**无关:一个写坏的请求
|
|
185
|
-
* 在哪条车道上都是写坏的,而多租户腿本来就要忽略整只数组,告诉调用方「你这个键写错了」只多不少。
|
|
186
|
-
* 形不合(非数组 / 条目非对象)**不归本门**——那是 `validateRequestMcp` 的既有宽容形,本门不抢裁决。
|
|
187
|
-
*/
|
|
188
|
-
export declare function assertRequestMcpContentOrigin(bodyMcp: unknown): void;
|
|
189
|
-
/**
|
|
190
|
-
* S-1515 —— `mcpServers[].transport`(http)上两个身份头的**值域门**(fresh 腿限定,同步 400):`principalHeader` 与 `assertionHeader`
|
|
191
|
-
* **同设**(`request.body_shape`),或 `assertionHeader` 在场而**不是字符串**(`request.field_invalid`)。
|
|
192
|
-
*
|
|
193
|
-
* 前者:两个键回答同一个问题(「这台 http 服务器的请求上用哪个头送身份」),同设 = 两个答案。settings 面由契约包在解析期判拒;
|
|
194
|
-
* 请求面在这里判拒 —— **同一条规则、两处执行者**,判据与拒句单点在 `mcp-assertion-header.ts`(对契约包原句逐字,差分钉守)。
|
|
195
|
-
* 后者:契约包的 zod 类型拒它;请求面若把它读成"没写过",一条鉴权声明就无声消失、server 却照常挂上去连。
|
|
196
|
-
*
|
|
197
|
-
* ## 位置与 leg 分流(与 {@link assertRequestMcpContentOrigin} 同款理由,不发明第二套)
|
|
198
|
-
* 排在腿首、任何副作用之前;**只在 fresh 腿**:`checkpoint_ctx` 存的是原始 `TaskRequestBody`,耐久 resume 逐字重放它,
|
|
199
|
-
* 从这里逃出去的 `HttpError` 会被折成 `409 resume_blocked_by_policy`,把一条 park 任务永久砖死。resume 腿走读器
|
|
200
|
-
* ({@link normalizeServer}):**降级到升级前的读法**(server 照挂、`principalHeader` 胜、`assertionHeader` 不生效)+ `classifyHonored` 一行 warn ——
|
|
201
|
-
* 不拒、也不摘 server。判据是纯值域、与租户车道无关(多租户腿本来就忽略整只数组,告诉调用方「你这条写错了」只多不少)。
|
|
202
|
-
*/
|
|
203
|
-
export declare function assertRequestMcpIdentityHeaders(bodyMcp: unknown): void;
|
|
204
152
|
/** 一条**原始**请求条目上两个身份头的问题类(没有问题 / 不是对象 / 不是 http 传输 ⇒ `undefined`)—— 提交腿的门、探测口、重放腿的披露三处共读,
|
|
205
153
|
* 判据本体在 `mcp-assertion-header.ts`;这里只负责把「条目 → 传输对象」那一步读出来。 */
|
|
206
154
|
export declare function requestEntryIdentityIssue(entry: unknown): McpIdentityHeadersIssue | undefined;
|
|
207
|
-
/** 问题类 → wire 拒面(码 + 句子):两扇拒绝门(提交腿 / 探测口)
|
|
208
|
-
*
|
|
209
|
-
export declare function requestEntryIdentityRefusal(entry: unknown): {
|
|
155
|
+
/** 问题类 → wire 拒面(码 + 句子):两扇拒绝门(提交腿 / 探测口)同一份。`both_set` 是键**组合**不合法(`request.body_shape`),句子对契约包解析期原句逐字,条目名过唯一的回显清洗
|
|
156
|
+
* (剥控制 / 双向字符 + 截长;读不出名字的条目用与丢弃点同一个占位);`assertionHeader` 非串是该键的值不合法,与其余所有受理键同一个拒形({@link entryFieldRefusal},点名 `<at>.transport.assertionHeader`)。 */
|
|
157
|
+
export declare function requestEntryIdentityRefusal(entry: unknown, at: string): {
|
|
210
158
|
errorCode: "request.body_shape" | "request.field_invalid";
|
|
211
159
|
message: string;
|
|
212
160
|
} | undefined;
|
|
213
161
|
/**
|
|
214
|
-
* S-
|
|
215
|
-
*
|
|
216
|
-
*
|
|
217
|
-
* ##
|
|
218
|
-
*
|
|
219
|
-
*
|
|
220
|
-
*
|
|
221
|
-
*
|
|
222
|
-
*
|
|
223
|
-
*
|
|
224
|
-
*
|
|
225
|
-
*
|
|
226
|
-
*
|
|
227
|
-
*
|
|
162
|
+
* S-1629 —— 请求体 `mcpServers[i]` **每个受理键**的值域门(fresh 腿限定,同步 400):**一条律**,取代此前按键各开一扇的三道门
|
|
163
|
+
* (`contentOrigin` / 身份头 / `toolAxes`)与其余键上「整条悄悄摘掉」「悄悄丢键」两种沉默。
|
|
164
|
+
*
|
|
165
|
+
* ## 律
|
|
166
|
+
* fresh 提交腿上,条目的**任一受理键**写坏 ⇒ `400 request.field_invalid`,拒句点名 `mcpServers[i].<键路径>`、server 名与**该键判官的原因句**。
|
|
167
|
+
* 不挂一半、不丢声明、不默认:调用方是请求体作者,同一扇门对同一类错误只有一种答案。修前同一条条目上有三种(`toolAxes` / `contentOrigin` 坏 ⇒ 400;
|
|
168
|
+
* `allowTools` / `toolFaces` / `name` / `transport` 坏 ⇒ 200 + 整条 server 不进 + 运维 warn;`elicitation` / `source` / `transport.{args,env,headers,principalHeader}`
|
|
169
|
+
* 坏 ⇒ 200 + 悄悄丢键),而「两个键方向相反所以处置不同」只是读器**对存量 body** 的取舍({@link normalizeServer}),不是给在场调用方的答复该有的差别:
|
|
170
|
+
* 对一个 fresh 调用方,「收窄声明没生效」与「放宽声明没生效」同样是一份没有生效的声明,响亮拒比两种沉默都好。
|
|
171
|
+
*
|
|
172
|
+
* ## 每个键一只判官({@link ENTRY_KEY_JUDGES},表的键集两向钉在 `MCP_SERVER_ENTRY_KEYS` 上:新增受理键而没写判官 ⇒ 编译红)
|
|
173
|
+
* 判官**就是读器用的那一只**,不是第二份:`toolAxes` = 引擎根导出的 `readMcpToolAxes`;`toolFaces` = `readMcpToolFaces`;`contentOrigin` = `isContentOriginWord`;
|
|
174
|
+
* `source` = `isMcpSourceLabel`;`name` = `isUsableWireName`;`allowTools` / `transport.args` = {@link isStringArray};`transport.env` / `headers` = {@link isStringRecord};
|
|
175
|
+
* `elicitation` = {@link isElicitationFlag};`transport` 的硬字段(`kind` / `command` / `url`)与软字段(`args` / `env` / `headers` / `principalHeader`)= {@link readRequestTransport}
|
|
176
|
+
* (读器投影 transport 的那一只函数,门读它报的 `lost`)。两个身份头的**键组合**(同设 / `assertionHeader` 非串)仍归 `mcp-assertion-header.ts` 的判官({@link requestEntryIdentityRefusal},
|
|
177
|
+
* 码 `request.body_shape` 的那一句对契约包原句逐字),在逐键之前先判。
|
|
178
|
+
*
|
|
179
|
+
* ## 位置与 leg 分流(沿用本仓成文的 `opts.leg` 姿势,不发明第二套)
|
|
180
|
+
* 排在腿首、`assertRequestMcpUnlocked` 旁边 —— **任何副作用之前**(①阶段会把附件行持久绑定到 session,②阶段会写 per-session 的 cwd/env 表;一个最终 400 的请求若已经改掉这两样,
|
|
181
|
+
* 它就改变了后续被接受的任务会物化什么、在哪执行)。**只在 fresh 腿**:`checkpoint_ctx` 存的是**原始** `TaskRequestBody`,耐久 resume 逐字重放它,从这里逃出去的 `HttpError`
|
|
182
|
+
* 会被 `src/http/server.ts` 折成 `409 resume_blocked_by_policy` ⇒ 一条**升级前** park、body 里带着当时被受理的坏值的任务,升级后永久恢复不了
|
|
183
|
+
* (本仓三处 `opts.leg` 先例 —— `appendSystemPrompt` 案 / `@model` 名单 [ref] 终裁 (a) / `compactionModel` 案 —— 逐字写着「砖死 resume 比降级糟」)。
|
|
184
|
+
* 重放腿走读器({@link normalizeServer}),处置**逐字不变**:坏硬键(`name` / `transport` 硬字段 / `allowTools` / `toolAxes` / `toolFaces`)⇒ 这一条 server 整条不进 + 运维 warn
|
|
185
|
+
* `mcp_injection_dropped` + 用户面通告 `mcp.injection_dropped` 点名 `field`(`toolAxes` 在一次性迁移 M19 的窗开着时先按升级前读法重读一次,过了就挂,见读器的注);坏软键(`contentOrigin` / `elicitation` / `source` / `transport` 软字段)⇒ 丢键、server 照挂(`contentOrigin` 另有 `mcp_content_class_dropped` 留声)。
|
|
186
|
+
*
|
|
187
|
+
* ## 射程
|
|
188
|
+
* 判据是**纯值域**、与租户车道无关:一个写坏的请求在哪条车道上都是写坏的,而多租户腿本来就忽略整只数组,告诉调用方「你这个键写错了」只多不少。
|
|
189
|
+
* 只判前 `MAX_REQUEST_MCP_SERVERS` 条(能被挂载的那一段,与读器同一个切片)。不在本门的:条目本身不是对象 / `mcpServers` 不是数组(`validateRequestMcp` 的既有宽容形)、词表外的键(读器对未知键宽容,探测口才闭集)、超上限的尾项(`over_cap` 点名;它们不挂载,为一个永远不会生效的声明拒掉整份请求是新开一条拒绝面)。
|
|
190
|
+
* 关于 `contentOrigin` 的信任语义(core 把它定性为**部署面**键):请求腿的条目只有在 {@link mcpInjectionHonored} 为真(单用户 = 请求者就是这台 worker 的超管)时才被读进 `TaskSpec.mcp`,
|
|
191
|
+
* 多租户部署整只数组被忽略 ⇒ 租户**结构上**够不着这个键;名字撞车时 baseline 恒赢({@link mergeRequestMcp})⇒ 调用方也改写不了一台**已配置** server 的声明。
|
|
228
192
|
*/
|
|
229
|
-
export declare function
|
|
193
|
+
export declare function assertRequestMcpEntries(bodyMcp: unknown): void;
|
|
230
194
|
/**
|
|
231
|
-
* 一条**原始**请求条目上 `toolAxes` 的拒面(码 + 句子);没有问题 / 条目不是对象 / 没写 `toolAxes` ⇒ `undefined
|
|
232
|
-
*
|
|
233
|
-
*
|
|
195
|
+
* 一条**原始**请求条目上 `toolAxes` 的拒面(码 + 句子);没有问题 / 条目不是对象 / 没写 `toolAxes` ⇒ `undefined`。探测口(`mcp-probe.ts` 的 `entryValueIssue`)在它自己的通用读器之前读这一只,
|
|
196
|
+
* 所以 `toolAxes` 在两扇拒绝门上是**同一句**:判官 = {@link ENTRY_KEY_JUDGES} 的 `toolAxes` 格(引擎的 `readMcpToolAxes`),句子 = {@link entryFieldRefusal}(与 fresh 提交腿的每个键同一个形)。
|
|
197
|
+
* `at` = 条目的键路径(`mcpServers[3]`)。
|
|
234
198
|
*/
|
|
235
199
|
export declare function requestEntryToolAxesRefusal(entry: unknown, at: string): {
|
|
236
200
|
errorCode: "request.field_invalid";
|