@sema-agent/server 7.120.0 → 7.122.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (149) hide show
  1. package/MIGRATION.md +173 -0
  2. package/README.md +1 -1
  3. package/README.zh-CN.md +3 -1
  4. package/USAGE.md +97 -29
  5. package/deploy/sema-up/chart/templates/server-v4pro.yaml +2 -2
  6. package/deploy/sema-up/chart/templates/server.yaml +2 -2
  7. package/deploy/sema-up/stack/sema-stack-cluster.yaml +1 -1
  8. package/dist/approval-reconciler.d.ts +8 -0
  9. package/dist/approval-reconciler.js +72 -52
  10. package/dist/auto-mode-face.d.ts +9 -2
  11. package/dist/auto-mode-face.js +5 -3
  12. package/dist/bench/s1/live-deps.js +1 -0
  13. package/dist/boot/config-center.d.ts +5 -7
  14. package/dist/boot/config-center.js +33 -9
  15. package/dist/boot/engine-lease.d.ts +1 -1
  16. package/dist/boot/leader.js +1 -0
  17. package/dist/boot/reapers.d.ts +3 -0
  18. package/dist/boot/reapers.js +2 -1
  19. package/dist/boot/resolve-spec.js +2 -4
  20. package/dist/boot/shutdown.d.ts +9 -10
  21. package/dist/boot/shutdown.js +59 -16
  22. package/dist/boot/stage-08-reapers.js +2 -2
  23. package/dist/boot/stage-09-leader.d.ts +2 -9
  24. package/dist/boot/stage-10-http-server.d.ts +1 -9
  25. package/dist/boot/stage-10-http-server.js +31 -8
  26. package/dist/capabilities/composite-members.d.ts +1 -1
  27. package/dist/capabilities/composite-members.js +1 -1
  28. package/dist/capabilities/memory-notice.d.ts +1 -1
  29. package/dist/config-catalog.js +12 -6
  30. package/dist/config-center/center-request.d.ts +13 -1
  31. package/dist/config-center/center-request.js +25 -10
  32. package/dist/config-types.d.ts +44 -5
  33. package/dist/config-types.js +6 -0
  34. package/dist/config.d.ts +35 -6
  35. package/dist/config.js +113 -33
  36. package/dist/fleet/drain-entry.d.ts +112 -0
  37. package/dist/fleet/drain-entry.js +38 -0
  38. package/dist/fleet/engine-signal.d.ts +144 -0
  39. package/dist/fleet/engine-signal.js +60 -0
  40. package/dist/fleet/serving-lease.d.ts +47 -0
  41. package/dist/fleet/serving-lease.js +233 -0
  42. package/dist/fleet-client.d.ts +35 -19
  43. package/dist/fleet-client.js +49 -25
  44. package/dist/governance-overrides.d.ts +7 -2
  45. package/dist/http/admission.js +7 -5
  46. package/dist/http/approval-sweeps.js +5 -7
  47. package/dist/http/checkpoint-ctx.d.ts +60 -11
  48. package/dist/http/checkpoint-ctx.js +23 -9
  49. package/dist/http/dispatch.js +33 -16
  50. package/dist/http/health-self-report.d.ts +60 -0
  51. package/dist/http/health-self-report.js +53 -0
  52. package/dist/http/principal-gate.d.ts +8 -1
  53. package/dist/http/principal-gate.js +1 -0
  54. package/dist/http/readiness.d.ts +25 -5
  55. package/dist/http/readiness.js +31 -6
  56. package/dist/http/resume-legs.js +9 -9
  57. package/dist/http/route-ctx.d.ts +25 -7
  58. package/dist/http/route-table.d.ts +6 -0
  59. package/dist/http/route-table.js +8 -1
  60. package/dist/http/routes/a2a-serve.d.ts +1 -1
  61. package/dist/http/routes/a2a-serve.js +5 -5
  62. package/dist/http/routes/admin-drain.d.ts +15 -31
  63. package/dist/http/routes/admin-drain.js +28 -10
  64. package/dist/http/routes/approvals-assistant.d.ts +4 -3
  65. package/dist/http/routes/approvals-assistant.js +9 -9
  66. package/dist/http/routes/capabilities.js +7 -5
  67. package/dist/http/routes/devices.js +3 -2
  68. package/dist/http/routes/fleet.d.ts +1 -0
  69. package/dist/http/routes/fleet.js +1 -1
  70. package/dist/http/routes/images.d.ts +1 -0
  71. package/dist/http/routes/images.js +1 -1
  72. package/dist/http/routes/leader.d.ts +1 -1
  73. package/dist/http/routes/leader.js +7 -1
  74. package/dist/http/routes/memory-consolidation.d.ts +1 -1
  75. package/dist/http/routes/memory-consolidation.js +1 -1
  76. package/dist/http/routes/notify-wake.d.ts +1 -1
  77. package/dist/http/routes/notify-wake.js +1 -1
  78. package/dist/http/routes/rules.js +4 -3
  79. package/dist/http/routes/runs.d.ts +4 -2
  80. package/dist/http/routes/runs.js +7 -6
  81. package/dist/http/routes/sessions.d.ts +1 -0
  82. package/dist/http/routes/sessions.js +1 -1
  83. package/dist/http/routes/side-query.d.ts +1 -1
  84. package/dist/http/routes/side-query.js +1 -1
  85. package/dist/http/routes/tasks.d.ts +2 -2
  86. package/dist/http/routes/tasks.js +3 -3
  87. package/dist/http/routes/trace-usage.d.ts +1 -0
  88. package/dist/http/routes/trace-usage.js +1 -1
  89. package/dist/http/routes/workflows.d.ts +1 -0
  90. package/dist/http/routes/workflows.js +1 -1
  91. package/dist/http/send.d.ts +13 -0
  92. package/dist/http/send.js +9 -0
  93. package/dist/http/server.d.ts +10 -21
  94. package/dist/http/server.js +20 -1
  95. package/dist/http/stored-body-replay.d.ts +27 -0
  96. package/dist/http/stored-body-replay.js +23 -0
  97. package/dist/http/wire-types.d.ts +7 -1
  98. package/dist/leader/leader.d.ts +22 -1
  99. package/dist/leader/leader.js +4 -1
  100. package/dist/leader/repair-wire.d.ts +1 -1
  101. package/dist/leader/repair-wire.js +2 -2
  102. package/dist/leader/run-entry.d.ts +27 -0
  103. package/dist/leader/run-entry.js +29 -0
  104. package/dist/leader/wire.d.ts +10 -0
  105. package/dist/leader/wire.js +15 -10
  106. package/dist/model-reach-scope.d.ts +15 -3
  107. package/dist/model-reach-scope.js +3 -2
  108. package/dist/model-route-url.d.ts +13 -20
  109. package/dist/model-route-url.js +28 -24
  110. package/dist/model-unreachable-notice.d.ts +2 -1
  111. package/dist/model-unreachable-notice.js +1 -0
  112. package/dist/observability/fail-open.d.ts +35 -2
  113. package/dist/observability/fail-open.js +20 -0
  114. package/dist/plugins/approval-ask-store-file.d.ts +1 -0
  115. package/dist/plugins/approval-ask-store-file.js +3 -0
  116. package/dist/plugins/approval-ask-store-memory.d.ts +1 -0
  117. package/dist/plugins/approval-ask-store-memory.js +6 -0
  118. package/dist/plugins/approval-ask-store-sql.d.ts +12 -0
  119. package/dist/plugins/approval-ask-store-sql.js +4 -0
  120. package/dist/plugins/one-time-migrations.js +14 -1
  121. package/dist/plugins/park-commit-watch.d.ts +19 -5
  122. package/dist/plugins/park-commit-watch.js +24 -17
  123. package/dist/plugins/permission-rule-store-sql.d.ts +2 -1
  124. package/dist/plugins/remote-env-device.js +0 -1
  125. package/dist/request-key-closure.js +1 -1
  126. package/dist/router/route-orchestration.d.ts +4 -2
  127. package/dist/router/route-orchestration.js +2 -1
  128. package/dist/rules-consent.d.ts +6 -2
  129. package/dist/rules-consent.js +3 -0
  130. package/dist/run-local.js +3 -1
  131. package/dist/runs.js +1 -1
  132. package/dist/runtime-governance.d.ts +44 -32
  133. package/dist/runtime-governance.js +23 -9
  134. package/dist/sighup-idle.d.ts +7 -1
  135. package/dist/sighup-idle.js +4 -4
  136. package/dist/task-settings.d.ts +37 -14
  137. package/dist/task-settings.js +20 -20
  138. package/dist/tool-approval.d.ts +13 -0
  139. package/dist/tool-approval.js +17 -2
  140. package/dist/trace/core-keyset-guard.d.ts +2 -2
  141. package/dist/trace/redact.d.ts +3 -3
  142. package/dist/trace/self-minted-wire.d.ts +64 -0
  143. package/dist/trace/self-minted-wire.js +30 -0
  144. package/dist/unattended-permission-mode.d.ts +17 -13
  145. package/dist/unattended-permission-mode.js +3 -1
  146. package/dist/url-shape.d.ts +31 -0
  147. package/dist/url-shape.js +41 -0
  148. package/package.json +3 -3
  149. package/scripts/drain-check-local.mjs +1 -1
@@ -121,6 +121,16 @@ export declare const DRAIN_SECOND_SIGNAL_MIN_GAP_BOUNDS: {
121
121
  readonly maxMs: 10000;
122
122
  };
