@sema-agent/server 7.87.2 → 7.88.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 (51) hide show
  1. package/dist/auth-bridge.js +5 -3
  2. package/dist/boot/device-lane.d.ts +12 -19
  3. package/dist/boot/device-lane.js +3 -13
  4. package/dist/boot/stage-05-execution-env.js +1 -2
  5. package/dist/boot/stage-07-capability-layer.js +2 -1
  6. package/dist/boot/stage-09-leader.js +2 -1
  7. package/dist/boot/stage-10-http-server.js +4 -2
  8. package/dist/boot/stores.js +2 -1
  9. package/dist/brain.js +3 -2
  10. package/dist/config-catalog.js +4 -3
  11. package/dist/config-center/apply-effective.js +3 -2
  12. package/dist/config-center/http-client.d.ts +11 -2
  13. package/dist/config-center/http-client.js +1 -1
  14. package/dist/config-center/read-warnings.d.ts +1 -1
  15. package/dist/config-center/read-warnings.js +5 -3
  16. package/dist/config-center/types.d.ts +3 -1
  17. package/dist/config-invariants.d.ts +2 -2
  18. package/dist/config-invariants.js +12 -0
  19. package/dist/config-provider.d.ts +9 -1
  20. package/dist/config-provider.js +3 -1
  21. package/dist/config-types.d.ts +7 -0
  22. package/dist/config.js +28 -3
  23. package/dist/device-enrollment.d.ts +25 -4
  24. package/dist/device-enrollment.js +20 -2
  25. package/dist/device-protocol.d.ts +15 -0
  26. package/dist/device-protocol.js +2 -0
  27. package/dist/device-ws-hub.d.ts +11 -0
  28. package/dist/device-ws-hub.js +6 -0
  29. package/dist/fleet-client.js +2 -1
  30. package/dist/http/admission.js +10 -1
  31. package/dist/http/device-placement.d.ts +177 -0
  32. package/dist/http/device-placement.js +148 -0
  33. package/dist/http/durable-run-leg.js +4 -0
  34. package/dist/http/resume-legs.js +4 -0
  35. package/dist/http/route-ctx.d.ts +8 -0
  36. package/dist/http/routes/capabilities.js +1 -1
  37. package/dist/http/routes/devices.d.ts +18 -8
  38. package/dist/http/routes/devices.js +175 -12
  39. package/dist/http/routes/tasks.js +10 -0
  40. package/dist/http/server.d.ts +13 -0
  41. package/dist/http/wire-types.d.ts +15 -0
  42. package/dist/model-route-url.d.ts +15 -0
  43. package/dist/model-route-url.js +19 -0
  44. package/dist/request-key-closure.js +1 -1
  45. package/dist/run-local.js +2 -1
  46. package/dist/sealed-key.js +2 -1
  47. package/dist/security.d.ts +6 -0
  48. package/dist/security.js +3 -0
  49. package/dist/trace/redact.d.ts +20 -0
  50. package/dist/trace/redact.js +117 -26
  51. package/package.json +7 -3
@@ -1,5 +1,6 @@
1
1
  import { createPublicKey, verify as cryptoVerify } from "node:crypto";
2
2
  import { AUTH_BRIDGE_ALGS, DEFAULT_CLOCK_SKEW_SEC, DEFAULT_JWKS_REFRESH_MIN_INTERVAL_SEC, DEFAULT_OFFLINE_GRACE_SEC, REGISTRY_JWKS_PATH, RegistryJwks, classifyTimeClaims, jwksWithinOfflineGrace, shouldForceJwksRefresh, toVerifiedIdentity, } from "@sema-agent/settings-schema/api/auth-bridge";
3
+ import { buildDisplaySafeUrl } from "./trace/redact.js";
3
4
  const DEFAULT_JWKS_TTL_SEC = 15 * 60;
4
5
  const MAX_JWKS_BYTES = 256 * 1024;
5
6
  const MAX_TOKEN_CHARS = 16 * 1024;
