@sema-agent/server 7.105.0 → 7.106.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 (174) hide show
  1. package/MIGRATION.md +82 -0
  2. package/USAGE.md +134 -42
  3. package/deploy/sema-up/chart/templates/server-v4pro.yaml +2 -0
  4. package/deploy/sema-up/cluster-up.sh +15 -2
  5. package/deploy/sema-up/kube-up.sh +8 -3
  6. package/deploy/sema-up/lib/flagshape.sh +29 -0
  7. package/deploy/sema-up/lib/parse-machines.py +4 -1
  8. package/deploy/sema-up/sema-up.sh +11 -9
  9. package/deploy/sema-up/stack/sema-stack-cluster.yaml +3 -1
  10. package/dist/approval-card.d.ts +1 -1
  11. package/dist/approval.d.ts +6 -1
  12. package/dist/approval.js +4 -4
  13. package/dist/auth-bridge.d.ts +14 -0
  14. package/dist/auth-bridge.js +7 -0
  15. package/dist/auto-mode-face.d.ts +49 -40
  16. package/dist/auto-mode-face.js +6 -68
  17. package/dist/bench/s1/live-deps.d.ts +9 -2
  18. package/dist/bench/s1/live-deps.js +13 -8
  19. package/dist/boot/budget-tracing.js +3 -2
  20. package/dist/boot/config-center.d.ts +4 -0
  21. package/dist/boot/config-center.js +95 -37
  22. package/dist/boot/coordinators.d.ts +1 -0
  23. package/dist/boot/coordinators.js +3 -1
  24. package/dist/boot/device-lane.d.ts +2 -1
  25. package/dist/boot/execution-env.d.ts +11 -0
  26. package/dist/boot/execution-env.js +30 -6
  27. package/dist/boot/execution-lane.js +2 -3
  28. package/dist/boot/leader.d.ts +2 -1
  29. package/dist/boot/leader.js +2 -0
  30. package/dist/boot/parked-revive-gate.d.ts +10 -7
  31. package/dist/boot/parked-revive-gate.js +4 -11
  32. package/dist/boot/resolve-spec.d.ts +1 -4
  33. package/dist/boot/resolve-spec.js +44 -21
  34. package/dist/boot/runner-deps.d.ts +5 -8
  35. package/dist/boot/runner-deps.js +3 -9
  36. package/dist/boot/side-query-lane.d.ts +2 -2
  37. package/dist/boot/stage-03-store-backend.d.ts +1 -1
  38. package/dist/boot/stage-03-store-backend.js +2 -2
  39. package/dist/boot/stage-04-stores.d.ts +1 -1
  40. package/dist/boot/stage-05-execution-env.d.ts +2 -1
  41. package/dist/boot/stage-05-execution-env.js +2 -2
  42. package/dist/boot/stage-06-runners.d.ts +3 -2
  43. package/dist/boot/stage-06-runners.js +2 -5
  44. package/dist/boot/stage-07-capability-layer.d.ts +2 -2
  45. package/dist/boot/stage-07-capability-layer.js +1 -2
  46. package/dist/boot/stage-08-reapers.d.ts +2 -2
  47. package/dist/boot/stage-09-leader.d.ts +2 -2
  48. package/dist/boot/stage-09-leader.js +2 -2
  49. package/dist/boot/stage-10-http-server.d.ts +2 -2
  50. package/dist/boot/stage-10-http-server.js +11 -7
  51. package/dist/boot/stores.d.ts +1 -1
  52. package/dist/boot/stores.js +6 -4
  53. package/dist/brain.d.ts +24 -2
  54. package/dist/brain.js +29 -10
  55. package/dist/budget.js +3 -0
  56. package/dist/capabilities/center-wiring.d.ts +64 -0
  57. package/dist/capabilities/center-wiring.js +24 -0
  58. package/dist/capabilities/composite-members.d.ts +89 -0
  59. package/dist/capabilities/composite-members.js +20 -0
  60. package/dist/capabilities/execution-lane.d.ts +3 -0
  61. package/dist/capabilities/execution-lane.js +5 -2
  62. package/dist/config-catalog.d.ts +30 -5
  63. package/dist/config-catalog.js +75 -26
  64. package/dist/config-center/apply-effective.d.ts +1 -1
  65. package/dist/config-center/apply-effective.js +7 -8
  66. package/dist/config-center/apply-ledger.d.ts +17 -0
  67. package/dist/config-center/apply-ledger.js +5 -0
  68. package/dist/config-center/center-request.d.ts +17 -0
  69. package/dist/config-center/center-request.js +4 -0
  70. package/dist/config-center/pull-state.d.ts +29 -0
  71. package/dist/config-center/pull-state.js +2 -0
  72. package/dist/config-types.d.ts +41 -14
  73. package/dist/config-types.js +1 -0
  74. package/dist/config.d.ts +35 -0
  75. package/dist/config.js +69 -16
  76. package/dist/cross-session-settings.d.ts +1 -1
  77. package/dist/deployment-governance.d.ts +12 -1
  78. package/dist/deployment-governance.js +2 -0
  79. package/dist/execution-lane-caps.d.ts +30 -2
  80. package/dist/execution-lane-caps.js +12 -0
  81. package/dist/fleet-client.d.ts +18 -13
  82. package/dist/fleet-client.js +32 -22
  83. package/dist/fleet-lease.d.ts +72 -47
  84. package/dist/fleet-lease.js +66 -14
  85. package/dist/governance-http-status.d.ts +0 -1
  86. package/dist/governance-http-status.js +0 -1
  87. package/dist/hosted-posture.d.ts +6 -17
  88. package/dist/hosted-posture.js +11 -6
  89. package/dist/http/admission.js +14 -9
  90. package/dist/http/approval-sweeps.js +13 -1
  91. package/dist/http/dispatch.js +1 -1
  92. package/dist/http/durable-run-leg.js +2 -0
  93. package/dist/http/request-guards.d.ts +25 -0
  94. package/dist/http/request-guards.js +25 -5
  95. package/dist/http/resume-legs.js +19 -11
  96. package/dist/http/route-ctx.d.ts +9 -0
  97. package/dist/http/routes/approvals-assistant.js +0 -1
  98. package/dist/http/routes/capabilities.js +12 -11
  99. package/dist/http/routes/config-catalog.js +2 -1
  100. package/dist/http/routes/diagnostics.js +2 -0
  101. package/dist/http/routes/tasks.js +4 -1
  102. package/dist/http/server.d.ts +20 -4
  103. package/dist/http/session-inherit-env.d.ts +37 -0
  104. package/dist/http/session-inherit-env.js +6 -0
  105. package/dist/http/wire-types.d.ts +19 -28
  106. package/dist/index.d.ts +2 -1
  107. package/dist/index.js +2 -1
  108. package/dist/key-resolver.d.ts +74 -19
  109. package/dist/key-resolver.js +65 -7
  110. package/dist/leader/leader.d.ts +6 -0
  111. package/dist/leader/leader.js +2 -1
  112. package/dist/leader/repair-wire.js +2 -1
  113. package/dist/leader/wire.d.ts +9 -1
  114. package/dist/leader/wire.js +10 -6
  115. package/dist/observability/fail-open.d.ts +5 -1
  116. package/dist/observability/fail-open.js +5 -1
  117. package/dist/observability/metrics.js +2 -0
  118. package/dist/observability/permission-rule-events.d.ts +1 -1
  119. package/dist/observability/permission-rule-events.js +1 -1
  120. package/dist/observability/run-terminal.d.ts +38 -1
  121. package/dist/observability/run-terminal.js +2 -5
  122. package/dist/observability/secret-env-scrub.js +1 -1
  123. package/dist/observability/tool-trace.d.ts +2 -0
  124. package/dist/operator-ask.d.ts +29 -1
  125. package/dist/operator-ask.js +6 -0
  126. package/dist/parked-decide.d.ts +14 -1
  127. package/dist/parked-decide.js +8 -1
  128. package/dist/plugins/session-policy-store-sql.d.ts +38 -7
  129. package/dist/plugins/session-policy-store-sql.js +64 -22
  130. package/dist/plugins/sql-driver.d.ts +1 -1
  131. package/dist/plugins/store-backend.d.ts +12 -3
  132. package/dist/plugins/store-backend.js +16 -5
  133. package/dist/plugins/tidb-pool.js +1 -1
  134. package/dist/read-face-posture.d.ts +4 -4
  135. package/dist/request-key-closure.js +2 -2
  136. package/dist/request-value-closure.d.ts +17 -5
  137. package/dist/request-value-closure.js +12 -0
  138. package/dist/router/route-orchestration.d.ts +11 -0
  139. package/dist/router/route-orchestration.js +7 -6
  140. package/dist/run-local.js +4 -2
  141. package/dist/runs.d.ts +2 -1
  142. package/dist/runs.js +4 -4
  143. package/dist/runtime-caps-resolver.d.ts +4 -10
  144. package/dist/runtime-governance.d.ts +8 -11
  145. package/dist/runtime-governance.js +10 -41
  146. package/dist/sealed-key.d.ts +0 -40
  147. package/dist/sealed-key.js +0 -20
  148. package/dist/server-secret-env.d.ts +11 -12
  149. package/dist/session-policy-wire.d.ts +5 -3
  150. package/dist/session-titler.d.ts +11 -0
  151. package/dist/session-titler.js +6 -2
  152. package/dist/sql-engine-posture.d.ts +2 -2
  153. package/dist/store-posture.d.ts +53 -0
  154. package/dist/store-posture.js +32 -0
  155. package/dist/task-settings.d.ts +34 -45
  156. package/dist/task-settings.js +46 -25
  157. package/dist/tool-approval.d.ts +51 -22
  158. package/dist/tool-approval.js +44 -8
  159. package/dist/tool-declaration-http-status.d.ts +12 -0
  160. package/dist/tool-declaration-http-status.js +12 -0
  161. package/dist/trace/core-keyset-guard.d.ts +7 -7
  162. package/dist/trace/engine-notice-wire.d.ts +1 -1
  163. package/dist/trace/engine-notice-wire.js +5 -0
  164. package/dist/trace/project.d.ts +3 -2
  165. package/dist/trace/project.js +51 -3
  166. package/dist/trace/wire-projection-faces.d.ts +1 -1
  167. package/dist/trace/wire-projection-faces.js +1 -1
  168. package/dist/unattended-permission-mode.d.ts +110 -0
  169. package/dist/unattended-permission-mode.js +65 -0
  170. package/package.json +3 -3
  171. package/dist/boot/session-shell-gate-registry.d.ts +0 -39
  172. package/dist/boot/session-shell-gate-registry.js +0 -21
  173. package/dist/roster-bound-http-status.d.ts +0 -6
  174. package/dist/roster-bound-http-status.js +0 -6
