@sema-agent/server 7.92.1 → 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.
Files changed (60) hide show
  1. package/README.md +1 -0
  2. package/README.zh-CN.md +1 -0
  3. package/dist/approval-ask-machine.d.ts +1 -1
  4. package/dist/approval-card.d.ts +10 -7
  5. package/dist/approval-card.js +3 -3
  6. package/dist/bench/s1/arms.js +5 -6
  7. package/dist/bench/s1/live-deps.js +2 -2
  8. package/dist/boot/resolve-spec.js +1 -0
  9. package/dist/boot/stage-06-runners.js +3 -2
  10. package/dist/boot/stores.js +9 -0
  11. package/dist/capabilities/mcp-probe.d.ts +290 -0
  12. package/dist/capabilities/mcp-probe.js +318 -0
  13. package/dist/http/resume-legs.js +12 -10
  14. package/dist/http/route-ctx.d.ts +3 -0
  15. package/dist/http/routes/capabilities.d.ts +17 -0
  16. package/dist/http/routes/capabilities.js +93 -1
  17. package/dist/http/routes/memory-origin.d.ts +1 -1
  18. package/dist/http/routes/rules.js +49 -13
  19. package/dist/http/routes/runs.js +3 -2
  20. package/dist/http/routes/session-sync.js +24 -12
  21. package/dist/http/routes/sessions.js +12 -8
  22. package/dist/http/routes/tasks.js +13 -13
  23. package/dist/http/send.d.ts +11 -0
  24. package/dist/http/send.js +9 -0
  25. package/dist/http/server.js +3 -2
  26. package/dist/http/wire-types.d.ts +17 -1
  27. package/dist/leader/fanout.js +3 -3
  28. package/dist/leader/leader.js +4 -4
  29. package/dist/leader/repair-wire.js +2 -2
  30. package/dist/leader/wire.js +2 -2
  31. package/dist/observability/metrics.js +2 -0
  32. package/dist/permission-rule-vocab.d.ts +9 -9
  33. package/dist/plugins/file-run-store.js +3 -3
  34. package/dist/plugins/memory-run-store.js +3 -3
  35. package/dist/plugins/remote-env-host.d.ts +5 -0
  36. package/dist/plugins/remote-env-host.js +39 -37
  37. package/dist/plugins/run-store-sql.js +3 -3
  38. package/dist/plugins/schema-consistency.d.ts +88 -0
  39. package/dist/plugins/schema-consistency.js +202 -0
  40. package/dist/plugins/schema-ensure-legs.d.ts +55 -0
  41. package/dist/plugins/schema-ensure-legs.js +87 -0
  42. package/dist/plugins/session-policy-store-sql.js +23 -5
  43. package/dist/request-key-closure.js +1 -1
  44. package/dist/rules-consent.d.ts +42 -4
  45. package/dist/rules-consent.js +23 -7
  46. package/dist/run-local.js +2 -2
  47. package/dist/runs.js +14 -14
  48. package/dist/task-mcp.d.ts +32 -0
  49. package/dist/task-mcp.js +7 -0
  50. package/dist/terminal.d.ts +58 -4
  51. package/dist/terminal.js +28 -6
  52. package/dist/tool-approval.d.ts +22 -1
  53. package/dist/tool-approval.js +1 -0
  54. package/dist/trace/core-keyset-guard.d.ts +3 -3
  55. package/dist/trace/ledger-sink.js +3 -2
  56. package/dist/trace/project.d.ts +24 -0
  57. package/dist/trace/project.js +40 -37
  58. package/dist/trace/wire-projection-faces.d.ts +209 -0
  59. package/dist/trace/wire-projection-faces.js +62 -0
  60. package/package.json +3 -3
