@sema-agent/server 7.120.0 → 7.121.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 +75 -0
  2. package/USAGE.md +67 -20
  3. package/deploy/sema-up/chart/templates/server-v4pro.yaml +2 -2
  4. package/deploy/sema-up/chart/templates/server.yaml +2 -2
  5. package/deploy/sema-up/stack/sema-stack-cluster.yaml +1 -1
  6. package/dist/boot/config-center.d.ts +5 -7
  7. package/dist/boot/config-center.js +33 -9
  8. package/dist/boot/engine-lease.d.ts +1 -1
  9. package/dist/boot/resolve-spec.js +1 -0
  10. package/dist/boot/shutdown.d.ts +9 -10
  11. package/dist/boot/shutdown.js +54 -16
  12. package/dist/boot/stage-09-leader.d.ts +2 -9
  13. package/dist/boot/stage-10-http-server.d.ts +1 -9
  14. package/dist/boot/stage-10-http-server.js +31 -8
  15. package/dist/capabilities/composite-members.d.ts +1 -1
  16. package/dist/capabilities/composite-members.js +1 -1
  17. package/dist/config-catalog.js +11 -5
  18. package/dist/config-center/center-request.d.ts +16 -1
  19. package/dist/config-center/center-request.js +36 -5
  20. package/dist/config-types.d.ts +39 -3
  21. package/dist/config-types.js +6 -0
  22. package/dist/config.d.ts +33 -4
  23. package/dist/config.js +99 -7
  24. package/dist/fleet/drain-entry.d.ts +87 -0
  25. package/dist/fleet/drain-entry.js +35 -0
  26. package/dist/fleet/engine-signal.d.ts +144 -0
  27. package/dist/fleet/engine-signal.js +60 -0
  28. package/dist/fleet/serving-lease.d.ts +47 -0
  29. package/dist/fleet/serving-lease.js +235 -0
  30. package/dist/fleet-client.d.ts +35 -19
  31. package/dist/fleet-client.js +49 -25
  32. package/dist/http/approval-sweeps.js +3 -5
  33. package/dist/http/dispatch.js +18 -13
  34. package/dist/http/health-self-report.d.ts +60 -0
  35. package/dist/http/health-self-report.js +53 -0
  36. package/dist/http/principal-gate.d.ts +8 -1
  37. package/dist/http/principal-gate.js +1 -0
  38. package/dist/http/readiness.d.ts +25 -5
  39. package/dist/http/readiness.js +31 -6
  40. package/dist/http/resume-legs.js +1 -1
  41. package/dist/http/route-ctx.d.ts +6 -4
  42. package/dist/http/route-table.d.ts +3 -0
  43. package/dist/http/route-table.js +4 -1
  44. package/dist/http/routes/a2a-serve.d.ts +1 -1
  45. package/dist/http/routes/a2a-serve.js +5 -5
  46. package/dist/http/routes/admin-drain.d.ts +12 -31
  47. package/dist/http/routes/admin-drain.js +15 -9
  48. package/dist/http/routes/approvals-assistant.d.ts +3 -3
  49. package/dist/http/routes/approvals-assistant.js +3 -3
  50. package/dist/http/routes/capabilities.js +2 -1
  51. package/dist/http/routes/devices.js +3 -2
  52. package/dist/http/routes/leader.d.ts +1 -1
  53. package/dist/http/routes/leader.js +1 -1
  54. package/dist/http/routes/memory-consolidation.d.ts +1 -1
  55. package/dist/http/routes/memory-consolidation.js +1 -1
  56. package/dist/http/routes/notify-wake.d.ts +1 -1
  57. package/dist/http/routes/notify-wake.js +1 -1
  58. package/dist/http/routes/rules.js +4 -3
  59. package/dist/http/routes/runs.d.ts +2 -2
  60. package/dist/http/routes/runs.js +2 -2
  61. package/dist/http/routes/side-query.d.ts +1 -1
  62. package/dist/http/routes/side-query.js +1 -1
  63. package/dist/http/routes/tasks.d.ts +2 -2
  64. package/dist/http/routes/tasks.js +3 -3
  65. package/dist/http/server.d.ts +7 -21
  66. package/dist/http/server.js +20 -1
  67. package/dist/http/wire-types.d.ts +7 -1
  68. package/dist/model-reach-scope.d.ts +15 -3
  69. package/dist/model-reach-scope.js +3 -2
  70. package/dist/model-unreachable-notice.d.ts +2 -1
  71. package/dist/model-unreachable-notice.js +1 -0
  72. package/dist/observability/fail-open.d.ts +16 -0
  73. package/dist/observability/fail-open.js +16 -0
  74. package/dist/plugins/one-time-migrations.js +1 -1
  75. package/dist/plugins/permission-rule-store-sql.d.ts +2 -1
  76. package/dist/plugins/remote-env-device.js +0 -1
  77. package/dist/request-key-closure.js +1 -1
  78. package/dist/rules-consent.d.ts +6 -2
  79. package/dist/rules-consent.js +3 -0
  80. package/dist/runs.js +1 -1
  81. package/dist/tool-approval.d.ts +7 -0
  82. package/dist/tool-approval.js +4 -1
  83. package/dist/trace/core-keyset-guard.d.ts +2 -2
  84. package/dist/trace/self-minted-wire.d.ts +64 -0
  85. package/dist/trace/self-minted-wire.js +30 -0
  86. package/package.json +2 -2
@@ -8,14 +8,16 @@ import { installProcessExitBoundary } from "./exit-boundary.js";
8
8
  import { createEngineLeaseTracker } from "./engine-lease.js";
9
9
  import { writeExitLast, readExitLast, clearExitLast } from "./exit-last.js";
10
10
  import { isScriptRealmRejection, describeRejectionReason } from "../orchestration/hardened-vm-runner.js";
11
+ import { DRAIN_CAUSE_COUNTS_AS_FIRST_STOP_SIGNAL, HARD_STOP_ACTION_ID } from "../fleet/drain-entry.js";
11
12
  const HARD_STOP_GRACE_MS = 10_000;
13
+ const FINAL_EXIT_MS = 15_000;
12
14
  export function backgroundGraceRemainingMs(a) {
13
15
  if (a.softStopped <= 0)
14
16
  return 0;
15
17
  return Math.max(0, Math.min(a.softStoppedAt + a.graceMs, a.deadlineMs) - a.now);
16
18
  }
