@sema-agent/server 7.121.0 → 7.123.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 (136) hide show
  1. package/MIGRATION.md +175 -0
  2. package/README.md +6 -1
  3. package/README.zh-CN.md +6 -1
  4. package/USAGE.md +57 -18
  5. package/dist/approval-reconciler.d.ts +8 -0
  6. package/dist/approval-reconciler.js +72 -52
  7. package/dist/audit.d.ts +1 -1
  8. package/dist/audit.js +52 -22
  9. package/dist/auto-mode-face.d.ts +16 -5
  10. package/dist/auto-mode-face.js +7 -4
  11. package/dist/bench/s1/live-deps.js +1 -0
  12. package/dist/boot/config-center.js +3 -0
  13. package/dist/boot/leader.js +1 -0
  14. package/dist/boot/reapers.d.ts +3 -0
  15. package/dist/boot/reapers.js +2 -1
  16. package/dist/boot/resolve-spec.js +1 -4
  17. package/dist/boot/shutdown.js +6 -1
  18. package/dist/boot/stage-08-reapers.js +2 -2
  19. package/dist/capabilities/memory-notice.d.ts +1 -1
  20. package/dist/config-catalog.js +4 -4
  21. package/dist/config-center/apply-effective.d.ts +1 -1
  22. package/dist/config-center/apply-effective.js +5 -3
  23. package/dist/config-center/center-request.d.ts +6 -9
  24. package/dist/config-center/center-request.js +11 -27
  25. package/dist/config-center/http-client.js +3 -3
  26. package/dist/config-center/read-warnings.d.ts +56 -35
  27. package/dist/config-center/read-warnings.js +67 -19
  28. package/dist/config-center/types.d.ts +7 -8
  29. package/dist/config-lkg.d.ts +12 -9
  30. package/dist/config-lkg.js +38 -39
  31. package/dist/config-provider.d.ts +3 -2
  32. package/dist/config-provider.js +2 -2
  33. package/dist/config-types.d.ts +20 -9
  34. package/dist/config-types.js +1 -1
  35. package/dist/config.d.ts +10 -10
  36. package/dist/config.js +92 -127
  37. package/dist/fleet/drain-entry.d.ts +28 -3
  38. package/dist/fleet/drain-entry.js +3 -0
  39. package/dist/fleet/engine-signal.d.ts +54 -23
  40. package/dist/fleet/engine-signal.js +29 -5
  41. package/dist/fleet/serving-lease.js +13 -15
  42. package/dist/fleet-client.d.ts +8 -6
  43. package/dist/fleet-client.js +4 -3
  44. package/dist/governance-overrides.d.ts +7 -2
  45. package/dist/http/admission.js +7 -5
  46. package/dist/http/approval-sweeps.js +2 -2
  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 +22 -19
  50. package/dist/http/health-self-report.d.ts +3 -0
  51. package/dist/http/resume-legs.js +8 -8
  52. package/dist/http/route-ctx.d.ts +19 -3
  53. package/dist/http/route-table.d.ts +3 -0
  54. package/dist/http/route-table.js +4 -0
  55. package/dist/http/routes/admin-drain.d.ts +5 -2
  56. package/dist/http/routes/admin-drain.js +14 -2
  57. package/dist/http/routes/approvals-assistant.d.ts +1 -0
  58. package/dist/http/routes/approvals-assistant.js +6 -6
  59. package/dist/http/routes/capabilities.js +7 -5
  60. package/dist/http/routes/fleet.d.ts +1 -0
  61. package/dist/http/routes/fleet.js +1 -1
  62. package/dist/http/routes/images.d.ts +1 -0
  63. package/dist/http/routes/images.js +1 -1
  64. package/dist/http/routes/leader.js +6 -0
  65. package/dist/http/routes/runs.d.ts +2 -0
  66. package/dist/http/routes/runs.js +5 -4
  67. package/dist/http/routes/sessions.d.ts +1 -0
  68. package/dist/http/routes/sessions.js +2 -5
  69. package/dist/http/routes/trace-usage.d.ts +1 -0
  70. package/dist/http/routes/trace-usage.js +1 -1
  71. package/dist/http/routes/workflows.d.ts +1 -0
  72. package/dist/http/routes/workflows.js +1 -1
  73. package/dist/http/send.d.ts +13 -0
  74. package/dist/http/send.js +9 -0
  75. package/dist/http/server.d.ts +3 -0
  76. package/dist/http/stored-body-replay.d.ts +28 -0
  77. package/dist/http/stored-body-replay.js +22 -0
  78. package/dist/index.d.ts +2 -0
  79. package/dist/index.js +1 -0
  80. package/dist/leader/leader.d.ts +23 -1
  81. package/dist/leader/leader.js +6 -1
  82. package/dist/leader/repair-wire.d.ts +1 -1
  83. package/dist/leader/repair-wire.js +2 -2
  84. package/dist/leader/run-entry.d.ts +27 -0
  85. package/dist/leader/run-entry.js +29 -0
  86. package/dist/leader/wire.d.ts +10 -0
  87. package/dist/leader/wire.js +15 -10
  88. package/dist/model-entry-refusal.d.ts +18 -9
  89. package/dist/model-entry-refusal.js +19 -14
  90. package/dist/model-route-endpoint.d.ts +21 -1
  91. package/dist/model-route-endpoint.js +9 -3
  92. package/dist/model-route-gap.d.ts +103 -0
  93. package/dist/model-route-gap.js +80 -0
  94. package/dist/model-route-url.d.ts +13 -20
  95. package/dist/model-route-url.js +28 -24
  96. package/dist/namespaced-rule-name.d.ts +16 -30
  97. package/dist/namespaced-rule-name.js +2 -20
  98. package/dist/observability/fail-open.d.ts +23 -10
  99. package/dist/observability/fail-open.js +8 -8
  100. package/dist/plugins/approval-ask-store-file.d.ts +1 -0
  101. package/dist/plugins/approval-ask-store-file.js +3 -0
  102. package/dist/plugins/approval-ask-store-memory.d.ts +1 -0
  103. package/dist/plugins/approval-ask-store-memory.js +6 -0
  104. package/dist/plugins/approval-ask-store-sql.d.ts +12 -0
  105. package/dist/plugins/approval-ask-store-sql.js +4 -0
  106. package/dist/plugins/one-time-migrations.js +40 -0
  107. package/dist/plugins/park-commit-watch.d.ts +19 -5
  108. package/dist/plugins/park-commit-watch.js +24 -17
  109. package/dist/plugins/session-policy-store-sql.js +1 -2
  110. package/dist/plugins/workflow-run-store-sql.d.ts +4 -2
  111. package/dist/router/route-orchestration.d.ts +4 -2
  112. package/dist/router/route-orchestration.js +2 -1
  113. package/dist/rule-entry-list.d.ts +3 -13
  114. package/dist/rule-entry-list.js +1 -12
  115. package/dist/run-local.js +4 -2
  116. package/dist/runtime-governance.d.ts +67 -32
  117. package/dist/runtime-governance.js +28 -9
  118. package/dist/session-policy-wire.d.ts +47 -49
  119. package/dist/session-policy-wire.js +41 -36
  120. package/dist/session-sync.d.ts +5 -1
  121. package/dist/session-sync.js +2 -1
  122. package/dist/sighup-idle.d.ts +7 -1
  123. package/dist/sighup-idle.js +4 -4
  124. package/dist/spec-fields.d.ts +4 -4
  125. package/dist/spec-fields.js +1 -4
  126. package/dist/task-settings.d.ts +36 -14
  127. package/dist/task-settings.js +20 -23
  128. package/dist/tool-approval.d.ts +6 -0
  129. package/dist/tool-approval.js +13 -1
  130. package/dist/trace/redact.d.ts +3 -3
  131. package/dist/unattended-permission-mode.d.ts +17 -13
  132. package/dist/unattended-permission-mode.js +3 -1
  133. package/dist/url-shape.d.ts +31 -0
  134. package/dist/url-shape.js +41 -0
  135. package/package.json +3 -3
  136. package/scripts/drain-check-local.mjs +1 -1
