@sema-agent/server 7.101.0 → 7.102.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 (86) hide show
  1. package/MIGRATION.md +23 -0
  2. package/README.md +1 -1
  3. package/README.zh-CN.md +1 -1
  4. package/USAGE.md +58 -4
  5. package/dist/approval-card.d.ts +35 -2
  6. package/dist/approval-card.js +19 -1
  7. package/dist/background-session-reap.d.ts +55 -0
  8. package/dist/background-session-reap.js +90 -0
  9. package/dist/boot/config-center.d.ts +16 -4
  10. package/dist/boot/config-center.js +44 -15
  11. package/dist/boot/exit-boundary.js +17 -0
  12. package/dist/boot/runner-deps.js +1 -0
  13. package/dist/boot/runtime-caps.js +3 -2
  14. package/dist/boot/session-faces.js +3 -4
  15. package/dist/boot/shutdown.d.ts +2 -1
  16. package/dist/boot/shutdown.js +33 -12
  17. package/dist/boot/stage-07-capability-layer.d.ts +1 -1
  18. package/dist/boot/stage-07-capability-layer.js +6 -4
  19. package/dist/boot/stage-08-reapers.d.ts +1 -1
  20. package/dist/boot/stage-09-leader.d.ts +1 -1
  21. package/dist/boot/stage-10-http-server.d.ts +1 -1
  22. package/dist/boot/stage-10-http-server.js +9 -3
  23. package/dist/capabilities/builtin-tools.d.ts +14 -1
  24. package/dist/capabilities/builtin-tools.js +3 -0
  25. package/dist/capabilities/scenarios.d.ts +1 -1
  26. package/dist/capabilities/scenarios.js +3 -3
  27. package/dist/center-credential-renewer.d.ts +76 -0
  28. package/dist/center-credential-renewer.js +359 -0
  29. package/dist/center-credential.d.ts +76 -22
  30. package/dist/center-credential.js +119 -43
  31. package/dist/config-catalog.js +9 -4
  32. package/dist/config-center/apply-effective.js +5 -12
  33. package/dist/config-center/center-request.d.ts +13 -0
  34. package/dist/config-center/center-request.js +23 -3
  35. package/dist/config-center/facade.d.ts +1 -1
  36. package/dist/config-center/http-client.js +2 -5
  37. package/dist/config-center/read-warnings.d.ts +19 -48
  38. package/dist/config-center/read-warnings.js +7 -49
  39. package/dist/config-center/restart-signal.d.ts +8 -11
  40. package/dist/config-center/types.d.ts +4 -2
  41. package/dist/config-provider.js +1 -48
  42. package/dist/config-types.d.ts +77 -5
  43. package/dist/config-types.js +5 -0
  44. package/dist/config.js +49 -13
  45. package/dist/device-store.d.ts +1 -1
  46. package/dist/fleet/fleet-bus.d.ts +30 -4
  47. package/dist/fleet/fleet-bus.js +20 -4
  48. package/dist/fleet/fleet-reconciler.d.ts +29 -6
  49. package/dist/fleet/fleet-reconciler.js +109 -79
  50. package/dist/fleet-client.d.ts +21 -7
  51. package/dist/fleet-client.js +41 -12
  52. package/dist/fleet-lease.d.ts +1 -1
  53. package/dist/http/approval-sweeps.js +5 -20
  54. package/dist/http/dispatch.js +20 -14
  55. package/dist/http/readiness.d.ts +87 -0
  56. package/dist/http/readiness.js +54 -0
  57. package/dist/http/resume-legs.js +5 -4
  58. package/dist/http/route-table.d.ts +7 -0
  59. package/dist/http/route-table.js +6 -0
  60. package/dist/http/routes/a2a-serve.js +6 -14
  61. package/dist/http/routes/approvals-assistant.js +1 -0
  62. package/dist/http/routes/background.d.ts +80 -0
  63. package/dist/http/routes/background.js +94 -0
  64. package/dist/http/routes/capabilities.js +2 -0
  65. package/dist/http/server.d.ts +21 -8
  66. package/dist/http/server.js +2 -2
  67. package/dist/model-entry-refusal.d.ts +11 -10
  68. package/dist/model-entry-refusal.js +13 -10
  69. package/dist/observability/fail-open.d.ts +6 -2
  70. package/dist/observability/fail-open.js +6 -2
  71. package/dist/parked-decide.d.ts +11 -5
  72. package/dist/parked-decide.js +4 -1
  73. package/dist/parked-revive-surface.d.ts +47 -0
  74. package/dist/parked-revive-surface.js +20 -0
  75. package/dist/plugins/background-shell-support.d.ts +20 -1
  76. package/dist/plugins/background-shell-support.js +26 -0
  77. package/dist/plugins/host-platform.d.ts +11 -1
  78. package/dist/plugins/host-platform.js +11 -0
  79. package/dist/plugins/remote-env-host.d.ts +14 -1
  80. package/dist/plugins/remote-env-host.js +18 -5
  81. package/dist/tool-approval.d.ts +39 -1
  82. package/dist/tool-approval.js +37 -8
  83. package/dist/trace/core-keyset-guard.d.ts +3 -3
  84. package/dist/trace/engine-notice-wire.d.ts +1 -1
  85. package/dist/trace/engine-notice-wire.js +2 -0
  86. package/package.json +3 -3
@@ -2,6 +2,7 @@ import { readFileSync } from "node:fs";
2
2
  import { createHash } from "node:crypto";
3
3
  import { resolve as resolvePath } from "node:path";
4
4
  import { looksLikeJwt } from "@sema-agent/settings-schema/api/auth-bridge";
