@sema-agent/server 7.127.2 → 7.129.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 (112) hide show
  1. package/MIGRATION.md +130 -0
  2. package/USAGE.md +137 -20
  3. package/dist/approval-card.d.ts +53 -26
  4. package/dist/approval-card.js +40 -11
  5. package/dist/approval-crash-convergence.js +4 -4
  6. package/dist/audit.js +4 -1
  7. package/dist/bake-runner/main.js +2 -1
  8. package/dist/boot/execution-lane.d.ts +11 -0
  9. package/dist/boot/execution-lane.js +6 -0
  10. package/dist/boot/memory-consolidation.d.ts +46 -16
  11. package/dist/boot/memory-consolidation.js +15 -8
  12. package/dist/boot/resolve-spec.js +3 -1
  13. package/dist/boot/side-query-lane.d.ts +7 -20
  14. package/dist/boot/side-query-lane.js +0 -24
  15. package/dist/boot/stage-01-config.js +3 -0
  16. package/dist/boot/stage-06-runners.js +1 -1
  17. package/dist/boot/stores.js +2 -2
  18. package/dist/boot/web-admin.js +31 -20
  19. package/dist/boot/webfetch-summarize-lane.d.ts +10 -7
  20. package/dist/boot/webfetch-summarize-lane.js +2 -3
  21. package/dist/brain.js +6 -9
  22. package/dist/capabilities/builtin-tools.d.ts +2 -2
  23. package/dist/capabilities/engine-capabilities.d.ts +14 -0
  24. package/dist/capabilities/engine-capabilities.js +12 -0
  25. package/dist/capabilities/mcp-probe.js +11 -5
  26. package/dist/capability-facts.d.ts +3 -1
  27. package/dist/capability-facts.js +1 -0
  28. package/dist/config-catalog.js +1 -1
  29. package/dist/config-provider.d.ts +17 -4
  30. package/dist/config-provider.js +8 -6
  31. package/dist/config-types.d.ts +6 -5
  32. package/dist/egress-redirect.d.ts +12 -0
  33. package/dist/egress-redirect.js +7 -0
  34. package/dist/entry-mode-clamp-notice.d.ts +30 -0
  35. package/dist/entry-mode-clamp-notice.js +13 -0
  36. package/dist/fleet/fleet-bus.d.ts +7 -0
  37. package/dist/fleet/fleet-bus.js +1 -0
  38. package/dist/hooks/hook-llm.js +6 -2
  39. package/dist/hooks/hook-runner.d.ts +1 -0
  40. package/dist/hooks/hook-runner.js +10 -4
  41. package/dist/host-lsp-manager.d.ts +3 -3
  42. package/dist/http/admission.js +5 -2
  43. package/dist/http/resume-legs.js +46 -27
  44. package/dist/http/route-ctx.d.ts +1 -1
  45. package/dist/http/routes/approvals-assistant.js +6 -4
  46. package/dist/http/routes/capabilities.js +1 -0
  47. package/dist/http/routes/session-sync.js +3 -4
  48. package/dist/http/routes/sessions.js +4 -17
  49. package/dist/http/send.d.ts +1 -1
  50. package/dist/http/send.js +4 -3
  51. package/dist/http/wire-types.d.ts +14 -5
  52. package/dist/index.d.ts +1 -1
  53. package/dist/index.js +1 -1
  54. package/dist/main.js +2 -0
  55. package/dist/mcp-assertion-header.d.ts +89 -0
  56. package/dist/mcp-assertion-header.js +49 -0
  57. package/dist/observability/fail-open.d.ts +7 -3
  58. package/dist/observability/fail-open.js +7 -3
  59. package/dist/observability/secret-env-scrub.d.ts +37 -3
  60. package/dist/observability/secret-env-scrub.js +2 -2
  61. package/dist/parked-decide.js +5 -2
  62. package/dist/plugins/host-platform.d.ts +43 -13
  63. package/dist/plugins/host-platform.js +67 -18
  64. package/dist/plugins/memory-embedder.js +5 -0
  65. package/dist/plugins/one-time-migrations.d.ts +180 -9
  66. package/dist/plugins/one-time-migrations.js +315 -3
  67. package/dist/plugins/pg-pool.js +3 -1
  68. package/dist/plugins/remote-env-host.d.ts +17 -20
  69. package/dist/plugins/remote-env-host.js +16 -28
  70. package/dist/plugins/session-policy-store-sql.d.ts +16 -4
  71. package/dist/plugins/session-policy-store-sql.js +17 -20
  72. package/dist/plugins/session-rules-column.d.ts +16 -0
  73. package/dist/plugins/session-rules-column.js +6 -0
  74. package/dist/plugins/tidb-pool.js +4 -1
  75. package/dist/read-face-posture.d.ts +2 -1
  76. package/dist/request-value-closure.js +1 -1
  77. package/dist/retired-card-words.d.ts +17 -0
  78. package/dist/retired-card-words.js +49 -0
  79. package/dist/rule-entry-list.js +2 -0
  80. package/dist/run-local.js +15 -6
  81. package/dist/server-secret-env.d.ts +77 -85
  82. package/dist/server-secret-env.js +40 -26
  83. package/dist/session-policy-wire.d.ts +61 -124
  84. package/dist/session-policy-wire.js +59 -121
  85. package/dist/session-sync.d.ts +8 -6
  86. package/dist/session-sync.js +6 -5
  87. package/dist/spec-fields.d.ts +3 -2
  88. package/dist/standing-answer-grant.d.ts +17 -9
  89. package/dist/standing-answer-grant.js +11 -8
  90. package/dist/task-mcp.d.ts +31 -3
  91. package/dist/task-mcp.js +42 -4
  92. package/dist/task-settings.d.ts +6 -1
  93. package/dist/task-settings.js +1 -1
  94. package/dist/tool-approval.d.ts +14 -7
  95. package/dist/tool-approval.js +23 -18
  96. package/dist/trace/core-keyset-guard.d.ts +3 -3
  97. package/dist/trace/engine-notice-wire.d.ts +1 -1
  98. package/dist/trace/engine-notice-wire.js +1 -0
  99. package/dist/trace/project.d.ts +7 -4
  100. package/dist/trace/project.js +7 -76
  101. package/dist/trace/tool-output-media.d.ts +10 -4
  102. package/dist/trace/tool-output-media.js +7 -29
  103. package/dist/web-admin-client/apply-effective.js +28 -11
  104. package/dist/web-admin-client/facade.d.ts +2 -2
  105. package/dist/web-admin-client/facade.js +2 -2
  106. package/dist/web-admin-client/http-client.d.ts +2 -2
  107. package/dist/web-admin-client/read-face.d.ts +73 -20
  108. package/dist/web-admin-client/read-face.js +76 -36
  109. package/dist/web-admin-client/skills-mcp.d.ts +20 -17
  110. package/dist/web-admin-client/skills-mcp.js +27 -8
  111. package/dist/web-admin-client/types.d.ts +18 -0
  112. package/package.json +3 -3