package/dist/config.js CHANGED
@@ -10,14 +10,14 @@ import { DEFAULT_ELICITATION_THROTTLE } from "./elicitation.js";
10
10
  import { isV2ScopeKey, memoryReplicationCoverage } from "./memory-scope.js";
11
11
  import { DEFAULT_QUESTION_THROTTLE } from "./question.js";
12
12
  import { GIT_API_KINDS, isGitApiKind } from "./git-api-kind.js";
13
- import { CONFIG_PROVIDER_WORDS, RETENTION_MODES, MEMORY_CONSOLIDATION_MIN_INTERVAL_SEC, RESILIENCE_DEFAULTS, TASK_WRITE_FACE_ESCALATIONS, MAX_TIMER_DELAY_MS, AUTO_MODE_CLASSIFIER_TIMEOUT_BOUNDS, DRAIN_GRACE_FLOOR_MS, DRAIN_SECOND_SIGNAL_MIN_GAP_BOUNDS, DRAIN_SECOND_SIGNAL_MIN_GAP_DEFAULT_MS, FIRST_BYTE_TIMEOUT_BOUNDS } from "./config-types.js";
13
+ import { CONFIG_PROVIDER_WORDS, DB_BACKEND_WORDS, RETENTION_MODES, MEMORY_CONSOLIDATION_MIN_INTERVAL_SEC, RESILIENCE_DEFAULTS, TASK_WRITE_FACE_ESCALATIONS, MAX_TIMER_DELAY_MS, AUTO_MODE_CLASSIFIER_TIMEOUT_BOUNDS, DRAIN_GRACE_FLOOR_MS, DRAIN_SECOND_SIGNAL_MIN_GAP_BOUNDS, DRAIN_SECOND_SIGNAL_MIN_GAP_DEFAULT_MS, FIRST_BYTE_TIMEOUT_BOUNDS } from "./config-types.js";
14
14
  import { buildWriteProtectionPosture } from "./write-protection.js";
15
15
  import { normalizeSshFingerprint } from "./ssh-host-key.js";
16
16
  import { buildReadFacePostureNotice, readFaceForPosture } from "./read-face-posture.js";
17
17
  import { THINKING_LEVELS, THINKING_FORMATS, buildModelCompat, isThinkingFormat, isThinkingTier } from "./model-compat.js";
18
18
  import {} from "./posture-source.js";
19
19
  import { CONTROL_AND_BIDI_STRIP_RE } from "./text-bidi.js";
20
- import { REMOTE_EXEC_PROVIDER_WORDS, executionLaneOf } from "./execution-lane-caps.js";
20
+ import { REMOTE_EXEC_PROVIDER_WORDS, executionLaneOf, isSingleMachineDefaultLane } from "./execution-lane-caps.js";
21
21
  import { AUTONOMY_MODES } from "./runtime-governance.js";
22
22
  import { createCenterCredentialSource, readCenterCredentialDeclaration } from "./center-credential.js";
23
23
  import { parseCenterCredentialRenewLeadMs } from "./center-credential-renewer.js";