@@ -1,5 +1,5 @@
1
- import { type DirectWritableRuleBehavior } 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";
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 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
  /**
@@ -599,18 +599,56 @@ export type RuleWritten =
599
599
  {
600
600
  status: "persisted" | "no-op";
601
601
  rev: number;
602
+ stillLive: Extract<RemovalLiveness, "yes">;
602
603
  row: PersistedRuleWireRow;
603
604
  }
605
+ /** 同上两个词,但**写后读回时它已经不在了** —— 一次合法并发(自己的 / operator 的撤销挤在了「引擎落了
606
+ * dot」与「读回那一行」之间)。
607
+ *
608
+ * 🔴 B3(core fable 复审 2026-09-20):这一形**不是** `indeterminate`。core 已经答了 `added` 并把 `rev`
609
+ * 交到手里 —— 「写没写」这一问有答案;没答案的是「它现在还站着吗」,而那是**另一个问题**,撤销面早就有
610
+ * 一只键在回答它({@link RemovalLiveness} 三词)。折成 `indeterminate` 的代价有名字:那条腿的处置是
611
+ * 「retry」,而照着重试会把**刚刚被人撤掉的收紧重新立起来**(core `AddResult.failed` 头注逐字警告过
612
+ * 这条路)。
613
+ *
614
+ * 🔴 `row` 在这一支**缺席**,而缺席由 `stillLive` 判别(不是「未知」):没有活行可端的时候编一行出来
615
+ * 才是谎 —— 亲读 core 的 `AddResult`,`added` 臂只有 `{status, rev, dot}`,**刻意**不带行(头注:之后
616
+ * 落地的撤销是「`list()` 答的普通后续事件」),而本仓的行形带着 `status: "live"` 这一格,给一行已经
617
+ * 不在的规则铸它就是第二个谎。 */
618
+ /** 🔴 `"unknown"`(7.93.0 合并复审 codex 验真后加):引擎已确认 `added`(`rev` 在手),而**写后读回本身抛了**
619
+ * (店读不出来)。「写没写」有答案、「现在站不站着」没人观察 —— 与撤销口 `no-op` + `unknown` 同词同义:
620
+ * **不要重试**(重发只会 `no-op`),用 `GET /v1/rules` 对账。此前这一形落域外通用 500 `internal.error`,
621
+ * 与「写前读库失败」(零写,重发安全)走同一个异常出口 —— 两种强度一句话,正是 `committed` 要拆开的那对。 */
622
+ | {
623
+ status: "persisted" | "no-op";
624
+ rev: number;
625
+ stillLive: Exclude<RemovalLiveness, "yes">;
626
+ row?: undefined;
627
+ }
604
628
  /** 裁定性不收(内容判据 / 方向判据)。`detail` 是给人看的那一半,`reason` 是给客户端分流的那一半。 */
605
629
  | {
606
630
  status: "rejected";
607
631
  reason: LocalImportRefusalCode;
608
632
  detail: string;
609
633
  }
610
- /** **不知道落没落**:店在这一条上出了状况。如实编,禁猜(端点侧 503 + 指路 `GET /v1/rules` 对账)。 */
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"`。 */
611
648
  | {
612
- status: "indeterminate";
649
+ status: "failed";
613
650
  detail: string;
651
+ committed: RuleWriteCommitted;
614
652
  };
615
653
  export interface RuleConsentLane {
616
654
  /** 卡道兑付口:一次 ask 决议携规则确认 ⇒ prepare→confirm→redeem 一气呵成。 */
@@ -736,10 +736,17 @@ 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
- return { status: "no-op", rev: before.rev, row: standing };
749
+ return { status: "no-op", rev: before.rev, stillLive: "yes", row: standing };
743
750
  const screened = screenLocalImportRow(row);
744
751
  if ("reason" in screened)
745
752
  return { status: "rejected", reason: screened.reason, detail: screened.detail };
@@ -750,23 +757,32 @@ 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);
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
+ }
760
775
  const landed = rowOf(readBack.rows);
761
776
  if (landed === undefined) {
762
777
  localImportLogger.warn("direct_rule_write_vanished_before_read_back", {
763
778
  behavior: input.behavior,
764
779
  scope: row.scope,
765
- note: "the store reported this rule as landed, and it was already gone when the row was read back — a revocation landed in between",
780
+ note: "the store reported this rule as in effect, and it was already gone when the row was read back — a revocation landed in between",
781
+ wrote: status === "persisted",
766
782
  });
767
- return { status: "indeterminate", detail: "this rule landed and was already gone when it was read back — a revocation landed in between" };
783
+ return { status, rev: added.rev, stillLive: "no" };
768
784
  }
769
- return { status: added.status === "no-op" ? "no-op" : "persisted", rev: added.rev, row: landed };
785
+ return { status, rev: added.rev, stillLive: "yes", row: landed };
770
786
  },
771
787
  };
