@sema-agent/server 7.119.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 (127) hide show
  1. package/MIGRATION.md +129 -0
  2. package/USAGE.md +72 -23
  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 +37 -13
  8. package/dist/boot/engine-lease.d.ts +1 -1
  9. package/dist/boot/execution-env.js +3 -2
  10. package/dist/boot/fatal-exit.d.ts +7 -1
  11. package/dist/boot/fatal-exit.js +2 -1
  12. package/dist/boot/resolve-spec.js +1 -0
  13. package/dist/boot/runner-deps.d.ts +4 -3
  14. package/dist/boot/runner-deps.js +7 -3
  15. package/dist/boot/shutdown.d.ts +9 -10
  16. package/dist/boot/shutdown.js +54 -16
  17. package/dist/boot/stage-01-config.js +2 -0
  18. package/dist/boot/stage-09-leader.d.ts +2 -9
  19. package/dist/boot/stage-10-http-server.d.ts +1 -9
  20. package/dist/boot/stage-10-http-server.js +31 -8
  21. package/dist/capabilities/builtin-tools.d.ts +31 -8
  22. package/dist/capabilities/builtin-tools.js +22 -1
  23. package/dist/capabilities/composite-members.d.ts +1 -1
  24. package/dist/capabilities/composite-members.js +1 -1
  25. package/dist/capabilities/scenarios.d.ts +8 -6
  26. package/dist/capabilities/scenarios.js +3 -3
  27. package/dist/capabilities/send-user-file-tool.d.ts +6 -3
  28. package/dist/capabilities/send-user-file-tool.js +1 -1
  29. package/dist/config-catalog.js +13 -7
  30. package/dist/config-center/center-request.d.ts +16 -1
  31. package/dist/config-center/center-request.js +36 -5
  32. package/dist/config-lkg.d.ts +33 -5
  33. package/dist/config-lkg.js +55 -11
  34. package/dist/config-types.d.ts +46 -3
  35. package/dist/config-types.js +6 -0
  36. package/dist/config.d.ts +33 -4
  37. package/dist/config.js +107 -15
  38. package/dist/deployment-governance.d.ts +3 -1
  39. package/dist/deployment-governance.js +3 -1
  40. package/dist/device-store.d.ts +7 -0
  41. package/dist/device-store.js +4 -0
  42. package/dist/device-ws-hub.js +17 -9
  43. package/dist/device-ws-protocol.d.ts +15 -0
  44. package/dist/fleet/drain-entry.d.ts +87 -0
  45. package/dist/fleet/drain-entry.js +35 -0
  46. package/dist/fleet/engine-signal.d.ts +144 -0
  47. package/dist/fleet/engine-signal.js +60 -0
  48. package/dist/fleet/serving-lease.d.ts +47 -0
  49. package/dist/fleet/serving-lease.js +235 -0
  50. package/dist/fleet-client.d.ts +35 -19
  51. package/dist/fleet-client.js +49 -25
  52. package/dist/http/approval-sweeps.js +3 -5
  53. package/dist/http/dispatch.js +18 -13
  54. package/dist/http/health-self-report.d.ts +60 -0
  55. package/dist/http/health-self-report.js +53 -0
  56. package/dist/http/principal-gate.d.ts +8 -1
  57. package/dist/http/principal-gate.js +1 -0
  58. package/dist/http/readiness.d.ts +25 -5
  59. package/dist/http/readiness.js +31 -6
  60. package/dist/http/resume-legs.js +5 -1
  61. package/dist/http/route-ctx.d.ts +6 -4
  62. package/dist/http/route-table.d.ts +3 -0
  63. package/dist/http/route-table.js +4 -1
  64. package/dist/http/routes/a2a-serve.d.ts +1 -1
  65. package/dist/http/routes/a2a-serve.js +5 -5
  66. package/dist/http/routes/admin-drain.d.ts +12 -31
  67. package/dist/http/routes/admin-drain.js +15 -9
  68. package/dist/http/routes/approvals-assistant.d.ts +3 -3
  69. package/dist/http/routes/approvals-assistant.js +3 -3
  70. package/dist/http/routes/capabilities.js +3 -1
  71. package/dist/http/routes/devices.js +6 -5
  72. package/dist/http/routes/leader.d.ts +1 -1
  73. package/dist/http/routes/leader.js +1 -1
  74. package/dist/http/routes/memory-consolidation.d.ts +1 -1
  75. package/dist/http/routes/memory-consolidation.js +1 -1
  76. package/dist/http/routes/notify-wake.d.ts +1 -1
  77. package/dist/http/routes/notify-wake.js +1 -1
  78. package/dist/http/routes/rules.js +14 -6
  79. package/dist/http/routes/runs.d.ts +2 -2
  80. package/dist/http/routes/runs.js +2 -2
  81. package/dist/http/routes/side-query.d.ts +1 -1
  82. package/dist/http/routes/side-query.js +1 -1
  83. package/dist/http/routes/tasks.d.ts +2 -2
  84. package/dist/http/routes/tasks.js +3 -3
  85. package/dist/http/server.d.ts +7 -21
  86. package/dist/http/server.js +20 -1
  87. package/dist/http/wire-types.d.ts +7 -1
  88. package/dist/list-entry-display.d.ts +14 -0
  89. package/dist/list-entry-display.js +14 -0
  90. package/dist/model-reach-scope.d.ts +15 -3
  91. package/dist/model-reach-scope.js +3 -2
  92. package/dist/model-unreachable-notice.d.ts +2 -1
  93. package/dist/model-unreachable-notice.js +1 -0
  94. package/dist/namespaced-rule-name.js +2 -6
  95. package/dist/observability/fail-open.d.ts +20 -0
  96. package/dist/observability/fail-open.js +20 -0
  97. package/dist/operator-ask.d.ts +1 -1
  98. package/dist/plugins/one-time-migrations.d.ts +8 -0
  99. package/dist/plugins/one-time-migrations.js +72 -3
  100. package/dist/plugins/permission-rule-store-file.js +2 -6
  101. package/dist/plugins/permission-rule-store-sql.d.ts +65 -6
  102. package/dist/plugins/permission-rule-store-sql.js +166 -59
  103. package/dist/plugins/remote-env-device.d.ts +12 -1
  104. package/dist/plugins/remote-env-device.js +13 -6
  105. package/dist/plugins/scheduler-support.d.ts +8 -4
  106. package/dist/plugins/scheduler-support.js +5 -5
  107. package/dist/request-key-closure.js +1 -1
  108. package/dist/rule-entry-list.d.ts +0 -1
  109. package/dist/rule-entry-list.js +4 -9
  110. package/dist/rules-consent.d.ts +16 -3
  111. package/dist/rules-consent.js +8 -4
  112. package/dist/run-local.js +2 -0
  113. package/dist/runs.js +1 -1
  114. package/dist/sema-config-dir.d.ts +19 -0
  115. package/dist/sema-config-dir.js +33 -0
  116. package/dist/session-policy-wire.js +5 -4
  117. package/dist/spec-fields.d.ts +5 -4
  118. package/dist/task-settings.d.ts +3 -3
  119. package/dist/tool-approval.d.ts +7 -0
  120. package/dist/tool-approval.js +4 -1
  121. package/dist/trace/core-keyset-guard.d.ts +2 -2
  122. package/dist/trace/engine-notice-wire.d.ts +6 -4
  123. package/dist/trace/engine-notice-wire.js +16 -9
  124. package/dist/trace/self-minted-wire.d.ts +78 -0
  125. package/dist/trace/self-minted-wire.js +32 -0
  126. package/docs/DEVICE-EXECUTOR-PROTOCOL.md +56 -14
  127. package/package.json +3 -3
