@sema-agent/server 7.130.0 → 7.131.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/MIGRATION.md +87 -0
  2. package/README.md +8 -5
  3. package/README.zh-CN.md +6 -4
  4. package/USAGE.md +36 -8
  5. package/dist/audit.js +4 -1
  6. package/dist/bench/s1/live-deps.js +2 -0
  7. package/dist/boot/resolve-spec.js +38 -15
  8. package/dist/boot/stage-02-web-admin-pull.js +9 -1
  9. package/dist/boot/stage-10-http-server.js +6 -3
  10. package/dist/capabilities/builtin-tools.js +2 -1
  11. package/dist/capabilities/composite-members.d.ts +1 -0
  12. package/dist/capabilities/composite-members.js +1 -0
  13. package/dist/capabilities/hands-lane.js +2 -2
  14. package/dist/capabilities/host-tool-names.d.ts +17 -0
  15. package/dist/capabilities/host-tool-names.js +15 -0
  16. package/dist/capabilities/mcp-probe.js +3 -3
  17. package/dist/capabilities/repo-tools.js +4 -3
  18. package/dist/capabilities/scenarios.d.ts +8 -7
  19. package/dist/capabilities/scenarios.js +5 -4
  20. package/dist/capabilities/select-environment-tool.js +2 -1
  21. package/dist/capabilities/send-user-file-tool.d.ts +0 -1
  22. package/dist/capabilities/send-user-file-tool.js +2 -2
  23. package/dist/capabilities/tool-defer.js +2 -1
  24. package/dist/config-catalog.js +16 -5
  25. package/dist/config-invariants.js +2 -2
  26. package/dist/config-provider.js +2 -2
  27. package/dist/config-types.d.ts +6 -0
  28. package/dist/config.d.ts +2 -2
  29. package/dist/config.js +18 -17
  30. package/dist/elicitation.d.ts +7 -0
  31. package/dist/elicitation.js +20 -5
  32. package/dist/hooks/hook-llm.js +2 -2
  33. package/dist/hooks/hook-runner.d.ts +54 -8
  34. package/dist/hooks/hook-runner.js +86 -16
  35. package/dist/http/admission.js +2 -2
  36. package/dist/http/approval-sweeps.js +2 -2
  37. package/dist/http/checkpoint-ctx.d.ts +12 -11
  38. package/dist/http/checkpoint-ctx.js +4 -4
  39. package/dist/http/dispatch.js +1 -1
  40. package/dist/http/resume-legs.js +6 -5
  41. package/dist/http/routes/approvals-assistant.js +1 -1
  42. package/dist/http/routes/capabilities.js +6 -16
  43. package/dist/http/routes/runs.js +6 -10
  44. package/dist/http/routes/sessions.js +22 -10
  45. package/dist/http/routes/tasks.js +16 -11
  46. package/dist/http/scenario-read-face.d.ts +14 -0
  47. package/dist/http/scenario-read-face.js +5 -0
  48. package/dist/http/send.d.ts +10 -0
  49. package/dist/http/send.js +6 -0
  50. package/dist/http/server.d.ts +16 -12
  51. package/dist/http/server.js +2 -2
  52. package/dist/http/stored-body-replay.d.ts +10 -3
  53. package/dist/http/stored-body-replay.js +27 -6
  54. package/dist/http/wire-types.d.ts +18 -5
  55. package/dist/index.d.ts +2 -1
  56. package/dist/index.js +2 -1
  57. package/dist/mcp-assertion-header.d.ts +2 -2
  58. package/dist/mcp-assertion-header.js +3 -3
  59. package/dist/model-entry-refusal.d.ts +1 -7
  60. package/dist/model-route-endpoint.d.ts +51 -11
  61. package/dist/model-route-endpoint.js +45 -23
  62. package/dist/model-route-gap.d.ts +5 -4
  63. package/dist/model-route-gap.js +1 -1
  64. package/dist/model-select.d.ts +58 -0
  65. package/dist/model-select.js +31 -0
  66. package/dist/namespaced-rule-name.d.ts +7 -4
  67. package/dist/namespaced-rule-name.js +4 -3
  68. package/dist/observability/fail-open.d.ts +11 -6
  69. package/dist/observability/fail-open.js +5 -1
  70. package/dist/plugins/local-session-store.js +2 -2
  71. package/dist/plugins/one-time-migrations.d.ts +10 -1
  72. package/dist/plugins/one-time-migrations.js +16 -1
  73. package/dist/question.d.ts +18 -9
  74. package/dist/question.js +21 -13
  75. package/dist/request-key-closure.d.ts +2 -2
  76. package/dist/request-key-closure.js +16 -9
  77. package/dist/rule-entry-list.d.ts +124 -17
  78. package/dist/rule-entry-list.js +140 -24
  79. package/dist/run-local.d.ts +3 -3
  80. package/dist/run-local.js +8 -7
  81. package/dist/runs.d.ts +42 -2
  82. package/dist/runs.js +45 -2
  83. package/dist/spec-fields.d.ts +8 -4
  84. package/dist/stored-request-hooks-m20.d.ts +23 -0
  85. package/dist/stored-request-hooks-m20.js +124 -0
  86. package/dist/stored-request-tool-axes-m19.d.ts +1 -1
  87. package/dist/task-mcp.d.ts +41 -77
  88. package/dist/task-mcp.js +143 -75
  89. package/dist/task-settings.d.ts +41 -5
  90. package/dist/task-settings.js +39 -25
  91. package/dist/trace/core-keyset-guard.d.ts +2 -2
  92. package/dist/trace/ledger-sink.d.ts +29 -0
  93. package/dist/trace/ledger-sink.js +11 -0
  94. package/dist/trace/project.d.ts +6 -0
  95. package/dist/trace/project.js +11 -0
  96. package/dist/trace/wire-projection-faces.d.ts +1 -1
  97. package/dist/trace/wire-projection-faces.js +1 -1
  98. package/dist/user-authored-text.d.ts +7 -0
  99. package/dist/user-authored-text.js +3 -0
  100. package/dist/web-admin-client/apply-effective.js +10 -5
  101. package/dist/web-admin-client/types.d.ts +1 -1
  102. package/package.json +2 -2
  103. package/scripts/drain-check-local.mjs +3 -2
@@ -1,6 +1,6 @@
1
1
  import { SessionError, materializeA2aTools, materializeMcpTools, sanitizePathComponent } from "@sema-agent/core";
2
2
  import { a2aForScenario, mcpForScenario } from "../../web-admin-client/facade.js";
3
- import { isUuidShape, legacyUnownedSessionOk } from "../../security.js";
3
+ import { HttpError, isUuidShape, legacyUnownedSessionOk } from "../../security.js";
4
4
  import { windowMessages, truncateMessageBlobs, redactSessionViewForRead } from "../../audit.js";
