@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
|
@@ -7,7 +7,9 @@
|
|
|
7
7
|
* 治理矩阵的第三维消费腿)。装配次序契约不变:main.ts 仍在同一位置构造,构造条件逐字保持。
|
|
8
8
|
*/
|
|
9
9
|
import { join } from "node:path";
|
|
10
|
-
import { NodeExecutionEnv } from "@sema-agent/core";
|
|
10
|
+
import { createAutoModeDecider, NodeExecutionEnv } from "@sema-agent/core";
|
|
11
|
+
import { recordFailOpen } from "../observability/fail-open.js";
|
|
12
|
+
import { decodeCheckpointScope } from "../security.js";
|
|
11
13
|
import { createApprovalBaselinePolicy, createDeploymentGovernanceInputs, } from "../deployment-governance.js";
|
|
12
14
|
import { applyRuntimeGovernance } from "../runtime-governance.js";
|
|
13
15
|
import { DeferredSandboxPathEnv, isSandboxPathAdjudicationLane, sandboxPathEnvSlots } from "./deferred-sandbox-path-env.js";
|
|
@@ -57,6 +59,8 @@ function hostGateCwd(config, localRoot) {
|
|
|
57
59
|
* 时点语义(成文进 docs/ASSISTANT-WIRE-CONTRACT.md,免运维当 bug 报):
|
|
58
60
|
* · toolPolicy 轴 = **当前** config(与 resume 重折一致,收紧与放松都被兑现——放松是一次显式运维动作);
|
|
59
61
|
* · shellGate 轴 = core 取 max(live, park 时 seed)—— 收紧兑现、放松**不**兑现(fail-safe)。
|
|
62
|
+
* · `autoModeArmed` 轴(A-010.1)= 按**当前** per-principal entitlement 现解(见 {@link autoModePostureOf});
|
|
63
|
+
* 与 toolPolicy 轴同族:挂起期间权益被撤/被授 ⇒ 摘要对不上 ⇒ core pre-CAS 响亮拒,不静默换姿势跑。
|
|
60
64
|
* · `onAsk` 轴 = 部署的活体审批席(`deps.approverSeat`),**在场即供**:park 时那一层冻的就是它。
|
|
61
65
|
* 席位缺席的部署上不供,而**这不等于 auto-deny**(codex R3-中1 验真纠正):此形恒带
|
|
62
66
|
* `durableMandate`(见 {@link mandatePostureOf}),继承层判 `ask` 时 core 只要还看得见 checkpoint 店
|
|
@@ -70,8 +74,11 @@ function hostGateCwd(config, localRoot) {
|
|
|
70
74
|
export function createParkedReviveInheritedGate(deps) {
|
|
71
75
|
const { config, question, approvalExemptionStore, logger, localRoot, approverSeat } = deps;
|
|
72
76
|
const slots = deps.slots ?? sandboxPathEnvSlots;
|
|
73
|
-
|
|
74
|
-
|
|
77
|
+
return async (row) => {
|
|
78
|
+
// A-010.1:三位决议链元数据。**async 的唯一理由**就是其中两位要按 principal 现解 entitlement
|
|
79
|
+
// (core 的 resolver 座本身也是 sync-or-async),而这条腿的消费点(parked-decide 的 `decideParkedAgent`)
|
|
80
|
+
// 从来就在 async 里,「席位必须同步」是本文件此前自设的约束,不是结构事实。
|
|
81
|
+
const metadata = await chainMetadataOf(deps, row);
|
|
75
82
|
// lane 分形与 resolve-spec 同一判别式(单一属主,取值处只此一个)。
|
|
76
83
|
// 🔑 沙箱腿的 slot 键 = **row.sessionId**(子代自己的会话,design/181 §2.2 裁定):revive 后 core 以
|
|
77
84
|
// 该 sessionId 铸 env 并注册 slot(subagent → prepare-task → 工厂装饰器),这个键真能绑上。
|
|
@@ -108,7 +115,7 @@ export function createParkedReviveInheritedGate(deps) {
|
|
|
108
115
|
{
|
|
109
116
|
policy: governed.toolPolicy ?? baseline,
|
|
110
117
|
...(approverSeat !== undefined ? { onAsk: approverSeat } : {}),
|
|
111
|
-
...
|
|
118
|
+
...metadata,
|
|
112
119
|
},
|
|
113
120
|
],
|
|
114
121
|
...(governed.shellGate !== undefined ? { shellGate: governed.shellGate } : {}),
|
|
@@ -116,7 +123,9 @@ export function createParkedReviveInheritedGate(deps) {
|
|
|
116
123
|
};
|
|
117
124
|
}
|
|
118
125
|
/**
|
|
119
|
-
* 重建条目的**决议链元数据**(core 5.22.0 F-012)——
|
|
126
|
+
* 重建条目的**决议链元数据**(core 5.22.0 F-012)—— 三位的逐字重算,**一次** entitlement 现解喂全三位
|
|
127
|
+
* (第三位与两位 mandate 的 `forceDurableGate` 项读的是同一次 caps,分两次解会开一个「同一条链里两位读到
|
|
128
|
+
* 不同代 entitlement」的窗)。
|
|
120
129
|
*
|
|
121
130
|
* 为什么它承重:5.22.0 把 re-supply 契约从 count 换成 `cpv1:` 内容摘要
|
|
122
131
|
* (`constraintChainDigest`),而摘要 **把这两位卷了进去**。park 时那一层记的是什么,赎回腿就必须供
|
|
@@ -140,17 +149,130 @@ export function createParkedReviveInheritedGate(deps) {
|
|
|
140
149
|
* 协调器生效(`isLiveQuestionFace` 判 true);缺席 ⇒ spec stamp `QUESTION_AWAITS_RESUME`
|
|
141
150
|
* (该 sentinel 被 `isLiveQuestionFace` 判 false)。两支都归结为 `question` 在不在场。
|
|
142
151
|
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
152
|
+
* ✅ **原「已披露残余」销账(A-010.1 同车)**:`runtimeCaps.forceDurableGate`(center 逐 principal 的
|
|
153
|
+
* 授权位)此前写着「在本装配点解不出来——resolver 是 async 而重供席位的形是同步 `(row) => gate`」。那条
|
|
154
|
+
* 阻塞是**本文件自设**的,不是结构事实:唯一的消费点(`parked-decide.ts` 的 `decideParkedAgent`)本来
|
|
155
|
+
* 就在 async 里。席位随本件改成 async 之后,该位与 `autoModeArmed` 走同一条 caps 现解腿一并补齐 —— 否则
|
|
156
|
+
* 「该位为真 + 部署装了活体席」这个组合上 park 记 true 而本腿供 false,摘要恒不匹配、这批 principal 的
|
|
157
|
+
* parked 卡同样永不可赎(与 A-010.1 同族同因的第二个总失效面)。
|
|
158
|
+
*
|
|
159
|
+
* ⚠️ 时点语义与 toolPolicy 轴同族:两位都按**当前** entitlement 现解,挂起期间被撤/被授 ⇒ 摘要对不上
|
|
160
|
+
* ⇒ core pre-CAS 响亮拒(带 `parked_resume.startup_failed` 码可观测),绝不静默按新姿势跑。
|
|
161
|
+
*/
|
|
162
|
+
async function chainMetadataOf(deps, row) {
|
|
163
|
+
const caps = await resolveCapsFor(deps, row);
|
|
164
|
+
const forced = caps?.forceDurableGate === true;
|
|
165
|
+
return {
|
|
166
|
+
...(forced || deps.approverSeat === undefined ? { durableMandate: true } : {}),
|
|
167
|
+
...(forced || deps.question === undefined ? { contentMandate: true } : {}),
|
|
168
|
+
...autoModePostureOf(deps, row, caps),
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* 本行 principal 的 entitlement 现解(三位元数据共用**一次**)。
|
|
173
|
+
*
|
|
174
|
+
* principal 从哪来:`row.scope`。承重的不是「两个写入方碰巧写了同一个字符串」,而是**取数结构**:
|
|
175
|
+
* `findParkedAgentForCheckpoint` 走的是 `agentStore.listByScope(cp.scope, …)` 这条**按 scope 分区**的
|
|
176
|
+
* 查询(parked-decide.ts),所以任何能走到本函数的行都满足 `row.scope === cp.scope` —— 这是构造式的,
|
|
177
|
+
* 不是巧合。而 `cp.scope` 由 `boot/resolve-spec.ts` 写成 `encodeCheckpointScope(auth.principal)`
|
|
178
|
+
* (legacy resume 腿反向读回 `principal: decodeCheckpointScope(cp.scope)` 是同一等式的另一半),
|
|
179
|
+
* 故 `decodeCheckpointScope(row.scope)` 就是那条 run 的 `spec.principal`,与 core 喂给
|
|
180
|
+
* `runtimeCapsResolver(spec.principal)` 的**同一个**值 —— 摘要两侧因此解到同一份 caps。
|
|
181
|
+
*
|
|
182
|
+
* ⚠️ 别把 `row.scope` 当成「和 `cp.scope` 同一个写入方铸的」:**不是**。写行那一侧(core `subagent.js`
|
|
183
|
+
* 的 `ctx.principal ?? opts.background.scope`,本仓 `capabilities/scenarios.ts` 填字面 `"default"`)用的是
|
|
184
|
+
* `principal ?? "default"` 这套约定,与 `encodeCheckpointScope` 的 `"_"` 哨兵**不同源**。有 principal 时
|
|
185
|
+
* 两套折出同一个字符串(encode 只在 null/undefined 时才替换),所以上面那条等式成立;**匿名** run 上两者
|
|
186
|
+
* 分别是 `"default"` 与 `"_"`,分区查询直接落空 ⇒ 本函数根本到不了(decide 退回 legacy resume 腿,
|
|
187
|
+
* parked-decide.ts 自述的「未命中=零回归」形)。谁哪天去弥合那个匿名未命中,必须**连本函数一起**重看:
|
|
188
|
+
* 那时 `decodeCheckpointScope("default")` 会解出字符串 `"default"` 并把它当租户名递进 resolver。
|
|
189
|
+
*
|
|
190
|
+
* resolver 缺席(无 center / dry-run)⇒ core 侧 `runtimeCaps` 也恒 undefined ⇒ 这两位在 park 时也从不
|
|
191
|
+
* 由 caps 置位 ⇒ 逐字零行为差。resolver 抛错(契约上它自己 fail-closed 且不抛)⇒ 当作没解出来:元数据
|
|
192
|
+
* 位按「无 caps」算 ⇒ 与 park 记的对不上就响亮拒,**绝不**静默按更松的姿势赎回。
|
|
193
|
+
*/
|
|
194
|
+
async function resolveCapsFor(deps, row) {
|
|
195
|
+
if (deps.resolveRuntimeCaps === undefined)
|
|
196
|
+
return undefined;
|
|
197
|
+
try {
|
|
198
|
+
return await deps.resolveRuntimeCaps(decodeCheckpointScope(row.scope));
|
|
199
|
+
}
|
|
200
|
+
catch (err) {
|
|
201
|
+
// 留痕是必需的:否则「这批卡为什么突然赎不动」查无痕迹。
|
|
202
|
+
deps.logger.warn("parked_revive_caps_unresolved", { handle: row.handle, err: err instanceof Error ? err.message : String(err) });
|
|
203
|
+
return undefined;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* A-010.1 —— 重建条目的**第三位**决议链元数据 `autoModeArmed`(core 5.22.0 F-012 与两位 mandate 同车)。
|
|
208
|
+
*
|
|
209
|
+
* ## 病(本函数存在的唯一理由)
|
|
210
|
+
* core 的摘要口(`tool-policy.js` `constraintChainEntryOf`)把**三**位卷进 `cpv1:` 摘要,而判据是
|
|
211
|
+
* 「条目上有没有 `autoMode` 这个键」:park 时 `prepare-task.js` 的 `inheritedGateForChildren` 写
|
|
212
|
+
* `...(autoModeDecider !== undefined ? { autoMode: { decider } } : {})`,赎回时 `runtask.js` 的 pre-CAS
|
|
213
|
+
* 门按 `pc.autoMode !== undefined` 逐字重算。本腿此前只供 policy/onAsk/两位 mandate,**从不供** autoMode
|
|
214
|
+
* ⇒ auto-mode 权益 principal 的每一条跨副本 parked 赎回摘要恒不匹配、`resume.parent_constraint_mismatch`
|
|
215
|
+
* 响亮拒、卡永不可赎(方向 fail-safe,但功能面对这批 principal 是整条失效)。
|
|
216
|
+
*
|
|
217
|
+
* ## core 的原式(逐字对位,不绕过)
|
|
218
|
+
* ```
|
|
219
|
+
* if (runtimeCaps?.autoMode === true && deps.autoMode !== undefined) autoModeDecider = createAutoModeDecider(…)
|
|
220
|
+
* ```
|
|
221
|
+
* 两个半场本仓分别对位:
|
|
222
|
+
* · `deps.autoMode !== undefined` ⇒ {@link ParkedReviveGateDeps.autoModeSeatMounted}(装配面恒挂,见
|
|
223
|
+
* `boot/runner-deps.ts` 的 `autoMode:{onBreakerOpen}`——**信任门**,单挂它武装不了任何东西);
|
|
224
|
+
* · `runtimeCaps.autoMode === true` ⇒ {@link resolveCapsFor} 按**本行的 principal** 现解(时点语义、
|
|
225
|
+
* principal 从哪来、resolver 缺席/抛错怎么判,全在那只函数的头注)。
|
|
226
|
+
*
|
|
227
|
+
* ## ⛔ 已披露残余:decider 的**身份**跨不了进程(P-DEBT,不是等价替换)
|
|
228
|
+
* 摘要只看键在不在,但 core 在赎回后的每一次继承 ask 上会**真的调** `pc.autoMode.decider`
|
|
229
|
+
* (`prepare-task.js` 的继承包装器,顺序:分类器 → 冻结审批席)。而那只 decider 是祖先任务上的活闭包
|
|
230
|
+
* (绑着它自己的 session 转写窗 + brain),跨副本重建不出来 —— 与本文件 `approverSeat` 那格「诚实的降级」
|
|
231
|
+
* 同族。本腿交的是 **core 自己的构造器**铸的一只 decider,其 classify 腿如实拒答 ⇒ core 收到
|
|
232
|
+
* `unavailable`(它自己 `.catch(() => ({kind:"unavailable"}))` 的同一形)⇒ **不产生任何自动裁决**,原样
|
|
233
|
+
* 落到祖先冻结审批席那条链上(本腿的席位又是无 ALS 的降级形 ⇒ `approverUnavailable` ⇒ 再 park 给人)。
|
|
234
|
+
* 方向:分类器本会 `allow` 的调用改成问人(**更严**);本会 `block` 的也改成问人(**不是自动放行**,但
|
|
235
|
+
* 确实比自动拒松一档)。所以它按 P-DEBT 记债而不是当合法兜底 —— tag
|
|
236
|
+
* `server.parked-revive.ancestor-classifier-unreachable`,逐次计数 + 一次性 warn,收口件二选一:core 把
|
|
237
|
+
* 分类器判据持久进链条目,或让祖先 decider 有可跨进程重建的形。
|
|
238
|
+
*/
|
|
239
|
+
function autoModePostureOf(deps, row, caps) {
|
|
240
|
+
if (deps.autoModeSeatMounted !== true || caps?.autoMode !== true)
|
|
241
|
+
return {};
|
|
242
|
+
return { autoMode: { decider: createUnreachableAncestorClassifier(deps, row) } };
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* 祖先分类器的**跨进程降级形**(全部理由在 {@link autoModePostureOf} 的残余段)。
|
|
246
|
+
*
|
|
247
|
+
* 内核用 core 自己的 `createAutoModeDecider` 铸 —— 超时/熔断/verdict 词表(`unavailable` 这个词本身)
|
|
248
|
+
* 都归 core 单一属主,本仓只提供一条**如实拒答**的 classify 腿,绝不在此手抄 `{kind:"unavailable"}`。
|
|
249
|
+
* `failureThreshold: 1` 让熔断一拍即开:第一次被咨询就把一次性告警喊出来,之后每次 `decide` 在 core 内
|
|
250
|
+
* 早返 `unavailable`(不再进 classify、不再起 15s 定时器)。
|
|
251
|
+
*
|
|
252
|
+
* 🔴 计数**必须**打在外层 `decide` 上、不能打在 `classify` 里:熔断开了以后 core 早返、classify 再也不
|
|
253
|
+
* 被调用,计数打在里面就永远只记 1 —— 而这个 tag 的语义(和普查表里那一行的文字)是「**丢了祖先分类器
|
|
254
|
+
* 判决的继承 ask 次数**」。打在里面等于让遥测读数与它自称的含义不是一回事,债就在账上消失了。
|
|
149
255
|
*/
|
|
150
|
-
function
|
|
256
|
+
function createUnreachableAncestorClassifier(deps, row) {
|
|
257
|
+
const inner = createAutoModeDecider({
|
|
258
|
+
failureThreshold: 1,
|
|
259
|
+
classify: async () => {
|
|
260
|
+
throw new Error("the ancestor task's auto-mode classifier is a live closure over that task's own transcript/brain — it cannot be " +
|
|
261
|
+
"reconstructed on a fresh replica, so this inherited layer has no classifier verdict to give (the call falls " +
|
|
262
|
+
"through to the ancestor's frozen approver chain)");
|
|
263
|
+
},
|
|
264
|
+
onBreakerOpen: () => deps.logger.warn("parked_revive_ancestor_classifier_unreachable", {
|
|
265
|
+
handle: row.handle,
|
|
266
|
+
sessionId: row.sessionId,
|
|
267
|
+
note: "an inherited ancestor layer was auto-mode armed at park time; its classifier does not survive the process boundary — inherited asks resolve on the approver chain (durable park) instead of a classifier verdict",
|
|
268
|
+
}),
|
|
269
|
+
});
|
|
151
270
|
return {
|
|
152
|
-
|
|
153
|
-
|
|
271
|
+
breakerOpen: () => inner.breakerOpen(),
|
|
272
|
+
decide: async (input, signal) => {
|
|
273
|
+
recordFailOpen("server.parked-revive.ancestor-classifier-unreachable", row.handle);
|
|
274
|
+
return inner.decide(input, signal);
|
|
275
|
+
},
|
|
154
276
|
};
|
|
155
277
|
}
|
|
156
278
|
/** host 腿的裁决 env(真 fs;构造是纯字段赋值,不 spawn)。**每次赎回现铸**——与「禁 memoize」同因:
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #203 §3(design/203 v2 §6 F4 的**残余采纳**)—— 默认 ON 的**休眠行审计**。
|
|
3
|
+
*
|
|
4
|
+
* 🔴 这条审计存在的唯一理由:`PERMISSION_RULES_ENABLED` 的默认值在本车从 OFF 翻成 ON,而**旋钮翻转
|
|
5
|
+
* 与规则行的寿命是两条独立的时间线**。三张表自 #154 车二起就随中央 `ensureSchema` 建、并且从来没有被
|
|
6
|
+
* 清过,所以下面这两类部署在升级的那一刻会突然带着**既有**规则行开始放行:
|
|
7
|
+
* · 曾经把旋钮开过一段时间又关掉的部署(行留在库里,只是没人读);
|
|
8
|
+
* · 从旧库恢复/克隆出来的部署(行跟着数据一起来的)。
|
|
9
|
+
* 对运维来说,这是「我什么都没改,worker 却开始少问我了」——一次**放宽面**上的静默变化。审计做的事
|
|
10
|
+
* 很小但是承重:在启动日志里把它说出来,让「少问」是一条能被读到的因果,而不是一次玄学。
|
|
11
|
+
*
|
|
12
|
+
* ── 三条判据,逐条有理由 ────────────────────────────────────────────────────────────────────────
|
|
13
|
+
* ① **店缺席 ⇒ 整条不上场**(零 I/O、零行)。店缺席包括:显式关旋钮、`local` 车道没接 File 店的旧
|
|
14
|
+
* 部署、以及压根没有 backend 的 env-only worker。没有店就没有行,谈不上唤醒。
|
|
15
|
+
* ② **零桶 ⇒ 不打行**。一条「0 条既有规则随默认 ON 激活」的行是纯噪音:它对读日志的人不传递任何
|
|
16
|
+
* 可行动的信息,却让真正有东西被唤醒的那一行变得不显眼。
|
|
17
|
+
* ③ **运维**显式**表过态(`PERMISSION_RULES_ENABLED` 被设成 true 或 false)⇒ 不打行**。那些行本来
|
|
18
|
+
* 就是活的(或者旋钮被显式关着、店根本没装配),默认值翻不翻转对它们零影响 —— 说「随默认 ON
|
|
19
|
+
* 激活」是**假话**,而在启动日志里说假话比不说更坏。判据用 `permissionRulesEnabledExplicit`
|
|
20
|
+
* (config.ts 与 `dbBackendExplicit` 同姿势),不是猜。
|
|
21
|
+
*
|
|
22
|
+
* ── 失败方向 ────────────────────────────────────────────────────────────────────────────────────
|
|
23
|
+
* 数不出来(库抖动 / 文件读不了)⇒ **不拒启、也不编一个 0 出来**:返回 `undefined` + 一条 warn。
|
|
24
|
+
* 这是一条纯诊断面(它不参与任何放行/拒绝判决),所以它的降级方向是「安静地不知道」而不是「谎报
|
|
25
|
+
* 零」——`{ buckets: 0 }` 会被下游读成「确认没有休眠行」,那是把一次读失败伪装成一个结论。
|
|
26
|
+
* 因为它零判决,这条 fail-open 不属于 `docs/FAIL-OPEN-CENSUS.md` 的安全轴 P 类,故不占 `recordFailOpen`
|
|
27
|
+
* 的标签;它的可见性由那条 warn 承担。
|
|
28
|
+
*/
|
|
29
|
+
/** 审计只需要一只桶计数口——**刻意**不吃整个 `PermissionRuleStoreBundle`:诊断面对店的其余能力
|
|
30
|
+
* 一无所知也不该知道,窄口让测试能用四行 fixture 驱动真实现同一段代码。 */
|
|
31
|
+
export interface DormantRuleCounter {
|
|
32
|
+
/** 库里已存在的规则**桶**(一只 owner 一只桶)数量。 */
|
|
33
|
+
countBuckets(): Promise<number>;
|
|
34
|
+
}
|
|
35
|
+
export interface DormantRuleAuditCtx {
|
|
36
|
+
/** 已装配的规则店束;`undefined` = 本部署没有规则店(判据 ①)。 */
|
|
37
|
+
stores: DormantRuleCounter | undefined;
|
|
38
|
+
logger: {
|
|
39
|
+
info(msg: string, meta?: unknown): void;
|
|
40
|
+
warn?(msg: string, meta?: unknown): void;
|
|
41
|
+
};
|
|
42
|
+
/** 运维是否**显式**设过 `PERMISSION_RULES_ENABLED`(判据 ③)。 */
|
|
43
|
+
explicit: boolean;
|
|
44
|
+
}
|
|
45
|
+
/** boot 期一次性调用。返回读数(`undefined` = 没店 / 数不出来),绝不抛。 */
|
|
46
|
+
export declare function auditDormantPermissionRules(ctx: DormantRuleAuditCtx): Promise<{
|
|
47
|
+
buckets: number;
|
|
48
|
+
} | undefined>;
|
|
49
|
+
//# sourceMappingURL=permission-rules-audit.d.ts.map
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #203 §3(design/203 v2 §6 F4 的**残余采纳**)—— 默认 ON 的**休眠行审计**。
|
|
3
|
+
*
|
|
4
|
+
* 🔴 这条审计存在的唯一理由:`PERMISSION_RULES_ENABLED` 的默认值在本车从 OFF 翻成 ON,而**旋钮翻转
|
|
5
|
+
* 与规则行的寿命是两条独立的时间线**。三张表自 #154 车二起就随中央 `ensureSchema` 建、并且从来没有被
|
|
6
|
+
* 清过,所以下面这两类部署在升级的那一刻会突然带着**既有**规则行开始放行:
|
|
7
|
+
* · 曾经把旋钮开过一段时间又关掉的部署(行留在库里,只是没人读);
|
|
8
|
+
* · 从旧库恢复/克隆出来的部署(行跟着数据一起来的)。
|
|
9
|
+
* 对运维来说,这是「我什么都没改,worker 却开始少问我了」——一次**放宽面**上的静默变化。审计做的事
|
|
10
|
+
* 很小但是承重:在启动日志里把它说出来,让「少问」是一条能被读到的因果,而不是一次玄学。
|
|
11
|
+
*
|
|
12
|
+
* ── 三条判据,逐条有理由 ────────────────────────────────────────────────────────────────────────
|
|
13
|
+
* ① **店缺席 ⇒ 整条不上场**(零 I/O、零行)。店缺席包括:显式关旋钮、`local` 车道没接 File 店的旧
|
|
14
|
+
* 部署、以及压根没有 backend 的 env-only worker。没有店就没有行,谈不上唤醒。
|
|
15
|
+
* ② **零桶 ⇒ 不打行**。一条「0 条既有规则随默认 ON 激活」的行是纯噪音:它对读日志的人不传递任何
|
|
16
|
+
* 可行动的信息,却让真正有东西被唤醒的那一行变得不显眼。
|
|
17
|
+
* ③ **运维**显式**表过态(`PERMISSION_RULES_ENABLED` 被设成 true 或 false)⇒ 不打行**。那些行本来
|
|
18
|
+
* 就是活的(或者旋钮被显式关着、店根本没装配),默认值翻不翻转对它们零影响 —— 说「随默认 ON
|
|
19
|
+
* 激活」是**假话**,而在启动日志里说假话比不说更坏。判据用 `permissionRulesEnabledExplicit`
|
|
20
|
+
* (config.ts 与 `dbBackendExplicit` 同姿势),不是猜。
|
|
21
|
+
*
|
|
22
|
+
* ── 失败方向 ────────────────────────────────────────────────────────────────────────────────────
|
|
23
|
+
* 数不出来(库抖动 / 文件读不了)⇒ **不拒启、也不编一个 0 出来**:返回 `undefined` + 一条 warn。
|
|
24
|
+
* 这是一条纯诊断面(它不参与任何放行/拒绝判决),所以它的降级方向是「安静地不知道」而不是「谎报
|
|
25
|
+
* 零」——`{ buckets: 0 }` 会被下游读成「确认没有休眠行」,那是把一次读失败伪装成一个结论。
|
|
26
|
+
* 因为它零判决,这条 fail-open 不属于 `docs/FAIL-OPEN-CENSUS.md` 的安全轴 P 类,故不占 `recordFailOpen`
|
|
27
|
+
* 的标签;它的可见性由那条 warn 承担。
|
|
28
|
+
*/
|
|
29
|
+
/** boot 期一次性调用。返回读数(`undefined` = 没店 / 数不出来),绝不抛。 */
|
|
30
|
+
export async function auditDormantPermissionRules(ctx) {
|
|
31
|
+
if (ctx.stores === undefined)
|
|
32
|
+
return undefined; // ①
|
|
33
|
+
let buckets;
|
|
34
|
+
try {
|
|
35
|
+
buckets = await ctx.stores.countBuckets();
|
|
36
|
+
}
|
|
37
|
+
catch (err) {
|
|
38
|
+
ctx.logger.warn?.("permission_rules_dormant_audit_failed", {
|
|
39
|
+
error: err instanceof Error ? err.message : String(err),
|
|
40
|
+
note: "the boot audit could not count pre-existing permission-rule buckets; the lane itself is unaffected",
|
|
41
|
+
});
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
if (buckets > 0 && !ctx.explicit) {
|
|
45
|
+
// ②③ 都不成立才说话。文案给的是**一条可行动的因果**:看到少问了 ⇒ 这里有 N 只既有桶 ⇒ 用
|
|
46
|
+
// `GET /v1/rules` 看、用 `DELETE /v1/rules` 收,或者把旋钮显式关掉。
|
|
47
|
+
//
|
|
48
|
+
// 🔴 **local-owner 桶的现状必须同拍说清**(#203 收官件 / #205 件5,core 5.24.0 提货批):
|
|
49
|
+
// `countBuckets` 数的是**所有**桶,其中可能有一只**身份缺席**的 local-owner 桶(File 后端 =
|
|
50
|
+
// `permission-rules/local-owner.json`,SQL 后端 = owner_key `local-owner` 的行)。而 `GET`/`DELETE
|
|
51
|
+
// /v1/rules` 两口是 **principal 寻址**的(无 principal 一律 401,契约上刻意的边界)—— 对那只桶,
|
|
52
|
+
// 上面那句「inspect with GET / revoke with DELETE」是**兑不出来的承诺**。
|
|
53
|
+
//
|
|
54
|
+
// ⚠️ 文案只说**兑得出来**的话(codex 对抗复审 R1-[medium],验真后改):第一稿在这里写的是
|
|
55
|
+
// 「adopt it into a principal first」——**本仓没有那条路**。收编入口的来源词表逐字只有
|
|
56
|
+
// `{kind:"principal"}`(`adoption/wire.ts` 的 `AdoptionSourceSchema`,`.strict()` 拒 local-owner),
|
|
57
|
+
// core 的 `adoptFilePermissionRuleStore` 在 src/ 下零调用点,SQL 侧连对应原语都没有。指一条走不通
|
|
58
|
+
// 的路比不指更坏:运维会去试,试不通才发现自己被误导。
|
|
59
|
+
// 今天**真能做的**只有两件:①`PERMISSION_RULES_ENABLED=false` 把整条车道关掉(上面已给);
|
|
60
|
+
// ②运维直接动存储。
|
|
61
|
+
// 🔴 第二条的**谓词必须是真的**(codex 对抗复审 R2-[medium],验真后改):第一稿写的是
|
|
62
|
+
// 「owner_key='local-owner'」——那条 SQL **一行都删不到**。`buildRuleOwnerKey`
|
|
63
|
+
// (`plugins/permission-rule-store-sql.ts`)把 owner_key 算成 `sha256("local-owner")` 的十六进制,
|
|
64
|
+
// 字面判别式存在**另一列** `owner_kind` 上。给一条匹配零行的删除语句比不给更坏:它会静默成功,
|
|
65
|
+
// 运维以为收回了权限而规则还在放行。真谓词 = `permission_rule` 表的 `owner_kind='local-owner'`。
|
|
66
|
+
// File 后端那侧是固定文件名,照给。
|
|
67
|
+
// 店层与车道层其实已接通(core 5.24.0 的 `removePersistedRule` 收 `RuleOwner`,本仓
|
|
68
|
+
// `rules-consent.ts` 的 `listRules`/`removeRule` 已能寻址该桶,格见
|
|
69
|
+
// `test/rules-consent-local-owner.test.ts`),缺的只是一个 HTTP 开口 —— 开不开、开在哪条门
|
|
70
|
+
// (operator 面 / run-local 面)属产品面裁定,不在本车擅开。
|
|
71
|
+
ctx.logger.info("permission_rules_activated_by_default", {
|
|
72
|
+
buckets,
|
|
73
|
+
knob: "PERMISSION_RULES_ENABLED",
|
|
74
|
+
explicit: false,
|
|
75
|
+
note: `${buckets} pre-existing permission-rule bucket(s) went live with this release's ON-by-default flip — ` +
|
|
76
|
+
"inspect with GET /v1/rules, revoke with DELETE /v1/rules, or set PERMISSION_RULES_ENABLED=false to keep the lane off. " +
|
|
77
|
+
"NOTE: an identity-less local-owner bucket (if this count includes one) is NOT reachable through those two " +
|
|
78
|
+
"principal-addressed endpoints, and this build exposes NO self-service path for it — either turn the knob off, " +
|
|
79
|
+
"or remove the bucket at the storage layer (file backend: the permission-rules/local-owner.json file; " +
|
|
80
|
+
"SQL backend: the permission_rule row WHERE owner_kind='local-owner')",
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
return { buckets };
|
|
84
|
+
}
|
|
85
|
+
//# sourceMappingURL=permission-rules-audit.js.map
|
package/dist/boot/reapers.d.ts
CHANGED
|
@@ -51,6 +51,12 @@ export interface ThrottledReaperCatch {
|
|
|
51
51
|
* survives across ticks — a fresh instance per tick would never accumulate past 1.
|
|
52
52
|
*/
|
|
53
53
|
export declare function createThrottledReaperCatch(name: string, logger: Logger, threshold?: number): ThrottledReaperCatch;
|
|
54
|
+
/** A-010.17 —— 维护 tick 真正消费的那**一手**(窄口;理由见 `ReapersCtx.permissionRuleStores`)。
|
|
55
|
+
* 可选成员:File 车道没有这一面(追加日志的压实是另一件事,如实登记在 `rules-consent.ts` 的
|
|
56
|
+
* `PermissionRuleStoreBundle.reapExpired` 注里),缺席 ⇒ 本腿零调用。 */
|
|
57
|
+
export interface PermissionRuleRetentionSweeper {
|
|
58
|
+
reapExpired?(nowMs: number): Promise<number>;
|
|
59
|
+
}
|
|
54
60
|
export interface ReapersCtx {
|
|
55
61
|
config: ServiceConfig;
|
|
56
62
|
logger: Logger;
|
|
@@ -80,6 +86,15 @@ export interface ReapersCtx {
|
|
|
80
86
|
* `TOOL_APPROVAL_ENABLED=false` 的部署恒 undefined ⇒ 收敛器照常收敛,只是不发通知帧(壳侧靠
|
|
81
87
|
* 重连 preamble 对账,见 approval-card.ts 的 `ApprovalRevokeFrame` 顶注)。 */
|
|
82
88
|
toolApproval: ToolApprovalCoordinator | undefined;
|
|
89
|
+
/** A-010.17:规则店的**保留期口**。`PERMISSION_RULES_ENABLED=false` 或后端没实装 ⇒ undefined
|
|
90
|
+
* ⇒ 本腿根本不注册(零扫描),与它的同族旋钮腿同姿势。
|
|
91
|
+
*
|
|
92
|
+
* 🔴 类型刻意窄到 {@link PermissionRuleRetentionSweeper} 而不是整个 `PermissionRuleStoreBundle`:
|
|
93
|
+
* 维护 tick 对那只束的其余能力(provider / approvals / tickets)一无所知也不该知道 —— 窄口让测试
|
|
94
|
+
* 能用**两行**字面量驱动真实现的同一段代码,而不必为了满足一个大接口去铸 `as unknown as` 宽松断言
|
|
95
|
+
* (`boot/permission-rules-audit.ts` 的 `DormantRuleCounter` 是同一条判据的先例)。
|
|
96
|
+
* 生产传的仍是整只束(结构上满足这个窄口)。 */
|
|
97
|
+
permissionRuleStores: PermissionRuleRetentionSweeper | undefined;
|
|
83
98
|
/** 晚绑(server 造出来才有)——见文件头「位置即契约」①。 */
|
|
84
99
|
getRunDenySweep: () => ((now: number) => Promise<void>) | undefined;
|
|
85
100
|
}
|