@sema-agent/server 7.135.0 → 7.136.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 +64 -2
- package/USAGE.md +62 -25
- package/deploy/sema-up/README.md +1 -1
- package/deploy/sema-up/chart/templates/NOTES.txt +1 -1
- package/deploy/sema-up/chart/values.yaml +1 -1
- package/dist/approval-card.d.ts +60 -4
- package/dist/approval-card.js +18 -1
- package/dist/approval-content-kind.d.ts +1 -0
- package/dist/approval-content-kind.js +1 -1
- package/dist/auth-bridge.d.ts +34 -5
- package/dist/auth-bridge.js +20 -3
- package/dist/boot/budget-tracing.d.ts +1 -2
- package/dist/boot/budget-tracing.js +2 -2
- package/dist/boot/org-memory.js +1 -1
- package/dist/boot/resolve-spec.js +10 -7
- package/dist/boot/runner-deps.js +4 -2
- package/dist/boot/stage-04-stores.d.ts +1 -1
- package/dist/boot/stage-05-execution-env.d.ts +1 -1
- package/dist/boot/stage-06-runners.d.ts +1 -1
- package/dist/boot/stage-07-capability-layer.d.ts +1 -1
- 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/stores.js +2 -3
- package/dist/boot/web-admin.js +10 -17
- package/dist/brain.js +12 -3
- package/dist/budget.d.ts +3 -3
- package/dist/budget.js +4 -0
- package/dist/capabilities/mcp-probe.d.ts +5 -4
- package/dist/capability-facts.d.ts +3 -7
- package/dist/capability-facts.js +3 -5
- package/dist/center-credential.d.ts +18 -4
- package/dist/center-credential.js +18 -3
- package/dist/config-catalog.js +12 -10
- package/dist/config-d-migration.js +1 -1
- package/dist/config-invariants.d.ts +1 -1
- package/dist/config-invariants.js +8 -0
- package/dist/config-lkg.d.ts +0 -38
- package/dist/config-lkg.js +1 -83
- package/dist/config-types.d.ts +12 -4
- package/dist/config.js +26 -8
- package/dist/deployment-shape.d.ts +1 -1
- package/dist/elicitation.d.ts +8 -7
- package/dist/elicitation.js +9 -8
- package/dist/file-history-disclosure.d.ts +6 -4
- package/dist/file-history-disclosure.js +22 -6
- package/dist/fleet-client.d.ts +32 -1
- package/dist/fleet-client.js +24 -4
- package/dist/fleet-lease.d.ts +7 -3
- package/dist/fleet-lease.js +31 -23
- package/dist/hooks/hook-llm.js +14 -8
- package/dist/hooks/hook-runner.d.ts +4 -4
- package/dist/hooks/hook-runner.js +2 -2
- package/dist/http/checkpoint-ctx.d.ts +28 -9
- package/dist/http/checkpoint-ctx.js +5 -1
- package/dist/http/dispatch.js +9 -9
- package/dist/http/resume-ctx-read.d.ts +2 -16
- package/dist/http/resume-ctx-read.js +2 -3
- package/dist/http/routes/approvals-assistant.d.ts +0 -2
- package/dist/http/routes/approvals-assistant.js +47 -12
- package/dist/http/routes/runs.js +2 -2
- package/dist/http/routes/session-sync.js +31 -9
- package/dist/http/routes/sessions.js +18 -14
- package/dist/http/submit-ctx.js +1 -1
- package/dist/http/wire-types.d.ts +5 -3
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/model-route-gap.d.ts +49 -4
- package/dist/model-route-gap.js +34 -1
- package/dist/object-store-buckets.d.ts +13 -0
- package/dist/object-store-buckets.js +3 -0
- package/dist/observability/fail-open.d.ts +6 -6
- package/dist/observability/fail-open.js +6 -6
- package/dist/observability/metrics.js +1 -1
- package/dist/observability/principal-context.d.ts +4 -1
- package/dist/observability/principal-context.js +6 -1
- package/dist/parked-decide.d.ts +4 -3
- package/dist/parked-decide.js +26 -7
- package/dist/peer-directory.d.ts +2 -1
- package/dist/peer-directory.js +5 -5
- package/dist/plugins/approval-ask-store-sql.d.ts +8 -0
- package/dist/plugins/approval-ask-store-sql.js +3 -0
- package/dist/plugins/blob-backend.js +2 -1
- package/dist/plugins/checkpoint-index-columns.d.ts +50 -1
- package/dist/plugins/checkpoint-index-columns.js +28 -0
- package/dist/plugins/checkpoint-store-sql.d.ts +27 -3
- package/dist/plugins/checkpoint-store-sql.js +58 -29
- package/dist/plugins/file-history-store-sql.d.ts +6 -0
- package/dist/plugins/file-history-store-sql.js +30 -23
- package/dist/plugins/local-checkpoint-store.js +4 -3
- package/dist/plugins/one-time-migrations.d.ts +21 -28
- package/dist/plugins/one-time-migrations.js +39 -123
- package/dist/plugins/remote-env-k8s.d.ts +18 -5
- package/dist/plugins/remote-env-k8s.js +24 -7
- package/dist/plugins/s3-answer.d.ts +15 -0
- package/dist/plugins/s3-answer.js +5 -0
- package/dist/plugins/snapshot-bucket-presence.d.ts +39 -4
- package/dist/plugins/snapshot-bucket-presence.js +35 -7
- package/dist/plugins/sql-errors.d.ts +39 -0
- package/dist/plugins/sql-errors.js +49 -1
- package/dist/plugins/store-fault.d.ts +4 -0
- package/dist/plugins/store-fault.js +8 -0
- package/dist/plugins/tidb-pool.js +1 -1
- package/dist/plugins/web-search.d.ts +1 -1
- package/dist/plugins/web-search.js +2 -2
- package/dist/request-declaration-refusal.d.ts +94 -0
- package/dist/request-declaration-refusal.js +78 -0
- package/dist/request-image-urls.d.ts +6 -0
- package/dist/request-image-urls.js +27 -0
- package/dist/request-key-closure.d.ts +5 -0
- package/dist/request-key-closure.js +4 -1
- package/dist/runtime-caps-resolver.js +1 -1
- package/dist/sandbox-pkg-source.d.ts +28 -1
- package/dist/sandbox-pkg-source.js +1 -2
- package/dist/sealed-key.d.ts +19 -0
- package/dist/sealed-key.js +47 -4
- package/dist/session-incarnation.d.ts +16 -5
- package/dist/session-incarnation.js +7 -4
- package/dist/session-row.d.ts +3 -17
- package/dist/session-row.js +4 -0
- package/dist/session-sync.d.ts +15 -3
- package/dist/session-sync.js +5 -2
- package/dist/task-a2a.d.ts +52 -81
- package/dist/task-a2a.js +219 -89
- package/dist/task-mcp.d.ts +6 -14
- package/dist/task-mcp.js +16 -51
- package/dist/tool-approval.d.ts +23 -10
- package/dist/tool-approval.js +51 -36
- package/dist/trace/core-keyset-guard.d.ts +2 -2
- package/dist/trace/exit-seat.js +37 -13
- package/dist/trace/redact.d.ts +28 -12
- package/dist/trace/redact.js +233 -94
- package/dist/trace/self-minted-wire.d.ts +267 -0
- package/dist/trace/self-minted-wire.js +79 -0
- package/dist/url-credential-refusal.d.ts +52 -0
- package/dist/url-credential-refusal.js +14 -0
- package/dist/url-refusal.d.ts +13 -4
- package/dist/url-refusal.js +8 -2
- package/dist/workspace-space.d.ts +27 -0
- package/dist/workspace-space.js +26 -0
- package/package.json +3 -2
- package/scripts/drain-check-local.mjs +2 -1
- package/scripts/drain-check.mysql.sql +3 -1
- package/scripts/drain-check.pg.sql +3 -1
- package/skills/schedule.md +1 -1
- package/wire-changes/server@7.104.0.json +134 -0
- package/wire-changes/server@7.105.0.json +53 -0
- package/wire-changes/server@7.106.0.json +475 -0
- package/wire-changes/server@7.107.0.json +64 -0
- package/wire-changes/server@7.108.0.json +153 -0
- package/wire-changes/server@7.109.0.json +46 -0
- package/wire-changes/server@7.110.0.json +90 -0
- package/wire-changes/server@7.111.0.json +86 -0
- package/wire-changes/server@7.112.0.json +60 -0
- package/wire-changes/server@7.113.0.json +26 -0
- package/wire-changes/server@7.114.0.json +26 -0
- package/wire-changes/server@7.115.0.json +97 -0
- package/wire-changes/server@7.116.0.json +218 -0
- package/wire-changes/server@7.116.1.json +26 -0
- package/wire-changes/server@7.117.0.json +65 -0
- package/wire-changes/server@7.118.0.json +129 -0
- package/wire-changes/server@7.119.0.json +65 -0
- package/wire-changes/server@7.120.0.json +89 -0
- package/wire-changes/server@7.121.0.json +219 -0
- package/wire-changes/server@7.122.0.json +54 -0
- package/wire-changes/server@7.123.0.json +26 -0
- package/wire-changes/server@7.124.0.json +336 -0
- package/wire-changes/server@7.124.1.json +26 -0
- package/wire-changes/server@7.125.0.json +111 -0
- package/wire-changes/server@7.126.0.json +285 -0
- package/wire-changes/server@7.127.0.json +77 -0
- package/wire-changes/server@7.127.1.json +26 -0
- package/wire-changes/server@7.127.2.json +53 -0
- package/wire-changes/server@7.128.0.json +26 -0
- package/wire-changes/server@7.129.0.json +106 -0
- package/wire-changes/server@7.129.1.json +26 -0
- package/wire-changes/server@7.130.0.json +26 -0
- package/wire-changes/server@7.131.0.json +118 -0
- package/wire-changes/server@7.132.0.json +226 -0
- package/wire-changes/server@7.133.0.json +1155 -0
- package/wire-changes/server@7.134.0.json +107 -0
- package/wire-changes/server@7.135.0.json +174 -0
- package/wire-changes/server@7.136.0.json +448 -0
- package/dist/url-credential.d.ts +0 -52
- package/dist/url-credential.js +0 -49
|
@@ -129,11 +129,13 @@ export declare function rowTooLargeWallOf(dialect: SqlDialect, err: unknown): Ro
|
|
|
129
129
|
* (`ErrEntryTooLarge`,事务层的语句错;真库读数:同池上 8025 写之后的读照常 —— `checkpoint-row-size-db-integration` ②③④)。
|
|
130
130
|
* 断连形(`ECONNRESET` / `EPIPE` / `PROTOCOL_CONNECTION_LOST`)也不在表里:那是 mysql2 的**致命错误**,连接对象自己已经 `_removeFromPool`。
|
|
131
131
|
* PostgreSQL 没有对应面:node-pg 的 `pool.query` 对任何错误都丢弃那条 client。
|
|
132
|
+
* `statementRefusal`:这一行的错说的是**这一条语句**被拒(连接坏只是后果)—— 同一句话换一条连接重试仍被拒。{@link isConnectionFaultError} 读同一张表,认其余各行为连接类故障,唯独不认带这个标记的行。
|
|
132
133
|
* 新增一行 = 改表 + 在 `test/db-closed-connection-never-pooled.test.ts` 的码表里加一格。
|
|
133
134
|
*/
|
|
134
135
|
export declare const MYSQL_CONNECTION_UNUSABLE_ERRORS: ReadonlyArray<{
|
|
135
136
|
readonly errno: number;
|
|
136
137
|
readonly code: string;
|
|
138
|
+
readonly statementRefusal?: true;
|
|
137
139
|
}>;
|
|
138
140
|
/**
|
|
139
141
|
* 「抛出这个错误的那条连接还能不能回池」的单一判据:{@link MYSQL_CONNECTION_UNUSABLE_ERRORS} 里的错 ⇒ **不能** —— 持连接的一方(`sql-driver.ts` 的 `leaseOf`)
|
|
@@ -141,6 +143,43 @@ export declare const MYSQL_CONNECTION_UNUSABLE_ERRORS: ReadonlyArray<{
|
|
|
141
143
|
* 认不出的一律不是(连接照常归还;断连形由 mysql2 自己清出池)。只管 MySQL 协议的错误形:PG 的错误没有这两个键,恒 false。
|
|
142
144
|
*/
|
|
143
145
|
export declare function isConnectionUnusableError(err: unknown): boolean;
|
|
146
|
+
/**
|
|
147
|
+
* **连接类故障**的识别表(闭集;逐行登记「为什么」)—— 「到库的那条连接坏了 / 库此刻不接」:与这一条语句的内容无关、什么都不改稍后再试可能成。
|
|
148
|
+
* 读者(整仓一只判官 `plugins/store-fault.ts` 的 `storeFaultClassOf`,三处同读):审批队列读口、文件历史店的折点(`permanence: "transient"`)、会话同步拉取口(`503`)。
|
|
149
|
+
*
|
|
150
|
+
* 识别集 = 三张表的并集,**没有第四处**:
|
|
151
|
+
* · 本表(下):套接字层与协议层的断连 / 拒连 / 连接数满 / 库的关停与只读切换,按 `code` 登记(MySQL 协议的数字 errno 在场的并登 —— 同本文件其余谓词的姿势:`errno` 与 `code` 是同一件事的两种写法);
|
|
152
|
+
* · {@link MYSQL_CONNECTION_UNUSABLE_ERRORS}(「这条连接不许再回池」的 MySQL 协议错):整表读入,**唯独不认带 `statementRefusal` 的行**(1153:拒的是这一条语句,不是连接);
|
|
153
|
+
* · {@link CONNECTION_FAULT_DRIVER_SENTENCES}:驱动自己抛的**无 `code` 的裸 `Error`**(node-pg 的断连在这一形),按整句相等认。
|
|
154
|
+
*
|
|
155
|
+
* **不在识别集里**(认不出一律不是 —— 错判成连接类的代价是调用方被劝「再试」去撞一个注定失败的语句):
|
|
156
|
+
* · 语句级的错:唯一键冲突、CHECK 被拒、缺表缺列、语法错、权限错、行 / 包超墙(1153 / 8025 / `54000`);
|
|
157
|
+
* · 死锁(引擎的仲裁产物,重试的前提在调用方的整事务重来,见 {@link isDeadlockError})与锁等待 / 语句超时(延迟信号,重试只把长等待乘上次数);
|
|
158
|
+
* · 认证类拒连(MySQL 1045、PG `28xxx`、`08004`:凭据 / 白名单要人去改)与协议违例(PG `08P01`、mysql2 `PROTOCOL_ERROR`:我方的码病);
|
|
159
|
+
* · TiDB 自己的忙 / 区域不可用(9001 / 9005 一族)—— 候真库逐码读数再登,今天不在表里(登记在案的未收项);
|
|
160
|
+
* · 池已被我方关闭(`Pool is closed.` / `Cannot use a pool after calling end on the pool`):那是本进程在收摊,不是库的事。
|
|
161
|
+
* 只看被抛出的**这一只**对象(不沿 `cause` 链往下找):店层把驱动错包进自己的类时,包它的那一层要不要继承连接类,归那一层的契约说,不在这里猜。
|
|
162
|
+
*/
|
|
163
|
+
export declare const CONNECTION_FAULT_ERRORS: ReadonlyArray<{
|
|
164
|
+
readonly code: string;
|
|
165
|
+
readonly errno?: number;
|
|
166
|
+
readonly why: string;
|
|
167
|
+
}>;
|
|
168
|
+
/**
|
|
169
|
+
* 驱动**自己抛的、不带 `code` 的裸 `Error`** 的整句 —— 连接类故障的第三种归因键。node-pg 的断连 / 池连接超时就是这一形(`new Error('Connection terminated unexpectedly')`,
|
|
170
|
+
* 没有任何结构化字段可读),mysql2 的「连接已关闭」同形。**整句相等**才认(不是 `includes`):一句夹着用户路径 / 数据的散文伪造不出驱动句;且只认 `code` 缺席的 `Error`。
|
|
171
|
+
* 每一行的 `source` 是它在驱动源码里的出处文件,`test/store-fault-judge.test.ts` 读已装驱动的源码核句子仍在(驱动升级改了句子 ⇒ 红,而不是判官悄悄静音)。
|
|
172
|
+
*/
|
|
173
|
+
export declare const CONNECTION_FAULT_DRIVER_SENTENCES: ReadonlyArray<{
|
|
174
|
+
readonly sentence: string;
|
|
175
|
+
readonly source: string;
|
|
176
|
+
readonly why: string;
|
|
177
|
+
}>;
|
|
178
|
+
/**
|
|
179
|
+
* 「这个错误**是**连接类故障吗」—— {@link CONNECTION_FAULT_ERRORS} 的唯一判据(识别集与排除项见该表头注)。纯谓词、不抛;认不出的一律不是。
|
|
180
|
+
* 归因键三种:`code`(字符串)、`errno`(数字)、无 `code` 的驱动裸 `Error` 的整句 —— 只读被抛出的这一只对象身上的结构化字段 / 整句,**不读散文里的在册词**。
|
|
181
|
+
*/
|
|
182
|
+
export declare function isConnectionFaultError(err: unknown): boolean;
|
|
144
183
|
/**
|
|
145
184
|
* 检查点店的两笔写(续跑输入 `putCtx` / 停驻行 `put`)被引擎以「一行装不下」拒 ⇒ **这一只**带码错误(码 `store.row_too_large`),不以裸驱动错上抛
|
|
146
185
|
* (其余店写今天仍是各自的驱动错 —— 未收,登记在案)。
|
|
@@ -64,7 +64,7 @@ export function rowTooLargeWallOf(dialect, err) {
|
|
|
64
64
|
export const MYSQL_CONNECTION_UNUSABLE_ERRORS = [
|
|
65
65
|
{ errno: 1053, code: "ER_SERVER_SHUTDOWN" },
|
|
66
66
|
{ errno: 1152, code: "ER_ABORTING_CONNECTION" },
|
|
67
|
-
{ errno: 1153, code: "ER_NET_PACKET_TOO_LARGE" },
|
|
67
|
+
{ errno: 1153, code: "ER_NET_PACKET_TOO_LARGE", statementRefusal: true },
|
|
68
68
|
{ errno: 1158, code: "ER_NET_READ_ERROR" },
|
|
69
69
|
{ errno: 1159, code: "ER_NET_READ_INTERRUPTED" },
|
|
70
70
|
{ errno: 1160, code: "ER_NET_ERROR_ON_WRITE" },
|
|
@@ -82,6 +82,54 @@ export function isConnectionUnusableError(err) {
|
|
|
82
82
|
const code = keyOf(err, "code");
|
|
83
83
|
return (typeof errno === "number" && UNUSABLE_ERRNOS.has(errno)) || (typeof code === "string" && UNUSABLE_CODES.has(code));
|
|
84
84
|
}
|
|
85
|
+
export const CONNECTION_FAULT_ERRORS = [
|
|
86
|
+
{ code: "ECONNRESET", why: "对端重置了连接(库重启 / 代理断流 / 空闲连接被回收)" },
|
|
87
|
+
{ code: "ECONNREFUSED", why: "库此刻不接连接(进程没起 / 端口没开 / 滚动重启中)" },
|
|
88
|
+
{ code: "ECONNABORTED", why: "连接在建立或使用中被本机协议栈中止" },
|
|
89
|
+
{ code: "EPIPE", why: "写一条对端已经关掉的连接" },
|
|
90
|
+
{ code: "ETIMEDOUT", why: "连接 / 套接字超时(mysql2 的 `connectTimeout` 同答此码)" },
|
|
91
|
+
{ code: "EHOSTUNREACH", why: "到库的主机不可达(路由 / 节点切换)" },
|
|
92
|
+
{ code: "ENETUNREACH", why: "到库的网络不可达" },
|
|
93
|
+
{ code: "EAI_AGAIN", why: "DNS 临时失败(「稍后再试」是它自己的定义)" },
|
|
94
|
+
{ code: "PROTOCOL_CONNECTION_LOST", why: "mysql2:服务端关了连接(`Connection lost: The server closed the connection.`,带 `fatal: true`)" },
|
|
95
|
+
{ code: "ER_CON_COUNT_ERROR", errno: 1040, why: "连接数满(`Too many connections`)—— 别人放连接之后就好" },
|
|
96
|
+
{ code: "08000", why: "PG connection_exception" },
|
|
97
|
+
{ code: "08001", why: "PG sqlclient_unable_to_establish_sqlconnection" },
|
|
98
|
+
{ code: "08003", why: "PG connection_does_not_exist" },
|
|
99
|
+
{ code: "08006", why: "PG connection_failure" },
|
|
100
|
+
{ code: "57P01", why: "PG admin_shutdown(库被管理员关停 / 重启)" },
|
|
101
|
+
{ code: "57P02", why: "PG crash_shutdown" },
|
|
102
|
+
{ code: "57P03", why: "PG cannot_connect_now(启动 / 恢复中)" },
|
|
103
|
+
{ code: "53300", why: "PG too_many_connections" },
|
|
104
|
+
{ code: "25006", why: "PG read_only_sql_transaction(故障切换后落在只读节点)" },
|
|
105
|
+
];
|
|
106
|
+
export const CONNECTION_FAULT_DRIVER_SENTENCES = [
|
|
107
|
+
{ sentence: "Connection terminated unexpectedly", source: "pg/lib/client.js", why: "node-pg:连接在使用中被对端关掉" },
|
|
108
|
+
{ sentence: "Client has encountered a connection error and is not queryable", source: "pg/lib/client.js", why: "node-pg:这条 client 的连接已出错,后续语句直接拒" },
|
|
109
|
+
{ sentence: "Connection terminated due to connection timeout", source: "pg-pool/index.js", why: "node-pg 池:建连超过 `connectionTimeoutMillis`" },
|
|
110
|
+
{ sentence: "timeout exceeded when trying to connect", source: "pg-pool/index.js", why: "node-pg 池:排队等连接超过 `connectionTimeoutMillis`" },
|
|
111
|
+
{ sentence: "Can't add new command when connection is in closed state", source: "mysql2/lib/base/connection.js", why: "mysql2:往一条已被对端关掉的连接上发语句(带 `fatal: true`、无 `code`)" },
|
|
112
|
+
];
|
|
113
|
+
const CONNECTION_FAULT_CODES = new Set([
|
|
114
|
+
...CONNECTION_FAULT_ERRORS.map((r) => r.code),
|
|
115
|
+
...MYSQL_CONNECTION_UNUSABLE_ERRORS.filter((r) => r.statementRefusal !== true).map((r) => r.code),
|
|
116
|
+
]);
|
|
117
|
+
const CONNECTION_FAULT_ERRNOS = new Set([
|
|
118
|
+
...CONNECTION_FAULT_ERRORS.flatMap((r) => (r.errno === undefined ? [] : [r.errno])),
|
|
119
|
+
...MYSQL_CONNECTION_UNUSABLE_ERRORS.filter((r) => r.statementRefusal !== true).map((r) => r.errno),
|
|
120
|
+
]);
|
|
121
|
+
const CONNECTION_FAULT_SENTENCES = new Set(CONNECTION_FAULT_DRIVER_SENTENCES.map((r) => r.sentence));
|
|
122
|
+
export function isConnectionFaultError(err) {
|
|
123
|
+
if (err === null || typeof err !== "object")
|
|
124
|
+
return false;
|
|
125
|
+
const code = keyOf(err, "code");
|
|
126
|
+
const errno = keyOf(err, "errno");
|
|
127
|
+
if (typeof code === "string" && CONNECTION_FAULT_CODES.has(code))
|
|
128
|
+
return true;
|
|
129
|
+
if (typeof errno === "number" && CONNECTION_FAULT_ERRNOS.has(errno))
|
|
130
|
+
return true;
|
|
131
|
+
return err instanceof Error && code === undefined && CONNECTION_FAULT_SENTENCES.has(err.message);
|
|
132
|
+
}
|
|
85
133
|
export class StoreRowTooLargeError extends Error {
|
|
86
134
|
row;
|
|
87
135
|
wall;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { isConnectionFaultError } from "./sql-errors.js";
|
|
2
|
+
import { StoreCorruptError } from "./sql-json-column.js";
|
|
3
|
+
export function storeFaultClassOf(err) {
|
|
4
|
+
if (err instanceof StoreCorruptError)
|
|
5
|
+
return "corrupt";
|
|
6
|
+
return isConnectionFaultError(err) ? "connection" : "unclassified";
|
|
7
|
+
}
|
|
8
|
+
//# sourceMappingURL=store-fault.js.map
|
|
@@ -86,7 +86,7 @@ export const SCHEMA_STATEMENTS = [
|
|
|
86
86
|
placed_at DATETIME(3) NULL,
|
|
87
87
|
-- S-790 / S-506(design/330 §2):会话化身 —— 同一个会话 id 上**这一位住客**的不透明令牌。铸点只在新建行(register /
|
|
88
88
|
-- 无 id 创建 / fork / 导入新行),删除随行消失,同 id 重登记铸新的;'' = 没铸过,读口响亮拒。列定义的唯一属主 =
|
|
89
|
-
-- src/session-incarnation.ts 的 SESSION_META_INCARNATION_COLUMN(
|
|
89
|
+
-- src/session-incarnation.ts 的 SESSION_META_INCARNATION_COLUMN(存量库缺这一列 ⇒ 起服列册对账拒启,拒句里的 ALTER 由本建表语句生成)。
|
|
90
90
|
-- 不上 wire、不进模型上下文、日志只打前 8 位。
|
|
91
91
|
${SESSION_META_INCARNATION_COLUMN.tidb},
|
|
92
92
|
PRIMARY KEY (session_id),
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
* 验证搜索配对不对。挂哪儿(boot 期一次性 warn / `/health` 子字段 / 显式端点)未定 ——
|
|
55
55
|
* 注意 boot 期探活会给每次启动加一次出网,多租/离线部署要能关。
|
|
56
56
|
*/
|
|
57
|
-
import { type UrlCredentialFacts } from "../url-credential.js";
|
|
57
|
+
import { type UrlCredentialFacts } from "../url-credential-refusal.js";
|
|
58
58
|
/**
|
|
59
59
|
* Search providers this backend speaks —— **词表的唯一属主**(运行期与类型同一份)。
|
|
60
60
|
* Absent `WEB_SEARCH_PROVIDER` ⇒ no backend ⇒ WebSearch is not assembled.
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { validateHeaderValue } from "node:http";
|
|
2
2
|
import { createSearxngSearchBackend } from "@sema-agent/core";
|
|
3
|
-
import { isHierarchicalUrl } from "@sema-agent/settings-schema";
|
|
3
|
+
import { findUrlCredential, findUrlUserinfo, isHierarchicalUrl } from "@sema-agent/settings-schema";
|
|
4
4
|
import { redactSecrets } from "../trace/redact.js";
|
|
5
|
-
import { buildRequestUrlCredentialRefusal,
|
|
5
|
+
import { buildRequestUrlCredentialRefusal, urlCredentialFacts } from "../url-credential-refusal.js";
|
|
6
6
|
export const WEB_SEARCH_PROVIDERS = ["brave", "tavily", "searxng"];
|
|
7
7
|
function isWebSearchProvider(word) {
|
|
8
8
|
return WEB_SEARCH_PROVIDERS.includes(word);
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* **请求体里「声明列表」(`mcpServers` · `a2aPeers`)这一类受理座的拒绝骨架** —— 列表级 / 条目级 / 键值级的拒形、拒句尾、撞帽机读位,在这里铸一处。
|
|
3
|
+
*
|
|
4
|
+
* ## 为什么在这里
|
|
5
|
+
* 两张列表是同一个问题的两个实例:调用方随请求带一份「我要挂这些外部服务」的声明,受理口要回答「这份声明能不能原样用」。答案的**形**一样 ——
|
|
6
|
+
* ① **列表**:在场却不是数组 / 超过挂载上限 / 某一条不是对象 ⇒ `400 request.field_invalid`(整份拒而不是挂前 N 条 / 摘掉读不出的那条:一份被悄悄截短的声明,在发送方那里读起来是「生效了」);
|
|
7
|
+
* 超上限用撞帽的唯一铸点({@link limitRefusal}),拒体带机读位一组键(`field` · `reason:"over_cap"` · `count` · `cap`);
|
|
8
|
+
* ② **键名**:条目层的词表外键 ⇒ `400 request.body_shape` + `unknownKeys`(句子由各座自己写 —— 受理键表与嵌套层是座位自己的事);
|
|
9
|
+
* ③ **键值**:每个受理键一只判官 ⇒ `400 request.field_invalid`,句子 `<坐标>.<键路径> (<subject> "<名>") is not a usable declaration — <判官原因句>. …`,不回显值;
|
|
10
|
+
* 判官答「URL 带凭据」时拒体另带机读位 `field` / `reason`({@link urlCredentialFacts})。
|
|
11
|
+
* 第一处坏形即答:先列表、后条目;条目里先键名、后键值。各座只写**自己的**两件事:受理键表 + 键值判官表(以及它自己的词表外键句)。拒形、句尾、撞帽句不在座位文件里手拼 ——
|
|
12
|
+
* 两份手拼的拒形迟早分岔(`a2aPeers` 上线时就是缺了这一整套,坏形换回 200 + 运维 warn,调用方以为声明生效了)。
|
|
13
|
+
*
|
|
14
|
+
* ## 射程
|
|
15
|
+
* 纯值域、与租户车道无关;只在 fresh 提交腿上问(`boot/resolve-spec.ts` 的 `opts.leg`):重放存量 body 的腿走宽容读器,一个 4xx 打在 resume 上会永久砖死一条 parked 任务。
|
|
16
|
+
*/
|
|
17
|
+
import { type UrlCredential } from "@sema-agent/settings-schema";
|
|
18
|
+
import { type LimitFacts } from "./limit-refusal.js";
|
|
19
|
+
import { type UrlCredentialFacts } from "./url-credential-refusal.js";
|
|
20
|
+
/** 一座声明列表的名字与上限、以及拒句里要用的称呼(座位的常量,铸点只读)。 */
|
|
21
|
+
export interface DeclarationSeat {
|
|
22
|
+
/** 请求体里这张列表的键,也是机读坐标的根(`mcpServers` | `a2aPeers`)。 */
|
|
23
|
+
readonly list: string;
|
|
24
|
+
/** 挂载上限(条数;超过 ⇒ 撞帽拒)。 */
|
|
25
|
+
readonly cap: number;
|
|
26
|
+
/** 一条声明所声明的东西(拒句里的单数名词:`server` | `peer`)。 */
|
|
27
|
+
readonly subject: string;
|
|
28
|
+
/** 「一条声明」的整称(`an MCP server declaration`)。 */
|
|
29
|
+
readonly declaration: string;
|
|
30
|
+
/** 「多条声明」的整称(`MCP server declarations`)。 */
|
|
31
|
+
readonly declarations: string;
|
|
32
|
+
}
|
|
33
|
+
/** 一次拒绝:码 + 句 + 词表外的键名表(只在闭集那一形上在场)+ 机读位(撞帽 / URL 带凭据两形上在场)。受理口(同步 400)与探测口(`POST …/probe`)原样转述同一份。 */
|
|
34
|
+
export interface DeclarationRefusal {
|
|
35
|
+
readonly errorCode: "request.body_shape" | "request.field_invalid";
|
|
36
|
+
readonly message: string;
|
|
37
|
+
/** 词表外的键,全路径形(已清洗 + 截长 + 排序 + 封顶)。 */
|
|
38
|
+
readonly unknownKeys?: readonly string[];
|
|
39
|
+
/** 撞帽:`field` · `reason:"over_cap"` · `count` · `cap`;URL 带凭据:`field` · `reason`(判官的拒因词)。 */
|
|
40
|
+
readonly facts?: UrlCredentialFacts | LimitFacts;
|
|
41
|
+
}
|
|
42
|
+
/** 一处键值坏形:键路径(相对条目)+ 判官的原因句;答「URL 带凭据」时另带判官的答案(⇒ 拒体带机读位)。 */
|
|
43
|
+
export interface DeclarationKeyIssue {
|
|
44
|
+
readonly path: string;
|
|
45
|
+
readonly reason: string;
|
|
46
|
+
readonly credential?: UrlCredential;
|
|
47
|
+
}
|
|
48
|
+
/** 受理键 → 判官。`at` 是这一条的体内坐标(`a2aPeers[2]`),座位需要写绝对坐标的出路句时用。缺席(`undefined`)恒合法 = 真「没声明」;在场的 `null` 不是缺席。 */
|
|
49
|
+
export type DeclarationKeyJudge<E extends Record<string, unknown> = Record<string, unknown>> = (entry: E, at: string) => DeclarationKeyIssue | undefined;
|
|
50
|
+
/** 读不出名字的条目用的占位(非空:一条连名字都读不出的条目仍然要被说出来)。 */
|
|
51
|
+
export declare const MALFORMED_NAME = "<malformed>";
|
|
52
|
+
/** 条目的**申报名**(拿不到名字时的占位)—— 拒句、丢弃点、通告同读一只。 */
|
|
53
|
+
export declare function declaredName(e: unknown): string;
|
|
54
|
+
/** `name` 键的判官(两座同读:申报名线长界与首尾空白的唯一判官在契约包 {@link isUsableWireName})。 */
|
|
55
|
+
export declare function declarationNameIssue(entry: Record<string, unknown>): DeclarationKeyIssue | undefined;
|
|
56
|
+
/** 列表级拒句的收尾句:说清「为什么整份被拒、而不是只按读得出的那部分办」与出路(空列表 = 什么都不声明)。 */
|
|
57
|
+
export declare const LIST_REFUSAL_TAIL: string;
|
|
58
|
+
/** 列表级的一处坏形 → 拒(码恒 `request.field_invalid`)。`head` 只说位置与形的**种类**,不回显值。 */
|
|
59
|
+
export declare function declarationListRefusal(head: string): DeclarationRefusal;
|
|
60
|
+
/**
|
|
61
|
+
* 一处键值坏形**怎么被说出来**:体内坐标(`<条目坐标>.<键路径>`)+ 判官的原因句(只剥控制与双向字符,不回显值)。受理口的拒句与读器的丢弃因(请求腿运维 warn 的 `field` / `reason`)读**这一只** ——
|
|
62
|
+
* 原因只来自判官表,组句只在这里(两边各写一份,迟早分岔)。
|
|
63
|
+
*/
|
|
64
|
+
export declare function declarationIssueFacts(at: string, issue: DeclarationKeyIssue): {
|
|
65
|
+
readonly field: string;
|
|
66
|
+
readonly reason: string;
|
|
67
|
+
};
|
|
68
|
+
/**
|
|
69
|
+
* 列表条数撞帽**怎么被说出来**:坐标 = 列表键(`a2aPeers`)+ 撞帽铸点({@link limitRefusal})的头一句(`<列表> has N entries — at most M are accepted`)。受理口的撞帽拒句以同一句开头(接拒句尾),
|
|
70
|
+
* 读器的丢弃因读这一只 —— 与 {@link declarationIssueFacts} 同族:`reason` 只有一种内容 = 句子;机读词(`over_cap`)留在拒体的 `facts`,不进日志的 `reason`。
|
|
71
|
+
*/
|
|
72
|
+
export declare function declarationOverCapFacts(seat: DeclarationSeat, count: number): {
|
|
73
|
+
readonly field: string;
|
|
74
|
+
readonly reason: string;
|
|
75
|
+
};
|
|
76
|
+
/**
|
|
77
|
+
* 一条条目不是对象(`null` / 原始值 / 数组)的头一句:`<条目坐标> is <值的种类>, not <整称> (an object)`;是对象 ⇒ `undefined`。列表级拒({@link requestDeclarationRefusal})与读器的丢弃因读同一只,
|
|
78
|
+
* 「什么算对象」只在这里判。
|
|
79
|
+
*/
|
|
80
|
+
export declare function declarationEntryNotObject(seat: DeclarationSeat, entry: unknown, at: string): string | undefined;
|
|
81
|
+
/** 一处键值坏形 → 拒。**所有键同一个形**;条目名过唯一的回显清洗,读不出名字的条目用占位,坐标与原因句由 {@link declarationIssueFacts} 铸。 */
|
|
82
|
+
export declare function declarationFieldRefusal(seat: DeclarationSeat, entry: Record<string, unknown>, at: string, issue: DeclarationKeyIssue): DeclarationRefusal;
|
|
83
|
+
/** 一条条目的键值判官表按受理键序问一遍,第一处坏形即返回。 */
|
|
84
|
+
export declare function judgeDeclarationKeys<K extends string, E extends Record<string, unknown>>(seat: DeclarationSeat, entry: E, at: string, keys: readonly K[], judges: {
|
|
85
|
+
readonly [P in K]: DeclarationKeyJudge<E>;
|
|
86
|
+
}): DeclarationRefusal | undefined;
|
|
87
|
+
/**
|
|
88
|
+
* 一张声明列表「能不能原样用」的回答者骨架:列表级(缺席 / `null` ⇒ 没声明;不是数组 / 超上限 / 条目不是对象)在这里,条目级交座位的 `judgeEntry`(词表外的键 → 键值)。
|
|
89
|
+
* `undefined` = 能原样用(或没声明;各门自己定缺席的意思)。
|
|
90
|
+
*/
|
|
91
|
+
export declare function requestDeclarationRefusal(seat: DeclarationSeat, list: unknown, judgeEntry: (entry: Record<string, unknown>, at: string) => DeclarationRefusal | undefined): DeclarationRefusal | undefined;
|
|
92
|
+
/** 受理口的转述:有答案 ⇒ 同步 400(码 / 句 / `unknownKeys` / 机读位原样);没有 ⇒ 什么也不做。 */
|
|
93
|
+
export declare function throwDeclarationRefusal(refusal: DeclarationRefusal | undefined): void;
|
|
94
|
+
//# sourceMappingURL=request-declaration-refusal.d.ts.map
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { describeBadWireName, describeValueShape, isUsableWireName, stripControlAndBidi } from "@sema-agent/settings-schema";
|
|
2
|
+
import { createCodedHttpError } from "./security.js";
|
|
3
|
+
import { limitRefusal } from "./limit-refusal.js";
|
|
4
|
+
import { echoKeyName } from "./request-key-closure.js";
|
|
5
|
+
import { urlCredentialFacts } from "./url-credential-refusal.js";
|
|
6
|
+
export const MALFORMED_NAME = "<malformed>";
|
|
7
|
+
export function declaredName(e) {
|
|
8
|
+
return e && typeof e === "object" && typeof e.name === "string" && e.name !== ""
|
|
9
|
+
? String(e.name)
|
|
10
|
+
: MALFORMED_NAME;
|
|
11
|
+
}
|
|
12
|
+
export function declarationNameIssue(entry) {
|
|
13
|
+
return isUsableWireName(entry.name) ? undefined : { path: "name", reason: `must be a non-empty string within the wire-name length bound — got ${describeBadWireName(entry.name)}` };
|
|
14
|
+
}
|
|
15
|
+
export const LIST_REFUSAL_TAIL = "The request is refused rather than acting on part of the list: declarations that silently did not apply would read as in effect to whoever sent them. " +
|
|
16
|
+
"Fix the list (an empty list declares none).";
|
|
17
|
+
export function declarationListRefusal(head) {
|
|
18
|
+
return { errorCode: "request.field_invalid", message: `${head}. ${LIST_REFUSAL_TAIL}` };
|
|
19
|
+
}
|
|
20
|
+
function entryRefusalTail(subject) {
|
|
21
|
+
return (`The request is refused rather than mounting the ${subject} without it: a declaration that silently did not apply would leave the ${subject} running differently from what you declared. ` +
|
|
22
|
+
"Fix the entry (an optional key may also be omitted).");
|
|
23
|
+
}
|
|
24
|
+
export function declarationIssueFacts(at, issue) {
|
|
25
|
+
return { field: `${at}.${issue.path}`, reason: stripControlAndBidi(issue.reason) };
|
|
26
|
+
}
|
|
27
|
+
export function declarationOverCapFacts(seat, count) {
|
|
28
|
+
const capped = limitRefusal(seat.list, "entries", count, seat.cap);
|
|
29
|
+
return { field: capped.facts.field, reason: capped.message };
|
|
30
|
+
}
|
|
31
|
+
export function declarationEntryNotObject(seat, entry, at) {
|
|
32
|
+
return entry === null || typeof entry !== "object" || Array.isArray(entry) ? `${at} is ${describeValueShape(entry)}, not ${seat.declaration} (an object)` : undefined;
|
|
33
|
+
}
|
|
34
|
+
export function declarationFieldRefusal(seat, entry, at, issue) {
|
|
35
|
+
const name = echoKeyName("", declaredName(entry));
|
|
36
|
+
const { field, reason } = declarationIssueFacts(at, issue);
|
|
37
|
+
return {
|
|
38
|
+
errorCode: "request.field_invalid",
|
|
39
|
+
message: `${field} (${seat.subject} "${name}") is not a usable declaration — ${reason}. ${entryRefusalTail(seat.subject)}`,
|
|
40
|
+
...(issue.credential === undefined ? {} : { facts: urlCredentialFacts(`${at}.${issue.path}`, issue.credential) }),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
export function judgeDeclarationKeys(seat, entry, at, keys, judges) {
|
|
44
|
+
for (const key of keys) {
|
|
45
|
+
const issue = judges[key](entry, at);
|
|
46
|
+
if (issue !== undefined)
|
|
47
|
+
return declarationFieldRefusal(seat, entry, at, issue);
|
|
48
|
+
}
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
export function requestDeclarationRefusal(seat, list, judgeEntry) {
|
|
52
|
+
if (list === undefined || list === null)
|
|
53
|
+
return undefined;
|
|
54
|
+
if (!Array.isArray(list))
|
|
55
|
+
return declarationListRefusal(`${seat.list} is ${describeValueShape(list)}, not an array of ${seat.declarations}`);
|
|
56
|
+
if (list.length > seat.cap) {
|
|
57
|
+
const capped = limitRefusal(seat.list, "entries", list.length, seat.cap, { tail: LIST_REFUSAL_TAIL });
|
|
58
|
+
return { errorCode: "request.field_invalid", message: capped.message, facts: capped.facts };
|
|
59
|
+
}
|
|
60
|
+
for (let i = 0; i < list.length; i++) {
|
|
61
|
+
const entry = list[i];
|
|
62
|
+
const at = `${seat.list}[${i}]`;
|
|
63
|
+
const notObject = declarationEntryNotObject(seat, entry, at);
|
|
64
|
+
if (notObject !== undefined)
|
|
65
|
+
return declarationListRefusal(notObject);
|
|
66
|
+
const refusal = judgeEntry(entry, at);
|
|
67
|
+
if (refusal !== undefined)
|
|
68
|
+
return refusal;
|
|
69
|
+
}
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
export function throwDeclarationRefusal(refusal) {
|
|
73
|
+
if (refusal === undefined)
|
|
74
|
+
return;
|
|
75
|
+
const extra = { ...(refusal.unknownKeys === undefined ? {} : { unknownKeys: [...refusal.unknownKeys] }), ...(refusal.facts ?? {}) };
|
|
76
|
+
throw createCodedHttpError(400, refusal.errorCode, refusal.message, Object.keys(extra).length === 0 ? undefined : extra);
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=request-declaration-refusal.js.map
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type UrlCredentialRefusal } from "./url-credential-refusal.js";
|
|
2
|
+
/** 请求体 `images` 的 URL 座:任一条目的串值 `url` 带凭据 ⇒ 拒(整句 + 机读位),列表序第一处即答;没有 ⇒ `undefined`。`images` 不是数组 / 缺席 ⇒ `undefined`(形归受理口别的门)。 */
|
|
3
|
+
export declare function requestImageUrlRefusal(images: unknown): UrlCredentialRefusal | undefined;
|
|
4
|
+
/** fresh 提交腿的 `images` URL 门:{@link requestImageUrlRefusal} 有答案 ⇒ 同步 400(码 / 句 / 机读位原样)。 */
|
|
5
|
+
export declare function assertRequestImageUrls(bodyImages: unknown): void;
|
|
6
|
+
//# sourceMappingURL=request-image-urls.d.ts.map
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { createCodedHttpError } from "./security.js";
|
|
2
|
+
import { findUrlCredential } from "@sema-agent/settings-schema";
|
|
3
|
+
import { buildRequestUrlCredentialRefusal } from "./url-credential-refusal.js";
|
|
4
|
+
function inlineImageExit(at) {
|
|
5
|
+
return `${at} can carry the image bytes inline: send { "data": <base64>, "mimeType": "image/png" | "image/jpeg" | "image/gif" | "image/webp" } instead of a URL that holds the credential`;
|
|
6
|
+
}
|
|
7
|
+
export function requestImageUrlRefusal(images) {
|
|
8
|
+
if (!Array.isArray(images))
|
|
9
|
+
return undefined;
|
|
10
|
+
for (const [i, image] of images.entries()) {
|
|
11
|
+
if (image === null || typeof image !== "object")
|
|
12
|
+
continue;
|
|
13
|
+
const url = image.url;
|
|
14
|
+
const found = typeof url === "string" ? findUrlCredential(url) : undefined;
|
|
15
|
+
if (found === undefined)
|
|
16
|
+
continue;
|
|
17
|
+
const at = `images[${i}]`;
|
|
18
|
+
return buildRequestUrlCredentialRefusal(`${at}.url`, found, inlineImageExit(at));
|
|
19
|
+
}
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
export function assertRequestImageUrls(bodyImages) {
|
|
23
|
+
const refusal = requestImageUrlRefusal(bodyImages);
|
|
24
|
+
if (refusal !== undefined)
|
|
25
|
+
throw createCodedHttpError(400, "request.field_invalid", refusal.message, refusal.facts);
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=request-image-urls.js.map
|
|
@@ -17,6 +17,11 @@ export declare function scanKeyClosure(bag: object, prefix: string, accepted: Re
|
|
|
17
17
|
unknown: string[];
|
|
18
18
|
unsupported: string[];
|
|
19
19
|
}): void;
|
|
20
|
+
/** {@link scanKeyClosure} 的名字形:键名已经由调用方读成一张表(请求入口的快照只带词表外的键**名**,值不读),分拣规则同一条。 */
|
|
21
|
+
export declare function scanKeyNames(names: readonly string[], prefix: string, accepted: ReadonlySet<string>, tolerated: ReadonlySet<string>, into: {
|
|
22
|
+
unknown: string[];
|
|
23
|
+
unsupported: string[];
|
|
24
|
+
}): void;
|
|
20
25
|
/** 一张分类表的文案片段(封顶后只报数)。 */
|
|
21
26
|
export declare function keyClosureClause(names: string[], label: string): string;
|
|
22
27
|
/**
|
|
@@ -5,7 +5,10 @@ export function echoKeyName(prefix, key) {
|
|
|
5
5
|
return `${prefix}${echoCallerText(key)}`;
|
|
6
6
|
}
|
|
7
7
|
export function scanKeyClosure(bag, prefix, accepted, tolerated, into) {
|
|
8
|
-
|
|
8
|
+
scanKeyNames(Object.keys(bag), prefix, accepted, tolerated, into);
|
|
9
|
+
}
|
|
10
|
+
export function scanKeyNames(names, prefix, accepted, tolerated, into) {
|
|
11
|
+
for (const k of names) {
|
|
9
12
|
if (accepted.has(k))
|
|
10
13
|
continue;
|
|
11
14
|
(tolerated.has(k) ? into.unsupported : into.unknown).push(echoKeyName(prefix, k));
|
|
@@ -95,7 +95,7 @@ export function createPrincipalEntitlementsClient(opts) {
|
|
|
95
95
|
center: { baseUrl: opts.center.baseUrl, credential },
|
|
96
96
|
cache: new Map(),
|
|
97
97
|
inflight: new Map(),
|
|
98
|
-
}));
|
|
98
|
+
})).now;
|
|
99
99
|
const readMintedDeny = new WeakSet();
|
|
100
100
|
const MAX_CACHE = 10_000;
|
|
101
101
|
const putCache = (cache, key, entry) => {
|
|
@@ -1,4 +1,31 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* RFC B5/B4 抽象点② (sema-internal server/docs/design/ENV-SELECT-AND-REGION-SOURCE.md): the deployment-region package-source →
|
|
3
|
+
* sandbox env derivation. ENV-FIRST: pod/sandbox env OVERRIDES the image's baked `ENV` (k8s container env wins
|
|
4
|
+
* over image config and is visible to every exec'd process; e2b sandboxEnv likewise) — so the env-overridable
|
|
5
|
+
* ecosystem pieces (pip/uv/npm/corepack/go/rustup/flutter/playwright/nuget) switch region on EXISTING images,
|
|
6
|
+
* no rebake needed. Only the file-bound pieces (apt sources, /etc/gemrc, composer global config, cargo
|
|
7
|
+
* config.toml) still need the in-image `pkg-source.sh` hook (rebaked images; bake prep chowns those files).
|
|
8
|
+
*
|
|
9
|
+
* Profiles:
|
|
10
|
+
* cn → inject only the SEMA_PKG_SOURCE marker (the pool's images bake the CN set as their defaults).
|
|
11
|
+
* global → official endpoints for every env-overridable ecosystem (overrides the baked CN ENV values).
|
|
12
|
+
* custom → SEMA_* explicit URLs (from the service process env) forwarded verbatim + mapped onto the same
|
|
13
|
+
* ecosystem variables where a mapping is unambiguous (apt has no env form — hook-only).
|
|
14
|
+
*
|
|
15
|
+
* The marker `SEMA_PKG_SOURCE` always rides along: the image's pkg-source.sh hook (pod-start / profile.d)
|
|
16
|
+
* materializes the file-bound pieces from it; images without the hook simply ignore it.
|
|
17
|
+
*
|
|
18
|
+
* ## One law: no URL that rides into the model-visible sandbox env carries a credential
|
|
19
|
+
* Everything this module derives lands in the env of every sandbox process — including the ones the model drives
|
|
20
|
+
* (`env`, `printenv`) — so `https://user:pass@mirror/…` or `https://mirror/simple?token=…` among the `custom` addresses would
|
|
21
|
+
* hand the deployment's package-source credential to the model. {@link customPkgSourceFromEnv} is the ONE reader of the six
|
|
22
|
+
* addresses and the one place that judges them, in two questions: the shape (the shared URL-shape judge `httpUrlFlaws`, the same
|
|
23
|
+
* one MEMORY_EMBEDDER_ENDPOINT / A2A_SERVE_URL answer to, plus this seat's backslash rule) and the credential (the shared
|
|
24
|
+
* credential judge `findUrlCredential` — userinfo, or a query / fragment parameter named in the log redactor's credential-name
|
|
25
|
+
* table; the request-face MCP `transport.url` answers to the same judge). Either says no ⇒ it throws, startup refuses. Never
|
|
26
|
+
* stripped, never rewritten, never forwarded quietly. The request face has no package-source address key at all (deployment face only).
|
|
27
|
+
*/
|
|
28
|
+
import { type UrlCredential } from "@sema-agent/settings-schema";
|
|
2
29
|
/** Explicit URLs for `custom` (RFC B: 极端情况出口). Read from the service process env (operator-trusted). */
|
|
3
30
|
export interface CustomPkgSource {
|
|
4
31
|
aptMirror?: string;
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { httpUrlFlaws } from "@sema-agent/settings-schema";
|
|
1
|
+
import { findUrlCredential, httpUrlFlaws } from "@sema-agent/settings-schema";
|
|
2
2
|
import { buildHttpUrlRefusal, buildUrlCredentialRefusal } from "./url-refusal.js";
|
|
3
|
-
import { findUrlCredential } from "./url-credential.js";
|
|
4
3
|
const GLOBAL_ENV = {
|
|
5
4
|
PIP_INDEX_URL: "https://pypi.org/simple",
|
|
6
5
|
UV_DEFAULT_INDEX: "https://pypi.org/simple",
|
package/dist/sealed-key.d.ts
CHANGED
|
@@ -54,6 +54,25 @@ export declare function projectSealedKeyCapability(face: SealedKeyPublicFace | u
|
|
|
54
54
|
/** Default id = fingerprint of the RAW public-key bytes: `exec-<sha256(pk)[0..12) hex>`.
|
|
55
55
|
* Deterministic → regenerating the id from the same key file is stable across boots. */
|
|
56
56
|
export declare function deriveKeyId(publicKeyRaw: Uint8Array): string;
|
|
57
|
+
/** {@link judgeSealedKeyPublicKey} 的拒因(闭集)。 */
|
|
58
|
+
export type SealedKeyPublicKeyFlaw = "non_canonical_base64" | "bad_length" | "small_order";
|
|
59
|
+
/** 公钥判官的拒(类型化;`flaw` 闭集)。公钥是公开材料,句子里点名它无妨;从不带私钥 / 密文。 */
|
|
60
|
+
export declare class SealedKeyPublicKeyRefusedError extends Error {
|
|
61
|
+
readonly flaw: SealedKeyPublicKeyFlaw;
|
|
62
|
+
constructor(flaw: SealedKeyPublicKeyFlaw);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* **X25519 公钥的唯一判官** —— 保管店装载({@link ensureSealedKeyStore},每份钥匙文件)、向中心登记({@link reportExecutionPublicKey})、
|
|
66
|
+
* 封之前({@link createCheckpointCtxSealing},对着最新那把)三处同读这一只;过了交回原始 32 字节,不过抛 {@link SealedKeyPublicKeyRefusedError}。
|
|
67
|
+
* 1. **规范 base64**:解出来的字节再编码回去必须逐字等于原串 —— 一句话盖住字母表、补齐、填充位三件(一把钥匙只有一种写法);
|
|
68
|
+
* 2. **32 字节**;
|
|
69
|
+
* 3. **不是小阶点**:对 {@link SMALL_ORDER_PROBE_SCALAR} 做 `crypto_scalarmult`,结果全零 ⇔ 小阶(libsodium 此时不交结果、抛 `weak public key`)。
|
|
70
|
+
* 判法取「标量乘结果全零」而不是手抄小阶点表:与 noble 同一判(`x25519.getSharedSecret` 零结果即抛,包侧 `bad_public_key` 读的就是它)、
|
|
71
|
+
* 就是 RFC 7748 §6.1 要求的那一检;非规范编码(p、p+1)与第 255 位置位的写法由 X25519 自己的约化 / 掩位盖住,不必逐个列;
|
|
72
|
+
* 高位别名(第 255 位置位、掩掉后是同一个非小阶点)照收 —— 两边同口径。依赖已在树里(libsodium),不新增一张要人维护的表。
|
|
73
|
+
* 前置:libsodium 已就绪(三处调用方都先等过它);没就绪 ⇒ 当场抛普通错误(不是判决)。
|
|
74
|
+
*/
|
|
75
|
+
export declare function judgeSealedKeyPublicKey(publicKeyB64: string): Uint8Array;
|
|
57
76
|
/**
|
|
58
77
|
* Load every keypair under `dir` (creating dir + ONE fresh keypair when none exist) and return the
|
|
59
78
|
* custody store. Invalid/corrupt files are skipped with a warn (they may be an OLD format or a
|
package/dist/sealed-key.js
CHANGED
|
@@ -27,6 +27,37 @@ function b64ToBytes(b64, what) {
|
|
|
27
27
|
throw new Error(`${what}: not valid base64`);
|
|
28
28
|
return new Uint8Array(Buffer.from(b64, "base64"));
|
|
29
29
|
}
|
|
30
|
+
export class SealedKeyPublicKeyRefusedError extends Error {
|
|
31
|
+
flaw;
|
|
32
|
+
constructor(flaw) {
|
|
33
|
+
super(`sealed-key public key refused (${flaw}): ${SEALED_KEY_PUBLIC_KEY_FLAW_TEXT[flaw]}`);
|
|
34
|
+
this.name = "SealedKeyPublicKeyRefusedError";
|
|
35
|
+
this.flaw = flaw;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const SEALED_KEY_PUBLIC_KEY_FLAW_TEXT = {
|
|
39
|
+
non_canonical_base64: "not the canonical standard-base64 spelling of 32 bytes (alphabet A-Z a-z 0-9 + /, `=` padding, zero padding bits)",
|
|
40
|
+
bad_length: "does not decode to 32 bytes",
|
|
41
|
+
small_order: "an X25519 small-order point — anything sealed against it can be opened without the private key",
|
|
42
|
+
};
|
|
43
|
+
const SMALL_ORDER_PROBE_SCALAR = new Uint8Array(32).fill(7);
|
|
44
|
+
export function judgeSealedKeyPublicKey(publicKeyB64) {
|
|
45
|
+
const sodium = _sodium;
|
|
46
|
+
if (typeof sodium.crypto_scalarmult !== "function")
|
|
47
|
+
throw new Error("judgeSealedKeyPublicKey: libsodium is not initialized — await its ready promise first");
|
|
48
|
+
const bytes = new Uint8Array(Buffer.from(publicKeyB64, "base64"));
|
|
49
|
+
if (Buffer.from(bytes).toString("base64") !== publicKeyB64)
|
|
50
|
+
throw new SealedKeyPublicKeyRefusedError("non_canonical_base64");
|
|
51
|
+
if (bytes.length !== sodium.crypto_box_PUBLICKEYBYTES)
|
|
52
|
+
throw new SealedKeyPublicKeyRefusedError("bad_length");
|
|
53
|
+
try {
|
|
54
|
+
sodium.crypto_scalarmult(SMALL_ORDER_PROBE_SCALAR, bytes);
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
throw new SealedKeyPublicKeyRefusedError("small_order");
|
|
58
|
+
}
|
|
59
|
+
return bytes;
|
|
60
|
+
}
|
|
30
61
|
export async function ensureSealedKeyStore(dir, logger) {
|
|
31
62
|
await _sodium.ready;
|
|
32
63
|
const sodium = _sodium;
|
|
@@ -40,9 +71,9 @@ export async function ensureSealedKeyStore(dir, logger) {
|
|
|
40
71
|
if (raw.alg !== SEALED_KEY_ALG || typeof raw.publicKey !== "string" || typeof raw.privateKey !== "string" || typeof raw.publicKeyId !== "string") {
|
|
41
72
|
throw new Error("missing/unsupported fields");
|
|
42
73
|
}
|
|
43
|
-
const publicKey =
|
|
74
|
+
const publicKey = judgeSealedKeyPublicKey(raw.publicKey);
|
|
44
75
|
const privateKey = b64ToBytes(raw.privateKey, "privateKey");
|
|
45
|
-
if (
|
|
76
|
+
if (privateKey.length !== sodium.crypto_box_SECRETKEYBYTES)
|
|
46
77
|
throw new Error("bad key length");
|
|
47
78
|
if (name !== `${raw.publicKeyId}.json`)
|
|
48
79
|
logger?.warn("sealed_key_filename_drift", { file: name, publicKeyId: raw.publicKeyId, note: "content publicKeyId wins; rename the file to silence this" });
|
|
@@ -53,7 +84,7 @@ export async function ensureSealedKeyStore(dir, logger) {
|
|
|
53
84
|
keys.set(raw.publicKeyId, { publicKey, privateKey, publicKeyB64: raw.publicKey, createdAt: typeof raw.createdAt === "string" ? raw.createdAt : "" });
|
|
54
85
|
}
|
|
55
86
|
catch (err) {
|
|
56
|
-
logger?.warn("sealed_key_file_unreadable", { file: path, errKind: err instanceof Error ? err.name : "unknown", note: "skipped (NOT deleted) — ciphertexts sealed for this key will fail loud per-model" });
|
|
87
|
+
logger?.warn("sealed_key_file_unreadable", { file: path, errKind: err instanceof Error ? err.name : "unknown", ...(err instanceof SealedKeyPublicKeyRefusedError ? { flaw: err.flaw } : {}), note: "skipped (NOT deleted) — ciphertexts sealed for this key will fail loud per-model" });
|
|
57
88
|
}
|
|
58
89
|
}
|
|
59
90
|
if (keys.size === 0) {
|
|
@@ -101,7 +132,7 @@ export function createCheckpointCtxSealing(store) {
|
|
|
101
132
|
if (typeof sodium.crypto_box_seal !== "function")
|
|
102
133
|
throw new Error("createCheckpointCtxSealing: libsodium is not initialized — build the store with ensureSealedKeyStore() first");
|
|
103
134
|
const { publicKeyId } = store.latest;
|
|
104
|
-
const publicKey =
|
|
135
|
+
const publicKey = judgeSealedKeyPublicKey(store.latest.publicKey);
|
|
105
136
|
const sealing = createUncheckedCheckpointCtxSealing(store, publicKeyId, publicKey);
|
|
106
137
|
const probeSession = "sealed-key-self-test";
|
|
107
138
|
let roundTrip;
|
|
@@ -144,6 +175,18 @@ function createUncheckedCheckpointCtxSealing(store, publicKeyId, publicKey) {
|
|
|
144
175
|
}
|
|
145
176
|
export async function reportExecutionPublicKey(opts) {
|
|
146
177
|
const { baseUrl, adminToken, key, notes, logger } = opts;
|
|
178
|
+
await _sodium.ready;
|
|
179
|
+
try {
|
|
180
|
+
judgeSealedKeyPublicKey(key.publicKey);
|
|
181
|
+
}
|
|
182
|
+
catch (err) {
|
|
183
|
+
logger?.warn("sealed_key_public_key_refused", {
|
|
184
|
+
publicKeyId: key.publicKeyId,
|
|
185
|
+
...(err instanceof SealedKeyPublicKeyRefusedError ? { flaw: err.flaw } : { errKind: err instanceof Error ? err.name : "unknown" }),
|
|
186
|
+
note: "this public key is not registered with the center and not printed for manual registration — fix or remove the key file in the sealed-key directory and restart",
|
|
187
|
+
});
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
147
190
|
const manual = (why) => {
|
|
148
191
|
logger?.warn("sealed_key_public_key", {
|
|
149
192
|
publicKeyId: key.publicKeyId,
|