123
123
  export declare const DRAIN_SECOND_SIGNAL_MIN_GAP_DEFAULT_MS = 250;
124
+ /**
125
+ * 本仓读一个租户 scope 的**唯一**读法:给了就是它,缺席 = settings-schema 的缺省 scope(`GLOBAL_SCOPE`;缺省值只在契约包里写一处)。
126
+ * 引擎自己的 scope(`config.ts` 解析 env `FLEET_SCOPE`)与排空请求的 `scope`(`fleet/drain-entry.ts`)走同一只 —— 两侧缺席的读法不会分家。
127
+ * 住在本叶(而不在 `fleet/drain-entry.ts`):`config.ts` 读它,而 drain-entry 值引 `plugins/store-contracts.ts`,config 不值引 plugins 层。
128
+ */
129
+ export declare function fleetScopeOf(raw: string | undefined): string;
130
+ /** 中心基址三席(`SEMA_REGISTRY_URL` / `SEMA_REGISTRY_LIVE_URL` / `SEMA_REGISTRY_WEB_URL`)与公告地址的形句 —— 判官 `config-center/center-request.ts` `pathBaseUrlRefusal` 一只(形判 = `url-shape.ts`),配置目录四行同读这一句。 */
131
+ export declare const CENTER_BASE_URL_RULE = "\u8BBE\u4E86\u5C31\u5FC5\u987B\u662F\u53EF\u89E3\u6790\u7684 http(s) URL,\u4E14\u4E0D\u5E26\u51ED\u636E / query / fragment(\u57FA\u5740\u540E\u9762\u8981\u62FC\u8DEF\u5F84);\u574F\u5F62\u62D2\u542F(\u6CA1\u63A5\u4E2D\u5FC3\u4E5F\u62D2),\u62D2\u53E5\u53EA\u70B9\u540D\u89C4\u5219,\u4E0D\u56DE\u663E scheme";
132
+ /** 编排器片 2:`FLEET_LEASE_OUTAGE_MAX_MS` 的缺省(24 h)—— 中心 / 库故障、中心降级在今天都靠 LKG 无限期容忍,10 分钟级的窗会把它们变成数据面停服。 */
133
+ export declare const FLEET_LEASE_OUTAGE_MAX_DEFAULT_MS = 86400000;
124
134
  /**
125
135
  * S-753(core 7.33.0 #1121)—— `MODEL_FIRST_BYTE_TIMEOUT_MS`(→ core `BrainTimeoutConfig.firstByteTimeoutMs`,`fetch()` → 响应头
126
136
  * 的等待)的**合法域**,闭区间、整数 ms。解析器(`config.ts`)与目录行(`config-catalog.ts`)共读这一处。
@@ -1469,8 +1479,9 @@ export interface ServiceConfigFlat {
1469
1479
  * `TaskSpec.enablePlanMode:true` but NOT `handsReadOnly` (writable-start) → core auto-mounts `enter_plan_mode`
1470
1480
  * (+ `present_plan`) so the model can self-call `enter_plan_mode` at ANY time it judges a task needs planning
1471
1481
  * (CC EnterPlanMode parity). 🆕 DEFAULT ON (clay: model-driven plan is resident — the model decides when to plan);
1472
- * set `PLAN_MODE_ENABLED=false` to opt out. USER-driven plan (`body.permissionMode=plan` → enablePlanMode +
1473
- * handsReadOnly, read-only from start) is unaffected/independent. */
1482
+ * set `PLAN_MODE_ENABLED=false` to opt out. A USER-driven plan (`body.permissionMode=plan`, bundle `defaultMode`, or
1483
+ * `AUTONOMY=plan`) is the same engine mode word, independent of this knob: the engine's mode table mounts the plan
1484
+ * tools for a declared `plan` itself, and no plan entry clamps the hands (only `AUTONOMY=read-only` does). */
1474
1485
  planModeEnabled: boolean;
1475
1486
  /**
1476
1487
  * [ref]⑥(core 5.56 `Hooks.timeoutMs` 座)—— **每一只 hook 席位单次调用的时间上限**(ms)。
@@ -2001,6 +2012,9 @@ export interface ServiceConfigFlat {
2001
2012
  /** LKG 落盘路径的覆写(`CONFIG_LKG_PATH`,绝对路径;单一读法 `config-lkg.ts` `lkgPathOverrideOf`)。缺席 = LKG 落在配置根下按箱派生。
2002
2013
  * 读者:LKG 的落盘 / 读回(`boot/config-center.ts`)与读拒行(部署姿态座席族 —— LKG 在哪,读拒就守哪,`lkgReadDenyRows`)。 */
2003
2014
  configLkgPath?: string;
2015
+ /** 远程配置的 LKG(last-known-good)盘缓存读写开关(`CONFIG_LKG_ENABLED`,缺省开;只对远程 provider 有意义)。单一读法:`loadConfig` 铸这一席
2016
+ * (同时登记极性行),`boot/config-center.ts` 的两个读点(boot 的 LKG 来源判定、skill 正文缓存目录)只读它 —— 起服之后改环境变量不影响。 */
2017
+ configLkgEnabled: boolean;
2004
2018
  /** Structured log level. */
2005
2019
  logLevel: "debug" | "info" | "warn" | "error";
2006
2020
  /** Per-principal request cap per minute on task/run endpoints. 0 disables. */
