@sema-agent/server 7.92.2 → 7.93.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/README.md CHANGED
@@ -221,6 +221,7 @@ One row per endpoint family (not exhaustive):
221
221
  |-----------------|----------------|
222
222
  | `GET /health` · `GET /metrics` | Liveness + Prometheus metrics (`/metrics/summary`, `/metrics/plan-cache`) |
223
223
  | `GET /v1/capabilities` | Deployment capability discovery — what this deployment can actually do, so clients never probe 501s |
224
+ | `GET /v1/capabilities/mcp` · `POST /v1/capabilities/mcp/probe` | Per-server MCP status with **no run required**: dial each declared server, list its tools, close it again — the first for this deployment's own declarations, the second for a caller's `.mcp.json`. Rows are the engine's own wiring-manifest rows, so a one-shot command and an interactive session read the same verdict. Rate-capped per caller and cached briefly (it really dials) |
224
225
  | `GET /v1/models` | Model catalog (names only; no gateway URLs or keys) |
225
226
  | `POST /v1/tasks` · `/v1/tasks/stream` | Synchronous task execution; SSE variant streams typed `TaskEvent`s token by token |
226
227
  | `POST /v1/runs` · `GET /v1/runs/:id` | Asynchronous runs: immediate `202`, background execution, poll for status/result |