@@ -19,6 +20,7 @@ function b64urlJson(seg) {
19
20
  const stripSlash = (s) => s.replace(/\/+$/, "");
20
21
  export function createRegistryJwtVerifier(opts) {
21
22
  const issuer = stripSlash(opts.issuer);
23
+ const displayIssuer = buildDisplaySafeUrl(issuer);
22
24
  const jwksUrl = opts.jwksUrl ?? `${issuer}${REGISTRY_JWKS_PATH}`;
23
25
  const clockSkewSec = opts.clockSkewSec ?? DEFAULT_CLOCK_SKEW_SEC;
24
26
  const refreshMin = opts.jwksRefreshMinIntervalSec ?? DEFAULT_JWKS_REFRESH_MIN_INTERVAL_SEC;
@@ -32,7 +34,7 @@ export function createRegistryJwtVerifier(opts) {
32
34
  let inflight;
33
35
  const fail = (error, kid) => {
34
36
  opts.metrics?.inc("auth_bridge_verify_total", { outcome: error });
35
- opts.logger?.warn?.("auth_bridge_rejected", { error, ...(kid ? { kid: kid.slice(0, 128) } : {}), iss: issuer });
37
+ opts.logger?.warn?.("auth_bridge_rejected", { error, ...(kid ? { kid: kid.slice(0, 128) } : {}), iss: displayIssuer });
36
38
  return { ok: false, error };
37
39
  };
38
40
  async function refreshJwks() {
@@ -57,7 +59,7 @@ export function createRegistryJwtVerifier(opts) {
57
59
  next.set(k.kid, createPublicKey({ key: { kty: "RSA", n: k.n, e: k.e }, format: "jwk" }));
58
60
  }
59
61
  catch {
60
- opts.logger?.warn?.("auth_bridge_jwk_unusable", { kid: k.kid.slice(0, 128), iss: issuer });
62
+ opts.logger?.warn?.("auth_bridge_jwk_unusable", { kid: k.kid.slice(0, 128), iss: displayIssuer });
61
63
  }
62
64
  }
63
65
  keys.clear();
@@ -66,7 +68,7 @@ export function createRegistryJwtVerifier(opts) {
66
68
  lastFetchSuccessMs = now();
67
69
  }
68
70
  catch (e) {
69
- opts.logger?.warn?.("auth_bridge_jwks_fetch_failed", { iss: issuer, error: String(e).slice(0, 200) });
71
+ opts.logger?.warn?.("auth_bridge_jwks_fetch_failed", { iss: displayIssuer, error: String(e).slice(0, 200) });
70
72
  }
71
73
  finally {
72
74
  inflight = undefined;
@@ -13,13 +13,12 @@
13
13
  * 「这台机器上,device lane 的三件装配得起来吗」。装不起来就返回 `undefined`,由
14
14
  * {@link assertDeviceLaneWired} 响亮拒启([ref]:执行车道上没有静默降级臂)。
15
15
  */
16
- import { type DeviceEnrollRateLimiter, type DeviceEnrollment } from "../device-enrollment.js";
16
+ import { type DeviceEnrollment } from "../device-enrollment.js";
17
17
  import type { DeviceStore } from "../device-store.js";
18
18
  import { type DeviceWsHub } from "../device-ws-hub.js";
19
19
  import type { ServiceConfig } from "../config.js";
20
20
  import type { Logger } from "../observability/logger.js";
21
21
  import type { Metrics } from "../observability/metrics.js";
22
- import type { RateGate } from "../observability/rate-limit.js";
23
22
  import type { StoreBackend } from "../plugins/store-backend.js";
24
23
  export interface DeviceLaneCtx {
25
24
  config: ServiceConfig;
@@ -28,15 +27,6 @@ export interface DeviceLaneCtx {
28
27
  backend: StoreBackend | undefined;
29
28
  /** 本副本 id(写进连接租约行 —— §4.6 的跨副本排障面)。 */
30
29
  replicaId: string;
31
- /**
32
- * 既有的**跨副本** RateGate(SQL 后端形 = `TiDBRateLimiter`/`PgRateLimiter`;单副本形 = 进程内
33
- * `RateLimiter`)。A-2 的移交义务③:enrollment 的限速不自建第二套计数面,适配这一只。
34
- *
35
- * 🔴 传的是**取值器**不是值:那一只建在 main.ts 的下游(它要 `counterDegradeHook`),而 device lane
36
- * 必须建在 `createExecutionEnv` 之前 —— 顺序对不上。取值器让「谁先建」不再是正确性问题:boot 期
37
- * 不可能有 enroll 请求,取不到 ⇒ 座**抛**(fail-closed,enrollment 翻成 429),不是放行。
38
- */
39
- rateGate?: (() => RateGate | undefined) | undefined;
40
30
  }
41
31
  export interface DeviceLane {
42
32
  store: DeviceStore;
@@ -44,17 +34,20 @@ export interface DeviceLane {
44
34
  hub: DeviceWsHub;
45
35
  }
46
36
  /**
47
- * 把既有的**同步** {@link RateGate} 适配成 enrollment 要的异步座(A-2 移交义务③)。
37
+ * 注册口(`POST /v1/devices/enroll`,pre-auth)的限速策略 —— **本车道自己的地板,不挂任何运维旋钮**。
48
38
  *
49
- * 🔴 为什么是适配而不是新建一只:限速面在本仓已有属主(`observability/rate-limit.ts` + 两支 SQL 孪生),
50
- * 而 SQL 孪生是**跨副本**的 —— enrollment 自带的 `createFixedWindowRateLimiter` 诚实地只是每副本窗
51
- * (它自己的头注就这么写),对 token 撞库来说「N 个副本 = N 倍配额」。适配既有的那一只,撞库预算才是
52
- * 全局的一份。
39
+ * 🔴 此前这里把全局 RateGate 适配成注册限速器,于是限额**值**也跟着全局 `RATE_LIMIT_RPM` 走;而它缺省是 0,
40
+ * 0 在那只限速器里是「关断哨兵」⇒ 缺省部署上注册口**完全不限速**(实测同一窗 200 次尝试拒绝 0 次),
41
+ * 设计 §7-T5 ⑤ 对 enroll token 撞库的那道防线在出厂形上不存在。安全地板挂在一根语义无关的运维旋钮上,
42
+ * 是「部署级旋钮禁挂派生腿」那条教训的又一形。⇒ 注册限速恒用本策略的固定窗,与 `RATE_LIMIT_RPM` 无关。
53
43
  *
54
- * 键前缀 `device-enroll:` 是必须的:RateGate 的键空间与 HTTP 面的 principal 键**共用一张表**,不加前缀
55
- * 会让一次 enrollment 尝试吃掉调用方的普通请求配额(反过来也一样)。
44
+ * 诚实边界:窗是**每副本**的。device 车道的部署形是单副本(`docs/DEPLOY-PREREQS.md`),滚动期两个进程
45
+ * 短暂重叠时上界 = 限额 × 重叠副本数;不声称跨副本全局上界。
56
46
  */
57
- export declare function createDeviceEnrollRateLimiter(gate: () => RateGate | undefined): DeviceEnrollRateLimiter;
47
+ export declare const DEVICE_ENROLL_RATE_POLICY: {
48
+ readonly limit: 10;
49
+ readonly windowMs: 60000;
50
+ };
58
51
  /**
59
52
  * 建齐 device lane 的三件。返回 `undefined` = **建不起来**(没有 SQL 后端 ⇒ 没有四表店)。
60
53
  *
@@ -1,16 +1,6 @@
1
1
  import { createDeviceEnrollment, createFixedWindowRateLimiter } from "../device-enrollment.js";
2
2
  import { createDeviceWsHub } from "../device-ws-hub.js";
3
- export function createDeviceEnrollRateLimiter(gate) {
4
- return {
5
- check: async (key) => {
6
- const g = gate();
7
- if (!g)
8
- throw new Error("device enrollment rate gate is not wired yet (boot window) — refusing rather than admitting unmetered enrollment attempts");
9
- const verdict = g.check(`device-enroll:${key}`);
10
- return { allowed: verdict.allowed, retryAfterSec: verdict.retryAfterSec };
11
- },
12
- };
13
- }
3
+ export const DEVICE_ENROLL_RATE_POLICY = { limit: 10, windowMs: 60_000 };
14
4
  export function createDeviceLane(ctx) {
15
5
  const { config, logger, metrics, backend, replicaId } = ctx;
16
6
  const store = backend?.device?.();
@@ -19,7 +9,7 @@ export function createDeviceLane(ctx) {
19
9
  const knobs = config.remoteExec?.provider === "device" ? config.remoteExec : undefined;
20
10
  const enrollment = createDeviceEnrollment({
21
11
  store,
22
- ...(ctx.rateGate ? { rateLimiter: createDeviceEnrollRateLimiter(ctx.rateGate) } : { rateLimiter: createFixedWindowRateLimiter({ limit: 10, windowMs: 60_000 }) }),
12
+ rateLimiter: createFixedWindowRateLimiter(DEVICE_ENROLL_RATE_POLICY),
23
13
  });
24
14
  const hub = createDeviceWsHub({
25
15
  store,
@@ -41,7 +31,7 @@ export function createDeviceLane(ctx) {
41
31
  replicaId,
42
32
  maxInflightPerDevice: knobs?.maxInflightPerDevice ?? 4,
43
33
  maxPreAuthPerRemote: knobs?.maxPreAuthPerRemote ?? 8,
44
- crossReplicaEnrollRateLimit: ctx.rateGate !== undefined,
34
+ enrollRateLimit: `${DEVICE_ENROLL_RATE_POLICY.limit}/${DEVICE_ENROLL_RATE_POLICY.windowMs / 1000}s per caller identity (per replica; independent of RATE_LIMIT_RPM)`,
45
35
  });
46
36
  return { store, enrollment, hub };
47
37
  }
@@ -2,10 +2,9 @@ import { createToolTracer, createPermissionDeniedMeter } from "../observability/
2
2
  import { composeHooks } from "../hooks/hook-runner.js";
3
3
  import { assertDeviceLaneWired, createDeviceLane } from "./device-lane.js";
4
4
  import { createExecutionEnv } from "./execution-env.js";
5
- import { rateLimiter } from "./late-binds.js";
6
5
  export function runStage05(ctx) {
7
6
  const { backend, config, instanceId, logger, metrics, taskAttachmentStore } = ctx;
8
- const deviceLane = config.remoteExec?.provider === "device" ? createDeviceLane({ config, logger, metrics, backend, replicaId: instanceId, rateGate: () => rateLimiter }) : undefined;
7
+ const deviceLane = config.remoteExec?.provider === "device" ? createDeviceLane({ config, logger, metrics, backend, replicaId: instanceId }) : undefined;
9
8
  const { perTaskImage, sessionEnvSelection, perSessionCwd, setSessionCwd, setSessionShellEnv, fenceSessionOwner, dropSessionScoped, executionEnvFactory, worktreeReap, sendUserFileTaskEnvs, lspManager, } = createExecutionEnv({ config, logger, metrics, taskAttachmentStore, ...(deviceLane ? { deviceLane } : {}) });
10
9
  assertDeviceLaneWired(config.remoteExec?.provider === "device", deviceLane, executionEnvFactory !== undefined);
11
10
  const toolTracer = config.toolTrace ? createToolTracer(logger) : undefined;
@@ -20,6 +20,7 @@ import { createParkedReviveInheritedGate } from "./parked-revive-gate.js";
20
20
  import { createWebFetchSummarizeLane } from "./webfetch-summarize-lane.js";
21
21
  import { createRunnerDepsOnAsk } from "./runner-deps.js";
22
22
  import { bindRateLimiter, bindRunStore } from "./late-binds.js";
23
+ import { buildDisplaySafeUrl } from "../trace/redact.js";
23
24
  export async function runStage07(ctx) {
24
25
  const { backend, backgroundAgentStore, brain, checkpointStore, config, configCenter, counterDegradeHook, durableEnabled, handslessSubRunner, localRoot, logger, memoryEngine, metrics, question, runner, runnerDeps, sendUserFileToolSpec, sessionEnvSelection, sessionStore, subRunner, subRunnerSessions, taskListLane, toolApproval } = ctx;
25
26
  const authorize = createAuthorizer(config, sessionStore);
@@ -66,7 +67,7 @@ export async function runStage07(ctx) {
66
67
  logger.info("a2a_serve_enabled", {
67
68
  card: "/.well-known/agent-card.json",
68
69
  rpc: "/v1/a2a",
69
- advertisedUrl: config.a2aServe.url,
70
+ advertisedUrl: buildDisplaySafeUrl(config.a2aServe.url),
70
71
  skills: config.a2aServe.skills.length,
71
72
  blockingWaitMs: config.a2aServe.blockingWaitMs,
72
73
  tasksUsable: runStore !== undefined,
@@ -4,6 +4,7 @@ import { createRegistryJwtVerifier } from "../auth-bridge.js";
4
4
  import { createSessionFaces } from "./session-faces.js";
5
5
  import { createLeaderFace } from "./leader.js";
6
6
  import { assertStaticWiringConsistent } from "../http/routes/diagnostics.js";
7
+ import { buildDisplaySafeUrl } from "../trace/redact.js";
7
8
  export function runStage09(ctx) {
8
9
  const { approvalExemptionStore, backend, brain, checkpointStore, config, configCenter, deviceLane, dropSessionScoped, durableEnabled, executionEnvFactory, fileHistoryStore, governanceSeams, localRoot, logger, metrics, pricing, resumeAnchorStore, runStore, runnerDeps, sessionPolicyStore, sessionStore, streamApprovalGate, taskAttachmentStore, taskListLane, toolResultStore, tracer, usageWindowStore, workflowCompletionInbox } = ctx;
9
10
  const { ownerAware, sessionAudit, sessionWatchRegistry, purgeSession, instrumentDegenerate, planCacheProbe } = createSessionFaces({
@@ -26,7 +27,7 @@ export function runStage09(ctx) {
26
27
  })
27
28
  : undefined;
28
29
  if (registryJwtVerifier)
29
- logger.info("auth_bridge_enabled", { issuer: authBridgeIssuer });
30
+ logger.info("auth_bridge_enabled", { issuer: buildDisplaySafeUrl(authBridgeIssuer ?? "") });
30
31
  const hookWakeBus = {};
31
32
  const wiringSpecTemplate = durableEnabled
32
33
  ? { checkpointStore, durableApproval: { scope: encodeCheckpointScope(undefined) } }
@@ -21,6 +21,7 @@ import { createRunnerDepsOnAsk } from "./runner-deps.js";
21
21
  import { installShutdownHandlers } from "./shutdown.js";
22
22
  import { bindServerLateSeats } from "./late-binds.js";
23
23
  import { serviceVersion } from "../service-version.js";
24
+ import { buildDisplaySafeUrl } from "../trace/redact.js";
24
25
  export async function runStage10(ctx) {
25
26
  const { approvalAskAudit, approvalExemptionStore, approvalNonceStore, authorize, backend, backgroundAgentStore, brain, breakerState, centerRuntimeCapsResolver, checkpointStore, config, configCenter, costQuota, deploymentHooks, deviceLane, drainState, durableEnabled, elicitation, fenceSessionOwner, fileHistoryStore, fleetBus, fleetLease, fleetUsage, governanceSeams, handslessRunner, hookAgent, hookLlm, hookWakeBus, imageBakes, imageIndex, instanceId, instrumentDegenerate, leaderEndpoint, localRoot, logger, lspManager, mcpRevocations, memoryBundleFaces, memoryComplianceFaces, memoryConsolidationFaces, memoryEngine, memoryExportBackend, memoryOptOutGrantStore, memoryOriginFaces, memoryPosture, memorySyncCursors, metrics, modelUsageTracker, orgMemoryAdmission, otelExporter, outcomeSink, ownerAware, parkedReviveInheritedGate, parkedReviveTool, perSessionCwd, perTaskImage, planCacheProbe, pricing, principalCaps, promptManifestTracker, purgeSession, question, rateLimiter, reaper, registryJwtVerifier, resumeAnchorStore, retentionLaneStarted, retentionLaneStore, ruleConsent, runStore, runner, runnerDeps, runtimeCapsPeekResolver, scenarioDetails, scenarios, selectEnvTool, sendFileLedger, sendUserFileEmitter, sendUserFileToolSpec, sessionAudit, sessionEnvSelection, sessionMemoryStatusFace, sessionPolicyStore, sessionShellGateRegistry, sessionTitler, sessionWatchRegistry, setSessionCwd, setSessionShellEnv, sharedMemoryStore, sideQueryAccounting, singleUserAutoAcceptBaseline, sqlEngineFacts, sqlWorkflowRunStore, staticWiring, storeBackendDegraded, subRunner, subagentSteerRegistry, taskAttachmentStore, taskLimitCaps, taskTimeoutSec, toolApproval, toolResultStore, usageWindowStore, webSearchProvider, workflowAgentRegistry, workflowAgentSessionIndex, workflowCompletionInbox, workflowJournalStore, workflowModelAllowlistSnapshot, workflowNotifyGate, workflowNotifyJournal, workflowRecoverOpts, workflowRunStore, workflowsCapable } = ctx;
26
27
  const stores = {
@@ -67,6 +68,7 @@ export async function runStage10(ctx) {
67
68
  const seams = {
68
69
  deviceHub: deviceLane?.hub,
69
70
  deviceStore: deviceLane?.store,
71
+ deviceEnrollment: deviceLane?.enrollment,
70
72
  sessionAudit,
71
73
  routeJudgeBrain: brain,
72
74
  purgeSession: purgeSession ? purgeSession : undefined,
@@ -237,7 +239,7 @@ export async function runStage10(ctx) {
237
239
  logger: { info: (m, d) => logger.info(m, d ?? {}), warn: (m, d) => logger.warn(m, d ?? {}) },
238
240
  });
239
241
  if (fleetClient)
240
- logger.info("fleet_client_started", { worker: config.configCenter?.worker, address: process.env.FLEET_ADVERTISE_ADDRESS });
242
+ logger.info("fleet_client_started", { worker: config.configCenter?.worker, address: buildDisplaySafeUrl(process.env.FLEET_ADVERTISE_ADDRESS ?? "") });
241
243
  logger.info("listening", {
242
244
  port: config.port,
243
245
  bindHost: bindHost ?? "0.0.0.0/::(all interfaces)",
@@ -255,7 +257,7 @@ export async function runStage10(ctx) {
255
257
  maxPrincipalCostUsd: config.maxPrincipalCostUsd || null,
256
258
  costQuota: config.maxPrincipalCostUsd > 0 ? counterStoreLabel(backend?.costQuota, backend?.kind) : "off",
257
259
  toolResultStore: toolResultStore ? (backend?.kind === "local" ? "file(local)" : "shared(sql)") : "in-process",
258
- otel: config.otel ? config.otel.endpoint : null,
260
+ otel: config.otel ? buildDisplaySafeUrl(config.otel.endpoint) : null,
259
261
  perModelKeys: Object.keys(config.modelApiKeyEnv).length,
260
262
  sealedModelKeys: Object.values(config.modelApiKeys).filter((v) => typeof v === "string").length,
261
263
  sealedPoisonedModels: Object.values(config.modelApiKeys).filter((v) => typeof v !== "string").length,
@@ -32,6 +32,7 @@ import { resolveStreamApprovalGate } from "../tool-approval.js";
32
32
  import { buildMemoryWriteBoundaryAudit, effectiveMemoryPersistenceCapable } from "./memory-boundary.js";
33
33
  import { buildMemoryPosture } from "../memory-posture.js";
34
34
  import { establishSqlEnginePosture } from "../sql-engine-posture.js";
35
+ import { buildDisplaySafeUrl } from "../trace/redact.js";
35
36
  const DARK_REASON_PROSE = {
36
37
  engine_off: () => "MEMORY_ENGINE=off",
37
38
  multi_tenant_file_backend: () => "multi-tenant on the FILE backend (no tenant isolation; set MEMORY_ENGINE_BACKEND=pg|tidb to light up)",
@@ -361,7 +362,7 @@ export async function openStores(ctx) {
361
362
  ...(config.memorySync.maxPushEntries !== undefined ? { maxPushEntries: config.memorySync.maxPushEntries } : {}),
362
363
  ...(config.memorySync.maxPullEntries !== undefined ? { maxPullEntries: config.memorySync.maxPullEntries } : {}),
363
364
  });
364
- logger.info("memory_sync_enabled", { url: config.memorySync.url, scope: config.memorySync.scope, cursorPath: memorySyncRunner.cursorPath });
365
+ logger.info("memory_sync_enabled", { url: buildDisplaySafeUrl(config.memorySync.url), scope: config.memorySync.scope, cursorPath: memorySyncRunner.cursorPath });
365
366
  memorySyncRunner.trigger("boot");
366
367
  }
367
368
  else {
package/dist/brain.js CHANGED
@@ -3,6 +3,7 @@ import { normalizeBaseUrl } from "@sema-agent/core";
3
3
  import { buildDefaultResilience } from "./config-types.js";
4
4
  import { resolveModelApiKey } from "./key-resolver.js";
5
5
  import { catalogBrainLane } from "./model-provider.js";
6
+ import { buildDisplaySafeUrl } from "./trace/redact.js";
6
7
  function isOffDeploymentRoot(baseUrl, config) {
7
8
  if (!baseUrl)
8
9
  return false;
@@ -119,7 +120,7 @@ export function brainSummary(config) {
119
120
  const hasFailover = config.gatewayFallbackUrls.length > 0;
120
121
  const breakerActive = r.circuitBreaker && hasFailover;
121
122
  return {
122
- gateway: config.gatewayBaseUrl,
123
+ gateway: buildDisplaySafeUrl(config.gatewayBaseUrl),
123
124
  gatewayFallbacks: config.gatewayFallbackUrls.length,
124
125
  failover: hasFailover,
125
126
  anthropicRoute: Boolean(config.anthropic),
@@ -137,7 +138,7 @@ export function brainSummary(config) {
137
138
  circuitBreakerNoop: r.circuitBreaker && !hasFailover ? "needs ≥2 gateway routes" : null,
138
139
  reactiveDegradeTo: config.degrade?.reactive && config.models?.[config.degrade.to] ? config.degrade.to : null,
139
140
  reactiveDegradeBaseUrl: config.degrade?.reactive && config.models?.[config.degrade.to]
140
- ? config.models[config.degrade.to].baseUrl || config.gatewayBaseUrl
141
+ ? buildDisplaySafeUrl(config.models[config.degrade.to].baseUrl || config.gatewayBaseUrl)
141
142
  : null,
142
143
  ...((() => {
143
144
  const target = config.degrade?.reactive ? config.models?.[config.degrade.to] : undefined;
@@ -55,7 +55,7 @@ export const CONFIG_CATALOG = [
55
55
  r("ALLOW_UNAUTHED_WRITES", "auth", "boolean", "无 service token 时仍接受任务提交(本地开发逃生口;缺省 fail-closed 拒提交)", { staticDefault: "false", danger: SEC, configKey: "allowUnauthedWrites" }),
56
56
  r("ANTHROPIC_API_KEY", "modelPlane", "secret", "Anthropic 协议路线的 x-api-key 凭据(与 AUTH_TOKEN 二选一,AUTH_TOKEN 优先;与 BASE_URL 同在时可推断 provider)", { danger: [...SEC, ...BILL] }),
57
57
  r("ANTHROPIC_AUTH_TOKEN", "modelPlane", "secret", "Anthropic 兼容生态的 Bearer 凭据(设了则不发 x-api-key)", { danger: [...SEC, ...BILL] }),
58
- r("ANTHROPIC_BASE_URL", "modelPlane", "url", "Anthropic 协议路线 base URL(与凭据同在且 MODEL_PROVIDER 未设 ⇒ 推断 provider=anthropic 并 boot notice)"),
58
+ r("ANTHROPIC_BASE_URL", "modelPlane", "url", "Anthropic 协议路线 base URL(与凭据同在且 MODEL_PROVIDER 未设 ⇒ 推断 provider=anthropic 并 boot notice;S-475:带 userinfo 拒启,机读码 `config.gateway_url_userinfo`)"),
59
59
  r("ANTHROPIC_CACHE_BREAKPOINTS", "modelPlane", "boolean", "Anthropic 路线 prompt-cache breakpoints 开关", { staticDefault: "true" }),
60
60
  r("ANTHROPIC_MAX_RETRIES", "modelPlane", "number", "Anthropic 路线 SDK 每次调用重试上限", { derivedDefaultNote: "缺席 ⇒ 不传引擎,core/SDK 默认当家" }),
61
61
  r("ANTHROPIC_VERSION", "modelPlane", "string", "Anthropic API version 头(缺省由 SDK 定)"),
@@ -107,6 +107,7 @@ export const CONFIG_CATALOG = [
107
107
  r("DELEGATION_MAX_CONCURRENT", "orchestration", "number", "委派入口并发帽", { derivedDefaultNote: "缺席 ⇒ 不铸键,core 自缺省(CC parity 20)" }),
108
108
  r("DELEGATION_MAX_PER_SESSION", "orchestration", "number", "委派入口每会话累计帽", { derivedDefaultNote: "缺席 ⇒ 不铸键,core 自缺省(CC parity 200)" }),
109
109
  r("DEVICE_DELIVERY_TIMEOUT_MS", "orchestration", "number", "device 车道等在途槽上限(ms;到期 device.busy)", { derivedDefaultNote: "缺席 ⇒ adapter 默认 10s" }),
110
+ r("DEVICE_ENROLL_TOKEN_TTL_SEC", "orchestration", "number", "enrollment token 有效期(整秒,自 DB now 起算;域 [1,86400],坏值拒启)", { danger: SEC, derivedDefaultNote: "缺席 ⇒ 签发点默认 900(15min)" }),
110
111
  r("DEVICE_EXEC_TIMEOUT_MS", "orchestration", "number", "device 车道指令级默认超时(ms;per-command 可收紧)", { derivedDefaultNote: "缺席 ⇒ adapter 默认 1800s" }),
111
112
  r("DEVICE_MAX_INFLIGHT_PER_DEVICE", "orchestration", "number", "per-device 在途上限(超额云侧排队)", { derivedDefaultNote: "缺席 ⇒ 4(§11-O3 裁定值)" }),
112
113
  r("DEVICE_MAX_PREAUTH_PER_REMOTE", "orchestration", "number", "未鉴权握手的 per-remote 并发帽(正整数或字面 off;XFF/NAT 形用 off+全局帽)", { danger: SEC, derivedDefaultNote: "缺席 ⇒ 8" }),
@@ -281,8 +282,8 @@ export const CONFIG_CATALOG = [
281
282
  r("MODEL_EXTRA_BODY", "modelPlane", "opaque", "生成参数直通 JSON(top_k/logit_bias 等;必须静态 —— 前缀缓存键序)", { danger: SEC }),
282
283
  r("MODEL_FIRST_TOKEN_TIMEOUT_MS", "modelPlane", "number", "首 token 超时(ms;0=关 —— 开流不吐字的唯一看门狗;自托管长 prefill 后端按此档留足)", { staticDefault: String(RESILIENCE_DEFAULTS.firstTokenTimeoutMs.value) }),
283
284
  r("MODEL_FREQUENCY_PENALTY", "modelPlane", "number", "frequency_penalty 直通(压过 EXTRA_BODY 同键)"),
284
- r("MODEL_GATEWAY_BASEURL", "modelPlane", "url", "openai 兼容网关 base URL(默认 localhost 占位 —— 公开分发不烤内网坐标)", { staticDefault: "http://127.0.0.1:8000/v1", configKey: "gatewayBaseUrl", center: cModels("models[].baseUrl") }),
285
- r("MODEL_GATEWAY_FALLBACK_URLS", "modelPlane", "url", "网关故障转移地址表(CSV)", { configKey: "gatewayFallbackUrls" }),
285
+ r("MODEL_GATEWAY_BASEURL", "modelPlane", "url", "openai 兼容网关 base URL(默认 localhost 占位 —— 公开分发不烤内网坐标;S-475:带 userinfo 拒启,机读码 `config.gateway_url_userinfo` —— WHATWG 禁止用带凭据的 URL 构造 fetch Request,凭据放 MODEL_API_KEY)", { staticDefault: "http://127.0.0.1:8000/v1", configKey: "gatewayBaseUrl", center: cModels("models[].baseUrl") }),
286
+ r("MODEL_GATEWAY_FALLBACK_URLS", "modelPlane", "url", "网关故障转移地址表(CSV;S-475:**逐项**判 userinfo,任一项带 ⇒ 拒启,机读码 `config.gateway_url_userinfo`)", { configKey: "gatewayFallbackUrls" }),
286
287
  r("MODEL_ID", "modelPlane", "string", "主模型 id(3.0.0 起无出厂缺省,缺 ⇒ 拒启指路)", { configKey: "model", resolve: (c) => c.model.id, center: cModels("models[].id") }),
287
288
  r("MODEL_IDLE_TIMEOUT_MS", "modelPlane", "number", "流中静默看门狗(ms;0=关)", { staticDefault: String(RESILIENCE_DEFAULTS.idleTimeoutMs.value) }),
288
289
  r("MODEL_MAX_TOKENS", "modelPlane", "number", "主模型输出上限", { derivedDefaultNote: "缺席 ⇒ min(32000, contextWindow)", resolve: (c) => c.model.maxTokens ?? null, center: cModels("models[].maxTokens") }),
@@ -6,6 +6,7 @@ import { validateCommandRules } from "../runtime-governance.js";
6
6
  import { projectCollabToWorkflows, registerCollabWorkflows } from "../capabilities/collab-workflows.js";
7
7
  import { stageLimits } from "./stage-limits.js";
8
8
  import { BATCH1_LIMIT_KEYS } from "./hot-keys-registry.js";
9
+ import { buildDisplaySafeUrl } from "../trace/redact.js";
9
10
  const charsPerTokenOf = (v) => (typeof v === "number" && Number.isFinite(v) && v > 0 ? v : undefined);
10
11
  function envDerivedModel(config) {
11
12
  const m = config.envModel;
@@ -200,9 +201,9 @@ function stageEffective(config, eff, logger, opts) {
200
201
  ok = false;
201
202
  }
202
203
  if (ok)
203
- logger?.warn("sema_registry_model_base_url", { model: m.name, baseUrl: rawBaseUrl, hint: "registry-directed gateway override — this model's key authenticates AGAINST THIS URL" });
204
+ logger?.warn("sema_registry_model_base_url", { model: m.name, baseUrl: buildDisplaySafeUrl(rawBaseUrl), hint: "registry-directed gateway override — this model's key authenticates AGAINST THIS URL" });
204
205
  else {
205
- logger?.warn("sema_registry_model_base_url_rejected", { model: m.name, baseUrl: String(rawBaseUrl).slice(0, 120), hint: "only http(s) URLs are honored — falling back to the boot-env gateway" });
206
+ logger?.warn("sema_registry_model_base_url_rejected", { model: m.name, baseUrl: buildDisplaySafeUrl(String(rawBaseUrl)).slice(0, 120), hint: "only http(s) URLs are honored — falling back to the boot-env gateway" });
206
207
  safeUrl = undefined;
207
208
  }
208
209
  }
@@ -1,10 +1,18 @@
1
- import { type EntitlementRuntimeCaps } from "@sema-agent/settings-schema";
1
+ import { type EffectiveReadWarning, type EntitlementRuntimeCaps } from "@sema-agent/settings-schema";
2
2
  import type { ScenarioRuling } from "../capabilities/scenarios.js";
3
3
  import type { EffectiveConfig, ExecutionRuling } from "./types.js";
4
4
  /** GET the effective config (Bearer + ETag). null = 304 (unchanged). Throws on transport/HTTP error, and on a
5
5
  * payload that is not an effective config at all(非对象 / version 非有限数 / gate 域坏形——[ref] 裁B)。
6
6
  * `domainErrors`:坏 catalog 域(schema default 已落)逐域单列——与本地腿 `FetchEffectiveResult` 同键同义,
7
- * boot/refresh 的候选门直接消费。 */
7
+ * boot/refresh 的候选门直接消费。
8
+ *
9
+ * `readWarnings`:契约 `onWarning` 流的**原值**(未折叠),与本地腿同键同义。折叠是**有损**的——
10
+ * `hosts-grandfathered` / `legacy-governance-lifted` 两员按律折成 `null`(值已被收编接受,见
11
+ * `read-warnings.ts` 头注),于是只看 `domainErrors` 的人**永远**看不见兼容臂有没有开着;而契约把
12
+ * `legacy-governance-lifted` 铸出来,理由逐字写在 `legacyGovernanceFromRuntime` 的 SUNSET 注里:
13
+ * 「关窗的前提是有人知道它还开着」。两条读腿各返一份原流,那句话在本仓才成立。
14
+ * 🔐 原流的 `error` 字段**未经** `redactConfigError`(那道脱敏挂在真正进日志流的 `domainErrors` 产出边界上)
15
+ * —— 任何把本键路由进结构化日志/审计流的消费者必须自己先脱敏。 */
8
16
  export declare function fetchEffective(baseUrl: string, token: string, etag: string | undefined, fetchImpl?: typeof fetch, worker?: string): Promise<{
9
17
  effective: EffectiveConfig;
10
18
  etag?: string;
@@ -12,6 +20,7 @@ export declare function fetchEffective(baseUrl: string, token: string, etag: str
12
20
  domain: string;
13
21
  error: string;
14
22
  }>;
23
+ readWarnings: readonly EffectiveReadWarning[];
15
24
  } | null>;
16
25
  /**
17
26
  * [ref] §K (core 1.157 `RunnerDeps.runtimeCapsResolver`) — GET the PER-PRINCIPAL runtime
@@ -29,7 +29,7 @@ export async function fetchEffective(baseUrl, token, etag, fetchImpl = fetch, wo
29
29
  domainErrors.push(f);
30
30
  }
31
31
  const effective = wire;
32
- return { effective, etag: res.headers.get("etag") ?? undefined, ...(domainErrors.length > 0 ? { domainErrors } : {}) };
32
+ return { effective, etag: res.headers.get("etag") ?? undefined, readWarnings: warnings, ...(domainErrors.length > 0 ? { domainErrors } : {}) };
33
33
  }
34
34
  export async function fetchPrincipalCaps(baseUrl, token, principal, etag, fetchImpl = fetch, worker) {
35
35
  const raw = await requestPrincipalView(baseUrl, token, principal, etag, fetchImpl, worker);
@@ -19,7 +19,7 @@
19
19
  * 候选门整拒,而它在 1.10.0 上零告警零 domainErrors(亲验读数)。兼容臂的关窗观测归属主(settings-schema 的
20
20
  * `onWarning` 流),不借候选门这条带副作用的通道传信息。
21
21
  */
22
- import type { EffectiveReadWarning } from "@sema-agent/settings-schema";
22
+ import { type EffectiveReadWarning } from "@sema-agent/settings-schema";
23
23
  export type FoldedReadWarning = {
24
24
  readonly domain: string;
25
25
  readonly error: string;
@@ -1,3 +1,4 @@
1
+ import { formatConfigPath } from "@sema-agent/settings-schema";
1
2
  const MAX_ERROR_CHARS = 600;
2
3
  const cap = (s) => s.slice(0, MAX_ERROR_CHARS);
3
4
  const errorText = (e) => (e instanceof Error ? e.message : String(e));
@@ -8,10 +9,11 @@ const keyList = (prefix, keys, truncatedFrom) => {
8
9
  let used = 0;
9
10
  const suffixFor = (n) => (n < total ? ` (+${total - n} more, list truncated)` : "");
10
11
  for (const k of keys) {
11
- const piece = (shown.length > 0 ? ", " : "") + k;
12
+ const rendered = formatConfigPath(k);
13
+ const piece = (shown.length > 0 ? ", " : "") + rendered;
12
14
  if (used + piece.length + suffixFor(shown.length + 1).length > budget)
13
15
  break;
14
- shown.push(k);
16
+ shown.push(rendered);
15
17
  used += piece.length;
16
18
  }
17
19
  return `${prefix}${shown.join(", ")}${suffixFor(shown.length)}`;
@@ -24,7 +26,7 @@ export function foldEffectiveReadWarning(w) {
24
26
  case "domain-defaulted":
25
27
  return { domain: w.domain, error: cap(errorText(w.error)) };
26
28
  case "entry-dropped":
27
- return { domain: w.domain, error: cap(`entry dropped by domain schema: ${w.path} (${errorText(w.error)})`) };
29
+ return { domain: w.domain, error: cap(`entry dropped by domain schema: ${formatConfigPath(w.path)} (${errorText(w.error)})`) };
28
30
  case "unknown-keys-dropped":
29
31
  return { domain: w.domain, error: keyList("unknown keys dropped by domain schema: ", w.keys, w.truncatedFrom) };
30
32
  case "unknown-keys-carried":
@@ -61,7 +61,9 @@ export interface CenterModel {
61
61
  * ⚠️ 前身两键 `defaultThinking` / `reasoningEffortLevels` 已随本件删除:它们从来没有声明面 ——
62
62
  * settings-schema 的 `ModelEntry` 里没有这两个键,**两条腿**(本地 `parseDomainLoud` / 远端
63
63
  * `readEffectiveWire`)都把它们当未知键剥掉(1.10.0 夹具实测:`unknown-keys-dropped
64
- * {keys:["models.0.defaultThinking","models.0.reasoningEffortLevels"]}`),`mapToServiceEffective` 的
64
+ * {keys:["models.0.defaultThinking","models.0.reasoningEffortLevels"]}`;settings-schema 3.0.0 起同一条
65
+ * 告警的坐标是**段数组** `{keys:[["models",0,"defaultThinking"],…]}`,出门形走 `formatConfigPath`),
66
+ * `mapToServiceEffective` 的
65
67
  * CLOSED 投影也从未列过它们。留着 = 一份读不到任何东西的类型承诺 + `compat.reasoningEffortLevels`
66
68
  * 之外的第二个写者([ref]:同一语义面禁两个写者)。roster 级 `defaultThinking` 若要真做,归 schema 补键
67
69
  * 另立单据(见 CHANGELOG 未闭环)。
@@ -28,9 +28,9 @@ import type { ServiceConfigFlat } from "./config-types.js";
28
28
  */
29
29
  export declare const LIVENESS_HEARTBEAT_MS = 30000;
30
30
  /** 闭集:新增一条不变量必须在这里加词(拼错 = 编译红,而不是运行期悄悄不判)。 */
31
- export type CrossInvariantId = "run-stale-heartbeat" | "bind-posture" | "bake-runner-credential" | "direct-door-anchors" | "operator-principals-required" | "bake-operator-principals" | "sensitive-write-patterns" | "leader-host-lane" | "leader-durable-store" | "device-durable-store" | "device-durable-approval" | "model-provider-word";
31
+ export type CrossInvariantId = "run-stale-heartbeat" | "bind-posture" | "bake-runner-credential" | "direct-door-anchors" | "operator-principals-required" | "bake-operator-principals" | "sensitive-write-patterns" | "leader-host-lane" | "leader-durable-store" | "device-durable-store" | "device-durable-approval" | "model-provider-word" | "gateway-url-userinfo";
32
32
  /** 表读的键集(取自平铺配置型 ⇒ 键名/类型永不与真配置漂)。 */
33
- export type CrossInvariantView = Readonly<Pick<ServiceConfigFlat, "runStaleSec" | "authToken" | "authTokens" | "allowUnauthedWrites" | "imageBakes" | "directApprovalDoor" | "durableApproval" | "approvalHmacKeys" | "principalJwtPubkeys" | "principalJwtIss" | "principalJwtAud" | "requirePrincipal" | "operatorPrincipals" | "sensitiveWritePatterns" | "leaderEnabled" | "remoteExec" | "dbBackend" | "tidb" | "pg" | "envModel">>;
33
+ export type CrossInvariantView = Readonly<Pick<ServiceConfigFlat, "runStaleSec" | "authToken" | "authTokens" | "allowUnauthedWrites" | "imageBakes" | "directApprovalDoor" | "durableApproval" | "approvalHmacKeys" | "principalJwtPubkeys" | "principalJwtIss" | "principalJwtAud" | "requirePrincipal" | "operatorPrincipals" | "sensitiveWritePatterns" | "leaderEnabled" | "remoteExec" | "dbBackend" | "tidb" | "pg" | "envModel" | "gatewayBaseUrl" | "gatewayFallbackUrls" | "anthropic">>;
34
34
  export interface CrossInvariantInput {
35
35
  /** 现役配置(boot 期 = 候选自身)。 */
36
36
  readonly live: CrossInvariantView;
@@ -1,5 +1,6 @@
1
1
  import { assertGuardPatternsUsable } from "./deployment-governance.js";
2
2
  import { modelProviderRefusal } from "./model-provider.js";
3
+ import { modelRouteUrlRefusal } from "./model-route-url.js";
3
4
  export const LIVENESS_HEARTBEAT_MS = 30_000;
4
5
  const anyServiceAuth = (v) => Boolean(v.authToken) || Object.keys(v.authTokens ?? {}).length > 0;
5
6
  const mustNarrowBind = (v) => Boolean(v.allowUnauthedWrites) && !anyServiceAuth(v);
@@ -137,6 +138,17 @@ export const CONFIG_CROSS_INVARIANTS = [
137
138
  bootAnchor: { file: "src/config.ts", marker: "CROSS-INVARIANT:model-provider-word" },
138
139
  check: ({ next }) => (next.envModel.provider === undefined ? undefined : modelProviderRefusal(next.envModel.provider)),
139
140
  },
141
+ {
142
+ id: "gateway-url-userinfo",
143
+ keys: ["gatewayBaseUrl", "gatewayFallbackUrls", "anthropic"],
144
+ restartReason: "model-route-url",
145
+ bootAnchor: { file: "src/config.ts", marker: "CROSS-INVARIANT:gateway-url-userinfo" },
146
+ check: ({ next }) => modelRouteUrlRefusal("MODEL_GATEWAY_BASEURL", next.gatewayBaseUrl, "MODEL_API_KEY") ??
147
+ next.gatewayFallbackUrls
148
+ .map((u, i) => modelRouteUrlRefusal(`MODEL_GATEWAY_FALLBACK_URLS[${i}]`, u, "MODEL_API_KEY"))
149
+ .find((r) => r !== undefined) ??
150
+ modelRouteUrlRefusal("ANTHROPIC_BASE_URL", next.anthropic?.baseUrl ?? "", "ANTHROPIC_API_KEY"),
151
+ },
140
152
  ];
141
153
  export function crossInvariant(id) {
142
154
  const inv = CONFIG_CROSS_INVARIANTS.find((i) => i.id === id);
@@ -1,5 +1,5 @@
1
1
  import { FileConfigStore } from "@sema-agent/settings-schema/node";
2
- import { type EffectiveConfig as AgentConfigEffective } from "@sema-agent/settings-schema";
2
+ import { type EffectiveConfig as AgentConfigEffective, type EffectiveReadWarning } from "@sema-agent/settings-schema";
3
3
  import { fetchEffective as remoteFetchEffective, fetchSkillContent as remoteFetchSkillContent, type EffectiveConfig } from "./config-center/facade.js";
4
4
  /** Result of a `fetchEffective` — EXACTLY the config-center's return: the effective config + its etag, or
5
5
  * `null` for "unchanged" (remote 304; local: the version matched the caller's prior etag).
@@ -10,6 +10,13 @@ import { fetchEffective as remoteFetchEffective, fetchSkillContent as remoteFetc
10
10
  /** 壳写在 `config.d/models.json` 条目上的装饰键(`sema*`)自 settings-schema 1.5.0 起是**声明过的扩展命名空间**
11
11
  * (`EXTENSION_NAMESPACES`):schema 照旧 strip、但不再报「未知键」,所以本函数收到的每一个未知键都是拼错/越界 ⇒ 候选门错误。
12
12
  * 此前把这类键当「通知」放行的整套下游机制(域豁免/键路径闭集/满额特判/单独的通知面)已全部撤回——意图在声明处说,消费方不猜。 */
13
+ /** `readWarnings`:契约 `onWarning` 流的**原值**(未折叠),两条读腿同键同义。折叠是有损的——
14
+ * `hosts-grandfathered` / `legacy-governance-lifted` 按律折成 `null`,只看 `domainErrors` 的人永远看不见
15
+ * 兼容臂还开着,而契约铸出 `legacy-governance-lifted` 的理由正是「关窗的前提是有人知道它还开着」
16
+ * (`legacyGovernanceFromRuntime` SUNSET 注)。恒在场(空数组 = 这次读零告警),不用缺席表达「没有」
17
+ * ——缺席/空两形并存就是又一处二义。
18
+ * 🔐 原流的 `error` 未经 {@link redactConfigError}(那道脱敏挂在真正进日志流的 `domainErrors` 产出边界上),
19
+ * 路由进日志/审计流的消费者必须自己先脱敏。 */
13
20
  export type FetchEffectiveResult = {
14
21
  effective: EffectiveConfig;
15
22
  etag?: string;
@@ -17,6 +24,7 @@ export type FetchEffectiveResult = {
17
24
  domain: string;
18
25
  error: string;
19
26
  }>;
27
+ readWarnings: readonly EffectiveReadWarning[];
20
28
  } | null;
21
29
  /** Sentinel returned by {@link raceBootFetch} when the boot pull outlived its budget — the caller
22
30
  * starts serving on the env fallback and the SAME promise settles in the background (never cancelled). */
@@ -61,9 +61,11 @@ export class LocalConfigProvider {
61
61
  }
62
62
  async fetchEffective(etag) {
63
63
  const parseWarnings = [];
64
+ const readWarnings = [];
64
65
  const store = this.#injectedStore ??
65
66
  new FileConfigStore(this.root, {
66
67
  onWarning: (w) => {
68
+ readWarnings.push(w);
67
69
  const f = foldEffectiveReadWarning(w);
68
70
  if (f !== null)
69
71
  parseWarnings.push(f);
@@ -82,7 +84,7 @@ export class LocalConfigProvider {
82
84
  }
83
85
  const parseErrors = parseWarnings.map((w) => ({ domain: w.domain, error: redactConfigError(w.error) }));
84
86
  const errors = [...domainErrors.map((de) => ({ domain: de.domain, error: redactConfigError(de.error) })), ...parseErrors, ...refErrors];
85
- return { effective: mapToServiceEffective(eff, eff.version), etag: tag, ...(errors.length > 0 ? { domainErrors: errors } : {}) };
87
+ return { effective: mapToServiceEffective(eff, eff.version), etag: tag, readWarnings, ...(errors.length > 0 ? { domainErrors: errors } : {}) };
86
88
  }
87
89
  async fetchSkillContent(contentHash) {
88
90
  const skills = await this.store.getDomain("skills");
@@ -778,6 +778,13 @@ export interface ServiceConfigFlat {
778
778
  * 形会把全员折叠成单键,per-remote 层退化成共享预算而非公平层,`off` + 全局帽 64 是诚实形)。
779
779
  * 坏值拒启([ref] A 档),词表与拒因见 config.ts `parseDevicePreAuthPerRemoteEnv`。 */
780
780
  maxPreAuthPerRemote?: number | "off";
781
+ /** `DEVICE_ENROLL_TOKEN_TTL_SEC`(§4.7 第五个旋钮;默认 900 = §7-T5 ① 的 15min)= enrollment
782
+ * token 的有效期**时长**(整秒,自 **DB now** 起算 —— 时间权威是库,不是副本墙钟)。唯一消费点 =
783
+ * 签发动词 `POST /v1/devices/enroll-tokens`(`http/routes/devices.ts`);S-470 之前它刻意未铸
784
+ * (端点不存在 ⇒ 铸了就是一个「配了不生效」的死键,[ref] 病族),随该端点同批铸 + 接线。
785
+ * 域 = `[1, 86400]` 的十进制正整数,坏值**拒启**([ref] A 档;解析器 `parseDeviceEnrollTokenTtlEnv`)。
786
+ * 缺席 ⇒ 签发点的属主默认 900(不在 config 层抄第二份默认,与本族其余旋钮同姿势)。 */
787
+ enrollTokenTtlSec?: number;
781
788
  /** `DEVICE_SUPERSEDE`(默认 `stale_only`)= 同 deviceId 二连的顶替策略(§5.2)。 */
782
789
  supersede?: "stale_only" | "deny";
783
790
  } | {
package/dist/config.js CHANGED
@@ -18,6 +18,8 @@ import { THINKING_LEVELS, THINKING_FORMATS, buildModelCompat, isThinkingFormat,
18
18
  import {} from "./posture-source.js";
19
19
  import { CONTROL_AND_BIDI_STRIP_RE } from "./text-bidi.js";
20
20
  import { modelProviderRefusal } from "./model-provider.js";
21
+ import { modelRouteUrlRefusal } from "./model-route-url.js";
22
+ import { buildDisplaySafeUrl } from "./trace/redact.js";
21
23
  export { RETENTION_MODES, MEMORY_CONSOLIDATION_MIN_INTERVAL_SEC } from "./config-types.js";
22
24
  function csv(name) {
23
25
  return (process.env[name] ?? "")
@@ -333,6 +335,18 @@ function optNumEnvStrict(name) {
333
335
  return undefined;
334
336
  return parseNumOrFail(name, raw);
335
337
  }
338
+ function parseDeviceEnrollTokenTtlEnv() {
339
+ const raw = process.env.DEVICE_ENROLL_TOKEN_TTL_SEC;
340
+ if (raw === undefined || raw === "")
341
+ return undefined;
342
+ if (!/^[1-9]\d*$/.test(raw) || Number(raw) > 86_400) {
343
+ throw new Error(`env DEVICE_ENROLL_TOKEN_TTL_SEC=${JSON.stringify(raw)} must be a whole number of seconds in [1, 86400] — ` +
344
+ `a malformed TTL would otherwise run as the default (900s) while the operator believes their value is in force ` +
345
+ `(#210: deployment knobs never fall back silently). "0"/negative would issue tokens that are already expired; ` +
346
+ `an over-a-day enrollment window is a hoarding surface (device-executor-lane-v2 §7-T5 ①).`);
347
+ }
348
+ return Number(raw);
349
+ }
336
350
  function parseDevicePreAuthPerRemoteEnv() {
337
351
  const raw = process.env.DEVICE_MAX_PREAUTH_PER_REMOTE;
338
352
  if (raw === undefined || raw === "")
@@ -783,6 +797,16 @@ function parseModelDomain() {
783
797
  throw new Error(refusal);
784
798
  }
785
799
  }
800
+ for (const [envKey, raw, knob] of [
801
+ ["MODEL_GATEWAY_BASEURL", gatewayBaseUrl, "MODEL_API_KEY"],
802
+ ...csv("MODEL_GATEWAY_FALLBACK_URLS").map((u, i) => [`MODEL_GATEWAY_FALLBACK_URLS[${i}]`, u, "MODEL_API_KEY"]),
803
+ ["ANTHROPIC_BASE_URL", firstSetEnv("ANTHROPIC_BASE_URL") ?? "", "ANTHROPIC_API_KEY"],
804
+ ]) {
805
+ const refusal = modelRouteUrlRefusal(envKey, raw, knob);
806
+ if (refusal !== undefined) {
807
+ throw new Error(refusal);
808
+ }
809
+ }
786
810
  const inferredAnthropicBaseUrl = firstSetEnv("ANTHROPIC_BASE_URL");
787
811
  const inferredAnthropicCred = firstSetEnv("ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN");
788
812
  const provider = (() => {
@@ -793,7 +817,7 @@ function parseModelDomain() {
793
817
  event: "model_provider_inferred",
794
818
  fields: {
795
819
  provider: "anthropic",
796
- baseUrl: inferredAnthropicBaseUrl,
820
+ baseUrl: buildDisplaySafeUrl(inferredAnthropicBaseUrl),
797
821
  hint: "MODEL_PROVIDER is unset but an explicit ANTHROPIC base URL + credential are configured — inferred provider=anthropic (set MODEL_PROVIDER explicitly to override)",
798
822
  },
799
823
  });
@@ -1728,6 +1752,7 @@ function parseOrchestrationDomain(ctx) {
1728
1752
  ...((n) => (n !== undefined ? { execTimeoutMs: n } : {}))(optNumEnvStrict("DEVICE_EXEC_TIMEOUT_MS")),
1729
1753
  ...((n) => (n !== undefined ? { maxInflightPerDevice: n } : {}))(optNumEnvStrict("DEVICE_MAX_INFLIGHT_PER_DEVICE")),
1730
1754
  ...((n) => (n !== undefined ? { maxPreAuthPerRemote: n } : {}))(parseDevicePreAuthPerRemoteEnv()),
1755
+ ...((n) => (n !== undefined ? { enrollTokenTtlSec: n } : {}))(parseDeviceEnrollTokenTtlEnv()),
1731
1756
  ...(process.env.DEVICE_SUPERSEDE ? { supersede: enumEnv("DEVICE_SUPERSEDE", "stale_only", ["stale_only", "deny"]) } : {}),
1732
1757
  }
1733
1758
  :
@@ -1878,7 +1903,7 @@ function parseGitApiKind(raw, baseUrl) {
1878
1903
  if (!baseUrl)
1879
1904
  return kind;
1880
1905
  if (!URL.canParse(baseUrl)) {
1881
- throw new Error(`GIT_API_BASEURL="${baseUrl}" is not a valid URL (expected e.g. https://git.example.com)`);
1906
+ throw new Error(`GIT_API_BASEURL="${buildDisplaySafeUrl(baseUrl)}" is not a valid URL (expected e.g. https://git.example.com)`);
1882
1907
  }
1883
1908
  const u = new URL(baseUrl);
1884
1909
  const host = u.hostname.replace(/\.$/, "").toLowerCase();
@@ -1894,7 +1919,7 @@ function parseGitApiKind(raw, baseUrl) {
1894
1919
  throw new Error(`GIT_API_BASEURL for github.com must be exactly https://api.github.com (no path; got path "${u.pathname}"). GHE uses https://<ghe-host>/api/v3.`);
1895
1920
  }
1896
1921
  if (u.search || u.hash) {
1897
- throw new Error(`GIT_API_BASEURL must not carry a query or fragment (got "${baseUrl}")`);
1922
+ throw new Error(`GIT_API_BASEURL must not carry a query or fragment (got "${buildDisplaySafeUrl(baseUrl)}")`);
1898
1923
  }
1899
1924
  }
1900
1925
  return kind;