package/dist/config.js CHANGED
@@ -10,7 +10,10 @@ import { DEFAULT_ELICITATION_THROTTLE } from "./elicitation.js";
10
10
  import { isV2ScopeKey, memoryReplicationCoverage, replicationCovers } 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, 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, FLEET_LEASE_OUTAGE_MAX_DEFAULT_MS, fleetScopeOf } from "./config-types.js";
14
+ import { FLEET_GENERATION_ENV, FLEET_SCOPE_ENV, SERVING_LEASE_TTL_FLOOR_MS, isCanonicalGeneration } from "@sema-agent/settings-schema/fleet";
15
+ import { ScopeId } from "@sema-agent/settings-schema/api/scopes";
16
+ import { centerLaneConfigured, pathBaseUrlFlaw } from "./config-center/center-request.js";
14
17
  import { resolveLocalDataRoot } from "./local-data-root.js";
15
18
  import { buildWriteProtectionPosture } from "./write-protection.js";
16
19
  import { normalizeSshFingerprint } from "./ssh-host-key.js";
@@ -20,6 +23,8 @@ import {} from "./posture-source.js";
20
23
  import { CONTROL_AND_BIDI_STRIP_RE } from "./text-bidi.js";
21
24
  import { describeUnmatchableRuleName, ruleEntryToolName, unmatchableRuleNameOf } from "./namespaced-rule-name.js";
22
25
  import { bakeOperatorPrincipalsRefusal, bakeRunnerCredentialRefusal, halfOpenDirectDoorRefusal, leaderDurableStoreRefusal, operatorPrincipalsRequiredRefusal } from "./config-invariants.js";
26
+ import { declaredSemaConfigDir } from "./sema-config-dir.js";
27
+ import { lkgPathOverrideOf } from "./config-lkg.js";
23
28
  import { REMOTE_EXEC_PROVIDER_WORDS, executionLaneOf, executionLaneRequiredRefusal, isSingleMachineDefaultLane, laneRequiredEnv } from "./execution-lane-caps.js";
24
29
  import { MEMORY_ENGINE_BACKENDS, SESSION_BACKENDS, STORE_BACKENDS } from "@sema-agent/settings-schema/execution-lanes";
25
30
  import { AUTONOMY_MODES } from "./runtime-governance.js";
@@ -29,6 +34,7 @@ import { parseCenterCredentialRenewLeadMs } from "./center-credential-renewer.js
29
34
  import { modelProviderRefusal } from "./model-provider.js";
30
35
  import { modelRouteUrlRefusal } from "./model-route-url.js";
31
36
  import { modelRouteEndpointRefusal } from "./model-route-endpoint.js";
37
+ import { displayListEntry } from "./list-entry-display.js";
32
38
  import { buildDisplaySafeUrl, redactSecrets } from "./trace/redact.js";
33
39
  import { allowlistedEnv } from "./server-secret-env.js";
34
40
  import { ENV_NAME_ALLOWLIST_KNOBS, ENV_NAME_ALLOWLIST_KNOB_NAMES, describeAllowlistPostures } from "./env-name-allowlist-knobs.js";
@@ -485,7 +491,6 @@ function boolEnv(name, def) {
485
491
  registerKnob({ name, polarity: def ? "opt-out" : "opt-in", value, source });
486
492
  return value;
487
493
  }
488
- export const configLkgEnabled = () => boolEnv("CONFIG_LKG_ENABLED", true);
489
494
  export const hostBackgroundShellEnabled = () => boolEnv("HOST_BG_ENABLED", true);
490
495
  export const hostExecSpoolEnabled = () => boolEnv("HOST_EXEC_SPOOL_ENABLED", true);
491
496
  function parseReadDenyPatternsEnv(raw) {
@@ -1209,17 +1214,11 @@ function parseDegradeOn(words) {
1209
1214
  }
1210
1215
  return words;
1211
1216
  }
1212
- const PRESENTED_NAME_MAX = 160;
1213
- function presentToolName(name) {
1214
- const clean = name.replace(CONTROL_AND_BIDI_STRIP_RE, "");
1215
- return clean.length <= PRESENTED_NAME_MAX ? clean : `${clean.slice(0, PRESENTED_NAME_MAX)}…(${name.length} chars)`;
1216
- }
1217
1217
  function ccRuleFormGuidance(name) {
1218
1218
  if (!name.includes("(") && !name.includes(")"))
1219
1219
  return undefined;
1220
- const shown = presentToolName(name);
1221
- return (`CC-form rule specifier — this list matches the WHOLE tool name verbatim, so it can only ever match a tool literally named "${shown}". ` +
1222
- `Drop the parentheses to gate the tool itself ("${presentToolName(name.split("(")[0] ?? name)}"); ` +
1220
+ return (`CC-form rule specifier — this list matches the WHOLE tool name verbatim, so it can only ever match a tool literally named ${displayListEntry(name)}. ` +
1221
+ `Drop the parentheses to gate the tool itself (${displayListEntry(name.split("(")[0] ?? name)}); ` +
1223
1222
  `to decide per COMMAND use \`runtime.commandPolicy\` ({command, decision} — bare argv[0] names); ` +
1224
1223
  `to put every shell call behind an approval use MANUAL_MODE_SHELL_GATE=always`);
1225
1224
  }
@@ -1264,7 +1263,7 @@ export function unmatchableToolNamesRefusal(source, names, judgment) {
1264
1263
  return undefined;
1265
1264
  const message = (`${source} has ${bad.length === 1 ? "an entry" : "entries"} that can never take effect — ${bad.length === 1 ? "it" : "each"} would never match a live tool or cannot be compiled into a rule ` +
1266
1265
  `(names are read exactly as written: no alias folding, no protocol prefix added): ` +
1267
- bad.map((b) => `${presentToolName(b.name)} → ${b.guidance}`).join("; ")).replace(CONTROL_AND_BIDI_STRIP_RE, "");
1266
+ bad.map((b) => `${displayListEntry(b.name)} → ${b.guidance}`).join("; ")).replace(CONTROL_AND_BIDI_STRIP_RE, "");
1268
1267
  return { message, names: bad.map((b) => b.name) };
1269
1268
  }