5
5
  import { redactSecrets } from "../../trace/redact.js";
6
6
  import { recordFailOpen } from "../../observability/fail-open.js";
@@ -8,7 +8,7 @@ import { MCP_DIAL_DEADLINE_MS, raceDeadline } from "../mcp-dial-deadline.js";
8
8
  import { sentMemoryComplianceFailure } from "../memory-compliance-failure-reply.js";
9
9
  import { MAX_SETTINGS_OUTPUT_STYLE_CHARS, MAX_SETTINGS_ENV_VARS, MAX_SETTINGS_ENV_KEY_CHARS, MAX_SETTINGS_ENV_VALUE_CHARS } from "../../task-settings.js";
10
10
  import { readWritableSessionPermissionRules } from "../../session-policy-wire.js";
11
- import { sendJson, sendError, sendSessionError, sendSessionPolicyError, sseHeaders } from "../send.js";
11
+ import { sendJson, sendError, sendSessionError, sendSessionPolicyError, sseHeaders, httpErrorBody } from "../send.js";
12
12
  import { gatedPrincipal, explicitOperatorOk, sessionOwnerReadable, sendPrincipalRefusal, sendUnauthorized } from "../principal-gate.js";
13
13
  import { assertNever, SESSION_NOT_FOUND_MESSAGE } from "../route-ctx.js";
14
14
  import * as facts from "../../capability-facts.js";
@@ -144,10 +144,21 @@ export async function handleSessions(req, res, match, ctx) {
144
144
  return;
145
145
  }
146
146
  const lastLegMcp = await lastLegMcpFor(deps, safeDecode(match.params.sessionId), principal);
147
- const specs = mcpForScenario(deps.config.mcpServers, deps.config.defaultScenario);
148
- const cacheKey = `${deps.config.defaultScenario}::${principal ?? ""}`;
147
+ const ledgerHalf = lastLegMcp === undefined ? {} : { lastLegMcp };
148
+ let scenarioName;
149
+ try {
150
+ scenarioName = await deps.scenarioGate(principal, undefined);
151
+ }
152
+ catch (e) {
153
+ if (!(e instanceof HttpError))
154
+ throw e;
155
+ sendJson(res, 200, { asOf: new Date().toISOString(), ...ledgerHalf, scenarioRefusal: httpErrorBody(e) });
156
+ return;
157
+ }
158
+ const specs = mcpForScenario(deps.config.mcpServers, scenarioName);
159
+ const cacheKey = `${scenarioName}::${principal ?? ""}`;
149
160
  if (!specs || specs.length === 0) {
150
- sendJson(res, 200, { servers: [], asOf: new Date().toISOString(), ...(lastLegMcp === undefined ? {} : { lastLegMcp }) });
161
+ sendJson(res, 200, { servers: [], asOf: new Date().toISOString(), ...ledgerHalf });
151
162
  return;
152
163
  }
153
164
  const cached = mcpStatusCache.get(cacheKey);
@@ -179,23 +190,24 @@ export async function handleSessions(req, res, match, ctx) {
179
190
  })();
180
191
  mcpStatusCache.set(cacheKey, { at: Date.now(), panel });
181
192
  }
182
- sendJson(res, 200, { ...(await mcpStatusCache.get(cacheKey).panel), ...(lastLegMcp === undefined ? {} : { lastLegMcp }) });
193
+ sendJson(res, 200, { ...(await mcpStatusCache.get(cacheKey).panel), ...ledgerHalf });
183
194
  return;
184
195
  }
185
196
  }
