@sema-agent/server 7.102.0 → 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 +61 -0
- package/README.md +3 -3
- package/README.zh-CN.md +3 -3
- package/USAGE.md +99 -6
- package/dist/background-session-reap.d.ts +4 -1
- package/dist/bake-runner/main.d.ts +21 -3
- package/dist/bake-runner/main.js +18 -11
- package/dist/boot/config-center.d.ts +7 -2
- package/dist/boot/config-center.js +18 -4
- package/dist/boot/stage-01-config.js +2 -0
- package/dist/boot/stage-10-http-server.js +6 -1
- package/dist/brain.d.ts +28 -8
- package/dist/brain.js +20 -4
- package/dist/capabilities/center-plugins.d.ts +9 -3
- package/dist/capabilities/center-plugins.js +5 -10
- package/dist/config-catalog.js +9 -5
- package/dist/config-center/apply-effective.d.ts +16 -0
- package/dist/config-center/apply-effective.js +23 -11
- package/dist/config-center/mcp-revocation.d.ts +2 -2
- package/dist/config-center/mcp-revocation.js +1 -1
- package/dist/config-center/restart-signal.d.ts +0 -10
- package/dist/config-center/restart-signal.js +2 -12
- package/dist/config-types.d.ts +35 -17
- package/dist/config-types.js +3 -3
- package/dist/config.d.ts +7 -0
- package/dist/config.js +43 -17
- 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 +9 -3
- package/dist/fleet-client.js +2 -0
- package/dist/host-lsp-manager.d.ts +4 -3
- package/dist/host-lsp-manager.js +2 -6
- package/dist/http/dispatch.js +4 -0
- package/dist/http/server.d.ts +4 -0
- package/dist/leader/wire.d.ts +8 -1
- package/dist/leader/wire.js +4 -3
- package/dist/model-entry-refusal.d.ts +31 -9
- package/dist/model-entry-refusal.js +22 -10
- package/dist/model-route-endpoint.d.ts +10 -0
- package/dist/model-route-endpoint.js +6 -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/plugins/host-platform.d.ts +6 -1
- package/dist/plugins/remote-env-host.js +3 -11
- 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/trace/core-keyset-guard.d.ts +6 -1
- 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
|
@@ -10,7 +10,7 @@ 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, 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 } 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";
|
|
@@ -26,7 +26,7 @@ import { modelRouteUrlRefusal } from "./model-route-url.js";
|
|
|
26
26
|
import { modelRouteEndpointRefusal } from "./model-route-endpoint.js";
|
|
27
27
|
import { buildDisplaySafeUrl, redactSecrets } from "./trace/redact.js";
|
|
28
28
|
import { allowlistedEnv } from "./server-secret-env.js";
|
|
29
|
-
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";
|
|
30
30
|
export { RETENTION_MODES, MEMORY_CONSOLIDATION_MIN_INTERVAL_SEC } from "./config-types.js";
|
|
31
31
|
function csv(name) {
|
|
32
32
|
return (process.env[name] ?? "")
|
|
@@ -104,6 +104,7 @@ function defaultSkillsDir() {
|
|
|
104
104
|
return "skills";
|
|
105
105
|
}
|
|
106
106
|
}
|
|
107
|
+
export const RETIRED_ENV_REFUSAL_CODE = "config.env_key_retired";
|
|
107
108
|
const RETIRED_ENVS = [
|
|
108
109
|
["TIDB_HOST", "MYSQL_HOST", "renamed"], ["TIDB_PORT", "MYSQL_PORT", "renamed"], ["TIDB_USER", "MYSQL_USER", "renamed"],
|
|
109
110
|
["TIDB_PASSWORD", "MYSQL_PASSWORD", "renamed"], ["TIDB_DATABASE", "MYSQL_DATABASE", "renamed"], ["TIDB_POOL_SIZE", "MYSQL_POOL_SIZE", "renamed"],
|
|
@@ -120,22 +121,33 @@ const RETIRED_ENVS = [
|
|
|
120
121
|
["OA_ISSUE_LABELS", "the OA scenario was removed in 5.0.0", "removed"],
|
|
121
122
|
["APPROVAL_POLL_MS", "the OA poll-approval lane was removed in 5.0.0; the durable checkpoint lane is the only gate", "removed"],
|
|
122
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
|
+
],
|
|
123
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
|
+
}
|
|
124
145
|
function rejectRetiredEnvs() {
|
|
125
146
|
const hit = RETIRED_ENVS.filter(([o]) => process.env[o] !== undefined && process.env[o] !== "");
|
|
126
147
|
if (hit.length === 0)
|
|
127
148
|
return;
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
const parts = [];
|
|
131
|
-
if (renamed.length > 0) {
|
|
132
|
-
parts.push(`${renamed.map(([o, n]) => `${o} → use ${n}`).join("; ")} — rename the env key(s); the value semantics are unchanged`);
|
|
133
|
-
}
|
|
134
|
-
if (removed.length > 0) {
|
|
135
|
-
parts.push(`${removed.map(([o, n]) => `${o} (${n})`).join("; ")} — remove the env key(s): the capability itself was retired, ` +
|
|
136
|
-
`there is no replacement name and no rename that restores it`);
|
|
137
|
-
}
|
|
138
|
-
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(" | ")}.`);
|
|
139
151
|
}
|
|
140
152
|
export function numEnv(name, fallback) {
|
|
141
153
|
return parseNumOrFail(name, env(name, fallback));
|
|
@@ -491,9 +503,8 @@ function parseNameListEnv(name, raw, opts) {
|
|
|
491
503
|
return names;
|
|
492
504
|
}
|
|
493
505
|
const ENV_VAR_NAME_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
494
|
-
const ENV_NAME_ALLOWLIST_KNOB_NAMES = new Set(Object.keys(ENV_NAME_ALLOWLIST_KNOBS));
|
|
495
506
|
const ENV_NAME_ALLOWLIST_WITHHELD = "<withheld: entries are not echoed — a value pasted in place of a name can pass the name check>";
|
|
496
|
-
function parseEnvNameAllowlist(knob, singleUserTurnkey) {
|
|
507
|
+
export function parseEnvNameAllowlist(knob, singleUserTurnkey) {
|
|
497
508
|
const raw = process.env[knob];
|
|
498
509
|
if (raw === undefined || raw === "")
|
|
499
510
|
return undefined;
|
|
@@ -513,7 +524,7 @@ function parseEnvNameAllowlist(knob, singleUserTurnkey) {
|
|
|
513
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)." +
|
|
514
525
|
` Write ${knob}=NAME,NAME with bare variable names, e.g. ${spec.example}.`);
|
|
515
526
|
});
|
|
516
|
-
if (
|
|
527
|
+
if ("multiTenant" in spec && singleUserTurnkey !== true) {
|
|
517
528
|
const posture = spec.multiTenant;
|
|
518
529
|
switch (posture.kind) {
|
|
519
530
|
case "ignored":
|
|
@@ -680,6 +691,21 @@ function parseAutoModeClassifierTimeoutMs(raw) {
|
|
|
680
691
|
}
|
|
681
692
|
return { autoModeClassifierTimeoutMs: n };
|
|
682
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
|
+
}
|
|
683
709
|
function headerNameEnv(name, fallback) {
|
|
684
710
|
const v = env(name, fallback);
|
|
685
711
|
if (!/^[A-Za-z0-9_-]+$/.test(v))
|
|
@@ -1045,7 +1071,7 @@ function parseModelDomain() {
|
|
|
1045
1071
|
}
|
|
1046
1072
|
: undefined,
|
|
1047
1073
|
resilience: {
|
|
1048
|
-
|
|
1074
|
+
...parseFirstByteTimeoutMs(process.env.MODEL_FIRST_BYTE_TIMEOUT_MS),
|
|
1049
1075
|
firstTokenTimeoutMs: numEnv("MODEL_FIRST_TOKEN_TIMEOUT_MS", String(RESILIENCE_DEFAULTS.firstTokenTimeoutMs.value)),
|
|
1050
1076
|
idleTimeoutMs: numEnv("MODEL_IDLE_TIMEOUT_MS", String(RESILIENCE_DEFAULTS.idleTimeoutMs.value)),
|
|
1051
1077
|
circuitBreaker: boolEnv("MODEL_CIRCUIT_BREAKER", RESILIENCE_DEFAULTS.circuitBreaker.value),
|
|
@@ -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
|
@@ -12,8 +12,11 @@
|
|
|
12
12
|
* - **`restart?`(S-741)**:在场 ⇔ 本副本必须重启才能应用配置 —— 取自 `/health.restart` **同一只**取值口(装配层接
|
|
13
13
|
* `deployment.restartState`,config-center 的 `restartState()`),同一个对象,形 = settings-schema `RestartSignal`;
|
|
14
14
|
* 缺席即键缺席(中心按行整体替换,不合并 —— 一条不带它的心跳就是「不再需要」)。
|
|
15
|
-
* -
|
|
16
|
-
* `
|
|
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`
|
|
17
20
|
* (两处 = 两类输入各自的唯一写者,触发都是 `announceNow`)。退出前 DELETE(显式注销,幂等)。
|
|
18
21
|
* - **usage 批报**:tracer 的 `brain.call` 已带 ALS principal —— FleetUsageAccumulator 按 principal×model
|
|
19
22
|
* 累计,每窗口(默认 60s,半开 [windowStartMs, windowEndMs))POST /api/fleet/usage。幂等键
|
|
@@ -21,7 +24,7 @@
|
|
|
21
24
|
* 向前合并不会双计 —— 只延迟,不丢量)。
|
|
22
25
|
* - 全部 fire-and-forget + swallow-guarded:fleet 面是运维旁路,绝不影响任务面;网络错误只 warn 一次/连败类。
|
|
23
26
|
*/
|
|
24
|
-
import { UsageReport, type RestartSignal } from "@sema-agent/settings-schema/fleet";
|
|
27
|
+
import { UsageReport, type ModelRefusalsSignal, type RestartSignal } from "@sema-agent/settings-schema/fleet";
|
|
25
28
|
import type { ReadinessVerdict } from "./http/readiness.js";
|
|
26
29
|
import { type CenterEndpoint } from "./config-center/center-request.js";
|
|
27
30
|
/** Bound on distinct principal×model keys per window — a runaway/multi-tenant burst collapses into
|
|
@@ -80,6 +83,9 @@ export interface FleetClientOpts {
|
|
|
80
83
|
/** S-741:restart 信号(装配层接 `/health` 同一只取值口 `deployment.restartState`)。每条公告现读一次;`undefined` ⇒ 公告不带
|
|
81
84
|
* `restart` 键。必填:心跳面没有「不知道要不要重启」这一形。 */
|
|
82
85
|
restart: () => RestartSignal | undefined;
|
|
86
|
+
/** S-768:模型拒因信号(装配层接 `/health` 同一只取值口 `deployment.modelRefusalsState`)。每条公告现读一次;`undefined` ⇒ 公告不带
|
|
87
|
+
* `modelRefusals` 键。必填:与 `restart` 同 —— 心跳面没有「不知道有没有拒」这一形(老引擎不发这个键,中心按 `version` 判「不可知」)。 */
|
|
88
|
+
modelRefusals: () => ModelRefusalsSignal | undefined;
|
|
83
89
|
usage?: FleetUsageAccumulator;
|
|
84
90
|
heartbeatMs?: number;
|
|
85
91
|
usageWindowMs?: number;
|
package/dist/fleet-client.js
CHANGED
|
@@ -87,6 +87,7 @@ export function startFleetClient(opts) {
|
|
|
87
87
|
try {
|
|
88
88
|
const verdict = opts.readiness();
|
|
89
89
|
const restart = opts.restart();
|
|
90
|
+
const modelRefusals = opts.modelRefusals();
|
|
90
91
|
const body = {
|
|
91
92
|
instanceId: opts.instanceId,
|
|
92
93
|
worker: opts.center.worker,
|
|
@@ -96,6 +97,7 @@ export function startFleetClient(opts) {
|
|
|
96
97
|
draining: !verdict.ready && verdict.reasons.includes("draining"),
|
|
97
98
|
heartbeatMs,
|
|
98
99
|
...(restart !== undefined ? { restart } : {}),
|
|
100
|
+
...(modelRefusals !== undefined ? { modelRefusals } : {}),
|
|
99
101
|
};
|
|
100
102
|
const { res } = await send("fleet-instances", "POST", body);
|
|
101
103
|
if (!res.ok)
|
|
@@ -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)
|
package/dist/http/dispatch.js
CHANGED
|
@@ -145,6 +145,10 @@ export function createDispatch(ctx) {
|
|
|
145
145
|
const credential = deps.centerCredentialState?.();
|
|
146
146
|
return credential ? { centerCredential: credential } : {};
|
|
147
147
|
})(),
|
|
148
|
+
...(() => {
|
|
149
|
+
const modelRefusals = deps.modelRefusalsState?.();
|
|
150
|
+
return modelRefusals ? { modelRefusals } : {};
|
|
151
|
+
})(),
|
|
148
152
|
...(() => {
|
|
149
153
|
const probe = deps.storeLiveState?.();
|
|
150
154
|
if (!probe)
|
package/dist/http/server.d.ts
CHANGED
|
@@ -680,6 +680,10 @@ export interface ServiceDeploymentDeps {
|
|
|
680
680
|
* `buildCenterCredentialHealthView` 铸形,只有闭集词 / 时刻 / 计数,不带路径与令牌)。缺席 / 返回 undefined = 健康或没有中心
|
|
681
681
|
* ⇒ `/health` 形状逐字不变(additive)。 */
|
|
682
682
|
centerCredentialState?: () => import("../center-credential-renewer.js").CenterCredentialHealthView | undefined;
|
|
683
|
+
/** S-768:模型拒因信号(`/health.modelRefusals`,settings-schema 6.2.0 `ModelRefusalsSignal` 形:名 → 码 + 组拒码)。fleet 心跳读**这同一只**
|
|
684
|
+
* (`boot/stage-10-http-server.ts` → `modelRefusals: () => deployment.modelRefusalsState?.()`)—— 一处铸、两出口。缺席 / 返回 undefined =
|
|
685
|
+
* 没有可报的拒或没有配置管道 ⇒ `/health` 形状逐字不变(additive)。 */
|
|
686
|
+
modelRefusalsState?: () => import("@sema-agent/settings-schema").ModelRefusalsSignal | undefined;
|
|
683
687
|
/** [ref] 追加件:`POST /v1/admin/config/refresh` 的实现面 —— 触发**一次既有**的 refresh 拍(在飞则 join)。
|
|
684
688
|
* 缺席 / 回 `"not-wired"` = 本部署没有配置管道 ⇒ 该口诚实 4xx(绝不吞成 200)。 */
|
|
685
689
|
configRefreshNow?: () => Promise<"triggered" | "not-wired" | "failed">;
|
package/dist/leader/wire.d.ts
CHANGED
|
@@ -264,7 +264,14 @@ export declare const LEADER_PUSH_LOCAL_TIMEOUT_MS = 30000;
|
|
|
264
264
|
/** Coordinator(control plane,持有推送凭据)的 git push 闭包——从 `createLeaderRunner` 抽出为独立工厂,
|
|
265
265
|
* 仅依赖 durableRemote/targetRef/git 身份(不依赖 wire 内其余状态),因此可以脱离完整的
|
|
266
266
|
* plan→provision→fan-out→merge 管线单测(那条管线需要真 brain + E2B/k8s env)。行为与抽出前逐字相同,
|
|
267
|
-
* 唯一新增是六条 execFileSync 各自的 `timeout`。
|
|
267
|
+
* 唯一新增是六条 execFileSync 各自的 `timeout`。
|
|
268
|
+
*
|
|
269
|
+
* 7.103.0(S-684 残余):六条 git 子进程的 env 一律 = `gitSubprocessEnv({ isolateConfig: false })` + `LC_ALL=C`(修前:
|
|
270
|
+
* `{ ...process.env, LC_ALL: "C" }`,两条 `git config` 连 env 都不交 ⇒ 整份服务 env 进 git 与它拉起的 helper / hook)。
|
|
271
|
+
* **不隔离宿主 git 配置**(读后定):写凭据是控制面宿主的(「Coordinator's creds」,`durableRemote` 由受信调用方给出,
|
|
272
|
+
* 代码里从不带凭据),而宿主侧能供凭据的形 —— 全局配置里的 credential helper(自带存储)/ `url.<…>.insteadOf` 改写 /
|
|
273
|
+
* `core.sshCommand` —— 都住在全局 git 配置里,隔离会把它们一起切掉;URL 里带凭据的形隔离与否都能用,所以隔离只会打断前者。
|
|
274
|
+
* 仍然剥密 + `GIT_TERMINAL_PROMPT=0`:helper 看得到的只剩非密钥 env,凭据缺席 ⇒ 响亮失败而不是挂在 prompt 上。 */
|
|
268
275
|
export declare function createCoordinatorPush(durableRemote: string, targetRef: string | undefined, ident: {
|
|
269
276
|
name: string;
|
|
270
277
|
email: string;
|
package/dist/leader/wire.js
CHANGED
|
@@ -8,6 +8,7 @@ import { Runner, combinePolicies, createDurableQuestionPolicy, QUESTION_AWAITS_R
|
|
|
8
8
|
import { RemoteContainerExecutionEnv } from "../plugins/remote-env-e2b.js";
|
|
9
9
|
import { E2B_SANDBOX_HOME } from "../execution-lane-caps.js";
|
|
10
10
|
import { createDurableAskPolicy } from "../approval.js";
|
|
11
|
+
import { gitSubprocessEnv } from "../server-secret-env.js";
|
|
11
12
|
import { presignS3Url } from "../plugins/s3-presign.js";
|
|
12
13
|
import { buildUploadScript, uploadStepSuffix, fetchUploadedDiff, withStaging } from "./diffup.js";
|
|
13
14
|
import { pullDiff, safeRev } from "./diffout.js";
|
|
@@ -100,11 +101,11 @@ export const LEADER_PUSH_LOCAL_TIMEOUT_MS = 30_000;
|
|
|
100
101
|
export function createCoordinatorPush(durableRemote, targetRef, ident) {
|
|
101
102
|
return async (integratedPatch, baseSha) => {
|
|
102
103
|
const dir = mkdtempSync(join(tmpdir(), "leader-coord-"));
|
|
103
|
-
const gitEnv = { ...
|
|
104
|
+
const gitEnv = { ...gitSubprocessEnv({ site: "server.leader.git-push", isolateConfig: false }), LC_ALL: "C" };
|
|
104
105
|
try {
|
|
105
106
|
execFileSync("git", ["clone", "-q", durableRemote, dir], { env: gitEnv, timeout: LEADER_PUSH_NETWORK_TIMEOUT_MS });
|
|
106
|
-
execFileSync("git", ["-C", dir, "config", "user.name", ident.name], { timeout: LEADER_PUSH_LOCAL_TIMEOUT_MS });
|
|
107
|
-
execFileSync("git", ["-C", dir, "config", "user.email", ident.email], { timeout: LEADER_PUSH_LOCAL_TIMEOUT_MS });
|
|
107
|
+
execFileSync("git", ["-C", dir, "config", "user.name", ident.name], { env: gitEnv, timeout: LEADER_PUSH_LOCAL_TIMEOUT_MS });
|
|
108
|
+
execFileSync("git", ["-C", dir, "config", "user.email", ident.email], { env: gitEnv, timeout: LEADER_PUSH_LOCAL_TIMEOUT_MS });
|
|
108
109
|
writeFileSync(join(dir, ".leader.patch"), integratedPatch);
|
|
109
110
|
execFileSync("git", ["-C", dir, "am", "--3way", ".leader.patch"], { env: gitEnv, timeout: LEADER_PUSH_LOCAL_TIMEOUT_MS });
|
|
110
111
|
rmSync(join(dir, ".leader.patch"));
|
|
@@ -1,15 +1,20 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
import { type ModelEntryRefusalCode, type ModelPlaneRefusalCode, type ModelRefusalsSignal } from "@sema-agent/settings-schema";
|
|
2
|
+
/**
|
|
3
|
+
* 条目拒因码 → 本仓铸它的成因 —— **以码为键**(S-768):码闭集的属主是 settings-schema(`MODEL_ENTRY_REFUSAL_CODES`,6.2.0 起;此前本仓
|
|
4
|
+
* 自持一份同名码表,撞名已消灭、零别名)。那边加一个条目码 ⇒ 本表缺键 ⇒ 本仓 tsc 红(只拿 `satisfies` 约束码值证明的是「子集」,
|
|
5
|
+
* 不是「相等」;以码为键才证相等)。成因闭集由本表派生,{@link refusalEventOf} 对它穷举 —— 加成因同样编译红。
|
|
6
|
+
* 码的拼法随 `config.*` 家族,按**规则**命名:出现在拒因句的方括号里、warn 字段里、`modelRefusals` 里。
|
|
7
|
+
*/
|
|
8
|
+
export declare const ENTRY_REFUSAL_REASON_OF: {
|
|
9
|
+
readonly "config.model_no_brain": "no_brain";
|
|
10
|
+
readonly "config.model_entry_invalid": "entry_invalid";
|
|
7
11
|
};
|
|
8
|
-
|
|
9
|
-
|
|
12
|
+
/** 不可用的成因 —— **闭集**,由 {@link ENTRY_REFUSAL_REASON_OF} 派生。 */
|
|
13
|
+
export type ModelEntryRefusalReason = (typeof ENTRY_REFUSAL_REASON_OF)[ModelEntryRefusalCode];
|
|
14
|
+
/** 「这张表没有缺省模型」的机读码(S-705;boot 拒启与热路径模型面组拒同一个码;组拒码闭集属主 = settings-schema)。 */
|
|
10
15
|
export declare const DEFAULT_MODEL_MISSING_CODE = "config.default_model_missing";
|
|
11
16
|
/** 🔴 S-712「被拒的名字被档位 / 别名增广铸到了别的条目上(或被丢身份载体形不对、读不出名字,无从证明没有)」的机读码
|
|
12
|
-
* (不变量 `refused-names-not-minted`;热路径 / boot 首次 apply / dry-run
|
|
17
|
+
* (不变量 `refused-names-not-minted`;热路径 / boot 首次 apply / dry-run 同一个码;组拒码闭集属主 = settings-schema)。 */
|
|
13
18
|
export declare const REFUSED_NAME_MINTED_CODE = "config.model_refused_name_minted";
|
|
14
19
|
/**
|
|
15
20
|
* 一条不可用条目的**事实**(纯数据;`config.modelRefusals[<name>]` 的值)。
|
|
@@ -50,6 +55,8 @@ export declare function judgeCatalogEntry(m: CatalogEntryLike, ctx: CatalogEntry
|
|
|
50
55
|
/** 一批已启用条目的判决:可用的(保序)、不可用的(按 catalog name)、以及形不对的被丢身份载体行(坐标)。 */
|
|
51
56
|
export interface CatalogEntriesVerdict<M extends CatalogEntryLike> {
|
|
52
57
|
readonly usable: readonly M[];
|
|
58
|
+
/** 键 = catalog name(**每一个**名,含秘密形 / 空串 / `__proto__`:引擎自己的判决保持完整 —— S-712 不变量与选择面的「让出」都读它;
|
|
59
|
+
* 名筛只在出口,见 {@link buildModelRefusalsSignal})。键以自有数据属性写入(`__proto__` 不走原型 setter)。 */
|
|
53
60
|
readonly refused: Readonly<Record<string, ModelEntryRefusal>>;
|
|
54
61
|
/** S-712:被丢身份**载体形不对**的行坐标(`models.3`;载体不是列表时是一个固定标记)。读腿只为有名的被丢条目铸载体行
|
|
55
62
|
* (`read-warnings.ts` `DroppedModelIdentity`,`name` 必在),所以这一形只来自绕过读腿的输入被改过(盘上 LKG 被手改 / 手写
|
|
@@ -65,6 +72,21 @@ export interface CatalogEntriesVerdict<M extends CatalogEntryLike> {
|
|
|
65
72
|
* 与一条可用 / 已判不可用条目同名时不覆盖(可用条目仍可用;同名撞车由不变量 `refused-names-not-minted` 响亮拒)。
|
|
66
73
|
*/
|
|
67
74
|
export declare function judgeCatalogEntries<M extends CatalogEntryLike>(enabled: readonly M[], ctx: CatalogEntryVerdictCtx, dropped?: unknown): CatalogEntriesVerdict<M>;
|
|
75
|
+
/** S-768:一次 apply 对「最新交到手的那份候选目录」的模型拒因判决(纯数据;`ApplyReport.modelRefusals`)。 */
|
|
76
|
+
export interface ModelRefusalVerdict {
|
|
77
|
+
/** 那份候选的逐条判决(键 = catalog name,**完整**,未过名筛 —— 名筛在出口 {@link buildModelRefusalsSignal})。 */
|
|
78
|
+
readonly entries: Readonly<Record<string, ModelEntryRefusal>>;
|
|
79
|
+
/** 那份候选的模型面组拒码(组拒时在场;一个候选恰一个组拒判决)。 */
|
|
80
|
+
readonly plane?: ModelPlaneRefusalCode;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* S-768 —— 判决 → `/health.modelRefusals` 与 fleet 心跳 `modelRefusals` 的 wire 形(settings-schema `ModelRefusalsSignal`,纯投影):
|
|
84
|
+
* 名 → 码,**值只码**(拒因事实里的上游 id / provider 词 / 坏字段键 / 出路句都不上 wire —— `entry_invalid` 行是没人验过的行,id / provider
|
|
85
|
+
* 可能就是坏行原样;句子是给本机运维的散文,不是契约)。**名筛在这里、只在这里**:键过 settings-schema `isReportableRefusalName`(唯一判据,
|
|
86
|
+
* import 不复制)—— 秘密形 / 空串 / `__proto__` 这类出不了门的名不进键、不计数;引擎内部的判决不受影响(本机日志的写名行为与基线同)。
|
|
87
|
+
* 逐条空且无组拒 ⇒ `undefined`(键缺席:「没有」只有一种拼法)。
|
|
88
|
+
*/
|
|
89
|
+
export declare function buildModelRefusalsSignal(v: ModelRefusalVerdict | undefined): ModelRefusalsSignal | undefined;
|
|
68
90
|
/** 成因 → 它那一条 warn 的事件名(**穷举**)。两个名字是票面点名的既有/新事件,运维的告警规则按名挂。 */
|
|
69
91
|
export declare function refusalEventOf(reason: ModelEntryRefusalReason): "sema_registry_model_no_brain" | "sema_registry_model_entry_invalid";
|
|
70
92
|
/** warn 字段(不含值):`field` = 第一处坏字段(告警规则最常按它分组),`fields` = 全部。 */
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { CC_MODEL_TIER_ALIASES, DEFAULT_TIER_ORDER, expandTiers } from "@sema-agent/core";
|
|
2
|
-
import { ModelEntry } from "@sema-agent/settings-schema";
|
|
2
|
+
import { ModelEntry, isReportableRefusalName } from "@sema-agent/settings-schema";
|
|
3
3
|
import { catalogBrainLane } from "./model-provider.js";
|
|
4
4
|
import { describeSchemaIssueFields, describeSchemaIssues } from "./config-center/read-warnings.js";
|
|
5
5
|
import { DEFAULT_MODEL_REF } from "./model-select.js";
|
|
6
|
-
export const
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
export const ENTRY_REFUSAL_REASON_OF = {
|
|
7
|
+
"config.model_no_brain": "no_brain",
|
|
8
|
+
"config.model_entry_invalid": "entry_invalid",
|
|
9
9
|
};
|
|
10
|
+
function refusalCause(code) {
|
|
11
|
+
return { reason: ENTRY_REFUSAL_REASON_OF[code], code };
|
|
12
|
+
}
|
|
10
13
|
export const DEFAULT_MODEL_MISSING_CODE = "config.default_model_missing";
|
|
11
14
|
export const REFUSED_NAME_MINTED_CODE = "config.model_refused_name_minted";
|
|
12
15
|
function laneMount(lane, ctx) {
|
|
@@ -27,8 +30,7 @@ export function judgeCatalogEntry(m, ctx) {
|
|
|
27
30
|
const mount = laneMount(lane, ctx);
|
|
28
31
|
if (!mount.mounted) {
|
|
29
32
|
return {
|
|
30
|
-
|
|
31
|
-
code: MODEL_ENTRY_REFUSAL_CODES.no_brain,
|
|
33
|
+
...refusalCause("config.model_no_brain"),
|
|
32
34
|
id: m.id,
|
|
33
35
|
provider: m.provider,
|
|
34
36
|
fields: [],
|
|
@@ -41,8 +43,7 @@ export function judgeCatalogEntry(m, ctx) {
|
|
|
41
43
|
}
|
|
42
44
|
function entryInvalidRefusal(id, provider, fields, issues) {
|
|
43
45
|
return {
|
|
44
|
-
|
|
45
|
-
code: MODEL_ENTRY_REFUSAL_CODES.entry_invalid,
|
|
46
|
+
...refusalCause("config.model_entry_invalid"),
|
|
46
47
|
id,
|
|
47
48
|
provider,
|
|
48
49
|
fields: [...fields],
|
|
@@ -54,12 +55,15 @@ const MALFORMED_CARRIER_NOT_A_LIST = "(droppedIdentities is not a list)";
|
|
|
54
55
|
export function judgeCatalogEntries(enabled, ctx, dropped) {
|
|
55
56
|
const usable = [];
|
|
56
57
|
const refused = {};
|
|
58
|
+
const mint = (name, r) => {
|
|
59
|
+
Object.defineProperty(refused, name, { value: r, enumerable: true, writable: true, configurable: true });
|
|
60
|
+
};
|
|
57
61
|
for (const m of enabled) {
|
|
58
62
|
const r = judgeCatalogEntry(m, ctx);
|
|
59
63
|
if (r === undefined)
|
|
60
64
|
usable.push(m);
|
|
61
65
|
else
|
|
62
|
-
|
|
66
|
+
mint(String(m.name), r);
|
|
63
67
|
}
|
|
64
68
|
const rows = dropped === undefined ? [] : Array.isArray(dropped) ? dropped : undefined;
|
|
65
69
|
if (rows === undefined)
|
|
@@ -74,10 +78,18 @@ export function judgeCatalogEntries(enabled, ctx, dropped) {
|
|
|
74
78
|
if (Object.hasOwn(refused, e.name))
|
|
75
79
|
continue;
|
|
76
80
|
const fields = Array.isArray(e.fields) ? e.fields.filter((f) => typeof f === "string") : [];
|
|
77
|
-
|
|
81
|
+
mint(e.name, entryInvalidRefusal(typeof e.id === "string" ? e.id : "", "", fields, typeof e.issues === "string" ? e.issues : fields.join(", ")));
|
|
78
82
|
}
|
|
79
83
|
return { usable, refused, malformed };
|
|
80
84
|
}
|
|
85
|
+
export function buildModelRefusalsSignal(v) {
|
|
86
|
+
if (v === undefined)
|
|
87
|
+
return undefined;
|
|
88
|
+
const entries = Object.fromEntries(Object.entries(v.entries).flatMap(([name, r]) => (isReportableRefusalName(name) ? [[name, r.code]] : [])));
|
|
89
|
+
if (Object.keys(entries).length === 0 && v.plane === undefined)
|
|
90
|
+
return undefined;
|
|
91
|
+
return v.plane === undefined ? { entries } : { entries, plane: v.plane };
|
|
92
|
+
}
|
|
81
93
|
export function refusalEventOf(reason) {
|
|
82
94
|
switch (reason) {
|
|
83
95
|
case "no_brain":
|
|
@@ -79,4 +79,14 @@ export interface ModelRouteEndpointView {
|
|
|
79
79
|
* 与别的键指同一只对象时不重复点名;只有它一个键时(手搓夹具)照样点名 `default`。
|
|
80
80
|
*/
|
|
81
81
|
export declare function modelRouteEndpointRefusal(view: ModelRouteEndpointView): string | undefined;
|
|
82
|
+
/** S-768:部署级判决的结构化形 —— 码与拒因句由**同一次**判决给出(一个不变量 id 两个码,组拒码在判决那刻当数据带出,不从句子里抠)。 */
|
|
83
|
+
export interface ModelRouteEndpointVerdict {
|
|
84
|
+
readonly code: typeof GATEWAY_FALLBACK_WITHOUT_PRIMARY_CODE | typeof MODEL_ROUTE_ENDPOINT_MISSING_CODE;
|
|
85
|
+
readonly reason: string;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* **判据属主**(S-768 起的结构化形;{@link modelRouteEndpointRefusal} 是它的句子投影):`undefined` = 通过。判序即 {@link modelRouteEndpointRefusal}
|
|
89
|
+
* 头注所列 —— ① 备用无主先说,② 逐条目解析不到端点全部点名。加一枝 = 在这里同时给出码与句(返回型强制两者同在)。
|
|
90
|
+
*/
|
|
91
|
+
export declare function judgeModelRouteEndpoint(view: ModelRouteEndpointView): ModelRouteEndpointVerdict | undefined;
|
|
82
92
|
//# sourceMappingURL=model-route-endpoint.d.ts.map
|
|
@@ -14,13 +14,17 @@ export function resolveOpenaiRouteEndpoint(routeName, routeBaseUrl, gatewayBaseU
|
|
|
14
14
|
return baseUrl !== undefined ? { ok: true, baseUrl } : { ok: false, refusal: routeEndpointMissingRefusal([`"${routeName}"`]) };
|
|
15
15
|
}
|
|
16
16
|
export function modelRouteEndpointRefusal(view) {
|
|
17
|
+
return judgeModelRouteEndpoint(view)?.reason;
|
|
18
|
+
}
|
|
19
|
+
export function judgeModelRouteEndpoint(view) {
|
|
17
20
|
const gateway = declared(view.gatewayBaseUrl);
|
|
18
21
|
if (gateway === undefined && view.gatewayFallbackUrls.length > 0) {
|
|
19
|
-
|
|
22
|
+
const reason = (`env MODEL_GATEWAY_FALLBACK_URLS is set but MODEL_GATEWAY_BASEURL is not [${GATEWAY_FALLBACK_WITHOUT_PRIMARY_CODE}] — ` +
|
|
20
23
|
"the fallback URLs are mirrors of the primary gateway (same provider, same account, same key scope), tried only " +
|
|
21
24
|
"after the primary fails; with no primary there is nothing to fail over from, and MODEL_GATEWAY_BASEURL has no " +
|
|
22
25
|
`factory default. Set MODEL_GATEWAY_BASEURL to the primary gateway (for one on this machine: ${LOCAL_GATEWAY_HINT}), ` +
|
|
23
26
|
"or unset MODEL_GATEWAY_FALLBACK_URLS.");
|
|
27
|
+
return { code: GATEWAY_FALLBACK_WITHOUT_PRIMARY_CODE, reason };
|
|
24
28
|
}
|
|
25
29
|
const anthropicMounted = view.anthropic !== undefined;
|
|
26
30
|
const entries = Object.entries(view.models ?? {});
|
|
@@ -36,7 +40,7 @@ export function modelRouteEndpointRefusal(view) {
|
|
|
36
40
|
? `"${name}" (provider "anthropic", but no Anthropic route is mounted — ANTHROPIC_API_KEY / ANTHROPIC_AUTH_TOKEN are unset — so it falls to the openai-compatible lane)`
|
|
37
41
|
: `"${name}"`);
|
|
38
42
|
}
|
|
39
|
-
return unresolved.length > 0 ? routeEndpointMissingRefusal(unresolved) : undefined;
|
|
43
|
+
return unresolved.length > 0 ? { code: MODEL_ROUTE_ENDPOINT_MISSING_CODE, reason: routeEndpointMissingRefusal(unresolved) } : undefined;
|
|
40
44
|
}
|
|
41
45
|
function routeEndpointMissingRefusal(quotedNames) {
|
|
42
46
|
const one = quotedNames.length === 1;
|
|
@@ -27,6 +27,18 @@ export declare const SECRET_ENV_SCRUB_SITES: {
|
|
|
27
27
|
readonly note: "宿主车道本地语言服务器子进程 env(core 7.30.1 #1073 的 `NodeLspManagerOptions.env`)—— 语言服务器会执行仓里的代码(构建脚本/插件/工作区配置),一次泄漏等于把本进程的凭据交给仓";
|
|
28
28
|
readonly remedy: `LSP_ENV_ALLOW=NAME,NAME \u70B9\u540D\u653E\u884C(\u539F\u6837\u4EA4\u7ED9\u8BED\u8A00\u670D\u52A1\u5668,\u90E8\u7F72\u65B9\u663E\u5F0F\u627F\u62C5;${string});\u503C\u4E0D\u8FDB\u65E5\u5FD7`;
|
|
29
29
|
};
|
|
30
|
+
readonly "server.center-plugins.git-clone": {
|
|
31
|
+
readonly note: "center 下发插件的 git clone / fetch 子进程 env —— 克隆的是 center 声明里的第三方仓,git 与它拉起的 transport / askpass 以本进程身份运行";
|
|
32
|
+
readonly remedy: null;
|
|
33
|
+
};
|
|
34
|
+
readonly "server.leader.git-push": {
|
|
35
|
+
readonly note: "leader 控制面把集成结果推回 durable remote 的 git 子进程 env —— 这条腿保留宿主 git 配置(写凭据可能走宿主 credential helper),helper / hook 以本进程身份运行";
|
|
36
|
+
readonly remedy: null;
|
|
37
|
+
};
|
|
38
|
+
readonly "server.bake-runner.spawn-env": {
|
|
39
|
+
readonly note: "bake-runner(构建宿主守护进程)交给 build.sh 与主机命令(git fetch / docker push / docker image inspect / df)的 env —— 配方脚本在构建宿主上以 runner 身份运行,一次泄漏等于把 runner 的 BAKE_RUNNER_TOKEN 交给配方";
|
|
40
|
+
readonly remedy: "BAKE_ENV_ALLOW=NAME,NAME 点名放行(原样交给 build.sh 与主机命令,部署方显式承担;bake-runner 进程的旋钮,没有多租户姿态);值不进日志";
|
|
41
|
+
};
|
|
30
42
|
};
|
|
31
43
|
/** 词表键推导的闭集类型。 */
|
|
32
44
|
export type SecretEnvScrubSite = keyof typeof SECRET_ENV_SCRUB_SITES;
|
|
@@ -13,6 +13,18 @@ export const SECRET_ENV_SCRUB_SITES = {
|
|
|
13
13
|
note: "宿主车道本地语言服务器子进程 env(core 7.30.1 #1073 的 `NodeLspManagerOptions.env`)—— 语言服务器会执行仓里的代码(构建脚本/插件/工作区配置),一次泄漏等于把本进程的凭据交给仓",
|
|
14
14
|
remedy: `LSP_ENV_ALLOW=NAME,NAME 点名放行(原样交给语言服务器,部署方显式承担;${describeAllowlistPostures()});值不进日志`,
|
|
15
15
|
},
|
|
16
|
+
"server.center-plugins.git-clone": {
|
|
17
|
+
note: "center 下发插件的 git clone / fetch 子进程 env —— 克隆的是 center 声明里的第三方仓,git 与它拉起的 transport / askpass 以本进程身份运行",
|
|
18
|
+
remedy: null,
|
|
19
|
+
},
|
|
20
|
+
"server.leader.git-push": {
|
|
21
|
+
note: "leader 控制面把集成结果推回 durable remote 的 git 子进程 env —— 这条腿保留宿主 git 配置(写凭据可能走宿主 credential helper),helper / hook 以本进程身份运行",
|
|
22
|
+
remedy: null,
|
|
23
|
+
},
|
|
24
|
+
"server.bake-runner.spawn-env": {
|
|
25
|
+
note: "bake-runner(构建宿主守护进程)交给 build.sh 与主机命令(git fetch / docker push / docker image inspect / df)的 env —— 配方脚本在构建宿主上以 runner 身份运行,一次泄漏等于把 runner 的 BAKE_RUNNER_TOKEN 交给配方",
|
|
26
|
+
remedy: "BAKE_ENV_ALLOW=NAME,NAME 点名放行(原样交给 build.sh 与主机命令,部署方显式承担;bake-runner 进程的旋钮,没有多租户姿态);值不进日志",
|
|
27
|
+
},
|
|
16
28
|
};
|
|
17
29
|
const SCRUB_COUNTER = "secret_env_scrubbed_total";
|
|
18
30
|
const DISCLOSED_CAP = 512;
|