@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.
@@ -6,7 +6,7 @@
6
6
  * cut: ONLY TaskRequestBody (its field types are inline or core imports — no other server.ts-local satellites);
7
7
  * sibling request/response shapes can migrate here later on the same pattern.
8
8
  */
9
- import type { A2aServerSpec, AgentDefinition, McpServerSpec, SkillSpec } from "@sema-agent/core";
9
+ import type { A2aServerSpec, AgentDefinition, ApproverPosture, McpServerSpec, SkillSpec } from "@sema-agent/core";
10
10
  import type { SettingsPermissionMode } from "../task-settings.js";
11
11
  import type { wiringManifestFaceEventData } from "../trace/project.js";
12
12
  export interface TaskRequestBody {
@@ -316,6 +316,22 @@ export interface TaskRequestBody {
316
316
  * `SETTINGS_PERMISSION_MODES`(受理腿 `isSettingsPermissionMode` 与两句 400 文案同读那一份)。
317
317
  * 改前这里是第 6 份手抄的五词联合 —— 加/退一个模式时 wire 型面与受理腿会各走各的。 */
318
318
  permissionMode?: SettingsPermissionMode;
319
+ /**
320
+ * **#1000**(core 7.26.0 `TaskSpec.approverPosture`;CC-116 装配口)—— 宿主对这个 run 的**审批姿态声明**:
321
+ * 「这些卡由我自己自动答」。一个词(`"bypass"`),闭集属主在 core(`ApproverPosture` / `APPROVER_POSTURES`)。
322
+ *
323
+ * 🔴 **它不授权任何东西,也不是一只门**(core 头注逐字:"a statement about who answers this run's approval
324
+ * cards, never an instruction to the gate";引擎只把它**渲**给模型,没有任何裁决腿读它)。所以它与
325
+ * {@link permissionMode} 的 `"bypassPermissions"` **不是**一回事,也不能互相派生:后者选的是**门的形状**
326
+ * (且只能收紧、deny-wins),前者只说「谁来答卡」。两者在语义上同族,声明它而不开 `bypassPermissions`
327
+ * 只是对模型说了一句与实际不符的话 —— 见契约文档那一段的「谁受伤」。
328
+ *
329
+ * 🔴 **本仓零判据、原样过境**(`boot/resolve-spec.ts` 写点):词的拒绝逻辑属引擎
330
+ * (`refuseInvalidApproverPosture` 响亮拒一个不认识的词),server **不加第二道门** —— 在下游再判一次就是
331
+ * 同一语义面的第二个判官(源头修复纪律 [ref]),而且本仓抄一份词表的那天 core 加第二个词就会被静默 400 掉。
332
+ * 型也从 core 的 `ApproverPosture` 派生(不抄字面联合)。
333
+ */
334
+ approverPosture?: ApproverPosture;
319
335
  /** MF-30 memory PAUSE (shell-host contract, option B per-request — clay 2026-06-27): `false` makes THIS run
320
336
  * read-only over long-term memory (`TaskSpec.memory.writeScope:null` — the [ref] memory ENGINE materializes/
321
337
  * reads but its harvest commits nothing). Absent/`true` ⇒ normal read+write. The shell's `/memory` pause carries
@@ -1,5 +1,6 @@
1
1
  import { DENIED_BY_VALUES } from "@sema-agent/core";
2
2
  import { buildPermissionRuleEventsHelp } from "./permission-rule-events.js";
3
+ import { MCP_PROBE_OUTCOMES } from "../capabilities/mcp-probe.js";
3
4
  const DEFAULT_BUCKETS = [0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10, 30, 60];
4
5
  function labelKey(labels) {
5
6
  return Object.keys(labels)
@@ -214,6 +215,7 @@ export function createMetrics() {
214
215
  m.counter("http_requests_total", "HTTP requests by route/method/status");
215
216
  m.histogram("http_request_duration_seconds", "HTTP request duration");
216
217
  m.counter("rate_limited_total", "Requests rejected by the rate limiter");
218
+ m.counter("mcp_probe_total", `No-run MCP status face outcomes by outcome (${MCP_PROBE_OUTCOMES.join("/")} — see src/capabilities/mcp-probe.ts MCP_PROBE_OUTCOMES; rate_limited has TWO causes: the per-identity fixed window AND the replica-wide in-flight cap, which can refuse caller B because of caller A)`);
217
219
  m.counter("tasks_total", "Finished tasks by terminal status");
218
220
  m.counter("task_tokens_total", "Total model tokens consumed by tasks");
219
221
  m.histogram("task_duration_seconds", "Async run wall-clock duration");
@@ -1,4 +1,4 @@
1
- import type { RemoveResult, RuleApprovalRecord, RuleBehavior } from "@sema-agent/core";
1
+ import type { RuleApprovalRecord, RuleBehavior } from "@sema-agent/core";
2
2
  /**
3
3
  * `PersistedRuleTool` **不再是闭词表**(core 7.9.0 / [ref]):工具名自 core 的**目录**派生
4
4
  * (`pathTarget` 声明 ⇒ 路径文法;`Bash` ⇒ 命令文法),型是 `string`。于是本文件对它的处置从
@@ -61,13 +61,13 @@ export declare const RULE_APPROVAL_RECORD_SCHEMA: RuleApprovalRecord["schema"];
61
61
  * `"unknown"` = **回答这一问的那次读回没做成** —— 这个域唯一不肯说的那句话是「看不见 ⇒ 没有」,
62
62
  * 第三个词就是它的反面。
63
63
  *
64
- * 🔴 **型从 core 的导出型派生,不是抄一份字面联合**:`RemovalLiveness` 本身没有从包根导出
65
- * (亲验 `dist/index.d.ts`:只导 `RemoveResult`),而 `RemoveResult` 的 removed 臂上就带着它 ⇒ 从那里
66
- * `Extract` 出来。抄一份 `"yes"|"no"|"unknown"` 的下场与本文件顶注说的那一族一样:core 加第四个词
67
- * (或改掉某一个)时,抄件不会红。派生形则**编译期**先红在每一处穷举上。
68
- * (已向 core 请托把这只型放上包根导出面;到货后本行换成直接 import,词表值零变化。)
64
+ * 🔴 **型从 core 的包根导出接过来,本仓零抄**(core 7.26.0 S-538 ⓐ:`RemovalLiveness` 随请托上了导出面)。
65
+ * 上一版是 `Extract<RemoveResult, {status:"removed"}>["stillLive"]` —— 那是派生而非手抄(值不会漂),但它把
66
+ * 一只**独立的闭集**寄生在某一个臂的形状上:core 哪天给 `removed` 臂改名、或让这只词表先在别的臂上落地,
67
+ * 派生式就会指向一个与词表无关的东西。现在两者同名同源。
68
+ * 🔴 **运行期词表 `REMOVAL_LIVENESS_WORDS` 刻意不 re-export**:本仓对这三个词零运行期判据(值由 core 在两只
69
+ * 200 体上原样透传,分支全走编译期穷尽),开一扇没人走的门只会让台账多一行「引用而不消费」的例外。
70
+ * 哪天真要判「这个串在不在表里」,那时从 core 直接读它。
69
71
  */
70
- export type RemovalLiveness = Extract<RemoveResult, {
71
- status: "removed";
72
- }>["stillLive"];
72
+ export type { RemovalLiveness } from "@sema-agent/core";
73
73
  //# sourceMappingURL=permission-rule-vocab.d.ts.map
@@ -134,6 +134,11 @@ export declare class RemoteHostExecutionEnv implements RemoteExecutionEnv, Sched
134
134
  * ② 把读拉进来会让 teardown 去等一次可能**永不 settle** 的读(workspace 里的 FIFO / 特殊文件),
135
135
  * 把 destroy 挂死 —— 拿一条新的可用性病换一条不存在的正确性收益。
136
136
  * 判据因此是一句话:**「可能建路径或改路径的那些面」进在飞集合**,不是「过了 fsReady 的都进」。
137
+ *
138
+ * ── 不止文件面(S-541)─────────────────────────────────────────────────────────────────────────
139
+ * 判据是「会不会建/改路径」,**不是**「在不在 `env` 的文件面上」。后台 shell 的 `launch`(`makeBgDriver`)
140
+ * 在 spool 根下 `mkdir` 一棵子树,而它整段期间 `bgManager.hasShells()` 还是 false(`shells.set` 在
141
+ * `await launch(ctx)` 之后)⇒ 同一条竞态的另一张脸,同一只集合治。见那处头注。
137
142
  */
138
143
  private withInflightFs;
139
144
  /**
@@ -213,45 +213,47 @@ export class RemoteHostExecutionEnv {
213
213
  makeBgDriver() {
214
214
  const self = this;
215
215
  return {
216
- async launch(command, options, ctx) {
217
- const ready = await self.shellReady();
218
- if (!ready.ok)
219
- throw new BackgroundShellError("spawn_failed", ready.error.message);
220
- const dir = path.join(self.ensureSpoolRoot(), "bg-" + randomBytes(8).toString("hex"));
221
- await fs.mkdir(dir, { recursive: true });
222
- const outPath = path.join(dir, "out");
223
- const errPath = path.join(dir, "err");
224
- const outFh = await fs.open(outPath, "w");
225
- const errFh = await fs.open(errPath, "w");
226
- let child;
227
- try {
228
- const sh = hostShell();
229
- child = spawn(sh.shell, [...sh.args, command], {
230
- cwd: self.resolve(options?.cwd ?? self.cwd),
231
- env: self.mergeEnv(options?.env),
232
- ...spawnGroupOptions(),
233
- stdio: ["ignore", outFh.fd, errFh.fd],
234
- });
235
- }
236
- catch (e) {
216
+ launch(command, options, ctx) {
217
+ return self.withInflightFs(async () => {
218
+ const ready = await self.shellReady();
219
+ if (!ready.ok)
220
+ throw new BackgroundShellError("spawn_failed", ready.error.message);
221
+ const dir = path.join(self.ensureSpoolRoot(), "bg-" + randomBytes(8).toString("hex"));
222
+ await fs.mkdir(dir, { recursive: true });
223
+ const outPath = path.join(dir, "out");
224
+ const errPath = path.join(dir, "err");
225
+ const outFh = await fs.open(outPath, "w");
226
+ const errFh = await fs.open(errPath, "w");
227
+ let child;
228
+ try {
229
+ const sh = hostShell();
230
+ child = spawn(sh.shell, [...sh.args, command], {
231
+ cwd: self.resolve(options?.cwd ?? self.cwd),
232
+ env: self.mergeEnv(options?.env),
233
+ ...spawnGroupOptions(),
234
+ stdio: ["ignore", outFh.fd, errFh.fd],
235
+ });
236
+ }
237
+ catch (e) {
238
+ await outFh.close().catch(() => { });
239
+ await errFh.close().catch(() => { });
240
+ await fs.rm(dir, { recursive: true, force: true }).catch(() => { });
241
+ throw new BackgroundShellError("spawn_failed", e instanceof Error ? e.message : String(e), e);
242
+ }
243
+ const pid = child.pid;
244
+ if (pid != null) {
245
+ child.on("close", (code, signal) => ctx.onTerminal(false, code ?? (signal ? 128 + (signalNumber(signal) ?? 9) : 0)));
246
+ child.on("error", () => ctx.onTerminal(true));
247
+ child.unref();
248
+ }
237
249
  await outFh.close().catch(() => { });
238
250
  await errFh.close().catch(() => { });
239
- await fs.rm(dir, { recursive: true, force: true }).catch(() => { });
240
- throw new BackgroundShellError("spawn_failed", e instanceof Error ? e.message : String(e), e);
241
- }
242
- const pid = child.pid;
243
- if (pid != null) {
244
- child.on("close", (code, signal) => ctx.onTerminal(false, code ?? (signal ? 128 + (signalNumber(signal) ?? 9) : 0)));
245
- child.on("error", () => ctx.onTerminal(true));
246
- child.unref();
247
- }
248
- await outFh.close().catch(() => { });
249
- await errFh.close().catch(() => { });
250
- if (pid == null) {
251
- await fs.rm(dir, { recursive: true, force: true }).catch(() => { });
252
- throw new BackgroundShellError("spawn_failed", "host background spawn produced no pid");
253
- }
254
- return { pid, dir, outPath, errPath, stdoutCursor: 0, stderrCursor: 0 };
251
+ if (pid == null) {
252
+ await fs.rm(dir, { recursive: true, force: true }).catch(() => { });
253
+ throw new BackgroundShellError("spawn_failed", "host background spawn produced no pid");
254
+ }
255
+ return { pid, dir, outPath, errPath, stdoutCursor: 0, stderrCursor: 0 };
256
+ });
255
257
  },
256
258
  async read(state) {
257
259
  if (state.mem) {
@@ -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", "approverPosture", "attachmentIds", "attachments", "capabilitiesNeeded", "cascade", "clientContext",
23
23
  "compaction", "compactionModel", "council", "cwd", "debate", "deferTools", "deviceId", "enableFork",
24
24
  "excludeAllTools", "excludeTools",
25
25
  "finalVerification", "forwardSubagentEvents", "images", "interactiveTools", "jobId", "limits", "maxCostUsd",
@@ -1,5 +1,5 @@
1
1
  import { type DirectWritableRuleBehavior, type RemovalLiveness } from "./permission-rule-vocab.js";
2
- import { type RuleOwner, type ImportPreview, type RedeemedBatchMember, type ImportedSettingsLayer, type PersistedRule, type RuleBehavior, type RuleCandidate, type EffectivePermissionRule, type RemoveResult, type EditedRuleTextPrecheck, type RuleScope, type RuleOffer } from "@sema-agent/core";
2
+ import { type RuleOwner, type ImportPreview, type RedeemedBatchMember, type ImportedSettingsLayer, type PersistedRule, type RuleBehavior, type RuleCandidate, type EffectivePermissionRule, type RemoveResult, type RuleWriteCommitted, type EditedRuleTextPrecheck, type RuleScope, type RuleOffer } from "@sema-agent/core";
3
3
  import type { DurableRulePartitionProvider, PermissionRuleStoreProvider, RuleApprovalRecordStore } from "@sema-agent/core";
4
4
  import { type RuleImportTicket, type RuleTicketPurpose, type RuleTicketReclaimResult, type RuleTicketRedeemResult, type RuleTicketSnapshot } from "./plugins/permission-rule-store-sql.js";
5
5
  /**
@@ -615,10 +615,14 @@ export type RuleWritten =
615
615
  * 才是谎 —— 亲读 core 的 `AddResult`,`added` 臂只有 `{status, rev, dot}`,**刻意**不带行(头注:之后
616
616
  * 落地的撤销是「`list()` 答的普通后续事件」),而本仓的行形带着 `status: "live"` 这一格,给一行已经
617
617
  * 不在的规则铸它就是第二个谎。 */
618
+ /** 🔴 `"unknown"`(7.93.0 合并复审 codex 验真后加):引擎已确认 `added`(`rev` 在手),而**写后读回本身抛了**
619
+ * (店读不出来)。「写没写」有答案、「现在站不站着」没人观察 —— 与撤销口 `no-op` + `unknown` 同词同义:
620
+ * **不要重试**(重发只会 `no-op`),用 `GET /v1/rules` 对账。此前这一形落域外通用 500 `internal.error`,
621
+ * 与「写前读库失败」(零写,重发安全)走同一个异常出口 —— 两种强度一句话,正是 `committed` 要拆开的那对。 */
618
622
  | {
619
623
  status: "persisted" | "no-op";
620
624
  rev: number;
621
- stillLive: Extract<RemovalLiveness, "no">;
625
+ stillLive: Exclude<RemovalLiveness, "yes">;
622
626
  row?: undefined;
623
627
  }
624
628
  /** 裁定性不收(内容判据 / 方向判据)。`detail` 是给人看的那一半,`reason` 是给客户端分流的那一半。 */
@@ -627,11 +631,24 @@ export type RuleWritten =
627
631
  reason: LocalImportRefusalCode;
628
632
  detail: string;
629
633
  }
630
- /** **不知道落没落**:店在这一条上出了状况(core 的 `failed` 两种强度)。如实编,禁猜(端点侧 503 +
631
- * 指路 `GET /v1/rules` 对账)。**「写落了随后被撤」不在本臂** —— 那一形有答案,见上面 `stillLive:"no"`。 */
634
+ /** 终态写失败。`detail` 是给人看的那一半;`committed`({@link RuleWriteCommitted},core 的两词闭集)是
635
+ * 给机器分流的那一半 —— 它回答的是**调用方下一步该做什么**。
636
+ *
637
+ * 🔴 **词从 core 的 `AddResult.failed` 原样接过来,不从报文里猜**(core 7.26.0 [ref])。此前本臂叫
638
+ * `indeterminate` 且只带 `detail`:core 把「店自己答了、确知零写」与「apply 抛了、落没落不知道」两种强度
639
+ * **只写在散文里**,本仓于是把两者一律折成「不确定」+ 端点 503「retry」。代价是那句 retry 对两种强度**一句
640
+ * 都不对**:`"no"` 的那半确知零写、重发同一份体是安全的(而旧文案让客户端以为要先对账);`"unknown"` 的那半
641
+ * 可能已经落了,而盲重试会**再铸一枚 dot**、把刚被人撤掉的收紧重新立起来(core `AddResult.failed` 头注逐字
642
+ * 警告过这条路)——那正是本臂最需要说清的一句话,旧形却把它和安全的那半说成了同一句。
643
+ *
644
+ * 🔴 臂名随词改成 core 的 `failed`(不再叫 `indeterminate`):拿到判别位之后,「不确定」对 `"no"` 这半是
645
+ * **假的**,而一个名字与它所含的一半相反的臂,下一个读它的人一定读错。两个词表合成一个,规则少一条。
646
+ *
647
+ * **「写落了随后被撤」不在本臂** —— 那一形有答案,见上面 `stillLive:"no"`。 */
632
648
  | {
633
- status: "indeterminate";
649
+ status: "failed";
634
650
  detail: string;
651
+ committed: RuleWriteCommitted;
635
652
  };
636
653
  export interface RuleConsentLane {
637
654
  /** 卡道兑付口:一次 ask 决议携规则确认 ⇒ prepare→confirm→redeem 一气呵成。 */
@@ -736,7 +736,14 @@ export function createRuleConsentLane(stores, opts) {
736
736
  adds: [],
737
737
  };
738
738
  const rowOf = (rows) => rows.find((r) => sameWireRuleIdentity(r, { behavior: input.behavior, rule: written.rule.rule, scope: row.scope }));
739
- const before = await lane.listRules(input.principal);
739
+ let before;
740
+ try {
741
+ before = await lane.listRules(input.principal);
742
+ }
743
+ catch (e) {
744
+ localImportLogger.warn("direct_rule_write_pre_read_failed", { behavior: input.behavior, err: e instanceof Error ? e.message : String(e) });
745
+ return { status: "failed", detail: `the rule store could not be read before writing: ${e instanceof Error ? e.message : String(e)}`, committed: "no" };
746
+ }
740
747
  const standing = rowOf(before.rows);
741
748
  if (standing !== undefined)
742
749
  return { status: "no-op", rev: before.rev, stillLive: "yes", row: standing };
@@ -750,15 +757,22 @@ export function createRuleConsentLane(stores, opts) {
750
757
  case "no-op":
751
758
  break;
752
759
  case "failed":
753
- return { status: "indeterminate", detail: added.error };
760
+ return { status: "failed", detail: added.error, committed: added.committed };
754
761
  default: {
755
762
  const unknown = added;
756
763
  throw new Error(`unknown addPersistedRule outcome: ${JSON.stringify(unknown)}`);
757
764
  }
758
765
  }
759
- const readBack = await lane.listRules(input.principal);
760
- const landed = rowOf(readBack.rows);
761
766
  const status = added.status === "no-op" ? "no-op" : "persisted";
767
+ let readBack;
768
+ try {
769
+ readBack = await lane.listRules(input.principal);
770
+ }
771
+ catch (e) {
772
+ localImportLogger.warn("direct_rule_write_read_back_failed", { behavior: input.behavior, scope: row.scope, rev: added.rev, wrote: status === "persisted", err: e instanceof Error ? e.message : String(e) });
773
+ return { status, rev: added.rev, stillLive: "unknown" };
774
+ }
775
+ const landed = rowOf(readBack.rows);
762
776
  if (landed === undefined) {
763
777
  localImportLogger.warn("direct_rule_write_vanished_before_read_back", {
764
778
  behavior: input.behavior,
@@ -41,6 +41,38 @@ export declare const MAX_REQUEST_MCP_SERVERS = 32;
41
41
  * 有界点名两头都占:该说的仍然说得出,量级钉死在与挂载同阶。
42
42
  */
43
43
  export declare const MAX_REQUEST_MCP_DROP_NAMES = 32;
44
+ /**
45
+ * S-481 —— 一条 `mcpServers[]` 条目在 wire 上的**键闭集**,与它两种传输各自的键闭集。
46
+ *
47
+ * ## 属主为什么在这里
48
+ * 这个文件就是「请求腿的 MCP 条目形」的家({@link normalizeServer} 逐键读的正是这些)。S-481 的探测口
49
+ * ({@link import("./capabilities/mcp-probe.js").readMcpProbeBody})要的是**同一个**词表 —— 它在自己那边
50
+ * 再写一份就是一份手抄投影([ref]:任何仓第一排查点),而两份哪天分岔,窄的那一份会把**合法**的键当成
51
+ * 拼写错误 400 掉。
52
+ *
53
+ * ## 🔴 两向钉在 core 的 `McpServerSpec` 上(不是手抄)
54
+ * 下面六道编译期门:型上多一个键而表没跟 ⇒ 红;表里写一个型上没有的词(改名 / 拼错 / 删键后的残条目)
55
+ * ⇒ 也红。于是这三张表是从 core 的型**派生**出来的数据,而不是誰记得去同步的清单。
56
+ *
57
+ * ## 两个消费点的处置**刻意不同**(同一张表,不同的门)
58
+ * · 提交腿({@link normalizeServer})对词表外的键**宽容**(读不认识的键就当没有)—— 那条腿要能安全地
59
+ * 重放存量 body,一个 4xx 打在 resume 上会永久砖死一条 parked 任务(本仓三处 `opts.leg` 先例);
60
+ * · 探测口**响亮拒**(400,点名 `mcpServers[i].<键>`)—— 它问的是「这台到底能不能用」,把一个拼错的键
61
+ * 悄悄忽略再回一份好看的名单,正是那条面要根除的假答案。
62
+ * 两个门,一个词表。表在这里,门在各自的腿上。
63
+ */
64
+ export declare const MCP_SERVER_ENTRY_KEYS: readonly ["allowTools", "contentOrigin", "elicitation", "name", "source", "toolAxes", "toolFaces", "transport"];
65
+ /** `transport.kind:"stdio"` 的键闭集(两向钉在 core 那一支上)。 */
66
+ export declare const STDIO_MCP_TRANSPORT_KEYS: readonly ["args", "command", "env", "kind"];
67
+ /** `transport.kind:"http"` 的键闭集(同上)。 */
68
+ export declare const HTTP_MCP_TRANSPORT_KEYS: readonly ["headers", "kind", "principalHeader", "url"];
69
+ /** 条目层:`{}` 与缺席等价的可选 Record 键。 */
70
+ export declare const MCP_ENTRY_EMPTY_RECORD_KEYS: readonly ["toolAxes", "toolFaces"];
71
+ /** 条目层:`[]` 与缺席等价的可选数组键。 */
72
+ export declare const MCP_ENTRY_EMPTY_ARRAY_KEYS: readonly ["allowTools"];
73
+ /** 传输层(两种合起来):`{}` / `[]` 与缺席等价的键。 */
74
+ export declare const MCP_TRANSPORT_EMPTY_RECORD_KEYS: readonly ["env", "headers"];
75
+ export declare const MCP_TRANSPORT_EMPTY_ARRAY_KEYS: readonly ["args"];
44
76
  /**
45
77
  * Does THIS deployment honor caller-supplied per-request MCP servers? TRUE for a SINGLE-USER deployment
46
78
  * (`requirePrincipal !== true`) — the requester is the super-admin of their own worker, so their `.mcp.json` is
package/dist/task-mcp.js CHANGED
@@ -8,6 +8,13 @@ import { readMcpToolFaces } from "./mcp-tool-faces.js";
8
8
  import { isToolAxisIrreversibility, isToolEffect } from "./tool-axis-words.js";
9
9
  export const MAX_REQUEST_MCP_SERVERS = 32;
10
10
  export const MAX_REQUEST_MCP_DROP_NAMES = MAX_REQUEST_MCP_SERVERS;
11
+ export const MCP_SERVER_ENTRY_KEYS = ["allowTools", "contentOrigin", "elicitation", "name", "source", "toolAxes", "toolFaces", "transport"];
12
+ export const STDIO_MCP_TRANSPORT_KEYS = ["args", "command", "env", "kind"];
13
+ export const HTTP_MCP_TRANSPORT_KEYS = ["headers", "kind", "principalHeader", "url"];
14
+ export const MCP_ENTRY_EMPTY_RECORD_KEYS = ["toolAxes", "toolFaces"];
15
+ export const MCP_ENTRY_EMPTY_ARRAY_KEYS = ["allowTools"];
16
+ export const MCP_TRANSPORT_EMPTY_RECORD_KEYS = ["env", "headers"];
17
+ export const MCP_TRANSPORT_EMPTY_ARRAY_KEYS = ["args"];
11
18
  export function mcpInjectionHonored(config) {
12
19
  if (config.lockedConfigKeys?.includes("mcp") === true)
13
20
  return false;
@@ -124,6 +124,27 @@ export interface ToolApprovalFrame {
124
124
  * 本键让 live-only 部署(店缺席、帧无卡)也能读到出身位;安全类标记不得依赖店在场([ref] 结构性
125
125
  * 缺口成文)。消费语义(cli 挂点已预埋):在场 ⇒ 一切自动放行让位(含记住的规则与 bypass 姿态)。 */
126
126
  requiresRealApproval?: true;
127
+ /**
128
+ * **[ref]**(core 7.26.0;**ADDITIVE**,`"tool_approval"` only)—— **这一问没有任何规则能清掉**
129
+ * (`AskRequest.mandated`:org / 治理层、保护判官、hook 的筛查权、被祖先的应答者裁决的问…… 由引擎在闸站
130
+ * 盖章,策略方不可自填、只可收紧)。壳据它渲染「规则清不掉这一问」,并让**声明了自动应答姿态**的席位知道
131
+ * 哪些卡它不该自动答。
132
+ *
133
+ * 🔴 **别拿 `ruleOffersAbsence:"mandated"` 顶替它**(core 头注逐字警告过这条路):那只键回答的是**规则报价
134
+ * 车道**有什么可给,而那条车道对它不说话的工具、**没接规则店的部署**(本仓的 live-only 形)、装不下规则的
135
+ * 任务都是结构性沉默 —— 它的缺席**不是**断言。本键是这一问**自己**的属性,凡强制皆在场。读错的代价 core
136
+ * 实测过:同一条根外读命令在 shell 工具上是强制问、在 monitor 工具上看起来是普通问。
137
+ *
138
+ * 🔴 **它不是 `requiresRealApproval`**:强制只说「没有规则能清掉」,**不**说「没有应答者可以答」——
139
+ * 一个人、一只 hook、一张一揽子 `onAsk:"allow"` 席照常答得了强制卡。两键别互推。
140
+ *
141
+ * 🔴 **真才带,缺席绝不编 `false`**(`governanceForced` / `requiresRealApproval` 同形)。缺席同时覆盖
142
+ * 「不是强制」「老引擎」「这是一张从 park 行上重铸的耐久卡」三形 —— **耐久面上本仓不造孪生座**:core 的
143
+ * 寄存行(`PendingAction`)没有这一位(KL-997a 亲读登记),行上强制表现为 `ruleOffersAbsence:"mandated"`
144
+ * 且无 `ruleOffers`;在 `card_json` 侧自铸一个就是本仓成了这个事实的第二个源(而 park 那侧根本没有输入)。
145
+ * 🔴 echo-only:server 不据它做任何裁决(强制的判定属主在 core 的闸站)。
146
+ */
147
+ mandated?: true;
127
148
  /**
128
149
  * **S-125③/[ref]**(core 7.5.0;**ADDITIVE**,`"tool_approval"` only)—— 这只 ask 的**出身**:
129
150
  * 谁提的这一问,一个 core 闭集词(`ASK_ORIGINS` **11 员**,core 7.14.0 现值:content_question /
@@ -179,7 +200,7 @@ export interface ToolApprovalFrame {
179
200
  *
180
201
  * 🔴 **原样回传**(core 契约 `shell.read_boundary.grant_candidate_is_the_grant`):`dir` 是绝对、已做词法
181
202
  * 折叠、用读边界自己的拼法写的串;**显示的串就是要加的串**。壳可以为了好看渲一个缩短形,回传必须是
182
- * 本键里的原串。⇒ 本仓的姿势是「整只逐字带,或整只丢」,**绝不截**(见 `MAX_READ_ROOT_CANDIDATE_DIR`)。
203
+ * 本键里的原串。⇒ 本仓的姿势是「整只逐字带,或整只丢」,**绝不截**(界见 core 的 `READ_ROOT_CANDIDATE_DIR_MAX`)。
183
204
  *
184
205
  * 🔴 **echo-only,零判读**:server 不从命令文本重推目录,也不复核它真不真能清掉这只 ask —— core 已经
185
206
  * 把提议的根放回读边界又走了一遍才递出来,下游再判一遍就是同一事实的第二个判官(与 `origin` /
@@ -854,6 +854,7 @@ export class ToolApprovalCoordinator {
854
854
  message: typeof req.message === "string" ? redactDeep(req.message) : "",
855
855
  ...(governanceForced ? { governanceForced: true } : {}),
856
856
  ...(req.requiresRealApproval === true ? { requiresRealApproval: true } : {}),
857
+ ...(req.mandated === true ? { mandated: true } : {}),
857
858
  ...(typeof req.origin === "string" && req.origin.length > 0 ? { origin: req.origin } : {}),
858
859
  ...(typeof req.persistedRuleShadowed === "string" && req.persistedRuleShadowed !== ""
859
860
  ? { persistedRuleShadowed: redactSecrets(req.persistedRuleShadowed).slice(0, MAX_RULE_TEXT_CHARS) }
@@ -26,7 +26,7 @@ import type { ToolApprovalFrame } from "../tool-approval.js";
26
26
  * 本文件只有类型、不产 JS,所以被 `import type` 引用不新增任何运行期边。 */
27
27
  export type AssertAllKeysHandled<T extends never> = T;
28
28
  type NotificationProjected = "task_id" | "task_type" | "toolUseId" | "status" | "summary" | "result" | "output_file" | "usage" | "sessionId" | "seq" | "lines" | "stoppedBy" | "source" | "exitCode" | "partial" | "diagnostics" | "recentSteps" | "editedFiles" | "resumable" | "completionId" | "error" | "errorCode" | "apiFailure" | "agentMessage" | "_sema_provenance";
29
- type NotificationExcluded = "peer" | "crossSessionMessage" | "crossSessionNotice";
29
+ type NotificationExcluded = (typeof import("./wire-projection-faces.js").WIRE_PROJECTION_FACES)["task_notification"]["excluded"][number];
30
30
  type _GuardNotification = AssertAllKeysHandled<Exclude<keyof TaskNotificationPayload, NotificationProjected | NotificationExcluded>>;
31
31
  type _GuardNotificationReverse = AssertAllKeysHandled<Exclude<NotificationProjected | NotificationExcluded, keyof TaskNotificationPayload>>;
32
32
  type ProvenanceProjected = "kind" | "from" | "taskId" | "seq" | "agentType";
@@ -43,7 +43,7 @@ type _GuardBgNotifReverse = AssertAllKeysHandled<Exclude<BgNotifProjected | BgNo
43
43
  type RosterProjected = "name" | "agentId" | "sessionId" | "toolUseId" | "owner" | "scope" | "sessionScoped" | "rootSessionId" | "model" | "modelFallback" | "createdAt";
44
44
  type _GuardRoster = AssertAllKeysHandled<Exclude<keyof RosterEntry, RosterProjected>>;
45
45
  type _GuardRosterReverse = AssertAllKeysHandled<Exclude<RosterProjected, keyof RosterEntry>>;
46
- type AskProjected = "toolName" | "toolCallId" | "args" | "message" | "sourceTaskId" | "fromSubagent" | "sourceAgentName" | "delegation" | "ruleOffers" | "ruleOffersAbsence" | "persistedRuleShadowed" | "probeCause" | "ruleEvidence" | "requiresRealApproval" | "denialLimitFallback" | "origin" | "ruleStoreUnreadable" | "readRootCandidate";
46
+ type AskProjected = "toolName" | "toolCallId" | "args" | "message" | "sourceTaskId" | "fromSubagent" | "sourceAgentName" | "delegation" | "ruleOffers" | "ruleOffersAbsence" | "persistedRuleShadowed" | "probeCause" | "ruleEvidence" | "requiresRealApproval" | "denialLimitFallback" | "origin" | "ruleStoreUnreadable" | "readRootCandidate" | "mandated";
47
47
  type AskExcluded = "preview" | "principal" | "riskAxes" | "boundInputHash" | "isDelegatedChild" | "probeReason" | "hasBidiControls" | "previewWithheld" | "execCwd";
48
48
  type _GuardAsk = AssertAllKeysHandled<Exclude<keyof AskRequest, AskProjected | AskExcluded>>;
49
49
  type _GuardAskReverse = AssertAllKeysHandled<Exclude<AskProjected | AskExcluded, keyof AskRequest>>;
@@ -134,7 +134,7 @@ type SummaryProjected = "sessionId" | "scope" | "createdAt" | "gateKind" | "seve
134
134
  type SummaryExcluded = "token" | "toolInput" | "toolName" | "toolCallId" | "preview" | "principal" | "sourceTaskId" | "contentKind" | "restoreMode" | "checkpointId" | "previewWithheld";
135
135
  type _GuardInboxSummary = AssertAllKeysHandled<Exclude<keyof CheckpointSummary, SummaryProjected | SummaryExcluded>>;
136
136
  type _GuardInboxSummaryReverse = AssertAllKeysHandled<Exclude<SummaryProjected | SummaryExcluded, keyof CheckpointSummary>>;
137
- type TaskSpecWired = "objective" | "principal" | "images" | "sessionId" | "requireExistingSession" | "oneShot" | "clientContext" | "resumeAt" | "resumeAtMode" | "model" | "compactionModel" | "thinking" | "systemPrompt" | "appendSystemPrompt" | "promptProvider" | "memory" | "suggestNextPrompts" | "restoreFiles" | "acceptPartialRestore" | "rewindFilesTo" | "tools" | "excludeTools" | "deferTools" | "toolMaterializeStrategy" | "promptProfile" | "toolPolicy" | "onAsk" | "onQuestion" | "hooks" | "durableApproval" | "resourceSuspend" | "resilience" | "finalVerification" | "checkpointStore" | "handsReadOnly" | "memoryPersistenceCapable" | "additionalDirectories" | "additionalReadDirectories" | "enablePlanMode" | "interactiveTools" | "enableFork" | "shellGate" | "writeFace" | "mcp" | "a2a" | "skills" | "backgroundScope" | "envFacts" | "limits" | "configOverrides" | "forwardSubagentEvents" | "retainSubagentSessions" | "retainBackgroundProcesses" | "outputSchema" | "outputRetries" | "compaction" | "attachments" | "selfOrchestration" | "getApiKeyAndHeaders" | "interactionPosture" | "agents" | "taskId" | "signal" | "preemptSignal" | "modelRole" | "enableBlockedReport" | "maxSuspends" | "autoModeRequested" | "skillListingBudgetFraction" | "skillListingMaxDescChars" | "excludeAllTools";
137
+ type TaskSpecWired = "objective" | "principal" | "images" | "sessionId" | "requireExistingSession" | "oneShot" | "clientContext" | "resumeAt" | "resumeAtMode" | "model" | "compactionModel" | "thinking" | "systemPrompt" | "appendSystemPrompt" | "promptProvider" | "memory" | "suggestNextPrompts" | "restoreFiles" | "acceptPartialRestore" | "rewindFilesTo" | "tools" | "excludeTools" | "deferTools" | "toolMaterializeStrategy" | "promptProfile" | "toolPolicy" | "onAsk" | "onQuestion" | "hooks" | "durableApproval" | "resourceSuspend" | "resilience" | "finalVerification" | "checkpointStore" | "handsReadOnly" | "memoryPersistenceCapable" | "additionalDirectories" | "additionalReadDirectories" | "enablePlanMode" | "interactiveTools" | "enableFork" | "shellGate" | "writeFace" | "mcp" | "a2a" | "skills" | "backgroundScope" | "envFacts" | "limits" | "configOverrides" | "forwardSubagentEvents" | "retainSubagentSessions" | "retainBackgroundProcesses" | "outputSchema" | "outputRetries" | "compaction" | "attachments" | "selfOrchestration" | "getApiKeyAndHeaders" | "interactionPosture" | "agents" | "approverPosture" | "taskId" | "signal" | "preemptSignal" | "modelRole" | "enableBlockedReport" | "maxSuspends" | "autoModeRequested" | "skillListingBudgetFraction" | "skillListingMaxDescChars" | "excludeAllTools";
138
138
  type TaskSpecUnwired = "actor" | "roles" | "lspManager" | "tracer" | "readFace" | "readDenyPatterns" | "interactiveQuestionFallback" | "alwaysLoadTools" | "restoreGatedTools" | "deferSelfResolve" | "basePolicyForResumeEdit" | "lspDiagnostics" | "streamingToolExecution" | "toolResultThresholdChars";
139
139
  type _GuardTaskSpec = AssertAllKeysHandled<Exclude<keyof TaskSpec, TaskSpecWired | TaskSpecUnwired>>;
140
140
  type _GuardTaskSpecReverse = AssertAllKeysHandled<Exclude<TaskSpecWired | TaskSpecUnwired, keyof TaskSpec>>;
@@ -11,6 +11,17 @@
11
11
  * `output` (+ `truncated`) — redacted + size-bounded at append (runs.ts) and read straight off the event here (the
12
12
  * old session-join `toolOutput` hook is gone, there's a real source now); `eventId`/`parentToolCallId` (§E2) ride
13
13
  * each content event, additive / tolerate-absent.
14
+ *
15
+ * 🔴 **本族 `*EventData` 构造器的入参型不是白名单**(S-545 起,别再照旧读法维护它)。
16
+ * 每只构造器的形参型是**这条投影的本地入参形**,故意比 core 的分支型松:三条腿里有两条(durable 账本 /
17
+ * resume 回放)手里只有磁盘上的 `Record<string, unknown>`,窄值型在那里结构上不成立,所以 `unknown` +
18
+ * 内部逐层窄化是**有意**的,不是欠账;核过的 core 键在场性由投影体自己的条件展开臂决定。
19
+ * 「这一版 core 的键有没有真的上 wire」这件事的判据**不在形参型上** —— 它在
20
+ * `trace/wire-projection-faces.ts` 的登记表 + `test/wire-whitelist-exhaustiveness.test.ts`:门把 core 型的
21
+ * 键集从**安装态 `.d.ts`** 机器派生出来,喂一枚全键 fixture 真跑一遍本族每只构造器,再拿**投影体**的键集
22
+ * 双向对账(多投一枚 / 漏投一枚各自一条变异负控)。旧形只比形参签名,于是「入参型里写着但投影体里从来
23
+ * 没有」这一类(`human_input.principal` 就是一例)与「忘了投」逐字同形 —— 那正是被替掉的盲区。
24
+ * 新加一只构造器 ⇒ 同批往登记表加一行,否则普查格红。
14
25
  */
15
26
  import type { GateOutcome, McpDelivered, TaskResult, SystemInjectionPriority } from "@sema-agent/core";
16
27
  import type { RunEvent, RunRecord } from "../plugins/store-contracts.js";
@@ -650,6 +661,19 @@ export declare function workspaceChangedEventData(ev: {
650
661
  * 都红不了(2026-09-04 实测:该钉 26/26 全绿,红出 `autoMode` 的是编译期门)。段级 + 成员级的**编译期**门在
651
662
  * `core-keyset-guard.ts` ⑥b —— 那才是「core 加了段/加了成员」的可靠红点,两道并用缺一不可。
652
663
  */
664
+ /**
665
+ * 🔴 **`wiring_manifest.mcp[]` 一行上 wire 的形 —— 全仓唯一属主**(S-481 从 {@link wiringManifestEventData}
666
+ * 的内联 IIFE **逐字**提出来,零行为变化;既有钉在 `test/wiring-manifest-projection.test.ts`)。
667
+ *
668
+ * 为什么必须是一只具名函数:S-481 又开了两条读面(`GET /v1/capabilities/mcp` /
669
+ * `POST /v1/capabilities/mcp/probe`),它们发的是**同一种**行。留在内联里,那两条面只能各自再挑一遍键
670
+ * —— 而契约 §G.7 第 3 条(`error` 远端原话**有意不投**,理由是「再脱一遍就是同一语义面的第二个写者」)
671
+ * 这种**裁定**一旦有两个实现,下一个人只会改到其中一个。
672
+ *
673
+ * 逐条目挑键(不是整只判形):`name` / `status` 缺一 ⇒ 丢**这一条**;数组本身非数组 ⇒ 整段缺席。
674
+ * 逐键判据与失败方向的理由全在下面各行的注里,`liveness` 那一格的裁定在 §G.7 第 2bis 条。
675
+ */
676
+ export declare function projectWiringManifestMcpRows(mcpIn: unknown): Array<Record<string, unknown>> | undefined;
653
677
  /**
654
678
  * `tool_roster_delta`(core 7.8.0 [ref] ⑪ 新 `TaskEvent` 臂)—— **运行期**名册变更:整只变更后名册
655
679
  * (`delta.roster`)+ 一段只有持有 `fromDigest` 的消费者才用得上的摘要。
@@ -371,6 +371,45 @@ export function workspaceChangedEventData(ev) {
371
371
  ...identityFields(ev),
372
372
  };
373
373
  }
374
+ export function projectWiringManifestMcpRows(mcpIn) {
375
+ const str = (v) => (typeof v === "string" && v.length > 0 ? v : undefined);
376
+ const num = (v) => (typeof v === "number" && Number.isFinite(v) ? v : undefined);
377
+ const sec = (v) => (typeof v === "object" && v !== null && !Array.isArray(v) ? { ...v } : undefined);
378
+ if (!Array.isArray(mcpIn))
379
+ return undefined;
380
+ const rows = [];
381
+ for (const raw of mcpIn) {
382
+ const e = sec(raw);
383
+ if (e === undefined)
384
+ continue;
385
+ const name = str(e.name);
386
+ const status = str(e.status);
387
+ if (name === undefined || status === undefined)
388
+ continue;
389
+ const liveness = (() => {
390
+ const rec = sec(e.liveness);
391
+ if (rec === undefined)
392
+ return undefined;
393
+ const state = str(rec.state);
394
+ const observedAt = num(rec.observedAt);
395
+ if (state === undefined || !MCP_LIVENESS_STATES.includes(state) || observedAt === undefined)
396
+ return undefined;
397
+ const errorCode = str(rec.errorCode);
398
+ return { state, observedAt, ...(errorCode === undefined ? {} : { errorCode }) };
399
+ })();
400
+ rows.push({
401
+ name,
402
+ status,
403
+ ...(num(e.toolCount) !== undefined ? { toolCount: e.toolCount } : {}),
404
+ ...(str(e.source) !== undefined ? { source: e.source } : {}),
405
+ ...(str(e.errorCode) !== undefined ? { errorCode: e.errorCode } : {}),
406
+ ...(typeof e.delivered === "string" && MCP_DELIVERY_VERDICTS.includes(e.delivered) ? { delivered: e.delivered } : {}),
407
+ ...(num(e.httpStatus) !== undefined ? { httpStatus: e.httpStatus } : {}),
408
+ ...(liveness !== undefined ? { liveness } : {}),
409
+ });
410
+ }
411
+ return rows;
412
+ }
374
413
  export function toolRosterDeltaEventData(ev) {
375
414
  if (!Value.Check(ToolRosterDelta, ev.delta)) {
376
415
  recordFailOpen("server.trace.tool-roster-malformed", "tool_roster_delta.delta did not fit core ToolRosterDelta");
@@ -495,43 +534,7 @@ export function wiringManifestEventData(ev) {
495
534
  return dropProjectedFace("wiring_manifest.writeProtection.targetView is outside core WRITE_PROTECTION_TARGET_VIEWS");
496
535
  return { targetView };
497
536
  })();
498
- const mcpIn = m.mcp;
499
- const mcp = (() => {
500
- if (!Array.isArray(mcpIn))
501
- return undefined;
502
- const rows = [];
503
- for (const raw of mcpIn) {
504
- const e = sec(raw);
505
- if (e === undefined)
506
- continue;
507
- const name = str(e.name);
508
- const status = str(e.status);
509
- if (name === undefined || status === undefined)
510
- continue;
511
- const liveness = (() => {
512
- const rec = sec(e.liveness);
513
- if (rec === undefined)
514
- return undefined;
515
- const state = str(rec.state);
516
- const observedAt = num(rec.observedAt);
517
- if (state === undefined || !MCP_LIVENESS_STATES.includes(state) || observedAt === undefined)
518
- return undefined;
519
- const errorCode = str(rec.errorCode);
520
- return { state, observedAt, ...(errorCode === undefined ? {} : { errorCode }) };
521
- })();
522
- rows.push({
523
- name,
524
- status,
525
- ...(num(e.toolCount) !== undefined ? { toolCount: e.toolCount } : {}),
526
- ...(str(e.source) !== undefined ? { source: e.source } : {}),
527
- ...(str(e.errorCode) !== undefined ? { errorCode: e.errorCode } : {}),
528
- ...(typeof e.delivered === "string" && MCP_DELIVERY_VERDICTS.includes(e.delivered) ? { delivered: e.delivered } : {}),
529
- ...(num(e.httpStatus) !== undefined ? { httpStatus: e.httpStatus } : {}),
530
- ...(liveness !== undefined ? { liveness } : {}),
531
- });
532
- }
533
- return rows;
534
- })();
537
+ const mcp = projectWiringManifestMcpRows(m.mcp);
535
538
  return {
536
539
  ...(num(m.schemaVersion) !== undefined ? { schemaVersion: m.schemaVersion } : {}),
537
540
  ...(str(legIn.kind) !== undefined ? { leg: { kind: legIn.kind } } : {}),