@sema-agent/server 7.22.0-rc.1 → 7.23.0-rc.1

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.md +2 -2
  2. package/README.zh-CN.md +2 -2
  3. package/USAGE.md +6 -4
  4. package/deploy/sema-up/chart/values.yaml +1 -1
  5. package/dist/approval-card.d.ts +53 -0
  6. package/dist/approval-card.js +104 -1
  7. package/dist/bench/s1/run-firm.js +1 -1
  8. package/dist/boot/budget-tracing.d.ts +0 -1
  9. package/dist/boot/resolve-spec.js +3 -1
  10. package/dist/budget.d.ts +0 -11
  11. package/dist/budget.js +10 -22
  12. package/dist/capabilities/scenarios.js +14 -8
  13. package/dist/config.d.ts +1 -1
  14. package/dist/config.js +12 -1
  15. package/dist/fleet/fleet-bus.d.ts +8 -3
  16. package/dist/fleet/fleet-bus.js +10 -5
  17. package/dist/http/route-ctx.d.ts +11 -0
  18. package/dist/http/route-ctx.js +16 -1
  19. package/dist/http/routes/runs.d.ts +3 -0
  20. package/dist/http/routes/runs.js +28 -24
  21. package/dist/http/routes/side-query.js +4 -13
  22. package/dist/http/routes/tasks.js +15 -3
  23. package/dist/http/server.d.ts +4 -3
  24. package/dist/http/server.js +8 -4
  25. package/dist/http/wire-types.d.ts +3 -1
  26. package/dist/lsp/manager.d.ts +9 -0
  27. package/dist/lsp/manager.js +8 -26
  28. package/dist/main.js +4 -0
  29. package/dist/observability/fail-open.d.ts +4 -0
  30. package/dist/observability/fail-open.js +4 -0
  31. package/dist/observability/otel-exporter.js +7 -1
  32. package/dist/observability/secret-env-scrub.d.ts +53 -0
  33. package/dist/observability/secret-env-scrub.js +135 -0
  34. package/dist/plugins/image-bake-store-sql.d.ts +8 -1
  35. package/dist/plugins/image-bake-store-sql.js +8 -1
  36. package/dist/plugins/image-index-sql.d.ts +13 -3
  37. package/dist/plugins/image-index-sql.js +13 -3
  38. package/dist/plugins/permission-rule-store-sql.d.ts +9 -72
  39. package/dist/plugins/permission-rule-store-sql.js +36 -64
  40. package/dist/plugins/remote-env-host.js +10 -2
  41. package/dist/project-memory.js +7 -1
  42. package/dist/run-local.js +7 -0
  43. package/dist/runtime-governance.d.ts +1 -1
  44. package/dist/runtime-governance.js +12 -15
  45. package/dist/security.d.ts +3 -1
  46. package/dist/security.js +15 -7
  47. package/dist/task-settings.js +10 -5
  48. package/dist/tool-approval.d.ts +20 -1
  49. package/dist/tool-approval.js +4 -1
  50. package/dist/trace/core-keyset-guard.d.ts +1 -1
  51. package/dist/trace/project.d.ts +2 -0
  52. package/dist/trace/project.js +11 -0
  53. package/package.json +3 -3
package/README.md CHANGED
@@ -95,13 +95,13 @@ Requirements: Node ≥ 20 (npm path) and an OpenAI-compatible model gateway.
95
95
  ```bash
96
96
  # A) npm
97
97
  npm install @sema-agent/server
98
- MODEL_GATEWAY_BASEURL=https://api.deepseek.com MODEL_ID=deepseek-v4-pro \
98
+ MODEL_GATEWAY_BASEURL=https://api.deepseek.com MODEL_ID=deepseek-v4-flash \
99
99
  MODEL_API_KEY=<your-key> SERVICE_AUTH_TOKEN=<pick-one> \
100
100
  node node_modules/@sema-agent/server/dist/main.js # → :8090
101
101
 
102
102
  # B) container (zero local deps, anonymous pull)
103
103
  docker run -p 8090:8090 \
104
- -e MODEL_GATEWAY_BASEURL=https://api.deepseek.com -e MODEL_ID=deepseek-v4-pro \
104
+ -e MODEL_GATEWAY_BASEURL=https://api.deepseek.com -e MODEL_ID=deepseek-v4-flash \
105
105
  -e MODEL_API_KEY=<your-key> -e SERVICE_AUTH_TOKEN=<pick-one> \
106
106
  ghcr.io/sema-agent/sema-server:latest # or docker.io/claybobby/sema-server:latest
107
107
 
package/README.zh-CN.md CHANGED
@@ -89,13 +89,13 @@
89
89
  ```bash
90
90
  # A) npm
91
91
  npm install @sema-agent/server
92
- MODEL_GATEWAY_BASEURL=https://api.deepseek.com MODEL_ID=deepseek-v4-pro \
92
+ MODEL_GATEWAY_BASEURL=https://api.deepseek.com MODEL_ID=deepseek-v4-flash \
93
93
  MODEL_API_KEY=<你的-key> SERVICE_AUTH_TOKEN=<自定> \
94
94
  node node_modules/@sema-agent/server/dist/main.js # → :8090
95
95
 
96
96
  # B) 容器(零依赖,匿名可拉)
97
97
  docker run -p 8090:8090 \
98
- -e MODEL_GATEWAY_BASEURL=https://api.deepseek.com -e MODEL_ID=deepseek-v4-pro \
98
+ -e MODEL_GATEWAY_BASEURL=https://api.deepseek.com -e MODEL_ID=deepseek-v4-flash \
99
99
  -e MODEL_API_KEY=<你的-key> -e SERVICE_AUTH_TOKEN=<自定> \
100
100
  ghcr.io/sema-agent/sema-server:latest # 或 docker.io/claybobby/sema-server:latest
101
101
 
package/USAGE.md CHANGED
@@ -103,7 +103,7 @@ MODEL_DEGRADE_ON=rate_limit,breaker_open # 可选,反应式触发器子集;词
103
103
  # 不想要这两类触发就显式设回 rate_limit,breaker_open。拼错词 boot 拒启(不静默忽略)。
104
104
  ```
105
105
  - 反应式降级 brain 包在**最外层**(core council #8);fallback brain 用自己的凭据(decorator 清掉主模型的 per-call key 防泄漏给别的 provider)。**坑**:`MODEL_DEGRADE_TO` 最好别和被限流的是同一网关/账号,否则反应式切过去照样撞同一个 rate_limit。
106
- - **定价怎么设**:`model.cost` 来自 `MODEL_COST_INPUT/OUTPUT/CACHE_READ/CACHE_WRITE`(**USD per 1M tokens**,默认 0 = `costUsd` 读 0)。云模型(如 review-gw 的 deepseek-v4-pro)必须设,否则 spend 恒为 $0;本地自托管(qwen)留 0 即对(无 per-token 外部花费)。字段名是 `costUsd`,**非美元计价的网关要先折算**(如 DeepSeek 官方 CNY ÷ 汇率)。配置控制面管的模型走 `CenterModel.cost`(中心存价、不存 secret)。
106
+ - **定价怎么设**:`model.cost` 来自 `MODEL_COST_INPUT/OUTPUT/CACHE_READ/CACHE_WRITE`(**USD per 1M tokens**,默认 0 = `costUsd` 读 0)。云模型(如 review-gw 的 deepseek-v4-flash)必须设,否则 spend 恒为 $0;本地自托管(qwen)留 0 即对(无 per-token 外部花费)。字段名是 `costUsd`,**非美元计价的网关要先折算**(如 DeepSeek 官方 CNY ÷ 汇率)。配置控制面管的模型走 `CenterModel.cost`(中心存价、不存 secret)。
107
107
  - 成本计量**自动开**:从 config 的 `model.cost`(per-1M 绝对 USD)注入 `pricing`,core 算出权威的整数 `costMicroUsd`(避免浮点累计误差)。`/metrics` 新增:`model_cost_micro_usd_total{model}`(覆盖所有 brain 调用=主任务+异步+council 子任务的总花费)、`brain_first_token_ms`(网关 hang 早警)、`brain_call_latency_ms`、`tool_calls_total{name,ok}`、`budget_exceeded_total{code}`、`cost_quota_rejected_total`、`degraded_total{reason}`(1.40 降级)。
108
108
  - **近预算降级 vs 硬闸**:降级(`MODEL_DEGRADE_TO`,到 `atCostFraction` 切便宜模型)是**撑长**预算、任务仍完成(出口质量下降、发 `task.degraded` 事件可告警);硬闸(`maxCostUsd` 全额)仍在,切了便宜模型还超全额 → `budget.exceeded` 停。
109
109
  - **`METRICS_TOKEN`**(可选,只读):设了它,`GET /metrics`+`/metrics/summary` 接受**它或** `SERVICE_AUTH_TOKEN`。**全 fleet 设同一个值** → 管理端(sema-web,规划名 sema-admin)用**一个** token 拉所有 worker 的指标,**无需持有各 worker 的全权 `SERVICE_AUTH_TOKEN`**(不破坏 secret 边界)。即使泄露也只暴露指标(只读)。
@@ -131,7 +131,7 @@ OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4318 # 设了才开;周期
131
131
  OTEL_EXPORT_INTERVAL_MS=15000 OTEL_SERVICE_NAME=sema-server
132
132
  OTEL_EXPORTER_OTLP_HEADERS=authorization=Bearer xxx # 逗号分隔的 k=v(可选,如鉴权头)
133
133
  ```
