@sema-agent/server 7.101.1 → 7.103.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 +84 -0
- package/README.md +4 -4
- package/README.zh-CN.md +4 -4
- package/USAGE.md +156 -9
- package/dist/approval-card.d.ts +35 -2
- package/dist/approval-card.js +19 -1
- package/dist/background-session-reap.d.ts +58 -0
- package/dist/background-session-reap.js +90 -0
- package/dist/bake-runner/main.d.ts +21 -3
- package/dist/bake-runner/main.js +18 -11
- package/dist/boot/config-center.d.ts +21 -4
- package/dist/boot/config-center.js +62 -19
- package/dist/boot/exit-boundary.js +17 -0
- package/dist/boot/runner-deps.js +1 -0
- package/dist/boot/runtime-caps.js +3 -2
- package/dist/boot/session-faces.js +3 -4
- package/dist/boot/shutdown.d.ts +2 -1
- package/dist/boot/shutdown.js +33 -12
- package/dist/boot/stage-01-config.js +2 -0
- package/dist/boot/stage-07-capability-layer.d.ts +1 -1
- package/dist/boot/stage-07-capability-layer.js +6 -4
- package/dist/boot/stage-08-reapers.d.ts +1 -1
- package/dist/boot/stage-09-leader.d.ts +1 -1
- package/dist/boot/stage-10-http-server.d.ts +1 -1
- package/dist/boot/stage-10-http-server.js +13 -4
- package/dist/brain.d.ts +28 -8
- package/dist/brain.js +20 -4
- package/dist/capabilities/builtin-tools.d.ts +14 -1
- package/dist/capabilities/builtin-tools.js +3 -0
- package/dist/capabilities/center-plugins.d.ts +9 -3
- package/dist/capabilities/center-plugins.js +5 -10
- package/dist/capabilities/scenarios.d.ts +1 -1
- package/dist/capabilities/scenarios.js +3 -3
- package/dist/center-credential-renewer.d.ts +76 -0
- package/dist/center-credential-renewer.js +359 -0
- package/dist/center-credential.d.ts +76 -22
- package/dist/center-credential.js +119 -43
- package/dist/config-catalog.js +17 -8
- package/dist/config-center/apply-effective.d.ts +16 -0
- package/dist/config-center/apply-effective.js +26 -21
- package/dist/config-center/center-request.d.ts +13 -0
- package/dist/config-center/center-request.js +23 -3
- package/dist/config-center/facade.d.ts +1 -1
- package/dist/config-center/http-client.js +2 -5
- package/dist/config-center/mcp-revocation.d.ts +2 -2
- package/dist/config-center/mcp-revocation.js +1 -1
- package/dist/config-center/read-warnings.d.ts +19 -48
- package/dist/config-center/read-warnings.js +7 -49
- package/dist/config-center/restart-signal.d.ts +8 -21
- package/dist/config-center/restart-signal.js +2 -12
- package/dist/config-center/types.d.ts +4 -2
- package/dist/config-provider.js +1 -48
- package/dist/config-types.d.ts +107 -17
- package/dist/config-types.js +7 -2
- package/dist/config.d.ts +7 -0
- package/dist/config.js +91 -29
- package/dist/device-store.d.ts +1 -1
- package/dist/env-name-allowlist-knobs.d.ts +21 -7
- package/dist/env-name-allowlist-knobs.js +7 -2
- package/dist/fleet-client.d.ts +27 -7
- package/dist/fleet-client.js +43 -12
- package/dist/fleet-lease.d.ts +1 -1
- package/dist/host-lsp-manager.d.ts +4 -3
- package/dist/host-lsp-manager.js +2 -6
- package/dist/http/approval-sweeps.js +5 -20
- package/dist/http/dispatch.js +24 -14
- package/dist/http/readiness.d.ts +87 -0
- package/dist/http/readiness.js +54 -0
- package/dist/http/resume-legs.js +5 -4
- package/dist/http/route-table.d.ts +7 -0
- package/dist/http/route-table.js +6 -0
- package/dist/http/routes/a2a-serve.js +6 -14
- package/dist/http/routes/approvals-assistant.js +1 -0
- package/dist/http/routes/background.d.ts +80 -0
- package/dist/http/routes/background.js +94 -0
- package/dist/http/routes/capabilities.js +2 -0
- package/dist/http/server.d.ts +25 -8
- package/dist/http/server.js +2 -2
- package/dist/leader/wire.d.ts +8 -1
- package/dist/leader/wire.js +4 -3
- package/dist/model-entry-refusal.d.ts +42 -19
- package/dist/model-entry-refusal.js +34 -19
- package/dist/model-route-endpoint.d.ts +10 -0
- package/dist/model-route-endpoint.js +6 -2
- package/dist/observability/fail-open.d.ts +2 -2
- package/dist/observability/fail-open.js +2 -2
- package/dist/observability/secret-env-scrub.d.ts +12 -0
- package/dist/observability/secret-env-scrub.js +12 -0
- package/dist/outbound-dispatcher.d.ts +125 -0
- package/dist/outbound-dispatcher.js +211 -0
- package/dist/parked-decide.d.ts +11 -5
- package/dist/parked-decide.js +4 -1
- package/dist/parked-revive-surface.d.ts +47 -0
- package/dist/parked-revive-surface.js +20 -0
- package/dist/plugins/background-shell-support.d.ts +20 -1
- package/dist/plugins/background-shell-support.js +26 -0
- package/dist/plugins/host-platform.d.ts +16 -1
- package/dist/plugins/host-platform.js +11 -0
- package/dist/plugins/remote-env-host.d.ts +14 -1
- package/dist/plugins/remote-env-host.js +15 -10
- package/dist/project-memory.js +3 -6
- package/dist/run-local.js +2 -0
- package/dist/server-secret-env.d.ts +97 -24
- package/dist/server-secret-env.js +37 -1
- package/dist/tool-approval.d.ts +30 -1
- package/dist/tool-approval.js +31 -8
- package/dist/trace/core-keyset-guard.d.ts +9 -4
- package/dist/trace/engine-notice-wire.d.ts +1 -1
- package/dist/trace/engine-notice-wire.js +2 -0
- package/dist/trace/project.js +13 -0
- package/dist/trace/redact.d.ts +11 -0
- package/dist/trace/redact.js +1 -0
- package/package.json +4 -3
package/dist/config.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type Model, type PathRuleBases, type RuleBehavior } from "@sema-agent/core";
|
|
2
2
|
import { type RemoteExecProvider } from "./execution-lane-caps.js";
|
|
3
3
|
import { type Autonomy } from "./runtime-governance.js";
|
|
4
|
+
import { type RunnerEnvNameAllowlistKnob, type ServerEnvNameAllowlistKnob } from "./env-name-allowlist-knobs.js";
|
|
4
5
|
import type { ServiceConfig, ServiceConfigFlat, ServiceConfigGroups } from "./config-types.js";
|
|
5
6
|
/** [ref] A1: the config TYPE face lives in the leaf module config-types.ts; re-exported here so
|
|
6
7
|
* every existing `from "./config.js"` importer compiles unchanged (pure-type consumers should prefer
|
|
@@ -31,6 +32,8 @@ export declare function parseUsageWindows(raw: string | undefined): readonly {
|
|
|
31
32
|
maxCostUsd?: number;
|
|
32
33
|
anchor: "first-use" | "rolling";
|
|
33
34
|
}[] | undefined;
|
|
35
|
+
/** 墓碑拒启码(机读;拒句方括号里)。 */
|
|
36
|
+
export declare const RETIRED_ENV_REFUSAL_CODE = "config.env_key_retired";
|
|
34
37
|
/** Numeric env with validation: a non-numeric value FAILS at startup instead of silently becoming `NaN`
|
|
35
38
|
* (which e.g. slips past the `runStaleSec` liveness guard and silently disables the reaper). */
|
|
36
39
|
export declare function numEnv(name: string, fallback: string): number;
|
|
@@ -231,6 +234,10 @@ export declare const hostExecSpoolEnabled: () => boolean;
|
|
|
231
234
|
* 门 = `test/deploy-lanes-contract.test.ts`,它读的是本表而不是任何副本。
|
|
232
235
|
*/
|
|
233
236
|
export declare const REQUIRED_ENVS_BY_LANE: Record<RemoteExecProvider, readonly string[]>;
|
|
237
|
+
/** 名单型 env 旋钮的**唯一**解析器(规则全文见上方 `ENV_NAME_ALLOWLIST_KNOBS` 头注)。`undefined` = 没配(未设 / 空串)。
|
|
238
|
+
* 服务侧旋钮要交本进程的姿态(③);runner 旋钮没有姿态,签名上就不收 —— 读者与行形由叶表的类型一起定,传错是编译红。 */
|
|
239
|
+
export declare function parseEnvNameAllowlist(knob: ServerEnvNameAllowlistKnob, singleUserTurnkey: boolean): readonly string[] | undefined;
|
|
240
|
+
export declare function parseEnvNameAllowlist(knob: RunnerEnvNameAllowlistKnob): readonly string[] | undefined;
|
|
234
241
|
/** [ref]① 1M dual-window (core 1.289 `Model.autoCompactTokens`): an EXPLICIT positive value wins (env
|
|
235
242
|
* `MODEL_AUTO_COMPACT_TOKENS` / center roster `autoCompactTokens`); else the **model-id table** applies —
|
|
236
243
|
* sonnet-5-family id ∧ contextWindow>=1e6 ⇒ 967000 (the faithful shape of CC's single-entry per-model
|
package/dist/config.js
CHANGED
|
@@ -4,13 +4,13 @@ import { homedir } from "node:os";
|
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { BUILTIN_COMPLIANCE_DENIES, COMPLIANCE_CAPABILITIES, CROSS_SESSION_INBOUND_SETTINGS, CROSS_SESSION_DIALOG_EXPIRY_VALUES, compileReadDeny, resolveReadDenyBuiltins, CODE_AGENT_PROMPT, formatUserScope, isModelPriced, PROTOCOL_TABLE, protocolOf, RECOMMENDED_SENSITIVE_PATTERNS, RETIRED_TOOL_NAMES, resolveFileHistoryRetention, resolveLockedKeys, validatePermissionRules, WRITE_PROTECTED_DEFAULT_TABLE, resolveWriteProtectedTable, parseRuleText } from "@sema-agent/core";
|
|
7
|
-
import { ROSTER_PRIMARY_ROLES, ROSTER_CHEAP_ROLES } from "@sema-agent/settings-schema";
|
|
7
|
+
import { ROSTER_PRIMARY_ROLES, ROSTER_CHEAP_ROLES, CENTER_CREDENTIAL_ENV } from "@sema-agent/settings-schema";
|
|
8
8
|
import { parseApprovalHmacKeys, parsePrincipalJwks } from "./auth-keys.js";
|
|
9
9
|
import { DEFAULT_ELICITATION_THROTTLE } from "./elicitation.js";
|
|
10
10
|
import { isV2ScopeKey, memoryReplicationCoverage } from "./memory-scope.js";
|
|
11
11
|
import { DEFAULT_QUESTION_THROTTLE } from "./question.js";
|
|
12
12
|
import { GIT_API_KINDS, isGitApiKind } from "./git-api-kind.js";
|
|
13
|
-
import { RETENTION_MODES, MEMORY_CONSOLIDATION_MIN_INTERVAL_SEC, RESILIENCE_DEFAULTS, TASK_WRITE_FACE_ESCALATIONS } from "./config-types.js";
|
|
13
|
+
import { RETENTION_MODES, MEMORY_CONSOLIDATION_MIN_INTERVAL_SEC, RESILIENCE_DEFAULTS, TASK_WRITE_FACE_ESCALATIONS, MAX_TIMER_DELAY_MS, AUTO_MODE_CLASSIFIER_TIMEOUT_BOUNDS, DRAIN_GRACE_FLOOR_MS, DRAIN_SECOND_SIGNAL_MIN_GAP_BOUNDS, DRAIN_SECOND_SIGNAL_MIN_GAP_DEFAULT_MS, FIRST_BYTE_TIMEOUT_BOUNDS } from "./config-types.js";
|
|
14
14
|
import { buildWriteProtectionPosture } from "./write-protection.js";
|
|
15
15
|
import { normalizeSshFingerprint } from "./ssh-host-key.js";
|
|
16
16
|
import { readFaceForPosture } from "./read-face-posture.js";
|
|
@@ -20,12 +20,13 @@ import { CONTROL_AND_BIDI_STRIP_RE } from "./text-bidi.js";
|
|
|
20
20
|
import { REMOTE_EXEC_PROVIDER_WORDS, executionLaneOf } from "./execution-lane-caps.js";
|
|
21
21
|
import { AUTONOMY_MODES } from "./runtime-governance.js";
|
|
22
22
|
import { createCenterCredentialSource, readCenterCredentialDeclaration } from "./center-credential.js";
|
|
23
|
+
import { parseCenterCredentialRenewLeadMs } from "./center-credential-renewer.js";
|
|
23
24
|
import { modelProviderRefusal } from "./model-provider.js";
|
|
24
25
|
import { modelRouteUrlRefusal } from "./model-route-url.js";
|
|
25
26
|
import { modelRouteEndpointRefusal } from "./model-route-endpoint.js";
|
|
26
27
|
import { buildDisplaySafeUrl, redactSecrets } from "./trace/redact.js";
|
|
27
28
|
import { allowlistedEnv } from "./server-secret-env.js";
|
|
28
|
-
import { ENV_NAME_ALLOWLIST_KNOBS, describeAllowlistPostures } from "./env-name-allowlist-knobs.js";
|
|
29
|
+
import { ENV_NAME_ALLOWLIST_KNOBS, ENV_NAME_ALLOWLIST_KNOB_NAMES, describeAllowlistPostures } from "./env-name-allowlist-knobs.js";
|
|
29
30
|
export { RETENTION_MODES, MEMORY_CONSOLIDATION_MIN_INTERVAL_SEC } from "./config-types.js";
|
|
30
31
|
function csv(name) {
|
|
31
32
|
return (process.env[name] ?? "")
|
|
@@ -103,10 +104,11 @@ function defaultSkillsDir() {
|
|
|
103
104
|
return "skills";
|
|
104
105
|
}
|
|
105
106
|
}
|
|
107
|
+
export const RETIRED_ENV_REFUSAL_CODE = "config.env_key_retired";
|
|
106
108
|
const RETIRED_ENVS = [
|
|
107
109
|
["TIDB_HOST", "MYSQL_HOST", "renamed"], ["TIDB_PORT", "MYSQL_PORT", "renamed"], ["TIDB_USER", "MYSQL_USER", "renamed"],
|
|
108
110
|
["TIDB_PASSWORD", "MYSQL_PASSWORD", "renamed"], ["TIDB_DATABASE", "MYSQL_DATABASE", "renamed"], ["TIDB_POOL_SIZE", "MYSQL_POOL_SIZE", "renamed"],
|
|
109
|
-
["CONFIG_CENTER_URL", "SEMA_REGISTRY_URL", "renamed"], ["CONFIG_CENTER_TOKEN",
|
|
111
|
+
["CONFIG_CENTER_URL", "SEMA_REGISTRY_URL", "renamed"], ["CONFIG_CENTER_TOKEN", CENTER_CREDENTIAL_ENV.token, "renamed"],
|
|
110
112
|
["CONFIG_CENTER_DRY_RUN", "SEMA_REGISTRY_DRY_RUN", "renamed"], ["CONFIG_CENTER_WORKER", "SEMA_REGISTRY_WORKER", "renamed"],
|
|
111
113
|
["ANTHROPIC_BASEURL", "ANTHROPIC_BASE_URL", "renamed"],
|
|
112
114
|
["REGISTRY_ADMIN_TOKEN", "SEMA_REGISTRY_ADMIN_TOKEN", "renamed"],
|
|
@@ -119,22 +121,33 @@ const RETIRED_ENVS = [
|
|
|
119
121
|
["OA_ISSUE_LABELS", "the OA scenario was removed in 5.0.0", "removed"],
|
|
120
122
|
["APPROVAL_POLL_MS", "the OA poll-approval lane was removed in 5.0.0; the durable checkpoint lane is the only gate", "removed"],
|
|
121
123
|
["LEADER_CONFLICT_ROUNDS", "the bounded merge-conflict-resolution agent was deleted in 7.83.0 按 C-R63 删除,合不上交用户:leader 交回 result.conflict(分支 + 冲突文件),run 状态 needs_human", "removed"],
|
|
124
|
+
[
|
|
125
|
+
"MODEL_CONNECT_TIMEOUT_MS",
|
|
126
|
+
"MODEL_FIRST_BYTE_TIMEOUT_MS",
|
|
127
|
+
"replaced",
|
|
128
|
+
"since core 7.33.0 the TCP + TLS connect clock is the transport's own; the wait from sending the request to its response headers — " +
|
|
129
|
+
"what this key really bounded — is MODEL_FIRST_BYTE_TIMEOUT_MS (unset = core's own default, plus an upload allowance and one " +
|
|
130
|
+
"widened retry; 0 = no engine clock on that segment and the transport's header / body clocks off too — the TCP + TLS connect " +
|
|
131
|
+
"keeps the transport's own clock)",
|
|
132
|
+
],
|
|
122
133
|
];
|
|
134
|
+
const RETIRED_ENV_SENTENCE = {
|
|
135
|
+
renamed: (hits) => `${hits.map(([old, to]) => `${old} → use ${to}`).join("; ")} — rename the env key(s); the value semantics are unchanged`,
|
|
136
|
+
replaced: (hits) => `${hits.map(([old, to, , why]) => `${old} → ${to} (${why})`).join("; ")} — set the replacement deliberately: ` +
|
|
137
|
+
`its value semantics differ from the retired key's, so do not copy the old value across unread`,
|
|
138
|
+
removed: (hits) => `${hits.map(([old, why]) => `${old} (${why})`).join("; ")} — remove the env key(s): the capability itself was retired, ` +
|
|
139
|
+
`there is no replacement name and no rename that restores it`,
|
|
140
|
+
};
|
|
141
|
+
function retiredEnvSentence(kind, hit) {
|
|
142
|
+
const ofKind = hit.filter((h) => h[2] === kind);
|
|
143
|
+
return ofKind.length > 0 ? [RETIRED_ENV_SENTENCE[kind](ofKind)] : [];
|
|
144
|
+
}
|
|
123
145
|
function rejectRetiredEnvs() {
|
|
124
146
|
const hit = RETIRED_ENVS.filter(([o]) => process.env[o] !== undefined && process.env[o] !== "");
|
|
125
147
|
if (hit.length === 0)
|
|
126
148
|
return;
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
const parts = [];
|
|
130
|
-
if (renamed.length > 0) {
|
|
131
|
-
parts.push(`${renamed.map(([o, n]) => `${o} → use ${n}`).join("; ")} — rename the env key(s); the value semantics are unchanged`);
|
|
132
|
-
}
|
|
133
|
-
if (removed.length > 0) {
|
|
134
|
-
parts.push(`${removed.map(([o, n]) => `${o} (${n})`).join("; ")} — remove the env key(s): the capability itself was retired, ` +
|
|
135
|
-
`there is no replacement name and no rename that restores it`);
|
|
136
|
-
}
|
|
137
|
-
throw new Error(`retired env name(s) set ([2354] 兼容面全清,5.0.0 起拒启): ${parts.join(" | ")}.`);
|
|
149
|
+
const parts = Object.keys(RETIRED_ENV_SENTENCE).flatMap((kind) => retiredEnvSentence(kind, hit));
|
|
150
|
+
throw new Error(`retired env name(s) set [${RETIRED_ENV_REFUSAL_CODE}] ([2354] 兼容面全清,5.0.0 起拒启): ${parts.join(" | ")}.`);
|
|
138
151
|
}
|
|
139
152
|
export function numEnv(name, fallback) {
|
|
140
153
|
return parseNumOrFail(name, env(name, fallback));
|
|
@@ -490,9 +503,8 @@ function parseNameListEnv(name, raw, opts) {
|
|
|
490
503
|
return names;
|
|
491
504
|
}
|
|
492
505
|
const ENV_VAR_NAME_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
493
|
-
const ENV_NAME_ALLOWLIST_KNOB_NAMES = new Set(Object.keys(ENV_NAME_ALLOWLIST_KNOBS));
|
|
494
506
|
const ENV_NAME_ALLOWLIST_WITHHELD = "<withheld: entries are not echoed — a value pasted in place of a name can pass the name check>";
|
|
495
|
-
function parseEnvNameAllowlist(knob, singleUserTurnkey) {
|
|
507
|
+
export function parseEnvNameAllowlist(knob, singleUserTurnkey) {
|
|
496
508
|
const raw = process.env[knob];
|
|
497
509
|
if (raw === undefined || raw === "")
|
|
498
510
|
return undefined;
|
|
@@ -512,7 +524,7 @@ function parseEnvNameAllowlist(knob, singleUserTurnkey) {
|
|
|
512
524
|
" — refusing to start rather than silently allowing a partial list (#157). The value is not echoed (a pasted NAME=value pair may carry a secret)." +
|
|
513
525
|
` Write ${knob}=NAME,NAME with bare variable names, e.g. ${spec.example}.`);
|
|
514
526
|
});
|
|
515
|
-
if (
|
|
527
|
+
if ("multiTenant" in spec && singleUserTurnkey !== true) {
|
|
516
528
|
const posture = spec.multiTenant;
|
|
517
529
|
switch (posture.kind) {
|
|
518
530
|
case "ignored":
|
|
@@ -664,6 +676,36 @@ function optionalEnumKey(key, envName, raw, allowed) {
|
|
|
664
676
|
}
|
|
665
677
|
return { [key]: raw };
|
|
666
678
|
}
|
|
679
|
+
function parseAutoModeClassifierTimeoutMs(raw) {
|
|
680
|
+
if (raw === undefined || raw === "")
|
|
681
|
+
return {};
|
|
682
|
+
const { minMs, maxMs } = AUTO_MODE_CLASSIFIER_TIMEOUT_BOUNDS;
|
|
683
|
+
const n = /^[1-9]\d*$/.test(raw) ? Number(raw) : Number.NaN;
|
|
684
|
+
if (!Number.isSafeInteger(n) || n < minMs || n > maxMs) {
|
|
685
|
+
const shown = raw.length > 40 ? `${raw.slice(0, 40)}…(${raw.length} chars)` : raw;
|
|
686
|
+
throw new Error(`env AUTO_MODE_CLASSIFIER_TIMEOUT_MS=${JSON.stringify(shown)} must be a whole number of milliseconds in [${minMs}, ${maxMs}] ` +
|
|
687
|
+
`(decimal digits only; unset = core's own default). It caps ONE rung of the auto-mode classifier ladder — a rung past the cap ` +
|
|
688
|
+
`did not rule, and with no rung left the gated call is DENIED. "0" has no "off" meaning on this seat; a sub-second value is the ` +
|
|
689
|
+
`seconds-for-milliseconds slip; above ${maxMs} Node collapses the timer to 1ms. Refusing to boot rather than running the core ` +
|
|
690
|
+
`default while the operator believes their value is in force (#210).`);
|
|
691
|
+
}
|
|
692
|
+
return { autoModeClassifierTimeoutMs: n };
|
|
693
|
+
}
|
|
694
|
+
function parseFirstByteTimeoutMs(raw) {
|
|
695
|
+
if (raw === undefined || raw === "")
|
|
696
|
+
return {};
|
|
697
|
+
const { minMs, maxMs } = FIRST_BYTE_TIMEOUT_BOUNDS;
|
|
698
|
+
const n = /^(0|[1-9]\d*)$/.test(raw) ? Number(raw) : Number.NaN;
|
|
699
|
+
if (!Number.isSafeInteger(n) || n < minMs || n > maxMs) {
|
|
700
|
+
const shown = raw.length > 40 ? `${raw.slice(0, 40)}…(${raw.length} chars)` : raw;
|
|
701
|
+
throw new Error(`env MODEL_FIRST_BYTE_TIMEOUT_MS=${JSON.stringify(shown)} must be a number of milliseconds: a whole number in [${minMs}, ${maxMs}] ` +
|
|
702
|
+
`(decimal digits only; unset = core's own default). It bounds the wait from sending the request to its response headers; "0" = no engine clock on ` +
|
|
703
|
+
`that segment and the transport's header / body clocks off too (the TCP + TLS connect keeps the transport's own clock); ` +
|
|
704
|
+
`above ${maxMs} Node collapses the timer to 1ms. Refusing to boot rather ` +
|
|
705
|
+
`than running the core default while the operator believes their value is in force (#210).`);
|
|
706
|
+
}
|
|
707
|
+
return { firstByteTimeoutMs: n };
|
|
708
|
+
}
|
|
667
709
|
function headerNameEnv(name, fallback) {
|
|
668
710
|
const v = env(name, fallback);
|
|
669
711
|
if (!/^[A-Za-z0-9_-]+$/.test(v))
|
|
@@ -1029,7 +1071,7 @@ function parseModelDomain() {
|
|
|
1029
1071
|
}
|
|
1030
1072
|
: undefined,
|
|
1031
1073
|
resilience: {
|
|
1032
|
-
|
|
1074
|
+
...parseFirstByteTimeoutMs(process.env.MODEL_FIRST_BYTE_TIMEOUT_MS),
|
|
1033
1075
|
firstTokenTimeoutMs: numEnv("MODEL_FIRST_TOKEN_TIMEOUT_MS", String(RESILIENCE_DEFAULTS.firstTokenTimeoutMs.value)),
|
|
1034
1076
|
idleTimeoutMs: numEnv("MODEL_IDLE_TIMEOUT_MS", String(RESILIENCE_DEFAULTS.idleTimeoutMs.value)),
|
|
1035
1077
|
circuitBreaker: boolEnv("MODEL_CIRCUIT_BREAKER", RESILIENCE_DEFAULTS.circuitBreaker.value),
|
|
@@ -1167,7 +1209,6 @@ export function formatUnmatchableToolNames(source, bad) {
|
|
|
1167
1209
|
return (`${source} names tool(s) that can never match a live tool (these lists are RAW whole-name comparisons — core 5.0.0 RB-476 removed the alias/auto-prefix folds): ` +
|
|
1168
1210
|
bad.map((b) => `${presentToolName(b.name)} → ${b.guidance}`).join("; ")).replace(CONTROL_AND_BIDI_STRIP_RE, "");
|
|
1169
1211
|
}
|
|
1170
|
-
const MAX_TIMER_DELAY_MS = 2_147_483_647;
|
|
1171
1212
|
function streamApprovalConfig(gates) {
|
|
1172
1213
|
const cfg = {
|
|
1173
1214
|
enabled: boolEnv("STREAM_APPROVAL_ENABLED", true),
|
|
@@ -1284,6 +1325,7 @@ function parseApprovalDomain(ctx) {
|
|
|
1284
1325
|
})(process.env.MANUAL_MODE_SHELL_GATE),
|
|
1285
1326
|
permissionsDisableAutoMode: boolEnv("PERMISSIONS_DISABLE_AUTO_MODE", false),
|
|
1286
1327
|
...optionalEnumKey("autoModeClassifierFallback", "AUTO_MODE_CLASSIFIER_FALLBACK", process.env.AUTO_MODE_CLASSIFIER_FALLBACK, ["main", "none"]),
|
|
1328
|
+
...parseAutoModeClassifierTimeoutMs(process.env.AUTO_MODE_CLASSIFIER_TIMEOUT_MS),
|
|
1287
1329
|
...optionalEnumKey("crossSessionInbound", "CROSS_SESSION_INBOUND", process.env.CROSS_SESSION_INBOUND, CROSS_SESSION_INBOUND_SETTINGS),
|
|
1288
1330
|
...optionalEnumKey("crossSessionDialogExpiry", "CROSS_SESSION_DIALOG_EXPIRY", process.env.CROSS_SESSION_DIALOG_EXPIRY, CROSS_SESSION_DIALOG_EXPIRY_VALUES),
|
|
1289
1331
|
mcpElicitation: {
|
|
@@ -1400,6 +1442,22 @@ function lingerMsEnv(name, raw, defaultMs, disableHint) {
|
|
|
1400
1442
|
}
|
|
1401
1443
|
return n;
|
|
1402
1444
|
}
|
|
1445
|
+
function parseDrainSecondSignalMinGapMs(raw) {
|
|
1446
|
+
if (raw === undefined || raw === "")
|
|
1447
|
+
return DRAIN_SECOND_SIGNAL_MIN_GAP_DEFAULT_MS;
|
|
1448
|
+
const { minMs, maxMs } = DRAIN_SECOND_SIGNAL_MIN_GAP_BOUNDS;
|
|
1449
|
+
const n = /^(0|[1-9]\d*)$/.test(raw) ? Number(raw) : Number.NaN;
|
|
1450
|
+
if (!Number.isSafeInteger(n) || n < minMs || n > maxMs) {
|
|
1451
|
+
const shown = raw.length > 40 ? `${raw.slice(0, 40)}…(${raw.length} chars)` : raw;
|
|
1452
|
+
throw new Error(`env DRAIN_SECOND_SIGNAL_MIN_GAP_MS=${JSON.stringify(shown)} must be a whole number of milliseconds in [${minMs}, ${maxMs}] ` +
|
|
1453
|
+
`(decimal digits only; unset = ${DRAIN_SECOND_SIGNAL_MIN_GAP_DEFAULT_MS}). During a drain, a second SIGTERM / SIGINT hard-stops ` +
|
|
1454
|
+
`only when it arrives at least this long after the drain began — closer ones are the same beat delivered twice and are ` +
|
|
1455
|
+
`ignored. "0" turns that filter off (every second signal hard-stops, the 7.101.x behavior); above ${maxMs} (the ` +
|
|
1456
|
+
`DRAIN_GRACE_MS floor) a second signal could never count inside the shortest drain window. Refusing to boot rather ` +
|
|
1457
|
+
`than running the default while the operator believes their value is in force (#210).`);
|
|
1458
|
+
}
|
|
1459
|
+
return n;
|
|
1460
|
+
}
|
|
1403
1461
|
function parseMemoryEmbedder(ctx) {
|
|
1404
1462
|
const endpoint = process.env.MEMORY_EMBEDDER_ENDPOINT || undefined;
|
|
1405
1463
|
const model = process.env.MEMORY_EMBEDDER_MODEL || undefined;
|
|
@@ -1926,7 +1984,7 @@ function parseOrchestrationDomain(ctx) {
|
|
|
1926
1984
|
export const LIMITS_HTTP_FLOORS = {
|
|
1927
1985
|
attachmentMaxBytes: 1024,
|
|
1928
1986
|
attachmentUnboundTtlMs: 60_000,
|
|
1929
|
-
drainGraceMs:
|
|
1987
|
+
drainGraceMs: DRAIN_GRACE_FLOOR_MS,
|
|
1930
1988
|
};
|
|
1931
1989
|
function parseLimitsHttpDomain() {
|
|
1932
1990
|
return {
|
|
@@ -1957,6 +2015,7 @@ function parseLimitsHttpDomain() {
|
|
|
1957
2015
|
})(),
|
|
1958
2016
|
},
|
|
1959
2017
|
drainGraceMs: clampEnvWithWarn("DRAIN_GRACE_MS", String(10 * 60 * 1000), LIMITS_HTTP_FLOORS.drainGraceMs),
|
|
2018
|
+
drainSecondSignalMinGapMs: parseDrainSecondSignalMinGapMs(process.env.DRAIN_SECOND_SIGNAL_MIN_GAP_MS),
|
|
1960
2019
|
sighupIdleGraceMs: clampEnvWithWarn("SIGHUP_IDLE_GRACE_MS", String(120_000), 5_000),
|
|
1961
2020
|
...parentPidEnv(),
|
|
1962
2021
|
engineAutoExit: boolEnv("ENGINE_AUTO_EXIT", false),
|
|
@@ -2021,7 +2080,8 @@ function parseGitApiKind(raw, baseUrl) {
|
|
|
2021
2080
|
}
|
|
2022
2081
|
return kind;
|
|
2023
2082
|
}
|
|
2024
|
-
function parseIntegrationsDomain() {
|
|
2083
|
+
function parseIntegrationsDomain(ctx) {
|
|
2084
|
+
const { requirePrincipal } = ctx;
|
|
2025
2085
|
return {
|
|
2026
2086
|
pluginsAllowHosts: (process.env.PLUGINS_ALLOW_HOSTS ?? "github.com").split(",").map((h) => h.trim()).filter(Boolean),
|
|
2027
2087
|
configBootFetchBudgetMs: clampEnvWithWarn("CONFIG_BOOT_FETCH_BUDGET_MS", "1500", 0),
|
|
@@ -2030,17 +2090,19 @@ function parseIntegrationsDomain() {
|
|
|
2030
2090
|
gitApiKind: parseGitApiKind(process.env.GIT_API_KIND, process.env.GIT_API_BASEURL),
|
|
2031
2091
|
defaultScenario: env("DEFAULT_SCENARIO", "code"),
|
|
2032
2092
|
skillsDir: env("SKILLS_DIR", defaultSkillsDir()),
|
|
2033
|
-
|
|
2093
|
+
centerCredentialRenewLeadMs: parseCenterCredentialRenewLeadMs(process.env.CENTER_CREDENTIAL_RENEW_LEAD_MS),
|
|
2094
|
+
configCenter: process.env.SEMA_REGISTRY_URL ? parseConfigCenter(env("SEMA_REGISTRY_URL"), requirePrincipal) : undefined,
|
|
2034
2095
|
configProvider: process.env.CONFIG_PROVIDER || undefined,
|
|
2035
2096
|
configLocalDir: process.env.CONFIG_LOCAL_DIR || undefined,
|
|
2036
2097
|
a2aServe: parseA2aServe(),
|
|
2037
2098
|
};
|
|
2038
2099
|
}
|
|
2039
|
-
function parseConfigCenter(baseUrl) {
|
|
2100
|
+
function parseConfigCenter(baseUrl, requirePrincipal) {
|
|
2040
2101
|
const worker = process.env.SEMA_REGISTRY_WORKER || undefined;
|
|
2102
|
+
const declared = readCenterCredentialDeclaration();
|
|
2041
2103
|
return {
|
|
2042
2104
|
baseUrl,
|
|
2043
|
-
credential: createCenterCredentialSource(
|
|
2105
|
+
credential: createCenterCredentialSource(declared.from === "file" ? { ...declared, multiTenant: requirePrincipal } : declared, worker),
|
|
2044
2106
|
dryRun: boolEnv("SEMA_REGISTRY_DRY_RUN", false),
|
|
2045
2107
|
worker,
|
|
2046
2108
|
};
|
|
@@ -2141,7 +2203,7 @@ const APPROVAL_GROUP_KEYS = [
|
|
|
2141
2203
|
"approvalRequire", "approvalDeny", "approvalTimeoutSec", "approvalAutoBudget", "approvalNeverAuto",
|
|
2142
2204
|
"approvalHmacKeys", "durableApproval", "durableApprovalSource", "streamAskWindowMsSource", "directApprovalDoor", "directDoorActive", "resourceSuspend",
|
|
2143
2205
|
"resourceSuspendTtlSec", "resourceSuspendTotalTokens", "resourceSuspendTotalBudgetUsd", "resourceSuspendMaxSlices", "askQuestionEnabled", "questionThrottle", "toolApprovalEnabled", "permissionRulesEnabled", "permissionRulesEnabledExplicit", "streamAskWindowMarginMs", "streamApproval", "unattendedApprovalPolicy", "mcpElicitation",
|
|
2144
|
-
"sensitiveWritePatterns", "writeProtectedPaths", "manualModeShellGate", "permissionsDisableAutoMode", "autoModeClassifierFallback", "crossSessionInbound", "crossSessionDialogExpiry",
|
|
2206
|
+
"sensitiveWritePatterns", "writeProtectedPaths", "manualModeShellGate", "permissionsDisableAutoMode", "autoModeClassifierFallback", "autoModeClassifierTimeoutMs", "crossSessionInbound", "crossSessionDialogExpiry",
|
|
2145
2207
|
];
|
|
2146
2208
|
const MEMORY_GROUP_KEYS = [
|
|
2147
2209
|
"memoryEngineEnabled", "memoryEngineDir", "memoryEngineRemoteLaneAllowed", "memoryEngineBackend", "memoryScope",
|
|
@@ -2166,7 +2228,7 @@ const ORCHESTRATION_GROUP_KEYS = [
|
|
|
2166
2228
|
];
|
|
2167
2229
|
const LIMITS_HTTP_GROUP_KEYS = [
|
|
2168
2230
|
"port", "attachmentOrphanGraceMs", "workspaceFileMaxBytes", "attachmentMaxBytes", "attachmentMimeAllowlist",
|
|
2169
|
-
"attachmentUnboundTtlMs", "infraCostRates", "drainGraceMs", "sighupIdleGraceMs", "parentPid",
|
|
2231
|
+
"attachmentUnboundTtlMs", "infraCostRates", "drainGraceMs", "drainSecondSignalMinGapMs", "sighupIdleGraceMs", "parentPid",
|
|
2170
2232
|
"engineAutoExit", "engineLingerMs", "engineOrphanLingerMs",
|
|
2171
2233
|
"rateLimitPerMin", "maxTaskCostUsd",
|
|
2172
2234
|
"maxTaskTokens", "maxPrincipalCostUsd", "costQuotaWindowSec", "reapIntervalSec", "runStaleSec", "toolResultTtlSec",
|
|
@@ -2174,7 +2236,7 @@ const LIMITS_HTTP_GROUP_KEYS = [
|
|
|
2174
2236
|
const OBSERVABILITY_GROUP_KEYS = ["metricsToken", "traceToken", "toolTrace", "traceThinking", "logLevel", "otel"];
|
|
2175
2237
|
const INTEGRATIONS_GROUP_KEYS = [
|
|
2176
2238
|
"pluginsAllowHosts", "mcpServers", "a2aPeers", "a2aServe", "configBootFetchBudgetMs", "gitApiBaseUrl", "gitApiToken", "gitApiKind", "defaultScenario",
|
|
2177
|
-
"skillsDir", "configCenter", "configProvider", "configLocalDir",
|
|
2239
|
+
"skillsDir", "centerCredentialRenewLeadMs", "configCenter", "configProvider", "configLocalDir",
|
|
2178
2240
|
];
|
|
2179
2241
|
export const CONFIG_GROUP_KEYS = {
|
|
2180
2242
|
store: STORE_GROUP_KEYS,
|
|
@@ -2234,7 +2296,7 @@ export function loadConfig() {
|
|
|
2234
2296
|
const orchestration = parseOrchestrationDomain({ postureOn, singleUserTurnkey });
|
|
2235
2297
|
const limitsHttp = parseLimitsHttpDomain();
|
|
2236
2298
|
const observability = parseObservabilityDomain();
|
|
2237
|
-
const integrations = parseIntegrationsDomain();
|
|
2299
|
+
const integrations = parseIntegrationsDomain({ requirePrincipal });
|
|
2238
2300
|
const { directApprovalDoor, durableApproval: dgDurable, approvalHmacKeys: dgHmacKeys } = approval;
|
|
2239
2301
|
const { principalJwtPubkeys, principalJwtIss, principalJwtAud, operatorPrincipals: dgOperators, authTokens } = auth;
|
|
2240
2302
|
const { enabled: imageBakesEnabled, runnerPrincipal: bakeRunnerPrincipal } = orchestration.imageBakes;
|
package/dist/device-store.d.ts
CHANGED
|
@@ -50,7 +50,7 @@ export declare const DEVICE_RATE_LIMIT_CODE: "limit.rate_exceeded";
|
|
|
50
50
|
* 排空/停机拒绝复用**既有**的通用码(`draining`,`docs/ASSISTANT-WIRE-CONTRACT.md` 附录 A 已收录,503)。
|
|
51
51
|
*
|
|
52
52
|
* 🔴 刻意不铸 `device.draining`([ref]/[ref]):§4.8 的 device 闭集**冻结**且无 draining 成员,而排空
|
|
53
|
-
* 语义面全部署早有属主词 —— HTTP 计费提交拒臂(`sendError(res, 503, "draining", …)
|
|
53
|
+
* 语义面全部署早有属主词 —— HTTP 计费提交拒臂(`http/readiness.ts` 的 `billableRefusalOf` 唯一铸点,S-739 前是 `sendError(res, 503, "draining", …)`;SDK spec 逐字引用
|
|
54
54
|
* 的冻结字面)、drain 期新 upgrade 拒(503)、WS hello 拒帧 / bye 帧的 `draining` 分支键。为同一语义再铸
|
|
55
55
|
* `device.*` 同义码 = 消费端要认两套(上面 rate-limit 条的同一判据);且 `device.draining` 在 HTTP 层
|
|
56
56
|
* 零发送点,写进附录 A 即幽灵行(`error-code-catalog-live` 的「文档多码」臂当场红)。
|
|
@@ -4,31 +4,45 @@
|
|
|
4
4
|
* 解析规则(未设 / 形错拒启 / 姿态 / 生效名单)的全文在 `config.ts` `parseEnvNameAllowlist` 的头注;本表只装**各旋钮自己的**事实,
|
|
5
5
|
* 不是解析规则:
|
|
6
6
|
* · `example` 进拒句;
|
|
7
|
-
* · `multiTenant` = `REQUIRE_PRINCIPAL=true`
|
|
7
|
+
* · `multiTenant` = `REQUIRE_PRINCIPAL=true` 下名单作不作数 —— **只有服务进程读的旋钮有这一列**(姿态是服务进程的概念):
|
|
8
8
|
* - `LSP_ENV_ALLOW` ⇒ ignored:host 腿语言服务器在多租户姿态下本来就不起,名单无处作数,设了要说出来(S-699)。
|
|
9
9
|
* - 两只沙箱名单 ⇒ honored:S-713 照录修前语义(任何姿态下都转发),S-715 主会话裁定维持 —— 沙箱 env 是部署方为隔离沙箱显式配的
|
|
10
10
|
* 带外凭据(e2b / docker 车道本来就是多租户 fleet 的主形),与 LSP 那条「本机子进程」的形不同;多租户下起服说一句。
|
|
11
|
-
*
|
|
11
|
+
* · `BAKE_ENV_ALLOW`(S-684 残余,7.103.0)由 **bake-runner 进程**读(构建宿主守护进程:没有主体、没有租户 ⇒ 没有姿态列,
|
|
12
|
+
* 不进 {@link describeAllowlistPostures} 的姿态句)。它与三只服务侧旋钮同一只解析器、同一只「点名放行」谓词、同一条「原文按凭据类」律。
|
|
13
|
+
* 表是 `Record<旋钮, …>`:闭集加一只旋钮而不补行 = 编译红;行形随读它的进程定(服务进程 ⇒ 必须写姿态列)。
|
|
12
14
|
*
|
|
13
15
|
* 🔴 **姿态列只在这里写一次**([ref];7.101.0 合并复审 O9):凡要对人说「多租户下哪只旋钮忽略、哪只照转发」的句子,一律取
|
|
14
16
|
* {@link describeAllowlistPostures} 的读数 —— 今天三处:`config_env_forwarded_multi_tenant.note`(`config.ts` 解析器)、LSP 剥除现场的
|
|
15
17
|
* 出路句(`observability/secret-env-scrub.ts`)、配置目录 `LSP_ENV_ALLOW` 行(`config-catalog.ts`)。修前三处各手抄一遍,表改一格它们
|
|
16
18
|
* 当场撒谎。为什么是纯叶:`config-catalog.ts` 被 `config.ts` 值引,表留在 `config.ts` 会成装载环。
|
|
17
19
|
*/
|
|
18
|
-
|
|
20
|
+
/** 服务进程(`config.ts` `loadConfig`)读的名单型旋钮 —— 有多租户姿态。 */
|
|
21
|
+
export type ServerEnvNameAllowlistKnob = "LSP_ENV_ALLOW" | "E2B_SANDBOX_ENV" | "DOCKER_SANDBOX_ENV";
|
|
22
|
+
/** bake-runner 进程(`bake-runner/main.ts` `loadEnv`)读的名单型旋钮 —— 构建宿主守护进程没有主体 / 租户,没有姿态。 */
|
|
23
|
+
export type RunnerEnvNameAllowlistKnob = "BAKE_ENV_ALLOW";
|
|
24
|
+
export type EnvNameAllowlistKnob = ServerEnvNameAllowlistKnob | RunnerEnvNameAllowlistKnob;
|
|
19
25
|
export type EnvNameAllowlistPosture = {
|
|
20
26
|
readonly kind: "honored";
|
|
21
27
|
} | {
|
|
22
28
|
readonly kind: "ignored";
|
|
23
29
|
readonly why: string;
|
|
24
30
|
};
|
|
31
|
+
/** 服务侧旋钮一行:拒句示例 + 多租户姿态。 */
|
|
32
|
+
export interface ServerEnvNameAllowlistKnobRow {
|
|
33
|
+
readonly example: string;
|
|
34
|
+
readonly multiTenant: EnvNameAllowlistPosture;
|
|
35
|
+
}
|
|
36
|
+
/** runner 旋钮一行:只有拒句示例(没有姿态可言)。 */
|
|
37
|
+
export interface RunnerEnvNameAllowlistKnobRow {
|
|
38
|
+
readonly example: string;
|
|
39
|
+
}
|
|
25
40
|
export type EnvNameAllowlistKnobTable = {
|
|
26
|
-
readonly [K in EnvNameAllowlistKnob]:
|
|
27
|
-
readonly example: string;
|
|
28
|
-
readonly multiTenant: EnvNameAllowlistPosture;
|
|
29
|
-
};
|
|
41
|
+
readonly [K in EnvNameAllowlistKnob]: K extends ServerEnvNameAllowlistKnob ? ServerEnvNameAllowlistKnobRow : RunnerEnvNameAllowlistKnobRow;
|
|
30
42
|
};
|
|
31
43
|
export declare const ENV_NAME_ALLOWLIST_KNOBS: EnvNameAllowlistKnobTable;
|
|
44
|
+
/** 名单型旋钮的全部名字(四只;表键派生)—— 两处用:解析器的「点名名单型旋钮 = 形错」、剥除的「名单原文按凭据类」臂。 */
|
|
45
|
+
export declare const ENV_NAME_ALLOWLIST_KNOB_NAMES: ReadonlySet<string>;
|
|
32
46
|
/**
|
|
33
47
|
* 多租户姿态句 —— **由表派生**,不许在别处手写。形:`under REQUIRE_PRINCIPAL=true: <旋钮> is ignored; <旋钮> / <旋钮> are forwarded as configured`
|
|
34
48
|
* (按 {@link POSTURE_PHRASE} 的 kind 序分组、组内按表序;空组不出现)。只有旋钮名与姿态,不带任何名单内容。
|
|
@@ -5,15 +5,20 @@ export const ENV_NAME_ALLOWLIST_KNOBS = {
|
|
|
5
5
|
},
|
|
6
6
|
E2B_SANDBOX_ENV: { example: "E2B_SANDBOX_ENV=GIT_TOKEN,NPM_TOKEN", multiTenant: { kind: "honored" } },
|
|
7
7
|
DOCKER_SANDBOX_ENV: { example: "DOCKER_SANDBOX_ENV=GIT_TOKEN,NPM_TOKEN", multiTenant: { kind: "honored" } },
|
|
8
|
+
BAKE_ENV_ALLOW: { example: "BAKE_ENV_ALLOW=REGISTRY_PASSWORD,NPM_TOKEN" },
|
|
8
9
|
};
|
|
10
|
+
export const ENV_NAME_ALLOWLIST_KNOB_NAMES = new Set(Object.keys(ENV_NAME_ALLOWLIST_KNOBS));
|
|
9
11
|
const POSTURE_PHRASE = {
|
|
10
12
|
ignored: { one: "is ignored", many: "are ignored" },
|
|
11
13
|
honored: { one: "is forwarded as configured", many: "are forwarded as configured" },
|
|
12
14
|
};
|
|
13
15
|
export function describeAllowlistPostures(table = ENV_NAME_ALLOWLIST_KNOBS) {
|
|
14
|
-
const
|
|
16
|
+
const postured = Object.keys(table).flatMap((k) => {
|
|
17
|
+
const row = table[k];
|
|
18
|
+
return "multiTenant" in row ? [{ knob: k, kind: row.multiTenant.kind }] : [];
|
|
19
|
+
});
|
|
15
20
|
const groups = Object.keys(POSTURE_PHRASE).flatMap((kind) => {
|
|
16
|
-
const named =
|
|
21
|
+
const named = postured.filter((p) => p.kind === kind).map((p) => p.knob);
|
|
17
22
|
if (named.length === 0)
|
|
18
23
|
return [];
|
|
19
24
|
const phrase = POSTURE_PHRASE[kind];
|
package/dist/fleet-client.d.ts
CHANGED
|
@@ -4,16 +4,28 @@
|
|
|
4
4
|
* - **可选分层(D3)**:仅当 center 已配置(sema-registry lane 的 baseUrl+token+worker)∧ `FLEET_ADVERTISE_ADDRESS`
|
|
5
5
|
* 显式声明时才启动 —— 不配 center 的部署零行为变化。address 不自猜(k8s pod IP / docker alias / 127.0.0.1:port
|
|
6
6
|
* 只有部署环境自己知道,故用显式 env)。
|
|
7
|
-
* - **announce=注册=心跳同形状幂等 upsert**:boot 后 POST 一次 + 每 heartbeatMs
|
|
8
|
-
* `
|
|
9
|
-
*
|
|
7
|
+
* - **announce=注册=心跳同形状幂等 upsert**:boot 后(listen 之后)POST 一次 + 每 heartbeatMs 续。
|
|
8
|
+
* `ready` / `draining` 两字段由**同一只**就绪谓词的同一次读铸(S-740:`http/readiness.ts` 的 `readinessOf`,
|
|
9
|
+
* 与 `/health.ready`、`GET /readyz`、计费提交门同源):`ready = verdict.ready`(= roster 已落 ∧ ¬draining),
|
|
10
|
+
* `draining = reasons ∋ "draining"`。修前 `ready = !isDraining()`,不含 roster ⇒ roster 未落时心跳说「就绪」
|
|
11
|
+
* 而计费提交在 503。
|
|
12
|
+
* - **`restart?`(S-741)**:在场 ⇔ 本副本必须重启才能应用配置 —— 取自 `/health.restart` **同一只**取值口(装配层接
|
|
13
|
+
* `deployment.restartState`,config-center 的 `restartState()`),同一个对象,形 = settings-schema `RestartSignal`;
|
|
14
|
+
* 缺席即键缺席(中心按行整体替换,不合并 —— 一条不带它的心跳就是「不再需要」)。
|
|
15
|
+
* - **`modelRefusals?`(S-768)**:在场 ⇔ 本副本对最近一次被判的中心候选目录有可报的拒(逐条名 → 码 / 模型面组拒码)—— 取自
|
|
16
|
+
* `/health.modelRefusals` **同一只**取值口(装配层接 `deployment.modelRefusalsState`),同一个对象,形 = settings-schema
|
|
17
|
+
* `ModelRefusalsSignal`;缺席即键缺席(同 `restart`)。
|
|
18
|
+
* - **自报状态翻转即刻再 announce**(第一时间自摘流 / 自挂流 / 报重启 / 报拒因,比任何观测都快):draining 翻转由收尾链直调
|
|
19
|
+
* `announceNow`;roster 落地、restart 信号与模型拒因变化由装配层把 config-center 的 `onSelfReportChange` 接到 `announceNow`
|
|
20
|
+
* (两处 = 两类输入各自的唯一写者,触发都是 `announceNow`)。退出前 DELETE(显式注销,幂等)。
|
|
10
21
|
* - **usage 批报**:tracer 的 `brain.call` 已带 ALS principal —— FleetUsageAccumulator 按 principal×model
|
|
11
22
|
* 累计,每窗口(默认 60s,半开 [windowStartMs, windowEndMs))POST /api/fleet/usage。幂等键
|
|
12
23
|
* (worker,instanceId,windowStartMs) 重发安全;**发送失败 → merge 回累计器**,并入下一窗(失败窗从未入账,
|
|
13
24
|
* 向前合并不会双计 —— 只延迟,不丢量)。
|
|
14
25
|
* - 全部 fire-and-forget + swallow-guarded:fleet 面是运维旁路,绝不影响任务面;网络错误只 warn 一次/连败类。
|
|
15
26
|
*/
|
|
16
|
-
import { UsageReport } from "@sema-agent/settings-schema/fleet";
|
|
27
|
+
import { UsageReport, type ModelRefusalsSignal, type RestartSignal } from "@sema-agent/settings-schema/fleet";
|
|
28
|
+
import type { ReadinessVerdict } from "./http/readiness.js";
|
|
17
29
|
import { type CenterEndpoint } from "./config-center/center-request.js";
|
|
18
30
|
/** Bound on distinct principal×model keys per window — a runaway/multi-tenant burst collapses into
|
|
19
31
|
* `__other__` instead of growing unbounded (same posture as budget.ts cardinalityGuard). */
|
|
@@ -65,8 +77,15 @@ export interface FleetClientOpts {
|
|
|
65
77
|
/** Reachable base URL of THIS instance — the operator's explicit declaration (`FLEET_ADVERTISE_ADDRESS`). */
|
|
66
78
|
advertiseAddress: string;
|
|
67
79
|
version: string;
|
|
68
|
-
/**
|
|
69
|
-
|
|
80
|
+
/** S-740:就绪判(装配层接 `readinessOf(deployment)` —— HTTP 面读的同一个 deps 组)。每条公告现读**一次**,
|
|
81
|
+
* `ready` 与 `draining` 两字段都由这一次读铸(不会撕裂成 `ready:true` + `draining:true`)。 */
|
|
82
|
+
readiness: () => ReadinessVerdict;
|
|
83
|
+
/** S-741:restart 信号(装配层接 `/health` 同一只取值口 `deployment.restartState`)。每条公告现读一次;`undefined` ⇒ 公告不带
|
|
84
|
+
* `restart` 键。必填:心跳面没有「不知道要不要重启」这一形。 */
|
|
85
|
+
restart: () => RestartSignal | undefined;
|
|
86
|
+
/** S-768:模型拒因信号(装配层接 `/health` 同一只取值口 `deployment.modelRefusalsState`)。每条公告现读一次;`undefined` ⇒ 公告不带
|
|
87
|
+
* `modelRefusals` 键。必填:与 `restart` 同 —— 心跳面没有「不知道有没有拒」这一形(老引擎不发这个键,中心按 `version` 判「不可知」)。 */
|
|
88
|
+
modelRefusals: () => ModelRefusalsSignal | undefined;
|
|
70
89
|
usage?: FleetUsageAccumulator;
|
|
71
90
|
heartbeatMs?: number;
|
|
72
91
|
usageWindowMs?: number;
|
|
@@ -78,7 +97,8 @@ export interface FleetClientOpts {
|
|
|
78
97
|
fetchImpl?: typeof fetch;
|
|
79
98
|
}
|
|
80
99
|
export interface FleetClient {
|
|
81
|
-
/** Immediate out-of-cycle announce — call when
|
|
100
|
+
/** Immediate out-of-cycle announce — call when a self-reported input flips (draining: self-remove from routing FIRST;
|
|
101
|
+
* roster landed: become routable without waiting a heartbeat; restart signal changed: S-741). */
|
|
82
102
|
announceNow: () => Promise<void>;
|
|
83
103
|
/** Stop timers + best-effort DELETE deregistration (bounded; never throws). */
|
|
84
104
|
stop: () => Promise<void>;
|
package/dist/fleet-client.js
CHANGED
|
@@ -81,17 +81,24 @@ export function startFleetClient(opts) {
|
|
|
81
81
|
const send = (route, method, json) => centerFetch(opts.center, { kind: route }, { method, json, timeoutMs: FLEET_CLIENT_FETCH_TIMEOUT_MS, fetchImpl: f });
|
|
82
82
|
let stopped = false;
|
|
83
83
|
let failStreak = 0;
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
worker: opts.center.worker,
|
|
88
|
-
address: opts.advertiseAddress,
|
|
89
|
-
version: opts.version,
|
|
90
|
-
ready: !opts.isDraining(),
|
|
91
|
-
draining: opts.isDraining(),
|
|
92
|
-
heartbeatMs,
|
|
93
|
-
};
|
|
84
|
+
const announceOnce = async () => {
|
|
85
|
+
if (stopped)
|
|
86
|
+
return;
|
|
94
87
|
try {
|
|
88
|
+
const verdict = opts.readiness();
|
|
89
|
+
const restart = opts.restart();
|
|
90
|
+
const modelRefusals = opts.modelRefusals();
|
|
91
|
+
const body = {
|
|
92
|
+
instanceId: opts.instanceId,
|
|
93
|
+
worker: opts.center.worker,
|
|
94
|
+
address: opts.advertiseAddress,
|
|
95
|
+
version: opts.version,
|
|
96
|
+
ready: verdict.ready,
|
|
97
|
+
draining: !verdict.ready && verdict.reasons.includes("draining"),
|
|
98
|
+
heartbeatMs,
|
|
99
|
+
...(restart !== undefined ? { restart } : {}),
|
|
100
|
+
...(modelRefusals !== undefined ? { modelRefusals } : {}),
|
|
101
|
+
};
|
|
95
102
|
const { res } = await send("fleet-instances", "POST", body);
|
|
96
103
|
if (!res.ok)
|
|
97
104
|
throw new Error(`announce ${res.status}${await rejectionSnippet(res)}`);
|
|
@@ -105,6 +112,26 @@ export function startFleetClient(opts) {
|
|
|
105
112
|
opts.logger?.warn("fleet_announce_failed", { error: err instanceof Error ? err.message : String(err) });
|
|
106
113
|
}
|
|
107
114
|
};
|
|
115
|
+
let inFlight;
|
|
116
|
+
let again = false;
|
|
117
|
+
const announceNow = () => {
|
|
118
|
+
if (inFlight !== undefined) {
|
|
119
|
+
again = true;
|
|
120
|
+
return inFlight;
|
|
121
|
+
}
|
|
122
|
+
inFlight = (async () => {
|
|
123
|
+
try {
|
|
124
|
+
do {
|
|
125
|
+
again = false;
|
|
126
|
+
await announceOnce();
|
|
127
|
+
} while (again && !stopped);
|
|
128
|
+
}
|
|
129
|
+
finally {
|
|
130
|
+
inFlight = undefined;
|
|
131
|
+
}
|
|
132
|
+
})();
|
|
133
|
+
return inFlight;
|
|
134
|
+
};
|
|
108
135
|
let windowStartMs = Date.now();
|
|
109
136
|
const flushUsage = async () => {
|
|
110
137
|
if (!opts.usage)
|
|
@@ -131,13 +158,17 @@ export function startFleetClient(opts) {
|
|
|
131
158
|
hb.unref?.();
|
|
132
159
|
const uw = opts.usage ? setInterval(() => void flushUsage(), usageWindowMs) : undefined;
|
|
133
160
|
uw?.unref?.();
|
|
161
|
+
let stopping = false;
|
|
134
162
|
const stop = async () => {
|
|
135
|
-
if (
|
|
163
|
+
if (stopping)
|
|
136
164
|
return;
|
|
137
|
-
|
|
165
|
+
stopping = true;
|
|
138
166
|
clearInterval(hb);
|
|
139
167
|
if (uw)
|
|
140
168
|
clearInterval(uw);
|
|
169
|
+
while (inFlight !== undefined)
|
|
170
|
+
await inFlight;
|
|
171
|
+
stopped = true;
|
|
141
172
|
await flushUsage();
|
|
142
173
|
try {
|
|
143
174
|
await send("fleet-instances", "DELETE", { instanceId: opts.instanceId, worker: opts.center.worker });
|
package/dist/fleet-lease.d.ts
CHANGED
|
@@ -116,7 +116,7 @@ export declare class FleetLeaseManager {
|
|
|
116
116
|
/**
|
|
117
117
|
* Env/config gate(与 startFleetClientFromEnv 同 D3 分层姿势,但 lease 不需要 FLEET_ADVERTISE_ADDRESS ——
|
|
118
118
|
* 它是出站消费面,不是入站可达性声明):sema-registry lane 配齐(baseUrl + **声明了**凭证 + worker)即启;任一缺失 →
|
|
119
|
-
* undefined,非 fleet 部署零行为变化。S-651:按**声明**装、不按内容装 —— `SEMA_REGISTRY_TOKEN_FILE`
|
|
119
|
+
* undefined,非 fleet 部署零行为变化。S-651:按**声明**装、不按内容装 —— `SEMA_REGISTRY_TOKEN_FILE`(`CENTER_CREDENTIAL_ENV.tokenFile`)在场而
|
|
120
120
|
* 文件此刻不可读,管理器照样装上(每次协商取值,失败走响亮的鉴权失败臂;文件恢复即恢复)。
|
|
121
121
|
*/
|
|
122
122
|
export declare function createFleetLeaseFromEnv(cfg: {
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* `scrubServerSecrets` 的结果)。
|
|
10
10
|
*
|
|
11
11
|
* ## 规则(一句)
|
|
12
|
-
* 语言服务器子进程的 env =
|
|
12
|
+
* 语言服务器子进程的 env = `scrubServerSecrets`(process.env) ∪ 部署方点名放回的键(S-699,见下节;缺省空名单)——
|
|
13
13
|
* 剥除与模型可驱动的 host shell(`inheritEnv:"scrub"`)
|
|
14
14
|
* 和 project-memory 的 git 子进程**同一只函数**:目录 `type:"secret"` 键按名剥 ∘ core 形状规则按形剥。只交 core 的默认
|
|
15
15
|
* 会漏掉形状规则认不出的本仓凭据键(今天是 `OTEL_EXPORTER_OTLP_HEADERS`),那正是 S-684 当初的病形。
|
|
@@ -23,8 +23,9 @@
|
|
|
23
23
|
* ## 点名放行(S-699,`LSP_ENV_ALLOW`)
|
|
24
24
|
* 剥除会连语言服务器自己的真实依赖一起剥掉(`.npmrc` 的 `${NPM_TOKEN}` 插值、rust-analyzer `cargo metadata` 私有 registry 的
|
|
25
25
|
* `CARGO_REGISTRIES_<名>_TOKEN` —— 形状规则按名判,分不出「给语言服务器用的令牌」与「编排器的令牌」)。出路 = 部署方点名:
|
|
26
|
-
* env =
|
|
27
|
-
* `inheritEnv:[names]` 同一只谓词)从原 env
|
|
26
|
+
* env = `scrubServerSecrets`(process.env) 之上把 `config.lspEnvAllow` 点到的键经 `allowlistedEnv`(与 host shell
|
|
27
|
+
* `inheritEnv:[names]` 同一只谓词)从原 env 原样放回;没点到的照旧剥 —— 7.103.0 起这件组合只有一只:{@link hostSubprocessEnv}
|
|
28
|
+
* (`server-secret-env.ts`,bake-runner 的 `BAKE_ENV_ALLOW` 调同一只)。名单由 `config.ts` 解析(形错拒启;
|
|
28
29
|
* `REQUIRE_PRINCIPAL=true` 下落空名单并 warn 一次),本函数只读 `config.lspEnvAllow`,不在这里再判一次姿态。
|
|
29
30
|
* 放回的键不是剥除 ⇒ 不进 `secret_env_scrubbed` 的键表与计数;剥掉的键经既有现场 `server.lsp.spawn-env` 一行 warn
|
|
30
31
|
* (带当前放行名单与出路句),这一行就是「剥了什么」的用户可见面 —— 语言服务器侧从不报。
|
package/dist/host-lsp-manager.js
CHANGED
|
@@ -1,11 +1,7 @@
|
|
|
1
1
|
import { NodeLspManager } from "@sema-agent/core";
|
|
2
|
-
import {
|
|
3
|
-
import { allowlistedEnv, scrubServerSecrets } from "./server-secret-env.js";
|
|
2
|
+
import { hostSubprocessEnv } from "./server-secret-env.js";
|
|
4
3
|
export function createHostLspManager(config, log) {
|
|
5
|
-
const
|
|
6
|
-
const composed = { ...scrubServerSecrets(process.env, scrubFindings), ...allowlistedEnv(config.lspEnvAllow, process.env) };
|
|
7
|
-
const findings = scrubFindings.filter((f) => composed[f.key] === undefined);
|
|
8
|
-
recordSecretEnvScrub("server.lsp.spawn-env", findings, config.lspEnvAllow);
|
|
4
|
+
const composed = hostSubprocessEnv({ site: "server.lsp.spawn-env", allow: config.lspEnvAllow });
|
|
9
5
|
const env = {};
|
|
10
6
|
for (const [k, v] of Object.entries(composed))
|
|
11
7
|
if (v !== undefined)
|
|
@@ -6,6 +6,7 @@ import { APPROVAL_GATE_KINDS } from "../tool-approval.js";
|
|
|
6
6
|
import { createWallClockJumpGuard } from "../wall-clock-jump-guard.js";
|
|
7
7
|
import { hasServiceAuth } from "./dispatch.js";
|
|
8
8
|
import { gatedPrincipal } from "./principal-gate.js";
|
|
9
|
+
import { readinessOf, billableRefusalOf } from "./readiness.js";
|
|
9
10
|
export function createApprovalSweeps(ctx) {
|
|
10
11
|
const { deps, resumeCheckpoint, mayDiscloseRowError } = ctx;
|
|
11
12
|
const denySweepClockGuard = createWallClockJumpGuard({
|
|
@@ -71,26 +72,10 @@ export function createApprovalSweeps(ctx) {
|
|
|
71
72
|
if (!hasServiceAuth(deps.config) && !deps.config.allowUnauthedWrites) {
|
|
72
73
|
return { status: 503, body: { error: "this worker requires a service auth token (set SERVICE_AUTH_TOKEN) before accepting task submissions", errorCode: "auth.service_token_required" } };
|
|
73
74
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
error: "draining",
|
|
79
|
-
errorCode: "draining",
|
|
80
|
-
message: "this instance is draining for shutdown/upgrade — retry against the replacement instance",
|
|
81
|
-
...(deps.drainState.reason ? { reason: deps.drainState.reason } : {}),
|
|
82
|
-
},
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
if (deps.modelReady && !deps.modelReady()) {
|
|
86
|
-
return {
|
|
87
|
-
status: 503,
|
|
88
|
-
body: {
|
|
89
|
-
error: "model_roster_pending",
|
|
90
|
-
errorCode: "state.model_roster_pending",
|
|
91
|
-
message: "this worker has no model yet (waiting for the first effective-config pull to land the roster) — retry shortly",
|
|
92
|
-
},
|
|
93
|
-
};
|
|
75
|
+
const readiness = readinessOf(deps);
|
|
76
|
+
if (!readiness.ready) {
|
|
77
|
+
const refusal = billableRefusalOf(readiness, deps.drainState?.reason);
|
|
78
|
+
return { status: refusal.status, body: refusal.body };
|
|
94
79
|
}
|
|
95
80
|
if (deps.costQuota && req.httpReq) {
|
|
96
81
|
const quotaPrincipal = gatedPrincipal(req.httpReq, deps.config);
|