package/README.zh-CN.md CHANGED
@@ -191,6 +191,7 @@ Web search(部署 env `WEB_SEARCH_*` 七键、per-request `settings.webSearch`
191
191
  |--------|----------|
192
192
  | `GET /health` · `GET /metrics` | 存活探针 + Prometheus 指标(`/metrics/summary`、`/metrics/plan-cache`) |
193
193
  | `GET /v1/capabilities` | 部署能力发现 —— 本部署真正能做什么,客户端免 501 探测 |
194
+ | `GET /v1/capabilities/mcp` · `POST /v1/capabilities/mcp/probe` | **无需先跑一条 run** 的逐台 MCP 状态:现连、列工具、关掉 —— 前者答本部署自己申报的服务器,后者答调用方自带的 `.mcp.json`。行就是引擎装配名册里的那一行,所以一次性命令与交互会话读到同一个判定。它会真拨号,所以按调用方限速 + 短窗复用 |
194
195
  | `GET /v1/models` | 模型目录(仅名字,不含网关 URL/key) |
195
196
  | `POST /v1/tasks` · `/v1/tasks/stream` | 同步任务执行;SSE 变体逐 token 流式输出类型化 `TaskEvent` |
196
197
  | `POST /v1/runs` · `GET /v1/runs/:id` | 异步 run:立即 `202`,后台续跑,轮询状态/结果 |
@@ -21,7 +21,7 @@
21
21
  * 没有方法、没有捕获的行为。
22
22
  */
23
23
  import { z } from "zod";
24
- import { type AskEvidenceAbsence, type AskRequest as CoreAskRequest, type ReadRootGrantCandidate, type RuleOffer } from "@sema-agent/core";
24
+ import { READ_ROOT_CANDIDATE_DIR_MAX, type AskEvidenceAbsence, type AskRequest as CoreAskRequest, type ReadRootGrantCandidate, type RuleOffer } from "@sema-agent/core";
25
25
  import type { AskRow } from "./plugins/approval-ask-store-sql.js";
26
26
  /** 模型自由文本(`sourceAgentName` / `delegation.agentName`)的限长(设计稿 §6.2)。设计 §3.1 把
27
27
  * 「限长 + 脱敏」写成 **server 新增责任**(引擎无此层):spawning model 挑的名字是自由文本,
@@ -270,15 +270,19 @@ export type ProbeCauseProjection = z.infer<typeof ProbeCauseSchema>;
270
270
  */
271
271
  export declare function readProbeCause(req: unknown): ProbeCauseProjection | undefined;
272
272
  /**
273
- * [ref](core 7.19.0)—— `readRootCandidate.dir` 的**上限**。core 自己的同族界是
274
- * `READ_ROOT_CANDIDATE_DIR_MAX = 1024`(`src/core/read-root-candidate.ts`),**未从包根导出**,故钉本地
275
- * 孪生 + 本出处注记(与 `MAX_SKILL_CONTENT_CHARS` 同款处置)。
273
+ * [ref](core 7.19.0)—— `readRootCandidate.dir` 的**上限**。
274
+ *
275
+ * 🔴 **本地孪生退役,改读 core 的导出**(core 7.26.0 / S-538 ⓐ,[ref] 手抄投影下线):此前这里是
276
+ * `export const MAX_READ_ROOT_CANDIDATE_DIR = 1024`,注里写着「core 的 `READ_ROOT_CANDIDATE_DIR_MAX`
277
+ * **未从包根导出**,故钉本地孪生」—— 那句话在 7.26.0 起是过期的,而一份手抄的界与它的属主分家时**不会红**:
278
+ * core 把界调宽,本仓照旧按旧数扣掉一个 core 认可的目录(卡上少一条出路,而人只会读成「没有出路」)。
279
+ * 旧名整条删、不留别名(硬 breaking 三句:本常量是 server 内部导出,零外部消费者,tsc 红即通知)。
276
280
  *
277
281
  * 🔴 超限的方向是**整只丢,不截** —— core 契约 `shell.read_boundary.grant_candidate_is_the_grant`:卡上
278
282
  * 显示的串就是人要加进读目录的那个串,截过的目录是**另一个**目录,加进去也清不掉这只 ask。与
279
283
  * {@link MAX_PROBE_CAUSE_CODE} 的 `code` 超限整只丢逐字同判据。
280
284
  */
281
- export declare const MAX_READ_ROOT_CANDIDATE_DIR = 1024;
285
+ export { READ_ROOT_CANDIDATE_DIR_MAX };
282
286
  /** 窄读的输出形 —— **就是 core 的那只型**,不在本仓再声明一份同形结构(重声明 = 上游改形那天本仓静默不红)。 */
283
287
  export type ReadRootCandidateProjection = ReadRootGrantCandidate;
284
288
  /**
@@ -287,7 +291,7 @@ export type ReadRootCandidateProjection = ReadRootGrantCandidate;
287
291
  *
288
292
  * 🔴 **零判读**:server 既不从命令文本重推目录、也不校验它是不是真的能清掉这只 ask —— 那是 core 已经
289
293
  * 做过的判断(它把提议的根放回读边界又走了一遍),在下游再判一次就是同一个事实的第二个判官。
290
- * 🔴 形不合 ⇒ **按缺席处置**;超限 ⇒ **整只丢**(见 {@link MAX_READ_ROOT_CANDIDATE_DIR})。
294
+ * 🔴 形不合 ⇒ **按缺席处置**;超限 ⇒ **整只丢**(界见 {@link READ_ROOT_CANDIDATE_DIR_MAX},core 的属主值)。
291
295
  * 🔴 **缺席不是断言**:它同时覆盖「这一族 ask 没有任何目录能清掉」(敏感路径 deny 行、读边界压根读不懂
292
296
  * 的命令、递归遍历、非读边界 ask……)与「老引擎」两形 —— 消费端只读在场,永远不读缺席。
293
297
  *
@@ -814,5 +818,4 @@ export declare function buildApprovalPreambleSseFrames(frames: readonly Approval
814
818
  event: string;
815
819
  data: unknown;
816
820
  }>;
817
- export {};
818
821
  //# sourceMappingURL=approval-card.d.ts.map
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- import { ASK_EVIDENCE_ABSENCE_VALUES, MAX_RULE_TEXT_CHARS } from "@sema-agent/core";
2
+ import { ASK_EVIDENCE_ABSENCE_VALUES, MAX_RULE_TEXT_CHARS, READ_ROOT_CANDIDATE_DIR_MAX } from "@sema-agent/core";
3
3
  import { redactSecrets } from "./trace/redact.js";
4
4
  import { recordFailOpen } from "./observability/fail-open.js";
5
5
  import { PERSISTED_RULE_MATCHES, UNCOVERED_SEGMENT_REASONS } from "./permission-rule-vocab.js";
@@ -160,7 +160,7 @@ export function readProbeCause(req) {
160
160
  ...(raw.further !== undefined ? { further: family(raw.further) } : {}),
161
161
  };
162
162
  }
163
- export const MAX_READ_ROOT_CANDIDATE_DIR = 1024;
163
+ export { READ_ROOT_CANDIDATE_DIR_MAX };
164
164
  const ReadRootCandidateEnvelopeSchema = z.object({
165
165
  readRootCandidate: z.object({ dir: z.string().min(1), clearsThisAsk: z.literal(true), covers: z.literal("exact").optional() }).optional(),
166
166
  });
@@ -169,7 +169,7 @@ export function readReadRootCandidate(req) {
169
169
  const raw = parsed.success ? parsed.data.readRootCandidate : undefined;
170
170
  if (raw === undefined)
171
171
  return undefined;
172
- if (raw.dir.length > MAX_READ_ROOT_CANDIDATE_DIR)
172
+ if (raw.dir.length > READ_ROOT_CANDIDATE_DIR_MAX)
173
173
  return undefined;
174
174
  return { dir: raw.dir, clearsThisAsk: true, ...(raw.covers !== undefined ? { covers: raw.covers } : {}) };
175
175
  }
@@ -296,6 +296,7 @@ export function createResolveSpec(ctx) {
296
296
  ...(effMode ? { shellGate: shellGateForMode(effMode) } : {}),
297
297
  ...((f) => (f !== undefined ? { writeFace: f } : {}))(effMode !== undefined ? writeFaceForMode(effMode) : undefined),
298
298
  ...((f) => (f !== undefined ? { autoModeRequested: f } : {}))(effMode !== undefined ? autoModeRequestedForMode(effMode) : undefined),
299
+ ...(body.approverPosture !== undefined ? { approverPosture: body.approverPosture } : {}),
299
300
  selfOrchestration: selfOrchestrationFromBody({ selfOrchestration: body.selfOrchestration === true || parsedSettings.settings?.ultracode === true }, config, Boolean(centerRuntimeCapsResolver)),
300
301
  forwardSubagentEvents: body.forwardSubagentEvents === true ? true : undefined,
301
302
  retainSubagentSessions: normalizeRetainSubagentSessions(body.retainSubagentSessions),
@@ -0,0 +1,290 @@
1
+ /**
2
+ * S-481 —— **无 run 可读的逐台 MCP 状态面**的 server 半场(设计稿 `2026-09-19-s481-mcp-status-face-v1`)。
3
+ *
4
+ * ## 病(deploy 报案 I-1 / cli L-303)
5
+ * 壳的一次性命令(`sema mcp list`)与交互会话对同一台 MCP 服务器给出**相反**的答案:命令行报 Failed、
6
+ * 会话里那台 server 的工具却能用。根因不是判定不同,而是**没有面** —— 一个从未起过 run 的会话在本服务上
7
+ * 只有 `GET /v1/capabilities.mcp` 那一枚布尔,壳只能自己去拨号、自己造词。
8
+ *
9
+ * ## 这里立的两条面(词表与行形与 run 完全同源)
10
+ * · `GET /v1/capabilities/mcp` —— 本部署**中心配置**的逐台状态;
11
+ * · `POST /v1/capabilities/mcp/probe` —— 探测**调用方自带**的规格(壳的 `.mcp.json`)。
12
+ *
13
+ * ## 🔴 一条铸点纪律:**本模块一行都不拼**
14
+ * 行 = core 7.26.0 `probeMcpServers` 的 `entries`,那是 `mcpManifestEntries` 的产物,**逐字**等于一条
15
+ * 准备好的腿推上 `wiring_manifest.mcp[]` 的那些行(core `@contract` 原话:same projection, same failure
16
+ * vocabulary, same neutralized and bounded remote text)。server 若自己按 `McpServerStatus` 拼一份,那就是
17
+ * 同一个语义面上的**第二份**铸点 —— 它与引擎面的漂移没有任何门看得见(既有 `GET /v1/sessions/:id/mcp`
18
+ * 面板正是那一形,它的行是手拼的;本面刻意不沿用它,理由记在 DEBTS 而不是在这里再复制一遍)。
19
+ *
20
+ * ## 亲核到的 core 签名(7.26.0 `dist/core/mcp-probe.d.ts`,**以 d.ts 为准**)
21
+ * `probeMcpServers(specs: readonly McpServerSpec[], opts?: McpProbeOptions): Promise<McpProbeResult>`
22
+ * 设计小稿写的是 `probeMcpServers(specs, principal, opts)` —— **d.ts 赢**:`principal` 在 `opts` 里,不是
23
+ * 第二个位置参。`{ entries, servers }` **恒与 `specs` 同序同长**(`@contract mcp.probe.index_aligned`),
24
+ * 失败台占位带 `errorCode` ⇒ 消费方按**下标**对拍,不按 `name`(名不保证唯一)。
25
+ *
26
+ * ## 三问(behavior-facing,契约与 DEPLOY-PREREQS 同文)
27
+ * · **谁需要**:壳的一次性命令与交互车道要同源(同一台 server、同一套词表、同一个判定);
28
+ * · **谁受伤**:探测是**真拨号**(HTTP 连接 / stdio 起子进程),被滥用就是探测风暴与子进程堆积;
29
+ * · **什么补偿**:每 principal 固定限速({@link MCP_PROBE_RATE_POLICY})+ 同规格 30s 有界缓存
30
+ * ({@link MCP_PROBE_CACHE_TTL_MS} / {@link MCP_PROBE_CACHE_MAX_ENTRIES},single-flight)+ 每台裁决有上界
31
+ * ({@link MCP_PROBE_VERDICT_TIMEOUT_MS})+ 自带规格那一口整条被 {@link mcpInjectionHonored} 挡在多租户
32
+ * 之外。**如实交代**:core 的 `timeoutMs` 界的是**裁决**不是拨号,而一个答完握手就卡住 SEND 的对端
33
+ * 在 core 那边没有任何期限(core `docs/KNOWN-LIMITS.md`)—— 那一形上本面同样没有期限,补偿只有限速与缓存。
34
+ */
35
+ import { type McpServerSpec } from "@sema-agent/core";
36
+ /**
37
+ * 本面的**拒码闭集**(设计稿 §2 的四枚)。闭集写成型,是为了让 {@link MCP_PROBE_HTTP_STATUS} 那张表
38
+ * 「漏一枚 = 编译红」—— [ref] 的安全轴纪律:词表是闭集,未处置的成员必须是编译错误而不是运行期惊喜。
39
+ */
40
+ export declare const MCP_PROBE_REFUSAL_CODES: readonly ["capability.mcp_injection_required", "limit.rate_exceeded", "request.body_shape", "request.field_invalid", "state.mcp_probe_incomplete"];
41
+ export type McpProbeRefusalCode = (typeof MCP_PROBE_REFUSAL_CODES)[number];
42
+ /**
43
+ * 🔴 **状态档取单表**(`GOVERNANCE_HTTP_STATUS` 的同族形,S-201② 的那条律):一个码在本服务上只有**一个**
44
+ * 状态,而路由层**一个字面量都不许手写**。写成 `as const satisfies` 两头都占:格是字面量型(直接当
45
+ * `sendError` 的状态参数用,零 `?? 400` 兜底 —— 兜底就是第二份真源),`satisfies` 保住闭集门。
46
+ */
47
+ export declare const MCP_PROBE_HTTP_STATUS: {
48
+ readonly "capability.mcp_injection_required": 501;
49
+ readonly "limit.rate_exceeded": 429;
50
+ readonly "request.body_shape": 400;
51
+ readonly "request.field_invalid": 400;
52
+ readonly "state.mcp_probe_incomplete": 503;
53
+ };
54
+ /**
55
+ * 每 principal 的固定窗限速。**刻意不挂任何无关旋钮**(S-470 的同一条教训,`DEVICE_ENROLL_RATE_POLICY`
56
+ * 先例):全局 `RATE_LIMIT_RPM` 缺省是 `0` = 关断,把一条真拨号的面挂在它上面 = 出厂形不限速。
57
+ * 12/60s 是**安全地板**,不是可调参数;窗是**每副本**的(多副本部署上有效上界 = 12 × 副本数,如实记)。
58
+ */
59
+ export declare const MCP_PROBE_RATE_POLICY: {
60
+ readonly limit: 12;
61
+ readonly windowMs: 60000;
62
+ };
63
+ /** 同 principal 同规格的结果复用窗(设计稿 §3)。命中**不计**限速、**回原 `probedAt` 时刻**。 */
64
+ export declare const MCP_PROBE_CACHE_TTL_MS = 30000;
65
+ /** **已落地**结果的条数硬帽 —— 一只缓存自己绝不能变成资源耗尽点(`createFixedWindowRateLimiter` 同一条有界形)。 */
66
+ export declare const MCP_PROBE_CACHE_MAX_ENTRIES = 256;
67
+ /**
68
+ * **同时在跑**的走查条数硬帽(codex r1 [high] 的第二半)。
69
+ *
70
+ * 为什么它必须是一道**拒**、不能靠逐出:在途的那一格**就是** single-flight 本身,把它从表里逐出去等于
71
+ * 撤掉这条面唯一的「同一份申报只拨一次」保证 —— 而那正是这道帽本来要保护的资源(连接 / 子进程)。
72
+ * ⇒ 帽满时**响亮拒**(429 + 退避提示),让调用方稍后再问,而不是悄悄多开一次拨号。
73
+ * 数字取限速额度的量级(12/60s/身份):在一台健康的部署上一次走查以毫秒~秒计,这道帽结构上摸不到;
74
+ * 摸到它就说明这台机器上真的有一批拨号卡着,而那时候**少开**才是对的方向。
75
+ */
76
+ export declare const MCP_PROBE_MAX_INFLIGHT = 32;
77
+ /**
78
+ * 每台**裁决**上界,喂 core 的 `opts.timeoutMs`。
79
+ *
80
+ * 🔴 **不是新旋钮**:没有 env、不进 config-catalog、部署方改不了它 —— 「不新铸旋钮」这条要求管的是
81
+ * **旋钮**,不是常量。数值与既有 `GET /v1/sessions/:id/mcp` 面板的物化上界同阶(那边 10s),而**名字刻意
82
+ * 独立**:本仓 `sessions.ts` 的四枚同族时限逐字写着理由 ——「两条腿的物化成本不同族,将来任一侧调窗时不该
83
+ * 被另一侧的名字绑架」。那边界的是**整次物化**,这边界的是**每台的裁决**,连语义都不是一回事。
84
+ *
85
+ * ⚠️ core 的硬条款(d.ts 逐字):`timeoutMs` 界**裁决**不界**拨号** —— 被时钟放弃的那次拨号仍在跑,
86
+ * core 在它落地时才 dispose。所以本面的「探测完零遗留」有一个**残留上界**,不是「立刻」:见 {@link MCP_PROBE_RESIDUE_SLACK_MS}。
87
+ */
88
+ export declare const MCP_PROBE_VERDICT_TIMEOUT_MS = 10000;
89
+ /**
90
+ * **整次走查**的上界(本面自己的钟,不是 core 的)—— **按申报条数派生**,不是一个魔法常量。
91
+ *
92
+ * 🔴 为什么非有这口钟不可(异源复核 [high],亲读 core d.ts 后确认):core 的 `timeoutMs` 界的是**每台的裁决**,
93
+ * 而整次调用**不会**在最后一次拨号落地之前 resolve —— 而「答完握手就卡住发送」的那一形在 core 那边
94
+ * **没有任何期限**(`docs/KNOWN-LIMITS.md`)。没有本钟时:那只 promise 永不落地 ⇒ 它永久占住一个缓存格与
95
+ * 一个在途位,而每一个后来问同一份申报的请求都会 join 上它、跟着一起永久挂住。
96
+ *
97
+ * 🔴 为什么**派生**而不是钉一个数:钉死的那个数必须照最大申报(32 台)取,于是一次**只问一台**的请求要
98
+ * 白等 60s 才被告知「答不出来」—— 一个与它问的东西无关的宽限。派生式 = `ceil(n / 并发) × 每台裁决 + 收尸阶梯`,
99
+ * 与 core 自己成文的走查时长公式**逐字同形**(d.ts:"its own duration is those per-dial times summed over
100
+ * `ceil(n / concurrency)` rounds"),所以在一台**对端会答或会失败**的部署上这口钟结构上摸不到 —— 摸到它
101
+ * 就是真有一台对端在卡发送。读数:1 台 ⇒ 14s;32 台 ⇒ 44s。
102
+ * 兄弟先例:`GET /v1/sessions/:id/mcp` 面板的 `MCP_MATERIALIZE_TIMEOUT_MS`(那边界的也是整次物化,但它是
103
+ * 一个定值;本面的申报条数由调用方决定,所以定值在这里就是那个「与所问无关的宽限」)。
104
+ * **纯数据** ⇒ `build*`。
105
+ */
106
+ export declare function buildMcpProbeWalkTimeoutMs(declarations: number): number;
107
+ /**
108
+ * 一次走查里**同时追裁决**的台数,喂 core 的 `opts.concurrency`。
109
+ *
110
+ * 为什么不用缺省的 1:core 的 d.ts 明写整次调用的时长 = 每台时长在 `ceil(n / concurrency)` 轮上的和。
111
+ * 缺省 1 下一个 32 台(={@link MAX_REQUEST_MCP_SERVERS})的申报最坏是 32 × {@link MCP_PROBE_VERDICT_TIMEOUT_MS}
112
+ * —— 一条 HTTP 请求挂五分钟。8 把最坏收进 4 轮。代价 core 也写明了:并发只影响 `connectMs` 这个读数的
113
+ * 争用度,**行与下标对齐不受影响**(而本面根本不发 `connectMs`)。
114
+ * ⚠️ 它**不是**「同时开几条传输」的帽:被时钟放弃的拨号仍在飞,最坏可达一条/台申报(core 原话)。
115
+ */
116
+ export declare const MCP_PROBE_CONCURRENCY = 8;
117
+ /**
118
+ * 「探测完子进程零遗留」的**残留上界**(判据 G6 用它,**不自定**)。
119
+ *
120
+ * 逐字抄 core 7.26.0 CHANGELOG 的「界」句与 `mcp-probe.d.ts` 的 `@contract mcp.probe.zero_residue_bound`:
121
+ * *the stdio close ladder then adds ≤ 4 000 ms (SIGTERM, then SIGKILL)*,整句是
122
+ * *that dial's residue is gone within `max(timeoutMs, the handshake budget + the listing budget) + 4 000 ms`*。
123
+ * 4 000 就是那个 `+ 4 000 ms`;同一句话的另一半 —— *a server that exits when its input closes is gone at
124
+ * return* —— 是规矩服务器的那一形(G6 的正控钉在这一半上)。
125
+ */
126
+ export declare const MCP_PROBE_RESIDUE_SLACK_MS = 4000;
127
+ /** 观测计数的 `outcome` 闭集(`mcp_probe_total{outcome}`)。 */
128
+ export declare const MCP_PROBE_OUTCOMES: readonly ["probed", "cached", "empty", "gate_closed", "rate_limited", "bad_request", "failed"];
129
+ export type McpProbeOutcome = (typeof MCP_PROBE_OUTCOMES)[number];
130
+ /** 两条面的**同一个**体形(设计稿 §2:`POST` 回「200 同形体」)。行**只**来自 core 的 `entries`。 */
131
+ export interface McpProbeFaceBody {
132
+ /** 这份读数是**哪一刻**的(缓存命中回原时刻 —— 一个复用的答案假装自己是新的,就是一条假事实)。 */
133
+ readonly probedAt: string;
134
+ /** 这份读数还能复用多久(秒)= {@link MCP_PROBE_CACHE_TTL_MS}。 */
135
+ readonly ttlSec: number;
136
+ /**
137
+ * 逐字 = `wiring_manifest.mcp[]` 在 wire 上的那一行 —— **同一只投影**
138
+ * ({@link projectWiringManifestMcpRows},契约 §G.7),不是本面自己挑的一份键。与入参申报**同序同长**
139
+ * (core `@contract mcp.probe.index_aligned`;下标对齐,不按 name)。
140
+ *
141
+ * 🔴 **`error`(远端原话)因此在本面上同样缺席**,而这**不是**本车的选择:§G.7 第 3 条是一条成文裁定
142
+ * —— 那一格是这帧上唯一的远端作者自由文本,core 7.5.0 [ref] 已把 MCP 远端错误文本的脱敏收敛到**一个**
143
+ * 铸点,server 在读面再脱一遍就是同一语义面的第二个写者(两遍脱敏的重叠通常比原缺陷更坏且静默)。
144
+ * 本车的 G7 当场量到了这条裁定的分量:不走这只投影时,一条 `http://user:pw@host` 形申报的**用户名**
145
+ * 会逐字上 wire(core 的中立化只遮口令位)。可操作的因由在 `errorCode` 闭集上;要远端原话得先走
146
+ * §G.7 第 3 条那条「带触发条件的裁定」。
147
+ */
148
+ readonly servers: readonly Record<string, unknown>[];
149
+ }
150
+ /** 一次拒绝的**结构性**产物:码 + 句 + 可机读的键名表(消费端不必解析文案)。状态由 {@link MCP_PROBE_HTTP_STATUS} 单表答。 */
151
+ export interface McpProbeRefusal {
152
+ /** 🔴 键名是 `errorCode`,**不是** `code`:3.0.0 起 wire 错误体只有一个机器判别键,`test/error-code-key-gate.test.ts`
153
+ * 连「错误体附近的对象字面量里出现 `code:`」都拦(本批红先实证:第一版写成 `code` 当场被那道门逮住)。 */
154
+ readonly errorCode: McpProbeRefusalCode;
155
+ readonly message: string;
156
+ /** `request.body_shape` 专用:词表外 / 本面不接线的键,全路径形(已清洗+截断+排序+封顶)。 */
157
+ readonly unknownKeys?: readonly string[];
158
+ }
159
+ /**
160
+ * `POST …/probe` 的体读器:闭形、逐条点名、零静默丢。成功 ⇒ 交给 core 的 `specs`(**原样**转发被读出来的
161
+ * 条目,`McpServerSpec` 的可选键一个都不重写 —— 重写就是在 core 的型上做第二次投影)。
162
+ */
163
+ export declare function readMcpProbeBody(raw: unknown): {
164
+ ok: true;
165
+ specs: McpServerSpec[];
166
+ } | {
167
+ ok: false;
168
+ refusal: McpProbeRefusal;
169
+ };
170
+ /**
171
+ * 本面**唯一**的调用方身份归一(异源复核 [low]:此前缓存键用 `principal ?? ""`、限速键用 `… : "local"`
172
+ * —— 两份归一,于是一个 principal **字面就叫 `local`** 的调用方与「无 principal」共用同一个限速窗,
173
+ * 而缓存键又不撞。⇒ 两条键从此读同一只函数)。
174
+ *
175
+ * 无 principal(单用户部署上恒如此)折成 `local`:那台机器上只有一个人。
176
+ */
177
+ export declare function buildMcpProbeIdentity(principal: string | undefined): string;
178
+ /** 缓存键 = `(身份, sha256(规范化 specs))`。**纯数据** ⇒ `build*`。 */
179
+ export declare function buildMcpProbeCacheKey(principal: string | undefined, specs: readonly McpServerSpec[]): string;
180
+ /** 限速键(设计稿 §3:`mcp-probe:<身份>`)。**纯数据** ⇒ `build*`。 */
181
+ export declare function buildMcpProbeRateKey(principal: string | undefined): string;
182
+ /** 本域独占的运行期状态(每 server 实例一份 —— 模块级会把缓存与限速窗跨实例串味)。 */
183
+ export interface McpProbeLocal {
184
+ /**
185
+ * 固定窗限速({@link MCP_PROBE_RATE_POLICY});键由 {@link buildMcpProbeRateKey} 铸。
186
+ *
187
+ * 🔴 实现**不在本文件**:它是既有的 {@link createFixedWindowRateLimiter}(device 注册口那一只)。
188
+ * 异源复核 [high] 逐行对出本文件此前手抄了同一套算法(同一份 `{startedAtMs,count}`、同一个
189
+ * `MAX_WINDOWS = 10_000`、同一句「过期先清、仍超清最旧一半」、同一个 `retryAfterSec` 算式),而注里
190
+ * 还**引着**那只函数的名字 —— 一条规则两份实现、没有任何编译期连接,正是 [ref] 的第一排查点。
191
+ */
192
+ check(key: string): Promise<{
193
+ allowed: boolean;
194
+ retryAfterSec?: number;
195
+ }>;
196
+ /**
197
+ * single-flight + TTL 的结果表;命中回**同一只** promise(并发首拨塌成一次走查)。
198
+ *
199
+ * 🔴 **两条判据,不是一条**(codex 对抗复审 r1 [high],亲跑复现后按类修):
200
+ * · 这一格**还在跑** ⇒ 恒命中,**与年龄无关** —— 它就是 single-flight 本身;
201
+ * · 这一格**已落地** ⇒ 从**落地那一刻**起算 {@link MCP_PROBE_CACHE_TTL_MS}。
202
+ * 修前的起点是**拨号开始**那一刻,于是一次跑得比窗口长的走查(慢服务器 / 卡住的对端)30s 之后从表里
203
+ * 消失而它还在跑 ⇒ 同一份申报的下一次请求另起一次走查,single-flight 恰好在最该生效的那一形上失效,
204
+ * 连接与子进程按限速额度叠加。红先读数(G5-bis):一台答完握手就不再答列表的 stdio 服务器,两次请求
205
+ * 之间跨过窗口 ⇒ **两个**子进程。
206
+ */
207
+ cached(key: string, now: number): Promise<McpProbeFaceBody> | undefined;
208
+ /**
209
+ * 登记一次走查。🔴 **格的寿命由 `walk.dials` 驱动,不由 `walk.answer` 驱动**(合并树 codex ① [high]):
210
+ * · `dials` 还没落地 ⇒ 这一格**在途**:恒命中(同规格不再拨号)、不计复用窗、算一个在途位;
211
+ * · `dials` **resolve** ⇒ 从那一刻起算 {@link MCP_PROBE_CACHE_TTL_MS}(正常的结果复用);
212
+ * · `dials` **reject** ⇒ 整格丢掉(下一次请求重新拨,而不是被一条缓存起来的失败钉住半分钟)。
213
+ * 应答面(`answer`)可能**早于** `dials` 就有结论(本面的期限到点)—— 那一段窗口里这一格仍然是「在途」,
214
+ * 后来者拿到的是**同一只** `answer`(立刻那句 503),资源计数也仍然算着它。
215
+ */
216
+ remember(key: string, at: number, walk: McpProbeWalk, now?: () => number): void;
217
+ /** 缓存里现在有几条(判据面读它,不去摸内部表)。 */
218
+ size(): number;
219
+ /** 现在有几次走查**还在跑**(帽 = {@link MCP_PROBE_MAX_INFLIGHT};判据面读它,不去摸内部表)。 */
220
+ inflight(): number;
221
+ }
222
+ /** 带行为的东西 ⇒ `create*`(工厂命名律:`build*` 出纯数据,`create*` 出活对象)。 */
223
+ export declare function createMcpProbeLocal(): McpProbeLocal;
224
+ /**
225
+ * 一次走查。**两只 promise,两件事**(合并树 codex ① [high],亲跑复现后按类修):
226
+ * · `answer` —— **HTTP 应答面**:到本面的期限就 reject {@link McpProbeIncomplete},调用腿据它答 503;
227
+ * · `dials` —— **拨号生命周期**:core 真的把它开过的每一条都 dispose 完了才 settle。
228
+ *
229
+ * 🔴 为什么必须分家(修前是同一只):core 的拨号**不收取消**、要等 dial 落地才 dispose,所以「本面不再等它」
230
+ * 与「那条连接已经没了」是**两个时刻**。修前两者共用一只 promise ⇒ 超时那一拍就把缓存格与在途计数一起释放:
231
+ * ① 那条**仍然开着**的拨号不再计入在途帽,② 同规格重试会**重新拨一次**。对一个握手后卡 SEND 的对端,
232
+ * 限速只能降累积速度、限不住资源**总量** —— 红先实测:反复问同一份申报,对端存活连接从 1 涨到 6。
233
+ * 分家之后:在途格与资源计数**直到 `dials` 落地才释放**,期间同规格请求挂在**同一只** `answer` 上
234
+ * (立刻拿到那句 503,不再拨号)。
235
+ *
236
+ * `entries` 一个字都不改写(不排序、不过滤、不补键):它与入参 `specs` 同序同长是 core 的成文契约,
237
+ * 消费方按**下标**对拍自己的申报表。**带行为** ⇒ `create*`。
238
+ */
239
+ export interface McpProbeWalk {
240
+ readonly answer: Promise<McpProbeFaceBody>;
241
+ readonly dials: Promise<unknown>;
242
+ }
243
+ export declare function createMcpProbeWalk(specs: readonly McpServerSpec[], principal: string | undefined, now?: () => number): McpProbeWalk;
244
+ /** 整次走查没在 {@link MCP_PROBE_WALK_TIMEOUT_MS} 内落地。**具名型**而不是一个字符串比较:调用腿要据它
245
+ * 分诊(503 + 退避),而按错误文案分诊是本仓明令不许的形。 */
246
+ export declare class McpProbeIncomplete extends Error {
247
+ readonly boundMs: number;
248
+ constructor(boundMs: number);
249
+ }
250
+ /** 整次走查超界那一句。**不是**「探测失败」——它说的是「这台机器现在答不出来,稍后可能就能」。 */
251
+ export declare function buildMcpProbeIncompleteRefusal(boundMs: number): McpProbeRefusal;
252
+ /** 零申报时的诚实空面(**不**进缓存、**不**占限速:一次没有拨号的回答不该消耗任何配额)。 */
253
+ export declare function buildEmptyMcpProbeFace(now?: number): McpProbeFaceBody;
254
+ /**
255
+ * {@link mcpInjectionHonored} 为假时那一句 —— **三条否决各自点名**(多租户 / 锁 / 合规档位),因为调用方
256
+ * 的下一步动作三条各不相同,而一句「not honored」把三件事压成一件。码只有一枚:消费端的分支是同一个
257
+ * (换部署形态 / 改旋钮,而不是重试)。
258
+ */
259
+ export declare function buildMcpInjectionRefusal(): McpProbeRefusal;
260
+ /**
261
+ * 限速那一句。与上面三只同住一个模块,是本面**四句 wire 文案的唯一属主** —— 一条面的措辞散在路由体里,
262
+ * 下一个人改一个字时没有任何地方会红。
263
+ *
264
+ * ⚠️ **如实交代一条覆盖边界**:全仓的 `api-error-text-freeze` 门锚的是 `sendError(res, <字面三位数>,
265
+ * "<字面码>", …)`,而本面刻意**不在调用点写字面状态**(状态从 {@link MCP_PROBE_HTTP_STATUS} 单表取,
266
+ * S-201② 那条律),于是这四句结构上够不着那道门。⇒ 它们由 `test/mcp-status-face-s481.test.ts` 的
267
+ * 「四句逐字节」那一格钉住(改一个字当场红)。把那道全局门扩到「常量表出站」形是另一台车。
268
+ */
269
+ export declare function buildMcpProbeRateRefusal(): McpProbeRefusal;
270
+ /**
271
+ * 在途帽满那一句(codex r1 [high] 的第二半)。**沿用** `limit.rate_exceeded` 而不新铸第五枚码:消费端的
272
+ * 分支与限速逐字相同(稍后再问),而拒码闭集每多一枚,按闭集写 `switch` 的消费端就多一次改。
273
+ * `retryAfterSec` 是**提示**不是承诺,而这一点必须说出来:每台的裁决有上界,但一个答完握手就卡住发送的
274
+ * 对端在引擎那边没有任何期限(core `docs/KNOWN-LIMITS.md`),所以「前面那次走查应该在这之内结束」是
275
+ * 常态下的话,不是最坏情况的保证。
276
+ */
277
+ export declare function buildMcpProbeInflightRefusal(): McpProbeRefusal;
278
+ /**
279
+ * 自带申报那一口上、core 的物化**拒了整份申报集**那一句(唯一的成文因由:两台在同一个命名空间前缀下挂载,
280
+ * 在任何传输被触碰**之前**就被拒)。
281
+ *
282
+ * 异源复核 [medium]:修前这一形在两条口上都被抛给分派器尾 ⇒ `500 internal.error`。可自带申报那一口的申报
283
+ * 是**调用方**写的,把它答成 500 是把调用方的错说成这台机器故障;中心申报那一口反过来 —— 那是部署方写的,
284
+ * 5xx 才是对的方向。⇒ 一条规则:**谁写的申报,谁的错**。
285
+ *
286
+ * 🔴 **不回显因由**:那句话来自申报集本身,可能带调用方的内情(名字、地址形)。调用方手里有自己的体,
287
+ * 点名「两台撞了命名空间」就够他自己找;原文留在那一层的日志里。
288
+ */
289
+ export declare function buildMcpProbeDeclarationRefusal(): McpProbeRefusal;
290
+ //# sourceMappingURL=mcp-probe.d.ts.map