134
- - **零依赖**:不引 OTel SDK(顾及内网镜像 + bun 单文件),直接按 OTLP/JSON 协议把指标注册表 POST 给 collector;Prometheus `/metrics` 不受影响、两者可并存。best-effort——collector 挂了只记日志、绝不影响服务。计数器→Sum(monotonic)、gauge→Gauge、histogram→Histogram(累积桶转成 OTLP 的 per-bucket + `+Inf` 溢出桶)。
134
+ - **零依赖**:不引 OTel SDK(顾及内网镜像 + bun 单文件),直接按 OTLP/JSON 协议把指标注册表 POST 给 collector;Prometheus `/metrics` 不受影响、两者可并存。best-effort——collector 挂了只记日志、绝不影响服务;⚠️ 这意味着**metrics 轴此前对导出失败零信号**(Prometheus-only 部署看不见 collector 持续宕机)——7.23.0 起每次导出失败计入 `fail_open_total{tag="server.otel.export-failed"}`,告警规则可直接盯这条。计数器→Sum(monotonic)、gauge→Gauge、histogram→Histogram(累积桶转成 OTLP 的 per-bucket + `+Inf` 溢出桶)。
135
135
 
136
136
  **可选 — 成本分层 + `@-model`(core 1.24 role map)**
137
137
  ```bash
@@ -148,7 +148,7 @@ MODEL_ID=<your-model-name> MODEL_CHEAP_ID=<your-cheap-model-name>
148
148
  MODEL_CODE_ROLES=default,subagent # 不设=全中立;仅这些角色在「任务没自带 systemPrompt」时跑编码提示
149
149
  ```
150
150
  - 经 `RoleSpec.systemPrompt` 挂在**角色**上,非开发角色保持中立、全局默认 `DEFAULT_SYSTEM_PROMPT` 不变;任务自带 `systemPrompt`(或客户端注入)时仍优先。
151
- - **验证门**(core 1.44,opt-in):请求体带 `verify:true`(可选 `verifyRounds`,夹到 [1,5]、默认 2)→ 任务跑完后由**独立只读对抗 verifier**(`verifier` 角色,默认=主模型)证据强制地"试图 break 它",FAIL 则把 findings 注回同 session 续跑修复→重验,循环到 PASS 或轮数上限。结果带 `verification:{verdict,rounds,findings,evidence}`(`verdict` 看质量,`result`/`status` 仍是实现的)。**仅 `/v1/tasks`(同步)与 `/v1/runs`(异步)**——`/v1/tasks/stream` 不支持(多轮非单流,请求 verify 会 400)。verifier 工具默认 = 实现任务工具滤掉 `effect:"write"`(只读边界)。`/metrics` 加 `verifications_total{verdict}`。
151
+ - **验证门**(core 1.44,opt-in):请求体带 `verify:true`(可选 `verifyRounds`,夹到 [1,5]、默认 2)→ 任务跑完后由**独立只读对抗 verifier**(`verifier` 角色,默认=主模型)证据强制地"试图 break 它",FAIL 则把 findings 注回同 session 续跑修复→重验,循环到 PASS 或轮数上限。结果带 `verification:{verdict,rounds,findings,evidence}`(`verdict` 看质量,`result`/`status` 仍是实现的)。**仅 `/v1/tasks`(同步)与 `/v1/runs`(异步)**——`/v1/tasks/stream` 不支持(多轮非单流,请求 verify 会 400)。verifier 工具默认 = 实现任务工具滤掉 `effect:"write"`(只读边界)。`/metrics` 加 `verifications_total{verdict}`。⚠️ 同步 `/v1/tasks` 形**没有事件通道**(过程帧无处回放,只回终局 JSON)——带 `verify`/`cascade` 的 200 体自 7.23.0 起附可选 `notice` 键指路 `POST /v1/runs`(要过程可见性用异步形;[3979])。
152
152
  - **记忆(design/138 文件记忆引擎,2026-07-08 起唯一记忆面)**:core 注入式文件引擎——任务开始时 materialize 记忆目录(`MEMORY_ENGINE_DIR`,默认 `~/.ai-agent`),模型用**普通文件技能**读写记忆(CC `# Memory` 指令 + 派生索引;无 remember/recall 工具),任务边界 harvest 门(secret/cap 扫描)提交。单用户默认开,`MEMORY_ENGINE=off` 显式关;多租户恒关(文件基座无租户隔离,fail-closed)。旧 SQL 记忆面(`MEMORY_BACKEND`/`EMBEDDING_*`/`MEMORY_READ_LIMIT`/去重/向量检索、`GET/DELETE /v1/memory` 与 session memory 写 verb)已退役,数据不迁移——升级后对库跑一次 `scripts/drop-memory-tables.sql`。`body.memoryWrite:false` 仍是每请求只读开关(harvest 不提交)。
153
153
  **`MEMORY_PERSISTENCE_CAPABLE`(三态,7.14.0 起)**=**部署自述**「本机能不能把用户的『记住 X』落到持久处」,与请求键 `memoryWrite` 两轴正交(后者收紧本次运行,前者是 operator 声明;任一取否即只读方向)。缺省不设=引擎按 roster 自行推断;`true`=本部署有推断看不见的持久通道(自定义 writer / 记忆 MCP / 远程执行车道与记忆根共享挂载);`false`=强制只读披露并关掉文件工具往记忆根的写通道。不认得的词拒启。
154
154
  🔴 **`false` 自 core 5.27.0(7.14.0 提货)起还多一层——但只在 file 记忆引擎上**(`MEMORY_ENGINE_BACKEND` 未设 = 默认单机形):该会话是**受限会话**——materialize/搜索/harvest 一律按**已提交账**供给,记忆根下无事务背书的磁盘分歧既不收编也不供给,而是留盘 + 响亮点名(`restricted_divergence`,本服务把它计进 `memory_harvest_rejections_total{code}` 并 warn 一条),等下一次**非受限**会话走正常门收编。合法流程不受影响:用户手改、`git pull` 落下的删除照旧(延后,不销毁),并发的可写会话提交的变更算有事务背书。
@@ -445,7 +445,9 @@ SEMA_PARENT_PID=$$ # 壳把自己的 pid 传给它 spawn 出来的引擎
445
445
  错误文案直接给出该改成什么。所以不存在"两个名字同时设"的状态,也不存在"旧名还在悄悄生效"的状态。
