@sema-agent/server 7.101.1 → 7.102.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 +23 -0
- package/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/USAGE.md +58 -4
- package/dist/approval-card.d.ts +35 -2
- package/dist/approval-card.js +19 -1
- package/dist/background-session-reap.d.ts +55 -0
- package/dist/background-session-reap.js +90 -0
- package/dist/boot/config-center.d.ts +16 -4
- package/dist/boot/config-center.js +44 -15
- 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-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 +7 -3
- package/dist/capabilities/builtin-tools.d.ts +14 -1
- package/dist/capabilities/builtin-tools.js +3 -0
- 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 +9 -4
- package/dist/config-center/apply-effective.js +5 -12
- 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/read-warnings.d.ts +19 -48
- package/dist/config-center/read-warnings.js +7 -49
- package/dist/config-center/restart-signal.d.ts +8 -11
- package/dist/config-center/types.d.ts +4 -2
- package/dist/config-provider.js +1 -48
- package/dist/config-types.d.ts +77 -5
- package/dist/config-types.js +5 -0
- package/dist/config.js +49 -13
- package/dist/device-store.d.ts +1 -1
- package/dist/fleet-client.d.ts +21 -7
- package/dist/fleet-client.js +41 -12
- package/dist/fleet-lease.d.ts +1 -1
- package/dist/http/approval-sweeps.js +5 -20
- package/dist/http/dispatch.js +20 -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 +21 -8
- package/dist/http/server.js +2 -2
- package/dist/model-entry-refusal.d.ts +11 -10
- package/dist/model-entry-refusal.js +13 -10
- package/dist/observability/fail-open.d.ts +2 -2
- package/dist/observability/fail-open.js +2 -2
- 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 +11 -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 +18 -5
- package/dist/tool-approval.d.ts +30 -1
- package/dist/tool-approval.js +31 -8
- package/dist/trace/core-keyset-guard.d.ts +3 -3
- package/dist/trace/engine-notice-wire.d.ts +1 -1
- package/dist/trace/engine-notice-wire.js +2 -0
- package/package.json +3 -3
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { RestartSignal } from "@sema-agent/settings-schema/fleet";
|
|
1
2
|
import type { EffectiveConfig } from "./types.js";
|
|
2
3
|
export type RestartSlice = "skills" | "mcp" | "a2a" | "scenarios" | "degrade-route" | "read-face" | "memory-consolidation";
|
|
3
4
|
/** What the CALLER must contribute about boot-baked catalog consumers that live OUTSIDE the Runner — the
|
|
@@ -31,18 +32,14 @@ export interface RestartSliceCtx {
|
|
|
31
32
|
* = 装载器的出厂名单 `DEFAULT_PLUGIN_ALLOW_HOSTS`。 */
|
|
32
33
|
pluginsAllowHosts?: readonly string[];
|
|
33
34
|
}
|
|
34
|
-
/**
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
35
|
+
/** S-741(车OG):本引擎发布的重启信号。形的唯一定义是 settings-schema 6.1.0 `RestartSignal`(`{ reasons, version, since }`,在场即信号,
|
|
36
|
+
* 不夹 `restartRequired` 布尔;本地手写的那份接口已删),这里只把 `reasons` 收窄到本文件的闭集 {@link RestartSlice} —— 写口
|
|
37
|
+
* (`boot/config-center.ts` `publishRestart`)按它收,拼错 / 已退役的词编译红(opus 对抗腿 #2:直接用上游型时 `reasons` 是
|
|
38
|
+
* `string[]`,闭集的编译期判据丢了)。`/health.restart` 与 fleet 心跳 `restart` 两个出口交 config-center 同一只 `restartState()`
|
|
39
|
+
* 的同一个对象,按契约形出门。 */
|
|
40
|
+
export type EngineRestartSignal = RestartSignal & {
|
|
40
41
|
reasons: RestartSlice[];
|
|
41
|
-
|
|
42
|
-
version: number;
|
|
43
|
-
/** Epoch ms when this restart first became required (stable across refreshes while `reasons` is unchanged). */
|
|
44
|
-
since: number;
|
|
45
|
-
}
|
|
42
|
+
};
|
|
46
43
|
/** Canonical, key-sorted JSON (array order preserved) so two semantically-equal effective slices fingerprint
|
|
47
44
|
* identically regardless of object key order from the center serializer.
|
|
48
45
|
*
|
|
@@ -265,9 +265,11 @@ export interface EffectiveConfig {
|
|
|
265
265
|
tierGroups?: unknown;
|
|
266
266
|
activeTierGroup?: string;
|
|
267
267
|
/** 🔴 S-712 —— **本仓读腿铸的**载体(不是 wire 键:中心在模型域里写同名键会被 schema 当未知键剥掉并进候选门):这次读
|
|
268
|
-
* 被 schema
|
|
268
|
+
* 被 schema 逐条丢掉的**启用**、**有名**目录条目的身份(取 settings-schema 6.1.0 告警自带的 `identity`,见
|
|
269
|
+
* `read-warnings.ts` `splitFoldedReadWarnings`)。模型面 stage 把它们铸成 `config.model_entry_invalid` 墓碑进 `modelRefusals`,
|
|
269
270
|
* 不变量 `refused-names-not-minted` 据此判增广有没有把它们的名字铸到别的条目上。挂在 effective 上(而不是读结果旁边)是为了
|
|
270
|
-
* **随 effective 落 LKG**:重启从 LKG 起服时墓碑还在。形见 {@link DroppedModelIdentity}(纯身份,不含值)
|
|
271
|
+
* **随 effective 落 LKG**:重启从 LKG 起服时墓碑还在。形见 {@link DroppedModelIdentity}(纯身份,不含值);盘上形不对
|
|
272
|
+
* (不是列表 / 行缺字符串 `name`)⇒ 模型面整组拒。 */
|
|
271
273
|
droppedIdentities?: DroppedModelIdentity[];
|
|
272
274
|
};
|
|
273
275
|
/** registry-core 0.7 collab domain (teams' successor) — consumed by the slice-1.5 projection
|
package/dist/config-provider.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { readMcpSourceLabel } from "./mcp-source-label.js";
|
|
2
2
|
import { createHmac, randomBytes } from "node:crypto";
|
|
3
3
|
import { splitFoldedReadWarnings, withDroppedIdentities } from "./config-center/read-warnings.js";
|
|
4
|
-
import { readFile, stat } from "node:fs/promises";
|
|
5
4
|
import { FileConfigStore } from "@sema-agent/settings-schema/node";
|
|
6
5
|
import { findSkillContent, skillContentHash, refIntegrityIssues, createSiblingResolver, formatConfigPath, REF_CHECKED_DOMAINS, } from "@sema-agent/settings-schema";
|
|
7
6
|
import { redactSecrets } from "./trace/redact.js";
|
|
@@ -12,24 +11,6 @@ const OPERAND_FINGERPRINT_KEY = randomBytes(16);
|
|
|
12
11
|
function redactConfigError(text) {
|
|
13
12
|
return redactSecrets(text).replace(/"[^"]*"|'[^']*'/g, (m) => `"#${createHmac("sha256", OPERAND_FINGERPRINT_KEY).update(m.slice(1, -1)).digest("hex").slice(0, 8)}"`);
|
|
14
13
|
}
|
|
15
|
-
async function readRawDomainDoc(path) {
|
|
16
|
-
try {
|
|
17
|
-
const doc = JSON.parse(await readFile(path, "utf8"));
|
|
18
|
-
return { ok: true, doc };
|
|
19
|
-
}
|
|
20
|
-
catch (e) {
|
|
21
|
-
return { ok: false, why: e instanceof Error ? e.message : String(e) };
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
async function statSignature(path) {
|
|
25
|
-
try {
|
|
26
|
-
const st = await stat(path, { bigint: true });
|
|
27
|
-
return { ok: true, sig: `${st.ino}:${st.size}:${st.mtimeNs}:${st.ctimeNs}` };
|
|
28
|
-
}
|
|
29
|
-
catch (e) {
|
|
30
|
-
return { ok: false, why: e instanceof Error ? e.message : String(e) };
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
14
|
export const BOOT_FETCH_DEFERRED = Symbol("boot-fetch-deferred");
|
|
34
15
|
export function raceBootFetch(fetch, budgetMs) {
|
|
35
16
|
if (budgetMs <= 0)
|
|
@@ -101,9 +82,7 @@ export class LocalConfigProvider {
|
|
|
101
82
|
if (issues.length > 0)
|
|
102
83
|
refErrors.push({ domain: d, error: issues.map((i) => `${formatConfigPath(i.path)}: ${redactConfigError(i.message)}`).join("; ") });
|
|
103
84
|
}
|
|
104
|
-
const
|
|
105
|
-
const rawModels = modelDropped ? await this.#provenRawModelsDoc(store, eff.version) : undefined;
|
|
106
|
-
const folded = splitFoldedReadWarnings(readWarnings, { raw: rawModels, parsed: eff.models.models ?? [] });
|
|
85
|
+
const folded = splitFoldedReadWarnings(readWarnings);
|
|
107
86
|
const parseErrors = folded.domainErrors.map((w) => ({ domain: w.domain, error: redactConfigError(w.error) }));
|
|
108
87
|
const droppedEntries = folded.droppedEntries.map((d) => ({ ...d, error: redactConfigError(d.error) }));
|
|
109
88
|
const errors = [...domainErrors.map((de) => ({ domain: de.domain, error: redactConfigError(de.error) })), ...parseErrors, ...refErrors];
|
|
@@ -115,32 +94,6 @@ export class LocalConfigProvider {
|
|
|
115
94
|
...(droppedEntries.length > 0 ? { droppedEntries } : {}),
|
|
116
95
|
};
|
|
117
96
|
}
|
|
118
|
-
async #provenRawModelsDoc(store, version) {
|
|
119
|
-
const path = store.domainPath("models");
|
|
120
|
-
const unproven = (why) => {
|
|
121
|
-
logger.warn("config_models_raw_unproven", { dir: this.root, why: redactConfigError(why), note: "the dropped model entries of this read cannot be identified from the exact snapshot that was parsed — the candidate's model plane is refused as a group (no tombstone is taken from another version of the file); the next poll re-reads" });
|
|
122
|
-
return undefined;
|
|
123
|
-
};
|
|
124
|
-
const s1 = await statSignature(path);
|
|
125
|
-
const raw = await readRawDomainDoc(path);
|
|
126
|
-
let again;
|
|
127
|
-
try {
|
|
128
|
-
again = (await (this.#injectedStore ?? new FileConfigStore(this.root)).getEffective({ tolerant: true })).effective.version;
|
|
129
|
-
}
|
|
130
|
-
catch (e) {
|
|
131
|
-
again = `re-parse failed: ${e instanceof Error ? e.message : String(e)}`;
|
|
132
|
-
}
|
|
133
|
-
const s2 = await statSignature(path);
|
|
134
|
-
if (!raw.ok)
|
|
135
|
-
return unproven(`config.d/models.json unreadable on the identity re-read: ${raw.why}`);
|
|
136
|
-
if (typeof again !== "number")
|
|
137
|
-
return unproven(again);
|
|
138
|
-
if (!s1.ok || !s2.ok || s1.sig !== s2.sig)
|
|
139
|
-
return unproven("config.d/models.json changed (or could not be stat-ed) while its identity re-read was being proven");
|
|
140
|
-
if (again !== version)
|
|
141
|
-
return unproven("config.d changed between the parse and the identity re-read (content version differs)");
|
|
142
|
-
return raw.doc;
|
|
143
|
-
}
|
|
144
97
|
async fetchSkillContent(contentHash) {
|
|
145
98
|
const skills = await this.store.getDomain("skills");
|
|
146
99
|
if (!skills)
|
package/dist/config-types.d.ts
CHANGED
|
@@ -62,6 +62,56 @@ export interface RetentionSweepConfig {
|
|
|
62
62
|
* 那道拒之后它即刻生效 —— 留着是为了「那天不必有人再猜一个数」。
|
|
63
63
|
*/
|
|
64
64
|
export declare const MEMORY_CONSOLIDATION_MIN_INTERVAL_SEC = 300;
|
|
65
|
+
/**
|
|
66
|
+
* Node 的 `setTimeout` delay 上界(2^31−1 ms ≈ 24.8 天)。**超过它的 delay 被静默折成 1ms** —— 任何「一个时长
|
|
67
|
+
* 旋钮的值最终会变成一次 `setTimeout` 的 delay」的键都必须在上界处拒启,不能夹取([ref] + codex r1-[high]:夹取
|
|
68
|
+
* 只是把一句谎换成另一句)。S-757 起从 `config.ts` 的模块私有常量提到本纯叶:解析器与配置目录的合法域句读
|
|
69
|
+
* 同一个数([ref]:边界不手抄)。
|
|
70
|
+
*/
|
|
71
|
+
export declare const MAX_TIMER_DELAY_MS = 2147483647;
|
|
72
|
+
/**
|
|
73
|
+
* S-757(test [ref] / core #1125)—— `AUTO_MODE_CLASSIFIER_TIMEOUT_MS`(→ core `RunnerDeps.autoMode.timeoutMs`)
|
|
74
|
+
* 的**合法域**,闭区间、整数 ms。解析器(`config.ts`)与目录行(`config-catalog.ts`)共读这一处。
|
|
75
|
+
*
|
|
76
|
+
* · **下界 1000**:亚秒值是「秒写成毫秒」的手滑形(`180` 想说三分钟,落地 180 ms ⇒ 每一级立即判「没裁」⇒ 门控
|
|
77
|
+
* 调用整条拒)。`0` 同拒:core 对这一座**没有**「关」的语义(d.ts 未写 0 的含义;分类器不判就走下一级 / 拒),
|
|
78
|
+
* 写 0 表达不出任何可执行的意图。
|
|
79
|
+
* · **上界 = {@link MAX_TIMER_DELAY_MS}**:core 拿这个数直接武装 `setTimeout`(7.32.1 `dist/core/auto-mode.js`
|
|
80
|
+
* `withDeadline`,座上不做值校验),更大的值被 Node 折成 1 ms —— 「抬上限」变成「立即超时」,方向正好反了。
|
|
81
|
+
* **不另设更紧的政策上界**:这是**一级**梯子的等待上限(不是整次分类;`classifierFallback=main` 时最坏 =
|
|
82
|
+
* 两级 × 上限 + 一次装配),而本级调用同时收到 run 的中止信号(`withDeadline` 把外层 signal 接进本级),
|
|
83
|
+
* run 被取消 / 硬 abort 时不靠这个数收尾。
|
|
84
|
+
*/
|
|
85
|
+
export declare const AUTO_MODE_CLASSIFIER_TIMEOUT_BOUNDS: {
|
|
86
|
+
readonly minMs: 1000;
|
|
87
|
+
readonly maxMs: 2147483647;
|
|
88
|
+
};
|
|
89
|
+
/**
|
|
90
|
+
* `DRAIN_GRACE_MS` 的**地板**(10s)—— 夹取(env,[ref] B 档)与中心 stage 校验(`stage-limits.ts` 的 `atLeast`)两条写入腿
|
|
91
|
+
* 都以它为下界,于是「排空窗」在任何写入通道上都不会短于它。S-752 起从 `config.ts` 的 `LIMITS_HTTP_FLOORS` 字面提到本
|
|
92
|
+
* 纯叶:{@link DRAIN_SECOND_SIGNAL_MIN_GAP_BOUNDS} 的上界与配置目录的合法域句共读这一个数([ref]:边界不手抄;
|
|
93
|
+
* 目录模块不能值引 `config.ts` —— `config → server-secret-env → config-catalog` 已成链)。`LIMITS_HTTP_FLOORS` 仍是
|
|
94
|
+
* 那张表,只是这一格改为引用本常量,零语义变化。
|
|
95
|
+
*/
|
|
96
|
+
export declare const DRAIN_GRACE_FLOOR_MS = 10000;
|
|
97
|
+
/**
|
|
98
|
+
* S-752(test [ref])—— `DRAIN_SECOND_SIGNAL_MIN_GAP_MS` 的**合法域**(闭区间、整数 ms)与缺省。解析器(`config.ts`)
|
|
99
|
+
* 与目录行(`config-catalog.ts`)共读这一处。
|
|
100
|
+
*
|
|
101
|
+
* · **语义**:排空期里一发停止信号(SIGTERM / SIGINT)只有在与**排空起点**相隔 ≥ 本值时才算硬停意图;更近的是同一拍的
|
|
102
|
+
* 重复投递(壳心跳误判 / 编排器重试),记一行就忽略。
|
|
103
|
+
* · **缺省 250**:人手连按两次的最短间隔量级 —— 同一毫秒的重复投递(事故形)远在其内,有意的第二发远在其外。
|
|
104
|
+
* · **下界 0 = 关**:恒按「第二发 = 硬停」,即 7.101.x 行为逐字。0 在这里有真含义(不同于 `AUTO_MODE_CLASSIFIER_TIMEOUT_MS`
|
|
105
|
+
* 的 0),所以放行。
|
|
106
|
+
* · **上界 = {@link DRAIN_GRACE_FLOOR_MS}**:本闸不能比**最短**可能的排空窗还宽 —— 更宽的值会让「排空期的第二发」在整个窗
|
|
107
|
+
* 里都不算数(信号面的硬梯子被静默摘掉,只剩 SIGKILL)。取地板而不是「≤ 本进程的 `DRAIN_GRACE_MS`」:后者是中心热改键,
|
|
108
|
+
* boot 期跨键校验会被一次热改在运行期推翻;地板则在所有写入腿上都成立,无需第二处校验。
|
|
109
|
+
*/
|
|
110
|
+
export declare const DRAIN_SECOND_SIGNAL_MIN_GAP_BOUNDS: {
|
|
111
|
+
readonly minMs: 0;
|
|
112
|
+
readonly maxMs: 10000;
|
|
113
|
+
};
|
|
114
|
+
export declare const DRAIN_SECOND_SIGNAL_MIN_GAP_DEFAULT_MS = 250;
|
|
65
115
|
/**
|
|
66
116
|
* S-220 —— **`resilience` 块缺省的唯一属主**(S-202 的单源从一根扩到整族)。
|
|
67
117
|
*
|
|
@@ -1556,6 +1606,15 @@ export interface ServiceConfigFlat {
|
|
|
1556
1606
|
* 真源。所以缺席时这台机器跟随 core 的 `main`,运维面由 DEPLOY-PREREQS 的升级段点名(「默认变了」
|
|
1557
1607
|
* 是升级须知,不是一个可以静默过去的实现细节)。 */
|
|
1558
1608
|
autoModeClassifierFallback?: "main" | "none";
|
|
1609
|
+
/** 7.102.0 S-757(test [ref] / core #1125)—— **分类器单级判决上限**(`AUTO_MODE_CLASSIFIER_TIMEOUT_MS`,
|
|
1610
|
+
* 部署级数值旋钮,整数 ms,合法域 {@link AUTO_MODE_CLASSIFIER_TIMEOUT_BOUNDS})→ core `RunnerDeps.autoMode.timeoutMs`。
|
|
1611
|
+
*
|
|
1612
|
+
* 为什么要一根旋钮:这一级到点没裁 = 梯子往下走,走完仍没裁 ⇒ 这次门控调用**整条拒**、不出卡。自建慢网关
|
|
1613
|
+
* (排队 + prefill 以分钟计)在 core 缺省上限内答不出 ⇒ auto 模式在这类部署上等于「工具一律拒」。
|
|
1614
|
+
*
|
|
1615
|
+
* 🔴 **缺席 = 不铸键**(与 {@link autoModeClassifierFallback} 同律):本仓不复制上游缺省数字,core 改缺省之日
|
|
1616
|
+
* 本仓不会成为第二份真源。旋钮**无条件**:不挂任何客户端 / 请求面派生腿。坏值(非十进制整数 / 越界)boot 拒启。 */
|
|
1617
|
+
autoModeClassifierTimeoutMs?: number;
|
|
1559
1618
|
/** S-111([ref] §4.4)—— CC `settings.crossSessionInbound` 的**部署/组织层**(引擎层形的 `managed` 层,
|
|
1560
1619
|
* CC `policySettings` 对位):本部署对入站跨会话消息的治理三态。`accept` 投递 / `hold` 停在收件会话的待审
|
|
1561
1620
|
* 队列 / `refuse` 整体退出车道。**未设 = 本层不设**(不是 accept):引擎读作「managed 层未表态」,由其余层
|
|
@@ -1607,8 +1666,14 @@ export interface ServiceConfigFlat {
|
|
|
1607
1666
|
* instance) to finish before the hard shutdown. During drain, new billable submissions get 503+Retry-After and
|
|
1608
1667
|
* /health carries `draining:true`. Default 10min (an interactive turn is minutes, not seconds — a
|
|
1609
1668
|
* 10s hard-cut would kill long turns); floor 10s. Env: DRAIN_GRACE_MS. SIGINT (Ctrl-C dev
|
|
1610
|
-
* ergonomics)
|
|
1669
|
+
* ergonomics) hard-stops immediately; DURING a drain, a second SIGTERM / SIGINT hard-stops only when it arrives
|
|
1670
|
+
* at least {@link drainSecondSignalMinGapMs} after the drain began (S-752). */
|
|
1611
1671
|
drainGraceMs: number;
|
|
1672
|
+
/** S-752(test [ref]):排空期「第二发信号 = 硬停」的最小间隔(ms,锚 = 排空起点)。不足 ⇒ 同一拍的重复投递,一行
|
|
1673
|
+
* info `drain_duplicate_signal_ignored` 后忽略、排空继续;`0` = 关(7.101.x 行为)。合法域
|
|
1674
|
+
* {@link DRAIN_SECOND_SIGNAL_MIN_GAP_BOUNDS},缺省 {@link DRAIN_SECOND_SIGNAL_MIN_GAP_DEFAULT_MS};坏值拒启([ref] A 档)。
|
|
1675
|
+
* 部署级旋钮,**无条件**解析。 Env: `DRAIN_SECOND_SIGNAL_MIN_GAP_MS`。 */
|
|
1676
|
+
drainSecondSignalMinGapMs: number;
|
|
1612
1677
|
/** B3 (shell [ref]②): SIGHUP idle window. SIGHUP means "the process-group owner's terminal went away" — but
|
|
1613
1678
|
* under the shared-engine shape (several shell sessions on one config, engine rides the FIRST shell's process
|
|
1614
1679
|
* group per the T11 anti-orphan call) other live sessions may still be using this engine, so an unconditional
|
|
@@ -1955,13 +2020,20 @@ export interface ServiceConfigFlat {
|
|
|
1955
2020
|
* that worker's roster via `/effective?worker=<name>`. Unset → the global roster (backward compatible).
|
|
1956
2021
|
* `credential`(S-651,BREAKING:取代 7.98.0 及以前的 `token: string` 启动快照):凭证来源活对象
|
|
1957
2022
|
* (`center-credential.ts`),每条出站请求经它取一次值 —— `SEMA_REGISTRY_TOKEN`(值)或
|
|
1958
|
-
* `SEMA_REGISTRY_TOKEN_FILE`(每次读文件,轮换 / 换账号不重启)
|
|
2023
|
+
* `SEMA_REGISTRY_TOKEN_FILE`(每次读文件,轮换 / 换账号不重启)二选一(两键名与判官的属主 = settings-schema
|
|
2024
|
+
* `CENTER_CREDENTIAL_ENV` / `judgeCenterCredentialDeclaration`,S-742)。 */
|
|
1959
2025
|
configCenter?: {
|
|
1960
2026
|
baseUrl: string;
|
|
1961
2027
|
credential: CenterCredentialSource;
|
|
1962
2028
|
dryRun: boolean;
|
|
1963
2029
|
worker?: string;
|
|
1964
2030
|
};
|
|
2031
|
+
/** S-698(design-325 §2):中心凭证**续签提前量**(ms)—— `SEMA_REGISTRY_TOKEN_FILE` 指向人的登录会话信封
|
|
2032
|
+
* (`kind:"user-session"`)时,访问令牌离到期 ≤ 本值即由引擎续签并原子改写同一文件(`center-credential-renewer.ts`)。
|
|
2033
|
+
* 缺省 5 min;`0` = 关续签、只读信封(壳负责续);其余必须是整数毫秒且落在 `CENTER_CREDENTIAL_RENEW_LEAD_MIN_MS`(一次续签请求的
|
|
2034
|
+
* 传输上界)与 `CENTER_CREDENTIAL_RENEW_LEAD_MAX_MS`(访问令牌寿命之半)之间,坏值拒启(解析口 `parseCenterCredentialRenewLeadMs`)。
|
|
2035
|
+
* 裸行文件 / `SEMA_REGISTRY_TOKEN` 值形不受它影响(不续签)。`CENTER_CREDENTIAL_RENEW_LEAD_MS`。 */
|
|
2036
|
+
centerCredentialRenewLeadMs: number;
|
|
1965
2037
|
/** Dual-mode config SOURCE (DUAL-MODE-DESIGN §4): "remote" (sema-registry HTTP) | "local" (.env + config.d
|
|
1966
2038
|
* via @sema-agent/settings-schema FileConfigStore) | unset → remote if SEMA_REGISTRY_URL (legacy CONFIG_CENTER_URL now boot-rejects, no dual-read) is set, else pure-env.
|
|
1967
2039
|
* Env: CONFIG_PROVIDER. The same resolver/EffectiveConfig either way (can't drift). */
|
|
@@ -1975,7 +2047,7 @@ export type ServiceStoreConfig = Pick<ServiceConfigFlat, "sessionBackend" | "ses
|
|
|
1975
2047
|
export type ServiceModelPlaneConfig = Pick<ServiceConfigFlat, "gatewayBaseUrl" | "gatewayApiKey" | "gatewayFallbackUrls" | "gatewayMaxRetries" | "anthropic" | "resilience" | "model" | "envModel" | "models" | "modelApiKeyEnv" | "modelApiKeys" | "modelRefusals" | "modelQuotaWeights" | "tiers" | "projects" | "roles" | "atModelAllowlist" | "cascadeLadder" | "degrade">;
|
|
1976
2048
|
/** 组:approval(审批 / HITL 门)。`directDoorActive` 无 env 解析腿(装配层三域合取的产物),但语义上
|
|
1977
2049
|
* 就是本组的门状态,故进组;`parseApprovalDomain` 的返回类型相应是 `Omit<…, "directDoorActive">`。 */
|
|
1978
|
-
export type ServiceApprovalConfig = Pick<ServiceConfigFlat, "approvalRequire" | "approvalDeny" | "approvalTimeoutSec" | "approvalAutoBudget" | "approvalNeverAuto" | "approvalHmacKeys" | "durableApproval" | "durableApprovalSource" | "streamAskWindowMsSource" | "directApprovalDoor" | "directDoorActive" | "resourceSuspend" | "resourceSuspendTtlSec" | "resourceSuspendTotalTokens" | "resourceSuspendTotalBudgetUsd" | "resourceSuspendMaxSlices" | "askQuestionEnabled" | "questionThrottle" | "toolApprovalEnabled" | "permissionRulesEnabled" | "permissionRulesEnabledExplicit" | "streamAskWindowMarginMs" | "streamApproval" | "unattendedApprovalPolicy" | "mcpElicitation" | "sensitiveWritePatterns" | "writeProtectedPaths" | "manualModeShellGate" | "permissionsDisableAutoMode" | "autoModeClassifierFallback" | "crossSessionInbound" | "crossSessionDialogExpiry">;
|
|
2050
|
+
export type ServiceApprovalConfig = Pick<ServiceConfigFlat, "approvalRequire" | "approvalDeny" | "approvalTimeoutSec" | "approvalAutoBudget" | "approvalNeverAuto" | "approvalHmacKeys" | "durableApproval" | "durableApprovalSource" | "streamAskWindowMsSource" | "directApprovalDoor" | "directDoorActive" | "resourceSuspend" | "resourceSuspendTtlSec" | "resourceSuspendTotalTokens" | "resourceSuspendTotalBudgetUsd" | "resourceSuspendMaxSlices" | "askQuestionEnabled" | "questionThrottle" | "toolApprovalEnabled" | "permissionRulesEnabled" | "permissionRulesEnabledExplicit" | "streamAskWindowMarginMs" | "streamApproval" | "unattendedApprovalPolicy" | "mcpElicitation" | "sensitiveWritePatterns" | "writeProtectedPaths" | "manualModeShellGate" | "permissionsDisableAutoMode" | "autoModeClassifierFallback" | "autoModeClassifierTimeoutMs" | "crossSessionInbound" | "crossSessionDialogExpiry">;
|
|
1979
2051
|
/** 组:memory(记忆面 + TOC 同步腿)。 */
|
|
1980
2052
|
export type ServiceMemoryConfig = Pick<ServiceConfigFlat, "memoryEngineEnabled" | "memoryEngineDir" | "memoryEngineRemoteLaneAllowed" | "memoryEngineBackend" | "memoryScope" | "memoryPersistenceCapable" | "memorySync" | "memoryEmbedder" | "memoryOrgAdmissionMode" | "memoryDelegationEvidence" | "memoryProvenance" | "memoryCapturePolicy" | "memoryConsolidationDriver" | "memoryOrgDirectoryJson" | "memoryOrgGrantTtlMs" | "memoryOrgUnavailableBackoffMs" | "projectMemoryEnabled" | "syncImportLeaseStaleSec">;
|
|
1981
2053
|
/** 组:auth(鉴权 / 身份 / 治理棒)。`commandPolicy` 只有 sema-registry 腿(无 env 标量形),故 env 解析
|
|
@@ -1988,11 +2060,11 @@ export type ServiceAuthConfig = Pick<ServiceConfigFlat, "authToken" | "authToken
|
|
|
1988
2060
|
/** 组:orchestration(编排 + 执行车道 + 沙箱面 + workflow/后台 agent 存留)。 */
|
|
1989
2061
|
export type ServiceOrchestrationConfig = Pick<ServiceConfigFlat, "remoteExec" | "remoteExecHomeDir" | "requireExecutionLane" | "worktreeIsolation" | "leaderEnabled" | "leaderFanoutEnabled" | "routerEnabled" | "selfOrchestrationEnabled" | "selfOrchestrationModels" | "selfOrchestrationWorkerIsolation" | "forkEnabled" | "experimentalObserverAgents" | "schedulerEnabled" | "schedulerSessionWakeup" | "schedulerSessionLifetime" | "schedulerStorePath" | "planModeEnabled" | "hooksTimeoutMs" | "workflowRunStoreBackend" | "workflowOrphanGraceMs" | "workflowJournalRetentionMs" | "workflowRunRetentionMs" | "usageWindows" | "workflowAgentsReadOnly" | "workflowSizeGuideline" | "backgroundAgentRetentionMs" | "backgroundAgentStaleRunningMs" | "backgroundAgentParkClaimStaleMs" | "delegationEntryCaps" | "rosterRetentionMs" | "leaderRunStaleMs" | "scratchpadSweepTtlMs" | "sandboxPkgSource" | "sessionAutoTitle" | "sessionAutoTitleSource" | "selectEnvironmentTool" | "envFactsEnabled" | "toolDeferLongtail" | "lspEnabled" | "lspHostEnabled" | "lspEnvAllow" | "imageBakes" | "readFace" | "readFaceSource" | "taskWriteFaceEscalation" | "readDenyPatterns" | "readDenyBuiltinTiers" | "readDenyBuiltinExclude">;
|
|
1990
2062
|
/** 组:limitsHttp(HTTP 面 + 各类上限/配额/回收窗)。 */
|
|
1991
|
-
export type ServiceLimitsHttpConfig = Pick<ServiceConfigFlat, "port" | "attachmentOrphanGraceMs" | "workspaceFileMaxBytes" | "attachmentMaxBytes" | "attachmentMimeAllowlist" | "attachmentUnboundTtlMs" | "infraCostRates" | "drainGraceMs" | "sighupIdleGraceMs" | "parentPid" | "engineAutoExit" | "engineLingerMs" | "engineOrphanLingerMs" | "rateLimitPerMin" | "maxTaskCostUsd" | "maxTaskTokens" | "maxPrincipalCostUsd" | "costQuotaWindowSec" | "reapIntervalSec" | "runStaleSec" | "toolResultTtlSec">;
|
|
2063
|
+
export type ServiceLimitsHttpConfig = Pick<ServiceConfigFlat, "port" | "attachmentOrphanGraceMs" | "workspaceFileMaxBytes" | "attachmentMaxBytes" | "attachmentMimeAllowlist" | "attachmentUnboundTtlMs" | "infraCostRates" | "drainGraceMs" | "drainSecondSignalMinGapMs" | "sighupIdleGraceMs" | "parentPid" | "engineAutoExit" | "engineLingerMs" | "engineOrphanLingerMs" | "rateLimitPerMin" | "maxTaskCostUsd" | "maxTaskTokens" | "maxPrincipalCostUsd" | "costQuotaWindowSec" | "reapIntervalSec" | "runStaleSec" | "toolResultTtlSec">;
|
|
1992
2064
|
/** 组:observability(可观测)。 */
|
|
1993
2065
|
export type ServiceObservabilityConfig = Pick<ServiceConfigFlat, "metricsToken" | "traceToken" | "toolTrace" | "traceThinking" | "logLevel" | "otel">;
|
|
1994
2066
|
/** 组:integrations(外部集成)。`mcpServers` / `a2aPeers` 由 sema-registry 适配器填(无 env 腿),归本组。 */
|
|
1995
|
-
export type ServiceIntegrationsConfig = Pick<ServiceConfigFlat, "pluginsAllowHosts" | "mcpServers" | "a2aPeers" | "a2aServe" | "configBootFetchBudgetMs" | "gitApiBaseUrl" | "gitApiToken" | "gitApiKind" | "defaultScenario" | "skillsDir" | "configCenter" | "configProvider" | "configLocalDir">;
|
|
2067
|
+
export type ServiceIntegrationsConfig = Pick<ServiceConfigFlat, "pluginsAllowHosts" | "mcpServers" | "a2aPeers" | "a2aServe" | "configBootFetchBudgetMs" | "gitApiBaseUrl" | "gitApiToken" | "gitApiKind" | "defaultScenario" | "skillsDir" | "centerCredentialRenewLeadMs" | "configCenter" | "configProvider" | "configLocalDir">;
|
|
1996
2068
|
/** 九个组槽。每组恒在场(`loadConfig` / `attachConfigGroups` 装好才交出配置),故不可选——新代码写
|
|
1997
2069
|
* `config.modelPlane.model` 不需要 `?.`(可选组会把 `Model` 污染成 `Model | undefined`)。 */
|
|
1998
2070
|
export interface ServiceConfigGroups {
|
package/dist/config-types.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
export const TASK_WRITE_FACE_ESCALATIONS = ["allow", "refuse"];
|
|
2
2
|
export const RETENTION_MODES = ["audit-only", "enforce"];
|
|
3
3
|
export const MEMORY_CONSOLIDATION_MIN_INTERVAL_SEC = 300;
|
|
4
|
+
export const MAX_TIMER_DELAY_MS = 2_147_483_647;
|
|
5
|
+
export const AUTO_MODE_CLASSIFIER_TIMEOUT_BOUNDS = { minMs: 1_000, maxMs: MAX_TIMER_DELAY_MS };
|
|
6
|
+
export const DRAIN_GRACE_FLOOR_MS = 10_000;
|
|
7
|
+
export const DRAIN_SECOND_SIGNAL_MIN_GAP_BOUNDS = { minMs: 0, maxMs: DRAIN_GRACE_FLOOR_MS };
|
|
8
|
+
export const DRAIN_SECOND_SIGNAL_MIN_GAP_DEFAULT_MS = 250;
|
|
4
9
|
export const RESILIENCE_DEFAULTS = {
|
|
5
10
|
connectTimeoutMs: { env: "MODEL_CONNECT_TIMEOUT_MS", value: 30_000 },
|
|
6
11
|
firstTokenTimeoutMs: { env: "MODEL_FIRST_TOKEN_TIMEOUT_MS", value: 600_000 },
|
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 } 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,6 +20,7 @@ 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";
|
|
@@ -106,7 +107,7 @@ function defaultSkillsDir() {
|
|
|
106
107
|
const RETIRED_ENVS = [
|
|
107
108
|
["TIDB_HOST", "MYSQL_HOST", "renamed"], ["TIDB_PORT", "MYSQL_PORT", "renamed"], ["TIDB_USER", "MYSQL_USER", "renamed"],
|
|
108
109
|
["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",
|
|
110
|
+
["CONFIG_CENTER_URL", "SEMA_REGISTRY_URL", "renamed"], ["CONFIG_CENTER_TOKEN", CENTER_CREDENTIAL_ENV.token, "renamed"],
|
|
110
111
|
["CONFIG_CENTER_DRY_RUN", "SEMA_REGISTRY_DRY_RUN", "renamed"], ["CONFIG_CENTER_WORKER", "SEMA_REGISTRY_WORKER", "renamed"],
|
|
111
112
|
["ANTHROPIC_BASEURL", "ANTHROPIC_BASE_URL", "renamed"],
|
|
112
113
|
["REGISTRY_ADMIN_TOKEN", "SEMA_REGISTRY_ADMIN_TOKEN", "renamed"],
|
|
@@ -664,6 +665,21 @@ function optionalEnumKey(key, envName, raw, allowed) {
|
|
|
664
665
|
}
|
|
665
666
|
return { [key]: raw };
|
|
666
667
|
}
|
|
668
|
+
function parseAutoModeClassifierTimeoutMs(raw) {
|
|
669
|
+
if (raw === undefined || raw === "")
|
|
670
|
+
return {};
|
|
671
|
+
const { minMs, maxMs } = AUTO_MODE_CLASSIFIER_TIMEOUT_BOUNDS;
|
|
672
|
+
const n = /^[1-9]\d*$/.test(raw) ? Number(raw) : Number.NaN;
|
|
673
|
+
if (!Number.isSafeInteger(n) || n < minMs || n > maxMs) {
|
|
674
|
+
const shown = raw.length > 40 ? `${raw.slice(0, 40)}…(${raw.length} chars)` : raw;
|
|
675
|
+
throw new Error(`env AUTO_MODE_CLASSIFIER_TIMEOUT_MS=${JSON.stringify(shown)} must be a whole number of milliseconds in [${minMs}, ${maxMs}] ` +
|
|
676
|
+
`(decimal digits only; unset = core's own default). It caps ONE rung of the auto-mode classifier ladder — a rung past the cap ` +
|
|
677
|
+
`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 ` +
|
|
678
|
+
`seconds-for-milliseconds slip; above ${maxMs} Node collapses the timer to 1ms. Refusing to boot rather than running the core ` +
|
|
679
|
+
`default while the operator believes their value is in force (#210).`);
|
|
680
|
+
}
|
|
681
|
+
return { autoModeClassifierTimeoutMs: n };
|
|
682
|
+
}
|
|
667
683
|
function headerNameEnv(name, fallback) {
|
|
668
684
|
const v = env(name, fallback);
|
|
669
685
|
if (!/^[A-Za-z0-9_-]+$/.test(v))
|
|
@@ -1167,7 +1183,6 @@ export function formatUnmatchableToolNames(source, bad) {
|
|
|
1167
1183
|
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
1184
|
bad.map((b) => `${presentToolName(b.name)} → ${b.guidance}`).join("; ")).replace(CONTROL_AND_BIDI_STRIP_RE, "");
|
|
1169
1185
|
}
|
|
1170
|
-
const MAX_TIMER_DELAY_MS = 2_147_483_647;
|
|
1171
1186
|
function streamApprovalConfig(gates) {
|
|
1172
1187
|
const cfg = {
|
|
1173
1188
|
enabled: boolEnv("STREAM_APPROVAL_ENABLED", true),
|
|
@@ -1284,6 +1299,7 @@ function parseApprovalDomain(ctx) {
|
|
|
1284
1299
|
})(process.env.MANUAL_MODE_SHELL_GATE),
|
|
1285
1300
|
permissionsDisableAutoMode: boolEnv("PERMISSIONS_DISABLE_AUTO_MODE", false),
|
|
1286
1301
|
...optionalEnumKey("autoModeClassifierFallback", "AUTO_MODE_CLASSIFIER_FALLBACK", process.env.AUTO_MODE_CLASSIFIER_FALLBACK, ["main", "none"]),
|
|
1302
|
+
...parseAutoModeClassifierTimeoutMs(process.env.AUTO_MODE_CLASSIFIER_TIMEOUT_MS),
|
|
1287
1303
|
...optionalEnumKey("crossSessionInbound", "CROSS_SESSION_INBOUND", process.env.CROSS_SESSION_INBOUND, CROSS_SESSION_INBOUND_SETTINGS),
|
|
1288
1304
|
...optionalEnumKey("crossSessionDialogExpiry", "CROSS_SESSION_DIALOG_EXPIRY", process.env.CROSS_SESSION_DIALOG_EXPIRY, CROSS_SESSION_DIALOG_EXPIRY_VALUES),
|
|
1289
1305
|
mcpElicitation: {
|
|
@@ -1400,6 +1416,22 @@ function lingerMsEnv(name, raw, defaultMs, disableHint) {
|
|
|
1400
1416
|
}
|
|
1401
1417
|
return n;
|
|
1402
1418
|
}
|
|
1419
|
+
function parseDrainSecondSignalMinGapMs(raw) {
|
|
1420
|
+
if (raw === undefined || raw === "")
|
|
1421
|
+
return DRAIN_SECOND_SIGNAL_MIN_GAP_DEFAULT_MS;
|
|
1422
|
+
const { minMs, maxMs } = DRAIN_SECOND_SIGNAL_MIN_GAP_BOUNDS;
|
|
1423
|
+
const n = /^(0|[1-9]\d*)$/.test(raw) ? Number(raw) : Number.NaN;
|
|
1424
|
+
if (!Number.isSafeInteger(n) || n < minMs || n > maxMs) {
|
|
1425
|
+
const shown = raw.length > 40 ? `${raw.slice(0, 40)}…(${raw.length} chars)` : raw;
|
|
1426
|
+
throw new Error(`env DRAIN_SECOND_SIGNAL_MIN_GAP_MS=${JSON.stringify(shown)} must be a whole number of milliseconds in [${minMs}, ${maxMs}] ` +
|
|
1427
|
+
`(decimal digits only; unset = ${DRAIN_SECOND_SIGNAL_MIN_GAP_DEFAULT_MS}). During a drain, a second SIGTERM / SIGINT hard-stops ` +
|
|
1428
|
+
`only when it arrives at least this long after the drain began — closer ones are the same beat delivered twice and are ` +
|
|
1429
|
+
`ignored. "0" turns that filter off (every second signal hard-stops, the 7.101.x behavior); above ${maxMs} (the ` +
|
|
1430
|
+
`DRAIN_GRACE_MS floor) a second signal could never count inside the shortest drain window. Refusing to boot rather ` +
|
|
1431
|
+
`than running the default while the operator believes their value is in force (#210).`);
|
|
1432
|
+
}
|
|
1433
|
+
return n;
|
|
1434
|
+
}
|
|
1403
1435
|
function parseMemoryEmbedder(ctx) {
|
|
1404
1436
|
const endpoint = process.env.MEMORY_EMBEDDER_ENDPOINT || undefined;
|
|
1405
1437
|
const model = process.env.MEMORY_EMBEDDER_MODEL || undefined;
|
|
@@ -1926,7 +1958,7 @@ function parseOrchestrationDomain(ctx) {
|
|
|
1926
1958
|
export const LIMITS_HTTP_FLOORS = {
|
|
1927
1959
|
attachmentMaxBytes: 1024,
|
|
1928
1960
|
attachmentUnboundTtlMs: 60_000,
|
|
1929
|
-
drainGraceMs:
|
|
1961
|
+
drainGraceMs: DRAIN_GRACE_FLOOR_MS,
|
|
1930
1962
|
};
|
|
1931
1963
|
function parseLimitsHttpDomain() {
|
|
1932
1964
|
return {
|
|
@@ -1957,6 +1989,7 @@ function parseLimitsHttpDomain() {
|
|
|
1957
1989
|
})(),
|
|
1958
1990
|
},
|
|
1959
1991
|
drainGraceMs: clampEnvWithWarn("DRAIN_GRACE_MS", String(10 * 60 * 1000), LIMITS_HTTP_FLOORS.drainGraceMs),
|
|
1992
|
+
drainSecondSignalMinGapMs: parseDrainSecondSignalMinGapMs(process.env.DRAIN_SECOND_SIGNAL_MIN_GAP_MS),
|
|
1960
1993
|
sighupIdleGraceMs: clampEnvWithWarn("SIGHUP_IDLE_GRACE_MS", String(120_000), 5_000),
|
|
1961
1994
|
...parentPidEnv(),
|
|
1962
1995
|
engineAutoExit: boolEnv("ENGINE_AUTO_EXIT", false),
|
|
@@ -2021,7 +2054,8 @@ function parseGitApiKind(raw, baseUrl) {
|
|
|
2021
2054
|
}
|
|
2022
2055
|
return kind;
|
|
2023
2056
|
}
|
|
2024
|
-
function parseIntegrationsDomain() {
|
|
2057
|
+
function parseIntegrationsDomain(ctx) {
|
|
2058
|
+
const { requirePrincipal } = ctx;
|
|
2025
2059
|
return {
|
|
2026
2060
|
pluginsAllowHosts: (process.env.PLUGINS_ALLOW_HOSTS ?? "github.com").split(",").map((h) => h.trim()).filter(Boolean),
|
|
2027
2061
|
configBootFetchBudgetMs: clampEnvWithWarn("CONFIG_BOOT_FETCH_BUDGET_MS", "1500", 0),
|
|
@@ -2030,17 +2064,19 @@ function parseIntegrationsDomain() {
|
|
|
2030
2064
|
gitApiKind: parseGitApiKind(process.env.GIT_API_KIND, process.env.GIT_API_BASEURL),
|
|
2031
2065
|
defaultScenario: env("DEFAULT_SCENARIO", "code"),
|
|
2032
2066
|
skillsDir: env("SKILLS_DIR", defaultSkillsDir()),
|
|
2033
|
-
|
|
2067
|
+
centerCredentialRenewLeadMs: parseCenterCredentialRenewLeadMs(process.env.CENTER_CREDENTIAL_RENEW_LEAD_MS),
|
|
2068
|
+
configCenter: process.env.SEMA_REGISTRY_URL ? parseConfigCenter(env("SEMA_REGISTRY_URL"), requirePrincipal) : undefined,
|
|
2034
2069
|
configProvider: process.env.CONFIG_PROVIDER || undefined,
|
|
2035
2070
|
configLocalDir: process.env.CONFIG_LOCAL_DIR || undefined,
|
|
2036
2071
|
a2aServe: parseA2aServe(),
|
|
2037
2072
|
};
|
|
2038
2073
|
}
|
|
2039
|
-
function parseConfigCenter(baseUrl) {
|
|
2074
|
+
function parseConfigCenter(baseUrl, requirePrincipal) {
|
|
2040
2075
|
const worker = process.env.SEMA_REGISTRY_WORKER || undefined;
|
|
2076
|
+
const declared = readCenterCredentialDeclaration();
|
|
2041
2077
|
return {
|
|
2042
2078
|
baseUrl,
|
|
2043
|
-
credential: createCenterCredentialSource(
|
|
2079
|
+
credential: createCenterCredentialSource(declared.from === "file" ? { ...declared, multiTenant: requirePrincipal } : declared, worker),
|
|
2044
2080
|
dryRun: boolEnv("SEMA_REGISTRY_DRY_RUN", false),
|
|
2045
2081
|
worker,
|
|
2046
2082
|
};
|
|
@@ -2141,7 +2177,7 @@ const APPROVAL_GROUP_KEYS = [
|
|
|
2141
2177
|
"approvalRequire", "approvalDeny", "approvalTimeoutSec", "approvalAutoBudget", "approvalNeverAuto",
|
|
2142
2178
|
"approvalHmacKeys", "durableApproval", "durableApprovalSource", "streamAskWindowMsSource", "directApprovalDoor", "directDoorActive", "resourceSuspend",
|
|
2143
2179
|
"resourceSuspendTtlSec", "resourceSuspendTotalTokens", "resourceSuspendTotalBudgetUsd", "resourceSuspendMaxSlices", "askQuestionEnabled", "questionThrottle", "toolApprovalEnabled", "permissionRulesEnabled", "permissionRulesEnabledExplicit", "streamAskWindowMarginMs", "streamApproval", "unattendedApprovalPolicy", "mcpElicitation",
|
|
2144
|
-
"sensitiveWritePatterns", "writeProtectedPaths", "manualModeShellGate", "permissionsDisableAutoMode", "autoModeClassifierFallback", "crossSessionInbound", "crossSessionDialogExpiry",
|
|
2180
|
+
"sensitiveWritePatterns", "writeProtectedPaths", "manualModeShellGate", "permissionsDisableAutoMode", "autoModeClassifierFallback", "autoModeClassifierTimeoutMs", "crossSessionInbound", "crossSessionDialogExpiry",
|
|
2145
2181
|
];
|
|
2146
2182
|
const MEMORY_GROUP_KEYS = [
|
|
2147
2183
|
"memoryEngineEnabled", "memoryEngineDir", "memoryEngineRemoteLaneAllowed", "memoryEngineBackend", "memoryScope",
|
|
@@ -2166,7 +2202,7 @@ const ORCHESTRATION_GROUP_KEYS = [
|
|
|
2166
2202
|
];
|
|
2167
2203
|
const LIMITS_HTTP_GROUP_KEYS = [
|
|
2168
2204
|
"port", "attachmentOrphanGraceMs", "workspaceFileMaxBytes", "attachmentMaxBytes", "attachmentMimeAllowlist",
|
|
2169
|
-
"attachmentUnboundTtlMs", "infraCostRates", "drainGraceMs", "sighupIdleGraceMs", "parentPid",
|
|
2205
|
+
"attachmentUnboundTtlMs", "infraCostRates", "drainGraceMs", "drainSecondSignalMinGapMs", "sighupIdleGraceMs", "parentPid",
|
|
2170
2206
|
"engineAutoExit", "engineLingerMs", "engineOrphanLingerMs",
|
|
2171
2207
|
"rateLimitPerMin", "maxTaskCostUsd",
|
|
2172
2208
|
"maxTaskTokens", "maxPrincipalCostUsd", "costQuotaWindowSec", "reapIntervalSec", "runStaleSec", "toolResultTtlSec",
|
|
@@ -2174,7 +2210,7 @@ const LIMITS_HTTP_GROUP_KEYS = [
|
|
|
2174
2210
|
const OBSERVABILITY_GROUP_KEYS = ["metricsToken", "traceToken", "toolTrace", "traceThinking", "logLevel", "otel"];
|
|
2175
2211
|
const INTEGRATIONS_GROUP_KEYS = [
|
|
2176
2212
|
"pluginsAllowHosts", "mcpServers", "a2aPeers", "a2aServe", "configBootFetchBudgetMs", "gitApiBaseUrl", "gitApiToken", "gitApiKind", "defaultScenario",
|
|
2177
|
-
"skillsDir", "configCenter", "configProvider", "configLocalDir",
|
|
2213
|
+
"skillsDir", "centerCredentialRenewLeadMs", "configCenter", "configProvider", "configLocalDir",
|
|
2178
2214
|
];
|
|
2179
2215
|
export const CONFIG_GROUP_KEYS = {
|
|
2180
2216
|
store: STORE_GROUP_KEYS,
|
|
@@ -2234,7 +2270,7 @@ export function loadConfig() {
|
|
|
2234
2270
|
const orchestration = parseOrchestrationDomain({ postureOn, singleUserTurnkey });
|
|
2235
2271
|
const limitsHttp = parseLimitsHttpDomain();
|
|
2236
2272
|
const observability = parseObservabilityDomain();
|
|
2237
|
-
const integrations = parseIntegrationsDomain();
|
|
2273
|
+
const integrations = parseIntegrationsDomain({ requirePrincipal });
|
|
2238
2274
|
const { directApprovalDoor, durableApproval: dgDurable, approvalHmacKeys: dgHmacKeys } = approval;
|
|
2239
2275
|
const { principalJwtPubkeys, principalJwtIss, principalJwtAud, operatorPrincipals: dgOperators, authTokens } = auth;
|
|
2240
2276
|
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` 的「文档多码」臂当场红)。
|
package/dist/fleet-client.d.ts
CHANGED
|
@@ -4,16 +4,25 @@
|
|
|
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
|
+
* - **自报状态翻转即刻再 announce**(第一时间自摘流 / 自挂流 / 报重启,比任何观测都快):draining 翻转由收尾链直调
|
|
16
|
+
* `announceNow`;roster 落地与 restart 信号变化由装配层把 config-center 的 `onSelfReportChange` 接到 `announceNow`
|
|
17
|
+
* (两处 = 两类输入各自的唯一写者,触发都是 `announceNow`)。退出前 DELETE(显式注销,幂等)。
|
|
10
18
|
* - **usage 批报**:tracer 的 `brain.call` 已带 ALS principal —— FleetUsageAccumulator 按 principal×model
|
|
11
19
|
* 累计,每窗口(默认 60s,半开 [windowStartMs, windowEndMs))POST /api/fleet/usage。幂等键
|
|
12
20
|
* (worker,instanceId,windowStartMs) 重发安全;**发送失败 → merge 回累计器**,并入下一窗(失败窗从未入账,
|
|
13
21
|
* 向前合并不会双计 —— 只延迟,不丢量)。
|
|
14
22
|
* - 全部 fire-and-forget + swallow-guarded:fleet 面是运维旁路,绝不影响任务面;网络错误只 warn 一次/连败类。
|
|
15
23
|
*/
|
|
16
|
-
import { UsageReport } from "@sema-agent/settings-schema/fleet";
|
|
24
|
+
import { UsageReport, type RestartSignal } from "@sema-agent/settings-schema/fleet";
|
|
25
|
+
import type { ReadinessVerdict } from "./http/readiness.js";
|
|
17
26
|
import { type CenterEndpoint } from "./config-center/center-request.js";
|
|
18
27
|
/** Bound on distinct principal×model keys per window — a runaway/multi-tenant burst collapses into
|
|
19
28
|
* `__other__` instead of growing unbounded (same posture as budget.ts cardinalityGuard). */
|
|
@@ -65,8 +74,12 @@ export interface FleetClientOpts {
|
|
|
65
74
|
/** Reachable base URL of THIS instance — the operator's explicit declaration (`FLEET_ADVERTISE_ADDRESS`). */
|
|
66
75
|
advertiseAddress: string;
|
|
67
76
|
version: string;
|
|
68
|
-
/**
|
|
69
|
-
|
|
77
|
+
/** S-740:就绪判(装配层接 `readinessOf(deployment)` —— HTTP 面读的同一个 deps 组)。每条公告现读**一次**,
|
|
78
|
+
* `ready` 与 `draining` 两字段都由这一次读铸(不会撕裂成 `ready:true` + `draining:true`)。 */
|
|
79
|
+
readiness: () => ReadinessVerdict;
|
|
80
|
+
/** S-741:restart 信号(装配层接 `/health` 同一只取值口 `deployment.restartState`)。每条公告现读一次;`undefined` ⇒ 公告不带
|
|
81
|
+
* `restart` 键。必填:心跳面没有「不知道要不要重启」这一形。 */
|
|
82
|
+
restart: () => RestartSignal | undefined;
|
|
70
83
|
usage?: FleetUsageAccumulator;
|
|
71
84
|
heartbeatMs?: number;
|
|
72
85
|
usageWindowMs?: number;
|
|
@@ -78,7 +91,8 @@ export interface FleetClientOpts {
|
|
|
78
91
|
fetchImpl?: typeof fetch;
|
|
79
92
|
}
|
|
80
93
|
export interface FleetClient {
|
|
81
|
-
/** Immediate out-of-cycle announce — call when
|
|
94
|
+
/** Immediate out-of-cycle announce — call when a self-reported input flips (draining: self-remove from routing FIRST;
|
|
95
|
+
* roster landed: become routable without waiting a heartbeat; restart signal changed: S-741). */
|
|
82
96
|
announceNow: () => Promise<void>;
|
|
83
97
|
/** Stop timers + best-effort DELETE deregistration (bounded; never throws). */
|
|
84
98
|
stop: () => Promise<void>;
|
package/dist/fleet-client.js
CHANGED
|
@@ -81,17 +81,22 @@ 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 body = {
|
|
91
|
+
instanceId: opts.instanceId,
|
|
92
|
+
worker: opts.center.worker,
|
|
93
|
+
address: opts.advertiseAddress,
|
|
94
|
+
version: opts.version,
|
|
95
|
+
ready: verdict.ready,
|
|
96
|
+
draining: !verdict.ready && verdict.reasons.includes("draining"),
|
|
97
|
+
heartbeatMs,
|
|
98
|
+
...(restart !== undefined ? { restart } : {}),
|
|
99
|
+
};
|
|
95
100
|
const { res } = await send("fleet-instances", "POST", body);
|
|
96
101
|
if (!res.ok)
|
|
97
102
|
throw new Error(`announce ${res.status}${await rejectionSnippet(res)}`);
|
|
@@ -105,6 +110,26 @@ export function startFleetClient(opts) {
|
|
|
105
110
|
opts.logger?.warn("fleet_announce_failed", { error: err instanceof Error ? err.message : String(err) });
|
|
106
111
|
}
|
|
107
112
|
};
|
|
113
|
+
let inFlight;
|
|
114
|
+
let again = false;
|
|
115
|
+
const announceNow = () => {
|
|
116
|
+
if (inFlight !== undefined) {
|
|
117
|
+
again = true;
|
|
118
|
+
return inFlight;
|
|
119
|
+
}
|
|
120
|
+
inFlight = (async () => {
|
|
121
|
+
try {
|
|
122
|
+
do {
|
|
123
|
+
again = false;
|
|
124
|
+
await announceOnce();
|
|
125
|
+
} while (again && !stopped);
|
|
126
|
+
}
|
|
127
|
+
finally {
|
|
128
|
+
inFlight = undefined;
|
|
129
|
+
}
|
|
130
|
+
})();
|
|
131
|
+
return inFlight;
|
|
132
|
+
};
|
|
108
133
|
let windowStartMs = Date.now();
|
|
109
134
|
const flushUsage = async () => {
|
|
110
135
|
if (!opts.usage)
|
|
@@ -131,13 +156,17 @@ export function startFleetClient(opts) {
|
|
|
131
156
|
hb.unref?.();
|
|
132
157
|
const uw = opts.usage ? setInterval(() => void flushUsage(), usageWindowMs) : undefined;
|
|
133
158
|
uw?.unref?.();
|
|
159
|
+
let stopping = false;
|
|
134
160
|
const stop = async () => {
|
|
135
|
-
if (
|
|
161
|
+
if (stopping)
|
|
136
162
|
return;
|
|
137
|
-
|
|
163
|
+
stopping = true;
|
|
138
164
|
clearInterval(hb);
|
|
139
165
|
if (uw)
|
|
140
166
|
clearInterval(uw);
|
|
167
|
+
while (inFlight !== undefined)
|
|
168
|
+
await inFlight;
|
|
169
|
+
stopped = true;
|
|
141
170
|
await flushUsage();
|
|
142
171
|
try {
|
|
143
172
|
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: {
|