@@ -0,0 +1,7 @@
1
+ export function egressRedirectRefusal(res, fix) {
2
+ if (res.type !== "opaqueredirect" && !(res.status >= 300 && res.status < 400))
3
+ return undefined;
4
+ void res.body?.cancel().catch(() => undefined);
5
+ return `endpoint answered with a redirect${res.status > 0 ? ` (HTTP ${res.status})` : ""}; it was not followed and no credential was sent anywhere else — ${fix}`;
6
+ }
7
+ //# sourceMappingURL=egress-redirect.js.map
@@ -0,0 +1,30 @@
1
+ /**
2
+ * S-1306 —— **入口地板钳制**通告(`engine_notice` 码 `permission.entry_mode_clamped`)的唯一铸点(core 7.51.0 #1527 入册:受众 user、
3
+ * 铸者 `NOTICE_MINTER` = `"server"`,引擎从不铸它)。
4
+ *
5
+ * ## 这件事为什么归本仓
6
+ * 运维 `AUTONOMY=plan` 的入口地板在**受理门**把调用方请求的模式词折回地板(`task-settings.ts` `stampEntryPermissionMode`,底下是
7
+ * `runtime-governance.ts` `entryPermissionMode` → core `clampPermissionMode`)—— 折叠发生在 run 到达引擎**之前**,所以引擎自己的
8
+ * `config.permission_mode_unavailable`(它只说它自己做的折叠)看不见这一折。修前 run 在另一个模式下起步,人一个字都看不到。
9
+ *
10
+ * ## 铸点(唯一)
11
+ * {@link buildEntryModeClampNotice} 是本码**唯一**的铸点;调用点只有一处:`http/admission.ts` 的 `prepareSpec`(提交入口,盖章之后、装配
12
+ * 之后 —— 那时会话 id 才定)。一次提交一次 ⇒ 一条 run 至多一帧(core 码册的去重单位「once per run」)。读的是盖章那一次求值的读数
13
+ * (`EntryPermissionMode`),不另判一遍「折没折」。续跑腿不铸:它们重放的是已盖过章的存储体,折叠只发生过一次。
14
+ * 装配期铸、腿开口那一拍投(`PreparedTaskSubmission.engineNotices` → `registerEngineNoticeLeg` 的 `pending`)—— 与请求腿 MCP 丢弃同一条投递路,
15
+ * 白名单 / 归属 / 消毒 / 隔离一个不绕。
16
+ *
17
+ * ## detail
18
+ * `{ requested, effective, ceiling, sessionId }` —— `requested` = 被折的那个词(体的生效词:顶层 `permissionMode` 或
19
+ * `settings.permissions.defaultMode`)、`effective` = run 起步的词、`ceiling` = 折它的地板,三个都是 core 的模式闭集词;`sessionId` = 归属键。
20
+ * 🔴 **无 `runId`**:钳制发生在受理门,引擎的 run 身份要到 prepare 才铸,这一刻不存在(core 码册那一行写了 `runId`;本仓不编造,
21
+ * 归属看它落在哪条 run 的流 / 账本上 —— 与 `instructions.source_changed` / `session.records_skipped` 同形)。
22
+ * 什么时候不铸:没有地板;没声明词(取地板本身不算改词,`clamped` 为假);请求的词不比地板松;会话 id 缺席(无处可投,宁缺不串)。
23
+ */
24
+ import type { EngineNotice, EngineNoticeCode } from "@sema-agent/core";
25
+ import type { EntryPermissionMode } from "./runtime-governance.js";
26
+ /** 码常量型 = core 码册(#1527 入册:受众 user、铸者 `"server"`)。 */
27
+ export declare const ENTRY_MODE_CLAMPED: EngineNoticeCode;
28
+ /** 一次受理门盖章的读数 → 至多一条通告(读法见文件头)。 */
29
+ export declare function buildEntryModeClampNotice(entry: EntryPermissionMode, sessionId: string | undefined): EngineNotice | undefined;
30
+ //# sourceMappingURL=entry-mode-clamp-notice.d.ts.map
@@ -0,0 +1,13 @@
1
+ export const ENTRY_MODE_CLAMPED = "permission.entry_mode_clamped";
2
+ export function buildEntryModeClampNotice(entry, sessionId) {
3
+ const { requested, effective, floor, clamped } = entry;
4
+ if (!clamped || requested === undefined || floor === undefined || effective === undefined || sessionId === undefined || sessionId.length === 0)
5
+ return undefined;
6
+ return {
7
+ code: ENTRY_MODE_CLAMPED,
8
+ message: `This run starts in permission mode "${effective}", not the requested "${requested}": ` +
9
+ `the deployment's entry floor ("${floor}") clamps every submitted run's mode to it.`,
10
+ detail: { requested, effective, ceiling: floor, sessionId },
11
+ };
12
+ }
13
+ //# sourceMappingURL=entry-mode-clamp-notice.js.map
@@ -420,6 +420,13 @@ export interface BgNotification {
420
420
  seq?: number;
421
421
  status: "completed" | "failed" | "killed";
422
422
  summary?: string;
423
+ /** S-1296(core 7.51.0 #1482 / #1528):the child's REPORT — the same string the completion frame the parent's model reads carries
424
+ * (core cuts it once at the frame's bound, redacting first), so a host that wakes an idle conversation reads it here instead of a
425
+ * second, differently-cut copy. UNTRUSTED child text ⇒ `redactSecrets` at this wire boundary, exactly like `summary` on this frame
426
+ * and `result` on the `task_notification` frame (one reading: the server redacts at its own boundary whatever upstream did).
427
+ * Notification-only (the task ROW is state and never carries it). ABSENT when the frame carries none (a killed / failed leg with no
428
+ * report, a withheld hand-back). */
429
+ result?: string;
423
430
  /** Background lifetime scope ([ref]), NOT the tenant scope. */
424
431
  scope: "task" | "session";
425
432
  parentTaskId?: string;
@@ -560,6 +560,7 @@ export function fleetBackgroundChildPublisher(bus, log) {
560
560
  ...(typeof e.seq === "number" ? { seq: e.seq } : {}),
561
561
  status: e.status ?? "failed",
562
562
  ...(e.summary ? { summary: redactSecrets(e.summary) } : {}),
563
+ ...(e.result !== undefined ? { result: redactSecrets(e.result) } : {}),
563
564
  scope: (m.sessionScoped || e.sessionScoped) ? "session" : "task",
564
565
  ...(m.parentTaskId ? { parentTaskId: m.parentTaskId } : m.resolvedParentId ? { parentTaskId: m.resolvedParentId } : {}),
565
566
  ...(e.completionId ? { completionId: e.completionId } : {}),
@@ -2,6 +2,7 @@ import { adjudicateModelRoute, joinRequestUrl, resolveRouteCredential, routeKeyO
2
2
  import { matchCatalogModel, refusedCatalogEntry } from "../model-select.js";
3
3
  import { modelUnavailableMessage } from "../model-entry-refusal.js";
4
4
  import { resolveOpenaiRouteEndpoint } from "../model-route-endpoint.js";
5
+ import { egressRedirectRefusal } from "../egress-redirect.js";
5
6
  import { buildDisplaySafeRefusal, redactSecrets } from "../trace/redact.js";
6
7
  export function buildHookFailureText(raw) {
7
8
  if (raw === "")
@@ -70,7 +71,7 @@ export function createHookLlm(deps) {
70
71
  buildDisplaySafeRefusal(routeRefusalText(verdict), [pick.baseUrl, declaredRoot ?? ""]),
71
72
  };
72
73
  }
73
- if ((verdict.posture === "unpinned" || verdict.posture === "keyless") && routeKeyOf(pick.baseUrl) !== routeKeyOf(declaredRoot)) {
74
+ if (verdict.posture === "keyless" && routeKeyOf(pick.baseUrl) !== routeKeyOf(declaredRoot)) {
74
75
  return {
75
76
  ok: false,
76
77
  error: `hook model "${pick.catalogRef}" points at ${pick.baseUrl}, which this deployment did not declare as its own endpoint, ` +
@@ -131,7 +132,10 @@ export function createHookLlm(deps) {
131
132
  headers = { "content-type": "application/json", ...(hookKey ? { authorization: `Bearer ${hookKey}` } : {}) };
132
133
  body = JSON.stringify({ model: pick.providerModelId, ...(maxOutputTokens !== undefined ? { max_tokens: maxOutputTokens } : {}), messages: [...(system ? [{ role: "system", content: system }] : []), { role: "user", content: prompt }], stream: false });
133
134
  }
134
- const r = await fetchImpl(url, { method: "POST", headers, body, signal: ac.signal });
135
+ const r = await fetchImpl(url, { method: "POST", headers, body, signal: ac.signal, redirect: "manual" });
136
+ const redirected = egressRedirectRefusal(r, "declare the final endpoint as this route's base URL");
137
+ if (redirected !== undefined)
138
+ return { ok: false, error: `hook llm ${redirected}` };
135
139
  if (!r.ok)
136
140
  return { ok: false, error: `hook llm HTTP ${r.status}` };
137
141
  let text;
@@ -2,6 +2,7 @@ import { HooksConfig, type HookEventName } from "@sema-agent/settings-schema/hoo
2
2
  import type { Hooks } from "@sema-agent/core";
3
3
  import type { Logger } from "../observability/logger.js";
4
4
  import type { HookNoticeObservation } from "../fleet/fleet-bus.js";
5
+ /** 服务侧防线上限(契约本身不设量纲;不设界=单请求可塞任意大配置/任意长命令)。超界=校验错(fail-loud)。 */
5
6
  export declare const MAX_HOOK_ENTRIES_PER_EVENT = 32;
6
7
  export declare const MAX_HOOK_COMMAND_CHARS = 8192;
7
8
  /** 单条 hook 超时上限(秒;条目可自设 timeout,但被此值夹住——一个 86400 的 timeout 会挂死工具门)。 */
@@ -9,7 +9,6 @@ import { redactSecrets, buildDisplaySafeUrl } from "../trace/redact.js";
9
9
  import { ccPromptSystemFor, wrapCondition, parseCcVerdict, CC_EVALUATOR_MAX_OUTPUT_TOKENS } from "./cc-stop-prompt.js";
10
10
  import { ccAgentHookSystemFor } from "./cc-agent-hook-prompt.js";
11
11
  import { renderBranchTranscript } from "./branch-transcript.js";
12
- void resolveHostShell().catch(() => undefined);
13
12
  export const MAX_HOOK_ENTRIES_PER_EVENT = 32;
14
13
  export const MAX_HOOK_COMMAND_CHARS = 8_192;
15
14
  export const MAX_HOOK_TIMEOUT_SECONDS = 600;
@@ -391,7 +390,13 @@ function buildHookContextField(parts) {
391
390
  const text = buildHookContext(parts);
392
391
  return text === undefined ? {} : { additionalContext: text };
393
392
  }
394
- function runCommandHook(entry, payload, ctx) {
393
+ async function runCommandHook(entry, payload, ctx) {
394
+ try {
395
+ await resolveHostShell();
396
+ }
397
+ catch (e) {
398
+ return { code: null, stdout: "", stderr: "", timedOut: false, spawnError: String(e) };
399
+ }
395
400
  return new Promise((resolve) => {
396
401
  const timeoutMs = Math.min(entry.timeout ?? DEFAULT_HOOK_TIMEOUT_SECONDS, MAX_HOOK_TIMEOUT_SECONDS) * 1000;
397
402
  let child;
@@ -454,10 +459,11 @@ function runCommandHook(entry, payload, ctx) {
454
459
  child.stdin?.end(`${JSON.stringify(boundedToolInputPayload(payload))}\n`);
455
460
  });
456
461
  }
457
- function fireAsyncCommandHook(entry, payload, ctx, event) {
462
+ async function fireAsyncCommandHook(entry, payload, ctx, event) {
458
463
  try {
459
464
  const rewake = entry.asyncRewake === true;
460
465
  const timeoutMs = Math.min(entry.timeout ?? DEFAULT_HOOK_TIMEOUT_SECONDS, MAX_HOOK_TIMEOUT_SECONDS) * 1000;
466
+ await resolveHostShell();
461
467
  const sh = hostShell();
462
468
  const child = spawn(sh.shell, [...sh.args, entry.command], {
463
469
  cwd: ctx.cwd,
@@ -754,7 +760,7 @@ async function runMatchingEntries(event, groups, matchValue, payload, ctx, onceF
754
760
  if (entry.type === "command" && (entry.async || entry.asyncRewake)) {
755
761
  if (entry.once)
756
762
  onceFired.add(onceKey);
757
- fireAsyncCommandHook(entry, payload, ctx, event);
763
+ void fireAsyncCommandHook(entry, payload, ctx, event);
758
764
  continue;
759
765
  }
760
766
  if (Date.now() > deadlineAt) {
@@ -6,10 +6,10 @@
6
6
  * `process.env`,脑钥 / 仓库令牌 / 本仓自己的凭据键全在里面,而语言服务器会执行仓里的代码(构建脚本、插件、
7
7
  * 工作区配置)。7.30.1 起 core 默认改成 `scrubSecretEnv(process.env)`(按键名**形状**剥),并开了
8
8
  * `NodeLspManagerOptions.env` 一格让宿主自己交 env(core 帖 [ref] ④;core 确认本仓的正确读法 = 交 host 车道**同一只**
9
- * `scrubServerSecrets` 的结果)。
9
+ * 本仓唯一组合 `hostSubprocessEnv` 的结果 —— 引擎 `inheritedHostEnv("scrub")` ∪ 点名放回)。
10
10
  *
11
11
  * ## 规则(一句)
12
- * 语言服务器子进程的 env = `scrubServerSecrets`(process.env) ∪ 部署方点名放回的键(S-699,见下节;缺省空名单)——
12
+ * 语言服务器子进程的 env = 引擎 `inheritedHostEnv("scrub")`(形状剥 ∪ 本进程起服声明的自有凭据名)∪ 部署方点名放回的键(S-699,见下节;缺省空名单)——
13
13
  * 剥除与模型可驱动的 host shell(`inheritEnv:"scrub"`)
14
14
  * 和 project-memory 的 git 子进程**同一只函数**:目录 `type:"secret"` 键按名剥 ∘ core 形状规则按形剥。只交 core 的默认
15
15
  * 会漏掉形状规则认不出的本仓凭据键(当时是 `OTEL_EXPORTER_OTLP_HEADERS`,core 7.33.7 #1180 起 core 按名剥它;今天只剩名单型旋钮三只),那正是 S-684 当初的病形。
@@ -23,7 +23,7 @@
23
23
  * ## 点名放行(S-699,`LSP_ENV_ALLOW`)
24
24
  * 剥除会连语言服务器自己的真实依赖一起剥掉(`.npmrc` 的 `${NPM_TOKEN}` 插值、rust-analyzer `cargo metadata` 私有 registry 的
25
25
  * `CARGO_REGISTRIES_<名>_TOKEN` —— 形状规则按名判,分不出「给语言服务器用的令牌」与「编排器的令牌」)。出路 = 部署方点名:
26
- * env = `scrubServerSecrets`(process.env) 之上把 `config.lspEnvAllow` 点到的键经 `allowlistedEnv`(与 host shell
26
+ * env = 引擎 `inheritedHostEnv("scrub")` 之上把 `config.lspEnvAllow` 点到的键经 `allowlistedEnv`(与沙箱三腿
27
27
  * `inheritEnv:[names]` 同一只谓词)从原 env 原样放回;没点到的照旧剥 —— 7.103.0 起这件组合只有一只:{@link hostSubprocessEnv}
28
28
  * (`server-secret-env.ts`,bake-runner 的 `BAKE_ENV_ALLOW` 调同一只)。名单由 `config.ts` 解析(形错拒启;
29
29
  * `REQUIRE_PRINCIPAL=true` 下落空名单并 warn 一次),本函数只读 `config.lspEnvAllow`,不在这里再判一次姿态。
@@ -3,6 +3,7 @@ import { isAtMentionable, matchCatalogModel, refusedCatalogEntry, selectableCata
3
3
  import { modelUnavailableMessage } from "../model-entry-refusal.js";
4
4
  import { HttpError, sessionIdShapeIssue } from "../security.js";
5
5
  import { MAX_SETTINGS_OUTPUT_STYLE_CHARS, MAX_SETTINGS_ENV_VARS, MAX_SETTINGS_ENV_KEY_CHARS, MAX_SETTINGS_ENV_VALUE_CHARS, PERMISSION_MODE_WORDS, parseTaskSettings, settingsPermissionBucketIssue, skillListingBudgetFractionOk, skillListingMaxDescOk, stampEntryPermissionMode, taskHooksHonored, taskSettingsKeyIssue } from "../task-settings.js";
6
+ import { buildEntryModeClampNotice } from "../entry-mode-clamp-notice.js";
6
7
  import { autonomyModeFloor } from "../runtime-governance.js";
7
8
  import { taskBodyKeyIssue } from "../request-key-closure.js";
8
9
  import { FILE_HISTORY_DISABLED_NO_RESTORE, fileHistoryRestoreConflict, inheritEnvFieldIssue, singleMemberOffFieldIssue } from "../request-value-closure.js";
@@ -629,7 +630,7 @@ export function createAdmission(ctx) {
629
630
  sendError(res, 400, "request.precondition_unmet", "cascade requested but no ladder configured — set MODEL_CASCADE_LADDER (catalog model names, cheapest→strongest)");
630
631
  return null;
631
632
  }
632
- const admitted = stampEntryPermissionMode(body, autonomyModeFloor(deps.config.autonomy));
633
+ const { body: admitted, entry: entryMode } = stampEntryPermissionMode(body, autonomyModeFloor(deps.config.autonomy));
633
634
  try {
634
635
  const auth = deps.authorize ? await deps.authorize({ req, body: admitted }) : undefined;
635
636
  const engineNotices = [];
@@ -644,7 +645,9 @@ export function createAdmission(ctx) {
644
645
  });
645
646
  const model = modelIdOf(spec.model);
646
647
  const acceptance = createRunAcceptance({ sessionTitler: deps.sessionTitler }, { ...(spec.sessionId ? { sessionId: spec.sessionId } : {}), objective: spec.objective, ...(model !== undefined ? { model } : {}) });
647
- return { spec, runner: runnerFor(spec), auth, verify, cascade, jobId: admitted.jobId, body: admitted, acceptance, ...(bindingIntent === undefined ? {} : { bindingIntent }), ...(engineNotices.length > 0 ? { engineNotices } : {}) };
648
+ const clampNotice = buildEntryModeClampNotice(entryMode, spec.sessionId);
649
+ const pendingNotices = clampNotice !== undefined ? [clampNotice, ...engineNotices] : engineNotices;
650
+ return { spec, runner: runnerFor(spec), auth, verify, cascade, jobId: admitted.jobId, body: admitted, acceptance, ...(bindingIntent === undefined ? {} : { bindingIntent }), ...(pendingNotices.length > 0 ? { engineNotices: pendingNotices } : {}) };
648
651
  }
649
652
  catch (err) {
650
653
  if (err instanceof HttpError) {
@@ -49,9 +49,8 @@ import { buildCheckpointVersionKeys, resumeBlockedByPolicyAnswer, storedBodyRepl
49
49
  import { storedBodyReplayRefusal } from "./stored-body-replay.js";
50
50
  import { httpErrorCode } from "./send.js";
51
51
  import { createRunAcceptance } from "../run-acceptance.js";
52
- function isCheckpointReopenedFailure(r) {
53
- const code = failureCodeOf(r.terminal);
54
- return code === "resume.env_failed" || code === "resume.tool_unavailable" || code === "resume.tool_contract_mismatch";
52
+ function legMayHaveReopenedCheckpoint(r) {
53
+ return r.terminal.kind === "failed" && nestedPauseOf(r.terminal) === undefined;
55
54
  }
56
55
  function nestedParkStatusOf(r) {
57
56
  const paused = nestedPauseOf(r.terminal);
@@ -476,6 +475,7 @@ export function createResumeLegs(ctx) {
476
475
  async function driveResumeLeg(args, onAccepted) {
477
476
  const resumeLegStartedAt = Date.now();
478
477
  const { runner: legRunner, token, sessionId, principal, fleetScope, taskConfig, resumeObjective, outcome, verifyRounds, resumeCtx } = args;
478
+ const reparkStatus = outcome.gate === "plan_review" ? "needs_review" : "suspended";
479
479
  const commitResumeCtx = () => deps.checkpointStore.putCtx(resumeCtx.sessionId, { ...resumeCtx.ctx, writerVersion: serviceVersion() }).catch((e) => {
480
480
  deps.metrics?.inc("resume_ctx_not_persisted_total");
481
481
  deps.logger?.warn?.("resume_ctx_not_persisted", { sessionId: resumeCtx.sessionId, gate: outcome.gate, error: errorText(e), note: "this continuation runs with the body it was given; the durable rebuild input still holds the PREVIOUS body, so the NEXT continuation of this session rebuilds from that one (never wider than this leg's)" });
@@ -562,6 +562,32 @@ export function createResumeLegs(ctx) {
562
562
  return true;
563
563
  }
564
564
  };
565
+ const settleCardForCancel = async () => {
566
+ const cs2 = deps.checkpointStore;
567
+ if (!cs2)
568
+ return;
569
+ try {
570
+ const live = await cs2.get(token);
571
+ if (live?.status === "pending" && !(await cs2.expire(token, live.scope))) {
572
+ deps.logger?.warn?.("resume_cancel_checkpoint_settle_lost", { taskId, sessionId });
573
+ }
574
+ }
575
+ catch (cancelSettleErr) {
576
+ deps.logger?.warn?.("resume_cancel_checkpoint_settle_failed", {
577
+ taskId, sessionId, err: cancelSettleErr instanceof Error ? cancelSettleErr.message : String(cancelSettleErr),
578
+ });
579
+ }
580
+ };
581
+ const reopenedAfterLeg = async (leg, cancelledAtDone) => {
582
+ if (freshRunClaimed || !legMayHaveReopenedCheckpoint(leg))
583
+ return false;
584
+ const pending = cancelledAtDone ? false : await confirmCheckpointReopened();
585
+ if (cancelledAtDone || cancelCtrl.cause() !== undefined) {
586
+ await settleCardForCancel();
587
+ return false;
588
+ }
589
+ return pending;
590
+ };
565
591
  let resumeHeartbeat;
566
592
  let resumeManifestLease = 0;
567
593
  let resumeStreamRef;
@@ -685,7 +711,7 @@ export function createResumeLegs(ctx) {
685
711
  const vrNested = nestedPauseOf(vr.terminal);
686
712
  const reParkStatus = nestedParkStatusOf(vr);
687
713
  const reSuspended = vrNested !== undefined;
688
- const vrReopened = isCheckpointReopenedFailure(vr) && (await confirmCheckpointReopened());
714
+ const vrReopened = await reopenedAfterLeg(vr, vrCause !== undefined);
689
715
  if (vrReopened)
690
716
  reopenConfirmedPark = true;
691
717
  if (taskId && deps.runStore) {
@@ -696,7 +722,7 @@ export function createResumeLegs(ctx) {
696
722
  if (reSuspended || vrReopened) {
697
723
  await appendModelUsageDelta(ap, deps.modelUsage, taskId);
698
724
  const vrParkCallId = vrNested !== undefined ? await parkToolCallId({ terminal: vrNested }, deps.checkpointStore) : undefined;
699
- await ap(reParkStatus ?? "suspended", { gate: vrNested?.gate ?? null, ...(vrParkCallId !== undefined ? { toolCallId: vrParkCallId } : {}), ...(vrReopened ? { reopened: failureCodeOf(vr.terminal) ?? null } : {}) });
725
+ await ap(reParkStatus ?? reparkStatus, { gate: vrNested?.gate ?? null, ...(vrParkCallId !== undefined ? { toolCallId: vrParkCallId } : {}), ...(vrReopened ? { reopened: failureCodeOf(vr.terminal) ?? null } : {}) });
700
726
  }
701
727
  else {
702
728
  safe = await attachModelUsage(safe, { append: ap, getEvents: (id, a) => rs2.getEvents(id, a), modelUsage: deps.modelUsage, taskId });
@@ -819,8 +845,12 @@ export function createResumeLegs(ctx) {
819
845
  resumeStreamRef = stream;
820
846
  if (taskId)
821
847
  steerableRuns.set(taskId, stream);
822
- if (!taskId || !rs)
823
- return sealResultForExit(await stream.result());
848
+ if (!taskId || !rs) {
849
+ const rNoRow = await stream.result();
850
+ if (await reopenedAfterLeg(rNoRow, cancelCtrl.cause() !== undefined))
851
+ reopenConfirmedPark = true;
852
+ return sealResultForExit(rNoRow);
853
+ }
824
854
  onAccepted?.(acceptedResumingReceipt(taskId, sessionId));
825
855
  let causeAtDone;
826
856
  for await (const ev of stream) {
@@ -951,10 +981,10 @@ export function createResumeLegs(ctx) {
951
981
  await appendModelUsageDelta(append, deps.modelUsage, taskId);
952
982
  return r;
953
983
  }
954
- if (isCheckpointReopenedFailure(r) && (await confirmCheckpointReopened())) {
984
+ if (await reopenedAfterLeg(r, causeAtDone !== undefined)) {
955
985
  reopenConfirmedPark = true;
956
986
  try {
957
- await append("suspended", { gate: null, reopened: failureCodeOf(r.terminal) ?? null });
987
+ await append(reparkStatus, { gate: null, reopened: failureCodeOf(r.terminal) ?? null });
958
988
  await appendModelUsageDelta(append, deps.modelUsage, taskId);
959
989
  }
960
990
  catch {
@@ -1000,20 +1030,7 @@ export function createResumeLegs(ctx) {
1000
1030
  const refusalCause = e.code === "checkpoint.invalid_outcome" ? decisionRefusalCauseOf(e, outcome.gate) : undefined;
1001
1031
  const disposition = resumeRefusalDisposition(e);
1002
1032
  if (disposition === "cancelled") {
1003
- const cs2 = deps.checkpointStore;
1004
- if (cs2) {
1005
- try {
1006
- const live = await cs2.get(token);
1007
- if (live?.status === "pending" && !(await cs2.expire(token, live.scope))) {
1008
- deps.logger?.warn?.("resume_cancel_checkpoint_settle_lost", { taskId, sessionId });
1009
- }
1010
- }
1011
- catch (cancelSettleErr) {
1012
- deps.logger?.warn?.("resume_cancel_checkpoint_settle_failed", {
1013
- taskId, sessionId, err: cancelSettleErr instanceof Error ? cancelSettleErr.message : String(cancelSettleErr),
1014
- });
1015
- }
1016
- }
1033
+ await settleCardForCancel();
1017
1034
  if (claimedRow && taskId && deps.runStore) {
1018
1035
  const handleCause = cancelCtrl.cause();
1019
1036
  if (handleCause === undefined)
@@ -1035,8 +1052,8 @@ export function createResumeLegs(ctx) {
1035
1052
  settleFleet("failed");
1036
1053
  }
1037
1054
  else if (retriable) {
1038
- await (outcome.gate === "plan_review" ? deps.runStore.setNeedsReview(taskId, null) : deps.runStore.setSuspended(taskId, null)).catch(() => undefined);
1039
- settleFleet(outcome.gate === "plan_review" ? "needs_review" : "suspended");
1055
+ await (reparkStatus === "needs_review" ? deps.runStore.setNeedsReview(taskId, null) : deps.runStore.setSuspended(taskId, null)).catch(() => undefined);
1056
+ settleFleet(reparkStatus);
1040
1057
  }
1041
1058
  else {
1042
1059
  await deps.runStore.setTerminal(taskId, "failed", null, e.message).catch(() => undefined);
@@ -1099,14 +1116,16 @@ export function createResumeLegs(ctx) {
1099
1116
  const sealed = sealResultForExit(result);
1100
1117
  const resumePlane = outwardPlane(sealed.terminal);
1101
1118
  if (taskId && deps.runStore) {
1102
- if (resumePlane.status === "suspended" || checkpointReopened)
1119
+ if (checkpointReopened && reparkStatus === "needs_review")
1120
+ await deps.runStore.setNeedsReview(taskId, null);
1121
+ else if (resumePlane.status === "suspended" || checkpointReopened)
1103
1122
  await deps.runStore.setSuspended(taskId, sealed.terminal.kind === "paused" ? sealed : null);
1104
1123
  else if (resumePlane.status === "needs_review")
1105
1124
  await deps.runStore.setNeedsReview(taskId, sealed);
1106
1125
  else
1107
1126
  await deps.runStore.setTerminal(taskId, resumePlane.status, sealed, resumePlane.errorMessage ?? null);
1108
1127
  }
1109
- settleFleet(checkpointReopened ? "suspended" : resumePlane.status, fleetRunResiduals(sealed));
1128
+ settleFleet(checkpointReopened ? reparkStatus : resumePlane.status, fleetRunResiduals(sealed));
1110
1129
  return {
1111
1130
  status: 200,
1112
1131
  decisionCommitted: true,
@@ -128,7 +128,7 @@ export interface PreparedTaskSubmission {
128
128
  cascade?: boolean;
129
129
  jobId?: string;
130
130
  body: TaskRequestBody;
131
- /** L-167:**装配期**铸下的、这条会话的用户面 `EngineNotice`(今天只有请求腿 MCP 注入被丢弃一族)。
131
+ /** L-167:**装配期**铸下的、这条会话的用户面 `EngineNotice`(今天两族:请求腿 MCP 注入被丢弃;入口地板钳制 `permission.entry_mode_clamped`)。
132
132
  * 装配跑在这条腿的通告口开张之前,所以铸点交不出去 —— 由腿在 `registerEngineNoticeLeg` 那一拍
133
133
  * 经 `pending` 一起投(投递单点仍是那一只注册助手,不新开第二条机制)。缺席 = 本次装配没铸下任何通告。 */
134
134
  engineNotices?: readonly EngineNotice[];
@@ -18,7 +18,7 @@ import { sendJson, sendError, sseHeaders, SSE_MAX_STREAM_MS, SSE_HEARTBEAT_IDLE_
18
18
  import { gatedPrincipal, explicitOperatorOk, isOperator, sendPrincipalRefusal } from "../principal-gate.js";
19
19
  import { governanceOriginOf } from "../../runtime-governance.js";
20
20
  import { deploymentListAsks } from "../../approval.js";
21
- import { answerGrantsBeyondCall, grantBindingOf } from "../../standing-answer-grant.js";
21
+ import { callGrantBindingOf, questionGrantBindingOf } from "../../standing-answer-grant.js";
22
22
  import { sendResumeOutcome } from "../route-ctx.js";
23
23
  import { assertNever } from "../route-ctx.js";
24
24
  import * as facts from "../../capability-facts.js";
@@ -321,7 +321,7 @@ export async function handleApprovalsAssistant(req, res, match, ctx) {
321
321
  return;
322
322
  }
323
323
  const rows = await deps.approvalExemptionStore.list(exSessionId);
324
- sendJson(res, 200, { exemptions: rows.filter((r) => !answerGrantsBeyondCall({ toolName: r.toolName })) });
324
+ sendJson(res, 200, { exemptions: rows.filter((r) => callGrantBindingOf({ toolName: r.toolName }).kind === "none") });
325
325
  return;
326
326
  }
327
327
  if (exToolName === undefined) {
@@ -360,6 +360,7 @@ export async function handleApprovalsAssistant(req, res, match, ctx) {
360
360
  ...(s.boundGrantDigest !== undefined ? { boundGrantDigest: s.boundGrantDigest } : {}),
361
361
  ...(s.boundGrants !== undefined ? { boundGrants: s.boundGrants } : {}),
362
362
  ...(s.classifierNoRuling !== undefined ? { classifierNoRuling: s.classifierNoRuling } : {}),
363
+ ...(s.boundGrantAbsence !== undefined ? { boundGrantAbsence: s.boundGrantAbsence } : {}),
363
364
  objective,
364
365
  input,
365
366
  };
@@ -840,13 +841,14 @@ function projectPendingForWire(row, deployment) {
840
841
  const governance = deployment;
841
842
  const redacted = redactPendingDisclosures(row);
842
843
  const governanceForced = governanceOriginOf({ riskDescriptor: row.riskDescriptor ?? undefined }, governance);
844
+ const deploymentListQuestion = carriesDeploymentListQuestion(row.toolName !== null && deployment !== undefined && deploymentListAsks(deployment, { toolName: row.toolName }), row.classes);
843
845
  const rememberRefused = !standingAnswerDurablyRecordable({
844
846
  governanceForced: governanceForced === true,
845
847
  requiresRealApproval: row.requiresRealApproval === true,
846
848
  mandated: row.mandated === true || row.ruleOffersAbsence === "mandated",
847
- grantBinding: grantBindingOf(row),
849
+ grantBinding: questionGrantBindingOf(row, deploymentListQuestion),
848
850
  classes: row.classes,
849
- deploymentListQuestion: carriesDeploymentListQuestion(row.toolName !== null && deployment !== undefined && deploymentListAsks(deployment, { toolName: row.toolName }), row.classes),
851
+ deploymentListQuestion,
850
852
  });
851
853
  return { ...redacted, ...(governanceForced ? { governanceForced } : {}), ...(rememberRefused ? { rememberRefused: true } : {}) };
852
854
  }
@@ -73,6 +73,7 @@ export async function handleCapabilities(req, res, match, ctx) {
73
73
  const rulesLaneForCaller = facts.ruleLaneForCaller(deps, gatedPrincipal(req, deps.config)).available;
74
74
  sendJson(res, 200, gateCompositeMembers({
75
75
  ...deps.capabilities,
76
+ ...facts.engineCapabilityBits(),
76
77
  asyncRuns: facts.durableRuns(deps).available,
77
78
  artifacts: facts.durableRuns(deps).available,
78
79
  approvals: facts.checkpointPark(deps).available,
@@ -14,7 +14,7 @@ const SYNC_NDJSON_LINE_MAX_BYTES = 8 * 1024 * 1024;
14
14
  const SYNC_IMPORT_BATCH = 500;
15
15
  import { SHA256_HEX_RE } from "../../digest-form.js";
16
16
  import { assertNever, SESSION_NOT_FOUND_MESSAGE } from "../route-ctx.js";
17
- import { readSessionRulesRecords, describeSessionRulesRecordProblem, legacyToolNameProblemOf } from "../../session-policy-wire.js";
17
+ import { readSessionRulesRecords, describeSessionRulesRecordProblem } from "../../session-policy-wire.js";
18
18
  import { readSessionPinned, readSessionTitle } from "../../session-row.js";
19
19
  export function createSessionSyncLocal() {
20
20
  const pendingStagings = new Map();
@@ -232,9 +232,8 @@ export async function handleSessionSync(req, res, match, ctx) {
232
232
  }
233
233
  const policyRead = readSessionRulesRecords(body.policy);
234
234
  if (policyRead.problem !== undefined) {
235
- const legacy = legacyToolNameProblemOf(policyRead.problem);
236
- if (legacy !== undefined) {
237
- sendError(res, 400, "rules.legacy_tool_name", `policy: ${describeSessionRulesRecordProblem(policyRead.problem)}`, { ...legacy });
235
+ if (policyRead.problem.kind === "rules") {
236
+ sendSessionPolicyError(res, policyRead.problem.refusal, `policy: [${policyRead.problem.index}].rules: `);
238
237
  return;
239
238
  }
240
239
  sendError(res, 400, "request.field_invalid", `policy: ${describeSessionRulesRecordProblem(policyRead.problem)}`);
@@ -7,7 +7,7 @@ import { recordFailOpen } from "../../observability/fail-open.js";
7
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
- import { readWritableSessionPermissionRules, describeSessionPermissionRulesProblem } from "../../session-policy-wire.js";
10
+ import { readWritableSessionPermissionRules } from "../../session-policy-wire.js";
11
11
  import { sendJson, sendError, sendSessionError, sendSessionPolicyError, sseHeaders } 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";
@@ -819,22 +819,9 @@ export async function handleSessions(req, res, match, ctx) {
819
819
  return;
820
820
  }
821
821
  const read = readWritableSessionPermissionRules(body.rules);
822
- if (read.problem !== undefined) {
823
- switch (read.problem.kind) {
824
- case "not-an-object":
825
- sendError(res, 400, "request.body_shape", describeSessionPermissionRulesProblem(read.problem));
826
- return;
827
- case "field-not-rule-list":
828
- sendError(res, 400, "request.field_invalid", describeSessionPermissionRulesProblem(read.problem));
829
- return;
830
- case "legacy-tool-name":
831
- sendError(res, 400, "rules.legacy_tool_name", describeSessionPermissionRulesProblem(read.problem), { ...read.problem.refusal });
832
- return;
833
- default: {
834
- const unhandled = read.problem;
835
- throw new Error(`unhandled SessionPermissionRules problem: ${JSON.stringify(unhandled)}`);
836
- }
837
- }
822
+ if (read.refusal !== undefined) {
823
+ sendSessionPolicyError(res, read.refusal);
824
+ return;
838
825
  }
839
826
  if (body.expectedRev !== undefined && (typeof body.expectedRev !== "number" || !Number.isInteger(body.expectedRev) || body.expectedRev < 0)) {
840
827
  sendError(res, 400, "request.field_invalid", "expectedRev must be a non-negative integer");
@@ -84,7 +84,7 @@ export declare function httpErrorCode(status: number, code?: string): string;
84
84
  * 域外通用臂 ⇒ `500 internal.error "internal error"`:码丢,连带**修复路径**也丢(同一行 `PUT` 却答
85
85
  * `500 corrupt` 并点名那一列)。一只腿之后,新站点只会忘记调它,不会再各自发明一份映射。
86
86
  */
87
- export declare function sendSessionPolicyError(res: ServerResponse, e: unknown): boolean;
87
+ export declare function sendSessionPolicyError(res: ServerResponse, e: unknown, sentencePrefix?: string): boolean;
88
88
  /**
89
89
  * S-612 —— core `SessionError` → 应答,经 {@link sessionErrorHttp} 那**一只**映射(`src/session-error-wire.ts`)。
90
90
  * 回 `true` = 已应答,调用点 `return`;`false` = 不是 `SessionError`,或这个词在这一面上是登记过的留白(站点的既有
package/dist/http/send.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { SessionError, SessionPolicyError } from "@sema-agent/core";
2
- import { sessionPolicyErrorHttp } from "../session-policy-wire.js";
2
+ import { sessionPolicyErrorFacts, sessionPolicyErrorHttp, sessionPolicyErrorSentence } from "../session-policy-wire.js";
3
3
  import { sessionErrorHttp } from "../session-error-wire.js";
4
4
  import { redactErrorMessage } from "../observability/run-terminal.js";
5
5
  export const SSE_RESPONSE_MARK = Symbol("sema.http.sse-response");
@@ -48,11 +48,12 @@ export function httpErrorCode(status, code) {
48
48
  default: return status >= 500 ? "internal.error" : "request.rejected";
49
49
  }
50
50
  }
51
- export function sendSessionPolicyError(res, e) {
51
+ export function sendSessionPolicyError(res, e, sentencePrefix = "") {
52
52
  if (!(e instanceof SessionPolicyError))
53
53
  return false;
54
54
  const h = sessionPolicyErrorHttp(e.code);
55
- sendError(res, h.status, h.errorCode, redactErrorMessage(e.message) ?? "session policy error");
55
+ const facts = sessionPolicyErrorFacts(e);
56
+ sendError(res, h.status, h.errorCode, redactErrorMessage(`${sentencePrefix}${sessionPolicyErrorSentence(e)}`) ?? "session policy error", facts === undefined ? undefined : { ...facts });
56
57
  return true;
57
58
  }
58
59
  export function sendSessionError(res, e, face) {
@@ -6,7 +6,8 @@
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, ApprovalReach, McpServerSpec, PermissionMode, SkillSpec, TaskSpec } from "@sema-agent/core";
9
+ import type { A2aServerSpec, AgentDefinition, ApprovalReach, PermissionMode, SkillSpec, TaskSpec } from "@sema-agent/core";
10
+ import type { RequestMcpServerSpec } from "../mcp-assertion-header.js";
10
11
  import type { wiringManifestFaceEventData } from "../trace/project.js";
11
12
  import type { buildSyncSubmitBody } from "./sync-submit-body.js";
12
13
  export interface TaskRequestBody {
@@ -311,7 +312,8 @@ export interface TaskRequestBody {
311
312
  excludeTools?: string[];
312
313
  /** S-1448(core 7.49.0 #1623,[ref]):引擎的**可用性选择器** —— 这一 run 只用列出的工具(wire 名、别名、`Tool(content)` 整只、
313
314
  * `mcp__srv` / `mcp__srv__*` / 工具段通配;`"*"` = 本层不设名单;`[]` = 什么都不给)。宿主**原样透传**进 `TaskSpec.allowTools`:
314
- * 不算补集、不过滤、不与 `settings.permissions.allow`(免问)混词;没有任何工具答应的名字由引擎开跑前拒(`config.tool_face_control`,同步腿 400)。
315
+ * 不算补集、不过滤、不与 `settings.permissions.allow`(免问)混词;没有任何工具答应的名字照跑(原样保留,今天什么都不准,之后挂上同名工具即生效;引擎告知运维一次),
316
+ * 任何工具名都不可能有的拼法由引擎开跑前拒(`config.tool_face_control`,同步腿 400)。
315
317
  * `excludeTools` 是同一只运算的减项(deny 赢)。值形:字符串数组(受理门 400 `request.field_invalid`,与 `excludeTools` 同一只门);
316
318
  * 装着的引擎不认该座 ⇒ 400 `request.field_invalid` 点名所需引擎版本(不静默丢)。无租户门(收窄)。 */
317
319
  allowTools?: string[];
@@ -414,7 +416,7 @@ export interface TaskRequestBody {
414
416
  /**
415
417
  * S-895(7.106.0,core [ref] [ref] ②)—— 模型驱动的 host shell **继承多少用户环境**,由调用方按请求声明。
416
418
  * 闭集两词(词表外 ⇒ 400 `request.field_invalid`,两腿同判,判据属主 `request-value-closure.ts` 的 `inheritEnvFieldIssue`):
417
- * · `"all"` —— 整份继承(`{ ...process.env }` 逐字原样,CC 交互形);只在**单用户**部署兑现,射程 = 写容纳面那一只
419
+ * · `"all"` —— 整份用户环境**减本服务自有凭据名**(配置目录 secret 名 / 名单型旋钮名 / 模型条目 `apiKeyEnv`,起服向引擎声明一次,判在引擎;7.129.0 起,CC 交互形);只在**单用户**部署兑现,射程 = 写容纳面那一只
418
420
  * `hostPostureDeclarationsHonored`(`hosted-posture.ts`)⇒ 托管部署(`REQUIRE_PRINCIPAL=true`)响亮拒
419
421
  * 422 `request.host_posture_refused`;
420
422
  * · `"scrub"` —— 缺省清洗(剥凭据形变量),**任何车道、任何部署**都受理(它就是缺省;7.106.0 合并复审 X7)。
@@ -508,8 +510,15 @@ export interface TaskRequestBody {
508
510
  * 保留 —— 丢的只是声明。core 把这个键定性为**部署面**的键,本仓对它的 reject-or-strip 义务由既有的单用户闸
509
511
  * **结构性**兑现:多租户腿整只 `mcpServers` 被忽略 ⇒ 租户够不着;单用户腿 = core 明文豁免的超管面
510
512
  * (完整论证见 `task-mcp.ts` 的 `assertRequestMcpContentOrigin` 顶注)。部署面的正门是 center 下发的
511
- * `CenterMcpServer.contentOrigin`。 */
512
- mcpServers?: McpServerSpec[];
513
+ * `CenterMcpServer.contentOrigin`。
514
+ *
515
+ * S-1515:`transport.kind:"http"` 的条目可选 `assertionHeader`(托管连接器断言头的**头名**,契约包 settings-schema ≥12.2.0 同键)。
516
+ * 与 `principalHeader` **互斥**:同设 ⇒ 新提交腿同步 400 `request.body_shape`(拒句与契约包解析期那一句逐字相同);值不是字符串 ⇒ 400
517
+ * `request.field_invalid`。耐久 resume 腿不拒(重放存量 body),也**不摘 server**(摘了,park 在它工具审批上的任务批准后撞 `resume.tool_unavailable`
518
+ * 且每次重试重放同一份 body):降级到升级前的读法 —— server 照挂、`principalHeader` 胜、`assertionHeader` 不生效,一行 warn `mcp_assertion_header_ignored`。
519
+ * ⚠️ 本版**只承载、不发送**:引擎的 http 传输尚无断言头座,
520
+ * 声明被接收、server 照常挂载但不带断言头,并有一行运维 info `mcp_assertion_header_not_sent { server }`;发送随引擎座。 */
521
+ mcpServers?: RequestMcpServerSpec[];
513
522
  /** [DESIGN-269 车1] Caller-supplied per-request A2A peers (remote agents this task may call), aligned to
514
523
  * core `A2aServerSpec`. 🔒 honored on a SINGLE-USER deployment that has neither locked the `a2a` key nor
515
524
  * had `a2a_peers` denied by its compliance posture (task-a2a.ts `a2aInjectionHonored`, the same three-veto
package/dist/index.d.ts CHANGED
@@ -24,7 +24,7 @@ export { createAuthorizer, principalFrom, HttpError, type AuthContext, type Owne
24
24
  export { memoryScopeFor, memoryEngineBackendFor } from "./memory-scope.js";
25
25
  export type { TaskRequestBody } from "./http/wire-types.js";
26
26
  export { createHttpServer, type ServiceDeps, type RequestAuth, type FlatServiceDeps, type ServiceDepGroups, type ServiceCoreDeps, type ServiceStoreDeps, type ServiceCoordinatorDeps, type ServiceSeamDeps, type ServiceObservabilityDeps, type ServiceGovernanceDeps, type ServiceDeploymentDeps, type ServiceKnobDeps, } from "./http/server.js";
27
- export { createSideQueryLane, createPerModelKeyBrain, type KeyResolver, type SideQueryLane, type SideQueryLaneCtx } from "./boot/side-query-lane.js";
27
+ export { createSideQueryLane, type KeyResolver, type SideQueryLane, type SideQueryLaneCtx } from "./boot/side-query-lane.js";
28
28
  export { createKeyResolver, resolveModelApiKey, type ModelKeyRef } from "./key-resolver.js";
29
29
  export { isModelKeyPoison, ModelKeyPoisonedError, type ModelKeyPoison, type ModelKeyPoisonReason } from "./key-resolver.js";
30
30
  export { CATALOG_ENTRY_HEADER_LANES, CATALOG_ENTRY_HEADER_NAMES, type CatalogEntryHeaderName } from "./model-entry-headers.js";
package/dist/index.js CHANGED
@@ -21,7 +21,7 @@ export { ensureTiDBTaskListSchema, ensurePgTaskListSchema } from "./plugins/task
21
21
  export { createAuthorizer, principalFrom, HttpError, } from "./security.js";
22
22
  export { memoryScopeFor, memoryEngineBackendFor } from "./memory-scope.js";
23
23
  export { createHttpServer, } from "./http/server.js";
24
- export { createSideQueryLane, createPerModelKeyBrain } from "./boot/side-query-lane.js";
24
+ export { createSideQueryLane } from "./boot/side-query-lane.js";
25
25
  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";
package/dist/main.js CHANGED
@@ -4,6 +4,7 @@ import { runStage02 } from "./boot/stage-02-web-admin-pull.js";
4
4
  import { runStage03 } from "./boot/stage-03-store-backend.js";
5
5
  import { runStage04 } from "./boot/stage-04-stores.js";
6
6
  import { runStage05 } from "./boot/stage-05-execution-env.js";
7
+ import { settleHostLaneShell } from "./boot/execution-lane.js";
7
8
  import { runStage06 } from "./boot/stage-06-runners.js";
8
9
  import { runStage07 } from "./boot/stage-07-capability-layer.js";
9
10
  import { runStage08 } from "./boot/stage-08-reapers.js";
@@ -15,6 +16,7 @@ async function main() {
15
16
  const s03 = await runStage03(s02);
16
17
  const s04 = await runStage04(s03);
17
18
  const s05 = runStage05(s04);
19
+ await settleHostLaneShell(s05.config, s05.logger);
18
20
  const s06 = runStage06(s05);
19
21
  const s07 = await runStage07(s06);
20
22
  const s08 = runStage08(s07);