446
446
 
447
447
  **`LSP_ENABLED` 一分为二**:它原来同时驱动两条腿,而且两腿缺省相反——沙箱腿缺省**关**(要烤好的
448
- `ai-agent-code-lsp` 模板),host 腿缺省**开**(只要 PATH 上有 language server,没有就优雅退回 grep/read)。
448
+ `ai-agent-code-lsp` 模板;⚠️ 该模板**只烤 TS/JS**——Python/Go 烤在 `dev-base`,dart/kotlin/java/swift
449
+ 烤在 `dev-mobile`,四模板物理独立,`E2B_TEMPLATE` 选错则未烤语言的 LSP **静默退化到 grep/read**;
450
+ 对照表见 ARCHITECTURE §7,真源=`src/lsp/e2b-bridge.ts`),host 腿缺省**开**(只要 PATH 上有 language server,没有就优雅退回 grep/read)。
449
451
  现在沙箱腿仍是 `LSP_ENABLED`(opt-in),host 腿归 `LSP_HOST_ENABLED`(opt-out)。
450
452
  🪦 `LSP_ENABLED=false`(拆分前唯一的 host 腿逃生舱)自 3.0.0 起是墓碑:拒启并指路 `LSP_HOST_ENABLED`。
451
453
  `LSP_ENABLED=true` 不受影响——那是沙箱腿自己的 opt-in,语义没变。
@@ -64,7 +64,7 @@ server:
64
64
  # 公网入口=Gcore LB listener 8091 → nodePort,LB 面不归 chart 管)
65
65
  serverV4pro:
66
66
  enabled: false
67
- modelId: deepseek-v4-pro
67
+ modelId: deepseek-v4-flash
68
68
  replicas: 1
69
69
  nodePort: 30891
70
70
  image: "" # 留空 = 跟 server.image 同镜像
@@ -21,6 +21,7 @@
21
21
  * 没有方法、没有捕获的行为。
22
22
  */
23
23
  import { z } from "zod";
24
+ import { type AskEvidenceAbsence } from "@sema-agent/core";
24
25
  import type { AskRow } from "./plugins/approval-ask-store-sql.js";
25
26
  /** 模型自由文本(`sourceAgentName` / `delegation.agentName`)的限长(设计稿 §6.2)。设计 §3.1 把
26
27
  * 「限长 + 脱敏」写成 **server 新增责任**(引擎无此层):spawning model 挑的名字是自由文本,
@@ -121,6 +122,36 @@ export type ProbeCauseProjection = z.infer<typeof ProbeCauseSchema>;
121
122
  * (截出来的是另一个机器码);`shown` 超限 ⇒ **截条目**,`total` 逐字保真。
122
123
  */
123
124
  export declare function readProbeCause(req: unknown): ProbeCauseProjection | undefined;
125
+ /**
126
+ * #263 件 G2 —— `AskRequest.ruleEvidence` 的形(帧 / `card_json` / 读面共用;窄读入口是
127
+ * {@link readRuleEvidence})。三对成员各 = 值**或**五词命名缺席(`AskEvidenceAbsence`),core 引擎
128
+ * 盖章的证据恰一在场;server 只判形转录,**永不**自铸缺席词(server 铸词 = 谎报「某一层没报」)。
129
+ */
130
+ export declare const RuleEvidenceSchema: z.ZodObject<{
131
+ orgRevision: z.ZodOptional<z.ZodNumber>;
132
+ orgRevisionAbsent: z.ZodOptional<z.ZodCustom<AskEvidenceAbsence, AskEvidenceAbsence>>;
133
+ orgRule: z.ZodOptional<z.ZodString>;
134
+ orgRuleAbsent: z.ZodOptional<z.ZodCustom<AskEvidenceAbsence, AskEvidenceAbsence>>;
135
+ personalRuleDots: z.ZodOptional<z.ZodArray<z.ZodObject<{
136
+ actor: z.ZodString;
137
+ counter: z.ZodNumber;
138
+ }, z.core.$strict>>>;
139
+ personalRuleDotsAbsent: z.ZodOptional<z.ZodCustom<AskEvidenceAbsence, AskEvidenceAbsence>>;
140
+ }, z.core.$strict>;
141
+ export type RuleEvidenceProjection = z.infer<typeof RuleEvidenceSchema>;
142
+ /**
143
+ * `AskRequest.ruleEvidence`(core 5.35.0 design/252 G-2)的**边界窄读**——活卡帧(`ToolApprovalFrame.
144
+ * ruleEvidence`)与 `card_json`(`buildApprovalCard`)的**唯一**铸造点,两面结构性同值(与
145
+ * {@link readProbeCause} 同款分工)。姿势逐条:
146
+ * · `safeParse`,形不合 ⇒ 按缺席处置(宪法 [2704]);未知键 strip(core additive 加键不塌卡)。
147
+ * · **恰一不变量**:一对成员(值 / 缺席词)**双在场** ⇒ 整只丢——那是矛盾证据(引擎盖章的恰一,
148
+ * 双在场只可能是第三方 producer 的坏行),半解上卡比缺席更坏。双缺席**容忍**(老 producer 行)。
149
+ * · `orgRule` 内容族(管理员写的规则文本)⇒ `redactSecrets` 后截 `MAX_RULE_TEXT_CHARS`,**先脱敏
150
+ * 再截一次算定**(`persistedRuleShadowed` R2-[medium] 教训:redact 会变长,两面各截会漂);空串丢整只。
151
+ * · `dots.actor` 身份串 ⇒ `redactSecrets`(对正常 id 恒等);超限**整只丢**(见上限顶注,身份不可截)。
152
+ * · `orgRevision` / 缺席词 verbatim(数值 / 闭集词,非内容)。
153
+ */
154
+ export declare function readRuleEvidence(req: unknown): RuleEvidenceProjection | undefined;
124
155
  /**
125
156
  * design/172 §3.1 的**中性投影**——呈卡面看到的全部内容,与 wire 面的既有 `ToolApprovalFrame` 解耦。
126
157
  *
@@ -165,6 +196,17 @@ export declare const ApprovalCardSchema: z.ZodObject<{
165
196
  total: z.ZodNumber;
166
197
  }, z.core.$strict>>;
167
198
  }, z.core.$strict>>;
199
+ ruleEvidence: z.ZodOptional<z.ZodObject<{
200
+ orgRevision: z.ZodOptional<z.ZodNumber>;
201
+ orgRevisionAbsent: z.ZodOptional<z.ZodCustom<AskEvidenceAbsence, AskEvidenceAbsence>>;
202
+ orgRule: z.ZodOptional<z.ZodString>;
203
+ orgRuleAbsent: z.ZodOptional<z.ZodCustom<AskEvidenceAbsence, AskEvidenceAbsence>>;
204
+ personalRuleDots: z.ZodOptional<z.ZodArray<z.ZodObject<{
205
+ actor: z.ZodString;
206
+ counter: z.ZodNumber;
207
+ }, z.core.$strict>>>;
208
+ personalRuleDotsAbsent: z.ZodOptional<z.ZodCustom<AskEvidenceAbsence, AskEvidenceAbsence>>;
209
+ }, z.core.$strict>>;
168
210
  fromSubagent: z.ZodOptional<z.ZodLiteral<true>>;
169
211
  sourceTaskId: z.ZodOptional<z.ZodString>;
170
212
  sourceAgentName: z.ZodOptional<z.ZodString>;
@@ -216,6 +258,17 @@ export declare const ApprovalCardEnvelopeSchema: z.ZodObject<{
216
258
  total: z.ZodNumber;
217
259
  }, z.core.$strict>>;
218
260
  }, z.core.$strict>>;
261
+ ruleEvidence: z.ZodOptional<z.ZodObject<{
262
+ orgRevision: z.ZodOptional<z.ZodNumber>;
263
+ orgRevisionAbsent: z.ZodOptional<z.ZodCustom<AskEvidenceAbsence, AskEvidenceAbsence>>;
264
+ orgRule: z.ZodOptional<z.ZodString>;
265
+ orgRuleAbsent: z.ZodOptional<z.ZodCustom<AskEvidenceAbsence, AskEvidenceAbsence>>;
266
+ personalRuleDots: z.ZodOptional<z.ZodArray<z.ZodObject<{
267
+ actor: z.ZodString;
268
+ counter: z.ZodNumber;
269
+ }, z.core.$strict>>>;
270
+ personalRuleDotsAbsent: z.ZodOptional<z.ZodCustom<AskEvidenceAbsence, AskEvidenceAbsence>>;
271
+ }, z.core.$strict>>;
219
272
  fromSubagent: z.ZodOptional<z.ZodLiteral<true>>;
220
273
  sourceTaskId: z.ZodOptional<z.ZodString>;
221
274
  sourceAgentName: z.ZodOptional<z.ZodString>;
@@ -21,7 +21,8 @@
21
21
  * 没有方法、没有捕获的行为。
22
22
  */