@@ -74,7 +74,7 @@ export const CONFIG_CATALOG = [
74
74
  r("AUTH_BRIDGE_AUDIENCE", "auth", "string", "registry SSO-JWT 验签期望 aud", { danger: SEC }),
75
75
  r("AUTH_BRIDGE_ISSUER", "auth", "url", "registry SSO-JWT issuer(auth bridge 的开关之一;S-671 / C-R100 ① 起**不是**托管证据 —— 多租户只认 REQUIRE_PRINCIPAL=true)", { danger: SEC, derivedDefaultNote: "缺席 ⇒ 桥的装配处回退到 configCenter.baseUrl(配了 center 即默认开桥);显式值与回退值在托管判定上同判(都不算)", configKey: "authBridgeIssuer" }),
76
76
  r("AUTH_BRIDGE_JWKS_URL", "auth", "url", "registry SSO-JWT 的 JWKS 地址(缺省从 issuer 派生)", { danger: SEC }),
77
- r("AUTONOMY", "auth", "enum", "运维 autonomy 模式,tighten-only 叠到每个 TaskSpec(read-only/plan ⇒ 只读手,ask ⇒ 恒 shellGate);center 腿热且 non-sticky", { danger: SEC, enumValues: AUTONOMY_MODES, configKey: "autonomy", center: cGov("autonomy") }),
77
+ r("AUTONOMY", "auth", "enum", "运维 autonomy 模式(read-only ⇒ 只读手 + 写面 roots,每段续跑重折,子代同受、委派照挂;ask ⇒ 恒 shellGate,每段重折;plan ⇒ 提交入口的模式词地板:新提交的任务以 permissionMode:plan 起步,请求词钳到它之下,写与非只读调用在调用时问 / 拒,批准计划之后整条 run 按批准的档跑完);center 腿热且 non-sticky(plan 地板只作用于之后提交的任务)", { danger: SEC, enumValues: AUTONOMY_MODES, configKey: "autonomy", center: cGov("autonomy") }),
78
78
  r("AUTO_MODE_CLASSIFIER_FALLBACK", "approval", "enum", "auto 分类器席答不出裁决时的梯子(main=主模型代判 / none=只留分类器那一级,整条拒)", { enumValues: ["main", "none"], danger: SEC, derivedDefaultNote: "未设/空串 ⇒ 不铸键,跟随引擎缺省 main(core 7.18.0 起;空串按未设,与其余词表旋钮同判);none = 7.77.0 及以前的行为逐字;词表外的值启动期拒启。权限轴放宽,升级须知见 DEPLOY-PREREQS", configKey: "autoModeClassifierFallback" }),
79
79
  r("AUTO_MODE_CLASSIFIER_TIMEOUT_MS", "approval", "number", `auto 分类器单级判决上限(ms;缺席 = core 缺省;自托管慢网关按排队 + prefill 留足;一级到点没裁 ⇒ 走下一级,走完仍没裁 ⇒ 这次门控调用整条拒;须为 [${AUTO_MODE_CLASSIFIER_TIMEOUT_BOUNDS.minMs}, ${AUTO_MODE_CLASSIFIER_TIMEOUT_BOUNDS.maxMs}] 内十进制整数,0 / 小数 / 越界拒启)`, { derivedDefaultNote: "未设/空串 ⇒ 不铸键,跟随 core 缺省(本仓不复制上游数字;core 改缺省本旋钮照映不改)", configKey: "autoModeClassifierTimeoutMs" }),
80
80
  r("BAKE_CACHE_BASE", "orchestration", "url", "烤镜像 fetch-once 缓存源 base(注入 --cache-base;绝不受调用方控制)"),
@@ -95,7 +95,7 @@ export const CONFIG_CATALOG = [
95
95
  r("CONFIG_CATALOG_ENABLED", "auth", "boolean", "本目录端点 GET /v1/config/catalog 的旋钮(operator-only 面;合规部署可关 ⇒ 路径 404 + 能力位翻假)", { staticDefault: "true", danger: SEC, configKey: "configCatalogEnabled" }),
96
96
  r("CONFIG_LKG_DURABLE", "integrations", "boolean", "LKG 跨重启 handoff(严格 'true' 才开:重启先用 LKG 起服,tier 候选借此落地)", { staticDefault: "false" }),
97
97
  r("CONFIG_LKG_ENABLED", "integrations", "boolean", "last-known-good 配置盘缓存读写开关(仅远程 provider 有意义)", { staticDefault: "true", configKey: "configLkgEnabled" }),
98
- r("CONFIG_LKG_PATH", "integrations", "path", "LKG 落盘路径(设了 ⇒ 派生读拒行:这条路径与它的真实落点各一行,按路径段串匹配 —— 任意深度出现即拒、大小写不敏感,用户仓里同段串的路径一并读不到;空串拒启)", { derivedDefaultNote: "缺席 ⇒ 配置根(SEMA_CONFIG_DIR,缺省 ~/.sema)下按 worker + 凭证身份分箱派生", configKey: "configLkgPath" }),
98
+ r("CONFIG_LKG_PATH", "integrations", "path", "LKG 落盘路径(设了 ⇒ 派生一条读拒行 = 这条绝对路径:根锚定,只守这一个文件,经符号链接的目录时引擎沿链连目标一起守;大小写不敏感;用户仓里同尾的路径读得到;空串拒启)", { derivedDefaultNote: "缺席 ⇒ 配置根(SEMA_CONFIG_DIR,缺省 ~/.sema)下按 worker + 凭证身份分箱派生", configKey: "configLkgPath" }),
99
99
  r("CONFIG_LOCAL_DIR", "integrations", "path", "本地配置源根(config.d/<domain>.json);兼作数据根回落链一环", { configKey: "configLocalDir", danger: DATA }),
100
100
  r("CONFIG_PROVIDER", "integrations", "enum", "配置源双模选择(闭集,逐字比较;认不出的词 ⇒ 拒启,S-841)", { enumValues: CONFIG_PROVIDER_WORDS, derivedDefaultNote: "缺席:SEMA_REGISTRY_URL 在场 ⇒ remote,否则纯 env(无管道)", configKey: "configProvider" }),
101
101
  r("CONFIG_REQUIRE_ROSTER", "integrations", "boolean", "远程 registry 非 dryRun 时布防 ready 门:roster 未落地 ⇒ 计费提交 503 + /health ready:false(严格 'true')", { staticDefault: "false" }),
@@ -148,10 +148,10 @@ export const CONFIG_CATALOG = [
148
148
  r("EXPERIMENTAL_OBSERVER_AGENTS", "orchestration", "boolean", "observer 开闸线(实验面 ship-dark;多租户只认 center caps,env 被忽略)", { staticDefault: "false", configKey: "experimentalObserverAgents" }),
149
149
  r("FILE_HISTORY_CAPTURE", "store", "boolean", "文件历史**捕获**总开关(三条车道同义)。关 = 不再捕获任何改动文件的首次快照(落 core 的部署捕获位 `RunnerDeps.fileHistoryCapture: \"off\"`,能力词 `fileHistoryCapture` 翻 off);S-724(7.104.0)起**只关捕获**:历史店照接,已存历史照常 restoreFiles / rewindFilesTo / 2c session-sync。关掉 ≠ 删掉:已捕获的历史不被批量删除,仍按会话删除与留存规则清理 —— 要清存量走 DELETE 会话或留存旋钮;没有「整库历史不可用」的旋钮", { staticDefault: "true", danger: DATA, configKey: "fileHistoryCapture" }),
150
150
  r("FILE_HISTORY_RETENTION_KEEP", "store", "number", "per-file rewind 历史的保留上限(与 core FileStorageBackend 同域键形,三条车道一套语义)", { danger: DATA, derivedDefaultNote: "缺席 ⇒ core 缺省 100" }),
151
- r("FLEET_ADVERTISE_ADDRESS", "integrations", "url", "fleet 面向 center 公告的本 worker 可达地址(中心车道配齐 + 本键 ⇒ 公告环;无条件解析,非 http(s) 拒启,空串 = 未设。用量环不看本键,S-866 ④)", { configKey: "fleetAdvertiseAddress" }),
151
+ r("FLEET_ADVERTISE_ADDRESS", "integrations", "url", `fleet 面向 center 公告的本 worker 可达地址(中心车道配齐 + 本键 ⇒ 公告环;空串 = 未设;用量环不看本键)。中心往它后面拼路径,与中心基址三席同一条判据:${CENTER_BASE_URL_RULE}`, { configKey: "fleetAdvertiseAddress" }),
152
152
  r("FLEET_GENERATION", "integrations", "string", "fleet 成员的代际(配置渲染器写;引擎回声到心跳与 /health 的 generation)。缺席 ⇔ 非 fleet 成员(不续服务租约、不受围栏);在场必须是规范十进制串(\"0\" / \"7\";空串、前导零、符号、空白拒启)", { danger: SEC, configKey: "fleetGeneration" }),
153
153
  r("FLEET_LEASE_FAIL_CLOSED", "integrations", "boolean", "租约连接级失败(中心鉴权拒 / 凭证不可读 / 不可达 / 5xx / 超时 / 坏体)时计费入口拒 503 fleet.lease_unavailable(true)还是放行(false);409 not-lease 与耗尽 429 不受它管;词表外拒启", { danger: SEC, derivedDefaultNote: "缺席 ⇒ 按部署姿态派生:REQUIRE_PRINCIPAL=true ⇒ true(多租:额度是安全轴);单用户 ⇒ false(AP 姿态)。显式值压过派生。生效值见 caps centerWiring.failMode", configKey: "fleetLeaseFailClosed" }),
154
- r("FLEET_LEASE_OUTAGE_MAX_MS", "integrations", "number", "服务租约中心不可达的容忍上限 T_max(ms):租约过期满这么久 ⇒ 新计费提交 503 state.lease_expired(取消 / HITL 应答 / 续跑 / 读面照放行,在飞不动,/readyz 不翻);只在 fleet 成员 ∧ 中心宣告 servingLease 能力位时生效;< 60000(租约 TTL 地板)或非整数拒启", { danger: SEC, staticDefault: String(FLEET_LEASE_OUTAGE_MAX_DEFAULT_MS), configKey: "fleetLeaseOutageMaxMs" }),
154
+ r("FLEET_LEASE_OUTAGE_MAX_MS", "integrations", "number", "一台实例「活着而不服务」的最长时长 T_max(ms),两处读它:① 服务租约中心不可达的容忍上限 —— 租约过期满这么久 ⇒ 新计费提交 503 state.lease_expired(取消 / HITL 应答 / 续跑 / 读面照放行,在飞不动,/readyz 不翻),只在 fleet 成员 ∧ 中心宣告 servingLease 能力位时生效;② 排空口 POST /v1/admin/drain 的期限上界 —— deadlineAt 离此刻超过它 ⇒ 400 request.field_invalid,成员与非成员同读;< 60000(租约 TTL 地板)或非整数拒启", { danger: SEC, staticDefault: String(FLEET_LEASE_OUTAGE_MAX_DEFAULT_MS), configKey: "fleetLeaseOutageMaxMs" }),
155
155
  r("FLEET_SCOPE", "integrations", "string", "本引擎所在的租户 scope(配置渲染器为多租引擎写;唯一读者 = 排空口 POST /v1/admin/drain 的身份比对,请求不带 scope 也读成缺省 scope)。在场必须是 scope id 形(小写字母 / 数字 / 内部连字符,1–64;空串拒启);与 FLEET_GENERATION 不联动(成员缺席 ⇒ 缺省 scope)。续租体与心跳不带它(scope 由中心凭证绑定)", { danger: SEC, staticDefault: GLOBAL_SCOPE, configKey: "fleetScope" }),
156
156
  r("FORK_ENABLED", "orchestration", "boolean", "CC /fork(core Fork 工具)装配开关", { derivedDefaultNote: "posture 三态:缺席 = 单用户 ⇒ on / 多租户 ⇒ off(per-principal entitlement 另判)", configKey: "forkEnabled" }),
157
157
  r("GATEWAY_MAX_RETRIES", "modelPlane", "number", "openai 兼容腿每次调用重试上限", { derivedDefaultNote: "缺席 ⇒ 不传引擎,引擎默认当家" }),
@@ -44,7 +44,7 @@ export interface ApplyReport extends ConfigApplyReport {
44
44
  readonly fullyApplied: boolean;
45
45
  /** S-768:本候选目录的模型拒因判决 —— 逐条(`entries`,stage 判出的那份候选的,键已过名筛)+ 组拒码(`plane`,模型面组被拒时在场,
46
46
  * 判决那刻当数据带出)。落地与组拒同一只来源:两者都描述**这一份**候选(组拒时引擎自己的拒因表不换代,这里仍报被拒的那份)。
47
- * 候选不管模型面(零启用条目、零墓碑)⇒ `entries` 空、无 `plane`。`/health.modelRefusals` 与心跳的唯一数据源(config-center 投影)。 */
47
+ * 候选不管模型面(一条启用条目也没有 —— 幸存的与被丢的都算)⇒ `entries` 空、无 `plane`。`/health.modelRefusals` 与心跳的唯一数据源(config-center 投影)。 */
48
48
  readonly modelRefusals: ModelRefusalVerdict;
49
49
  }
50
50
  /**
@@ -166,6 +166,7 @@ function commitStaged(config, staged) {
166
166
  config.modelApiKeyEnv = mp.modelApiKeyEnv;
167
167
  config.modelApiKeys = mp.modelApiKeys;
168
168
  config.modelRefusals = mp.modelRefusals;
169
+ config.droppedCatalogEntries = mp.droppedCatalogEntries;
169
170
  mutateInPlace(config.modelQuotaWeights, mp.quotaWeights);
170
171
  mutateInPlace(config.models, mp.models);
171
172
  config.model = config.models.default;
@@ -190,7 +191,7 @@ function commitStaged(config, staged) {
190
191
  }
191
192
  export const MODEL_PLANE_DOMAINS = ["models", "collab"];
192
193
  const MODEL_PLANE_SURFACES = {
193
- models: ["models", "model", "modelRefusals", "modelApiKeyEnv", "modelApiKeys", "modelQuotaWeights", "roles", "tiers", "atModelAllowlist"],
194
+ models: ["models", "model", "modelRefusals", "droppedCatalogEntries", "modelApiKeyEnv", "modelApiKeys", "modelQuotaWeights", "roles", "tiers", "atModelAllowlist"],
194
195
  collab: ["collabWorkflows"],
195
196
  };
196
197
  const MODEL_PLANE_GROUP_KEYS = MODEL_PLANE_DOMAINS.flatMap((d) => MODEL_PLANE_SURFACES[d]);
@@ -230,8 +231,8 @@ function modelPlaneGroupReport(group) {
230
231
  const ROLE_BINDING_UNAVAILABLE_HINT = "tasks resolving this role fail: alias/tier expansion cannot mint this name; the engine refuses it loud — bind the role to a usable model, or make the entry usable (see the entry's own warn)";
231
232
  function stageModelPlane(config, eff, logger, sealedKeys) {
232
233
  const enabled = (eff.models?.models ?? []).filter((m) => m.enabled !== false);
233
- const verdict = judgeCatalogEntries(enabled, { anthropicRouteMounted: config.anthropic !== undefined }, eff.models?.droppedIdentities);
234
- if (enabled.length === 0 && Object.keys(verdict.refused).length === 0 && verdict.malformed.length === 0)
234
+ const verdict = judgeCatalogEntries(enabled, { anthropicRouteMounted: config.anthropic !== undefined }, eff.models?.droppedCatalogEntries);
235
+ if (enabled.length === 0 && verdict.malformed.length === 0 && verdict.dropped.every((row) => row.identity?.enabled === false))
235
236
  return { stages: { collab: stageCollabDomain(eff) } };
236
237
  {
237
238
  for (const [name, r] of Object.entries(verdict.refused))
@@ -347,6 +348,7 @@ function stageModelPlane(config, eff, logger, sealedKeys) {
347
348
  modelApiKeyEnv,
348
349
  modelApiKeys,
349
350
  modelRefusals: { ...verdict.refused },
351
+ droppedCatalogEntries: [...verdict.dropped],
350
352
  quotaWeights,
351
353
  roles,
352
354
  activeTiers,
@@ -73,16 +73,13 @@ export interface CenterRequestInit {
73
73
  readonly fetchImpl?: typeof fetch;
74
74
  }
75
75
  /**
76
- * 「后面要拼路径的基址」的**唯一**判官 —— 中心基址三席(`SEMA_REGISTRY_URL` / `SEMA_REGISTRY_LIVE_URL` / `SEMA_REGISTRY_WEB_URL`)与
77
- * 本实例的公告地址(`FLEET_ADVERTISE_ADDRESS`,中心往它后面拼 `/v1/...`)同读:可解析的 URL,scheme 是 http / https(Node 的 fetch 认
78
- * `file://` / `data:`,配错的地址不许变成读本地文件),**不带** userinfo / query / fragment(凭据不进一个会被记日志、转述、拼路径的值;
79
- * query / fragment 挂在基址上,一拼路径就落到错的位置:`http://x?a=1` + `/api/…` = `http://x?a=1/api/…`)。起服时 `config.ts` 判(拒启),
80
- * 请求期 {@link centerFetch} 对同一份基址再判一次(自拼 `ServiceConfig` 的嵌入方也吃同一条规则)。合格 ⇒ `undefined`;否则交回**点名规则**
81
- * 的短语(只回显 scheme,不回显值)。
76
+ * 「后面要拼路径的基址」的拒句 —— 中心基址三席(`SEMA_REGISTRY_URL` / `SEMA_REGISTRY_LIVE_URL` / `SEMA_REGISTRY_WEB_URL`)与本实例的
77
+ * 公告地址(`FLEET_ADVERTISE_ADDRESS`,中心往它后面拼 `/v1/...`)同读。判官是 `url-shape.ts` 那一只(层级式 http(s) URL;Node 的 fetch
78
+ * 认 `file://` / `data:`,配错的地址不许变成读本地文件),本函数只声明这一席禁哪几件({@link PATH_BASE_FORBIDS})。起服时 `config.ts` 判
79
+ * (拒启),请求期 {@link centerFetch} 对同一份基址再判一次(自拼 `ServiceConfig` 的嵌入方也吃同一条规则)。合格 ⇒ `undefined`;否则交回
80
+ * `<subject> <规则词>; got "<展示面安全形>"` —— 拒句里没有从值里解析出来的任何记号(scheme / userinfo / 主机名),捕获点照原样落日志即可。
82
81
  */
83
- /** 判官「scheme 不是 http / https」那条规则的不变句 —— 拒句 = `<席> ${PATH_BASE_SCHEME_RULE} (got "<scheme>")`(只回显 scheme)。钉读这一只,不手抄。 */
84
- export declare const PATH_BASE_SCHEME_RULE = "scheme must be http or https";
85
- export declare function pathBaseUrlFlaw(raw: string): string | undefined;
82
+ export declare function pathBaseUrlRefusal(subject: string, raw: string): string | undefined;
86
83
  /**
87
84
  * S-698 —— 凭证续签:`POST {issuer}/api/v1/auth/token/refresh`,form `refresh_token=…`(settings-schema `AUTH_V1_PATHS.tokenRefresh`)。
88
85
  * · 以 `issuer` 为根(可带路径前缀;同源与否由续签器先判,本函数只装配);query / fragment 不带过去。
@@ -2,6 +2,7 @@ import settingsSchemaPackage from "@sema-agent/settings-schema/package.json" wit
2
2
  import { AUTH_V1_PATHS } from "@sema-agent/settings-schema/api/auth";
3
3
  import { FLEET_LIVE_PATHS, FLEET_PATHS } from "@sema-agent/settings-schema/fleet";
4
4
  import { CenterCredentialUnavailableError } from "../center-credential.js";
5
+ import { buildHttpUrlRefusal, httpUrlFlaws } from "../url-shape.js";
5
6
  export const SCHEMA_MAJOR_HEADER = "x-sema-schema-major";
6
7
  export const CENTER_PULL_TIMEOUT_MS = 8000;
7
8
  export const SETTINGS_SCHEMA_MAJOR = (() => {
@@ -55,38 +56,21 @@ function routePath(route, kind) {
55
56
  return "/api/config/execution-keys";
56
57
  }
57
58
  }
58
- export const PATH_BASE_SCHEME_RULE = "scheme must be http or https";
59
- export function pathBaseUrlFlaw(raw) {
60
- let parsed;
61
- try {
62
- parsed = new URL(raw);
63
- }
64
- catch {
65
- parsed = undefined;
66
- }
67
- if (parsed === undefined)
68
- return "is not a parseable URL (no scheme? it must look like http(s)://host[:port][/path])";
69
- const broken = [
70
- ...(parsed.protocol !== "http:" && parsed.protocol !== "https:" ? [`${PATH_BASE_SCHEME_RULE} (got "${parsed.protocol.replace(/:$/, "")}")`] : []),
71
- ...(parsed.username !== "" || parsed.password !== "" ? ["must not carry userinfo (credentials never ride in a base URL: it is logged, reported and has paths appended)"] : []),
72
- ...(parsed.search !== "" || raw.includes("?") ? ["must not carry a query (request paths are appended to this base)"] : []),
73
- ...(parsed.hash !== "" || raw.includes("#") ? ["must not carry a fragment (request paths are appended to this base)"] : []),
74
- ];
75
- return broken.length === 0 ? undefined : broken.join("; ");
59
+ const PATH_BASE_FORBIDS = ["userinfo", "query", "fragment"];
60
+ export function pathBaseUrlRefusal(subject, raw) {
61
+ const flaws = httpUrlFlaws(raw, PATH_BASE_FORBIDS);
62
+ return flaws.length === 0 ? undefined : buildHttpUrlRefusal(subject, raw, flaws);
76
63
  }
77
64
  function assertPathBaseUrl(raw, what) {
78
- const flaw = pathBaseUrlFlaw(raw);
79
- if (flaw !== undefined)
80
- throw new Error(`${what} ${flaw}`);
81
- }
82
- function assertHttpScheme(url, what) {
83
- const scheme = url.protocol;
84
- if (scheme !== "http:" && scheme !== "https:")
85
- throw new Error(`${what} must be http(s), got "${scheme}"`);
65
+ const refusal = pathBaseUrlRefusal(what, raw);
66
+ if (refusal !== undefined)
67
+ throw new Error(refusal);
86
68
  }
87
69
  export async function centerTokenRefresh(issuer, refreshToken, init) {
70
+ const flaws = httpUrlFlaws(issuer, []);
71
+ if (flaws.length > 0)
72
+ throw new Error(buildHttpUrlRefusal("center credential issuer", issuer, flaws));
88
73
  const url = new URL(issuer);
89
- assertHttpScheme(url, "center credential issuer");
90
74
  url.pathname = `${url.pathname.replace(/\/+$/, "")}${AUTH_V1_PATHS.tokenRefresh}`;
91
75
  url.search = "";
92
76
  url.hash = "";
@@ -1,7 +1,7 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { readEffectiveWire, PrincipalCapsWire } from "@sema-agent/settings-schema";
3
3
  import { centerPromptsFromEffective } from "../prompts-domain-validate.js";
4
- import { splitFoldedReadWarnings, withDroppedIdentities } from "./read-warnings.js";
4
+ import { splitFoldedReadWarnings, withDroppedCatalogEntries } from "./read-warnings.js";
5
5
  import { CENTER_PULL_TIMEOUT_MS, centerFetch } from "./center-request.js";
6
6
  import { answerStillCurrent } from "../center-credential.js";
7
7
  export async function fetchEffective(center, etag, fetchImpl = fetch, worker) {
@@ -18,8 +18,8 @@ export async function fetchEffective(center, etag, fetchImpl = fetch, worker) {
18
18
  delete wire.prompts;
19
19
  else
20
20
  wire.prompts = centerPrompts;
21
- const { domainErrors, droppedEntries, droppedIdentities } = splitFoldedReadWarnings(warnings);
22
- const effective = withDroppedIdentities(wire, droppedIdentities);
21
+ const { domainErrors, droppedEntries, droppedCatalogEntries } = splitFoldedReadWarnings(warnings);
22
+ const effective = withDroppedCatalogEntries(wire, droppedCatalogEntries);
23
23
  return {
24
24
  effective,
25
25
  etag: res.headers.get("etag") ?? undefined,
@@ -25,7 +25,8 @@
25
25
  * 候选门整拒,而它在 1.10.0 上零告警零 domainErrors(亲验读数)。兼容臂的关窗观测归属主(settings-schema 的
26
26
  * `onWarning` 流),不借候选门这条带副作用的通道传信息。
27
27
  */
28
- import { type EffectiveReadWarning } from "@sema-agent/settings-schema";
28
+ import { z } from "zod";
29
+ import { type DroppedEntryIdentity, type DroppedEntryReasonCode, type EffectiveReadWarning } from "@sema-agent/settings-schema";
29
30
  /** 折叠产物:`scope` 决定消费方做什么 —— `"domain"` 进候选门(`domainErrors`),`"entry"` 进逐条丢弃面(`droppedEntries`)。 */
30
31
  export type FoldedReadWarning = {
31
32
  readonly scope: "domain";
@@ -36,12 +37,16 @@ export type FoldedReadWarning = {
36
37
  readonly domain: string;
37
38
  readonly entry: string;
38
39
  readonly error: string;
40
+ readonly reasonCode: DroppedEntryReasonCode;
39
41
  };
40
- /** 逐条丢弃面的一行(两条读腿的 `droppedEntries` 同形):`entry` = 被丢那条的出门形坐标(`models.1`),`error` = 哪些字段、为什么。 */
42
+ /** 逐条丢弃面的一行(两条读腿的 `droppedEntries` 同形,`config_entry_dropped` 日志行读它):`entry` = 被丢那条的出门形坐标(`models.1`),
43
+ * `error` = 哪些字段、为什么(人读),`reasonCode` = 为什么丢的**闭集词**(S-1295:settings-schema 铸、原样过境 —— 目录行的
44
+ * `GET /v1/models.droppedEntries[].reasonCode` 就是这同一个词,取自同一次折叠的同一行)。 */
41
45
  export type DroppedConfigEntry = {
42
46
  readonly domain: string;
43
47
  readonly entry: string;
44
48
  readonly error: string;
49
+ readonly reasonCode: DroppedEntryReasonCode;
45
50
  };
46
51
  /**
47
52
  * zod 形的错误 → 一行可读的字段诊断(`contextWindow: expected number, received string; api: custom`)。
@@ -58,41 +63,42 @@ export declare function describeSchemaIssueFields(error: unknown): string[];
58
63
  /** null = 不入诊断面(`hosts-grandfathered` / `legacy-governance-lifted`:值已被收编接受,与本地店同口径)。 */
59
64
  export declare function foldEffectiveReadWarning(w: EffectiveReadWarning): FoldedReadWarning | null;
60
65
  /**
61
- * 🔴 S-712 —— 一条被 schema 逐条丢掉的**启用**、**有名**目录条目的身份(模型面墓碑的来源;载体 = `EffectiveConfig.models.droppedIdentities`,
62
- * 随 effective 走 —— 盘上 LKG 从同一个对象落盘,重启从 LKG 起服墓碑不丢)。
66
+ * 🔴 S-712 / S-1295 —— **被丢目录条目的载体行**(载体 = `EffectiveConfig.models.droppedCatalogEntries`,读腿铸,随 effective 走 —— 盘上 LKG
67
+ * 从同一个对象落盘,重启从 LKG 起服时它还在)。形的**唯一定义**(型由它推出;判据按它判盘上来的行)。
63
68
  *
64
- * 为什么要交出身份:条目被丢之后它的名字从目录里消失,而档位增广(core `expandTiers`)会把一个**空出来的**档位词 / CC 别名
65
- * (条目恰好叫 `sonnet` / `pro` …)重新铸到另一条条目上 —— 显式点它的请求与点名它的角色绑定就这样被静默换了目的地
66
- * (gpt-6 腿 F2 / F3,真复现)。身份进 `modelRefusals` 之后,不变量 `refused-names-not-minted` 才判得到它。
69
+ * **规则一条:读腿把 schema 逐条丢掉的每一条目录条目(`models` 域的 `["models", <下标>]`)记一行,写入时不挑;每个读者读自己要的那一部分。**
70
+ * · 墓碑判据(`model-entry-refusal.ts` `judgeCatalogEntries`)—— 有名(`identity.name`)且未停用(`identity.enabled !== false`)的行;
71
+ * · `GET /v1/models` 的 `droppedEntries`({@link buildDroppedEntriesWire})—— 每一行的下标、闭集词与可披露身份;
72
+ * · `config_entry_dropped` 日志行读的是这一行由之而来的那条折叠行({@link DroppedConfigEntry},同一次折叠、同一个词)。
73
+ * (修前 S-712 的载体 `droppedIdentities` 在写入时只收「有名且启用」的行 —— 那是墓碑读者的筛子,装在了写入处;同一件事于是没有
74
+ * 第二个读者可用。筛子回到读者那里,载体就只剩「记下发生了什么」这一条律。)
67
75
  *
68
- * 🔐 纯身份,不含值:`name` / `id` 取自告警的 `identity`(settings-schema 6.1.0:只取被丢条目原文里的字符串形,脱敏器会改动的串
69
- * 整键扣下),`fields` 是坏字段的**键路径**,`issues` 是 {@link describeSchemaIssues} 的一行(键名 + 类型词)—— 被丢条目的原值
70
- * (可能是一段贴错位置的凭据)绝不进载体、日志、LKG 与 400 拒因句。
76
+ * 🔐 不含值:`identity` 取告警自带的身份(settings-schema 只铸可披露的标识符 —— 秘密形 / 非标识符的串 ⇒ 该键缺席,三键都没有 ⇒ 整个缺席);
77
+ * `index` 是条目在**这次读到的那份目录文档** `models` 数组里的原下标(数,不是串);`fields` 是坏字段的键路径、`issues` 是
78
+ * {@link describeSchemaIssues} 的一行(键名 + 类型词)—— 后两者只供本机拒因句,不上 wire。
71
79
  */
72
- export interface DroppedModelIdentity {
73
- /** 出门形坐标(`models.1`),与同一次读的 `droppedEntries[].entry` 同一串。 */
74
- readonly entry: string;
75
- /** 条目写的 `name`(读腿只为有名的行铸载体)。载体上缺了它 = 载体形不对(LKG 载体被改 / 手写 effective)⇒ 模型面整组拒,
76
- * 判在 `model-entry-refusal.ts` `judgeCatalogEntries`。 */
77
- readonly name: string;
78
- readonly id?: string;
79
- readonly fields: readonly string[];
80
- readonly issues: string;
81
- }
80
+ export declare const DroppedCatalogEntryRow: z.ZodObject<{
81
+ index: z.ZodNumber;
82
+ reasonCode: z.ZodEnum<{
83
+ "entry-schema-invalid": "entry-schema-invalid";
84
+ "entry-key-invalid": "entry-key-invalid";
85
+ "entry-unreadable": "entry-unreadable";
86
+ }>;
87
+ identity: z.ZodOptional<z.ZodObject<{
88
+ name: z.ZodOptional<z.ZodString>;
89
+ id: z.ZodOptional<z.ZodString>;
90
+ enabled: z.ZodOptional<z.ZodBoolean>;
91
+ }, z.core.$strict>>;
92
+ fields: z.ZodArray<z.ZodString>;
93
+ issues: z.ZodString;
94
+ }, z.core.$strict>;
95
+ export type DroppedCatalogEntry = z.infer<typeof DroppedCatalogEntryRow>;
82
96
  /**
83
97
  * 两条读腿(远端 `http-client.ts` / 本地 `config-provider.ts`)共用的**分面单点**:折叠产物按 `scope` 分进候选门
84
- * (`domainErrors`)与逐条丢弃面(`droppedEntries`)。`switch` 穷举 —— 加一种 scope 不补臂 = 编译红,两腿不会各分各的。
98
+ * (`domainErrors`)与逐条丢弃面(`droppedEntries`);逐条丢弃里的**目录条目**同一拍铸进目录载体(`droppedCatalogEntries`,
99
+ * 一次遍历、一行折叠、一个词 —— 日志行与载体行不各算一次)。`switch` 穷举 —— 加一种 scope 不补臂 = 编译红,两腿不会各分各的。
85
100
  * 文案**未脱敏**:进日志流之前各腿在自己的产出边界上过 `redactConfigError`(与 `domainErrors` 同一道)。
86
- *
87
- * 🔴 S-712 身份规则(一条,数据源 = 告警自带的 `identity`,settings-schema 6.1.0 在**同一次解析**里从被丢条目原文取 —— 本仓不再回读原文档):
88
- * **`identity.name` 是字符串 ⇒ 有名;有名且 `identity.enabled !== false` ⇒ 墓碑(进载体);其余一律不进载体。**
89
- * · 有名而停用 ⇒ 停用 = 不在目录(与 7.100.0 同义);
90
- * · 没有字符串名 —— `identity` 在场而无 `name`,或 `identity` 整个缺席 ⇒ **无名可留**,与停用同处置。按 settings-schema 6.1.0 的铸点
91
- * (`config-fns.ts` `readEntryIdentity`),「没有名」有两个成因、刻意不分:原文 `name` 不是字符串(`null` / 数 / 整行不是对象),
92
- * 或是字符串但脱敏器会改动它(秘密形 —— 扣下不出门)。后者对本判据同样安全:档位增广只铸闭集词(`ultra` / `max` / `pro` /
93
- * `flash` / `lite`、CC 别名、`best`),没有一个是秘密形,一个被扣下的名字无从被铸出;它的代价只是显式点它的请求拿到「未知模型」
94
- * 而不是 `request.model_unavailable`(那个名字本来也不许进日志与拒因句)。
95
- * 坏行本身照样走 `droppedEntries`(坐标 + 字段,不带值)。
101
+ * 坏行本身照样走 `droppedEntries`(坐标 + 字段 + 词,不带值)。
96
102
  */
97
103
  export declare function splitFoldedReadWarnings(warnings: readonly EffectiveReadWarning[]): {
98
104
  domainErrors: Array<{
@@ -100,10 +106,25 @@ export declare function splitFoldedReadWarnings(warnings: readonly EffectiveRead
100
106
  error: string;
101
107
  }>;
102
108
  droppedEntries: DroppedConfigEntry[];
103
- droppedIdentities: DroppedModelIdentity[];
109
+ droppedCatalogEntries: DroppedCatalogEntry[];
104
110
  };
105
- /** S-712:把本次读交出的被丢身份挂上 effective 的模型域(两腿同一只;空 ⇒ 不挂键)。 */
106
- export declare function withDroppedIdentities<E extends {
111
+ /** 把本次读交出的目录载体挂上 effective 的模型域(两腿同一只;空 ⇒ 不挂键)。 */
112
+ export declare function withDroppedCatalogEntries<E extends {
107
113
  models?: object;
108
- }>(effective: E, droppedIdentities: readonly DroppedModelIdentity[]): E;
114
+ }>(effective: E, rows: readonly DroppedCatalogEntry[]): E;
115
+ /** `GET /v1/models` 顶层 `droppedEntries` 的一行(wire 形;契约 §A.1d「`GET /v1/models` 的 `droppedEntries`」)。 */
116
+ export interface DroppedEntryWireRow {
117
+ readonly index: number;
118
+ readonly reasonCode: DroppedEntryReasonCode;
119
+ readonly identity?: DroppedEntryIdentity;
120
+ }
121
+ /**
122
+ * S-1295 —— 本代目录的被丢条目(`config.droppedCatalogEntries`)→ `GET /v1/models` 的 `droppedEntries`。纯投影,**唯一**出口。
123
+ * · 词:settings-schema 的闭集原样过境({@link reasonCodeForWire} 穷举:包加词 / 删词 ⇒ 本仓编译红,逼一次「这个词能不能上 wire」的显式决定);
124
+ * · 身份:`name` / `id` 在出口再问一次包的 `isDisclosableIdentity`(唯一判据,import 不复制)—— 载体可能来自被手改的盘上 LKG,读腿铸的身份
125
+ * 本来就过了同一只判据;不可披露 ⇒ 该键不出门(不打码:打过码的串不再是身份);`enabled` 是布尔照带;三键都不剩 ⇒ `identity` 缺席;
126
+ * · 不投 `fields` / `issues`(人读的诊断句,不是契约),不投坐标串(下标是数)。
127
+ * 恒返回数组(零条 = `[]`):键恒在场,缺席只意味着更早的 server。
128
+ */
129
+ export declare function buildDroppedEntriesWire(rows: readonly DroppedCatalogEntry[]): DroppedEntryWireRow[];
109
130
  //# sourceMappingURL=read-warnings.d.ts.map
@@ -1,4 +1,5 @@
1
- import { formatConfigPath } from "@sema-agent/settings-schema";
1
+ import { z } from "zod";
2
+ import { DROPPED_ENTRY_REASON_CODES, formatConfigPath, isDisclosableIdentity, } from "@sema-agent/settings-schema";
2
3
  export function describeSchemaIssues(error) {
3
4
  const issues = schemaIssuesOf(error);
4
5
  if (issues === undefined)
@@ -55,7 +56,7 @@ export function foldEffectiveReadWarning(w) {
55
56
  case "domain-defaulted":
56
57
  return { scope: "domain", domain: w.domain, error: cap(errorText(w.error)) };
57
58
  case "entry-dropped":
58
- return { scope: "entry", domain: w.domain, entry: formatConfigPath(w.path), error: cap(`entry dropped by domain schema: ${describeSchemaIssues(w.error) ?? errorText(w.error)}`) };
59
+ return { scope: "entry", domain: w.domain, entry: formatConfigPath(w.path), error: cap(`entry dropped by domain schema: ${describeSchemaIssues(w.error) ?? errorText(w.error)}`), reasonCode: droppedReasonCodeOf(w) };
59
60
  case "unknown-keys-dropped":
60
61
  return { scope: "domain", domain: w.domain, error: keyList("unknown keys dropped by domain schema: ", w.keys, w.truncatedFrom) };
61
62
  case "unknown-keys-carried":
@@ -68,12 +69,34 @@ export function foldEffectiveReadWarning(w) {
68
69
  }
69
70
  }
70
71
  }
71
- function isModelEntryDrop(w) {
72
- return w.kind === "entry-dropped" && w.domain === "models" && w.path.length === 2 && w.path[0] === "models" && typeof w.path[1] === "number";
72
+ function droppedReasonCodeOf(w) {
73
+ if (w.reasonCode === undefined) {
74
+ throw new Error(`entry-dropped warning at ${formatConfigPath(w.path)} carries no reasonCode — settings-schema mints one on every dropped entry, so this warning did not come from the pinned package`);
75
+ }
76
+ return w.reasonCode;
77
+ }
78
+ export const DroppedCatalogEntryRow = z
79
+ .object({
80
+ index: z.number().int().nonnegative(),
81
+ reasonCode: z.enum(DROPPED_ENTRY_REASON_CODES),
82
+ identity: z.object({ name: z.string().optional(), id: z.string().optional(), enabled: z.boolean().optional() }).strict().optional(),
83
+ fields: z.array(z.string()),
84
+ issues: z.string(),
85
+ })
86
+ .strict();
87
+ function catalogIndexOf(w) {
88
+ return w.kind === "entry-dropped" && w.domain === "models" && w.path.length === 2 && w.path[0] === "models" && typeof w.path[1] === "number" ? w.path[1] : undefined;
89
+ }
90
+ function identityOf(i) {
91
+ if (i === undefined)
92
+ return undefined;
93
+ const out = { ...(i.name !== undefined ? { name: i.name } : {}), ...(i.id !== undefined ? { id: i.id } : {}), ...(i.enabled !== undefined ? { enabled: i.enabled } : {}) };
94
+ return Object.keys(out).length > 0 ? out : undefined;
73
95
  }
74
96
  export function splitFoldedReadWarnings(warnings) {
75
97
  const domainErrors = [];
76
98
  const droppedEntries = [];
99
+ const droppedCatalogEntries = [];
77
100
  for (const w of warnings) {
78
101
  const f = foldEffectiveReadWarning(w);
79
102
  if (f === null)
@@ -82,29 +105,54 @@ export function splitFoldedReadWarnings(warnings) {
82
105
  case "domain":
83
106
  domainErrors.push({ domain: f.domain, error: f.error });
84
107
  break;
85
- case "entry":
86
- droppedEntries.push({ domain: f.domain, entry: f.entry, error: f.error });
108
+ case "entry": {
109
+ droppedEntries.push({ domain: f.domain, entry: f.entry, error: f.error, reasonCode: f.reasonCode });
110
+ const index = catalogIndexOf(w);
111
+ if (index !== undefined && w.kind === "entry-dropped") {
112
+ const identity = identityOf(w.identity);
113
+ droppedCatalogEntries.push({ index, reasonCode: f.reasonCode, ...(identity !== undefined ? { identity } : {}), fields: describeSchemaIssueFields(w.error), issues: describeSchemaIssues(w.error) ?? "(schema issues unreadable)" });
114
+ }
87
115
  break;
116
+ }
88
117
  default: {
89
118
  const never = f;
90
119
  throw new Error(`unhandled folded read-warning scope: ${JSON.stringify(never)}`);
91
120
  }
92
121
  }
93
122
  }
94
- const droppedIdentities = [];
95
- for (const w of warnings) {
96
- if (!isModelEntryDrop(w))
97
- continue;
98
- const identity = w.identity;
99
- if (identity?.name === undefined || identity.enabled === false)
100
- continue;
101
- droppedIdentities.push({ entry: formatConfigPath(w.path), name: identity.name, ...(identity.id !== undefined ? { id: identity.id } : {}), fields: describeSchemaIssueFields(w.error), issues: describeSchemaIssues(w.error) ?? "(schema issues unreadable)" });
102
- }
103
- return { domainErrors, droppedEntries, droppedIdentities };
123
+ return { domainErrors, droppedEntries, droppedCatalogEntries };
104
124
  }
105
- export function withDroppedIdentities(effective, droppedIdentities) {
106
- if (droppedIdentities.length === 0 || effective.models === undefined)
125
+ export function withDroppedCatalogEntries(effective, rows) {
126
+ if (rows.length === 0 || effective.models === undefined)
107
127
  return effective;
108
- return { ...effective, models: { ...effective.models, droppedIdentities: [...droppedIdentities] } };
128
+ return { ...effective, models: { ...effective.models, droppedCatalogEntries: [...rows] } };
129
+ }
130
+ export function buildDroppedEntriesWire(rows) {
131
+ return rows.map((r) => {
132
+ const identity = disclosableIdentityOf(r.identity);
133
+ return { index: r.index, reasonCode: reasonCodeForWire(r.reasonCode), ...(identity !== undefined ? { identity } : {}) };
134
+ });
135
+ }
136
+ function reasonCodeForWire(code) {
137
+ switch (code) {
138
+ case "entry-schema-invalid":
139
+ case "entry-key-invalid":
140
+ case "entry-unreadable":
141
+ return code;
142
+ default: {
143
+ const never = code;
144
+ throw new Error(`unhandled DroppedEntryReasonCode: ${JSON.stringify(never)}`);
145
+ }
146
+ }
147
+ }
148
+ function disclosableIdentityOf(i) {
149
+ if (i === undefined)
150
+ return undefined;
151
+ const out = {
152
+ ...(i.name !== undefined && isDisclosableIdentity(i.name) ? { name: i.name } : {}),
153
+ ...(i.id !== undefined && isDisclosableIdentity(i.id) ? { id: i.id } : {}),
154
+ ...(i.enabled !== undefined ? { enabled: i.enabled } : {}),
155
+ };
156
+ return Object.keys(out).length > 0 ? out : undefined;
109
157
  }
110
158
  //# sourceMappingURL=read-warnings.js.map
@@ -8,7 +8,7 @@
8
8
  import type { ToolContentOrigin } from "@sema-agent/core";
9
9
  import type { CollabTemplateWire } from "../capabilities/collab-wire.js";
10
10
  import type { Autonomy, CommandRule } from "../runtime-governance.js";
11
- import type { DroppedModelIdentity } from "./read-warnings.js";
11
+ import type { DroppedCatalogEntry } from "./read-warnings.js";
12
12
  export interface CenterModel {
13
13
  name: string;
14
14
  id: string;
@@ -271,13 +271,12 @@ export interface EffectiveConfig {
271
271
  default?: string;
272
272
  tierGroups?: unknown;
273
273
  activeTierGroup?: string;
274
- /** 🔴 S-712 —— **本仓读腿铸的**载体(不是 wire 键:中心在模型域里写同名键会被 schema 当未知键剥掉并进候选门):这次读
275
- * 被 schema 逐条丢掉的**启用**、**有名**目录条目的身份(取 settings-schema 6.1.0 告警自带的 `identity`,见
276
- * `read-warnings.ts` `splitFoldedReadWarnings`)。模型面 stage 把它们铸成 `config.model_entry_invalid` 墓碑进 `modelRefusals`,
277
- * 不变量 `refused-names-not-minted` 据此判增广有没有把它们的名字铸到别的条目上。挂在 effective 上(而不是读结果旁边)是为了
278
- * **随 effective 落 LKG**:重启从 LKG 起服时墓碑还在。形见 {@link DroppedModelIdentity}(纯身份,不含值);盘上形不对
279
- * (不是列表 / 行缺字符串 `name`)⇒ 模型面整组拒。 */
280
- droppedIdentities?: DroppedModelIdentity[];
274
+ /** 🔴 S-712 / S-1295 —— **本仓读腿铸的**载体(不是 wire 键:中心在模型域里写同名键会被 schema 当未知键剥掉并进候选门):这次读
275
+ * 被 schema 逐条丢掉的**每一条**目录条目一行(下标 + settings-schema 闭集词 `reasonCode` + 告警自带的 `identity` + 字段诊断;形与律见
276
+ * `read-warnings.ts` {@link DroppedCatalogEntry})。读者各取所需:模型面 stage 把有名且启用的行铸成 `config.model_entry_invalid` 墓碑
277
+ * (不变量 `refused-names-not-minted` 据此判增广),每一行与 `models` 同 commit 进 `config.droppedCatalogEntries`(`GET /v1/models.droppedEntries` 的源)。
278
+ * 挂在 effective 上(而不是读结果旁边)是为了**随 effective 落 LKG**:重启从 LKG 起服时它还在。盘上形不对 ⇒ 模型面整组拒。 */
279
+ droppedCatalogEntries?: DroppedCatalogEntry[];
281
280
  };
282
281
  /** registry-core 0.7 collab domain (teams' successor) — consumed by the slice-1.5 projection
283
282
  * (projectCollabToWorkflows → registerCollabWorkflows). Both lanes carry it: the remote worker /effective view
@@ -1,6 +1,6 @@
1
- /** 落盘信封。`effective`/`etag` 逐字=当时的 fetchEffective 返回(JSON verbatim 面)。 */
1
+ /** 落盘信封。`effective`/`etag` 逐字=当时的 fetchEffective 返回(JSON verbatim 面)。`formatVersion` 的语义见头注(3 = 载荷的被丢目录条目记录是全的)。 */
2
2
  export interface PersistedLkg {
3
- formatVersion: 2;
3
+ formatVersion: 3;
4
4
  savedAt: string;
5
5
  worker: string;
6
6
  /** 拉到这份候选时所用凭证的身份(`center-credential.ts`;摘要形,不含凭证原文)。 */
@@ -30,13 +30,14 @@ export declare function lkgPathOverrideOf(raw: string | undefined): string | und
30
30
  * LKG(含凭据)实际所在位置的**读拒行** —— 一条规则:读拒从 LKG 落在哪派生,不写死位置。
31
31
  * · 没覆写 ⇒ LKG 落在配置根下(`config-lkg-<箱>.json`):引擎内建表的 `~/.sema/config-lkg-*.json` 行在家目录与声明的根(`agentConfigDir`,
32
32
  * 根经链接时引擎连目标一起守)两处守它 ⇒ 不另加行(加了就是同一个文件两只判据,`READ_DENY_BUILTIN_EXCLUDE` 删那一行也删不掉它);
33
- * · 覆写 ⇒ LKG 在配置根外,引擎那一行够不着 ⇒ 这里派生行:它的**两种拼法**各一行 —— 覆写写下的绝对路径,与文件真正落的地方(路径经符号链接的
34
- * 目录时,写进去的文件在链接目标下;只守拼写的话读真实路径就绕过了 —— 车内复审 [high])。两者相同 ⇒ 一行。大小写不敏感(与内建行同读法)。
35
- * 拒句与内建行同一句(`sensitive-path read deny list (pattern …)`)。
36
- * **行的射程**(引擎读拒表的读法,不是本函数选的):行是路径段串 —— 引擎去掉开头的 `/`、不锚到根,这串段在任意深度出现即拒(大小写不敏感),
37
- * 所以挡的不止 LKG 那一个文件:用户仓里同段串的路径(`/app/config-lkg.json` ⇒ `…/app/config-lkg.json`)一并读不到、Grep 结果里也不出现。
38
- * 挡多了优于不挡;引擎提供锚定单文件的行之前如此(随包文案 MIGRATION / 目录行同句)。
39
- * 读者:部署姿态座席族(`boot/runner-deps.ts` `buildDeploymentPostureSeats` 的 `readDenyPatterns` 席 = 运维的行 ∪ 本函数的行)—— 每次铸席现解一次链接。
33
+ * · 覆写 ⇒ LKG 在配置根外,引擎那一行够不着 ⇒ 这里派生**一行** = 覆写的绝对路径(`/` 开头;`lkgPathOverrideOf` 已把相对值解成绝对)。大小写不敏感
34
+ * (与内建行同读法)。拒句与内建行同一句(`sensitive-path read deny list (pattern …)`)。
35
+ * **行的射程**(引擎读拒表的读法,core 7.48.0 #1442):`/` 开头的行**根锚定** —— 只守那一个绝对位置,用户仓里同尾的路径
36
+ * (`/app/config-lkg.json` 之于 `…/repo/app/config-lkg.json`)读得到、Grep 结果里也在;路径经符号链接的目录时,引擎在每次准备时把这一行
37
+ * **沿链放置**到它的字面头真正通向的地方(写进去的文件落在链接目标下,目标路径同样被拒)。
38
+ * 7.120.0–7.122.x 的两处旧读法随之退役:行是尾段任意深度匹配(挡掉用户仓里同尾的路径);本仓另派生一条 realpath 落点行(座席铸一次、
39
+ * 解不开计 P-DEBT)—— 那是「这条路径通向哪」的第二个读者,引擎沿链放置之后它是双读。
40
+ * 读者:部署姿态座席族(`boot/runner-deps.ts` `buildDeploymentPostureSeats` 的 `readDenyPatterns` 席 = 运维的行 ∪ 本函数的行)。
40
41
  */
41
42
  export declare function lkgReadDenyRows(configLkgPath: string | undefined): {
42
43
  pattern: string;
@@ -49,9 +50,11 @@ export declare function saveLkg(path: string, worker: string | undefined, identi
49
50
  * 读盘上 LKG。返回 undefined 的所有情形都「安静地回 env 兜底」(调用方 warn 点名 reason):
50
51
  * 文件缺席/JSON 坏/信封形不对/worker 或凭证身份不匹配(防同名文件跨 worker / 跨账号串包;`CONFIG_LKG_PATH`
51
52
  * 单一路径时这是唯一的一道)/prompts 面复判失败。
53
+ * `migrated`:读回时做过一次性迁移({@link migrateV2Lkg})—— 调用方响亮打一行。
52
54
  */
53
55
  export declare function loadLkg(path: string, worker: string | undefined, identity: string): Promise<{
54
56
  lkg: PersistedLkg;
57
+ migrated?: string;
55
58
  } | {
56
59
  lkg?: undefined;
57
60
  reason: string;