@sema-agent/server 7.35.1 → 7.36.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 (53) hide show
  1. package/README.zh-CN.md +1 -1
  2. package/USAGE.md +10 -1
  3. package/dist/boot/config-center.js +14 -4
  4. package/dist/boot/org-memory.d.ts +21 -0
  5. package/dist/boot/org-memory.js +1 -1
  6. package/dist/boot/parked-revive-gate.d.ts +16 -1
  7. package/dist/boot/parked-revive-gate.js +33 -68
  8. package/dist/boot/runner-deps.d.ts +1 -1
  9. package/dist/boot/runner-deps.js +18 -3
  10. package/dist/boot/side-query-lane.d.ts +124 -0
  11. package/dist/boot/side-query-lane.js +158 -0
  12. package/dist/boot/webfetch-summarize-lane.d.ts +60 -0
  13. package/dist/boot/webfetch-summarize-lane.js +67 -0
  14. package/dist/brain.js +15 -1
  15. package/dist/config-types.d.ts +27 -2
  16. package/dist/config.js +13 -3
  17. package/dist/degenerate-instrument.d.ts +13 -1
  18. package/dist/degenerate-instrument.js +13 -1
  19. package/dist/http/active-run-conflict.js +16 -0
  20. package/dist/http/routes/a2a-serve.js +12 -2
  21. package/dist/http/routes/admin-drain.d.ts +17 -0
  22. package/dist/http/routes/admin-drain.js +7 -1
  23. package/dist/http/routes/approvals-assistant.js +3 -3
  24. package/dist/http/routes/capabilities.js +11 -2
  25. package/dist/http/routes/rules.js +7 -4
  26. package/dist/http/routes/runs.js +55 -8
  27. package/dist/http/routes/sessions-list.js +1 -1
  28. package/dist/http/routes/sessions.js +4 -4
  29. package/dist/http/routes/side-query.js +6 -1
  30. package/dist/http/server.d.ts +18 -5
  31. package/dist/http/server.js +5 -1
  32. package/dist/index.d.ts +3 -0
  33. package/dist/index.js +14 -0
  34. package/dist/main.js +17 -28
  35. package/dist/memory-posture.d.ts +14 -1
  36. package/dist/memory-posture.js +2 -0
  37. package/dist/observability/fail-open.d.ts +12 -0
  38. package/dist/observability/fail-open.js +12 -0
  39. package/dist/parked-decide.d.ts +3 -1
  40. package/dist/parked-decide.js +34 -11
  41. package/dist/rules-consent.d.ts +20 -0
  42. package/dist/rules-consent.js +21 -0
  43. package/dist/security.d.ts +22 -0
  44. package/dist/security.js +24 -0
  45. package/dist/shared-memory-scope-authorizer.d.ts +36 -3
  46. package/dist/shared-memory-scope-authorizer.js +19 -4
  47. package/dist/store-live-probe.d.ts +26 -0
  48. package/dist/store-live-probe.js +60 -0
  49. package/dist/task-cwd.d.ts +16 -3
  50. package/dist/task-cwd.js +16 -3
  51. package/dist/tool-approval.d.ts +61 -2
  52. package/dist/tool-approval.js +159 -17
  53. package/package.json +2 -2
package/README.zh-CN.md CHANGED
@@ -139,7 +139,7 @@ curl -s localhost:8090/v1/tasks -H "Authorization: Bearer <SERVICE_AUTH_TOKEN>"
139
139
  | `MODEL_ID` | **必填** | 缺省模型 id ——**3.0.0 起无出厂缺省**。未设 = 启动即失败并指路该旋钮(旧的烤死缺省是内网模型名,外部部署必炸且炸在离根因最远处:网关 `400` + 标题 hook 连环告警)。填你的网关真正提供的模型名,或改由配置控制面下发目录 |
140
140
  | `MODEL_API_KEY` | — | 网关 key(可选) |
141
141
  | `SERVICE_AUTH_TOKEN` | — | 调用方需带 `Authorization: Bearer <token>` |
142
- | `DB_BACKEND` | `mysql` | SQL 引擎:`mysql`(任何 MySQL 协议库:MySQL/TiDB/MariaDB;`tidb` 为兼容别名)/ `pg`(PostgreSQL)/ `local`(免 DB 文件持久化)。显式设置 `mysql`/`pg` 时 session 自动转 durable |
142
+ | `DB_BACKEND` | `local`* | SQL 引擎:`mysql`(任何 MySQL 协议库:MySQL/TiDB/MariaDB —— TiDB 走这个值,**没有** `tidb` 别名,写它启动即拒)/ `pg`(PostgreSQL)/ `local`(免 DB 文件持久化)/ `memory`(显式纯内存)。显式设置 `mysql`/`pg` 时 session 自动转 durable。*单租户裸 boot 缺省 `local`;`REQUIRE_PRINCIPAL=true` 的多租户裸 boot 缺省 `memory`(local 与多租户互斥) |
143
143
  | `SESSION_BACKEND` | `memory`* | `memory` / `mysql`(durable 会话中心)/ `auto`。*显式 `DB_BACKEND=mysql/pg` 时默认转 durable |