23
23
  import { z } from "zod";
24
- import { MAX_RULE_TEXT_CHARS } from "@sema-agent/core";
24
+ import { ASK_EVIDENCE_ABSENCE_VALUES, MAX_RULE_TEXT_CHARS } from "@sema-agent/core";
25
+ import { redactSecrets } from "./trace/redact.js";
25
26
  /** 模型自由文本(`sourceAgentName` / `delegation.agentName`)的限长(设计稿 §6.2)。设计 §3.1 把
26
27
  * 「限长 + 脱敏」写成 **server 新增责任**(引擎无此层):spawning model 挑的名字是自由文本,
27
28
  * 不能指望壳去截——一条 100KB 的 agentName 在 server 侧就该被拒,而不是变成一张撑爆呈卡面的卡。 */
@@ -165,6 +166,97 @@ export function readProbeCause(req) {
165
166
  ...(raw.further !== undefined ? { further: family(raw.further) } : {}),
166
167
  };
167
168
  }
169
+ /**
170
+ * #263 件 G2(core 5.35.0 design/252 G-2)—— `ruleEvidence` 的**防御性上限**两兄弟(信封非政策,
171
+ * 与 {@link MAX_PROBE_CAUSE_CODE} 家族同姿势:core 侧值来自引擎自己的治理层,没有导出上限可抄)。
172
+ * `dot.actor` 是身份串(规则属主 id),256 取 core `APPROVER_ATTRIBUTION_MAX_CHARS` 的同族语义;
173
+ * dots 条数的真实域 = 同一条规则文本的并发批准数(个位数),16 是纯防御臂。
174
+ * 🔴 dots 超限 / actor 超限 ⇒ **整只丢**而不是截:core 契约明写「The set is the identity」——截过的
175
+ * 集合、截过的 actor 名的是**另一个**身份,比缺席更坏(与 probeCause `code` 超限同判据)。
176
+ */
177
+ const MAX_EVIDENCE_DOT_ACTOR = 256;
178
+ const MAX_EVIDENCE_DOTS = 16;
179
+ /** 五词缺席词表——单一属主 = core 的 `ASK_EVIDENCE_ABSENCE_VALUES`(闭集常量,不在本仓抄词;
180
+ * 上游加词时这里自动跟随)。`z.custom` 而不是 `z.enum`:后者要非空元组字面量型,从 readonly 数组
181
+ * 常量过去只能 `as unknown as`(类型卫生门禁),而运行期判据本来就是「在 core 词表里」这一件事。 */
182
+ const AskEvidenceAbsenceSchema = z.custom((v) => typeof v === "string" && ASK_EVIDENCE_ABSENCE_VALUES.includes(v));
183
+ /** 一颗规则 add dot(core `RuleDot`:add 的持久身份 `{actor, counter}`)。 */
184
+ const RuleDotSchema = z.object({ actor: z.string().min(1).max(MAX_EVIDENCE_DOT_ACTOR), counter: z.number().int().nonnegative() }).strict();
185
+ /**
186
+ * #263 件 G2 —— `AskRequest.ruleEvidence` 的形(帧 / `card_json` / 读面共用;窄读入口是
187
+ * {@link readRuleEvidence})。三对成员各 = 值**或**五词命名缺席(`AskEvidenceAbsence`),core 引擎
188
+ * 盖章的证据恰一在场;server 只判形转录,**永不**自铸缺席词(server 铸词 = 谎报「某一层没报」)。
189
+ */
190
+ export const RuleEvidenceSchema = z
191
+ .object({
192
+ orgRevision: z.number().finite().optional(),
193
+ orgRevisionAbsent: AskEvidenceAbsenceSchema.optional(),
194
+ orgRule: z.string().max(MAX_RULE_TEXT_CHARS).optional(),
195
+ orgRuleAbsent: AskEvidenceAbsenceSchema.optional(),
196
+ personalRuleDots: z.array(RuleDotSchema).max(MAX_EVIDENCE_DOTS).optional(),
197
+ personalRuleDotsAbsent: AskEvidenceAbsenceSchema.optional(),
198
+ })
199
+ .strict();
200
+ /** 窄读的入参形(不限长孪生,分工同 {@link ProbeCauseEnvelopeSchema}):只判结构,长度/脱敏由
201
+ * {@link readRuleEvidence} 判完之后一次算定。 */
202
+ const RuleEvidenceEnvelopeSchema = z.object({
203
+ ruleEvidence: z
204
+ .object({
205
+ orgRevision: z.number().finite().optional(),
206
+ orgRevisionAbsent: AskEvidenceAbsenceSchema.optional(),
207
+ orgRule: z.string().optional(),
208
+ orgRuleAbsent: AskEvidenceAbsenceSchema.optional(),
209
+ personalRuleDots: z.array(z.object({ actor: z.string().min(1), counter: z.number().int().nonnegative() })).optional(),
210
+ personalRuleDotsAbsent: AskEvidenceAbsenceSchema.optional(),
211
+ })
212
+ .optional(),
213
+ });
214
+ /**
215
+ * `AskRequest.ruleEvidence`(core 5.35.0 design/252 G-2)的**边界窄读**——活卡帧(`ToolApprovalFrame.
216
+ * ruleEvidence`)与 `card_json`(`buildApprovalCard`)的**唯一**铸造点,两面结构性同值(与
217
+ * {@link readProbeCause} 同款分工)。姿势逐条:
218
+ * · `safeParse`,形不合 ⇒ 按缺席处置(宪法 [2704]);未知键 strip(core additive 加键不塌卡)。
219
+ * · **恰一不变量**:一对成员(值 / 缺席词)**双在场** ⇒ 整只丢——那是矛盾证据(引擎盖章的恰一,
220
+ * 双在场只可能是第三方 producer 的坏行),半解上卡比缺席更坏。双缺席**容忍**(老 producer 行)。
221
+ * · `orgRule` 内容族(管理员写的规则文本)⇒ `redactSecrets` 后截 `MAX_RULE_TEXT_CHARS`,**先脱敏
222
+ * 再截一次算定**(`persistedRuleShadowed` R2-[medium] 教训:redact 会变长,两面各截会漂);空串丢整只。
223
+ * · `dots.actor` 身份串 ⇒ `redactSecrets`(对正常 id 恒等);超限**整只丢**(见上限顶注,身份不可截)。
224
+ * · `orgRevision` / 缺席词 verbatim(数值 / 闭集词,非内容)。
225
+ */
226
+ export function readRuleEvidence(req) {
227
+ const parsed = RuleEvidenceEnvelopeSchema.safeParse(req);
228
+ const raw = parsed.success ? parsed.data.ruleEvidence : undefined;
229
+ if (raw === undefined)
230
+ return undefined;
231
+ if ((raw.orgRevision !== undefined && raw.orgRevisionAbsent !== undefined) ||
232
+ (raw.orgRule !== undefined && raw.orgRuleAbsent !== undefined) ||
233
+ (raw.personalRuleDots !== undefined && raw.personalRuleDotsAbsent !== undefined)) {
234
+ return undefined;
235
+ }
236
+ let dots;
237
+ if (raw.personalRuleDots !== undefined) {
238
+ if (raw.personalRuleDots.length > MAX_EVIDENCE_DOTS)
239
+ return undefined;
240
+ dots = [];
241
+ for (const d of raw.personalRuleDots) {
242
+ const actor = redactSecrets(d.actor);
243
+ if (actor === "" || actor.length > MAX_EVIDENCE_DOT_ACTOR)
244
+ return undefined;
245
+ dots.push({ actor, counter: d.counter });
246
+ }
247
+ }
248
+ const orgRule = raw.orgRule !== undefined ? redactSecrets(raw.orgRule).slice(0, MAX_RULE_TEXT_CHARS) : undefined;
249
+ if (raw.orgRule !== undefined && (orgRule === undefined || orgRule === ""))
250
+ return undefined;
251
+ return {
252
+ ...(raw.orgRevision !== undefined ? { orgRevision: raw.orgRevision } : {}),
253
+ ...(raw.orgRevisionAbsent !== undefined ? { orgRevisionAbsent: raw.orgRevisionAbsent } : {}),
254
+ ...(orgRule !== undefined ? { orgRule } : {}),
255
+ ...(raw.orgRuleAbsent !== undefined ? { orgRuleAbsent: raw.orgRuleAbsent } : {}),
256
+ ...(dots !== undefined ? { personalRuleDots: dots } : {}),
257
+ ...(raw.personalRuleDotsAbsent !== undefined ? { personalRuleDotsAbsent: raw.personalRuleDotsAbsent } : {}),
258
+ };
259
+ }
168
260
  /**
169
261
  * design/172 §3.1 的**中性投影**——呈卡面看到的全部内容,与 wire 面的既有 `ToolApprovalFrame` 解耦。
170
262
  *
@@ -273,6 +365,13 @@ export const ApprovalCardSchema = z
273
365
  * `.strict()` 下旧二进制读不动带本键的新行 ⇒ 重放跳过 + 幂等重入走 park(fail-safe),`schemaVersion` 不动。
274
366
  */
