@sema-agent/server 7.124.0 → 7.125.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 +138 -0
- package/USAGE.md +46 -8
- package/dist/audit.d.ts +27 -5
- package/dist/audit.js +42 -8
- package/dist/boot/execution-env.d.ts +33 -1
- package/dist/boot/execution-env.js +31 -2
- package/dist/boot/resolve-spec.js +4 -3
- package/dist/boot/session-faces.d.ts +14 -1
- package/dist/boot/session-faces.js +6 -5
- package/dist/boot/stage-02-web-admin-pull.js +5 -1
- package/dist/boot/stage-05-execution-env.js +6 -2
- package/dist/boot/stage-09-leader.d.ts +1 -1
- package/dist/boot/stage-10-http-server.d.ts +1 -1
- package/dist/boot/web-admin.js +130 -140
- package/dist/capabilities/center-wiring.d.ts +2 -1
- package/dist/capabilities/composite-members.d.ts +1 -0
- package/dist/capabilities/composite-members.js +1 -0
- package/dist/capability-facts.d.ts +28 -3
- package/dist/capability-facts.js +20 -0
- package/dist/config-catalog.js +4 -1
- package/dist/config-d-migration.js +2 -1
- package/dist/config-lkg.d.ts +4 -1
- package/dist/config-lkg.js +16 -12
- package/dist/config-provider.d.ts +4 -1
- package/dist/config-provider.js +1 -0
- package/dist/config-types.d.ts +10 -3
- package/dist/config.js +44 -14
- package/dist/env-facts.d.ts +2 -2
- package/dist/env-facts.js +3 -2
- package/dist/fleet/fleet-bus.d.ts +24 -12
- package/dist/fleet/fleet-bus.js +4 -1
- package/dist/fleet/workspace-snapshot-residual.d.ts +10 -0
- package/dist/fleet/workspace-snapshot-residual.js +35 -0
- package/dist/hooks/hook-runner.d.ts +15 -27
- package/dist/hooks/hook-runner.js +19 -59
- package/dist/http/active-run-conflict.d.ts +3 -0
- package/dist/http/active-run-conflict.js +3 -0
- package/dist/http/admission.js +7 -7
- package/dist/http/approval-sweeps.js +2 -4
- package/dist/http/device-placement.js +1 -0
- package/dist/http/dispatch.js +1 -1
- package/dist/http/durable-run-leg.d.ts +20 -6
- package/dist/http/durable-run-leg.js +4 -4
- package/dist/http/idempotency.d.ts +83 -38
- package/dist/http/idempotency.js +38 -13
- package/dist/http/request-guards.d.ts +18 -0
- package/dist/http/request-guards.js +12 -3
- package/dist/http/resume-legs.js +7 -1
- package/dist/http/route-ctx.d.ts +3 -3
- package/dist/http/route-ctx.js +1 -1
- package/dist/http/routes/a2a-serve.js +27 -11
- package/dist/http/routes/capabilities.js +1 -0
- package/dist/http/routes/fleet.js +2 -0
- package/dist/http/routes/images.js +2 -2
- package/dist/http/routes/leader.d.ts +4 -0
- package/dist/http/routes/leader.js +13 -3
- package/dist/http/routes/observability.js +2 -0
- package/dist/http/routes/runs.d.ts +6 -2
- package/dist/http/routes/runs.js +71 -101
- package/dist/http/routes/session-sync.d.ts +3 -0
- package/dist/http/routes/session-sync.js +30 -1
- package/dist/http/routes/sessions-list.d.ts +11 -0
- package/dist/http/routes/sessions-list.js +6 -6
- package/dist/http/routes/sessions.d.ts +6 -1
- package/dist/http/routes/sessions.js +41 -10
- package/dist/http/routes/tasks.js +63 -21
- package/dist/http/run-read-face.d.ts +26 -0
- package/dist/http/run-read-face.js +67 -0
- package/dist/http/server.d.ts +4 -4
- package/dist/http/server.js +1 -2
- package/dist/http/submit-replay.d.ts +40 -0
- package/dist/http/submit-replay.js +22 -0
- package/dist/leader/endpoint.d.ts +10 -5
- package/dist/leader/endpoint.js +27 -5
- package/dist/observability/cost-quota.d.ts +51 -7
- package/dist/observability/cost-quota.js +35 -17
- package/dist/observability/fail-open.d.ts +1 -1
- package/dist/observability/fail-open.js +1 -1
- package/dist/observability/run-terminal.d.ts +5 -9
- package/dist/observability/run-terminal.js +2 -7
- package/dist/plugins/approval-ask-store-sql.js +1 -1
- package/dist/plugins/caching-session-store.d.ts +7 -2
- package/dist/plugins/caching-session-store.js +6 -0
- package/dist/plugins/device-store-sql.js +1 -1
- package/dist/plugins/ensure-index.js +22 -0
- package/dist/plugins/file-run-store.d.ts +24 -14
- package/dist/plugins/file-run-store.js +52 -12
- package/dist/plugins/leader-run-store-sql.d.ts +1 -11
- package/dist/plugins/leader-run-store-sql.js +1 -5
- package/dist/plugins/local-session-store.d.ts +35 -6
- package/dist/plugins/local-session-store.js +87 -18
- package/dist/plugins/memory-run-store.d.ts +13 -12
- package/dist/plugins/memory-run-store.js +25 -0
- package/dist/plugins/one-time-migrations.js +1 -1
- package/dist/plugins/pg-cost-quota.d.ts +1 -1
- package/dist/plugins/pg-cost-quota.js +2 -5
- package/dist/plugins/pg-pool.js +3 -0
- package/dist/plugins/pg-session-storage.d.ts +11 -1
- package/dist/plugins/pg-session-storage.js +44 -8
- package/dist/plugins/remote-env-k8s.d.ts +127 -12
- package/dist/plugins/remote-env-k8s.js +270 -27
- package/dist/plugins/run-store-sql.d.ts +26 -12
- package/dist/plugins/run-store-sql.js +50 -25
- package/dist/plugins/schema-consistency.d.ts +1 -13
- package/dist/plugins/schema-consistency.js +1 -41
- package/dist/plugins/schema-indexes.js +1 -0
- package/dist/plugins/schema-live-columns.d.ts +20 -0
- package/dist/plugins/schema-live-columns.js +42 -0
- package/dist/plugins/store-contracts.d.ts +40 -2
- package/dist/plugins/tidb-cost-quota.d.ts +1 -1
- package/dist/plugins/tidb-cost-quota.js +2 -5
- package/dist/plugins/tidb-pool.js +14 -3
- package/dist/plugins/tidb-session-store.d.ts +22 -2
- package/dist/plugins/tidb-session-store.js +33 -6
- package/dist/plugins/write-behind-counter.d.ts +12 -5
- package/dist/plugins/write-behind-counter.js +14 -7
- package/dist/prompts-domain-validate.js +3 -2
- package/dist/rules-consent.d.ts +2 -2
- package/dist/rules-consent.js +2 -2
- package/dist/run-local.js +31 -23
- package/dist/runs.d.ts +9 -0
- package/dist/runs.js +8 -0
- package/dist/runtime-governance.d.ts +3 -3
- package/dist/runtime-governance.js +2 -4
- package/dist/security.d.ts +19 -2
- package/dist/session-row.d.ts +53 -0
- package/dist/session-row.js +47 -0
- package/dist/session-sync.d.ts +6 -0
- package/dist/session-sync.js +5 -1
- package/dist/session-titler.d.ts +9 -5
- package/dist/session-titler.js +3 -1
- package/dist/spec-fields.js +2 -2
- package/dist/task-cwd.d.ts +0 -20
- package/dist/task-cwd.js +2 -10
- package/dist/tool-approval.d.ts +5 -7
- package/dist/trace/redact.d.ts +23 -50
- package/dist/trace/redact.js +58 -18
- package/dist/trace/self-minted-wire.d.ts +25 -13
- package/dist/trace/self-minted-wire.js +9 -4
- package/dist/web-admin-client/apply-effective.d.ts +10 -12
- package/dist/web-admin-client/apply-effective.js +2 -3
- package/dist/web-admin-client/candidate-gate.d.ts +69 -0
- package/dist/web-admin-client/candidate-gate.js +27 -0
- package/dist/web-admin-client/http-client.d.ts +2 -1
- package/dist/web-admin-client/http-client.js +2 -1
- package/dist/web-admin-client/pull-state.d.ts +6 -4
- package/dist/web-admin-client/read-warnings.d.ts +29 -5
- package/dist/web-admin-client/read-warnings.js +24 -4
- package/dist/web-admin-client/stage-limits.js +4 -1
- package/dist/workspace-space.d.ts +41 -0
- package/dist/workspace-space.js +5 -0
- package/package.json +3 -3
|
@@ -44,14 +44,16 @@ export interface ParseHooksConfigResult {
|
|
|
44
44
|
* 其实没跑,硬破坏三句要求的「响亮」没有着落。7.112.0(S-924)只拒**字面**别名;同一个错误写成 `^ListPeers$` / `ListPeers.*` / `(ListPeers)` 照样 200(S-995)。
|
|
45
45
|
*
|
|
46
46
|
* 规则(一条;字面形是它的特例):{@link deadToolMatcherIssue} 对 core 静态目录里**每一条退役别名**问「这只 matcher 选不选它 / 选不选它的正名」:
|
|
47
|
-
* matcher **命中别名 ∧ 不命中它的正名** ⇒ `400 request.field_invalid`(`admission.ts` fresh 门)
|
|
47
|
+
* matcher **命中别名 ∧ 不命中它的正名** ⇒ `400 request.field_invalid`(`admission.ts` fresh 门)。「选不选」**只有一只读法**:契约包的 `hookMatcherSelects`
|
|
48
|
+
* (名字表 / 空 / `*` / 正则三形都由它答;正则形**不锚定**,与壳同形;运行期 {@link hookMatcherMatches} 问的也是它,本仓不留副本)。按**一条顺序**判(两档共用,7.114.0 修复轮):
|
|
48
49
|
* ① **长度上限**:超 {@link MAX_MATCHER_EVAL_CHARS} 的 matcher 两档都不读 ⇒ 未判(reason = oversize);
|
|
49
|
-
* ②
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
50
|
+
* ② **形只问契约包的 `hookMatcherForm`**(与 `hookMatcherSelects` 同一只解析步,本仓不留形判),闭集三词、两档一张表:
|
|
51
|
+
* · `literal`(缺席 / 空 / `*` / 名字表形;契约不编、不跑调用方的正则)⇒ 两档都**直接问契约函数**、**不占求值预算**;
|
|
52
|
+
* · `regex` ⇒ **`"regex"` 档**(hooks 会被采纳的车道,`taskHooksHonored`)把**同一只契约函数**经 `node:vm` 的 `timeout` 带一次受理共享的预算调用({@link matcherSelectsWithin});
|
|
53
|
+
* **`"literal"` 档**(多租户车道:hooks 整只被 resolveSpec 忽略)= **不适用**:该车道没有被守的对象(hooks 不装配),不判、不算未判、不登记 —— **零调用方正则执行**
|
|
54
|
+
* (7.112.0 的字面判保留:裸 `ListPeers` / `Read|ListPeers` 照旧 400);
|
|
55
|
+
* · `invalid`(编不过)⇒ `"regex"` 档 = 未判 `invalid_regex`(不进预算求值 —— 形判已答「读不了」);`"literal"` 档 = 不适用(同上)。
|
|
56
|
+
* 形判在 `literal` 档也跑:契约的解析步对非名字表形**构造**(只解析、不执行)一次调用方的 pattern 以分出 regex / invalid —— 长度已被 ① 界住;执行恒为零。
|
|
55
57
|
* · **为什么两档**(codex r1 [high] + 主会话裁):受理面是**所有**车道,而运行期只有单用户车道跑 hooks。在受理线程上跑调用方的正则 = 给多租户加一个事件循环 DoS 面
|
|
56
58
|
* (实测:`(.*)*` ×10 + `X` 对 9 字符名字首次执行 1.7 s,18 字符嵌套形 15 s,24 字符 157 s;8 MiB 交替串编译 + 求值 9 s)—— 预算只界住一次请求、界不住「每个请求再拿一份」。
|
|
57
59
|
* 但也不能因此在多租户车道「不判决」(那会放宽已发行为:裸 `ListPeers` 的 400)。所以两车道同一条规则、两档深度。谓词 `taskHooksHonored` 是唯一属主(与 resolveSpec / capabilities 同调)。
|
|
@@ -66,7 +68,7 @@ export interface ParseHooksConfigResult {
|
|
|
66
68
|
* · **正名同在则不拒**:`正名|别名` / `List.*` 会命中正名,不是「静默不命中」,且正是 7.107.0 CHANGELOG 给的「滚动期两名都写」补偿形。
|
|
67
69
|
* 只拒「别名那一路永远打不中、而它的正名不在选中集里」—— 含 `Read|ListPeers`(7.112.0 已拒;主会话裁:规则形采「按条目逐条问」,不放宽;卡面整 matcher「匹配不到任何现役名」形会把它放回 200)。
|
|
68
70
|
* 多拒(相对卡面形)三例与出路:`Read|ListPeers` / `ListPeers|mcp__.*`(别名支永不命中,出路:改成正名)/ 排除式 `^(?!ListAgents).*`(出路:`^(?!ListAgents|ListPeers).*` 两名都不命中 ⇒ 放)。
|
|
69
|
-
* · **空白与大小写(S-995 / sema-test [ref] 起,运行期与 CC
|
|
71
|
+
* · **空白与大小写(S-995 / sema-test [ref] 起,运行期与 CC 同形)**:名字表读法(契约函数;`|` 与 `,` 分隔)容忍两端与分隔符两侧空白 ⇒ `" ListPeers"` / `"ListPeers "` / `"Read | ListPeers"` 会命中别名 ⇒ 落进本规则 400;
|
|
70
72
|
* **区分大小写、不剥词内空白** ⇒ `"listpeers"` / `"List Peers"` 对别名与正名都选不中,照旧受理(CC 同形)。
|
|
71
73
|
* · **不在本规则内**:两边都选不中的 matcher(大小写错 / 词内空白 / 纯错字;目录之外的 MCP / 嵌入方工具名同样「选不中目录里任何名」,受理面无从区分错字与它们)=
|
|
72
74
|
* **「选不到目录里任何名」另一类,候设计(台账另立号,主会话裁)**。
|
|
@@ -89,7 +91,7 @@ export interface ParseHooksConfigResult {
|
|
|
89
91
|
export declare const MAX_MATCHER_EVAL_CHARS = 8192;
|
|
90
92
|
/** 一次 fresh 受理里 regex 档**正则形**求值共享的墙钟预算(毫秒)。字面形不占它。良性求值 ~0.1 ms;灾难回溯的 matcher 把预算吃完就停,后面的正则形组直接「未判」。 */
|
|
91
93
|
export declare const ADMISSION_MATCHER_EVAL_BUDGET_MS = 100;
|
|
92
|
-
/** 受理面判据的求值深度(见上头注):`"regex"` =
|
|
94
|
+
/** 受理面判据的求值深度(见上头注):`"regex"` = 契约字面形直接问、正则形带预算问同一只契约函数;`"literal"` = 7.112.0 的字面判(只判契约字面形,零调用方正则执行;正则形 / 编不过的形不适用)。 */
|
|
93
95
|
export type MatcherEvalDepth = "regex" | "literal";
|
|
94
96
|
/**
|
|
95
97
|
* **仅供测试注入**的求值预算(生产调用点不传 ⇒ 预算 = {@link ADMISSION_MATCHER_EVAL_BUDGET_MS}、时钟 = `performance.now`)。
|
|
@@ -134,25 +136,11 @@ export declare function deadToolMatcherIssue(config: HooksConfig, depth: Matcher
|
|
|
134
136
|
*/
|
|
135
137
|
export declare function parseHooksConfig(raw: unknown): ParseHooksConfigResult;
|
|
136
138
|
/**
|
|
137
|
-
* matcher
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
* 等价性的依据(收车档 ⑥ / ⑦):
|
|
143
|
-
* · 黑盒 —— sema-test `repro/cc-r219-mcpstar-hookmatcher-20261003/`([ref],CC 2.1.280 真 TUI + 真 command 钩子):`" Bash"` / `"Bash "` / `" Bash "` / `"Read | Bash"` 命中 3/3,
|
|
144
|
-
* `"Ba sh"`(词内空白)/ `bash` / `BASH` 不命中(零告警),`^Bash$` / `Bash.*` / `(Bash)` / `Read|Bash` 照命中;PreToolUse 与 PostToolUse 同向。**逗号没有黑盒读数**,只有语料。
|
|
145
|
-
* · 语料 —— `sema-internal/blueprints/cc-decoded/cli280.js:216638` `uAe(matchValue, matcher, flag, …)` → `$mr(matcher, flag, …)`:`flag` 真时字符集门 `/^[a-zA-Z0-9_|, -]+$/` 过了才
|
|
146
|
-
* `matcher.split(/[|,]/).map(trim).filter(Boolean)` 做成员判;过不了才 `new RegExp(matcher)`(CC 的正则路径**不锚定**,本仓锚定 `^(?:…)$` —— 既有差异,不动)。
|
|
147
|
-
* 本函数的字符集与拆符与 CC 逐字同(空白只认空格 0x20)。
|
|
148
|
-
* · 已知差异(如实登记,本车不做):CC 的空白 / 逗号放行**按事件分档**(`dAe` 集合:工具事件 / PreCompact / PostCompact 等放,StopFailure 不放),本函数不分事件(第二批裁:同一只读法);
|
|
149
|
-
* CC 正则路径不锚定、本仓锚定。两条登记在 CHANGELOG 7.114.0 线 · 车 UW 已知边界。
|
|
150
|
-
* 零正则:单趟逐字符扫描(7.114.0 修复轮起;只为非空段分配名字,不按分隔符个数分配)。受理面两档({@link deadToolMatcherIssue})与运行期求值**共用这一只**读法,且两档都先走它、不占求值预算 ——
|
|
151
|
-
* 两档在字面子集上答案逐位相同是**构造出来的**,不是对拍出来的(超过长度上限的 matcher 两档都不读、同判未判)。
|
|
139
|
+
* 运行期的 matcher 读法席位:**问契约包的 `hookMatcherSelects`,别无其它**(壳与服务端都问这一只,谁都不留副本;读法全文见它的头注,这里不重述)。
|
|
140
|
+
* 三形(名字表 / 空 `*` 缺省 / 不锚定的 JS 正则)与「编不过 ⇒ `"invalid"`」都由它答;**对所有带 matcher 的事件同一只读法**(工具事件 / StopFailure 的 `error` 词 /
|
|
141
|
+
* PreCompact·PostCompact 的 `trigger` 词 / `if` 条件的名部),不按事件分档。`"invalid"` ⇒ 该组不跑(fail-closed 到「不跑」),由调用方记账一次(`hook_matcher_invalid`);
|
|
142
|
+
* 受理面({@link deadToolMatcherIssue})问的是同一只函数。
|
|
152
143
|
*/
|
|
153
|
-
export declare function matcherLiteralNames(matcher: string): readonly string[] | undefined;
|
|
154
|
-
/** CC matcher 语义:空/`*`/缺省匹配一切;**字面 / 名字表读法**({@link matcherLiteralNames}:`|` 与 `,` 分隔、每段 trim、不剥词内空白、区分大小写)按名字表成员判;
|
|
155
|
-
* 其余整体当 JS 正则({@link matcherPatternMatches};锚定全匹配;S-995 起正则路径一字不动);非法正则 → 该条不匹配(fail-closed 到"不跑",由调用方记账一次)。 */
|
|
156
144
|
export declare function hookMatcherMatches(matcher: string | undefined, value: string): boolean | "invalid";
|
|
157
145
|
/** `if` 条目条件(CC-exact,对照 CC 2.1.187/198 实测行为核验的真语义,两版一致):
|
|
158
146
|
* 值 = permission-rule 模式(`ToolName` 或 `ToolName(arg-模式)`),【不是】表达式语言——按当前工具调用
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import vm from "node:vm";
|
|
3
3
|
import { hostShell, resolveHostShell, adoptHostForegroundChild } from "../plugins/host-platform.js";
|
|
4
|
-
import { HooksConfig, DEFAULT_HOOK_TIMEOUT_SECONDS, HOOK_EVENT_OWNER, HOOK_EVENT_MATCHER_FIELD, } from "@sema-agent/settings-schema/hooks";
|
|
4
|
+
import { HooksConfig, DEFAULT_HOOK_TIMEOUT_SECONDS, HOOK_EVENT_OWNER, HOOK_EVENT_MATCHER_FIELD, hookMatcherForm, hookMatcherSelects, } from "@sema-agent/settings-schema/hooks";
|
|
5
5
|
import { describeToolCatalog, isEngineRefusal, receiveHookResult } from "@sema-agent/core";
|
|
6
6
|
import { boundedNoticeText } from "../fleet/fleet-bus.js";
|
|
7
7
|
import { recordFailOpen } from "../observability/fail-open.js";
|
|
@@ -81,13 +81,13 @@ function engineToolAliases() {
|
|
|
81
81
|
return table;
|
|
82
82
|
}
|
|
83
83
|
let matcherEvalBox;
|
|
84
|
-
function
|
|
84
|
+
function matcherSelectsWithin(matcher, name, budget) {
|
|
85
85
|
const remaining = budget.deadlineMs - budget.now();
|
|
86
86
|
if (remaining <= 0)
|
|
87
87
|
return "over_budget";
|
|
88
88
|
matcherEvalBox ??= {
|
|
89
|
-
script: new vm.Script("
|
|
90
|
-
context: vm.createContext({
|
|
89
|
+
script: new vm.Script("hookMatcherSelects(matcher, name)"),
|
|
90
|
+
context: vm.createContext({ hookMatcherSelects, matcher: "", name: "" }),
|
|
91
91
|
};
|
|
92
92
|
matcherEvalBox.context.matcher = matcher;
|
|
93
93
|
matcherEvalBox.context.name = name;
|
|
@@ -101,31 +101,30 @@ function matcherPatternMatchesWithin(matcher, name, budget) {
|
|
|
101
101
|
throw err;
|
|
102
102
|
}
|
|
103
103
|
}
|
|
104
|
+
const MATCHER_EVALUATION = {
|
|
105
|
+
literal: { regex: "inline", literal: "inline" },
|
|
106
|
+
regex: { regex: "budgeted", literal: "not_applicable" },
|
|
107
|
+
invalid: { regex: "unreadable", literal: "not_applicable" },
|
|
108
|
+
};
|
|
104
109
|
function deadAliasInMatcher(matcher, depth, budget) {
|
|
105
110
|
if (matcher.length > MAX_MATCHER_EVAL_CHARS)
|
|
106
111
|
return { kind: "unjudged", reason: "oversize" };
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
if (names !== undefined) {
|
|
111
|
-
for (const [alias, canonical] of engineToolAliases()) {
|
|
112
|
-
if (names.includes(alias) && !names.includes(canonical))
|
|
113
|
-
return { kind: "dead", alias, canonical };
|
|
114
|
-
}
|
|
115
|
-
return undefined;
|
|
116
|
-
}
|
|
117
|
-
switch (depth) {
|
|
118
|
-
case "literal":
|
|
112
|
+
const evaluate = MATCHER_EVALUATION[hookMatcherForm(matcher)][depth];
|
|
113
|
+
switch (evaluate) {
|
|
114
|
+
case "not_applicable":
|
|
119
115
|
return undefined;
|
|
120
|
-
case "
|
|
116
|
+
case "unreadable":
|
|
117
|
+
return { kind: "unjudged", reason: "invalid_regex" };
|
|
118
|
+
case "inline":
|
|
119
|
+
case "budgeted":
|
|
121
120
|
break;
|
|
122
121
|
default: {
|
|
123
|
-
const unreachable =
|
|
122
|
+
const unreachable = evaluate;
|
|
124
123
|
return unreachable;
|
|
125
124
|
}
|
|
126
125
|
}
|
|
127
126
|
const selects = (name) => {
|
|
128
|
-
const verdict =
|
|
127
|
+
const verdict = evaluate === "inline" ? hookMatcherSelects(matcher, name) : matcherSelectsWithin(matcher, name, budget);
|
|
129
128
|
return verdict === "invalid" ? "invalid_regex" : verdict;
|
|
130
129
|
};
|
|
131
130
|
for (const [alias, canonical] of engineToolAliases()) {
|
|
@@ -214,47 +213,8 @@ export function parseHooksConfig(raw) {
|
|
|
214
213
|
}
|
|
215
214
|
return { config };
|
|
216
215
|
}
|
|
217
|
-
export function matcherLiteralNames(matcher) {
|
|
218
|
-
const names = [];
|
|
219
|
-
let first = -1;
|
|
220
|
-
let last = -1;
|
|
221
|
-
for (let i = 0; i <= matcher.length; i++) {
|
|
222
|
-
const c = i < matcher.length ? matcher.charCodeAt(i) : 0x7c;
|
|
223
|
-
if (c === 0x7c || c === 0x2c) {
|
|
224
|
-
if (first !== -1)
|
|
225
|
-
names.push(matcher.slice(first, last + 1));
|
|
226
|
-
first = -1;
|
|
227
|
-
last = -1;
|
|
228
|
-
continue;
|
|
229
|
-
}
|
|
230
|
-
if (c === 0x20)
|
|
231
|
-
continue;
|
|
232
|
-
const nameChar = (c >= 0x30 && c <= 0x39) || (c >= 0x41 && c <= 0x5a) || (c >= 0x61 && c <= 0x7a) || c === 0x5f || c === 0x2d;
|
|
233
|
-
if (!nameChar)
|
|
234
|
-
return undefined;
|
|
235
|
-
if (first === -1)
|
|
236
|
-
first = i;
|
|
237
|
-
last = i;
|
|
238
|
-
}
|
|
239
|
-
return names;
|
|
240
|
-
}
|
|
241
|
-
function matcherPatternMatches(matcher, value) {
|
|
242
|
-
if (matcher === value)
|
|
243
|
-
return true;
|
|
244
|
-
try {
|
|
245
|
-
return new RegExp(`^(?:${matcher})$`).test(value);
|
|
246
|
-
}
|
|
247
|
-
catch {
|
|
248
|
-
return "invalid";
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
216
|
export function hookMatcherMatches(matcher, value) {
|
|
252
|
-
|
|
253
|
-
return true;
|
|
254
|
-
const names = matcherLiteralNames(matcher);
|
|
255
|
-
if (names !== undefined)
|
|
256
|
-
return names.includes(value);
|
|
257
|
-
return matcherPatternMatches(matcher, value);
|
|
217
|
+
return hookMatcherSelects(matcher, value);
|
|
258
218
|
}
|
|
259
219
|
export function hookIfMatches(cond, payload) {
|
|
260
220
|
if (cond.length > 500 || (cond.match(/\*/g)?.length ?? 0) > 16)
|
|
@@ -149,6 +149,9 @@ export interface ActiveRunConflictDoneResult {
|
|
|
149
149
|
stale?: true;
|
|
150
150
|
pendingGate?: PendingGateMaterial;
|
|
151
151
|
}
|
|
152
|
+
/** 一份提交拒绝体是不是**会话占用冲突**体(`buildActiveRunConflict` 的产物)—— 判别位 = 机器码,与 sdk 终帧联合里冲突臂的窄化同一个位。
|
|
153
|
+
* 只收那一只铸点的产物:调用方是流式腿把「拒绝」编码成终帧的投影({@link toDoneFrameResult} 的入口),别处的 409 体不过这里。 */
|
|
154
|
+
export declare function isActiveRunConflictBody(body: unknown): body is ActiveRunConflictBody;
|
|
152
155
|
export declare function toDoneFrameResult(body: ActiveRunConflictBody): ActiveRunConflictDoneResult;
|
|
153
156
|
interface ConflictProbeDeps<TToken> {
|
|
154
157
|
/** 结构形入参,但 `status` 取 run 行的**闭词表**(合并码重扫):判 park 要走词表属主
|
|
@@ -27,6 +27,9 @@ function safeCheckpointIdOf(raw, token) {
|
|
|
27
27
|
return undefined;
|
|
28
28
|
return raw;
|
|
29
29
|
}
|
|
30
|
+
export function isActiveRunConflictBody(body) {
|
|
31
|
+
return typeof body === "object" && body !== null && "activeTaskId" in body && body.errorCode === "conflict.session_active_run";
|
|
32
|
+
}
|
|
30
33
|
export function toDoneFrameResult(body) {
|
|
31
34
|
return {
|
|
32
35
|
status: "failed",
|
package/dist/http/admission.js
CHANGED
|
@@ -10,12 +10,12 @@ import * as facts from "../capability-facts.js";
|
|
|
10
10
|
import { buildDeadToolMatcherRefusal, deadToolMatcherIssue, parseHooksConfig } from "../hooks/hook-runner.js";
|
|
11
11
|
import { judgeRequestWebSearch } from "../plugins/web-search.js";
|
|
12
12
|
import { normalizeApproachNotice, skillToolListOk, skillToolListRefusal, unknownTaskAgentModelRef, validateTaskAgents } from "../spec-fields.js";
|
|
13
|
-
import {
|
|
13
|
+
import { MAX_ADDITIONAL_DIRS } from "../task-cwd.js";
|
|
14
14
|
import { verifyRoundsFromBody } from "./verify-rounds.js";
|
|
15
15
|
import { skillCarriesRetiredManifest } from "./retired-skill-manifest.js";
|
|
16
16
|
import { resolveDevicePlacementIntent } from "./device-placement.js";
|
|
17
17
|
import { sendError, httpErrorCode } from "./send.js";
|
|
18
|
-
import { WEB_ADMIN_ENV } from "@sema-agent/settings-schema";
|
|
18
|
+
import { ABSOLUTE_HOST_PATH_MAX, isAbsoluteHostPath, WEB_ADMIN_ENV } from "@sema-agent/settings-schema";
|
|
19
19
|
const APPROVAL_REACH_WORDS = APPROVAL_REACH.map((word) => JSON.stringify(word)).join(" / ");
|
|
20
20
|
export const MAX_SKILL_CONTENT_CHARS = 1_048_576;
|
|
21
21
|
export const MAX_SYSTEM_PROMPT_CHARS = 16_384;
|
|
@@ -34,8 +34,8 @@ export function validateUserSkills(skills) {
|
|
|
34
34
|
return "skill.description must be a string";
|
|
35
35
|
if (typeof s["content"] !== "string" || s["content"].length === 0 || s["content"].length > MAX_SKILL_CONTENT_CHARS)
|
|
36
36
|
return `skill.content must be a non-empty string of at most ${MAX_SKILL_CONTENT_CHARS} characters (the engine rejects skills over this size outright at load time)`;
|
|
37
|
-
if (s["baseDir"] !== undefined && !
|
|
38
|
-
return `skill.baseDir must be an absolute path string of at most ${
|
|
37
|
+
if (s["baseDir"] !== undefined && !isAbsoluteHostPath(s["baseDir"]))
|
|
38
|
+
return `skill.baseDir must be an absolute path string of at most ${ABSOLUTE_HOST_PATH_MAX} characters with no ".." segment and no NUL byte (it is not resolved or read here — the engine only substitutes it into the skill's own text)`;
|
|
39
39
|
if (skillCarriesRetiredManifest(s))
|
|
40
40
|
return SKILL_MANIFEST_RETIRED;
|
|
41
41
|
for (const key of ["allowedTools", "disallowedTools"]) {
|
|
@@ -103,7 +103,7 @@ export function createAdmission(ctx) {
|
|
|
103
103
|
return null;
|
|
104
104
|
}
|
|
105
105
|
}
|
|
106
|
-
if (body.cwd !== undefined && !
|
|
106
|
+
if (body.cwd !== undefined && !isAbsoluteHostPath(body.cwd)) {
|
|
107
107
|
sendError(res, 400, "request.field_invalid", "cwd must be a non-empty absolute host path");
|
|
108
108
|
return null;
|
|
109
109
|
}
|
|
@@ -117,7 +117,7 @@ export function createAdmission(ctx) {
|
|
|
117
117
|
sendError(res, 400, "request.field_invalid", `additionalDirectories must have at most ${MAX_ADDITIONAL_DIRS} entries`);
|
|
118
118
|
return null;
|
|
119
119
|
}
|
|
120
|
-
const bad = addDirsRaw.find((d) => !
|
|
120
|
+
const bad = addDirsRaw.find((d) => !isAbsoluteHostPath(d));
|
|
121
121
|
if (bad !== undefined) {
|
|
122
122
|
sendError(res, 400, "request.field_invalid", "each additionalDirectories entry must be a non-empty absolute host path (no '..' segments)");
|
|
123
123
|
return null;
|
|
@@ -133,7 +133,7 @@ export function createAdmission(ctx) {
|
|
|
133
133
|
sendError(res, 400, "request.field_invalid", `additionalReadDirectories must have at most ${MAX_ADDITIONAL_DIRS} entries`);
|
|
134
134
|
return null;
|
|
135
135
|
}
|
|
136
|
-
const badRead = addReadDirsRaw.find((d) => !
|
|
136
|
+
const badRead = addReadDirsRaw.find((d) => !isAbsoluteHostPath(d));
|
|
137
137
|
if (badRead !== undefined) {
|
|
138
138
|
sendError(res, 400, "request.field_invalid", "each additionalReadDirectories entry must be a non-empty absolute host path (no '..' segments)");
|
|
139
139
|
return null;
|
|
@@ -6,6 +6,7 @@ import { APPROVAL_GATE_KINDS } from "../tool-approval.js";
|
|
|
6
6
|
import { createWallClockJumpGuard } from "../wall-clock-jump-guard.js";
|
|
7
7
|
import { hasServiceAuth } from "./dispatch.js";
|
|
8
8
|
import { gatedPrincipal } from "./principal-gate.js";
|
|
9
|
+
import { costQuotaRefusalOf } from "./request-guards.js";
|
|
9
10
|
import { admissionRefusalOf, readinessOf } from "./readiness.js";
|
|
10
11
|
import { readResumeCtx } from "./resume-ctx-read.js";
|
|
11
12
|
import { storedBodyReplayRefusal } from "./stored-body-replay.js";
|
|
@@ -116,10 +117,7 @@ export function createApprovalSweeps(ctx) {
|
|
|
116
117
|
const d = deps.costQuota.check(quotaPrincipal);
|
|
117
118
|
if (!d.allowed) {
|
|
118
119
|
deps.metrics?.inc("cost_quota_rejected_total");
|
|
119
|
-
return
|
|
120
|
-
status: 429,
|
|
121
|
-
body: { error: "cost quota exceeded", errorCode: "limit.cost_quota_exceeded", usedMicroUsd: d.usedMicroUsd, limitMicroUsd: d.limitMicroUsd, retryAfterSec: d.retryAfterSec },
|
|
122
|
-
};
|
|
120
|
+
return costQuotaRefusalOf(d);
|
|
123
121
|
}
|
|
124
122
|
}
|
|
125
123
|
}
|
|
@@ -92,6 +92,7 @@ export async function commitPlacement(ctx, runStore, taskId, intent) {
|
|
|
92
92
|
const releaseRun = async (why) => {
|
|
93
93
|
try {
|
|
94
94
|
await runStore.setTerminal(taskId, "failed", null, why);
|
|
95
|
+
await runStore.releaseIdemKey(taskId);
|
|
95
96
|
}
|
|
96
97
|
catch (err) {
|
|
97
98
|
ctx.logger?.warn("device_placement_run_release_failed", { taskId, err: err instanceof Error ? err.message : String(err) });
|
package/dist/http/dispatch.js
CHANGED
|
@@ -35,7 +35,7 @@ function cors(res, req, origins, principalHeader) {
|
|
|
35
35
|
if (allow === undefined)
|
|
36
36
|
return;
|
|
37
37
|
res.setHeader("access-control-allow-origin", allow);
|
|
38
|
-
res.setHeader("access-control-allow-methods",
|
|
38
|
+
res.setHeader("access-control-allow-methods", [...ROUTE_METHOD_ORDER, "OPTIONS"].join(", "));
|
|
39
39
|
res.setHeader("access-control-allow-headers", `content-type, authorization, last-event-id, ${principalHeader}, idempotency-key, x-detach-on-disconnect, ${PRINCIPAL_TOKEN_HEADER}, ${APPROVAL_MAC_HEADER}, ${APPROVAL_MAC_KID_HEADER}, if-none-match`);
|
|
40
40
|
res.setHeader("access-control-expose-headers", "x-task-id, retry-after");
|
|
41
41
|
}
|
|
@@ -9,12 +9,13 @@
|
|
|
9
9
|
* 当年从路由体里提取出来时,头注写的就是同一条判词)。方向:`routes/runs.ts` 与 `http/server.ts`
|
|
10
10
|
* 都 import 本模块,本模块谁都不 import —— 与 `http/run-meta.ts` 同一条「两个域共用一件 ⇒ 单独成文件」。
|
|
11
11
|
*
|
|
12
|
-
* 边界(本腿**不**含的东西,留给各自的门)
|
|
12
|
+
* 边界(本腿**不**含的东西,留给各自的门):同键重放的回执(`http/submit-replay.ts`)、`Idempotency-Key` 的在飞合流、
|
|
13
13
|
* lease / 治理窗 pre-admission、属主门。它们是**提交面**的判据(读请求、写响应),而本腿只吃已经备好的
|
|
14
14
|
* `PreparedTaskSubmission` 并交回一份收据 —— 这正是它能被一条没有 `ServerResponse` 的内部车道复用的原因。
|
|
15
15
|
*/
|
|
16
16
|
import { type RunInternals } from "@sema-agent/core";
|
|
17
17
|
import type { RunStore } from "../plugins/store-backend.js";
|
|
18
|
+
import type { RunRecord } from "../plugins/store-contracts.js";
|
|
18
19
|
import type { PreparedTaskSubmission, RouteCtxBase } from "./route-ctx.js";
|
|
19
20
|
/**
|
|
20
21
|
* 🔴 **TRUSTED run internals** —— 由**部署方**(不是模型、不是请求体、不是工具参数)铸给这一跑的
|
|
@@ -34,6 +35,8 @@ export interface DurableRunLegArgs {
|
|
|
34
35
|
prepared: PreparedTaskSubmission;
|
|
35
36
|
/** 调用方自带的 taskId(`POST /v1/runs` 的 `body.taskId` 幂等键);缺席 ⇒ 本腿铸 uuidv7。 */
|
|
36
37
|
clientTaskId?: string;
|
|
38
|
+
/** 这次提交的幂等键(`idemKeyOf(raw, caller, "run")`)—— 落 `task_run.idem_key`;缺席 = 调用方没带 `Idempotency-Key`。 */
|
|
39
|
+
idemKey?: string;
|
|
37
40
|
/** 🔒 这条 run 的 **fleet 行 scope** —— 必须与 `GET /v1/fleet/stream` 过滤用的是同一个身份,且绝不是
|
|
38
41
|
* 自报的 `x-agent-principal` 头(自报头会把行打到受害者的 scope 上,或打到 `default` 而真属主的流把它
|
|
39
42
|
* 过滤掉)。提交门传 `gatedPrincipal(req) ?? "default"`;`/decide` 的第三条车道传 resume 家族的
|
|
@@ -49,16 +52,27 @@ export interface DurableRunLegArgs {
|
|
|
49
52
|
trustedInternals?: TrustedRunInternals;
|
|
50
53
|
}
|
|
51
54
|
/**
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
* 协议渲染归调用方(HTTP `sendJson` / A2A 协议对象 / `/decide` 的受理体)。
|
|
55
|
+
* 本腿的结局:一份收据,或「这次提交的一把幂等钥匙已经指向一条行」(店的原子赢或观察的**观察**臂 —— 本腿不碰
|
|
56
|
+
* 请求、不判属主,回执归提交面的唯一回执函数 `replayOfExistingRun`)。另一位在每一形上恒 `undefined`。
|
|
55
57
|
*/
|
|
56
|
-
export
|
|
58
|
+
export type DurableRunLegOutcome = {
|
|
57
59
|
status: number;
|
|
58
60
|
body: {
|
|
59
61
|
taskId: string;
|
|
60
62
|
sessionId: string;
|
|
61
63
|
status: string;
|
|
62
64
|
} | object;
|
|
63
|
-
|
|
65
|
+
existing?: undefined;
|
|
66
|
+
} | {
|
|
67
|
+
existing: RunRecord;
|
|
68
|
+
status?: undefined;
|
|
69
|
+
body?: undefined;
|
|
70
|
+
};
|
|
71
|
+
/**
|
|
72
|
+
* 建行 + 驱动。返回 `202` = 新行已建并在跑(体带 `taskId`/`sessionId`);`409` = 会话被别的 run 占着,
|
|
73
|
+
* **没有**建行(体 = `buildActiveRunConflict` 的既有形,含 `errorCode: "conflict.session_active_run"`);
|
|
74
|
+
* `existing` = 这次提交的钥匙(`idemKey` / `clientTaskId`)已经指向一条行,没建新行。
|
|
75
|
+
* 协议渲染归调用方(HTTP `sendJson` / A2A 协议对象 / `/decide` 的受理体)。
|
|
76
|
+
*/
|
|
77
|
+
export declare function mintAndDriveDurableRun(ctx: RouteCtxBase, runStore: RunStore, args: DurableRunLegArgs): Promise<DurableRunLegOutcome>;
|
|
64
78
|
//# sourceMappingURL=durable-run-leg.d.ts.map
|
|
@@ -15,17 +15,16 @@ export async function mintAndDriveDurableRun(ctx, runStore, args) {
|
|
|
15
15
|
const { inflightRuns, preemptableRuns, steerableRuns } = ctx.registry;
|
|
16
16
|
const sessionId = args.prepared.spec.sessionId ?? uuidv7();
|
|
17
17
|
const taskId = args.clientTaskId ?? uuidv7();
|
|
18
|
-
const created = await runStore.createRun(taskId, sessionId, args.prepared.auth?.principal ?? null, deps.instanceId ?? "default", runMeta(args.prepared, args.source));
|
|
18
|
+
const created = await runStore.createRun(taskId, sessionId, args.prepared.auth?.principal ?? null, deps.instanceId ?? "default", { ...runMeta(args.prepared, args.source), idemKey: args.idemKey ?? null });
|
|
19
19
|
if (created.ok)
|
|
20
20
|
clearTurnActivity(taskId);
|
|
21
21
|
if (created.ok)
|
|
22
22
|
recordResourceWindow(taskId, args.prepared.spec.resourceSuspend);
|
|
23
23
|
if (created.ok)
|
|
24
24
|
deps.sessionTitler?.maybeTitle(sessionId, args.prepared.spec.objective, typeof args.prepared.spec.model === "string" ? args.prepared.spec.model : args.prepared.spec.model?.id);
|
|
25
|
+
if (created.existing)
|
|
26
|
+
return { existing: created.existing };
|
|
25
27
|
if (!created.ok) {
|
|
26
|
-
if (args.clientTaskId && created.activeTaskId === args.clientTaskId) {
|
|
27
|
-
return { status: 202, body: { taskId: args.clientTaskId, sessionId, status: "running" } };
|
|
28
|
-
}
|
|
29
28
|
return { status: 409, body: await buildActiveRunConflict({ runStore, checkpointStore: deps.checkpointStore, governance: deps.config, runStaleSec: deps.config.runStaleSec, turnActivity: readTurnActivityMs }, sessionId, created.activeTaskId) };
|
|
30
29
|
}
|
|
31
30
|
const placementReject = await commitPlacement(deps, runStore, taskId, args.prepared.bindingIntent);
|
|
@@ -51,6 +50,7 @@ export async function mintAndDriveDurableRun(ctx, runStore, args) {
|
|
|
51
50
|
runId: taskId,
|
|
52
51
|
scope: args.fleetScope,
|
|
53
52
|
rootTaskId: taskId,
|
|
53
|
+
sessionId,
|
|
54
54
|
...fleetRunLabels(args.prepared.spec.objective),
|
|
55
55
|
});
|
|
56
56
|
void runInBackground(args.prepared.runner, { ...args.prepared.spec, sessionId }, runStore, taskId, deps.metrics, args.prepared.auth?.principal, args.prepared.verify, args.prepared.cascade ? cascadeConfig(deps.config.cascadeLadder, args.prepared.spec.limits?.maxCostUsd) : undefined, deps.instrumentDegenerate, deps.planCacheProbe, deps.config.traceThinking, inflightRuns, preemptableRuns, steerableRuns, deps.modelUsage, deps.elicitation, args.elicitOwner, captureTurnAnchor, fleetPub, captureUserMessageAnchor, deps.question, deps.workflowCompletionInbox, deps.subagentSteerRegistry, (m, x) => deps.logger?.info?.(m, x), deps.sendUserFile, deps.promptManifests, deps.toolApproval
|
|
@@ -1,65 +1,110 @@
|
|
|
1
|
+
import type { IncomingMessage } from "node:http";
|
|
2
|
+
import type { ServiceConfig } from "../config-types.js";
|
|
1
3
|
import type { WiringManifestReceipt } from "./wire-types.js";
|
|
4
|
+
import type { SubmitReplayFace } from "./submit-replay.js";
|
|
2
5
|
/**
|
|
3
|
-
*
|
|
6
|
+
* 同进程的**在飞合流** —— 并发同键的提交共享同一个 promise(避免同一进程里双起)。它**不是**重放的真源。
|
|
4
7
|
*
|
|
5
|
-
* -
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* transient 409) is evicted so a genuine retry re-runs.
|
|
9
|
-
* - `peek(key)` returns the cached (in-flight or settled) promise without producing — letting a caller
|
|
10
|
-
* short-circuit a retry BEFORE side gates (rate/quota), so the retry gets the same response, not a fresh 429.
|
|
11
|
-
* An SSE stream wraps its live run in `run()` so the in-flight promise is stored too; a retry then peeks it.
|
|
8
|
+
* 提交幂等的律:`Idempotency-Key` 的执法点 = 资源行上的 `idem_key` UNIQUE(`task_run` / `leader_run` /
|
|
9
|
+
* `image_bake`);同键再投 ⇒ 读到那一行 ⇒ 唯一回执函数作答(`http/submit-replay.ts`)。本类只管「那一行还在
|
|
10
|
+
* 半路上」的那一刻:
|
|
12
11
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* (
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* is the alternative for long tasks.)
|
|
12
|
+
* - `run(key, produce)` 对同一个键至多调一次 `produce`;并发的同键调用拿到**同一个** promise(两份同体回执)。
|
|
13
|
+
* promise 一落定(成或败)条目即删 —— 之后的同键请求读行,不读这里。于是「同进程重试」与「跨副本 / 重启后
|
|
14
|
+
* 重试」读的是同一张账、拿到的是同一种回执,不存在「15 分钟内回缓存体、之后回行」的两个判官。
|
|
15
|
+
* - `peek(key)` 交出在飞的 promise(不产出)—— 让调用方在副门(rate/quota)**之前**合流,并发的那一条不吃新 429。
|
|
16
|
+
* SSE 腿把整条活流包在 `run()` 里,在飞期间到达的同键重试 peek 到它、等它落定。
|
|
17
|
+
* - TTL 只约束**条目**:一个落不了定的 promise(挂死的腿)最多被合流 `ttlMs`,过期即不再被 peek(之后的同键请求
|
|
18
|
+
* 去读行)。被遗弃的条目在表长过阈值后才扫(热路径无 O(n))。单线程事件循环语义:同一 tick 内没有 get→set 竞态。
|
|
21
19
|
*/
|
|
22
20
|
export declare class IdempotencyCache<T> {
|
|
23
21
|
private readonly ttlMs;
|
|
24
22
|
private readonly sweepThreshold;
|
|
25
23
|
private readonly entries;
|
|
26
24
|
constructor(ttlMs?: number, sweepThreshold?: number);
|
|
27
|
-
/**
|
|
25
|
+
/** 在飞(未过 TTL)的 promise,或 undefined —— 不产出。 */
|
|
28
26
|
peek(key: string): Promise<T> | undefined;
|
|
29
|
-
run(key: string | undefined, produce: () => Promise<T
|
|
27
|
+
run(key: string | undefined, produce: () => Promise<T>): Promise<T>;
|
|
30
28
|
get size(): number;
|
|
31
|
-
/** Return the
|
|
29
|
+
/** Return the in-flight promise if present AND within the TTL; drop it and return undefined if expired. */
|
|
32
30
|
private live;
|
|
33
31
|
/** Reclaim expired entries only once the map is large — avoids an O(n) scan on every call (council). */
|
|
34
32
|
private maybeSweep;
|
|
35
33
|
}
|
|
36
34
|
/**
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
* `
|
|
41
|
-
*
|
|
42
|
-
*
|
|
35
|
+
* `Idempotency-Key` 的**资源族**闭集 —— 键的作用域里那一位是**资源族,不是端点**。
|
|
36
|
+
*
|
|
37
|
+
* · `run` —— 一条 run(`POST /v1/tasks` · `/v1/tasks/stream` · `/v1/runs` · A2A `message/send` 都建同一种行;
|
|
38
|
+
* 同一条 run 换腿重试仍同键,不会重复计费)。同族的两个动词口(`/v1/runs/:id/steer` · `/interrupt`)
|
|
39
|
+
* 也按本族取键,收成引擎的 `inputId`(它们的去重属主是引擎的输入队列,不是本仓的行)。
|
|
40
|
+
* · `leader` —— 一条 leader run(`leader_run.idem_key`)。
|
|
41
|
+
* · `bake` —— 一次镜像烘焙(`image_bake.idem_key`)。
|
|
42
|
+
*
|
|
43
|
+
* 跨族同一个原始键**互不命中**(它们是不同资源表上的不同行)。
|
|
44
|
+
*/
|
|
45
|
+
export type IdemFamily = "run" | "leader" | "bake";
|
|
46
|
+
/**
|
|
47
|
+
* 一次提交的**调用方身份**(幂等的作用域就是它)—— 授权口建立的那两样:验过的 principal(`gatedPrincipal`)与凭据族
|
|
48
|
+
* (`source`:鉴权门由出示的服务凭据派生的系统名,`RouteRequestState.source` 是它唯一的赋值点)。由 {@link idemCallerOf}
|
|
49
|
+
* 从请求一处读出;键里用的那一位由 {@link idemIdentityOf} 一处定。
|
|
50
|
+
*/
|
|
51
|
+
export interface IdemCaller {
|
|
52
|
+
readonly principal: string | null | undefined;
|
|
53
|
+
readonly source: string | null | undefined;
|
|
54
|
+
}
|
|
55
|
+
/** 本次请求的 {@link IdemCaller} —— 一处读:验过的 principal + 鉴权门派生的凭据族(两样都是既有的席位,这里不另判身份)。 */
|
|
56
|
+
export declare function idemCallerOf(req: IncomingMessage, ctx: {
|
|
57
|
+
readonly deps: {
|
|
58
|
+
readonly config: ServiceConfig;
|
|
59
|
+
};
|
|
60
|
+
readonly req: {
|
|
61
|
+
readonly source: string | null;
|
|
62
|
+
};
|
|
63
|
+
}): IdemCaller;
|
|
64
|
+
/**
|
|
65
|
+
* 幂等的**身份位** —— 一条规则:`principal ?? source`。有验过的 principal 用 principal;没有 principal 用凭据族(按系统令牌区分的
|
|
66
|
+
* 多系统部署,请求都不带 principal);两者皆无 ⇒ 空(无凭据的单租部署)。两位调用方用同一个原始键得到两把不同的键 —— 不给跨租户
|
|
67
|
+
* 预言机,也不让 B 的提交被 A 的行吞掉。同一次提交的两把钥匙(`idem_key` 与 A2A 派生的 taskId)都吃这一位,作用域相同。
|
|
68
|
+
* 编码带种类字:principal 与凭据族是两个命名空间(一个叫 `sysA` 的 principal 与凭据族 `sysA` 不共用键空间)。
|
|
69
|
+
*/
|
|
70
|
+
export declare function idemIdentityOf(caller: IdemCaller): string;
|
|
71
|
+
/**
|
|
72
|
+
* 提交幂等键的**唯一**铸法:`sha256(身份位 ␟ family ␟ raw)` 的 hex(定宽 64)。
|
|
73
|
+
*
|
|
74
|
+
* - 身份位 = {@link idemIdentityOf}(`principal ?? source`)。
|
|
75
|
+
* - `family` = 资源族({@link IdemFamily}),不是端点。
|
|
76
|
+
* - 定宽 hash:落库列是 `VARCHAR(64)`,原始键与身份的长度上界都不进列宽;库里也不留调用方的原始键字面量。
|
|
77
|
+
* `\x1f`(单元分隔符)不可能出现在 HTTP 头值里,三段不会互相串位。
|
|
78
|
+
*/
|
|
79
|
+
export declare function idemKeyOf(raw: string, caller: IdemCaller, family: IdemFamily): string;
|
|
80
|
+
/**
|
|
81
|
+
* 在飞合流的键 = 提交幂等键 × 这条腿的**回执形**(`durable` / `sync`)× 这次请求自带的 taskId。
|
|
82
|
+
*
|
|
83
|
+
* 合流交出的是**首个请求那条腿的结局**,所以只有「同一条 run、同一种回执形、同一把 taskId」的请求才许合流:durable 腿
|
|
84
|
+
* 合到同步腿的在飞结局上会拿到 200 引擎结果而不是 202 收据;带另一个 taskId 的请求合进来就绕过了「key 指向别的行」
|
|
85
|
+
* 那条 409。形不同(或 taskId 不同)的请求各自走到建行,由店的原子赢或观察 + 唯一回执函数作答 —— 仍是一条 run,
|
|
86
|
+
* 各拿自己腿的回执形。同步腿与流式腿同形(流式腿把同一份体放进 `done.result`),合流照旧。
|
|
43
87
|
*/
|
|
44
|
-
export declare function
|
|
88
|
+
export declare function inflightKeyOf(idemKey: string, face: SubmitReplayFace, clientTaskId?: string): string;
|
|
45
89
|
/**
|
|
46
|
-
* 提交腿的**运行结局**(
|
|
90
|
+
* 提交腿的**运行结局**(在飞合流共享的那一只)—— `status` + `body` 是它一直以来的两位;S-528 起多一位
|
|
47
91
|
* `wiringManifest`。
|
|
48
92
|
*
|
|
49
|
-
* 🔴 **为什么回执挂在 outcome 上、不铸进 `body`**(codex 对抗复审 [medium],红先复现后修)
|
|
50
|
-
* `
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
93
|
+
* 🔴 **为什么回执挂在 outcome 上、不铸进 `body`**(codex 对抗复审 [medium],红先复现后修):在飞合流按资源族
|
|
94
|
+
* `run` 取键(`idemKeyOf`),同一条 run 的几条提交腿同族 —— 同一个 key 先走 `/v1/tasks/stream`、在它**还在跑**时
|
|
95
|
+
* 再走 `/v1/tasks`,合流到的是前者的那份结局。回执若铸在同步腿的体里,这条**确实产了帧**的 run 在非流式面上
|
|
96
|
+
* 就成了「键缺席」,而 present-iff 的读法会把它读成「这台引擎不给回执」。挂在 outcome 上 = 它是**这条 run 的
|
|
97
|
+
* 属性**(哪条腿跑的都一样),由各腿自己的应答构造器在送出那一刻按自己的 wire 形挂。
|
|
98
|
+
* 落定之后的同键重放不走这里:它读行,体是 run 行读面 + `replay: true`(`http/submit-replay.ts`)—— 回执是起手那一帧
|
|
99
|
+
* 的活捕获、不落行,所以重放体上**不带** `wiringManifest`(`replay: true` 就是那一形的判别位)。
|
|
55
100
|
*
|
|
56
|
-
* 🔴 **租户面安全(别在复审里当成漏)
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
* (各自重跑、各自归账),这一位从 BL-3 起就是这么设计的。
|
|
101
|
+
* 🔴 **租户面安全(别在复审里当成漏)**:合流的那份回执是按**第一位调用方**的 `explicitOperatorOk` 投的,
|
|
102
|
+
* 而键本身就按身份位分区(`idemKeyOf` 的 `principal ?? source`)⇒ 合流到它的只可能是**同一个** principal(无 principal 时
|
|
103
|
+
* 是同一个凭据族,两边的 `explicitOperatorOk` 都是假),同一份部署 operator 名单下求值恒同 ⇒ operator 面不可能经合流落到
|
|
104
|
+
* 租户手里。跨租户 / 跨系统同 key 是**不同的键**(各自重跑、各自归账),这一位从 BL-3 起就是这么设计的。
|
|
60
105
|
*
|
|
61
106
|
* 对照:同一条 200 体上的 `notice` 仍铸在 `body` 里,因为它说的是**本次请求**带没带 verify/cascade
|
|
62
|
-
* (
|
|
107
|
+
* (合流腿在 `prepareSpec` 之前就答了,结构上不知道这件事);回执说的是**那条 run**。两者的归属不同,
|
|
63
108
|
* 不是可以统一的两处写法。
|
|
64
109
|
*/
|
|
65
110
|
export interface SubmitOutcome {
|