@sema-agent/server 7.97.0 → 7.99.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 (70) hide show
  1. package/MIGRATION.md +11 -0
  2. package/README.md +3 -3
  3. package/README.zh-CN.md +3 -3
  4. package/USAGE.md +18 -9
  5. package/deploy/sema-up/cluster-up.sh +8 -4
  6. package/deploy/sema-up/kube-up.sh +40 -15
  7. package/deploy/sema-up/lib/envf.sh +241 -0
  8. package/deploy/sema-up/sema-up.sh +162 -101
  9. package/deploy/sema-up/smoke.sh +8 -1
  10. package/dist/boot/config-center.js +169 -24
  11. package/dist/boot/org-memory.d.ts +2 -1
  12. package/dist/boot/org-memory.js +9 -5
  13. package/dist/boot/resolve-spec.js +9 -4
  14. package/dist/boot/runtime-caps.js +4 -4
  15. package/dist/brain.js +28 -18
  16. package/dist/capabilities/center-prompts.d.ts +3 -0
  17. package/dist/capabilities/center-prompts.js +1 -1
  18. package/dist/center-credential.d.ts +112 -0
  19. package/dist/center-credential.js +131 -0
  20. package/dist/config-catalog.js +7 -6
  21. package/dist/config-center/apply-effective.js +7 -1
  22. package/dist/config-center/center-request.d.ts +60 -0
  23. package/dist/config-center/center-request.js +71 -0
  24. package/dist/config-center/facade.d.ts +2 -1
  25. package/dist/config-center/http-client.d.ts +17 -9
  26. package/dist/config-center/http-client.js +27 -36
  27. package/dist/config-center/skills-mcp.d.ts +4 -1
  28. package/dist/config-center/skills-mcp.js +2 -2
  29. package/dist/config-invariants.d.ts +2 -2
  30. package/dist/config-invariants.js +9 -9
  31. package/dist/config-lkg.d.ts +47 -8
  32. package/dist/config-lkg.js +107 -20
  33. package/dist/config-provider.d.ts +8 -7
  34. package/dist/config-provider.js +5 -5
  35. package/dist/config-types.d.ts +20 -6
  36. package/dist/config.d.ts +9 -0
  37. package/dist/config.js +53 -18
  38. package/dist/fleet-client.d.ts +8 -8
  39. package/dist/fleet-client.js +7 -7
  40. package/dist/fleet-lease.d.ts +11 -11
  41. package/dist/fleet-lease.js +17 -13
  42. package/dist/hooks/hook-llm.d.ts +1 -1
  43. package/dist/hooks/hook-llm.js +3 -1
  44. package/dist/hosted-posture.d.ts +49 -93
  45. package/dist/hosted-posture.js +1 -25
  46. package/dist/memory-scope.d.ts +62 -11
  47. package/dist/memory-scope.js +32 -3
  48. package/dist/model-provider.d.ts +13 -0
  49. package/dist/model-provider.js +3 -0
  50. package/dist/model-route-endpoint.d.ts +82 -0
  51. package/dist/model-route-endpoint.js +55 -0
  52. package/dist/observability/secret-env-scrub.d.ts +2 -26
  53. package/dist/observability/secret-env-scrub.js +2 -2
  54. package/dist/plugins/remote-env-host.d.ts +3 -1
  55. package/dist/plugins/remote-env-host.js +3 -2
  56. package/dist/project-identity.d.ts +29 -0
  57. package/dist/project-identity.js +120 -0
  58. package/dist/project-memory.d.ts +28 -1
  59. package/dist/project-memory.js +22 -22
  60. package/dist/run-local.d.ts +14 -4
  61. package/dist/run-local.js +8 -7
  62. package/dist/runtime-caps-resolver.d.ts +8 -8
  63. package/dist/runtime-caps-resolver.js +16 -14
  64. package/dist/sealed-key.js +3 -7
  65. package/dist/server-secret-env.d.ts +41 -0
  66. package/dist/server-secret-env.js +18 -0
  67. package/dist/tool-approval.d.ts +48 -1
  68. package/dist/tool-approval.js +10 -3
  69. package/dist/trace/redact.d.ts +1 -1
  70. package/package.json +4 -3
package/dist/config.d.ts CHANGED
@@ -243,6 +243,15 @@ export declare const REQUIRED_ENVS_BY_LANE: Record<RemoteExecProvider, readonly
243
243
  * a loud CONFIG_NOTICES line ("removed-1m"); the center lane's roster culture is explicit declaration.
244
244
  * Shared by the env lane (main + cheap models) and the center lane (sema-registry toModel). */
245
245
  export declare function applyAutoCompactWindow(m: Model, explicit?: number): "explicit" | "sonnet5" | "removed-1m" | "removed";