275
367
  probeCause: ProbeCauseSchema.optional(),
368
+ /**
369
+ * #263 件 G2(core 5.35.0 design/252 G-2):这只 ask 背后的**规则出处证据**——org 快照 revision /
370
+ * 命中的 org 规则原文 / 被越级的个人规则 add dots,每员 = 值或五词命名缺席。展示/对账元数据,
371
+ * **永不**是裁决输入(core 契约逐字:removing it would leave every decision byte-identical)。
372
+ * 回滚窗代价与 `probeCause` 逐字同族(`.strict()` 下旧二进制读不动新行 ⇒ 重放跳过 + park fail-safe)。
373
+ */
374
+ ruleEvidence: RuleEvidenceSchema.optional(),
276
375
  /** 委派出处(子代 ask 才在场;判别键 = `fromSubagent`,core RB-39②)。 */
277
376
  fromSubagent: z.literal(true).optional(),
278
377
  sourceTaskId: z.string().max(MAX_IDENT).optional(),
@@ -351,6 +450,8 @@ export function buildApprovalCard(source, req, requiresRealApproval) {
351
450
  // #253 件 G1:`probeCause` 与两轴同款,从 `req`(core 的 AskRequest)窄读 —— 发帧点用**同一个**函数
352
451
  // 铸帧上那一份,于是「帧与卡同值」是结构事实而不是两处各挑一次键的巧合(§6.5 同款判据)。
353
452
  const probeCause = readProbeCause(req);
453
+ // #263 件 G2:`ruleEvidence` 同款(readRuleEvidence 是帧/卡唯一铸造点,redact+截长在函数内一次算定)。
454
+ const ruleEvidence = readRuleEvidence(req);
354
455
  const toolCallId = clip(source.toolCallId, MAX_IDENT);
355
456
  const sourceTaskId = clip(source.sourceTaskId, MAX_IDENT);
356
457
  const sourceAgentName = clip(source.sourceAgentName, MAX_AGENT_NAME);
@@ -373,6 +474,8 @@ export function buildApprovalCard(source, req, requiresRealApproval) {
373
474
  // #253 件 G1:结构化探针因由 —— 与活卡帧**同一个**窄读函数(`readProbeCause`),两面结构性同值;
374
475
  // 形不合/缺席 ⇒ 不铸键(语义见 `ApprovalCardSchema.probeCause` 顶注)。
375
476
  ...(probeCause !== undefined ? { probeCause } : {}),
477
+ // #263 件 G2:规则出处证据 —— 同款同源(`readRuleEvidence`);形不合/缺席 ⇒ 不铸键。
478
+ ...(ruleEvidence !== undefined ? { ruleEvidence } : {}),
376
479
  // #154 车二:候选**逐字**投影(空数组 ⇒ 不投键 —— 「没有候选」与「本部署不供候选」在卡面上都读作
377
480
  // 「本卡无此选项」,铸一个空数组只会让消费端多一条无意义的分支)。拷贝一份:卡不与调用方共享可变引用。
378
481
  ...(source.ruleSuggestions !== undefined && source.ruleSuggestions.length > 0
@@ -304,7 +304,7 @@ export async function main(argv = process.argv.slice(2)) {
304
304
  process.exit(2);
305
305
  return;
306
306
  }
307
- const modelId = liveRuntime?.modelId ?? "deepseek-v4-pro";
307
+ const modelId = liveRuntime?.modelId ?? "deepseek-v4-flash";
308
308
  const log = (msg, meta) => console.error(`[s1 run-firm] ${msg}`, meta ? JSON.stringify(meta) : "");
309
309
  const result = await runFirm({
310
310
  seeds: args.seeds,
@@ -30,7 +30,6 @@ export declare function createBudgetAndTracing(ctx: BudgetTracingCtx): {
30
30
  tracer: import("@sema-agent/core").TracerHook;
31
31
  sideQueryAccounting: (principal: string | undefined, r: {
32
32
  model: string;
33
- family?: "input-includes-cached" | "input-excludes-cached";
34
33
  usage?: {
35
34
  input?: number;
36
35
  output?: number;
@@ -432,7 +432,9 @@ export function createResolveSpec(ctx) {
432
432
  // 的 auth 从 checkpoint 重建、无此字段 → 回落持久化 body.projectId(原始提交已过形状门,与 memoryWrite
433
433
  // 的 rides-the-persisted-body 姿势一致),让种子在 resume 腿等价重放。查表 miss(未登记/已下架)= 无种子,
434
434
  // 不 fail(登记簿是加性配置面,不是准入门)。
435
- const s4ProjectId = auth?.resolvedProjectId ?? (typeof body.projectId === "string" && body.projectId ? body.projectId : undefined);
435
+ // A-033.1(#256):回落腿同样折小写 —— authorizer 那一格自 core `/i` 词法起收下大写形并归一,
436
+ // 若这条回落腿原样传,同一个 projectId 在首提交与 resume 两腿会查两个不同的 `config.projects` 键。
437
+ const s4ProjectId = auth?.resolvedProjectId ?? (typeof body.projectId === "string" && body.projectId ? body.projectId.toLowerCase() : undefined);
436
438
  const s4DefaultScopes = s4ProjectId ? config.projects[s4ProjectId]?.defaultScopes : undefined;
437
439
  return { resumeAtEntryId, rewindFilesToEntryId, s4DefaultScopes };
438
440
  };
package/dist/budget.d.ts CHANGED
@@ -102,19 +102,8 @@ export declare function createTracer(metrics: Pick<Metrics, "inc" | "observe">,
102
102
  * (保守,绝不少收);根修=core 在 SideQueryResult 上回权威 costMicroUsd(已提货 core)。model 标签/
103
103
  * weight 键用**路由身份 r.model**(池配置有界)非 servedModel(provider 上报,无界)。
104
104
  */
105
- /** core `usage-accounting.cacheFamilyOf` 的私拷镜像(core exports map 只开 barrel,该函数未导出——
106
- * 已提货 core barrel 导出,到货即换,normalizeAgentName 同款纪律)。逐字对拍源:
107
- * dist/core/runner/usage-accounting.js(override 别名 anthropic/openai + api 判 anthropic-messages/
108
- * bedrock-converse-stream)。undefined 入参(catalog 查不到)→ undefined,调用方走保守臂。 */
109
- export declare function cacheFamilyOfMirror(model: {
110
- api?: string;
111
- params?: {
112
- promptCacheFamily?: string;
113
- };
114
- } | undefined): "input-includes-cached" | "input-excludes-cached" | undefined;
115
105
  export declare function createSideQueryAccountant(metrics: Pick<Metrics, "inc">, costQuota?: QuotaTracker, fleetUsage?: Pick<FleetUsageAccumulator, "record">, fleetLease?: Pick<FleetLeaseManager, "recordSpend">, quotaWeightFor?: (model: string) => number): (principal: string | undefined, r: {
116
106
  model: string;
117
- family?: "input-includes-cached" | "input-excludes-cached";
118
107
  usage?: {
119
108
  input?: number;
120
109
  output?: number;
package/dist/budget.js CHANGED
@@ -341,20 +341,6 @@ export function createTracer(metrics, costQuota, modelUsage, fleetUsage, fleetLe
341
341
  * (保守,绝不少收);根修=core 在 SideQueryResult 上回权威 costMicroUsd(已提货 core)。model 标签/
342
342
  * weight 键用**路由身份 r.model**(池配置有界)非 servedModel(provider 上报,无界)。
343
343
  */
344
- /** core `usage-accounting.cacheFamilyOf` 的私拷镜像(core exports map 只开 barrel,该函数未导出——
345
- * 已提货 core barrel 导出,到货即换,normalizeAgentName 同款纪律)。逐字对拍源:
346
- * dist/core/runner/usage-accounting.js(override 别名 anthropic/openai + api 判 anthropic-messages/
347
- * bedrock-converse-stream)。undefined 入参(catalog 查不到)→ undefined,调用方走保守臂。 */
348
- export function cacheFamilyOfMirror(model) {
349
- if (!model)
350
- return undefined;
351
- const o = model.params?.promptCacheFamily;
352
- if (o === "input-excludes-cached" || o === "anthropic")
353
- return "input-excludes-cached";
354
- if (o === "input-includes-cached" || o === "openai")
355
- return "input-includes-cached";
356
- return model.api === "anthropic-messages" || model.api === "bedrock-converse-stream" ? "input-excludes-cached" : "input-includes-cached";
357
- }
358
344
  export function createSideQueryAccountant(metrics, costQuota,
359
345
  // 依赖面收窄到真消费的方法(§X 同 ModelKeyRef 一式):全形对象照传(结构超集),测试桩零铸型。
360
346
  fleetUsage, fleetLease, quotaWeightFor) {
@@ -363,14 +349,16 @@ fleetUsage, fleetLease, quotaWeightFor) {
363
349
  const output = typeof r.usage?.output === "number" ? r.usage.output : 0;
364
350
  const cacheRead = typeof r.usage?.cacheRead === "number" ? r.usage.cacheRead : 0;
365
351
  const cacheWrite = typeof r.usage?.cacheWrite === "number" ? r.usage.cacheWrite : 0;
366
- // codex R4 high:prompt tokens 必须按 cache family 折算(core promptTokensOf 同式)——Anthropic 族
367
- // (input-excludes-cached)的 cache 命中几乎全部在 input 之外,不加 cacheRead/cacheWrite 就是配额轴
368
- // 大幅少收(lease 可被 cache-heavy side query 绕穿)。family 未知(catalog 查不到)走**保守臂**
369
- // (含 cache——OpenAI 族此臂多收,方向与 cost 残差一致:宁多勿少,记档同一条)。
370
- // core 3.0.0 核实(S1 提货站):此段的输入源是 `SideQueryResult.usage`(llm 层生料 `Usage`,
371
- // engine/llm/types.d.ts)——不是 3.0.0 翻新过的 `brain.call`/`turn_end` trace 事件面,3.0.0 未动这个类型,
372
- // 本折算继续承重(零改动)。
373
- const promptTokens = r.family === "input-includes-cached" ? input : input + cacheRead + cacheWrite;
352
+ // #257(codex #256-R3 high 定谳):core side-query 出口已把 `usage.input` **归一为 uncached 量**
353
+ // ——对每个 provider 族(side-query.d.ts:「`input` NORMALIZED to the uncached prompt quantity …
354
+ // for EVERY provider family」,与 run loop usage-accounting 同一道门)。所以配额轴折算恒为
355
+ // input + cacheRead + cacheWrite,**不再按 cache family 分臂**:曾经的
356
+ // `family === "input-includes-cached" ? input : …` 分支建立在「OpenAI 族 input 仍含 cache」这个
357
+ // 已被 core 出口归一推翻的前提上,每次 cache 命中都把 OpenAI 族配额/lease 少收 cacheRead+cacheWrite
358
+ // (红先钉 = budget.test「#257 OpenAI 族 cache 命中不许少计」格)。family 判定链
359
+ // (cacheFamilyOfMirror + catalog 快照)随本修整体退役——LEDGER cacheFamilyOf/CacheFamily
360
+ // 两行同步改 skip(消费点消失,理由=core 出口归一)。
361
+ const promptTokens = input + cacheRead + cacheWrite;
374
362
  // RB-368/#59 同判据(createTracer 上方注释同款纪律):`usage.cost.total` 整键缺席 = 未知(unpriced 部署的
375
363
  // side query 真形),不是「花了 $0」——折 0 就是把「不知道」编造成「免费」。已知(哪怕显式 0)才带键。
376
364
  const costTotal = typeof r.usage?.cost?.total === "number" ? r.usage.cost.total : undefined;
@@ -1,4 +1,4 @@
1
- import { CODE_AGENT_PROMPT, CODE_SYSTEM_PROMPT, DEFAULT_SYSTEM_PROMPT, assembleCodeTools, defaultTaskRegistry } from "@sema-agent/core";
1
+ import { CODE_AGENT_PROMPT, CODE_SYSTEM_PROMPT, DEFAULT_SYSTEM_PROMPT, WEB_FETCH_TOOL_NAME, assembleCodeTools, defaultTaskRegistry } from "@sema-agent/core";
2
2
  import { HttpError } from "../security.js";
3
3
  import { nowTool } from "./builtin-tools.js";
4
4
  import { parseRepo, repoToolsFor } from "./repo-tools.js";
@@ -164,7 +164,10 @@ export function buildScenarios(deps) {
164
164
  : {}),
165
165
  },
166
166
  ...(webSearch ? { webSearch } : {}),
167
- }).filter((t) => t.name !== "WebFetch" || deps.requirePrincipal !== true),
167
+ // A-033.1(#256):工具名取 core `WEB_FETCH_TOOL_NAME` 而不是裸串 —— 这是**多租户闸**
168
+ // (allowHosts 未设 = 全公网可达 ⇒ 一个租户能把另一个租户的内网可达面当出口)。core 改名该工具时,
169
+ // 裸串比对恒不相等,闸门整条不生效且零症状:名单里照样挂着一只全公网 WebFetch。
170
+ }).filter((t) => t.name !== WEB_FETCH_TOOL_NAME || deps.requirePrincipal !== true),
168
171
  ],
169
172
  // default/code 是手带并集的**故意消费者**(上方 rank-1 BLOCKER 注:CC agent loop MUST have the
170
173
  // full-body roster)——#196 判别位在这里表态 full,收窄只落在声明了 none 的场景上。
@@ -238,7 +241,10 @@ export function buildScenarios(deps) {
238
241
  }
239
242
  /** Resolve the scenario for a request (falls back to `default`). */
240
243
  export function selectScenario(scenarios, name) {
241
- return (name && scenarios[name]) || scenarios.default;
244
+ // #193 P2-13:own-property 守卫——`Record` 字面量的裸索引对原型链键("constructor" 形状)会把
245
+ // Object.prototype 上的函数当 Scenario 返回。上游 resolve-spec 按 Object.keys 校验 ⇒ 当前不可利用,
246
+ // 本守卫是纵深防御(本函数是导出面,不得依赖单一调用方的入口校验)。
247
+ return (name && Object.hasOwn(scenarios, name) && scenarios[name]) || scenarios.default;
242
248
  }
243
249
  /** 把属主的判别式摊平成 wire 上的两个键(可用臂整键省略原因,契约=「缺席即没有理由」)。 */
244
250
  function availabilityFields(deps, toolset) {
@@ -251,7 +257,7 @@ function availabilityFields(deps, toolset) {
251
257
  export function builtinScenarioDetails(scenarios, deps) {
252
258
  const probe = (name, req, principal, fallback) => {
253
259
  try {
254
- return (scenarios[name]?.(req, principal)?.tools ?? []).map((t) => t.name);
260
+ return ((Object.hasOwn(scenarios, name) ? scenarios[name] : undefined)?.(req, principal)?.tools ?? []).map((t) => t.name); // P2-13 同族守卫
255
261
  }
256
262
  catch {
257
263
  return fallback; // 工厂 fail-loud(部署未配该场景的后端)→ 静态名单(web 仍能展示工具面选项)
@@ -283,8 +289,8 @@ export function centerScenarioDetails(specs, builtinNames, deps) {
283
289
  continue;
284
290
  if (typeof spec.name !== "string" || !SCENARIO_NAME_RE.test(spec.name))
285
291
  continue;
286
- if (!TOOLSETS[spec.toolset])
287
- continue;
292
+ if (!Object.hasOwn(TOOLSETS, spec.toolset) || !TOOLSETS[spec.toolset])
293
+ continue; // P2-13 同族守卫
288
294
  out[spec.name] = {
289
295
  name: spec.name,
290
296
  source: "center",
@@ -329,7 +335,7 @@ const SCENARIO_AVAILABLE = { available: true };
329
335
  * 词表外的 toolset(内建的 `full-body`/`team`)不依赖后端 ⇒ 恒可用。
330
336
  */
331
337
  export function scenarioAvailability(deps, toolset) {
332
- if (TOOLSETS[toolset]?.requiresRepo === true && deps.repoClient === undefined) {
338
+ if (Object.hasOwn(TOOLSETS, toolset) && TOOLSETS[toolset]?.requiresRepo === true && deps.repoClient === undefined) { // P2-13 同族守卫
333
339
  return { available: false, reason: "git_client_unconfigured" };
334
340
  }
335
341
  return SCENARIO_AVAILABLE;
@@ -369,7 +375,7 @@ export function centerScenarios(deps, specs, builtinNames, logger) {
369
375
  logger?.warn("sema_registry_scenario_invalid", { name: String(spec?.name), reason: "name must match " + String(SCENARIO_NAME_RE) });
370
376
  continue;
371
377
  }
372
- const toolset = TOOLSETS[spec.toolset];
378
+ const toolset = Object.hasOwn(TOOLSETS, spec.toolset) ? TOOLSETS[spec.toolset] : undefined; // P2-13 同族守卫
373
379
  if (!toolset) {
374
380
  logger?.warn("sema_registry_scenario_invalid", { name: spec.name, reason: `unknown toolset "${String(spec.toolset)}" (have: ${Object.keys(TOOLSETS).join(", ")})` });
375
381
  continue;
package/dist/config.d.ts CHANGED
@@ -64,7 +64,7 @@ export declare function parseNumOrFailNonNegative(name: string, raw: string | un
64
64
  * `invalid-default` 是**缺省种类**(不写 `kind` 即是它)—— 历史形 `{env,raw}` 的三个存量生产者
65
65
  * (`optFinitePositiveEnv` / `boolEnv` / `boolEnvWithLegacyNegated`)因此逐字不动,它们的断言也不破。
66
66
  */
67
- type ConfigWarningKind = "invalid-default" | "clamped" | "empty-preserved" | "entry-rejected" | "set-rejected";
67
+ type ConfigWarningKind = "invalid-default" | "clamped" | "empty-preserved" | "entry-rejected" | "set-rejected" | "ineffective";
68
68
  interface ConfigWarning {
69
69
  env: string;
70
70
  /** 环境原文。`entry-rejected` 上恒为脱敏占位 —— 那两条 env 装的是凭据,原文永不进日志。 */
package/dist/config.js CHANGED
@@ -255,6 +255,9 @@ export function logConfigDiagnostics(logger) {
255
255
  case "set-rejected":
256
256
  logger.warn("config_env_set_rejected", { env: w.env, problem: w.detail });
257
257
  break;
258
+ case "ineffective":
259
+ logger.warn("config_env_set_but_ineffective", { env: w.env, raw: w.raw, problem: w.detail });
260
+ break;
258
261
  case "invalid-default":
259
262
  logger.warn("config_env_invalid_using_default", { env: w.env, raw: w.raw, ...(w.used !== undefined ? { used: w.used } : {}) });
260
263
  break;
@@ -2038,7 +2041,15 @@ function parseLimitsHttpDomain() {
2038
2041
  // 基建成本恒 0,查不到是自己那个负号被吃掉了。
2039
2042
  toolCallMicroUsd: clampEnvWithWarn("INFRA_COST_TOOL_CALL", "0", 0),
2040
2043
  sandboxSecMicroUsd: clampEnvWithWarn("INFRA_COST_SANDBOX_SEC", "0", 0),
2041
- egressGbMicroUsd: clampEnvWithWarn("INFRA_COST_EGRESS_GB", "0", 0),
2044
+ // #193 车5(P1-4):per-run 出网计量未建(cost-taxonomy `infraUsageFromEvents` 恒 `egressBytes: 0`),
2045
+ // 该费率设成任何值都恒计 $0。旋钮保留(接真计量是产品件),但设非零必点名——静默会让 operator
2046
+ // 以为出网已计费。接上计量的那批必须同时删掉这条 push(test/config-knob-loudness #193 格看守)。
2047
+ egressGbMicroUsd: (() => {
2048
+ const v = clampEnvWithWarn("INFRA_COST_EGRESS_GB", "0", 0);
2049
+ if (v > 0)
2050
+ CONFIG_WARNINGS.push({ env: "INFRA_COST_EGRESS_GB", raw: env("INFRA_COST_EGRESS_GB", "0"), kind: "ineffective", detail: "egress metering is not wired (per-run egressBytes is a constant 0) — this rate multiplies zero and will never bill anything" });
2051
+ return v;
2052
+ })(),
2042
2053
  },
2043
2054
  drainGraceMs: clampEnvWithWarn("DRAIN_GRACE_MS", String(10 * 60 * 1000), 10_000), // SIGTERM drain window, default 10min, floor 10s(#210 B 档:把 600 当成"秒"填进来的手滑最常见,夹取后必须点名)
2044
2055
  sighupIdleGraceMs: clampEnvWithWarn("SIGHUP_IDLE_GRACE_MS", String(120_000), 5_000), // B3: SIGHUP idle-shutdown window, default 2min, floor 5s
@@ -4,9 +4,14 @@ import type { RunRecord } from "../plugins/store-contracts.js";
4
4
  * `onChildTerminal`(fleet「subagent 树」只渲 agent 子代)。`background_bash`/`monitor`/`external`
5
5
  * 不属 agent fleet 树——它们此前每条都打,fleet-bus 的「无 tick 无 claim」臂给 b\* 与 m\* handle 合成
6
6
  * agent 形瞬态终帧,壳终态留存池把这帧渲 60s = footer 幽灵子行;monitor 的 `status:"event"` 还被
7
- * 终态映射折成 `failed`。**两侧都正向枚举**(新 task_type/新 status 默认不铸行=安全方向)。
8
- * 与 core `isDelegatedAgentTerminal`(task-notification.js,未从包根导出)语义同源——core 根导出后
9
- * 换装 import 删本镜像(提货件已上板) */
7
+ * 终态映射折成 `failed`。
8
+ *
9
+ * 🔴 A-033.1(#256):判别**本体已换 core 的 {@link isDelegatedAgentTerminal}**。此前这里是一份手写语义
10
+ * 孪生(自己枚举 `completed|failed|killed|cancelled`),头注自陈「与 core 语义同源——core 根导出后换装
11
+ * import 删本镜像」。core 现已从包根导出(`index.d.ts:109`),且它的终态集与 pending 店的驱逐偏好共用
12
+ * 同一份 `TERMINAL_STATUSES`(core 原话:「so "terminal" can never mean two things in this module」)——
13
+ * 孪生留在这里等于让 fleet 面板对「终态」的理解可以和引擎分家。本函数保留为**具名的 fleet 语义位**
14
+ * (调用点读的是「这条通知该不该打 fleet 的 onChildTerminal」,不是「core 怎么判终态」)。 */
10
15
  export declare function isFleetAgentTerminalNotification(n: Pick<TaskNotificationPayload, "task_type" | "status">): boolean;
11
16
  /** The fleet vocabulary the shell renders (MF-Fleet `FleetTask.status`). The service maps its run/workflow status
12
17
  * onto this neutral set; `awaiting approval` = a needs-review/plan-approval park, `waiting` = a durable suspend.
@@ -21,18 +21,23 @@
21
21
  * replica-local). A cross-replica fleet roll-up is a fleet-token-gated trace-API concern (separate), not this.
22
22
  */
23
23
  import { EventEmitter } from "node:events";
24
- import { deriveAgentDisplayStatus } from "@sema-agent/core";
24
+ import { deriveAgentDisplayStatus, isDelegatedAgentTerminal } from "@sema-agent/core";
25
25
  import { recordFailOpen } from "../observability/fail-open.js";
26
26
  import { redactSecrets } from "../trace/redact.js";
27
27
  /** [2687-cli] 幽灵行案的单源判别:一条 `task_notification` 只有在 **agent 族 × 终态** 时才允许打
28
28
  * `onChildTerminal`(fleet「subagent 树」只渲 agent 子代)。`background_bash`/`monitor`/`external`
29
29
  * 不属 agent fleet 树——它们此前每条都打,fleet-bus 的「无 tick 无 claim」臂给 b\* 与 m\* handle 合成
30
30
  * agent 形瞬态终帧,壳终态留存池把这帧渲 60s = footer 幽灵子行;monitor 的 `status:"event"` 还被
31
- * 终态映射折成 `failed`。**两侧都正向枚举**(新 task_type/新 status 默认不铸行=安全方向)。
32
- * 与 core `isDelegatedAgentTerminal`(task-notification.js,未从包根导出)语义同源——core 根导出后
33
- * 换装 import 删本镜像(提货件已上板) */
31
+ * 终态映射折成 `failed`。
32
+ *
33
+ * 🔴 A-033.1(#256):判别**本体已换 core 的 {@link isDelegatedAgentTerminal}**。此前这里是一份手写语义
34
+ * 孪生(自己枚举 `completed|failed|killed|cancelled`),头注自陈「与 core 语义同源——core 根导出后换装
35
+ * import 删本镜像」。core 现已从包根导出(`index.d.ts:109`),且它的终态集与 pending 店的驱逐偏好共用
36
+ * 同一份 `TERMINAL_STATUSES`(core 原话:「so "terminal" can never mean two things in this module」)——
37
+ * 孪生留在这里等于让 fleet 面板对「终态」的理解可以和引擎分家。本函数保留为**具名的 fleet 语义位**
38
+ * (调用点读的是「这条通知该不该打 fleet 的 onChildTerminal」,不是「core 怎么判终态」)。 */
34
39
  export function isFleetAgentTerminalNotification(n) {
35
- return n.task_type === "background_agent" && (n.status === "completed" || n.status === "failed" || n.status === "killed" || n.status === "cancelled");
40
+ return isDelegatedAgentTerminal(n);
36
41
  }
37
42
  /**
38
43
  * durable {@link WorkflowRun} → 一条 fleet workflow 行的**纯投影**(`build*`=纯数据,无行为、不发布)。
@@ -173,4 +173,15 @@ export type RouteHandler = (req: IncomingMessage, res: ServerResponse, url: stri
173
173
  export interface RouteMiss {
174
174
  fell: boolean;
175
175
  }
176
+ /**
177
+ * #262(A-041 存在性 oracle 封口)——`not_found.run` 的**唯一**文案源,unknown 与 not-yours 两臂共用。
178
+ * 修前:unknown 臂发这条长指路文案、not-yours 臂发短文「run not found」,同 code 下按**文案长度**即可
179
+ * 枚举他人 run id 的存在性(822 行「no existence oracle」注释的意图被长短差打穿)。not-yours 臂只可能
180
+ * 拿到非 a\* id(store 查到行才有 owner 比对;a\* 与 wa\* 形 id 永不在 run store)⇒ 非 a\* 长文串足以让
181
+ * 两臂逐字节同形,指路价值不丢。钉=audit-fixes-http「#262」格(两臂全文比对);文案变更走
182
+ * api-error-text-freeze 基线流程。
183
+ */
184
+ export declare const RUN_NOT_FOUND_MESSAGE = "run not found \u2014 the id belongs to no run in this deployment's run store (a run from another server process, or an in-memory store that did not survive a restart, is not visible here)";
185
+ /** [1434] 前缀=域判别子既成契约:a\* 与 wa\* 形 id 给 TaskOutput/journal 指路;其余给 {@link RUN_NOT_FOUND_MESSAGE}。 */
186
+ export declare function runNotFoundMessage(id: string): string;
176
187
  //# sourceMappingURL=route-ctx.d.ts.map
@@ -1,2 +1,17 @@
1
- export {};
1
+ /**
2
+ * #262(A-041 存在性 oracle 封口)——`not_found.run` 的**唯一**文案源,unknown 与 not-yours 两臂共用。
3
+ * 修前:unknown 臂发这条长指路文案、not-yours 臂发短文「run not found」,同 code 下按**文案长度**即可
4
+ * 枚举他人 run id 的存在性(822 行「no existence oracle」注释的意图被长短差打穿)。not-yours 臂只可能
5
+ * 拿到非 a\* id(store 查到行才有 owner 比对;a\* 与 wa\* 形 id 永不在 run store)⇒ 非 a\* 长文串足以让
6
+ * 两臂逐字节同形,指路价值不丢。钉=audit-fixes-http「#262」格(两臂全文比对);文案变更走
7
+ * api-error-text-freeze 基线流程。
8
+ */
9
+ export const RUN_NOT_FOUND_MESSAGE = "run not found — the id belongs to no run in this deployment's run store (a run from another server process, or an in-memory store that did not survive a restart, is not visible here)";
10
+ /** [1434] 前缀=域判别子既成契约:a\* 与 wa\* 形 id 给 TaskOutput/journal 指路;其余给 {@link RUN_NOT_FOUND_MESSAGE}。 */
11
+ export function runNotFoundMessage(id) {
12
+ const aStar = /^(?:a|wa)[0-9a-f]{4,}$/i.test(id);
13
+ return aStar
14
+ ? `run not found — "${id}" looks like an agent handle (a*/wa* domain), which is never in the run store; use the TaskOutput tool (or the workflow journal / subagents surface) to read it, and the run's durable taskId (X-Task-Id) for this endpoint`
15
+ : RUN_NOT_FOUND_MESSAGE;
16
+ }
2
17
  //# sourceMappingURL=route-ctx.js.map