@sema-agent/server 3.1.0 → 3.2.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.
- package/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/USAGE.md +9 -9
- package/dist/approval-hmac.js +1 -2
- package/dist/bench/s1/live-deps.js +1 -1
- package/dist/boot/config-center.d.ts +1 -1
- package/dist/boot/config-center.js +6 -6
- package/dist/boot/resolve-spec.js +5 -5
- package/dist/brain.js +42 -9
- package/dist/config-center/apply-effective.d.ts +1 -1
- package/dist/config-center/apply-effective.js +4 -4
- package/dist/config-center/facade.d.ts +47 -0
- package/dist/config-center/facade.js +46 -0
- package/dist/config-center/http-client.d.ts +1 -1
- package/dist/config-center/http-client.js +6 -6
- package/dist/config-center/restart-signal.d.ts +1 -1
- package/dist/config-center/restart-signal.js +36 -16
- package/dist/config-center/skills-mcp.js +1 -1
- package/dist/config-center/types.d.ts +2 -2
- package/dist/config-provider.d.ts +7 -7
- package/dist/config-provider.js +9 -9
- package/dist/config.d.ts +1 -1
- package/dist/config.js +5 -5
- package/dist/http/routes/approvals-assistant.js +3 -3
- package/dist/http/routes/runs.js +7 -7
- package/dist/http/routes/sessions.js +1 -1
- package/dist/http/routes/trace-usage.js +2 -2
- package/dist/http/server.d.ts +1 -1
- package/dist/http/server.js +3 -3
- package/dist/key-resolver.d.ts +14 -0
- package/dist/key-resolver.js +27 -11
- package/dist/run-local.js +2 -2
- package/dist/runtime-caps-resolver.d.ts +1 -1
- package/dist/runtime-caps-resolver.js +1 -1
- package/dist/runtime-governance.d.ts +1 -1
- package/dist/runtime-governance.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -132,7 +132,7 @@ The server is configured entirely through environment variables. The most import
|
|
|
132
132
|
| `PORT` | `8090` | HTTP listen port |
|
|
133
133
|
| `BIND_HOST` (alias `HOST`) | see note | Listen address. An explicit value **always wins**. Default: `127.0.0.1` when the write face is unauthenticated (`ALLOW_UNAUTHED_WRITES=true` **and** no service token configured), otherwise all interfaces — deployments with a token are unaffected. |
|
|
134
134
|
| `MODEL_GATEWAY_BASEURL` | `http://127.0.0.1:8000/v1` | OpenAI-compatible gateway base URL (without `/chat/completions`) |
|
|
135
|
-
| `MODEL_ID` | **required** | Default model id — **no factory default since 3.0.0**. Unset ⇒ the server refuses to boot with a message naming the knob (the old baked-in default was an internal-only model name, so every external deployment failed later and further from the cause: a gateway `400` plus a cascade of title-hook warnings). Set it to whatever model name your gateway serves, or supply the catalog via the
|
|
135
|
+
| `MODEL_ID` | **required** | Default model id — **no factory default since 3.0.0**. Unset ⇒ the server refuses to boot with a message naming the knob (the old baked-in default was an internal-only model name, so every external deployment failed later and further from the cause: a gateway `400` plus a cascade of title-hook warnings). Set it to whatever model name your gateway serves, or supply the catalog via the config-center control plane |
|
|
136
136
|
| `MODEL_API_KEY` | — | Gateway API key (optional) |
|
|
137
137
|
| `SERVICE_AUTH_TOKEN` | — | Callers must send `Authorization: Bearer <token>` |
|
|
138
138
|
| `DB_BACKEND` | `local`* | `mysql` (any MySQL-protocol DB: MySQL/TiDB/MariaDB; `tidb` alias) / `pg` (PostgreSQL) / `local` (file-backed, no DB) / `memory` (explicit in-memory: nothing survives a restart, durable-runs faces 501). *Bare boot (no DB env at all) defaults to `local` so a single-user machine keeps its runs across restarts; any SQL signal (`SESSION_BACKEND` or `TIDB_/MYSQL_/PG_HOST`) keeps the `mysql` engine default, and `REQUIRE_PRINCIPAL=true` bare boots stay `memory` (the local file store has no tenant isolation — a warning says so). A DEFAULT-derived `local` that cannot create its data root degrades to memory with a warning + the `store_backend_degraded` gauge; an EXPLICIT `DB_BACKEND=local` fails loud instead. Setting `mysql`/`pg` explicitly also switches sessions to durable |
|
package/README.zh-CN.md
CHANGED
|
@@ -125,7 +125,7 @@ curl -s localhost:8090/v1/tasks -H "Authorization: Bearer <SERVICE_AUTH_TOKEN>"
|
|
|
125
125
|
| `PORT` | `8090` | HTTP 监听端口 |
|
|
126
126
|
| `BIND_HOST`(兼容 `HOST`) | 见说明 | 监听地址。显式值**恒生效**。缺省:写面无鉴权时(`ALLOW_UNAUTHED_WRITES=true` **且**未配任何 service token)= `127.0.0.1`,否则全接口——配了 token 的部署不受影响。 |
|
|
127
127
|
| `MODEL_GATEWAY_BASEURL` | `http://127.0.0.1:8000/v1` | OpenAI 兼容网关地址(不带 `/chat/completions`) |
|
|
128
|
-
| `MODEL_ID` | **必填** | 缺省模型 id ——**3.0.0 起无出厂缺省**。未设 = 启动即失败并指路该旋钮(旧的烤死缺省是内网模型名,外部部署必炸且炸在离根因最远处:网关 `400` + 标题 hook 连环告警)
|
|
128
|
+
| `MODEL_ID` | **必填** | 缺省模型 id ——**3.0.0 起无出厂缺省**。未设 = 启动即失败并指路该旋钮(旧的烤死缺省是内网模型名,外部部署必炸且炸在离根因最远处:网关 `400` + 标题 hook 连环告警)。填你的网关真正提供的模型名,或改由配置控制面下发目录 |
|
|
129
129
|
| `MODEL_API_KEY` | — | 网关 key(可选) |
|
|
130
130
|
| `SERVICE_AUTH_TOKEN` | — | 调用方需带 `Authorization: Bearer <token>` |
|
|
131
131
|
| `DB_BACKEND` | `mysql` | SQL 引擎:`mysql`(任何 MySQL 协议库:MySQL/TiDB/MariaDB;`tidb` 为兼容别名)/ `pg`(PostgreSQL)/ `local`(免 DB 文件持久化)。显式设置 `mysql`/`pg` 时 session 自动转 durable |
|
package/USAGE.md
CHANGED
|
@@ -78,11 +78,11 @@ MODEL_DEGRADE_REACTIVE=true # ② 反应式(1.39):主模型 rate_limit
|
|
|
78
78
|
MODEL_DEGRADE_ON=rate_limit,breaker_open # 可选,反应式触发器子集;默认两者都开
|
|
79
79
|
```
|
|
80
80
|
- 反应式降级 brain 包在**最外层**(core council #8);fallback brain 用自己的凭据(decorator 清掉主模型的 per-call key 防泄漏给别的 provider)。**坑**:`MODEL_DEGRADE_TO` 最好别和被限流的是同一网关/账号,否则反应式切过去照样撞同一个 rate_limit。
|
|
81
|
-
- **定价怎么设**:`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 ÷ 汇率)
|
|
81
|
+
- **定价怎么设**:`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)。
|
|
82
82
|
- 成本计量**自动开**:从 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 降级)。
|
|
83
83
|
- **近预算降级 vs 硬闸**:降级(`MODEL_DEGRADE_TO`,到 `atCostFraction` 切便宜模型)是**撑长**预算、任务仍完成(出口质量下降、发 `task.degraded` 事件可告警);硬闸(`maxCostUsd` 全额)仍在,切了便宜模型还超全额 → `budget.exceeded` 停。
|
|
84
|
-
- **`METRICS_TOKEN`**(可选,只读):设了它,`GET /metrics`+`/metrics/summary` 接受**它或** `SERVICE_AUTH_TOKEN`。**全 fleet 设同一个值** →
|
|
85
|
-
- **`GET /metrics/summary`**(token-gated,同 `/metrics`):`/metrics` 的**精炼 JSON**——`{model, runsActive, tasks{status}, tokensTotal, costUsd, costUsdByModel, taskDurationAvgSec, brainFirstTokenAvgMs, brainCallAvgMs, cacheHitRateAvg, rateLimited, costQuotaRejected, budgetExceeded, degraded, cascade, verifications, councilRuns, toolErrors}`。给**轻量 fleet 看板**用(sema-
|
|
84
|
+
- **`METRICS_TOKEN`**(可选,只读):设了它,`GET /metrics`+`/metrics/summary` 接受**它或** `SERVICE_AUTH_TOKEN`。**全 fleet 设同一个值** → 管理端(sema-web,规划名 sema-admin)用**一个** token 拉所有 worker 的指标,**无需持有各 worker 的全权 `SERVICE_AUTH_TOKEN`**(不破坏 secret 边界)。即使泄露也只暴露指标(只读)。
|
|
85
|
+
- **`GET /metrics/summary`**(token-gated,同 `/metrics`):`/metrics` 的**精炼 JSON**——`{model, runsActive, tasks{status}, tokensTotal, costUsd, costUsdByModel, taskDurationAvgSec, brainFirstTokenAvgMs, brainCallAvgMs, cacheHitRateAvg, rateLimited, costQuotaRejected, budgetExceeded, degraded, cascade, verifications, councilRuns, toolErrors}`。给**轻量 fleet 看板**用(管理端 sema-web 的 fleet 页按 worker 拉、渲染卡片、按轮询算速率;**不用 Prometheus/Grafana**)。counter 是累计值、histogram 报均值。
|
|
86
86
|
- 预算闸是 core 强制的:`maxCostUsd` pre-call 估算(`budget.precall`,没花钱就拒)+ 流中途取消 + turn 边界(`budget.exceeded`);`maxTokens` 超 → `budget.exceeded`。**review 网关**建议设 `MAX_TASK_COST_USD` 防单任务烧掉共享云 key。
|
|
87
87
|
- **per-principal 累计配额**:用 `AsyncLocalStorage` 把 principal 透传到 cost tracer,所以**council/team 子任务的花费也算到发起人头上**。**配了 `SESSION_BACKEND=tidb` 时自动跨副本共享**(`cost_quota` 表,写后聚合的**原子自增** `micro=micro+delta`,对齐固定窗,最终一致——多副本花费 SUM 到一起、不丢增量);否则 in-memory per-replica 滚动窗(单副本兜底)。启动日志 `costQuota` 字段回显 `shared(tidb)`/`in-process`/`off`。跨副本是最终一致(flush 间隔内峰值可能略超,由**硬 per-task `maxCostUsd` 兜底**)。
|
|
88
88
|
- **`RATE_LIMIT_RPM` 请求限流同样自动跨副本**(`SESSION_BACKEND=tidb` 时,`rate_limit` 表,与配额共用 `WriteBehindCounter`;启动日志 `rateLimit` 回显)。注意:写后聚合 = **软限流**(边界上短暂略超 OK,适合公平/热调用方防护);要**硬合规上限**得另走 CAS/原子计数,不靠写后聚合——和断路器跨副本同款权衡。
|
|
@@ -130,7 +130,7 @@ MODEL_CODE_ROLES=default,subagent # 不设=全中立;仅这些角色在「
|
|
|
130
130
|
MODEL_CASCADE_LADDER=deepseek-flash,deepseek-pro # 目录里的模型名,cheapest→strongest;不设=cascade 不可用
|
|
131
131
|
```
|
|
132
132
|
- 结果带 `cascadeOutcome:"passed"|"exhausted"` + `escalated`/`finalRung`/`attempts[]`。`/metrics` 加 `cascade_total{outcome}`。
|
|
133
|
-
- **每档用自己的上游 key
|
|
133
|
+
- **每档用自己的上游 key**:从配置控制面模型的 `apiKeyEnv`(env 变量**名**,非密钥值)解析——同一网关下不同模型/账号各用各的 key,没配 `apiKeyEnv` 的模型回落到网关 key(`MODEL_API_KEY`)。`baseUrl` 仍由 brain 层统一持有(非目标)。启动日志 `perModelKeys=N` 报有几个模型带了自己的 key。
|
|
134
134
|
|
|
135
135
|
**嵌入形契约(1.309+;桌面宿主把引擎作为依赖内嵌启动的正门)**
|
|
136
136
|
- 入口:`import "@sema-agent/server/main"`(exports 正门;此前宿主只能用 node_modules 路径字符串定位
|
|
@@ -184,16 +184,16 @@ SEND_USER_FILE_SANDBOX_PUT_ENDPOINT=… # 可选:沙箱直传 PUT 的端
|
|
|
184
184
|
- **执行 lane 与租户门**:e2b/k8s 沙箱 lane 任意租户可用(沙箱文件系统=租户边界,沙箱内 `curl -T` 直传、字节不中转、凭据不进沙箱);host/ssh lane 仅单用户部署(`REQUIRE_PRINCIPAL` 未开)时挂载。
|
|
185
185
|
- **web 侧感知**:`GET /v1/capabilities` 透出 `sendUserFile`(READY 语义:工具真挂载**且**公网端点在场——调用真能成功才 yes)与 `s3PublicEndpoint`(渲染用公网 base;绝不透出密钥/内网端点)。
|
|
186
186
|
|
|
187
|
-
**可选 —
|
|
187
|
+
**可选 — 接配置控制面(中心化模型/角色/团队配置)**
|
|
188
188
|
```bash
|
|
189
189
|
# 新名(orchestrator 现注入);旧名 CONFIG_CENTER_* 仍兼容(dual-read,新名优先、旧名回退)
|
|
190
|
-
SEMA_REGISTRY_URL=http://<
|
|
191
|
-
SEMA_REGISTRY_TOKEN=<SERVICE_PULL_TOKEN 的值> #
|
|
190
|
+
SEMA_REGISTRY_URL=http://<config-center-host>:3100 # 启动拉 GET /api/config/effective(Bearer+ETag),覆盖 env 兜底
|
|
191
|
+
SEMA_REGISTRY_TOKEN=<SERVICE_PULL_TOKEN 的值> # 取自配置控制面主机 .env;只读拉取令牌
|
|
192
192
|
SEMA_REGISTRY_DRY_RUN=true # 安全灰度:只 LOG 中心配置 vs env 推导的差异,不 apply
|
|
193
193
|
SEMA_REGISTRY_WORKER=<worker名> # 可选:拉取 /effective?worker=<名> 取该 worker 的 roster(reconciler 按 worker 注);不设=全局 roster(向后兼容)
|
|
194
194
|
```
|
|
195
195
|
- 中心**空/未发布** → `applyEffective` 回落 env + 内建 teams 并 warn `config_center_unpublished`,**不影响在跑的服务**(接了也安全)。
|
|
196
|
-
- **灰度姿势**(
|
|
196
|
+
- **灰度姿势**(配置控制面 AI 建议):先 `SEMA_REGISTRY_DRY_RUN=true` 起一轮,看日志 `sema_registry_dry_run`(中心给的 models/roles/teams + 会否覆盖 default、per-model apiKeyEnv)对得上 env 再去掉该 flag 真正 apply。
|
|
197
197
|
- 拉取**只读、只取逻辑配置**(模型名册/角色/团队);密钥/网关仍在本服务 env(中心只发 env-**名** 引用,不发密钥值)。models/roles 改动需重启生效,teams 60s 热刷。回滚=去掉 `SEMA_REGISTRY_URL` 即纯 env。
|
|
198
198
|
- **仅 `/v1/tasks`(同步)+ `/v1/runs`(异步)**——`/v1/tasks/stream` 不支持(多次尝试非单流,400)。与 `verify` **互斥**(同时给 → 400)。
|
|
199
199
|
- 成本上界 = 任务的 `maxCostUsd`(防冷重跑税)。**三条 core 警示**:① 每档**冷重跑**重付输入成本(便宜档常过才划算);② 门收到**未脱敏**输出(自定义门转发外部 verifier 要脱敏);③ **写工具会跑 N 次**——**只用于只读/幂等任务**(每次升级整任务重跑)。开放式任务(找全 bug/文笔)没有可判定 oracle、级联会空转,那种用 `team`(广度对抗)而非级联(深度阶梯)。
|
|
@@ -264,7 +264,7 @@ SEMA_REGISTRY_WORKER=<worker名> # 可选:拉取 /effective?worke
|
|
|
264
264
|
| `sessionId` | — | 续聊:带上次返回的 `sessionId`,服务端自动 wake 历史 |
|
|
265
265
|
| `images` | — | 图文输入 `[{data,mimeType}|{url}]`(模型需支持 vision) |
|
|
266
266
|
| `attachmentIds` | — | D-1 通用文件上传(1.289+):先 `POST /v1/attachments?name=…`(raw body,content-type=mime)拿句柄,提交时引用 ≤16 个;文件物化到执行环境工作目录 `attachments/` 下,objective 尾部自动追加文件清单(内容不进会话流)。单文件缺省 ≤32 MiB(`ATTACHMENT_MAX_BYTES`);可配 mime 白名单(`ATTACHMENT_MIME_ALLOWLIST` CSV,缺省不限);上传后未引用的按 `ATTACHMENT_UNBOUND_TTL_MS`(缺省 24h)回收。**云形态(tidb/pg)字节本体存对象存储——MinIO 必配**(`MINIO_ENDPOINT/MINIO_ACCESS_KEY/MINIO_SECRET_KEY`,与快照 lane 同一组变量),未配则附件面 501;local 形走本地文件店。 |
|
|
267
|
-
| `scenario` | — | `default`(默认)/ `code-review`(见 §5)/ `scan`(同 §5 的 repo 只读工具但**中性无框架提示词**——objective+中心下发 skill 全权主导输出,OA 扫描类用)/
|
|
267
|
+
| `scenario` | — | `default`(默认)/ `code-review`(见 §5)/ `scan`(同 §5 的 repo 只读工具但**中性无框架提示词**——objective+中心下发 skill 全权主导输出,OA 扫描类用)/ **配置控制面可声明任意新场景**(`{name, toolset: none\|repo-readonly, prompt?}`,组合即配置、能力钉死在部署;restart-to-apply;center 可覆盖内建名,boot 日志 `config_center_scenarios.shadowsBuiltin` 可审计) |
|
|
268
268
|
| `repo` / `council` / `debate` | — | `repo` 为 `code-review`/`scan` 必填;`council`/`debate` 仅 `code-review`,见 §5 |
|
|
269
269
|
| ~~model / tools / prompt~~ | 🚫 | **不接受**——服务端注入 |
|
|
270
270
|
|
package/dist/approval-hmac.js
CHANGED
|
@@ -4,8 +4,7 @@
|
|
|
4
4
|
* ./principal-jwt.ts imports {@link verifyApprovalHmac} from here for the combined proof).
|
|
5
5
|
*
|
|
6
6
|
* ✅ WIRED into the resolve path (was "🔴 SCAFFOLD — NOT YET WIRED" — that claim went STALE; a 2026-07-25 doc-rot
|
|
7
|
-
* sweep traced the real call chain: {@link verifyApprovalHmac} ← `verifyDirectDoorProof` ←
|
|
8
|
-
* routes at `http/server.ts:4796` and `:4907`). So "not wired" was wrong; "not turned on" is right, and turning it
|
|
7
|
+
* sweep traced the real call chain: {@link verifyApprovalHmac} ← `verifyDirectDoorProof` ← * routes at `http/server.ts`(resumeCheckpoint 链;A9 拆分后行级坐标弃用,文件级指路+grep verifyDirectDoorProof 取证). So "not wired" was wrong; "not turned on" is right, and turning it
|
|
9
8
|
* on is a DEPLOYMENT + KEY-PROVISIONING task (sema-registry mints/rotates the set), not a coding task.
|
|
10
9
|
*
|
|
11
10
|
* FULL activation chain (an independent 2026-07-25 cross-check caught the first draft of this note naming only the
|
|
@@ -66,7 +66,7 @@ export function liveRuntimeConfigFromEnv(env = process.env) {
|
|
|
66
66
|
if (!(costInput > 0) || !(costOutput > 0))
|
|
67
67
|
return undefined;
|
|
68
68
|
return {
|
|
69
|
-
modelId: env.MODEL_ID
|
|
69
|
+
modelId: env.MODEL_ID ?? (() => { throw new Error("MODEL_ID is required for the S1 bench harness (no factory default since server 3.0.0 — same invariant as loadConfig)"); })(),
|
|
70
70
|
gatewayBaseUrl: env.MODEL_GATEWAY_BASEURL || "https://api.deepseek.com",
|
|
71
71
|
deepseekApiKey,
|
|
72
72
|
e2bApiKey,
|
|
@@ -6,7 +6,7 @@ import type { LoadedSkill } from "../capabilities/skills.js";
|
|
|
6
6
|
import { type ServiceConfig } from "../config.js";
|
|
7
7
|
import type { Logger } from "../observability/logger.js";
|
|
8
8
|
import type { Metrics } from "../observability/metrics.js";
|
|
9
|
-
import { type RestartSignal } from "../
|
|
9
|
+
import { type RestartSignal } from "../config-center/facade.js";
|
|
10
10
|
export interface ConfigCenterRuntimeCtx {
|
|
11
11
|
config: ServiceConfig;
|
|
12
12
|
logger: Logger;
|
|
@@ -45,10 +45,10 @@ import { defaultLkgPath, defaultSkillCacheDir, saveLkg, loadLkg } from "../confi
|
|
|
45
45
|
import { createConfigProvider, raceBootFetch, BOOT_FETCH_DEFERRED } from "../config-provider.js";
|
|
46
46
|
import { createKeyResolver } from "../key-resolver.js";
|
|
47
47
|
import { ensureSealedKeyStore, reportExecutionPublicKey } from "../sealed-key.js";
|
|
48
|
-
import { applyEffective, mutateInPlace, logEffectiveDiff, applyCenterSkills, resolveMcpServers, restartReasons, modelPlaneChanged, planeHasActiveTiers, fetchPromptArtifact } from "../
|
|
48
|
+
import { applyEffective, mutateInPlace, logEffectiveDiff, applyCenterSkills, resolveMcpServers, restartReasons, modelPlaneChanged, planeHasActiveTiers, fetchPromptArtifact } from "../config-center/facade.js";
|
|
49
49
|
export async function createConfigCenterRuntime(ctx) {
|
|
50
50
|
const { config, logger, metrics, localRoot } = ctx;
|
|
51
|
-
//
|
|
51
|
+
// Config-center: pull the effective config on boot and apply it OVER the env
|
|
52
52
|
// defaults — env is the fallback. Unset or unreachable → keep the env-derived config. Applied BEFORE
|
|
53
53
|
// the Runner is built so models/roles/teams take effect. (Logical config only; secrets stay in env.)
|
|
54
54
|
let ccEtag;
|
|
@@ -81,10 +81,10 @@ export async function createConfigCenterRuntime(ctx) {
|
|
|
81
81
|
// same-generation 200 recomputes reasons vs boot as [] and would otherwise silently clear the signal).
|
|
82
82
|
// Set at the boot writer (R34 arms); folded into every tick's reasons; only a restart resolves it.
|
|
83
83
|
let bootSkillStale = false;
|
|
84
|
-
//
|
|
84
|
+
// Config-center restart signal: set by the refresh loop when a pull's restart-to-apply slices
|
|
85
85
|
// (skills/mcp/scenarios/runtime-gates) differ from boot; read by GET /health for orchestrator auto-restart.
|
|
86
86
|
let pendingRestart;
|
|
87
|
-
// DUAL-MODE-DESIGN §4: the config SOURCE is a backend-selectable seam — remote (
|
|
87
|
+
// DUAL-MODE-DESIGN §4: the config SOURCE is a backend-selectable seam — remote (config-center HTTP, the fleet
|
|
88
88
|
// default) | local (.env + config.d via @sema-agent/registry-core FileConfigStore, the single-machine/TOC source).
|
|
89
89
|
// The provider exists when SEMA_REGISTRY_URL (legacy CONFIG_CENTER_URL) is set OR CONFIG_PROVIDER=local is explicit; neither = pure-env
|
|
90
90
|
// (byte-identical to before). The remote provider DELEGATES verbatim to fetchEffective, so the fleet path is
|
|
@@ -473,9 +473,9 @@ export async function createConfigCenterRuntime(ctx) {
|
|
|
473
473
|
logger.warn("model_roster_pending", {
|
|
474
474
|
note: "CONFIG_REQUIRE_ROSTER=true and no center roster has landed yet — the env MODEL_ID is treated as a PLACEHOLDER, so billable submissions 503 (and /health reports ready:false) until the first effective pull lands ≥1 enabled model. Unset the knob if this worker's env model is authoritative.",
|
|
475
475
|
});
|
|
476
|
-
// Per-model API key (
|
|
476
|
+
// Per-model API key (config-center `apiKeyEnv`): resolve each model's own upstream key per brain call /
|
|
477
477
|
// cascade rung. undefined when no per-model keys are configured → spec field stays unset (core default).
|
|
478
|
-
// `let` (not const): rebuilt on a
|
|
478
|
+
// `let` (not const): rebuilt on a config-center refresh so per-model key ADDITIONS hot-apply too (the
|
|
479
479
|
// resolver is undefined when no per-model keys exist, so in-place mutation alone wouldn't cover empty→non-empty).
|
|
480
480
|
let keyResolver;
|
|
481
481
|
// 复审 2026-07-29 P1-11:restart 片里唯一一块 EffectiveConfig **看不见**的输入 —— 反应式降级车道由 env
|
|
@@ -40,7 +40,7 @@ import { isIsolatedExecEnv, routeServiceTask, supPostureOverrides } from "../rou
|
|
|
40
40
|
import { gateExecutionLane } from "../runtime-caps-resolver.js";
|
|
41
41
|
import { applyRuntimeGovernance, stripDelegationTools } from "../runtime-governance.js";
|
|
42
42
|
import { HttpError, memorySpecForRequest } from "../security.js";
|
|
43
|
-
import { mcpForScenario } from "../
|
|
43
|
+
import { mcpForScenario } from "../config-center/facade.js";
|
|
44
44
|
import { normalizeAttachments, normalizeResilience, normalizeResumeAtMode, normalizeSuggestNextPrompts, promptProfileFromBody, resolveTaskLimits, retainBackgroundProcessesFromBody, taskAgentsSpecFragment, toolNameListFromBody } from "../spec-fields.js";
|
|
45
45
|
import { cwdHonored, effectiveHostWorkspace, inProcessSingleUserLane, isValidCwd, parseAdditionalDirectories, satisfiedByProcessCwd, shellEnvMismatchCount } from "../task-cwd.js";
|
|
46
46
|
import { resolveRequestMcp } from "../task-mcp.js";
|
|
@@ -312,8 +312,8 @@ export function createResolveSpec(ctx) {
|
|
|
312
312
|
// them to bounded text placeholders per current model (the pass-through posture above). That modality drop
|
|
313
313
|
// was invisible fleet-side; count + warn at submit. modelSupportsImages is false-safe (undeclared input →
|
|
314
314
|
// true → no signal), matching core's "never wrongly strip" posture — no false positives for verbatim
|
|
315
|
-
// pass-through models. ⚠️
|
|
316
|
-
// an EXPLICIT input list (
|
|
315
|
+
// pass-through models. ⚠️ config-center lane caveat (review LOW-2): the roster maps `vision?: boolean` to
|
|
316
|
+
// an EXPLICIT input list (config-center/facade.ts) — a vision-capable roster model that FORGOT the flag becomes
|
|
317
317
|
// declared text-only, so this signal fires AND core really does degrade its images (signal matches
|
|
318
318
|
// behavior; the fix for that footgun is the roster entry, not this counter). The per-run durable event rides core's vision.placeholder trace kind once we
|
|
319
319
|
// consume >=1.245 (the authoritative replacement-time signal), not this submit-time predictor.
|
|
@@ -586,7 +586,7 @@ export function createResolveSpec(ctx) {
|
|
|
586
586
|
// L2 ultracode (design/111): the preset FLOORS thinking at the ultra tier (= max, via core resolveReasoningProfile);
|
|
587
587
|
// awareness rides for free (core composes it from thinking∈{xhigh,max}). Else the explicit reasoningEffort.
|
|
588
588
|
thinking: effectiveThinking(body.reasoningEffort, parsedSettings.settings?.ultracode === true),
|
|
589
|
-
// Per-model auth (
|
|
589
|
+
// Per-model auth (config-center apiKeyEnv): core calls this per brain call / cascade rung so each
|
|
590
590
|
// model uses its own upstream key; a model without one falls back to the gateway key. undefined
|
|
591
591
|
// when no per-model keys are configured → unchanged single-key behavior.
|
|
592
592
|
getApiKeyAndHeaders: getKeyResolver(), // A10 搬运改写②:活引用取值(原 `keyResolver`)
|
|
@@ -950,7 +950,7 @@ export function createResolveSpec(ctx) {
|
|
|
950
950
|
throw new HttpError(400, "sandboxImageProfile is only supported on the k8s sandbox backend");
|
|
951
951
|
}
|
|
952
952
|
if (!imageIndex) {
|
|
953
|
-
throw new HttpError(400, "sandboxImageProfile requires the image index (
|
|
953
|
+
throw new HttpError(400, "sandboxImageProfile requires the image index (config-center backend not configured)");
|
|
954
954
|
}
|
|
955
955
|
if (!auth?.sessionId) {
|
|
956
956
|
throw new HttpError(400, "sandboxImageProfile requires a resolved session");
|
package/dist/brain.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createAnthropicBrain, createCircuitBreakerBrain, createDegradingBrain, createFailoverBrain, createOpenAIBrain, createRoutingBrain, } from "@sema-agent/core";
|
|
2
|
+
import { resolveModelApiKey } from "./key-resolver.js";
|
|
2
3
|
const DEFAULT_RESILIENCE = {
|
|
3
4
|
// Watchdog defaults ON — mirrors config.ts env defaults so a config object without a
|
|
4
5
|
// resilience block gets the same posture. connect/firstToken per the [880] fleet audit
|
|
@@ -95,20 +96,52 @@ export function createBrain(config, deps = {}) {
|
|
|
95
96
|
// credentials (the decorator clears the primary's per-call apiKey so it can't leak to a different
|
|
96
97
|
// provider). fallbackModel resolved from the catalog.
|
|
97
98
|
if (config.degrade?.reactive) {
|
|
98
|
-
const
|
|
99
|
+
const degradeTo = config.degrade.to;
|
|
100
|
+
const fallbackModel = config.models[degradeTo];
|
|
99
101
|
if (fallbackModel) {
|
|
100
102
|
// Cross-gateway degrade (audit B, brain.ts): the degrading decorator clears the per-call apiKey
|
|
101
103
|
// (so the primary's key can't leak to another provider), which left the fallback authenticating
|
|
102
104
|
// with the PRIMARY gateway key even when the catalog points the target at a different gateway.
|
|
103
|
-
// Resolve the target's own key
|
|
104
|
-
//
|
|
105
|
-
//
|
|
106
|
-
//
|
|
107
|
-
|
|
108
|
-
|
|
105
|
+
// Resolve the target's own key via the SAME chain the main path uses; `model.baseUrl` already wins
|
|
106
|
+
// inside the openai brain — pass it explicitly so the route is visible here. No per-model key =
|
|
107
|
+
// same-gateway degrade (unchanged), which stays meaningful for per-model limits (e.g. deepseek pro→flash).
|
|
108
|
+
//
|
|
109
|
+
// 复审 2026-07-29(同族缺口): this resolution used to read `config.modelApiKeyEnv` ONLY — half of the
|
|
110
|
+
// main path's chain. A degrade target under sealed-box custody lands in `config.modelApiKeys` and, by the
|
|
111
|
+
// registry-core mutual-exclusion contract, NEVER in `modelApiKeyEnv` ⇒ the hop silently authenticated the
|
|
112
|
+
// target's own gateway with the PRIMARY gateway key (wrong key upstream, not fail-loud), and a poison pill
|
|
113
|
+
// (unsealable key) degraded to "no per-model key configured" = the exact silent gateway-account burn the
|
|
114
|
+
// poison pill exists to prevent. Now it calls `resolveModelApiKey` — one chain, one implementation:
|
|
115
|
+
// sealed → env-NAME reference → gateway key, poison throws.
|
|
116
|
+
const fbApiKey = () => {
|
|
117
|
+
// Two lookups for the same reason the pre-fix code had them: `degrade.to` may name the catalog's
|
|
118
|
+
// `default` alias, whose Model carries the REAL name that the key maps are keyed by (and vice versa on
|
|
119
|
+
// env-lane catalogs). Poison in either lookup throws — never falls through to the gateway key.
|
|
120
|
+
const own = resolveModelApiKey(fallbackModel.name, config.modelApiKeyEnv ?? {}, process.env, config.modelApiKeys ?? {}) ??
|
|
121
|
+
resolveModelApiKey(degradeTo, config.modelApiKeyEnv ?? {}, process.env, config.modelApiKeys ?? {});
|
|
122
|
+
return own ?? config.gatewayApiKey;
|
|
123
|
+
};
|
|
109
124
|
const fallback = fallbackModel.provider === "anthropic" && anthropicBrain
|
|
110
|
-
?
|
|
111
|
-
|
|
125
|
+
? // Cloud Anthropic target: the shared cloud route (its own ANTHROPIC_* credential) — a per-model key
|
|
126
|
+
// on an anthropic degrade target is NOT honored here (the hop shares the primary's brain instance).
|
|
127
|
+
// Named residual, not an oversight: the openai-compatible lane below is where catalog-directed
|
|
128
|
+
// per-model credentials live (every sema-registry `apiKeyEnv`/`sealedApiKey` deployment to date).
|
|
129
|
+
anthropicBrain
|
|
130
|
+
: createOpenAIBrain({
|
|
131
|
+
baseUrl: fallbackModel.baseUrl || config.gatewayBaseUrl,
|
|
132
|
+
// HOT by getter (core reads `config.apiKey` per request inside buildRequest, openai.js:296): the key
|
|
133
|
+
// VALUE tracks refreshes exactly like the main path's rebuilt keyResolver — `applyEffective`
|
|
134
|
+
// REASSIGNS config.modelApiKeys/modelApiKeyEnv, and this closure holds `config`, so it always reads
|
|
135
|
+
// the live tables. Freezing it at boot would make a rotated/newly-sealed key take effect only after a
|
|
136
|
+
// restart, silently. The STRUCTURAL half (target present/absent, its baseUrl/id/apiKeyEnv/sealedApiKey)
|
|
137
|
+
// stays boot-frozen by construction and is registered as the `degrade-route` restart slice.
|
|
138
|
+
get apiKey() {
|
|
139
|
+
return fbApiKey();
|
|
140
|
+
},
|
|
141
|
+
maxRetries: 2,
|
|
142
|
+
fetchImpl,
|
|
143
|
+
...timeouts,
|
|
144
|
+
});
|
|
112
145
|
brain = createDegradingBrain({ primary: brain, fallback, fallbackModel, downgradeOn: config.degrade.downgradeOn });
|
|
113
146
|
}
|
|
114
147
|
}
|
|
@@ -77,7 +77,7 @@ export declare function resolveDefaultModelName(eff: EffectiveConfig, has: (name
|
|
|
77
77
|
};
|
|
78
78
|
/**
|
|
79
79
|
* Read-only comparison (SEMA_REGISTRY_DRY_RUN): log what the center config WOULD change vs the current
|
|
80
|
-
* (env-derived) config, WITHOUT applying it. The safe-rollout step the
|
|
80
|
+
* (env-derived) config, WITHOUT applying it. The safe-rollout step the config-center recommends — verify
|
|
81
81
|
* the center's models/roles/teams match (or intentionally differ from) the env baseline before going live.
|
|
82
82
|
*/
|
|
83
83
|
export declare function logEffectiveDiff(config: ServiceConfig, eff: EffectiveConfig, logger?: Logger): void;
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* EffectiveConfig application — mutate the boot `ServiceConfig` OVER with a pulled `EffectiveConfig`
|
|
3
3
|
* (models/roles/tiers/projects `applyEffective`, the 6 runtime gates `applyRuntimeGates`, the per-request-hot
|
|
4
4
|
* governance baton `applyRuntimeHot`) plus their shared helpers (`toModel`, `mutateInPlace`,
|
|
5
|
-
* `resolveDefaultModelName`, `logEffectiveDiff`, `runtimeHasActiveGate`). Split out of `
|
|
5
|
+
* `resolveDefaultModelName`, `logEffectiveDiff`, `runtimeHasActiveGate`). Split out of `facade.ts`
|
|
6
6
|
* (design/158 A13, internal-lossless — the facade re-exports every symbol below unchanged).
|
|
7
7
|
*/
|
|
8
8
|
import { isThinkingLevel } from "@sema-agent/core";
|
|
@@ -79,11 +79,11 @@ export function mutateInPlace(target, source) {
|
|
|
79
79
|
Object.assign(target, source);
|
|
80
80
|
}
|
|
81
81
|
export function applyEffective(config, eff, logger, opts = {}) {
|
|
82
|
-
// version 0 / empty effective = the
|
|
82
|
+
// version 0 / empty effective = the config-center has nothing for us yet — typically CONFIG_PUBLISH_MODE
|
|
83
83
|
// is ON but nothing has been published. We do NOT wipe: models/roles fall back to env (the enabled>0
|
|
84
84
|
// guard below), teams to BUILTIN_TEAMS (registerTeams resets to built-ins). Warn once at boot so the
|
|
85
85
|
// operator knows their config isn't live (publish a version, or disable the publish gate). See the
|
|
86
|
-
//
|
|
86
|
+
// config-center's ORCHESTRATOR-REVIEW §④ publish-gate contract.
|
|
87
87
|
if (!opts.teamsOnly && !eff.version) {
|
|
88
88
|
logger?.warn("sema_registry_unpublished", {
|
|
89
89
|
version: eff.version,
|
|
@@ -437,7 +437,7 @@ export function resolveDefaultModelName(eff, has, fallback, onDangling) {
|
|
|
437
437
|
}
|
|
438
438
|
/**
|
|
439
439
|
* Read-only comparison (SEMA_REGISTRY_DRY_RUN): log what the center config WOULD change vs the current
|
|
440
|
-
* (env-derived) config, WITHOUT applying it. The safe-rollout step the
|
|
440
|
+
* (env-derived) config, WITHOUT applying it. The safe-rollout step the config-center recommends — verify
|
|
441
441
|
* the center's models/roles/teams match (or intentionally differ from) the env baseline before going live.
|
|
442
442
|
*/
|
|
443
443
|
export function logEffectiveDiff(config, eff, logger) {
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config-center adapter — pull the effective config from the config-center control plane and
|
|
3
|
+
* apply it OVER the env-derived defaults (env = fallback, center = override). Follows the
|
|
4
|
+
* "universal internal schema → translate at the boundary" pattern, with our security boundary kept:
|
|
5
|
+
*
|
|
6
|
+
* (name history: this module tracks the control plane's product name, which has changed over time
|
|
7
|
+
* — registry → sema-web → planned sema-admin. The module itself keeps a neutral, product-name-free
|
|
8
|
+
* identifier ("config-center") and does not chase the rename.)
|
|
9
|
+
*
|
|
10
|
+
* - the CENTER owns the LOGICAL config: the model roster (names/capabilities/tier), the role map,
|
|
11
|
+
* and team templates;
|
|
12
|
+
* - the SERVICE env still owns the SECRETS: API keys stay in env (the center never stores a secret);
|
|
13
|
+
* keys are per-model via `apiKeyEnv` → `config.modelApiKeyEnv` → the spec's `getApiKeyAndHeaders`
|
|
14
|
+
* (core 1.45), so each model/cascade-rung authenticates with its own upstream key.
|
|
15
|
+
*
|
|
16
|
+
* Per-model `baseUrl` IS transported (a catalog model may live on a different gateway;
|
|
17
|
+
* core brain honors model.baseUrl, absent = "" = boot-env endpoint).
|
|
18
|
+
*
|
|
19
|
+
* Hot-reload status (复审 2026-07-29 P1-11 — 亲读判定,取代此处旧的 "models/roles are restart-to-apply"
|
|
20
|
+
* TODO, which went stale when `mutateInPlace` landed):
|
|
21
|
+
* - models/roles/roster/teams/projects/autonomy — **HOT**. `applyEffective` mutates `config.models`/
|
|
22
|
+
* `config.roles` IN PLACE, and core's Runner reads `this.deps.models/roles` per task off that very
|
|
23
|
+
* reference. Both brains re-resolve `model.baseUrl || config.baseUrl` inside `buildRequest()` on every
|
|
24
|
+
* call (core 2.1.0 `brain/openai.js` + `brain/anthropic.js`), so a moved gateway takes effect on the
|
|
25
|
+
* next task with no restart.
|
|
26
|
+
* - the same plane under a **tier-frozen** Runner — deferred, not hot: core expands a PRIVATE catalog copy
|
|
27
|
+
* at construction, so `main` defers the whole plane and the `models-tiers` restart slice signals.
|
|
28
|
+
* - **`degrade-route`** — the one genuinely boot-frozen catalog consumer left: reactive degrade
|
|
29
|
+
* (`MODEL_DEGRADE_REACTIVE`) bakes the target Model + its gateway + its key into the brain composition
|
|
30
|
+
* at boot. It cannot be hot-applied without rebuilding the brain, so it is registered on the existing
|
|
31
|
+
* restartRequired /health contract instead (see `restart-signal.ts`). Fail-loud beats serving a stale
|
|
32
|
+
* gateway silently on the rate-limit path.
|
|
33
|
+
* - skills/mcp/scenarios/runtime-gates — restart-to-apply by construction (baked into the boot wiring).
|
|
34
|
+
*
|
|
35
|
+
* (design/158 A13, internal-lossless) This module is now a FACADE: the implementation lives
|
|
36
|
+
* alongside it in `src/config-center/` split by responsibility group (HTTP client / EffectiveConfig
|
|
37
|
+
* application / restart-signal detection / skills+MCP consumption). Every symbol below is
|
|
38
|
+
* re-exported UNCHANGED — existing `from ".../config-center/facade.js"` imports need zero changes
|
|
39
|
+
* across a rename of this file's *contents*; a path rename (this move, 2026-07-29) does require the
|
|
40
|
+
* importers to update, which was done in the same commit.
|
|
41
|
+
*/
|
|
42
|
+
export { fetchEffective, fetchPrincipalCaps, ConfigCenterHttpError, fetchSkillContent, fetchPromptArtifact, fetchPromptBlob, } from "./http-client.js";
|
|
43
|
+
export { mutateInPlace, applyEffective, applyRuntimeGates, applyRuntimeHot, resolveDefaultModelName, logEffectiveDiff, runtimeHasActiveGate, } from "./apply-effective.js";
|
|
44
|
+
export { restartReasons, planeHasActiveTiers, modelPlaneChanged, type RestartSlice, type RestartSliceCtx, type RestartSignal, } from "./restart-signal.js";
|
|
45
|
+
export { applyCenterSkills, resolveMcpServers, mcpForScenario } from "./skills-mcp.js";
|
|
46
|
+
export type { CenterSkillManifest, CenterMcpServer, EffectiveConfig, ExecutionRuling, SessionMirrorRuling, } from "./types.js";
|
|
47
|
+
//# sourceMappingURL=facade.d.ts.map
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config-center adapter — pull the effective config from the config-center control plane and
|
|
3
|
+
* apply it OVER the env-derived defaults (env = fallback, center = override). Follows the
|
|
4
|
+
* "universal internal schema → translate at the boundary" pattern, with our security boundary kept:
|
|
5
|
+
*
|
|
6
|
+
* (name history: this module tracks the control plane's product name, which has changed over time
|
|
7
|
+
* — registry → sema-web → planned sema-admin. The module itself keeps a neutral, product-name-free
|
|
8
|
+
* identifier ("config-center") and does not chase the rename.)
|
|
9
|
+
*
|
|
10
|
+
* - the CENTER owns the LOGICAL config: the model roster (names/capabilities/tier), the role map,
|
|
11
|
+
* and team templates;
|
|
12
|
+
* - the SERVICE env still owns the SECRETS: API keys stay in env (the center never stores a secret);
|
|
13
|
+
* keys are per-model via `apiKeyEnv` → `config.modelApiKeyEnv` → the spec's `getApiKeyAndHeaders`
|
|
14
|
+
* (core 1.45), so each model/cascade-rung authenticates with its own upstream key.
|
|
15
|
+
*
|
|
16
|
+
* Per-model `baseUrl` IS transported (a catalog model may live on a different gateway;
|
|
17
|
+
* core brain honors model.baseUrl, absent = "" = boot-env endpoint).
|
|
18
|
+
*
|
|
19
|
+
* Hot-reload status (复审 2026-07-29 P1-11 — 亲读判定,取代此处旧的 "models/roles are restart-to-apply"
|
|
20
|
+
* TODO, which went stale when `mutateInPlace` landed):
|
|
21
|
+
* - models/roles/roster/teams/projects/autonomy — **HOT**. `applyEffective` mutates `config.models`/
|
|
22
|
+
* `config.roles` IN PLACE, and core's Runner reads `this.deps.models/roles` per task off that very
|
|
23
|
+
* reference. Both brains re-resolve `model.baseUrl || config.baseUrl` inside `buildRequest()` on every
|
|
24
|
+
* call (core 2.1.0 `brain/openai.js` + `brain/anthropic.js`), so a moved gateway takes effect on the
|
|
25
|
+
* next task with no restart.
|
|
26
|
+
* - the same plane under a **tier-frozen** Runner — deferred, not hot: core expands a PRIVATE catalog copy
|
|
27
|
+
* at construction, so `main` defers the whole plane and the `models-tiers` restart slice signals.
|
|
28
|
+
* - **`degrade-route`** — the one genuinely boot-frozen catalog consumer left: reactive degrade
|
|
29
|
+
* (`MODEL_DEGRADE_REACTIVE`) bakes the target Model + its gateway + its key into the brain composition
|
|
30
|
+
* at boot. It cannot be hot-applied without rebuilding the brain, so it is registered on the existing
|
|
31
|
+
* restartRequired /health contract instead (see `restart-signal.ts`). Fail-loud beats serving a stale
|
|
32
|
+
* gateway silently on the rate-limit path.
|
|
33
|
+
* - skills/mcp/scenarios/runtime-gates — restart-to-apply by construction (baked into the boot wiring).
|
|
34
|
+
*
|
|
35
|
+
* (design/158 A13, internal-lossless) This module is now a FACADE: the implementation lives
|
|
36
|
+
* alongside it in `src/config-center/` split by responsibility group (HTTP client / EffectiveConfig
|
|
37
|
+
* application / restart-signal detection / skills+MCP consumption). Every symbol below is
|
|
38
|
+
* re-exported UNCHANGED — existing `from ".../config-center/facade.js"` imports need zero changes
|
|
39
|
+
* across a rename of this file's *contents*; a path rename (this move, 2026-07-29) does require the
|
|
40
|
+
* importers to update, which was done in the same commit.
|
|
41
|
+
*/
|
|
42
|
+
export { fetchEffective, fetchPrincipalCaps, ConfigCenterHttpError, fetchSkillContent, fetchPromptArtifact, fetchPromptBlob, } from "./http-client.js";
|
|
43
|
+
export { mutateInPlace, applyEffective, applyRuntimeGates, applyRuntimeHot, resolveDefaultModelName, logEffectiveDiff, runtimeHasActiveGate, } from "./apply-effective.js";
|
|
44
|
+
export { restartReasons, planeHasActiveTiers, modelPlaneChanged, } from "./restart-signal.js";
|
|
45
|
+
export { applyCenterSkills, resolveMcpServers, mcpForScenario } from "./skills-mcp.js";
|
|
46
|
+
//# sourceMappingURL=facade.js.map
|
|
@@ -33,7 +33,7 @@ export declare function fetchPrincipalCaps(baseUrl: string, token: string, princ
|
|
|
33
33
|
executionDrift?: string;
|
|
34
34
|
etag?: string;
|
|
35
35
|
} | null>;
|
|
36
|
-
/** An HTTP error from a
|
|
36
|
+
/** An HTTP error from a config-center fetch that carries the response status so callers can branch on it
|
|
37
37
|
* (e.g. translate a 404 to "unknown hash → undefined") WITHOUT parsing the message string. */
|
|
38
38
|
export declare class ConfigCenterHttpError extends Error {
|
|
39
39
|
readonly status: number;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* config-center HTTP client — the transport calls the service makes against the config-center
|
|
3
3
|
* (`fetchEffective`/`fetchPrincipalCaps`/`fetchSkillContent`/`fetchPromptArtifact`/`fetchPromptBlob`) plus
|
|
4
|
-
* the shared HTTP-error type. Split out of `
|
|
4
|
+
* the shared HTTP-error type. Split out of `facade.ts` (design/158 A13, internal-lossless — the
|
|
5
5
|
* facade re-exports every symbol below unchanged).
|
|
6
6
|
*/
|
|
7
7
|
import { createHash } from "node:crypto";
|
|
@@ -22,7 +22,7 @@ export async function fetchEffective(baseUrl, token, etag, fetchImpl = fetch, wo
|
|
|
22
22
|
if (res.status === 304)
|
|
23
23
|
return null;
|
|
24
24
|
if (!res.ok)
|
|
25
|
-
throw new Error(`
|
|
25
|
+
throw new Error(`config-center HTTP ${res.status}`);
|
|
26
26
|
return { effective: (await res.json()), etag: res.headers.get("etag") ?? undefined };
|
|
27
27
|
}
|
|
28
28
|
/**
|
|
@@ -62,11 +62,11 @@ export async function fetchPrincipalCaps(baseUrl, token, principal, etag, fetchI
|
|
|
62
62
|
// misbehaving — DON'T treat it as "unchanged" (that surfaces undefined caps = no restriction = fail OPEN).
|
|
63
63
|
// Throw so the resolver fail-closes (review B). With an etag, 304 = genuine "unchanged" → null.
|
|
64
64
|
if (!etag)
|
|
65
|
-
throw new Error("
|
|
65
|
+
throw new Error("config-center returned 304 to a non-conditional principal-caps request");
|
|
66
66
|
return null;
|
|
67
67
|
}
|
|
68
68
|
if (!res.ok)
|
|
69
|
-
throw new Error(`
|
|
69
|
+
throw new Error(`config-center principal-caps HTTP ${res.status}`);
|
|
70
70
|
const body = (await res.json());
|
|
71
71
|
return {
|
|
72
72
|
runtimeCaps: body.runtimeCaps ?? null,
|
|
@@ -118,7 +118,7 @@ export async function fetchPrincipalCaps(baseUrl, token, principal, etag, fetchI
|
|
|
118
118
|
etag: res.headers.get("etag") ?? undefined,
|
|
119
119
|
};
|
|
120
120
|
}
|
|
121
|
-
/** An HTTP error from a
|
|
121
|
+
/** An HTTP error from a config-center fetch that carries the response status so callers can branch on it
|
|
122
122
|
* (e.g. translate a 404 to "unknown hash → undefined") WITHOUT parsing the message string. */
|
|
123
123
|
export class ConfigCenterHttpError extends Error {
|
|
124
124
|
status;
|
|
@@ -15,7 +15,7 @@ export interface RestartSignal {
|
|
|
15
15
|
restartRequired: true;
|
|
16
16
|
/** Which restart-to-apply slices differ from the value baked at boot. */
|
|
17
17
|
reasons: RestartSlice[];
|
|
18
|
-
/** The
|
|
18
|
+
/** The config-center version that currently differs from boot (latest seen). */
|
|
19
19
|
version: number;
|
|
20
20
|
/** Epoch ms when this restart first became required (stable across refreshes while `reasons` is unchanged). */
|
|
21
21
|
since: number;
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* `restart`), computed against the process's BOOT config (not presence, else a refresh would re-fire the same
|
|
4
4
|
* diff and loop-restart). Also owns the model-plane deferral predicates (`planeHasActiveTiers`/
|
|
5
5
|
* `modelPlaneChanged`) main.ts uses to decide whether a hot-apply is safe under a tier-frozen Runner. Split out
|
|
6
|
-
* of `
|
|
6
|
+
* of `facade.ts` (design/158 A13, internal-lossless — the facade re-exports every symbol below unchanged).
|
|
7
7
|
*/
|
|
8
8
|
import { resolveActiveTiers } from "@sema-agent/registry-core";
|
|
9
9
|
import { RUNTIME_GATE_KEYS, runtimeGatePresent, resolveDefaultModelName } from "./apply-effective.js";
|
|
@@ -31,21 +31,37 @@ function enabledOnly(rows) {
|
|
|
31
31
|
return null;
|
|
32
32
|
return rows.filter((r) => r.enabled !== false);
|
|
33
33
|
}
|
|
34
|
-
/**
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
* the WHOLE entry minus the
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
34
|
+
/** One catalog entry, reduced to the fields whose change is genuinely restart-to-apply. Consumers of a
|
|
35
|
+
* boot-frozen catalog entry read almost every field on the FULLY-DERIVED Model (`toModel`) — id → body.model,
|
|
36
|
+
* baseUrl → the endpoint, provider → which brain, maxTokens/compat/extraBody/input → the request body,
|
|
37
|
+
* apiKeyEnv/sealedApiKey → the credential — so we fingerprint the WHOLE entry minus the three provably HOT
|
|
38
|
+
* fields:
|
|
39
|
+
* - `cost` — refreshed by `mutateInPlace(pricing, buildPricing(config.models))`, and core prefers
|
|
40
|
+
* `deps.pricing[model.id]` over the (possibly frozen) `Model.cost`; `toModel` always emits
|
|
41
|
+
* a cost object, so every center-lane model has a live pricing entry.
|
|
42
|
+
* - `quotaWeight` — refreshed by `mutateInPlace(config.modelQuotaWeights, …)`, read live per burn.
|
|
43
|
+
* - `enabled` — `enabledOnly` already filtered on it, so among the entries that reach here it is
|
|
44
|
+
* `true`-or-absent: two encodings of ONE state. Keeping it would let a center serializer
|
|
45
|
+
* that starts/stops emitting the explicit `true` manufacture a restart out of nothing.
|
|
46
|
+
* Every real enable/disable FLIP changes the filtered SET, so it still signals.
|
|
47
|
+
* A DENY-list (not an allow-list) on purpose: a catalog field added later rides the fingerprint automatically,
|
|
48
|
+
* so the failure mode of drift is a spurious restart, never a silently missed one.
|
|
49
|
+
*
|
|
50
|
+
* Shared by BOTH catalog-entry fingerprints (`degrade-route`, `models-tiers` + `modelPlaneChanged`) since the
|
|
51
|
+
* 复审 2026-07-29 留档: models-tiers had kept all three, so on a tiers-active deployment a pure re-pricing /
|
|
52
|
+
* weight tweak / serializer writing `enabled:true` rolling-restarted the fleet. The two fingerprints must
|
|
53
|
+
* subtract the SAME set — under active tiers `modelPlaneChanged` defers the plane and the refresh loop then
|
|
54
|
+
* FORCE-pushes the `models-tiers` reason for a deferred candidate (boot/config-center.ts R15/R22), so
|
|
55
|
+
* subtracting in only one of them is either no-op (restart still fires) or incoherent (applied yet signalled). */
|
|
56
|
+
function catalogEntryFingerprint(entry) {
|
|
46
57
|
const { cost: _cost, quotaWeight: _quotaWeight, enabled: _enabled, ...route } = entry;
|
|
47
58
|
return route;
|
|
48
59
|
}
|
|
60
|
+
/** The enabled entries of a model plane, each reduced by `catalogEntryFingerprint` (null = no plane published). */
|
|
61
|
+
function planeEntriesFingerprint(rows) {
|
|
62
|
+
const enabled = enabledOnly(rows);
|
|
63
|
+
return enabled ? enabled.map((m) => catalogEntryFingerprint(m)) : null;
|
|
64
|
+
}
|
|
49
65
|
function restartSliceValue(eff, slice, ctx) {
|
|
50
66
|
if (!eff)
|
|
51
67
|
return null;
|
|
@@ -82,7 +98,8 @@ function restartSliceValue(eff, slice, ctx) {
|
|
|
82
98
|
// dangling candidates fall through silently here — applyEffective owns the warn.
|
|
83
99
|
const names = new Set((enabled ?? []).map((m) => m.name));
|
|
84
100
|
const def = resolveDefaultModelName(eff, (n) => names.has(n), enabled?.[0]?.name ?? "");
|
|
85
|
-
|
|
101
|
+
// Entries reduced by the shared deny-list (cost/quotaWeight/enabled are hot — see catalogEntryFingerprint).
|
|
102
|
+
return { models: planeEntriesFingerprint(eff.models?.models), tiers: active, default: def.name };
|
|
86
103
|
}
|
|
87
104
|
case "degrade-route": {
|
|
88
105
|
const to = ctx?.reactiveDegradeTo;
|
|
@@ -91,7 +108,7 @@ function restartSliceValue(eff, slice, ctx) {
|
|
|
91
108
|
// Present↔absent is itself a change: with no enabled entry named `to`, createBrain composes NO
|
|
92
109
|
// degrading shell at all, so the flip only takes effect at the next boot.
|
|
93
110
|
const target = (enabledOnly(eff.models?.models) ?? []).find((m) => m.name === to);
|
|
94
|
-
return target ?
|
|
111
|
+
return target ? catalogEntryFingerprint(target) : null;
|
|
95
112
|
}
|
|
96
113
|
}
|
|
97
114
|
}
|
|
@@ -126,7 +143,10 @@ export function modelPlaneChanged(prev, next) {
|
|
|
126
143
|
const active = e.models ? (resolveActiveTiers(e.models) ?? null) : null;
|
|
127
144
|
const names = new Set((enabled ?? []).map((m) => m.name));
|
|
128
145
|
const def = resolveDefaultModelName(e, (n) => names.has(n), enabled?.[0]?.name ?? "");
|
|
129
|
-
|
|
146
|
+
// Same reduction as the models-tiers slice — the two MUST subtract the same hot fields (see
|
|
147
|
+
// catalogEntryFingerprint): a deferred candidate force-pushes that slice's reason, so any divergence
|
|
148
|
+
// either re-manufactures the restart we just subtracted or signals a plane that was applied hot.
|
|
149
|
+
return stableStringify({ models: planeEntriesFingerprint(e.models?.models), tiers: active, default: def.name });
|
|
130
150
|
};
|
|
131
151
|
return fp(prev) !== fp(next);
|
|
132
152
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Two small consumption lanes that both project a center manifest domain onto a per-request runtime shape:
|
|
3
3
|
* skills application (`applyCenterSkills` — merge center skills OVER the image baseline, content-addressed
|
|
4
4
|
* fetch+verify+cache) and MCP server resolution (`resolveMcpServers`/`mcpForScenario` — env-NAME ref
|
|
5
|
-
* resolution + per-scenario filtering). Split out of `
|
|
5
|
+
* resolution + per-scenario filtering). Split out of `facade.ts` (design/158 A13, internal-lossless —
|
|
6
6
|
* the facade re-exports every symbol below unchanged).
|
|
7
7
|
*/
|
|
8
8
|
import { skillContentHash } from "@sema-agent/registry-core";
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* config-center wire types — the
|
|
2
|
+
* config-center wire types — the config-center `/api/config/effective` payload shapes (`CenterModel`,
|
|
3
3
|
* `CenterTeam`, `CenterSkillManifest`, `CenterMcpServer`, `EffectiveConfig`) plus the per-principal
|
|
4
4
|
* execution/session-mirror ruling shapes riding the caps view (`ExecutionRuling`/`SessionMirrorRuling`).
|
|
5
|
-
* Pure type/interface declarations — no runtime logic. Split out of `
|
|
5
|
+
* Pure type/interface declarations — no runtime logic. Split out of `facade.ts` (design/158 A13,
|
|
6
6
|
* internal-lossless: the facade re-exports every symbol below unchanged).
|
|
7
7
|
*/
|
|
8
8
|
import type { CollabTemplateWire } from "../capabilities/collab-wire.js";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { FileConfigStore } from "@sema-agent/registry-core/node";
|
|
2
2
|
import { type EffectiveConfig as AgentConfigEffective } from "@sema-agent/registry-core";
|
|
3
|
-
import { fetchEffective as remoteFetchEffective, fetchSkillContent as remoteFetchSkillContent, type EffectiveConfig } from "./
|
|
4
|
-
/** Result of a `fetchEffective` — EXACTLY
|
|
3
|
+
import { fetchEffective as remoteFetchEffective, fetchSkillContent as remoteFetchSkillContent, type EffectiveConfig } from "./config-center/facade.js";
|
|
4
|
+
/** Result of a `fetchEffective` — EXACTLY the config-center's return: the effective config + its etag, or
|
|
5
5
|
* `null` for "unchanged" (remote 304; local: the version matched the caller's prior etag).
|
|
6
6
|
* `domainErrors`([898] registry-core 0.10.12 tolerant seam,local lane only):catalog 域坏文件不再
|
|
7
7
|
* 连坐全包回落 env——坏域按该域 schema default 落+错误单列,好域照常生效;caller(main.ts)对每条打
|
|
@@ -29,12 +29,12 @@ export declare const BOOT_FETCH_DEFERRED: unique symbol;
|
|
|
29
29
|
export declare function raceBootFetch<T>(fetch: Promise<T>, budgetMs: number): Promise<T | typeof BOOT_FETCH_DEFERRED>;
|
|
30
30
|
/**
|
|
31
31
|
* The two transport calls the service makes to obtain config, behind a backend-selectable seam. Both
|
|
32
|
-
* signatures and return shapes match `
|
|
32
|
+
* signatures and return shapes match `facade.fetchEffective` / `facade.fetchSkillContent`
|
|
33
33
|
* EXACTLY so that, ONCE WIRED, the caller (main.ts) could swap remote↔local without touching applyEffective
|
|
34
34
|
* et al. (main.ts does not swap on this seam yet — see the NOT-YET-WIRED note at the top of this module).
|
|
35
35
|
*/
|
|
36
36
|
export interface ConfigProvider {
|
|
37
|
-
/** "remote" =
|
|
37
|
+
/** "remote" = config-center HTTP; "local" = on-disk FileConfigStore. */
|
|
38
38
|
readonly kind: "remote" | "local";
|
|
39
39
|
/**
|
|
40
40
|
* Pull the effective config. `etag` is the caller's last-seen version token (`if-none-match` on remote;
|
|
@@ -72,9 +72,9 @@ export interface ConfigProviderInput {
|
|
|
72
72
|
localDir?: string;
|
|
73
73
|
}
|
|
74
74
|
/**
|
|
75
|
-
* RemoteConfigProvider — DELEGATES to the exported
|
|
75
|
+
* RemoteConfigProvider — DELEGATES to the exported config-center HTTP functions. No HTTP is reimplemented;
|
|
76
76
|
* baseUrl/token/worker are bound once and passed through. The two delegate fns are injectable (defaulting
|
|
77
|
-
* to the real
|
|
77
|
+
* to the real config-center exports) so a unit test can assert delegation without module-mocking.
|
|
78
78
|
*/
|
|
79
79
|
export declare class RemoteConfigProvider implements ConfigProvider {
|
|
80
80
|
private readonly cc;
|
|
@@ -120,7 +120,7 @@ export declare class LocalConfigProvider implements ConfigProvider {
|
|
|
120
120
|
export declare function mapToServiceEffective(eff: AgentConfigEffective, version: number): EffectiveConfig;
|
|
121
121
|
/**
|
|
122
122
|
* Pick the backend off a CLOSED `provider` enum (`local` | `remote` | undefined): LOCAL when
|
|
123
|
-
* `CONFIG_PROVIDER=local`, OR when no
|
|
123
|
+
* `CONFIG_PROVIDER=local`, OR when no config-center URL is configured (a lone box with config.d/ on disk
|
|
124
124
|
* and no control plane). REMOTE otherwise (a configured center URL, the default fleet posture). A
|
|
125
125
|
* `CONFIG_PROVIDER=remote` with no URL is a misconfiguration — we fall through to local rather than
|
|
126
126
|
* constructing a remote provider with no endpoint (the caller would have skipped the remote pull entirely
|
package/dist/config-provider.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* ConfigProvider — the dual-mode seam over WHERE the effective config comes from (see
|
|
3
3
|
* sema-registry docs/DUAL-MODE-DESIGN.md §4).
|
|
4
4
|
*
|
|
5
|
-
* The service consumes
|
|
5
|
+
* The service consumes the config-center through EXACTLY two transport calls — `fetchEffective` (pull the
|
|
6
6
|
* effective config, ETag-conditional) and `fetchSkillContent` (lazy-pull one skill body by content hash).
|
|
7
7
|
* Everything downstream (applyEffective / applyRuntimeGates / resolveMcpServers / mcpForScenario / the
|
|
8
8
|
* skill overlay) is transport-agnostic: it operates on the returned `EffectiveConfig` / skill string.
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* This module factors those two calls behind a `ConfigProvider` interface with two backends:
|
|
18
18
|
*
|
|
19
19
|
* - {@link RemoteConfigProvider} — the existing behaviour. DELEGATES verbatim to the exported
|
|
20
|
-
* `
|
|
20
|
+
* `facade.fetchEffective` / `facade.fetchSkillContent` (HTTP + Bearer + ETag + hash
|
|
21
21
|
* verify). No HTTP is reimplemented here; this is a thin wrapper that binds baseUrl/token/worker.
|
|
22
22
|
*
|
|
23
23
|
* - {@link LocalConfigProvider} — reads the SAME config contract from the local filesystem via
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
*
|
|
31
31
|
* SECRET BOUNDARY (unchanged in both modes): the config plane carries only env-NAME refs — `apiKeyEnv`,
|
|
32
32
|
* `envRefs`/`headerRefs`, `tokenEnv`. The real secret VALUE is never stored in config.d/<domain>.json and
|
|
33
|
-
* never read by this module; it is resolved from `process.env` downstream (
|
|
33
|
+
* never read by this module; it is resolved from `process.env` downstream (config-center/facade.ts
|
|
34
34
|
* `applyEffective`/`resolveMcpServers`). The local store is symmetric with the remote center on this: it
|
|
35
35
|
* ships NAMEs only. (Unit test asserts the local path resolves env-NAME→value via process.env and never
|
|
36
36
|
* persists a value.)
|
|
@@ -39,7 +39,7 @@ import { createHmac, randomBytes } from "node:crypto";
|
|
|
39
39
|
import { FileConfigStore } from "@sema-agent/registry-core/node";
|
|
40
40
|
import { findSkillContent, skillContentHash, refIntegrityIssues, siblingResolver, } from "@sema-agent/registry-core";
|
|
41
41
|
import { redactSecrets } from "./trace/redact.js";
|
|
42
|
-
import { fetchEffective as remoteFetchEffective, fetchSkillContent as remoteFetchSkillContent, ConfigCenterHttpError, } from "./
|
|
42
|
+
import { fetchEffective as remoteFetchEffective, fetchSkillContent as remoteFetchSkillContent, ConfigCenterHttpError, } from "./config-center/facade.js";
|
|
43
43
|
import { createLogger } from "./observability/logger.js";
|
|
44
44
|
const logger = createLogger();
|
|
45
45
|
/**
|
|
@@ -79,9 +79,9 @@ export function raceBootFetch(fetch, budgetMs) {
|
|
|
79
79
|
return Promise.race([fetch.finally(() => clearTimeout(timer)), budget]);
|
|
80
80
|
}
|
|
81
81
|
/**
|
|
82
|
-
* RemoteConfigProvider — DELEGATES to the exported
|
|
82
|
+
* RemoteConfigProvider — DELEGATES to the exported config-center HTTP functions. No HTTP is reimplemented;
|
|
83
83
|
* baseUrl/token/worker are bound once and passed through. The two delegate fns are injectable (defaulting
|
|
84
|
-
* to the real
|
|
84
|
+
* to the real config-center exports) so a unit test can assert delegation without module-mocking.
|
|
85
85
|
*/
|
|
86
86
|
export class RemoteConfigProvider {
|
|
87
87
|
cc;
|
|
@@ -93,7 +93,7 @@ export class RemoteConfigProvider {
|
|
|
93
93
|
}
|
|
94
94
|
fetchEffective(etag) {
|
|
95
95
|
const fn = this.deps.fetchEffective ?? remoteFetchEffective;
|
|
96
|
-
// 5th arg = worker scope; transport (fetchImpl) stays
|
|
96
|
+
// 5th arg = worker scope; transport (fetchImpl) stays the config-center's default.
|
|
97
97
|
return fn(this.cc.baseUrl, this.cc.token, etag, undefined, this.cc.worker);
|
|
98
98
|
}
|
|
99
99
|
async fetchSkillContent(contentHash) {
|
|
@@ -219,7 +219,7 @@ export function mapToServiceEffective(eff, version) {
|
|
|
219
219
|
// [874] registry-core 0.10.11:池结构级 `default?`(catalog name 目录键,与 roles.default 角色词两回事)。
|
|
220
220
|
// 本 mapper 是 CLOSED 投影——不直通则本地 lane 静默丢键(autoCompactTokens 同类病),applyEffective 的
|
|
221
221
|
// resolveDefaultModelName 第一优先级在本地 lane 永远点不亮。写面悬空 ref 由 registry-core superRefine
|
|
222
|
-
// fail-loud;存量/未经 schema 的路径仍由消费方 warn 降级(
|
|
222
|
+
// fail-loud;存量/未经 schema 的路径仍由消费方 warn 降级(config-center/facade.ts resolveDefaultModelName)。
|
|
223
223
|
...(eff.models.default !== undefined ? { default: eff.models.default } : {}),
|
|
224
224
|
...(eff.models.atModelAllowlist !== undefined ? { atModelAllowlist: eff.models.atModelAllowlist } : {}),
|
|
225
225
|
// registry-core 0.9.0 档位组: verbatim passthrough — applyEffective's resolveActiveTiers reads
|
|
@@ -322,7 +322,7 @@ export function mapToServiceEffective(eff, version) {
|
|
|
322
322
|
}
|
|
323
323
|
/**
|
|
324
324
|
* Pick the backend off a CLOSED `provider` enum (`local` | `remote` | undefined): LOCAL when
|
|
325
|
-
* `CONFIG_PROVIDER=local`, OR when no
|
|
325
|
+
* `CONFIG_PROVIDER=local`, OR when no config-center URL is configured (a lone box with config.d/ on disk
|
|
326
326
|
* and no control plane). REMOTE otherwise (a configured center URL, the default fleet posture). A
|
|
327
327
|
* `CONFIG_PROVIDER=remote` with no URL is a misconfiguration — we fall through to local rather than
|
|
328
328
|
* constructing a remote provider with no endpoint (the caller would have skipped the remote pull entirely
|
package/dist/config.d.ts
CHANGED
|
@@ -8,7 +8,7 @@ export type { ServiceConfig, ScopedMcpServer, ImageBakeConfig, ServiceConfigFlat
|
|
|
8
8
|
/** Parse the AUTONOMY env into a validated autonomy mode. Unset/empty → undefined (unmanaged → no extra
|
|
9
9
|
* tightening). An UNKNOWN value FAILS at startup rather than silently becoming a no-op (a typo'd `AUTONOMY=readonly`
|
|
10
10
|
* must not silently leave a deployment ungoverned — fail-loud, same discipline as numEnv). Exported so the HOT
|
|
11
|
-
* config overlay (
|
|
11
|
+
* config overlay (config-center/facade.ts `applyRuntimeHot`) can re-derive the ENV BASELINE to revert to when center
|
|
12
12
|
* stops managing `autonomy` (a stale center override must not stick — see applyRuntimeHot). */
|
|
13
13
|
export declare function parseAutonomy(raw: string | undefined): Autonomy | undefined;
|
|
14
14
|
export declare function drainConfigWarnings(): Array<{
|
package/dist/config.js
CHANGED
|
@@ -15,7 +15,7 @@ const AUTONOMY_MODES = ["read-only", "ask", "plan", "auto"];
|
|
|
15
15
|
/** Parse the AUTONOMY env into a validated autonomy mode. Unset/empty → undefined (unmanaged → no extra
|
|
16
16
|
* tightening). An UNKNOWN value FAILS at startup rather than silently becoming a no-op (a typo'd `AUTONOMY=readonly`
|
|
17
17
|
* must not silently leave a deployment ungoverned — fail-loud, same discipline as numEnv). Exported so the HOT
|
|
18
|
-
* config overlay (
|
|
18
|
+
* config overlay (config-center/facade.ts `applyRuntimeHot`) can re-derive the ENV BASELINE to revert to when center
|
|
19
19
|
* stops managing `autonomy` (a stale center override must not stick — see applyRuntimeHot). */
|
|
20
20
|
export function parseAutonomy(raw) {
|
|
21
21
|
const v = raw?.trim();
|
|
@@ -534,8 +534,8 @@ function parseModelDomain() {
|
|
|
534
534
|
// when body.images hits a text-only model) AND the vision flag on GET /v1/models. The env-lane default model
|
|
535
535
|
// declares vision via MODEL_VISION — DEFAULT "true" keeps the historical image-capable behavior, so set
|
|
536
536
|
// MODEL_VISION=false for a text-only model (e.g. deepseek) to make the precheck actually fire instead of letting
|
|
537
|
-
// images透传 to an opaque downstream-gateway 400. (The centralized
|
|
538
|
-
// per-model vision flag — see
|
|
537
|
+
// images透传 to an opaque downstream-gateway 400. (The centralized config-center lane derives input from its own
|
|
538
|
+
// per-model vision flag — see config-center/facade.ts.)
|
|
539
539
|
input: env("MODEL_VISION", "true") === "true" ? ["text", "image"] : ["text"],
|
|
540
540
|
// Per-1M-token USD pricing (core `modelCostToPricing`: input→inputPer1M, …). Default 0 keeps the
|
|
541
541
|
// historical behavior (spend reads $0) — set MODEL_COST_* on a deploy to make `model_cost_micro_usd`
|
|
@@ -624,7 +624,7 @@ function parseModelDomain() {
|
|
|
624
624
|
// Static catalog audit for this batch: ZERO baked 1M entries exist (env lane defaults 262144; center lane
|
|
625
625
|
// inherits/declares per roster) — so this is a DERIVATION on the declared window, not a data edit: any
|
|
626
626
|
// deployment that sets MODEL_CONTEXT_WINDOW>=1e6 (or a center roster with a 1M contextWindow —
|
|
627
|
-
//
|
|
627
|
+
// config-center/facade.ts applies the same helper) gets the field automatically. <1M ⇒ field ABSENT (core's plain
|
|
628
628
|
// W-33000/0.7W geometry already matches CC there).
|
|
629
629
|
applyAutoCompactWindow(model);
|
|
630
630
|
if (cheapModel)
|
|
@@ -849,7 +849,7 @@ function parseMemoryDomain(ctx) {
|
|
|
849
849
|
memoryEngineBackend,
|
|
850
850
|
memoryScope, // hoisted above (the 142-S2.5-W1 sync-scope default consumes it)
|
|
851
851
|
...(memorySync ? { memorySync } : {}),
|
|
852
|
-
projectMemoryEnabled, // design/113 C4 opt-out, flipped positive in design/158 B4 (legacy PROJECT_MEMORY_DISABLED
|
|
852
|
+
projectMemoryEnabled, // design/113 C4 opt-out, flipped positive in design/158 B4 (3.0.0 起 legacy PROJECT_MEMORY_DISABLED = fail-loud 墓碑)
|
|
853
853
|
syncImportLeaseStaleSec: Math.max(0, numEnv("SYNC_IMPORT_LEASE_STALE_SEC", "600")),
|
|
854
854
|
};
|
|
855
855
|
}
|
|
@@ -209,7 +209,7 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
|
|
|
209
209
|
// (yields a task, runs no model — see the billable-route classifier) → NO lease gate, else an exhausted
|
|
210
210
|
// tenant could not stop its own spending.
|
|
211
211
|
if (!deps.runStore) {
|
|
212
|
-
sendError(res, 501, "capability.run_store_required", "preemption requires
|
|
212
|
+
sendError(res, 501, "capability.run_store_required", "preemption requires a durable run store (DB_BACKEND=mysql|pg)");
|
|
213
213
|
return;
|
|
214
214
|
}
|
|
215
215
|
// requirePrincipal parity with the sibling mutating endpoints (runOwnerOk / cancel / runs): 401 before the
|
|
@@ -282,7 +282,7 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
|
|
|
282
282
|
if (rateLimited(req, res) || quotaExceeded(req, res))
|
|
283
283
|
return; // 🔴 复审 C2:lease admitted INSIDE driveResumeIntoRunLog on the CHECKPOINT-OWNER principal (the billed tenant), not the request principal — a cross-tenant operator resume must charge the owner's lease, not the operator's. resume hits TiDB + runs the model
|
|
284
284
|
if (!deps.runStore) {
|
|
285
|
-
sendError(res, 501, "capability.run_store_required", "resume requires
|
|
285
|
+
sendError(res, 501, "capability.run_store_required", "resume requires a durable run store (DB_BACKEND=mysql|pg)");
|
|
286
286
|
return;
|
|
287
287
|
}
|
|
288
288
|
if (deps.config.requirePrincipal && principal === undefined) {
|
|
@@ -319,7 +319,7 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
|
|
|
319
319
|
if (rateLimited(req, res) || quotaExceeded(req, res))
|
|
320
320
|
return; // 🔴 复审 C2:lease admitted in driveResumeIntoRunLog (owner principal). resume hits TiDB + runs the model (approve/edit)
|
|
321
321
|
if (!deps.runStore) {
|
|
322
|
-
sendError(res, 501, "capability.run_store_required", "plan_review requires
|
|
322
|
+
sendError(res, 501, "capability.run_store_required", "plan_review requires a durable run store (DB_BACKEND=mysql|pg)");
|
|
323
323
|
return;
|
|
324
324
|
}
|
|
325
325
|
if (deps.config.requirePrincipal && principal === undefined) {
|
package/dist/http/routes/runs.js
CHANGED
|
@@ -93,7 +93,7 @@ async function handleRunsBody(req, res, url, ctx, miss) {
|
|
|
93
93
|
// Async run: create + return ids immediately, execute in the background.
|
|
94
94
|
if (req.method === "POST" && url === "/v1/runs") {
|
|
95
95
|
if (!deps.runStore) {
|
|
96
|
-
sendError(res, 501, "capability.run_store_required", "async runs require
|
|
96
|
+
sendError(res, 501, "capability.run_store_required", "async runs require a durable run store (DB_BACKEND=mysql|pg) (SESSION_BACKEND=tidb)");
|
|
97
97
|
return;
|
|
98
98
|
}
|
|
99
99
|
// Idempotency-Key dedup (center blocker): a retried create returns the SAME taskId instead of starting a
|
|
@@ -222,7 +222,7 @@ async function handleRunsBody(req, res, url, ctx, miss) {
|
|
|
222
222
|
const runMatch = req.method === "GET" ? RUN_ID_RE.exec(url) : null;
|
|
223
223
|
if (runMatch) {
|
|
224
224
|
if (!deps.runStore) {
|
|
225
|
-
sendError(res, 501, "capability.run_store_required", "async runs require
|
|
225
|
+
sendError(res, 501, "capability.run_store_required", "async runs require a durable run store (DB_BACKEND=mysql|pg)");
|
|
226
226
|
return;
|
|
227
227
|
}
|
|
228
228
|
const taskId = runMatch[1];
|
|
@@ -310,7 +310,7 @@ async function handleRunsBody(req, res, url, ctx, miss) {
|
|
|
310
310
|
const cancelMatch = req.method === "POST" ? RUN_CANCEL_RE.exec(url) : null;
|
|
311
311
|
if (cancelMatch) {
|
|
312
312
|
if (!deps.runStore) {
|
|
313
|
-
sendError(res, 501, "capability.run_store_required", "async runs require
|
|
313
|
+
sendError(res, 501, "capability.run_store_required", "async runs require a durable run store (DB_BACKEND=mysql|pg)");
|
|
314
314
|
return;
|
|
315
315
|
}
|
|
316
316
|
// Principal check BEFORE the lookup (parity with GET /v1/runs/:id): no 404-vs-401 existence oracle.
|
|
@@ -496,7 +496,7 @@ async function handleRunsBody(req, res, url, ctx, miss) {
|
|
|
496
496
|
if (rateLimited(req, res) || quotaExceeded(req, res) || (await leaseDenied(req, res)))
|
|
497
497
|
return; // mutating + hits TiDB / runs the model
|
|
498
498
|
if (!deps.runStore) {
|
|
499
|
-
sendError(res, 501, "capability.run_store_required", "async runs require
|
|
499
|
+
sendError(res, 501, "capability.run_store_required", "async runs require a durable run store (DB_BACKEND=mysql|pg)");
|
|
500
500
|
return;
|
|
501
501
|
}
|
|
502
502
|
// Per-tenant identity MUST come from gatedPrincipal (direct-door secure single point), NOT principalFrom:
|
|
@@ -713,7 +713,7 @@ async function handleRunsBody(req, res, url, ctx, miss) {
|
|
|
713
713
|
if (rateLimited(req, res) || quotaExceeded(req, res) || (await leaseDenied(req, res)))
|
|
714
714
|
return; // mutating + runs the model (compaction summarizes)
|
|
715
715
|
if (!deps.runStore) {
|
|
716
|
-
sendError(res, 501, "capability.run_store_required", "async runs require
|
|
716
|
+
sendError(res, 501, "capability.run_store_required", "async runs require a durable run store (DB_BACKEND=mysql|pg)");
|
|
717
717
|
return;
|
|
718
718
|
}
|
|
719
719
|
const principal = gatedPrincipal(req, deps.config); // direct-door-secure identity, never the spoofable header
|
|
@@ -818,7 +818,7 @@ async function handleRunsBody(req, res, url, ctx, miss) {
|
|
|
818
818
|
if (rateLimited(req, res))
|
|
819
819
|
return; // mutating, but runs no model (no quota gate — parity with cancel, not steer)
|
|
820
820
|
if (!deps.runStore) {
|
|
821
|
-
sendError(res, 501, "capability.run_store_required", "async runs require
|
|
821
|
+
sendError(res, 501, "capability.run_store_required", "async runs require a durable run store (DB_BACKEND=mysql|pg)");
|
|
822
822
|
return;
|
|
823
823
|
}
|
|
824
824
|
const principal = gatedPrincipal(req, deps.config); // direct-door-secure identity, never the spoofable header
|
|
@@ -1183,7 +1183,7 @@ async function handleRunVerbsBody(req, res, url, ctx, miss) {
|
|
|
1183
1183
|
if (rateLimited(req, res) || quotaExceeded(req, res) || (await leaseDenied(req, res)))
|
|
1184
1184
|
return; // mutating + drives a model agent
|
|
1185
1185
|
if (!deps.runStore) {
|
|
1186
|
-
sendError(res, 501, "capability.run_store_required", "async runs require
|
|
1186
|
+
sendError(res, 501, "capability.run_store_required", "async runs require a durable run store (DB_BACKEND=mysql|pg)");
|
|
1187
1187
|
return;
|
|
1188
1188
|
}
|
|
1189
1189
|
// Identity from gatedPrincipal (direct-door-secure single point), NEVER the spoofable header — `trusted`
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { materializeMcpTools, sanitizePathComponent, SessionPolicyError } from "@sema-agent/core";
|
|
2
|
-
import { mcpForScenario } from "../../
|
|
2
|
+
import { mcpForScenario } from "../../config-center/facade.js";
|
|
3
3
|
import { isUuidV7, isUuidShape } from "../../security.js";
|
|
4
4
|
import { windowMessages, truncateMessageBlobs } from "../../audit.js";
|
|
5
5
|
import { redactSecrets } from "../../trace/redact.js";
|
|
@@ -36,7 +36,7 @@ async function handleTraceUsageBody(req, res, url, ctx, miss) {
|
|
|
36
36
|
return;
|
|
37
37
|
}
|
|
38
38
|
if (!deps.runStore) {
|
|
39
|
-
sendError(res, 501, "capability.run_store_required", "trace API requires
|
|
39
|
+
sendError(res, 501, "capability.run_store_required", "trace API requires a durable run store (DB_BACKEND=mysql|pg) (SESSION_BACKEND=tidb)");
|
|
40
40
|
return;
|
|
41
41
|
}
|
|
42
42
|
const q = new URL(req.url ?? "", "http://x").searchParams;
|
|
@@ -56,7 +56,7 @@ async function handleTraceUsageBody(req, res, url, ctx, miss) {
|
|
|
56
56
|
return;
|
|
57
57
|
}
|
|
58
58
|
if (!deps.runStore) {
|
|
59
|
-
sendError(res, 501, "capability.run_store_required", "trace API requires
|
|
59
|
+
sendError(res, 501, "capability.run_store_required", "trace API requires a durable run store (DB_BACKEND=mysql|pg) (SESSION_BACKEND=tidb)");
|
|
60
60
|
return;
|
|
61
61
|
}
|
|
62
62
|
const query = new URL(req.url ?? "", "http://x").searchParams;
|
package/dist/http/server.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { IncomingMessage } from "node:http";
|
|
|
3
3
|
import { type Runner, type TaskSpec, type TaskResult, type WorkflowRunStore, type MemoryEntry } from "@sema-agent/core";
|
|
4
4
|
import type { TaskRequestBody } from "./wire-types.js";
|
|
5
5
|
import type { ServiceConfig } from "../config-types.js";
|
|
6
|
-
import { type RestartSignal, type SessionMirrorRuling } from "../
|
|
6
|
+
import { type RestartSignal, type SessionMirrorRuling } from "../config-center/facade.js";
|
|
7
7
|
import { type OwnerAwareSessionStore } from "../security.js";
|
|
8
8
|
import type { RunStore, ApprovalStore, ImageIndex, ImageBake, CheckpointStoreFull, ResumeAnchorStore, ApprovalExemptionStore, ServiceSessionPolicyStore, ServiceFileSnapshotStore, StoreBackend } from "../plugins/store-backend.js";
|
|
9
9
|
import { type MemorySyncRequest, type MemorySyncResponse } from "../memory-sync.js";
|
package/dist/http/server.js
CHANGED
|
@@ -4,7 +4,7 @@ import { createHash } from "node:crypto";
|
|
|
4
4
|
import { uuidv7, isThinkingLevel, expandTiers, resumeWithVerification, CheckpointError, HAND_TOOL_EFFECTS, canonicalToolName, defaultTaskRegistry, validatePendingSteer, subscribeWorkflow } from "@sema-agent/core"; // canonicalToolName = core single-source (1.162; replaced the transitional service mirror)
|
|
5
5
|
import { decideParkedAgent, findParkedAgentForCheckpoint } from "../parked-decide.js";
|
|
6
6
|
import { matchCatalogModel } from "../model-select.js";
|
|
7
|
-
import {} from "../
|
|
7
|
+
import {} from "../config-center/facade.js";
|
|
8
8
|
import { HttpError, principalFrom, verifiedPrincipal, setSsoPrincipal, ssoVerifiedPrincipal, setSsoScope, isUuidV7, verifyDirectDoorProof } from "../security.js";
|
|
9
9
|
import { exportSession, importSession } from "../session-sync.js";
|
|
10
10
|
import {} from "../memory-sync.js";
|
|
@@ -407,7 +407,7 @@ export function createHttpServer(rawDeps) {
|
|
|
407
407
|
return;
|
|
408
408
|
}
|
|
409
409
|
// Metrics (read-only): authorized by EITHER the full authToken OR a read-only metricsToken — so a
|
|
410
|
-
// control plane (
|
|
410
|
+
// control plane (config-center) can pull metrics fleet-wide with one token, never holding each
|
|
411
411
|
// worker's full authToken. Handled BEFORE the global gate so metricsToken-only callers aren't 401'd.
|
|
412
412
|
if (req.method === "GET" && (url === "/metrics" || url === "/metrics/summary" || url === "/metrics/plan-cache")) {
|
|
413
413
|
if (!deps.metrics) {
|
|
@@ -495,7 +495,7 @@ export function createHttpServer(rawDeps) {
|
|
|
495
495
|
reqState.source = source;
|
|
496
496
|
// Mandatory service token on submission endpoints (center: a worker MUST validate a service-to-service
|
|
497
497
|
// token on task creation — not "if auth is configured" — else anything in-cluster can submit & bill
|
|
498
|
-
// directly, bypassing
|
|
498
|
+
// directly, bypassing config-center's auth/audit). Fail-closed: the POST task/run/leader endpoints refuse
|
|
499
499
|
// when no authToken is set, unless explicitly opted out for local dev (ALLOW_UNAUTHED_WRITES=true).
|
|
500
500
|
// The bake door (POST /v1/images/bakes*) is build-host-RCE-capable and authed by the Bearer-token-no-cookie
|
|
501
501
|
// model (§P2.4b) — it MUST also refuse when no service token is configured (else a forged principal header
|
package/dist/key-resolver.d.ts
CHANGED
|
@@ -26,4 +26,18 @@ import { type SealedKeyPoison } from "./sealed-key.js";
|
|
|
26
26
|
export declare function createKeyResolver(modelApiKeyEnv: Record<string, string>, env?: NodeJS.ProcessEnv, modelApiKeys?: Record<string, string | SealedKeyPoison>): ((model: Model) => Promise<{
|
|
27
27
|
apiKey: string;
|
|
28
28
|
} | undefined>) | undefined;
|
|
29
|
+
/**
|
|
30
|
+
* The ONE per-model key chain, synchronous: **sealed custody → env-NAME reference → (caller's) gateway key**,
|
|
31
|
+
* with a poisoned sealed entry THROWING instead of returning undefined. `createKeyResolver` is the async
|
|
32
|
+
* `getApiKeyAndHeaders` face of exactly this function — every other consumer must call it rather than
|
|
33
|
+
* re-implement a subset of the chain.
|
|
34
|
+
*
|
|
35
|
+
* 复审 2026-07-29(同族缺口):`brain.ts` 的反应式降级 hop 曾只查 `modelApiKeyEnv` —— sealed-box 托管密钥的
|
|
36
|
+
* 降级目标(按互斥契约**永不**落 modelApiKeyEnv)于是静默拿网关 key 打自己的网关(错 key 打上游),毒丸更
|
|
37
|
+
* 被当成"没配 key"。链只有一条,重复实现就会长出这种半条链的偏差,故此处抽出同步内核共用。
|
|
38
|
+
*
|
|
39
|
+
* `undefined` = this model has no per-model key ⇒ the CALLER's gateway/default credential applies (the
|
|
40
|
+
* additive contract). An empty env value counts as unset (unchanged from the original inline chain).
|
|
41
|
+
*/
|
|
42
|
+
export declare function resolveModelApiKey(modelName: string, modelApiKeyEnv: Record<string, string>, env?: NodeJS.ProcessEnv, modelApiKeys?: Record<string, string | SealedKeyPoison>): string | undefined;
|
|
29
43
|
//# sourceMappingURL=key-resolver.d.ts.map
|
package/dist/key-resolver.js
CHANGED
|
@@ -26,18 +26,34 @@ export function createKeyResolver(modelApiKeyEnv, env = process.env, modelApiKey
|
|
|
26
26
|
if (Object.keys(modelApiKeyEnv).length === 0 && Object.keys(modelApiKeys).length === 0)
|
|
27
27
|
return undefined;
|
|
28
28
|
return async (model) => {
|
|
29
|
-
const
|
|
30
|
-
// Poison pill: the operator configured a sealed key for this model but it cannot be unsealed —
|
|
31
|
-
// fail the brain call LOUD (never undefined = never the gateway fallback).
|
|
32
|
-
if (isSealedKeyPoison(sealed))
|
|
33
|
-
throw new SealedKeyPoisonedError(model.name, sealed);
|
|
34
|
-
if (sealed)
|
|
35
|
-
return { apiKey: sealed }; // sealed-box custody wins (registry-core: sealed 压过 apiKeyEnv)
|
|
36
|
-
const envName = modelApiKeyEnv[model.name];
|
|
37
|
-
if (!envName)
|
|
38
|
-
return undefined; // model has no per-model key → core uses the gateway key
|
|
39
|
-
const apiKey = env[envName];
|
|
29
|
+
const apiKey = resolveModelApiKey(model.name, modelApiKeyEnv, env, modelApiKeys);
|
|
40
30
|
return apiKey ? { apiKey } : undefined;
|
|
41
31
|
};
|
|
42
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* The ONE per-model key chain, synchronous: **sealed custody → env-NAME reference → (caller's) gateway key**,
|
|
35
|
+
* with a poisoned sealed entry THROWING instead of returning undefined. `createKeyResolver` is the async
|
|
36
|
+
* `getApiKeyAndHeaders` face of exactly this function — every other consumer must call it rather than
|
|
37
|
+
* re-implement a subset of the chain.
|
|
38
|
+
*
|
|
39
|
+
* 复审 2026-07-29(同族缺口):`brain.ts` 的反应式降级 hop 曾只查 `modelApiKeyEnv` —— sealed-box 托管密钥的
|
|
40
|
+
* 降级目标(按互斥契约**永不**落 modelApiKeyEnv)于是静默拿网关 key 打自己的网关(错 key 打上游),毒丸更
|
|
41
|
+
* 被当成"没配 key"。链只有一条,重复实现就会长出这种半条链的偏差,故此处抽出同步内核共用。
|
|
42
|
+
*
|
|
43
|
+
* `undefined` = this model has no per-model key ⇒ the CALLER's gateway/default credential applies (the
|
|
44
|
+
* additive contract). An empty env value counts as unset (unchanged from the original inline chain).
|
|
45
|
+
*/
|
|
46
|
+
export function resolveModelApiKey(modelName, modelApiKeyEnv, env = process.env, modelApiKeys = {}) {
|
|
47
|
+
const sealed = modelApiKeys[modelName];
|
|
48
|
+
// Poison pill: the operator configured a sealed key for this model but it cannot be unsealed —
|
|
49
|
+
// fail the brain call LOUD (never undefined = never the gateway fallback).
|
|
50
|
+
if (isSealedKeyPoison(sealed))
|
|
51
|
+
throw new SealedKeyPoisonedError(modelName, sealed);
|
|
52
|
+
if (sealed)
|
|
53
|
+
return sealed; // sealed-box custody wins (registry-core: sealed 压过 apiKeyEnv)
|
|
54
|
+
const envName = modelApiKeyEnv[modelName];
|
|
55
|
+
if (!envName)
|
|
56
|
+
return undefined; // model has no per-model key → core uses the gateway key
|
|
57
|
+
return env[envName] || undefined;
|
|
58
|
+
}
|
|
43
59
|
//# sourceMappingURL=key-resolver.js.map
|
package/dist/run-local.js
CHANGED
|
@@ -33,7 +33,7 @@ import { taskWallClockSec } from "./task-workflow.js";
|
|
|
33
33
|
import { validatePromptsDomain, applyCatalogToSource, CORE_ENGINE_VERSION } from "./capabilities/center-prompts.js";
|
|
34
34
|
import { loadConfig, logConfigDiagnostics } from "./config.js";
|
|
35
35
|
import { createConfigProvider } from "./config-provider.js";
|
|
36
|
-
import { applyEffective, resolveMcpServers, mcpForScenario } from "./
|
|
36
|
+
import { applyEffective, resolveMcpServers, mcpForScenario } from "./config-center/facade.js";
|
|
37
37
|
import { hostExecutionEnvFactory } from "./plugins/remote-env-host.js";
|
|
38
38
|
import { makeLoadProjectMemory, makeProbeInstructionSources } from "./project-memory.js";
|
|
39
39
|
import { loadSkills } from "./capabilities/skills.js";
|
|
@@ -272,7 +272,7 @@ export async function runLocal(argv, deps = {}) {
|
|
|
272
272
|
return 2;
|
|
273
273
|
}
|
|
274
274
|
const sessionStore = fileBackend.sessionStore;
|
|
275
|
-
const keyResolver = createKeyResolver(config.modelApiKeyEnv);
|
|
275
|
+
const keyResolver = createKeyResolver(config.modelApiKeyEnv, process.env, config.modelApiKeys); // [2006] 后续:三参全链(sealed→env→undefined),与 boot 正形同源——二参形曾让 sealed 模型静默落网关 key
|
|
276
276
|
// Host exec lane: run on THIS machine, in a FIXED workspace (CC-like) — the cwd by default (or --workspace /
|
|
277
277
|
// a config.d workdir). The agent's file edits PERSIST there + are user-visible; the dir is the user's OWN and
|
|
278
278
|
// is NEVER deleted (workspaceDir mode). One env per task; the Runner owns its lifetime.
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
*/
|
|
29
29
|
import type { RuntimeCaps } from "@sema-agent/core";
|
|
30
30
|
import type { EntitlementRuntimeCaps } from "@sema-agent/registry-core";
|
|
31
|
-
import { fetchPrincipalCaps, type ExecutionRuling } from "./
|
|
31
|
+
import { fetchPrincipalCaps, type ExecutionRuling } from "./config-center/facade.js";
|
|
32
32
|
import type { ScenarioRuling } from "./capabilities/scenarios.js";
|
|
33
33
|
export interface EntitlementsResolverOpts {
|
|
34
34
|
baseUrl: string;
|
|
@@ -51,7 +51,7 @@ export type Autonomy = "read-only" | "ask" | "plan" | "auto";
|
|
|
51
51
|
export declare const COARSE_SHELL_TOOLS_MIRROR: readonly string[];
|
|
52
52
|
/** Validate `commandPolicy` rules — the shape (array of {command, decision}), the `command` (a bare argv[0] name,
|
|
53
53
|
* see {@link VALID_COMMAND_NAME}), AND the `decision` enum. Returns the list of human-readable errors (empty =
|
|
54
|
-
* OK). Called at config-apply (
|
|
54
|
+
* OK). Called at config-apply (config-center/facade.ts `applyRuntimeHot`) so a malformed rule that BYPASSED the
|
|
55
55
|
* registry schema (a hand-edited config.d / a non-conformant publish) is rejected LOUDLY rather than
|
|
56
56
|
* silently mis-compiling — review MEDIUM: an out-of-enum `decision` (e.g. "DENY") would otherwise fall through
|
|
57
57
|
* {@link compileCommandPolicy}'s `decision==="deny"?…:"ask"` to ASK, silently WEAKENING an intended deny. */
|
|
@@ -108,7 +108,7 @@ const VALID_COMMAND_NAME = /^[A-Za-z0-9._+-]+$/;
|
|
|
108
108
|
const VALID_DECISIONS = new Set(["allow", "ask", "deny"]);
|
|
109
109
|
/** Validate `commandPolicy` rules — the shape (array of {command, decision}), the `command` (a bare argv[0] name,
|
|
110
110
|
* see {@link VALID_COMMAND_NAME}), AND the `decision` enum. Returns the list of human-readable errors (empty =
|
|
111
|
-
* OK). Called at config-apply (
|
|
111
|
+
* OK). Called at config-apply (config-center/facade.ts `applyRuntimeHot`) so a malformed rule that BYPASSED the
|
|
112
112
|
* registry schema (a hand-edited config.d / a non-conformant publish) is rejected LOUDLY rather than
|
|
113
113
|
* silently mis-compiling — review MEDIUM: an out-of-enum `decision` (e.g. "DENY") would otherwise fall through
|
|
114
114
|
* {@link compileCommandPolicy}'s `decision==="deny"?…:"ask"` to ASK, silently WEAKENING an intended deny. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sema-agent/server",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.2.0",
|
|
4
4
|
"description": "Sema Server — the server/API implementation layer for Sema, wiring core, registry, model providers, and cloud agent execution. Built on @sema-agent/core.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "BUSL-1.1",
|