246
+ /**
247
+ * **严格名册布防**的唯一判据(boot 就绪门 `boot/config-center.ts` 与 S-623 端点判据同调这一只)。
248
+ *
249
+ * 布防 = 远程 registry ∧ 非 dryRun ∧ `CONFIG_REQUIRE_ROSTER=true`(严格 `"true"`,直读 env、不进 ServiceConfig —— 纯
250
+ * boot 期姿势旋钮,原判据逐字搬来,零语义变化)。布防时运维声明 env MODEL_ID 是**占位**:名册落地前计费提交 503、
251
+ * `/health` ready:false,落地后整张模型表被中心目录换装。dryRun 是 compare-only(名册从不被应用),local / 纯 env
252
+ * 部署没有「中心」可等 ⇒ 都不布防。
253
+ */
254
+ export declare function isRosterGateArmed(configCenter: ServiceConfig["configCenter"]): boolean;
246
255
  /**
247
256
  * 一条工具名**永不匹配任何现役工具**的三种拼法,及其修正指引。审批/权限名单是 server 侧 ToolPolicy 的
248
257
  * RAW 逐字比对(canonicalToolName 折叠面已随 core 5.0.0 [ref] 退役),所以写错的名字不会报错、只会静默
package/dist/config.js CHANGED
@@ -7,7 +7,7 @@ import { BUILTIN_COMPLIANCE_DENIES, COMPLIANCE_CAPABILITIES, CROSS_SESSION_INBOU
7
7
  import { ROSTER_PRIMARY_ROLES, ROSTER_CHEAP_ROLES } from "@sema-agent/settings-schema";
8
8
  import { parseApprovalHmacKeys, parsePrincipalJwks } from "./auth-keys.js";
9
9
  import { DEFAULT_ELICITATION_THROTTLE } from "./elicitation.js";
10
- import { isV2ScopeKey } from "./memory-scope.js";
10
+ import { isV2ScopeKey, memoryReplicationCoverage } 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
13
  import { RETENTION_MODES, MEMORY_CONSOLIDATION_MIN_INTERVAL_SEC, RESILIENCE_DEFAULTS, TASK_WRITE_FACE_ESCALATIONS } from "./config-types.js";
@@ -16,12 +16,13 @@ import { normalizeSshFingerprint } from "./ssh-host-key.js";
16
16
  import { readFaceForPosture } from "./read-face-posture.js";
17
17
  import { THINKING_LEVELS, THINKING_FORMATS, buildModelCompat, isThinkingFormat, isThinkingTier } from "./model-compat.js";
18
18
  import {} from "./posture-source.js";
19
- import { hostedPrincipalRefusal } from "./hosted-posture.js";
20
19
  import { CONTROL_AND_BIDI_STRIP_RE } from "./text-bidi.js";
21
20
  import { REMOTE_EXEC_PROVIDER_WORDS, executionLaneOf } from "./execution-lane-caps.js";
22
21
  import { AUTONOMY_MODES } from "./runtime-governance.js";
22
+ import { createCenterCredentialSource, readCenterCredentialDeclaration } from "./center-credential.js";
23
23
  import { modelProviderRefusal } from "./model-provider.js";
24
24
  import { modelRouteUrlRefusal } from "./model-route-url.js";
25
+ import { modelRouteEndpointRefusal } from "./model-route-endpoint.js";
25
26
  import { buildDisplaySafeUrl, redactSecrets } from "./trace/redact.js";
26
27
  export { RETENTION_MODES, MEMORY_CONSOLIDATION_MIN_INTERVAL_SEC } from "./config-types.js";
27
28
  function csv(name) {
@@ -795,8 +796,11 @@ function parseStoreDomain(ctx) {
795
796
  : undefined,
796
797
  };
797
798
  }
799
+ export function isRosterGateArmed(configCenter) {
800
+ return configCenter !== undefined && !configCenter.dryRun && process.env.CONFIG_REQUIRE_ROSTER === "true";
801
+ }
798
802
  function parseModelDomain() {
799
- const gatewayBaseUrl = env("MODEL_GATEWAY_BASEURL", "http://127.0.0.1:8000/v1");
803
+ const gatewayBaseUrl = firstSetEnv("MODEL_GATEWAY_BASEURL");
800
804
  const modelId = env("MODEL_ID", "");
801
805
  if (modelId === "") {
802
806
  throw new Error("MODEL_ID is required (no factory default since server 3.0.0) — set MODEL_ID=<model name your gateway serves>, " +
@@ -811,7 +815,7 @@ function parseModelDomain() {
811
815
  }
812
816
  }
813
817
  for (const [envKey, raw, knob] of [
814
- ["MODEL_GATEWAY_BASEURL", gatewayBaseUrl, "MODEL_API_KEY"],
818
+ ["MODEL_GATEWAY_BASEURL", gatewayBaseUrl ?? "", "MODEL_API_KEY"],
815
819
  ...csv("MODEL_GATEWAY_FALLBACK_URLS").map((u, i) => [`MODEL_GATEWAY_FALLBACK_URLS[${i}]`, u, "MODEL_API_KEY"]),
816
820
  ["ANTHROPIC_BASE_URL", firstSetEnv("ANTHROPIC_BASE_URL") ?? "", "ANTHROPIC_API_KEY"],
817
821
  ]) {
@@ -918,6 +922,15 @@ function parseModelDomain() {
918
922
  const models = { default: model, [model.id]: model };
919
923
  if (cheapModel)
920
924
  models[cheapModel.id] = cheapModel;
925
+ const gatewayApiKey = process.env.MODEL_API_KEY;
926
+ if (gatewayBaseUrl === undefined && gatewayApiKey !== undefined && gatewayApiKey !== "") {
927
+ CONFIG_NOTICES.push({
928
+ event: "model_gateway_key_unpaired",
929
+ fields: {
930
+ hint: "MODEL_API_KEY is set but MODEL_GATEWAY_BASEURL is not — MODEL_API_KEY is the gateway's credential and pairs only with the gateway's own root, so with no gateway it is NOT loaded and is sent to no route (a model with its own baseUrl authenticates with its own per-model key). Set MODEL_GATEWAY_BASEURL to use it, or unset MODEL_API_KEY to silence this line.",
931
+ },
932
+ });
933
+ }
921
934
  const cheapRef = cheapModel ? cheapModel.id : DEFAULT_MODEL_REF;
922
935
  const verifierEnv = process.env.MODEL_VERIFIER;
923
936
  const verifierRef = verifierEnv && models[verifierEnv] ? verifierEnv : DEFAULT_MODEL_REF;
@@ -940,8 +953,7 @@ function parseModelDomain() {
940
953
  }
941
954
  const degradeOn = parseDegradeOn(csv("MODEL_DEGRADE_ON"));
942
955
  return {
943
- gatewayBaseUrl,
944
- gatewayApiKey: process.env.MODEL_API_KEY,
956
+ ...(gatewayBaseUrl !== undefined ? { gatewayBaseUrl, gatewayApiKey } : {}),
945
957
  gatewayFallbackUrls: csv("MODEL_GATEWAY_FALLBACK_URLS"),
946
958
  ...(process.env.GATEWAY_MAX_RETRIES ? { gatewayMaxRetries: numEnvBounded("GATEWAY_MAX_RETRIES", "2", 0, 20) } : {}),
947
959
  anthropic: firstSetEnv("ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN")
@@ -1469,6 +1481,19 @@ function parseMemoryDomain(ctx) {
1469
1481
  ...syncEntryCap("MEMORY_SYNC_MAX_PULL_ENTRIES", "maxPullEntries"),
1470
1482
  };
1471
1483
  }
1484
+ const replication = memoryReplicationCoverage(memorySync !== undefined ? { memorySync } : {});
1485
+ if (replication.kind === "scopes" && memoryEngineEnabled && !requirePrincipal && memoryScope !== undefined) {
1486
+ CONFIG_NOTICES.push({
1487
+ event: "memory_project_scope_yields_to_sync",
1488
+ fields: {
1489
+ replicatedScopes: replication.scopes.join(","),
1490
+ writeScope: memoryScope,
1491
+ hint: "memory sync (MEMORY_SYNC_*) replicates only the scope(s) listed — the per-project memory layer (S-512, repo:<key>) is not replicated, " +
1492
+ "so sessions do NOT write project-keyed memory here: new memory lands in the global layer (MEMORY_SCOPE), the same as 7.97.0, and every " +
1493
+ "project reads it. Project-layer replication is a separate change; unset MEMORY_SYNC_URL to get per-project memory without sync.",
1494
+ },
1495
+ });
1496
+ }
1472
1497
  const projectMemoryEnabled = boolEnvWithLegacyNegated("PROJECT_MEMORY_ENABLED", "PROJECT_MEMORY_DISABLED", true);
1473
1498
  const memoryOrgAdmissionMode = enumEnv("MEMORY_ORG_ADMISSION_MODE", "enforce", ["audit", "enforce"]);
1474
1499
  const memoryOrgDirectoryJson = process.env.MEMORY_ORG_DIRECTORY_JSON || undefined;
@@ -1940,19 +1965,21 @@ function parseIntegrationsDomain() {
1940
1965
  gitApiKind: parseGitApiKind(process.env.GIT_API_KIND, process.env.GIT_API_BASEURL),
1941
1966
  defaultScenario: env("DEFAULT_SCENARIO", "code"),
1942
1967
  skillsDir: env("SKILLS_DIR", defaultSkillsDir()),
1943
- configCenter: process.env.SEMA_REGISTRY_URL
1944
- ? {
1945
- baseUrl: env("SEMA_REGISTRY_URL"),
1946
- token: process.env.SEMA_REGISTRY_TOKEN || "",
1947
- dryRun: boolEnv("SEMA_REGISTRY_DRY_RUN", false),
1948
- worker: process.env.SEMA_REGISTRY_WORKER || undefined,
1949
- }
1950
- : undefined,
1968
+ configCenter: process.env.SEMA_REGISTRY_URL ? parseConfigCenter(env("SEMA_REGISTRY_URL")) : undefined,
1951
1969
  configProvider: process.env.CONFIG_PROVIDER || undefined,
1952
1970
  configLocalDir: process.env.CONFIG_LOCAL_DIR || undefined,
1953
1971
  a2aServe: parseA2aServe(),
1954
1972
  };
1955
1973
  }
1974
+ function parseConfigCenter(baseUrl) {
1975
+ const worker = process.env.SEMA_REGISTRY_WORKER || undefined;
1976
+ return {
1977
+ baseUrl,
1978
+ credential: createCenterCredentialSource(readCenterCredentialDeclaration(), worker),
1979
+ dryRun: boolEnv("SEMA_REGISTRY_DRY_RUN", false),
1980
+ worker,
1981
+ };
1982
+ }
1956
1983
  const A2A_SERVE_MAX_SKILLS = 64;
1957
1984
  const A2A_SERVE_MAX_ID_CHARS = 128;
1958
1985
  const A2A_SERVE_MAX_TEXT_CHARS = 2048;
@@ -2170,10 +2197,6 @@ export function loadConfig() {
2170
2197
  if (dgDurable && requirePrincipal && dgOperators.length === 0) {
2171
2198
  throw new Error("DURABLE_APPROVAL=true + REQUIRE_PRINCIPAL=true but OPERATOR_PRINCIPALS is empty — on a multi-tenant worker the empty operator set makes isOperator() true-for-all, so every authenticated tenant could read every other tenant's pending-approval queue (GET /v1/approvals & siblings). Set OPERATOR_PRINCIPALS, or unset REQUIRE_PRINCIPAL if this is a single-tenant deployment (#157-① boot invariant)");
2172
2199
  }
2173
- const hostedRefusal = hostedPrincipalRefusal({ requirePrincipal, principalJwtPubkeys, principalJwtIss, principalJwtAud, authBridgeIssuer: auth.authBridgeIssuer });
2174
- if (hostedRefusal !== undefined) {
2175
- throw new Error(hostedRefusal);
2176
- }
2177
2200
  if (imageBakesEnabled && dgOperators.length === 0) {
2178
2201
  throw new Error("IMAGE_BAKES_ENABLED=true but OPERATOR_PRINCIPALS is empty — the bake door runs build.sh on the privileged build host; an empty operator set makes isOperator() true-for-all (world-writable RCE). Set OPERATOR_PRINCIPALS or disable bakes (IMAGE-API-DESIGN.md §P2.4a boot invariant)");
2179
2202
  }
@@ -2225,6 +2248,18 @@ export function loadConfig() {
2225
2248
  `or set CONFIG_PROVIDER=local to take the single-machine \`host\` default, ` +
2226
2249
  `or unset REQUIRE_EXECUTION_LANE to keep running without hands on purpose.`);
2227
2250
  }
2251
+ {
2252
+ const envLaneDispatchable = !isRosterGateArmed(integrations.configCenter);
2253
+ const refusal = modelRouteEndpointRefusal({
2254
+ gatewayBaseUrl: modelPlane.gatewayBaseUrl,
2255
+ gatewayFallbackUrls: modelPlane.gatewayFallbackUrls,
2256
+ anthropic: modelPlane.anthropic,
2257
+ models: envLaneDispatchable ? modelPlane.models : {},
2258
+ });
2259
+ if (refusal !== undefined) {
2260
+ throw new Error(refusal);
2261
+ }
2262
+ }
2228
2263
  EXEC_LANE_NOTICE = buildExecLaneNotice(orchestration.remoteExec);
2229
2264
  return attachConfigGroups({
2230
2265
  ...store,
@@ -13,6 +13,7 @@
13
13
  * - 全部 fire-and-forget + swallow-guarded:fleet 面是运维旁路,绝不影响任务面;网络错误只 warn 一次/连败类。
14
14
  */
