@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.
- package/MIGRATION.md +129 -0
- package/USAGE.md +72 -23
- package/deploy/sema-up/chart/templates/server-v4pro.yaml +2 -2
- package/deploy/sema-up/chart/templates/server.yaml +2 -2
- package/deploy/sema-up/stack/sema-stack-cluster.yaml +1 -1
- package/dist/boot/config-center.d.ts +5 -7
- package/dist/boot/config-center.js +37 -13
- package/dist/boot/engine-lease.d.ts +1 -1
- package/dist/boot/execution-env.js +3 -2
- package/dist/boot/fatal-exit.d.ts +7 -1
- package/dist/boot/fatal-exit.js +2 -1
- package/dist/boot/resolve-spec.js +1 -0
- package/dist/boot/runner-deps.d.ts +4 -3
- package/dist/boot/runner-deps.js +7 -3
- package/dist/boot/shutdown.d.ts +9 -10
- package/dist/boot/shutdown.js +54 -16
- package/dist/boot/stage-01-config.js +2 -0
- package/dist/boot/stage-09-leader.d.ts +2 -9
- package/dist/boot/stage-10-http-server.d.ts +1 -9
- package/dist/boot/stage-10-http-server.js +31 -8
- package/dist/capabilities/builtin-tools.d.ts +31 -8
- package/dist/capabilities/builtin-tools.js +22 -1
- package/dist/capabilities/composite-members.d.ts +1 -1
- package/dist/capabilities/composite-members.js +1 -1
- package/dist/capabilities/scenarios.d.ts +8 -6
- package/dist/capabilities/scenarios.js +3 -3
- package/dist/capabilities/send-user-file-tool.d.ts +6 -3
- package/dist/capabilities/send-user-file-tool.js +1 -1
- package/dist/config-catalog.js +13 -7
- package/dist/config-center/center-request.d.ts +16 -1
- package/dist/config-center/center-request.js +36 -5
- package/dist/config-lkg.d.ts +33 -5
- package/dist/config-lkg.js +55 -11
- package/dist/config-types.d.ts +46 -3
- package/dist/config-types.js +6 -0
- package/dist/config.d.ts +33 -4
- package/dist/config.js +107 -15
- package/dist/deployment-governance.d.ts +3 -1
- package/dist/deployment-governance.js +3 -1
- package/dist/device-store.d.ts +7 -0
- package/dist/device-store.js +4 -0
- package/dist/device-ws-hub.js +17 -9
- package/dist/device-ws-protocol.d.ts +15 -0
- package/dist/fleet/drain-entry.d.ts +87 -0
- package/dist/fleet/drain-entry.js +35 -0
- package/dist/fleet/engine-signal.d.ts +144 -0
- package/dist/fleet/engine-signal.js +60 -0
- package/dist/fleet/serving-lease.d.ts +47 -0
- package/dist/fleet/serving-lease.js +235 -0
- package/dist/fleet-client.d.ts +35 -19
- package/dist/fleet-client.js +49 -25
- package/dist/http/approval-sweeps.js +3 -5
- package/dist/http/dispatch.js +18 -13
- package/dist/http/health-self-report.d.ts +60 -0
- package/dist/http/health-self-report.js +53 -0
- package/dist/http/principal-gate.d.ts +8 -1
- package/dist/http/principal-gate.js +1 -0
- package/dist/http/readiness.d.ts +25 -5
- package/dist/http/readiness.js +31 -6
- package/dist/http/resume-legs.js +5 -1
- package/dist/http/route-ctx.d.ts +6 -4
- package/dist/http/route-table.d.ts +3 -0
- package/dist/http/route-table.js +4 -1
- package/dist/http/routes/a2a-serve.d.ts +1 -1
- package/dist/http/routes/a2a-serve.js +5 -5
- package/dist/http/routes/admin-drain.d.ts +12 -31
- package/dist/http/routes/admin-drain.js +15 -9
- package/dist/http/routes/approvals-assistant.d.ts +3 -3
- package/dist/http/routes/approvals-assistant.js +3 -3
- package/dist/http/routes/capabilities.js +3 -1
- package/dist/http/routes/devices.js +6 -5
- package/dist/http/routes/leader.d.ts +1 -1
- package/dist/http/routes/leader.js +1 -1
- package/dist/http/routes/memory-consolidation.d.ts +1 -1
- package/dist/http/routes/memory-consolidation.js +1 -1
- package/dist/http/routes/notify-wake.d.ts +1 -1
- package/dist/http/routes/notify-wake.js +1 -1
- package/dist/http/routes/rules.js +14 -6
- package/dist/http/routes/runs.d.ts +2 -2
- package/dist/http/routes/runs.js +2 -2
- package/dist/http/routes/side-query.d.ts +1 -1
- package/dist/http/routes/side-query.js +1 -1
- package/dist/http/routes/tasks.d.ts +2 -2
- package/dist/http/routes/tasks.js +3 -3
- package/dist/http/server.d.ts +7 -21
- package/dist/http/server.js +20 -1
- package/dist/http/wire-types.d.ts +7 -1
- package/dist/list-entry-display.d.ts +14 -0
- package/dist/list-entry-display.js +14 -0
- package/dist/model-reach-scope.d.ts +15 -3
- package/dist/model-reach-scope.js +3 -2
- package/dist/model-unreachable-notice.d.ts +2 -1
- package/dist/model-unreachable-notice.js +1 -0
- package/dist/namespaced-rule-name.js +2 -6
- package/dist/observability/fail-open.d.ts +20 -0
- package/dist/observability/fail-open.js +20 -0
- package/dist/operator-ask.d.ts +1 -1
- package/dist/plugins/one-time-migrations.d.ts +8 -0
- package/dist/plugins/one-time-migrations.js +72 -3
- package/dist/plugins/permission-rule-store-file.js +2 -6
- package/dist/plugins/permission-rule-store-sql.d.ts +65 -6
- package/dist/plugins/permission-rule-store-sql.js +166 -59
- package/dist/plugins/remote-env-device.d.ts +12 -1
- package/dist/plugins/remote-env-device.js +13 -6
- package/dist/plugins/scheduler-support.d.ts +8 -4
- package/dist/plugins/scheduler-support.js +5 -5
- package/dist/request-key-closure.js +1 -1
- package/dist/rule-entry-list.d.ts +0 -1
- package/dist/rule-entry-list.js +4 -9
- package/dist/rules-consent.d.ts +16 -3
- package/dist/rules-consent.js +8 -4
- package/dist/run-local.js +2 -0
- package/dist/runs.js +1 -1
- package/dist/sema-config-dir.d.ts +19 -0
- package/dist/sema-config-dir.js +33 -0
- package/dist/session-policy-wire.js +5 -4
- package/dist/spec-fields.d.ts +5 -4
- package/dist/task-settings.d.ts +3 -3
- package/dist/tool-approval.d.ts +7 -0
- package/dist/tool-approval.js +4 -1
- package/dist/trace/core-keyset-guard.d.ts +2 -2
- package/dist/trace/engine-notice-wire.d.ts +6 -4
- package/dist/trace/engine-notice-wire.js +16 -9
- package/dist/trace/self-minted-wire.d.ts +78 -0
- package/dist/trace/self-minted-wire.js +32 -0
- package/docs/DEVICE-EXECUTOR-PROTOCOL.md +56 -14
- 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
|
-
|
|
1221
|
-
|
|
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) => `${
|
|
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
|
|
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
|
|
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` / `
|
|
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
|
})()
|
package/dist/device-store.d.ts
CHANGED
|
@@ -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;
|
package/dist/device-store.js
CHANGED
|
@@ -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);
|
package/dist/device-ws-hub.js
CHANGED
|
@@ -425,7 +425,20 @@ export function createDeviceWsHub(options) {
|
|
|
425
425
|
clearTimeout(conn.handshakeTimer);
|
|
426
426
|
conn.handshakeTimer = null;
|
|
427
427
|
releasePreAuthSlot(conn);
|
|
428
|
-
|
|
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
|
-
|
|
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,
|
|
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
|