5
+ import { CENTER_CREDENTIAL_ENV, judgeCenterCredentialDeclaration, parseCenterCredentialFile } from "@sema-agent/settings-schema";
5
6
  export class CenterCredentialUnavailableError extends Error {
6
7
  reason;
7
8
  constructor(reason) {
@@ -10,20 +11,9 @@ export class CenterCredentialUnavailableError extends Error {
10
11
  this.name = "CenterCredentialUnavailableError";
11
12
  }
12
13
  }
13
- export const CENTER_TOKEN_ENV = "SEMA_REGISTRY_TOKEN";
14
- export const CENTER_TOKEN_FILE_ENV = "SEMA_REGISTRY_TOKEN_FILE";
15
14
  export function readCenterCredentialDeclaration(env = process.env) {
16
- const token = env.SEMA_REGISTRY_TOKEN ?? "";
17
- const file = env.SEMA_REGISTRY_TOKEN_FILE ?? "";
18
- if (token !== "" && file !== "") {
19
- throw new Error(`${CENTER_TOKEN_ENV} and ${CENTER_TOKEN_FILE_ENV} are both set — pick exactly one: ${CENTER_TOKEN_ENV} carries the credential value, ` +
20
- `${CENTER_TOKEN_FILE_ENV} names a file re-read on every request (rotation without restart). Two writers on one credential is a guess, not a config.`);
21
- }
22
- if (file !== "")
23
- return { from: "file", path: resolvePath(file) };
24
- if (token !== "")
25
- return { from: "env", token };
26
- return { from: "absent" };
15
+ const declared = judgeCenterCredentialDeclaration(env);
16
+ return declared.from === "file" ? { from: "file", path: resolvePath(declared.path) } : declared;
27
17
  }
28
18
  const digest16 = (s) => createHash("sha256").update(s, "utf8").digest("hex").slice(0, 16);
29
19
  function jwtSubject(bearer) {
@@ -38,56 +28,142 @@ function jwtSubject(bearer) {
38
28
  return { missing: "no sub claim" };
39
29
  return typeof parsed.sub === "string" && parsed.sub !== "" ? { sub: parsed.sub } : { missing: "sub is not a non-empty string" };
40
30
  }
31
+ function userTokenIdentity(bearer) {
32
+ const s = jwtSubject(bearer);
33
+ return "sub" in s ? `user-jwt:${digest16(s.sub)}` : `user-jwt-opaque:${digest16(bearer)}`;
34
+ }
41
35
  function classify(bearer, worker) {
42
- if (looksLikeJwt(bearer)) {
43
- const s = jwtSubject(bearer);
44
- return { ok: true, kind: "user-jwt", bearer, identity: "sub" in s ? `user-jwt:${digest16(s.sub)}` : `user-jwt-opaque:${digest16(bearer)}` };
45
- }
36
+ if (looksLikeJwt(bearer))
37
+ return { ok: true, kind: "user-jwt", bearer, identity: userTokenIdentity(bearer) };
46
38
  return { ok: true, kind: "pull-token", bearer, identity: pullTokenIdentity(worker), workerScoped: bearer.startsWith("wpt_") };
47
39
  }
48
40
  export function pullTokenIdentity(worker) {
49
41
  return `pull-token:${worker ?? "global"}`;
50
42
  }
51
43
  const NONE = { ok: true, kind: "none", identity: "none" };
52
- export function createCenterCredentialSource(declaration, worker, deps = {}) {
53
- switch (declaration.from) {
44
+ const PARSER_EMPTY_REASON = "empty";
45
+ function readCredentialText(text, spec, worker, now) {
46
+ const where = `${CENTER_CREDENTIAL_ENV.tokenFile} ${spec.path}`;
47
+ const file = parseCenterCredentialFile(text);
48
+ switch (file.kind) {
49
+ case "bare":
50
+ return classify(file.bearer, worker);
51
+ case "unreadable":
52
+ return file.reason === PARSER_EMPTY_REASON
53
+ ? { ok: false, failure: "missing", reason: `${where} is empty` }
54
+ : { ok: false, failure: "malformed", reason: `${where} does not hold a center credential (${file.reason})` };
55
+ case "envelope":
56
+ return readSession(file.envelope, where, spec.multiTenant, now);
57
+ }
58
+ }
59
+ function readSession(envelope, where, multiTenant, now) {
60
+ if (multiTenant) {
61
+ return {
62
+ ok: false,
63
+ failure: "multi_tenant_session",
64
+ reason: `${where} holds a personal sign-in session (a "user-session" envelope) and this deployment is multi-tenant (REQUIRE_PRINCIPAL=true) — a multi-tenant service never acts as one person's session; give it the worker pull token (one bare line) instead`,
65
+ };
66
+ }
67
+ const expiresAt = Date.parse(envelope.expiresAt);
68
+ if (!Number.isFinite(expiresAt)) {
69
+ return { ok: false, failure: "malformed", reason: `${where} holds an envelope whose expiresAt is not a readable instant` };
70
+ }
71
+ const identity = userTokenIdentity(envelope.accessToken);
72
+ if (expiresAt <= now) {
73
+ return { ok: false, failure: "expired", reason: `${where} holds a sign-in session whose access token expired at ${envelope.expiresAt} — not sent (the renewer re-mints it; sign in again if renewal keeps failing)`, identity, expiresAt };
74
+ }
75
+ return { ok: true, kind: "user-session", bearer: envelope.accessToken, identity, expiresAt };
76
+ }
77
+ export function createCenterCredentialSource(spec, worker, deps = {}) {
78
+ const listeners = new Set();
79
+ const onRead = (listener) => {
80
+ listeners.add(listener);
81
+ return () => {
82
+ listeners.delete(listener);
83
+ };
84
+ };
85
+ const observed = (r) => {
86
+ for (const listener of listeners)
87
+ listener(r);
88
+ return r;
89
+ };
90
+ switch (spec.from) {
54
91
  case "absent":
55
- return { origin: declaration, declared: false, read: () => NONE, health: () => ({ state: "ok" }) };
92
+ return { origin: spec, declared: false, read: () => observed(NONE), health: () => ({ state: "ok" }), onRead };
56
93
  case "env": {
57
- const fixed = classify(declaration.token, worker);
58
- return { origin: { from: "env" }, declared: true, read: () => fixed, health: () => ({ state: "ok" }) };
94
+ const fixed = classify(spec.token, worker);
95
+ return { origin: { from: "env" }, declared: true, read: () => observed(fixed), health: () => ({ state: "ok" }), onRead };
59
96
  }
60
97
  case "file": {
61
98
  const readFile = deps.readFile ?? ((p) => readFileSync(p, "utf8"));
62
99
  const now = deps.now ?? Date.now;
63
100
  let health = { state: "ok" };
64
- const fail = (reason) => {
65
- health = health.state === "unreadable" ? { ...health, reason, failures: health.failures + 1 } : { state: "unreadable", reason, since: now(), failures: 1 };
66
- return { ok: false, reason };
101
+ const readOnce = () => {
102
+ let text;
103
+ try {
104
+ text = readFile(spec.path);
105
+ }
106
+ catch (err) {
107
+ const code = err instanceof Error && "code" in err && typeof err.code === "string" ? err.code : "read error";
108
+ return { ok: false, failure: "missing", reason: `${CENTER_CREDENTIAL_ENV.tokenFile} ${spec.path} is unreadable (${code})` };
109
+ }
110
+ return readCredentialText(text, spec, worker, now());
67
111
  };
68
112
  return {
69
- origin: declaration,
113
+ origin: { from: "file", path: spec.path },
70
114
  declared: true,
71
115
  read() {
72
- let text;
73
- try {
74
- text = readFile(declaration.path);
75
- }
76
- catch (err) {
77
- const code = err instanceof Error && "code" in err && typeof err.code === "string" ? err.code : "read error";
78
- return fail(`${CENTER_TOKEN_FILE_ENV} ${declaration.path} is unreadable (${code})`);
79
- }
80
- const bearer = text.trim();
81
- if (bearer === "")
82
- return fail(`${CENTER_TOKEN_FILE_ENV} ${declaration.path} is empty`);
83
- health = { state: "ok" };
84
- return classify(bearer, worker);
116
+ const r = readOnce();
117
+ if (r.ok)
118
+ health = { state: "ok" };
119
+ else if (health.state === "unreadable")
120
+ health = { ...health, failure: r.failure, reason: r.reason, failures: health.failures + 1 };
121
+ else
122
+ health = { state: "unreadable", failure: r.failure, reason: r.reason, since: now(), failures: 1 };
123
+ return observed(r);
85
124
  },
86
125
  health: () => health,
126
+ onRead,
87
127
  };
88
128
  }
89
129
  }
90
130
  }
131
+ export const CENTER_CREDENTIAL_REFUSAL_CODES = {
132
+ malformed: "config.center_credential_malformed",
133
+ multi_tenant_session: "config.center_credential_user_session_multi_tenant",
134
+ };
135
+ export function judgeCenterCredentialAtBoot(read) {
136
+ if (read.ok)
137
+ return { verdict: "serve" };
138
+ switch (read.failure) {
139
+ case "missing":
140
+ return {
141
+ verdict: "loud",
142
+ failure: read.failure,
143
+ reason: read.reason,
144
+ effect: "every center request fails its own failure arm until the file is readable (config pull keeps env/LKG-less boot, caps fail-closed, lease takes the loud auth-error arm); no on-disk LKG / prompt state is read back (credential identity unknown)",
145
+ };
146
+ case "expired":
147
+ return {
148
+ verdict: "loud",
149
+ failure: read.failure,
150
+ reason: read.reason,
151
+ effect: "no center request carries the expired token; the renewer re-mints it now when renewal is on (CENTER_CREDENTIAL_RENEW_LEAD_MS > 0), otherwise requests fail their own failure arms until the shell rewrites the file; the on-disk LKG / prompt state of this identity is read back as usual",
152
+ };
153
+ case "malformed": {
154
+ const code = CENTER_CREDENTIAL_REFUSAL_CODES.malformed;
155
+ return {
156
+ verdict: "refuse",
157
+ code,
158
+ sentence: `center credential refused at boot [${code}]: ${read.reason}. The file must hold one bare token line, or the sign-in envelope the shell writes (a CenterCredentialEnvelope JSON object); sign in again so the shell rewrites it, or fix the file — its content is never echoed.`,
159
+ };
160
+ }
161
+ case "multi_tenant_session": {
162
+ const code = CENTER_CREDENTIAL_REFUSAL_CODES.multi_tenant_session;
163
+ return { verdict: "refuse", code, sentence: `center credential refused at boot [${code}]: ${read.reason}.` };
164
+ }
165
+ }
166
+ }
91
167
  export function scopedTokenNeedsWorker(read, worker) {
92
168
  return read.ok && read.kind === "pull-token" && read.workerScoped && !worker;
93
169
  }
@@ -95,8 +171,7 @@ export function centerStateBin(worker, identity) {
95
171
  return `${worker === undefined ? "global" : `worker-${digest16(worker)}`}.id-${digest16(identity)}`;
96
172
  }
97
173
  export function credentialIdentityNow(source) {
98
- const r = source.read();
99
- return r.ok ? r.identity : undefined;
174
+ return source.read().identity;
100
175
  }
101
176
  export function answerStillCurrent(source, usedIdentity) {
102
177
  return usedIdentity !== undefined && credentialIdentityNow(source) === usedIdentity;
@@ -121,11 +196,12 @@ function pinCredential(source, identity) {
121
196
  if (!r.ok)
122
197
  return r;
123
198
  if (identity === undefined) {
124
- return { ok: false, reason: "this state bin was opened while the center credential was unreadable (identity unknown) — it never talks to the center; the next call runs in the current identity's bin" };
199
+ return { ok: false, failure: "missing", reason: "this state bin was opened while the center credential was unreadable (identity unknown) — it never talks to the center; the next call runs in the current identity's bin" };
125
200
  }
126
- return r.identity === identity ? r : { ok: false, reason: "center credential identity changed — this request belongs to the previous identity's state bin and is not sent" };
201
+ return r.identity === identity ? r : { ok: false, failure: "missing", reason: "center credential identity changed — this request belongs to the previous identity's state bin and is not sent" };
127
202
  },
128
203
  health: () => source.health(),
204
+ onRead: (listener) => source.onRead(listener),
129
205
  };
130
206
  }
131
207
  //# sourceMappingURL=center-credential.js.map
@@ -1,6 +1,8 @@
1
1
  import { CROSS_SESSION_INBOUND_SETTINGS, CROSS_SESSION_DIALOG_EXPIRY_VALUES } from "@sema-agent/core";
2
+ import { CENTER_CREDENTIAL_ENV } from "@sema-agent/settings-schema";
3
+ import { CENTER_CREDENTIAL_RENEW_LEAD_DEFAULT_MS, CENTER_CREDENTIAL_RENEW_LEAD_MAX_MS, CENTER_CREDENTIAL_RENEW_LEAD_MIN_MS } from "./center-credential-renewer.js";
2
4
  import { THINKING_FORMATS } from "./model-compat.js";
3
- import { RESILIENCE_DEFAULTS, TASK_WRITE_FACE_ESCALATIONS } from "./config-types.js";
5
+ import { AUTO_MODE_CLASSIFIER_TIMEOUT_BOUNDS, DRAIN_SECOND_SIGNAL_MIN_GAP_BOUNDS, DRAIN_SECOND_SIGNAL_MIN_GAP_DEFAULT_MS, RESILIENCE_DEFAULTS, TASK_WRITE_FACE_ESCALATIONS } from "./config-types.js";
4
6
  import { webSearchProviderFromEnv, WEB_SEARCH_PROVIDERS } from "./plugins/web-search.js";
5
7
  import { AUTONOMY_MODES } from "./runtime-governance.js";
6
8
  import { REMOTE_EXEC_PROVIDER_WORDS } from "./execution-lane-caps.js";
@@ -78,6 +80,7 @@ export const CONFIG_CATALOG = [
78
80
  r("AUTH_BRIDGE_JWKS_URL", "auth", "url", "registry SSO-JWT 的 JWKS 地址(缺省从 issuer 派生)", { danger: SEC }),
79
81
  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") }),
80
82
  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" }),
83
+ 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" }),
81
84
  r("BAKE_CACHE_BASE", "orchestration", "url", "烤镜像 fetch-once 缓存源 base(注入 --cache-base;绝不受调用方控制)"),
82
85
  r("BAKE_IMAGE_REGISTRY", "orchestration", "string", "烤镜像自动登记的 registry host 前缀(repo=registry/profile)"),
83
86
  r("BAKE_LEASE_MS", "orchestration", "number", "bake 租约窗(ms;runner 心跳推进,过期被 reaper 抢)", { staticDefault: "90000" }),
@@ -89,6 +92,7 @@ export const CONFIG_CATALOG = [
89
92
  r("BG_AGENT_RETENTION_MS", "orchestration", "number", "TERMINAL 后台 agent 行保留期(ms,reaper 按 scope 清)", { staticDefault: String(7 * 24 * 60 * 60 * 1000), danger: DATA, configKey: "backgroundAgentRetentionMs" }),
90
93
  r("BG_AGENT_STALE_RUNNING_MS", "orchestration", "number", "running 后台 agent 行写者心跳断更超此值翻 failed(ms;下限 10min 硬钳)", { staticDefault: String(15 * 60_000), configKey: "backgroundAgentStaleRunningMs" }),
91
94
  r("BIND_HOST", "auth", "string", "监听绑址(显式值恒生效)", { danger: SEC, derivedDefaultNote: "缺席:无鉴权写面(allowUnauthedWrites ∧ 零凭证)自动收窄 127.0.0.1,其余全接口", resolve: (c) => c.bindHost ?? null }),
95
+ r("CENTER_CREDENTIAL_RENEW_LEAD_MS", "integrations", "number", `center 凭证续签提前量(ms;${CENTER_CREDENTIAL_ENV.tokenFile} 指向人的登录会话信封时,访问令牌离到期 ≤ 本值即由引擎续签并原子改写同一文件;0 = 关续签只读信封;其余须为 [${CENTER_CREDENTIAL_RENEW_LEAD_MIN_MS}, ${CENTER_CREDENTIAL_RENEW_LEAD_MAX_MS}] 内整数,坏值拒启;裸行 / 值形凭证不续签)`, { staticDefault: String(CENTER_CREDENTIAL_RENEW_LEAD_DEFAULT_MS), danger: SEC, configKey: "centerCredentialRenewLeadMs" }),
92
96
  r("COMPLIANCE_ADDITIONAL_DENIES", "auth", "csv", "合规档位之上额外拒绝的能力名(只能收紧;词表=core 闭集,拼错拒启)", { danger: SEC }),
93
97
  r("COMPLIANCE_PROFILE", "auth", "enum", "本部署合规档位(core compliancePostureResolver 供数;拼错拒启)", { danger: SEC, enumValues: ["hipaa", "zdr"], resolve: (c) => c.compliancePosture?.profile ?? null }),
94
98
  r("CONFIG_BOOT_FETCH_BUDGET_MS", "integrations", "number", "boot 首拉配置的抢答窗(ms;过窗以 env/兜底起服,拉取转后台热应用)", { staticDefault: "1500", configKey: "configBootFetchBudgetMs" }),
@@ -128,6 +132,7 @@ export const CONFIG_CATALOG = [
128
132
  r("DOCKER_PIDS_LIMIT", "orchestration", "number", "local-docker fork-bomb 背栓(--pids-limit;非法值回 adapter 默认 512+warn)", { danger: SEC, derivedDefaultNote: "缺席 ⇒ adapter 默认 512" }),
129
133
  r("DOCKER_SANDBOX_ENV", "orchestration", "secret", "local-docker 车道每条 docker exec 以 -e 注入的本进程 env 点名表(NAME,NAME;点到的键从 worker env 原样取,值不进 spec/center/日志;与 LSP_ENV_ALLOW / E2B_SANDBOX_ENV 同一只解析器与「点名放行」谓词:形错 / 点名名单型旋钮拒启且不回显值;本进程环境里没有的名按序号 warn、不转发;只在本车道被选中时求值;原文按凭据类对待:只回在场位、不进任何子进程)", { danger: SEC }),
130
134
  r("DRAIN_GRACE_MS", "limitsHttp", "number", "SIGTERM 优雅排空窗(ms;期间新计费提交 503+Retry-After)。出厂默认是**服务器部署**的取值;壳自 spawn 的引擎会被壳注入更短的窗 ⇒ 排壳形关机预算时以本行的 effectiveValue(= 本进程实际生效值)为准,不照抄 staticDefault", { staticDefault: String(10 * 60 * 1000), configKey: "drainGraceMs", center: cLimits("drainGraceMs") }),
135
+ r("DRAIN_SECOND_SIGNAL_MIN_GAP_MS", "limitsHttp", "number", `排空期「第二发信号 = 硬停」的最小间隔(ms;锚 = 排空起点。SIGTERM / SIGINT 与起点相隔不足 ⇒ 同一拍的重复投递,一行 drain_duplicate_signal_ignored 后忽略、排空继续;0 = 关,恒按第二发硬停;须为 [${DRAIN_SECOND_SIGNAL_MIN_GAP_BOUNDS.minMs}, ${DRAIN_SECOND_SIGNAL_MIN_GAP_BOUNDS.maxMs}] 内十进制整数(上界 = DRAIN_GRACE_MS 地板),小数 / 负数 / 越界拒启)`, { staticDefault: String(DRAIN_SECOND_SIGNAL_MIN_GAP_DEFAULT_MS), configKey: "drainSecondSignalMinGapMs" }),
131
136
  r("DURABLE_APPROVAL", "approval", "boolean", "durable suspend/resume HITL(需 SQL 店;决定 checkpointStore 与 /v1/approvals 面的存在性)", { danger: SEC, derivedDefaultNote: "posture 三态(S-178):显式词恒赢,缺席 = 单用户(REQUIRE_PRINCIPAL≠true)且有耐久店 ⇒ on / 多租户或无耐久店 ⇒ off;生效来源见 GET /v1/diagnostics/wiring 的 serverGates.durableApproval.source", configKey: "durableApproval" }),
132
137
  r("E2B_ALLOW_NET", "orchestration", "boolean", "e2b 沙箱出网许可", { danger: SEC, derivedDefaultNote: "缺席 ⇒ 不铸键(adapter 默认允许)" }),
133
138
  r("E2B_API_KEY", "orchestration", "secret", "E2B 车道 API 凭据(REMOTE_EXEC=e2b 必需件)", { danger: [...SEC, ...BILL] }),
@@ -157,7 +162,7 @@ export const CONFIG_CATALOG = [
157
162
  r("HOST_BG_DEFAULT_TIMEOUT_SEC", "orchestration", "number", "host 后台 shell 缺省超时(秒)", { staticDefault: "300" }),
158
163
  r("HOST_BG_ENABLED", "orchestration", "boolean", "host 车道后台 shell 开关", { staticDefault: "true" }),
159
164
  r("HOST_BG_FILE_CAP", "orchestration", "number", "host 后台 shell 磁盘护栏(字节;失控输出被 reap)", { staticDefault: String(64 * 1024 * 1024) }),
160
- r("HOST_BG_KILL_GRACE_MS", "orchestration", "number", "host 后台 shell SIGTERM→SIGKILL 宽限(ms)", { staticDefault: "1000" }),
165
+ r("HOST_BG_KILL_GRACE_MS", "orchestration", "number", "host 后台 shell SIGTERM→SIGKILL 宽限(ms);会话删除 / 会话停止口在软停与硬收之间等同一只", { staticDefault: "1000" }),
161
166
  r("HOST_BG_MAX_CONCURRENT", "orchestration", "number", "host 后台 shell 并发帽", { staticDefault: "8" }),
162
167
  r("HOST_BG_MAX_TIMEOUT_SEC", "orchestration", "number", "host 后台 shell 超时上限(秒)", { staticDefault: "1800" }),
163
168
  r("HOST_BG_MEM_CAP", "orchestration", "number", "host 后台 shell 内存流护栏(字节)", { staticDefault: String(8 * 1024 * 1024) }),
@@ -395,8 +400,8 @@ export const CONFIG_CATALOG = [
395
400
  r("SEMA_PIP_INDEX", "orchestration", "url", "SANDBOX_PKG_SOURCE=custom 的显式 pip index 地址"),
396
401
  r("SEMA_REGISTRY_ADMIN_TOKEN", "integrations", "secret", "写 center 的管理凭证(仅 boot 上报执行公钥用;刻意不入 ServiceConfig —— 不进 configHash/日志面)", { danger: SEC, scope: "bootstrap-only" }),
397
402
  r("SEMA_REGISTRY_DRY_RUN", "integrations", "boolean", "center 腿 dry-run(拉而不应用)", { staticDefault: "false" }),
398
- r("SEMA_REGISTRY_TOKEN", "integrations", "secret", "center 拉取凭据(值形;与 SEMA_REGISTRY_TOKEN_FILE 二选一,都设=拒启)", { danger: SEC }),
399
- r("SEMA_REGISTRY_TOKEN_FILE", "integrations", "path", "center 拉取凭据文件(每条中心请求都重读 ⇒ 轮换 / 换账号不重启;与 SEMA_REGISTRY_TOKEN 二选一,都设=拒启;文件不可读=响亮 + 健康态 unreadable,不当作无凭证)", { danger: SEC }),
403
+ r(CENTER_CREDENTIAL_ENV.token, "integrations", "secret", `center 拉取凭据(值形;与 ${CENTER_CREDENTIAL_ENV.tokenFile} 二选一,都设=拒启)`, { danger: SEC }),
404
+ r(CENTER_CREDENTIAL_ENV.tokenFile, "integrations", "path", `center 拉取凭据文件(每条中心请求都重读 ⇒ 轮换 / 换账号不重启;与 ${CENTER_CREDENTIAL_ENV.token} 二选一,都设=拒启;内容两种类:裸行 = 静态 bearer / JSON 信封 user-session = 人的登录会话(可续签,S-698);文件不可读 / 为空 = 响亮 + 健康态 unreadable,不当作无凭证;内容读不成 / 多租户读到信封 = 拒启)`, { danger: SEC }),
400
405
  r("SEMA_REGISTRY_URL", "integrations", "url", "sema-registry 地址(设了=remote 配置源开关)"),
401
406
  r("SEMA_REGISTRY_WORKER", "integrations", "string", "本 worker 在 center 的名字(per-worker effective)"),
402
407
  r("SEMA_UV_INDEX", "orchestration", "url", "SANDBOX_PKG_SOURCE=custom 的显式 uv index 地址"),
@@ -8,7 +8,7 @@ import { stageLimits } from "./stage-limits.js";
8
8
  import { APPROVAL_GROUP_KEYS, BATCH1_LIMIT_KEYS } from "./hot-keys-registry.js";
9
9
  import { buildDisplaySafeUrl } from "../trace/redact.js";
10
10
  import { crossInvariant } from "../config-invariants.js";
11
- import { judgeCatalogEntries, mintedRefusedNames, refusalEventOf, refusalWarnFields, REFUSED_NAME_MINTED_CODE, unreadableDroppedRefusal } from "../model-entry-refusal.js";
11
+ import { judgeCatalogEntries, mintedRefusedNames, refusalEventOf, refusalWarnFields, malformedDroppedCarrierRefusal } from "../model-entry-refusal.js";
12
12
  import { gateIntentUnserviceableReason } from "../approval.js";
13
13
  import { refusedGroups } from "./apply-ledger.js";
14
14
  const charsPerTokenOf = (v) => (typeof v === "number" && Number.isFinite(v) && v > 0 ? v : undefined);
@@ -215,11 +215,10 @@ function modelPlaneGroupReport(group) {
215
215
  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)";
216
216
  function stageModelPlane(config, eff, logger, sealedKeys) {
217
217
  const enabled = (eff.models?.models ?? []).filter((m) => m.enabled !== false);
218
- const droppedEnabled = eff.models?.droppedIdentities;
219
- if (enabled.length === 0 && !(Array.isArray(droppedEnabled) && droppedEnabled.length > 0))
218
+ const verdict = judgeCatalogEntries(enabled, { anthropicRouteMounted: config.anthropic !== undefined }, eff.models?.droppedIdentities);
219
+ if (enabled.length === 0 && Object.keys(verdict.refused).length === 0 && verdict.malformed.length === 0)
220
220
  return { stages: { collab: stageCollabDomain(eff) } };
221
221
  {
222
- const verdict = judgeCatalogEntries(enabled, { anthropicRouteMounted: config.anthropic !== undefined }, eff.models.droppedIdentities);
223
222
  for (const [name, r] of Object.entries(verdict.refused))
224
223
  logger?.warn(refusalEventOf(r.reason), refusalWarnFields(name, r));
225
224
  const refuse = (invariant, reason, detail = {}) => {
@@ -232,14 +231,8 @@ function stageModelPlane(config, eff, logger, sealedKeys) {
232
231
  });
233
232
  return { stages: {}, rejected: { keys: MODEL_PLANE_GROUP_KEYS, reason, invariant }, refused: verdict.refused };
234
233
  };
235
- if (verdict.unreadable.length > 0) {
236
- logger?.warn("sema_registry_model_entry_dropped_unproven", {
237
- entries: [...verdict.unreadable],
238
- code: REFUSED_NAME_MINTED_CODE,
239
- hint: "these catalog entries were dropped by the settings-schema read and the raw document could not be matched to the snapshot that was parsed (e.g. the local file changed between two reads) — their names cannot be proven, so no tombstone can be minted and the candidate's model plane is refused (a name nobody can prove cannot be shown not to be re-minted by the tier/alias expansion); the next read re-judges",
240
- });
241
- return refuse("refused-names-not-minted", unreadableDroppedRefusal(verdict.unreadable));
242
- }
234
+ if (verdict.malformed.length > 0)
235
+ return refuse("refused-names-not-minted", malformedDroppedCarrierRefusal(verdict.malformed), { entries: [...verdict.malformed] });
243
236
  const usable = verdict.usable;
244
237
  const models = {};
245
238
  const modelApiKeyEnv = {};
@@ -1,5 +1,7 @@
1
1
  import { type CenterCredentialSource } from "../center-credential.js";
2
2
  export declare const SCHEMA_MAJOR_HEADER = "x-sema-schema-major";
3
+ /** 8s:对中心请求 / 响应式调用的传输上界(拉配置 / 两条 principal 视图 / 内容拉取 / 凭证续签同一值)。 */
4
+ export declare const CENTER_PULL_TIMEOUT_MS = 8000;
3
5
  /** 已装 settings-schema 的 major(机器派生)。版本号不成形 ⇒ 模块装载即抛(装坏了的包不该带着一个猜出来的
4
6
  * major 去跟中心协商 wire 形)。 */
5
7
  export declare const SETTINGS_SCHEMA_MAJOR: string;
@@ -49,6 +51,17 @@ export interface CenterRequestInit {
49
51
  readonly timeoutMs: number;
50
52
  readonly fetchImpl?: typeof fetch;
51
53
  }
54
+ /**
55
+ * S-698 —— 凭证续签:`POST {issuer}/api/v1/auth/token/refresh`,form `refresh_token=…`(settings-schema `AUTH_V1_PATHS.tokenRefresh`)。
56
+ * · 以 `issuer` 为根(可带路径前缀;同源与否由续签器先判,本函数只装配);query / fragment 不带过去。
57
+ * · **不带 Authorization**:这条端点凭 refresh token 认人,手里的 access token 可能已经过期。
58
+ * · `redirect: "manual"`:3xx 不跟 —— POST 307/308 会把体(= refresh token)原样带去 `Location`,同源判据在重定向面前失效。
59
+ * 3xx 原样交回,续签器按「被拒」处置。
60
+ */
61
+ export declare function centerTokenRefresh(issuer: string, refreshToken: string, init: {
62
+ readonly timeoutMs: number;
63
+ readonly fetchImpl?: typeof fetch;
64
+ }): Promise<Response>;
52
65
  /**
53
66
  * 对中心发一条请求。返回响应与**这次请求所用凭证的身份**(调用方据此给拿回来的候选打分箱标记,
54
67
  * 见 `boot/config-center.ts` 的 LKG / prompt 状态分箱)。
@@ -1,6 +1,8 @@
1
1
  import settingsSchemaPackage from "@sema-agent/settings-schema/package.json" with { type: "json" };
2
+ import { AUTH_V1_PATHS } from "@sema-agent/settings-schema/api/auth";
2
3
  import { CenterCredentialUnavailableError } from "../center-credential.js";
3
4
  export const SCHEMA_MAJOR_HEADER = "x-sema-schema-major";
5
+ export const CENTER_PULL_TIMEOUT_MS = 8000;
4
6
  export const SETTINGS_SCHEMA_MAJOR = (() => {
5
7
  const version = settingsSchemaPackage.version;
6
8
  const m = /^(\d+)\.\d+\.\d+/.exec(version);
@@ -19,6 +21,7 @@ export function pullViewQuery(view) {
19
21
  function contentRoot(kind) {
20
22
  switch (kind) {
21
23
  case "user-jwt":
24
+ case "user-session":
22
25
  return "/api/v1/me";
23
26
  case "pull-token":
24
27
  case "none":
@@ -45,10 +48,27 @@ function routePath(route, kind) {
45
48
  return "/api/config/execution-keys";
46
49
  }
47
50
  }
48
- export async function centerFetch(center, route, init) {
49
- const scheme = new URL(center.baseUrl).protocol;
51
+ function assertHttpScheme(url, what) {
52
+ const scheme = url.protocol;
50
53
  if (scheme !== "http:" && scheme !== "https:")
51
- throw new Error(`SEMA_REGISTRY_URL must be http(s), got "${scheme}"`);
54
+ throw new Error(`${what} must be http(s), got "${scheme}"`);
55
+ }
56
+ export async function centerTokenRefresh(issuer, refreshToken, init) {
57
+ const url = new URL(issuer);
58
+ assertHttpScheme(url, "center credential issuer");
59
+ url.pathname = `${url.pathname.replace(/\/+$/, "")}${AUTH_V1_PATHS.tokenRefresh}`;
60
+ url.search = "";
61
+ url.hash = "";
62
+ return (init.fetchImpl ?? fetch)(url, {
63
+ method: "POST",
64
+ headers: { [SCHEMA_MAJOR_HEADER]: SETTINGS_SCHEMA_MAJOR, "content-type": "application/x-www-form-urlencoded" },
65
+ body: new URLSearchParams({ refresh_token: refreshToken }).toString(),
66
+ redirect: "manual",
67
+ signal: AbortSignal.timeout(init.timeoutMs),
68
+ });
69
+ }
70
+ export async function centerFetch(center, route, init) {
71
+ assertHttpScheme(new URL(center.baseUrl), "SEMA_REGISTRY_URL");
52
72
  const cred = center.credential.read();
53
73
  if (!cred.ok)
54
74
  throw new CenterCredentialUnavailableError(cred.reason);
@@ -49,7 +49,7 @@
49
49
  */
50
50
  export { fetchEffective, fetchPrincipalCaps, fetchPrincipalOrgMemory, ConfigCenterHttpError, fetchSkillContent, fetchPromptArtifact, fetchPromptBlob, } from "./http-client.js";
51
51
  export { mutateInPlace, applyEffective, type ApplyReport, applyRuntimeGates, applyRuntimeHot, resolveDefaultModelName, logEffectiveDiff, MODEL_PLANE_DOMAINS, type ModelPlaneDomain, type ModelPlaneOf, withModelPlaneOf, buildEnvOnlyModelPlane, } from "./apply-effective.js";
52
- export { restartReasons, modelPlaneChanged, type RestartSlice, type RestartSliceCtx, type RestartSignal, } from "./restart-signal.js";
52
+ export { restartReasons, modelPlaneChanged, type RestartSlice, type RestartSliceCtx, type EngineRestartSignal, } from "./restart-signal.js";
53
53
  export { applyCenterSkills, resolveMcpServers, mcpForScenario, resolveA2aPeers, a2aForScenario } from "./skills-mcp.js";
54
54
  export { applyCenterReadFace, CENTER_READ_FACE_LIVE_KEYS, type CenterReadFaceApply, type CenterReadFaceReport, type ReadFaceBaseline } from "./read-face.js";
55
55
  export type { CenterSkillManifest, CenterMcpServer, CenterA2aPeer, CenterModel, CenterReadFace, EffectiveConfig, ExecutionRuling, SessionMirrorRuling, } from "./types.js";
@@ -2,9 +2,8 @@ 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
4
  import { splitFoldedReadWarnings, withDroppedIdentities } from "./read-warnings.js";
5
- import { centerFetch } from "./center-request.js";
5
+ import { CENTER_PULL_TIMEOUT_MS, centerFetch } from "./center-request.js";
6
6
  import { answerStillCurrent } from "../center-credential.js";
7
- const CENTER_PULL_TIMEOUT_MS = 8000;
8
7
  export async function fetchEffective(center, etag, fetchImpl = fetch, worker) {
9
8
  const { res, identity } = await centerFetch(center, { kind: "effective", worker }, { etag, timeoutMs: CENTER_PULL_TIMEOUT_MS, fetchImpl });
10
9
  if (res.status === 304)
@@ -19,9 +18,7 @@ export async function fetchEffective(center, etag, fetchImpl = fetch, worker) {
19
18
  delete wire.prompts;
20
19
  else
21
20
  wire.prompts = centerPrompts;
22
- const rawModels = typeof body === "object" && body !== null && !Array.isArray(body) ? body.models : undefined;
23
- const parsedModels = wire.models?.models;
24
- const { domainErrors, droppedEntries, droppedIdentities } = splitFoldedReadWarnings(warnings, { raw: rawModels, parsed: Array.isArray(parsedModels) ? parsedModels : undefined });
21
+ const { domainErrors, droppedEntries, droppedIdentities } = splitFoldedReadWarnings(warnings);
25
22
  const effective = withDroppedIdentities(wire, droppedIdentities);
26
23
  return {
27
24
  effective,
@@ -25,7 +25,7 @@
25
25
  * 候选门整拒,而它在 1.10.0 上零告警零 domainErrors(亲验读数)。兼容臂的关窗观测归属主(settings-schema 的
26
26
  * `onWarning` 流),不借候选门这条带副作用的通道传信息。
27
27
  */
28
- import { type ConfigPath, type EffectiveReadWarning } from "@sema-agent/settings-schema";
28
+ import { type EffectiveReadWarning } from "@sema-agent/settings-schema";
29
29
  /** 折叠产物:`scope` 决定消费方做什么 —— `"domain"` 进候选门(`domainErrors`),`"entry"` 进逐条丢弃面(`droppedEntries`)。 */
30
30
  export type FoldedReadWarning = {
31
31
  readonly scope: "domain";
@@ -58,72 +58,43 @@ export declare function describeSchemaIssueFields(error: unknown): string[];
58
58
  /** null = 不入诊断面(`hosts-grandfathered` / `legacy-governance-lifted`:值已被收编接受,与本地店同口径)。 */
59
59
  export declare function foldEffectiveReadWarning(w: EffectiveReadWarning): FoldedReadWarning | null;
60
60
  /**
61
- * 🔴 S-712 —— 一条被 schema 逐条丢掉的**启用**目录条目的身份(模型面墓碑的来源;载体 = `EffectiveConfig.models.droppedIdentities`,
61
+ * 🔴 S-712 —— 一条被 schema 逐条丢掉的**启用**、**有名**目录条目的身份(模型面墓碑的来源;载体 = `EffectiveConfig.models.droppedIdentities`,
62
62
  * 随 effective 走 —— 盘上 LKG 从同一个对象落盘,重启从 LKG 起服墓碑不丢)。
63
63
  *
64
64
  * 为什么要交出身份:条目被丢之后它的名字从目录里消失,而档位增广(core `expandTiers`)会把一个**空出来的**档位词 / CC 别名
65
65
  * (条目恰好叫 `sonnet` / `pro` …)重新铸到另一条条目上 —— 显式点它的请求与点名它的角色绑定就这样被静默换了目的地
66
66
  * (gpt-6 腿 F2 / F3,真复现)。身份进 `modelRefusals` 之后,不变量 `refused-names-not-minted` 才判得到它。
67
67
  *
68
- * 🔐 纯身份,不含值:`name` / `id` / `provider` 只取**字符串**(条目自己写的词,本就进日志与拒因句),`fields` 是坏字段的
69
- * **键路径**,`issues` 是 {@link describeSchemaIssues} 的一行(键名 + 类型词)—— 被丢条目的原值(可能是一段贴错位置的凭据)
70
- * 绝不进载体、日志、LKG 与 400 拒因句。
68
+ * 🔐 纯身份,不含值:`name` / `id` 取自告警的 `identity`(settings-schema 6.1.0:只取被丢条目原文里的字符串形,脱敏器会改动的串
69
+ * 整键扣下),`fields` 是坏字段的**键路径**,`issues` 是 {@link describeSchemaIssues} 的一行(键名 + 类型词)—— 被丢条目的原值
70
+ * (可能是一段贴错位置的凭据)绝不进载体、日志、LKG 与 400 拒因句。
71
71
  */
72
72
  export interface DroppedModelIdentity {
73
73
  /** 出门形坐标(`models.1`),与同一次读的 `droppedEntries[].entry` 同一串。 */
74
74
  readonly entry: string;
75
- /** 条目写的 `name`。**缺席 = 身份证明不了**(只剩一形:原文档与本次解析对不上 / 坐标在原文档里不存在,见 {@link droppedEntryIdentity})
76
- * ⇒ 模型面整组拒:证明不了的名字无从证明它不被增广铸出。原文档那一格读到了却没有字符串名的行**不进**载体(没有可保留的名字,
77
- * 与停用同处置 —— 7.101.0 验修轮 V-2)。 */
78
- readonly name?: string;
75
+ /** 条目写的 `name`(读腿只为有名的行铸载体)。载体上缺了它 = 载体形不对(LKG 载体被改 / 手写 effective)⇒ 模型面整组拒,
76
+ * 判在 `model-entry-refusal.ts` `judgeCatalogEntries`。 */
77
+ readonly name: string;
79
78
  readonly id?: string;
80
- readonly provider?: string;
81
79
  readonly fields: readonly string[];
82
80
  readonly issues: string;
83
81
  }
84
- /** 本次读的模型域**原文**与**解析产物**(两腿各自手里都有这两件:远端 = 响应体与 `readEffectiveWire` 输出;本地 = 盘上
85
- * `config.d/models.json` 与 `FileConfigStore` 输出)。 */
86
- export interface ModelsDomainSource {
87
- /** 模型域的原始文档(`{ models: [...], roles, … }`);读不到 ⇒ `undefined`(身份一律证明不了)。 */
88
- readonly raw: unknown;
89
- /** 同一次读解析后幸存的条目(schema 逐条丢弃之后的 `models.models`)。 */
90
- readonly parsed: readonly unknown[] | undefined;
91
- }
92
- /**
93
- * 一条被丢行在原文档里那一格的读数 —— **三态**(7.101.0 验修轮 V-2,规则变小:「读不出」只剩一种含义):
94
- * · `named`:格读到了、`name` 是字符串 ⇒ 身份已定(启用的铸墓碑,停用的不是);
95
- * · `nameless`:格读到了、没有字符串 `name`(`null` / 数 / 整格不是对象)⇒ 身份**也**已定:**没有可保留的名字** —— 请求、
96
- * `@`-mention、角色绑定都点不到一个非字符串名,档位增广也铸不出它 ⇒ 与 `enabled:false` 同处置(不是墓碑、不拒组);
97
- * · `undefined`(由 {@link droppedEntryIdentity} 答):格读不到 —— 坐标在原文档里不存在;调用方在原文档与本次解析对不上
98
- * (`sameRead` 假)时根本不来读 ⇒ 两者同是「身份证明不了」,交一条不带 `name` 的载体(模型面据此整组拒)。
99
- * 此前 `nameless` 被折进「读不出」⇒ 一行 `{ name: null }` 的坏行(哪怕是停用的)就让整个模型面被拒(codex 验修腿 V-2,boot 腿对
100
- * v7.100.0 的窄回归:那时 boot 对同一文档宽容采纳幸存条目)。
101
- */
102
- export type DroppedEntryCell = {
103
- readonly kind: "named";
104
- readonly name: string;
105
- readonly id?: string;
106
- readonly provider?: string;
107
- readonly enabled: boolean;
108
- } | {
109
- readonly kind: "nameless";
110
- };
111
- /**
112
- * 🔴 S-712 **唯一**的身份读法:按告警坐标(`path`,模型域文档内的段坐标 —— settings-schema 已把它回写成原文档坐标)回读
113
- * 原文档那一格,答 {@link DroppedEntryCell} 三态之一;坐标在原文档里走不通 ⇒ `undefined`(格读不到)。字符串才算
114
- * (`name` / `id` / `provider`),`enabled` 只认字面 `false` 为停用。
115
- * settings-schema 6.1.0 的 `entry-dropped` 自带 `name` / `id` 到货后,本函数改读告警(同一只函数,换数据源),原文档入参随之删除。
116
- */
117
- export declare function droppedEntryIdentity(rawDoc: unknown, path: ConfigPath): DroppedEntryCell | undefined;
118
82
  /**
119
83
  * 两条读腿(远端 `http-client.ts` / 本地 `config-provider.ts`)共用的**分面单点**:折叠产物按 `scope` 分进候选门
120
84
  * (`domainErrors`)与逐条丢弃面(`droppedEntries`)。`switch` 穷举 —— 加一种 scope 不补臂 = 编译红,两腿不会各分各的。
121
85
  * 文案**未脱敏**:进日志流之前各腿在自己的产出边界上过 `redactConfigError`(与 `domainErrors` 同一道)。
122
- * S-712:同一次折叠把被丢**模型目录条目**的身份交出(`droppedIdentities`,见 {@link DroppedModelIdentity}),按 {@link DroppedEntryCell}
123
- * 三态分流:有名且启用 ⇒ 墓碑;停用的、没有字符串名的 ⇒ 不进载体(停用 = 不在目录,与 7.100.0 同义;无名 = 没有可保留的名字);
124
- * 格读不到 / 原文档对不上 ⇒ 不带 `name` 的载体(证明不了)。坏行本身照样走 `droppedEntries`(坐标 + 字段,不带值)。
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`(坐标 + 字段,不带值)。
125
96
  */
126
- export declare function splitFoldedReadWarnings(warnings: readonly EffectiveReadWarning[], models: ModelsDomainSource): {
97
+ export declare function splitFoldedReadWarnings(warnings: readonly EffectiveReadWarning[]): {
127
98
  domainErrors: Array<{
128
99
  domain: string;
129
100
  error: string;
@@ -68,38 +68,10 @@ export function foldEffectiveReadWarning(w) {
68
68
  }
69
69
  }
70
70
  }
71
- const isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
72
- export function droppedEntryIdentity(rawDoc, path) {
73
- let at = rawDoc;
74
- for (const seg of path) {
75
- if (typeof seg === "number") {
76
- if (!Array.isArray(at) || !Number.isInteger(seg) || seg < 0 || seg >= at.length)
77
- return undefined;
78
- at = at[seg];
79
- }
80
- else {
81
- if (!isRecord(at) || !Object.hasOwn(at, seg))
82
- return undefined;
83
- at = at[seg];
84
- }
85
- }
86
- if (!isRecord(at) || typeof at.name !== "string")
87
- return { kind: "nameless" };
88
- return { kind: "named", name: at.name, ...(typeof at.id === "string" ? { id: at.id } : {}), ...(typeof at.provider === "string" ? { provider: at.provider } : {}), enabled: at.enabled !== false };
89
- }
90
71
  function isModelEntryDrop(w) {
91
72
  return w.kind === "entry-dropped" && w.domain === "models" && w.path.length === 2 && w.path[0] === "models" && typeof w.path[1] === "number";
92
73
  }
93
- function sameRead(source, dropped) {
94
- const rawModels = isRecord(source.raw) ? source.raw.models : undefined;
95
- if (!Array.isArray(rawModels) || source.parsed === undefined)
96
- return false;
97
- if ([...dropped].some((i) => i >= rawModels.length))
98
- return false;
99
- const survivors = rawModels.filter((_, i) => !dropped.has(i));
100
- return survivors.length === source.parsed.length && survivors.every((s, k) => isRecord(s) && isRecord(source.parsed[k]) && s.name === source.parsed[k].name);
101
- }
102
- export function splitFoldedReadWarnings(warnings, models) {
74
+ export function splitFoldedReadWarnings(warnings) {
103
75
  const domainErrors = [];
104
76
  const droppedEntries = [];
105
77
  for (const w of warnings) {
@@ -119,28 +91,14 @@ export function splitFoldedReadWarnings(warnings, models) {
119
91
  }
120
92
  }
121
93
  }
122
- const modelDrops = warnings.filter(isModelEntryDrop);
123
- const readable = sameRead(models, new Set(modelDrops.map((w) => w.path[1])));
124
94
  const droppedIdentities = [];
125
- for (const w of modelDrops) {
126
- const unproven = { entry: formatConfigPath(w.path), fields: describeSchemaIssueFields(w.error), issues: describeSchemaIssues(w.error) ?? "(schema issues unreadable)" };
127
- const cell = readable ? droppedEntryIdentity(models.raw, w.path) : undefined;
128
- if (cell === undefined) {
129
- droppedIdentities.push(unproven);
95
+ for (const w of warnings) {
96
+ if (!isModelEntryDrop(w))
130
97
  continue;
131
- }
132
- switch (cell.kind) {
133
- case "nameless":
134
- break;
135
- case "named":
136
- if (cell.enabled)
137
- droppedIdentities.push({ ...unproven, name: cell.name, ...(cell.id !== undefined ? { id: cell.id } : {}), ...(cell.provider !== undefined ? { provider: cell.provider } : {}) });
138
- break;
139
- default: {
140
- const never = cell;
141
- throw new Error(`unhandled dropped-entry cell: ${JSON.stringify(never)}`);
142
- }
143
- }
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)" });
144
102
  }
145
103
  return { domainErrors, droppedEntries, droppedIdentities };
146
104
  }