772
788
  return lane;
package/dist/run-local.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { DEFAULT_MODEL_REF } from "./model-select.js";
3
- import { terminalProjection } from "@sema-agent/core";
3
+ import { outwardPlane } from "./terminal.js";
4
4
  import { homedir } from "node:os";
5
5
  import { join } from "node:path";
6
6
  import { realpathSync } from "node:fs";
@@ -535,7 +535,7 @@ export async function runLocal(argv, deps = {}) {
535
535
  print(result.result);
536
536
  }
537
537
  else {
538
- const plane = terminalProjection(result.terminal);
538
+ const plane = outwardPlane(result.terminal);
539
539
  const salvage = result.result || result.salvagedOutput || plane.blockedReason || "";
540
540
  if (salvage)
541
541
  print(salvage);
package/dist/runs.js CHANGED
@@ -1,5 +1,5 @@
1
- import { runWithVerification, runCascade, defaultTaskRegistry, terminalProjection, DURABLE_AGENT_HANDLE_RE } from "@sema-agent/core";
2
- import { failureCodeOf, stripResumeToken } from "./terminal.js";
1
+ import { runWithVerification, runCascade, defaultTaskRegistry, DURABLE_AGENT_HANDLE_RE } from "@sema-agent/core";
2
+ import { failureCodeOf, sealResultForExit, outwardPlane } from "./terminal.js";
3
3
  import { GOVERNANCE_HTTP_STATUS } from "./governance-http-status.js";
4
4
  import { redactErrorMessage } from "./observability/run-terminal.js";
5
5
  import { withPrincipal } from "./observability/principal-context.js";
@@ -311,10 +311,10 @@ export async function runInBackground(runner, spec, runStore, taskId, metrics, p
311
311
  await flush();
312
312
  evictIfConflict(runner, spec.sessionId, result);
313
313
  await flushPromptManifest();
314
- const safe = await withModelUsage(stripResumeToken(result));
314
+ const safe = await withModelUsage(sealResultForExit(result));
315
315
  await append("done", { result: safe });
316
316
  reached = { kind: "done", result: safe };
317
- const plane = terminalProjection(safe.terminal);
317
+ const plane = outwardPlane(safe.terminal);
318
318
  await runStore.setTerminal(taskId, plane.status, safe, plane.errorMessage ?? null);
319
319
  recordDone(safe, plane.status);
320
320
  return;
@@ -324,10 +324,10 @@ export async function runInBackground(runner, spec, runStore, taskId, metrics, p
324
324
  await flush();
325
325
  evictIfConflict(runner, spec.sessionId, result);
326
326
  await flushPromptManifest();
327
- const safe = await withModelUsage(stripResumeToken(result));
327
+ const safe = await withModelUsage(sealResultForExit(result));
328
328
  await append("done", { result: safe });
329
329
  reached = { kind: "done", result: safe };
330
- const plane = terminalProjection(safe.terminal);
330
+ const plane = outwardPlane(safe.terminal);
331
331
  await runStore.setTerminal(taskId, plane.status, safe, plane.errorMessage ?? null);
332
332
  recordDone(safe, plane.status);
333
333
  return;
@@ -358,7 +358,7 @@ export async function runInBackground(runner, spec, runStore, taskId, metrics, p
358
358
  }
359
359
  if (ev.type === "done") {
360
360
  await flush();
361
- const parkStatus = ev.result.terminal.kind === "paused" ? terminalProjection(ev.result.terminal).status : undefined;
361
+ const parkStatus = ev.result.terminal.kind === "paused" ? outwardPlane(ev.result.terminal).status : undefined;
362
362
  if (parkStatus === "suspended") {
363
363
  await sink.appendParked("suspended", await graftParkToolCallId(ev.result, parkCheckpointReader));
364
364
  await flushModelUsage();
@@ -376,10 +376,10 @@ export async function runInBackground(runner, spec, runStore, taskId, metrics, p
376
376
  {
377
377
  const settled = cancelledAtDone ? cancelledResult(ev.result) : ev.result;
378
378
  settledAsCancel = failureCodeOf(settled.terminal) === CANCELLED_CODE;
379
- const safe = await withModelUsage(stripResumeToken(settled));
379
+ const safe = await withModelUsage(sealResultForExit(settled));
380
380
  await sink.appendDone(safe);
381
381
  reached = { kind: "done", result: safe };
382
- const plane = terminalProjection(safe.terminal);
382
+ const plane = outwardPlane(safe.terminal);
383
383
  await runStore.setTerminal(taskId, plane.status, safe, plane.errorMessage ?? null);
384
384
  recordDone(safe, failureCodeOf(safe.terminal) === CANCELLED_CODE ? CANCELLED_CODE : plane.status);
385
385
  }
@@ -436,7 +436,7 @@ export async function runInBackground(runner, spec, runStore, taskId, metrics, p
436
436
  recordFailOpen("server.runs.terminal-ledger-write-failed", `arm=append-clean-cancel ${errText(e)}`);
437
437
  }
438
438
  reached = { kind: "done", result: c };
439
- await runStore.setTerminal(taskId, "failed", c, terminalProjection(c.terminal).errorMessage ?? null);
439
+ await runStore.setTerminal(taskId, "failed", c, outwardPlane(c.terminal).errorMessage ?? null);
440
440
  recordDone(c, CANCELLED_CODE);
441
441
  }
442
442
  }
@@ -450,7 +450,7 @@ export async function runInBackground(runner, spec, runStore, taskId, metrics, p
450
450
  recordFailOpen("server.runs.terminal-ledger-write-failed", `arm=flush-catch ${errText(e)}`);
451
451
  }
452
452
  if (reached?.kind === "done") {
453
- const plane = terminalProjection(reached.result.terminal);
453
+ const plane = outwardPlane(reached.result.terminal);
454
454
  settledAsCancel = failureCodeOf(reached.result.terminal) === CANCELLED_CODE;
455
455
  await runStore.setTerminal(taskId, plane.status, reached.result, plane.errorMessage ?? null);
456
456
  }
@@ -469,7 +469,7 @@ export async function runInBackground(runner, spec, runStore, taskId, metrics, p
469
469
  catch (e) {
470
470
  recordFailOpen("server.runs.terminal-ledger-write-failed", `arm=append-thrown-cancel ${errText(e)}`);
471
471
  }
472
- await runStore.setTerminal(taskId, "failed", c, terminalProjection(c.terminal).errorMessage ?? null);
472
+ await runStore.setTerminal(taskId, "failed", c, outwardPlane(c.terminal).errorMessage ?? null);
473
473
  }
474
474
  else {
475
475
  settledAsCancel = false;
@@ -486,7 +486,7 @@ export async function runInBackground(runner, spec, runStore, taskId, metrics, p
486
486
  catch {
487
487
  }
488
488
  if (reached?.kind === "done")
489
- recordDone(reached.result, failureCodeOf(reached.result.terminal) === CANCELLED_CODE ? CANCELLED_CODE : terminalProjection(reached.result.terminal).status);
489
+ recordDone(reached.result, failureCodeOf(reached.result.terminal) === CANCELLED_CODE ? CANCELLED_CODE : outwardPlane(reached.result.terminal).status);
490
490
  else if (reached?.kind === "suspended") {
491
491
  metrics?.addGauge("runs_active", -1);
492
492
  metrics?.inc("tasks_total", { status: "suspended" });
@@ -503,7 +503,7 @@ export async function runInBackground(runner, spec, runStore, taskId, metrics, p
503
503
  unregisterEngineNotice();
504
504
  if (fleetPublisher) {
505
505
  if (reached?.kind === "done")
506
- fleetPublisher.onTerminal(failureCodeOf(reached.result.terminal) === CANCELLED_CODE ? CANCELLED_CODE : terminalProjection(reached.result.terminal).status, fleetRunResiduals(reached.result));
506
+ fleetPublisher.onTerminal(failureCodeOf(reached.result.terminal) === CANCELLED_CODE ? CANCELLED_CODE : outwardPlane(reached.result.terminal).status, fleetRunResiduals(reached.result));
507
507
  else if (reached?.kind === "suspended")
508
508
  fleetPublisher.onTerminal("suspended");
509
509
  else if (reached?.kind === "needs_review")
@@ -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;
@@ -16,9 +16,9 @@
16
16
  * (run 行的 `status` / `error` 列、`tasks_total{status}` 计量标签、fleet 行的终态词)。出口之外不拼。
17
17
  *
18
18
  * wire 面(HTTP 任务体、SSE `done.result`)**就是结果本身**,所以它跟着 core 一起丢八键、带 `terminal`
19
- * —— 那里没有投影,只有转发(见 {@link stripResumeToken} 的例外)。
19
+ * —— 那里没有投影,只有转发(见 {@link sealResultForExit} 的例外)。
20
20
  */
21
- import { type PausedCause, type TaskStatus, type TerminalCause } from "@sema-agent/core";
21
+ import { terminalProjection, type PausedCause, type TaskStatus, type TerminalCause } from "@sema-agent/core";
22
22
  /**
23
23
  * 这次失败带的**机器码**(`terminal.failed.code`);非失败终局 ⇒ `undefined`。
24
24
  *
@@ -31,7 +31,46 @@ import { type PausedCause, type TaskStatus, type TerminalCause } from "@sema-age
31
31
  */
32
32
  export declare function failureCodeOf(terminal: TerminalCause): string | undefined;
33
33
  /**
34
- * 把**耐久恢复能力凭据**(`CheckpointToken`)从一条结果里摘掉,再让它进可重放账本 / run 行 / wire。
34
+ * 本仓**对外机器码**的词法(S-548)。附录 A 那张表、core 的点名族(`limits.*` / `config.*` /
35
+ * `resume_at.*` / `output.*` / brain 扁平码)、工具层自注册码 —— 全体成员都是这一形:**小写**的
36
+ * `a-z0-9_`,用 `.` 分族。门 {@link OUTWARD_CODE_SHAPE} 在 `test/terminal-store-error-fold.test.ts`
37
+ * 里对**目录里真有的每一个码**断言这条词法(判据由 catalog 机器派生,不是手抄的一张清单)。
38
+ */
39
+ export declare const OUTWARD_CODE_SHAPE: RegExp;
40
+ /** 域外失败码的**唯一**落点(附录外的新码;原码进日志与 message,不丢)。 */
41
+ export declare const STORE_ERROR_CODE = "internal.store_error";
42
+ /**
43
+ * 一个失败码要上 wire 之前的**归一**(S-548)。在词法内 ⇒ 原样;词法外 ⇒ {@link STORE_ERROR_CODE}。
44
+ *
45
+ * ── 病(test T-47,三库一致)────────────────────────────────────────────────────────────────────
46
+ * 本仓的 SQL 店把**驱动原生错误**原样抛回引擎,而 core 的 `errorCodeOf` 沿 `err.code`/`cause` 链取
47
+ * **第一个非空字符串**当这次失败的机器码 —— 于是 mysql2 的 `ER_BAD_FIELD_ERROR` / node-pg 的 SQLSTATE
48
+ * `42703` **逐字**成了 `POST /v1/tasks` 回给消费端的 `terminal.failed.code`。消费端拿到的是一个它的码表里
49
+ * 根本没有、而且随数据库厂商/版本变化的串。
50
+ *
51
+ * ── 为什么判据是**词法**而不是「在不在附录 A 那张表里」────────────────────────────────────────
52
+ * 亲读实现证伪了白名单形:`output.invalid` / `suspend.loop` / `resume_at.*` 这些**合法的引擎终局码**
53
+ * 今天**不在**附录 A(那张表的辖域是 4xx/5xx 错误体,不是 run 终局),白名单会把它们一起折掉 ——
54
+ * 那不仅丢诊断,还会当场打死 `resumeAtHttpStatus` 的 404/422 分诊腿。词法判据则是一条**正面**规则:
55
+ * 「本仓与引擎的码全是小写点分」,而驱动码全是 SCREAMING_SNAKE(`ER_*` / `PROTOCOL_*` / `ECONNRESET`)
56
+ * 或五位 SQLSTATE(`42703`)—— 两个字符集不相交,一条规则覆盖整类,**零逐码特判**。
57
+ *
58
+ * ⚠️ **辖域如实**:本臂触发条件是「码出了本仓的词法」,所以任何依赖铸出的域外码都落这里,不只 SQL 驱动。
59
+ * 今天可观察的产出方**只有**耐久店那一层(引擎自己的传输/brain 错误在 core 侧已被分类成小写码),故码名
60
+ * 取 `internal.store_error`;原码逐字进 message 与 warn 行,归因从不丢失。
61
+ */
62
+ export declare function outwardFailureCode(code: string): string;
63
+ /**
64
+ * **一条结果离开引擎、进入本仓(可重放账本 / run 行 / wire)的唯一封口** —— 两件事,一个出口:
65
+ * ①摘掉**耐久恢复能力凭据**(`CheckpointToken`);②把**域外的失败码**折成本仓的 `internal.store_error`
66
+ * ({@link outwardFailureCode},S-548)。
67
+ *
68
+ * 🔴 为什么两件事合在一只函数里而不是并列两只(7.92.2 改名 `stripResumeToken` → `sealResultForExit`):
69
+ * 它们是**同一个出口上的同一件事** —— 「这条结果可以见人了吗」。并列两只 = 六个结算点各自要记得调两次,
70
+ * 而漏调的那一次不会红、只会在某天的 wire 上露出一个不该露的东西(凭据或驱动码)。名字跟着职责改:
71
+ * 旧名只说了摘凭据那一半,一个只做一半的名字会让下一个人在另一半上另开出口。
72
+ *
73
+ * ── ① 摘凭据 ──
35
74
  * token 就是 resume 凭据(token-as-auth,search [18] Q1b),**永不离开本服务**;非密的 `gate` /
36
75
  * `checkpointId` / `restoreMode` 有意保留(对账位)。
37
76
  *
@@ -49,9 +88,24 @@ export declare function failureCodeOf(terminal: TerminalCause): string | undefin
49
88
  *
50
89
  * 对任何输入安全(非普通对象 / 无 `terminal` / 因由不带 token ⇒ **同一引用**原样返回,不重塑)。
51
90
  */
52
- export declare function stripResumeToken<T extends {
91
+ export declare function sealResultForExit<T extends {
53
92
  terminal: TerminalCause;
54
93
  }>(result: T): T;
94
+ /**
95
+ * **投影出口的唯一属主**(S-548)—— core 的 `terminalProjection` + 对外码归一,一次。
96
+ *
97
+ * `docs/ASSISTANT-WIRE-CONTRACT.md` §5.0 说清了终局有**两张脸**:①结果**本身**(`done.result` / 200 体 /
98
+ * `GET /v1/runs/:id → result`,零投影)与②**五词投影**(run 行的 `status` / `error_code` 列、fleet 终态词、
99
+ * 决策腿 200 体的三格平面词)。两张脸各有一个属主:①是 {@link sealResultForExit},②就是本函数。
100
+ * **两个属主都折码**,否则同一次失败在两张脸上说两个不同的码 —— 而缺的那一张正是 S-548 修前仍在泄漏的
101
+ * 那条路(codex 对抗复审本轮 [medium],亲读证实:决策腿的 200 体从**未封口的** result 投影 `errorCode`,
102
+ * 于是驱动原生码照旧上 wire,尽管同一段代码落库那份已经折过)。
103
+ *
104
+ * ⇒ 纪律:**`src/` 里除本文件外不许再直接调 core 的 `terminalProjection`**(门在
105
+ * `test/terminal-store-error-fold.test.ts` ④)。折叠**幂等**(折过的码再折还是它自己),所以对一个已经封口的
106
+ * 结果调本函数是安全的 no-op —— 调用方不需要记得"这个 result 封过了没有"。
107
+ */
108
+ export declare function outwardPlane(terminal: TerminalCause): ReturnType<typeof terminalProjection>;
55
109
  /**
56
110
  * 这条腿的结果里**藏着的那次耐久暂停** —— 嵌套边界(verify / cascade)遇到自己驱动不了的耐久 park 时,
57
111
  * 在**它的**边界上如实报 `failed`(码是 `unexpected.suspended` / `unexpected.needs_review`),而把那次
package/dist/terminal.js CHANGED
@@ -3,15 +3,37 @@ import { recordFailOpen } from "./observability/fail-open.js";
3
3
  export function failureCodeOf(terminal) {
4
4
  return terminal.kind === "failed" ? terminal.code : undefined;
5
5
  }
6
- export function stripResumeToken(result) {
6
+ export const OUTWARD_CODE_SHAPE = /^[a-z][a-z0-9_]*(?:\.[a-z0-9_]+)*$/;
7
+ export const STORE_ERROR_CODE = "internal.store_error";
8
+ export function outwardFailureCode(code) {
9
+ return OUTWARD_CODE_SHAPE.test(code) ? code : STORE_ERROR_CODE;
10
+ }
11
+ export function sealResultForExit(result) {
7
12
  const t = result?.terminal;
8
13
  if (t === null || typeof t !== "object")
9
14
  return result;
10
15
  if (t.kind === "paused")
11
16
  return { ...result, terminal: stripPausedToken(t) };
12
- if (t.kind === "failed" && t.nestedPause !== undefined)
13
- return { ...result, terminal: { ...t, nestedPause: stripPausedToken(t.nestedPause) } };
14
- return result;
17
+ if (t.kind !== "failed")
18
+ return result;
19
+ let terminal = t;
20
+ if (t.nestedPause !== undefined)
21
+ terminal = { ...terminal, nestedPause: stripPausedToken(t.nestedPause) };
22
+ if (t.code !== undefined) {
23
+ const outward = outwardFailureCode(t.code);
24
+ if (outward !== t.code) {
25
+ console.warn(`[terminal] outward_failure_code_folded original=${t.code} outward=${outward}`, { message: t.message });
26
+ terminal = { ...terminal, code: outward, message: `${t.message ?? "the durable store failed"} [original code: ${t.code}]` };
27
+ }
28
+ }
29
+ return terminal === t ? result : { ...result, terminal };
30
+ }
31
+ export function outwardPlane(terminal) {
32
+ const plane = terminalProjection(terminal);
33
+ if (plane.errorCode === undefined)
34
+ return plane;
35
+ const outward = outwardFailureCode(plane.errorCode);
36
+ return outward === plane.errorCode ? plane : { ...plane, errorCode: outward };
15
37
  }
16
38
  function stripPausedToken(paused) {
17
39
  const { token: _drop, ...rest } = paused;
@@ -30,7 +52,7 @@ export function persistedPlaneOf(result) {
30
52
  if (r.terminal !== null && typeof r.terminal === "object") {
31
53
  let projected;
32
54
  try {
33
- projected = terminalProjection(r.terminal);
55
+ projected = outwardPlane(r.terminal);
34
56
  }
35
57
  catch (e) {
36
58
  recordFailOpen("server.terminal.persisted-plane-malformed", e instanceof Error ? e.message : String(e));
@@ -47,7 +69,7 @@ export function persistedPlaneOf(result) {
47
69
  }
48
70
  return {
49
71
  ...(typeof r.status === "string" ? { status: r.status } : {}),
50
- ...(typeof r.errorCode === "string" ? { errorCode: r.errorCode } : {}),
72
+ ...(typeof r.errorCode === "string" ? { errorCode: outwardFailureCode(r.errorCode) } : {}),
51
73
  ...(typeof r.errorMessage === "string" ? { errorMessage: r.errorMessage } : {}),
52
74
  ...(typeof r.blockedReason === "string" ? { blockedReason: r.blockedReason } : {}),
53
75
  };
@@ -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>>;
@@ -1,4 +1,5 @@
1
- import { terminalProjection } from "@sema-agent/core";
1
+ import {} from "@sema-agent/core";
2
+ import { outwardPlane } from "../terminal.js";
2
3
  import { modelTextEventData, toolStartEventData, toolEndEventData, taskProgressEventData, taskNotificationEventData, compactedEventData, diagnosticsEventData, brainStatusEventData, steeringInjectedEventData, compactionOutcomeEventData, workspaceChangedEventData, wiringManifestEventData, toolRosterDeltaEventData, toolDisclosureEventData, humanInputEventData, turnEndEventData, contextUsageEventData } from "./project.js";
3
4
  import { NotifiedKeys, taskNotificationStreamKey } from "../orchestration/workflow-completion-inbox.js";
4
5
  import { isParkLedgerEventType } from "./ledger-events.js";
@@ -164,7 +165,7 @@ export function createLedgerSink(opts) {
164
165
  const onDone = async (result) => {
165
166
  await flush();
166
167
  if (result.terminal.kind === "paused") {
167
- const parkKind = terminalProjection(result.terminal).status;
168
+ const parkKind = outwardPlane(result.terminal).status;
168
169
  if (isParkLedgerEventType(parkKind)) {
169
170
  await appendParked(parkKind, result);
170
171
  return;
@@ -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` 的消费者才用得上的摘要。