1270
1269
  const APPROVAL_GRANT_ENV = "APPROVAL_GRANT";
@@ -1856,7 +1855,6 @@ function parseOrchestrationDomain(ctx) {
1856
1855
  const workflowSizeGuideline = workflowSizeGuidelineRaw;
1857
1856
  const usageWindows = parseUsageWindows(process.env.USAGE_WINDOWS);
1858
1857
  const bakeRunnerPrincipal = env("BAKE_RUNNER_PRINCIPAL", "system:bake-runner");
1859
- configLkgEnabled();
1860
1858
  hostBackgroundShellEnabled();
1861
1859
  hostExecSpoolEnabled();
1862
1860
  const lspHostEnabled = (() => {
@@ -1899,6 +1897,9 @@ function parseOrchestrationDomain(ctx) {
1899
1897
  assertReadDenyBuiltinsResolvable(tiers, exclude);
1900
1898
  return { ...(tiers !== undefined ? { readDenyBuiltinTiers: tiers } : {}), ...(exclude !== undefined ? { readDenyBuiltinExclude: exclude } : {}) };
1901
1899
  })(),
1900
+ ...((dir) => (dir !== undefined ? { agentConfigDir: dir } : {}))(declaredSemaConfigDir()),
1901
+ ...((p) => (p !== undefined ? { configLkgPath: p } : {}))(lkgPathOverrideOf(process.env.CONFIG_LKG_PATH)),
1902
+ configLkgEnabled: boolEnv("CONFIG_LKG_ENABLED", true),
1902
1903
  ...((h) => (h !== undefined ? { remoteExecHomeDir: h } : {}))((() => {
1903
1904
  const raw = process.env.REMOTE_EXEC_HOME_DIR;
1904
1905
  if (raw === undefined || raw === "")
@@ -2187,6 +2188,13 @@ function parseGitApiKind(raw, baseUrl) {
2187
2188
  }
2188
2189
  function parseIntegrationsDomain(ctx) {
2189
2190
  const { requirePrincipal, configProvider } = ctx;
2191
+ const registryUrl = parseCenterBaseUrl("SEMA_REGISTRY_URL", process.env.SEMA_REGISTRY_URL, `It is the center base every config pull, fleet and lease request is sent to; setting it selects the center as the config source.`);
2192
+ const registryLiveUrl = parseCenterBaseUrl("SEMA_REGISTRY_LIVE_URL", process.env.SEMA_REGISTRY_LIVE_URL, `It is the center base the fleet liveness endpoints (heartbeat, serving lease) are sent to; unset it to send them to SEMA_REGISTRY_URL.`);
2193
+ const registryWebUrl = parseCenterBaseUrl("SEMA_REGISTRY_WEB_URL", process.env.SEMA_REGISTRY_WEB_URL, `It is the center's web console base this engine reports on GET /health (center.webBaseUrl); unset it if the center has no web console to link to.`);
2194
+ const configCenter = registryUrl !== undefined ? parseConfigCenter(registryUrl, requirePrincipal, registryLiveUrl, registryWebUrl) : undefined;
2195
+ const fleetGeneration = parseFleetGeneration(process.env.FLEET_GENERATION);
2196
+ const fleetAdvertiseAddress = parseFleetAdvertiseAddress(process.env.FLEET_ADVERTISE_ADDRESS);
2197
+ fleetFenceLaneOf({ ...fleetGeneration, configCenter, ...fleetAdvertiseAddress });
2190
2198
  return {
2191
2199
  pluginsAllowHosts: (process.env.PLUGINS_ALLOW_HOSTS ?? "github.com").split(",").map((h) => h.trim()).filter(Boolean),
2192
2200
  configBootFetchBudgetMs: clampEnvWithWarn("CONFIG_BOOT_FETCH_BUDGET_MS", "1500", 0),
@@ -2196,24 +2204,107 @@ function parseIntegrationsDomain(ctx) {
2196
2204
  defaultScenario: env("DEFAULT_SCENARIO", "code"),
2197
2205
  skillsDir: env("SKILLS_DIR", defaultSkillsDir()),
2198
2206
  centerCredentialRenewLeadMs: parseCenterCredentialRenewLeadMs(process.env.CENTER_CREDENTIAL_RENEW_LEAD_MS),
2199
- configCenter: process.env.SEMA_REGISTRY_URL ? parseConfigCenter(env("SEMA_REGISTRY_URL"), requirePrincipal) : undefined,
2207
+ configCenter,
2200
2208
  fleetLeaseFailClosed: parseFleetLeaseFailClosed(process.env.FLEET_LEASE_FAIL_CLOSED, requirePrincipal),
2209
+ ...fleetGeneration,
2210
+ fleetScope: parseFleetScope(process.env.FLEET_SCOPE),
2211
+ ...fleetAdvertiseAddress,
2212
+ fleetLeaseOutageMaxMs: parseFleetLeaseOutageMaxMs(process.env.FLEET_LEASE_OUTAGE_MAX_MS),
2201
2213
  ...(configProvider !== undefined ? { configProvider } : {}),
2202
2214
  configLocalDir: process.env.CONFIG_LOCAL_DIR || undefined,
2203
2215
  a2aServe: parseA2aServe(),
2204
2216
  };
2205
2217
  }
2218
+ function parseFleetGeneration(raw) {
2219
+ const name = "FLEET_GENERATION";
2220
+ if (raw === undefined)
2221
+ return {};
2222
+ const shown = raw.length > 40 ? `${raw.slice(0, 40)}…(${raw.length} chars)` : raw;
2223
+ if (!isCanonicalGeneration(raw)) {
2224
+ throw fleetEnvRefusal("generation_invalid", `env ${name}=${JSON.stringify(shown)} is not a generation: it must be a canonical non-negative decimal string ("0", "7", "10" — no leading zero, ` +
2225
+ `sign, whitespace or fraction, and not empty). The config renderer sets it on fleet members; unset it entirely for a standalone engine. ` +
2226
+ `Refusing to boot rather than running outside the serving-lease fence.`);
2227
+ }
2228
+ return { fleetGeneration: raw };
2229
+ }
2230
+ export const FLEET_ENV_REFUSAL_CODES = {
2231
+ generation_invalid: "config.fleet_generation_invalid",
2232
+ scope_invalid: "config.fleet_scope_invalid",
2233
+ advertise_address_invalid: "config.fleet_advertise_address_invalid",
2234
+ member_unfenced: "config.fleet_member_unfenced",
2235
+ };
2236
+ function fleetEnvRefusal(rule, message) {
2237
+ return Object.assign(new Error(message), { code: FLEET_ENV_REFUSAL_CODES[rule] });
2238
+ }
2239
+ function parseFleetScope(raw) {
2240
+ const name = "FLEET_SCOPE";
2241
+ if (raw !== undefined && !ScopeId.safeParse(raw).success) {
2242
+ const shown = raw.length > 40 ? `${raw.slice(0, 40)}…(${raw.length} chars)` : raw;
2243
+ throw fleetEnvRefusal("scope_invalid", `env ${name}=${JSON.stringify(shown)} is not a scope id: it must be a lowercase slug of letters, digits and inner dashes, 1–64 characters, ` +
2244
+ `not empty. The config renderer sets it on engines that serve a tenant scope; unset it for the default scope ("${fleetScopeOf(undefined)}"). ` +
2245
+ `Refusing to boot rather than answering drain requests under the wrong identity.`);
2246
+ }
2247
+ return fleetScopeOf(raw);
2248
+ }
2249
+ function parseFleetAdvertiseAddress(raw) {
2250
+ if (raw === undefined || raw === "")
2251
+ return {};
2252
+ const why = pathBaseUrlFlaw(raw);
2253
+ if (why !== undefined) {
2254
+ throw fleetEnvRefusal("advertise_address_invalid", `env FLEET_ADVERTISE_ADDRESS ${why} (an http(s) base URL, e.g. "http://10.0.0.5:8090"); got ${JSON.stringify(buildDisplaySafeUrl(raw))} — ` +
2255
+ `the fleet center rejects any other shape on every announce. Unset it if this engine does not announce itself.`);
2256
+ }
2257
+ return { fleetAdvertiseAddress: raw };
2258
+ }
2259
+ export function fleetFenceLaneOf(cfg) {
2260
+ const generation = cfg.fleetGeneration;
2261
+ if (generation === undefined)
2262
+ return undefined;
2263
+ const laneConfigured = centerLaneConfigured(cfg);
2264
+ if (laneConfigured && cfg.fleetAdvertiseAddress !== undefined)
2265
+ return { center: cfg.configCenter, generation };
2266
+ const missing = [
2267
+ ...(laneConfigured ? [] : [`the center lane (SEMA_REGISTRY_URL + ${CENTER_CREDENTIAL_ENV.token} or ${CENTER_CREDENTIAL_ENV.tokenFile} + SEMA_REGISTRY_WORKER)`]),
2268
+ ...(cfg.fleetAdvertiseAddress === undefined ? ["the announce address (FLEET_ADVERTISE_ADDRESS)"] : []),
2269
+ ];
2270
+ throw fleetEnvRefusal("member_unfenced", `fleet member cannot be fenced — env FLEET_GENERATION=${JSON.stringify(generation)} makes this engine a fleet member, ` +
2271
+ `but ${missing.join(" and ")} ${missing.length === 1 ? "is" : "are"} not configured. A member renews its serving lease with the center and hears whether ` +
2272
+ `the center enforces the lease only in the heartbeat reply (the heartbeat needs the announce address); without them it would serve outside the fence. ` +
2273
+ `Configure them, or unset FLEET_GENERATION for a standalone engine.`);
2274
+ }
2275
+ function parseFleetLeaseOutageMaxMs(raw) {
2276
+ if (raw === undefined || raw === "")
2277
+ return FLEET_LEASE_OUTAGE_MAX_DEFAULT_MS;
2278
+ const n = /^(0|[1-9]\d*)$/.test(raw) ? Number(raw) : Number.NaN;
2279
+ if (!Number.isSafeInteger(n) || n < SERVING_LEASE_TTL_FLOOR_MS) {
2280
+ const shown = raw.length > 40 ? `${raw.slice(0, 40)}…(${raw.length} chars)` : raw;
2281
+ throw new Error(`env FLEET_LEASE_OUTAGE_MAX_MS=${JSON.stringify(shown)} must be a whole number of milliseconds ≥ ${SERVING_LEASE_TTL_FLOOR_MS} (the serving-lease TTL floor; ` +
2282
+ `decimal digits only; unset = ${FLEET_LEASE_OUTAGE_MAX_DEFAULT_MS}, 24 h). It bounds how long this engine keeps taking new submissions after its serving ` +
2283
+ `lease expired with the center unreachable — "0" and "infinite" are not values (the tolerance must be bounded and non-trivial). Refusing to boot.`);
2284
+ }
2285
+ return n;
2286
+ }
2206
2287
  function parseFleetLeaseFailClosed(raw, requirePrincipal) {
2207
2288
  const explicit = parseBoolWord("FLEET_LEASE_FAIL_CLOSED", raw);
2208
2289
  const value = explicit ?? requirePrincipal;
2209
2290
  registerKnob({ name: "FLEET_LEASE_FAIL_CLOSED", polarity: "posture", value, source: explicit !== undefined ? "env" : requirePrincipal ? "posture" : "default" });
2210
2291
  return value;
2211
2292
  }
2212
- function parseConfigCenter(baseUrl, requirePrincipal) {
2293
+ function parseCenterBaseUrl(name, raw, role) {
2294
+ if (raw === undefined || raw === "")
2295
+ return undefined;
2296
+ const why = pathBaseUrlFlaw(raw);
2297
+ if (why !== undefined)
2298
+ throw new Error(`env ${name} ${why}; got ${JSON.stringify(buildDisplaySafeUrl(raw))}. ${role}`);
2299
+ return raw;
2300
+ }
2301
+ function parseConfigCenter(baseUrl, requirePrincipal, liveBaseUrl, webBaseUrl) {
2213
2302
  const worker = process.env.SEMA_REGISTRY_WORKER || undefined;
2214
2303
  const declared = readCenterCredentialDeclaration();
2215
2304
  return {
2216
2305
  baseUrl,
2306
+ ...(liveBaseUrl !== undefined ? { liveBaseUrl } : {}),
2307
+ ...(webBaseUrl !== undefined ? { webBaseUrl } : {}),
2217
2308
  credential: createCenterCredentialSource(declared.from === "file" ? { ...declared, multiTenant: requirePrincipal } : declared, worker),
2218
2309
  dryRun: boolEnv("SEMA_REGISTRY_DRY_RUN", false),
2219
2310
  worker,
@@ -2337,6 +2428,7 @@ const ORCHESTRATION_GROUP_KEYS = [
2337
2428
  "backgroundAgentParkClaimStaleMs", "delegationEntryCaps", "rosterRetentionMs", "leaderRunStaleMs", "scratchpadSweepTtlMs", "sandboxPkgSource",
2338
2429
  "sessionAutoTitle", "sessionAutoTitleSource", "selectEnvironmentTool", "envFactsEnabled", "toolDeferLongtail", "lspEnabled", "lspHostEnabled", "lspEnvAllow",
2339
2430
  "imageBakes", "readFace", "readFaceSource", "taskWriteFaceEscalation", "readDenyPatterns", "readDenyBuiltinTiers", "readDenyBuiltinExclude",
2431
+ "agentConfigDir", "configLkgPath", "configLkgEnabled",
2340
2432
  ];
2341
2433
  const LIMITS_HTTP_GROUP_KEYS = [
2342
2434
  "port", "attachmentOrphanGraceMs", "workspaceFileMaxBytes", "attachmentMaxBytes", "attachmentMimeAllowlist",
@@ -2348,7 +2440,7 @@ const LIMITS_HTTP_GROUP_KEYS = [
2348
2440
  const OBSERVABILITY_GROUP_KEYS = ["metricsToken", "traceToken", "toolTrace", "traceThinking", "logLevel", "otel"];
2349
2441
  const INTEGRATIONS_GROUP_KEYS = [
2350
2442
  "pluginsAllowHosts", "mcpServers", "a2aPeers", "a2aServe", "configBootFetchBudgetMs", "gitApiBaseUrl", "gitApiToken", "gitApiKind", "defaultScenario",
2351
- "skillsDir", "centerCredentialRenewLeadMs", "configCenter", "fleetLeaseFailClosed", "configProvider", "configLocalDir",
2443
+ "skillsDir", "centerCredentialRenewLeadMs", "configCenter", "fleetLeaseFailClosed", "fleetGeneration", "fleetScope", "fleetAdvertiseAddress", "fleetLeaseOutageMaxMs", "configProvider", "configLocalDir",
2352
2444
  ];
2353
2445
  export const CONFIG_GROUP_KEYS = {
2354
2446
  store: STORE_GROUP_KEYS,
@@ -11,6 +11,8 @@ export interface DeploymentGovernanceConfigView {
11
11
  readonly commandPolicy?: ServiceConfig["commandPolicy"];
12
12
  readonly manualModeShellGate?: ServiceConfig["manualModeShellGate"];
13
13
  readonly sensitiveWritePatterns: ServiceConfig["sensitiveWritePatterns"];
14
+ /** core 7.46.0 #1419:部署声明的配置根 —— 守卫集的 `.sema/…` 模式在根下同样守(与 Runner 的同名座同一个值,`loadConfig` 铸)。 */
15
+ readonly agentConfigDir?: ServiceConfig["agentConfigDir"];
14
16
  /**
15
17
  * 本部署的**本地店根**(`LOCAL_DATA_ROOT → CONFIG_LOCAL_DIR → AGENT_DATA_DIR → ~/.ai-agent`)—— 在本口只喂
16
18
  * **誊本门**(`<localDataRoot>/sessions`)。S-133 起它**不再**当守卫集的 `dataRoot`(那是引擎记忆根,见
@@ -185,7 +187,7 @@ type WriteTargetForm = "unstamped-relative" | "absolute-or-stamped";
185
187
  * · `writeTargetGuard`(守卫集策略自带,shell 写站问「这个绝对目标守不守」)—— 只在本层的形**覆盖守卫的入参形**时带上
186
188
  * ({@link WRITE_TARGET_GUARD_DOMAIN}):真身层带;词法层不带(它的 env 对绝对形报错 ⇒ core 的「判不了就守」会把每一次
187
189
  * shell 写都判成受守,正与两层射程互补的原设计相反)。这不是给某一形开的特例,是同一条「成员描述它守得住的那部分」的读法;
188
- * · `projection` / `pathDenyRules` / `nameSets` —— 守卫集策略不带,本包装也不报(报了就是替它声称一张它没有的拒表)。
190
+ * · `projection` / `pathRules` / `nameSets` —— 守卫集策略不带,本包装也不报(报了就是替它声称一张它没有的拒表)。
189
191
  * 修前(core 7.33.x)这里是一只 `{ check }` 字面量,7.34.0 起那只字面量把真身层的守卫剥掉了 —— 沙箱 lane 的 shell 写站
190
192
  * 因此看不见部署守卫集(`cp x .env` 在 acceptEdits 下被引擎 place 并答掉)。
191
193
  */
@@ -87,18 +87,20 @@ export function createDeploymentGovernanceInputs(config, pathAdjudication, roots
87
87
  const sensitivePathLayers = config.sensitiveWritePatterns.length > 0
88
88
  ? (() => {
89
89
  const dataRootOpt = roots.engineDataRoot !== undefined ? { dataRoot: roots.engineDataRoot } : {};
90
+ const configRootOpt = config.agentConfigDir !== undefined ? { agentConfigDir: config.agentConfigDir } : {};
90
91
  const realTarget = createSensitivePathPolicy({
91
92
  env: pathAdjudication.env,
92
93
  patterns: config.sensitiveWritePatterns,
93
94
  ...(pathAdjudication.cwd !== undefined ? { rootPath: pathAdjudication.cwd } : {}),
94
95
  ...dataRootOpt,
96
+ ...configRootOpt,
95
97
  });
96
98
  const transcriptGuard = config.localDataRoot !== undefined ? [createTranscriptIntegrityPolicy({ sessionsDirs: [join(config.localDataRoot, "sessions")] })] : [];
97
99
  if (pathAdjudication.cwd !== undefined)
98
100
  return [realTarget, ...transcriptGuard];
99
101
  return [
100
102
  scopedToWriteTargetForm(realTarget, "absolute-or-stamped"),
101
- scopedToWriteTargetForm(createSensitivePathPolicy({ env: new RelativeTargetLexicalEnv(), patterns: config.sensitiveWritePatterns, ...dataRootOpt }), "unstamped-relative"),
103
+ scopedToWriteTargetForm(createSensitivePathPolicy({ env: new RelativeTargetLexicalEnv(), patterns: config.sensitiveWritePatterns, ...dataRootOpt, ...configRootOpt }), "unstamped-relative"),
102
104
  ...transcriptGuard,
103
105
  ];
104
106
  })()
@@ -161,6 +161,13 @@ export declare const DEVICE_TEXT_LIMITS: {
161
161
  readonly connEpoch: 64;
162
162
  readonly replicaId: 128;
163
163
  };
164
+ /**
165
+ * 设备侧路径「是不是绝对路径」的**唯一判据**(v1 设备都是 POSIX:`platformOs` 闭集 `darwin` | `linux`;用 `posix` 而不是 `node:path`,
166
+ * server 跑在什么平台上与设备路径语义无关)。两处读它、不写第二份:入籍口(`http/routes/devices.ts`,`workspaceRoot` 不过 ⇒ 400)与
167
+ * 铸 env(`plugins/remote-env-device.ts` `effectiveDeviceCwd`,有效工作目录不过 ⇒ 响亮拒;那一道守的是入籍口收紧之前落库的存量行与
168
+ * 会话 / 委派链来的 cwd)。
169
+ */
170
+ export declare function isDeviceAbsolutePath(p: string): boolean;
164
171
  export declare function assertDeviceOwnerShape(owner: DeviceOwner): void;
165
172
  export declare function assertDeviceIdShape(deviceId: string): void;
166
173
  export declare function assertRootSessionIdShape(rootSessionId: string): void;
@@ -1,4 +1,5 @@
1
1
  import { createHash, randomBytes, randomInt } from "node:crypto";
2
+ import { posix } from "node:path";
2
3
  export const DEVICE_ERROR_CODES = {
3
4
  NOT_BOUND: "device.not_bound",
4
5
  OFFLINE: "device.offline",
@@ -86,6 +87,9 @@ export const DEVICE_TEXT_LIMITS = {
86
87
  connEpoch: 64,
87
88
  replicaId: 128,
88
89
  };
90
+ export function isDeviceAbsolutePath(p) {
91
+ return posix.isAbsolute(p);
92
+ }
89
93
  export function assertDeviceOwnerShape(owner) {
90
94
  assertIdentityKey("owner.tenant", owner.tenant, DEVICE_KEY_MAX_BYTES);
91
95
  assertIdentityKey("owner.subject", owner.subject, DEVICE_KEY_MAX_BYTES);
@@ -425,7 +425,20 @@ export function createDeviceWsHub(options) {
425
425
  clearTimeout(conn.handshakeTimer);
426
426
  conn.handshakeTimer = null;
427
427
  releasePreAuthSlot(conn);
428
- sendServerFrame(conn, { t: "helloAck", ok: true, heartbeatIntervalMs, maxInflight: maxInflightPerDevice, gen: conn.genStr, pendingInstructions: pendingIds, chunkWatermarks });
428
+ const helloAck = {
429
+ t: "helloAck",
430
+ ok: true,
431
+ heartbeatIntervalMs,
432
+ maxInflight: maxInflightPerDevice,
433
+ gen: conn.genStr,
434
+ pendingInstructions: pendingIds,
435
+ chunkWatermarks,
436
+ reconnectGraceMs,
437
+ cancelGraceMs,
438
+ chunkGapTimeoutMs,
439
+ executionCeilingGraceMs: execGraceMs,
440
+ };
441
+ sendServerFrame(conn, helloAck);
429
442
  logger?.info?.("device_connected", { deviceId: row.deviceId, generation: acquired.generation, epoch: frame.epoch, replicaId });
430
443
  metrics?.inc("device_connected_total");
431
444
  }
@@ -469,12 +482,7 @@ export function createDeviceWsHub(options) {
469
482
  }
470
483
  }
471
484
  function failOnChunkGap(p) {
472
- const missing = p.nextStreamSeq;
473
- settle(p, {
474
- ok: false,
475
- errorCode: "outcome_unknown",
476
- message: `the output stream had a gap (missing chunk streamSeq ${missing}) so the result cannot be reported faithfully — ${OUTCOME_UNKNOWN_SENTENCE}`,
477
- });
485
+ settle(p, outcomeUnknown(`the output stream had a gap (missing chunk streamSeq ${p.nextStreamSeq}) so the result cannot be reported faithfully`));
478
486
  }
479
487
  function chunkPayloadBytes(frame) {
480
488
  return frame.dataB64 === undefined ? 0 : Math.floor((frame.dataB64.length * 3) / 4);
@@ -549,11 +557,11 @@ export function createDeviceWsHub(options) {
549
557
  if (complaint !== null) {
550
558
  metrics?.inc("device_stream_terminator_missing_total");
551
559
  sendServerFrame(conn, { t: "resultAck", gen: conn.genStr, instructionId: p.instructionId });
552
- settle(p, { ok: false, errorCode: "outcome_unknown", message: `${complaint} — ${OUTCOME_UNKNOWN_SENTENCE}` });
560
+ settle(p, outcomeUnknown(complaint));
553
561
  return;
554
562
  }
555
563
  sendServerFrame(conn, { t: "resultAck", gen: conn.genStr, instructionId: p.instructionId });
556
- settle(p, outcome);
564
+ settle(p, !outcome.ok && outcome.errorCode === "outcome_unknown" ? outcomeUnknown(outcome.message) : outcome);
557
565
  }
558
566
  function onDeviceText(conn, text) {
559
567
  conn.lastFrameAt = Date.now();
@@ -235,6 +235,21 @@ export type DeviceHelloAckFrame = {
235
235
  gen: string;
236
236
  pendingInstructions: string[];
237
237
  chunkWatermarks: Record<string, number>;
238
+ /**
239
+ * 四个**宽限位**(additive,协议 §5.6 不升版):本连接所在 server 副本**此刻生效**的四只预算,值与汇聚端
240
+ * 计时器读的是同一只常量 —— 不是文档里的「默认值」,是这台部署的实际值。executor 据此定自己的重连
241
+ * 退避上界 / 取消应答时限 / 补片节奏 / 执行兜底预算,不必抄文档里的数。
242
+ * · `reconnectGraceMs`:断连后在途不判丢的窗(R4;运维旋钮 `DEVICE_RECONNECT_GRACE_MS`);
243
+ * · `cancelGraceMs`:`cancel` 送达后等终局回执的窗(R11);
244
+ * · `chunkGapTimeoutMs`:等缺失 `streamSeq` 的上限(R8);
245
+ * · `executionCeilingGraceMs`:`timeoutMs` 之后云侧兜底再等的宽限(R12:`timeoutMs` + 本值)。
246
+ * **可选位**:7.120+ 服务端恒发;缺席 = 老服务端,按协议 §1.5 回落文档缺省(不得当 0)。型上标可选,是为了让拿这只型读老服务端帧的
247
+ * 消费方被编译器逼着处理缺席;本服务自己的铸点按 `Required<DeviceHelloAckFrame>` 构造(`device-ws-hub.ts`),漏一位编译红。
248
+ */
249
+ reconnectGraceMs?: number;
250
+ cancelGraceMs?: number;
251
+ chunkGapTimeoutMs?: number;
252
+ executionCeilingGraceMs?: number;
238
253
  };
239
254
  export type DeviceHelloRejectFrame = {
240
255
  t: "helloReject";
@@ -0,0 +1,87 @@
1
+ /**
2
+ * 排空的**一只入口**(编排器长远稿片 2 件 2;契约型 = settings-schema 8.1.0 `DrainRequest` / `DrainReceipt` / `EngineDraining`)。
3
+ *
4
+ * ## 规则
5
+ *
6
+ * 1. **一只入口,两条车道**:进入排空只有 `boot/shutdown.ts` 的 `enterDrain`(装在 {@link DrainState.enter} 上)一处。Docker 车道 / 中心直调
7
+ * 经 `POST /v1/admin/drain`(体 = `DrainRequest`);Kubernetes 车道经服务租约被拒(`fleet/serving-lease.ts`,`actionId = lease:<代际>`);
8
+ * 非编排路径 = 停止信号(SIGTERM)/ SIGHUP 升级 / 附着租约自退,动作号由引擎自铸({@link ENGINE_DRAIN_CAUSES})。全部汇到同一只入口。
9
+ * 2. **期限在进入那一拍定死**:请求带的 `deadlineAt` 原样;引擎自起的排空 = 起点 + 此刻的排空窗(`drainGraceMs`)。之后改窗(中心热改
10
+ * `limits.drainGraceMs`)不动在跑的期限 —— 排空驱动读的是期限,不是窗。
11
+ * 3. **幂等**:已在排空 ⇒ 不重置,回执 `already_draining` 带**第一次**的动作 / 起点 / 期限(不论这次的动作号是否相同)。
12
+ * 4. **身份**:逐字段比,任一不符 ⇒ 409 `conflict.fleet_identity`,零状态变化。
13
+ * · `scope`:两侧都经 {@link fleetScopeOf} 读成值(缺席 = settings-schema 的缺省 scope,线上任何一处缺席的 scope 都读成它)再严格相等。
14
+ * 引擎这一侧的唯一来源 = env `FLEET_SCOPE`(`config.fleetScope`,`config.ts` 解析一次);请求不带 `scope` = 点名缺省 scope。
15
+ * · `worker` / `expectedGeneration`:缺席对缺席(非 fleet 成员没有代际,不带 `expectedGeneration` 的请求是单机形,不是通配)。
16
+ * · `instanceId`:请求带了才比(它是引擎每进程自铸的身份,`/health` 上可读);「点名的是不是别的进程」与 run 行同一只判据
17
+ * `runProvenElsewhere`。
18
+ * 引擎自己的 worker / instanceId 读不出 ⇒ 带了这一项的请求不符(认不出自己就不认任何点名;没配 `SEMA_REGISTRY_WORKER` 的引擎因此
19
+ * 只能用停止信号排空)。
20
+ * 5. **进入那一拍让出在飞**:本副本可抢占的在飞腿经 core `TaskSpec.preemptSignal` 让出(在下一个干净的回合边界耐久停驻,
21
+ * `resource_limit` / `preempt`;不合格的腿上 core 静默不动,等到期限硬停 —— 既有 `crashed` 臂);活的审批卡按断连同路改路耐久停驻
22
+ * (协调器 `beginDrain`,core `onAsk` 答 `"unavailable"` 的合同);排空期间新登记的可抢占腿在登记那一拍让出(同一条规则)。
23
+ *
24
+ * 纯类型 + 纯函数叶(值 import 只有 scope 读法叶与 run 行判词叶)。
25
+ */
26
+ import type { DrainReceipt, DrainRequest, EngineDraining } from "@sema-agent/settings-schema/fleet";
27
+ /**
28
+ * 引擎自铸的动作号 —— **一张表**(排空事实与回执里不来自请求体的全部拼法;请求体的 `actionId` 由调用方自取):
29
+ * · {@link ENGINE_DRAIN_CAUSES}:没有请求承载的排空成因(停止信号 / SIGHUP 升级 / 附着租约自退);
30
+ * · {@link HARD_STOP_ACTION_ID}:硬停(未经排空)已在进行时排空口回执里的动作号 —— 不是一种排空成因,只说「这只进程已经在停,而且比你要的更快」;
31
+ * · `lease:<代际>`:服务租约被拒的排空({@link leaseDrainActionId},每代一只:同一代被拒两次是同一个动作)。
32
+ * 按成因穷举的地方({@link DRAIN_CAUSE_COUNTS_AS_FIRST_STOP_SIGNAL})加一种成因不补格 = 编译红。
33
+ */
34
+ export declare const ENGINE_DRAIN_CAUSES: readonly ["signal:SIGTERM", "signal:SIGHUP", "self-exit:attachment"];
35
+ export type EngineDrainCause = (typeof ENGINE_DRAIN_CAUSES)[number];
36
+ export declare const HARD_STOP_ACTION_ID = "hard-stop";
37
+ export type LeaseDrainActionId = `lease:${string}`;
38
+ /** 服务租约被拒时的动作号(每代一只)。 */
39
+ export declare function leaseDrainActionId(generation: string): LeaseDrainActionId;
40
+ /**
41
+ * 每种排空成因的**入口那一拍算不算本次排空的第一发停止信号**(`boot/shutdown.ts` 的「第二发 = 硬停」读它;它答的不是「这是不是 `StopSignal`」——
42
+ * `shutdown.ts` 的 `StopSignal` 闭集只有 SIGTERM / SIGINT,那是「哪些信号走停止处理器」)。算 ⇒ 之后隔 ≥ 最小间隔的一发即硬停;不算 ⇒ 期限由入口定死,
43
+ * 之后到达的第一发停止信号只被吸收(不缩短期限)。SIGHUP 算:SIGHUP 升级成排空要的就是**第二发** SIGHUP(第一发只开空闲窗),那是人 / 宿主又发了一次
44
+ * 停的意图,与 SIGTERM 进的排空同一个读法(7.120.0 起的行为)。闭集 Record。
45
+ */
46
+ export declare const DRAIN_CAUSE_COUNTS_AS_FIRST_STOP_SIGNAL: {
47
+ readonly [C in EngineDrainCause]: boolean;
48
+ };
49
+ /** 入口的入参:动作号 + 期限(epoch ms)。 */
50
+ export interface DrainEntry {
51
+ readonly actionId: string;
52
+ readonly deadlineAt: number;
53
+ }
54
+ /**
55
+ * 引擎与 HTTP 面共享的排空盒子(装配层建一只,`createHttpServer` 与收尾链各自往上装读口 / 入口)。
56
+ * · `draining` / `since` / `actionId` / `deadlineAt` —— **只由** {@link enter} 整组写入(排空事实 = 契约 `EngineDraining`);
57
+ * · `enter` —— 唯一入口(收尾链装;缺席 = 这个进程没有收尾链,如嵌入宿主 / 测试夹具);
58
+ * · `yieldInflight` —— 让出本副本在飞的可抢占腿(`createHttpServer` 装);
59
+ * · 其余四只是既有的读口(在飞数 / 活性时钟 / 附着流),语义见 `http/server.ts`。
60
+ */
61
+ export interface DrainState {
62
+ draining: boolean;
63
+ since?: number;
64
+ actionId?: string;
65
+ deadlineAt?: number;
66
+ enter?: (entry: DrainEntry) => DrainReceipt;
67
+ yieldInflight?: () => void;
68
+ inflight?: () => number;
69
+ lastActivityAt?: () => number;
70
+ attachedStreams?: () => number;
71
+ lastAttachEndedAt?: () => number;
72
+ }
73
+ /** 排空盒子里的排空事实(三键齐才是;入口整组写,所以「缺一」只出现在手搭的夹具上)。 */
74
+ export declare function drainingFactOf(s: Pick<DrainState, "draining" | "since" | "actionId" | "deadlineAt">): EngineDraining | undefined;
75
+ /** 本进程的身份。`scope` 已读成值(`config.fleetScope`);其余三项与 `/health` 上读到的同一份。 */
76
+ export interface EngineIdentity {
77
+ readonly scope: string;
78
+ readonly worker?: string;
79
+ readonly generation?: string;
80
+ readonly instanceId?: string;
81
+ }
82
+ /** 身份四元的字段名(请求侧拼法;409 拒句按它点名,不回显值)。 */
83
+ export declare const DRAIN_IDENTITY_FIELDS: readonly ["scope", "worker", "expectedGeneration", "instanceId"];
84
+ export type DrainIdentityField = (typeof DRAIN_IDENTITY_FIELDS)[number];
85
+ /** 规则 4:不符的字段(按 {@link DRAIN_IDENTITY_FIELDS} 序;空 ⇔ 认得)。 */
86
+ export declare function drainIdentityMismatches(req: Pick<DrainRequest, "scope" | "worker" | "expectedGeneration" | "instanceId">, self: EngineIdentity): DrainIdentityField[];
87
+ //# sourceMappingURL=drain-entry.d.ts.map
@@ -0,0 +1,35 @@
1
+ import { fleetScopeOf } from "../config-types.js";
2
+ import { runProvenElsewhere } from "../plugins/store-contracts.js";
3
+ export const ENGINE_DRAIN_CAUSES = ["signal:SIGTERM", "signal:SIGHUP", "self-exit:attachment"];
4
+ export const HARD_STOP_ACTION_ID = "hard-stop";
5
+ export function leaseDrainActionId(generation) {
6
+ return `lease:${generation}`;
7
+ }
8
+ export const DRAIN_CAUSE_COUNTS_AS_FIRST_STOP_SIGNAL = {
9
+ "signal:SIGTERM": true,
10
+ "signal:SIGHUP": true,
11
+ "self-exit:attachment": false,
12
+ };
13
+ export function drainingFactOf(s) {
14
+ return s.draining && s.actionId !== undefined && s.since !== undefined && s.deadlineAt !== undefined ? { actionId: s.actionId, since: s.since, deadlineAt: s.deadlineAt } : undefined;
15
+ }
16
+ export const DRAIN_IDENTITY_FIELDS = ["scope", "worker", "expectedGeneration", "instanceId"];
17
+ export function drainIdentityMismatches(req, self) {
18
+ return DRAIN_IDENTITY_FIELDS.filter((f) => {
19
+ switch (f) {
20
+ case "scope":
21
+ return fleetScopeOf(req.scope) !== self.scope;
22
+ case "worker":
23
+ return self.worker === undefined || req.worker !== self.worker;
24
+ case "expectedGeneration":
25
+ return req.expectedGeneration !== self.generation;
26
+ case "instanceId":
27
+ return req.instanceId !== undefined && (self.instanceId === undefined || runProvenElsewhere({ instanceId: req.instanceId }, self.instanceId));
28
+ default: {
29
+ const unhandled = f;
30
+ throw new Error(`drain identity: unknown field ${String(unhandled)}`);
31
+ }
32
+ }
33
+ });
34
+ }
35
+ //# sourceMappingURL=drain-entry.js.map