@sema-agent/server 7.36.0-rc.1 → 7.36.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/USAGE.md +5 -0
- package/dist/http/routes/admin-drain.d.ts +4 -2
- package/dist/http/server.d.ts +3 -1
- package/dist/http/server.js +5 -1
- package/dist/store-live-probe.d.ts +26 -0
- package/dist/store-live-probe.js +60 -0
- package/package.json +1 -1
package/USAGE.md
CHANGED
|
@@ -224,6 +224,11 @@ MODEL_CASCADE_LADDER=deepseek-flash,deepseek-pro # 目录里的模型名,cheap
|
|
|
224
224
|
默认 15000;0=关;负/非数启动响亮拒)缓存一份 DB 真往返结果,`storeProbe:{live,ageMs,error?}` 在
|
|
225
225
|
探针接线时恒在,顶层告警键 `storeLive:false` 只在死时出现。**status 保持 "ok"**(liveness≠readiness
|
|
226
226
|
——DB 死不是进程死,摘流语义留给读键的编排器/LB)。local/memory 部署形状不变。
|
|
227
|
+
`error` 是**闭集词**(7.36.0 起,#305):`probe_timeout` / `connection_refused` / `connection_reset` /
|
|
228
|
+
`host_unreachable` / `dns_failure` / `network_timeout` / `auth_failed` / `connection_limit` /
|
|
229
|
+
`probe_failed`(未识别)。**驱动原始异常 message 不上这一面**——`/health` 免鉴权且缺省全网卡监听,而
|
|
230
|
+
连接类异常惯于回声整条 DSN(`mysql://user:pass@host:3306/db`)。要读全文去**日志**:探针在
|
|
231
|
+
live→dead 翻转拍打 `store_probe_dead`,`error` 字段是原文。
|
|
227
232
|
钉:`test/health-store-live.test.ts` / `test/store-live-probe.test.ts`。
|
|
228
233
|
- 数据驻留提示:`DB_BACKEND=local` 下显式 `SESSION_BACKEND=memory` 会被收编为 **durable(local)**
|
|
229
234
|
(1.292+ 裸 boot 默认 durable;/health 的 `sessionBackend` 报 `durable(local)`)——session 行落盘在
|
|
@@ -20,8 +20,10 @@
|
|
|
20
20
|
* **裁定:行为不改。** 判据三条:① `/health` 免鉴权是既成契约,而它正是这条信息的设计消费者(k8s
|
|
21
21
|
* probe / LB / 编排器带不了 token,`drainReason` 放这儿的全部理由就是给它们看);② 值的内容完全由
|
|
22
22
|
* operator 自选,且窗口只在 draining 期;③ 同一免鉴权体上早已有 `dataRoot` 绝对路径 / `pid` / `port` /
|
|
23
|
-
* `instanceId` / `configHash` / `storeProbe
|
|
24
|
-
* 把**原始异常 message 未截断**塞出去、DSN
|
|
23
|
+
* `instanceId` / `configHash` / `storeProbe`,单独收紧本键不改变那面的暴露量级。
|
|
24
|
+
* (那条另立的兄弟件 —— `storeProbe.error` 把**原始异常 message 未截断**塞出去、DSN 片段可随之外泄 ——
|
|
25
|
+
* 已在 #305 修掉:免鉴权面上它改成闭集词,原文只走日志轴。两件的判据不同故不合并:本键的值由 operator
|
|
26
|
+
* 自选且窗口只在 draining 期,`storeProbe.error` 的值则来自不受控的驱动异常。)
|
|
25
27
|
* ⇒ 真正的缺口是**告诫缺席**:此前路由头注、400 文案、CHANGELOG、wire 契约四处都只说「透出」,没有一处
|
|
26
28
|
* 说「其中一面免鉴权」,操作员要拼出「我写的字是公开的」得自己合并两处文档去推理。本批把它写进**写口
|
|
27
29
|
* 现场**(400 文案 + 本注 + USAGE 端点条目),因为那是操作员唯一会读到的地方。
|
package/dist/http/server.d.ts
CHANGED
|
@@ -469,7 +469,9 @@ export interface ServiceDeploymentDeps {
|
|
|
469
469
|
* 缺席(local/memory/探针关)= /health 形状不变;接线 = `storeProbe{live,ageMs,error?}` 恒在
|
|
470
470
|
* (「探过且活」与「没接线」机读可分),顶层告警键 `storeLive:false` 只在死时出现。status 恒 "ok"
|
|
471
471
|
* ——liveness≠readiness,DB 死不是进程死;摘流语义留给读键的编排器(与 durable/ready 同姿势,
|
|
472
|
-
* 披露不代裁)。钉:test/health-store-live.test.ts。
|
|
472
|
+
* 披露不代裁)。钉:test/health-store-live.test.ts。
|
|
473
|
+
* 🔴 #305:座席给的 `error` 是**驱动原始 message**(DSN/主机/账号常在其中),而 /health 在鉴权门
|
|
474
|
+
* 之前 ⇒ 路由把它过 `publicStoreProbeError` 换成闭集词才上线;原文只走日志轴。 */
|
|
473
475
|
storeLiveState?: () => {
|
|
474
476
|
live: boolean;
|
|
475
477
|
ageMs: number;
|
package/dist/http/server.js
CHANGED
|
@@ -14,6 +14,7 @@ import { normalizeApproachNotice, validateTaskAgents } from "../spec-fields.js";
|
|
|
14
14
|
import { isValidCwd, MAX_ADDITIONAL_DIRS } from "../task-cwd.js";
|
|
15
15
|
import { runInBackground, evictIfConflict, stripCheckpointToken, TurnAnchorCapture, HEARTBEAT_MS, markChildrenStoppedByUserOnAbort } from "../runs.js";
|
|
16
16
|
import { readTurnActivityMs, recordTurnActivity } from "../turn-activity.js";
|
|
17
|
+
import { publicStoreProbeError } from "../store-live-probe.js"; // #305:免鉴权 /health 面的探针错误闭集词表
|
|
17
18
|
import { looksLikeJwt } from "@sema-agent/registry-core/api/auth-bridge";
|
|
18
19
|
import {} from "../orchestration/workflow-agent-steer.js";
|
|
19
20
|
import { emitPendingWorkflowCompletions, taskNotificationInboxEntry, taskNotificationStreamKey, NotifiedKeys } from "../orchestration/workflow-completion-inbox.js";
|
|
@@ -498,7 +499,10 @@ export function createHttpServer(rawDeps) {
|
|
|
498
499
|
return {};
|
|
499
500
|
return {
|
|
500
501
|
...(probe.live ? {} : { storeLive: false }),
|
|
501
|
-
|
|
502
|
+
// #305:error 过闭集词表 —— 这一整个响应体在鉴权门**之前**发出,而座席给的是驱动原始
|
|
503
|
+
// message(DSN/主机/账号常在其中)。消毒钉在**这个 wire 装配点**而不是探针环里:座席是
|
|
504
|
+
// 可注入接缝,只有边界上的这一道对**任何**生产者成立(fail-closed 方向)。原文留日志轴。
|
|
505
|
+
storeProbe: { live: probe.live, ageMs: probe.ageMs, ...(probe.error !== undefined ? { error: publicStoreProbeError(probe.error) } : {}) },
|
|
502
506
|
};
|
|
503
507
|
})(),
|
|
504
508
|
});
|
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
* - 翻转披露(§M):live→dead warn `store_probe_dead`、dead→live info `store_probe_recovered`,
|
|
12
12
|
* 只在翻转拍记(连续死不逐拍刷日志——down 的 DB 不该制造日志风暴);
|
|
13
13
|
* - 诚实缺席:首拍落地前 state() = undefined(/health 键缺席=「还没探过」,不冒充「活」)。
|
|
14
|
+
* - 面分级(#305):`state().error` 是**原始** message —— 它只喂给凭证后/本机的消费面;免鉴权的
|
|
15
|
+
* `/health` 一格由 wire 装配点过 {@link publicStoreProbeError} 换成闭集词(理由见该常量头注)。
|
|
14
16
|
*
|
|
15
17
|
* 探针原语:调用方给 `probe`(main.ts 传 `() => backend.dbNowMs!()`——既有 S10 时钟探针的真 DB
|
|
16
18
|
* 往返,零新 SQL 面)。timer 全部 unref(环不得阻止进程退出)。
|
|
@@ -28,6 +30,30 @@ export interface StoreLiveProbe {
|
|
|
28
30
|
state: () => StoreLiveState | undefined;
|
|
29
31
|
stop: () => void;
|
|
30
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* #305(安全轴):`/health` 免鉴权面上 `storeProbe.error` 可披露的**闭集词表**。
|
|
35
|
+
*
|
|
36
|
+
* 为什么必须是闭集而不是「脱敏后的原文」:`/health` 排在 `http/server.ts` 的鉴权门**之前**、缺省全网卡
|
|
37
|
+
* 监听 ⇒ 任何能连到这个端口的人都读得到这一格;而它此前装的是**驱动原始异常 message 逐字**,DB 驱动
|
|
38
|
+
* 的连接类异常惯于把整条 DSN 回声进去(`mysql://user:pass@host:3306/db`)或带上内网主机名/账号名。
|
|
39
|
+
* `trace/redact.ts` 的 `redactSecrets` **挡不住这条**:它的 URL-userinfo 规则只把口令换成 `«redacted»`,
|
|
40
|
+
* **用户名与 `@` 之后的主机原样留下** —— 而本条要防的正是那两个。闭集词是严格更强的形(值恒为下列
|
|
41
|
+
* 字面量之一,构造上不可能携带调用方文本),因此不再叠加脱敏/长度帽:对一个恒为字面量的返回值,那两道
|
|
42
|
+
* 都只是装饰。
|
|
43
|
+
*
|
|
44
|
+
* 信息不丢:**完整原文走日志轴** —— 环在 live→dead 翻转拍打的 `store_probe_dead` warn 带 `{ error }`
|
|
45
|
+
* 原文,运维在自己的日志/采集里读得到全文,那一面本来就是凭证后的。
|
|
46
|
+
*/
|
|
47
|
+
export declare const STORE_PROBE_PUBLIC_PHRASES: readonly ["probe_timeout", "connection_refused", "connection_reset", "host_unreachable", "dns_failure", "network_timeout", "auth_failed", "connection_limit", "probe_failed"];
|
|
48
|
+
export type StoreProbePublicPhrase = (typeof STORE_PROBE_PUBLIC_PHRASES)[number];
|
|
49
|
+
/**
|
|
50
|
+
* 原始探针异常 message → 免鉴权面可披露的闭集词。**返回值恒是 {@link STORE_PROBE_PUBLIC_PHRASES} 的
|
|
51
|
+
* 一个字面量**(返回类型即闭集,写错一个词是编译错误),永不回声入参。
|
|
52
|
+
*
|
|
53
|
+
* 误分类的代价是「运维读到的分类不精确」,**不是泄露** —— 所以这里的判别用宽松的子串包含即可,不必为
|
|
54
|
+
* 精确性把原文碎片带出来。臂序有意义:自家的 race 超时先于泛化的 timeout 臂。
|
|
55
|
+
*/
|
|
56
|
+
export declare function publicStoreProbeError(raw: string): StoreProbePublicPhrase;
|
|
31
57
|
export declare function createStoreLiveProbe(opts: {
|
|
32
58
|
probe: () => Promise<unknown>;
|
|
33
59
|
intervalMs: number;
|
package/dist/store-live-probe.js
CHANGED
|
@@ -11,12 +11,72 @@
|
|
|
11
11
|
* - 翻转披露(§M):live→dead warn `store_probe_dead`、dead→live info `store_probe_recovered`,
|
|
12
12
|
* 只在翻转拍记(连续死不逐拍刷日志——down 的 DB 不该制造日志风暴);
|
|
13
13
|
* - 诚实缺席:首拍落地前 state() = undefined(/health 键缺席=「还没探过」,不冒充「活」)。
|
|
14
|
+
* - 面分级(#305):`state().error` 是**原始** message —— 它只喂给凭证后/本机的消费面;免鉴权的
|
|
15
|
+
* `/health` 一格由 wire 装配点过 {@link publicStoreProbeError} 换成闭集词(理由见该常量头注)。
|
|
14
16
|
*
|
|
15
17
|
* 探针原语:调用方给 `probe`(main.ts 传 `() => backend.dbNowMs!()`——既有 S10 时钟探针的真 DB
|
|
16
18
|
* 往返,零新 SQL 面)。timer 全部 unref(环不得阻止进程退出)。
|
|
17
19
|
*/
|
|
18
20
|
/** 单拍探针上界——挂死连接的判死时间;3s 对 15s 默认间隔留足余量(环内串行,无叠拍)。 */
|
|
19
21
|
export const STORE_PROBE_TIMEOUT_MS = 3000;
|
|
22
|
+
/**
|
|
23
|
+
* #305(安全轴):`/health` 免鉴权面上 `storeProbe.error` 可披露的**闭集词表**。
|
|
24
|
+
*
|
|
25
|
+
* 为什么必须是闭集而不是「脱敏后的原文」:`/health` 排在 `http/server.ts` 的鉴权门**之前**、缺省全网卡
|
|
26
|
+
* 监听 ⇒ 任何能连到这个端口的人都读得到这一格;而它此前装的是**驱动原始异常 message 逐字**,DB 驱动
|
|
27
|
+
* 的连接类异常惯于把整条 DSN 回声进去(`mysql://user:pass@host:3306/db`)或带上内网主机名/账号名。
|
|
28
|
+
* `trace/redact.ts` 的 `redactSecrets` **挡不住这条**:它的 URL-userinfo 规则只把口令换成 `«redacted»`,
|
|
29
|
+
* **用户名与 `@` 之后的主机原样留下** —— 而本条要防的正是那两个。闭集词是严格更强的形(值恒为下列
|
|
30
|
+
* 字面量之一,构造上不可能携带调用方文本),因此不再叠加脱敏/长度帽:对一个恒为字面量的返回值,那两道
|
|
31
|
+
* 都只是装饰。
|
|
32
|
+
*
|
|
33
|
+
* 信息不丢:**完整原文走日志轴** —— 环在 live→dead 翻转拍打的 `store_probe_dead` warn 带 `{ error }`
|
|
34
|
+
* 原文,运维在自己的日志/采集里读得到全文,那一面本来就是凭证后的。
|
|
35
|
+
*/
|
|
36
|
+
export const STORE_PROBE_PUBLIC_PHRASES = [
|
|
37
|
+
"probe_timeout",
|
|
38
|
+
"connection_refused",
|
|
39
|
+
"connection_reset",
|
|
40
|
+
"host_unreachable",
|
|
41
|
+
"dns_failure",
|
|
42
|
+
"network_timeout",
|
|
43
|
+
"auth_failed",
|
|
44
|
+
"connection_limit",
|
|
45
|
+
/** 未识别臂 —— **刻意的**:分类不认得的异常一律丢原文取本词(fail-closed 的默认方向是「少说」,
|
|
46
|
+
* 不是「截断了再说」;截断的前 160 字恰恰是驱动最爱放 DSN 的位置)。 */
|
|
47
|
+
"probe_failed",
|
|
48
|
+
];
|
|
49
|
+
/**
|
|
50
|
+
* 原始探针异常 message → 免鉴权面可披露的闭集词。**返回值恒是 {@link STORE_PROBE_PUBLIC_PHRASES} 的
|
|
51
|
+
* 一个字面量**(返回类型即闭集,写错一个词是编译错误),永不回声入参。
|
|
52
|
+
*
|
|
53
|
+
* 误分类的代价是「运维读到的分类不精确」,**不是泄露** —— 所以这里的判别用宽松的子串包含即可,不必为
|
|
54
|
+
* 精确性把原文碎片带出来。臂序有意义:自家的 race 超时先于泛化的 timeout 臂。
|
|
55
|
+
*/
|
|
56
|
+
export function publicStoreProbeError(raw) {
|
|
57
|
+
const t = raw.toLowerCase();
|
|
58
|
+
if (t.includes("probe timed out"))
|
|
59
|
+
return "probe_timeout"; // 本环自铸的 race 超时(见 probeOnce)
|
|
60
|
+
if (t.includes("econnrefused") || t.includes("connection refused"))
|
|
61
|
+
return "connection_refused";
|
|
62
|
+
if (t.includes("econnreset") || t.includes("connection reset"))
|
|
63
|
+
return "connection_reset";
|
|
64
|
+
if (t.includes("ehostunreach") || t.includes("enetunreach") || t.includes("enetdown"))
|
|
65
|
+
return "host_unreachable";
|
|
66
|
+
if (t.includes("enotfound") || t.includes("eai_again") || t.includes("getaddrinfo"))
|
|
67
|
+
return "dns_failure";
|
|
68
|
+
if (t.includes("etimedout") || t.includes("timed out") || t.includes("timeout"))
|
|
69
|
+
return "network_timeout";
|
|
70
|
+
// mysql `ER_ACCESS_DENIED_ERROR` / pg `28P01 password authentication failed` / 通用 auth 措辞。
|
|
71
|
+
if (t.includes("access denied") || t.includes("authentication failed") || t.includes("28p01") || t.includes("er_access_denied") || t.includes("er_dbaccess_denied")) {
|
|
72
|
+
return "auth_failed";
|
|
73
|
+
}
|
|
74
|
+
// mysql `ER_CON_COUNT_ERROR` / pg `53300 sorry, too many clients already` / 池耗尽措辞。
|
|
75
|
+
if (t.includes("too many connections") || t.includes("too many clients") || t.includes("53300") || t.includes("er_con_count_error") || t.includes("connection slots")) {
|
|
76
|
+
return "connection_limit";
|
|
77
|
+
}
|
|
78
|
+
return "probe_failed";
|
|
79
|
+
}
|
|
20
80
|
export function createStoreLiveProbe(opts) {
|
|
21
81
|
const timeoutMs = opts.timeoutMs ?? STORE_PROBE_TIMEOUT_MS;
|
|
22
82
|
let last;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sema-agent/server",
|
|
3
|
-
"version": "7.36.0
|
|
3
|
+
"version": "7.36.0",
|
|
4
4
|
"description": "Sema Server — the server/API implementation layer for Sema, wiring core, registry, model providers, and cloud agent execution. Built on @sema-agent/core.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "BUSL-1.1",
|