@@ -2090,9 +2104,15 @@ export interface ServiceConfigFlat {
2090
2104
  * `credential`(S-651,BREAKING:取代 7.98.0 及以前的 `token: string` 启动快照):凭证来源活对象
2091
2105
  * (`center-credential.ts`),每条出站请求经它取一次值 —— `SEMA_REGISTRY_TOKEN`(值)或
2092
2106
  * `SEMA_REGISTRY_TOKEN_FILE`(每次读文件,轮换 / 换账号不重启)二选一(两键名与判官的属主 = settings-schema
2093
- * `CENTER_CREDENTIAL_ENV` / `judgeCenterCredentialDeclaration`,S-742)。 */
2107
+ * `CENTER_CREDENTIAL_ENV` / `judgeCenterCredentialDeclaration`,S-742)。
2108
+ * `liveBaseUrl`(编排器片 2,env `SEMA_REGISTRY_LIVE_URL`):fleet 活性组(心跳 / 注销、服务租约 —— settings-schema `FLEET_LIVE_PATHS`)的基址,
2109
+ * 给「管理面在就绪门后、活性面另走一条不看就绪的入口」的部署;缺席 ⇒ 活性组也打 `baseUrl`(这条缺省只在 `centerFetch` 读)。
2110
+ * `webBaseUrl`(7.121.0,env `SEMA_REGISTRY_WEB_URL`):这个中心的 **web 面固定基址**,运维声明、引擎原样转述到 `/health.center.webBaseUrl`
2111
+ * (壳按它拼会话深链;路径怎么拼归壳)。起服时判成「可拼路径的基址」:http(s)、不带凭据 / query / fragment,否则拒启;缺席即缺席,引擎不拼不猜。 */
2094
2112
  configCenter?: {
2095
2113
  baseUrl: string;
2114
+ liveBaseUrl?: string;
2115
+ webBaseUrl?: string;
2096
2116
  credential: CenterCredentialSource;
2097
2117
  dryRun: boolean;
2098
2118
  worker?: string;
@@ -2103,6 +2123,25 @@ export interface ServiceConfigFlat {
2103
2123
  * 显式值压过派生(部署级旋钮无条件,与 `READ_FACE` 缺省按 posture 派生 S-167 同形)。只在装了租约管理器的部署上有意义
2104
2124
  * (`lease:"absent"` 的部署不参与);生效值上 caps `centerWiring.failMode`。 */
2105
2125
  fleetLeaseFailClosed: boolean;
2126
+ /** 编排器片 2:fleet 成员的**代际**(env `FLEET_GENERATION`,settings-schema `FLEET_GENERATION_ENV`;配置渲染器是唯一写手)。
2127
+ * 缺席 ⇔ 非 fleet 成员(单机形:不续服务租约、不受围栏)。在场则必须是规范十进制串(`"0"`、`"7"`;不许前导零 / 符号 / 空白 / 空串),
2128
+ * 否则拒启 —— 「设了但读不成代际」是渲染器或手配的事故,不是「非成员」。引擎把它原样回声到心跳与 `/health`(`generation`)。 */
2129
+ fleetGeneration?: string;
2130
+ /** 编排器片 2:本引擎所在的**租户 scope**(env `FLEET_SCOPE`,配置渲染器写)。`loadConfig` 解析一次:缺席 ⇒ 缺省 scope(settings-schema
2131
+ * `GLOBAL_SCOPE`,经本文件的 `fleetScopeOf` —— 本仓唯一读法);在场必须是 settings-schema `ScopeId` 形,否则拒启。
2132
+ * 唯一读者 = 排空口 `POST /v1/admin/drain` 的身份比对;续租体与心跳不带它(那两处的 scope 由中心凭证绑定)。 */
2133
+ fleetScope: string;
2134
+ /** `FLEET_ADVERTISE_ADDRESS`:本实例对中心公告的可达地址(运维声明;k8s pod IP / docker alias / 127.0.0.1:port 只有部署环境自己知道)。
2135
+ * `loadConfig` **无条件**解析一次:未设 / 空串 ⇒ 缺席(公告环不装);在场必须是可解析的 http(s) URL,否则拒启。读者:fleet 公告环、
2136
+ * 围栏判官 `fleetFenceLaneOf`、`/health.center.sessionsVisible`、启动日志 —— 都读这一席,不读 env。 */
2137
+ fleetAdvertiseAddress?: string;
2138
+ /** 编排器片 2:一台实例「活着而不服务」的最长时长 `T_max`(ms;env `FLEET_LEASE_OUTAGE_MAX_MS`)。缺省 86 400 000(24 h);地板 = 服务租约
2139
+ * TTL 地板(settings-schema `SERVING_LEASE_TTL_FLOOR_MS`,60 s);不许 0 / 无穷,坏值拒启。`loadConfig` 无条件解析(成员与非成员同读)。两个读者:
2140
+ * ① 服务租约**中心不可达的容忍上限**(`fleet/serving-lease.ts`;只在 fleet 成员 ∧ 中心宣告了 `servingLease` 能力位时生效):租约过期(中心
2141
+ * 不可达 / 答 unknown)之后照常服务,直到过期满这么久 ⇒ 新计费提交 503 `state.lease_expired`(取消 / HITL 应答 / 续跑 / 读面照放行),在飞不动、
2142
+ * 不自停;中心恢复授予即恢复。② 排空口 `POST /v1/admin/drain` 的**期限准入上界**(`fleet/drain-entry.ts` 规则 2):`deadlineAt` 离此刻超过它 ⇒
2143
+ * 400 `request.field_invalid`。 */
2144
+ fleetLeaseOutageMaxMs: number;
2106
2145
  /** S-698(design-325 §2):中心凭证**续签提前量**(ms)—— `SEMA_REGISTRY_TOKEN_FILE` 指向人的登录会话信封
2107
2146
  * (`kind:"user-session"`)时,访问令牌离到期 ≤ 本值即由引擎续签并原子改写同一文件(`center-credential-renewer.ts`)。
2108
2147
  * 缺省 5 min;`0` = 关续签、只读信封(壳负责续);其余必须是整数毫秒且落在 `CENTER_CREDENTIAL_RENEW_LEAD_MIN_MS`(一次续签请求的
@@ -2134,13 +2173,13 @@ export type ServiceMemoryConfig = Pick<ServiceConfigFlat, "memoryEngineEnabled"
2134
2173
  * config 这一层它只是一句声明。 */
2135
2174
  export type ServiceAuthConfig = Pick<ServiceConfigFlat, "authToken" | "authTokens" | "allowUnauthedWrites" | "corsOrigins" | "principalHeader" | "requirePrincipal" | "autonomy" | "commandPolicy" | "compliancePosture" | "lockedConfigKeys" | "configCatalogEnabled" | "retentionPolicy" | "retentionSweep" | "operatorPrincipals" | "principalJwtPubkeys" | "principalJwtIss" | "principalJwtAud" | "authBridgeIssuer" | "principalJwtMaxTtlSec" | "bindHost" | "bindHostSource">;
2136
2175
  /** 组:orchestration(编排 + 执行车道 + 沙箱面 + workflow/后台 agent 存留)。 */
2137
- export type ServiceOrchestrationConfig = Pick<ServiceConfigFlat, "remoteExec" | "remoteExecHomeDir" | "requireExecutionLane" | "worktreeIsolation" | "leaderEnabled" | "leaderFanoutEnabled" | "routerEnabled" | "selfOrchestrationEnabled" | "selfOrchestrationModels" | "selfOrchestrationWorkerIsolation" | "forkEnabled" | "experimentalObserverAgents" | "schedulerEnabled" | "schedulerSessionWakeup" | "schedulerSessionLifetime" | "schedulerStorePath" | "planModeEnabled" | "hooksTimeoutMs" | "workflowRunStoreBackend" | "workflowOrphanGraceMs" | "workflowJournalRetentionMs" | "workflowRunRetentionMs" | "usageWindows" | "workflowAgentsReadOnly" | "workflowSizeGuideline" | "backgroundAgentRetentionMs" | "backgroundAgentStaleRunningMs" | "backgroundAgentParkClaimStaleMs" | "delegationEntryCaps" | "rosterRetentionMs" | "leaderRunStaleMs" | "scratchpadSweepTtlMs" | "sandboxPkgSource" | "sessionAutoTitle" | "sessionAutoTitleSource" | "selectEnvironmentTool" | "envFactsEnabled" | "toolDeferLongtail" | "lspEnabled" | "lspHostEnabled" | "lspEnvAllow" | "imageBakes" | "readFace" | "readFaceSource" | "taskWriteFaceEscalation" | "readDenyPatterns" | "readDenyBuiltinTiers" | "readDenyBuiltinExclude" | "agentConfigDir" | "configLkgPath">;
2176
+ export type ServiceOrchestrationConfig = Pick<ServiceConfigFlat, "remoteExec" | "remoteExecHomeDir" | "requireExecutionLane" | "worktreeIsolation" | "leaderEnabled" | "leaderFanoutEnabled" | "routerEnabled" | "selfOrchestrationEnabled" | "selfOrchestrationModels" | "selfOrchestrationWorkerIsolation" | "forkEnabled" | "experimentalObserverAgents" | "schedulerEnabled" | "schedulerSessionWakeup" | "schedulerSessionLifetime" | "schedulerStorePath" | "planModeEnabled" | "hooksTimeoutMs" | "workflowRunStoreBackend" | "workflowOrphanGraceMs" | "workflowJournalRetentionMs" | "workflowRunRetentionMs" | "usageWindows" | "workflowAgentsReadOnly" | "workflowSizeGuideline" | "backgroundAgentRetentionMs" | "backgroundAgentStaleRunningMs" | "backgroundAgentParkClaimStaleMs" | "delegationEntryCaps" | "rosterRetentionMs" | "leaderRunStaleMs" | "scratchpadSweepTtlMs" | "sandboxPkgSource" | "sessionAutoTitle" | "sessionAutoTitleSource" | "selectEnvironmentTool" | "envFactsEnabled" | "toolDeferLongtail" | "lspEnabled" | "lspHostEnabled" | "lspEnvAllow" | "imageBakes" | "readFace" | "readFaceSource" | "taskWriteFaceEscalation" | "readDenyPatterns" | "readDenyBuiltinTiers" | "readDenyBuiltinExclude" | "agentConfigDir" | "configLkgPath" | "configLkgEnabled">;
2138
2177
  /** 组:limitsHttp(HTTP 面 + 各类上限/配额/回收窗)。 */
2139
2178
  export type ServiceLimitsHttpConfig = Pick<ServiceConfigFlat, "port" | "attachmentOrphanGraceMs" | "workspaceFileMaxBytes" | "attachmentMaxBytes" | "attachmentMimeAllowlist" | "attachmentUnboundTtlMs" | "infraCostRates" | "drainGraceMs" | "drainSecondSignalMinGapMs" | "sighupIdleGraceMs" | "parentPid" | "engineAutoExit" | "engineLingerMs" | "engineOrphanLingerMs" | "rateLimitPerMin" | "maxTaskCostUsd" | "maxTaskTokens" | "maxPrincipalCostUsd" | "costQuotaWindowSec" | "reapIntervalSec" | "runStaleSec" | "toolResultTtlSec">;
2140
2179
  /** 组:observability(可观测)。 */
2141
2180
  export type ServiceObservabilityConfig = Pick<ServiceConfigFlat, "metricsToken" | "traceToken" | "toolTrace" | "traceThinking" | "logLevel" | "otel">;
2142
2181
  /** 组:integrations(外部集成)。`mcpServers` / `a2aPeers` 由 sema-registry 适配器填(无 env 腿),归本组。 */
2143
- export type ServiceIntegrationsConfig = Pick<ServiceConfigFlat, "pluginsAllowHosts" | "mcpServers" | "a2aPeers" | "a2aServe" | "configBootFetchBudgetMs" | "gitApiBaseUrl" | "gitApiToken" | "gitApiKind" | "defaultScenario" | "skillsDir" | "centerCredentialRenewLeadMs" | "configCenter" | "fleetLeaseFailClosed" | "configProvider" | "configLocalDir">;
2182
+ export type ServiceIntegrationsConfig = Pick<ServiceConfigFlat, "pluginsAllowHosts" | "mcpServers" | "a2aPeers" | "a2aServe" | "configBootFetchBudgetMs" | "gitApiBaseUrl" | "gitApiToken" | "gitApiKind" | "defaultScenario" | "skillsDir" | "centerCredentialRenewLeadMs" | "configCenter" | "fleetLeaseFailClosed" | "fleetGeneration" | "fleetScope" | "fleetAdvertiseAddress" | "fleetLeaseOutageMaxMs" | "configProvider" | "configLocalDir">;
2144
2183
  /** 九个组槽。每组恒在场(`loadConfig` / `attachConfigGroups` 装好才交出配置),故不可选——新代码写
2145
2184
  * `config.modelPlane.model` 不需要 `?.`(可选组会把 `Model` 污染成 `Model | undefined`)。 */
2146
2185
  export interface ServiceConfigGroups {
@@ -1,4 +1,5 @@
1
1
  import { DRAIN_GRACE_FLOOR_MS } from "@sema-agent/settings-schema/fleet";
2
+ import { GLOBAL_SCOPE } from "@sema-agent/settings-schema/api/scopes";
2
3
  export const TASK_WRITE_FACE_ESCALATIONS = ["allow", "refuse"];
3
4
  export const RETENTION_MODES = ["audit-only", "enforce"];
4
5
  export const CONFIG_PROVIDER_WORDS = ["local", "remote"];
@@ -8,6 +9,11 @@ export const AUTO_MODE_CLASSIFIER_TIMEOUT_BOUNDS = { minMs: 1_000, maxMs: MAX_TI
8
9
  export { DRAIN_GRACE_FLOOR_MS };
9
10
  export const DRAIN_SECOND_SIGNAL_MIN_GAP_BOUNDS = { minMs: 0, maxMs: DRAIN_GRACE_FLOOR_MS };
10
11
  export const DRAIN_SECOND_SIGNAL_MIN_GAP_DEFAULT_MS = 250;
12
+ export function fleetScopeOf(raw) {
13
+ return raw ?? GLOBAL_SCOPE;
14
+ }
15
+ export const CENTER_BASE_URL_RULE = "设了就必须是可解析的 http(s) URL,且不带凭据 / query / fragment(基址后面要拼路径);坏形拒启(没接中心也拒),拒句只点名规则,不回显 scheme";
16
+ export const FLEET_LEASE_OUTAGE_MAX_DEFAULT_MS = 86_400_000;
11
17
  export const FIRST_BYTE_TIMEOUT_BOUNDS = { minMs: 0, maxMs: MAX_TIMER_DELAY_MS };
12
18
  export const RESILIENCE_DEFAULTS = {
13
19
  firstTokenTimeoutMs: { env: "MODEL_FIRST_TOKEN_TIMEOUT_MS", value: 600_000 },
package/dist/config.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { type Model, type PathRuleBases, type PermissionRule } from "@sema-agent/core";
2
+ import { type CenterEndpoint } from "./config-center/center-request.js";
2
3
  import { type Autonomy } from "./runtime-governance.js";
3
4
  import { type RunnerEnvNameAllowlistKnob, type ServerEnvNameAllowlistKnob } from "./env-name-allowlist-knobs.js";
4
5
  import type { ServiceConfig, ServiceConfigFlat, ServiceConfigGroups } from "./config-types.js";
@@ -243,10 +244,7 @@ export declare function configKnobTable(): ConfigKnobRecord[];
243
244
  /** [ref] B4 ②:D 族四枚(`PROJECT_MEMORY` / `CONFIG_LKG` / `HOST_BG` / `HOST_EXEC_SPOOL`)的正名 —— 缺省开,读点只读正名。
244
245
  * 3.0.0 之前它们叫 `X_DISABLED`(读点上的双重否定);那四个旧负名是墓碑,在 `RETIRED_ENVS` 里(S-588 起在场即拒启,不论正名在不在)。
245
246
  *
246
- * LKG (last-known-good) persistence of the remote effective-config fetch. Read LAZILY at both main.ts sites
247
- * rather than lifted onto `ServiceConfig`, so this stays a pure env knob; `loadConfig` pre-registers it (below)
248
- * so its polarity row appears at boot with everything else. */
249
- export declare const configLkgEnabled: () => boolean;
247
+ * 其中 `CONFIG_LKG_ENABLED`(远程配置的 LKG 盘缓存)已升成配置席 `configLkgEnabled`,由 `loadConfig` 一次铸出;这里只剩另外两个懒读孪生。 */
250
248
  /** The `host` exec lane's background-shell capability kill switch (read per capability probe in remote-env-host.ts). */
251
249
  export declare const hostBackgroundShellEnabled: () => boolean;
252
250
  /** The `host` exec lane's spool-file stdio form; false ⇒ the pre-1.226 pipe fallback (read per exec). */
@@ -302,8 +300,8 @@ export declare function isRosterGateArmed(configCenter: ServiceConfig["configCen
302
300
  * 的判词 `unmintable_segment`,句子带它的 guidance 与 1:1 现名);本仓 7.116.0–7.117.x 那只自比 `mintNamespacePrefix` 的对照随之退役。
303
301
  * 引擎判官放行、却一只工具都够不着的一形 —— **工具段为空**(`mcp__srv__`)—— 由本仓判(`namespaced-rule-name.ts`,会话规则写口两腿与 SQL 店写口同调)。
304
302
  * ⚠️ 两段覆盖形 `mcp__<server>`(服务器段是自铸形)够得着(S-1047 收掉的分叉,见下)。
305
- * - **core 的文法编不出来的串**(名字里带反斜杠、单条 >512 字符、空串):判词取自 core 自己的
306
- * `validatePermissionRules`,见 {@link uncompilableRuleGuidance}。[ref]①([ref])补齐 ——
303
+ * - **core 的文法编不出来的串**:判词取自 core 自己的 `validatePermissionRules`,射程与指引见 {@link uncompilableRuleGuidance}
304
+ * (名表消费者只递得来名字里带反斜杠、单条 >512 字符、空串三种;规则编译器消费者递来的每一条内容形都走这里)。[ref]①([ref])补齐 ——
307
305
  * 此前只有 `ask` 腿(经 `createPermissionRulePolicy` 的 throw)拒得出这一族,`deny` 静默 no-op。
308
306
  *
309
307
  * 已接受的权衡(与退役表墓碑同一条):部署自有工具真叫 `figma__x` 的极端形会被误拒——boot 期无 roster 可
@@ -402,6 +400,37 @@ export declare const LIMITS_HTTP_FLOORS: {
402
400
  readonly attachmentUnboundTtlMs: 60000;
403
401
  readonly drainGraceMs: 10000;
404
402
  };
403
+ /**
404
+ * 编排器片 2 —— fleet 成员面 env 的拒启码(拼法随 `config.*` 家族,按**规则**命名)。拒启异常把码挂在 `code` 上(`boot/fatal-exit.ts` 按
405
+ * `<name> [<code>]: <message>` 印进 fatal 标题),句子本身不再重复方括号。闭集:加一种 = 加一个键。
406
+ */
407
+ export declare const FLEET_ENV_REFUSAL_CODES: {
408
+ readonly generation_invalid: "config.fleet_generation_invalid";
409
+ readonly scope_invalid: "config.fleet_scope_invalid";
410
+ readonly advertise_address_invalid: "config.fleet_advertise_address_invalid";
411
+ readonly member_unfenced: "config.fleet_member_unfenced";
412
+ };
413
+ export type FleetEnvRefusalCode = (typeof FLEET_ENV_REFUSAL_CODES)[keyof typeof FLEET_ENV_REFUSAL_CODES];
414
+ /** 一只 fleet 成员的围栏车道:续租打的中心端点(车道对象原样,含活性基址)+ 代际。 */
415
+ export interface FleetFenceLane {
416
+ readonly center: CenterEndpoint & {
417
+ readonly worker: string;
418
+ };
419
+ readonly generation: string;
420
+ }
421
+ /**
422
+ * 编排器片 2 规则 0(成员 ⇒ 围栏车道必须齐)的**唯一**判官:非成员(`fleetGeneration` 缺席)⇒ `undefined`;成员 ∧ 中心车道齐
423
+ * (`centerLaneConfigured`)∧ 公告地址在 ⇒ 车道;成员而缺任一件 ⇒ 拒启(点名缺的键)。`loadConfig` 在 integrations 域里调一次(拒启早于
424
+ * 任何装配);装配层(第 10 段)对同一份配置再调一次取车道值 —— 同一只判官,过了 `loadConfig` 的配置在那里不会再拒。
425
+ * 读成「非成员」或「装不上就算了」会让一只本该受围栏的实例悄悄脱离围栏:能力位只在心跳答复里,不公告就永远听不到。
426
+ */
427
+ export declare function fleetFenceLaneOf(cfg: {
428
+ readonly fleetGeneration?: string | undefined;
429
+ readonly configCenter?: (CenterEndpoint & {
430
+ readonly worker?: string | undefined;
431
+ }) | undefined;
432
+ readonly fleetAdvertiseAddress?: string | undefined;
433
+ }): FleetFenceLane | undefined;
405
434
  /** 组名 → 该组取景的平铺键(introspection 面:测试用它钉「每个平铺键恰好被一组取景」)。 */
406
435
  export declare const CONFIG_GROUP_KEYS: {
407
436
  readonly [G in keyof ServiceConfigGroups]: readonly (keyof ServiceConfigFlat)[];
package/dist/config.js CHANGED
@@ -10,7 +10,10 @@ import { DEFAULT_ELICITATION_THROTTLE } from "./elicitation.js";
10
10
  import { isV2ScopeKey, memoryReplicationCoverage, replicationCovers } from "./memory-scope.js";
11
11
  import { DEFAULT_QUESTION_THROTTLE } from "./question.js";
12
12
  import { GIT_API_KINDS, isGitApiKind } from "./git-api-kind.js";
13
- import { CONFIG_PROVIDER_WORDS, RETENTION_MODES, MEMORY_CONSOLIDATION_MIN_INTERVAL_SEC, RESILIENCE_DEFAULTS, TASK_WRITE_FACE_ESCALATIONS, MAX_TIMER_DELAY_MS, AUTO_MODE_CLASSIFIER_TIMEOUT_BOUNDS, DRAIN_GRACE_FLOOR_MS, DRAIN_SECOND_SIGNAL_MIN_GAP_BOUNDS, DRAIN_SECOND_SIGNAL_MIN_GAP_DEFAULT_MS, FIRST_BYTE_TIMEOUT_BOUNDS } from "./config-types.js";
13
+ import { CONFIG_PROVIDER_WORDS, RETENTION_MODES, MEMORY_CONSOLIDATION_MIN_INTERVAL_SEC, RESILIENCE_DEFAULTS, TASK_WRITE_FACE_ESCALATIONS, MAX_TIMER_DELAY_MS, AUTO_MODE_CLASSIFIER_TIMEOUT_BOUNDS, DRAIN_GRACE_FLOOR_MS, DRAIN_SECOND_SIGNAL_MIN_GAP_BOUNDS, DRAIN_SECOND_SIGNAL_MIN_GAP_DEFAULT_MS, FIRST_BYTE_TIMEOUT_BOUNDS, FLEET_LEASE_OUTAGE_MAX_DEFAULT_MS, fleetScopeOf } from "./config-types.js";
14
+ import { FLEET_GENERATION_ENV, FLEET_SCOPE_ENV, SERVING_LEASE_TTL_FLOOR_MS, isCanonicalGeneration } from "@sema-agent/settings-schema/fleet";
15
+ import { ScopeId } from "@sema-agent/settings-schema/api/scopes";
16
+ import { centerLaneConfigured, pathBaseUrlRefusal } from "./config-center/center-request.js";
14
17
  import { resolveLocalDataRoot } from "./local-data-root.js";
15
18
  import { buildWriteProtectionPosture } from "./write-protection.js";
16
19
  import { normalizeSshFingerprint } from "./ssh-host-key.js";
@@ -30,6 +33,7 @@ import { createCenterCredentialSource, readCenterCredentialDeclaration } from ".
30
33
  import { parseCenterCredentialRenewLeadMs } from "./center-credential-renewer.js";
31
34
  import { modelProviderRefusal } from "./model-provider.js";
32
35
  import { modelRouteUrlRefusal } from "./model-route-url.js";
36
+ import { buildHttpUrlRefusal, httpUrlFlaws } from "./url-shape.js";
33
37
  import { modelRouteEndpointRefusal } from "./model-route-endpoint.js";
34
38
  import { displayListEntry } from "./list-entry-display.js";
35
39
  import { buildDisplaySafeUrl, redactSecrets } from "./trace/redact.js";
@@ -488,7 +492,6 @@ function boolEnv(name, def) {
488
492
  registerKnob({ name, polarity: def ? "opt-out" : "opt-in", value, source });
489
493
  return value;
490
494
  }
491
- export const configLkgEnabled = () => boolEnv("CONFIG_LKG_ENABLED", true);
492
495
  export const hostBackgroundShellEnabled = () => boolEnv("HOST_BG_ENABLED", true);
493
496
  export const hostExecSpoolEnabled = () => boolEnv("HOST_EXEC_SPOOL_ENABLED", true);
494
497
  function parseReadDenyPatternsEnv(raw) {
@@ -1225,9 +1228,7 @@ function uncompilableRuleGuidance(name, behavior, pathBases) {
1225
1228
  const issue = issues[0];
1226
1229
  if (issue === undefined)
1227
1230
  return undefined;
1228
- return (`${issue.code} — core's rule grammar cannot compile this name (${issue.message}), so a table keyed by it matches nothing. ` +
1229
- `Tool names carry no backslashes and are far shorter than the 512-character rule cap; check the spelling, and use ` +
1230
- `mcp__<server>__* / a2a__<peer>__* for "every tool on that peer"`);
1231
+ return `${issue.code} — core's rule grammar cannot compile this entry: ${issue.message}`;
1231
1232
  }
1232
1233
  function findUnmatchableToolNames(names, opts) {
1233
1234
  const out = [];
@@ -1562,16 +1563,11 @@ function parseMemoryEmbedder(ctx) {
1562
1563
  if (endpoint === undefined || model === undefined || dimRaw === undefined) {
1563
1564
  throw new Error(`MEMORY_EMBEDDER_* is half-configured — missing ${missing.join(", ")}. The memory vector plane needs all three of MEMORY_EMBEDDER_ENDPOINT, MEMORY_EMBEDDER_MODEL, MEMORY_EMBEDDER_DIM; refusing to start rather than silently running lexical-only retrieval while the deployment believes vectors are on (#228).`);
1564
1565
  }
1565
- if (!URL.canParse(endpoint)) {
1566
- throw new Error(`MEMORY_EMBEDDER_ENDPOINT="${buildDisplaySafeUrl(endpoint)}" is not a valid URL (expected e.g. https://embeddings.example.com/v1 — the OpenAI-compatible base, or the full .../embeddings endpoint)`);
1567
- }
1568
- const parsedEndpoint = new URL(endpoint);
1569
- const proto = parsedEndpoint.protocol;
1570
- if (proto !== "http:" && proto !== "https:") {
1571
- throw new Error(`MEMORY_EMBEDDER_ENDPOINT="${buildDisplaySafeUrl(endpoint)}" must be http(s) (got "${proto}") — the embedder leg is an HTTP POST to an OpenAI-compatible /embeddings face`);
1572
- }
1573
- if (parsedEndpoint.username !== "" || parsedEndpoint.password !== "") {
1574
- throw new Error("MEMORY_EMBEDDER_ENDPOINT must not carry credentials in the URL (user:password@host) — URL userinfo leaks into transport errors, logs and memory conflict reports. Use MEMORY_EMBEDDER_API_KEY (sent as a Bearer header) instead.");
1566
+ const endpointFlaws = httpUrlFlaws(endpoint, ["userinfo"]);
1567
+ if (endpointFlaws.length > 0) {
1568
+ throw new Error(`${buildHttpUrlRefusal("MEMORY_EMBEDDER_ENDPOINT", endpoint, endpointFlaws)} — the embedder leg is an HTTP POST to an OpenAI-compatible /embeddings face ` +
1569
+ `(expected e.g. https://embeddings.example.com/v1 — the OpenAI-compatible base, or the full .../embeddings endpoint); ` +
1570
+ `credentials in the URL leak into transport errors, logs and memory conflict reports, so use MEMORY_EMBEDDER_API_KEY (sent as a Bearer header) instead.`);
1575
1571
  }
1576
1572
  const dimensions = Number(dimRaw);
1577
1573
  if (!Number.isSafeInteger(dimensions) || dimensions < 1) {
@@ -1853,7 +1849,6 @@ function parseOrchestrationDomain(ctx) {
1853
1849
  const workflowSizeGuideline = workflowSizeGuidelineRaw;
1854
1850
  const usageWindows = parseUsageWindows(process.env.USAGE_WINDOWS);
1855
1851
  const bakeRunnerPrincipal = env("BAKE_RUNNER_PRINCIPAL", "system:bake-runner");
1856
- configLkgEnabled();
1857
1852
  hostBackgroundShellEnabled();
1858
1853
  hostExecSpoolEnabled();
1859
1854
  const lspHostEnabled = (() => {
@@ -1898,6 +1893,7 @@ function parseOrchestrationDomain(ctx) {
1898
1893
  })(),
1899
1894
  ...((dir) => (dir !== undefined ? { agentConfigDir: dir } : {}))(declaredSemaConfigDir()),
1900
1895
  ...((p) => (p !== undefined ? { configLkgPath: p } : {}))(lkgPathOverrideOf(process.env.CONFIG_LKG_PATH)),
1896
+ configLkgEnabled: boolEnv("CONFIG_LKG_ENABLED", true),
1901
1897
  ...((h) => (h !== undefined ? { remoteExecHomeDir: h } : {}))((() => {
1902
1898
  const raw = process.env.REMOTE_EXEC_HOME_DIR;
1903
1899
  if (raw === undefined || raw === "")
@@ -2186,6 +2182,13 @@ function parseGitApiKind(raw, baseUrl) {
2186
2182
  }
2187
2183
  function parseIntegrationsDomain(ctx) {
2188
2184
  const { requirePrincipal, configProvider } = ctx;
2185
+ const registryUrl = parseCenterBaseUrl("SEMA_REGISTRY_URL", process.env.SEMA_REGISTRY_URL, `It is the center base every config pull, fleet and lease request is sent to; setting it selects the center as the config source.`);
2186
+ const registryLiveUrl = parseCenterBaseUrl("SEMA_REGISTRY_LIVE_URL", process.env.SEMA_REGISTRY_LIVE_URL, `It is the center base the fleet liveness endpoints (heartbeat, serving lease) are sent to; unset it to send them to SEMA_REGISTRY_URL.`);
2187
+ const registryWebUrl = parseCenterBaseUrl("SEMA_REGISTRY_WEB_URL", process.env.SEMA_REGISTRY_WEB_URL, `It is the center's web console base this engine reports on GET /health (center.webBaseUrl); unset it if the center has no web console to link to.`);
2188
+ const configCenter = registryUrl !== undefined ? parseConfigCenter(registryUrl, requirePrincipal, registryLiveUrl, registryWebUrl) : undefined;
2189
+ const fleetGeneration = parseFleetGeneration(process.env.FLEET_GENERATION);
2190
+ const fleetAdvertiseAddress = parseFleetAdvertiseAddress(process.env.FLEET_ADVERTISE_ADDRESS);
2191
+ fleetFenceLaneOf({ ...fleetGeneration, configCenter, ...fleetAdvertiseAddress });
2189
2192
  return {
2190
2193
  pluginsAllowHosts: (process.env.PLUGINS_ALLOW_HOSTS ?? "github.com").split(",").map((h) => h.trim()).filter(Boolean),
2191
2194
  configBootFetchBudgetMs: clampEnvWithWarn("CONFIG_BOOT_FETCH_BUDGET_MS", "1500", 0),
@@ -2195,24 +2198,108 @@ function parseIntegrationsDomain(ctx) {
2195
2198
  defaultScenario: env("DEFAULT_SCENARIO", "code"),
2196
2199
  skillsDir: env("SKILLS_DIR", defaultSkillsDir()),
2197
2200
  centerCredentialRenewLeadMs: parseCenterCredentialRenewLeadMs(process.env.CENTER_CREDENTIAL_RENEW_LEAD_MS),
2198
- configCenter: process.env.SEMA_REGISTRY_URL ? parseConfigCenter(env("SEMA_REGISTRY_URL"), requirePrincipal) : undefined,
2201
+ configCenter,
2199
2202
  fleetLeaseFailClosed: parseFleetLeaseFailClosed(process.env.FLEET_LEASE_FAIL_CLOSED, requirePrincipal),
2203
+ ...fleetGeneration,
2204
+ fleetScope: parseFleetScope(process.env.FLEET_SCOPE),
2205
+ ...fleetAdvertiseAddress,
2206
+ fleetLeaseOutageMaxMs: parseFleetLeaseOutageMaxMs(process.env.FLEET_LEASE_OUTAGE_MAX_MS),
2200
2207
  ...(configProvider !== undefined ? { configProvider } : {}),
2201
2208
  configLocalDir: process.env.CONFIG_LOCAL_DIR || undefined,
2202
2209
  a2aServe: parseA2aServe(),
2203
2210
  };
2204
2211
  }
2212
+ function parseFleetGeneration(raw) {
2213
+ const name = "FLEET_GENERATION";
2214
+ if (raw === undefined)
2215
+ return {};
2216
+ const shown = raw.length > 40 ? `${raw.slice(0, 40)}…(${raw.length} chars)` : raw;
2217
+ if (!isCanonicalGeneration(raw)) {
2218
+ throw fleetEnvRefusal("generation_invalid", `env ${name}=${JSON.stringify(shown)} is not a generation: it must be a canonical non-negative decimal string ("0", "7", "10" — no leading zero, ` +
2219
+ `sign, whitespace or fraction, and not empty). The config renderer sets it on fleet members; unset it entirely for a standalone engine. ` +
2220
+ `Refusing to boot rather than running outside the serving-lease fence.`);
2221
+ }
2222
+ return { fleetGeneration: raw };
2223
+ }
2224
+ export const FLEET_ENV_REFUSAL_CODES = {
2225
+ generation_invalid: "config.fleet_generation_invalid",
2226
+ scope_invalid: "config.fleet_scope_invalid",
2227
+ advertise_address_invalid: "config.fleet_advertise_address_invalid",
2228
+ member_unfenced: "config.fleet_member_unfenced",
2229
+ };
2230
+ function fleetEnvRefusal(rule, message) {
2231
+ return Object.assign(new Error(message), { code: FLEET_ENV_REFUSAL_CODES[rule] });
2232
+ }
2233
+ function parseFleetScope(raw) {
2234
+ const name = "FLEET_SCOPE";
2235
+ if (raw !== undefined && !ScopeId.safeParse(raw).success) {
2236
+ const shown = raw.length > 40 ? `${raw.slice(0, 40)}…(${raw.length} chars)` : raw;
2237
+ throw fleetEnvRefusal("scope_invalid", `env ${name}=${JSON.stringify(shown)} is not a scope id: it must be a lowercase slug of letters, digits and inner dashes, 1–64 characters, ` +
2238
+ `not empty. The config renderer sets it on engines that serve a tenant scope; unset it for the default scope ("${fleetScopeOf(undefined)}"). ` +
2239
+ `Refusing to boot rather than answering drain requests under the wrong identity.`);
2240
+ }
2241
+ return fleetScopeOf(raw);
2242
+ }
2243
+ function parseFleetAdvertiseAddress(raw) {
2244
+ if (raw === undefined || raw === "")
2245
+ return {};
2246
+ const refusal = pathBaseUrlRefusal("env FLEET_ADVERTISE_ADDRESS", raw);
2247
+ if (refusal !== undefined) {
2248
+ throw fleetEnvRefusal("advertise_address_invalid", `${refusal} — it must be an http(s) base URL such as "http://10.0.0.5:8090": the fleet center rejects any other shape on every announce. ` +
2249
+ `Unset it if this engine does not announce itself.`);
2250
+ }
2251
+ return { fleetAdvertiseAddress: raw };
2252
+ }
2253
+ export function fleetFenceLaneOf(cfg) {
2254
+ const generation = cfg.fleetGeneration;
2255
+ if (generation === undefined)
2256
+ return undefined;
2257
+ const laneConfigured = centerLaneConfigured(cfg);
2258
+ if (laneConfigured && cfg.fleetAdvertiseAddress !== undefined)
2259
+ return { center: cfg.configCenter, generation };
2260
+ const missing = [
2261
+ ...(laneConfigured ? [] : [`the center lane (SEMA_REGISTRY_URL + ${CENTER_CREDENTIAL_ENV.token} or ${CENTER_CREDENTIAL_ENV.tokenFile} + SEMA_REGISTRY_WORKER)`]),
2262
+ ...(cfg.fleetAdvertiseAddress === undefined ? ["the announce address (FLEET_ADVERTISE_ADDRESS)"] : []),
2263
+ ];
2264
+ throw fleetEnvRefusal("member_unfenced", `fleet member cannot be fenced — env FLEET_GENERATION=${JSON.stringify(generation)} makes this engine a fleet member, ` +
2265
+ `but ${missing.join(" and ")} ${missing.length === 1 ? "is" : "are"} not configured. A member renews its serving lease with the center and hears whether ` +
2266
+ `the center enforces the lease only in the heartbeat reply (the heartbeat needs the announce address); without them it would serve outside the fence. ` +
2267
+ `Configure them, or unset FLEET_GENERATION for a standalone engine.`);
2268
+ }
2269
+ function parseFleetLeaseOutageMaxMs(raw) {
2270
+ if (raw === undefined || raw === "")
2271
+ return FLEET_LEASE_OUTAGE_MAX_DEFAULT_MS;
2272
+ const n = /^(0|[1-9]\d*)$/.test(raw) ? Number(raw) : Number.NaN;
2273
+ if (!Number.isSafeInteger(n) || n < SERVING_LEASE_TTL_FLOOR_MS) {
2274
+ const shown = raw.length > 40 ? `${raw.slice(0, 40)}…(${raw.length} chars)` : raw;
2275
+ throw new Error(`env FLEET_LEASE_OUTAGE_MAX_MS=${JSON.stringify(shown)} must be a whole number of milliseconds ≥ ${SERVING_LEASE_TTL_FLOOR_MS} (the serving-lease TTL floor; ` +
2276
+ `decimal digits only; unset = ${FLEET_LEASE_OUTAGE_MAX_DEFAULT_MS}, 24 h). It bounds how long this engine keeps taking new submissions after its serving ` +
2277
+ `lease expired with the center unreachable, and how far from now a drain request's deadlineAt may be — "0" and "infinite" are not values ` +
2278
+ `(the bound must be finite and non-trivial). Refusing to boot.`);
2279
+ }
2280
+ return n;
2281
+ }
2205
2282
  function parseFleetLeaseFailClosed(raw, requirePrincipal) {
2206
2283
  const explicit = parseBoolWord("FLEET_LEASE_FAIL_CLOSED", raw);
2207
2284
  const value = explicit ?? requirePrincipal;
2208
2285
  registerKnob({ name: "FLEET_LEASE_FAIL_CLOSED", polarity: "posture", value, source: explicit !== undefined ? "env" : requirePrincipal ? "posture" : "default" });
2209
2286
  return value;
2210
2287
  }
2211
- function parseConfigCenter(baseUrl, requirePrincipal) {
2288
+ function parseCenterBaseUrl(name, raw, role) {
2289
+ if (raw === undefined || raw === "")
2290
+ return undefined;
2291
+ const refusal = pathBaseUrlRefusal(`env ${name}`, raw);
2292
+ if (refusal !== undefined)
2293
+ throw new Error(`${refusal}. ${role}`);
2294
+ return raw;
2295
+ }
2296
+ function parseConfigCenter(baseUrl, requirePrincipal, liveBaseUrl, webBaseUrl) {
2212
2297
  const worker = process.env.SEMA_REGISTRY_WORKER || undefined;
2213
2298
  const declared = readCenterCredentialDeclaration();
2214
2299
  return {
2215
2300
  baseUrl,
2301
+ ...(liveBaseUrl !== undefined ? { liveBaseUrl } : {}),
2302
+ ...(webBaseUrl !== undefined ? { webBaseUrl } : {}),
2216
2303
  credential: createCenterCredentialSource(declared.from === "file" ? { ...declared, multiTenant: requirePrincipal } : declared, worker),
2217
2304
  dryRun: boolEnv("SEMA_REGISTRY_DRY_RUN", false),
2218
2305
  worker,
@@ -2230,20 +2317,13 @@ function parseA2aServe() {
2230
2317
  `and that address is the operator's to declare (it is NOT derived from the request Host header: an anonymous discovery caller must not decide what our public card advertises). ` +
2231
2318
  `Set A2A_SERVE_URL to this deployment's externally reachable /v1/a2a URL (e.g. "https://agent.example.com/v1/a2a").`);
2232
2319
  }
2233
- let parsed;
2234
- try {
2235
- parsed = new URL(rawUrl);
2236
- }
2237
- catch {
2238
- parsed = undefined;
2239
- }
2240
- if (parsed === undefined || (parsed.protocol !== "http:" && parsed.protocol !== "https:")) {
2241
- throw new Error(`A2A_SERVE_URL must be a parseable http(s) URL (e.g. "https://agent.example.com/v1/a2a"); got ${JSON.stringify(buildDisplaySafeUrl(rawUrl))}`);
2242
- }
2243
- if (parsed.username !== "" || parsed.password !== "") {
2244
- throw new Error(`A2A_SERVE_URL must not carry userinfo (credentials before "@") — it is published verbatim on the ANONYMOUSLY readable agent card, so those credentials would be handed to every discovery caller. ` +
2245
- `Inbound authentication on this endpoint is header-shaped (a service credential), not URL-embedded; strip the userinfo and keep the secret in the caller's headers.`);
2320
+ const urlFlaws = httpUrlFlaws(rawUrl, ["userinfo"]);
2321
+ if (urlFlaws.length > 0) {
2322
+ throw new Error(`${buildHttpUrlRefusal("A2A_SERVE_URL", rawUrl, urlFlaws)} — it is published verbatim on the ANONYMOUSLY readable agent card as this deployment's JSON-RPC endpoint ` +
2323
+ `(e.g. "https://agent.example.com/v1/a2a"), so it must be a plain http(s) address and must not hand credentials to every discovery caller: ` +
2324
+ `inbound authentication on this endpoint is header-shaped (a service credential), not URL-embedded.`);
2246
2325
  }
2326
+ const parsed = new URL(rawUrl);
2247
2327
  const a2aCredentialScan = redactSecrets(parsed.href);
2248
2328
  if (a2aCredentialScan !== parsed.href) {
2249
2329
  throw new Error(`A2A_SERVE_URL carries something the credential-redaction law recognizes (${a2aCredentialScan}) — this address is published VERBATIM on the ANONYMOUSLY readable agent card (the card's url is a functional address: peers fetch it, so it is never rewritten for display), ` +
@@ -2336,7 +2416,7 @@ const ORCHESTRATION_GROUP_KEYS = [
2336
2416
  "backgroundAgentParkClaimStaleMs", "delegationEntryCaps", "rosterRetentionMs", "leaderRunStaleMs", "scratchpadSweepTtlMs", "sandboxPkgSource",
2337
2417
  "sessionAutoTitle", "sessionAutoTitleSource", "selectEnvironmentTool", "envFactsEnabled", "toolDeferLongtail", "lspEnabled", "lspHostEnabled", "lspEnvAllow",
2338
2418
  "imageBakes", "readFace", "readFaceSource", "taskWriteFaceEscalation", "readDenyPatterns", "readDenyBuiltinTiers", "readDenyBuiltinExclude",
2339
- "agentConfigDir", "configLkgPath",
2419
+ "agentConfigDir", "configLkgPath", "configLkgEnabled",
2340
2420
  ];
2341
2421
  const LIMITS_HTTP_GROUP_KEYS = [
2342
2422
  "port", "attachmentOrphanGraceMs", "workspaceFileMaxBytes", "attachmentMaxBytes", "attachmentMimeAllowlist",
@@ -2348,7 +2428,7 @@ const LIMITS_HTTP_GROUP_KEYS = [
2348
2428
  const OBSERVABILITY_GROUP_KEYS = ["metricsToken", "traceToken", "toolTrace", "traceThinking", "logLevel", "otel"];
2349
2429
  const INTEGRATIONS_GROUP_KEYS = [
2350
2430
  "pluginsAllowHosts", "mcpServers", "a2aPeers", "a2aServe", "configBootFetchBudgetMs", "gitApiBaseUrl", "gitApiToken", "gitApiKind", "defaultScenario",
2351
- "skillsDir", "centerCredentialRenewLeadMs", "configCenter", "fleetLeaseFailClosed", "configProvider", "configLocalDir",
2431
+ "skillsDir", "centerCredentialRenewLeadMs", "configCenter", "fleetLeaseFailClosed", "fleetGeneration", "fleetScope", "fleetAdvertiseAddress", "fleetLeaseOutageMaxMs", "configProvider", "configLocalDir",
2352
2432
  ];
2353
2433
  export const CONFIG_GROUP_KEYS = {
2354
2434
  store: STORE_GROUP_KEYS,
@@ -0,0 +1,112 @@
1
+ /**
2
+ * 排空的**一只入口**(编排器长远稿片 2 件 2;契约型 = settings-schema 8.1.0 `DrainRequest` / `DrainReceipt` / `EngineDraining`)。
3
+ *
4
+ * ## 规则
5
+ *
6
+ * 1. **一只入口,两条车道**:进入排空只有 `boot/shutdown.ts` 的 `enterDrain`(装在 {@link DrainState.enter} 上)一处。Docker 车道 / 中心直调
7
+ * 经 `POST /v1/admin/drain`(体 = `DrainRequest`);Kubernetes 车道经服务租约被拒(`fleet/serving-lease.ts`,`actionId = lease:<代际>`);
8
+ * 非编排路径 = 停止信号(SIGTERM)/ SIGHUP 升级 / 附着租约自退,动作号由引擎自铸({@link ENGINE_DRAIN_CAUSES})。全部汇到同一只入口。
9
+ * 2. **期限在进入那一拍定死**:请求带的 `deadlineAt` 原样;引擎自起的排空 = 起点 + 此刻的排空窗(`drainGraceMs`)。之后改窗(中心热改
10
+ * `limits.drainGraceMs`)不动在跑的期限 —— 排空驱动读的是期限,不是窗。
11
+ * 请求带的期限有**准入上界** = 进入那一拍 + `config.fleetLeaseOutageMaxMs`(`T_max`,env `FLEET_LEASE_OUTAGE_MAX_MS`,`loadConfig` 无条件
12
+ * 解析、缺省 24 h,成员与非成员同读):一台实例「活着而不服务」最长多久本就由这一席定义(服务租约失联后最多再接单多久,
13
+ * `fleet/serving-lease.ts`),排空口不另立第二个数。上界由调用方随入参声明({@link DrainEntry.deadlineBoundMs};排空口声明它,引擎自起的
14
+ * 排空不声明 —— 那些期限由引擎自己的窗定,拒了就是不排空),**由入口判**:只有这一发会进入排空时才判(已在排空 / 硬停已在进行 ⇒ 期限
15
+ * 不会被采用,照规则 3 回执);越界 ⇒ 零状态变化,入口答 {@link DrainDeadlineRefusal}(排空口 400 `request.field_invalid`)。「会不会
16
+ * 采用这只期限」只有入口一处判 —— 调用方不另读 `draining` 去猜。已过的期限不越界(进入即到期:在飞为零当拍硬停,否则下一拍)。
17
+ * 3. **幂等**:已在排空 ⇒ 不重置,回执 `already_draining` 带**第一次**的动作 / 起点 / 期限(不论这次的动作号是否相同)。
18
+ * 4. **身份**:逐字段比,任一不符 ⇒ 409 `conflict.fleet_identity`,零状态变化。
19
+ * · `scope`:两侧都经 {@link fleetScopeOf} 读成值(缺席 = settings-schema 的缺省 scope,线上任何一处缺席的 scope 都读成它)再严格相等。
20
+ * 引擎这一侧的唯一来源 = env `FLEET_SCOPE`(`config.fleetScope`,`config.ts` 解析一次);请求不带 `scope` = 点名缺省 scope。
21
+ * · `worker` / `expectedGeneration`:缺席对缺席(非 fleet 成员没有代际,不带 `expectedGeneration` 的请求是单机形,不是通配)。
22
+ * · `instanceId`:请求带了才比(它是引擎每进程自铸的身份,`/health` 上可读);「点名的是不是别的进程」与 run 行同一只判据
23
+ * `runProvenElsewhere`。
24
+ * 引擎自己的 worker / instanceId 读不出 ⇒ 带了这一项的请求不符(认不出自己就不认任何点名;没配 `SEMA_REGISTRY_WORKER` 的引擎因此
25
+ * 只能用停止信号排空)。
26
+ * 5. **进入那一拍让出在飞**:本副本可抢占的在飞腿经 core `TaskSpec.preemptSignal` 让出(在下一个干净的回合边界耐久停驻,
27
+ * `resource_limit` / `preempt`;不合格的腿上 core 静默不动,等到期限硬停 —— 既有 `crashed` 臂);活的审批卡按断连同路改路耐久停驻
28
+ * (协调器 `beginDrain`,core `onAsk` 答 `"unavailable"` 的合同);排空期间新登记的可抢占腿在登记那一拍让出(同一条规则)。
29
+ *
30
+ * 纯类型 + 纯函数叶(值 import 只有 scope 读法叶与 run 行判词叶)。
31
+ */
32
+ import type { DrainReceipt, DrainRequest, EngineDraining } from "@sema-agent/settings-schema/fleet";
33
+ /**
34
+ * 引擎自铸的动作号 —— **一张表**(排空事实与回执里不来自请求体的全部拼法;请求体的 `actionId` 由调用方自取):
35
+ * · {@link ENGINE_DRAIN_CAUSES}:没有请求承载的排空成因(停止信号 / SIGHUP 升级 / 附着租约自退);
36
+ * · {@link HARD_STOP_ACTION_ID}:硬停(未经排空)已在进行时排空口回执里的动作号 —— 不是一种排空成因,只说「这只进程已经在停,而且比你要的更快」;
37
+ * · `lease:<代际>`:服务租约被拒的排空({@link leaseDrainActionId},每代一只:同一代被拒两次是同一个动作)。
38
+ * 按成因穷举的地方({@link DRAIN_CAUSE_COUNTS_AS_FIRST_STOP_SIGNAL})加一种成因不补格 = 编译红。
39
+ */
40
+ export declare const ENGINE_DRAIN_CAUSES: readonly ["signal:SIGTERM", "signal:SIGHUP", "self-exit:attachment"];
41
+ export type EngineDrainCause = (typeof ENGINE_DRAIN_CAUSES)[number];
42
+ export declare const HARD_STOP_ACTION_ID = "hard-stop";
43
+ export type LeaseDrainActionId = `lease:${string}`;
44
+ /** 服务租约被拒时的动作号(每代一只)。 */
45
+ export declare function leaseDrainActionId(generation: string): LeaseDrainActionId;
46
+ /**
47
+ * 每种排空成因的**入口那一拍算不算本次排空的第一发停止信号**(`boot/shutdown.ts` 的「第二发 = 硬停」读它;它答的不是「这是不是 `StopSignal`」——
48
+ * `shutdown.ts` 的 `StopSignal` 闭集只有 SIGTERM / SIGINT,那是「哪些信号走停止处理器」)。算 ⇒ 之后隔 ≥ 最小间隔的一发即硬停;不算 ⇒ 期限由入口定死,
49
+ * 之后到达的第一发停止信号只被吸收(不缩短期限)。SIGHUP 算:SIGHUP 升级成排空要的就是**第二发** SIGHUP(第一发只开空闲窗),那是人 / 宿主又发了一次
50
+ * 停的意图,与 SIGTERM 进的排空同一个读法(7.120.0 起的行为)。闭集 Record。
51
+ */
52
+ export declare const DRAIN_CAUSE_COUNTS_AS_FIRST_STOP_SIGNAL: {
53
+ readonly [C in EngineDrainCause]: boolean;
54
+ };
55
+ /** 入口的入参:动作号 + 期限(epoch ms)+ 调用方声明的期限准入上界(规则 2)。 */
56
+ export interface DrainEntry {
57
+ readonly actionId: string;
58
+ readonly deadlineAt: number;
59
+ /** 期限的准入上界(ms):这一发会进入排空而 `deadlineAt − 进入那一拍 > 它` ⇒ 入口零状态变化,答 {@link DrainDeadlineRefusal}。
60
+ * 排空口传 `config.fleetLeaseOutageMaxMs`;引擎自起的排空(停止信号 / 租约被拒 / 附着租约自退)不传。 */
61
+ readonly deadlineBoundMs?: number;
62
+ }
63
+ /** 入口拒了这只期限(规则 2):越过调用方声明的上界,什么都没变。 */
64
+ export interface DrainDeadlineRefusal {
65
+ readonly state: "deadline_beyond_bound";
66
+ readonly boundMs: number;
67
+ }
68
+ /** 入口的答:契约回执(进入 / 已在排空),或期限越界的拒(只在调用方声明了上界时出现)。按 `state` 穷举。 */
69
+ export type DrainEntryAnswer = DrainReceipt | DrainDeadlineRefusal;
70
+ /**
71
+ * 引擎与 HTTP 面共享的排空盒子(装配层建一只,`createHttpServer` 与收尾链各自往上装读口 / 入口)。
72
+ * · `draining` / `since` / `actionId` / `deadlineAt` —— **只由** {@link enter} 整组写入(排空事实 = 契约 `EngineDraining`);
73
+ * · `enter` —— 唯一入口(收尾链装;缺席 = 这个进程没有收尾链,如嵌入宿主 / 测试夹具);
74
+ * · `yieldInflight` —— 让出本副本在飞的可抢占腿(`createHttpServer` 装);
75
+ * · `endSubscriptions` —— 关掉本副本打开着的只读订阅流(发一帧 `draining` 终帧再关;`createHttpServer` 装,S-1281)。订阅流不是在飞的工作
76
+ * (不进 `inflight`),排空不等它;收尾链在排空进入那一拍与硬停起点各调一次(排空期间新开的订阅在硬停那一拍收);
77
+ * · 其余四只是既有的读口(在飞数 / 活性时钟 / 附着流),语义见 `http/server.ts`。`inflight` 只数**工作**(run 腿 / 计费提交 / HITL 应答 /
78
+ * admission 期的普通请求),只读订阅只进 `attachedStreams`(S-1281)。
79
+ */
80
+ export interface DrainState {
81
+ draining: boolean;
82
+ since?: number;
83
+ actionId?: string;
84
+ deadlineAt?: number;
85
+ enter?: (entry: DrainEntry) => DrainEntryAnswer;
86
+ yieldInflight?: () => void;
87
+ endSubscriptions?: () => void;
88
+ inflight?: () => number;
89
+ lastActivityAt?: () => number;
90
+ attachedStreams?: () => number;
91
+ lastAttachEndedAt?: () => number;
92
+ }
93
+ /** 排空盒子里的排空事实(三键齐才是;入口整组写,所以「缺一」只出现在手搭的夹具上)。 */
94
+ export declare function drainingFactOf(s: Pick<DrainState, "draining" | "since" | "actionId" | "deadlineAt">): EngineDraining | undefined;
95
+ /**
96
+ * 规则 2 的准入上界:期限离进入那一拍(本引擎的挂钟)超过 `boundMs` ⇔ 越界。恰好等于上界不越界;已过的期限不越界。入口判它
97
+ * (`boot/shutdown.ts` 的 enterDrain,在「已在排空 / 硬停已在进行」两臂之后)。
98
+ */
99
+ export declare function drainDeadlineBeyondBound(deadlineAt: number, now: number, boundMs: number): boolean;
100
+ /** 本进程的身份。`scope` 已读成值(`config.fleetScope`);其余三项与 `/health` 上读到的同一份。 */
101
+ export interface EngineIdentity {
102
+ readonly scope: string;
103
+ readonly worker?: string;
104
+ readonly generation?: string;
105
+ readonly instanceId?: string;
106
+ }
107
+ /** 身份四元的字段名(请求侧拼法;409 拒句按它点名,不回显值)。 */
108
+ export declare const DRAIN_IDENTITY_FIELDS: readonly ["scope", "worker", "expectedGeneration", "instanceId"];
109
+ export type DrainIdentityField = (typeof DRAIN_IDENTITY_FIELDS)[number];
110
+ /** 规则 4:不符的字段(按 {@link DRAIN_IDENTITY_FIELDS} 序;空 ⇔ 认得)。 */
111
+ export declare function drainIdentityMismatches(req: Pick<DrainRequest, "scope" | "worker" | "expectedGeneration" | "instanceId">, self: EngineIdentity): DrainIdentityField[];
112
+ //# sourceMappingURL=drain-entry.d.ts.map
@@ -0,0 +1,38 @@
1
+ import { fleetScopeOf } from "../config-types.js";
2
+ import { runProvenElsewhere } from "../plugins/store-contracts.js";
3
+ export const ENGINE_DRAIN_CAUSES = ["signal:SIGTERM", "signal:SIGHUP", "self-exit:attachment"];
4
+ export const HARD_STOP_ACTION_ID = "hard-stop";
5
+ export function leaseDrainActionId(generation) {
6
+ return `lease:${generation}`;
7
+ }
8
+ export const DRAIN_CAUSE_COUNTS_AS_FIRST_STOP_SIGNAL = {
9
+ "signal:SIGTERM": true,
10
+ "signal:SIGHUP": true,
11
+ "self-exit:attachment": false,
12
+ };
13
+ export function drainingFactOf(s) {
14
+ return s.draining && s.actionId !== undefined && s.since !== undefined && s.deadlineAt !== undefined ? { actionId: s.actionId, since: s.since, deadlineAt: s.deadlineAt } : undefined;
15
+ }
16
+ export function drainDeadlineBeyondBound(deadlineAt, now, boundMs) {
17
+ return deadlineAt - now > boundMs;
18
+ }
19
+ export const DRAIN_IDENTITY_FIELDS = ["scope", "worker", "expectedGeneration", "instanceId"];
20
+ export function drainIdentityMismatches(req, self) {
21
+ return DRAIN_IDENTITY_FIELDS.filter((f) => {
22
+ switch (f) {
23
+ case "scope":
24
+ return fleetScopeOf(req.scope) !== self.scope;
25
+ case "worker":
26
+ return self.worker === undefined || req.worker !== self.worker;
27
+ case "expectedGeneration":
28
+ return req.expectedGeneration !== self.generation;
29
+ case "instanceId":
30
+ return req.instanceId !== undefined && (self.instanceId === undefined || runProvenElsewhere({ instanceId: req.instanceId }, self.instanceId));
31
+ default: {
32
+ const unhandled = f;
33
+ throw new Error(`drain identity: unknown field ${String(unhandled)}`);
34
+ }
35
+ }
36
+ });
37
+ }
38
+ //# sourceMappingURL=drain-entry.js.map