15
15
  import { UsageReport } from "@sema-agent/settings-schema/fleet";
16
+ import { type CenterEndpoint } from "./config-center/center-request.js";
16
17
  /** Bound on distinct principal×model keys per window — a runaway/multi-tenant burst collapses into
17
18
  * `__other__` instead of growing unbounded (same posture as budget.ts cardinalityGuard). */
18
19
  export declare const FLEET_USAGE_KEY_CAP = 1024;
@@ -55,9 +56,8 @@ export declare class FleetUsageAccumulator {
55
56
  get size(): number;
56
57
  }
57
58
  export interface FleetClientOpts {
58
- center: {
59
- baseUrl: string;
60
- token: string;
59
+ /** S-651:凭证是**来源**(每条公告 / 用量 / 注销请求各取一次值),不是启动期烧进 header 的字符串。 */
60
+ center: CenterEndpoint & {
61
61
  worker: string;
62
62
  };
63
63
  instanceId: string;
@@ -88,16 +88,16 @@ export interface FleetClient {
88
88
  */
89
89
  export declare function startFleetClient(opts: FleetClientOpts): FleetClient;
90
90
  /**
91
- * Env/config gate (D3 可选分层): start ONLY when the sema-registry lane is configured (baseUrl+token+worker)
91
+ * Env/config gate (D3 可选分层): start ONLY when the sema-registry lane is configured (baseUrl + a DECLARED credential
92
+ * + worker — S-651: declared, not "currently readable": a token file that is unreadable right now still starts the
93
+ * client, every request re-reads it)
92
94
  * AND the operator explicitly declared this instance's reachable address. Anything missing → undefined (zero
93
95
  * behavior change for non-fleet deployments); worker missing is warned when the rest is present (half-configured).
94
96
  */
95
97
  export declare function startFleetClientFromEnv(cfg: {
96
- configCenter?: {
97
- baseUrl: string;
98
- token: string;
98
+ configCenter?: (CenterEndpoint & {
99
99
  worker?: string;
100
- };
100
+ }) | undefined;
101
101
  }, opts: Omit<FleetClientOpts, "center" | "advertiseAddress"> & {
102
102
  advertiseAddress?: string;
103
103
  }): FleetClient | undefined;
@@ -1,5 +1,6 @@
1
1
  import { InstanceAnnouncement, UsageReport, DEFAULT_HEARTBEAT_MS } from "@sema-agent/settings-schema/fleet";
2
2
  import { buildDisplaySafeUrl } from "./trace/redact.js";
3
+ import { centerFetch } from "./config-center/center-request.js";
3
4
  export const FLEET_USAGE_KEY_CAP = 1024;
4
5
  export const FLEET_CLIENT_FETCH_TIMEOUT_MS = 5000;
5
6
  export class FleetUsageAccumulator {
@@ -77,8 +78,7 @@ export function startFleetClient(opts) {
77
78
  const f = opts.fetchImpl ?? fetch;
78
79
  const heartbeatMs = opts.heartbeatMs ?? DEFAULT_HEARTBEAT_MS;
79
80
  const usageWindowMs = opts.usageWindowMs ?? 60_000;
80
- const base = opts.center.baseUrl.replace(/\/+$/, "");
81
- const headers = { authorization: `Bearer ${opts.center.token}`, "content-type": "application/json" };
81
+ const send = (route, method, json) => centerFetch(opts.center, { kind: route }, { method, json, timeoutMs: FLEET_CLIENT_FETCH_TIMEOUT_MS, fetchImpl: f });
82
82
  let stopped = false;
83
83
  let failStreak = 0;
84
84
  const announceNow = async () => {
@@ -92,7 +92,7 @@ export function startFleetClient(opts) {
92
92
  heartbeatMs,
93
93
  };
94
94
  try {
95
- const res = await f(`${base}/api/fleet/instances`, { method: "POST", headers, body: JSON.stringify(body), signal: AbortSignal.timeout(FLEET_CLIENT_FETCH_TIMEOUT_MS) });
95
+ const { res } = await send("fleet-instances", "POST", body);
96
96
  if (!res.ok)
97
97
  throw new Error(`announce ${res.status}${await rejectionSnippet(res)}`);
98
98
  if (failStreak > 0)
@@ -117,7 +117,7 @@ export function startFleetClient(opts) {
117
117
  return;
118
118
  const report = { worker: opts.center.worker, instanceId: opts.instanceId, windowStartMs: start, windowEndMs, entries };
119
119
  try {
120
- const res = await f(`${base}/api/fleet/usage`, { method: "POST", headers, body: JSON.stringify({ reports: [report] }), signal: AbortSignal.timeout(FLEET_CLIENT_FETCH_TIMEOUT_MS) });
120
+ const { res } = await send("fleet-usage", "POST", { reports: [report] });
121
121
  if (!res.ok)
122
122
  throw new Error(`usage ${res.status}${await rejectionSnippet(res)}`);
123
123
  }
@@ -140,7 +140,7 @@ export function startFleetClient(opts) {
140
140
  clearInterval(uw);
141
141
  await flushUsage();
142
142
  try {
143
- await f(`${base}/api/fleet/instances`, { method: "DELETE", headers, body: JSON.stringify({ instanceId: opts.instanceId, worker: opts.center.worker }), signal: AbortSignal.timeout(FLEET_CLIENT_FETCH_TIMEOUT_MS) });
143
+ await send("fleet-instances", "DELETE", { instanceId: opts.instanceId, worker: opts.center.worker });
144
144
  }
145
145
  catch {
146
146
  }
@@ -150,7 +150,7 @@ export function startFleetClient(opts) {
150
150
  export function startFleetClientFromEnv(cfg, opts) {
151
151
  const cc = cfg.configCenter;
152
152
  const address = opts.advertiseAddress ?? process.env.FLEET_ADVERTISE_ADDRESS;
153
- if (!cc?.baseUrl || !cc.token || !address)
153
+ if (!cc?.baseUrl || !cc.credential.declared || !address)
154
154
  return undefined;
155
155
  let parsed;
156
156
  try {
@@ -166,6 +166,6 @@ export function startFleetClientFromEnv(cfg, opts) {
166
166
  opts.logger?.warn("fleet_client_no_worker_scope", { note: "FLEET_ADVERTISE_ADDRESS set but SEMA_REGISTRY_WORKER missing — announce needs the worker (dispatch key); fleet client not started" });
167
167
  return undefined;
168
168
  }
169
- return startFleetClient({ ...opts, center: { baseUrl: cc.baseUrl, token: cc.token, worker: cc.worker }, advertiseAddress: address });
169
+ return startFleetClient({ ...opts, center: { baseUrl: cc.baseUrl, credential: cc.credential, worker: cc.worker }, advertiseAddress: address });
170
170
  }
171
171
  //# sourceMappingURL=fleet-client.js.map
@@ -24,6 +24,7 @@
24
24
  * - 全部 swallow-guarded:lease 面绝不 fault 任务提交路径。
25
25
  */
26
26
  import { QuotaLease } from "@sema-agent/settings-schema/fleet";
27
+ import { type CenterEndpoint } from "./config-center/center-request.js";
27
28
  /** 负缓存 TTL:非 lease-enforced 的 principal 多久后再问一次 center(entitlement 升级为 lease 的收敛上界)。 */
28
29
  export declare const NOT_LEASE_TTL_MS: number;
29
30
  /** center 判 budget exhausted 后,多久内的重复提交直接拒绝而不再打 center(防 per-request 打爆)。 */
@@ -62,9 +63,8 @@ export type LeaseAdmission = {
62
63
  detail?: QuotaDenyDetail;
63
64
  };
64
65
  export interface FleetLeaseOpts {
65
- center: {
66
- baseUrl: string;
67
- token: string;
66
+ /** S-651:凭证是**来源**(每次协商取一次值),不是构造期烧进 header 的字符串。 */
67
+ center: CenterEndpoint & {
68
68
  worker: string;
69
69
  };
70
70
  fetchImpl?: typeof fetch;
@@ -82,8 +82,6 @@ export declare class FleetLeaseManager {
82
82
  private readonly state;
83
83
  private readonly inflight;
84
84
  private readonly f;
85
- private readonly base;
86
- private readonly headers;
87
85
  private readonly now;
88
86
  private failStreak;
89
87
  constructor(opts: FleetLeaseOpts);
@@ -108,21 +106,23 @@ export declare class FleetLeaseManager {
108
106
  }): void;
109
107
  /** Test/observability hook: the current state kind for a principal (never exposes the token). */
110
108
  peek(principal: string): PrincipalState["kind"] | undefined;
109
+ /** 🔴 复审 O2(Opus)的鉴权失败臂,S-651 起两个入口共用(中心 401/403/400;凭证文件不可读):运维配置事故,
110
+ * 不是可用性事件。仍 fail-open(凭证抖一下不许把租户锁在门外),但**每次**都 warn + 专用计数,且**不缓存**。 */
111
+ private authFailOpen;
111
112
  private set;
112
113
  private issueOrRenew;
113
114
  private issueOrRenewInner;
114
115
  }
115
116
  /**
116
117
  * Env/config gate(与 startFleetClientFromEnv 同 D3 分层姿势,但 lease 不需要 FLEET_ADVERTISE_ADDRESS ——
117
- * 它是出站消费面,不是入站可达性声明):sema-registry lane 配齐(baseUrl+token+worker)即启;任一缺失 →
118
- * undefined,非 fleet 部署零行为变化。
118
+ * 它是出站消费面,不是入站可达性声明):sema-registry lane 配齐(baseUrl + **声明了**凭证 + worker)即启;任一缺失 →
119
+ * undefined,非 fleet 部署零行为变化。S-651:按**声明**装、不按内容装 —— `SEMA_REGISTRY_TOKEN_FILE` 在场而
120
+ * 文件此刻不可读,管理器照样装上(每次协商取值,失败走响亮的鉴权失败臂;文件恢复即恢复)。
119
121
  */
120
122
  export declare function createFleetLeaseFromEnv(cfg: {
121
- configCenter?: {
122
- baseUrl: string;
123
- token: string;
123
+ configCenter?: (CenterEndpoint & {
124
124
  worker?: string;
125
- };
125
+ }) | undefined;
126
126
  }, opts: Omit<FleetLeaseOpts, "center">): FleetLeaseManager | undefined;
127
127
  export {};
128
128
  //# sourceMappingURL=fleet-lease.d.ts.map
@@ -1,4 +1,6 @@
1
1
  import { QuotaLease, leaseExhausted, shouldRenewLease } from "@sema-agent/settings-schema/fleet";
2
+ import { CenterCredentialUnavailableError } from "./center-credential.js";
3
+ import { centerFetch } from "./config-center/center-request.js";
2
4
  export const NOT_LEASE_TTL_MS = 10 * 60 * 1000;
3
5
  export const EXHAUSTED_RETRY_MS = 30 * 1000;
4
6
  export const LEASE_PRINCIPAL_CAP = 4096;
@@ -8,15 +10,11 @@ export class FleetLeaseManager {
8
10
  state = new Map();
9
11
  inflight = new Map();
10
12
  f;
11
- base;
12
- headers;
13
13
  now;
14
14
  failStreak = 0;
15
15
  constructor(opts) {
16
16
  this.opts = opts;
17
17
  this.f = opts.fetchImpl ?? fetch;
18
- this.base = opts.center.baseUrl.replace(/\/+$/, "");
19
- this.headers = { authorization: `Bearer ${opts.center.token}`, "content-type": "application/json" };
20
18
  this.now = opts.now ?? Date.now;
21
19
  }
22
20
  async admit(principal) {
@@ -46,6 +44,11 @@ export class FleetLeaseManager {
46
44
  peek(principal) {
47
45
  return this.state.get(principal)?.kind;
48
46
  }
47
+ authFailOpen(principal, status, note) {
48
+ this.opts.logger?.warn?.("fleet_lease_auth_error_fail_open", { status, principal, note });
49
+ this.opts.metrics?.inc("fleet_lease_auth_error_total", { status: String(status) });
50
+ return { ok: true };
51
+ }
49
52
  set(principal, st) {
50
53
  if (!this.state.has(principal) && this.state.size >= LEASE_PRINCIPAL_CAP) {
51
54
  const oldest = this.state.keys().next().value;
@@ -64,11 +67,11 @@ export class FleetLeaseManager {
64
67
  }
65
68
  async issueOrRenewInner(principal) {
66
69
  try {
67
- const res = await this.f(`${this.base}/api/fleet/leases`, {
70
+ const { res } = await centerFetch(this.opts.center, { kind: "fleet-leases" }, {
68
71
  method: "POST",
69
- headers: this.headers,
70
- body: JSON.stringify({ principal, worker: this.opts.center.worker }),
71
- signal: AbortSignal.timeout(LEASE_FETCH_TIMEOUT_MS),
72
+ json: { principal, worker: this.opts.center.worker },
73
+ timeoutMs: LEASE_FETCH_TIMEOUT_MS,
74
+ fetchImpl: this.f,
72
75
  });
73
76
  if (res.status === 200) {
74
77
  const body = (await res.json());
@@ -100,13 +103,14 @@ export class FleetLeaseManager {
100
103
  return { ok: true };
101
104
  }
102
105
  if (res.status === 401 || res.status === 403 || res.status === 400) {
103
- this.opts.logger?.warn?.("fleet_lease_auth_error_fail_open", { status: res.status, principal, note: "center rejected the lease request — check the service token / worker id / center URL (budget NOT enforced until fixed)" });
104
- this.opts.metrics?.inc("fleet_lease_auth_error_total", { status: String(res.status) });
105
- return { ok: true };
106
+ return this.authFailOpen(principal, res.status, "center rejected the lease request — check the service token / worker id / center URL (budget NOT enforced until fixed)");
106
107
  }
107
108
  throw new Error(`lease ${res.status}`);
108
109
  }
109
110
  catch (err) {
111
+ if (err instanceof CenterCredentialUnavailableError) {
112
+ return this.authFailOpen(principal, "credential-unreadable", `${err.reason} — no lease request sent (budget NOT enforced until the credential is readable)`);
113
+ }
110
114
  this.failStreak += 1;
111
115
  if (this.failStreak === 1)
112
116
  this.opts.logger?.warn?.("fleet_lease_unavailable_fail_open", { error: err instanceof Error ? err.message : String(err) });
@@ -117,8 +121,8 @@ export class FleetLeaseManager {
117
121
  }
118
122
  export function createFleetLeaseFromEnv(cfg, opts) {
119
123
  const cc = cfg.configCenter;
120
- if (!cc?.baseUrl || !cc.token || !cc.worker)
124
+ if (!cc?.baseUrl || !cc.credential.declared || !cc.worker)
121
125
  return undefined;
122
- return new FleetLeaseManager({ ...opts, center: { baseUrl: cc.baseUrl, token: cc.token, worker: cc.worker } });
126
+ return new FleetLeaseManager({ ...opts, center: { baseUrl: cc.baseUrl, credential: cc.credential, worker: cc.worker } });
123
127
  }
124
128
  //# sourceMappingURL=fleet-lease.js.map
@@ -9,7 +9,7 @@ import type { HookLlmCall } from "./hook-runner.js";
9
9
  * - `openai-completions` ⇒ POST {base}/chat/completions(现状,字节不变);
10
10
  * - `anthropic-messages` ⇒ POST {base}/v1/messages(x-api-key / Authorization: Bearer + anthropic-version
11
11
  * 头,messages 数组,回包取 content[].text)。此前 hookLlm 只会说 openai 形且缺省 baseUrl 恒落
12
- * `config.gatewayBaseUrl`(缺省 http://127.0.0.1:8000/v1)——anthropic 单路由部署(主模型
12
+ * `config.gatewayBaseUrl`(当年它有一个本机 8000 端口的出厂缺省,S-623 已删)——anthropic 单路由部署(主模型
13
13
  * provider=anthropic 走 ANTHROPIC_BASE_URL,没配 MODEL_GATEWAY_BASEURL)下 titler 每会话打死
14
14
  * localhost:8000 ⇒ `session_title_failed: TypeError: fetch failed`;即使端点活着,anthropic-messages
15
15
  * 模型也不能用 /chat/completions 形调。
@@ -1,5 +1,6 @@
1
1
  import { adjudicateModelRoute, joinRequestUrl, resolveRouteCredential, routeKeyOf, routeRefusalText } from "@sema-agent/core";
2
2
  import { matchCatalogModel } from "../model-select.js";
3
+ import { resolveOpenaiRouteEndpoint } from "../model-route-endpoint.js";
3
4
  import { buildDisplaySafeRefusal, redactSecrets } from "../trace/redact.js";
4
5
  export function buildHookFailureText(raw) {
5
6
  if (raw === "")
@@ -26,7 +27,8 @@ export function createHookLlm(deps) {
26
27
  }
27
28
  return { ok: true, catalogRef, providerModelId, baseUrl, api };
28
29
  }
29
- return { ok: true, catalogRef, providerModelId, baseUrl: m?.baseUrl || config.gatewayBaseUrl, api };
30
+ const endpoint = resolveOpenaiRouteEndpoint(catalogRef, m?.baseUrl, config.gatewayBaseUrl);
31
+ return endpoint.ok ? { ok: true, catalogRef, providerModelId, baseUrl: endpoint.baseUrl, api } : { ok: false, error: endpoint.refusal };
30
32
  };
31
33
  const hookModelFor = (requested) => {
32
34
  const shape = (ref) => shapeFrom(ref, config.models[ref], config.models[ref]?.id ?? ref);
@@ -1,5 +1,5 @@
1
1
  /**
2
- * S-591 / S-564(车LT,7.95.0)—— **「这台部署是不是托管 / 多租」的单一铸点**。
2
+ * S-591 / S-564(车LT,7.95.0)→ S-671 / B-10(车NB,7.99.0)—— **「这台部署是不是托管 / 多租」的单一铸点**。
3
3
  *
4
4
  * ## 为什么是一处
5
5
  *
@@ -21,60 +21,62 @@
21
21
  * **`deviceCwdHonored` 这一臂里没有 `requirePrincipal` 项**(`task-cwd.ts`:device 车道上那条路径落在
22
22
  * **发起者自己的设备**上,归属由 `device_session` 绑定行验过 ⇒ 跨租户面结构性不存在)。
23
23
  *
24
- * 两条判据的**交集之外**有一形谁都不管:**多用户部署但运维没开 `REQUIRE_PRINCIPAL`,也没有 org 治理**。
25
- * 那台机器上每一条赦免都成立(它自称单用户),而每一道天花板都不成立(它没有 org 分区)。
26
- * ⇒ 先把这个判定位铸成**一处**,再让两条腿都读它(clay 裁定③,2026-09-22):**不许各自加特判**——
24
+ * ⇒ 先把这个判定位铸成**一处**,再让各条腿都读它(clay 裁定③,2026-09-22):**不许各自加特判**——
27
25
  * 每加一条特判,「这台机器算不算托管」在本仓就多一个可以自己漂的答案。
28
26
  *
29
- * ## 判据的**唯一**标准:运维**显式点名了一个外部身份签发方**
30
- *
31
- * 🔴 判据里**必须**有不读 `requirePrincipal` 的臂,否则腿①(S-564)是同义反复
32
- * (「`REQUIRE_PRINCIPAL` 没设 ⇒ 要求设 `REQUIRE_PRINCIPAL`」永不触发)。所以词表分两类:
33
- * · `require-principal` —— 运维**已经说了**这是多租户(定义式,零假阳);
34
- * · 另外两条 —— 运维没说,但他**显式点了一个外部签发方的名字**。一台机器一个可信主人的形
35
- * (CC 的信任模型)不需要向任何人验签:签发方存在 ⇒ 身份来自这台机器之外 ⇒ 有别人。
36
- *
37
- * ## 🔴 「受控于控制面」**不是**托管证据(主会话裁定⑥ 的二选一,本仓选这一侧)
38
- *
39
- * 初版收过第三条臂 `managed-worker`(`SEMA_REGISTRY_WORKER` 在场 = reconciler 管着这台 worker)。
40
- * **实测两个反例后撤回**:
41
- * · `src/run-local.ts` 强设 `REQUIRE_PRINCIPAL=false` 之后才 `loadConfig()` —— 一个共享 `.env` 带着
42
- * registry 键就让本机 CLI **拒启**,而拒因指的那根旋钮正是它自己会覆盖回去的(codex 对抗复审同批独立复现);
43
- * · 本仓自己的 `test/config-coverage-blocks.test.ts` 两格(configCenter 块)就是「有 worker 名、没开
44
- * `REQUIRE_PRINCIPAL`」——它们在初版上当场红。
45
- * ⇒ **fleet 管理轴 ≠ 租户轴**。同理 `AUTH_BRIDGE_ISSUER` 只收**显式 env**,**绝不**收 boot 期那条
46
- * `?? config.configCenter?.baseUrl` 的回退(`boot/stage-09-leader.ts`)—— 那条回退的事实来源同样是
47
- * 「配了 center」,收它等于把刚撤回的那条臂换个名字装回来,精度还更差。
48
- *
49
- * ## 精度优先于召回(逐条写下被排除的候选,不留「我没想到」的空白)
50
- *
51
- * 假阳 = 一台单用户 turnkey 被拒启 / 被收走一个它一直在用的能力。假阴 = 缺口照旧。
52
- * 两者都坏,但假阳会当场打到在跑的部署,所以词表只收**单用户形写不出来**的证据:
27
+ * ## 判据:托管 ⇔ 运维显式声明 `REQUIRE_PRINCIPAL=true`(clay 裁定 [ref] ①,2026-09-23)
28
+ *
29
+ * 只有一条定义式证据,零推断。7.95.0–7.98.x 还收过两条「运维点名了一个外部身份签发方」的推断臂
30
+ * (`PRINCIPAL_JWT_PUBKEYS/ISS/AUD` 任一在场;`AUTH_BRIDGE_ISSUER` 显式在场),[ref] ① 裁 (a) 撤回:
31
+ * · **真假阳**:本机自起引擎、配了 principal-JWT / auth-bridge 签发方的单用户部署(web-admin 8090 真案)
32
+ * 在 7.98.0 被判托管而拒启,只能滚回 7.93.5;
33
+ * · **与回退 issuer 不同判**:auth-bridge 的 `?? configCenter.baseUrl` 回退(`boot/stage-09-leader.ts`)
34
+ * 从来不算证据,而回退桥的引擎同样把单用户授予开给每一个验签通过的 registry JWT 调用者 —— 同一个
35
+ * 租户轴事实,「显式写了 issuer」与「从 center 推出 issuer」是同一形。裁定后两者同判。
36
+ * ⇒ 签发方配置是**身份验签轴**(谁的 JWT 算数),不是租户轴(这台机器有几个主人)。「受控于控制面」
37
+ * (`SEMA_REGISTRY_*`,含 worker 名)同理是 fleet 管理轴,本判据同样不读。
38
+ *
39
+ * 随两臂一起删掉的(按类删干净,规则集变小):S-564 腿①「托管 ∧ 未开 `REQUIRE_PRINCIPAL` ⇒ 拒启」
40
+ * (判据只剩读 `requirePrincipal` 的那一臂之后,那条合取恒假 ⇒ 结构上不可达)及其跨键不变量行;
41
+ * `run-local.ts` 为那两臂清 env 的清除口。
42
+ *
43
+ * ## 被排除的候选(逐条写下,不留「我没想到」的空白)
44
+ *
45
+ * 假阳 = 一台单用户部署被收走一个它一直在用的能力(7.98.0 时还会被拒启)。词表不收任何推断臂:
46
+ * · ❌ `PRINCIPAL_JWT_*` / `AUTH_BRIDGE_ISSUER` 在场 —— 见上一段([ref] ① 撤回;加回来 = 翻案,要新裁定)。
47
+ * · ❌ `configCenter`(含 `worker` 名)/ auth-bridge 的 center 回退 —— fleet 管理轴。初版(车LT)收过
48
+ * `managed-worker` 臂,实测两个反例后撤回:`run-local.ts` 强设 `REQUIRE_PRINCIPAL=false` 的本机 CLI 在一个
49
+ * 带 registry 键的共享 `.env` 上拒启;本仓自己的 `test/config-coverage-blocks.test.ts` configCenter 两格当场红。
53
50
  * · ❌ `operatorPrincipals` 非空 —— **真假阳**:`IMAGE_BAKES_ENABLED` 那条既有不变量
54
- * (`config-invariants.ts` 的 `bake-operator-principals`)**强制**开烤门的部署把它填非空,
55
- * 于是一台开了烤门的单用户机器会被判成托管。
56
- * · ❌ `configCenter`(含 `worker` 名)/ auth-bridge 的 center 回退 —— 见上一段,已实测撤回。
51
+ * (`config-invariants.ts` 的 `bake-operator-principals`)**强制**开烤门的部署把它填非空。
57
52
  * · ❌ `SERVICE_AUTH_TOKENS` 条目数 —— 一台机器上同时给 cli 与 web-admin 发一只令牌是单用户常态。
58
53
  * · ❌ `DB_BACKEND=mysql|pg` —— 那是**耐久性**轴,不是租户轴(单用户要 durable run 也得开)。
59
54
  * · ❌ `BIND_HOST` 非回环 —— 容器里 `0.0.0.0` 是单用户常态(compose/k8s 默认)。
60
- * · ❌ `COMPLIANCE_PROFILE` / `RETENTION_MAX_AGE_DAYS` / `LOCKED_CONFIG_KEYS` —— 数据/治理轴;
61
- * 一个自己给自己上合规档的单用户部署不该因此被拒启。
62
- *
63
- * **已知召回缺口(显式登记,不用「应该很少」填空)**:一台既不验 principal-JWT、也没点 auth-bridge
64
- * 签发方的团队机器(例如只靠反代加一层 basic auth)本表**看不见**。它的运维面补偿是:
65
- * `REQUIRE_PRINCIPAL=true` 本身就是那台机器该设的键,而 `docs/DEPLOY-PREREQS.md` 的「托管形前置」段
66
- * 把判据写成了给人读的清单。
55
+ * · ❌ `COMPLIANCE_PROFILE` / `RETENTION_MAX_AGE_DAYS` / `LOCKED_CONFIG_KEYS` —— 数据/治理轴。
56
+ *
57
+ * ## 🔴 召回缺口是**全量**的(显式登记,不用「应该很少」填空)
58
+ *
59
+ * 一台多用户 / 团队机器只要运维**忘设** `REQUIRE_PRINCIPAL`,本判据就**看不见**它 —— 无论它有没有
60
+ * 配签发方、受不受 reconciler 管。那台机器上:`principal` 缺席走单用户 `"default"` 赦免 ⇒ 全服无主会话
61
+ * 被列给每一个调用方;上面四条能力授予面(hooks / `settings.env` / `cwd` / `additionalDirectories`)对每个
62
+ * 调用方打开;写容纳面天花板与 `approverPosture` 留声两条腿(下文)也按单用户放行。
63
+ * `settings.env` 的射程要说准:适配器的 `env` 是**并**到 `process.env` 上的逐键覆盖(`plugins/remote-env-host.ts`
64
+ * 的 `mergeEnv`),而 `task-settings.ts` 的 `cleanEnvMap` 没有任何键名拒集 ⇒ `PATH` / `LD_PRELOAD` /
65
+ * `NODE_OPTIONS` 一视同仁。
66
+ * **补偿**:`REQUIRE_PRINCIPAL=true` 本来就是那台机器该设的键(运维自己的声明;server 不替他推断)——
67
+ * `docs/DEPLOY-PREREQS.md` 的「托管 / 多租部署」段把它写成部署前置,并列出忘设它的后果。
67
68
  */
68
69
  import { type ServiceConfigFlat } from "./config-types.js";
69
- /** 托管/多租的**证据词表**(闭集;加词 ⇒ {@link hostedEvidenceNote} 的穷举 switch 编译红)。 */
70
- export declare const HOSTED_EVIDENCE_WORDS: readonly ["require-principal", "principal-jwt", "auth-bridge-issuer"];
71
- /** 一条证据(闭集三词)。 */
70
+ /** 托管/多租的**证据词表**(闭集;加词 ⇒ {@link hostedEvidenceNote} 的穷举 switch 编译红)。
71
+ * 🔴 恰一词是裁定([ref] ①),不是暂时的:加回一条推断臂 = 翻案,先要新裁定。 */
72
+ export declare const HOSTED_EVIDENCE_WORDS: readonly ["require-principal"];
73
+ /** 一条证据(闭集一词)。 */
72
74
  export type HostedEvidence = (typeof HOSTED_EVIDENCE_WORDS)[number];
73
75
  /**
74
- * 本判据读的配置键集 —— 取自平铺配置型(键名/类型永不与真配置漂)。
75
- * `ServiceConfig` 与 `CrossInvariantView` 都**结构上**满足本形,所以 boot 腿、热路径腿与请求腿读同一只函数。
76
+ * 本判据读的配置键集 —— 取自平铺配置型(键名/类型永不与真配置漂)。`ServiceConfig` **结构上**满足本形,
77
+ * 所以请求腿(阶段④)与能力面读同一只函数。
76
78
  */
77
- export type HostedPostureInput = Readonly<Pick<ServiceConfigFlat, "requirePrincipal" | "principalJwtPubkeys" | "principalJwtIss" | "principalJwtAud" | "authBridgeIssuer">>;
79
+ export type HostedPostureInput = Readonly<Pick<ServiceConfigFlat, "requirePrincipal">>;
78
80
  /** 一台 worker 的托管姿态事实(值 + 证据)——纯数据。 */
79
81
  export interface HostedPostureFacts {
80
82
  /** 这台部署是不是托管 / 多租。 */
@@ -83,60 +85,14 @@ export interface HostedPostureFacts {
83
85
  readonly evidence: readonly HostedEvidence[];
84
86
  }
85
87
  /**
86
- * 判定位的**唯一**铸点(纯数据 ⇒ `build*`)。
88
+ * 判定位的**唯一**铸点(纯数据 ⇒ `build*`)。托管 ⇔ `requirePrincipal === true`([ref] ①)。
87
89
  *
88
- * 🔴 `evidence` 收**全部**命中而不是第一条:拒因要告诉运维「这台机器凭哪几件事被判成托管」,
89
- * 只报一条会让他改掉那一条之后再撞一次同样的拒(两次拒启之间他学不到全貌)。
90
+ * `evidence` 仍是数组(按词表顺序收**全部**命中):拒因逐字点它们,运维才知道改哪儿;词表是闭集,
91
+ * 形状不随词数变。
90
92
  */
91
93
  export declare function buildHostedPosture(c: HostedPostureInput): HostedPostureFacts;
92
- /**
93
- * 判据里**非定义式**那两条证据背后的 env 名 —— 即「运维显式点名了一个外部身份签发方」的全部拼法。
94
- *
95
- * 🔴 **单一属主**:`src/run-local.ts` 那条「强制单用户」的腿按本表清 env(W-新8)。它**结构上**兑现不了
96
- * 这些键(它把 `REQUIRE_PRINCIPAL` 焊死成 false ⇒ principal 门根本不上场),留着它们只会让判据看见一个
97
- * 假事实、然后拿一个**它自己会覆盖回去**的旋钮当拒因。名表抄第二份的那天,加一条证据 = run-local 静默漏清。
98
- * ⚠️ 表里**没有** `REQUIRE_PRINCIPAL`(那是定义式那一条,run-local 自己显式写值)也**没有**
99
- * `SEMA_REGISTRY_*`(fleet 管理轴,本判据根本不读 —— 本机 CLI 从组织下发面取配置是正当用法)。
100
- */
101
- export declare const HOSTED_EVIDENCE_ENV_KEYS: readonly ["PRINCIPAL_JWT_PUBKEYS", "PRINCIPAL_JWT_ISS", "PRINCIPAL_JWT_AUD", "AUTH_BRIDGE_ISSUER"];
102
- /**
103
- * 把上表那几根 env **清掉**(`run-local.ts` 的「强制单用户」腿调它)。
104
- *
105
- * 🔴 **为什么是四行字面 `delete` 而不是照表循环**:配置目录的 D1 对账门按文件数**动态 env 读点**并且是
106
- * **只降不升的棘轮** —— `process.env[k]` 那种计算属性读它一个都不放过(理由是动态读让「这个文件读了哪些
107
- * 键」变成机器看不见的事)。照表循环会给 `run-local.ts` 顶上第 3 处动态读、当场撞棘轮。⇒ 名字写成字面,
108
- * **但只写这一处**;上面那张表仍是**声明面**的属主,两者的一致性由
109
- * `test/hosted-posture.test.ts` 的行为格看着(逐个设上再调一次,断言全没了)——不是靠人眼比对。
110
- *
111
- * ⚠️ 表里**没有** `REQUIRE_PRINCIPAL`(定义式那条,调用方自己显式写值)也没有 `SEMA_REGISTRY_*`
112
- * (fleet 管理轴,本判据根本不读)。
113
- */
114
- export declare function clearHostedEvidenceEnv(): void;
115
94
  /** 一条证据的运维事实句 —— **穷举 switch,无缺省臂**(词表加员即编译红)。 */
116
95
  export declare function hostedEvidenceNote(e: HostedEvidence): string;
117
- /**
118
- * **腿①(S-564)** —— 托管形部署必须显式开 `REQUIRE_PRINCIPAL`。返回拒因;`undefined` = 通过。
119
- *
120
- * 判据与文案的**唯一属主**:boot 期的拒启 throw(`config.ts` 的跨域不变量区)与
121
- * `CONFIG_CROSS_INVARIANTS` 表(`config-invariants.ts`)**调同一只**,不各抄一份([ref] 的纪律 +
122
- * [ref]「禁手抄投影」)。
123
- *
124
- * 🔴 **表里那一行今天是「预置纵深」,不是在岗执法**(主会话裁定⑤,撤回初版那句「热路径同样在岗」):
125
- * `checkCrossInvariants` 的候选视图(`config-hot` 那条腿)今天只覆写 limits 那 14 员键,本判据读的
126
- * 5 个键**一个都不在里面** ⇒ 那一行在热路径上**永不返回拒因**。它先在场是纵深(哪天候选视图覆盖到
127
- * auth 域,它自己就活),不是现在有人在执法。**假覆盖比没覆盖更坏**(7.94.0 自己刚立的反面规矩)。
128
- *
129
- * 🔴 **为什么是拒启而不是 warn**:`principal` 缺席在本仓走 `"default"` 赦免 —— 一台没开这根旋钮的
130
- * 多用户机器会把**全服无主会话**列进 `ListAgents`,并且把 hooks / shellEnv / cwd / 根围栏拓宽四条
131
- * 能力授予面一并对每个调用方打开(四条面的谓词各不相同,逐条在模块头注 —— 别照「都一样」读)。
132
- * 安全轴上的 fail-open 不许有安静的兜底臂([ref])。
133
- *
134
- * 🔴 **`settings.env` 那一条的射程要说准**(亲读 `plugins/remote-env-host.ts` 的 `mergeEnv`):适配器的
135
- * `env` 是**并**到 `process.env` 上的逐键覆盖(不是替),而 `task-settings.ts` 的 `cleanEnvMap` 只判
136
- * 类型/键长/值长/条数、**没有任何键名拒集** ⇒ `PATH` / `LD_PRELOAD` / `NODE_OPTIONS` 一视同仁。
137
- * 那条面唯一的防线就是上面那道闸;闸一漂,面全开。
138
- */
139
- export declare function hostedPrincipalRefusal(c: HostedPostureInput): string | undefined;
140
96
  /** 腿② 读的键集 = 射程判据 + 天花板旋钮。
141
97
  * 🔴 档位的**词表属主在 `config-types.ts`**(`TASK_WRITE_FACE_ESCALATIONS`)—— 本模块刻意**不留第二份**:
142
98
  * 施工期真犯过一次(两处各写一份 `["allow","refuse"]`,加词的那天两边会分家),[ref] 第一排查点。 */