@sema-agent/server 3.19.0 → 3.21.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/dist/boot/stores.js +8 -6
- package/dist/capabilities/scenarios.d.ts +2 -2
- package/dist/config.d.ts +14 -0
- package/dist/config.js +16 -2
- package/dist/fleet/fleet-bus.d.ts +2 -0
- package/dist/fleet/fleet-bus.js +34 -7
- package/dist/http/active-run-conflict.d.ts +68 -0
- package/dist/http/active-run-conflict.js +89 -0
- package/dist/http/routes/runs.js +3 -1
- package/dist/http/routes/tasks.js +5 -3
- package/dist/http/server.js +10 -26
- package/dist/leader/wire.js +10 -4
- package/dist/main.js +10 -2
- package/dist/plugins/file-run-store.js +16 -3
- package/dist/plugins/memory-run-store.js +14 -3
- package/dist/plugins/run-store-sql.js +6 -0
- package/dist/plugins/web-search.d.ts +14 -0
- package/dist/plugins/web-search.js +51 -2
- package/dist/run-local.js +7 -2
- package/dist/security.d.ts +17 -0
- package/dist/security.js +28 -0
- package/package.json +3 -3
package/dist/boot/stores.js
CHANGED
|
@@ -304,12 +304,14 @@ export async function openStores(ctx) {
|
|
|
304
304
|
}
|
|
305
305
|
}
|
|
306
306
|
const sessionStore = createSessionStore(config, backend, metrics); // S25: stale-affinity evict fingerprint
|
|
307
|
-
// S6
|
|
308
|
-
//
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
307
|
+
// 🪦 S6 启动门(旧):判据曾是 `!sessionStore.ownerOf`,core 2.11.0 给 TtlSessionStore 补了 owner 面后**恒假**。
|
|
308
|
+
// 2026-08-01 复审一度把判据改成「归属能否跨重启存活」以复活它 —— **那是错的,已撤**(cli [C46] 的
|
|
309
|
+
// 多租真机围栏当场证伪:`REQUIRE_PRINCIPAL=true` + 内存店必须**起得来**且真隔离)。
|
|
310
|
+
// 错在威胁模型:内存店重启后**会话数据本身也没了**,"先到者认领 session id" 认领到的是空壳,
|
|
311
|
+
// alice 的历史随进程消失 —— 归属丢失 ≠ 数据泄露。真正危险的是**数据留存而归属丢失**的 file 形,
|
|
312
|
+
// 那一形由下面那道 local 门拦住(它的论证里 "session/run CONTENT is durable there" 正是这个区别)。
|
|
313
|
+
// ⇒ 这道门防的威胁(store 没有归属能力)已被 core 2.11.0 消除,补偿该撤,不该换个判据续命。
|
|
314
|
+
// 判据跟目的走、不跟补偿走(cli [C46] 的原话,我抄下)。反向钉见 test/security.test.ts。
|
|
313
315
|
// The local in-memory backend HAS an ownerOf (LocalSessionStore), so the guard above passes — but its owner map is
|
|
314
316
|
// process-local + lost on restart, so it cannot DURABLY enforce multi-tenant ownership (post-restart a session id is
|
|
315
317
|
// re-claimable by whoever attaches first). Refuse REQUIRE_PRINCIPAL on it: a real multi-tenant
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type BackgroundAgentStore, type CheckpointStore, type PromptProvider, type Runner, type SkillSpec, type SubagentSpawnContext, type ToolSpec, type WebSearchConfig } from "@sema-agent/core";
|
|
1
|
+
import { type WebFetchConfig, type BackgroundAgentStore, type CheckpointStore, type PromptProvider, type Runner, type SkillSpec, type SubagentSpawnContext, type ToolSpec, type WebSearchConfig } from "@sema-agent/core";
|
|
2
2
|
import type { Metrics } from "../observability/metrics.js";
|
|
3
3
|
import type { Logger } from "../observability/logger.js";
|
|
4
4
|
import { GiteaClient } from "./repo-tools.js";
|
|
@@ -42,7 +42,7 @@ export interface ScenarioDeps {
|
|
|
42
42
|
* main.ts 构造一次注入)。在场 ⇒ 装配实参携 `webFetch.summarize`——带 prompt 的 WebFetch 走 cheap
|
|
43
43
|
* 模型摘要而非整页灌上下文([1870] L1 的 ~660× 放大就是这缝);缺席 ⇒ `webFetch` 键不铸(修前
|
|
44
44
|
* 字节形,core 落「summarization unavailable」诚实兜底)。 */
|
|
45
|
-
webFetchSummarize?:
|
|
45
|
+
webFetchSummarize?: NonNullable<NonNullable<WebFetchConfig["summarize"]>>;
|
|
46
46
|
metrics?: Metrics;
|
|
47
47
|
logger?: Logger;
|
|
48
48
|
/** OA backend coordinates for the `oa` scenario (read /api/v1, write /api/assets). */
|
package/dist/config.d.ts
CHANGED
|
@@ -11,6 +11,20 @@ export type { ServiceConfig, ScopedMcpServer, ImageBakeConfig, ServiceConfigFlat
|
|
|
11
11
|
* config overlay (config-center/facade.ts `applyRuntimeHot`) can re-derive the ENV BASELINE to revert to when center
|
|
12
12
|
* stops managing `autonomy` (a stale center override must not stick — see applyRuntimeHot). */
|
|
13
13
|
export declare function parseAutonomy(raw: string | undefined): Autonomy | undefined;
|
|
14
|
+
/** Numeric env with validation: a non-numeric value FAILS at startup instead of silently becoming `NaN`
|
|
15
|
+
* (which e.g. slips past the `runStaleSec` liveness guard and silently disables the reaper). */
|
|
16
|
+
export declare function numEnv(name: string, fallback: string): number;
|
|
17
|
+
/**
|
|
18
|
+
* 数值 env 的核心判据,**接受 raw 值**(而非自己去读 process.env),这样注入式配置也能复用同一条判据。
|
|
19
|
+
*
|
|
20
|
+
* 提出来的理由(2026-08-01 gap-sweep):`leader/wire.ts` 的四个资源旋钮各自手写了
|
|
21
|
+
* `Number(x) || default`,因为 numEnv 只会读 process.env、够不到它们注入进来的 env 对象。
|
|
22
|
+
* 结果是同一个失败模式在另一处复发,而且**回落方向更坏**:那边落到的是慷慨的默认值,
|
|
23
|
+
* 症状从「设了没生效」变成「想收紧却被松绑」。判据只有一份,谁都能调,才不会各写各的。
|
|
24
|
+
*
|
|
25
|
+
* `undefined`/空串 ⇒ 交给调用方的 fallback 语义处理(本函数只判「给了值但不是数」)。
|
|
26
|
+
*/
|
|
27
|
+
export declare function parseNumOrFail(name: string, raw: string | undefined): number;
|
|
14
28
|
export declare function drainConfigWarnings(): Array<{
|
|
15
29
|
env: string;
|
|
16
30
|
raw: string;
|
package/dist/config.js
CHANGED
|
@@ -73,8 +73,22 @@ function env2(primary, alias, fallback) {
|
|
|
73
73
|
}
|
|
74
74
|
/** Numeric env with validation: a non-numeric value FAILS at startup instead of silently becoming `NaN`
|
|
75
75
|
* (which e.g. slips past the `runStaleSec` liveness guard and silently disables the reaper). */
|
|
76
|
-
function numEnv(name, fallback) {
|
|
77
|
-
|
|
76
|
+
export function numEnv(name, fallback) {
|
|
77
|
+
return parseNumOrFail(name, env(name, fallback));
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* 数值 env 的核心判据,**接受 raw 值**(而非自己去读 process.env),这样注入式配置也能复用同一条判据。
|
|
81
|
+
*
|
|
82
|
+
* 提出来的理由(2026-08-01 gap-sweep):`leader/wire.ts` 的四个资源旋钮各自手写了
|
|
83
|
+
* `Number(x) || default`,因为 numEnv 只会读 process.env、够不到它们注入进来的 env 对象。
|
|
84
|
+
* 结果是同一个失败模式在另一处复发,而且**回落方向更坏**:那边落到的是慷慨的默认值,
|
|
85
|
+
* 症状从「设了没生效」变成「想收紧却被松绑」。判据只有一份,谁都能调,才不会各写各的。
|
|
86
|
+
*
|
|
87
|
+
* `undefined`/空串 ⇒ 交给调用方的 fallback 语义处理(本函数只判「给了值但不是数」)。
|
|
88
|
+
*/
|
|
89
|
+
export function parseNumOrFail(name, raw) {
|
|
90
|
+
if (raw === undefined)
|
|
91
|
+
return NaN;
|
|
78
92
|
const n = Number(raw);
|
|
79
93
|
if (!Number.isFinite(n))
|
|
80
94
|
throw new Error(`env ${name}="${raw}" must be a number`);
|
|
@@ -291,6 +291,8 @@ export declare class FleetEventBus {
|
|
|
291
291
|
releaseBackgroundChildLane(scope: string | undefined, childUuid: string): void;
|
|
292
292
|
/** run-leg 车道:该子代是否已有 BCE 真行(有 ⇒ 让位,不铸复合 id 行)。 */
|
|
293
293
|
backgroundChildLaneRow(scope: string | undefined, childUuid: string): string | undefined;
|
|
294
|
+
/** [2268] 让位迁移料的读面:迁移「只补缺席位」需要看得见存活行现有什么(BCE 是权威车道,不覆盖)。 */
|
|
295
|
+
taskRow(id: string): Readonly<FleetTaskRow> | undefined;
|
|
294
296
|
/** Upsert a workflow row (MERGE by `id`) + fan out. */
|
|
295
297
|
publishWorkflow(delta: Partial<FleetWorkflowRow> & {
|
|
296
298
|
id: string;
|
package/dist/fleet/fleet-bus.js
CHANGED
|
@@ -86,6 +86,10 @@ export class FleetEventBus {
|
|
|
86
86
|
backgroundChildLaneRow(scope, childUuid) {
|
|
87
87
|
return this.bceClaims.get(FleetEventBus.bceKey(scope, childUuid));
|
|
88
88
|
}
|
|
89
|
+
/** [2268] 让位迁移料的读面:迁移「只补缺席位」需要看得见存活行现有什么(BCE 是权威车道,不覆盖)。 */
|
|
90
|
+
taskRow(id) {
|
|
91
|
+
return this.tasks.get(id);
|
|
92
|
+
}
|
|
89
93
|
// └────────────────────────────────────────────────────────────────────────────────────────────────┘
|
|
90
94
|
/** Upsert a workflow row (MERGE by `id`) + fan out. */
|
|
91
95
|
publishWorkflow(delta) {
|
|
@@ -172,20 +176,43 @@ export function fleetRunPublisher(bus, run) {
|
|
|
172
176
|
* registry ⇒ 永远没有 BCE 真行,让位就等于让这些 agent 从面板消失。那一形保留复合行,并在帧上带
|
|
173
177
|
* `sourceLane:"run-leg"` 明示这是过渡形——消费端不必再自己发明集合级去重判据。
|
|
174
178
|
*/
|
|
175
|
-
const yieldToBackgroundChildLane = (childTaskId, cid) => {
|
|
176
|
-
|
|
179
|
+
const yieldToBackgroundChildLane = (childTaskId, cid, tickName) => {
|
|
180
|
+
const bceRowId = bus.backgroundChildLaneRow(run.scope, childTaskId);
|
|
181
|
+
if (bceRowId === undefined)
|
|
177
182
|
return false;
|
|
178
183
|
if (children.has(cid)) {
|
|
179
184
|
// 我们早于 claim 铸过一行 —— 清场(BCE 侧的后缀扫是同一意图的反应式兜底,两者幂等叠加无害)
|
|
180
185
|
bus.removeTask(cid);
|
|
181
186
|
children.delete(cid);
|
|
182
|
-
childStartedAt
|
|
187
|
+
// ⚠️ childStartedAt 故意不删 —— 它是下面 elapsed 供给的计时基准([2268])
|
|
183
188
|
for (const [k, v] of childByToolCall)
|
|
184
189
|
if (v === cid)
|
|
185
190
|
childByToolCall.delete(k);
|
|
186
191
|
}
|
|
192
|
+
// 🔴 [2268] 迁移料([2269] 认领「迁移」臂):此前让位=删「有名的 run-leg 行」留「无名的 BCE 行」
|
|
193
|
+
// ((unnamed)/0s/详情空白三症状同源)。tick 手上的显示料补给存活行,**只补缺席位** —— BCE 是权威
|
|
194
|
+
// 车道,它已有的 name/真 elapsed 一律不碰。elapsed 供给是持续的(本闸每拍都过),不是一次性冻结;
|
|
195
|
+
// elapsedDonor 记住「这行的 elapsed 是我们供的」,避免把 BCE 自己的真 elapsed 误当缺席重置。
|
|
196
|
+
// transcriptId 不在 run-leg tick 手上,迁不了(诚实范围;那半归 BCE 行自身后续 tick)。
|
|
197
|
+
const row = bus.taskRow(bceRowId);
|
|
198
|
+
if (row) {
|
|
199
|
+
const donorName = row.name === undefined && tickName !== undefined ? redactSecrets(tickName) : undefined;
|
|
200
|
+
if (!childStartedAt.has(cid))
|
|
201
|
+
childStartedAt.set(cid, Date.now());
|
|
202
|
+
if (elapsedDonor.has(cid) || row.elapsedMs === undefined || row.elapsedMs === 0)
|
|
203
|
+
elapsedDonor.add(cid);
|
|
204
|
+
const elapsed = elapsedDonor.has(cid) ? Date.now() - childStartedAt.get(cid) : undefined;
|
|
205
|
+
if (donorName !== undefined || elapsed !== undefined) {
|
|
206
|
+
bus.publishTask({
|
|
207
|
+
id: bceRowId,
|
|
208
|
+
...(donorName !== undefined ? { name: donorName, agentType: donorName } : {}),
|
|
209
|
+
...(elapsed !== undefined ? { elapsedMs: elapsed } : {}),
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
}
|
|
187
213
|
return true;
|
|
188
214
|
};
|
|
215
|
+
const elapsedDonor = new Set(); // [2268] 我们在为哪些 cid 供 elapsed(见上)
|
|
189
216
|
const sumTokens = (usage) => {
|
|
190
217
|
const u = usage;
|
|
191
218
|
if (!u)
|
|
@@ -219,8 +246,8 @@ export function fleetRunPublisher(bus, run) {
|
|
|
219
246
|
else if (ev.type === "task_progress" && ev.taskId) {
|
|
220
247
|
// core 1.147 subagent usage tick (per-turn, cumulative) → a CHILD fleet row nested under the run.
|
|
221
248
|
const cid = childId(ev.taskId);
|
|
222
|
-
if (yieldToBackgroundChildLane(ev.taskId, cid))
|
|
223
|
-
return; // [2070]① BCE 真行在场 ⇒ 让位不铸
|
|
249
|
+
if (yieldToBackgroundChildLane(ev.taskId, cid, ev.name))
|
|
250
|
+
return; // [2070]① BCE 真行在场 ⇒ 让位不铸([2268] 随迁显示料)
|
|
224
251
|
children.add(cid);
|
|
225
252
|
// BC-2 (core 1.151): use the subagent's sanitized display `name` (taskName/agent-type) for the child row.
|
|
226
253
|
// [2070]③:name 缺席时**不铸**(此前回落 taskId=子代 uuid,人眼乱码)——行 IDENTITY 一直是 `cid`
|
|
@@ -273,8 +300,8 @@ export function fleetRunPublisher(bus, run) {
|
|
|
273
300
|
if (ev.type !== "task_progress" || !ev.taskId)
|
|
274
301
|
return;
|
|
275
302
|
const cid = childId(ev.taskId);
|
|
276
|
-
if (yieldToBackgroundChildLane(ev.taskId, cid))
|
|
277
|
-
return; // [2070]① BCE 真行在场 ⇒ 让位不铸
|
|
303
|
+
if (yieldToBackgroundChildLane(ev.taskId, cid, ev.name))
|
|
304
|
+
return; // [2070]① BCE 真行在场 ⇒ 让位不铸([2268] 随迁显示料)
|
|
278
305
|
children.add(cid);
|
|
279
306
|
const parentId = ev.parentTaskId && ev.parentTaskId !== run.rootTaskId ? childId(ev.parentTaskId) : run.runId;
|
|
280
307
|
// [WF2-A] redact the forwarded subagent name for parity with the top-level run name (see onEvent above).
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* [2252]/[2255]①/[2257] 追加件:409 `conflict.session_active_run` 的**真出路材料**装配(单源)。
|
|
3
|
+
*
|
|
4
|
+
* 事故形:park(suspended/needs_review)占着 session claim,客户端每条新提交吃 409,而旧文案只教
|
|
5
|
+
* cancel(毁灭当前 run)。对 parked 会话,「继续」的唯一合法动作是去**对的** resume 入口决议——但四条
|
|
6
|
+
* resume 入口各认各的 gate.kind,客户端不知道等的是哪种门,只能试错。本模块把答案放进 409 响应体:
|
|
7
|
+
* activeTaskStatus(区分「插话/取消」与「决议/取消」两族出路)
|
|
8
|
+
* pendingGate { kind, decidePath }(parked 且真有 pending checkpoint 时)
|
|
9
|
+
*
|
|
10
|
+
* 🔴 token 永不上 wire(approvals-assistant 纪律:resume 寻址=sessionId,checkpoint token 是秘密能力)。
|
|
11
|
+
* 🔴 失败方向:材料是 best-effort 增强——store 面任何失败都不得挡 409 本体,退化为旧形状,绝不 throw。
|
|
12
|
+
* 🔴 gateKind→入口映射是**这张表在仓里的唯一函数形**;它的知识此前散在 server.ts 三处 guard 文案里
|
|
13
|
+
* (`gate_not_tool_approval`/`gate_not_resumable`/`wake.gate_pending` 的指路句)。三处文案与本表若漂移,
|
|
14
|
+
* test/session-active-conflict-materials.test.ts 的分门用例会红。认不出的 kind ⇒ null(诚实缺席,不铸假门)。
|
|
15
|
+
*/
|
|
16
|
+
/** 与 runs.ts/tasks.ts 三个 409 位共享的旧文案(byte-frozen:api-error-text-freeze 门认这句)。 */
|
|
17
|
+
export declare const ACTIVE_RUN_CONFLICT_BASE_TEXT = "session already has an active run \u2014 POST /v1/runs/{activeTaskId}/cancel stops it (same-instance interactive runs abort immediately)";
|
|
18
|
+
export interface ActiveRunConflictBody {
|
|
19
|
+
error: string;
|
|
20
|
+
errorCode: "conflict.session_active_run";
|
|
21
|
+
activeTaskId: string | null;
|
|
22
|
+
activeTaskStatus?: string;
|
|
23
|
+
pendingGate?: {
|
|
24
|
+
kind: string;
|
|
25
|
+
decidePath: string;
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/** gate.kind → 它的那一个 resume 入口(sessionId/taskId 寻址,无秘密)。 */
|
|
29
|
+
export declare function resumeEntryForGate(kind: string, ids: {
|
|
30
|
+
sessionId: string;
|
|
31
|
+
taskId: string;
|
|
32
|
+
}): string | null;
|
|
33
|
+
/** token 泛型:真身是 branded CheckpointToken(秘密能力,只在本函数内部流转,绝不进响应体)。 */
|
|
34
|
+
/**
|
|
35
|
+
* SSE 车道的 done 帧 result(拒绝形)。与 409 body **同一铸体处** —— 此前 tasks.ts 在 res.write 里
|
|
36
|
+
* 手搓内联对象挑键,形状没有名字、没有类型、只活在那一行字符串里(conditional-spread 死键的老坑形)。
|
|
37
|
+
* 这里铸,SDK 的 done 帧 result 类型与本形同车对齐(sdk-api-guarantee-duty),live-contract 套件对已发包真跑。
|
|
38
|
+
*/
|
|
39
|
+
export interface ActiveRunConflictDoneResult {
|
|
40
|
+
status: "failed";
|
|
41
|
+
errorMessage: string;
|
|
42
|
+
activeTaskId: string | null;
|
|
43
|
+
activeTaskStatus?: string;
|
|
44
|
+
pendingGate?: {
|
|
45
|
+
kind: string;
|
|
46
|
+
decidePath: string;
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
export declare function toDoneFrameResult(body: ActiveRunConflictBody): ActiveRunConflictDoneResult;
|
|
50
|
+
interface ConflictProbeDeps<TToken> {
|
|
51
|
+
runStore?: {
|
|
52
|
+
getRun?: (id: string) => Promise<{
|
|
53
|
+
status?: string;
|
|
54
|
+
} | null | undefined>;
|
|
55
|
+
} | undefined;
|
|
56
|
+
checkpointStore?: {
|
|
57
|
+
peekPendingScope?: (sessionId: string) => Promise<string | null | undefined>;
|
|
58
|
+
findPendingTokenBySession?: (sessionId: string, scope?: string) => Promise<TToken | null | undefined>;
|
|
59
|
+
get?: (token: TToken) => Promise<{
|
|
60
|
+
gate?: {
|
|
61
|
+
kind?: string;
|
|
62
|
+
};
|
|
63
|
+
} | null | undefined>;
|
|
64
|
+
} | undefined;
|
|
65
|
+
}
|
|
66
|
+
export declare function buildActiveRunConflict<TToken>(deps: ConflictProbeDeps<TToken>, sessionId: string, activeTaskId: string | null | undefined): Promise<ActiveRunConflictBody>;
|
|
67
|
+
export {};
|
|
68
|
+
//# sourceMappingURL=active-run-conflict.d.ts.map
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* [2252]/[2255]①/[2257] 追加件:409 `conflict.session_active_run` 的**真出路材料**装配(单源)。
|
|
3
|
+
*
|
|
4
|
+
* 事故形:park(suspended/needs_review)占着 session claim,客户端每条新提交吃 409,而旧文案只教
|
|
5
|
+
* cancel(毁灭当前 run)。对 parked 会话,「继续」的唯一合法动作是去**对的** resume 入口决议——但四条
|
|
6
|
+
* resume 入口各认各的 gate.kind,客户端不知道等的是哪种门,只能试错。本模块把答案放进 409 响应体:
|
|
7
|
+
* activeTaskStatus(区分「插话/取消」与「决议/取消」两族出路)
|
|
8
|
+
* pendingGate { kind, decidePath }(parked 且真有 pending checkpoint 时)
|
|
9
|
+
*
|
|
10
|
+
* 🔴 token 永不上 wire(approvals-assistant 纪律:resume 寻址=sessionId,checkpoint token 是秘密能力)。
|
|
11
|
+
* 🔴 失败方向:材料是 best-effort 增强——store 面任何失败都不得挡 409 本体,退化为旧形状,绝不 throw。
|
|
12
|
+
* 🔴 gateKind→入口映射是**这张表在仓里的唯一函数形**;它的知识此前散在 server.ts 三处 guard 文案里
|
|
13
|
+
* (`gate_not_tool_approval`/`gate_not_resumable`/`wake.gate_pending` 的指路句)。三处文案与本表若漂移,
|
|
14
|
+
* test/session-active-conflict-materials.test.ts 的分门用例会红。认不出的 kind ⇒ null(诚实缺席,不铸假门)。
|
|
15
|
+
*/
|
|
16
|
+
/** 与 runs.ts/tasks.ts 三个 409 位共享的旧文案(byte-frozen:api-error-text-freeze 门认这句)。 */
|
|
17
|
+
export const ACTIVE_RUN_CONFLICT_BASE_TEXT = "session already has an active run — POST /v1/runs/{activeTaskId}/cancel stops it (same-instance interactive runs abort immediately)";
|
|
18
|
+
/** gate.kind → 它的那一个 resume 入口(sessionId/taskId 寻址,无秘密)。 */
|
|
19
|
+
export function resumeEntryForGate(kind, ids) {
|
|
20
|
+
switch (kind) {
|
|
21
|
+
case "tool_approval":
|
|
22
|
+
case "human":
|
|
23
|
+
case "irreversible_ask":
|
|
24
|
+
case "policy_ask":
|
|
25
|
+
return `/v1/approvals/${ids.sessionId}/decide`;
|
|
26
|
+
case "plan_review":
|
|
27
|
+
return `/v1/assistant/tasks/${ids.taskId}/plan_review`;
|
|
28
|
+
case "resource_limit":
|
|
29
|
+
return `/v1/assistant/tasks/${ids.taskId}/resume`;
|
|
30
|
+
case "task_done":
|
|
31
|
+
return `/v1/sessions/${ids.sessionId}/wake`;
|
|
32
|
+
default:
|
|
33
|
+
return null; // 未知门型:宁缺毋假 —— 客户端仍有 activeTaskStatus + cancel 这条保底真路
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
export function toDoneFrameResult(body) {
|
|
37
|
+
return {
|
|
38
|
+
status: "failed",
|
|
39
|
+
errorMessage: body.error,
|
|
40
|
+
activeTaskId: body.activeTaskId,
|
|
41
|
+
...(body.activeTaskStatus !== undefined ? { activeTaskStatus: body.activeTaskStatus } : {}),
|
|
42
|
+
...(body.pendingGate !== undefined ? { pendingGate: body.pendingGate } : {}),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
export async function buildActiveRunConflict(deps, sessionId, activeTaskId) {
|
|
46
|
+
const base = {
|
|
47
|
+
error: ACTIVE_RUN_CONFLICT_BASE_TEXT,
|
|
48
|
+
errorCode: "conflict.session_active_run",
|
|
49
|
+
activeTaskId: activeTaskId ?? null,
|
|
50
|
+
};
|
|
51
|
+
if (!activeTaskId)
|
|
52
|
+
return base;
|
|
53
|
+
try {
|
|
54
|
+
const row = await deps.runStore?.getRun?.(activeTaskId);
|
|
55
|
+
const status = row?.status;
|
|
56
|
+
if (status !== "running" && status !== "suspended" && status !== "needs_review")
|
|
57
|
+
return base;
|
|
58
|
+
if (status === "running") {
|
|
59
|
+
return {
|
|
60
|
+
...base,
|
|
61
|
+
error: "session already has an active run — POST /v1/runs/{activeTaskId}/steer injects a message into the running turn (queued, applied at the next turn boundary), or POST /v1/runs/{activeTaskId}/cancel stops it (same-instance interactive runs abort immediately)",
|
|
62
|
+
activeTaskStatus: status,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
// parked:找 pending checkpoint 的门型,指到它的那一个 resume 入口
|
|
66
|
+
const cs = deps.checkpointStore;
|
|
67
|
+
let pendingGate;
|
|
68
|
+
if (cs?.findPendingTokenBySession) {
|
|
69
|
+
const scope = (await cs.peekPendingScope?.(sessionId)) ?? undefined;
|
|
70
|
+
const token = scope === null ? null : await cs.findPendingTokenBySession(sessionId, scope);
|
|
71
|
+
const kind = token ? (await cs.get?.(token))?.gate?.kind : undefined;
|
|
72
|
+
const decidePath = kind ? resumeEntryForGate(kind, { sessionId, taskId: activeTaskId }) : null;
|
|
73
|
+
if (kind && decidePath)
|
|
74
|
+
pendingGate = { kind, decidePath };
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
...base,
|
|
78
|
+
error: pendingGate
|
|
79
|
+
? `session already has an active run — it is parked waiting for a decision (${status}); resolve it via POST ${pendingGate.decidePath}, or POST /v1/runs/{activeTaskId}/cancel abandons it`
|
|
80
|
+
: `session already has an active run — it is parked (${status}); POST /v1/runs/{activeTaskId}/cancel abandons it (its pending decision could not be located on this instance)`,
|
|
81
|
+
activeTaskStatus: status,
|
|
82
|
+
...(pendingGate ? { pendingGate } : {}),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return base; // 材料装配的任何失败都退化为旧形状 —— 增强绝不成为新故障点
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
//# sourceMappingURL=active-run-conflict.js.map
|
package/dist/http/routes/runs.js
CHANGED
|
@@ -10,6 +10,7 @@ import { scopedIdempotencyKey } from "../idempotency.js";
|
|
|
10
10
|
import { streamSseLog } from "../sse-log.js";
|
|
11
11
|
import { normalizeRunEventType } from "../../trace/project.js";
|
|
12
12
|
import { sendJson, sendError, httpErrorCode, sseHeaders } from "../send.js";
|
|
13
|
+
import { buildActiveRunConflict } from "../active-run-conflict.js";
|
|
13
14
|
import { headerStr, gatedPrincipal, explicitOperatorOk } from "../principal-gate.js";
|
|
14
15
|
// server.ts 侧的 routeLabel / isBillableSubmitPath 仍要用下面这些正则,故本模块导出(方向恒为 server.ts → routes/*)。
|
|
15
16
|
export const RUN_ID_RE = /^\/v1\/runs\/([^/]+)(\/events)?$/;
|
|
@@ -161,7 +162,8 @@ async function handleRunsBody(req, res, url, ctx, miss) {
|
|
|
161
162
|
if (clientTaskId && created.activeTaskId === clientTaskId) {
|
|
162
163
|
return { status: 202, body: { taskId: clientTaskId, sessionId, status: "running" } };
|
|
163
164
|
}
|
|
164
|
-
|
|
165
|
+
// [2255]① 真出路材料:activeTaskStatus + parked 时 pendingGate{kind,decidePath}(best-effort,失败退旧形)
|
|
166
|
+
return { status: 409, body: await buildActiveRunConflict({ runStore, checkpointStore: deps.checkpointStore }, sessionId, created.activeTaskId) };
|
|
165
167
|
}
|
|
166
168
|
// Durable F4: persist the resume rebuild inputs (sessionId-keyed) so an operator can resume from any
|
|
167
169
|
// replica even after this worker is gone — core's checkpoint blob can't carry service scenario context.
|
|
@@ -10,6 +10,7 @@ import { turnEndEventData, contextUsageEventData, toolStartEventData, toolEndEve
|
|
|
10
10
|
import { cascadeConfig, runMeta } from "../run-meta.js";
|
|
11
11
|
import { scopedIdempotencyKey } from "../idempotency.js";
|
|
12
12
|
import { sendJson, sendError, sseHeaders } from "../send.js";
|
|
13
|
+
import { buildActiveRunConflict, toDoneFrameResult } from "../active-run-conflict.js";
|
|
13
14
|
import { headerStr, gatedPrincipal } from "../principal-gate.js";
|
|
14
15
|
export async function handleTasks(req, res, url, ctx) {
|
|
15
16
|
const miss = { fell: false };
|
|
@@ -216,10 +217,11 @@ async function handleTasksBody(req, res, url, ctx, miss) {
|
|
|
216
217
|
if (created.ok)
|
|
217
218
|
deps.sessionTitler?.maybeTitle(prepared.spec.sessionId, prepared.spec.objective, typeof prepared.spec.model === "string" ? prepared.spec.model : prepared.spec.model?.id); // fire-and-forget; in-titler dedupe;model 跟 turn([1992]②)
|
|
218
219
|
if (!created.ok) {
|
|
219
|
-
const conflict = {
|
|
220
|
+
const conflict = await buildActiveRunConflict({ runStore: deps.runStore, checkpointStore: deps.checkpointStore }, prepared.spec.sessionId, created.activeTaskId); // [2255]① 真出路材料
|
|
220
221
|
// Headers are already SSE — encode the rejection as the stream's terminal event; the 409 in
|
|
221
222
|
// the returned resp is for a deduplicated concurrent caller (and is never idem-cached).
|
|
222
|
-
|
|
223
|
+
// done 帧 additive 携带同一份材料 —— 形状铸在 toDoneFrameResult(有名字有类型),不在这行内联挑键。
|
|
224
|
+
res.write(`data: ${JSON.stringify({ type: "done", result: toDoneFrameResult(conflict) })}\n\n`);
|
|
223
225
|
return { status: 409, body: conflict };
|
|
224
226
|
}
|
|
225
227
|
durableTaskId = tid;
|
|
@@ -971,7 +973,7 @@ async function handleTasksBody(req, res, url, ctx, miss) {
|
|
|
971
973
|
const tid = uuidv7();
|
|
972
974
|
const created = await deps.runStore.createRun(tid, prepared.spec.sessionId, principal ?? null, deps.instanceId ?? "default", runMeta(prepared, source));
|
|
973
975
|
if (!created.ok)
|
|
974
|
-
return { status: 409, body: {
|
|
976
|
+
return { status: 409, body: await buildActiveRunConflict({ runStore: deps.runStore, checkpointStore: deps.checkpointStore }, prepared.spec.sessionId, created.activeTaskId) }; // [2255]① 真出路材料
|
|
975
977
|
deps.sessionTitler?.maybeTitle(prepared.spec.sessionId, prepared.spec.objective, typeof prepared.spec.model === "string" ? prepared.spec.model : prepared.spec.model?.id); // fire-and-forget;model 跟 turn([1992]②)
|
|
976
978
|
durableTaskId = tid;
|
|
977
979
|
}
|
package/dist/http/server.js
CHANGED
|
@@ -5,7 +5,7 @@ import { uuidv7, isThinkingLevel, expandTiers, resumeWithVerification, Checkpoin
|
|
|
5
5
|
import { decideParkedAgent, findParkedAgentForCheckpoint } from "../parked-decide.js";
|
|
6
6
|
import { matchCatalogModel } from "../model-select.js";
|
|
7
7
|
import {} from "../config-center/facade.js";
|
|
8
|
-
import { HttpError, principalFrom, verifiedPrincipal, setSsoPrincipal, ssoVerifiedPrincipal, setSsoScope, isUuidV7, verifyDirectDoorProof } from "../security.js";
|
|
8
|
+
import { HttpError, principalFrom, verifiedPrincipal, setSsoPrincipal, ssoVerifiedPrincipal, setSsoScope, isUuidV7, verifyDirectDoorProof, isDestructiveSessionWrite } from "../security.js";
|
|
9
9
|
import {} from "../memory-sync.js";
|
|
10
10
|
import { MAX_SETTINGS_OUTPUT_STYLE_CHARS, MAX_SETTINGS_PERMISSION_RULES, MAX_SETTINGS_ENV_VARS, MAX_SETTINGS_ENV_KEY_CHARS, MAX_SETTINGS_ENV_VALUE_CHARS } from "../task-settings.js";
|
|
11
11
|
import { parseHooksConfig } from "../hooks/hook-runner.js";
|
|
@@ -533,32 +533,16 @@ export function createHttpServer(rawDeps) {
|
|
|
533
533
|
sendError(res, 503, "state.model_roster_pending", "model_roster_pending", { message: "this worker has no model yet (waiting for the first effective-config pull to land the roster) — retry shortly" });
|
|
534
534
|
return;
|
|
535
535
|
}
|
|
536
|
-
// E6
|
|
537
|
-
//
|
|
538
|
-
//
|
|
539
|
-
if (req.method === "PUT" &&
|
|
540
|
-
!anyServiceAuth &&
|
|
541
|
-
!deps.config.allowUnauthedWrites &&
|
|
542
|
-
/^\/v1\/sessions\/[^/]+\/policy$/.test(url)) {
|
|
543
|
-
sendError(res, 503, "auth.service_token_required", "this worker requires a service auth token (set SERVICE_AUTH_TOKEN) before accepting session-policy writes");
|
|
544
|
-
return;
|
|
545
|
-
}
|
|
546
|
-
// 🔴 E6 的**兄弟路径**(2026-08-01 缝合审):上面那道门只挂在 policy 的 PUT 腿上,而 session-sync 的
|
|
547
|
-
// import 提交腿(`routes/session-sync.ts` 的 ② policy 段)对**同一个 sessionPolicyStore** 调 `putRules`。
|
|
548
|
-
// 该腿的授权门是 `sessionOwnerScopeForWrite`,在「无 service token + 未开 ALLOW_UNAUTHED_WRITES +
|
|
549
|
-
// 未开 REQUIRE_PRINCIPAL」这一形下是 fleet-wide ⇒ 集群内**任意**调用方可对**任意** session 走两阶段
|
|
550
|
-
// import,顺带重写它的 policy 规则。`operatorOk` 走 explicitOperatorOk(无 operator 名单时为 false)
|
|
551
|
-
// ⇒ 只能 tighten 不能放宽 —— 但上面那道门自己写明的威胁就是「forged principal 可以 **tighten/DoS**
|
|
552
|
-
// 一个会话的工具」,严丝合缝落在同一句话上。
|
|
536
|
+
// E6 fail-closed:对**既有会话**的破坏性写,在「无 service token + 未开 ALLOW_UNAUTHED_WRITES」
|
|
537
|
+
// 这一形下一律拒。此形下 `sessionOwnerScopeForWrite` 判 fleet-wide ⇒ 集群内任意调用方可对任意
|
|
538
|
+
// session 动手;policy 腿担心的是「forged principal 能 tighten/DoS 一个会话的工具」。
|
|
553
539
|
//
|
|
554
|
-
//
|
|
555
|
-
//
|
|
556
|
-
//
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
/^\/v1\/sessions\/[^/]+\/sync\/import$/.test(url)) {
|
|
561
|
-
sendError(res, 503, "auth.service_token_required", "this worker requires a service auth token (set SERVICE_AUTH_TOKEN) before accepting a session-sync import");
|
|
540
|
+
// 🔴 2026-08-01 复审:这里原本是**逐条路由白名单**(PUT …/policy 与 POST …/sync/import 两块),
|
|
541
|
+
// 而 `DELETE /v1/sessions/:id`(删整条会话)**不在其中** —— 同一威胁形、爆炸半径更大却没门。
|
|
542
|
+
// 改成族判定(`isDestructiveSessionWrite`),新增破坏性会话路由自动落进门内。
|
|
543
|
+
// import 腿仍拦在 **Phase A**(建 staging 之前),因为族判据匹配的就是那个 URL,不是 Phase B。
|
|
544
|
+
if (isDestructiveSessionWrite(req.method ?? "", url) && !anyServiceAuth && !deps.config.allowUnauthedWrites) {
|
|
545
|
+
sendError(res, 503, "auth.service_token_required", "this worker requires a service auth token (set SERVICE_AUTH_TOKEN) before accepting destructive session writes (policy / sync-import / fork / delete)");
|
|
562
546
|
return;
|
|
563
547
|
}
|
|
564
548
|
// design/158 A9:能力/场景/模型目录域(routes/capabilities.ts)。位置=全局 service-credential 门之后、
|
package/dist/leader/wire.js
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* network; the MVP seeds the base from the Coordinator (no worker→remote net). MVP Planner = caller-provided
|
|
12
12
|
* `subtasks` (an LLM DAG-planner is a follow-up).
|
|
13
13
|
*/
|
|
14
|
+
import { parseNumOrFail } from "../config.js";
|
|
14
15
|
import { execFileSync } from "node:child_process";
|
|
15
16
|
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
16
17
|
import { join } from "node:path";
|
|
@@ -104,14 +105,19 @@ export async function stageWorkerEnv(env, opts) {
|
|
|
104
105
|
* (URL) silently killed long autonomous builds or expired the diff upload mid-run. core's turn cap is now a
|
|
105
106
|
* high net (10000), so the leader gives each worker an explicit triple bound; a sub-task's own limits override.
|
|
106
107
|
*/
|
|
108
|
+
// 🔴 2026-08-01 gap-sweep:四个旋钮原为 `Number(x) || default` —— 非法值(`"20usd"`/`"1800000ms"`)
|
|
109
|
+
// 得 NaN 后**静默落到慷慨的默认值**,方向是**松绑**:运维想把 worker 预算从 $100 收到 $20,实际拿到
|
|
110
|
+
// $100(5 倍);想把寿命从 24h 收到 30min,实际拿到 24h,而 leaderTimeoutMs 还级联 presignTtlSec
|
|
111
|
+
// ⇒ diff 上传预签名 URL 的暴露窗口一起放大。改走与 numEnv 同源的 parseNumOrFail:非法值当场报错。
|
|
112
|
+
// 刻意**零行为变更**:`|| default` 保留,所以未设与显式 0 的既有语义一个没动。
|
|
107
113
|
export function leaderResourceConfig(env = process.env) {
|
|
108
|
-
const leaderTimeoutMs = Math.max(600_000, Math.floor(
|
|
109
|
-
const workerMaxTurns = Math.floor(
|
|
110
|
-
const workerBudgetUsd =
|
|
114
|
+
const leaderTimeoutMs = Math.max(600_000, Math.floor(parseNumOrFail("LEADER_TIMEOUT_MS", env.LEADER_TIMEOUT_MS) || 0) || 86_400_000); // default 24h (was 10min)
|
|
115
|
+
const workerMaxTurns = Math.floor(parseNumOrFail("LEADER_WORKER_MAX_TURNS", env.LEADER_WORKER_MAX_TURNS) || 0) || 10_000;
|
|
116
|
+
const workerBudgetUsd = (parseNumOrFail("LEADER_WORKER_BUDGET_USD", env.LEADER_WORKER_BUDGET_USD) || 0) || 100;
|
|
111
117
|
const resourceSuspendOn = String(env.LEADER_RESOURCE_SUSPEND ?? "").toLowerCase() === "true";
|
|
112
118
|
// Per-slice window: a fraction of the total so a worker suspends+resumes several times across its budget
|
|
113
119
|
// (default = total/4, min $1). Smaller window ⇒ more, shorter slices ⇒ progress saved more often.
|
|
114
|
-
const sliceMaxCostUsd =
|
|
120
|
+
const sliceMaxCostUsd = (parseNumOrFail("LEADER_WORKER_SLICE_BUDGET_USD", env.LEADER_WORKER_SLICE_BUDGET_USD) || 0) || Math.max(1, workerBudgetUsd / 4);
|
|
115
121
|
return {
|
|
116
122
|
leaderTimeoutMs,
|
|
117
123
|
workerMaxTurns,
|
package/dist/main.js
CHANGED
|
@@ -7,7 +7,7 @@ import { posIntEnv } from "./session-watch.js";
|
|
|
7
7
|
import { selectEnvironmentTool } from "./capabilities/select-environment-tool.js";
|
|
8
8
|
import { subagentSendUserFileExtraTools } from "./capabilities/send-user-file-tool.js";
|
|
9
9
|
import { brainSummary } from "./brain.js";
|
|
10
|
-
import { loadConfig, logConfigDiagnostics, resolveBindHost, splitLocalRoots, parseCapEnv } from "./config.js";
|
|
10
|
+
import { loadConfig, logConfigDiagnostics, resolveBindHost, splitLocalRoots, parseCapEnv, numEnv } from "./config.js";
|
|
11
11
|
import { isPricingConfigured } from "./budget.js"; // 缝合审 M3:capabilities.pricingConfigured 的单一真源判据
|
|
12
12
|
import { drainNumEnvWarnings } from "./plugins/remote-shell.js";
|
|
13
13
|
import { ensureChildSessionDurableWithPromotion } from "./plugins/session-store.js";
|
|
@@ -362,7 +362,12 @@ async function main() {
|
|
|
362
362
|
// Per-task wall-clock override (seconds). 0/absent = keep the built-in 600 (1500 council/debate/team) —
|
|
363
363
|
// the override only ever RAISES (Math.max at the use site), so a misconfigured low value cannot shrink
|
|
364
364
|
// the council budget.
|
|
365
|
-
|
|
365
|
+
// 🔴 2026-08-01 env fail-loud 族:原为 `Math.max(0, Math.floor(Number(env ?? "0")) || 0)` ——
|
|
366
|
+
// `TASK_TIMEOUT_SEC="3600s"`(带单位后缀,运维最常见的写法)得 NaN,再被 `|| 0` 折成 0,
|
|
367
|
+
// 而 0 在这里的语义是**无超时**。运维以为设了一小时上限,实际把超时关掉了,方向恰好相反,
|
|
368
|
+
// 且启动期零提示。改走 numEnv:非法值当场 fail-loud 并指名键与实际值。
|
|
369
|
+
// (numEnv 的注释早就预言了这个失败模式,只是它没 export、跨文件够不到,于是这两处手写了。)
|
|
370
|
+
const taskTimeoutSec = Math.max(0, Math.floor(numEnv("TASK_TIMEOUT_SEC", "0")));
|
|
366
371
|
// [854]④ per-request 配速的运营方上限旋钮(可选;缺省不设=不封顶):body.limits 每键各自被对应旋钮
|
|
367
372
|
// Math.min 封顶(normalizeLimits)。多租部署想约束 caller 自报配速时才设;单用户 turnkey 通常留空。
|
|
368
373
|
// ⚠️ 语义=只钳「显式请求值」,不是全队默认限额:body 缺席(或重放体里的非法值被 defensive DROP)⇒ 该键
|
|
@@ -552,6 +557,9 @@ async function main() {
|
|
|
552
557
|
// ② **fail-soft**:core resolveTaskModel 在无可解析角色时 throw,这是全仓唯一 boot 期裸调用;
|
|
553
558
|
// center 发一张既无 summarize 又无 default 的 roles 表就会拒启。解析失败 ⇒ 本次不摘要
|
|
554
559
|
// (core 的 summarize 抛错是 fail-open:回退原文+note),绝不崩 boot。
|
|
560
|
+
// core 2.13.0:返回形放宽为 additive union(`string | { text, truncated? }`)—— 这里**原样转发**
|
|
561
|
+
// core summarizer 的返回值,不在本仓收窄成 string(收窄会把 core 的截断披露 `truncated` 吃掉,
|
|
562
|
+
// 让「内容被截断」这个事实在 fence 外消失)。
|
|
555
563
|
const webFetchSummarize = async (content, prompt, signal) => {
|
|
556
564
|
const model = (() => {
|
|
557
565
|
try {
|
|
@@ -293,8 +293,17 @@ export class FileRunStore {
|
|
|
293
293
|
* [1.207 codex M1] 负向形漏 blocked/timeout);claim 释放保持无条件(幂等)。 */
|
|
294
294
|
async setTerminal(taskId, status, result, error) {
|
|
295
295
|
const r = this.runs.get(taskId);
|
|
296
|
-
if (!r)
|
|
296
|
+
if (!r) {
|
|
297
|
+
// [2255]③ claim 孤儿修:行不在(跨进程窗 —— claim 落盘、行未落/已失;R9 只兜同进程写失败)时,
|
|
298
|
+
// 终局动词仍必须能放锁:cancel 是文案承诺的用户自救路,它对孤儿失效=会话钉死到 reaper/boot。
|
|
299
|
+
// 反查 active 索引找持有该 taskId 的 session(O(claims),claims 数=活跃会话数,小)。
|
|
300
|
+
for (const [sessionId, tid] of this.active)
|
|
301
|
+
if (tid === taskId) {
|
|
302
|
+
this.releaseClaim(sessionId);
|
|
303
|
+
break;
|
|
304
|
+
}
|
|
297
305
|
return;
|
|
306
|
+
}
|
|
298
307
|
if (r.status === "running" || r.status === "suspended" || r.status === "needs_review") {
|
|
299
308
|
r.status = status;
|
|
300
309
|
r.result = result;
|
|
@@ -535,8 +544,10 @@ export class FileRunStore {
|
|
|
535
544
|
*/
|
|
536
545
|
async reapSuspended(olderThanMs) {
|
|
537
546
|
const probe = this.checkpointProbe;
|
|
538
|
-
if (!probe)
|
|
547
|
+
if (!probe) {
|
|
548
|
+
this.sweepClaims();
|
|
539
549
|
return 0;
|
|
550
|
+
} // [2255]③:时间腿无 probe=诚实 NO-OP,但 claim 不变式维护不跟 probe 走
|
|
540
551
|
const cutoff = Date.now() - olderThanMs;
|
|
541
552
|
let reaped = 0;
|
|
542
553
|
for (const r of this.runs.values()) {
|
|
@@ -566,8 +577,10 @@ export class FileRunStore {
|
|
|
566
577
|
*/
|
|
567
578
|
async failSuspendedWithExpiredCheckpoint() {
|
|
568
579
|
const probe = this.checkpointProbe;
|
|
569
|
-
if (!probe)
|
|
580
|
+
if (!probe) {
|
|
581
|
+
this.sweepClaims();
|
|
570
582
|
return 0;
|
|
583
|
+
} // [2255]③ 同上
|
|
571
584
|
let reaped = 0;
|
|
572
585
|
for (const r of this.runs.values()) {
|
|
573
586
|
if (r.status !== "suspended" && r.status !== "needs_review")
|
|
@@ -127,8 +127,15 @@ export class MemoryRunStore {
|
|
|
127
127
|
* [1.207 codex M1] 负向形漏 blocked/timeout);claim 释放保持无条件(幂等)。 */
|
|
128
128
|
async setTerminal(taskId, status, result, error) {
|
|
129
129
|
const r = this.runs.get(taskId);
|
|
130
|
-
if (!r)
|
|
130
|
+
if (!r) {
|
|
131
|
+
// [2255]③ file 孪生同判据(parity oracle):行缺席时终局动词仍放锁(反查 active)。
|
|
132
|
+
for (const [sessionId, tid] of this.active)
|
|
133
|
+
if (tid === taskId) {
|
|
134
|
+
this.active.delete(sessionId);
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
131
137
|
return;
|
|
138
|
+
}
|
|
132
139
|
if (r.status === "running" || r.status === "suspended" || r.status === "needs_review") {
|
|
133
140
|
r.status = status;
|
|
134
141
|
r.result = result;
|
|
@@ -328,8 +335,10 @@ export class MemoryRunStore {
|
|
|
328
335
|
*/
|
|
329
336
|
async reapSuspended(olderThanMs) {
|
|
330
337
|
const probe = this.checkpointProbe;
|
|
331
|
-
if (!probe)
|
|
338
|
+
if (!probe) {
|
|
339
|
+
this.releaseTerminalClaims();
|
|
332
340
|
return 0;
|
|
341
|
+
} // [2255]③ claim 不变式维护不跟 probe 走
|
|
333
342
|
const cutoff = Date.now() - olderThanMs;
|
|
334
343
|
let reaped = 0;
|
|
335
344
|
for (const r of this.runs.values()) {
|
|
@@ -358,8 +367,10 @@ export class MemoryRunStore {
|
|
|
358
367
|
*/
|
|
359
368
|
async failSuspendedWithExpiredCheckpoint() {
|
|
360
369
|
const probe = this.checkpointProbe;
|
|
361
|
-
if (!probe)
|
|
370
|
+
if (!probe) {
|
|
371
|
+
this.releaseTerminalClaims();
|
|
362
372
|
return 0;
|
|
373
|
+
} // [2255]③ claim 不变式维护不跟 probe 走
|
|
363
374
|
let reaped = 0;
|
|
364
375
|
for (const r of this.runs.values()) {
|
|
365
376
|
if (r.status !== "suspended" && r.status !== "needs_review")
|
|
@@ -520,6 +520,12 @@ export class SqlRunStore {
|
|
|
520
520
|
// claim — the session stays locked until an operator resumes it or reapSuspended expires it. (Was
|
|
521
521
|
// `<> 'running'`, which would have prematurely unlocked a parked suspended session.)
|
|
522
522
|
await conn.query(this.q("DELETE ta FROM task_active ta JOIN task_run tr ON ta.task_id = tr.task_id WHERE tr.status NOT IN ('running','suspended','needs_review')", "DELETE FROM task_active ta USING task_run tr WHERE ta.task_id = tr.task_id AND tr.status NOT IN ('running','suspended','needs_review')"));
|
|
523
|
+
// [2255]③ 孤儿 claim 清扫(纵深防御):claim 无对应 task_run 行时,三处 INNER JOIN 释放腿永远碰不到
|
|
524
|
+
// 它。正常路径不产孤儿(createRun 单事务、setTerminal 无条件 DELETE、deleteBySession 同事务对称)——
|
|
525
|
+
// 这形只来自带外(手工 SQL/半截迁移),但一旦出现该 session 只剩 cancel 一条自救路。挂在 reapStale
|
|
526
|
+
// (每 tick 无条件跑)一处即可。同文双方言(关联 NOT EXISTS 指向**另一张**表,两引擎都合法);
|
|
527
|
+
// 无竞态:createRun 的 claim+row 同事务提交,已提交的 claim 必有行。
|
|
528
|
+
await conn.query(this.q("DELETE FROM task_active WHERE NOT EXISTS (SELECT 1 FROM task_run tr WHERE tr.task_id = task_active.task_id)", "DELETE FROM task_active WHERE NOT EXISTS (SELECT 1 FROM task_run tr WHERE tr.task_id = task_active.task_id)"));
|
|
523
529
|
});
|
|
524
530
|
return reaped;
|
|
525
531
|
}
|
|
@@ -90,6 +90,20 @@ export declare function setWebSearchBadPayloadObserver(fn: ((provider: string) =
|
|
|
90
90
|
* stays captured in this closure — it is never surfaced to the model or the tool args.
|
|
91
91
|
*/
|
|
92
92
|
export declare function createWebSearchBackend(cfg: WebSearchBackendConfig): WebSearchBackend;
|
|
93
|
+
/**
|
|
94
|
+
* `WEB_SEARCH_SEARXNG_PARAMS` / `settings.webSearch.searxngParams` 的解析 —— SearXNG 实例侧参数
|
|
95
|
+
* (`engines=` / `language=` 等)。
|
|
96
|
+
*
|
|
97
|
+
* 🔴 [2240] cli 逐串直证:3.18.0 我加了 `searxngParams` 字段和消费点,**却没开任何配置入口** ——
|
|
98
|
+
* 两条门都没有位置填它,于是我在 [2239] 说的「这条是给你们的」是句空话。单测直接构造
|
|
99
|
+
* `WebSearchBackendConfig` 传进去,**天然绕过了配置入口这一层**,于是「能力在」与「配得进」之间
|
|
100
|
+
* 断开,而两边的绿都是真的。判据:**按消费端实际能填的那个入口验,不是按自己构造的对象验。**
|
|
101
|
+
*
|
|
102
|
+
* 形状:`k=v` 用 `;` 分隔(`engines=bing,duckduckgo;language=zh-CN`)——值里本来就常含逗号,
|
|
103
|
+
* 所以分隔符取 `;` 而不是 `,`。整串解析不出任何一对 ⇒ **返回 undefined,键整个不铸**
|
|
104
|
+
* (不产出空对象:空对象会让下游以为「配了但是空的」,与本仓「缺席就要缺席得干净」同源)。
|
|
105
|
+
*/
|
|
106
|
+
export declare function parseSearxngParams(raw: unknown): Record<string, string> | undefined;
|
|
93
107
|
/**
|
|
94
108
|
* #81④ 探活门控 —— **默认 OFF**,显式开才探。
|
|
95
109
|
*
|
|
@@ -152,11 +152,17 @@ async function tavilySearch(fetchImpl, cfg, query, max, signal, opts) {
|
|
|
152
152
|
* ⚠️ core adapter **不管**这三样,必须留在外层(换真源最容易丢的就是它们,已上红先钉):
|
|
153
153
|
* ① `maxResults` 截断 ② 统一形状 `{title,url,snippet}` ③ 坏 payload 观测器。
|
|
154
154
|
*/
|
|
155
|
-
async function searxngSearch(fetchImpl, cfg, query, max, signal, opts) {
|
|
155
|
+
async function searxngSearch(fetchImpl, cfg, query, max, signal, timeoutMs, opts) {
|
|
156
156
|
if (!cfg.endpoint)
|
|
157
157
|
throw new Error("WEB_SEARCH_ENDPOINT (the SearXNG instance URL) is required for the searxng provider");
|
|
158
|
+
// 🔴 timeoutMs 必须显式传:core 的 adapter 内部是 `options.timeoutMs ?? 10_000`,不传就用它自己的
|
|
159
|
+
// 10 秒。外层 createWebSearchBackend 那道 withTimeout 只在**更短**时先到 —— 部署方把
|
|
160
|
+
// WEB_SEARCH_TIMEOUT_MS 设成大于 10s 的值(慢实例/自建 SearXNG 的常见需要)会被**静默封顶在 10s**,
|
|
161
|
+
// 而 3.18.0 的 CHANGELOG 还把 core「自带 timeout」当纯收益卖(见 3.20.0 的更正段)。
|
|
162
|
+
// 两层同值不冲突:内层是 adapter 自己的 AbortSignal.any,外层是本文件的 withTimeout。
|
|
158
163
|
const search = createSearxngSearchBackend(cfg.endpoint, {
|
|
159
164
|
fetchImpl,
|
|
165
|
+
timeoutMs,
|
|
160
166
|
...(cfg.searxngParams ? { extraParams: cfg.searxngParams } : {}),
|
|
161
167
|
});
|
|
162
168
|
let rows;
|
|
@@ -196,7 +202,7 @@ export function createWebSearchBackend(cfg) {
|
|
|
196
202
|
switch (cfg.provider) {
|
|
197
203
|
case "brave": return await braveSearch(fetchImpl, cfg, q, maxResults, sig);
|
|
198
204
|
case "tavily": return await tavilySearch(fetchImpl, cfg, q, maxResults, sig, opts);
|
|
199
|
-
case "searxng": return await searxngSearch(fetchImpl, cfg, q, maxResults, sig, opts);
|
|
205
|
+
case "searxng": return await searxngSearch(fetchImpl, cfg, q, maxResults, sig, timeoutMs, opts);
|
|
200
206
|
}
|
|
201
207
|
}
|
|
202
208
|
finally {
|
|
@@ -205,6 +211,41 @@ export function createWebSearchBackend(cfg) {
|
|
|
205
211
|
};
|
|
206
212
|
return { search, maxResults };
|
|
207
213
|
}
|
|
214
|
+
/**
|
|
215
|
+
* `WEB_SEARCH_SEARXNG_PARAMS` / `settings.webSearch.searxngParams` 的解析 —— SearXNG 实例侧参数
|
|
216
|
+
* (`engines=` / `language=` 等)。
|
|
217
|
+
*
|
|
218
|
+
* 🔴 [2240] cli 逐串直证:3.18.0 我加了 `searxngParams` 字段和消费点,**却没开任何配置入口** ——
|
|
219
|
+
* 两条门都没有位置填它,于是我在 [2239] 说的「这条是给你们的」是句空话。单测直接构造
|
|
220
|
+
* `WebSearchBackendConfig` 传进去,**天然绕过了配置入口这一层**,于是「能力在」与「配得进」之间
|
|
221
|
+
* 断开,而两边的绿都是真的。判据:**按消费端实际能填的那个入口验,不是按自己构造的对象验。**
|
|
222
|
+
*
|
|
223
|
+
* 形状:`k=v` 用 `;` 分隔(`engines=bing,duckduckgo;language=zh-CN`)——值里本来就常含逗号,
|
|
224
|
+
* 所以分隔符取 `;` 而不是 `,`。整串解析不出任何一对 ⇒ **返回 undefined,键整个不铸**
|
|
225
|
+
* (不产出空对象:空对象会让下游以为「配了但是空的」,与本仓「缺席就要缺席得干净」同源)。
|
|
226
|
+
*/
|
|
227
|
+
export function parseSearxngParams(raw) {
|
|
228
|
+
if (typeof raw === "object" && raw !== null) {
|
|
229
|
+
// settings 门传的是已解析的表;只收「字符串→字符串」,任一值非字符串 ⇒ 整表不铸(不半吞)。
|
|
230
|
+
const entries = Object.entries(raw);
|
|
231
|
+
if (entries.length === 0 || entries.some(([, v]) => typeof v !== "string"))
|
|
232
|
+
return undefined;
|
|
233
|
+
return Object.fromEntries(entries);
|
|
234
|
+
}
|
|
235
|
+
if (typeof raw !== "string")
|
|
236
|
+
return undefined;
|
|
237
|
+
const out = {};
|
|
238
|
+
for (const pair of raw.split(";")) {
|
|
239
|
+
const i = pair.indexOf("=");
|
|
240
|
+
if (i <= 0)
|
|
241
|
+
continue;
|
|
242
|
+
const k = pair.slice(0, i).trim();
|
|
243
|
+
const v = pair.slice(i + 1).trim();
|
|
244
|
+
if (k && v)
|
|
245
|
+
out[k] = v;
|
|
246
|
+
}
|
|
247
|
+
return Object.keys(out).length > 0 ? out : undefined;
|
|
248
|
+
}
|
|
208
249
|
/**
|
|
209
250
|
* #81④ 探活门控 —— **默认 OFF**,显式开才探。
|
|
210
251
|
*
|
|
@@ -224,12 +265,14 @@ export function webSearchConfigFromEnv(env = process.env) {
|
|
|
224
265
|
const provider = env.WEB_SEARCH_PROVIDER?.trim().toLowerCase();
|
|
225
266
|
if (provider !== "brave" && provider !== "tavily" && provider !== "searxng")
|
|
226
267
|
return undefined;
|
|
268
|
+
const searxngParams = parseSearxngParams(env.WEB_SEARCH_SEARXNG_PARAMS); // [2240] 部署 env 门
|
|
227
269
|
const maxRaw = Number(env.WEB_SEARCH_MAX_RESULTS ?? "");
|
|
228
270
|
const timeoutRaw = Number(env.WEB_SEARCH_TIMEOUT_MS ?? "");
|
|
229
271
|
return {
|
|
230
272
|
provider,
|
|
231
273
|
...(env.WEB_SEARCH_API_KEY ? { apiKey: env.WEB_SEARCH_API_KEY } : {}),
|
|
232
274
|
...(env.WEB_SEARCH_ENDPOINT ? { endpoint: env.WEB_SEARCH_ENDPOINT } : {}),
|
|
275
|
+
...(searxngParams ? { searxngParams } : {}), // [2240] 解析不出任何一对 ⇒ 键整个不铸(不产出空对象)
|
|
233
276
|
...(Number.isFinite(maxRaw) && maxRaw > 0 ? { maxResults: Math.floor(maxRaw) } : {}),
|
|
234
277
|
...(Number.isFinite(timeoutRaw) && timeoutRaw > 0 ? { timeoutMs: Math.floor(timeoutRaw) } : {}),
|
|
235
278
|
};
|
|
@@ -253,6 +296,12 @@ export function webSearchConfigFromSettings(raw) {
|
|
|
253
296
|
provider,
|
|
254
297
|
...(typeof s.apiKey === "string" && s.apiKey ? { apiKey: s.apiKey } : {}),
|
|
255
298
|
...(typeof s.endpoint === "string" && s.endpoint ? { endpoint: s.endpoint } : {}),
|
|
299
|
+
// [2240]:与 endpoint **同门同辖域** —— 安全边界不在字段粒度,而在调用方把整条门 gate 到
|
|
300
|
+
// 单用户 host lane(见本函数头注的 SSRF 段)。单独把它挡在门外是个没有理由的例外。
|
|
301
|
+
...(() => {
|
|
302
|
+
const p = parseSearxngParams(s.searxngParams);
|
|
303
|
+
return p ? { searxngParams: p } : {};
|
|
304
|
+
})(),
|
|
256
305
|
...(max ? { maxResults: max } : {}),
|
|
257
306
|
};
|
|
258
307
|
}
|
package/dist/run-local.js
CHANGED
|
@@ -31,7 +31,7 @@ import { createBrain } from "./brain.js";
|
|
|
31
31
|
import { ForkRoutingSessionStore } from "./plugins/fork-routing-session-store.js";
|
|
32
32
|
import { taskWallClockSec } from "./task-workflow.js";
|
|
33
33
|
import { validatePromptsDomain, applyCatalogToSource, CORE_ENGINE_VERSION } from "./capabilities/center-prompts.js";
|
|
34
|
-
import { loadConfig, logConfigDiagnostics } from "./config.js";
|
|
34
|
+
import { loadConfig, logConfigDiagnostics, numEnv } from "./config.js";
|
|
35
35
|
import { createConfigProvider } from "./config-provider.js";
|
|
36
36
|
import { applyEffective, resolveMcpServers, mcpForScenario } from "./config-center/facade.js";
|
|
37
37
|
import { hostExecutionEnvFactory } from "./plugins/remote-env-host.js";
|
|
@@ -440,7 +440,12 @@ export async function runLocal(argv, deps = {}) {
|
|
|
440
440
|
// wins with its raise-only floor (team 1500s — a multi-agent coordinator legitimately exceeds 600s).
|
|
441
441
|
// [854]④ 不落此腿:per-request body.limits 是 HTTP body 车道(resolveTaskLimits,main.ts resolveSpec);
|
|
442
442
|
// run-local 无 body,配速仍走 env(要 CLI flag 时另开件,别在这里半做)。
|
|
443
|
-
|
|
443
|
+
// 🔴 2026-08-01 env fail-loud 族:原为 `Math.max(0, Math.floor(Number(env ?? "0")) || 0)` ——
|
|
444
|
+
// `TASK_TIMEOUT_SEC="3600s"`(带单位后缀,运维最常见的写法)得 NaN,再被 `|| 0` 折成 0,
|
|
445
|
+
// 而 0 在这里的语义是**无超时**。运维以为设了一小时上限,实际把超时关掉了,方向恰好相反,
|
|
446
|
+
// 且启动期零提示。改走 numEnv:非法值当场 fail-loud 并指名键与实际值。
|
|
447
|
+
// (numEnv 的注释早就预言了这个失败模式,只是它没 export、跨文件够不到,于是这两处手写了。)
|
|
448
|
+
const taskTimeoutSec = Math.max(0, Math.floor(numEnv("TASK_TIMEOUT_SEC", "0")));
|
|
444
449
|
const timeoutSec = taskWallClockSec(taskTimeoutSec, false, scenarioName === "team");
|
|
445
450
|
const mcp = mcpForScenario(config.mcpServers, scenarioName);
|
|
446
451
|
const spec = {
|
package/dist/security.d.ts
CHANGED
|
@@ -23,6 +23,23 @@ export interface SessionListItem {
|
|
|
23
23
|
/** A SessionStore that may additionally expose tenant-ownership ops (the TiDB store does) AND the
|
|
24
24
|
* `SessionRepo` enumeration/lifecycle seam (§0.5: list/fork/delete homed on the session abstraction's own
|
|
25
25
|
* tables, not the `task_run` runs ledger). All optional — an in-memory dev store implements none. */
|
|
26
|
+
/**
|
|
27
|
+
* E6 fail-closed 守卫的辖域判据:**这个请求是不是一次「破坏性会话写」**。
|
|
28
|
+
*
|
|
29
|
+
* 🔴 2026-08-01 复审逮到:守卫原本是**逐条路由白名单**(`PUT …/policy`、`POST …/sync/import`),
|
|
30
|
+
* 而 `DELETE /v1/sessions/:id`(删整条会话)不在其中 —— 同一威胁形、爆炸半径**更大**,却没门。
|
|
31
|
+
*
|
|
32
|
+
* 威胁形逐字同源(守卫自己的注释):「无 service token + 未开 `ALLOW_UNAUTHED_WRITES`」这一形下
|
|
33
|
+
* `sessionOwnerScopeForWrite` 判 fleet-wide ⇒ 集群内**任意**调用方可对**任意** session 动手。
|
|
34
|
+
* policy 腿担心的是「forged principal 能 tighten/DoS 一个会话的工具」;DELETE 直接把会话删掉。
|
|
35
|
+
*
|
|
36
|
+
* ⇒ 枚举改**族判定**:新增一条破坏性会话路由时自动落进门内,不必记得回来加白名单。
|
|
37
|
+
* 这是本仓「同一条论证只在枚举到的路径上执行」这一族的第 N 次,修法统一为把判据提成函数。
|
|
38
|
+
*
|
|
39
|
+
* 辖域**只含**对**既有会话**的破坏性写:改规则 / 整段替换 / 删除 / 分叉(读源会话全历史)。
|
|
40
|
+
* 不含读面,也不含 `POST /v1/tasks` 那条提交腿 —— 它有自己的同族门(别重复拦,双拦会让错误归因变糊)。
|
|
41
|
+
*/
|
|
42
|
+
export declare function isDestructiveSessionWrite(method: string, url: string): boolean;
|
|
26
43
|
export type OwnerAwareSessionStore = SessionStore & {
|
|
27
44
|
ownerOf?: (sessionId: string) => Promise<string | null | undefined>;
|
|
28
45
|
register?: (sessionId: string, owner: string | null) => Promise<void>;
|
package/dist/security.js
CHANGED
|
@@ -11,6 +11,34 @@ import { memoryScopeFor } from "./memory-scope.js"; // design/158 N17: see re-ex
|
|
|
11
11
|
// `verifiedPrincipal` still calls verifyPrincipalJwt for the direct-door branch (value import below); every
|
|
12
12
|
// symbol from both groups is re-exported near the bottom of this file so existing importers keep working.
|
|
13
13
|
import { verifyPrincipalJwt } from "./principal-jwt.js";
|
|
14
|
+
/** A SessionStore that may additionally expose tenant-ownership ops (the TiDB store does) AND the
|
|
15
|
+
* `SessionRepo` enumeration/lifecycle seam (§0.5: list/fork/delete homed on the session abstraction's own
|
|
16
|
+
* tables, not the `task_run` runs ledger). All optional — an in-memory dev store implements none. */
|
|
17
|
+
/**
|
|
18
|
+
* E6 fail-closed 守卫的辖域判据:**这个请求是不是一次「破坏性会话写」**。
|
|
19
|
+
*
|
|
20
|
+
* 🔴 2026-08-01 复审逮到:守卫原本是**逐条路由白名单**(`PUT …/policy`、`POST …/sync/import`),
|
|
21
|
+
* 而 `DELETE /v1/sessions/:id`(删整条会话)不在其中 —— 同一威胁形、爆炸半径**更大**,却没门。
|
|
22
|
+
*
|
|
23
|
+
* 威胁形逐字同源(守卫自己的注释):「无 service token + 未开 `ALLOW_UNAUTHED_WRITES`」这一形下
|
|
24
|
+
* `sessionOwnerScopeForWrite` 判 fleet-wide ⇒ 集群内**任意**调用方可对**任意** session 动手。
|
|
25
|
+
* policy 腿担心的是「forged principal 能 tighten/DoS 一个会话的工具」;DELETE 直接把会话删掉。
|
|
26
|
+
*
|
|
27
|
+
* ⇒ 枚举改**族判定**:新增一条破坏性会话路由时自动落进门内,不必记得回来加白名单。
|
|
28
|
+
* 这是本仓「同一条论证只在枚举到的路径上执行」这一族的第 N 次,修法统一为把判据提成函数。
|
|
29
|
+
*
|
|
30
|
+
* 辖域**只含**对**既有会话**的破坏性写:改规则 / 整段替换 / 删除 / 分叉(读源会话全历史)。
|
|
31
|
+
* 不含读面,也不含 `POST /v1/tasks` 那条提交腿 —— 它有自己的同族门(别重复拦,双拦会让错误归因变糊)。
|
|
32
|
+
*/
|
|
33
|
+
export function isDestructiveSessionWrite(method, url) {
|
|
34
|
+
if (method === "DELETE")
|
|
35
|
+
return /^\/v1\/sessions\/[^/]+$/.test(url);
|
|
36
|
+
if (method === "PUT")
|
|
37
|
+
return /^\/v1\/sessions\/[^/]+\/policy$/.test(url);
|
|
38
|
+
if (method === "POST")
|
|
39
|
+
return /^\/v1\/sessions\/[^/]+\/(sync\/import|fork)$/.test(url);
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
14
42
|
/** The shape core's `uuidv7()` mints: canonical 8-4-4-4-12 lowercase hex with version nibble `7` and the RFC-4122
|
|
15
43
|
* variant (`8`/`9`/`a`/`b`). Used to shape-validate a caller-supplied session id on the §0.5 fork/delete routes so
|
|
16
44
|
* a crafted id (LIKE metacharacters, over-long, non-canonical) is rejected up front — a SECOND layer behind the
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sema-agent/server",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.21.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",
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"build:binary:run-local:darwin-arm64": "bun build --compile --target=bun-darwin-arm64 src/run-local.ts --outfile dist/run-local-darwin-arm64"
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
|
-
"@sema-agent/core": "^2.
|
|
57
|
+
"@sema-agent/core": "^2.13.0",
|
|
58
58
|
"@sema-agent/registry-core": "^0.11.0",
|
|
59
59
|
"e2b": "^2.28.0",
|
|
60
60
|
"libsodium-wrappers": "^0.8.4",
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
"sharp": "^0.35.3"
|
|
69
69
|
},
|
|
70
70
|
"devDependencies": {
|
|
71
|
-
"@sema-agent/sdk": "^2.
|
|
71
|
+
"@sema-agent/sdk": "^2.3.0",
|
|
72
72
|
"@types/libsodium-wrappers": "^0.7.14",
|
|
73
73
|
"@types/node": "22.10.2",
|
|
74
74
|
"@types/pg": "^8.20.0",
|