@sema-agent/server 7.11.0 → 7.13.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/README.md +1 -1
- package/USAGE.md +86 -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/auth-keys.d.ts +28 -4
- package/dist/auth-keys.js +60 -15
- package/dist/boot/parked-revive-gate.d.ts +18 -2
- package/dist/boot/parked-revive-gate.js +136 -14
- package/dist/boot/permission-rules-audit.d.ts +49 -0
- package/dist/boot/permission-rules-audit.js +85 -0
- package/dist/boot/reapers.d.ts +15 -0
- package/dist/boot/reapers.js +101 -44
- package/dist/boot/resolve-spec.js +43 -12
- package/dist/boot/runner-deps.d.ts +16 -2
- package/dist/boot/runner-deps.js +5 -4
- package/dist/budget.js +22 -0
- package/dist/config-types.d.ts +31 -11
- package/dist/config.d.ts +28 -2
- package/dist/config.js +348 -79
- package/dist/governance-ask-marks.js +8 -2
- package/dist/http/active-run-conflict.d.ts +33 -8
- package/dist/http/active-run-conflict.js +37 -2
- package/dist/http/route-ctx.d.ts +6 -3
- package/dist/http/routes/adoption.js +25 -2
- package/dist/http/routes/approvals-assistant.js +35 -4
- package/dist/http/routes/capabilities.js +69 -10
- package/dist/http/routes/images.js +18 -0
- package/dist/http/routes/rules.d.ts +19 -7
- package/dist/http/routes/rules.js +180 -4
- package/dist/http/routes/runs.js +21 -5
- package/dist/http/routes/tasks.js +18 -6
- package/dist/http/server.d.ts +30 -10
- package/dist/http/server.js +183 -19
- package/dist/http/wire-types.d.ts +48 -0
- package/dist/main.js +65 -7
- package/dist/observability/fail-open.d.ts +8 -0
- package/dist/observability/fail-open.js +8 -0
- package/dist/observability/metrics.js +2 -1
- package/dist/observability/tool-trace.d.ts +5 -1
- package/dist/observability/tool-trace.js +33 -6
- package/dist/parked-decide.d.ts +13 -3
- package/dist/parked-decide.js +10 -1
- package/dist/plugins/adoption-log-sql.d.ts +40 -0
- package/dist/plugins/adoption-log-sql.js +69 -2
- package/dist/plugins/file-run-store.d.ts +85 -1
- package/dist/plugins/file-run-store.js +450 -17
- package/dist/plugins/permission-rule-store-file.d.ts +83 -0
- package/dist/plugins/permission-rule-store-file.js +371 -0
- package/dist/plugins/permission-rule-store-sql.d.ts +52 -0
- package/dist/plugins/permission-rule-store-sql.js +71 -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 +12 -6
- package/dist/plugins/store-backend.js +82 -10
- package/dist/rules-consent.d.ts +98 -1
- package/dist/rules-consent.js +84 -1
- package/dist/run-local.js +126 -15
- package/dist/runtime-governance.d.ts +33 -0
- package/dist/runtime-governance.js +41 -3
- package/dist/task-settings.d.ts +44 -0
- package/dist/task-settings.js +57 -1
- package/dist/tool-approval.d.ts +38 -1
- package/dist/tool-approval.js +125 -26
- package/dist/trace/core-keyset-guard.d.ts +14 -3
- package/dist/trace/project.d.ts +19 -2
- package/dist/trace/project.js +24 -4
- package/package.json +3 -3
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* [2942]/[2943] `governanceForced` 的**判定缝** —— operator 治理层产的 ask 在 server 进程内的标记通道。
|
|
3
3
|
*
|
|
4
|
-
* 🔴 为什么需要一条 out-of-band 通道(侦察结论,亲验装树 core 5.16.x
|
|
5
|
-
* `PermissionResult.decisionReason`(
|
|
4
|
+
* 🔴 为什么需要一条 out-of-band 通道(侦察结论,亲验装树 core dist;5.16.x 首次落笔,5.23.0 复核仍成立):
|
|
5
|
+
* `PermissionResult.decisionReason`(闭集,词表以 core 的 `DecisionReason` 为准 —— 本文**刻意不复述**那
|
|
6
|
+
* 张表:复述出来的词表会随 core 加员静默过期,而这段话的论点与表里有几个词无关)**到不了帧铸点**。
|
|
6
7
|
* core 在 `dist/core/runner/prepare-task.js` 的 ask 铸造点(`resolveAsk({...})`,三处继承臂 + 主臂)只把
|
|
7
8
|
* `toolName / toolCallId / args / preview / message / askSourceIdentity() / riskAxesOf() / requiresRealApproval`
|
|
8
9
|
* 装进 `AskRequest`;`decisionReason` 连同整个 `PermissionResult` 一起留在 core 内部。`AskRequest` 的类型面
|
|
9
10
|
* (`dist/core/tool-policy.d.ts:78`)也确认没有这个键。所以「映射 decisionReason ⇒ governanceForced」这条
|
|
10
11
|
* 缝在**当前 core 上不存在**——它不是没接线,是没有这个字段可读。
|
|
12
|
+
* ([3372] 提货批复核)core 5.23.0 给这个闭集加了 `org_rule` / `org_unavailable` 两个**治理来源**的词
|
|
13
|
+
* (design/182 org 层)。这看着像是本表终于有了上游判据,其实**不是**:①`AskRequest` 的类型面在 5.23.0
|
|
14
|
+
* 上逐字未变,那两个词仍然到不了帧铸点(缝还是不存在);②即便够得着,它们说的是**组织**发布的快照,
|
|
15
|
+
* 与本键的语义(**本部署运维治理层**产的 ask)不是一回事,直接映射会把两种来源混成一个标记。本仓无
|
|
16
|
+
* org 层装配口(`ToolGateInput.orgRules` 无声明点),该臂当前不可达。
|
|
11
17
|
*
|
|
12
18
|
* 另一侧同样不可用:`riskAxes.irreversible` 在 shellGate 上场时对**每一次** shell 调用都为真(core
|
|
13
19
|
* `prepare-task.js:1620-1631` 无条件 `irreversibilityTier.set("Bash"/"Monitor", …)`),与「这只 ask 是谁
|
|
@@ -13,17 +13,37 @@
|
|
|
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 { type Autonomy } from "../runtime-governance.js";
|
|
17
|
+
/** #220 归因的部署侧入参(两个治理旋钮;判据属主见 `governanceMandatesShellGateAlways`)。 */
|
|
18
|
+
export interface GovernancePosture {
|
|
19
|
+
autonomy?: Autonomy;
|
|
20
|
+
manualModeShellGate?: "always" | "classify";
|
|
21
|
+
}
|
|
16
22
|
/** 与 runs.ts/tasks.ts 三个 409 位共享的旧文案(byte-frozen:api-error-text-freeze 门认这句)。 */
|
|
17
23
|
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)";
|
|
24
|
+
/**
|
|
25
|
+
* 409 体(与 SSE done 帧 result 共用)里的**待批门材料**。
|
|
26
|
+
*
|
|
27
|
+
* `kind`/`decidePath` = 出路(去哪决议);#220 追加的 `governanceForced` = **出身**(这道门是谁下的)——
|
|
28
|
+
* [3513] T3「park 后待批通道事件」的读者此前只知道「有一道 X 型门」,不知道它是运维治理层强制的还是
|
|
29
|
+
* 别的来源,于是「我都开了 bypassPermissions 为什么还在问」这个 UX 缺口在 park 腿上原样复现
|
|
30
|
+
* ([3031]#1/[3038] 在活卡腿上的实证)。
|
|
31
|
+
*
|
|
32
|
+
* 🔴 **additive + 只在为真时在场**:与活卡帧 `ToolApprovalFrame.governanceForced` **同一条纪律**——
|
|
33
|
+
* 缺席 ≠「这不是治理门」,而是「没有治理来源的证据」(判据够不着的形也落在缺席里,见装配处的注)。
|
|
34
|
+
*/
|
|
35
|
+
export interface PendingGateMaterial {
|
|
36
|
+
kind: string;
|
|
37
|
+
decidePath: string;
|
|
38
|
+
/** 见 {@link PendingGateMaterial} 顶注:`true` 才在场,恒不写 `false`。 */
|
|
39
|
+
governanceForced?: true;
|
|
40
|
+
}
|
|
18
41
|
export interface ActiveRunConflictBody {
|
|
19
42
|
error: string;
|
|
20
43
|
errorCode: "conflict.session_active_run";
|
|
21
44
|
activeTaskId: string | null;
|
|
22
45
|
activeTaskStatus?: string;
|
|
23
|
-
pendingGate?:
|
|
24
|
-
kind: string;
|
|
25
|
-
decidePath: string;
|
|
26
|
-
};
|
|
46
|
+
pendingGate?: PendingGateMaterial;
|
|
27
47
|
}
|
|
28
48
|
/** gate.kind → 它的那一个 resume 入口(sessionId/taskId 寻址,无秘密)。 */
|
|
29
49
|
export declare function resumeEntryForGate(kind: string, ids: {
|
|
@@ -43,10 +63,7 @@ export interface ActiveRunConflictDoneResult {
|
|
|
43
63
|
errorMessage: string;
|
|
44
64
|
activeTaskId: string | null;
|
|
45
65
|
activeTaskStatus?: string;
|
|
46
|
-
pendingGate?:
|
|
47
|
-
kind: string;
|
|
48
|
-
decidePath: string;
|
|
49
|
-
};
|
|
66
|
+
pendingGate?: PendingGateMaterial;
|
|
50
67
|
}
|
|
51
68
|
export declare function toDoneFrameResult(body: ActiveRunConflictBody): ActiveRunConflictDoneResult;
|
|
52
69
|
interface ConflictProbeDeps<TToken> {
|
|
@@ -58,12 +75,20 @@ interface ConflictProbeDeps<TToken> {
|
|
|
58
75
|
checkpointStore?: {
|
|
59
76
|
peekPendingScope?: (sessionId: string) => Promise<string | null | undefined>;
|
|
60
77
|
findPendingTokenBySession?: (sessionId: string, scope?: string) => Promise<TToken | null | undefined>;
|
|
78
|
+
/** 结构形(不 import core 的 `Checkpoint`):只声明本模块**真读**的两格。`riskDescriptor` 是
|
|
79
|
+
* core 在 mint 时挂到升级门上的判读元数据(display/triage only),`shellGateDoctrine` 是 2026-08-05
|
|
80
|
+
* 裁定加的取证格 —— 见下方 `governanceOriginOf` 的推导论证。 */
|
|
61
81
|
get?: (token: TToken) => Promise<{
|
|
62
82
|
gate?: {
|
|
63
83
|
kind?: string;
|
|
84
|
+
riskDescriptor?: {
|
|
85
|
+
shellGateDoctrine?: "classify" | "always";
|
|
86
|
+
};
|
|
64
87
|
};
|
|
65
88
|
} | null | undefined>;
|
|
66
89
|
} | undefined;
|
|
90
|
+
/** #220:出身归因的部署侧一半(缺席 ⇒ 归不出治理出身 ⇒ 键缺席 —— 与 best-effort 同方向)。 */
|
|
91
|
+
governance?: GovernancePosture | undefined;
|
|
67
92
|
}
|
|
68
93
|
export declare function buildActiveRunConflict<TToken>(deps: ConflictProbeDeps<TToken>, sessionId: string, activeTaskId: string | null | undefined): Promise<ActiveRunConflictBody>;
|
|
69
94
|
export {};
|
|
@@ -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,
|
package/dist/http/route-ctx.d.ts
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* 本文件对 server.ts 的引用一律 `import type`(tsc 擦除,非装载边)。
|
|
16
16
|
*/
|
|
17
17
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
18
|
-
import type { TaskStream, TaskSpec, TaskResult, QuestionAnswer, CheckpointToken, ResumeOutcome, Runner } from "@sema-agent/core";
|
|
18
|
+
import type { ApprovalSettledBy, TaskStream, TaskSpec, TaskResult, QuestionAnswer, CheckpointToken, ResumeOutcome, Runner } from "@sema-agent/core";
|
|
19
19
|
import type { FlatServiceDeps, RequestAuth } from "./server.js";
|
|
20
20
|
import type { TaskRequestBody, DecideBinding } from "./wire-types.js";
|
|
21
21
|
import type { IdempotencyCache } from "./idempotency.js";
|
|
@@ -120,8 +120,11 @@ export interface RouteLegs {
|
|
|
120
120
|
prepareSpec(req: IncomingMessage, res: ServerResponse): Promise<PreparedTaskSubmission | null>;
|
|
121
121
|
/** 同步腿的终局记账(计费/配额/指标),tasks 域用。 */
|
|
122
122
|
finalizeTaskResult(result: TaskResult, principal: string | undefined, objective: string, sessionId: string | undefined): void;
|
|
123
|
-
/** approvals 决策腿:session → pending checkpoint → markResuming CAS → 驱动续跑。
|
|
124
|
-
|
|
123
|
+
/** approvals 决策腿:session → pending checkpoint → markResuming CAS → 驱动续跑。
|
|
124
|
+
* `settledBy` = **这次结算的出处**(core `ApprovalSettledBy`),必填:本腿有两个调用方(HTTP
|
|
125
|
+
* `/decide` = 人;内部 D-D SLA deny-sweep = 窗到期),而函数内部分辨不出谁在叫它 —— 必填参数把
|
|
126
|
+
* 「漏报出处」变成编译错,不是一条下游把「没人答」渲染成「有人拒」的静默假出处。 */
|
|
127
|
+
resumeCheckpoint(sessionId: string, decision: "approve" | "deny", reason: string | undefined, settledBy: ApprovalSettledBy, req?: IncomingMessage, answer?: QuestionAnswer, binding?: DecideBinding, onResumeCommitted?: (overrideSessionId?: string) => Promise<void>): Promise<{
|
|
125
128
|
status: number;
|
|
126
129
|
body: object;
|
|
127
130
|
}>;
|
|
@@ -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
|
|
@@ -637,7 +639,8 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
|
|
|
637
639
|
}
|
|
638
640
|
}
|
|
639
641
|
: undefined;
|
|
640
|
-
|
|
642
|
+
// #204 件6①:出处 = `"human"` —— 这条腿是运维**亲自**在 HTTP 上给的终局(approve/deny 两向都是)。
|
|
643
|
+
const out = await resumeCheckpoint(sessionId, decision, body.reason ?? undefined, "human", req, answer, binding, grantOnCommit);
|
|
641
644
|
if (remember && deps.approvalExemptionStore) {
|
|
642
645
|
sendJson(res, out.status, { ...out.body, rememberApplied });
|
|
643
646
|
return;
|
|
@@ -658,6 +661,32 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
|
|
|
658
661
|
* connection runs its OWN poll loop — fine for a handful of operators; a shared fan-out poll is a future
|
|
659
662
|
* optimization if the operator count grows.) */
|
|
660
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
|
+
}
|
|
661
690
|
/**
|
|
662
691
|
* GET /v1/approvals/stream (design/80 native push): SSE — pushes pending-approval deltas so the portal
|
|
663
692
|
* SUBSCRIBES ONCE instead of polling GET /v1/approvals every ~10s (better UX: near-real-time + no client poll
|
|
@@ -695,7 +724,9 @@ export async function streamApprovals(req, res, cs, scope, pollMs = APPROVALS_ST
|
|
|
695
724
|
while (!closed) {
|
|
696
725
|
let pending;
|
|
697
726
|
try {
|
|
698
|
-
|
|
727
|
+
// #209 件4:与 `GET /v1/approvals` **同一个**投影函数 —— 两条 durable 读面各洗各的就会漂
|
|
728
|
+
// (SSE 那条恰恰是 operator 常驻订阅的那条,漏掉它等于没修)。
|
|
729
|
+
pending = (await cs.listPending(scope)).map(redactPendingDisclosures);
|
|
699
730
|
}
|
|
700
731
|
catch {
|
|
701
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
|
|
@@ -85,14 +94,51 @@ async function handleCapabilitiesBody(req, res, url, ctx, miss) {
|
|
|
85
94
|
// org 折叠面),所以"说 yes ⟺ 面真能用"是结构性的:少一个,域不挂载、这里也翻假,消费方看到的
|
|
86
95
|
// 与它真能打通的永远一致。折叠面缺席时刻意不报 true —— 一个没有成员性判据的共享读面比没有更糟。
|
|
87
96
|
sharedMemory: Boolean(deps.sharedMemoryStore && deps.orgMemoryDirectory),
|
|
88
|
-
// design/183 form b:收编面(operator lane)
|
|
89
|
-
// adoptionLog 工厂在场(local backend
|
|
90
|
-
//
|
|
91
|
-
|
|
92
|
-
//
|
|
93
|
-
//
|
|
94
|
-
//
|
|
95
|
-
|
|
97
|
+
// design/183 form b:收编面(operator lane)。谓词 = **两项合取**,逐项对着 handleAdoption 的
|
|
98
|
+
// 拒绝臂写:①`adoptionLog` 工厂在场(local backend 刻意不实现——单用户数据根没有多租身份轴可
|
|
99
|
+
// 重绑)⇒ 否则 501;②operator 名单**非空** ⇒ 否则 `explicitOperatorOk` 对任何身份都返回 false,
|
|
100
|
+
// 那条路由**恒 403**。
|
|
101
|
+
// 🔴 第二项是 [3397]-4(黑板 cli 审计)逮到的真缺口:旧谓词只查 ① ⇒ 「装了 SQL 后端但没配
|
|
102
|
+
// OPERATOR_PRINCIPALS」的部署位报 true、路由恒 403 —— 本仓「says yes ⟺ route works」被实测证否。
|
|
103
|
+
// 空名单在这条轴上**刻意**不走 `isOperator` 的「人人都是 operator」旧义(那是一道世界可写的门,
|
|
104
|
+
// 见 principal-gate.ts 的 `explicitOperatorOk` 头注),所以补的是能力位、不是放宽授权。
|
|
105
|
+
adoption: Boolean(deps.backend?.adoptionLog) && deps.config.operatorPrincipals.length > 0,
|
|
106
|
+
// [3397]-5:`GET /v1/outcomes`(design/73 §7.2 机械信号只读聚合)。此前**有 501 拒而无能力位** ——
|
|
107
|
+
// 消费端只能 trial-by-501,与本仓「gate-don't-trial-by-501」的姿势相反。谓词逐项对着该路由的
|
|
108
|
+
// 拒绝臂:①可查询的账面在场(`outcomeSink.summary`;File sink 只写 JSONL,没有查询面 ⇒ 501);
|
|
109
|
+
// ②多租户下 operator 名单非空(单用户形无此门 —— 唯一的用户就是 operator,路由那侧的
|
|
110
|
+
// `if (deps.config.requirePrincipal)` 逐字如此)。②与 adoption 是**同一族**缺口,同批一起补。
|
|
111
|
+
outcomeLedger: Boolean(deps.outcomeSink?.summary) && (deps.config.requirePrincipal !== true || deps.config.operatorPrincipals.length > 0),
|
|
112
|
+
// #154 车二 + #203 §2:规则车道(cc-import 两口 + 撤销面两口 + respond 的 persistRule 兑付口)。
|
|
113
|
+
// 谓词与 `/v1/rules/*` 全族的 501 同源:ruleConsent lane 在场 ⟺ 总开关未被显式关 ∧ 规则店 wired
|
|
114
|
+
// (SQL 双方言,或 #203 起 local 的 File 三面束)。说 yes ⟺ **四口都活**,不是「有一口活」。
|
|
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,
|
|
131
|
+
// #205 SDK 车上游请托二连(2026-08-10):
|
|
132
|
+
// · `permissionRulesRevoke` —— 撤销面两口(GET/DELETE /v1/rules)**存在性**的独立信号。谓词与
|
|
133
|
+
// `permissionRules` 同源,但**在场性**本身就是版本信号:≤7.11.0 的 worker `permissionRules` 已为
|
|
134
|
+
// true 而这两口 404(位早于路由),消费端无从区分「面没开」与「版本太老」——本位缺席 = 老版本,
|
|
135
|
+
// 探 404 即可;在场 = 路由已铸。不折进 `permissionRules`:那个位的成文语义(「四口都活」)在
|
|
136
|
+
// ≤7.11.0 的已发布包上已经是谎,只能加位不能改位。
|
|
137
|
+
permissionRulesRevoke: Boolean(deps.ruleConsent),
|
|
138
|
+
// · `oneShot` —— server 消费 `TaskRequest.oneShot`(缺席不写透传 + 非 boolean 400)。旧 server 对
|
|
139
|
+
// 未知键静默容忍 ⇒ 调用方声明了一次性形却拿不到 core 的 block-wait 指引而不自知;本位在场 = 键
|
|
140
|
+
// 真被消费。恒 true(消费不依赖任何可选设施)。
|
|
141
|
+
oneShot: true,
|
|
96
142
|
// CLI surfaces: /v1/policy is always served (config surface); /v1/usage returns real
|
|
97
143
|
// cumulative spend only when a per-principal quota is configured (else enabled:false).
|
|
98
144
|
policy: true,
|
|
@@ -139,14 +185,27 @@ async function handleCapabilitiesBody(req, res, url, ctx, miss) {
|
|
|
139
185
|
// routes (same gate as steer — the verbs share the registry + run store; retain-off runs 409 resume.retain_off).
|
|
140
186
|
subagentResume: Boolean(deps.subagentSteerRegistry) && Boolean(deps.runStore),
|
|
141
187
|
// TaskRequest.settings (client per-request SemaSettings stamp) → projected into the engine TIGHTEN-ONLY
|
|
142
|
-
// (deny-wins, after the deployment + operator baseline). v1 wires permissions + permissionMode
|
|
143
|
-
//
|
|
188
|
+
// (deny-wins, after the deployment + operator baseline). v1 wires permissions + permissionMode + model +
|
|
189
|
+
// outputStyle.
|
|
190
|
+
// 🔴 保鲜(design/201 施工期自查):旧文「`bypassPermissions` is NEVER honored from settings」是
|
|
191
|
+
// **pre-[816] 拍照,已作废** —— `parseTaskSettings` 自 [816]/[820] 起白名单收**五**模式,bundle 形的
|
|
192
|
+
// bypass 与顶层键同权(两者的优先序由 `effectivePermissionMode` 单点裁定)。语义不是「开门」而是
|
|
193
|
+
// 「不加模式派生的那道门」:部署/运维基线在此之外合成,settings 掀不掉(tightenTaskSpec deny-wins)。
|
|
194
|
+
// 留着那句过期的否定句比留个欠账更伤:消费方会据此以为 bundle 形 bypass 无效而改用别的写法。
|
|
144
195
|
// `env`([2400] X-8):与真消费点同谓词——resolve-spec 的 setSessionShellEnv 只在 cwdHonored
|
|
145
196
|
// (单用户 host lane)注入 shell env;其余 lane 收到即警告忽略。此前硬编码 false 是「TaskSpec 无
|
|
146
197
|
// per-task env 注入」时代的陈旧断言(R-survey shellEnv seam 落地后失真=说没有但其实有,活能力被藏)。
|
|
147
198
|
// `hooks`(hook-runner 阶段一):TRUE 当本部署会真跑 hook 命令 = 单用户闸(hook 命令跑在
|
|
148
199
|
// worker host,class ② 能力授予;resolveSpec 的 taskHooks 装配同一谓词)。多租户 = false(收到警告忽略)。
|
|
149
200
|
taskSettings: { permissions: true, permissionMode: true, model: true, outputStyle: true, env: cwdHonored(deps.config), hooks: deps.config.requirePrincipal !== true },
|
|
201
|
+
// design/201 §3 前提②:显式 `permissionMode` 会被翻译成 `spec.shellGate` 档
|
|
202
|
+
// (bypassPermissions ⇒ off,auto/default/acceptEdits/plan ⇒ classify;表态缺席 ⇒ 不写键)。
|
|
203
|
+
// **恒 true 且与翻译表同一个 commit** —— 这一位不是"配置在不在",而是"这份二进制里有没有那张表",
|
|
204
|
+
// 所以 says yes ⟺ 翻译真在,是结构性的(某个 deps 缺席不会让它变假)。
|
|
205
|
+
// 消费半场:壳(cli #223)据此撤掉它对 `MANUAL_MODE_SHELL_GATE` 的无条件注入。判据必须是
|
|
206
|
+
// **所连 server 广告本位**,不是壳自身版本 —— 新壳连老 server / 回滚到旧二进制的副本 / 混版 fleet
|
|
207
|
+
// 三形下,壳看见本位缺席就继续注 env,于是"翻译还没到货但壳已经撤注"那个放宽窗结构上不存在。
|
|
208
|
+
modeShellGateTranslation: true,
|
|
150
209
|
// [1476] R1 / [1478] R2: TaskRequest.appendSystemPrompt is accepted top-level → TaskSpec.appendSystemPrompt
|
|
151
210
|
// (first-class lane for the shell's product-knowledge block). A shell probes THIS flag: false/absent ⇒ fall
|
|
152
211
|
// back to the settings.outputStyle ride-along (older servers fold that into the same spec field).
|
|
@@ -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
|
|
@@ -1,15 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* #154 车二 ——
|
|
2
|
+
* #154 车二 + #203 §2 —— **规则车道的 wire 面**(design/179 §7 的导入半场 + design/203 的撤销半场)。
|
|
3
3
|
*
|
|
4
|
-
* POST
|
|
5
|
-
* POST
|
|
4
|
+
* POST /v1/rules/cc-import/prepare → 预览 + 一张**一次性、principal 绑定、有期、载荷绑定**的票
|
|
5
|
+
* POST /v1/rules/cc-import/redeem → 原子消费票 → confirm → 批量兑付进店
|
|
6
|
+
* GET /v1/rules → 列出自己名下**活着**的规则(keyset 分页,游标绑 rev)
|
|
7
|
+
* DELETE /v1/rules → 按 (rule, scope) 内容撤销,**恒经** core `removePersistedRule`
|
|
6
8
|
*
|
|
7
9
|
* **lane = principal**(不是 operator):用户导的是**他自己**的规则,一条规则的语义就是「这个人自己
|
|
8
|
-
* 说过 yes」。operator
|
|
9
|
-
*
|
|
10
|
-
*
|
|
10
|
+
* 说过 yes」。operator 在**扩权**这条路上没有位置——他既不该替租户扩权,也不该被要求代人点确认。
|
|
11
|
+
* 因此**导入两口**只认已验明的 principal,连 `explicitOperatorOk` 的旁路都不给:一个 operator 想给
|
|
12
|
+
* 自己导规则,用他自己的 principal 走同一条路即可(那时他就是租户)。
|
|
11
13
|
*
|
|
12
|
-
*
|
|
14
|
+
* 🔴 **撤销面两口多一条 operator 越权域,而这不是上面那句话的例外,是它的对偶**(design/203 §2 设计题
|
|
15
|
+
* 一裁定):`?principal=` / body 里的 `principal` 让 operator 读**或收回**任一租户的规则。方向决定授权:
|
|
16
|
+
* 收回是**收紧**(core 的 `removePersistedRule` 头注逐字:「a host may narrow on a user's behalf, it may
|
|
17
|
+
* not widen」),而多租户管理员能回收权限正是撤销面的本义。读那一半同样只开给 operator ——
|
|
18
|
+
* 规则文本里含命令样式,跨 principal 读是信息面泄露,所以**不开第三方 principal 相互读**。
|
|
19
|
+
* 判据用 `explicitOperatorOk`(空名单 = 全拒),**绝不**从 service token 推断身份:一个共享的部署凭证
|
|
20
|
+
* 不是一个人。
|
|
21
|
+
*
|
|
22
|
+
* **billable = false**:四口都不烧模型(读 settings 文本 / 读写规则行),不进 `isBillableSubmitPath`。
|
|
13
23
|
*
|
|
14
24
|
* 🔴 **四类拒绝在 wire 上同形 404**(信任边界 ①):unknown ticket / 别人的 ticket / 过期 / 已用过 ——
|
|
15
25
|
* 四个成因返回不同的码就是一台存在性 oracle(拿别人的票撞库能探到「它在别处存在」)。分类只进服务端
|
|
@@ -19,5 +29,7 @@ import type { IncomingMessage, ServerResponse } from "node:http";
|
|
|
19
29
|
import type { RouteCtx } from "../route-ctx.js";
|
|
20
30
|
export declare const RULES_CC_IMPORT_PREPARE_PATH = "/v1/rules/cc-import/prepare";
|
|
21
31
|
export declare const RULES_CC_IMPORT_REDEEM_PATH = "/v1/rules/cc-import/redeem";
|
|
32
|
+
/** 撤销面两口共用的字面路径(GET 列举 / DELETE 撤销)。 */
|
|
33
|
+
export declare const RULES_PATH = "/v1/rules";
|
|
22
34
|
export declare function handleRules(req: IncomingMessage, res: ServerResponse, url: string, ctx: RouteCtx): Promise<boolean>;
|
|
23
35
|
//# sourceMappingURL=rules.d.ts.map
|