@sema-agent/server 7.121.0 → 7.123.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 +175 -0
- package/README.md +6 -1
- package/README.zh-CN.md +6 -1
- package/USAGE.md +57 -18
- package/dist/approval-reconciler.d.ts +8 -0
- package/dist/approval-reconciler.js +72 -52
- package/dist/audit.d.ts +1 -1
- package/dist/audit.js +52 -22
- package/dist/auto-mode-face.d.ts +16 -5
- package/dist/auto-mode-face.js +7 -4
- package/dist/bench/s1/live-deps.js +1 -0
- package/dist/boot/config-center.js +3 -0
- package/dist/boot/leader.js +1 -0
- package/dist/boot/reapers.d.ts +3 -0
- package/dist/boot/reapers.js +2 -1
- package/dist/boot/resolve-spec.js +1 -4
- package/dist/boot/shutdown.js +6 -1
- package/dist/boot/stage-08-reapers.js +2 -2
- package/dist/capabilities/memory-notice.d.ts +1 -1
- package/dist/config-catalog.js +4 -4
- package/dist/config-center/apply-effective.d.ts +1 -1
- package/dist/config-center/apply-effective.js +5 -3
- package/dist/config-center/center-request.d.ts +6 -9
- package/dist/config-center/center-request.js +11 -27
- package/dist/config-center/http-client.js +3 -3
- package/dist/config-center/read-warnings.d.ts +56 -35
- package/dist/config-center/read-warnings.js +67 -19
- package/dist/config-center/types.d.ts +7 -8
- package/dist/config-lkg.d.ts +12 -9
- package/dist/config-lkg.js +38 -39
- package/dist/config-provider.d.ts +3 -2
- package/dist/config-provider.js +2 -2
- package/dist/config-types.d.ts +20 -9
- package/dist/config-types.js +1 -1
- package/dist/config.d.ts +10 -10
- package/dist/config.js +92 -127
- package/dist/fleet/drain-entry.d.ts +28 -3
- package/dist/fleet/drain-entry.js +3 -0
- package/dist/fleet/engine-signal.d.ts +54 -23
- package/dist/fleet/engine-signal.js +29 -5
- package/dist/fleet/serving-lease.js +13 -15
- package/dist/fleet-client.d.ts +8 -6
- package/dist/fleet-client.js +4 -3
- package/dist/governance-overrides.d.ts +7 -2
- package/dist/http/admission.js +7 -5
- package/dist/http/approval-sweeps.js +2 -2
- package/dist/http/checkpoint-ctx.d.ts +60 -11
- package/dist/http/checkpoint-ctx.js +23 -9
- package/dist/http/dispatch.js +22 -19
- package/dist/http/health-self-report.d.ts +3 -0
- package/dist/http/resume-legs.js +8 -8
- package/dist/http/route-ctx.d.ts +19 -3
- package/dist/http/route-table.d.ts +3 -0
- package/dist/http/route-table.js +4 -0
- package/dist/http/routes/admin-drain.d.ts +5 -2
- package/dist/http/routes/admin-drain.js +14 -2
- package/dist/http/routes/approvals-assistant.d.ts +1 -0
- package/dist/http/routes/approvals-assistant.js +6 -6
- package/dist/http/routes/capabilities.js +7 -5
- package/dist/http/routes/fleet.d.ts +1 -0
- package/dist/http/routes/fleet.js +1 -1
- package/dist/http/routes/images.d.ts +1 -0
- package/dist/http/routes/images.js +1 -1
- package/dist/http/routes/leader.js +6 -0
- package/dist/http/routes/runs.d.ts +2 -0
- package/dist/http/routes/runs.js +5 -4
- package/dist/http/routes/sessions.d.ts +1 -0
- package/dist/http/routes/sessions.js +2 -5
- package/dist/http/routes/trace-usage.d.ts +1 -0
- package/dist/http/routes/trace-usage.js +1 -1
- package/dist/http/routes/workflows.d.ts +1 -0
- package/dist/http/routes/workflows.js +1 -1
- package/dist/http/send.d.ts +13 -0
- package/dist/http/send.js +9 -0
- package/dist/http/server.d.ts +3 -0
- package/dist/http/stored-body-replay.d.ts +28 -0
- package/dist/http/stored-body-replay.js +22 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/leader/leader.d.ts +23 -1
- package/dist/leader/leader.js +6 -1
- package/dist/leader/repair-wire.d.ts +1 -1
- package/dist/leader/repair-wire.js +2 -2
- package/dist/leader/run-entry.d.ts +27 -0
- package/dist/leader/run-entry.js +29 -0
- package/dist/leader/wire.d.ts +10 -0
- package/dist/leader/wire.js +15 -10
- package/dist/model-entry-refusal.d.ts +18 -9
- package/dist/model-entry-refusal.js +19 -14
- package/dist/model-route-endpoint.d.ts +21 -1
- package/dist/model-route-endpoint.js +9 -3
- package/dist/model-route-gap.d.ts +103 -0
- package/dist/model-route-gap.js +80 -0
- package/dist/model-route-url.d.ts +13 -20
- package/dist/model-route-url.js +28 -24
- package/dist/namespaced-rule-name.d.ts +16 -30
- package/dist/namespaced-rule-name.js +2 -20
- package/dist/observability/fail-open.d.ts +23 -10
- package/dist/observability/fail-open.js +8 -8
- package/dist/plugins/approval-ask-store-file.d.ts +1 -0
- package/dist/plugins/approval-ask-store-file.js +3 -0
- package/dist/plugins/approval-ask-store-memory.d.ts +1 -0
- package/dist/plugins/approval-ask-store-memory.js +6 -0
- package/dist/plugins/approval-ask-store-sql.d.ts +12 -0
- package/dist/plugins/approval-ask-store-sql.js +4 -0
- package/dist/plugins/one-time-migrations.js +40 -0
- package/dist/plugins/park-commit-watch.d.ts +19 -5
- package/dist/plugins/park-commit-watch.js +24 -17
- package/dist/plugins/session-policy-store-sql.js +1 -2
- package/dist/plugins/workflow-run-store-sql.d.ts +4 -2
- package/dist/router/route-orchestration.d.ts +4 -2
- package/dist/router/route-orchestration.js +2 -1
- package/dist/rule-entry-list.d.ts +3 -13
- package/dist/rule-entry-list.js +1 -12
- package/dist/run-local.js +4 -2
- package/dist/runtime-governance.d.ts +67 -32
- package/dist/runtime-governance.js +28 -9
- package/dist/session-policy-wire.d.ts +47 -49
- package/dist/session-policy-wire.js +41 -36
- package/dist/session-sync.d.ts +5 -1
- package/dist/session-sync.js +2 -1
- package/dist/sighup-idle.d.ts +7 -1
- package/dist/sighup-idle.js +4 -4
- package/dist/spec-fields.d.ts +4 -4
- package/dist/spec-fields.js +1 -4
- package/dist/task-settings.d.ts +36 -14
- package/dist/task-settings.js +20 -23
- package/dist/tool-approval.d.ts +6 -0
- package/dist/tool-approval.js +13 -1
- package/dist/trace/redact.d.ts +3 -3
- package/dist/unattended-permission-mode.d.ts +17 -13
- package/dist/unattended-permission-mode.js +3 -1
- package/dist/url-shape.d.ts +31 -0
- package/dist/url-shape.js +41 -0
- package/package.json +3 -3
- package/scripts/drain-check-local.mjs +1 -1
package/dist/config.js
CHANGED
|
@@ -13,7 +13,7 @@ import { GIT_API_KINDS, isGitApiKind } from "./git-api-kind.js";
|
|
|
13
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
14
|
import { FLEET_GENERATION_ENV, FLEET_SCOPE_ENV, SERVING_LEASE_TTL_FLOOR_MS, isCanonicalGeneration } from "@sema-agent/settings-schema/fleet";
|
|
15
15
|
import { ScopeId } from "@sema-agent/settings-schema/api/scopes";
|
|
16
|
-
import { centerLaneConfigured,
|
|
16
|
+
import { centerLaneConfigured, pathBaseUrlRefusal } from "./config-center/center-request.js";
|
|
17
17
|
import { resolveLocalDataRoot } from "./local-data-root.js";
|
|
18
18
|
import { buildWriteProtectionPosture } from "./write-protection.js";
|
|
19
19
|
import { normalizeSshFingerprint } from "./ssh-host-key.js";
|
|
@@ -28,22 +28,20 @@ import { lkgPathOverrideOf } from "./config-lkg.js";
|
|
|
28
28
|
import { REMOTE_EXEC_PROVIDER_WORDS, executionLaneOf, executionLaneRequiredRefusal, isSingleMachineDefaultLane, laneRequiredEnv } from "./execution-lane-caps.js";
|
|
29
29
|
import { MEMORY_ENGINE_BACKENDS, SESSION_BACKENDS, STORE_BACKENDS } from "@sema-agent/settings-schema/execution-lanes";
|
|
30
30
|
import { AUTONOMY_MODES } from "./runtime-governance.js";
|
|
31
|
-
import { allowLayerEntryCountIssue,
|
|
31
|
+
import { allowLayerEntryCountIssue, describeInertRuleEntries, inertRuleEntriesOf, ruleEntryListShapeOk } from "./rule-entry-list.js";
|
|
32
32
|
import { createCenterCredentialSource, readCenterCredentialDeclaration } from "./center-credential.js";
|
|
33
33
|
import { parseCenterCredentialRenewLeadMs } from "./center-credential-renewer.js";
|
|
34
34
|
import { modelProviderRefusal } from "./model-provider.js";
|
|
35
35
|
import { modelRouteUrlRefusal } from "./model-route-url.js";
|
|
36
|
-
import {
|
|
36
|
+
import { buildHttpUrlRefusal, httpUrlFlaws } from "./url-shape.js";
|
|
37
|
+
import { csvOf, envLaneTable, judgeModelRouteGap, mintedCheapId, readModelRouteEnv } from "./model-route-gap.js";
|
|
37
38
|
import { displayListEntry } from "./list-entry-display.js";
|
|
38
39
|
import { buildDisplaySafeUrl, redactSecrets } from "./trace/redact.js";
|
|
39
40
|
import { allowlistedEnv } from "./server-secret-env.js";
|
|
40
41
|
import { ENV_NAME_ALLOWLIST_KNOBS, ENV_NAME_ALLOWLIST_KNOB_NAMES, describeAllowlistPostures } from "./env-name-allowlist-knobs.js";
|
|
41
42
|
export { RETENTION_MODES, MEMORY_CONSOLIDATION_MIN_INTERVAL_SEC } from "./config-types.js";
|
|
42
43
|
function csv(name) {
|
|
43
|
-
return (process.env[name]
|
|
44
|
-
.split(",")
|
|
45
|
-
.map((s) => s.trim())
|
|
46
|
-
.filter(Boolean);
|
|
44
|
+
return csvOf(process.env[name]);
|
|
47
45
|
}
|
|
48
46
|
export function parseAutonomy(raw) {
|
|
49
47
|
const v = raw?.trim();
|
|
@@ -311,14 +309,6 @@ export function logConfigDiagnostics(logger) {
|
|
|
311
309
|
}
|
|
312
310
|
}
|
|
313
311
|
}
|
|
314
|
-
function firstSetEnv(...names) {
|
|
315
|
-
for (const n of names) {
|
|
316
|
-
const v = process.env[n];
|
|
317
|
-
if (v !== undefined && v !== "")
|
|
318
|
-
return v;
|
|
319
|
-
}
|
|
320
|
-
return undefined;
|
|
321
|
-
}
|
|
322
312
|
function clampEnvWithWarn(name, fallback, min, max = Number.POSITIVE_INFINITY) {
|
|
323
313
|
const n = numEnv(name, fallback);
|
|
324
314
|
const used = Math.min(max, Math.max(min, n));
|
|
@@ -951,44 +941,37 @@ export function isRosterGateArmed(configCenter) {
|
|
|
951
941
|
return configCenter !== undefined && !configCenter.dryRun && process.env.CONFIG_REQUIRE_ROSTER === "true";
|
|
952
942
|
}
|
|
953
943
|
function parseModelDomain() {
|
|
954
|
-
const
|
|
955
|
-
const
|
|
956
|
-
const
|
|
957
|
-
if (
|
|
958
|
-
const refusal = modelProviderRefusal(
|
|
944
|
+
const route = readModelRouteEnv(process.env);
|
|
945
|
+
const gatewayBaseUrl = route.gatewayBaseUrl;
|
|
946
|
+
const modelId = route.mainId;
|
|
947
|
+
if (route.providerSource === "explicit") {
|
|
948
|
+
const refusal = modelProviderRefusal(route.provider);
|
|
959
949
|
if (refusal !== undefined) {
|
|
960
950
|
throw new Error(refusal);
|
|
961
951
|
}
|
|
962
952
|
}
|
|
963
953
|
for (const [envKey, raw, knob] of [
|
|
964
954
|
["MODEL_GATEWAY_BASEURL", gatewayBaseUrl ?? "", "MODEL_API_KEY"],
|
|
965
|
-
...
|
|
966
|
-
["ANTHROPIC_BASE_URL",
|
|
955
|
+
...route.gatewayFallbackUrls.map((u, i) => [`MODEL_GATEWAY_FALLBACK_URLS[${i}]`, u, "MODEL_API_KEY"]),
|
|
956
|
+
["ANTHROPIC_BASE_URL", route.anthropicBaseUrl ?? "", "ANTHROPIC_API_KEY"],
|
|
967
957
|
]) {
|
|
968
958
|
const refusal = modelRouteUrlRefusal(envKey, raw, knob);
|
|
969
959
|
if (refusal !== undefined) {
|
|
970
960
|
throw new Error(refusal);
|
|
971
961
|
}
|
|
972
962
|
}
|
|
973
|
-
const
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
hint: "MODEL_PROVIDER is unset but an explicit ANTHROPIC base URL + credential are configured — inferred provider=anthropic (set MODEL_PROVIDER explicitly to override)",
|
|
986
|
-
},
|
|
987
|
-
});
|
|
988
|
-
return "anthropic";
|
|
989
|
-
}
|
|
990
|
-
return "gateway";
|
|
991
|
-
})();
|
|
963
|
+
const provider = route.provider;
|
|
964
|
+
if (route.providerSource === "inferred") {
|
|
965
|
+
CONFIG_NOTICES.push({
|
|
966
|
+
level: "warn",
|
|
967
|
+
event: "model_provider_inferred",
|
|
968
|
+
fields: {
|
|
969
|
+
provider: "anthropic",
|
|
970
|
+
baseUrl: buildDisplaySafeUrl(route.anthropicBaseUrl),
|
|
971
|
+
hint: "MODEL_PROVIDER is unset but an explicit ANTHROPIC base URL + credential are configured — inferred provider=anthropic (set MODEL_PROVIDER explicitly to override)",
|
|
972
|
+
},
|
|
973
|
+
});
|
|
974
|
+
}
|
|
992
975
|
const extraBody = buildExtraBody();
|
|
993
976
|
const thinkingDefault = parseDefaultThinking(process.env.MODEL_DEFAULT_THINKING);
|
|
994
977
|
const envCompatDeclared = buildModelCompat({
|
|
@@ -1020,35 +1003,40 @@ function parseModelDomain() {
|
|
|
1020
1003
|
...(envCompat ? { compat: envCompat } : {}),
|
|
1021
1004
|
...(extraBody ? { extraBody } : {}),
|
|
1022
1005
|
};
|
|
1023
|
-
const
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1006
|
+
const cheapSlot = route.cheapSlot;
|
|
1007
|
+
switch (cheapSlot.kind) {
|
|
1008
|
+
case "without_main":
|
|
1009
|
+
CONFIG_NOTICES.push({
|
|
1010
|
+
level: "warn",
|
|
1011
|
+
event: "model_cheap_slot_without_main",
|
|
1012
|
+
fields: { hint: "MODEL_CHEAP_ID is set but MODEL_ID is not — the env cheap slot derives from the env main model, so it is not minted; the catalog (config.d/models.json or the config center) serves the roles. Set MODEL_ID to use the env slots, or unset MODEL_CHEAP_ID to silence this line." },
|
|
1013
|
+
});
|
|
1014
|
+
break;
|
|
1015
|
+
case "same_as_main":
|
|
1016
|
+
CONFIG_NOTICES.push({
|
|
1017
|
+
level: "warn",
|
|
1018
|
+
event: "config_env_entry_cheap_same_as_main",
|
|
1019
|
+
fields: {
|
|
1020
|
+
env: "MODEL_CHEAP_ID",
|
|
1021
|
+
model: modelId,
|
|
1022
|
+
hint: `MODEL_CHEAP_ID names the same model as MODEL_ID ("${modelId}") — the env cheap slot is the main gateway under a different id, ` +
|
|
1023
|
+
`so the same id IS the main model: no cheap slot is minted and subagent / summarize stay on the main model (roles -> default); ` +
|
|
1024
|
+
`any other MODEL_CHEAP_* key set here describes that slot and is not applied. Set MODEL_CHEAP_ID to a different (cheaper) model ` +
|
|
1025
|
+
`id to run those roles cheap, or declare two differently-named entries in config.d/models.json to give one upstream id a second ` +
|
|
1026
|
+
`shape (price / reasoning / window); unset MODEL_CHEAP_ID to silence this line.`,
|
|
1027
|
+
},
|
|
1028
|
+
});
|
|
1029
|
+
break;
|
|
1030
|
+
case "absent":
|
|
1031
|
+
case "minted":
|
|
1032
|
+
break;
|
|
1046
1033
|
}
|
|
1047
|
-
const
|
|
1034
|
+
const cheapMintedId = mintedCheapId(cheapSlot);
|
|
1035
|
+
const cheapModel = cheapMintedId !== undefined
|
|
1048
1036
|
? {
|
|
1049
1037
|
...model,
|
|
1050
|
-
id:
|
|
1051
|
-
name:
|
|
1038
|
+
id: cheapMintedId,
|
|
1039
|
+
name: cheapMintedId,
|
|
1052
1040
|
input: ((v) => (v === undefined ? model.input : v ? ["text", "image"] : ["text"]))(parseBoolWord("MODEL_CHEAP_VISION", process.env.MODEL_CHEAP_VISION)),
|
|
1053
1041
|
...buildModelCostKey(buildCostOrAbsent({
|
|
1054
1042
|
input: costEnv("MODEL_CHEAP_COST_INPUT", "declares-zero"),
|
|
@@ -1092,9 +1080,7 @@ function parseModelDomain() {
|
|
|
1092
1080
|
}
|
|
1093
1081
|
noticeAutoCompactAboveWindow("cheap", "MODEL_CHEAP_AUTO_COMPACT_TOKENS", cheapModel);
|
|
1094
1082
|
}
|
|
1095
|
-
const models = modelId
|
|
1096
|
-
if (cheapModel)
|
|
1097
|
-
models[cheapModel.id] = cheapModel;
|
|
1083
|
+
const models = envLaneTable(modelId, model, cheapModel);
|
|
1098
1084
|
const gatewayApiKey = process.env.MODEL_API_KEY;
|
|
1099
1085
|
if (gatewayBaseUrl === undefined && gatewayApiKey !== undefined && gatewayApiKey !== "") {
|
|
1100
1086
|
CONFIG_NOTICES.push({
|
|
@@ -1128,13 +1114,13 @@ function parseModelDomain() {
|
|
|
1128
1114
|
const degradeOn = parseDegradeOn(csv("MODEL_DEGRADE_ON"));
|
|
1129
1115
|
return {
|
|
1130
1116
|
...(gatewayBaseUrl !== undefined ? { gatewayBaseUrl, gatewayApiKey } : {}),
|
|
1131
|
-
gatewayFallbackUrls:
|
|
1117
|
+
gatewayFallbackUrls: route.gatewayFallbackUrls,
|
|
1132
1118
|
...(process.env.GATEWAY_MAX_RETRIES ? { gatewayMaxRetries: numEnvBounded("GATEWAY_MAX_RETRIES", "2", 0, 20) } : {}),
|
|
1133
|
-
anthropic:
|
|
1119
|
+
anthropic: route.anthropicRouteMounted
|
|
1134
1120
|
? {
|
|
1135
|
-
apiKey:
|
|
1136
|
-
authToken:
|
|
1137
|
-
baseUrl:
|
|
1121
|
+
apiKey: route.anthropicApiKey,
|
|
1122
|
+
authToken: route.anthropicAuthToken,
|
|
1123
|
+
baseUrl: route.anthropicBaseUrl,
|
|
1138
1124
|
version: process.env.ANTHROPIC_VERSION,
|
|
1139
1125
|
cacheBreakpoints: boolEnv("ANTHROPIC_CACHE_BREAKPOINTS", true),
|
|
1140
1126
|
...(process.env.ANTHROPIC_MAX_RETRIES ? { maxRetries: numEnv("ANTHROPIC_MAX_RETRIES", "2") } : {}),
|
|
@@ -1154,6 +1140,7 @@ function parseModelDomain() {
|
|
|
1154
1140
|
modelApiKeyEnv: {},
|
|
1155
1141
|
modelApiKeys: {},
|
|
1156
1142
|
modelRefusals: {},
|
|
1143
|
+
droppedCatalogEntries: [],
|
|
1157
1144
|
modelQuotaWeights: {},
|
|
1158
1145
|
tiers: {},
|
|
1159
1146
|
projects: {},
|
|
@@ -1227,9 +1214,7 @@ function uncompilableRuleGuidance(name, behavior, pathBases) {
|
|
|
1227
1214
|
const issue = issues[0];
|
|
1228
1215
|
if (issue === undefined)
|
|
1229
1216
|
return undefined;
|
|
1230
|
-
return
|
|
1231
|
-
`Tool names carry no backslashes and are far shorter than the 512-character rule cap; check the spelling, and use ` +
|
|
1232
|
-
`mcp__<server>__* / a2a__<peer>__* for "every tool on that peer"`);
|
|
1217
|
+
return `${issue.code} — core's rule grammar cannot compile this entry: ${issue.message}`;
|
|
1233
1218
|
}
|
|
1234
1219
|
function findUnmatchableToolNames(names, opts) {
|
|
1235
1220
|
const out = [];
|
|
@@ -1273,9 +1258,6 @@ export function approvalGrantRefusal(names) {
|
|
|
1273
1258
|
const over = allowLayerEntryCountIssue(APPROVAL_GRANT_ENV, names);
|
|
1274
1259
|
if (over !== undefined)
|
|
1275
1260
|
return `${over.message} — refusing to start rather than running with a truncated list`;
|
|
1276
|
-
const empty = emptyToolSegmentEntryOf(names);
|
|
1277
|
-
if (empty !== undefined)
|
|
1278
|
-
return `${describeEmptyToolSegmentEntry(APPROVAL_GRANT_ENV, empty)} — refusing to start rather than running without it`;
|
|
1279
1261
|
const inert = inertRuleEntriesOf(names, "allow");
|
|
1280
1262
|
if (inert.length > 0)
|
|
1281
1263
|
return `${describeInertRuleEntries(APPROVAL_GRANT_ENV, inert)} — the engine would grant nothing for ${inert.length === 1 ? "it" : "them"}; refusing to start rather than running with an allow rule that never takes effect`;
|
|
@@ -1376,7 +1358,7 @@ function parseApprovalDomain(ctx) {
|
|
|
1376
1358
|
const trimmed = raw.trim();
|
|
1377
1359
|
if (trimmed === "" || trimmed.toLowerCase() === "off")
|
|
1378
1360
|
return [];
|
|
1379
|
-
return trimmed
|
|
1361
|
+
return csvOf(trimmed);
|
|
1380
1362
|
})(process.env.SENSITIVE_WRITE_PATTERNS),
|
|
1381
1363
|
...((decision) => (decision === undefined ? {} : { writeProtectedPaths: decision }))(parseWriteProtectedTableEnv(process.env.WRITE_PROTECTED_EXTRA, process.env.WRITE_PROTECTED_TABLE_REPLACE)),
|
|
1382
1364
|
...((raw) => {
|
|
@@ -1564,16 +1546,11 @@ function parseMemoryEmbedder(ctx) {
|
|
|
1564
1546
|
if (endpoint === undefined || model === undefined || dimRaw === undefined) {
|
|
1565
1547
|
throw new Error(`MEMORY_EMBEDDER_* is half-configured — missing ${missing.join(", ")}. The memory vector plane needs all three of MEMORY_EMBEDDER_ENDPOINT, MEMORY_EMBEDDER_MODEL, MEMORY_EMBEDDER_DIM; refusing to start rather than silently running lexical-only retrieval while the deployment believes vectors are on (#228).`);
|
|
1566
1548
|
}
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
if (proto !== "http:" && proto !== "https:") {
|
|
1573
|
-
throw new Error(`MEMORY_EMBEDDER_ENDPOINT="${buildDisplaySafeUrl(endpoint)}" must be http(s) (got "${proto}") — the embedder leg is an HTTP POST to an OpenAI-compatible /embeddings face`);
|
|
1574
|
-
}
|
|
1575
|
-
if (parsedEndpoint.username !== "" || parsedEndpoint.password !== "") {
|
|
1576
|
-
throw new Error("MEMORY_EMBEDDER_ENDPOINT must not carry credentials in the URL (user:password@host) — URL userinfo leaks into transport errors, logs and memory conflict reports. Use MEMORY_EMBEDDER_API_KEY (sent as a Bearer header) instead.");
|
|
1549
|
+
const endpointFlaws = httpUrlFlaws(endpoint, ["userinfo"]);
|
|
1550
|
+
if (endpointFlaws.length > 0) {
|
|
1551
|
+
throw new Error(`${buildHttpUrlRefusal("MEMORY_EMBEDDER_ENDPOINT", endpoint, endpointFlaws)} — the embedder leg is an HTTP POST to an OpenAI-compatible /embeddings face ` +
|
|
1552
|
+
`(expected e.g. https://embeddings.example.com/v1 — the OpenAI-compatible base, or the full .../embeddings endpoint); ` +
|
|
1553
|
+
`credentials in the URL leak into transport errors, logs and memory conflict reports, so use MEMORY_EMBEDDER_API_KEY (sent as a Bearer header) instead.`);
|
|
1577
1554
|
}
|
|
1578
1555
|
const dimensions = Number(dimRaw);
|
|
1579
1556
|
if (!Number.isSafeInteger(dimensions) || dimensions < 1) {
|
|
@@ -1832,7 +1809,7 @@ function parseAuthDomain(ctx) {
|
|
|
1832
1809
|
authToken: process.env.SERVICE_AUTH_TOKEN,
|
|
1833
1810
|
authTokens,
|
|
1834
1811
|
allowUnauthedWrites: boolEnv("ALLOW_UNAUTHED_WRITES", false),
|
|
1835
|
-
corsOrigins: (
|
|
1812
|
+
corsOrigins: csv("CORS_ORIGIN"),
|
|
1836
1813
|
principalHeader: headerNameEnv("PRINCIPAL_HEADER", "x-agent-principal"),
|
|
1837
1814
|
requirePrincipal,
|
|
1838
1815
|
autonomy: parseAutonomy(process.env.AUTONOMY),
|
|
@@ -2017,7 +1994,7 @@ function parseOrchestrationDomain(ctx) {
|
|
|
2017
1994
|
? {
|
|
2018
1995
|
repoRoot: process.env.WORKTREE_REPO_ROOT,
|
|
2019
1996
|
...(process.env.WORKTREE_ALLOWED_ROOTS
|
|
2020
|
-
? { allowedRoots:
|
|
1997
|
+
? { allowedRoots: csv("WORKTREE_ALLOWED_ROOTS") }
|
|
2021
1998
|
: {}),
|
|
2022
1999
|
...(process.env.WORKTREE_BASE_COMMIT ? { commit: process.env.WORKTREE_BASE_COMMIT } : {}),
|
|
2023
2000
|
}
|
|
@@ -2101,7 +2078,7 @@ function parseLimitsHttpDomain() {
|
|
|
2101
2078
|
workspaceFileMaxBytes: optFinitePositiveEnv("WORKSPACE_FILE_MAX_BYTES") ?? 8 * 1024 * 1024,
|
|
2102
2079
|
attachmentMaxBytes: clampEnvWithWarn("ATTACHMENT_MAX_BYTES", String(32 * 1024 * 1024), LIMITS_HTTP_FLOORS.attachmentMaxBytes),
|
|
2103
2080
|
...(process.env.ATTACHMENT_MIME_ALLOWLIST
|
|
2104
|
-
? { attachmentMimeAllowlist:
|
|
2081
|
+
? { attachmentMimeAllowlist: csv("ATTACHMENT_MIME_ALLOWLIST").map((s) => s.toLowerCase()) }
|
|
2105
2082
|
: {}),
|
|
2106
2083
|
attachmentUnboundTtlMs: clampEnvWithWarn("ATTACHMENT_UNBOUND_TTL_MS", String(24 * 3600 * 1000), LIMITS_HTTP_FLOORS.attachmentUnboundTtlMs),
|
|
2107
2084
|
infraCostRates: {
|
|
@@ -2196,7 +2173,7 @@ function parseIntegrationsDomain(ctx) {
|
|
|
2196
2173
|
const fleetAdvertiseAddress = parseFleetAdvertiseAddress(process.env.FLEET_ADVERTISE_ADDRESS);
|
|
2197
2174
|
fleetFenceLaneOf({ ...fleetGeneration, configCenter, ...fleetAdvertiseAddress });
|
|
2198
2175
|
return {
|
|
2199
|
-
pluginsAllowHosts: (process.env.PLUGINS_ALLOW_HOSTS ?? "github.com")
|
|
2176
|
+
pluginsAllowHosts: csvOf(process.env.PLUGINS_ALLOW_HOSTS ?? "github.com"),
|
|
2200
2177
|
configBootFetchBudgetMs: clampEnvWithWarn("CONFIG_BOOT_FETCH_BUDGET_MS", "1500", 0),
|
|
2201
2178
|
gitApiBaseUrl: process.env.GIT_API_BASEURL,
|
|
2202
2179
|
gitApiToken: process.env.GIT_API_TOKEN,
|
|
@@ -2249,10 +2226,10 @@ function parseFleetScope(raw) {
|
|
|
2249
2226
|
function parseFleetAdvertiseAddress(raw) {
|
|
2250
2227
|
if (raw === undefined || raw === "")
|
|
2251
2228
|
return {};
|
|
2252
|
-
const
|
|
2253
|
-
if (
|
|
2254
|
-
throw fleetEnvRefusal("advertise_address_invalid",
|
|
2255
|
-
`
|
|
2229
|
+
const refusal = pathBaseUrlRefusal("env FLEET_ADVERTISE_ADDRESS", raw);
|
|
2230
|
+
if (refusal !== undefined) {
|
|
2231
|
+
throw fleetEnvRefusal("advertise_address_invalid", `${refusal} — it must be an http(s) base URL such as "http://10.0.0.5:8090": the fleet center rejects any other shape on every announce. ` +
|
|
2232
|
+
`Unset it if this engine does not announce itself.`);
|
|
2256
2233
|
}
|
|
2257
2234
|
return { fleetAdvertiseAddress: raw };
|
|
2258
2235
|
}
|
|
@@ -2280,7 +2257,8 @@ function parseFleetLeaseOutageMaxMs(raw) {
|
|
|
2280
2257
|
const shown = raw.length > 40 ? `${raw.slice(0, 40)}…(${raw.length} chars)` : raw;
|
|
2281
2258
|
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
2259
|
`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
|
|
2260
|
+
`lease expired with the center unreachable, and how far from now a drain request's deadlineAt may be — "0" and "infinite" are not values ` +
|
|
2261
|
+
`(the bound must be finite and non-trivial). Refusing to boot.`);
|
|
2284
2262
|
}
|
|
2285
2263
|
return n;
|
|
2286
2264
|
}
|
|
@@ -2293,9 +2271,9 @@ function parseFleetLeaseFailClosed(raw, requirePrincipal) {
|
|
|
2293
2271
|
function parseCenterBaseUrl(name, raw, role) {
|
|
2294
2272
|
if (raw === undefined || raw === "")
|
|
2295
2273
|
return undefined;
|
|
2296
|
-
const
|
|
2297
|
-
if (
|
|
2298
|
-
throw new Error(
|
|
2274
|
+
const refusal = pathBaseUrlRefusal(`env ${name}`, raw);
|
|
2275
|
+
if (refusal !== undefined)
|
|
2276
|
+
throw new Error(`${refusal}. ${role}`);
|
|
2299
2277
|
return raw;
|
|
2300
2278
|
}
|
|
2301
2279
|
function parseConfigCenter(baseUrl, requirePrincipal, liveBaseUrl, webBaseUrl) {
|
|
@@ -2322,20 +2300,13 @@ function parseA2aServe() {
|
|
|
2322
2300
|
`and that address is the operator's to declare (it is NOT derived from the request Host header: an anonymous discovery caller must not decide what our public card advertises). ` +
|
|
2323
2301
|
`Set A2A_SERVE_URL to this deployment's externally reachable /v1/a2a URL (e.g. "https://agent.example.com/v1/a2a").`);
|
|
2324
2302
|
}
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
parsed = undefined;
|
|
2331
|
-
}
|
|
2332
|
-
if (parsed === undefined || (parsed.protocol !== "http:" && parsed.protocol !== "https:")) {
|
|
2333
|
-
throw new Error(`A2A_SERVE_URL must be a parseable http(s) URL (e.g. "https://agent.example.com/v1/a2a"); got ${JSON.stringify(buildDisplaySafeUrl(rawUrl))}`);
|
|
2334
|
-
}
|
|
2335
|
-
if (parsed.username !== "" || parsed.password !== "") {
|
|
2336
|
-
throw new Error(`A2A_SERVE_URL must not carry userinfo (credentials before "@") — it is published verbatim on the ANONYMOUSLY readable agent card, so those credentials would be handed to every discovery caller. ` +
|
|
2337
|
-
`Inbound authentication on this endpoint is header-shaped (a service credential), not URL-embedded; strip the userinfo and keep the secret in the caller's headers.`);
|
|
2303
|
+
const urlFlaws = httpUrlFlaws(rawUrl, ["userinfo"]);
|
|
2304
|
+
if (urlFlaws.length > 0) {
|
|
2305
|
+
throw new Error(`${buildHttpUrlRefusal("A2A_SERVE_URL", rawUrl, urlFlaws)} — it is published verbatim on the ANONYMOUSLY readable agent card as this deployment's JSON-RPC endpoint ` +
|
|
2306
|
+
`(e.g. "https://agent.example.com/v1/a2a"), so it must be a plain http(s) address and must not hand credentials to every discovery caller: ` +
|
|
2307
|
+
`inbound authentication on this endpoint is header-shaped (a service credential), not URL-embedded.`);
|
|
2338
2308
|
}
|
|
2309
|
+
const parsed = new URL(rawUrl);
|
|
2339
2310
|
const a2aCredentialScan = redactSecrets(parsed.href);
|
|
2340
2311
|
if (a2aCredentialScan !== parsed.href) {
|
|
2341
2312
|
throw new Error(`A2A_SERVE_URL carries something the credential-redaction law recognizes (${a2aCredentialScan}) — this address is published VERBATIM on the ANONYMOUSLY readable agent card (the card's url is a functional address: peers fetch it, so it is never rewritten for display), ` +
|
|
@@ -2400,7 +2371,7 @@ const STORE_GROUP_KEYS = [
|
|
|
2400
2371
|
];
|
|
2401
2372
|
const MODEL_PLANE_GROUP_KEYS = [
|
|
2402
2373
|
"gatewayBaseUrl", "gatewayApiKey", "gatewayFallbackUrls", "gatewayMaxRetries", "anthropic", "resilience", "model", "envModel", "models",
|
|
2403
|
-
"modelApiKeyEnv", "modelApiKeys", "modelRefusals", "modelQuotaWeights", "tiers", "projects", "roles", "atModelAllowlist", "cascadeLadder", "degrade",
|
|
2374
|
+
"modelApiKeyEnv", "modelApiKeys", "modelRefusals", "droppedCatalogEntries", "modelQuotaWeights", "tiers", "projects", "roles", "atModelAllowlist", "cascadeLadder", "degrade",
|
|
2404
2375
|
];
|
|
2405
2376
|
const APPROVAL_GROUP_KEYS = [
|
|
2406
2377
|
"approvalRequire", "approvalDeny", "approvalTimeoutSec", "approvalAutoBudget", "approvalNeverAuto", "approvalGrant",
|
|
@@ -2539,15 +2510,9 @@ export function loadConfig() {
|
|
|
2539
2510
|
throw new Error(laneRequiredRefusal);
|
|
2540
2511
|
}
|
|
2541
2512
|
{
|
|
2542
|
-
const
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
gatewayFallbackUrls: modelPlane.gatewayFallbackUrls,
|
|
2546
|
-
anthropic: modelPlane.anthropic,
|
|
2547
|
-
models: envLaneDispatchable ? modelPlane.models : {},
|
|
2548
|
-
});
|
|
2549
|
-
if (refusal !== undefined) {
|
|
2550
|
-
throw new Error(refusal);
|
|
2513
|
+
const routeGap = judgeModelRouteGap(process.env, { envLaneDispatchable: !isRosterGateArmed(integrations.configCenter) });
|
|
2514
|
+
if (routeGap.gap !== "none") {
|
|
2515
|
+
throw new Error(routeGap.reason);
|
|
2551
2516
|
}
|
|
2552
2517
|
}
|
|
2553
2518
|
EXEC_LANE_NOTICE = buildExecLaneNotice(orchestration.remoteExec);
|
|
@@ -8,6 +8,12 @@
|
|
|
8
8
|
* 非编排路径 = 停止信号(SIGTERM)/ SIGHUP 升级 / 附着租约自退,动作号由引擎自铸({@link ENGINE_DRAIN_CAUSES})。全部汇到同一只入口。
|
|
9
9
|
* 2. **期限在进入那一拍定死**:请求带的 `deadlineAt` 原样;引擎自起的排空 = 起点 + 此刻的排空窗(`drainGraceMs`)。之后改窗(中心热改
|
|
10
10
|
* `limits.drainGraceMs`)不动在跑的期限 —— 排空驱动读的是期限,不是窗。
|
|
11
|
+
* 请求带的期限有**准入上界** = 进入那一拍 + `config.fleetLeaseOutageMaxMs`(`T_max`,env `FLEET_LEASE_OUTAGE_MAX_MS`,`loadConfig` 无条件
|
|
12
|
+
* 解析、缺省 24 h,成员与非成员同读):一台实例「活着而不服务」最长多久本就由这一席定义(服务租约失联后最多再接单多久,
|
|
13
|
+
* `fleet/serving-lease.ts`),排空口不另立第二个数。上界由调用方随入参声明({@link DrainEntry.deadlineBoundMs};排空口声明它,引擎自起的
|
|
14
|
+
* 排空不声明 —— 那些期限由引擎自己的窗定,拒了就是不排空),**由入口判**:只有这一发会进入排空时才判(已在排空 / 硬停已在进行 ⇒ 期限
|
|
15
|
+
* 不会被采用,照规则 3 回执);越界 ⇒ 零状态变化,入口答 {@link DrainDeadlineRefusal}(排空口 400 `request.field_invalid`)。「会不会
|
|
16
|
+
* 采用这只期限」只有入口一处判 —— 调用方不另读 `draining` 去猜。已过的期限不越界(进入即到期:在飞为零当拍硬停,否则下一拍)。
|
|
11
17
|
* 3. **幂等**:已在排空 ⇒ 不重置,回执 `already_draining` 带**第一次**的动作 / 起点 / 期限(不论这次的动作号是否相同)。
|
|
12
18
|
* 4. **身份**:逐字段比,任一不符 ⇒ 409 `conflict.fleet_identity`,零状态变化。
|
|
13
19
|
* · `scope`:两侧都经 {@link fleetScopeOf} 读成值(缺席 = settings-schema 的缺省 scope,线上任何一处缺席的 scope 都读成它)再严格相等。
|
|
@@ -46,25 +52,39 @@ export declare function leaseDrainActionId(generation: string): LeaseDrainAction
|
|
|
46
52
|
export declare const DRAIN_CAUSE_COUNTS_AS_FIRST_STOP_SIGNAL: {
|
|
47
53
|
readonly [C in EngineDrainCause]: boolean;
|
|
48
54
|
};
|
|
49
|
-
/** 入口的入参:动作号 + 期限(epoch ms)。 */
|
|
55
|
+
/** 入口的入参:动作号 + 期限(epoch ms)+ 调用方声明的期限准入上界(规则 2)。 */
|
|
50
56
|
export interface DrainEntry {
|
|
51
57
|
readonly actionId: string;
|
|
52
58
|
readonly deadlineAt: number;
|
|
59
|
+
/** 期限的准入上界(ms):这一发会进入排空而 `deadlineAt − 进入那一拍 > 它` ⇒ 入口零状态变化,答 {@link DrainDeadlineRefusal}。
|
|
60
|
+
* 排空口传 `config.fleetLeaseOutageMaxMs`;引擎自起的排空(停止信号 / 租约被拒 / 附着租约自退)不传。 */
|
|
61
|
+
readonly deadlineBoundMs?: number;
|
|
53
62
|
}
|
|
63
|
+
/** 入口拒了这只期限(规则 2):越过调用方声明的上界,什么都没变。 */
|
|
64
|
+
export interface DrainDeadlineRefusal {
|
|
65
|
+
readonly state: "deadline_beyond_bound";
|
|
66
|
+
readonly boundMs: number;
|
|
67
|
+
}
|
|
68
|
+
/** 入口的答:契约回执(进入 / 已在排空),或期限越界的拒(只在调用方声明了上界时出现)。按 `state` 穷举。 */
|
|
69
|
+
export type DrainEntryAnswer = DrainReceipt | DrainDeadlineRefusal;
|
|
54
70
|
/**
|
|
55
71
|
* 引擎与 HTTP 面共享的排空盒子(装配层建一只,`createHttpServer` 与收尾链各自往上装读口 / 入口)。
|
|
56
72
|
* · `draining` / `since` / `actionId` / `deadlineAt` —— **只由** {@link enter} 整组写入(排空事实 = 契约 `EngineDraining`);
|
|
57
73
|
* · `enter` —— 唯一入口(收尾链装;缺席 = 这个进程没有收尾链,如嵌入宿主 / 测试夹具);
|
|
58
74
|
* · `yieldInflight` —— 让出本副本在飞的可抢占腿(`createHttpServer` 装);
|
|
59
|
-
* ·
|
|
75
|
+
* · `endSubscriptions` —— 关掉本副本打开着的只读订阅流(发一帧 `draining` 终帧再关;`createHttpServer` 装,S-1281)。订阅流不是在飞的工作
|
|
76
|
+
* (不进 `inflight`),排空不等它;收尾链在排空进入那一拍与硬停起点各调一次(排空期间新开的订阅在硬停那一拍收);
|
|
77
|
+
* · 其余四只是既有的读口(在飞数 / 活性时钟 / 附着流),语义见 `http/server.ts`。`inflight` 只数**工作**(run 腿 / 计费提交 / HITL 应答 /
|
|
78
|
+
* admission 期的普通请求),只读订阅只进 `attachedStreams`(S-1281)。
|
|
60
79
|
*/
|
|
61
80
|
export interface DrainState {
|
|
62
81
|
draining: boolean;
|
|
63
82
|
since?: number;
|
|
64
83
|
actionId?: string;
|
|
65
84
|
deadlineAt?: number;
|
|
66
|
-
enter?: (entry: DrainEntry) =>
|
|
85
|
+
enter?: (entry: DrainEntry) => DrainEntryAnswer;
|
|
67
86
|
yieldInflight?: () => void;
|
|
87
|
+
endSubscriptions?: () => void;
|
|
68
88
|
inflight?: () => number;
|
|
69
89
|
lastActivityAt?: () => number;
|
|
70
90
|
attachedStreams?: () => number;
|
|
@@ -72,6 +92,11 @@ export interface DrainState {
|
|
|
72
92
|
}
|
|
73
93
|
/** 排空盒子里的排空事实(三键齐才是;入口整组写,所以「缺一」只出现在手搭的夹具上)。 */
|
|
74
94
|
export declare function drainingFactOf(s: Pick<DrainState, "draining" | "since" | "actionId" | "deadlineAt">): EngineDraining | undefined;
|
|
95
|
+
/**
|
|
96
|
+
* 规则 2 的准入上界:期限离进入那一拍(本引擎的挂钟)超过 `boundMs` ⇔ 越界。恰好等于上界不越界;已过的期限不越界。入口判它
|
|
97
|
+
* (`boot/shutdown.ts` 的 enterDrain,在「已在排空 / 硬停已在进行」两臂之后)。
|
|
98
|
+
*/
|
|
99
|
+
export declare function drainDeadlineBeyondBound(deadlineAt: number, now: number, boundMs: number): boolean;
|
|
75
100
|
/** 本进程的身份。`scope` 已读成值(`config.fleetScope`);其余三项与 `/health` 上读到的同一份。 */
|
|
76
101
|
export interface EngineIdentity {
|
|
77
102
|
readonly scope: string;
|
|
@@ -13,6 +13,9 @@ export const DRAIN_CAUSE_COUNTS_AS_FIRST_STOP_SIGNAL = {
|
|
|
13
13
|
export function drainingFactOf(s) {
|
|
14
14
|
return s.draining && s.actionId !== undefined && s.since !== undefined && s.deadlineAt !== undefined ? { actionId: s.actionId, since: s.since, deadlineAt: s.deadlineAt } : undefined;
|
|
15
15
|
}
|
|
16
|
+
export function drainDeadlineBeyondBound(deadlineAt, now, boundMs) {
|
|
17
|
+
return deadlineAt - now > boundMs;
|
|
18
|
+
}
|
|
16
19
|
export const DRAIN_IDENTITY_FIELDS = ["scope", "worker", "expectedGeneration", "instanceId"];
|
|
17
20
|
export function drainIdentityMismatches(req, self) {
|
|
18
21
|
return DRAIN_IDENTITY_FIELDS.filter((f) => {
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*
|
|
6
6
|
* 1. **一只铸点,三个出口**:{@link mintEngineSignal} 是「引擎此刻自己说自己什么」的唯一来源。`GET /health`(诊断面)、
|
|
7
7
|
* `GET /readyz`(就绪探针)与 fleet 心跳(`fleet-client.ts`)各读它**一次**,再按各自的出口形投影;没有出口另算一遍
|
|
8
|
-
* 就绪 / 重启 / 代际 /
|
|
8
|
+
* 就绪 / 排空 / 重启 / 代际 / 租约 / 世代账,也没有出口绕过它去读 `deps` 上的这些输入。
|
|
9
9
|
* 2. **就绪**仍只有一只谓词(`http/readiness.ts` 的 `readinessOf`),铸点把它的判决原样带上(`verdict`)。`/readyz` 200 ⇔
|
|
10
10
|
* `ready ∧ ¬draining ∧ leaseState ≠ denied` —— 后两项不是新判据:`draining` 本来就是就绪原因,而租约被拒**同一拍**经
|
|
11
11
|
* 唯一的排空入口进入排空(`fleet/serving-lease.ts`),所以「被拒 ⇒ 排空 ⇒ 不就绪」是构造出来的,不靠第二只谓词。
|
|
@@ -15,21 +15,25 @@
|
|
|
15
15
|
* 是安全可重启的,扣着的片不阻止它们);被扣的片为空(只差发布重试)⇒ 不是重启事实,不铸(契约 `RestartSignal.reasons` 至少一词;
|
|
16
16
|
* 那一形的面只有每拍一行运维 warn + 计数)。「扣着」在两出口上只有这一种读法(7.120.x 的 `/health.modelPlaneDeferred` 本版删除)。
|
|
17
17
|
* 合成只在 {@link buildRestartSignal} 一处,`restartState()` 是它唯一的调用方 —— 两个出口读的是同一只取值口。
|
|
18
|
-
* 4.
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
18
|
+
* 4. **代际**:`generation` = env `FLEET_GENERATION` 的回声(规范十进制串;非规范在 boot 拒启,见 `config.ts`);缺席 ⇔ 非 fleet 成员。
|
|
19
|
+
* **身份不是引擎态**:`instanceId`(本进程起步自铸的那一只,`boot/stage-03` 的 uuidv7)是体的身份,只在两出口体的**顶层**说一次,
|
|
20
|
+
* 铸点不读它、契约形里不重复它。
|
|
21
|
+
* 5. **两形一读(展开 / 收缩)**:两出口今天各发两形 —— 嵌套键 {@link ENGINE_SIGNAL_KEY}(契约 `EngineSignal` 整只,{@link buildEngineSignal}),
|
|
22
|
+
* 与 8.0 扁平方言(布尔 `draining` + `drainingSince`、`/health` 就绪时不带 `ready`、`restart`、成员三键、`/health` 的世代账)。两形读的是
|
|
23
|
+
* **同一次**铸点读数里的**同一个字段**,投影全在本模块({@link buildEngineSignal} / {@link heartbeatReadinessKeys} / {@link healthDrainingKeys} /
|
|
24
|
+
* {@link fleetMembershipKeys} / {@link healthConfigLedgerKeys}),出口文件不拼引擎态的键。排空在两形上读的都是排空**事实**(`EngineDraining`,
|
|
25
|
+
* 三键齐才在),不是就绪原因 —— 所以「`engineSignal.draining` 在场 ⇔ 扁平 `draining` 为真、`drainingSince` = `engineSignal.draining.since`」
|
|
26
|
+
* 是构造出来的,不靠「唯一入口整组写四键」这条运行时不变量(那条不变量只管就绪原因与事实一致)。
|
|
27
|
+
* 心跳体 = 契约 `InstanceAnnouncement` + 嵌套键这一只:契约包 10.x 的心跳形还没有它的座位,10.x 中心按默认 strip 解析丢掉它、照收心跳;
|
|
28
|
+
* 世代账扁平两键契约心跳形没有座位,只上 `/health`(嵌套形里有,两出口都带)。非 fleet 部署的扁平键与 7.122.0 逐字同。
|
|
29
|
+
* 收缩:扁平布尔对(`draining` / `drainingSince`)settings-schema 12.0.0 从契约形删、本引擎 7.124.0+ 删发(届时删它们的投影);嵌套键名在
|
|
30
|
+
* 契约包自有这只常量的那一版提货时改读它。
|
|
27
31
|
* 6. **租约四态的词表属主是契约包**(`LeaseState`):本模块的读数型 {@link ServingLeaseView} 按它逐词对齐(下面的编译期断言两向钉),
|
|
28
32
|
* 契约加一个词 ⇒ 本仓编译红,不会悄悄多出一个本仓读不出的态。
|
|
29
33
|
*
|
|
30
|
-
* 模块形:纯函数(
|
|
34
|
+
* 模块形:纯函数(契约包只 import 型;值 import 只有两只纯叶 `http/readiness.ts` 与 `fleet/drain-entry.ts`)—— `http/` 与 `fleet-client.ts` 都读它,不许长出回环。
|
|
31
35
|
*/
|
|
32
|
-
import type { EngineDraining, InstanceAnnouncement, LeaseState, RestartSignal } from "@sema-agent/settings-schema/fleet";
|
|
36
|
+
import type { EngineDraining, EngineSignal, InstanceAnnouncement, LeaseState, RestartSignal } from "@sema-agent/settings-schema/fleet";
|
|
33
37
|
import { type ReadinessInputs, type ReadinessVerdict } from "../http/readiness.js";
|
|
34
38
|
/**
|
|
35
39
|
* 租约此刻的读数(租约客户端交出;纯数据)。态的词表 = 契约 `LeaseState`(四词的语义在契约注里:`none` 不生效 / `granted` 持有 /
|
|
@@ -94,9 +98,16 @@ export declare function configHandoffMissing(ledger: ConfigHandoffLedger): boole
|
|
|
94
98
|
/** `configHandoff` 的**唯一**合成点(纯):缺着 ⇒ `{ state: "missing", version: 应用着的那一代, since }`;否则缺席。`since` 由持账方锁存(发现缺着的那一拍)。
|
|
95
99
|
* `build*` = 纯数据;config-center 的 `configHandoffState()` 是唯一调用方。 */
|
|
96
100
|
export declare function buildConfigHandoff(ledger: ConfigHandoffLedger, since: number | undefined): ConfigHandoff | undefined;
|
|
101
|
+
/** 世代账的一次读数(config-center 的 `configApplyState()` 交出的那一份里本模块读的五键;`/health` 扁平五键与嵌套形的两键读同一份)。 */
|
|
102
|
+
export interface ConfigLedgerReading {
|
|
103
|
+
readonly configTargetVersion: number;
|
|
104
|
+
readonly configAppliedVersion: number;
|
|
105
|
+
readonly configTargetOrdinal: number;
|
|
106
|
+
readonly configAppliedOrdinal: number;
|
|
107
|
+
readonly configApplyStaleMs?: number;
|
|
108
|
+
}
|
|
97
109
|
/** 铸点读的输入 —— `ServiceDeps` 上的同名键(HTTP 面与心跳读的是**同一个**对象)。 */
|
|
98
110
|
export interface EngineSignalInputs extends ReadinessInputs {
|
|
99
|
-
readonly instanceId?: string;
|
|
100
111
|
/** `config.fleetGeneration` = env `FLEET_GENERATION` 的回声(boot 已判规范;缺席 ⇔ 非 fleet 成员)。 */
|
|
101
112
|
readonly config?: {
|
|
102
113
|
readonly fleetGeneration?: string;
|
|
@@ -108,34 +119,54 @@ export interface EngineSignalInputs extends ReadinessInputs {
|
|
|
108
119
|
};
|
|
109
120
|
readonly restartState?: () => RestartSignal | undefined;
|
|
110
121
|
readonly servingLease?: () => ServingLeaseView;
|
|
111
|
-
readonly configApplyState?: () =>
|
|
112
|
-
readonly configTargetVersion: number;
|
|
113
|
-
readonly configAppliedVersion: number;
|
|
114
|
-
} | undefined;
|
|
122
|
+
readonly configApplyState?: () => ConfigLedgerReading | undefined;
|
|
115
123
|
}
|
|
116
124
|
/** 铸出的信号(纯数据)。 */
|
|
117
125
|
export interface EngineSignalFacts {
|
|
118
126
|
/** 就绪判(`readinessOf` 原样)。 */
|
|
119
127
|
readonly verdict: ReadinessVerdict;
|
|
120
|
-
/** 在场 ⇔ 排空中(契约 `EngineDraining`:动作 / 起点 / 期限)。 */
|
|
128
|
+
/** 在场 ⇔ 排空中(契约 `EngineDraining`:动作 / 起点 / 期限)—— 两出口两形读排空的唯一字段(规则 5)。 */
|
|
121
129
|
readonly draining?: EngineDraining;
|
|
122
130
|
readonly restart?: RestartSignal;
|
|
123
131
|
readonly generation?: string;
|
|
124
|
-
readonly instanceId?: string;
|
|
125
132
|
readonly lease: ServingLeaseView;
|
|
126
|
-
|
|
127
|
-
readonly
|
|
133
|
+
/** 世代账的那一次读数(未接线 ⇒ 缺席)。 */
|
|
134
|
+
readonly configApply?: ConfigLedgerReading;
|
|
128
135
|
}
|
|
129
136
|
/** **唯一**铸点(活判,每次调用现读输入)。 */
|
|
130
137
|
export declare function mintEngineSignal(deps: EngineSignalInputs): EngineSignalFacts;
|
|
138
|
+
/**
|
|
139
|
+
* 两出口体上嵌套引擎信号的键名(规则 5;两出口同一只拼法)。契约包 10.x 没有这只常量(它的心跳形也没有这一座位);契约包自有它的那一版
|
|
140
|
+
* 提货时,本常量删、改读契约包的。
|
|
141
|
+
*/
|
|
142
|
+
export declare const ENGINE_SIGNAL_KEY = "engineSignal";
|
|
143
|
+
/** 体上的嵌套引擎信号:键 {@link ENGINE_SIGNAL_KEY},值 = 契约 `EngineSignal`。 */
|
|
144
|
+
export type EngineSignalCarrier = {
|
|
145
|
+
readonly [ENGINE_SIGNAL_KEY]: EngineSignal;
|
|
146
|
+
};
|
|
147
|
+
/**
|
|
148
|
+
* 契约形 `EngineSignal` 的**唯一**投影(规则 5;两出口同调,同一次读数 ⇒ 同值同序)。键序照契约形:`ready` 恒说;`draining` = 排空事实;
|
|
149
|
+
* `restart` = 取值口交出的那个对象;成员三键 = {@link fleetMembershipKeys};世代账两键 = 同一次读数。身份不在这里(规则 4)。`build*` = 纯数据。
|
|
150
|
+
*/
|
|
151
|
+
export declare function buildEngineSignal(s: EngineSignalFacts): EngineSignal;
|
|
131
152
|
/**
|
|
132
153
|
* fleet 成员身份三键的**唯一**投影(两出口同调,同值同序):`generation` / `leaseState` / `leaseUntil`。
|
|
133
154
|
* 在场律:非 fleet 成员(无 `generation`)⇒ 空对象(两出口逐字节同 7.120.0);成员 ⇒ `generation` + `leaseState` 恒在,
|
|
134
155
|
* `leaseUntil` 只在 `granted`。
|
|
135
156
|
*/
|
|
136
157
|
export declare function fleetMembershipKeys(s: EngineSignalFacts): FleetMembershipKeys;
|
|
137
|
-
/**
|
|
138
|
-
export declare function
|
|
158
|
+
/** 心跳的 8.0 扁平方言 `ready` / `draining`(恒带,`false` 也带):`ready` = 就绪判,`draining` = 排空事实在场(规则 5,与嵌套形读同一个字段)。 */
|
|
159
|
+
export declare function heartbeatReadinessKeys(s: EngineSignalFacts): Pick<InstanceAnnouncement, "ready" | "draining">;
|
|
160
|
+
/** `/health` 的 8.0 扁平排空对:只在排空中带 —— `draining: true` + `drainingSince` = 排空事实的 `since`(规则 5,与嵌套形读同一个字段)。 */
|
|
161
|
+
export declare function healthDrainingKeys(s: EngineSignalFacts): {
|
|
162
|
+
readonly draining?: true;
|
|
163
|
+
readonly drainingSince?: number;
|
|
164
|
+
};
|
|
165
|
+
/**
|
|
166
|
+
* `/health` 的世代账扁平键(未接线 ⇒ 全缺席):target / applied 两版本(与嵌套形同一次读数)+ 两只收货序数(进程内单调,「见过多少代 / 停在哪一代」
|
|
167
|
+
* 的推进判据)+ `configApplyStaleMs`(两者持续不等的时长,收敛即缺席)。只上 `/health`(契约心跳形没有这些座位)。
|
|
168
|
+
*/
|
|
169
|
+
export declare function healthConfigLedgerKeys(s: EngineSignalFacts): Partial<ConfigLedgerReading>;
|
|
139
170
|
/**
|
|
140
171
|
* `/health.restartRequired`(冗余顶层布尔,壳读它决定要不要问用户重启)的投影:在场 ⇔ 有**可执行**的重启信号
|
|
141
172
|
* (`restart` 在场且未被扣)—— 与 7.120.0 的读数逐字同(那时被扣的候选根本不出 `restart`)。
|