144
144
  | `REMOTE_EXEC` | 未设 | 沙箱执行通道:`host` / `local-docker` / `e2b` / `k8s` / `ssh` / `adb`;未设 = 进程内 stub(`CONFIG_PROVIDER=local` 时缺省转 `host`)。点名了通道但必需 env 不全(如 `e2b` 缺 `E2B_API_KEY`)或值不在闭集内 ⇒ **启动即拒**——不再静默降级到 host/进程内通道(#157 fail-closed) |
145
145
  | `CONFIG_PROVIDER` | 未设 | 配置来源:`local`(单机文件 `config.d/`)/ `remote`(registry 控制面) |
package/USAGE.md CHANGED
@@ -123,7 +123,9 @@ MODEL_EXTRA_BODY='{"top_k":40}' # JSON 逃生口(top_k / logit_bias 等;penalt
123
123
  ```
124
124
  - **brain 拥有的键永远赢**:`temperature`/`max_tokens` 等放进 `extraBody` 会被 strip + core 警告(`phase:"config"`),不会静默改;auth/content-type/version 头硬锁不可顶替(1.60 安全修复)。
125
125
  - **必须静态**:`extraBody` 在 boot 时按固定 env 建一次(稳定键序),**不可逐任务变**,否则破前缀缓存(design/9/31)。未设 → 请求字节级不变。
126
- - **退化打捞**:模型尾部循环退化时 core 切断,任务 `status:"failed"` + `errorCode:"output.degenerate"`,但 `salvagedOutput` 带着那一退化 turn 的**整段**文本(好内容 + 一段垃圾尾,**当前不裁尾**)。调用方读法:`status==="completed" ? result : salvagedOutput`。penalty 是预防、这是兜底。
126
+ - **退化打捞**:模型尾部循环退化时 core 切断,任务 `status:"failed"` + `errorCode:"output.degenerate"`,`salvagedOutput` 带回那一退化 turn 的文本,**尾部已由 core brain 流层裁掉**(逐字节重复只留一份重复单元 —— 声明为**有损**归一:不丢唯一字节,但重复**次数**不保真,「输出 N 份」这类语义拿回来只有一份)。
127
+ 🔴 **调用方读法(core 官方配方,别拿 `status` 三元代替)**:`const out = r.result || r.salvagedOutput || "";`
128
+ —— 先读 `result`,空了才回落。理由:`salvagedOutput` 只在**一张七员闭集**的失败终局上在场(逐字取自 core 的 `SALVAGE_ELIGIBLE_TERMINALS`:`output.degenerate`、`limits.max_tokens_exceeded`、`limits.max_cost_exceeded`、`limits.max_turns_exceeded`、`limits.max_walltime_exceeded`、`env.lifetime_expired`、`usage.window_exhausted`),**闭集之外**的失败终局(模型/供应商错 `provider.error`、`conflict`、未铸码的失败……)它**整键缺席**,而最终助手文本仍在 `result` 里;闭集**之内**它又与 `result` 取自**同一条** final message 文本。两头都指向同一条读法:先 `result`。只读 `salvagedOutput` 会在闭集外的每一条失败路径上把真产出当成"没有输出"丢掉。penalty 是预防、这是兜底。
127
129
 
128
130
  **可选 — OTLP/HTTP 指标导出(core 1.37 可观测)**
129
131
  ```bash
@@ -196,6 +198,7 @@ MODEL_CODE_ROLES=default,subagent # 不设=全中立;仅这些角色在「
196
198
  | `MEMORY_ORG_DIRECTORY_JSON` | 缺省 | 单机形静态授权表 `{"<principal>":{"org:acme":{"write":true}}}`。**启动期整表校验,坏表拒启动**。有 config-center 时被忽略(center 胜 + warn) |
197
199
  | `MEMORY_ORG_GRANT_TTL_MS` | `60000`(`[1000, 3600000]`) | 授予/负结果的缓存 TTL = 「有界 LKG」:**新任务/新 resume 腿**的准入判决滞后 ≤ 此值;**在跑任务不受吊销影响**(判决点在 prepare 期) |
198
200
  | `MEMORY_ORG_UNAVAILABLE_BACKOFF_MS` | `10000`(`[500, 600000]`) | 目录取不到之后的退避窗(只用于取数失败臂,不用于负结果) |
201
+ | `MEMORY_DELEGATION_EVIDENCE` | 缺省(=引擎自缺省 `static-face`) | **委派臂的证据标准**(core 5.45.0 design/324)。`static-face`=今日行为:委派的 attestation 缺失/未知**且**静态工具面够得着外部内容 ⇒ 标记本会话记忆为已污染(可能性即暴露)。`attested-only`=**只**豁免那一条静态面标记,且**真的豁免了一次**才响亮通告(`engine_notice` 的 `memory.delegation_static_mark_waived`,每个 prepared leg **至多**一次);送达 `external` attestation 照标、非委派的污染类工具照标、委派工具自身被分类为污染的照标。⚠️ 通告**不是 leg 计数器**:不含委派的 leg、污染面没挂载的 leg、送达了 attestation 的委派都不发 —— 看不到通告的常态含义是「没有可豁免的事」,不是「配置坏了」。坏值**启动期拒**。**部署席 only**(TaskSpec 无同名键,任务/受治脚本无法据此放松部署)。⚠️ **已接受的代价**:`attested-only` 下**后台**子代的真实外部接触不标记本会话(其内容经 TaskOutput / 任务通知注入 / AgentTranscript 摘要回流,三条都不带 attestation);异常收尾的前台子代同理。已标记的会话永不回滚清洗。运维读面 = `GET /v1/diagnostics/wiring` 的 `memoryPosture.delegationEvidence`(`null`=本部署未设,引擎自缺省) |
199
202
 
200
203
  ⚠️ **拒启动**:多租户 + 记忆面点亮 + `projects[].defaultScopes` 里有 org 键 + 目录源缺席 ⇒ 启动报错
201
204
  并点名 projectId(该部署的每个此类请求都会在 prepare 期整拒,响亮拒启动比静默全拒服务诚实)。
@@ -221,6 +224,11 @@ MODEL_CASCADE_LADDER=deepseek-flash,deepseek-pro # 目录里的模型名,cheap
221
224
  默认 15000;0=关;负/非数启动响亮拒)缓存一份 DB 真往返结果,`storeProbe:{live,ageMs,error?}` 在
222
225
  探针接线时恒在,顶层告警键 `storeLive:false` 只在死时出现。**status 保持 "ok"**(liveness≠readiness
223
226
  ——DB 死不是进程死,摘流语义留给读键的编排器/LB)。local/memory 部署形状不变。