17
19
  export function installShutdownHandlers(ctx) {
18
- const { config, logger, server, reaper, fleetReconcile, retentionLane, releaseRetentionLease, otelExporter, breakerState, costQuota, rateLimiter, runner, subRunner, lspManager, workflowNotifyJournal, fleetClient, backend, drainState, storeLiveProbe, configCenter, version, deviceHub, toolApproval, parkCommits, } = ctx;
20
+ const { config, logger, server, reaper, fleetReconcile, retentionLane, releaseRetentionLease, otelExporter, breakerState, costQuota, rateLimiter, runner, subRunner, lspManager, workflowNotifyJournal, fleetClient, backend, drainState, storeLiveProbe, configCenter, version, deviceHub, toolApproval, parkCommits, servingLease, } = ctx;
19
21
  const dataRoot = config.localDataRoot;
20
22
  if (dataRoot) {
21
23
  const prior = readCrashLast(dataRoot);
@@ -69,12 +71,14 @@ export function installShutdownHandlers(ctx) {
69
71
  };
70
72
  process.on("unhandledRejection", onUnhandledRejection);
71
73
  let closing = false;
74
+ let closingAt = 0;
72
75
  let parentWatch;
73
76
  let engineLease;
74
77
  const hardShutdown = () => {
75
78
  if (closing)
76
79
  return;
77
80
  closing = true;
81
+ closingAt = Date.now();
78
82
  if (dataRoot)
79
83
  clearCrashLast(dataRoot);
80
84
  const graceDeadlineMs = Date.now() + HARD_STOP_GRACE_MS;
@@ -101,6 +105,7 @@ export function installShutdownHandlers(ctx) {
101
105
  parentWatch?.stop();
102
106
  engineLease?.stop();
103
107
  storeLiveProbe?.stop();
108
+ servingLease?.stop();
104
109
  void deviceHub?.close().catch((err) => logger.warn("device_hub_close_failed", { err: String(err) }));
105
110
  configCenter?.stopRefreshLoop();
106
111
  otelExporter?.stop();
@@ -133,7 +138,7 @@ export function installShutdownHandlers(ctx) {
133
138
  .finally(() => backgroundGrace)
134
139
  .finally(() => process.exit(0));
135
140
  });
136
- const finalExit = setTimeout(() => { logger.warn("hard_shutdown_final_deadline", { afterMs: 15_000 }); process.exit(0); }, 15_000);
141
+ const finalExit = setTimeout(() => { logger.warn("hard_shutdown_final_deadline", { afterMs: FINAL_EXIT_MS }); process.exit(0); }, FINAL_EXIT_MS);
137
142
  finalExit.unref?.();
138
143
  server.closeIdleConnections?.();
139
144
  const forceClose = setTimeout(() => server.closeAllConnections?.(), HARD_STOP_GRACE_MS);
@@ -141,36 +146,67 @@ export function installShutdownHandlers(ctx) {
141
146
  };
142
147
  let draining = false;
143
148
  let drainStartedAt = 0;
149
+ let firstStopSignalAt;
144
150
  let drainTick;
145
- const drainThenShutdown = () => {
146
- if (closing || draining)
147
- return;
151
+ let current;
152
+ const enterDrain = (entry, bySignal = false, at = Date.now()) => {
153
+ if (current !== undefined)
154
+ return { ...current, state: "already_draining" };
155
+ if (closing)
156
+ return { actionId: HARD_STOP_ACTION_ID, since: closingAt, deadlineAt: closingAt + FINAL_EXIT_MS, state: "already_draining" };
148
157
  draining = true;
149
158
  drainStartedAt = performance.now();
159
+ firstStopSignalAt = bySignal ? drainStartedAt : undefined;
160
+ const since = at;
161
+ current = { actionId: entry.actionId, since, deadlineAt: entry.deadlineAt };
150
162
  drainState.draining = true;
151
- deviceHub?.beginDrain({ graceMs: () => config.drainGraceMs });
152
- drainState.since = Date.now();
163
+ drainState.since = since;
164
+ drainState.actionId = entry.actionId;
165
+ drainState.deadlineAt = entry.deadlineAt;
166
+ const windowMs = Math.max(0, entry.deadlineAt - since);
167
+ const deadlineMono = drainStartedAt + windowMs;
168
+ deviceHub?.beginDrain({ graceMs: () => windowMs });
169
+ drainState.yieldInflight?.();
170
+ toolApproval?.beginDrain();
153
171
  void fleetClient?.announceNow();
154
172
  const inflight = drainState.inflight?.() ?? 0;
155
- logger.info("draining_started", { inflight, graceMs: config.drainGraceMs });
173
+ logger.info("draining_started", { actionId: entry.actionId, inflight, deadlineAt: entry.deadlineAt, windowMs });
156
174
  if (inflight === 0) {
157
175
  hardShutdown();
158
- return;
176
+ return { ...current, state: "draining" };
159
177
  }
160
178
  const tick = setInterval(() => {
161
179
  const n = drainState.inflight?.() ?? 0;
162
- const elapsed = Math.round(performance.now() - drainStartedAt);
163
- if (n === 0 || elapsed >= config.drainGraceMs) {
180
+ const now = performance.now();
181
+ if (n === 0 || now >= deadlineMono) {
164
182
  clearInterval(tick);
165
183
  drainTick = undefined;
166
- logger.info("drain_complete", { inflight: n, elapsedMs: elapsed, timedOut: n > 0 });
184
+ logger.info("drain_complete", { actionId: entry.actionId, inflight: n, elapsedMs: Math.round(now - drainStartedAt), timedOut: n > 0 });
167
185
  hardShutdown();
168
186
  }
169
187
  }, 1_000);
170
188
  drainTick = tick;
189
+ return { ...current, state: "draining" };
190
+ };
191
+ const drainOnCause = (cause) => {
192
+ const at = Date.now();
193
+ enterDrain({ actionId: cause, deadlineAt: at + config.drainGraceMs }, DRAIN_CAUSE_COUNTS_AS_FIRST_STOP_SIGNAL[cause], at);
171
194
  };
195
+ const enterByRequest = (entry) => enterDrain(entry);
196
+ drainState.enter = enterByRequest;
172
197
  const onStopSignalWhileDraining = (signal) => {
173
- const sinceFirst = performance.now() - drainStartedAt;
198
+ const now = performance.now();
199
+ if (firstStopSignalAt === undefined) {
200
+ firstStopSignalAt = now;
201
+ logger.info("drain_signal_after_request_drain_ignored", {
202
+ signal,
203
+ actionId: current?.actionId,
204
+ deadlineAt: current?.deadlineAt,
205
+ note: "this drain was entered by a drain request / lease denial / self-exit and its deadline is fixed — the first stop signal does not shorten it; a second stop signal at least DRAIN_SECOND_SIGNAL_MIN_GAP_MS later hard-stops",
206
+ });
207
+ return;
208
+ }
209
+ const sinceFirst = now - firstStopSignalAt;
174
210
  const minGapMs = config.drainSecondSignalMinGapMs;
175
211
  if (sinceFirst < minGapMs) {
176
212
  logger.info("drain_duplicate_signal_ignored", { signal, sinceFirstMs: Math.floor(sinceFirst), minGapMs });
@@ -180,7 +216,7 @@ export function installShutdownHandlers(ctx) {
180
216
  hardShutdown();
181
217
  };
182
218
  const firstStopSignal = {
183
- SIGTERM: drainThenShutdown,
219
+ SIGTERM: () => drainOnCause("signal:SIGTERM"),
184
220
  SIGINT: hardShutdown,
185
221
  };
186
222
  const onStopSignal = (signal) => {
@@ -200,7 +236,7 @@ export function installShutdownHandlers(ctx) {
200
236
  inflight: () => drainState.inflight?.() ?? 0,
201
237
  lastActivityAt: () => drainState.lastActivityAt?.() ?? 0,
202
238
  isStopped: () => closing || draining,
203
- escalate: drainThenShutdown,
239
+ escalate: () => drainOnCause("signal:SIGHUP"),
204
240
  shutdown: hardShutdown,
205
241
  idleGraceMs: config.sighupIdleGraceMs,
206
242
  log: (event, fields) => logger.info(event, fields),
@@ -231,7 +267,7 @@ export function installShutdownHandlers(ctx) {
231
267
  selfExit: (report) => {
232
268
  if (dataRoot)
233
269
  writeExitLast(dataRoot, { version, ...report });
234
- drainThenShutdown();
270
+ drainOnCause("self-exit:attachment");
235
271
  },
236
272
  lingerMs: config.engineAutoExit ? config.engineLingerMs : Number.POSITIVE_INFINITY,
237
273
  ...(config.parentPid !== undefined
@@ -259,6 +295,8 @@ export function installShutdownHandlers(ctx) {
259
295
  }
260
296
  parentWatch?.stop();
261
297
  engineLease?.stop();
298
+ if (drainState.enter === enterByRequest)
299
+ drainState.enter = undefined;
262
300
  },
263
301
  };
264
302
  }
@@ -1,14 +1,7 @@
1
1
  import type { runStage08 } from "./stage-08-reapers.js";
2
+ import type { DrainState } from "../fleet/drain-entry.js";
2
3
  export declare function runStage09(ctx: Awaited<ReturnType<typeof runStage08>>): {
3
- drainState: {
4
- draining: boolean;
5
- since?: number;
6
- reason?: string;
7
- inflight?: () => number;
8
- lastActivityAt?: () => number;
9
- attachedStreams?: () => number;
10
- lastAttachEndedAt?: () => number;
11
- };
4
+ drainState: DrainState;
12
5
  hookWakeBus: {
13
6
  deliver?: (sessionId: string, text: string) => Promise<boolean>;
14
7
  };
@@ -1,14 +1,6 @@
1
1
  import type { runStage09 } from "./stage-09-leader.js";
2
2
  export declare function runStage10(ctx: Awaited<ReturnType<typeof runStage09>>): Promise<{
3
- drainState: {
4
- draining: boolean;
5
- since?: number;
6
- reason?: string;
7
- inflight?: () => number;
8
- lastActivityAt?: () => number;
9
- attachedStreams?: () => number;
10
- lastAttachEndedAt?: () => number;
11
- };
3
+ drainState: import("../fleet/drain-entry.js").DrainState;
12
4
  hookWakeBus: {
13
5
  deliver?: (sessionId: string, text: string) => Promise<boolean>;
14
6
  };
@@ -13,9 +13,11 @@ import { SQL_BLOB_DEFAULT_MAX_BYTES } from "../plugins/blob-backend.js";
13
13
  import { createStoreLiveProbe } from "../store-live-probe.js";
14
14
  import { counterStoreLabel } from "../plugins/store-backend.js";
15
15
  import { sessionStoreLabelOf } from "../store-posture.js";
16
- import { startFleetClientFromEnv } from "../fleet-client.js";
16
+ import { startFleetClientFromConfig } from "../fleet-client.js";
17
17
  import { createCenterWiringReader } from "../capabilities/center-wiring.js";
18
- import { readinessOf } from "../http/readiness.js";
18
+ import { mintEngineSignal } from "../fleet/engine-signal.js";
19
+ import { createServingLeaseClient } from "../fleet/serving-lease.js";
20
+ import { fleetFenceLaneOf } from "../config.js";
19
21
  import { createResolveSpec } from "./resolve-spec.js";
20
22
  import { executionLaneOf } from "../execution-lane-caps.js";
21
23
  import { startFleetReconciler } from "../fleet/fleet-reconciler.js";
@@ -133,6 +135,24 @@ export async function runStage10(ctx) {
133
135
  const storeLiveProbe = backend && backend.kind !== "local" && backend.dbNowMs && config.storeProbeIntervalMs > 0
134
136
  ? createStoreLiveProbe({ probe: () => backend.dbNowMs(), intervalMs: config.storeProbeIntervalMs, logger })
135
137
  : undefined;
138
+ const fenceLane = fleetFenceLaneOf(config);
139
+ const servingLease = fenceLane !== undefined
140
+ ? createServingLeaseClient({
141
+ center: fenceLane.center,
142
+ generation: fenceLane.generation,
143
+ instanceId,
144
+ dataRoot: config.localDataRoot,
145
+ outageMaxMs: config.fleetLeaseOutageMaxMs,
146
+ drain: (entry) => {
147
+ const enter = drainState.enter;
148
+ if (enter === undefined)
149
+ throw new Error("serving lease denied before the drain entry was installed — refusing to keep serving without a drain");
150
+ enter(entry);
151
+ },
152
+ drainWindowMs: () => config.drainGraceMs,
153
+ logger: { info: (m, d) => logger.info(m, d ?? {}), warn: (m, d) => logger.warn(m, d ?? {}), error: (m, d) => logger.error(m, d ?? {}) },
154
+ })
155
+ : undefined;
136
156
  const deployment = {
137
157
  sessionStoreLabel: sessionStoreLabelOf(config.sessionBackend, backend?.kind),
138
158
  modelReady: () => configCenter.modelReady(),
@@ -154,15 +174,16 @@ export async function runStage10(ctx) {
154
174
  },
155
175
  },
156
176
  restartState: () => configCenter.restartState(),
157
- planeDeferredState: () => configCenter.planeDeferredState(),
158
177
  configApplyState: () => configCenter.configApplyState(),
159
178
  centerCredentialState: () => configCenter.centerCredentialState(),
160
179
  modelRefusalsState: () => configCenter.modelRefusalsState(),
180
+ configHandoffState: () => configCenter.configHandoffState(),
161
181
  mcpRevocations,
162
182
  mcpRevocationState: () => configCenter.mcpRevocationState(),
163
183
  configRefreshNow: () => configCenter.refreshNow(),
164
184
  configCenterManagedKeys: () => configCenter.centerManagedKeys(),
165
185
  drainState,
186
+ servingLease: servingLease ? () => servingLease.view() : undefined,
166
187
  storePosture,
167
188
  centerWiring: createCenterWiringReader({ config, pullState: () => configCenter.centerPullState(), fleetLease, usageReport: fleetUsage !== undefined }),
168
189
  storeLiveState: storeLiveProbe ? () => storeLiveProbe.state() : undefined,
@@ -244,19 +265,19 @@ export async function runStage10(ctx) {
244
265
  ...("code" in e ? { code: String(e.code) } : {}),
245
266
  });
246
267
  });
247
- const fleetClient = startFleetClientFromEnv(config, {
268
+ const fleetClient = startFleetClientFromConfig(config, {
248
269
  instanceId,
249
270
  version: serviceVersion(),
250
- readiness: () => readinessOf(deployment),
251
- restart: () => deployment.restartState?.(),
271
+ signal: () => mintEngineSignal({ ...deployment, config }),
252
272
  modelRefusals: () => deployment.modelRefusalsState?.(),
273
+ ...(servingLease ? { onCenterCapabilities: (caps) => servingLease.noteCenterCapabilities(caps) } : {}),
253
274
  ...(fleetUsage ? { usage: fleetUsage } : {}),
254
275
  logger: { info: (m, d) => logger.info(m, d ?? {}), warn: (m, d) => logger.warn(m, d ?? {}) },
255
276
  });
256
277
  configCenter.onSelfReportChange(() => void fleetClient?.announceNow());
257
278
  if (fleetClient) {
258
- const advertised = process.env.FLEET_ADVERTISE_ADDRESS ?? "";
259
- logger.info("fleet_client_started", { worker: config.configCenter?.worker, usageReport: fleetUsage !== undefined, announce: advertised !== "", ...(advertised !== "" ? { address: buildDisplaySafeUrl(advertised) } : {}) });
279
+ const advertised = config.fleetAdvertiseAddress;
280
+ logger.info("fleet_client_started", { worker: config.configCenter?.worker, usageReport: fleetUsage !== undefined, announce: advertised !== undefined, ...(advertised !== undefined ? { address: buildDisplaySafeUrl(advertised) } : {}) });
260
281
  }
261
282
  const failoverFirstByte = buildFailoverFirstByteNotice(config);
262
283
  if (failoverFirstByte)
@@ -310,7 +331,9 @@ export async function runStage10(ctx) {
310
331
  deviceHub: deviceLane?.hub,
311
332
  retentionLane: retentionLaneStarted,
312
333
  releaseRetentionLease: retentionLaneStore !== undefined ? () => retentionLaneStore.release(instanceId) : undefined,
334
+ servingLease,
313
335
  });
336
+ servingLease?.resumeStickyDenial();
314
337
  return { ...ctx };
315
338
  }
316
339
  //# sourceMappingURL=stage-10-http-server.js.map
@@ -39,7 +39,7 @@ import type { NamedKeysOf } from "../request-key-closure.js";
39
39
  */
40
40
  export declare const COMPOSITE_CAPABILITY_MEMBERS: {
41
41
  readonly deviceExecutor: readonly ["enabled", "protocolVersion", "maxInflightPerDevice", "wsPath", "management"];
42
- readonly fleet: readonly ["stream", "sessionScope", "observe", "resume", "bgNotifyFailClosed", "steerPriority", "hookFailureNotice"];
42
+ readonly fleet: readonly ["stream", "sessionScope", "observe", "resume", "bgNotifyFailClosed", "steerPriority", "hookFailureNotice", "leaseClient", "generationEcho"];
43
43
  readonly memoryEngine: readonly ["backend", "vectorMode"];
44
44
  readonly store: readonly ["declared", "backend", "durable", "degradedReason", "sql"];
45
45
  readonly centerWiring: readonly ["effectivePull", "effectiveRevision", "lease", "leaseHealth", "failMode", "usageReport", "issuer", "caller"];
@@ -1,6 +1,6 @@
1
1
  export const COMPOSITE_CAPABILITY_MEMBERS = {
2
2
  deviceExecutor: ["enabled", "protocolVersion", "maxInflightPerDevice", "wsPath", "management"],
3
- fleet: ["stream", "sessionScope", "observe", "resume", "bgNotifyFailClosed", "steerPriority", "hookFailureNotice"],
3
+ fleet: ["stream", "sessionScope", "observe", "resume", "bgNotifyFailClosed", "steerPriority", "hookFailureNotice", "leaseClient", "generationEcho"],
4
4
  memoryEngine: ["backend", "vectorMode"],
5
5
  store: ["declared", "backend", "durable", "degradedReason", "sql"],
6
6
  centerWiring: ["effectivePull", "effectiveRevision", "lease", "leaseHealth", "failMode", "usageReport", "issuer", "caller"],
@@ -1,8 +1,9 @@
1
1
  import { CROSS_SESSION_INBOUND_SETTINGS, CROSS_SESSION_DIALOG_EXPIRY_VALUES } from "@sema-agent/core";
2
+ import { GLOBAL_SCOPE } from "@sema-agent/settings-schema/api/scopes";
2
3
  import { CENTER_CREDENTIAL_ENV, DOMAIN_SCHEMAS } from "@sema-agent/settings-schema";
3
4
  import { CENTER_CREDENTIAL_RENEW_LEAD_DEFAULT_MS, CENTER_CREDENTIAL_RENEW_LEAD_MAX_MS, CENTER_CREDENTIAL_RENEW_LEAD_MIN_MS } from "./center-credential-renewer.js";
4
5
  import { THINKING_FORMATS } from "./model-compat.js";
5
- import { AUTO_MODE_CLASSIFIER_TIMEOUT_BOUNDS, CONFIG_PROVIDER_WORDS, DRAIN_SECOND_SIGNAL_MIN_GAP_BOUNDS, DRAIN_SECOND_SIGNAL_MIN_GAP_DEFAULT_MS, FIRST_BYTE_TIMEOUT_BOUNDS, RESILIENCE_DEFAULTS, TASK_WRITE_FACE_ESCALATIONS } from "./config-types.js";
6
+ import { AUTO_MODE_CLASSIFIER_TIMEOUT_BOUNDS, CENTER_BASE_URL_RULE, CONFIG_PROVIDER_WORDS, DRAIN_SECOND_SIGNAL_MIN_GAP_BOUNDS, DRAIN_SECOND_SIGNAL_MIN_GAP_DEFAULT_MS, FIRST_BYTE_TIMEOUT_BOUNDS, FLEET_LEASE_OUTAGE_MAX_DEFAULT_MS, RESILIENCE_DEFAULTS, TASK_WRITE_FACE_ESCALATIONS } from "./config-types.js";
6
7
  import { webSearchProviderFromEnv, WEB_SEARCH_PROVIDERS } from "./plugins/web-search.js";
7
8
  import { AUTONOMY_MODES } from "./runtime-governance.js";
8
9
  import { REMOTE_EXEC_PROVIDER_WORDS } from "./execution-lane-caps.js";
@@ -93,7 +94,7 @@ export const CONFIG_CATALOG = [
93
94
  r("CONFIG_BOOT_FETCH_BUDGET_MS", "integrations", "number", "boot 首拉配置的抢答窗(ms;过窗以 env/兜底起服,拉取转后台热应用)", { staticDefault: "1500", configKey: "configBootFetchBudgetMs" }),
94
95
  r("CONFIG_CATALOG_ENABLED", "auth", "boolean", "本目录端点 GET /v1/config/catalog 的旋钮(operator-only 面;合规部署可关 ⇒ 路径 404 + 能力位翻假)", { staticDefault: "true", danger: SEC, configKey: "configCatalogEnabled" }),
95
96
  r("CONFIG_LKG_DURABLE", "integrations", "boolean", "LKG 跨重启 handoff(严格 'true' 才开:重启先用 LKG 起服,tier 候选借此落地)", { staticDefault: "false" }),
96
- r("CONFIG_LKG_ENABLED", "integrations", "boolean", "last-known-good 配置盘缓存读写开关(仅远程 provider 有意义)", { staticDefault: "true" }),
97
+ r("CONFIG_LKG_ENABLED", "integrations", "boolean", "last-known-good 配置盘缓存读写开关(仅远程 provider 有意义)", { staticDefault: "true", configKey: "configLkgEnabled" }),
97
98
  r("CONFIG_LKG_PATH", "integrations", "path", "LKG 落盘路径(设了 ⇒ 派生读拒行:这条路径与它的真实落点各一行,按路径段串匹配 —— 任意深度出现即拒、大小写不敏感,用户仓里同段串的路径一并读不到;空串拒启)", { derivedDefaultNote: "缺席 ⇒ 配置根(SEMA_CONFIG_DIR,缺省 ~/.sema)下按 worker + 凭证身份分箱派生", configKey: "configLkgPath" }),
98
99
  r("CONFIG_LOCAL_DIR", "integrations", "path", "本地配置源根(config.d/<domain>.json);兼作数据根回落链一环", { configKey: "configLocalDir", danger: DATA }),
99
100
  r("CONFIG_PROVIDER", "integrations", "enum", "配置源双模选择(闭集,逐字比较;认不出的词 ⇒ 拒启,S-841)", { enumValues: CONFIG_PROVIDER_WORDS, derivedDefaultNote: "缺席:SEMA_REGISTRY_URL 在场 ⇒ remote,否则纯 env(无管道)", configKey: "configProvider" }),
@@ -127,7 +128,7 @@ export const CONFIG_CATALOG = [
127
128
  r("DOCKER_PIDS_LIMIT", "orchestration", "number", "local-docker fork-bomb 背栓(--pids-limit;非法值回 adapter 默认 512+warn)", { danger: SEC, derivedDefaultNote: "缺席 ⇒ adapter 默认 512" }),
128
129
  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 }),
129
130
  r("DRAIN_GRACE_MS", "limitsHttp", "number", "SIGTERM 优雅排空窗(ms;期间新计费提交 503+Retry-After)。出厂默认是**服务器部署**的取值;壳自 spawn 的引擎会被壳注入更短的窗 ⇒ 排壳形关机预算时以本行的 effectiveValue(= 本进程实际生效值)为准,不照抄 staticDefault", { staticDefault: String(10 * 60 * 1000), configKey: "drainGraceMs", center: cLimits("drainGraceMs") }),
130
- 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
+ r("DRAIN_SECOND_SIGNAL_MIN_GAP_MS", "limitsHttp", "number", `排空期「第二发信号 = 硬停」的最小间隔(ms;锚 = 本次排空的第一发停止信号 —— SIGTERM / SIGHUP 升级进的排空,入口那一发就是它;排空口 / 租约被拒 / 附着租约自退进的排空,到达的第一发被吸收、不缩短期限。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
132
  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
133
  r("E2B_ALLOW_NET", "orchestration", "boolean", "e2b 沙箱出网许可", { danger: SEC, derivedDefaultNote: "缺席 ⇒ 不铸键(adapter 默认允许)" }),
133
134
  r("E2B_API_KEY", "orchestration", "secret", "E2B 车道 API 凭据(REMOTE_EXEC=e2b 必需件)", { danger: [...SEC, ...BILL] }),
@@ -147,8 +148,11 @@ export const CONFIG_CATALOG = [
147
148
  r("EXPERIMENTAL_OBSERVER_AGENTS", "orchestration", "boolean", "observer 开闸线(实验面 ship-dark;多租户只认 center caps,env 被忽略)", { staticDefault: "false", configKey: "experimentalObserverAgents" }),
148
149
  r("FILE_HISTORY_CAPTURE", "store", "boolean", "文件历史**捕获**总开关(三条车道同义)。关 = 不再捕获任何改动文件的首次快照(落 core 的部署捕获位 `RunnerDeps.fileHistoryCapture: \"off\"`,能力词 `fileHistoryCapture` 翻 off);S-724(7.104.0)起**只关捕获**:历史店照接,已存历史照常 restoreFiles / rewindFilesTo / 2c session-sync。关掉 ≠ 删掉:已捕获的历史不被批量删除,仍按会话删除与留存规则清理 —— 要清存量走 DELETE 会话或留存旋钮;没有「整库历史不可用」的旋钮", { staticDefault: "true", danger: DATA, configKey: "fileHistoryCapture" }),
149
150
  r("FILE_HISTORY_RETENTION_KEEP", "store", "number", "per-file rewind 历史的保留上限(与 core FileStorageBackend 同域键形,三条车道一套语义)", { danger: DATA, derivedDefaultNote: "缺席 ⇒ core 缺省 100" }),
150
- r("FLEET_ADVERTISE_ADDRESS", "integrations", "url", "fleet 面向 center 公告的本 worker 可达地址(中心车道配齐 + 本键 ⇒ 公告环;坏形拒启。用量环不看本键,S-866 ④)"),
151
+ r("FLEET_ADVERTISE_ADDRESS", "integrations", "url", "fleet 面向 center 公告的本 worker 可达地址(中心车道配齐 + 本键 ⇒ 公告环;无条件解析,非 http(s) 拒启,空串 = 未设。用量环不看本键,S-866 ④)", { configKey: "fleetAdvertiseAddress" }),
152
+ r("FLEET_GENERATION", "integrations", "string", "fleet 成员的代际(配置渲染器写;引擎回声到心跳与 /health 的 generation)。缺席 ⇔ 非 fleet 成员(不续服务租约、不受围栏);在场必须是规范十进制串(\"0\" / \"7\";空串、前导零、符号、空白拒启)", { danger: SEC, configKey: "fleetGeneration" }),
151
153
  r("FLEET_LEASE_FAIL_CLOSED", "integrations", "boolean", "租约连接级失败(中心鉴权拒 / 凭证不可读 / 不可达 / 5xx / 超时 / 坏体)时计费入口拒 503 fleet.lease_unavailable(true)还是放行(false);409 not-lease 与耗尽 429 不受它管;词表外拒启", { danger: SEC, derivedDefaultNote: "缺席 ⇒ 按部署姿态派生:REQUIRE_PRINCIPAL=true ⇒ true(多租:额度是安全轴);单用户 ⇒ false(AP 姿态)。显式值压过派生。生效值见 caps centerWiring.failMode", configKey: "fleetLeaseFailClosed" }),
154
+ r("FLEET_LEASE_OUTAGE_MAX_MS", "integrations", "number", "服务租约中心不可达的容忍上限 T_max(ms):租约过期满这么久 ⇒ 新计费提交 503 state.lease_expired(取消 / HITL 应答 / 续跑 / 读面照放行,在飞不动,/readyz 不翻);只在 fleet 成员 ∧ 中心宣告 servingLease 能力位时生效;< 60000(租约 TTL 地板)或非整数拒启", { danger: SEC, staticDefault: String(FLEET_LEASE_OUTAGE_MAX_DEFAULT_MS), configKey: "fleetLeaseOutageMaxMs" }),
155
+ r("FLEET_SCOPE", "integrations", "string", "本引擎所在的租户 scope(配置渲染器为多租引擎写;唯一读者 = 排空口 POST /v1/admin/drain 的身份比对,请求不带 scope 也读成缺省 scope)。在场必须是 scope id 形(小写字母 / 数字 / 内部连字符,1–64;空串拒启);与 FLEET_GENERATION 不联动(成员缺席 ⇒ 缺省 scope)。续租体与心跳不带它(scope 由中心凭证绑定)", { danger: SEC, staticDefault: GLOBAL_SCOPE, configKey: "fleetScope" }),
152
156
  r("FORK_ENABLED", "orchestration", "boolean", "CC /fork(core Fork 工具)装配开关", { derivedDefaultNote: "posture 三态:缺席 = 单用户 ⇒ on / 多租户 ⇒ off(per-principal entitlement 另判)", configKey: "forkEnabled" }),
153
157
  r("GATEWAY_MAX_RETRIES", "modelPlane", "number", "openai 兼容腿每次调用重试上限", { derivedDefaultNote: "缺席 ⇒ 不传引擎,引擎默认当家" }),
154
158
  r("GIT_API_BASEURL", "integrations", "url", "repo 工具的 Git host API base", { configKey: "gitApiBaseUrl" }),
@@ -399,9 +403,11 @@ export const CONFIG_CATALOG = [
399
403
  r("SEMA_PIP_INDEX", "orchestration", "url", "SANDBOX_PKG_SOURCE=custom 的显式 pip index 地址"),
400
404
  r("SEMA_REGISTRY_ADMIN_TOKEN", "integrations", "secret", "写 center 的管理凭证(仅 boot 上报执行公钥用;刻意不入 ServiceConfig —— 不进 configHash/日志面)", { danger: SEC, scope: "bootstrap-only" }),
401
405
  r("SEMA_REGISTRY_DRY_RUN", "integrations", "boolean", "center 腿 dry-run(拉而不应用)", { staticDefault: "false" }),
406
+ r("SEMA_REGISTRY_LIVE_URL", "integrations", "url", `fleet 活性组(心跳 / 注销、服务租约 = settings-schema FLEET_LIVE_PATHS)的中心基址:给管理面在就绪门后、活性面另走一条不看就绪的入口的部署。缺席 = SEMA_REGISTRY_URL。${CENTER_BASE_URL_RULE}`),
402
407
  r(CENTER_CREDENTIAL_ENV.token, "integrations", "secret", `center 拉取凭据(值形;与 ${CENTER_CREDENTIAL_ENV.tokenFile} 二选一,都设=拒启)`, { danger: SEC }),
403
408
  r(CENTER_CREDENTIAL_ENV.tokenFile, "integrations", "path", `center 拉取凭据文件(每条中心请求都重读 ⇒ 轮换 / 换账号不重启;与 ${CENTER_CREDENTIAL_ENV.token} 二选一,都设=拒启;内容两种类:裸行 = 静态 bearer / JSON 信封 user-session = 人的登录会话(可续签,S-698);文件不可读 / 为空 = 响亮 + 健康态 unreadable,不当作无凭证;内容读不成 / 多租户读到信封 = 拒启)`, { danger: SEC }),
404
- r("SEMA_REGISTRY_URL", "integrations", "url", "sema-registry 地址(设了=remote 配置源开关)"),
409
+ r("SEMA_REGISTRY_URL", "integrations", "url", `sema-registry 地址(设了=remote 配置源开关)。${CENTER_BASE_URL_RULE}`),
410
+ r("SEMA_REGISTRY_WEB_URL", "integrations", "url", `中心 web 面的固定基址:引擎原样转述到 /health 的 center.webBaseUrl,壳按它拼会话深链(路径归壳)。缺席即缺席(引擎不拼不猜);只在接了中心(SEMA_REGISTRY_URL)时挂。${CENTER_BASE_URL_RULE}`),
405
411
  r("SEMA_REGISTRY_WORKER", "integrations", "string", "本 worker 在 center 的名字(per-worker effective)"),
406
412
  r("SEMA_UV_INDEX", "orchestration", "url", "SANDBOX_PKG_SOURCE=custom 的显式 uv index 地址"),
407
413
  r("SEMA_UV_PY_MIRROR", "orchestration", "url", "SANDBOX_PKG_SOURCE=custom 的显式 uv python 分发镜像地址"),
@@ -8,13 +8,15 @@ export declare const SETTINGS_SCHEMA_MAJOR: string;
8
8
  /** 一个中心端点 = 基址 + 凭证来源(`ServiceConfig.configCenter` 结构上满足)。 */
9
9
  export interface CenterEndpoint {
10
10
  readonly baseUrl: string;
11
+ /** 活性组的基址(env `SEMA_REGISTRY_LIVE_URL` 的回声;缺席 ⇒ 活性组也打 {@link baseUrl})。「缺席 = 主基址」这条规则只在 {@link centerFetch} 读。 */
12
+ readonly liveBaseUrl?: string | undefined;
11
13
  readonly credential: CenterCredentialSource;
12
14
  }
13
15
  /**
14
16
  * S-866 ④ —— 「中心车道配齐」的**唯一**谓词:基址 ∧ **声明了**凭证(S-651:按声明,不按此刻读不读得出)∧ worker。
15
17
  *
16
18
  * 三处同调、不许各抄一份门:租约管理器(`fleet-lease.ts` `createFleetLeaseFromEnv`)、fleet 客户端的用量环
17
- * (`fleet-client.ts` `startFleetClientFromEnv`)、用量累加器(`boot/budget-tracing.ts`)。公告环另要一件可达性声明
19
+ * (`fleet-client.ts` `startFleetClientFromConfig`)、用量累加器(`boot/budget-tracing.ts`)。公告环另要一件可达性声明
18
20
  * `FLEET_ADVERTISE_ADDRESS` —— 那是公告环自己的第四件,不在本谓词里:用量是出站上报,与「中心能不能打到我」无关
19
21
  * (7.105.0 及以前累加器与用量环都挂在公告地址上,不公告的引擎于是从不报用量,租约的已耗量只剩中心自己的估计)。
20
22
  */
@@ -47,6 +49,8 @@ export type CenterRoute = {
47
49
  readonly kind: "fleet-instances";
48
50
  } | {
49
51
  readonly kind: "fleet-usage";
52
+ } | {
53
+ readonly kind: "fleet-serving-lease";
50
54
  } | {
51
55
  readonly kind: "execution-keys";
52
56
  };
@@ -68,6 +72,17 @@ export interface CenterRequestInit {
68
72
  readonly timeoutMs: number;
69
73
  readonly fetchImpl?: typeof fetch;
70
74
  }
75
+ /**
76
+ * 「后面要拼路径的基址」的**唯一**判官 —— 中心基址三席(`SEMA_REGISTRY_URL` / `SEMA_REGISTRY_LIVE_URL` / `SEMA_REGISTRY_WEB_URL`)与
77
+ * 本实例的公告地址(`FLEET_ADVERTISE_ADDRESS`,中心往它后面拼 `/v1/...`)同读:可解析的 URL,scheme 是 http / https(Node 的 fetch 认
78
+ * `file://` / `data:`,配错的地址不许变成读本地文件),**不带** userinfo / query / fragment(凭据不进一个会被记日志、转述、拼路径的值;
79
+ * query / fragment 挂在基址上,一拼路径就落到错的位置:`http://x?a=1` + `/api/…` = `http://x?a=1/api/…`)。起服时 `config.ts` 判(拒启),
80
+ * 请求期 {@link centerFetch} 对同一份基址再判一次(自拼 `ServiceConfig` 的嵌入方也吃同一条规则)。合格 ⇒ `undefined`;否则交回**点名规则**
81
+ * 的短语(只回显 scheme,不回显值)。
82
+ */
83
+ /** 判官「scheme 不是 http / https」那条规则的不变句 —— 拒句 = `<席> ${PATH_BASE_SCHEME_RULE} (got "<scheme>")`(只回显 scheme)。钉读这一只,不手抄。 */
84
+ export declare const PATH_BASE_SCHEME_RULE = "scheme must be http or https";
85
+ export declare function pathBaseUrlFlaw(raw: string): string | undefined;
71
86
  /**
72
87
  * S-698 —— 凭证续签:`POST {issuer}/api/v1/auth/token/refresh`,form `refresh_token=…`(settings-schema `AUTH_V1_PATHS.tokenRefresh`)。
73
88
  * · 以 `issuer` 为根(可带路径前缀;同源与否由续签器先判,本函数只装配);query / fragment 不带过去。
@@ -1,5 +1,6 @@
1
1
  import settingsSchemaPackage from "@sema-agent/settings-schema/package.json" with { type: "json" };
2
2
  import { AUTH_V1_PATHS } from "@sema-agent/settings-schema/api/auth";
3
+ import { FLEET_LIVE_PATHS, FLEET_PATHS } from "@sema-agent/settings-schema/fleet";
3
4
  import { CenterCredentialUnavailableError } from "../center-credential.js";
4
5
  export const SCHEMA_MAJOR_HEADER = "x-sema-schema-major";
5
6
  export const CENTER_PULL_TIMEOUT_MS = 8000;
@@ -43,15 +44,41 @@ function routePath(route, kind) {
43
44
  case "prompt-blob":
44
45
  return `${contentRoot(kind)}/prompts/blobs/${encodeURIComponent(route.digest)}`;
45
46
  case "fleet-leases":
46
- return "/api/fleet/leases";
47
+ return FLEET_PATHS.quotaLeases;
47
48
  case "fleet-instances":
48
- return "/api/fleet/instances";
49
+ return FLEET_PATHS.instances;
49
50
  case "fleet-usage":
50
- return "/api/fleet/usage";
51
+ return FLEET_PATHS.usage;
52
+ case "fleet-serving-lease":
53
+ return FLEET_PATHS.servingLease;
51
54
  case "execution-keys":
52
55
  return "/api/config/execution-keys";
53
56
  }
54
57
  }
58
+ export const PATH_BASE_SCHEME_RULE = "scheme must be http or https";
59
+ export function pathBaseUrlFlaw(raw) {
60
+ let parsed;
61
+ try {
62
+ parsed = new URL(raw);
63
+ }
64
+ catch {
65
+ parsed = undefined;
66
+ }
67
+ if (parsed === undefined)
68
+ return "is not a parseable URL (no scheme? it must look like http(s)://host[:port][/path])";
69
+ const broken = [
70
+ ...(parsed.protocol !== "http:" && parsed.protocol !== "https:" ? [`${PATH_BASE_SCHEME_RULE} (got "${parsed.protocol.replace(/:$/, "")}")`] : []),
71
+ ...(parsed.username !== "" || parsed.password !== "" ? ["must not carry userinfo (credentials never ride in a base URL: it is logged, reported and has paths appended)"] : []),
72
+ ...(parsed.search !== "" || raw.includes("?") ? ["must not carry a query (request paths are appended to this base)"] : []),
73
+ ...(parsed.hash !== "" || raw.includes("#") ? ["must not carry a fragment (request paths are appended to this base)"] : []),
74
+ ];
75
+ return broken.length === 0 ? undefined : broken.join("; ");
76
+ }
77
+ function assertPathBaseUrl(raw, what) {
78
+ const flaw = pathBaseUrlFlaw(raw);
79
+ if (flaw !== undefined)
80
+ throw new Error(`${what} ${flaw}`);
81
+ }
55
82
  function assertHttpScheme(url, what) {
56
83
  const scheme = url.protocol;
57
84
  if (scheme !== "http:" && scheme !== "https:")
@@ -72,7 +99,7 @@ export async function centerTokenRefresh(issuer, refreshToken, init) {
72
99
  });
73
100
  }
74
101
  export async function centerFetch(center, route, init) {
75
- assertHttpScheme(new URL(center.baseUrl), "SEMA_REGISTRY_URL");
102
+ assertPathBaseUrl(center.baseUrl, "SEMA_REGISTRY_URL");
76
103
  const cred = center.credential.read();
77
104
  if (!cred.ok)
78
105
  throw new CenterCredentialUnavailableError(cred.reason);
@@ -83,7 +110,11 @@ export async function centerFetch(center, route, init) {
83
110
  headers["content-type"] = "application/json";
84
111
  if (init.etag)
85
112
  headers["if-none-match"] = init.etag;
86
- const url = `${center.baseUrl.replace(/\/+$/, "")}${routePath(route, cred.kind)}`;
113
+ const path = routePath(route, cred.kind);
114
+ const liveBase = FLEET_LIVE_PATHS.includes(path) ? center.liveBaseUrl : undefined;
115
+ if (liveBase !== undefined)
116
+ assertPathBaseUrl(liveBase, "SEMA_REGISTRY_LIVE_URL");
117
+ const url = `${(liveBase ?? center.baseUrl).replace(/\/+$/, "")}${path}`;
87
118
  const res = await (init.fetchImpl ?? fetch)(url, {
88
119
  method: init.method ?? "GET",
89
120
  headers,
@@ -121,6 +121,16 @@ export declare const DRAIN_SECOND_SIGNAL_MIN_GAP_BOUNDS: {
121
121
  readonly maxMs: 10000;
122
122
  };
123
123
  export declare const DRAIN_SECOND_SIGNAL_MIN_GAP_DEFAULT_MS = 250;
124
+ /**
125
+ * 本仓读一个租户 scope 的**唯一**读法:给了就是它,缺席 = settings-schema 的缺省 scope(`GLOBAL_SCOPE`;缺省值只在契约包里写一处)。
126
+ * 引擎自己的 scope(`config.ts` 解析 env `FLEET_SCOPE`)与排空请求的 `scope`(`fleet/drain-entry.ts`)走同一只 —— 两侧缺席的读法不会分家。
127
+ * 住在本叶(而不在 `fleet/drain-entry.ts`):`config.ts` 读它,而 drain-entry 值引 `plugins/store-contracts.ts`,config 不值引 plugins 层。
128
+ */
129
+ export declare function fleetScopeOf(raw: string | undefined): string;
130
+ /** 中心基址三席(`SEMA_REGISTRY_URL` / `SEMA_REGISTRY_LIVE_URL` / `SEMA_REGISTRY_WEB_URL`)的形句 —— 判官 `config-center/center-request.ts` `pathBaseUrlFlaw` 一只(公告地址同判),配置目录三行同读这一句。 */
131
+ export declare const CENTER_BASE_URL_RULE = "\u8BBE\u4E86\u5C31\u5FC5\u987B\u662F\u53EF\u89E3\u6790\u7684 http(s) URL,\u4E14\u4E0D\u5E26\u51ED\u636E / query / fragment(\u57FA\u5740\u540E\u9762\u8981\u62FC\u8DEF\u5F84);\u574F\u5F62\u62D2\u542F(\u6CA1\u63A5\u4E2D\u5FC3\u4E5F\u62D2)";
132
+ /** 编排器片 2:`FLEET_LEASE_OUTAGE_MAX_MS` 的缺省(24 h)—— 中心 / 库故障、中心降级在今天都靠 LKG 无限期容忍,10 分钟级的窗会把它们变成数据面停服。 */
133
+ export declare const FLEET_LEASE_OUTAGE_MAX_DEFAULT_MS = 86400000;
124
134
  /**
125
135
  * S-753(core 7.33.0 #1121)—— `MODEL_FIRST_BYTE_TIMEOUT_MS`(→ core `BrainTimeoutConfig.firstByteTimeoutMs`,`fetch()` → 响应头
126
136
  * 的等待)的**合法域**,闭区间、整数 ms。解析器(`config.ts`)与目录行(`config-catalog.ts`)共读这一处。
@@ -2001,6 +2011,9 @@ export interface ServiceConfigFlat {
2001
2011
  /** LKG 落盘路径的覆写(`CONFIG_LKG_PATH`,绝对路径;单一读法 `config-lkg.ts` `lkgPathOverrideOf`)。缺席 = LKG 落在配置根下按箱派生。
2002
2012
  * 读者:LKG 的落盘 / 读回(`boot/config-center.ts`)与读拒行(部署姿态座席族 —— LKG 在哪,读拒就守哪,`lkgReadDenyRows`)。 */
2003
2013
  configLkgPath?: string;
2014
+ /** 远程配置的 LKG(last-known-good)盘缓存读写开关(`CONFIG_LKG_ENABLED`,缺省开;只对远程 provider 有意义)。单一读法:`loadConfig` 铸这一席
2015
+ * (同时登记极性行),`boot/config-center.ts` 的两个读点(boot 的 LKG 来源判定、skill 正文缓存目录)只读它 —— 起服之后改环境变量不影响。 */
2016
+ configLkgEnabled: boolean;
2004
2017
  /** Structured log level. */
2005
2018
  logLevel: "debug" | "info" | "warn" | "error";
2006
2019
  /** Per-principal request cap per minute on task/run endpoints. 0 disables. */
@@ -2090,9 +2103,15 @@ export interface ServiceConfigFlat {
2090
2103
  * `credential`(S-651,BREAKING:取代 7.98.0 及以前的 `token: string` 启动快照):凭证来源活对象
2091
2104
  * (`center-credential.ts`),每条出站请求经它取一次值 —— `SEMA_REGISTRY_TOKEN`(值)或
2092
2105
  * `SEMA_REGISTRY_TOKEN_FILE`(每次读文件,轮换 / 换账号不重启)二选一(两键名与判官的属主 = settings-schema
2093
- * `CENTER_CREDENTIAL_ENV` / `judgeCenterCredentialDeclaration`,S-742)。 */
2106
+ * `CENTER_CREDENTIAL_ENV` / `judgeCenterCredentialDeclaration`,S-742)。
2107
+ * `liveBaseUrl`(编排器片 2,env `SEMA_REGISTRY_LIVE_URL`):fleet 活性组(心跳 / 注销、服务租约 —— settings-schema `FLEET_LIVE_PATHS`)的基址,
2108
+ * 给「管理面在就绪门后、活性面另走一条不看就绪的入口」的部署;缺席 ⇒ 活性组也打 `baseUrl`(这条缺省只在 `centerFetch` 读)。
2109
+ * `webBaseUrl`(7.121.0,env `SEMA_REGISTRY_WEB_URL`):这个中心的 **web 面固定基址**,运维声明、引擎原样转述到 `/health.center.webBaseUrl`
2110
+ * (壳按它拼会话深链;路径怎么拼归壳)。起服时判成「可拼路径的基址」:http(s)、不带凭据 / query / fragment,否则拒启;缺席即缺席,引擎不拼不猜。 */
2094
2111
  configCenter?: {
2095
2112
  baseUrl: string;
2113
+ liveBaseUrl?: string;
2114
+ webBaseUrl?: string;
2096
2115
  credential: CenterCredentialSource;
2097
2116
  dryRun: boolean;
2098
2117
  worker?: string;
@@ -2103,6 +2122,23 @@ export interface ServiceConfigFlat {
2103
2122
  * 显式值压过派生(部署级旋钮无条件,与 `READ_FACE` 缺省按 posture 派生 S-167 同形)。只在装了租约管理器的部署上有意义
2104
2123
  * (`lease:"absent"` 的部署不参与);生效值上 caps `centerWiring.failMode`。 */
2105
2124
  fleetLeaseFailClosed: boolean;
2125
+ /** 编排器片 2:fleet 成员的**代际**(env `FLEET_GENERATION`,settings-schema `FLEET_GENERATION_ENV`;配置渲染器是唯一写手)。
2126
+ * 缺席 ⇔ 非 fleet 成员(单机形:不续服务租约、不受围栏)。在场则必须是规范十进制串(`"0"`、`"7"`;不许前导零 / 符号 / 空白 / 空串),
2127
+ * 否则拒启 —— 「设了但读不成代际」是渲染器或手配的事故,不是「非成员」。引擎把它原样回声到心跳与 `/health`(`generation`)。 */
2128
+ fleetGeneration?: string;
2129
+ /** 编排器片 2:本引擎所在的**租户 scope**(env `FLEET_SCOPE`,配置渲染器写)。`loadConfig` 解析一次:缺席 ⇒ 缺省 scope(settings-schema
2130
+ * `GLOBAL_SCOPE`,经本文件的 `fleetScopeOf` —— 本仓唯一读法);在场必须是 settings-schema `ScopeId` 形,否则拒启。
2131
+ * 唯一读者 = 排空口 `POST /v1/admin/drain` 的身份比对;续租体与心跳不带它(那两处的 scope 由中心凭证绑定)。 */
2132
+ fleetScope: string;
2133
+ /** `FLEET_ADVERTISE_ADDRESS`:本实例对中心公告的可达地址(运维声明;k8s pod IP / docker alias / 127.0.0.1:port 只有部署环境自己知道)。
2134
+ * `loadConfig` **无条件**解析一次:未设 / 空串 ⇒ 缺席(公告环不装);在场必须是可解析的 http(s) URL,否则拒启。读者:fleet 公告环、
2135
+ * 围栏判官 `fleetFenceLaneOf`、`/health.center.sessionsVisible`、启动日志 —— 都读这一席,不读 env。 */
2136
+ fleetAdvertiseAddress?: string;
2137
+ /** 编排器片 2:服务租约**中心不可达的容忍上限** `T_max`(ms;env `FLEET_LEASE_OUTAGE_MAX_MS`)。租约过期(中心不可达 / 答 unknown)之后照常服务,
2138
+ * 直到过期满这么久 ⇒ 新计费提交 503 `state.lease_expired`(取消 / HITL 应答 / 续跑 / 读面照放行),在飞不动、不自停;中心恢复授予即恢复。
2139
+ * 缺省 86 400 000(24 h);地板 = 服务租约 TTL 地板(settings-schema `SERVING_LEASE_TTL_FLOOR_MS`,60 s);不许 0 / 无穷,坏值拒启。
2140
+ * 只在租约生效时有意义(fleet 成员 ∧ 中心宣告了 `servingLease` 能力位)。 */
2141
+ fleetLeaseOutageMaxMs: number;
2106
2142
  /** S-698(design-325 §2):中心凭证**续签提前量**(ms)—— `SEMA_REGISTRY_TOKEN_FILE` 指向人的登录会话信封
2107
2143
  * (`kind:"user-session"`)时,访问令牌离到期 ≤ 本值即由引擎续签并原子改写同一文件(`center-credential-renewer.ts`)。
2108
2144
  * 缺省 5 min;`0` = 关续签、只读信封(壳负责续);其余必须是整数毫秒且落在 `CENTER_CREDENTIAL_RENEW_LEAD_MIN_MS`(一次续签请求的
@@ -2134,13 +2170,13 @@ export type ServiceMemoryConfig = Pick<ServiceConfigFlat, "memoryEngineEnabled"
2134
2170
  * config 这一层它只是一句声明。 */
2135
2171
  export type ServiceAuthConfig = Pick<ServiceConfigFlat, "authToken" | "authTokens" | "allowUnauthedWrites" | "corsOrigins" | "principalHeader" | "requirePrincipal" | "autonomy" | "commandPolicy" | "compliancePosture" | "lockedConfigKeys" | "configCatalogEnabled" | "retentionPolicy" | "retentionSweep" | "operatorPrincipals" | "principalJwtPubkeys" | "principalJwtIss" | "principalJwtAud" | "authBridgeIssuer" | "principalJwtMaxTtlSec" | "bindHost" | "bindHostSource">;
2136
2172
  /** 组:orchestration(编排 + 执行车道 + 沙箱面 + workflow/后台 agent 存留)。 */
2137
- export type ServiceOrchestrationConfig = Pick<ServiceConfigFlat, "remoteExec" | "remoteExecHomeDir" | "requireExecutionLane" | "worktreeIsolation" | "leaderEnabled" | "leaderFanoutEnabled" | "routerEnabled" | "selfOrchestrationEnabled" | "selfOrchestrationModels" | "selfOrchestrationWorkerIsolation" | "forkEnabled" | "experimentalObserverAgents" | "schedulerEnabled" | "schedulerSessionWakeup" | "schedulerSessionLifetime" | "schedulerStorePath" | "planModeEnabled" | "hooksTimeoutMs" | "workflowRunStoreBackend" | "workflowOrphanGraceMs" | "workflowJournalRetentionMs" | "workflowRunRetentionMs" | "usageWindows" | "workflowAgentsReadOnly" | "workflowSizeGuideline" | "backgroundAgentRetentionMs" | "backgroundAgentStaleRunningMs" | "backgroundAgentParkClaimStaleMs" | "delegationEntryCaps" | "rosterRetentionMs" | "leaderRunStaleMs" | "scratchpadSweepTtlMs" | "sandboxPkgSource" | "sessionAutoTitle" | "sessionAutoTitleSource" | "selectEnvironmentTool" | "envFactsEnabled" | "toolDeferLongtail" | "lspEnabled" | "lspHostEnabled" | "lspEnvAllow" | "imageBakes" | "readFace" | "readFaceSource" | "taskWriteFaceEscalation" | "readDenyPatterns" | "readDenyBuiltinTiers" | "readDenyBuiltinExclude" | "agentConfigDir" | "configLkgPath">;
2173
+ export type ServiceOrchestrationConfig = Pick<ServiceConfigFlat, "remoteExec" | "remoteExecHomeDir" | "requireExecutionLane" | "worktreeIsolation" | "leaderEnabled" | "leaderFanoutEnabled" | "routerEnabled" | "selfOrchestrationEnabled" | "selfOrchestrationModels" | "selfOrchestrationWorkerIsolation" | "forkEnabled" | "experimentalObserverAgents" | "schedulerEnabled" | "schedulerSessionWakeup" | "schedulerSessionLifetime" | "schedulerStorePath" | "planModeEnabled" | "hooksTimeoutMs" | "workflowRunStoreBackend" | "workflowOrphanGraceMs" | "workflowJournalRetentionMs" | "workflowRunRetentionMs" | "usageWindows" | "workflowAgentsReadOnly" | "workflowSizeGuideline" | "backgroundAgentRetentionMs" | "backgroundAgentStaleRunningMs" | "backgroundAgentParkClaimStaleMs" | "delegationEntryCaps" | "rosterRetentionMs" | "leaderRunStaleMs" | "scratchpadSweepTtlMs" | "sandboxPkgSource" | "sessionAutoTitle" | "sessionAutoTitleSource" | "selectEnvironmentTool" | "envFactsEnabled" | "toolDeferLongtail" | "lspEnabled" | "lspHostEnabled" | "lspEnvAllow" | "imageBakes" | "readFace" | "readFaceSource" | "taskWriteFaceEscalation" | "readDenyPatterns" | "readDenyBuiltinTiers" | "readDenyBuiltinExclude" | "agentConfigDir" | "configLkgPath" | "configLkgEnabled">;
2138
2174
  /** 组:limitsHttp(HTTP 面 + 各类上限/配额/回收窗)。 */
2139
2175
  export type ServiceLimitsHttpConfig = Pick<ServiceConfigFlat, "port" | "attachmentOrphanGraceMs" | "workspaceFileMaxBytes" | "attachmentMaxBytes" | "attachmentMimeAllowlist" | "attachmentUnboundTtlMs" | "infraCostRates" | "drainGraceMs" | "drainSecondSignalMinGapMs" | "sighupIdleGraceMs" | "parentPid" | "engineAutoExit" | "engineLingerMs" | "engineOrphanLingerMs" | "rateLimitPerMin" | "maxTaskCostUsd" | "maxTaskTokens" | "maxPrincipalCostUsd" | "costQuotaWindowSec" | "reapIntervalSec" | "runStaleSec" | "toolResultTtlSec">;
2140
2176
  /** 组:observability(可观测)。 */
2141
2177
  export type ServiceObservabilityConfig = Pick<ServiceConfigFlat, "metricsToken" | "traceToken" | "toolTrace" | "traceThinking" | "logLevel" | "otel">;
2142
2178
  /** 组:integrations(外部集成)。`mcpServers` / `a2aPeers` 由 sema-registry 适配器填(无 env 腿),归本组。 */
2143
- export type ServiceIntegrationsConfig = Pick<ServiceConfigFlat, "pluginsAllowHosts" | "mcpServers" | "a2aPeers" | "a2aServe" | "configBootFetchBudgetMs" | "gitApiBaseUrl" | "gitApiToken" | "gitApiKind" | "defaultScenario" | "skillsDir" | "centerCredentialRenewLeadMs" | "configCenter" | "fleetLeaseFailClosed" | "configProvider" | "configLocalDir">;
2179
+ export type ServiceIntegrationsConfig = Pick<ServiceConfigFlat, "pluginsAllowHosts" | "mcpServers" | "a2aPeers" | "a2aServe" | "configBootFetchBudgetMs" | "gitApiBaseUrl" | "gitApiToken" | "gitApiKind" | "defaultScenario" | "skillsDir" | "centerCredentialRenewLeadMs" | "configCenter" | "fleetLeaseFailClosed" | "fleetGeneration" | "fleetScope" | "fleetAdvertiseAddress" | "fleetLeaseOutageMaxMs" | "configProvider" | "configLocalDir">;
2144
2180
  /** 九个组槽。每组恒在场(`loadConfig` / `attachConfigGroups` 装好才交出配置),故不可选——新代码写
2145
2181
  * `config.modelPlane.model` 不需要 `?.`(可选组会把 `Model` 污染成 `Model | undefined`)。 */
2146
2182
  export interface ServiceConfigGroups {
@@ -1,4 +1,5 @@
1
1
  import { DRAIN_GRACE_FLOOR_MS } from "@sema-agent/settings-schema/fleet";
2
+ import { GLOBAL_SCOPE } from "@sema-agent/settings-schema/api/scopes";
2
3
  export const TASK_WRITE_FACE_ESCALATIONS = ["allow", "refuse"];
3
4
  export const RETENTION_MODES = ["audit-only", "enforce"];
4
5
  export const CONFIG_PROVIDER_WORDS = ["local", "remote"];
@@ -8,6 +9,11 @@ export const AUTO_MODE_CLASSIFIER_TIMEOUT_BOUNDS = { minMs: 1_000, maxMs: MAX_TI
8
9
  export { DRAIN_GRACE_FLOOR_MS };
9
10
  export const DRAIN_SECOND_SIGNAL_MIN_GAP_BOUNDS = { minMs: 0, maxMs: DRAIN_GRACE_FLOOR_MS };
10
11
  export const DRAIN_SECOND_SIGNAL_MIN_GAP_DEFAULT_MS = 250;
12
+ export function fleetScopeOf(raw) {
13
+ return raw ?? GLOBAL_SCOPE;
14
+ }
15
+ export const CENTER_BASE_URL_RULE = "设了就必须是可解析的 http(s) URL,且不带凭据 / query / fragment(基址后面要拼路径);坏形拒启(没接中心也拒)";
16
+ export const FLEET_LEASE_OUTAGE_MAX_DEFAULT_MS = 86_400_000;
11
17
  export const FIRST_BYTE_TIMEOUT_BOUNDS = { minMs: 0, maxMs: MAX_TIMER_DELAY_MS };
12
18
  export const RESILIENCE_DEFAULTS = {
13
19
  firstTokenTimeoutMs: { env: "MODEL_FIRST_TOKEN_TIMEOUT_MS", value: 600_000 },