@sema-agent/server 7.96.0 → 7.98.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 +5 -0
- package/README.md +2 -2
- package/README.zh-CN.md +2 -2
- package/USAGE.md +10 -1
- package/deploy/sema-up/cluster-up.sh +8 -4
- package/deploy/sema-up/kube-up.sh +40 -15
- package/deploy/sema-up/lib/envf.sh +173 -0
- package/deploy/sema-up/sema-up.sh +113 -94
- package/deploy/sema-up/smoke.sh +8 -1
- package/dist/approval.d.ts +28 -0
- package/dist/approval.js +24 -0
- package/dist/boot/budget-tracing.d.ts +1 -0
- package/dist/boot/budget-tracing.js +3 -2
- package/dist/boot/config-center.js +4 -7
- package/dist/boot/coordinators.js +2 -2
- package/dist/boot/deferred-sandbox-path-env.d.ts +5 -2
- package/dist/boot/deferred-sandbox-path-env.js +3 -1
- package/dist/boot/memory-boundary.d.ts +17 -2
- package/dist/boot/memory-boundary.js +11 -0
- package/dist/boot/reapers.d.ts +3 -1
- package/dist/boot/reapers.js +2 -2
- package/dist/boot/resolve-spec.js +15 -10
- package/dist/boot/runner-deps.d.ts +2 -1
- package/dist/boot/session-faces.d.ts +4 -1
- package/dist/boot/session-faces.js +5 -5
- package/dist/boot/stage-04-stores.d.ts +1 -0
- package/dist/boot/stage-04-stores.js +2 -2
- package/dist/boot/stage-05-execution-env.d.ts +1 -0
- package/dist/boot/stage-06-runners.d.ts +1 -0
- package/dist/boot/stage-07-capability-layer.d.ts +1 -0
- package/dist/boot/stage-07-capability-layer.js +10 -14
- package/dist/boot/stage-08-reapers.d.ts +2 -3
- package/dist/boot/stage-08-reapers.js +2 -2
- package/dist/boot/stage-09-leader.d.ts +1 -0
- package/dist/boot/stage-09-leader.js +2 -2
- package/dist/boot/stage-10-http-server.d.ts +1 -0
- package/dist/boot/stores.js +5 -11
- package/dist/brain.js +28 -18
- package/dist/capabilities/execution-lane.d.ts +5 -7
- package/dist/capabilities/execution-lane.js +2 -1
- package/dist/capabilities/memory-notice.d.ts +30 -12
- package/dist/capabilities/memory-notice.js +8 -1
- package/dist/capabilities/skills.d.ts +14 -0
- package/dist/capabilities/skills.js +4 -0
- package/dist/config-catalog.js +5 -4
- package/dist/config-center/apply-effective.js +7 -1
- package/dist/config-center/facade.d.ts +2 -1
- package/dist/config-invariants.d.ts +2 -2
- package/dist/config-invariants.js +9 -1
- package/dist/config-types.d.ts +20 -3
- package/dist/config.d.ts +9 -0
- package/dist/config.js +46 -8
- package/dist/env-facts.js +2 -4
- package/dist/execution-lane-caps.d.ts +36 -6
- package/dist/execution-lane-caps.js +4 -0
- package/dist/file-history-capture.d.ts +65 -0
- package/dist/file-history-capture.js +21 -0
- package/dist/hooks/hook-llm.d.ts +1 -1
- package/dist/hooks/hook-llm.js +3 -1
- package/dist/hosted-posture.d.ts +1 -1
- package/dist/http/resume-legs.d.ts +14 -4
- package/dist/http/resume-legs.js +26 -17
- package/dist/http/route-ctx.d.ts +35 -9
- package/dist/http/routes/approvals-assistant.d.ts +1 -1
- package/dist/http/routes/approvals-assistant.js +16 -6
- package/dist/http/routes/capabilities.js +2 -0
- package/dist/http/routes/runs.js +2 -2
- package/dist/http/routes/session-sync.js +6 -1
- package/dist/http/server.d.ts +4 -1
- package/dist/http/server.js +1 -1
- package/dist/http/verify-rounds.d.ts +1 -1
- package/dist/memory-scope.d.ts +68 -13
- package/dist/memory-scope.js +35 -5
- package/dist/model-provider.d.ts +13 -0
- package/dist/model-provider.js +3 -0
- package/dist/model-route-endpoint.d.ts +82 -0
- package/dist/model-route-endpoint.js +55 -0
- package/dist/observability/fail-open.d.ts +4 -0
- package/dist/observability/fail-open.js +4 -0
- package/dist/project-identity.d.ts +29 -0
- package/dist/project-identity.js +120 -0
- package/dist/run-local.d.ts +14 -4
- package/dist/run-local.js +12 -10
- package/dist/session-sync.d.ts +4 -1
- package/dist/session-sync.js +2 -2
- package/dist/task-cwd.d.ts +9 -3
- package/dist/task-cwd.js +3 -1
- package/dist/trace/redact.d.ts +1 -1
- package/package.json +1 -1
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { toolsRunOnThisHost } from "../task-cwd.js";
|
|
2
|
+
import { executionLaneOf } from "../execution-lane-caps.js";
|
|
2
3
|
export function projectExecutionLaneCapability(config) {
|
|
3
4
|
return {
|
|
4
|
-
provider: config.remoteExec?.provider
|
|
5
|
+
provider: executionLaneOf(config.remoteExec?.provider),
|
|
5
6
|
toolsOnThisHost: toolsRunOnThisHost(config),
|
|
6
7
|
};
|
|
7
8
|
}
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
*/
|
|
21
21
|
import { type PromptProvider, type ToolCatalogEntry } from "@sema-agent/core";
|
|
22
22
|
import type { ScenarioHands } from "./hands-lane.js";
|
|
23
|
+
import { type ExecutionLane } from "../execution-lane-caps.js";
|
|
23
24
|
/**
|
|
24
25
|
* 「已知落盘路」的判据,作用在 core **静态工具目录**的一行上(S-503 / [ref])—— 与 core 自己的
|
|
25
26
|
* `rosterCanPersist` 推断逐条同形,取的是同一批目录属性,不是它结论的抄件:
|
|
@@ -77,24 +78,41 @@ export interface MemoryFaceFacts {
|
|
|
77
78
|
/** 折完之后的最终 `TaskSpec.excludeTools`(roster 真卸载,不是延迟披露)。**全部**已知落盘路都被排掉
|
|
78
79
|
* ⇒ 手带在场也等于没有写通道 —— core 的 `rosterCanPersist` 也是按排除后的 roster 算的,这里跟上。 */
|
|
79
80
|
excludeTools: readonly string[] | undefined;
|
|
80
|
-
/**
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
|
|
88
|
-
|
|
81
|
+
/**
|
|
82
|
+
* 本部署的手带**挂没挂**(= core 拿没拿到执行环境;能力表 `coreRemoteExecutionEnv` 位,经
|
|
83
|
+
* {@link buildNoticeLaneFacts} 取)。`false` = `in-process`:core 落 `StubExecutionEnv`,手带整族不 mount
|
|
84
|
+
* ⇒ **一条落盘路都没有**,不管名册上写着什么名字。
|
|
85
|
+
* 🔴 S-602(clay 裁 [ref] A):改前这里只有一位 `remoteExecutionEnv`(判别位 = `REMOTE_EXEC` 设没设),
|
|
86
|
+
* 于是 in-process 读成「不是 remote ⇒ 手就在本机 fs 上 ⇒ 已知落盘路真的在」—— 而那条 lane 根本没有手,
|
|
87
|
+
* 用户说「记住 X」拿到的是无处落盘的幻觉回执([ref] 要消灭的原形)。一位拆成两位,各答各的问题。
|
|
88
|
+
*/
|
|
89
|
+
handsWired: boolean;
|
|
90
|
+
/**
|
|
91
|
+
* 手带的文件平面**不在**本 worker(能力表 `hostFilePlane` 答否;经 {@link buildNoticeLaneFacts} 取)——
|
|
92
|
+
* 沙箱 / 远端 / 设备车道。为什么这一位必须进判别式(codex 复审 round2 [high],已核真):core 的
|
|
93
|
+
* `rosterCanPersist` 在沙箱盘上把 Write/Edit/NotebookEdit/Bash **全部不算**落盘路(每任务即焚,够不着
|
|
94
|
+
* host 记忆根)。而 server 在任何 remote 车道上默认让记忆引擎 dark —— 两件事合起来正是**最常见的云部署形**:
|
|
95
|
+
* 引擎不在、手带在,却一份持久面都没有。
|
|
96
|
+
* ⚠️ `host` 车道这一位是 `false`(文件平面就是本机);core 自己仍把 host 判 remote,那一面由装配层的
|
|
97
|
+
* `effectiveMemoryPersistenceCapable`(host 腿自动声明 capable)在臂② 接住,不归本位。
|
|
98
|
+
*/
|
|
99
|
+
filePlaneRemote: boolean;
|
|
89
100
|
}
|
|
101
|
+
/**
|
|
102
|
+
* S-602 —— 判别式的两位**车道事实**,由能力表派生([ref]:不手抄投影)。resolve-spec 的装配点只调它,
|
|
103
|
+
* 不在调用点手写 `config.remoteExec !== undefined`(门 D R4)。
|
|
104
|
+
*/
|
|
105
|
+
export declare function buildNoticeLaneFacts(lane: ExecutionLane): Pick<MemoryFaceFacts, "handsWired" | "filePlaneRemote">;
|
|
90
106
|
/**
|
|
91
107
|
* 该不该把 `NO_PERSISTENT_MEMORY_NOTICE` 组进本次任务的 system prompt。
|
|
92
108
|
*
|
|
93
109
|
* 优先序(每一条都有独立理由,不是同一条判据的三种写法):
|
|
94
110
|
* ① engine 在场 ⇒ **否**(引擎的座位,见文件头注);
|
|
95
|
-
* ② 部署显式声明 ⇒ 声明说了算(`true` 撤回 / `false` 强制)—— operator
|
|
96
|
-
*
|
|
97
|
-
* ③ 否则推断:有已知落盘路(
|
|
111
|
+
* ② 部署显式声明(或 host 腿装配层自动声明)⇒ 声明说了算(`true` 撤回 / `false` 强制)—— operator 的部署
|
|
112
|
+
* 事实压过我们的推断,与 core 对 `memoryPersistenceCapable` 的处置同向;
|
|
113
|
+
* ③ 否则推断:有已知落盘路(手带挂上、场景不摘手、不是只读、文件平面在本机、且没被卸载决定卸光)⇒ 否;没有 ⇒ 是。
|
|
114
|
+
* 🔴 S-602 只改臂③ 里「车道」那一段(`handsWired` / `filePlaneRemote`),①② 的优先序一字不动 —— 「in-process ⇒
|
|
115
|
+
* 披露」只在这两臂**之后**成立(挂了记忆面、或声明了 capable 的 in-process 部署照旧不披露)。
|
|
98
116
|
*/
|
|
99
117
|
export declare function shouldDiscloseNoPersistentMemory(facts: MemoryFaceFacts): boolean;
|
|
100
118
|
/**
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describeToolCatalog, HANDS_BAND_TAGS, NO_PERSISTENT_MEMORY_NOTICE } from "@sema-agent/core";
|
|
2
2
|
import { hasConstitutionAnchors } from "../task-settings.js";
|
|
3
|
+
import { executionLaneCaps } from "../execution-lane-caps.js";
|
|
3
4
|
export function persistencePathToolNamesOf(entries) {
|
|
4
5
|
const names = new Set();
|
|
5
6
|
for (const e of entries) {
|
|
@@ -21,6 +22,10 @@ export function persistencePathToolNamesOf(entries) {
|
|
|
21
22
|
}
|
|
22
23
|
export const KNOWN_PERSISTENCE_TOOL_NAMES = persistencePathToolNamesOf(describeToolCatalog());
|
|
23
24
|
export const NO_PERSISTENT_MEMORY_SECTION_ID = "sema/memory.no-persistent";
|
|
25
|
+
export function buildNoticeLaneFacts(lane) {
|
|
26
|
+
const caps = executionLaneCaps(lane);
|
|
27
|
+
return { handsWired: caps.coreRemoteExecutionEnv === "supported", filePlaneRemote: caps.hostFilePlane !== "supported" };
|
|
28
|
+
}
|
|
24
29
|
function isUnmounted(facts, name) {
|
|
25
30
|
return facts.excludeAllTools || (facts.excludeTools?.includes(name) ?? false);
|
|
26
31
|
}
|
|
@@ -34,7 +39,9 @@ export function shouldDiscloseNoPersistentMemory(facts) {
|
|
|
34
39
|
return !facts.declaredCapable;
|
|
35
40
|
if (facts.hands === "none" || facts.handsReadOnly)
|
|
36
41
|
return true;
|
|
37
|
-
if (facts.
|
|
42
|
+
if (!facts.handsWired)
|
|
43
|
+
return true;
|
|
44
|
+
if (facts.filePlaneRemote)
|
|
38
45
|
return true;
|
|
39
46
|
return !anyPersistenceToolSurvives(facts);
|
|
40
47
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { SkillSpec } from "@sema-agent/core";
|
|
2
|
+
import type { RemoteExecProvider } from "../execution-lane-caps.js";
|
|
2
3
|
/**
|
|
3
4
|
* Built-in skills baked into the image, loaded ONCE at startup (no per-task disk IO). A skill is a
|
|
4
5
|
* Markdown capability (frontmatter + body). core uses **progressive disclosure** (core ≥1.98.0):
|
|
@@ -57,6 +58,19 @@ export interface LoadSkillsOptions {
|
|
|
57
58
|
* `label` = 错误文本里的对象名(center-plugins 用它约束 manifest 本体,报错不该指成 skill entry)。 */
|
|
58
59
|
export declare function assertConfined(p: string, rootReal: string, label?: string): void;
|
|
59
60
|
export declare function loadSkills(dir: string, opts?: LoadSkillsOptions): LoadedSkill[];
|
|
61
|
+
/**
|
|
62
|
+
* S-602 —— **出厂技能**的装载腿(`SKILLS_DIR` 自扫)。stage-07 与 run-local 两处装配点**都调这一只**:改前两处
|
|
63
|
+
* 各写一遍 `loadSkills(config.skillsDir, { toolsShareThisFilesystem: toolsRunOnThisHost(config) })`,测试只能
|
|
64
|
+
* 直传布尔去证 `loadSkills`(`baked-skills.test.ts`),证不到「config → 装配腿 → baseDir」这一段;收成一只
|
|
65
|
+
* 之后,联合格从 config 走到 `baseDir`(`test/capabilities-http.test.ts` S-602 段)证的就是两处装配点本人。
|
|
66
|
+
* 判据属主不变:`task-cwd.ts` 的 {@link toolsRunOnThisHost}(S-602 起 in-process ⇒ 假 ⇒ 目录形技能不带 `baseDir`)。
|
|
67
|
+
*/
|
|
68
|
+
export declare function loadDeploymentSkills(config: {
|
|
69
|
+
readonly skillsDir: string;
|
|
70
|
+
readonly remoteExec?: {
|
|
71
|
+
readonly provider: RemoteExecProvider;
|
|
72
|
+
} | undefined;
|
|
73
|
+
}): LoadedSkill[];
|
|
60
74
|
/** Skills applicable to a scenario: those tagged with it, plus untagged (global) ones.
|
|
61
75
|
* [ref]:alias 窗撤销,单键查询——退役旧名标签不再命中(迁移面在 7.44 发车帖/MIGRATION 记载)。 */
|
|
62
76
|
export declare function skillsForScenario(loaded: LoadedSkill[], scenario: string): SkillSpec[];
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { readdirSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
2
2
|
import { join, resolve, sep } from "node:path";
|
|
3
|
+
import { toolsRunOnThisHost } from "../task-cwd.js";
|
|
3
4
|
export function assertConfined(p, rootReal, label = "skill entry") {
|
|
4
5
|
let real;
|
|
5
6
|
try {
|
|
@@ -66,6 +67,9 @@ export function loadSkills(dir, opts) {
|
|
|
66
67
|
}
|
|
67
68
|
return out;
|
|
68
69
|
}
|
|
70
|
+
export function loadDeploymentSkills(config) {
|
|
71
|
+
return loadSkills(config.skillsDir, { toolsShareThisFilesystem: toolsRunOnThisHost(config) });
|
|
72
|
+
}
|
|
69
73
|
function collectAttachments(skillDir, rel, confineRoot) {
|
|
70
74
|
const files = [];
|
|
71
75
|
const here = rel === "" ? skillDir : join(skillDir, rel);
|
package/dist/config-catalog.js
CHANGED
|
@@ -145,6 +145,7 @@ export const CONFIG_CATALOG = [
|
|
|
145
145
|
r("ENGINE_LINGER_MS", "limitsHttp", "number", "父还活着时自退前三合取须持续成立的静默窗(ms;[1000,24h] 拒启带界)", { staticDefault: "60000", configKey: "engineLingerMs" }),
|
|
146
146
|
r("ENGINE_ORPHAN_LINGER_MS", "limitsHttp", "number", "父进程已不在(孤儿)时的静默窗(ms;[1000,24h] 拒启带界;SEMA_PARENT_PID 缺席则不生效)", { staticDefault: "2000", configKey: "engineOrphanLingerMs" }),
|
|
147
147
|
r("EXPERIMENTAL_OBSERVER_AGENTS", "orchestration", "boolean", "observer 开闸线(实验面 ship-dark;多租户只认 center caps,env 被忽略)", { staticDefault: "false", configKey: "experimentalObserverAgents" }),
|
|
148
|
+
r("FILE_HISTORY_CAPTURE", "store", "boolean", "文件历史**捕获**总开关(三条车道同义)。交换:关 = 不再捕获任何改动文件的首次快照,同时没有 rewind / restoreFiles / rewindFilesTo / 2c session-sync(对应能力位与 `fileHistoryCapture` 同源翻 off)。关掉 ≠ 删掉:已捕获的历史不被批量删除、也不再被读用,仍按会话删除与留存规则清理 —— 要清存量走 DELETE 会话或留存旋钮", { staticDefault: "true", danger: DATA, configKey: "fileHistoryCapture" }),
|
|
148
149
|
r("FILE_HISTORY_RETENTION_KEEP", "store", "number", "per-file rewind 历史的保留上限(与 core FileStorageBackend 同域键形,三条车道一套语义)", { danger: DATA, derivedDefaultNote: "缺席 ⇒ core 缺省 100" }),
|
|
149
150
|
r("FLEET_ADVERTISE_ADDRESS", "integrations", "url", "fleet 面向 center 公告的本 worker 可达地址(与 center 腿同在 ⇒ 启 fleet client;坏形拒启)"),
|
|
150
151
|
r("FORK_ENABLED", "orchestration", "boolean", "CC /fork(core Fork 工具)装配开关", { derivedDefaultNote: "posture 三态:缺席 = 单用户 ⇒ on / 多租户 ⇒ off(per-principal entitlement 另判)", configKey: "forkEnabled" }),
|
|
@@ -244,14 +245,14 @@ export const CONFIG_CATALOG = [
|
|
|
244
245
|
r("MEMORY_SYNC_MAX_PUSH_ENTRIES", "memory", "number", "TOC 同步单轮 push 条数帽(设了必须 ≥1 整数)", { derivedDefaultNote: "缺席 ⇒ 键缺席,core 默认" }),
|
|
245
246
|
r("MEMORY_SYNC_SCOPE", "memory", "string", "TOC 同步 scope 覆写", { derivedDefaultNote: "缺席 ⇒ 从 MEMORY_SCOPE 铸 user 盘键" }),
|
|
246
247
|
r("MEMORY_SYNC_TOKEN", "memory", "secret", "TOC 同步中心面的 Bearer(URL 设了它必须在,半配拒启)", { danger: SEC }),
|
|
247
|
-
r("MEMORY_SYNC_URL", "memory", "url", "TOC 同步中心地址(设了=开;file 记忆形专用,DB
|
|
248
|
+
r("MEMORY_SYNC_URL", "memory", "url", "TOC 同步中心地址(设了=开;file 记忆形专用,DB 后端配了拒启;同步只覆盖一层 ⇒ 单用户任务不按项目键控、写家落全局层 MEMORY_SCOPE(S-512 让位,启动通知 `memory_project_scope_yields_to_sync`))"),
|
|
248
249
|
r("METRICS_TOKEN", "observability", "secret", "/metrics 只读令牌", { danger: SEC }),
|
|
249
250
|
r("MINIO_ACCESS_KEY", "store", "secret", "对象存储 access key(快照 blob/leader 工作区/公链签发共用)", { danger: SEC }),
|
|
250
251
|
r("MINIO_BUCKET", "store", "string", "leader 工作区快照桶", { derivedDefaultNote: "缺席 ⇒ workspaces" }),
|
|
251
252
|
r("MINIO_ENDPOINT", "store", "url", "对象存储 endpoint(三键齐 ⇒ 快照 blob 走 MinIO,否则 SQL 行)", { danger: DATA }),
|
|
252
253
|
r("MINIO_REGION", "store", "string", "对象存储 region"),
|
|
253
254
|
r("MINIO_SECRET_KEY", "store", "secret", "对象存储 secret key", { danger: SEC }),
|
|
254
|
-
r("MODEL_API_KEY", "modelPlane", "secret", "网关腿共享 API key(per-model 覆写在 center
|
|
255
|
+
r("MODEL_API_KEY", "modelPlane", "secret", "网关腿共享 API key(per-model 覆写在 center 侧;S-623:只与 MODEL_GATEWAY_BASEURL 配对 —— 网关未设时不装载、不发往任何路由,启动通知 `model_gateway_key_unpaired`)", { danger: [...SEC, ...BILL] }),
|
|
255
256
|
r("MODEL_AUTO_COMPACT_TOKENS", "modelPlane", "number", "主槽 dual-window 触发阈(1M 窗不再隐式 967000,缺席则用普通触发几何)", { derivedDefaultNote: "缺席 ⇒ 不铸键(core W-33000 几何)" }),
|
|
256
257
|
r("MODEL_CASCADE_LADDER", "modelPlane", "csv", "质量门级联梯(目录名 cheapest→strongest;opt-in per-task cascade:true)", { danger: BILL, configKey: "cascadeLadder" }),
|
|
257
258
|
r("MODEL_CB_COOLDOWN_MS", "modelPlane", "number", "熔断冷却窗(ms)", { staticDefault: String(RESILIENCE_DEFAULTS.cooldownMs.value) }),
|
|
@@ -285,8 +286,8 @@ export const CONFIG_CATALOG = [
|
|
|
285
286
|
r("MODEL_EXTRA_BODY", "modelPlane", "opaque", "生成参数直通 JSON(top_k/logit_bias 等;必须静态 —— 前缀缓存键序)", { danger: SEC }),
|
|
286
287
|
r("MODEL_FIRST_TOKEN_TIMEOUT_MS", "modelPlane", "number", "首 token 超时(ms;0=关 —— 开流不吐字的唯一看门狗;自托管长 prefill 后端按此档留足)", { staticDefault: String(RESILIENCE_DEFAULTS.firstTokenTimeoutMs.value) }),
|
|
287
288
|
r("MODEL_FREQUENCY_PENALTY", "modelPlane", "number", "frequency_penalty 直通(压过 EXTRA_BODY 同键)"),
|
|
288
|
-
r("MODEL_GATEWAY_BASEURL", "modelPlane", "url", "openai 兼容网关 base URL(
|
|
289
|
-
r("MODEL_GATEWAY_FALLBACK_URLS", "modelPlane", "url", "网关故障转移地址表(CSV;S-475:**逐项**判 userinfo,任一项带 ⇒ 拒启,机读码 `config.gateway_url_userinfo`;S-483:逐项判可解析性,机读码 `config.model_route_url_invalid`;S-514:**逐项**判 `#fragment`,机读码 `config.model_route_url_fragment`)", { configKey: "gatewayFallbackUrls" }),
|
|
289
|
+
r("MODEL_GATEWAY_BASEURL", "modelPlane", "url", "openai 兼容网关 base URL(S-623:**无出厂缺省** —— 配置能派发到的每条 openai 兼容路由都必须解析到显式端点(条目自己的 baseUrl,或本键),否则拒启 / 热路径整批拒,机读码 `config.model_route_endpoint_missing`,拒因点名条目并给出本机写法 `MODEL_GATEWAY_BASEURL=<本机网关>/v1`;anthropic 单路由部署不设本键合法;S-475:带 userinfo 拒启,机读码 `config.gateway_url_userinfo` —— WHATWG 禁止用带凭据的 URL 构造 fetch Request,凭据放 MODEL_API_KEY;S-483:解析不动/不透明形拒启,机读码 `config.model_route_url_invalid`;S-514:带 `#fragment` 拒启,机读码 `config.model_route_url_fragment`)", { derivedDefaultNote: "缺席 ⇒ 本部署没有网关(不铸键);有 openai 路由落不到端点 ⇒ 拒启", configKey: "gatewayBaseUrl", center: cModels("models[].baseUrl") }),
|
|
290
|
+
r("MODEL_GATEWAY_FALLBACK_URLS", "modelPlane", "url", "网关故障转移地址表(CSV;S-623:主网关 MODEL_GATEWAY_BASEURL 未设而本键设了 ⇒ 拒启,机读码 `config.gateway_fallback_without_primary` —— 备用是主网关的镜像;S-475:**逐项**判 userinfo,任一项带 ⇒ 拒启,机读码 `config.gateway_url_userinfo`;S-483:逐项判可解析性,机读码 `config.model_route_url_invalid`;S-514:**逐项**判 `#fragment`,机读码 `config.model_route_url_fragment`)", { configKey: "gatewayFallbackUrls" }),
|
|
290
291
|
r("MODEL_ID", "modelPlane", "string", "主模型 id(3.0.0 起无出厂缺省,缺 ⇒ 拒启指路)", { configKey: "model", resolve: (c) => c.model.id, center: cModels("models[].id") }),
|
|
291
292
|
r("MODEL_IDLE_TIMEOUT_MS", "modelPlane", "number", "流中静默看门狗(ms;0=关)", { staticDefault: String(RESILIENCE_DEFAULTS.idleTimeoutMs.value) }),
|
|
292
293
|
r("MODEL_MAX_TOKENS", "modelPlane", "number", "主模型输出上限", { derivedDefaultNote: "缺席 ⇒ min(32000, contextWindow)", resolve: (c) => c.model.maxTokens ?? null, center: cModels("models[].maxTokens") }),
|
|
@@ -7,6 +7,7 @@ import { projectCollabToWorkflows, registerCollabWorkflows } from "../capabiliti
|
|
|
7
7
|
import { stageLimits } from "./stage-limits.js";
|
|
8
8
|
import { BATCH1_LIMIT_KEYS } from "./hot-keys-registry.js";
|
|
9
9
|
import { buildDisplaySafeUrl } from "../trace/redact.js";
|
|
10
|
+
import { crossInvariant } from "../config-invariants.js";
|
|
10
11
|
const charsPerTokenOf = (v) => (typeof v === "number" && Number.isFinite(v) && v > 0 ? v : undefined);
|
|
11
12
|
function envDerivedModel(config) {
|
|
12
13
|
const m = config.envModel;
|
|
@@ -203,7 +204,7 @@ function stageEffective(config, eff, logger, opts) {
|
|
|
203
204
|
if (ok)
|
|
204
205
|
logger?.warn("sema_registry_model_base_url", { model: m.name, baseUrl: buildDisplaySafeUrl(rawBaseUrl), hint: "registry-directed gateway override — this model's key authenticates AGAINST THIS URL" });
|
|
205
206
|
else {
|
|
206
|
-
logger?.warn("sema_registry_model_base_url_rejected", { model: m.name, baseUrl: buildDisplaySafeUrl(String(rawBaseUrl)), hint: "only http(s) URLs are honored — falling back to the
|
|
207
|
+
logger?.warn("sema_registry_model_base_url_rejected", { model: m.name, baseUrl: buildDisplaySafeUrl(String(rawBaseUrl)), hint: "only http(s) URLs are honored — falling back to the deployment gateway (MODEL_GATEWAY_BASEURL); with none declared the whole candidate is refused (model-route-endpoint)" });
|
|
207
208
|
safeUrl = undefined;
|
|
208
209
|
}
|
|
209
210
|
}
|
|
@@ -264,6 +265,11 @@ function stageEffective(config, eff, logger, opts) {
|
|
|
264
265
|
const activeTiers = resolveActiveTiers(eff.models) ?? {};
|
|
265
266
|
const picked = resolveDefaultModelName(eff, (n) => models[n] !== undefined, enabled[0].name, (source, name) => logger?.warn("sema_registry_default_dangling", { source, name, hint: "explicit default names a model that is not in the enabled catalog — falling to the next source" }));
|
|
266
267
|
models.default = models[picked.name];
|
|
268
|
+
const endpointRefusal = crossInvariant("model-route-endpoint").check({ live: config, next: { ...config, models } });
|
|
269
|
+
if (endpointRefusal !== undefined) {
|
|
270
|
+
throw new Error(`candidate model plane refused whole (cross-invariant model-route-endpoint): ${endpointRefusal} ` +
|
|
271
|
+
"The current generation keeps serving; the candidate is re-judged on the next pull.");
|
|
272
|
+
}
|
|
267
273
|
const atModelAllowlist = stageAtModelAllowlist(eff.models?.atModelAllowlist, models, logger);
|
|
268
274
|
modelPlane = {
|
|
269
275
|
models,
|
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
* (core 1.45), so each model/cascade-rung authenticates with its own upstream key.
|
|
16
16
|
*
|
|
17
17
|
* Per-model `baseUrl` IS transported (a catalog model may live on a different gateway;
|
|
18
|
-
* core brain honors model.baseUrl, absent = "" =
|
|
18
|
+
* core brain honors model.baseUrl, absent = "" = the deployment gateway `MODEL_GATEWAY_BASEURL`; S-623: with no gateway
|
|
19
|
+
* declared, a candidate carrying such an openai-lane entry is refused whole — cross-invariant `model-route-endpoint`).
|
|
19
20
|
*
|
|
20
21
|
* Hot-reload status (复审 2026-07-29 P1-11 — 亲读判定,取代此处旧的 "models/roles are restart-to-apply"
|
|
21
22
|
* TODO, which went stale when `mutateInPlace` landed):
|
|
@@ -28,9 +28,9 @@ import type { ServiceConfigFlat } from "./config-types.js";
|
|
|
28
28
|
*/
|
|
29
29
|
export declare const LIVENESS_HEARTBEAT_MS = 30000;
|
|
30
30
|
/** 闭集:新增一条不变量必须在这里加词(拼错 = 编译红,而不是运行期悄悄不判)。 */
|
|
31
|
-
export type CrossInvariantId = "run-stale-heartbeat" | "bind-posture" | "bake-runner-credential" | "direct-door-anchors" | "operator-principals-required" | "bake-operator-principals" | "sensitive-write-patterns" | "leader-host-lane" | "leader-durable-store" | "device-durable-store" | "device-durable-approval" | "model-provider-word" | "gateway-url-userinfo" | "require-execution-lane" | "hosted-requires-principal";
|
|
31
|
+
export type CrossInvariantId = "run-stale-heartbeat" | "bind-posture" | "bake-runner-credential" | "direct-door-anchors" | "operator-principals-required" | "bake-operator-principals" | "sensitive-write-patterns" | "leader-host-lane" | "leader-durable-store" | "device-durable-store" | "device-durable-approval" | "model-provider-word" | "gateway-url-userinfo" | "model-route-endpoint" | "require-execution-lane" | "hosted-requires-principal";
|
|
32
32
|
/** 表读的键集(取自平铺配置型 ⇒ 键名/类型永不与真配置漂)。 */
|
|
33
|
-
export type CrossInvariantView = Readonly<Pick<ServiceConfigFlat, "runStaleSec" | "authToken" | "authTokens" | "allowUnauthedWrites" | "imageBakes" | "directApprovalDoor" | "durableApproval" | "approvalHmacKeys" | "principalJwtPubkeys" | "principalJwtIss" | "principalJwtAud" | "requirePrincipal" | "operatorPrincipals" | "sensitiveWritePatterns" | "leaderEnabled" | "remoteExec" | "requireExecutionLane" | "dbBackend" | "tidb" | "pg" | "envModel" | "gatewayBaseUrl" | "gatewayFallbackUrls" | "anthropic" | "authBridgeIssuer">>;
|
|
33
|
+
export type CrossInvariantView = Readonly<Pick<ServiceConfigFlat, "runStaleSec" | "authToken" | "authTokens" | "allowUnauthedWrites" | "imageBakes" | "directApprovalDoor" | "durableApproval" | "approvalHmacKeys" | "principalJwtPubkeys" | "principalJwtIss" | "principalJwtAud" | "requirePrincipal" | "operatorPrincipals" | "sensitiveWritePatterns" | "leaderEnabled" | "remoteExec" | "requireExecutionLane" | "dbBackend" | "tidb" | "pg" | "envModel" | "gatewayBaseUrl" | "gatewayFallbackUrls" | "anthropic" | "models" | "authBridgeIssuer">>;
|
|
34
34
|
export interface CrossInvariantInput {
|
|
35
35
|
/** 现役配置(boot 期 = 候选自身)。 */
|
|
36
36
|
readonly live: CrossInvariantView;
|
|
@@ -2,6 +2,7 @@ import { assertGuardPatternsUsable } from "./deployment-governance.js";
|
|
|
2
2
|
import { hostedPrincipalRefusal } from "./hosted-posture.js";
|
|
3
3
|
import { modelProviderRefusal } from "./model-provider.js";
|
|
4
4
|
import { modelRouteUrlRefusal } from "./model-route-url.js";
|
|
5
|
+
import { modelRouteEndpointRefusal } from "./model-route-endpoint.js";
|
|
5
6
|
export const LIVENESS_HEARTBEAT_MS = 30_000;
|
|
6
7
|
const anyServiceAuth = (v) => Boolean(v.authToken) || Object.keys(v.authTokens ?? {}).length > 0;
|
|
7
8
|
const mustNarrowBind = (v) => Boolean(v.allowUnauthedWrites) && !anyServiceAuth(v);
|
|
@@ -160,12 +161,19 @@ export const CONFIG_CROSS_INVARIANTS = [
|
|
|
160
161
|
keys: ["gatewayBaseUrl", "gatewayFallbackUrls", "anthropic"],
|
|
161
162
|
restartReason: "model-route-url",
|
|
162
163
|
bootAnchor: { file: "src/config.ts", marker: "CROSS-INVARIANT:gateway-url-userinfo" },
|
|
163
|
-
check: ({ next }) => modelRouteUrlRefusal("MODEL_GATEWAY_BASEURL", next.gatewayBaseUrl, "MODEL_API_KEY") ??
|
|
164
|
+
check: ({ next }) => modelRouteUrlRefusal("MODEL_GATEWAY_BASEURL", next.gatewayBaseUrl ?? "", "MODEL_API_KEY") ??
|
|
164
165
|
next.gatewayFallbackUrls
|
|
165
166
|
.map((u, i) => modelRouteUrlRefusal(`MODEL_GATEWAY_FALLBACK_URLS[${i}]`, u, "MODEL_API_KEY"))
|
|
166
167
|
.find((r) => r !== undefined) ??
|
|
167
168
|
modelRouteUrlRefusal("ANTHROPIC_BASE_URL", next.anthropic?.baseUrl ?? "", "ANTHROPIC_API_KEY"),
|
|
168
169
|
},
|
|
170
|
+
{
|
|
171
|
+
id: "model-route-endpoint",
|
|
172
|
+
keys: ["gatewayBaseUrl", "gatewayFallbackUrls", "anthropic", "models"],
|
|
173
|
+
restartReason: "model-route-endpoint",
|
|
174
|
+
bootAnchor: { file: "src/config.ts", marker: "CROSS-INVARIANT:model-route-endpoint" },
|
|
175
|
+
check: ({ live, next }) => (next.models === live.models ? undefined : modelRouteEndpointRefusal(next)),
|
|
176
|
+
},
|
|
169
177
|
];
|
|
170
178
|
export function crossInvariant(id) {
|
|
171
179
|
const inv = CONFIG_CROSS_INVARIANTS.find((i) => i.id === id);
|
package/dist/config-types.d.ts
CHANGED
|
@@ -366,8 +366,18 @@ export interface ImageBakeConfig {
|
|
|
366
366
|
* fixtures that CONSTRUCT a config literal build this type and hand it to `attachConfigGroups`. */
|
|
367
367
|
export interface ServiceConfigFlat {
|
|
368
368
|
port: number;
|
|
369
|
-
/** OpenAI-compatible model gateway base URL (without /chat/completions)
|
|
370
|
-
|
|
369
|
+
/** OpenAI-compatible model gateway base URL (without /chat/completions) — `MODEL_GATEWAY_BASEURL`.
|
|
370
|
+
*
|
|
371
|
+
* 🔴 S-623:**缺席 = 这台部署没有网关**(没有出厂缺省;旧的本机占位值在读入那一步抹掉「来源」,让没配网关的
|
|
372
|
+
* 部署起得来、第一次调用才炸)。形取「可选键」而不是判别联合:消费方问的只有「有没有声明网关根、是哪个」,
|
|
373
|
+
* 一个只带一位载荷的联合不比 `string | undefined` 多说任何事,且与紧邻的 `gatewayApiKey?` / `gatewayMaxRetries?`
|
|
374
|
+
* 的「缺席不铸键」同一条 presence 律。一条 openai 路由请求打到哪儿,只经 `model-route-endpoint.ts` 的
|
|
375
|
+
* `openaiRouteBaseUrl` 求(路由自己的 `baseUrl` 胜、网关次之);「配置能派发到的每条 openai 路由都解析得到」
|
|
376
|
+
* 由 boot(config.ts)与热路径(apply-effective)同一只判据保证。 */
|
|
377
|
+
gatewayBaseUrl?: string;
|
|
378
|
+
/** `MODEL_API_KEY` —— **网关的**凭据,只与网关根配对([ref])。S-623:与 `gatewayBaseUrl` 同进同出 ——
|
|
379
|
+
* 没有网关就不装载(否则 brain 的声明根为空 ⇒ core 判 `unpinned` ⇒ 这把 key 会被送到每条自带 baseUrl 的
|
|
380
|
+
* openai 条目),由 `model_gateway_key_unpaired` 启动通知说出来。 */
|
|
371
381
|
gatewayApiKey?: string;
|
|
372
382
|
/** Secondary openai-compatible gateways for cross-gateway FAILOVER (same protocol + same model id).
|
|
373
383
|
* Tried in order after the primary when a gateway fails before streaming any content. */
|
|
@@ -956,6 +966,13 @@ export interface ServiceConfigFlat {
|
|
|
956
966
|
fileHistory?: {
|
|
957
967
|
retention?: FileHistoryRetentionPolicy;
|
|
958
968
|
};
|
|
969
|
+
/** S-589:文件历史**捕获**总开关(`FILE_HISTORY_CAPTURE`,布尔词表,缺省 true,未知词拒启)。三条车道
|
|
970
|
+
* (local / mysql / pg)同一语义:`false` ⇒ 装配点不把历史店交给 core(`RunnerDeps.fileHistoryStore` 缺席 ⇒
|
|
971
|
+
* 首次触碰捕获结构上不发生),同时没有 rewind / restore / `rewindFilesTo` / 2c session-sync(那几位能力位与
|
|
972
|
+
* 能力词 `fileHistoryCapture:"off"` 同源翻关)。**只管写入,不管擦除**:已捕获的历史照旧随会话删除 / 留存
|
|
973
|
+
* 规则清理,翻旋钮本身不做批量删除。判定的唯一属主是 `file-history-capture.ts` 的 `assembleFileHistory`。
|
|
974
|
+
* 刻意**不**并进上面的 `fileHistory` 键:那一格是 core 店构造选项的同域键形,原样透传给 core。 */
|
|
975
|
+
fileHistoryCapture: boolean;
|
|
959
976
|
/** plugins 域 clone host 白名单(`PLUGINS_ALLOW_HOSTS`,缺省 github.com;契约 [ref]① 双保险面,
|
|
960
977
|
* 消费在 capabilities/center-plugins.ts)。 */
|
|
961
978
|
pluginsAllowHosts: string[];
|
|
@@ -1905,7 +1922,7 @@ export interface ServiceConfigFlat {
|
|
|
1905
1922
|
configLocalDir?: string;
|
|
1906
1923
|
}
|
|
1907
1924
|
/** 组:store(持久化)—— DB 引擎三态、session 后端、SQL coords、快照 BLOB / SendUserFile 对象存储。 */
|
|
1908
|
-
export type ServiceStoreConfig = Pick<ServiceConfigFlat, "sessionBackend" | "sessionCacheTtlSec" | "storeProbeIntervalMs" | "storeLockWaitMs" | "dbBackend" | "dbBackendExplicit" | "localDataRoot" | "runStoreStrictHydrate" | "tidb" | "pg" | "dbQueryTimeoutMs" | "snapshotBlobStore" | "snapshotBlobSqlMaxBytes" | "snapshotBlobAllowSql" | "fileHistory" | "sendUserFile">;
|
|
1925
|
+
export type ServiceStoreConfig = Pick<ServiceConfigFlat, "sessionBackend" | "sessionCacheTtlSec" | "storeProbeIntervalMs" | "storeLockWaitMs" | "dbBackend" | "dbBackendExplicit" | "localDataRoot" | "runStoreStrictHydrate" | "tidb" | "pg" | "dbQueryTimeoutMs" | "snapshotBlobStore" | "snapshotBlobSqlMaxBytes" | "snapshotBlobAllowSql" | "fileHistory" | "fileHistoryCapture" | "sendUserFile">;
|
|
1909
1926
|
/** 组:modelPlane(模型面)—— 网关坐标、Anthropic 路线、韧性旋钮、主/廉价 model entry、role 表、降级梯。 */
|
|
1910
1927
|
export type ServiceModelPlaneConfig = Pick<ServiceConfigFlat, "gatewayBaseUrl" | "gatewayApiKey" | "gatewayFallbackUrls" | "gatewayMaxRetries" | "anthropic" | "resilience" | "model" | "envModel" | "models" | "modelApiKeyEnv" | "modelApiKeys" | "modelQuotaWeights" | "tiers" | "projects" | "roles" | "atModelAllowlist" | "cascadeLadder" | "degrade">;
|
|
1911
1928
|
/** 组:approval(审批 / HITL 门)。`directDoorActive` 无 env 解析腿(装配层三域合取的产物),但语义上
|
package/dist/config.d.ts
CHANGED
|
@@ -243,6 +243,15 @@ export declare const REQUIRED_ENVS_BY_LANE: Record<RemoteExecProvider, readonly
|
|
|
243
243
|
* a loud CONFIG_NOTICES line ("removed-1m"); the center lane's roster culture is explicit declaration.
|
|
244
244
|
* Shared by the env lane (main + cheap models) and the center lane (sema-registry toModel). */
|
|
245
245
|
export declare function applyAutoCompactWindow(m: Model, explicit?: number): "explicit" | "sonnet5" | "removed-1m" | "removed";
|
|
246
|
+
/**
|
|
247
|
+
* **严格名册布防**的唯一判据(boot 就绪门 `boot/config-center.ts` 与 S-623 端点判据同调这一只)。
|
|
248
|
+
*
|
|
249
|
+
* 布防 = 远程 registry ∧ 非 dryRun ∧ `CONFIG_REQUIRE_ROSTER=true`(严格 `"true"`,直读 env、不进 ServiceConfig —— 纯
|
|
250
|
+
* boot 期姿势旋钮,原判据逐字搬来,零语义变化)。布防时运维声明 env MODEL_ID 是**占位**:名册落地前计费提交 503、
|
|
251
|
+
* `/health` ready:false,落地后整张模型表被中心目录换装。dryRun 是 compare-only(名册从不被应用),local / 纯 env
|
|
252
|
+
* 部署没有「中心」可等 ⇒ 都不布防。
|
|
253
|
+
*/
|
|
254
|
+
export declare function isRosterGateArmed(configCenter: ServiceConfig["configCenter"]): boolean;
|
|
246
255
|
/**
|
|
247
256
|
* 一条工具名**永不匹配任何现役工具**的三种拼法,及其修正指引。审批/权限名单是 server 侧 ToolPolicy 的
|
|
248
257
|
* RAW 逐字比对(canonicalToolName 折叠面已随 core 5.0.0 [ref] 退役),所以写错的名字不会报错、只会静默
|
package/dist/config.js
CHANGED
|
@@ -7,7 +7,7 @@ import { BUILTIN_COMPLIANCE_DENIES, COMPLIANCE_CAPABILITIES, CROSS_SESSION_INBOU
|
|
|
7
7
|
import { ROSTER_PRIMARY_ROLES, ROSTER_CHEAP_ROLES } from "@sema-agent/settings-schema";
|
|
8
8
|
import { parseApprovalHmacKeys, parsePrincipalJwks } from "./auth-keys.js";
|
|
9
9
|
import { DEFAULT_ELICITATION_THROTTLE } from "./elicitation.js";
|
|
10
|
-
import { isV2ScopeKey } from "./memory-scope.js";
|
|
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
13
|
import { RETENTION_MODES, MEMORY_CONSOLIDATION_MIN_INTERVAL_SEC, RESILIENCE_DEFAULTS, TASK_WRITE_FACE_ESCALATIONS } from "./config-types.js";
|
|
@@ -18,10 +18,11 @@ import { THINKING_LEVELS, THINKING_FORMATS, buildModelCompat, isThinkingFormat,
|
|
|
18
18
|
import {} from "./posture-source.js";
|
|
19
19
|
import { hostedPrincipalRefusal } from "./hosted-posture.js";
|
|
20
20
|
import { CONTROL_AND_BIDI_STRIP_RE } from "./text-bidi.js";
|
|
21
|
-
import { REMOTE_EXEC_PROVIDER_WORDS } from "./execution-lane-caps.js";
|
|
21
|
+
import { REMOTE_EXEC_PROVIDER_WORDS, executionLaneOf } from "./execution-lane-caps.js";
|
|
22
22
|
import { AUTONOMY_MODES } from "./runtime-governance.js";
|
|
23
23
|
import { modelProviderRefusal } from "./model-provider.js";
|
|
24
24
|
import { modelRouteUrlRefusal } from "./model-route-url.js";
|
|
25
|
+
import { modelRouteEndpointRefusal } from "./model-route-endpoint.js";
|
|
25
26
|
import { buildDisplaySafeUrl, redactSecrets } from "./trace/redact.js";
|
|
26
27
|
export { RETENTION_MODES, MEMORY_CONSOLIDATION_MIN_INTERVAL_SEC } from "./config-types.js";
|
|
27
28
|
function csv(name) {
|
|
@@ -197,7 +198,7 @@ let EXEC_LANE_NOTICE;
|
|
|
197
198
|
function buildExecLaneNotice(remoteExec) {
|
|
198
199
|
const explicit = process.env.REMOTE_EXEC !== undefined && process.env.REMOTE_EXEC !== "";
|
|
199
200
|
return {
|
|
200
|
-
lane: remoteExec?.provider
|
|
201
|
+
lane: executionLaneOf(remoteExec?.provider),
|
|
201
202
|
source: explicit ? "explicit-env" : process.env.CONFIG_PROVIDER === "local" ? "default-local-provider" : "default-unset",
|
|
202
203
|
};
|
|
203
204
|
}
|
|
@@ -776,6 +777,7 @@ function parseStoreDomain(ctx) {
|
|
|
776
777
|
snapshotBlobSqlMaxBytes: optFinitePositiveEnv("SNAPSHOT_BLOB_SQL_MAX_BYTES"),
|
|
777
778
|
snapshotBlobAllowSql: boolEnv("SNAPSHOT_BLOB_ALLOW_SQL_BYTES", false),
|
|
778
779
|
fileHistory: parseFileHistoryRetention(),
|
|
780
|
+
fileHistoryCapture: boolEnv("FILE_HISTORY_CAPTURE", true),
|
|
779
781
|
sendUserFile: (process.env.MINIO_ENDPOINT || process.env.S3_ENDPOINT) && process.env.MINIO_ACCESS_KEY && process.env.MINIO_SECRET_KEY
|
|
780
782
|
? {
|
|
781
783
|
endpoint: (process.env.MINIO_ENDPOINT || process.env.S3_ENDPOINT),
|
|
@@ -794,8 +796,11 @@ function parseStoreDomain(ctx) {
|
|
|
794
796
|
: undefined,
|
|
795
797
|
};
|
|
796
798
|
}
|
|
799
|
+
export function isRosterGateArmed(configCenter) {
|
|
800
|
+
return configCenter !== undefined && !configCenter.dryRun && process.env.CONFIG_REQUIRE_ROSTER === "true";
|
|
801
|
+
}
|
|
797
802
|
function parseModelDomain() {
|
|
798
|
-
const gatewayBaseUrl =
|
|
803
|
+
const gatewayBaseUrl = firstSetEnv("MODEL_GATEWAY_BASEURL");
|
|
799
804
|
const modelId = env("MODEL_ID", "");
|
|
800
805
|
if (modelId === "") {
|
|
801
806
|
throw new Error("MODEL_ID is required (no factory default since server 3.0.0) — set MODEL_ID=<model name your gateway serves>, " +
|
|
@@ -810,7 +815,7 @@ function parseModelDomain() {
|
|
|
810
815
|
}
|
|
811
816
|
}
|
|
812
817
|
for (const [envKey, raw, knob] of [
|
|
813
|
-
["MODEL_GATEWAY_BASEURL", gatewayBaseUrl, "MODEL_API_KEY"],
|
|
818
|
+
["MODEL_GATEWAY_BASEURL", gatewayBaseUrl ?? "", "MODEL_API_KEY"],
|
|
814
819
|
...csv("MODEL_GATEWAY_FALLBACK_URLS").map((u, i) => [`MODEL_GATEWAY_FALLBACK_URLS[${i}]`, u, "MODEL_API_KEY"]),
|
|
815
820
|
["ANTHROPIC_BASE_URL", firstSetEnv("ANTHROPIC_BASE_URL") ?? "", "ANTHROPIC_API_KEY"],
|
|
816
821
|
]) {
|
|
@@ -917,6 +922,15 @@ function parseModelDomain() {
|
|
|
917
922
|
const models = { default: model, [model.id]: model };
|
|
918
923
|
if (cheapModel)
|
|
919
924
|
models[cheapModel.id] = cheapModel;
|
|
925
|
+
const gatewayApiKey = process.env.MODEL_API_KEY;
|
|
926
|
+
if (gatewayBaseUrl === undefined && gatewayApiKey !== undefined && gatewayApiKey !== "") {
|
|
927
|
+
CONFIG_NOTICES.push({
|
|
928
|
+
event: "model_gateway_key_unpaired",
|
|
929
|
+
fields: {
|
|
930
|
+
hint: "MODEL_API_KEY is set but MODEL_GATEWAY_BASEURL is not — MODEL_API_KEY is the gateway's credential and pairs only with the gateway's own root, so with no gateway it is NOT loaded and is sent to no route (a model with its own baseUrl authenticates with its own per-model key). Set MODEL_GATEWAY_BASEURL to use it, or unset MODEL_API_KEY to silence this line.",
|
|
931
|
+
},
|
|
932
|
+
});
|
|
933
|
+
}
|
|
920
934
|
const cheapRef = cheapModel ? cheapModel.id : DEFAULT_MODEL_REF;
|
|
921
935
|
const verifierEnv = process.env.MODEL_VERIFIER;
|
|
922
936
|
const verifierRef = verifierEnv && models[verifierEnv] ? verifierEnv : DEFAULT_MODEL_REF;
|
|
@@ -939,8 +953,7 @@ function parseModelDomain() {
|
|
|
939
953
|
}
|
|
940
954
|
const degradeOn = parseDegradeOn(csv("MODEL_DEGRADE_ON"));
|
|
941
955
|
return {
|
|
942
|
-
gatewayBaseUrl,
|
|
943
|
-
gatewayApiKey: process.env.MODEL_API_KEY,
|
|
956
|
+
...(gatewayBaseUrl !== undefined ? { gatewayBaseUrl, gatewayApiKey } : {}),
|
|
944
957
|
gatewayFallbackUrls: csv("MODEL_GATEWAY_FALLBACK_URLS"),
|
|
945
958
|
...(process.env.GATEWAY_MAX_RETRIES ? { gatewayMaxRetries: numEnvBounded("GATEWAY_MAX_RETRIES", "2", 0, 20) } : {}),
|
|
946
959
|
anthropic: firstSetEnv("ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN")
|
|
@@ -1468,6 +1481,19 @@ function parseMemoryDomain(ctx) {
|
|
|
1468
1481
|
...syncEntryCap("MEMORY_SYNC_MAX_PULL_ENTRIES", "maxPullEntries"),
|
|
1469
1482
|
};
|
|
1470
1483
|
}
|
|
1484
|
+
const replication = memoryReplicationCoverage(memorySync !== undefined ? { memorySync } : {});
|
|
1485
|
+
if (replication.kind === "scopes" && memoryEngineEnabled && !requirePrincipal && memoryScope !== undefined) {
|
|
1486
|
+
CONFIG_NOTICES.push({
|
|
1487
|
+
event: "memory_project_scope_yields_to_sync",
|
|
1488
|
+
fields: {
|
|
1489
|
+
replicatedScopes: replication.scopes.join(","),
|
|
1490
|
+
writeScope: memoryScope,
|
|
1491
|
+
hint: "memory sync (MEMORY_SYNC_*) replicates only the scope(s) listed — the per-project memory layer (S-512, repo:<key>) is not replicated, " +
|
|
1492
|
+
"so sessions do NOT write project-keyed memory here: new memory lands in the global layer (MEMORY_SCOPE), the same as 7.97.0, and every " +
|
|
1493
|
+
"project reads it. Project-layer replication is a separate change; unset MEMORY_SYNC_URL to get per-project memory without sync.",
|
|
1494
|
+
},
|
|
1495
|
+
});
|
|
1496
|
+
}
|
|
1471
1497
|
const projectMemoryEnabled = boolEnvWithLegacyNegated("PROJECT_MEMORY_ENABLED", "PROJECT_MEMORY_DISABLED", true);
|
|
1472
1498
|
const memoryOrgAdmissionMode = enumEnv("MEMORY_ORG_ADMISSION_MODE", "enforce", ["audit", "enforce"]);
|
|
1473
1499
|
const memoryOrgDirectoryJson = process.env.MEMORY_ORG_DIRECTORY_JSON || undefined;
|
|
@@ -2038,7 +2064,7 @@ function parseA2aServeSkills(raw) {
|
|
|
2038
2064
|
const STORE_GROUP_KEYS = [
|
|
2039
2065
|
"sessionBackend", "sessionCacheTtlSec", "storeProbeIntervalMs", "storeLockWaitMs", "dbBackend", "dbBackendExplicit", "localDataRoot",
|
|
2040
2066
|
"runStoreStrictHydrate",
|
|
2041
|
-
"tidb", "pg", "dbQueryTimeoutMs", "snapshotBlobStore", "snapshotBlobSqlMaxBytes", "snapshotBlobAllowSql", "fileHistory", "sendUserFile",
|
|
2067
|
+
"tidb", "pg", "dbQueryTimeoutMs", "snapshotBlobStore", "snapshotBlobSqlMaxBytes", "snapshotBlobAllowSql", "fileHistory", "fileHistoryCapture", "sendUserFile",
|
|
2042
2068
|
];
|
|
2043
2069
|
const MODEL_PLANE_GROUP_KEYS = [
|
|
2044
2070
|
"gatewayBaseUrl", "gatewayApiKey", "gatewayFallbackUrls", "gatewayMaxRetries", "anthropic", "resilience", "model", "envModel", "models",
|
|
@@ -2224,6 +2250,18 @@ export function loadConfig() {
|
|
|
2224
2250
|
`or set CONFIG_PROVIDER=local to take the single-machine \`host\` default, ` +
|
|
2225
2251
|
`or unset REQUIRE_EXECUTION_LANE to keep running without hands on purpose.`);
|
|
2226
2252
|
}
|
|
2253
|
+
{
|
|
2254
|
+
const envLaneDispatchable = !isRosterGateArmed(integrations.configCenter);
|
|
2255
|
+
const refusal = modelRouteEndpointRefusal({
|
|
2256
|
+
gatewayBaseUrl: modelPlane.gatewayBaseUrl,
|
|
2257
|
+
gatewayFallbackUrls: modelPlane.gatewayFallbackUrls,
|
|
2258
|
+
anthropic: modelPlane.anthropic,
|
|
2259
|
+
models: envLaneDispatchable ? modelPlane.models : {},
|
|
2260
|
+
});
|
|
2261
|
+
if (refusal !== undefined) {
|
|
2262
|
+
throw new Error(refusal);
|
|
2263
|
+
}
|
|
2264
|
+
}
|
|
2227
2265
|
EXEC_LANE_NOTICE = buildExecLaneNotice(orchestration.remoteExec);
|
|
2228
2266
|
return attachConfigGroups({
|
|
2229
2267
|
...store,
|
package/dist/env-facts.js
CHANGED
|
@@ -158,11 +158,9 @@ export async function sweepStaleScratchpads(localDataRoot, opts) {
|
|
|
158
158
|
return removed;
|
|
159
159
|
}
|
|
160
160
|
export function egressForRemoteExec(remoteExec) {
|
|
161
|
-
if (
|
|
162
|
-
return undefined;
|
|
163
|
-
if (remoteExec.provider === "e2b")
|
|
161
|
+
if (remoteExec?.provider === "e2b")
|
|
164
162
|
return remoteExec.allowInternetAccess === false ? "none" : "full";
|
|
165
|
-
if (remoteExec
|
|
163
|
+
if (remoteExec?.provider === "k8s")
|
|
166
164
|
return "full";
|
|
167
165
|
return undefined;
|
|
168
166
|
}
|
|
@@ -45,12 +45,18 @@ export interface ExecutionLaneCaps {
|
|
|
45
45
|
/** 记忆持久面的**自动**表态(部署可用 `MEMORY_PERSISTENCE_CAPABLE` 显式覆盖,那是旋钮不是车道位)。
|
|
46
46
|
* 判别式:`boot/memory-boundary.ts` 的 `effectiveMemoryPersistenceCapable`。 */
|
|
47
47
|
readonly memoryPersistenceCapable: LaneCapability;
|
|
48
|
-
/** 文件平面是否就在 worker 本机(= `hostSemanticsLane`)
|
|
49
|
-
*
|
|
50
|
-
*
|
|
48
|
+
/** 文件平面是否就在 worker 本机(= `hostSemanticsLane`)。🔴 S-602 起**消费点读本位**,不再各写判别式
|
|
49
|
+
* (改前 `resolve-spec` / `stores` / `memory-scope` / `deferred-sandbox-path-env` 各写一份
|
|
50
|
+
* `lane === undefined || lane === "host"` 的同义形,分叉过就是记忆边界判错盘)。读本位的真装配点:
|
|
51
|
+
* `boot/deferred-sandbox-path-env.ts` 的 `isSandboxPathAdjudicationLane`(resolve-spec 写门 / parked 赎回腿 /
|
|
52
|
+
* 执行环境装配三处的唯一属主)、`memory-scope.ts` 的 `memoryEngineRemoteLanePosture`、
|
|
53
|
+
* `boot/memory-boundary.ts` 的 `buildMemoryBoundaryLaneFacts`(stores)、`capabilities/memory-notice.ts` 的
|
|
54
|
+
* `buildNoticeLaneFacts`(resolve-spec)。⚠️ `in-process` 在本位上是 YES(hooks/进程内读确实在本机盘上),
|
|
55
|
+
* 但它**没有手** —— 问「手在不在本机盘上」要读 {@link handsOnWorkerFilePlane},不是只读本位。 */
|
|
51
56
|
readonly hostFilePlane: LaneCapability;
|
|
52
|
-
/** core 的 `isRemoteExecutionEnv` 对本车道的判决(
|
|
53
|
-
*
|
|
57
|
+
/** core 的 `isRemoteExecutionEnv` 对本车道的判决(鸭子类型,**含 host**)= core 拿没拿到执行环境 = 手带挂没挂。
|
|
58
|
+
* 5.26.0 起它决定 `# Memory` 写指令撤不撤。🔴 S-602 起消费点读本位(改前各写 `config.remoteExec !== undefined`);
|
|
59
|
+
* 对账测试拿 core 自己的 `isRemoteExecutionEnv` 问**真 adapter 实例**来核本位的值。 */
|
|
54
60
|
readonly coreRemoteExecutionEnv: LaneCapability;
|
|
55
61
|
/**
|
|
56
62
|
* S-354 / core 7.20.0([ref]):这条车道的适配器**声明**了 `capabilities.canonicalPathAuthoritative`
|
|
@@ -103,13 +109,37 @@ export declare const ALL_EXECUTION_LANES: readonly ExecutionLane[];
|
|
|
103
109
|
*/
|
|
104
110
|
export declare const REMOTE_EXEC_PROVIDER_WORDS: readonly RemoteExecProvider[];
|
|
105
111
|
/**
|
|
106
|
-
* `config.remoteExec?.provider` →
|
|
112
|
+
* `config.remoteExec?.provider` → 车道词。**全仓唯一**的缺席归一读口(S-602 门 D:`?? "in-process"` 只许住在这里,
|
|
113
|
+
* `?? "host"` 与 `lane === undefined || lane === "host"` 同义折零容忍)。
|
|
107
114
|
*
|
|
108
115
|
* · `undefined`(REMOTE_EXEC 未设)⇒ `"in-process"`,那是真车道不是缺席;
|
|
109
116
|
* · 认不出的词 ⇒ `undefined`,**调用方必须 fail-closed**(禁静默按某个默认车道处理)。生产链上这一臂
|
|
110
117
|
* 不可达:`loadConfig` 的 [ref] 拒启门先咬未知词;它存在是为了消费点自卫时有个响亮出口。
|
|
118
|
+
*
|
|
119
|
+
* 两个重载 = 类型收窄([ref] §2.2 A):喂**已装配的** config 形(`RemoteExecProvider | undefined`)⇒
|
|
120
|
+
* 返回型里没有 `undefined`,调用点不必为一条不可达的臂写兜底(兜底写多了就是下一处折词);喂开放字符串
|
|
121
|
+
* (测试、外部输入)⇒ 返回含 `undefined`,逼调用点表态。第一个重载的成立依据是**编译期**的:
|
|
122
|
+
* `RemoteExecProvider ⊂ ExecutionLane`(`ExecutionLane` 由它派生),而 {@link EXECUTION_LANE_WORDS} 是
|
|
123
|
+
* `Record<ExecutionLane, true>` —— 少一个词编译红,所以对 `RemoteExecProvider` 的每一个值 `isExecutionLane` 恒真。
|
|
111
124
|
*/
|
|
125
|
+
export declare function executionLaneOf(provider: RemoteExecProvider | undefined): ExecutionLane;
|
|
112
126
|
export declare function executionLaneOf(provider: string | undefined): ExecutionLane | undefined;
|
|
127
|
+
/**
|
|
128
|
+
* S-602 —— 「这条车道的**手**就在本 worker 的盘上吗」。**两位合取,都读表**(不手抄):
|
|
129
|
+
* · `coreRemoteExecutionEnv` —— core 拿到了执行环境 = 手带真挂上(`in-process` 答 NO:core 落 `StubExecutionEnv`);
|
|
130
|
+
* · `hostFilePlane` —— 那套手的文件平面就是本 worker 的盘(沙箱 / 远端 / 设备车道答 NO)。
|
|
131
|
+
* 今天只有 `host` 两位同 YES。
|
|
132
|
+
*
|
|
133
|
+
* 🔴 **只读 `hostFilePlane` 是错的**:`in-process` 那一位是 YES(hooks / 进程内读确实在本机盘上),而它
|
|
134
|
+
* **没有手**。clay 裁 [ref] ③ 的字面写「判据读 `hostFilePlane`」,意图是同一句的「无手不挂」—— 照字面
|
|
135
|
+
* 实现会让无手部署**继续**挂 SendUserFile,与意图相反;本函数按意图实现(收车档 che-MA 写明)。
|
|
136
|
+
*
|
|
137
|
+
* 三个消费点问的都是这一句,**同一只函数**,不许各写一份 `=== "host"`:
|
|
138
|
+
* ① `task-cwd.ts` 的 `toolsRunOnThisHost` —— wire `executionLane.toolsOnThisHost` + 出厂技能 `baseDir`;
|
|
139
|
+
* ② `boot/coordinators.ts` 的 SendUserFile 本机腿(经 ①;[ref] ③「无手不挂」);
|
|
140
|
+
* ③ `approval.ts` 的 `buildNoResponderBootWarns` —— 写门的 ask 只在「有手写本机盘」时才会发生。
|
|
141
|
+
*/
|
|
142
|
+
export declare function handsOnWorkerFilePlane(lane: ExecutionLane): boolean;
|
|
113
143
|
/** 把一位能力在**具体部署**上落成布尔:租户面是唯一入参(旋钮由消费点自己再合取)。
|
|
114
144
|
* 穷举 switch,无 default 臂 —— {@link LaneCapability} 加词而此处不加臂 = 编译红。 */
|
|
115
145
|
export declare function laneCapabilityHolds(cap: LaneCapability, deployment: {
|
|
@@ -162,6 +162,10 @@ export function executionLaneOf(provider) {
|
|
|
162
162
|
return "in-process";
|
|
163
163
|
return isExecutionLane(provider) ? provider : undefined;
|
|
164
164
|
}
|
|
165
|
+
export function handsOnWorkerFilePlane(lane) {
|
|
166
|
+
const caps = executionLaneCaps(lane);
|
|
167
|
+
return caps.hostFilePlane === "supported" && caps.coreRemoteExecutionEnv === "supported";
|
|
168
|
+
}
|
|
165
169
|
export function laneCapabilityHolds(cap, deployment) {
|
|
166
170
|
switch (cap) {
|
|
167
171
|
case "supported":
|