@sema-agent/server 7.12.0 → 7.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/USAGE.md +51 -2
- package/dist/adoption/plan.d.ts +38 -4
- package/dist/adoption/plan.js +72 -0
- package/dist/adoption/quiesce.d.ts +70 -0
- package/dist/adoption/quiesce.js +148 -0
- package/dist/adoption/runner.js +63 -5
- package/dist/adoption/sql.d.ts +15 -0
- package/dist/adoption/sql.js +18 -0
- package/dist/adoption/wire.d.ts +7 -1
- package/dist/adoption/wire.js +6 -0
- package/dist/approval-card.d.ts +5 -0
- package/dist/approval-card.js +22 -0
- package/dist/boot/coordinators.js +2 -1
- package/dist/boot/memory-boundary.d.ts +84 -0
- package/dist/boot/memory-boundary.js +110 -0
- package/dist/boot/permission-rules-audit.js +29 -1
- package/dist/boot/reapers.d.ts +15 -0
- package/dist/boot/reapers.js +101 -44
- package/dist/boot/resolve-spec.js +31 -0
- package/dist/boot/runner-deps.d.ts +16 -2
- package/dist/boot/runner-deps.js +24 -4
- package/dist/boot/stores.js +93 -3
- package/dist/capabilities/memory-notice.d.ts +83 -0
- package/dist/capabilities/memory-notice.js +90 -0
- package/dist/config-types.d.ts +100 -11
- package/dist/config.d.ts +1 -1
- package/dist/config.js +111 -1
- package/dist/governance-ask-marks.js +2 -1
- package/dist/http/active-run-conflict.d.ts +33 -8
- package/dist/http/active-run-conflict.js +37 -2
- package/dist/http/routes/adoption.js +25 -2
- package/dist/http/routes/approvals-assistant.js +33 -3
- package/dist/http/routes/capabilities.js +35 -5
- package/dist/http/routes/images.js +18 -0
- package/dist/http/routes/runs.js +21 -5
- package/dist/http/routes/tasks.js +18 -6
- package/dist/http/routes/trace-usage.js +43 -14
- package/dist/http/server.d.ts +35 -9
- package/dist/http/server.js +111 -17
- package/dist/http/wire-types.d.ts +6 -1
- package/dist/main.js +46 -6
- package/dist/observability/fail-open.d.ts +4 -0
- package/dist/observability/fail-open.js +4 -0
- package/dist/plugins/adoption-log-sql.d.ts +40 -0
- package/dist/plugins/adoption-log-sql.js +69 -2
- package/dist/plugins/approval-ask-store-sql.d.ts +2 -1
- package/dist/plugins/approval-ask-store-sql.js +2 -1
- package/dist/plugins/file-run-store.d.ts +85 -1
- package/dist/plugins/file-run-store.js +450 -17
- package/dist/plugins/memory-embedder.d.ts +44 -0
- package/dist/plugins/memory-embedder.js +173 -0
- package/dist/plugins/permission-rule-store-sql.d.ts +45 -0
- package/dist/plugins/permission-rule-store-sql.js +60 -2
- package/dist/plugins/shared-memory-store-sql.d.ts +23 -9
- package/dist/plugins/shared-memory-store-sql.js +55 -18
- package/dist/plugins/sql-driver.d.ts +19 -0
- package/dist/plugins/sql-driver.js +12 -0
- package/dist/plugins/store-backend.d.ts +3 -1
- package/dist/plugins/store-backend.js +24 -1
- package/dist/plugins/tidb-pool.js +11 -4
- package/dist/plugins/tool-result-store-sql.d.ts +35 -2
- package/dist/plugins/tool-result-store-sql.js +127 -11
- package/dist/plugins/web-search.d.ts +3 -1
- package/dist/plugins/web-search.js +3 -1
- package/dist/rules-consent.d.ts +33 -4
- package/dist/rules-consent.js +43 -2
- package/dist/run-local.js +6 -2
- package/dist/runtime-governance.d.ts +33 -0
- package/dist/runtime-governance.js +32 -0
- package/dist/security.js +3 -1
- package/dist/tool-approval.d.ts +32 -0
- package/dist/tool-approval.js +39 -0
- package/dist/trace/core-keyset-guard.d.ts +1 -1
- package/package.json +3 -3
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* (`gate_not_tool_approval`/`gate_not_resumable`/`wake.gate_pending` 的指路句)。三处文案与本表若漂移,
|
|
14
14
|
* test/session-active-conflict-materials.test.ts 的分门用例会红。认不出的 kind ⇒ null(诚实缺席,不铸假门)。
|
|
15
15
|
*/
|
|
16
|
+
import { governanceMandatesShellGateAlways } from "../runtime-governance.js";
|
|
16
17
|
/** 与 runs.ts/tasks.ts 三个 409 位共享的旧文案(byte-frozen:api-error-text-freeze 门认这句)。 */
|
|
17
18
|
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
19
|
/** gate.kind → 它的那一个 resume 入口(sessionId/taskId 寻址,无秘密)。 */
|
|
@@ -39,6 +40,37 @@ export function resumeEntryForGate(kind, ids) {
|
|
|
39
40
|
return null; // 未知门型:宁缺毋假 —— 客户端仍有 activeTaskStatus + cancel 这条保底真路
|
|
40
41
|
}
|
|
41
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* #220:推「这道门是运维治理层下的吗」。判据是**合取**,两半各管一件事:
|
|
45
|
+
*
|
|
46
|
+
* 行上的取证格 `gate.riskDescriptor.shellGateDoctrine === "always"`
|
|
47
|
+
* ∧ 本部署的治理层本来就要求 always(`governanceMandatesShellGateAlways`,单一判据属主在
|
|
48
|
+
* `runtime-governance.ts`,那里写着为什么单看行不够 —— SUP 路由姿态同样产 `"always"`)
|
|
49
|
+
*
|
|
50
|
+
* 第一半:core 在 mint 这道门时把「当时活着的 doctrine」如实写进 `riskDescriptor.shellGateDoctrine`
|
|
51
|
+
* (2026-08-05 取证裁定;`prepare-task` 只在**被 shell 门铸**的 ask 上写它),所以持久行自己带着证据 ——
|
|
52
|
+
* park 腿天然跨副本、跨重启,活卡腿那张 ALS 标记表在这里结构上够不着。
|
|
53
|
+
* 第二半:把「有效档 always」收窄到「治理层是真成因」,否则一条 SUP 路由出来的门会被谎报成治理强制。
|
|
54
|
+
*
|
|
55
|
+
* ⚠️ 残留(双向失真的窄在场向 + 根治路径)登记在 `governanceMandatesShellGateAlways` 的顶注,别在这里
|
|
56
|
+
* 复述;本位只承担「分诊提示」的分量,不参与任何门/CAS/resume 判定。
|
|
57
|
+
*
|
|
58
|
+
* 🔴 `"classify"` 档**刻意不标**:那一档由 core 的分类器逐调用裁决,一次 shell ask 可能出自分类器(治理)
|
|
59
|
+
* 也可能出自 `APPROVAL_REQUIRE` 这类别的门,行上无从分辨 —— 与活卡腿同一条「宁缺毋假」纪律。
|
|
60
|
+
*
|
|
61
|
+
* ⚠️ **askId 为什么不在这里**(#220 的另一半,如实记账):待批 ask 的 `askId` 只活在 `approval_ask` 行上
|
|
62
|
+
* (checkpoint 行没有这一列,core 的 `CheckpointGate`/`PendingAction`/`Checkpoint` 顶层都不带它)。反向
|
|
63
|
+
* (checkpoint → ask)今天**没有读口**:两条 list 读口都硬过滤 `state='STREAM_PENDING'`,而 park 完的行是
|
|
64
|
+
* `PARKED`;唯一精确的连接键是 ask 行上的 `gate_token`,要按它反查得给店加一个新读口(SQL 面 = 双库集成
|
|
65
|
+
* 门)。派生 `deriveAskId(...)` 也不行:它吃 `legKey`(= 上一腿 resume token 的摘要)与 `parentToolCallId`,
|
|
66
|
+
* 这两维从 checkpoint 行推不出来,二腿/子代形上会算出一个**错**的 id —— 在「等的谁」这条通道上,错 id
|
|
67
|
+
* 比缺席坏得多(本文件 `resumeEntryForGate` 的 default 臂是同一条纪律)。故本批诚实缺席。
|
|
68
|
+
*/
|
|
69
|
+
function governanceOriginOf(gate, governance) {
|
|
70
|
+
if (gate?.riskDescriptor?.shellGateDoctrine !== "always")
|
|
71
|
+
return undefined;
|
|
72
|
+
return governanceMandatesShellGateAlways(governance ?? {}) ? true : undefined;
|
|
73
|
+
}
|
|
42
74
|
export function toDoneFrameResult(body) {
|
|
43
75
|
return {
|
|
44
76
|
status: "failed",
|
|
@@ -75,10 +107,13 @@ export async function buildActiveRunConflict(deps, sessionId, activeTaskId) {
|
|
|
75
107
|
if (cs?.findPendingTokenBySession) {
|
|
76
108
|
const scope = (await cs.peekPendingScope?.(sessionId)) ?? undefined;
|
|
77
109
|
const token = scope === null ? null : await cs.findPendingTokenBySession(sessionId, scope);
|
|
78
|
-
const
|
|
110
|
+
const gate = token ? (await cs.get?.(token))?.gate : undefined;
|
|
111
|
+
const kind = gate?.kind;
|
|
79
112
|
const decidePath = kind ? resumeEntryForGate(kind, { sessionId, taskId: activeTaskId }) : null;
|
|
113
|
+
// #220:出身格与出路格**同一次读**里取(不为它多打一次库),`true` 才写键(见 PendingGateMaterial 顶注)。
|
|
114
|
+
const governanceForced = governanceOriginOf(gate, deps.governance);
|
|
80
115
|
if (kind && decidePath)
|
|
81
|
-
pendingGate = { kind, decidePath };
|
|
116
|
+
pendingGate = { kind, decidePath, ...(governanceForced ? { governanceForced } : {}) };
|
|
82
117
|
}
|
|
83
118
|
return {
|
|
84
119
|
...base,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createAdoptionRunner } from "../../adoption/runner.js";
|
|
2
2
|
import { AdoptionRequestSchema } from "../../adoption/wire.js";
|
|
3
|
-
import { checkAdoptionWidths } from "../../adoption/plan.js";
|
|
3
|
+
import { checkAdoptionPrincipalShape, checkAdoptionWidths } from "../../adoption/plan.js";
|
|
4
4
|
import { sendJson, sendError } from "../send.js";
|
|
5
5
|
import { gatedPrincipal, explicitOperatorOk } from "../principal-gate.js";
|
|
6
6
|
const ADOPTION_ID_RE = /^\/v1\/adoption\/([^/]+)$/;
|
|
@@ -21,6 +21,11 @@ function sendOutcome(res, out) {
|
|
|
21
21
|
return;
|
|
22
22
|
// 闭集穷举:`AdoptionRejectCode` 加一个成员而这里没加臂 ⇒ **编译红**(拒绝码是 wire 契约,
|
|
23
23
|
// 不许有一个走到默认臂的静默码)。
|
|
24
|
+
//
|
|
25
|
+
// 🔴 A-010.19(验真后修):上面这句话此前是**一句自我声明,不是一道门**。内层 switch 只有两条 case
|
|
26
|
+
// 而结尾是一个裸 `return;` —— 加第三个拒绝码时 TS 一个字都不会说,请求会走到那个裸 return、
|
|
27
|
+
// **一个字节都不写**就把响应扔在那里:调用方看到的是一条挂住的连接直到超时,而不是任何错误。
|
|
28
|
+
// 现在由下面的 `never` 断言真正兑现它:少一条臂 ⇒ `out` 收窄不到 `never` ⇒ 赋值失败 ⇒ 编译红。
|
|
24
29
|
case "rejected":
|
|
25
30
|
switch (out.code) {
|
|
26
31
|
case "adoption.destination_conflict":
|
|
@@ -37,9 +42,19 @@ function sendOutcome(res, out) {
|
|
|
37
42
|
});
|
|
38
43
|
return;
|
|
39
44
|
}
|
|
40
|
-
|
|
45
|
+
// 穷举证明:走到这里 `out.code` 必须已经收窄成 `never`。加一个 `AdoptionRejectCode` 成员而不加臂
|
|
46
|
+
// ⇒ 这一行编译红,红在**加码的那一刻**,而不是在生产上表现为一条永不应答的请求。
|
|
47
|
+
return assertNoRejectCodeLeft(out.code);
|
|
41
48
|
}
|
|
42
49
|
}
|
|
50
|
+
/** 闭集穷举的落点。**入参类型是 `never`** —— 这就是那道门本身;函数体永远跑不到,写一句响亮抛是为了
|
|
51
|
+
* 「万一有人用 `as` 把一个未知码塞进来」时仍然 fail-loud(不是静默挂住连接)。
|
|
52
|
+
* ⚠️ 形参**刻意不叫 `code`**:`error-code-key-gate` 按 `code:` 的字面形扫全树错误体(3.0.0 起
|
|
53
|
+
* `errorCode` 是唯一机器判别键),一个叫 `code` 的形参会被它当成一处 legacy 键站点。名字换掉比放宽
|
|
54
|
+
* 那道门便宜得多 —— 门宽一分,真站点就可能溜过去。 */
|
|
55
|
+
function assertNoRejectCodeLeft(unhandled) {
|
|
56
|
+
throw new Error(`adoption: unhandled reject code ${JSON.stringify(unhandled)} — the wire contract's closed set and this switch have drifted apart`);
|
|
57
|
+
}
|
|
43
58
|
export async function handleAdoption(req, res, url, ctx) {
|
|
44
59
|
const miss = { fell: false };
|
|
45
60
|
await handleAdoptionBody(req, res, url, ctx, miss);
|
|
@@ -101,6 +116,14 @@ async function handleAdoptionBody(req, res, url, ctx, miss) {
|
|
|
101
116
|
return;
|
|
102
117
|
}
|
|
103
118
|
const { fromPrincipal, toPrincipal } = parsed.data;
|
|
119
|
+
// 🔴 键空间卫生在**列宽之前**(A-010.15):带首尾空白的身份在两方言上的唯一键语义不等价
|
|
120
|
+
// (MySQL utf8mb4_bin 是 PAD SPACE),而它在本仓根本不是一个可能存在的真身份 —— 详见该函数头注。
|
|
121
|
+
// 排在列宽前面的理由与列宽自己那条次序判据同族:两条都能命中时,报**更根本**的那一个。
|
|
122
|
+
const badShape = checkAdoptionPrincipalShape(fromPrincipal, toPrincipal);
|
|
123
|
+
if (badShape !== undefined) {
|
|
124
|
+
sendError(res, 400, "request.field_invalid", "the adoption principals are not in a portable identity shape", { detail: badShape });
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
104
127
|
// 🔴 列宽校验在**铸行之前**(codex R1-F3):合法长度的 principal 也可能装不进最窄的那根被写列,
|
|
105
128
|
// 而那种失败若发生在腿的中途,phase 会永远停在 INTENT —— 每次重发和每次 boot 扫描都再撞一次同样的墙。
|
|
106
129
|
const tooWide = checkAdoptionWidths(fromPrincipal, toPrincipal);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { principalFrom, decodeCheckpointScope, PRINCIPAL_TOKEN_HEADER, APPROVAL_MAC_HEADER, APPROVAL_MAC_KID_HEADER } from "../../security.js";
|
|
2
2
|
import { verifyDirectDoorProof } from "../../principal-jwt.js";
|
|
3
3
|
import { MAX_APPROVAL_REASON_CHARS } from "../../approval-hmac.js";
|
|
4
|
-
import { redactedPreview } from "../../trace/redact.js";
|
|
4
|
+
import { redactedPreview, redactSecrets } from "../../trace/redact.js";
|
|
5
5
|
import { fleetRunLabels } from "../../fleet/fleet-bus.js"; // [2069]④ §3 行展示名与 fleet 行同源(见用处的注)
|
|
6
6
|
import { sleep } from "../sse-log.js";
|
|
7
7
|
import { sendJson, sendError, sseHeaders, SSE_MAX_STREAM_MS, SSE_HEARTBEAT_IDLE_MS } from "../send.js";
|
|
@@ -68,7 +68,9 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
|
|
|
68
68
|
const scope = operator
|
|
69
69
|
? (new URL(req.url ?? "", "http://x").searchParams.get("owner") ?? undefined) // operator: all (or ?owner)
|
|
70
70
|
: (principal ?? "__none__"); // non-operator: only its own scope (never others' pending)
|
|
71
|
-
|
|
71
|
+
// #209 件4:行整只上 wire(键集契约不变),但 `riskDescriptor.shadowedRule` 这一格先脱敏 ——
|
|
72
|
+
// 理由与 `/v1/approvals/stream` 共用同一个投影函数,见 `redactPendingDisclosures` 顶注。
|
|
73
|
+
sendJson(res, 200, { pending: (await cs.listPending(scope)).map(redactPendingDisclosures) });
|
|
72
74
|
return;
|
|
73
75
|
}
|
|
74
76
|
// exemptions surface — the UI's "本会话不再询问" state (list) + revoke. Same authz shape as
|
|
@@ -659,6 +661,32 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
|
|
|
659
661
|
* connection runs its OWN poll loop — fine for a handful of operators; a shared fan-out poll is a future
|
|
660
662
|
* optimization if the operator count grows.) */
|
|
661
663
|
const APPROVALS_STREAM_POLL_MS = 3000;
|
|
664
|
+
/**
|
|
665
|
+
* 🔴 #209 件4(codex 对抗复审 R3-[high],验真后修)—— **durable 读面的脱敏投影**。
|
|
666
|
+
*
|
|
667
|
+
* core 5.25.0 的 `RiskDescriptor.shadowedRule` = 被越级的那条持久规则的**原文**。core 侧只过
|
|
668
|
+
* `inlineUntrusted`(中和 + 限长),**不**做秘密脱敏 —— 它的 JSDoc 也只承诺中和,不承诺无秘密
|
|
669
|
+
* (与同结构里的 `summary` 不同:那一格 core 明写「NEVER raw secrets / full args / env」)。
|
|
670
|
+
* 而规则原文是**人手写的文本**,一条 `Bash(curl -H "Authorization: Bearer …")` 形的规则完全拼得出来。
|
|
671
|
+
*
|
|
672
|
+
* 同步路(`tool_approval` 帧 / `card_json`)在铸点就 `redactSecrets` 了;durable 路两条读面
|
|
673
|
+
* (`GET /v1/approvals` 与 `/v1/approvals/stream`)此前把 `listPending()` 的行**整只**上 wire ⇒
|
|
674
|
+
* 同一份内容在两条腿上一条脱敏、一条不脱敏,而 operator 的队列是**跨租户**可见的那一条。
|
|
675
|
+
*
|
|
676
|
+
* 修在**读面**而不是落库点,理由是本仓成文的分工(同文件 inbox 段那条 `redactedPreview(s.toolInput)` 的
|
|
677
|
+
* 逐字理由):「core bounds but does NOT redact it — redaction is the consumer's job」。落库点改写会与
|
|
678
|
+
* core 的属主面打架,而且救不了**别的副本**已经写下的行。
|
|
679
|
+
*
|
|
680
|
+
* 🔴 只动这一格,不顺手洗整只 descriptor:`summary` 有 core 的无秘密承诺、`touchedPaths` 是路径,
|
|
681
|
+
* 两者的现行为不在本批辖域内(要改得走各自的三问)。键集**不变** —— 「整只透传、不按键投影」那条契约
|
|
682
|
+
* (`test/approval-shadowed-rule-wire.test.ts` §3)照旧成立,变的只是这一格的**内容**。
|
|
683
|
+
*/
|
|
684
|
+
function redactPendingDisclosures(row) {
|
|
685
|
+
const rd = row.riskDescriptor;
|
|
686
|
+
if (rd === null || rd === undefined || typeof rd.shadowedRule !== "string")
|
|
687
|
+
return row;
|
|
688
|
+
return { ...row, riskDescriptor: { ...rd, shadowedRule: redactSecrets(rd.shadowedRule) } };
|
|
689
|
+
}
|
|
662
690
|
/**
|
|
663
691
|
* GET /v1/approvals/stream (design/80 native push): SSE — pushes pending-approval deltas so the portal
|
|
664
692
|
* SUBSCRIBES ONCE instead of polling GET /v1/approvals every ~10s (better UX: near-real-time + no client poll
|
|
@@ -696,7 +724,9 @@ export async function streamApprovals(req, res, cs, scope, pollMs = APPROVALS_ST
|
|
|
696
724
|
while (!closed) {
|
|
697
725
|
let pending;
|
|
698
726
|
try {
|
|
699
|
-
|
|
727
|
+
// #209 件4:与 `GET /v1/approvals` **同一个**投影函数 —— 两条 durable 读面各洗各的就会漂
|
|
728
|
+
// (SSE 那条恰恰是 operator 常驻订阅的那条,漏掉它等于没修)。
|
|
729
|
+
pending = (await cs.listPending(scope)).map(redactPendingDisclosures);
|
|
700
730
|
}
|
|
701
731
|
catch {
|
|
702
732
|
// a transient TiDB blip must NOT kill the subscription — heartbeat + retry next tick (fail-soft)
|
|
@@ -3,6 +3,7 @@ import { cwdHonored } from "../../task-cwd.js";
|
|
|
3
3
|
import { mcpInjectionHonored } from "../../task-mcp.js";
|
|
4
4
|
import { sendJson, sendError } from "../send.js";
|
|
5
5
|
import { resolveStreamApprovalGate } from "../../tool-approval.js";
|
|
6
|
+
import { gatedPrincipal } from "../principal-gate.js";
|
|
6
7
|
export async function handleCapabilities(req, res, url, ctx) {
|
|
7
8
|
const miss = { fell: false };
|
|
8
9
|
await handleCapabilitiesBody(req, res, url, ctx, miss);
|
|
@@ -75,6 +76,14 @@ async function handleCapabilitiesBody(req, res, url, ctx, miss) {
|
|
|
75
76
|
: false;
|
|
76
77
|
})(),
|
|
77
78
|
leader: Boolean(deps.leaderEndpoint),
|
|
79
|
+
// #F1([3397]-5 同族):sandbox-image-pool 的 P1 面(`GET /v1/images`、`/v1/images/:profile`、
|
|
80
|
+
// `/v1/images/digests/:digest`、`POST /v1/images/select`)。谓词**逐字**是 routes/images.ts 里那条
|
|
81
|
+
// 挂载合取式的 `deps.imageIndex` —— 缺席时该域整个不挂载,同文件的能力臂回 501
|
|
82
|
+
// `capability.image_index_required`(此前是与拼错路由同码的 404,消费端只能 trial-by-404)。
|
|
83
|
+
// local 后端刻意不实现 `imageIndex()`(池是 cloud/fleet-only),所以单机形上这一位诚实为 false。
|
|
84
|
+
// ⚠️ 辖域**只到 P1**:`/v1/images/bakes*`(P2 烤制控制面)由独立的 `imageBakes` 依赖挂载,
|
|
85
|
+
// 且是 operator/runner 内部面,不在本位的承诺里。
|
|
86
|
+
images: Boolean(deps.imageIndex),
|
|
78
87
|
// design/138 S1 (clay 2026-07-08): the legacy MemoryStore surface (GET/DELETE /v1/memory + the MF-30
|
|
79
88
|
// session write verbs) was DEPRECATED with the store itself — the new memory engine is model file skills
|
|
80
89
|
// over the injected memory dir, with NO service HTTP verbs. Hard-coded false so a shell that still knows
|
|
@@ -103,7 +112,22 @@ async function handleCapabilitiesBody(req, res, url, ctx, miss) {
|
|
|
103
112
|
// #154 车二 + #203 §2:规则车道(cc-import 两口 + 撤销面两口 + respond 的 persistRule 兑付口)。
|
|
104
113
|
// 谓词与 `/v1/rules/*` 全族的 501 同源:ruleConsent lane 在场 ⟺ 总开关未被显式关 ∧ 规则店 wired
|
|
105
114
|
// (SQL 双方言,或 #203 起 local 的 File 三面束)。说 yes ⟺ **四口都活**,不是「有一口活」。
|
|
106
|
-
|
|
115
|
+
//
|
|
116
|
+
// 🔴 A-010.20(验真后修,补第二条合取项):店在场**不等于**这四口活。规则车道是
|
|
117
|
+
// **principal 寻址**的 —— `routes/rules.ts` 的域头逐字是 `if (principal === undefined) 401`
|
|
118
|
+
// (无条件,与 `requirePrincipal` 无关),而铸卡侧 `buildRuleLaneMaterial` 与回决侧
|
|
119
|
+
// `persistRuleAfterDecision` 都有同源的 `owner === null ⇒ 不进车道 / rule_lane_unavailable` 一门。
|
|
120
|
+
// 于是「装了店、但调用方没有可验证身份」的部署上,旧谓词报 true 而**四口全废**:两口恒 401、
|
|
121
|
+
// 卡上恒无候选、`persistRule` 恒被拒。#203 给 local 车道接上 File 店之后,「单机 + 无 principal」
|
|
122
|
+
// 第一次同时满足其余条件 —— 这不再是纸面形。
|
|
123
|
+
//
|
|
124
|
+
// 🔴 为什么这一位是**按调用方**算而不是按部署算:`requirePrincipal=false` 的部署上,带 principal
|
|
125
|
+
// 的调用方与不带的调用方**答案真的不同**(前者四口全活,后者全废),没有任何一个部署级布尔能同时
|
|
126
|
+
// 对两者不说谎。`/v1/capabilities` 是每请求读面且与 `/v1/rules` 走同一道全局凭证门,所以
|
|
127
|
+
// 「这一位说 yes」⟺「**你**拿同一份头去打那四口真的能用」——这正是本文件顶注承诺的那条等价。
|
|
128
|
+
// ⚠️ 与下面的 `permissionRulesRevoke` **刻意不同源**:那一位是**存在性/版本**信号(见其注),
|
|
129
|
+
// 必须与调用方身份无关,否则老版本探测会把「我没带 principal」误读成「这个 worker 太老」。
|
|
130
|
+
permissionRules: Boolean(deps.ruleConsent) && gatedPrincipal(req, deps.config) !== undefined,
|
|
107
131
|
// #205 SDK 车上游请托二连(2026-08-10):
|
|
108
132
|
// · `permissionRulesRevoke` —— 撤销面两口(GET/DELETE /v1/rules)**存在性**的独立信号。谓词与
|
|
109
133
|
// `permissionRules` 同源,但**在场性**本身就是版本信号:≤7.11.0 的 worker `permissionRules` 已为
|
|
@@ -135,8 +159,11 @@ async function handleCapabilitiesBody(req, res, url, ctx, miss) {
|
|
|
135
159
|
// served this task — the RESOLVED `Model.id`, not the requested `TaskSpec.model` ref … Lets a UI echo
|
|
136
160
|
// 'served by X'". A grep for the identifier `effectiveModel` returns zero hits in core and that was mistaken
|
|
137
161
|
// for "core doesn't have it" (search by SEMANTICS, not by identifier name). It already reaches the wire:
|
|
138
|
-
// `POST /v1/tasks` returns the TaskResult verbatim (
|
|
139
|
-
//
|
|
162
|
+
// `POST /v1/tasks` returns the TaskResult verbatim (`routes/tasks.ts` `sendSubmitResult` — the sync submit
|
|
163
|
+
// leg's one response mouth; design/158 A9 moved the whole sync-submit surface OUT of `http/server.ts`, so the
|
|
164
|
+
// old `~L1852` anchor into that file is dead), the SDK declares `TaskResult.model`, and this server already
|
|
165
|
+
// consumes the resolved id internally for cache-family accounting (`budget.ts` `cacheFamilyOfMirror` + its
|
|
166
|
+
// caller in `routes/side-query.ts`, NOT `http/server.ts`). Pinned by
|
|
140
167
|
// `test/effective-model-echo-wire.test.ts` (real HTTP: resolved id echoed, absent key stays absent) so a
|
|
141
168
|
// future projection/whitelist on this leg can't silently drop it — the `toolEndEventData`-label bug class.
|
|
142
169
|
// Nothing is advertised as a capability BIT here because an additive result field needs no gate
|
|
@@ -330,9 +357,12 @@ async function handleCapabilitiesBody(req, res, url, ctx, miss) {
|
|
|
330
357
|
// `workflowsCapability(deps)` (hardened script runner ∧ governance), boot-computed in main.ts. Falls back
|
|
331
358
|
// to the `Boolean(workflowRunStore)` proxy when the boot flag isn't wired (older callers / tests).
|
|
332
359
|
// • `workflowsList` = can durably LIST workflow runs = `Boolean(workflowRunStore)` (GET /v1/workflows* 501s
|
|
333
|
-
// without it — list AND detail/stream, see the store gate
|
|
360
|
+
// without it — list AND detail/stream, see the store gate in `routes/workflows.ts`, the
|
|
361
|
+
// `if (!deps.workflowRunStore)` → 501 `capability.self_orchestration_required` arm).
|
|
334
362
|
// ⚠️ In the CURRENT service wiring these two COINCIDE: `workflowRunStore` is built IFF self-orchestration is on
|
|
335
|
-
// (InMemoryWorkflowRunStore when WORKFLOW_RUN_STORE=memory, else File —
|
|
363
|
+
// (InMemoryWorkflowRunStore when WORKFLOW_RUN_STORE=memory, else File — built in
|
|
364
|
+
// `boot/workflow-orchestration.ts`, which design/158 A10 split out of main.ts; main.ts itself no longer
|
|
365
|
+
// mentions InMemoryWorkflowRunStore at all, so the old `main.ts ~L527` anchor is dead), the SAME gate that
|
|
336
366
|
// wires the safe script runner `workflowsCapability` checks → `Boolean(workflowRunStore)` ≡ `workflowsCapability`
|
|
337
367
|
// always today. So this split is core's FORWARD-LOOKING orthogonal-axes CONTRACT, NOT a fix for a live
|
|
338
368
|
// divergence (the earlier "memory backend → no store → divergence" framing was WRONG — InMemory IS a store;
|
|
@@ -490,6 +490,24 @@ async function handleImagesBody(req, res, url, ctx, miss) {
|
|
|
490
490
|
return;
|
|
491
491
|
}
|
|
492
492
|
}
|
|
493
|
+
else if (url === "/v1/images" || url.startsWith("/v1/images/")) {
|
|
494
|
+
// 🔴 #F1([3397]-5 同族缺口):索引缺席时**整个域不挂载**,请求此前一路落到全局路由表尾的
|
|
495
|
+
// `404 not_found.route` —— 与「路由拼错了」同一个码,消费端结构上分不出「这个部署没接这个面」
|
|
496
|
+
// 与「我 URL 写错了」(trial-by-404 的最坏形:两种成因的处置完全相反)。改成诚实的能力拒。
|
|
497
|
+
//
|
|
498
|
+
// 谓词与上面那条挂载合取式**同一个符号**(`deps.imageIndex`),所以 `/v1/capabilities` 的 `images`
|
|
499
|
+
// 位(同一谓词)说 yes ⟺ 这条臂不触发 ⟺ 目录读面真能用。
|
|
500
|
+
//
|
|
501
|
+
// ⚠️ 判据是**按段**的(`=== "/v1/images"` 或 `"/v1/images/"` 前缀),不是上面挂载式那个裸前缀:
|
|
502
|
+
// `/v1/imagesfoo` 是**拼错的路由**,不是本域的路径,它该继续拿 404(索引在场的部署上它本来就落 404
|
|
503
|
+
// —— 两种部署形对同一个手滑给出不同的码,那正是本臂要消灭的那种含糊)。
|
|
504
|
+
//
|
|
505
|
+
// ⚠️ bake 子面**不误伤**:`deps.imageBakes` 在场时 `/v1/images/bakes*` 已在本函数最上面的域里应答
|
|
506
|
+
// 并 `return`,走不到这里;两者都缺席时(local 后端的真实形态——两个工厂它都不实现)本臂如实
|
|
507
|
+
// 回答「这个部署没有 sandbox-image-pool」,那对 bakes 路径也是真话。
|
|
508
|
+
sendError(res, 501, "capability.image_index_required", "the sandbox-image-pool requires a SQL store backend (DB_BACKEND=mysql|pg) — the local backend wires no image index");
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
493
511
|
miss.fell = true;
|
|
494
512
|
}
|
|
495
513
|
/** The image_bake SSE (P2.2/P2.8) — the SAME pump, with the run-trace-SSE-shaped bake frame
|
package/dist/http/routes/runs.js
CHANGED
|
@@ -161,11 +161,27 @@ async function streamRunEvents(req, res, deps, runStore, taskId, staleMs) {
|
|
|
161
161
|
// 「开关关 = 逐字零变化」(§6.5/§7.2)当场破。谓词同时省掉了关闭态每次开流的一次 store 往返
|
|
162
162
|
// 与那份 store 故障暴露面。钉:场景9(零帧 **且** 零 store 读 —— 只钉零帧会放过「读了但没投」)。
|
|
163
163
|
preamble: async (signal) => {
|
|
164
|
-
|
|
164
|
+
// 🔴 A-010.7(验真后修):谓词是**上场门**,不是裸开关。
|
|
165
|
+
// 原式是 `if (!config.streamApproval.enabled) return []` + 一个 `backend?.approvalAsk()` 兜底,
|
|
166
|
+
// 那是本仓早就收编掉的「局部合取式」形(见本文件 askDecision 口的头注:车4 落地时的临时判据,
|
|
167
|
+
// 交接条件写死了「换成消费**那一个**谓词」)。两处不同源的后果在 `STREAM_APPROVAL` 默认翻 ON
|
|
168
|
+
// 之后才显形:上场门是**五**项合取(还要 toolApproval 在场、账本非易失 `kind !== "local"`、
|
|
169
|
+
// park 设施在场),而这里只看了两项 ⇒ 一个协议**没有上场**的 SQL 部署
|
|
170
|
+
// (典型:`TOOL_APPROVAL_ENABLED=false`,或没开 `DURABLE_APPROVAL` ⇒ 无 park 设施)
|
|
171
|
+
// **每一次开流都照打一次库** —— 一次纯白打的 `listPendingByTask`,外加那份 store 故障暴露面,
|
|
172
|
+
// 而它按契约永远只能投出零帧(协议没上场 ⇒ 没有任何一条 `STREAM_PENDING` 行是本协议铸的)。
|
|
173
|
+
// 换成单一谓词后,「能力面说 false」⟺「本腿零 store 往返」是结构成立的。
|
|
174
|
+
const gate = resolveStreamApprovalGate({
|
|
175
|
+
toolApprovalEnabled: Boolean(deps.toolApproval),
|
|
176
|
+
streamApprovalEnabled: deps.config.streamApproval?.enabled === true,
|
|
177
|
+
backend: deps.backend,
|
|
178
|
+
parkFacility: deps.checkpointStore !== undefined,
|
|
179
|
+
});
|
|
180
|
+
// 协议未上场 ⇒ 零帧**且零 store 读**(场景9 的钉打的就是这两件事,不只是零帧)。
|
|
181
|
+
// env-only worker(无 durable backend)在 `no_backend` 那条臂上一并收掉:无 ask 账 ⇒ 无对账基准。
|
|
182
|
+
if (!gate.active)
|
|
165
183
|
return [];
|
|
166
|
-
const store =
|
|
167
|
-
if (!store)
|
|
168
|
-
return []; // env-only worker(无 durable backend)= 无 ask 账 ⇒ 无对账基准,诚实缺席
|
|
184
|
+
const store = gate.askStore;
|
|
169
185
|
const rows = await store.listPendingByTask(taskId, signal);
|
|
170
186
|
const { frames, skipped, dropped } = buildApprovalPreamble(rows, Date.now(), deps.config.streamApproval.replayMax);
|
|
171
187
|
if (skipped > 0)
|
|
@@ -284,7 +300,7 @@ async function handleRunsBody(req, res, url, ctx, miss) {
|
|
|
284
300
|
return { status: 202, body: { taskId: clientTaskId, sessionId, status: "running" } };
|
|
285
301
|
}
|
|
286
302
|
// [2255]① 真出路材料:activeTaskStatus + parked 时 pendingGate{kind,decidePath}(best-effort,失败退旧形)
|
|
287
|
-
return { status: 409, body: await buildActiveRunConflict({ runStore, checkpointStore: deps.checkpointStore }, sessionId, created.activeTaskId) };
|
|
303
|
+
return { status: 409, body: await buildActiveRunConflict({ runStore, checkpointStore: deps.checkpointStore, governance: deps.config }, sessionId, created.activeTaskId) };
|
|
288
304
|
}
|
|
289
305
|
// Durable F4: persist the resume rebuild inputs (sessionId-keyed) so an operator can resume from any
|
|
290
306
|
// replica even after this worker is gone — core's checkpoint blob can't carry service scenario context.
|
|
@@ -219,14 +219,26 @@ async function handleTasksBody(req, res, url, ctx, miss) {
|
|
|
219
219
|
// 有界(2s)+ fail-open:store 抛或挂起都只记一次 warn、零卡继续开流(§5.3)。
|
|
220
220
|
// 🔴 开关谓词与 runs.ts 同源同理由(codex 复审第二轮 high:回滚下表里可能有残留 STREAM_PENDING
|
|
221
221
|
// 行,无谓词会破「开关关 = 逐字零变化」;顺带省掉关闭态每次开流的一次 store 往返)。钉:场景9。
|
|
222
|
-
|
|
222
|
+
//
|
|
223
|
+
// 🔴 A-010.7 随修(**兄弟扫查**:台账只点名了 runs.ts 那一处,这里是同形的第二处):谓词是
|
|
224
|
+
// **上场门**不是裸开关。五项合取(协调器在场 / 开关真 / backend 在场 / 账本非易失 / park 设施在场)
|
|
225
|
+
// 里少查三项 ⇒ 一个协议根本没上场的 SQL 部署,每次开流仍要白打一次库;回滚时更坏 —— 别的副本
|
|
226
|
+
// 落下的残留 `STREAM_PENDING` 行会被无门的重放腿照常投上 wire。换成与协调器注入 / 能力面 /
|
|
227
|
+
// 回决 501 口**同一个**谓词,「说没上场」⟺「零帧且零 store 读」才是结构成立的。
|
|
228
|
+
const replayGate = resolveStreamApprovalGate({
|
|
229
|
+
toolApprovalEnabled: Boolean(deps.toolApproval),
|
|
230
|
+
streamApprovalEnabled: deps.config.streamApproval?.enabled === true,
|
|
231
|
+
backend: deps.backend,
|
|
232
|
+
parkFacility: deps.checkpointStore !== undefined,
|
|
233
|
+
});
|
|
234
|
+
const replaySessionId = replayGate.active ? prepared.spec.sessionId : undefined;
|
|
223
235
|
if (replaySessionId) {
|
|
224
236
|
const replayOwner = gatedPrincipal(req, deps.config) ?? null;
|
|
237
|
+
const askStore = replayGate.active ? replayGate.askStore : undefined;
|
|
225
238
|
const preambleFrames = await collectSsePreamble(async (signal) => {
|
|
226
|
-
|
|
227
|
-
if (!store)
|
|
239
|
+
if (!askStore)
|
|
228
240
|
return [];
|
|
229
|
-
const rows = await
|
|
241
|
+
const rows = await askStore.listPendingBySession(replaySessionId, replayOwner, signal);
|
|
230
242
|
const { frames, skipped, dropped } = buildApprovalPreamble(rows, Date.now(), deps.config.streamApproval.replayMax);
|
|
231
243
|
if (skipped > 0)
|
|
232
244
|
deps.logger?.warn?.("approval_preamble_row_skipped", { sessionId: replaySessionId, skipped, face: "tasks.stream" });
|
|
@@ -307,7 +319,7 @@ async function handleTasksBody(req, res, url, ctx, miss) {
|
|
|
307
319
|
if (created.ok)
|
|
308
320
|
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]②)
|
|
309
321
|
if (!created.ok) {
|
|
310
|
-
const conflict = await buildActiveRunConflict({ runStore: deps.runStore, checkpointStore: deps.checkpointStore }, prepared.spec.sessionId, created.activeTaskId); // [2255]① 真出路材料
|
|
322
|
+
const conflict = await buildActiveRunConflict({ runStore: deps.runStore, checkpointStore: deps.checkpointStore, governance: deps.config }, prepared.spec.sessionId, created.activeTaskId); // [2255]① 真出路材料
|
|
311
323
|
// Headers are already SSE — encode the rejection as the stream's terminal event; the 409 in
|
|
312
324
|
// the returned resp is for a deduplicated concurrent caller (and is never idem-cached).
|
|
313
325
|
// done 帧 additive 携带同一份材料 —— 形状铸在 toDoneFrameResult(有名字有类型),不在这行内联挑键。
|
|
@@ -1225,7 +1237,7 @@ async function handleTasksBody(req, res, url, ctx, miss) {
|
|
|
1225
1237
|
const tid = uuidv7();
|
|
1226
1238
|
const created = await deps.runStore.createRun(tid, prepared.spec.sessionId, principal ?? null, deps.instanceId ?? "default", runMeta(prepared, source));
|
|
1227
1239
|
if (!created.ok)
|
|
1228
|
-
return { status: 409, body: await buildActiveRunConflict({ runStore: deps.runStore, checkpointStore: deps.checkpointStore }, prepared.spec.sessionId, created.activeTaskId) }; // [2255]① 真出路材料
|
|
1240
|
+
return { status: 409, body: await buildActiveRunConflict({ runStore: deps.runStore, checkpointStore: deps.checkpointStore, governance: deps.config }, prepared.spec.sessionId, created.activeTaskId) }; // [2255]① 真出路材料
|
|
1229
1241
|
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]②)
|
|
1230
1242
|
durableTaskId = tid;
|
|
1231
1243
|
}
|
|
@@ -255,6 +255,11 @@ async function handleTaskArtifacts(res, runStore, taskId) {
|
|
|
255
255
|
* trace 的 tool-result 块与本判据读的是同一份日志,所以「trace 上看得见 ⇒ 这里读得到」成立;
|
|
256
256
|
* 同 session 里**别的 task** 的 ref(或日志已过保留窗被逐出的)按 404 同形拒,请到那条 task 上读。
|
|
257
257
|
*
|
|
258
|
+
* 🟢 **两条残余已随 core 5.26.0 #119 销账**(下面原文保留作病历)。core 按当年这里写的方向动了刀:
|
|
259
|
+
* `put` 带出处落库 + `ownerOf` 读回,读面改用出处判定(实现见 ②-a),于是 (a) 合成 id 铸的 ref 不再
|
|
260
|
+
* 读不到、(b) 拼接非单射的越权面结构上消失(ref 铸法本身也换成 `~` 四段单射)。日志重铸只剩**存量
|
|
261
|
+
* 兜底**一条腿(无出处的旧行),新写入一律经出处。
|
|
262
|
+
*
|
|
258
263
|
* ⚠️ 两处**已知残余**,都要 core 侧动刀才能消,写在明处而不是让下一个人自己撞(codex 复审 R3):
|
|
259
264
|
*
|
|
260
265
|
* (a) **覆盖面**:core 除了「按工具调用 id 卸载」这条主路,还从**合成 id** 铸 ref ——
|
|
@@ -315,22 +320,46 @@ ref, query) {
|
|
|
315
320
|
const run = ownedRun ?? (await runStore.getRun(taskId));
|
|
316
321
|
if (!run)
|
|
317
322
|
return notFound();
|
|
318
|
-
//
|
|
319
|
-
//
|
|
320
|
-
//
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
323
|
+
// ②-a 出处判定(#119,core 5.26.0 —— 上面 (a)/(b) 两条残余的**正解到货**)。
|
|
324
|
+
// 店里现在自己记着「这枚 ref 属于谁」(`put` 的第三参落库,`ownerOf` 读回),读面拿**自己已知的字段**
|
|
325
|
+
// 比对即可:零解析、零段转义规则复制,合成 id 铸的 ref 与拼接歧义两条残余同时消失。
|
|
326
|
+
//
|
|
327
|
+
// 🔴 为什么必须改而不是"锦上添花":5.26.0 起 offload ref 是四段单射形(内容坐标进 ref),服务端手里
|
|
328
|
+
// 没有内容 ⇒ 下面那条重铸判据**重铸不出来** ⇒ 每一枚真 offload ref 都会 404。读面整死。
|
|
329
|
+
//
|
|
330
|
+
// ⚠️ 辖域(有意的口径变化,写在明处):出处的粒度由 core 定 —— offload/budget/projection 三个写点只声明
|
|
331
|
+
// session,把 taskId 钉在那里会把「一个 session 的两条 task 共用一枚 ref」这个**设计内**的共享形变成拒绝
|
|
332
|
+
// (core d.ts 逐字裁过)。所以有出处时的判据是「同 session,且出处**若点名了** task 则必须是本 task」——
|
|
333
|
+
// 相对旧的「本 task 日志作证过」是**放宽到同 session**。放宽的边界仍在 owner 之内(run 归属门在本函数
|
|
334
|
+
// 之前已过,session 的属主就是这个 caller),换来的是这条读面在 5.26.0 上还活着。
|
|
335
|
+
const owner = store?.ownerOf ? await store.ownerOf(ref) : undefined;
|
|
336
|
+
const ownedByThisCaller = owner !== undefined && owner.sessionId === run.sessionId && (owner.taskId === undefined || owner.taskId === taskId);
|
|
337
|
+
// ②-b 无出处兜底:**没声明属主**的行按老判据走 —— 本 task 耐久日志作证过的 toolCallId,经 core 同一个
|
|
338
|
+
// 铸造点变成 ref 再精确相等。unowned 本身不是授权(core:读面对 unowned 一律 fail-closed),日志作证才是。
|
|
339
|
+
//
|
|
340
|
+
// ⚠️ 这条**不是**「读得到升级前的存量行」(codex 复审 medium,已核真并改口):5.26.0 的
|
|
341
|
+
// `buildToolResultRef` 铸的是 `~` 分隔形,重铸不出旧引擎的 `tr_<sid>_<call>`,而按旧规则手写一个 legacy
|
|
342
|
+
// 编码器 = 复制上游规则(本文件头注自己禁掉的那件事)。真正的处置在别处 —— 本批要求**删表重建**
|
|
343
|
+
// `tool_result`(见 CHANGELOG),所以升级后库里根本没有旧行。这一支覆盖的是「5.26.0 引擎写下、但写点
|
|
344
|
+
// 没声明出处」的行(core 自家的 offload 写点都带出处,所以现实里几乎为空),留着是纵深不是承诺。
|
|
345
|
+
// 成本口径:整份日志一读一扫,与同域的 `/turns`、`/artifacts` 同款;有出处时**根本不读日志**。
|
|
346
|
+
if (!ownedByThisCaller) {
|
|
347
|
+
if (owner !== undefined)
|
|
348
|
+
return notFound(); // 出处在,但不是这位 —— 不给第二次机会(日志重铸绕不过出处)
|
|
349
|
+
const events = await runStore.getEvents(taskId, 0);
|
|
350
|
+
let attested = false;
|
|
351
|
+
for (const ev of events) {
|
|
352
|
+
// 取键姿势与同域的 `trace/artifacts.ts` 逐字同款(同一份日志、同一个字段,读法不该有第二种)。
|
|
353
|
+
const { toolCallId: id } = (ev.data ?? {});
|
|
354
|
+
// 逐条比对而不是先建全集:命中即停,长日志上不必把整份集合物化。
|
|
355
|
+
if (typeof id === "string" && id.length > 0 && buildToolResultRef(run.sessionId, id) === ref) {
|
|
356
|
+
attested = true;
|
|
357
|
+
break;
|
|
358
|
+
}
|
|
330
359
|
}
|
|
360
|
+
if (!attested)
|
|
361
|
+
return notFound();
|
|
331
362
|
}
|
|
332
|
-
if (!attested)
|
|
333
|
-
return notFound();
|
|
334
363
|
if (!store)
|
|
335
364
|
return notFound(); // durable store 缺席:404 同形,不 501(不泄露部署形)
|
|
336
365
|
const slice = await store.get(ref, { ...(offset !== undefined ? { offset } : {}), ...(limit !== undefined ? { limit } : {}) });
|
package/dist/http/server.d.ts
CHANGED
|
@@ -511,23 +511,49 @@ export declare function validateUserSkills(skills: unknown): string | null;
|
|
|
511
511
|
* POST /v1/runs → async: 202 {taskId, sessionId, status}, runs in background (S1)
|
|
512
512
|
* GET /v1/runs/:id → run status + result (poll)
|
|
513
513
|
* GET /v1/runs/:id/events → SSE replay from Last-Event-ID, then tail to terminal (S2)
|
|
514
|
-
* POST /v1/sessions/:id/
|
|
514
|
+
* POST /v1/sessions/:id/notify → inject an out-of-session event (design/144 §2): live stream ⇒ core
|
|
515
|
+
* `TaskStream.notify()`; idle ⇒ parked in the session inbox, drained
|
|
516
|
+
* as a `task_notification` on the next stream open
|
|
517
|
+
* POST /v1/sessions/:id/wake → wake a `task_done` pure park with a message and resume it
|
|
518
|
+
* (design/144 §3). NOT a gate decision — a pending gate is refused
|
|
519
|
+
* (`wake.gate_pending`). Goes through `resumeWake`, which reuses the
|
|
520
|
+
* resume family's lease/CAS/run-log leg, so it BURNS MODEL TOKENS and
|
|
521
|
+
* is subject to the billable-submit gates (`SESSION_WAKE_RE`).
|
|
522
|
+
* (both live in `http/routes/notify-wake.ts` — design/158 A9 domain split)
|
|
515
523
|
*/
|
|
516
524
|
export declare function createHttpServer(rawDeps: ServiceDeps): http.Server & {
|
|
517
525
|
denyExpiredApprovals: (now: number) => Promise<void>;
|
|
518
526
|
};
|
|
519
|
-
/** SSE: replay durable events after Last-Event-ID, then tail until the run is terminal (or stale). */
|
|
520
|
-
/**
|
|
521
|
-
* The differences a concrete log (task_run | image_bake) feeds the ONE resumable SSE reader (P2.8). Everything
|
|
522
|
-
* the reader does — Last-Event-ID/`?from=` resume, the 416 retention boundary, the per-poll concurrent
|
|
523
|
-
* status+events read, the terminal re-fetch (the terminal event lands in the gap before the status flips), the
|
|
524
|
-
* stale fallback, the 15-min cap, the 15s idle heartbeat — is provider-agnostic and lives in `streamSseLog`.
|
|
525
|
-
* 🔴 The task_run provider MUST keep the existing wire bytes EXACTLY (center's relay + 730+ tests depend on it).
|
|
526
|
-
*/
|
|
527
527
|
/** POST endpoints that trigger BILLABLE work — the fail-closed auth guard must cover ALL of them (council: the
|
|
528
528
|
* guard's inline list had drifted from the handlers and missed `/v1/approvals/:id/decide`, which resumes a run
|
|
529
529
|
* via resumeCheckpoint/store.decide → paid tokens). Keep this in sync when adding a billable POST route. */
|
|
530
530
|
export declare function isBillableSubmitPath(url: string): boolean;
|
|
531
|
+
/**
|
|
532
|
+
* A-010.23(#209 件5)—— **改写门**:不烧模型、但在**没有任何 service credential** 的部署形下必须与
|
|
533
|
+
* billable / bake 两族一样 fail-closed 的写门。谓词与 `isBillableSubmitPath` **刻意分开**:这几扇门
|
|
534
|
+
* `billable=false`(零模型工作),塞进那张名单会让它们连带吃 drain 与 model-roster-pending 两道 503 ——
|
|
535
|
+
* 而收编与规则读写在排空期/roster 未落时做完全无害。
|
|
536
|
+
*
|
|
537
|
+
* 判据(两个合取项,缺一不进本表)= 「授权的唯一输入是 principal 头」∧「动作是**持久改写**」:
|
|
538
|
+
* · `POST /v1/adoption` —— operator-only 的一次性部署级动作,把一个 principal 名下**每一行**重写到另一个
|
|
539
|
+
* 身份;授权判据 `explicitOperatorOk(gatedPrincipal(...))`。
|
|
540
|
+
* · `POST /v1/rules/cc-import/*` —— principal lane 的**持久 allow 规则**铸造口;一条持久 allow 规则对后续
|
|
541
|
+
* 同命令的 classify 档 ask 常驻消音(core #144 之后仍然如此,只是不再消 mandated 的那些)。
|
|
542
|
+
* · `DELETE /v1/rules` —— 同一个规则店的**撤销**口。方向相反(收紧)但同样是持久改写,而且它多一条
|
|
543
|
+
* operator 越权域(`?principal=` / body 的 `principal` 让 operator 收回**任一**租户的规则)⇒ 伪造一个
|
|
544
|
+
* 列在 `OPERATOR_PRINCIPALS` 里的头,就能把每一位租户的规则一次清空。
|
|
545
|
+
* 无 service token 的部署上 `verifiedPrincipal` 走的是「BFF/gated:头已由上游验过」那一支,而这一形恰恰
|
|
546
|
+
* 是**没有**那个上游 —— 于是伪造一个头就能收编别人、替任意租户种下常驻放行、或替他们全删。bake 门当年
|
|
547
|
+
* 补的就是同一句话,`isDestructiveSessionWrite` 从逐条路由白名单改成族判定也是同一句话(那次的措辞逐字是
|
|
548
|
+
* 「a forged principal header alone could **tighten/DoS** a session's tools」——撤销口落的正是 DoS 那一半)。
|
|
549
|
+
*
|
|
550
|
+
* 🔴 **方法感知**(codex 对抗复审 R2-[high],验真后修):撤销走的是 `DELETE`,而本门最初只挂在 `POST` 的
|
|
551
|
+
* 合取里 —— 于是一个自称「持久改写」的谓词把爆炸半径最大的那条动词漏在门外,正是 `isDestructiveSessionWrite`
|
|
552
|
+
* 当年被抓到的同一形。签名因此收方法,与那只谓词逐字同形。
|
|
553
|
+
* 🔴 读面不进本表:`GET /v1/adoption/:id` 与 `GET /v1/rules` 各有自己的属主/operator 门,本门只拦写。
|
|
554
|
+
* 行为面钉:`test/rewrite-door-service-token-gate.test.ts`(四扇门各一正一反 + 逃生口格 + 读面负控)。
|
|
555
|
+
*/
|
|
556
|
+
export declare function isCredentialGatedRewrite(method: string, url: string): boolean;
|
|
531
557
|
/** Stable, low-cardinality route label for metrics/logs (ids collapsed to `:id`).
|
|
532
558
|
* [#104] 字面量**先于**模式:此前模式先查,五条精确路由被形状桶吞掉(`/v1/approvals/stream`
|
|
533
559
|
* 落 ":id"、`/v1/images/{bakes,select,register}` 落 ":profile"、`/v1/images/bakes/claim` 落
|