@@ -27,6 +27,7 @@ import { modelRouteEndpointRefusal } from "./model-route-endpoint.js";
27
27
  import { buildDisplaySafeUrl, redactSecrets } from "./trace/redact.js";
28
28
  import { allowlistedEnv } from "./server-secret-env.js";
29
29
  import { ENV_NAME_ALLOWLIST_KNOBS, ENV_NAME_ALLOWLIST_KNOB_NAMES, describeAllowlistPostures } from "./env-name-allowlist-knobs.js";
30
+ import { parseUnattendedPermissionMode } from "./unattended-permission-mode.js";
30
31
  export { RETENTION_MODES, MEMORY_CONSOLIDATION_MIN_INTERVAL_SEC } from "./config-types.js";
31
32
  function csv(name) {
32
33
  return (process.env[name] ?? "")
@@ -105,7 +106,7 @@ function defaultSkillsDir() {
105
106
  }
106
107
  }
107
108
  export const RETIRED_ENV_REFUSAL_CODE = "config.env_key_retired";
108
- const RETIRED_ENVS = [
109
+ export const RETIRED_ENVS = [
109
110
  ["TIDB_HOST", "MYSQL_HOST", "renamed"], ["TIDB_PORT", "MYSQL_PORT", "renamed"], ["TIDB_USER", "MYSQL_USER", "renamed"],
110
111
  ["TIDB_PASSWORD", "MYSQL_PASSWORD", "renamed"], ["TIDB_DATABASE", "MYSQL_DATABASE", "renamed"], ["TIDB_POOL_SIZE", "MYSQL_POOL_SIZE", "renamed"],
111
112
  ["CONFIG_CENTER_URL", "SEMA_REGISTRY_URL", "renamed"], ["CONFIG_CENTER_TOKEN", CENTER_CREDENTIAL_ENV.token, "renamed"],
@@ -135,6 +136,25 @@ const RETIRED_ENVS = [
135
136
  ["HOST_BG_DISABLED", "HOST_BG_ENABLED", "replaced", "negative-name alias retired in server 3.0.0 — unset it; the polarity is inverted: HOST_BG_DISABLED=true is HOST_BG_ENABLED=false (the positive name defaults on)"],
136
137
  ["HOST_EXEC_SPOOL_DISABLED", "HOST_EXEC_SPOOL_ENABLED", "replaced", "negative-name alias retired in server 3.0.0 — unset it; the polarity is inverted: HOST_EXEC_SPOOL_DISABLED=true is HOST_EXEC_SPOOL_ENABLED=false (the positive name defaults on)"],
137
138
  ];
139
+ for (const row of RETIRED_ENVS)
140
+ Object.freeze(row);
141
+ Object.freeze(RETIRED_ENVS);
142
+ export function buildRetiredEnvWire() {
143
+ return RETIRED_ENVS.map((row) => {
144
+ switch (row[2]) {
145
+ case "renamed":
146
+ return { name: row[0], kind: row[2], replacement: row[1] };
147
+ case "replaced":
148
+ return { name: row[0], kind: row[2], replacement: row[1], reason: row[3] };
149
+ case "removed":
150
+ return { name: row[0], kind: row[2], reason: row[1] };
151
+ default: {
152
+ const unhandled = row;
153
+ throw new Error(`unhandled retired-env kind: ${String(unhandled[2])}`);
154
+ }
155
+ }
156
+ });
157
+ }
138
158
  const RETIRED_ENV_SENTENCE = {
139
159
  renamed: (hits) => `${hits.map(([old, to]) => `${old} → use ${to}`).join("; ")} — rename the env key(s); the value semantics are unchanged`,
140
160
  replaced: (hits) => `${hits.map(([old, to, , why]) => `${old} → ${to} (${why})`).join("; ")} — set the replacement deliberately: ` +
@@ -205,11 +225,11 @@ export function drainConfigNotices() {
205
225
  return CONFIG_NOTICES.splice(0, CONFIG_NOTICES.length);
206
226
  }
207
227
  let EXEC_LANE_NOTICE;
208
- function buildExecLaneNotice(remoteExec, configProvider) {
209
- const explicit = process.env.REMOTE_EXEC !== undefined && process.env.REMOTE_EXEC !== "";
228
+ function buildExecLaneNotice(remoteExec) {
229
+ const lane = executionLaneOf(remoteExec?.provider);
210
230
  return {
211
- lane: executionLaneOf(remoteExec?.provider),
212
- source: explicit ? "explicit-env" : configProvider === "local" ? "default-local-provider" : "default-unset",
231
+ lane,
232
+ source: lane === "in-process" ? "default-unset" : isSingleMachineDefaultLane(remoteExec) ? "default-local-provider" : "explicit-env",
213
233
  };
214
234
  }
215
235
  function parseConfigProvider() {
@@ -822,6 +842,18 @@ function noticeAutoCompactAboveWindow(slot, envName, m) {
822
842
  },
823
843
  });
824
844
  }
845
+ function buildUnusedCoordinatesRefusal(keys, auto) {
846
+ const engine = keys.length === 1 ? (keys[0] === "PG_HOST" ? "pg" : "mysql") : "mysql|pg";
847
+ const probedHostKey = auto?.autoProbes === "pg" ? "PG_HOST" : "MYSQL_HOST";
848
+ const why = auto !== undefined
849
+ ? `SESSION_BACKEND=auto probes the ${auto.autoProbes} engine (DB_BACKEND ${process.env.DB_BACKEND ? "names it" : "unset ⇒ mysql"}), whose host key ${probedHostKey} is not set`
850
+ : "no durable session store is selected (neither DB_BACKEND=mysql|pg nor SESSION_BACKEND=mysql|auto), so the store would run IN MEMORY";
851
+ return (`${keys.join(" and ")} ${keys.length === 1 ? "is" : "are"} set but the current configuration never uses ${keys.length === 1 ? "it" : "them"}: ${why}. ` +
852
+ `Refusing to boot rather than running in memory silently. ` +
853
+ `To persist: DB_BACKEND=${engine} (or SESSION_BACKEND=mysql for the MySQL-protocol engine); ` +
854
+ `to run in memory: remove the coordinates, or set DB_BACKEND=memory; ` +
855
+ `for best effort (persist while the store is reachable, memory otherwise): SESSION_BACKEND=auto with the engine pointed right (DB_BACKEND=pg with PG_HOST, DB_BACKEND=mysql with MYSQL_HOST).`);
856
+ }
825
857
  function parseStoreDomain(ctx) {
826
858
  const { requirePrincipal } = ctx;
827
859
  const dbBackendSet = !!process.env.DB_BACKEND;
@@ -829,7 +861,7 @@ function parseStoreDomain(ctx) {
829
861
  !!(process.env.MYSQL_HOST || process.env.PG_HOST);
830
862
  const bareBoot = !dbBackendSet && !sqlSignal;
831
863
  const multiTenantBareBoot = bareBoot && requirePrincipal;
832
- const dbBackend = enumEnv("DB_BACKEND", bareBoot ? (multiTenantBareBoot ? "memory" : "local") : "mysql", ["mysql", "pg", "local", "memory"]);
864
+ const dbBackend = enumEnv("DB_BACKEND", bareBoot ? (multiTenantBareBoot ? "memory" : "local") : "mysql", DB_BACKEND_WORDS);
833
865
  if (multiTenantBareBoot) {
834
866
  CONFIG_NOTICES.push({
835
867
  level: "warn",
@@ -846,6 +878,11 @@ function parseStoreDomain(ctx) {
846
878
  const dbHostSet = dbBackend === "pg" ? !!process.env.PG_HOST : !!process.env.MYSQL_HOST;
847
879
  const needsTidb = sessionBackend === "tidb" || (sessionBackend === "auto" && dbHostSet);
848
880
  const needsDb = needsTidb;
881
+ const sqlCoordinateKeys = [...(process.env.MYSQL_HOST ? ["MYSQL_HOST"] : []), ...(process.env.PG_HOST ? ["PG_HOST"] : [])];
882
+ const coordinatesUnusedByDeclaration = process.env.DB_BACKEND === "memory" || process.env.DB_BACKEND === "local" || process.env.SESSION_BACKEND === "memory";
883
+ if (sqlCoordinateKeys.length > 0 && !needsDb && !coordinatesUnusedByDeclaration) {
884
+ throw new Error(buildUnusedCoordinatesRefusal(sqlCoordinateKeys, sessionBackend === "auto" ? { autoProbes: dbBackend } : undefined));
885
+ }
849
886
  return {
850
887
  sessionBackend,
851
888
  sessionCacheTtlSec: numEnv("SESSION_CACHE_TTL_SEC", "300"),
@@ -1290,6 +1327,8 @@ function unattendedApprovalPolicyEnv() {
1290
1327
  function parseApprovalDomain(ctx) {
1291
1328
  const { postureOn, singleUserTurnkey, infraReady } = ctx;
1292
1329
  const unattendedPolicy = unattendedApprovalPolicyEnv();
1330
+ const unattendedMode = parseUnattendedPermissionMode(process.env.UNATTENDED_PERMISSION_MODE);
1331
+ CONFIG_NOTICES.push({ level: "info", event: "unattended_permission_mode", fields: { value: unattendedMode.value, source: unattendedMode.source } });
1293
1332
  const streamAskWindowMsSource = postureSourceOfNumEnv(process.env.STREAM_ASK_WINDOW_MS, singleUserTurnkey);
1294
1333
  const toolApprovalEnabled = postureOn("TOOL_APPROVAL_ENABLED");
1295
1334
  let streamAskWindowMsEffectiveSource = streamAskWindowMsSource;
@@ -1335,7 +1374,7 @@ function parseApprovalDomain(ctx) {
1335
1374
  ...((decision) => (decision === undefined ? {} : { writeProtectedPaths: decision }))(parseWriteProtectedTableEnv(process.env.WRITE_PROTECTED_EXTRA, process.env.WRITE_PROTECTED_TABLE_REPLACE)),
1336
1375
  ...((raw) => {
1337
1376
  const trimmed = (raw ?? "").trim();
1338
- if (trimmed === "always" || trimmed === "classify")
1377
+ if (trimmed === "always")
1339
1378
  return { manualModeShellGate: trimmed };
1340
1379
  if (trimmed === "" || trimmed === "off") {
1341
1380
  if (trimmed === "off") {
@@ -1346,14 +1385,20 @@ function parseApprovalDomain(ctx) {
1346
1385
  env: "MANUAL_MODE_SHELL_GATE",
1347
1386
  raw: trimmed,
1348
1387
  effect: "accepted as a no-op (normalized to the knob being absent) — this knob has a TIGHTEN half only, so `off` cannot relax anything; " +
1349
- "the effective shell gate still comes from AUTONOMY and the client's permission mode. NOT symmetric with SENSITIVE_WRITE_PATTERNS=off, which really does clear a server-owned default set",
1388
+ "the effective shell gate still comes from AUTONOMY and the engine's permission-mode arbitration. NOT symmetric with SENSITIVE_WRITE_PATTERNS=off, which really does clear a server-owned default set",
1350
1389
  },
1351
1390
  });
1352
1391
  }
1353
1392
  return {};
1354
1393
  }
1355
- throw new Error(`env MANUAL_MODE_SHELL_GATE="${trimmed.slice(0, 120)}" is not a recognized value — use "always" (every shell call is gated), "classify" (core's classifier decides per call), or "off"/unset (this knob contributes nothing). ` +
1356
- `The words are matched EXACTLY (lowercase, surrounding whitespace trimmed): "OFF"/"Classify" land here too, deliberately — a closed word table with no case-folding is what keeps "it looked accepted" from meaning "it did nothing". ` +
1394
+ if (trimmed === "classify") {
1395
+ throw new Error(`env MANUAL_MODE_SHELL_GATE="classify" is retired (server 7.106.0 / @sema-agent/core 7.34.0): the engine now puts every full-shell command it cannot prove read-only to its question under EVERY permission mode — ` +
1396
+ `that tier is the default and needs no knob. What "classify" added on top was a question under a bypassPermissions request, which the engine now releases by mode. ` +
1397
+ `Use "always" to keep EVERY shell call behind an approval under every mode (bypassPermissions included), or unset it / "off" to rely on the engine's mode arbitration. ` +
1398
+ `Refusing to boot rather than accepting it as a no-op: that would silently drop the half the operator configured it for`);
1399
+ }
1400
+ throw new Error(`env MANUAL_MODE_SHELL_GATE="${trimmed.slice(0, 120)}" is not a recognized value — use "always" (every shell call is gated) or "off"/unset (this knob contributes nothing). ` +
1401
+ `The words are matched EXACTLY (lowercase, surrounding whitespace trimmed): "OFF"/"Always" land here too, deliberately — a closed word table with no case-folding is what keeps "it looked accepted" from meaning "it did nothing". ` +
1357
1402
  `Refusing to boot rather than treating a misspelled value as "unset": that silently ran the deployment with NO shell gate while the operator believed one was configured`);
1358
1403
  })(process.env.MANUAL_MODE_SHELL_GATE),
1359
1404
  permissionsDisableAutoMode: boolEnv("PERMISSIONS_DISABLE_AUTO_MODE", false),
@@ -1384,6 +1429,7 @@ function parseApprovalDomain(ctx) {
1384
1429
  streamAskWindowMsSource: streamAskWindowMsEffectiveSource,
1385
1430
  durableApprovalSource: postureSourceOfKnob("DURABLE_APPROVAL"),
1386
1431
  unattendedApprovalPolicy: unattendedPolicy,
1432
+ unattendedPermissionMode: unattendedMode.value,
1387
1433
  approvalTimeoutSec: numEnv("APPROVAL_TIMEOUT_SEC", "0"),
1388
1434
  };
1389
1435
  }
@@ -1949,7 +1995,7 @@ function parseOrchestrationDomain(ctx) {
1949
1995
  }
1950
1996
  :
1951
1997
  ctx.configProvider === "local"
1952
- ? { provider: "host" }
1998
+ ? { provider: "host", singleMachineDefault: true }
1953
1999
  : undefined,
1954
2000
  worktreeIsolation: boolEnv("WORKTREE_ISOLATION_ENABLED", false) && process.env.WORKTREE_REPO_ROOT
1955
2001
  ? {
@@ -2136,11 +2182,18 @@ function parseIntegrationsDomain(ctx) {
2136
2182
  skillsDir: env("SKILLS_DIR", defaultSkillsDir()),
2137
2183
  centerCredentialRenewLeadMs: parseCenterCredentialRenewLeadMs(process.env.CENTER_CREDENTIAL_RENEW_LEAD_MS),
2138
2184
  configCenter: process.env.SEMA_REGISTRY_URL ? parseConfigCenter(env("SEMA_REGISTRY_URL"), requirePrincipal) : undefined,
2185
+ fleetLeaseFailClosed: parseFleetLeaseFailClosed(process.env.FLEET_LEASE_FAIL_CLOSED, requirePrincipal),
2139
2186
  ...(configProvider !== undefined ? { configProvider } : {}),
2140
2187
  configLocalDir: process.env.CONFIG_LOCAL_DIR || undefined,
2141
2188
  a2aServe: parseA2aServe(),
2142
2189
  };
2143
2190
  }
2191
+ function parseFleetLeaseFailClosed(raw, requirePrincipal) {
2192
+ const explicit = parseBoolWord("FLEET_LEASE_FAIL_CLOSED", raw);
2193
+ const value = explicit ?? requirePrincipal;
2194
+ registerKnob({ name: "FLEET_LEASE_FAIL_CLOSED", polarity: "posture", value, source: explicit !== undefined ? "env" : requirePrincipal ? "posture" : "default" });
2195
+ return value;
2196
+ }
2144
2197
  function parseConfigCenter(baseUrl, requirePrincipal) {
2145
2198
  const worker = process.env.SEMA_REGISTRY_WORKER || undefined;
2146
2199
  const declared = readCenterCredentialDeclaration();
@@ -2246,7 +2299,7 @@ const MODEL_PLANE_GROUP_KEYS = [
2246
2299
  const APPROVAL_GROUP_KEYS = [
2247
2300
  "approvalRequire", "approvalDeny", "approvalTimeoutSec", "approvalAutoBudget", "approvalNeverAuto",
2248
2301
  "approvalHmacKeys", "durableApproval", "durableApprovalSource", "streamAskWindowMsSource", "directApprovalDoor", "directDoorActive", "resourceSuspend",
2249
- "resourceSuspendTtlSec", "resourceSuspendTotalTokens", "resourceSuspendTotalBudgetUsd", "resourceSuspendMaxSlices", "askQuestionEnabled", "questionThrottle", "toolApprovalEnabled", "permissionRulesEnabled", "permissionRulesEnabledExplicit", "streamAskWindowMarginMs", "streamApproval", "unattendedApprovalPolicy", "mcpElicitation",
2302
+ "resourceSuspendTtlSec", "resourceSuspendTotalTokens", "resourceSuspendTotalBudgetUsd", "resourceSuspendMaxSlices", "askQuestionEnabled", "questionThrottle", "toolApprovalEnabled", "permissionRulesEnabled", "permissionRulesEnabledExplicit", "streamAskWindowMarginMs", "streamApproval", "unattendedApprovalPolicy", "unattendedPermissionMode", "mcpElicitation",
2250
2303
  "sensitiveWritePatterns", "writeProtectedPaths", "manualModeShellGate", "permissionsDisableAutoMode", "autoModeClassifierFallback", "autoModeClassifierTimeoutMs", "crossSessionInbound", "crossSessionDialogExpiry",
2251
2304
  ];
2252
2305
  const MEMORY_GROUP_KEYS = [
@@ -2280,7 +2333,7 @@ const LIMITS_HTTP_GROUP_KEYS = [
2280
2333
  const OBSERVABILITY_GROUP_KEYS = ["metricsToken", "traceToken", "toolTrace", "traceThinking", "logLevel", "otel"];
2281
2334
  const INTEGRATIONS_GROUP_KEYS = [
2282
2335
  "pluginsAllowHosts", "mcpServers", "a2aPeers", "a2aServe", "configBootFetchBudgetMs", "gitApiBaseUrl", "gitApiToken", "gitApiKind", "defaultScenario",
2283
- "skillsDir", "centerCredentialRenewLeadMs", "configCenter", "configProvider", "configLocalDir",
2336
+ "skillsDir", "centerCredentialRenewLeadMs", "configCenter", "fleetLeaseFailClosed", "configProvider", "configLocalDir",
2284
2337
  ];
2285
2338
  export const CONFIG_GROUP_KEYS = {
2286
2339
  store: STORE_GROUP_KEYS,
@@ -2405,7 +2458,7 @@ export function loadConfig() {
2405
2458
  throw new Error(refusal);
2406
2459
  }
2407
2460
  }
2408
- EXEC_LANE_NOTICE = buildExecLaneNotice(orchestration.remoteExec, configProvider);
2461
+ EXEC_LANE_NOTICE = buildExecLaneNotice(orchestration.remoteExec);
2409
2462
  return attachConfigGroups({
2410
2463
  ...store,
2411
2464
  ...modelPlane,
@@ -19,7 +19,7 @@
19
19
  * 而 `crossSessionInbound` 在 CC 里是**每会话**的 settings 值。本服务是「一个 Runner × 多会话」的宿主:
20
20
  * 把某一次提交的 `body.settings.crossSessionInbound` 塞进部署级座位,等于让 A 会话的设置去治理 B 会话的
21
21
  * 排水——比不接线更糟,且是静默的。可选的补法只有两条,都不在本件射程内:
22
- * (a) 引擎开一个**每任务**座(形如既有的 `TaskSpec.autoModeRequested` 意图座)——上游诉求,已留档;
22
+ * (a) 引擎开一个**每任务**座(形如 `TaskSpec.permissionMode` 那样的每任务座)——上游诉求,已留档;
23
23
  * (b) 本仓用 per-run 异步上下文托住层,并保证**每一条** run 腿都建立该上下文——漏一条腿 =
24
24
  * 用户设的 `refuse` 静默失效 = 安全轴 fail-open,不许半接。
25
25
  * 在 (a)/(b) 落地之前,提交面对这两个键**明拒**(`TASK_SETTINGS_UNSUPPORTED_KEYS`,400 体 `unsupportedKeys`
@@ -179,7 +179,18 @@ export declare function createDurableQuestionGate(live: LiveQuestionFace | undef
179
179
  * 无 root/cwd 声明 ⇒ 家族 undefined ⇒ 只认 `/`-rooted,与引擎同一律)。
180
180
  */
181
181
  type WriteTargetForm = "unstamped-relative" | "absolute-or-stamped";
182
- /** 只在 `form` 那一类写目标上说话,其余显式 allow(让位);非写调用与提不出目标的调用原样交给 policy(它自己判 allow)。 */
182
+ /**
183
+ * 只在 `form` 那一类写目标上说话,其余显式 allow(让位);非写调用与提不出目标的调用原样交给 policy(它自己判 allow)。
184
+ *
185
+ * 🔴 **S-877(core 7.35.0 #1200):本包装不是 `wrapToolPolicy`,它自报成员**。core 的 wrap 契约是「包装至少拒内层所拒」,
186
+ * 而本包装**放行**内层会拒的另一形写目标 ⇒ 按契约它是一只自己的策略,内层的成员不能照搬(core wire 单点名这一只):
187
+ * · `writeTargetGuard`(守卫集策略自带,shell 写站问「这个绝对目标守不守」)—— 只在本层的形**覆盖守卫的入参形**时带上
188
+ * ({@link WRITE_TARGET_GUARD_DOMAIN}):真身层带;词法层不带(它的 env 对绝对形报错 ⇒ core 的「判不了就守」会把每一次
189
+ * shell 写都判成受守,正与两层射程互补的原设计相反)。这不是给某一形开的特例,是同一条「成员描述它守得住的那部分」的读法;
190
+ * · `projection` / `pathDenyRules` / `nameSets` —— 守卫集策略不带,本包装也不报(报了就是替它声称一张它没有的拒表)。
191
+ * 修前(core 7.33.x)这里是一只 `{ check }` 字面量,7.34.0 起那只字面量把真身层的守卫剥掉了 —— 沙箱 lane 的 shell 写站
192
+ * 因此看不见部署守卫集(`cp x .env` 在 acceptEdits 下被引擎 place 并答掉)。
193
+ */
183
194
  export declare function scopedToWriteTargetForm(policy: ToolPolicy, form: WriteTargetForm): ToolPolicy;
184
195
  export declare class RelativeTargetLexicalEnv extends StubExecutionEnv {
185
196
  /** 相对形 → `/<词法归一>`;绝对形 / 空串 / 含 NUL 一律报错(fail-closed 守底:`relativeTargetsOnly` 已把这些形挡在外面,
@@ -26,6 +26,7 @@ function writeTargetFormOf(req) {
26
26
  return "absolute-or-stamped";
27
27
  return "unstamped-relative";
28
28
  }
29
+ const WRITE_TARGET_GUARD_DOMAIN = "absolute-or-stamped";
29
30
  export function scopedToWriteTargetForm(policy, form) {
30
31
  return {
31
32
  async check(req, signal) {
@@ -34,6 +35,7 @@ export function scopedToWriteTargetForm(policy, form) {
34
35
  return { action: "allow" };
35
36
  return policy.check(req, signal);
36
37
  },
38
+ ...(form === WRITE_TARGET_GUARD_DOMAIN && policy.writeTargetGuard !== undefined ? { writeTargetGuard: policy.writeTargetGuard } : {}),
37
39
  };
38
40
  }
39
41
  export class RelativeTargetLexicalEnv extends StubExecutionEnv {
@@ -31,8 +31,19 @@ export interface ExecutionLaneCaps {
31
31
  /** SendUserFile 的**沙箱直传腿**(host/in-process 的本机 fs 源是另一条腿,不是本位)。
32
32
  * 判别式:`capabilities/sandbox-file-send.ts` 的 `sandboxSendLaneEnabled`。 */
33
33
  readonly sandboxSendUserFile: LaneCapability;
34
- /** OS 级隔离边界。判别式:`boot/execution-env.ts` 的 `isolated`(经 `remote_exec_enabled` 日志可观测)。 */
35
- readonly isolation: LaneCapability;
34
+ /**
35
+ * OS 级隔离边界(手跑在容器 / microVM / Kata pod 里,而不是一台真机上)。
36
+ * 🔴 S-281 起**本位即判别式**:全仓读它的唯一布尔口是 {@link laneIsolated}(boot `remote_exec_enabled.isolated` 与
37
+ * `remote_exec_real_system` 那一行、`config.d/remote-exec.json` 抢占披露的「隔离降级」判、wire `executionLane.isolated`
38
+ * + 诊断面同位、那一行 note 列的沙箱出路 —— 改前 boot 另手写一份 `e2b || k8s || local-docker`,与本表两处各算)。
39
+ * 本位的**独立作者** = 各 adapter 对 core 的 `capabilities.isolation` 声明:对账测试拿 core 自己的严格读器 `isIsolated`
40
+ * 问每条车道的**真 adapter 实例**,与本表、与 boot 行三方对拍。
41
+ * 型上**没有** `single-user-only` 这一档(隔离是车道的结构事实,不随租户面变)⇒ {@link laneIsolated} 的布尔化是穷举、无兜底。
42
+ * ⚠️ 口径 = **OS 级边界**(容器即算:共享宿主内核的 `local-docker`、runc 上的 k8s pod 都答 supported),**不是**「独立内核」强度。
43
+ * 另一根轴 `router/route-orchestration.ts` 的 `isIsolatedExecEnv`(A 值路由,只认 e2b 与 kata / gvisor 运行时,runc / local-docker
44
+ * fail-closed 判否)问的是 VM 级强度 —— 两个不同的问题,不是本位的第二份;收成一只会让其中一边答错(S-281 收车档登记)。
45
+ */
46
+ readonly isolation: Exclude<LaneCapability, "single-user-only">;
36
47
  /** 可挂起(k8s 以配了 `s3Snapshot` 为前提 —— 「结构上能不能」的口径)。判别式:同上的 `suspendable`。 */
37
48
  readonly suspendable: LaneCapability;
38
49
  /** `/tmp/scratchpad/<sessionId>` 远端约定。判别式:`plugins/remote-scratchpad.ts` 的 `isRemoteScratchpadLane`。 */
@@ -155,6 +166,23 @@ export declare function executionLaneOf(provider: string | undefined): Execution
155
166
  * ③ `approval.ts` 的 `buildNoResponderBootWarns` —— 写门的 ask 只在「有手写本机盘」时才会发生。
156
167
  */
157
168
  export declare function handsOnWorkerFilePlane(lane: ExecutionLane): boolean;
169
+ /**
170
+ * S-281 —— 「这条车道的手在不在 OS 级隔离边界里」。**全仓唯一**的布尔判别式(读表 `isolation` 位,见该位头注的读者清单)。
171
+ * 穷举 switch、无 default 臂:`isolation` 的型只有两档,加档 = 编译红。
172
+ *
173
+ * ⚠️ 本函数对 `in-process` 也答(表上那一行是 unsupported ⇒ false)—— 那一格是「表」的完整性,不是 wire 的说法:
174
+ * 无手部署**没有执行边界可言**,wire 上 `executionLane.isolated` 按 present-iff 缺席(`capabilities/execution-lane.ts`),
175
+ * 不是 false。boot 行只在装出了执行环境工厂时才打,够不着那一格。
176
+ */
177
+ export declare function laneIsolated(lane: ExecutionLane): boolean;
178
+ /**
179
+ * S-281 —— 这条车道是不是**单机形缺省**落点(`REMOTE_EXEC` 未设 / 空串 ∧ `CONFIG_PROVIDER=local`,没人点名)。
180
+ * 判据 = 装配出的车道值自己带的记号 `singleMachineDefault`(唯一写者 = `config.ts` 三元链的隐式 host 尾臂;显式 `REMOTE_EXEC=host`
181
+ * 与 `config.d/remote-exec.json` 覆写都铸不带它的新值)。**唯一读口**,读者两处:`config.ts` 车道落点一行的 `source`
182
+ * (`exec_lane_selected`,文件覆写之前播)与 `boot/execution-env.ts` 的 `remote_exec_real_system.note`(覆写之后打)——
183
+ * 同一个记号、两个时刻,不另按 env 原文重判。
184
+ */
185
+ export declare function isSingleMachineDefaultLane(remoteExec: ServiceConfigFlat["remoteExec"]): boolean;
158
186
  /** 把一位能力在**具体部署**上落成布尔:租户面是唯一入参(旋钮由消费点自己再合取)。
159
187
  * 穷举 switch,无 default 臂 —— {@link LaneCapability} 加词而此处不加臂 = 编译红。 */
160
188
  export declare function laneCapabilityHolds(cap: LaneCapability, deployment: {
@@ -174,6 +174,18 @@ export function handsOnWorkerFilePlane(lane) {
174
174
  const caps = executionLaneCaps(lane);
175
175
  return caps.hostFilePlane === "supported" && caps.coreRemoteExecutionEnv === "supported";
176
176
  }
177
+ export function laneIsolated(lane) {
178
+ const isolation = executionLaneCaps(lane).isolation;
179
+ switch (isolation) {
180
+ case "supported":
181
+ return true;
182
+ case "unsupported":
183
+ return false;
184
+ }
185
+ }
186
+ export function isSingleMachineDefaultLane(remoteExec) {
187
+ return remoteExec?.provider === "host" && remoteExec.singleMachineDefault === true;
188
+ }
177
189
  export function laneCapabilityHolds(cap, deployment) {
178
190
  switch (cap) {
179
191
  case "supported":
@@ -1,9 +1,10 @@
1
1
  /**
2
2
  * Fleet worker-side wiring(announce/heartbeat + usage 批报;契约 = registry-core 0.1.48+
3
3
  * `/fleet`,center 端点 = sema-registry `/api/fleet/*`)。设计要点:
4
- * - **可选分层(D3)**:仅当 center 已配置(sema-registry lane 的 baseUrl+token+worker)∧ `FLEET_ADVERTISE_ADDRESS`
5
- * 显式声明时才启动 —— 不配 center 的部署零行为变化。address 不自猜(k8s pod IP / docker alias / 127.0.0.1:port
6
- * 只有部署环境自己知道,故用显式 env)。
4
+ * - **可选分层(D3),两条环各自有门(S-866 ④)**:客户端在中心车道配齐时启动(`centerLaneConfigured` —— baseUrl + 声明了
5
+ * 凭证 + worker,与租约管理器、用量累加器**同一只**谓词);**用量环**只要这三件;**公告环**(announce / 心跳 / 注销)另要
6
+ * `FLEET_ADVERTISE_ADDRESS` 显式声明(可达性是运维的话,不自猜:k8s pod IP / docker alias / 127.0.0.1:port 只有部署环境自己
7
+ * 知道)。不配中心的部署零行为变化;配了中心而不公告的引擎只报用量、不注册(7.105.0 及以前它连用量都不报)。
7
8
  * - **announce=注册=心跳同形状幂等 upsert**:boot 后(listen 之后)POST 一次 + 每 heartbeatMs 续。
8
9
  * `ready` / `draining` 两字段由**同一只**就绪谓词的同一次读铸(S-740:`http/readiness.ts` 的 `readinessOf`,
9
10
  * 与 `/health.ready`、`GET /readyz`、计费提交门同源):`ready = verdict.ready`(= roster 已落 ∧ ¬draining),
@@ -74,8 +75,9 @@ export interface FleetClientOpts {
74
75
  worker: string;
75
76
  };
76
77
  instanceId: string;
77
- /** Reachable base URL of THIS instance — the operator's explicit declaration (`FLEET_ADVERTISE_ADDRESS`). */
78
- advertiseAddress: string;
78
+ /** Reachable base URL of THIS instance — the operator's explicit declaration (`FLEET_ADVERTISE_ADDRESS`).
79
+ * S-866 ④:**缺席 = 公告环不装**(不注册、不心跳、不注销;`announceNow` 是空操作),用量环照装。 */
80
+ advertiseAddress?: string | undefined;
79
81
  version: string;
80
82
  /** S-740:就绪判(装配层接 `readinessOf(deployment)` —— HTTP 面读的同一个 deps 组)。每条公告现读**一次**,
81
83
  * `ready` 与 `draining` 两字段都由这一次读铸(不会撕裂成 `ready:true` + `draining:true`)。 */
@@ -104,22 +106,25 @@ export interface FleetClient {
104
106
  stop: () => Promise<void>;
105
107
  }
106
108
  /**
107
- * Start the worker-side fleet loops. Caller has already gated on "center configured ∧ advertise address set"
108
- * (startFleetClientFromEnv below) — this function assumes a complete opts.
109
+ * Start the worker-side fleet loops. Caller has already gated on the center lane (startFleetClientFromEnv below).
110
+ * The announce loop runs iff `advertiseAddress` is present; the usage loop iff `usage` is present (S-866 ④: the two
111
+ * loops no longer share one gate).
109
112
  */
110
113
  export declare function startFleetClient(opts: FleetClientOpts): FleetClient;
111
114
  /**
112
- * Env/config gate (D3 可选分层): start ONLY when the sema-registry lane is configured (baseUrl + a DECLARED credential
113
- * + worker — S-651: declared, not "currently readable": a token file that is unreadable right now still starts the
114
- * client, every request re-reads it)
115
- * AND the operator explicitly declared this instance's reachable address. Anything missing → undefined (zero
116
- * behavior change for non-fleet deployments); worker missing is warned when the rest is present (half-configured).
115
+ * Env/config gate (D3 可选分层;S-866 ④ 两条环各自有门): start when the center lane is configured —
116
+ * {@link centerLaneConfigured}(baseUrl + a DECLARED credential + worker — S-651: declared, not "currently readable": a token
117
+ * file that is unreadable right now still starts the client, every request re-reads it), the SAME predicate the lease
118
+ * manager and the usage accumulator use. The usage loop needs nothing more; the announce loop additionally needs the
119
+ * operator's explicit reachable address (`FLEET_ADVERTISE_ADDRESS`; a malformed one refuses boot). Lane not configured →
120
+ * undefined (zero behavior change for non-fleet deployments); an address set while only the worker is missing is warned
121
+ * (half-configured announce).
117
122
  */
118
123
  export declare function startFleetClientFromEnv(cfg: {
119
124
  configCenter?: (CenterEndpoint & {
120
125
  worker?: string;
121
126
  }) | undefined;
122
127
  }, opts: Omit<FleetClientOpts, "center" | "advertiseAddress"> & {
123
- advertiseAddress?: string;
128
+ advertiseAddress?: string | undefined;
124
129
  }): FleetClient | undefined;
125
130
  //# sourceMappingURL=fleet-client.d.ts.map
@@ -1,6 +1,6 @@
1
1
  import { InstanceAnnouncement, UsageReport, DEFAULT_HEARTBEAT_MS } from "@sema-agent/settings-schema/fleet";
2
2
  import { buildDisplaySafeUrl } from "./trace/redact.js";
3
- import { centerFetch } from "./config-center/center-request.js";
3
+ import { centerFetch, centerLaneConfigured } from "./config-center/center-request.js";
4
4
  export const FLEET_USAGE_KEY_CAP = 1024;
5
5
  export const FLEET_CLIENT_FETCH_TIMEOUT_MS = 5000;
6
6
  export class FleetUsageAccumulator {
@@ -81,8 +81,9 @@ export function startFleetClient(opts) {
81
81
  const send = (route, method, json) => centerFetch(opts.center, { kind: route }, { method, json, timeoutMs: FLEET_CLIENT_FETCH_TIMEOUT_MS, fetchImpl: f });
82
82
  let stopped = false;
83
83
  let failStreak = 0;
84
+ const address = opts.advertiseAddress;
84
85
  const announceOnce = async () => {
85
- if (stopped)
86
+ if (stopped || address === undefined)
86
87
  return;
87
88
  try {
88
89
  const verdict = opts.readiness();
@@ -91,7 +92,7 @@ export function startFleetClient(opts) {
91
92
  const body = {
92
93
  instanceId: opts.instanceId,
93
94
  worker: opts.center.worker,
94
- address: opts.advertiseAddress,
95
+ address,
95
96
  version: opts.version,
96
97
  ready: verdict.ready,
97
98
  draining: !verdict.ready && verdict.reasons.includes("draining"),
@@ -153,9 +154,11 @@ export function startFleetClient(opts) {
153
154
  opts.logger?.warn("fleet_usage_report_failed", { error: err instanceof Error ? err.message : String(err), entries: entries.length });
154
155
  }
155
156
  };
156
- void announceNow();
157
- const hb = setInterval(() => void announceNow(), heartbeatMs);
158
- hb.unref?.();
157
+ const announcing = address !== undefined;
158
+ if (announcing)
159
+ void announceNow();
160
+ const hb = announcing ? setInterval(() => void announceNow(), heartbeatMs) : undefined;
161
+ hb?.unref?.();
159
162
  const uw = opts.usage ? setInterval(() => void flushUsage(), usageWindowMs) : undefined;
160
163
  uw?.unref?.();
161
164
  let stopping = false;
@@ -163,13 +166,16 @@ export function startFleetClient(opts) {
163
166
  if (stopping)
164
167
  return;
165
168
  stopping = true;
166
- clearInterval(hb);
169
+ if (hb)
170
+ clearInterval(hb);
167
171
  if (uw)
168
172
  clearInterval(uw);
169
173
  while (inFlight !== undefined)
170
174
  await inFlight;
171
175
  stopped = true;
172
176
  await flushUsage();
177
+ if (!announcing)
178
+ return;
173
179
  try {
174
180
  await send("fleet-instances", "DELETE", { instanceId: opts.instanceId, worker: opts.center.worker });
175
181
  }
@@ -180,23 +186,27 @@ export function startFleetClient(opts) {
180
186
  }
181
187
  export function startFleetClientFromEnv(cfg, opts) {
182
188
  const cc = cfg.configCenter;
183
- const address = opts.advertiseAddress ?? process.env.FLEET_ADVERTISE_ADDRESS;
184
- if (!cc?.baseUrl || !cc.credential.declared || !address)
185
- return undefined;
186
- let parsed;
187
- try {
188
- parsed = new URL(address);
189
- }
190
- catch {
191
- parsed = undefined;
192
- }
193
- if (parsed === undefined || (parsed.protocol !== "http:" && parsed.protocol !== "https:")) {
194
- throw new Error(`FLEET_ADVERTISE_ADDRESS must be a parseable http(s) base URL (e.g. "http://10.0.0.5:8090"); got "${buildDisplaySafeUrl(address)}" — the fleet center rejects any other shape on every announce`);
189
+ const raw = opts.advertiseAddress ?? process.env.FLEET_ADVERTISE_ADDRESS;
190
+ const address = raw === undefined || raw === "" ? undefined : raw;
191
+ if (address !== undefined && cc?.baseUrl && cc.credential.declared) {
192
+ let parsed;
193
+ try {
194
+ parsed = new URL(address);
195
+ }
196
+ catch {
197
+ parsed = undefined;
198
+ }
199
+ if (parsed === undefined || (parsed.protocol !== "http:" && parsed.protocol !== "https:")) {
200
+ throw new Error(`FLEET_ADVERTISE_ADDRESS must be a parseable http(s) base URL (e.g. "http://10.0.0.5:8090"); got "${buildDisplaySafeUrl(address)}" — the fleet center rejects any other shape on every announce`);
201
+ }
195
202
  }
196
- if (!cc.worker) {
197
- opts.logger?.warn("fleet_client_no_worker_scope", { note: "FLEET_ADVERTISE_ADDRESS set but SEMA_REGISTRY_WORKER missing — announce needs the worker (dispatch key); fleet client not started" });
203
+ if (!centerLaneConfigured(cfg)) {
204
+ if (address !== undefined && cc?.baseUrl && cc.credential.declared) {
205
+ opts.logger?.warn("fleet_client_no_worker_scope", { note: "FLEET_ADVERTISE_ADDRESS set but SEMA_REGISTRY_WORKER missing — announce needs the worker (dispatch key); fleet client not started" });
206
+ }
198
207
  return undefined;
199
208
  }
200
- return startFleetClient({ ...opts, center: { baseUrl: cc.baseUrl, credential: cc.credential, worker: cc.worker }, advertiseAddress: address });
209
+ const lane = cfg.configCenter;
210
+ return startFleetClient({ ...opts, center: { baseUrl: lane.baseUrl, credential: lane.credential, worker: lane.worker }, advertiseAddress: address });
201
211
  }
202
212
  //# sourceMappingURL=fleet-client.js.map