227
+ `error` 是**闭集词**(7.36.0 起,#305):`probe_timeout` / `connection_refused` / `connection_reset` /
228
+ `host_unreachable` / `dns_failure` / `network_timeout` / `auth_failed` / `connection_limit` /
229
+ `probe_failed`(未识别)。**驱动原始异常 message 不上这一面**——`/health` 免鉴权且缺省全网卡监听,而
230
+ 连接类异常惯于回声整条 DSN(`mysql://user:pass@host:3306/db`)。要读全文去**日志**:探针在
231
+ live→dead 翻转拍打 `store_probe_dead`,`error` 字段是原文。
224
232
  钉:`test/health-store-live.test.ts` / `test/store-live-probe.test.ts`。
225
233
  - 数据驻留提示:`DB_BACKEND=local` 下显式 `SESSION_BACKEND=memory` 会被收编为 **durable(local)**
226
234
  (1.292+ 裸 boot 默认 durable;/health 的 `sessionBackend` 报 `durable(local)`)——session 行落盘在
@@ -662,6 +670,7 @@ curl -N http://<host>:8090/v1/tasks/stream -H 'content-type: application/json' \
662
670
  | `POST /v1/approvals/<sessionId>/decide` `{"decision":"approve"|"deny","reason":"…"}` | 批/否并恢复挂起任务(CAS,重复决议 409);**仅 operator**(非 operator → 403)。5.0.0 起旧轮询腿 `POST /v1/approvals/<id>` 已退役 |
663
671
  | `GET /v1/agents/roster` | 后台 agent **名册**(7.29 起,需 durable background-agent store,否则 501 `capability.background_agent_store_required`):调用方自己 scope 的 `a…` 行,新→旧。行只带 `handle`/`name`/`agentType`/`status`/`spawnedAt`/`updatedAt`/`settledAt`(content-free;子代产物正文走 `GET /v1/runs/<id>/subagents/<handle>/output`)。查询:`?limit=`(1..100,默认 20)、`?before=`(上一页的 `nextBefore` 游标)、`?status=`(running\|parked\|completed\|failed\|killed)、`?session=`(按会话树窄化)、`?scope=`(**仅** service-token/单用户形;带 principal 的调用方钉死自己)。回体 `{agents,total,nextBefore?,truncated?}`;`truncated:true` = 租户行数超过名册窗(500),`total` 此时是窗内数。非属主的行**不出现也不计数**(无存在性 oracle) |
664
672
  | `GET /v1/capabilities/scenarios/<name>` | 单个场景的只读详情:工具面、提示词概览、**本部署现在跑不跑得动**(见下) |
673
+ | `POST /v1/admin/drain` `{"reason":"…"}` | **停机因由显式声明**(7.35.0+;operator-only —— principal 须在 `OPERATOR_PRINCIPALS` 内,空名单 = 谁都不是 operator ⇒ 恒 403)。编排壳在发 SIGTERM **之前**调它;server 记进程内 `drainState.reason`,draining 期由 `/health` 的 `drainReason` 与 503 `draining` 体的 `reason` 透出(未声明 = 两面键缺席)。声明是**覆盖式**、无 TTL、随进程重启清零 ⇒ 每次停机流程内都要重新声明。`reason` 非空、trim 后 ≤256 字符,坏值 400。🔴 **写口是特权的,读口不是**:`/health` 免鉴权(见下一行)且缺省全网卡监听 —— 任何能连到这个端口的人都读得到你写的那句话。**别在 reason 里写工单号、内部主机名或任何内部标识**。 |
665
674
  | `GET /health` | 健康(无需鉴权) |
666
675
  | `GET /metrics` | Prometheus 指标(有 token 时需带) |
667
676
 
@@ -824,6 +824,15 @@ export async function createConfigCenterRuntime(ctx) {
824
824
  // 对账/etag 推进/LKG 落盘都不得从旁路半应用同一个被拒世代(拒绝 warn 已在 applyEffective 留痕)。
825
825
  if (!committed)
826
826
  return;
827
+ // 🔴 #303 codex R1-F2(真 finding,红先钉在 test/apply-effective-stage-commit.test.ts):
828
+ // commit 的**派生两件**必须紧贴 commit,零 await —— 修前它们在下面 `await adoptCenterPrompts`
829
+ // 之后,而 commit 已经把 `config.models` 就地换代(三个消费面实时读)、把
830
+ // `modelApiKeyEnv/modelApiKeys` 整表重赋值,老 `keyResolver` 却还闭包着**上一世代**的表。
831
+ // 那段 await 里同名模型换端点+换 key 的下发 = 旧密钥发到新端点(401,或把上一家的凭据
832
+ // 递给下一家);单纯换 key = 一窗瞬时 401。现在目录与密钥表成对换代。
833
+ // (plane 被 defer 时 commit 压根没动模型面 ⇒ 这里按同一张旧表重建,内容不变,与修前同。)
834
+ mutateInPlace(pricing, buildPricing(config.models)); // hot: cost/model changes; Runner reads this.deps.pricing live
835
+ keyResolver = createKeyResolver(config.modelApiKeyEnv, process.env, config.modelApiKeys); // hot: per-model key add/remove/change (env-ref + sealed)
827
836
  if (planeDeferred) {
828
837
  logger.warn("models_tiers_plane_deferred", { version: r.effective.version, note: "tier-frozen Runner: the changed model plane (models/roles/tiers/default) is NOT hot-applied — admission stays on the Runner's generation; restart applies the new plane (models-tiers restart signal rides /health)" });
829
838
  }
@@ -838,8 +847,7 @@ export async function createConfigCenterRuntime(ctx) {
838
847
  // process applies the candidate pre-Runner and opens it at boot.
839
848
  if (!planeDeferred)
840
849
  markRosterLanded(r.effective); // boot pull 失败/未发布时,refresh 落 roster 同样开门
841
- mutateInPlace(pricing, buildPricing(config.models)); // hot: cost/model changes; Runner reads this.deps.pricing live
842
- keyResolver = createKeyResolver(config.modelApiKeyEnv, process.env, config.modelApiKeys); // hot: per-model key add/remove/change (env-ref + sealed)
850
+ // (pricing/keyResolver 的重建已上提到 commit 紧邻处 —— 见上方 #303 R1-F2 注)
843
851
  // skills/mcp/scenarios/runtime-gates are baked into the live process at boot (buildScenarios / boot
844
852
  // middleware) — a refresh carrying a DIFFERENT value can't hot-apply, only a restart re-reads them.
845
853
  // We compare against the BOOT snapshot (`effective`), NOT presence: an orchestrator auto-restarts on
@@ -1001,6 +1009,9 @@ export async function createConfigCenterRuntime(ctx) {
1001
1009
  // [2283]③(refresh 腿同款):CAS 拒绝 ⇒ 迟到 boot 拍整体跳过,旁路消费与 etag/LKG 都不动。
1002
1010
  if (!committedLate)
1003
1011
  return;
1012
+ // #303 R1-F2 的 late-boot 双胞胎(同一形,只修一条=另一条继续漏):派生两件紧贴 commit,零 await。
1013
+ mutateInPlace(pricing, buildPricing(config.models));
1014
+ keyResolver = createKeyResolver(config.modelApiKeyEnv, process.env, config.modelApiKeys);
1004
1015
  if (planeDeferredLate)
1005
1016
  logger.warn("models_tiers_plane_deferred", { version: r.effective.version, note: "tier-frozen Runner (env tiers): the late-boot center model plane is NOT hot-applied — restart applies it" });
1006
1017
  else {
@@ -1010,8 +1021,7 @@ export async function createConfigCenterRuntime(ctx) {
1010
1021
  await adoptCenterPrompts(r.effective, "boot-deferred");
1011
1022
  if (!planeDeferredLate)
1012
1023
  markRosterLanded(r.effective); // codex R13: same guard as the refresh lane — never open readiness off an unapplied plane
1013
- mutateInPlace(pricing, buildPricing(config.models));
1014
- keyResolver = createKeyResolver(config.modelApiKeyEnv, process.env, config.modelApiKeys);
1024
+ // (pricing/keyResolver 的重建已上提到 commit 紧邻处 —— 见上方 #303 R1-F2 注)
1015
1025
  effective = r.effective; // restart 比较基线=到货值(cadence 不再重复触发)
1016
1026
  latestEffective = r.effective;
1017
1027
  ccEtag = r.etag;
@@ -29,7 +29,28 @@ import type { Metrics } from "../observability/metrics.js";
29
29
  import { type OrgMemoryDirectory } from "../org-memory-admission.js";
30
30
  export interface OrgMemoryAdmissionWiring {
31
31
  memoryScopeAdmission: RunnerDeps["memoryScopeAdmission"];
32
+ /**
33
+ * core 准入 seam 的 deployment-origin **回落表**(boot 期一次值快照)。
34
+ *
35
+ * 🔴 A-057.40:这一席**故意**留快照,不跟 `config.projects` 的热应用走,理由是 core 根本不优先吃它:
36
+ * 本仓每个请求都在 `boot/resolve-spec.ts` 现读 `config.projects[...].defaultScopes`(热加的项目当场
37
+ * 进 spec),并在 `memory-scope.ts` 按 `originPolicy.multiTenant` 给每个 `org:` 键**盖章**;core 的
38
+ * `memory-admission` 先看盖章、只有没盖章时才查这张表 ⇒ 单用户部署下热注册项目的 org 键恒被盖成
39
+ * `deployment`,这张表陈旧只会让 `governed` / `orgGovernedProvenance` 两个布尔偏保守,**不产生拒绝**。
40
+ * 需要跟着热应用走的是另一个消费点(共享记忆读面)—— 见 {@link deploymentMemoryScopesLive}。
41
+ */
32
42
  deploymentMemoryScopes: RunnerDeps["deploymentMemoryScopes"];
43
+ /**
44
+ * 同一份判据的**取值口**(A-057.40 修):`createSharedMemoryScopeAuthorizer` 的
45
+ * `deploymentScopes` 席用它,每次 `resolve` 现算。
46
+ *
47
+ * 为什么这个消费点必须活取而上面那个可以不:共享记忆读面对 principal-less 请求**直接**把这组 scope
48
+ * 当作全部授权事实交出去(没有第二道盖章腿能纠正它)⇒ 快照在新增方向让新登记的 org 库变成终局式
49
+ * 空集、在撤销方向让已下架的 scope 继续被放行(fail-open),而 `config.projects` 恰恰是 config-center
50
+ * 标注「🟢 热生效」的字段、且 env 腿恒空(只可能由 center 灌)。判据本体与快照同源(同一个
51
+ * `collectDeploymentOrgScopes`),差别只有取值时机。
52
+ */
53
+ deploymentMemoryScopesLive: () => readonly string[];
33
54
  /** §7 第二消费者收编:同一个目录实例也喂 memory-policy 面的 `org:` 属主门(`ServiceDeps.orgMemoryDirectory`)。
34
55
  * **同实例**是这条收编的全部意义——「谁属于 org:acme」在一个进程里必须只有一个答案,两面共享同一份
35
56
  * TTL 缓存/退避窗/gen 高水位;各建各的目录会让准入门与策略面在吊销窗内公开分歧。缺席=无目录源
@@ -82,6 +82,6 @@ export function createOrgMemoryAdmissionWiring(opts) {
82
82
  },
83
83
  })
84
84
  : undefined;
85
- return { memoryScopeAdmission, deploymentMemoryScopes, directory };
85
+ return { memoryScopeAdmission, deploymentMemoryScopes, deploymentMemoryScopesLive: () => collectDeploymentOrgScopes(config), directory };
86
86
  }
87
87
  //# sourceMappingURL=org-memory.js.map
@@ -123,5 +123,20 @@ export type RebuiltParentConstraint = NonNullable<RebuiltInheritedGate["parentCo
123
123
  * ask 门、scratchpad 豁免……整类)park 时未持久化 ⇒ 重建不出。§2.3 已披露的残余,机制面有特征化钉
124
124
  * (test/parked-revive-e2e.test.ts);真 settings→park→赎回的端到端钉未建(候件,随收口件一起)。
125
125
  */
126
- export declare function createParkedReviveInheritedGate(deps: ParkedReviveGateDeps): (row: BackgroundAgentRecord) => Promise<RebuiltInheritedGate>;
126
+ /**
127
+ * 🔴 A-057.44 第二半(codex 对抗复审 medium,验真后采纳)—— 席位多收一位**权威 principal**。
128
+ *
129
+ * 为什么不能只看 `row.scope`:`principal ?? "default"` 这套约定在那一列上是**有损**的(匿名与一个真名叫
130
+ * `default` 的租户同形),而匿名别名匹配(见 `parked-decide.ts` 的 `backgroundScopesForCheckpointScope`)
131
+ * 让匿名行第一次真的走到本函数。解错 principal 的后果不是拒绝,是**解到另一份 caps** ⇒ 与 park 时记的
132
+ * 摘要对不上 ⇒ 刚刚变得可达的那批卡又赎不回(方向仍 fail-closed,但那是白丢一次赎回)。
133
+ * 权威值来自 **checkpoint 的 scope**(`decodeCheckpointScope(cp.scope)`)—— 那一列由
134
+ * `encodeCheckpointScope(auth?.principal)` 铸,与 core 喂给 `runtimeCapsResolver` 的是同一个值,无损。
135
+ * 参数**必填**(不给可选默认值):唯一的生产调用方是 `decideParkedAgent`,漏传要是编译错而不是静默回退。
136
+ */
137
+ export interface ParkedReviveIdentity {
138
+ /** `decodeCheckpointScope(cp.scope)`:匿名 ⇒ `undefined`,有 principal ⇒ 逐字那一个。 */
139
+ readonly principal: string | undefined;
140
+ }
141
+ export declare function createParkedReviveInheritedGate(deps: ParkedReviveGateDeps): (row: BackgroundAgentRecord, identity: ParkedReviveIdentity) => Promise<RebuiltInheritedGate>;
127
142
  //# sourceMappingURL=parked-revive-gate.d.ts.map
@@ -29,56 +29,14 @@ import { DeferredSandboxPathEnv, isSandboxPathAdjudicationLane, sandboxPathEnvSl
29
29
  function hostGateCwd(config, localRoot) {
30
30
  return join(config.localDataRoot ?? localRoot, "parked-revive-unrooted");
31
31
  }
32
- /**
33
- * 赎回腿的父约束链重建(design/181 件二)。
34
- *
35
- * 语义:**同/跨副本一致地重建「部署 ⊇ 操作员」两层完整链**——审批基线(durable question 门 + F4 高危写
36
- * 审批门 + 会话豁免探针)作 base 的 `toolPolicy` 座,经**折叠属主** `applyRuntimeGovernance`
37
- * (→ core `tightenTaskSpec`)叠上部署治理段(autonomy / commandPolicy / MANUAL_MODE_SHELL_GATE /
38
- * 守卫集),取其 `toolPolicy` 装进单层 `parentConstraints`。
39
- *
40
- * 🔴 **禁 memoize**:构造整体在 per-row lambda **内**。`autonomy`/`commandPolicy`/守卫集/审批四旋钮都是
41
- * 热改字段(registry 热应用换 config 引用),boot 期铸一次 = 把治理冻在启动那一刻的值上,而 resume 腿的
42
- * 书面语义是「按**当前** config 重折」(与 resolve-spec 的审批基线读活 config 同一姿势)。
43
- *
44
- * 🔴 **单层链是正确的响亮拒**(design/181 §2.5 特征化格):本腿恒建 count=1。嵌套子代(孙代 park,
45
- * count≥2)的重建本腿做不到——跨副本连「祖先各层分别是什么」都没有持久化——于是 core 的 pre-CAS
46
- * `resume.parent_constraint_mismatch` 会响亮拒绝,checkpoint 留 pending。**不许**顺手把它补齐成静默单层:
47
- * 那等于让孙代在一条比它挂起时更松的祖先链下复活。
48
- *
49
- * 交回**两条轴**(载体上本仓供得出的全部,见 {@link RebuiltInheritedGate} 的推导注):
50
- * · `parentConstraints` —— 上面那条重建链;
51
- * · `shellGate` —— 治理段折出来的 shell 门档位。**必须交**:core 按 rank 取 `max(live, seed)`,不交
52
- * 等于 `live` 缺席、max 恒等于 park 时的旧种子,于是「挂起期间运维收紧 AUTONOMY/MANUAL_MODE_SHELL_GATE」
53
- * 在赎回腿上整条失效(codex R1-高2 的真回归,红先复现)。交了之后收紧被兑现、放松不被兑现(fail-safe)。
54
- *
55
- * 载体上**故意不供**的三个槽(都不是本腿产得出的):`ancestorRules`(会话权限规则,属会话面)、
56
- * `admittedOrgScopes` / `orgAdmissionGoverned`(组织记忆准入,属 memory 面)—— 交一个瞎猜的值比不交更坏
57
- * (core 对这三个都是 seed 兜底)。
58
- *
59
- * 时点语义(成文进 docs/ASSISTANT-WIRE-CONTRACT.md,免运维当 bug 报):
60
- * · toolPolicy 轴 = **当前** config(与 resume 重折一致,收紧与放松都被兑现——放松是一次显式运维动作);
61
- * · shellGate 轴 = core 取 max(live, park 时 seed)—— 收紧兑现、放松**不**兑现(fail-safe)。
62
- * · `autoModeArmed` 轴(A-010.1)= 按**当前** per-principal entitlement 现解(见 {@link autoModePostureOf});
63
- * 与 toolPolicy 轴同族:挂起期间权益被撤/被授 ⇒ 摘要对不上 ⇒ core pre-CAS 响亮拒,不静默换姿势跑。
64
- * · `onAsk` 轴 = 部署的活体审批席(`deps.approverSeat`),**在场即供**:park 时那一层冻的就是它。
65
- * 席位缺席的部署上不供,而**这不等于 auto-deny**(codex R3-中1 验真纠正):此形恒带
66
- * `durableMandate`(见 {@link mandatePostureOf}),继承层判 `ask` 时 core 只要还看得见 checkpoint 店
67
- * + durable 审批配置就把它**重新浮回 durable 门**(再 park 一次,人再批一次);deny 只是「durable
68
- * 设施不在场」的兜底臂。⛔ 但**不是无条件**(codex R4-中1):core 对挂起链计 `suspendCount`,到
69
- * `maxSuspends`(默认 5)即终止任务而不再 park。
70
- * · 客户端每请求 settings 折出来的层(`settings.permissions` 的 allow/deny/ask、模式派生的 fs-write
71
- * ask 门、scratchpad 豁免……整类)park 时未持久化 ⇒ 重建不出。§2.3 已披露的残余,机制面有特征化钉
72
- * (test/parked-revive-e2e.test.ts);真 settings→park→赎回的端到端钉未建(候件,随收口件一起)。
73
- */
74
32
  export function createParkedReviveInheritedGate(deps) {
75
33
  const { config, question, approvalExemptionStore, logger, localRoot, approverSeat } = deps;
76
34
  const slots = deps.slots ?? sandboxPathEnvSlots;
77
- return async (row) => {
35
+ return async (row, identity) => {
78
36
  // A-010.1:三位决议链元数据。**async 的唯一理由**就是其中两位要按 principal 现解 entitlement
79
37
  // (core 的 resolver 座本身也是 sync-or-async),而这条腿的消费点(parked-decide 的 `decideParkedAgent`)
80
38
  // 从来就在 async 里,「席位必须同步」是本文件此前自设的约束,不是结构事实。
81
- const metadata = await chainMetadataOf(deps, row);
39
+ const metadata = await chainMetadataOf(deps, row, identity);
82
40
  // lane 分形与 resolve-spec 同一判别式(单一属主,取值处只此一个)。
83
41
  // 🔑 沙箱腿的 slot 键 = **row.sessionId**(子代自己的会话,design/181 §2.2 裁定):revive 后 core 以
84
42
  // 该 sessionId 铸 env 并注册 slot(subagent → prepare-task → 工厂装饰器),这个键真能绑上。
@@ -159,8 +117,8 @@ export function createParkedReviveInheritedGate(deps) {
159
117
  * ⚠️ 时点语义与 toolPolicy 轴同族:两位都按**当前** entitlement 现解,挂起期间被撤/被授 ⇒ 摘要对不上
160
118
  * ⇒ core pre-CAS 响亮拒(带 `parked_resume.startup_failed` 码可观测),绝不静默按新姿势跑。
161
119
  */
162
- async function chainMetadataOf(deps, row) {
163
- const caps = await resolveCapsFor(deps, row);
120
+ async function chainMetadataOf(deps, row, identity) {
121
+ const caps = await resolveCapsFor(deps, identity);
164
122
  const forced = caps?.forceDurableGate === true;
165
123
  return {
166
124
  ...(forced || deps.approverSeat === undefined ? { durableMandate: true } : {}),
@@ -171,35 +129,42 @@ async function chainMetadataOf(deps, row) {
171
129
  /**
172
130
  * 本行 principal 的 entitlement 现解(三位元数据共用**一次**)。
173
131
  *
174
- * principal 从哪来:`row.scope`。承重的不是「两个写入方碰巧写了同一个字符串」,而是**取数结构**:
175
- * `findParkedAgentForCheckpoint` 走的是 `agentStore.listByScope(cp.scope, …)` 这条**按 scope 分区**的
176
- * 查询(parked-decide.ts),所以任何能走到本函数的行都满足 `row.scope === cp.scope` —— 这是构造式的,
177
- * 不是巧合。而 `cp.scope` `boot/resolve-spec.ts` 写成 `encodeCheckpointScope(auth.principal)`
178
- * (legacy resume 腿反向读回 `principal: decodeCheckpointScope(cp.scope)` 是同一等式的另一半),
179
- * 故 `decodeCheckpointScope(row.scope)` 就是那条 run 的 `spec.principal`,与 core 喂给
180
- * `runtimeCapsResolver(spec.principal)` 的**同一个**值 —— 摘要两侧因此解到同一份 caps。
181
- *
182
- * ⚠️ 别把 `row.scope` 当成「和 `cp.scope` 同一个写入方铸的」:**不是**。写行那一侧(core `subagent.js`
183
- * `ctx.principal ?? opts.background.scope`,本仓 `capabilities/scenarios.ts` 填字面 `"default"`)用的是
184
- * `principal ?? "default"` 这套约定,与 `encodeCheckpointScope` 的 `"_"` 哨兵**不同源**。有 principal 时
185
- * 两套折出同一个字符串(encode 只在 null/undefined 时才替换),所以上面那条等式成立;**匿名** run 上两者
186
- * 分别是 `"default"` 与 `"_"`,分区查询直接落空 ⇒ 本函数根本到不了(decide 退回 legacy resume 腿,
187
- * parked-decide.ts 自述的「未命中=零回归」形)。谁哪天去弥合那个匿名未命中,必须**连本函数一起**重看:
188
- * 那时 `decodeCheckpointScope("default")` 会解出字符串 `"default"` 并把它当租户名递进 resolver。
189
- *
190
- * resolver 缺席(无 center / dry-run)⇒ core 侧 `runtimeCaps` 也恒 undefined ⇒ 这两位在 park 时也从不
191
- * 由 caps 置位 ⇒ 逐字零行为差。resolver 抛错(契约上它自己 fail-closed 且不抛)⇒ 当作没解出来:元数据
192
- * 位按「无 caps」算 park 记的对不上就响亮拒,**绝不**静默按更松的姿势赎回。
132
+ * principal 从哪来:**调用方交下来的 {@link ParkedReviveIdentity}**,值 = `decodeCheckpointScope(cp.scope)`。
133
+ * `cp.scope` `boot/resolve-spec.ts` 写成 `encodeCheckpointScope(auth.principal)`(legacy resume 腿反向
134
+ * 读回 `principal: decodeCheckpointScope(cp.scope)` 是同一等式的另一半),所以它就是那条 run 的
135
+ * `spec.principal`,与 core 喂给 `runtimeCapsResolver(spec.principal)` 的**同一个**值 —— 摘要两侧因此
136
+ * 解到同一份 caps。
137
+ *
138
+ * ⚠️ **不要退回去读 `row.scope`**。写行那一侧(core `subagent.js` `ctx.principal ?? opts.background.scope`,
139
+ * 本仓 `capabilities/scenarios.ts` 填字面 `"default"`)用的是 `principal ?? "default"` 这套约定,与
140
+ * `encodeCheckpointScope` `"_"` 哨兵**不同源**:有 principal 时两套折出同一个字符串(encode 只在
141
+ * null/undefined 时才替换),匿名时分别是 `"default"` `"_"`,而且那一列上**匿名与真名叫 `default` 的
142
+ * 租户不可分辨** —— 有损。
143
+ *
144
+ * 🔴 A-057.44(2026-08-19 两半皆修,本注随之改真)—— **匿名** run 上两者分别是 `"default"` 与 `"_"`,
145
+ * 修前分区查询直接落空 ⇒ 本函数在匿名部署上根本到不了(decide 退回 legacy resume 腿,而那条腿
146
+ * `getCtx(子代会话)` null ⇒ 恒 409)。第一半:`findParkedAgentForCheckpoint` 现在按
147
+ * `backgroundScopesForCheckpointScope(cp.scope)` 的**别名集**查(匿名时把 `"default"` 一并纳入),
148
+ * 于是本函数在匿名部署上首次可达。
149
+ *
150
+ * 第二半(codex 对抗复审 medium,验真后采纳)——**principal 不再从 `row.scope` 解**。本注的上一版警告过
151
+ * 「谁哪天弥合那个匿名未命中,必须连本函数一起重看:`decodeCheckpointScope("default")` 会解出字符串
152
+ * `"default"` 并把它当租户名递进 resolver」。那句警告在第一半落地的当天就兑现了:`row.scope` 在
153
+ * `principal ?? "default"` 约定下**结构性有损**(匿名与一个真名叫 `default` 的租户在这一列上同形),
154
+ * 解错的后果不是拒绝而是**解到另一份 caps** ⇒ 与 park 时冻的摘要对不上 ⇒ 刚变得可达的那批卡又赎不回。
155
+ * ⇒ 席位多收一位 {@link ParkedReviveIdentity}:权威 principal 由**调用方**从 `decodeCheckpointScope(cp.scope)`
156
+ * 现算并传下来(`parked-decide.ts` 的 `decideParkedAgent`),该列由 `encodeCheckpointScope(auth?.principal)`
157
+ * 铸、与 core 喂给 `runtimeCapsResolver` 的是同一个值,无损。参数**必填**:漏传是编译错,不是静默回退。
193
158
  */
194
- async function resolveCapsFor(deps, row) {
159
+ async function resolveCapsFor(deps, identity) {
195
160
  if (deps.resolveRuntimeCaps === undefined)
196
161
  return undefined;
197
162
  try {
198
- return await deps.resolveRuntimeCaps(decodeCheckpointScope(row.scope));
163
+ return await deps.resolveRuntimeCaps(identity.principal);
199
164
  }
200
165
  catch (err) {
201
166
  // 留痕是必需的:否则「这批卡为什么突然赎不动」查无痕迹。
202
- deps.logger.warn("parked_revive_caps_unresolved", { handle: row.handle, err: err instanceof Error ? err.message : String(err) });
167
+ deps.logger.warn("parked_revive_caps_unresolved", { principal: identity.principal ?? null, err: err instanceof Error ? err.message : String(err) });
203
168
  return undefined;
204
169
  }
205
170
  }
@@ -120,7 +120,7 @@ export interface RunnerDepsCtx {
120
120
  }
121
121
  /** design/158 A10 留档发现②:main runner `RunnerDeps` 与 main.ts subRunner 字面量之间此前手工重复
122
122
  * 的 ~15 个键,类型标注见 {@link createSharedRunnerDeps} 头注。 */
123
- export type SharedRunnerDeps = Pick<RunnerDeps, "brain" | "readFace" | "readDenyPatterns" | "readDenyBuiltinTiers" | "readDenyBuiltinExclude" | "models" | "roles" | "tiers" | "pricing" | "tracer" | "promptSource" | "executionEnvFactory" | "lspManager" | "backgroundAgentStore" | "mailboxStore" | "rosterStore" | "hooks" | "toolResultStore" | "hands" | "sessionPolicyStore" | "usageWindows" | "usageWindowStore" | "memoryScopeAdmission" | "deploymentMemoryScopes" | "sharedMemoryStores" | "compliancePostureResolver" | "lockedConfig" | "retentionPolicy" | "onNotice">;
123
+ export type SharedRunnerDeps = Pick<RunnerDeps, "brain" | "readFace" | "readDenyPatterns" | "readDenyBuiltinTiers" | "readDenyBuiltinExclude" | "memoryDelegationEvidence" | "models" | "roles" | "tiers" | "pricing" | "tracer" | "promptSource" | "executionEnvFactory" | "lspManager" | "backgroundAgentStore" | "mailboxStore" | "rosterStore" | "hooks" | "toolResultStore" | "hands" | "sessionPolicyStore" | "usageWindows" | "usageWindowStore" | "memoryScopeAdmission" | "deploymentMemoryScopes" | "sharedMemoryStores" | "compliancePostureResolver" | "lockedConfig" | "retentionPolicy" | "onNotice">;
124
124
  /**
125
125
  * design/158 A10 留档发现②(review 2026-07-29,[1543]§三族A 同源修补的延续):main runner 的
126
126
  * `RunnerDeps` 字面量(下方 `createRunnerDeps`)与 `main.ts` 里 subRunner 的 `new Runner({...})`
@@ -74,9 +74,14 @@ export function createSharedRunnerDeps(ctx) {
74
74
  const shared = {
75
75
  brain: ctx.brain,
76
76
  // core 5.28.0 EngineNotice 席(#240②,^5.28.0 floor 的编译锚之一):结构化运维通告——function 席
77
- // **替换** console.warn 行(core 契约:host 接管即拥有响亮权,不双发),故必须真转发不吞。三族
78
- // (config.env_timeout_discarded / config.materialize_env_discarded / tool_result.offload_put_failed)
79
- // 全部按 code logger.warn,detail 原样透传;共享基座=两 Runner(main/sub)同源同席([1543] 纪律)。
77
+ // **替换** console.warn 行(core 契约:host 接管即拥有响亮权,不双发),故必须真转发不吞。
78
+ // 🔴 转发体是**全族开放**的:按 `notice.code` 原样落 `logger.warn`,detail 逐字透传,不认词表、
79
+ // 不挑族 —— 族表的单一真源是 core 的 `EngineNotice.code` doc(`dist/core/types.d.ts`
80
+ // "Current families"),**不在本注里复述**。A-057.25:此处原写死「三族(config.env_timeout_discarded /
81
+ // config.materialize_env_discarded / tool_result.offload_put_failed)」,那是 5.28.0 当拍的快照,
82
+ // 5.29→5.45 六个提货代际零回填后已脱节 3/13(5.45.0 新增的三条 `memory.*` 就在漏的那半里)——
83
+ // 一张会漂的孪生词表比没有词表更坏:它会让下一个读它的人以为遥测面只需覆盖那三个码。
84
+ // 共享基座=两 Runner(main/sub)同源同席([1543] 纪律)。
80
85
  onNotice: createEngineNoticeForwarder(ctx.logger),
81
86
  // core 1.265: the active tier table (tier words + CC aliases → catalog keys, expandTiers at Runner
82
87
  // construction; empty = INERT by core contract). Same mutateInPlace reference applyEffective fills — a
@@ -99,6 +104,16 @@ export function createSharedRunnerDeps(ctx) {
99
104
  // 真值判会把它折回「缺席=默认全开」——正好把运维显式关掉的档位偷偷开回去。
100
105
  readDenyBuiltinTiers: ctx.config.readDenyBuiltinTiers ?? undefined,
101
106
  readDenyBuiltinExclude: ctx.config.readDenyBuiltinExclude ?? undefined,
107
+ // #299(core 5.45.0 design/324 #324①):委派臂的证据标准 —— 与上面四席同族的**部署座席**,同样必须
108
+ // **主 runner / subRunner / run-local 三族**同源(本基座的射程,`run-local.test.ts` 的 SHARED_KEYS
109
+ // 编译期闭包 + `boot-runner-deps-shared-base.test.ts` 的键集绊线各钉一半)。core 明写「每个 run 读
110
+ // **准备它的那只 Runner** 的 deps」⇒ 漏挂 subRunner 不是「子代少个旋钮」,而是委派子代按**另一套**
111
+ // 证据标准判自己的污染臂(运维选的 attested-only 对它不生效),正是 [1543]§三族A 那个事故族的形。
112
+ // ⚠️ **leader lane 不在本基座射程**(如实登记,既有族级缺口非本席引入):`src/leader/wire.ts` 的
113
+ // Runner 是独立字面量,上面四席同样到不了那里;今日影响结构性为零(leader 不挂记忆后端,core 的
114
+ // 铸点要求污染面在场)。边界钉 = `test/memory-delegation-evidence-lane.test.ts` 的 ⑤ 组。
115
+ // 缺席=undefined=core 自缺省(`static-face`)。
116
+ memoryDelegationEvidence: ctx.config.memoryDelegationEvidence ?? undefined,
102
117
  executionEnvFactory: ctx.executionEnvFactory ? ctx.executionEnvFactory : undefined,
103
118
  lspManager: ctx.lspManager ? ctx.lspManager : undefined,
104
119
  // core 1.364 durable bg agents 读半场(写半场=scenarioDeps.backgroundAgentStore 同实例,组装区注释)。
@@ -0,0 +1,124 @@
1
+ /**
2
+ * #303([4610] test 报「/model 中途切换校验探针经 POST /v1/side-query 对有效 key 返回上游 401」)——
3
+ * side-query 面的 **per-model key plane** 装配。
4
+ *
5
+ * ## 病灶(亲验 core 5.43.0 dist 实现行)
6
+ *
7
+ * core 的 side-query verb **结构性没有 key 座位**:
8
+ * · `core/side-query.d.ts` 的 `SideQuerySpec` 无 `apiKey` / `getApiKeyAndHeaders` 字段;
9
+ * · `SideQueryDeps` = `{ brain, models?, roles? }`;
10
+ * · `core/side-query.js` 铸 brain `options` 时只放 `signal/maxTokens/reasoning`,**不含 apiKey**。
11
+ * 而 openai brain 的 `buildRequest`(`brain/openai.js:281-282`)是这两行:
12
+ * `const apiKey = options?.apiKey ?? config.apiKey;` ← 缺席即回落 **brain 构造时的网关 key**
13
+ * `const root = (model.baseUrl || config.baseUrl || "")` ← 却**按模型**选路
14
+ * ⇒ 对带 per-model `baseUrl` + `apiKeyEnv`/`sealedApiKey` 的模型,side-query 把**主网关 key 发到
15
+ * 外部模型 URL**:用户看到上游 401,同时这是一次凭据外泄(安全轴)。
16
+ *
17
+ * 主推理链没有这个病:core 每次 brain 调用都调 `TaskSpec.getApiKeyAndHeaders`(server 座位=
18
+ * `boot/resolve-spec.ts` 的 `getKeyResolver()` 活取);hook-llm 面自己解析并对 off-route 模型
19
+ * fail-closed。side-query 是 key plane 三面里**唯一**缺席的那一面。
20
+ *
21
+ * ## 修形(窄、可撤)
22
+ *
23
+ * side-query 消费面**脱离 `Runner.sideQuery`**,直调 core 公开导出的 `runSideQuery`,并给它一只
24
+ * **key 注入 brain wrapper**:每次调用按本次 spec 解析出的 `Model` 现解 per-model key,有则注入
25
+ * `options.apiKey`(core 那行 `options?.apiKey ?? config.apiKey` 于是走前一支),没有则**不注入**
26
+ * (回落网关 key = 主推理链逐字同语义)。wrapper 只包 side-query 专用的这一只引用,主推理链的
27
+ * `RunnerDeps.brain` 零扰动。
28
+ *
29
+ * 🔴 **poison(sealed key 不可解)上抛,绝不静默回落网关 key** —— `resolveModelApiKey` 抛
30
+ * `SealedKeyPoisonedError` 是「这个模型配了托管密钥但解不开」的响亮态;吞成 undefined 等于用共享
31
+ * 网关账号去打这只模型的上游,正是毒丸机制存在的理由(CLAUDE.md #157 安全轴 fail-closed)。
32
+ *
33
+ * ## 具名残余:**无 per-model key 的 off-route 模型仍收网关 key**(codex R1-F1,逐条评估后如实留)
34
+ *
35
+ * 判据本身没变:`undefined` ⇒ 不注入 ⇒ core 用网关 key,即使这只模型的 `baseUrl` 指向别家。为什么
36
+ * **不**在本批把它改成 fail-closed(逐条,而不是"先不管"):
37
+ * ① **与主推理链同语义**才是当前的正确性基准 —— 任务面(core 每次调 `getApiKeyAndHeaders`,返回
38
+ * `undefined` 即网关 key)在**同一个** catalog、**同一批**模型上就是这个姿势。只把 side-query 收严,
39
+ * 等于让同一只模型「跑任务能用、问一句就 401」,而任务面才是量大的那一面 —— 半边收严的覆盖比
40
+ * 统一的宽松更难排障。
41
+ * ② **谁被伤到**:`baseUrl` 是**运维**在 config-center 里自己写的(不是用户输入,用户只能在
42
+ * `atModelAllowlist` 里点名单内的模型)。"同一账号多网关主机/多区域"是既有且正当的部署形,
43
+ * 它今天靠网关 key 工作;无版本协商地收严 = 这类部署当场 401,而且没有旋钮可回退。
44
+ * ③ hook 面(`hooks/hook-llm.ts`)确实是 fail-closed 的,但它的辖域是 **hook roster**(运维给钩子挑的
45
+ * 模型),不是用户可点的整本目录;两者的收严代价不同量级。
46
+ * ⇒ 本批**不改行为**,把它作为**跨面语义问题**上报(要收严就三面一起收严、带旋钮、走表态制),
47
+ * 而不是在这一面偷偷收严。真要收严时的正解:按 `normUrl` 比对 `gatewayBaseUrl`(+fallback 列表)
48
+ * 与 anthropic 路由,off-route ∧ 无 per-model key ⇒ 拒发,并同批改任务面。
49
+ *
50
+ * ## 撤除条件(设计成可撤形)
51
+ *
52
+ * core 一旦给 `SideQuerySpec` 补上 key 座位(`apiKey` 或 `getApiKeyAndHeaders`),本模块的 wrapper
53
+ * 即可撤:装配点改成把 `getKeyResolver()` 直接塞进 spec,`createPerModelKeyBrain` 整只删除。
54
+ * 撤除信号有机器钉:`test/side-query-key-plane.test.ts` 的「core 座位缺席钉」在 core 补座位那天先红。
55
+ * 在那之前,本模块**不是** upstream 语义的旁路重写 —— 它只在 server 自己的装配层给自己的 brain
56
+ * 引用补上 server 自己的凭据平面,core 的 side-query 语义(选路/归一/结果形)原样由 `runSideQuery` 决定。
57
+ */
58
+ import { type Brain, type SideQueryResult, type SideQuerySpec } from "@sema-agent/core";
59
+ import type { ServiceConfig } from "../config-types.js";
60
+ import type { ModelKeyRef } from "../key-resolver.js";
61
+ /** 与 `hook-llm` / `resolve-spec` 同一只座位类型(`createKeyResolver` 的产物;registry 热应用会整个换引用)。 */
62
+ export type KeyResolver = ((model: ModelKeyRef) => Promise<{
63
+ apiKey: string;
64
+ } | undefined>) | undefined;
65
+ /** side-query 执行席:HTTP 路由(`routes/side-query.ts`)唯一的 brain 出口。 */
66
+ export type SideQueryLane = (spec: SideQuerySpec) => Promise<SideQueryResult>;
67
+ /**
68
+ * 「这只模型的 baseUrl 打的是**别家**主机吗」—— core 的选路规则是 `model.baseUrl || config.baseUrl`,
69
+ * 所以只有 `model.baseUrl` 非空时才可能离开本部署网关。比较按 URL 归一(`new URL` 小写 host、丢默认端口、
70
+ * 去尾斜杠),裸字符串比会在 host 大小写/默认端口写法上假阴。
71
+ *
72
+ * 参照系缺席(`gatewayBaseUrl` 未给)⇒ 回 `false`:判不了就不判,别猜。
73
+ *
74
+ * ⚠️ `hooks/hook-llm.ts:118` 有一只逐字同形的内联归一器(它用同一判据做 **fail-closed 拒发**,本函数只
75
+ * 做**计数**)。两处合一属跨面收严件 #309 的射程(那时三面同批换判据),本批刻意不动 hook 面的 fail-closed
76
+ * 门 —— 在只改可观测性的批里重写另一条安全轴的门,风险与收益不对等。
77
+ */
78
+ export declare function isOffRouteBaseUrl(modelBaseUrl: string | undefined, gatewayBaseUrl: string | undefined): boolean;
79
+ export interface SideQueryLaneCtx {
80
+ /** 与 `RunnerDeps.brain` **同一只**实例(wrapper 在外面包,不改这只)。 */
81
+ brain: Brain;
82
+ /** 活配置引用:目录/角色表被 config-center 就地热应用(`mutateInPlace`),按调用取值才跟得上。 */
83
+ config: ServiceConfig;
84
+ /** 活引用取值 —— registry 热应用整个换 resolver 引用(`boot/config-center.ts` 每次 apply 重铸)。 */
85
+ getKeyResolver: () => KeyResolver;
86
+ }
87
+ /**
88
+ * per-model key 注入 wrapper。
89
+ *
90
+ * 语义(逐条对齐主推理链 `getApiKeyAndHeaders`):
91
+ * · `options.apiKey` **已在场** ⇒ 原样透传(上游/装饰器已经定了凭据,本层不抢);
92
+ * · resolver 缺席(部署根本没配 per-model key)⇒ 不注入 ⇒ 网关 key(今日行为逐字不变);
93
+ * · resolver 返回 undefined(这只模型没有自己的 key)⇒ 不注入 ⇒ 网关 key(additive 契约);
94
+ * · resolver 抛(sealed key 中毒)⇒ **上抛**,本次调用整体失败,一个字节都不发。
95
+ *
96
+ * `complete` 在场时同样包一层:core 的 `Brain.complete` 是 compaction/摘要腿的直调口,漏包等于给
97
+ * 同一个病留第二条路(side-query 今天不走它,但 wrapper 的透明性不该依赖调用方今天的用法)。
98
+ *
99
+ * 🔴 A-057.59 [PARTIAL high](2026-08-19)—— 「无 per-model key 的 off-route 模型仍收网关 key」这条
100
+ * 具名残余**行为不变**(逐条理由见模块顶注「具名残余」段:与主推理链
101
+ * `dist/engine/harness/agent-harness.js` 的 `auth?.apiKey !== undefined` 臂逐字同语义,单面收严会造成
102
+ * 「同一模型跑任务能用、问一句 401」;收严件 = 跨面 #309,带旋钮走表态制),但**每次调用零留痕**这一半
103
+ * 被判为要清偿的存量欠账:config-apply 期那条 `sema_registry_model_key_env_missing` warn 只覆盖
104
+ * 「声明了 env 名却没设」,覆盖不到「压根没配 per-model key 的 off-route 模型」,而后者才是真发生外泄
105
+ * 的那一形。按 CLAUDE.md #157「一时改不动的 P 类记 `P-DEBT`」补计数:命中条件 =
106
+ * **解不出 per-model key ∧ 该模型的 baseUrl 不是本部署网关** ⇒ 这一次调用真把网关凭据发给了别家主机。
107
+ * `gatewayBaseUrl` 缺席(调用方没给参照系)⇒ 不计(不猜),与「宁可漏计不可错计」同向。
108
+ */
109
+ export declare function createPerModelKeyBrain(brain: Brain, getKeyResolver: () => KeyResolver, opts?: {
110
+ readonly gatewayBaseUrl?: string;
111
+ }): Brain;
112
+ /**
113
+ * 装配 side-query 执行席 = 真 `runSideQuery` + 注入式 brain,models/roles 与 Runner 同源。
114
+ *
115
+ * `models` **按调用**取 `expandTiers(config.models, config.tiers)`:
116
+ * · 档位词 / CC 别名(`flash`/`opus`/…)在 side-query 上照常可选路 —— core 的 `Runner` 构造时做同一件事
117
+ * (`runtask.js:1432`),不做等于把档位词从这条路上悄悄拿掉;
118
+ * · 取「按调用」而不是「boot 一次」是因为 `config.models` 由 config-center 就地热应用,而 `Runner` 那次
119
+ * 展开是**构造时快照**(`expandTiers` 在 tiers 非空时返回新对象)——本路由的模型白名单门
120
+ * (`routes/side-query.ts` 的 `isModelAllowlisted`)本来就按请求现算同一张增广目录,两边取同一份
121
+ * 才不会出现「名单放行、选路说不认识」的分歧。
122
+ */
123
+ export declare function createSideQueryLane(ctx: SideQueryLaneCtx): SideQueryLane;
124
+ //# sourceMappingURL=side-query-lane.d.ts.map