186
197
  {
187
198
  if (match.decl.id === "session-a2a") {
188
- if (deps.config.requirePrincipal && !gatedPrincipal(req, deps.config)) {
199
+ const principal = gatedPrincipal(req, deps.config);
200
+ if (deps.config.requirePrincipal && !principal) {
189
201
  sendPrincipalRefusal(res, req, deps.config);
190
202
  return;
191
203
  }
192
- const specs = a2aForScenario(deps.config.a2aPeers, deps.config.defaultScenario);
204
+ const scenarioName = await deps.scenarioGate(principal, undefined);
205
+ const specs = a2aForScenario(deps.config.a2aPeers, scenarioName);
193
206
  if (!specs || specs.length === 0) {
194
207
  sendJson(res, 200, { peers: [], asOf: new Date().toISOString() });
195
208
  return;
196
209
  }
197
- const principal = gatedPrincipal(req, deps.config);
198
- const cacheKey = `${deps.config.defaultScenario}::${principal ?? ""}`;
210
+ const cacheKey = `${scenarioName}::${principal ?? ""}`;
199
211
  const cached = a2aStatusCache.get(cacheKey);
200
212
  if (!cached || Date.now() - cached.at > A2A_STATUS_TTL_MS) {
201
213
  if (a2aStatusCache.size > 1024)
@@ -1,13 +1,13 @@
1
1
  import { redactErrorMessage, redactHead, redactTerminalResult } from "../../observability/run-terminal.js";
2
2
  import { runWithVerification, runCascade, taskNotificationDedupKey, uuidv7 } from "@sema-agent/core";
3
- import { graftParkToolCallId, markChildrenStoppedByUserOnAbort, syncRefusalHttpStatus, releaseKeyIfZeroWorkRefusal, HEARTBEAT_MS } from "../../runs.js";
3
+ import { graftParkToolCallId, markChildrenStoppedByUserOnAbort, syncRefusalHttpStatus, releaseKeyIfZeroWorkRefusal, createPromptCommitReader, HEARTBEAT_MS } from "../../runs.js";
4
4
  import { sealResultForExit, outwardPlane } from "../../terminal.js";
5
5
  import { withPrincipal } from "../../observability/principal-context.js";
6
6
  import { fleetRunPublisher, fleetRunLabels, fleetRunResiduals, isFleetAgentTerminalNotification } from "../../fleet/fleet-bus.js";
7
7
  import { defaultSubagentTailBus } from "../../fleet/subagent-tail-bus.js";
8
8
  import { createReasoningSegments, reasoningStreamOfTask } from "../../trace/model-segment.js";
9
9
  import { emitPendingWorkflowCompletions, taskNotificationInboxEntry, taskNotificationStreamKeyOf, NotifiedKeys } from "../../orchestration/workflow-completion-inbox.js";
10
- import { createLedgerSink } from "../../trace/ledger-sink.js";
10
+ import { createLedgerSink, createLedgerTwinEmit } from "../../trace/ledger-sink.js";
11
11
  import { registerEngineNoticeLeg } from "../../trace/engine-notice-wire.js";
12
12
  import { clearTurnActivity, readTurnActivityMs, recordTurnActivity } from "../../turn-activity.js";
13
13
  import { buildCancelledLedgerRow, createRunAbort, mintCancelledResult } from "../../run-cancel-context.js";
@@ -249,6 +249,7 @@ export async function handleTasks(req, res, match, ctx) {
249
249
  let durableHeartbeat;
250
250
  const cancelledResult = (errorMessage, cause) => mintCancelledResult({ taskId: durableTaskId ?? "", sessionId: prepared.spec.sessionId ?? "", startedAtMs: streamLegStartedAt, message: errorMessage, cause });
251
251
  let userMsgEntryId;
252
+ const promptCommit = createPromptCommitReader();
252
253
  let anchorPut = false;
253
254
  const putRewindAnchor = () => {
254
255
  if (anchorPut || !userMsgEntryId || !durableTaskId || !prepared.spec.sessionId || !deps.resumeAnchorStore)
@@ -457,8 +458,9 @@ export async function handleTasks(req, res, match, ctx) {
457
458
  break;
458
459
  for (const end of ev.type === "done" ? [...childReasoning.cut(), ...topReasoning.cut()] : topReasoning.onEvent(ev))
459
460
  sseData(res, end);
460
- if (ev.type === "message_committed" && ev.role === "user")
461
- userMsgEntryId = ev.entryId;
461
+ const promptEntryId = promptCommit(ev);
462
+ if (promptEntryId !== undefined)
463
+ userMsgEntryId = promptEntryId;
462
464
  const causeAtDone = ev.type === "done" ? ac.cause() : undefined;
463
465
  await deps.userInputLedger?.observeLegEvent(liveStream, ev);
464
466
  prepared.acceptance.observe(ev);
@@ -594,6 +596,14 @@ export async function handleTasks(req, res, match, ctx) {
594
596
  }
595
597
  res.write(`event: ${frame.type}\ndata: ${JSON.stringify(frame)}\n\n`);
596
598
  };
599
+ const faceAlive = () => !res.writableEnded && !res.destroyed;
600
+ const emitElicitation = createLedgerTwinEmit({ sink: ledgerSink, writeLive: emitAsk, liveReachable: faceAlive });
601
+ const writeQuestionLive = (frame) => {
602
+ if (!faceAlive())
603
+ throw new Error("live stream ended — question frame undeliverable");
604
+ emitAsk(frame);
605
+ };
606
+ const emitQuestion = createLedgerTwinEmit({ sink: ledgerSink, writeLive: writeQuestionLive, liveReachable: faceAlive });
597
607
  await emitPendingWorkflowCompletions(deps.workflowCompletionInbox, prepared.spec.sessionId, askOwner, (frame) => {
598
608
  if (closed || res.writableEnded || res.destroyed)
599
609
  throw new Error("stream ended before the completion frame was written");
@@ -604,15 +614,10 @@ export async function handleTasks(req, res, match, ctx) {
604
614
  sseData(res, frame);
605
615
  }, { route: "sync-stream-open", connection: "live-sse", log: (m, x) => deps.logger?.info?.(m, x) });
606
616
  const withElicit = deps.elicitation
607
- ? () => deps.elicitation.runWithContext({ taskId: askTaskId, owner: askOwner, emit: emitAsk, abortSignal: ac.signal }, streamBody)
617
+ ? () => deps.elicitation.runWithContext({ taskId: askTaskId, owner: askOwner, emit: emitElicitation, abortSignal: ac.signal }, streamBody)
608
618
  : streamBody;
609
- const emitQuestion = (frame) => {
610
- if (res.writableEnded || res.destroyed)
611
- throw new Error("live stream ended — question frame undeliverable");
612
- emitAsk(frame);
613
- };
614
619
  const withQuestion = () => deps.question
615
- ? deps.question.runWithContext({ taskId: askTaskId, owner: askOwner, emit: emitQuestion, abortSignal: ac.signal, deliverable: () => !res.writableEnded && !res.destroyed }, withElicit)
620
+ ? deps.question.runWithContext({ taskId: askTaskId, owner: askOwner, emit: emitQuestion, abortSignal: ac.signal, deliverable: faceAlive }, withElicit)
616
621
  : withElicit();
617
622
  const emitApproval = (frame) => {
618
623
  if (res.writableEnded || res.destroyed)
@@ -0,0 +1,14 @@
1
+ /**
2
+ * 「本部署没有这个场景」的唯一答复(A9 叶:只依赖 `send.ts`)—— 场景详情读面(`GET /v1/capabilities/scenarios/:name`,名字不在 `scenarioDetails` 里)答它。
3
+ *
4
+ * 回答「此主体在此场景能看见哪些 MCP 服务器 / A2A 对端」的读口不经过这里:它们的场景名只由 `deps.scenarioGate` 给
5
+ * (`ServiceCoreDeps` 的必填件,`capabilities/scenarios.ts` `createScenarioGate` 铸的那一只),点名与缺席都问,拒就是门的 typed 错 —— 读口没有第二个场景名来源。
6
+ */
7
+ import type { ServerResponse } from "node:http";
8
+ /**
9
+ * 「本部署没有这个场景」的唯一答复。一句话、一个码,不各写一份。
10
+ * [ref] 信封统一:本答复曾是全站唯一对象形 error(`{error:{code}}`)——SDK toApiError 对它特判过一轮 TypeError 加固;统一为「字符串 error + 顶层 errorCode」邻位形
11
+ * (cancel/decide 同款),机器判据保留。
12
+ */
13
+ export declare function sendScenarioNotFound(res: ServerResponse): void;
14
+ //# sourceMappingURL=scenario-read-face.d.ts.map
@@ -0,0 +1,5 @@
1
+ import { sendError } from "./send.js";
2
+ export function sendScenarioNotFound(res) {
3
+ sendError(res, 404, "scenario_not_found", "scenario not found");
4
+ }
5
+ //# sourceMappingURL=scenario-read-face.js.map
@@ -9,6 +9,7 @@
9
9
  */
10
10
  import type { ServerResponse } from "node:http";
11
11
  import { type SessionErrorFace } from "../session-error-wire.js";
12
+ import type { HttpError } from "../security.js";
12
13
  /**
13
14
  * [ref](黑板 [ref]②)—— 一条响应「是 SSE 流」的**在场标记**。
14
15
  *
@@ -74,6 +75,15 @@ export declare function sendError(res: ServerResponse, status: number, errorCode
74
75
  * throw 点里只有 3 个带。为了让「errorCode 恒在」在这些站点也成立(而不是靠豁免表放行),按**状态码**
75
76
  * 给一个诚实的粗码:它只声明「这类拒绝」,不冒充比 throw 点更细的语义。带 `code` 的 throw 点原样优先。 */
76
77
  export declare function httpErrorCode(status: number, code?: string): string;
78
+ /**
79
+ * 一只 `HttpError` 的**拒体成员**:`{ error, errorCode, ...extra }` —— 码经 {@link httpErrorCode}(带码的 throw 原样优先,否则按状态给粗码),
80
+ * 句 = `message`,`extra`(拒的机读事实,如场景门的 `detail: { scenario, source }`)原样随体、排在最后。**全仓唯一组句器**:整面拒({@link sendHttpError}:
81
+ * 全局兜底 catch 与准入段的 typed 拒)与把同一只拒当**数据**带进 200 体的面板(`GET /v1/sessions/:id/mcp` 的 `scenarioRefusal`)读的是它 ——
82
+ * 同一只错在两处渲出的成员逐字同,面板不自己拼 `{ errorCode, error }`(拼了就丢 `extra`:壳分不出这次门拒是中心指派漂了还是部署缺省悬空)。
83
+ */
84
+ export declare function httpErrorBody(e: HttpError): Record<string, unknown>;
85
+ /** 整面拒:状态 = `e.status`,体 = {@link httpErrorBody}。 */
86
+ export declare function sendHttpError(res: ServerResponse, e: HttpError): void;
77
87
  /**
78
88
  * 🔴 B1 / S-535 —— `SessionPolicyError` → 应答,经 {@link sessionPolicyErrorHttp} 那**一只**映射
79
89
  * (`src/session-policy-wire.ts`)。回 `true` = 已应答,调用点 `return`。
package/dist/http/send.js CHANGED
@@ -48,6 +48,12 @@ export function httpErrorCode(status, code) {
48
48
  default: return status >= 500 ? "internal.error" : "request.rejected";
49
49
  }
50
50
  }
51
+ export function httpErrorBody(e) {
52
+ return { error: e.message, errorCode: httpErrorCode(e.status, e.code), ...e.extra };
53
+ }
54
+ export function sendHttpError(res, e) {
55
+ sendJson(res, e.status, httpErrorBody(e));
56
+ }
51
57
  export function sendSessionPolicyError(res, e, sentencePrefix = "") {
52
58
  if (!(e instanceof SessionPolicyError))
53
59
  return false;
@@ -58,7 +58,7 @@ export interface RequestAuth {
58
58
  * 平铺形的直接后果 = main.ts 一个上千行的装配字面量,键与键之间没有任何「属于同一件事」的编译期表达,
59
59
  * 键落错家/从未接线只能靠源码钉逐个盯(1.300/1.301/1.304/1.307/1.310 五起真缺陷都是这个病族)。
60
60
  *
61
- * 药:七个**窄**接口 + 顶层必填三件。装配点按组构造(每组一个带类型标注的中间 const ⇒ 受 TS 多余
61
+ * 药:七个**窄**接口 + 顶层必填五件。装配点按组构造(每组一个带类型标注的中间 const ⇒ 受 TS 多余
62
62
  * 属性检查保护),键放错组当场编译红。
63
63
  *
64
64
  * 兼容形(本批**不动消费点**):`ServiceDeps` = 平铺视图 ∩ 组槽。旧的平铺构造(96 个测试文件 +
@@ -66,8 +66,8 @@ export interface RequestAuth {
66
66
  * 入口摊平回平铺视图,server.ts 内的 ~700 处 `deps.x` 消费点原样保留。消费点迁移是后批的事——
67
67
  * 一步到位需要同批改 ~700 处 src + ~800 处 test 键位,与「先立形、后搬家」相比没有额外收益。
68
68
  */
69
- /** createHttpServer 的**必填四件**——引擎、部署配置、请求→TaskSpec 的映射、side-query 执行席。
70
- * 四者恒在场,故留在顶层(不进任何可选组):没有它们 server 根本起不来,包一层可选组只会把编译期
69
+ /** createHttpServer 的**必填五件**——引擎、部署配置、请求→TaskSpec 的映射、side-query 执行席、读口场景门。
70
+ * 五者恒在场,故留在顶层(不进任何可选组):没有它们 server 根本起不来,包一层可选组只会把编译期
71
71
  * 错误推迟到运行期(本仓 E-HIGH-1 病族的成因就是「可选键从未接线、编译期无声」)。 */
72
72
  export interface ServiceCoreDeps {
73
73
  runner: Runner;
@@ -123,6 +123,18 @@ export interface ServiceCoreDeps {
123
123
  * `runnerFor: () => runner` —— 那是一句表态,不是一个回落。
124
124
  */
125
125
  runnerFor: (spec: TaskSpec) => Runner;
126
+ /**
127
+ * 读口的场景名过门 —— `capabilities/scenarios.ts` `createScenarioGate` 铸的**那一只**(与 `resolveSpec` 同一个工厂、同一张场景表、同一份
128
+ * `principalCaps`、同一份部署缺省):凡回答「此主体在此场景能看见哪些 MCP 服务器 / A2A 对端」的口 —— `GET /v1/capabilities/mcp` 的状态面、
129
+ * `GET /v1/sessions/:id/mcp` 与 `…/a2a` 两张会话面板 —— 的场景名都由它给,**点名与缺席都问**:点名 ⇒ 判在册 / allowlist,
130
+ * 拒体(400 `scenario_unknown` 附在册词表 / 400 `scenario_not_allowed` 附 allowlist)与任务提交腿逐字同;缺席 ⇒ 指派链(中心 ruling 胜者,
131
+ * 否则部署缺省),与这位主体 run 时同一读法(指派悬空 ⇒ 503 `state.scenario_assignment_unknown`,同提交腿)。
132
+ * 读面上这只门走**只读腿**(`createScenarioGate(…, "peek")`):问中心裁定时 center 抖动的那一拍,不替这个主体的真提交留下 fail-closed 降级项
133
+ * (提交腿自己是 `"enforce"`)。
134
+ * 🔴 **必填**:读口的场景名只有这一个来源,没有「缺接缝 ⇒ 读部署缺省 / 点名 404」这类第二种读法 —— 漏接是**编译错**(嵌入方自己装配时,工厂与它的型从包根导出:
135
+ * `createScenarioGate` / `ScenarioGate` / `ScenarioGateLeg` / `ScenarioGateSources`)。
136
+ */
137
+ scenarioGate: import("../capabilities/scenarios.js").ScenarioGate;
126
138
  }
127
139
  /** **durable 持久面**:每个键 present ⇔ 对应路由/能力位在场,absent ⇒ 501(诚实缺席),
128
140
  * 绝不静默降级。装配点 = main.ts 的 store backend 开箱段。 */
@@ -563,14 +575,6 @@ export interface ServiceSeamDeps {
563
575
  * per-principal 查询走 caps client 的 executionRuling 同车(零额外 RTT);absent = 无 center(单用户/
564
576
  * dry-run)⇒ 观测面不接线,零行为差。fail-open:观测失败绝不影响 sync 面本身。 */
565
577
  sessionMirrorRuling?: (principal: string | undefined) => Promise<SessionMirrorRuling | undefined>;
566
- /** 请求点名的场景名过门 —— `capabilities/scenarios.ts` `createScenarioGate` 铸的**那一只**(与 `resolveSpec` 同一个工厂、同一张场景表、同一份
567
- * `principalCaps`、同一份部署缺省):`GET /v1/capabilities/mcp?scenario=<名>` 的状态面靠它回答「这个场景名在不在册、这位 principal 放不放行」,
568
- * 拒体(400 `scenario_unknown` 附在册词表 / 400 `scenario_not_allowed` 附 allowlist)因此与任务提交腿逐字同。
569
- * 读面上这只门走**只读腿**(`createScenarioGate(…, "peek")`):问中心裁定时 center 抖动的那一拍,不替这个主体的真提交留下 fail-closed 降级项
570
- * (提交腿自己是 `"enforce"`)。只在请求**点名了**场景时才问(缺席 ⇒ 部署缺省场景,不问门、不多一次中心往返;点名空串也交门答,与提交腿 `scenario: ""` 同码)。
571
- * 本接缝缺席 = 嵌入方的 HTTP 面没有场景表可问:状态面点名场景答 404 `scenario_not_found`,与场景详情读面({@link scenarioDetails} 缺席)同一个答复,
572
- * 不另造空表门、不静默回缺省集。 */
573
- scenarioGate?: import("../capabilities/scenarios.js").ScenarioGate;
574
578
  /** Degenerate-output a/b instrument: on errorCode=output.degenerate, inspect the session's turns to
575
579
  * classify where the salvageable answer was (counter + log). Fire-and-forget; no-op otherwise. */
576
580
  instrumentDegenerate?: (result: TaskResult) => void;
@@ -838,7 +842,7 @@ export interface ServiceDepGroups {
838
842
  deployment?: ServiceDeploymentDeps;
839
843
  knobs?: ServiceKnobDeps;
840
844
  }
841
- /** 平铺视图 = 必填三件 + 其余七组的全可选并集。**派生**自组接口,故与分组形永不漂。 */
845
+ /** 平铺视图 = 必填五件 + 其余七组的全可选并集。**派生**自组接口,故与分组形永不漂。 */
842
846
  export type FlatServiceDeps = ServiceCoreDeps & Partial<ServiceStoreDeps & ServiceCoordinatorDeps & ServiceSeamDeps & ServiceObservabilityDeps & ServiceGovernanceDeps & ServiceDeploymentDeps & ServiceKnobDeps>;
843
847
  /** createHttpServer 的入参:两形皆可(平铺=存量,分组=推荐)。同名键两形都给时,**顶层显式键赢**
844
848
  * (含显式 `undefined`)——一处装配点只该用一形,混用时以肉眼最近的那个为准。 */
@@ -17,7 +17,7 @@ import { cascadeConfig } from "./run-meta.js";
17
17
  export { cascadeConfig };
18
18
  import { coarseStatusForState, errorCodeForExit } from "./routes/images.js";
19
19
  export { coarseStatusForState, errorCodeForExit };
20
- import { sendError, sendSessionError, httpErrorCode, msg } from "./send.js";
20
+ import { sendError, sendHttpError, sendSessionError, msg } from "./send.js";
21
21
  import { createRequestGuards } from "./request-guards.js";
22
22
  import { createAdmission, MAX_SKILL_CONTENT_CHARS, MAX_SYSTEM_PROMPT_CHARS, MAX_OUTPUT_SCHEMA_CHARS, validateUserSkills } from "./admission.js";
23
23
  import { createResumeLegs } from "./resume-legs.js";
@@ -121,7 +121,7 @@ export function createHttpServer(rawDeps) {
121
121
  void handle(req, res).catch((err) => {
122
122
  if (err instanceof HttpError) {
123
123
  if (!res.headersSent)
124
- sendError(res, err.status, httpErrorCode(err.status, err.code), err.message, { ...(err.extra ?? {}) });
124
+ sendHttpError(res, err);
125
125
  else
126
126
  res.end();
127
127
  return;
@@ -9,6 +9,7 @@
9
9
  */
10
10
  import type { PermissionMode } from "@sema-agent/core";
11
11
  import { type Autonomy } from "../runtime-governance.js";
12
+ import { type LaneHomeDeployment } from "../execution-lane-caps.js";
12
13
  import { type StoredBodyReplayRefusal } from "./checkpoint-ctx.js";
13
14
  import type { CheckpointCtx } from "./route-ctx.js";
14
15
  /**
@@ -21,8 +22,14 @@ export declare function permissionModeReadingUnstated(ctx: CheckpointCtx, floor:
21
22
  floor: PermissionMode;
22
23
  } | undefined;
23
24
  /**
24
- * 三问一只判据:不能重放 ⇒ 返回拒的那条规则;能 ⇒ `undefined`。顺序:allow 读法记号 → 技能拒侧读成空 → 模式词读法记号(几问同中时答先中的那一问 ——
25
- * 任一问中都不重放,句子只需一句)。`autonomy` = 本部署**此刻**的 `config.autonomy`(活 config:热改即生效);地板由它的唯一属主算,问点不自己算。
25
+ * 预判读的部署配置:`autonomy`(第三问的模式词地板)与执行车道的 home 两位(`~/` 形规则那一问)。`ServiceConfig` 结构上满足,四个问点传 `deps.config` 整份。
26
26
  */
27
- export declare function storedBodyReplayRefusal(ctx: CheckpointCtx, autonomy: Autonomy | undefined): StoredBodyReplayRefusal | undefined;
27
+ export type StoredReplayDeployment = LaneHomeDeployment & {
28
+ readonly autonomy?: Autonomy | undefined;
29
+ };
30
+ /**
31
+ * 三问一只判据:不能重放 ⇒ 返回拒的那条规则;能 ⇒ `undefined`。顺序:allow 读法记号 → 请求设置里的条目表(**与拒点同序**:桶门〔形 / 空串 / 超帽〕→ 工具名清单读成空〔**只问限制方向**:放行方向的存量条目读成空,续跑照旧、只留一行运维 warn〕→ deny / ask 的名字层 + 规则文法)→ 模式词读法记号(几问同中时答先中的那一问 ——
32
+ * 任一问中都不重放,句子只需一句)。`deployment` = 本部署**此刻**的 `config`(活 config:热改即生效):`autonomy` 的地板由它的唯一属主算,`~/` 形规则的 home 由 `executionLaneHomeDir` 算,问点都不自己算。
33
+ */
34
+ export declare function storedBodyReplayRefusal(ctx: CheckpointCtx, deployment: StoredReplayDeployment): StoredBodyReplayRefusal | undefined;
28
35
  //# sourceMappingURL=stored-body-replay.d.ts.map
@@ -1,6 +1,8 @@
1
- import { storedBodyPermissionMode } from "../task-settings.js";
1
+ import { REQUEST_SETTINGS_REFUSAL_PREFIX, rulePolicyBucketsRefusal, settingsPermissionBucketIssue, storedBodyPermissionMode } from "../task-settings.js";
2
2
  import { autonomyModeFloor, entryPermissionMode } from "../runtime-governance.js";
3
- import { skillDenyInertEntryOf } from "../rule-entry-list.js";
3
+ import { inertToolListEntryOf, replayInertEntry } from "../rule-entry-list.js";
4
+ import { executionLaneHomeDir } from "../execution-lane-caps.js";
5
+ import { httpErrorCode } from "./send.js";
4
6
  import { allowListReadingEraOf, allowListReadingUnstated, permissionModeReadingStated } from "./checkpoint-ctx.js";
5
7
  export function permissionModeReadingUnstated(ctx, floor) {
6
8
  if (floor === undefined || permissionModeReadingStated(ctx))
@@ -8,14 +10,33 @@ export function permissionModeReadingUnstated(ctx, floor) {
8
10
  const stored = storedBodyPermissionMode(ctx.body);
9
11
  return entryPermissionMode(stored, floor).effective === stored ? undefined : { stored, floor };
10
12
  }
11
- export function storedBodyReplayRefusal(ctx, autonomy) {
13
+ const BUCKET_GATE_CODE = "request.field_invalid";
14
+ function storedPermissions(body) {
15
+ const permissions = body?.settings?.permissions;
16
+ return permissions !== null && typeof permissions === "object" && !Array.isArray(permissions) ? permissions : undefined;
17
+ }
18
+ function storedPermissionBucketRefusal(body) {
19
+ const issue = settingsPermissionBucketIssue(storedPermissions(body));
20
+ if (issue === null)
21
+ return undefined;
22
+ return replayInertEntry(issue.fields.field, issue.fields.field === "settings.permissions.allow" ? "allow" : "deny", BUCKET_GATE_CODE, issue.message, { ...issue.fields });
23
+ }
24
+ function storedRulePolicyRefusal(body, deployment) {
25
+ const permissions = storedPermissions(body);
26
+ if (permissions === undefined)
27
+ return undefined;
28
+ const home = executionLaneHomeDir(deployment);
29
+ const refused = rulePolicyBucketsRefusal({ ...(Array.isArray(permissions["ask"]) ? { ask: permissions["ask"] } : {}), ...(Array.isArray(permissions["deny"]) ? { deny: permissions["deny"] } : {}) }, home !== undefined ? { home } : {});
30
+ return refused === undefined ? undefined : replayInertEntry(`settings.permissions.${refused.bucket}`, "deny", httpErrorCode(422), `${REQUEST_SETTINGS_REFUSAL_PREFIX}${refused.message}`);
31
+ }
32
+ export function storedBodyReplayRefusal(ctx, deployment) {
12
33
  const writer = ctx.writerVersion !== undefined ? { writerVersion: ctx.writerVersion } : {};
13
34
  if (allowListReadingUnstated(ctx))
14
35
  return { kind: "allow_list_reading", era: allowListReadingEraOf(ctx), ...writer };
15
- const entry = skillDenyInertEntryOf(ctx.body);
36
+ const entry = storedPermissionBucketRefusal(ctx.body) ?? inertToolListEntryOf(ctx.body, ["deny"]) ?? storedRulePolicyRefusal(ctx.body, deployment);
16
37
  if (entry !== undefined)
17
- return { kind: "skill_deny_inert", entry };
18
- const mode = permissionModeReadingUnstated(ctx, autonomyModeFloor(autonomy));
38
+ return { kind: "inert_tool_list", entry };
39
+ const mode = permissionModeReadingUnstated(ctx, autonomyModeFloor(deployment.autonomy));
19
40
  if (mode !== undefined)
20
41
  return { kind: "permission_mode_reading", ...mode, ...writer };
21
42
  return undefined;
@@ -33,6 +33,12 @@ export interface TaskRequestBody {
33
33
  } | {
34
34
  url: string;
35
35
  }>;
36
+ /** 请求层用户附件(引擎 `TaskSpec.userAttachments`,至多 20 条):挂在**这次提交的目标**上,原样交给引擎 —— 每条以引擎铸的消息紧随目标送到模型,
37
+ * 交付结局在 `human_input{carrier:"objective"}.attachments` 报一次。条目形 = 引擎的 `UserAttachment`(`{ path, lines? }` 读本次 run 的工作区 /
38
+ * `{ name, text, startLine? }` / `{ name, image: { data, mimeType } }` 宿主持有的字节)。**形判属主 = 引擎**:本服务不另判,坏形 / 超 Read 上限的
39
+ * text / 过不了图片准入 / 无工作区的 path 由引擎在第一次模型调用之前拒 `config.user_attachment_invalid`(同步腿 400)。只随目标走:送话口不收
40
+ * (400 `request.body_shape`,键落 `unsupportedKeys`),续跑 / 投递腿的重建不带它(`http/resume-legs.ts` 的一次性键表)。能力位 `userAttachments`。 */
41
+ userAttachments?: TaskSpec["userAttachments"];
36
42
  /** D-1 通用文件上传:预上传附件的句柄数组(POST /v1/attachments 的 `id`)。提交时绑定到会话并在
37
43
  * 执行环境工作目录 `attachments/` 下物化;objective 尾部追加告知(名字/mime/尺寸,内容不进流)。
38
44
  * 上限 16/task;未知或他人 id → 400(fresh 腿)。⚠️ 名为 `attachmentIds` 而非 `attachments`——
@@ -308,12 +314,15 @@ export interface TaskRequestBody {
308
314
  * 坏形不挂键。不投递技能正文(正文照旧由 `Skill` 工具 / 调用方负责)。 */
309
315
  userInvokedSkills?: string[];
310
316
  /** [ref]② (core 1.314 工具面控制批): roster 真卸载清单(schema 不上 wire,manifest 收窄)。wire 工具名;
311
- * tighten-only(core 继承不变量 union 进子 spawn)。数组-of-string fail-loud at submit。 */
317
+ * tighten-only(core 继承不变量 union 进子 spawn)。数组-of-string fail-loud at submit。
318
+ * 读成空的条目(退役拼法 `bash`、大小写孪生 `BASH`、任何工具名都不可能有的拼法、`*`)7.131.0 起受理口 400 `config.tool_face_control` 并点名现名
319
+ * (拒侧读成空 = 放行,不照跑);读得出的名字与真未知名(拼写合法、没有近似也没有退役对应)照收。 */
312
320
  excludeTools?: string[];
313
321
  /** S-1448(core 7.49.0 #1623,[ref]):引擎的**可用性选择器** —— 这一 run 只用列出的工具(wire 名、别名、`Tool(content)` 整只、
314
322
  * `mcp__srv` / `mcp__srv__*` / 工具段通配;`"*"` = 本层不设名单;`[]` = 什么都不给)。宿主**原样透传**进 `TaskSpec.allowTools`:
315
- * 不算补集、不过滤、不与 `settings.permissions.allow`(免问)混词;没有任何工具答应的名字照跑(原样保留,今天什么都不准,之后挂上同名工具即生效;引擎告知运维一次),
316
- * 任何工具名都不可能有的拼法由引擎开跑前拒(`config.tool_face_control`,同步腿 400)。
323
+ * 不算补集、不过滤、不与 `settings.permissions.allow`(免问)混词;真未知名(拼写合法、没有近似也没有退役对应:懒挂载的合法形)照跑(原样保留,今天什么都不准,之后挂上同名工具即生效;引擎告知运维一次),
324
+ * 任何工具名都不可能有的拼法由引擎开跑前拒(`config.tool_face_control`,同步腿 400);7.131.0 起受理口另拒近似名(大小写孪生 `BASH`:想放的什么也没放 = 静默收窄,句子点名现名);
325
+ * 放行侧的退役名(`bash` · `Task`)、内容形(`Agent(Explore)`)与文法读不出照旧受理、交引擎既有通告。
317
326
  * `excludeTools` 是同一只运算的减项(deny 赢)。值形:字符串数组(受理门 400 `request.field_invalid`,与 `excludeTools` 同一只门);
318
327
  * 装着的引擎不认该座 ⇒ 400 `request.field_invalid` 点名所需引擎版本(不静默丢)。无租户门(收窄)。 */
319
328
  allowTools?: string[];
@@ -501,15 +510,19 @@ export interface TaskRequestBody {
501
510
  * worker, not the exec env); IGNORED on multi-tenant deployments (can't make the shared worker run an arbitrary
502
511
  * command). Validated + gated downstream; the center/config baseline wins on a name clash.
503
512
  *
513
+ * 每个受理键一条律:新提交(fresh)腿上,条目的任一受理键(`name` / `transport` 及其子键 / `allowTools` / `toolAxes` / `toolFaces` / `contentOrigin` / `elicitation` / `source`)
514
+ * 在场却判不出形 ⇒ 同步 400 `request.field_invalid`,拒句点名 `mcpServers[i].<键路径>`、server 名与该键判官的原因句(不回显值),整份请求不收;键缺席恒合法。
515
+ * 耐久 resume 重放存量 body 的腿不拒(会砖死已 park 任务):坏硬键整条 server 不进 + 运维 warn,坏软键丢键、server 照挂。
516
+ *
504
517
  * [ref](core 5.60.0):条目上可选的 `contentOrigin`(`"local"|"execution"|"external"`)= **内容
505
518
  * 出处声明**,只影响记忆写治理(会话污染标记),对审批门/策略/角色表零作用。缺席 = pre-378 逐字节
506
519
  * 现行为(判 external)。🔴 **新提交(fresh)腿上,词表外的值 ⇒ 同步 400 `request.field_invalid`** ——
507
520
  * 静默丢会把一次信任边界声明变成探针。⚠️ **耐久 resume 腿上不拒**:那条腿重放的是存量 body,4xx 会把
508
521
  * 一条 parked 任务永久砖死(折成 `409 resume_blocked_by_policy`),所以坏值在那里**降级**为「键缺席」+
509
- * 一条 `mcp_content_class_dropped` warn(本仓三处 `opts.leg` 先例的同款姿势)。两条腿上 server 本体都
522
+ * 一条 `mcp_content_class_dropped` warn(本仓三处 `opts.leg` 先例的同款姿势)。重放腿上 server 本体
510
523
  * 保留 —— 丢的只是声明。core 把这个键定性为**部署面**的键,本仓对它的 reject-or-strip 义务由既有的单用户闸
511
524
  * **结构性**兑现:多租户腿整只 `mcpServers` 被忽略 ⇒ 租户够不着;单用户腿 = core 明文豁免的超管面
512
- * (完整论证见 `task-mcp.ts` 的 `assertRequestMcpContentOrigin` 顶注)。部署面的正门是 center 下发的
525
+ * (完整论证见 `task-mcp.ts` 的 `assertRequestMcpEntries` 顶注)。部署面的正门是 center 下发的
513
526
  * `CenterMcpServer.contentOrigin`。
514
527
  *
515
528
  * S-1515:`transport.kind:"http"` 的条目可选 `assertionHeader`(托管连接器断言头的**头名**,契约包 settings-schema ≥12.2.0 同键)。
package/dist/index.d.ts CHANGED
@@ -17,7 +17,7 @@ export { RateLimiter, type RateDecision } from "./observability/rate-limit.js";
17
17
  export { GiteaClient, GitHubClient, createRepoClient, GIT_API_KINDS, isGitApiKind, repoToolsFor, parseRepo, type GitApiKind, type RepoCoords, type RepoReadClient } from "./capabilities/repo-tools.js";
18
18
  export { nowTool } from "./capabilities/builtin-tools.js";
19
19
  export { loadSkills, skillsForScenario, type LoadedSkill } from "./capabilities/skills.js";
20
- export { buildScenarios, selectScenario, type Scenario, type ScenarioBundle, type ScenarioContext, type ScenarioDeps, type ScenarioRequest, } from "./capabilities/scenarios.js";
20
+ export { buildScenarios, selectScenario, createScenarioGate, type Scenario, type ScenarioBundle, type ScenarioContext, type ScenarioDeps, type ScenarioGate, type ScenarioGateLeg, type ScenarioGateSources, type ScenarioRequest, } from "./capabilities/scenarios.js";
21
21
  export { createTaskListLane, taskListKeyFor, MAX_TASK_LIST_SESSIONS, type TaskListLane, type TaskListLaneBackend, type TaskListLaneOpts } from "./boot/task-list-lane.js";
22
22
  export { ensureTiDBTaskListSchema, ensurePgTaskListSchema } from "./plugins/task-list-store-sql.js";
23
23
  export { createAuthorizer, principalFrom, HttpError, type AuthContext, type OwnerAwareSessionStore, } from "./security.js";
@@ -30,5 +30,6 @@ export { isModelKeyPoison, ModelKeyPoisonedError, type ModelKeyPoison, type Mode
30
30
  export { CATALOG_ENTRY_HEADER_LANES, CATALOG_ENTRY_HEADER_NAMES, type CatalogEntryHeaderName } from "./model-entry-headers.js";
31
31
  export { judgeModelRouteGap, type ModelRouteGapReading, type ModelRouteGapOptions } from "./model-route-gap.js";
32
32
  export type { ModelRouteGap } from "./model-route-endpoint.js";
33
+ export { judgeServedModelRoute, type ServedModelRouteView, type ServedModelRouteOptions, type ModelRouteEndpointVerdict } from "./model-route-endpoint.js";
33
34
  export type { UserInputLedgerStore, UserInputAcceptance, NewUserInputAcceptance, OpenScanCursor, OpenScanPage, SettleHow, WriterGoneEvidence } from "./user-input-ledger.js";
34
35
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -15,7 +15,7 @@ export { RateLimiter } from "./observability/rate-limit.js";
15
15
  export { GiteaClient, GitHubClient, createRepoClient, GIT_API_KINDS, isGitApiKind, repoToolsFor, parseRepo } from "./capabilities/repo-tools.js";
16
16
  export { nowTool } from "./capabilities/builtin-tools.js";
17
17
  export { loadSkills, skillsForScenario } from "./capabilities/skills.js";
18
- export { buildScenarios, selectScenario, } from "./capabilities/scenarios.js";
18
+ export { buildScenarios, selectScenario, createScenarioGate, } from "./capabilities/scenarios.js";
19
19
  export { createTaskListLane, taskListKeyFor, MAX_TASK_LIST_SESSIONS } from "./boot/task-list-lane.js";
20
20
  export { ensureTiDBTaskListSchema, ensurePgTaskListSchema } from "./plugins/task-list-store-sql.js";
21
21
  export { createAuthorizer, principalFrom, HttpError, } from "./security.js";
@@ -26,4 +26,5 @@ export { createKeyResolver, resolveModelApiKey } from "./key-resolver.js";
26
26
  export { isModelKeyPoison, ModelKeyPoisonedError } from "./key-resolver.js";
27
27
  export { CATALOG_ENTRY_HEADER_LANES, CATALOG_ENTRY_HEADER_NAMES } from "./model-entry-headers.js";
28
28
  export { judgeModelRouteGap } from "./model-route-gap.js";
29
+ export { judgeServedModelRoute } from "./model-route-endpoint.js";
29
30
  //# sourceMappingURL=index.js.map
@@ -66,8 +66,8 @@ export declare function readMcpIdentityHeaders(transport: Record<string, unknown
66
66
  };
67
67
  /** 同设的拒句 —— 对契约包解析期那一句**逐字**(`server` = 条目名,调用方负责清洗后传入)。 */
68
68
  export declare function describeMcpIdentityHeadersConflict(server: string): string;
69
- /** `assertionHeader` 值不是字符串的拒句(值的形状只说种类,不回显值本身:它是一个调用方写的头名位置,可能是任何东西)。 */
70
- export declare function describeMcpAssertionHeaderNotAString(server: string, value: unknown): string;
69
+ /** `assertionHeader` 值不是字符串的**原因句**(不带条目名与键路径:点名由请求面统一的拒形 `entryFieldRefusal` 负责;值的形状只说种类,不回显值本身:它是一个调用方写的头名位置,可能是任何东西)。 */
70
+ export declare function describeMcpAssertionHeaderNotAString(value: unknown): string;
71
71
  /** 这一条请求面条目是否声明了 `assertionHeader`(= 铸 core 规格时会被剥掉、需要说一行的那一类)。 */
72
72
  export declare function declaresMcpAssertionHeader(spec: RequestMcpServerSpec): boolean;
73
73
  /** 请求面条目 → 交给引擎的规格:剥掉 `assertionHeader`(引擎无座);其余逐字不动。声明过它的条目由调用方另说一行 info。 */
@@ -16,9 +16,9 @@ export function readMcpIdentityHeaders(transport) {
16
16
  export function describeMcpIdentityHeadersConflict(server) {
17
17
  return `mcp server "${server}" sets both principalHeader and assertionHeader — an http entry names ONE header for the identity it sends: keep principalHeader (the end-user principal) or assertionHeader (the connector assertion), not both`;
18
18
  }
19
- export function describeMcpAssertionHeaderNotAString(server, value) {
20
- const kind = value === null ? "null" : Array.isArray(value) ? "array" : typeof value;
21
- return `mcp server "${server}" sets assertionHeader to a ${kind} — it must be a string (the HTTP header NAME the connector assertion travels in); omit the key to send no assertion header`;
19
+ export function describeMcpAssertionHeaderNotAString(value) {
20
+ const kind = value === null ? "null" : Array.isArray(value) ? "an array" : `a ${typeof value}`;
21
+ return `must be a string (the HTTP header NAME the connector assertion travels in) — got ${kind}; omit the key to send no assertion header`;
22
22
  }
23
23
  export function declaresMcpAssertionHeader(spec) {
24
24
  return spec.transport.kind === "http" && spec.transport.assertionHeader !== undefined;
@@ -1,5 +1,6 @@
1
1
  import { type ModelEntryRefusalCode, type ModelPlaneRefusalCode, type ModelRefusalsSignal } from "@sema-agent/settings-schema";
2
2
  import { type DroppedCatalogEntry } from "./web-admin-client/read-warnings.js";
3
+ import { type ModelTableLeg } from "./model-select.js";
3
4
  /**
4
5
  * 条目拒因码 → 本仓铸它的成因 —— **以码为键**(S-768):码闭集的属主是 settings-schema(`MODEL_ENTRY_REFUSAL_CODES`,6.2.0 起;此前本仓
5
6
  * 自持一份同名码表,撞名已消灭、零别名)。那边加一个条目码 ⇒ 本表缺键 ⇒ 本仓 tsc 红(只拿 `satisfies` 约束码值证明的是「子集」,
@@ -126,13 +127,6 @@ export interface DefaultModelView {
126
127
  readonly models: Readonly<Record<string, unknown>>;
127
128
  readonly modelRefusals: Readonly<Record<string, ModelEntryRefusal>>;
128
129
  }
129
- /**
130
- * 这张表是从哪条腿来的(闭集 —— 拒因句的出路按腿分:加腿不补 {@link defaultModelRefusal} 的臂 = 编译红)。
131
- * · `env` —— 进程此刻服务的表**没有目录在管**(没配目录 / 目录迟到且无 LKG / 目录的模型面这一组被拒):`MODEL_ID` 是出路之一;
132
- * · `catalog` —— 判的是一份**目录候选**(模型面 stage):目录一旦生效就整张换掉 env 表,`MODEL_ID` 救不了这份候选 ——
133
- * 出路只在目录里(修条目 / 换绑)。opus 腿 ④(7.101.0 合并复审):此前两腿同一句,目录腿教「设 MODEL_ID 即可」是假话。
134
- */
135
- export type ModelTableLeg = "env" | "catalog";
136
130
  /**
137
131
  * 🔴 S-705 —— **这张模型表有没有一只缺省模型**(`models.default`)。`undefined` = 有。
138
132
  *