@sema-agent/server 7.119.0 → 7.121.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/MIGRATION.md +129 -0
- package/USAGE.md +72 -23
- package/deploy/sema-up/chart/templates/server-v4pro.yaml +2 -2
- package/deploy/sema-up/chart/templates/server.yaml +2 -2
- package/deploy/sema-up/stack/sema-stack-cluster.yaml +1 -1
- package/dist/boot/config-center.d.ts +5 -7
- package/dist/boot/config-center.js +37 -13
- package/dist/boot/engine-lease.d.ts +1 -1
- package/dist/boot/execution-env.js +3 -2
- package/dist/boot/fatal-exit.d.ts +7 -1
- package/dist/boot/fatal-exit.js +2 -1
- package/dist/boot/resolve-spec.js +1 -0
- package/dist/boot/runner-deps.d.ts +4 -3
- package/dist/boot/runner-deps.js +7 -3
- package/dist/boot/shutdown.d.ts +9 -10
- package/dist/boot/shutdown.js +54 -16
- package/dist/boot/stage-01-config.js +2 -0
- package/dist/boot/stage-09-leader.d.ts +2 -9
- package/dist/boot/stage-10-http-server.d.ts +1 -9
- package/dist/boot/stage-10-http-server.js +31 -8
- package/dist/capabilities/builtin-tools.d.ts +31 -8
- package/dist/capabilities/builtin-tools.js +22 -1
- package/dist/capabilities/composite-members.d.ts +1 -1
- package/dist/capabilities/composite-members.js +1 -1
- package/dist/capabilities/scenarios.d.ts +8 -6
- package/dist/capabilities/scenarios.js +3 -3
- package/dist/capabilities/send-user-file-tool.d.ts +6 -3
- package/dist/capabilities/send-user-file-tool.js +1 -1
- package/dist/config-catalog.js +13 -7
- package/dist/config-center/center-request.d.ts +16 -1
- package/dist/config-center/center-request.js +36 -5
- package/dist/config-lkg.d.ts +33 -5
- package/dist/config-lkg.js +55 -11
- package/dist/config-types.d.ts +46 -3
- package/dist/config-types.js +6 -0
- package/dist/config.d.ts +33 -4
- package/dist/config.js +107 -15
- package/dist/deployment-governance.d.ts +3 -1
- package/dist/deployment-governance.js +3 -1
- package/dist/device-store.d.ts +7 -0
- package/dist/device-store.js +4 -0
- package/dist/device-ws-hub.js +17 -9
- package/dist/device-ws-protocol.d.ts +15 -0
- package/dist/fleet/drain-entry.d.ts +87 -0
- package/dist/fleet/drain-entry.js +35 -0
- package/dist/fleet/engine-signal.d.ts +144 -0
- package/dist/fleet/engine-signal.js +60 -0
- package/dist/fleet/serving-lease.d.ts +47 -0
- package/dist/fleet/serving-lease.js +235 -0
- package/dist/fleet-client.d.ts +35 -19
- package/dist/fleet-client.js +49 -25
- package/dist/http/approval-sweeps.js +3 -5
- package/dist/http/dispatch.js +18 -13
- package/dist/http/health-self-report.d.ts +60 -0
- package/dist/http/health-self-report.js +53 -0
- package/dist/http/principal-gate.d.ts +8 -1
- package/dist/http/principal-gate.js +1 -0
- package/dist/http/readiness.d.ts +25 -5
- package/dist/http/readiness.js +31 -6
- package/dist/http/resume-legs.js +5 -1
- package/dist/http/route-ctx.d.ts +6 -4
- package/dist/http/route-table.d.ts +3 -0
- package/dist/http/route-table.js +4 -1
- package/dist/http/routes/a2a-serve.d.ts +1 -1
- package/dist/http/routes/a2a-serve.js +5 -5
- package/dist/http/routes/admin-drain.d.ts +12 -31
- package/dist/http/routes/admin-drain.js +15 -9
- package/dist/http/routes/approvals-assistant.d.ts +3 -3
- package/dist/http/routes/approvals-assistant.js +3 -3
- package/dist/http/routes/capabilities.js +3 -1
- package/dist/http/routes/devices.js +6 -5
- package/dist/http/routes/leader.d.ts +1 -1
- package/dist/http/routes/leader.js +1 -1
- package/dist/http/routes/memory-consolidation.d.ts +1 -1
- package/dist/http/routes/memory-consolidation.js +1 -1
- package/dist/http/routes/notify-wake.d.ts +1 -1
- package/dist/http/routes/notify-wake.js +1 -1
- package/dist/http/routes/rules.js +14 -6
- package/dist/http/routes/runs.d.ts +2 -2
- package/dist/http/routes/runs.js +2 -2
- package/dist/http/routes/side-query.d.ts +1 -1
- package/dist/http/routes/side-query.js +1 -1
- package/dist/http/routes/tasks.d.ts +2 -2
- package/dist/http/routes/tasks.js +3 -3
- package/dist/http/server.d.ts +7 -21
- package/dist/http/server.js +20 -1
- package/dist/http/wire-types.d.ts +7 -1
- package/dist/list-entry-display.d.ts +14 -0
- package/dist/list-entry-display.js +14 -0
- package/dist/model-reach-scope.d.ts +15 -3
- package/dist/model-reach-scope.js +3 -2
- package/dist/model-unreachable-notice.d.ts +2 -1
- package/dist/model-unreachable-notice.js +1 -0
- package/dist/namespaced-rule-name.js +2 -6
- package/dist/observability/fail-open.d.ts +20 -0
- package/dist/observability/fail-open.js +20 -0
- package/dist/operator-ask.d.ts +1 -1
- package/dist/plugins/one-time-migrations.d.ts +8 -0
- package/dist/plugins/one-time-migrations.js +72 -3
- package/dist/plugins/permission-rule-store-file.js +2 -6
- package/dist/plugins/permission-rule-store-sql.d.ts +65 -6
- package/dist/plugins/permission-rule-store-sql.js +166 -59
- package/dist/plugins/remote-env-device.d.ts +12 -1
- package/dist/plugins/remote-env-device.js +13 -6
- package/dist/plugins/scheduler-support.d.ts +8 -4
- package/dist/plugins/scheduler-support.js +5 -5
- package/dist/request-key-closure.js +1 -1
- package/dist/rule-entry-list.d.ts +0 -1
- package/dist/rule-entry-list.js +4 -9
- package/dist/rules-consent.d.ts +16 -3
- package/dist/rules-consent.js +8 -4
- package/dist/run-local.js +2 -0
- package/dist/runs.js +1 -1
- package/dist/sema-config-dir.d.ts +19 -0
- package/dist/sema-config-dir.js +33 -0
- package/dist/session-policy-wire.js +5 -4
- package/dist/spec-fields.d.ts +5 -4
- package/dist/task-settings.d.ts +3 -3
- package/dist/tool-approval.d.ts +7 -0
- package/dist/tool-approval.js +4 -1
- package/dist/trace/core-keyset-guard.d.ts +2 -2
- package/dist/trace/engine-notice-wire.d.ts +6 -4
- package/dist/trace/engine-notice-wire.js +16 -9
- package/dist/trace/self-minted-wire.d.ts +78 -0
- package/dist/trace/self-minted-wire.js +32 -0
- package/docs/DEVICE-EXECUTOR-PROTOCOL.md +56 -14
- package/package.json +3 -3
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* 两层,一只 `AsyncLocalStorage`:
|
|
5
5
|
* · **腿层**(`withModelReachScope`,三条带用户面投递口的 run 腿各包一次:流式 `POST /v1/tasks/stream`、后台 `POST /v1/runs`、续跑):
|
|
6
|
-
*
|
|
6
|
+
* 带着这条腿的两枚身份 —— `sessionId`(通告归属键)与 `taskId`(这条腿的 run 资源 id,= 本腿交给引擎的 `spec.taskId`;7.121.0)——
|
|
7
|
+
* 与本腿已经说过的目标集合(去重单位 = 每腿每目标一次);
|
|
7
8
|
* · **调用层**(`createModelReachBrain` 每次模型调用包一次):一只可变的 {@link ModelCallReach},出网观察者
|
|
8
9
|
* (`outbound-dispatcher.ts` 的 undici 诊断通道订阅)把这次调用发出的每一条请求的**结局**记在上面 —— 最后一条是
|
|
9
10
|
* 「连上了、有了回答」还是「建连就失败了」。
|
|
@@ -33,6 +34,12 @@ export interface ModelCallReach {
|
|
|
33
34
|
/** 一条 run 腿的作用域。 */
|
|
34
35
|
export interface ModelReachLegScope {
|
|
35
36
|
readonly sessionId: string;
|
|
37
|
+
/**
|
|
38
|
+
* 7.121.0:这条腿的 **run 资源 id** —— 与本腿交给引擎的 `spec.taskId` 同值(流式腿 = `X-Task-Id` / `meta.taskId`,后台腿 = 提交回执的
|
|
39
|
+
* `taskId`,续跑腿 = 被续的那条 run 行 id)。缺席 = 这条腿没有 run 资源(没有 run 店的部署上的流式腿)。不是引擎的 run 身份
|
|
40
|
+
* (`TaskResult.runId`,每次 prepare 一铸)—— 那一枚在模型调用这一层读不到。
|
|
41
|
+
*/
|
|
42
|
+
readonly taskId?: string;
|
|
36
43
|
/** 本腿已经通告过的目标(`targetHost`)—— 去重单位 = 每腿每目标一次。 */
|
|
37
44
|
readonly announced: Set<string>;
|
|
38
45
|
}
|
|
@@ -43,9 +50,14 @@ export interface ModelReachStore {
|
|
|
43
50
|
export declare const modelReachContext: AsyncLocalStorage<ModelReachStore>;
|
|
44
51
|
/**
|
|
45
52
|
* 腿层入口:每条腿都开**自己的**作用域 —— `sessionId` 缺席(无会话腿)⇒ 空作用域(没有归属键就没有用户面可投;也不继承外层腿的
|
|
46
|
-
* 归属键 —— codex r1 旁注:修前缺席即原样执行、会沿用外层腿)。嵌套时内层覆盖外层 ——
|
|
53
|
+
* 归属键 —— codex r1 旁注:修前缺席即原样执行、会沿用外层腿)。嵌套时内层覆盖外层 —— 归属跟着「这一轮是谁的腿」走,两枚身份一起走
|
|
54
|
+
* (内层没有 run 资源就是没有,不借外层的)。两键都是**必填形参**(值可 `undefined`):三个调用点各自说清这条腿有没有 run 资源,
|
|
55
|
+
* 漏传一枚 = 编译红,不会静默变成「没有」。
|
|
47
56
|
*/
|
|
48
|
-
export declare function withModelReachScope<T>(
|
|
57
|
+
export declare function withModelReachScope<T>(leg: {
|
|
58
|
+
readonly sessionId: string | undefined;
|
|
59
|
+
readonly taskId: string | undefined;
|
|
60
|
+
}, fn: () => T): T;
|
|
49
61
|
/** 调用层入口(`createModelReachBrain` 用):在当前腿作用域(可能缺席)之上开一只新的调用观察。 */
|
|
50
62
|
export declare function withModelCallReach<T>(call: ModelCallReach, fn: () => T): T;
|
|
51
63
|
/** 观察者读口:当前异步上下文里的模型调用观察(作用域外 ⇒ `undefined`)。 */
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
2
|
export const modelReachContext = new AsyncLocalStorage();
|
|
3
|
-
export function withModelReachScope(
|
|
4
|
-
|
|
3
|
+
export function withModelReachScope(leg, fn) {
|
|
4
|
+
const { sessionId, taskId } = leg;
|
|
5
|
+
return modelReachContext.run(sessionId === undefined || sessionId.length === 0 ? {} : { leg: { sessionId, ...(taskId !== undefined && taskId.length > 0 ? { taskId } : {}), announced: new Set() } }, fn);
|
|
5
6
|
}
|
|
6
7
|
export function withModelCallReach(call, fn) {
|
|
7
8
|
const outer = modelReachContext.getStore();
|
|
@@ -20,7 +20,8 @@
|
|
|
20
20
|
* 没有归属键 ⇒ 不铸(运维日志 `outbound_connect_failed` 照有)。腿里的**辅助**调用(WebFetch 摘要器、压实、提示建议、分类器 …)与
|
|
21
21
|
* 主循环**同一条规则**:本帧说的是「这个模型端点够不着」,不是「这条 run 失败了」(措辞如此,不暗示终态);不设豁免名单 —— codex r1 / r2
|
|
22
22
|
* 两轮证明豁免名单结构上开放(core 自己发起的辅助调用本仓包不到)。按 **run 终态**归属是另一种设计,候裁(收车档 ⑤)。去重单位 = **每腿每目标一次**(同一条腿里
|
|
23
|
-
* 重试 / 子代对同一个网关再失败,不再说一遍)。受众 user;detail = `{ code, targetHost, proxyInUse?, remedy, sessionId }` ——
|
|
23
|
+
* 重试 / 子代对同一个网关再失败,不再说一遍)。受众 user;detail = `{ code, targetHost, proxyInUse?, remedy, sessionId, taskId? }` ——
|
|
24
|
+
* `taskId`(7.121.0)= 这条腿的 run 资源 id,腿没有 run 资源时缺席;引擎的 run 身份 `runId` 在这一层读不到,本码不带 ——
|
|
24
25
|
* `proxyInUse` 只在这一跳走了代理时在场,且只是**变量名**(代理地址是基础设施细节,不上租户 wire,与 `OutboundProxyError` 的
|
|
25
26
|
* F3 裁定同一条)。`targetHost`(模型端点的 `host:port`)对 user 受众是**新披露的部署事实**(F7 裁定:它是部署自己声明的模型端点,
|
|
26
27
|
* 不是基础设施秘密,可以上;代理地址仍不上)—— 不要读成「终局句里本就有」:代理形下终局句只带码与 `via outbound proxy (from <变量>)`、不含任何主机地址,
|
|
@@ -17,6 +17,7 @@ function announceModelUnreachable(leg, failure, router = defaultEngineNoticeRout
|
|
|
17
17
|
...(failure.proxyInUse !== null ? { proxyInUse: failure.proxyInUse } : {}),
|
|
18
18
|
remedy: MODEL_UNREACHABLE_REMEDY,
|
|
19
19
|
sessionId: leg.sessionId,
|
|
20
|
+
...(leg.taskId !== undefined ? { taskId: leg.taskId } : {}),
|
|
20
21
|
},
|
|
21
22
|
sessionId: leg.sessionId,
|
|
22
23
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { PROTOCOL_TABLE, isNamespacedCoveringRuleName, judgeRuleToolName, parsePermissionRule, protocolOf } from "@sema-agent/core";
|
|
2
|
-
import {
|
|
2
|
+
import { displayListEntry } from "./list-entry-display.js";
|
|
3
3
|
export function ruleEntryToolName(entry) {
|
|
4
4
|
return parsePermissionRule(entry).toolName;
|
|
5
5
|
}
|
|
@@ -11,10 +11,6 @@ export function unmatchableRuleNameOf(name) {
|
|
|
11
11
|
const ns = emptyToolSegmentOf(name);
|
|
12
12
|
return ns === undefined ? undefined : { kind: "empty_tool_segment", ns };
|
|
13
13
|
}
|
|
14
|
-
function presentName(name) {
|
|
15
|
-
const clean = name.replace(CONTROL_AND_BIDI_STRIP_RE, "");
|
|
16
|
-
return clean.length <= 160 ? clean : `${clean.slice(0, 160)}…(${name.length} chars)`;
|
|
17
|
-
}
|
|
18
14
|
export function describeUnmatchableRuleName(v, name) {
|
|
19
15
|
switch (v.kind) {
|
|
20
16
|
case "retired":
|
|
@@ -22,7 +18,7 @@ export function describeUnmatchableRuleName(v, name) {
|
|
|
22
18
|
case "unmintable_segment":
|
|
23
19
|
return v.guidance;
|
|
24
20
|
case "unprefixed_namespace":
|
|
25
|
-
return `pre-prefix short form — the live name carries a protocol prefix: ${PROTOCOL_TABLE.map((e) => `${e.prefix}${
|
|
21
|
+
return `pre-prefix short form — the live name carries a protocol prefix: ${PROTOCOL_TABLE.map((e) => displayListEntry(`${e.prefix}${name}`)).join(" or ")}`;
|
|
26
22
|
case "empty_tool_segment":
|
|
27
23
|
return describeEmptyToolSegment(v.ns);
|
|
28
24
|
default: {
|
|
@@ -394,6 +394,26 @@ export declare const FAIL_OPEN_TAGS: {
|
|
|
394
394
|
readonly cls: "F";
|
|
395
395
|
readonly note: "S-1195(7.119.0;`plugins/snapshot-bucket-presence.ts` 的 `ensureSnapshotBucket`,boot 期核一次快照桶 `SESSION_SNAPSHOT_BUCKET`;detail = `bucket=… endpoint=<buildDisplaySafeUrl 的显示形> <HTTP 状态 | request failed: 错类>`):presigned 列举对 `MINIO_ENDPOINT` **连不上 / 403 / 5xx / 没有 NoSuchBucket 的 404**,或探针到窗尾一次都没答过(detail 写 `probe still pending when the window ended`)⇒ **证明不了桶缺**,不拒启、照常起服(只有 404 + `NoSuchBucket` 才是「缺」,那一形窗内重试、到窗拒启,不走本 tag)。三问:①错误落到哪 —— 本 tag 的计数 / 探针行 / 记录器的一次性 warn,detail 带端点显示形与状态或错类;②谁受害 —— 桶其实不存在、或凭据不能列举的部署:之后每一次快照 offload 写与 SendUserFile 私有 presigned 请求会答 404 / 403,孤儿 sweep 每分钟一条失败,运维读到的是一串失败请求而不是「桶没建」(与 7.118.x 的既有形状同形,不是新损失);③缺席倒向哪边 —— 倒向**放行起服**:没有证据既不拒启、也不伪造「桶在场」或「桶缺」,不是门 / 权限 / 凭证。最坏后果 = 缺桶的部署晚一步才暴露;不放行的代价是对象存储一次抖动 / 一次 403 就让整只服务起不来。计数非零 = 这台部署启动时没能确认快照桶,按 detail 的端点与状态去核对网络 / 凭据 / 桶。";
|
|
396
396
|
};
|
|
397
|
+
readonly "server.config-lkg.landed-path-unresolved": {
|
|
398
|
+
readonly cls: "P-DEBT";
|
|
399
|
+
readonly note: "7.120.0(`config-lkg.ts` 的 `landedPathOf`,`lkgReadDenyRows` 每次铸部署姿态座席时调;detail = `segment=<那一段的路径>`):`CONFIG_LKG_PATH` 覆写路径上有一段**目录在、却解不开**(悬空符号链接 / 链接环 / 读不了的目录:`realpath` 失败而 `lstat` 不说「不存在」)⇒ 这一段之下 LKG 真正会落在哪说不出来,读拒行只剩声明的拼写那一行(照旧在),真实落点那一行缺席。段**不存在**(`ENOENT` / `ENOTDIR`:还没建出来)是常态,不进这一臂;末段(文件本身)是悬空链接也不进(落盘的 rename 换掉的是那条目录项,文件就落在声明的拼写上)。归 P 的理由:这一行守的是 LKG 文件(含凭据),链接之后通了、LKG 写到链接目标下,模型按目标路径读得到它,直到重启重铸座席。归 DEBT 的理由:引擎没有「锚定单文件」的读拒行、座席只在 Runner 构造时铸一次,本服务说不出将来的落点;拿文件名在任意深度拒会把用户仓里同名文件一并挡掉。计数非零 = 去看 detail 点名的那一段(链接指向哪、卷挂了没有),修好后重启。";
|
|
400
|
+
};
|
|
401
|
+
readonly "server.fleet.serving-lease-unconfirmed": {
|
|
402
|
+
readonly cls: "P-DEBT";
|
|
403
|
+
readonly note: "7.121.0 修复轮(`fleet/serving-lease.ts` 的 `renewOnce`,租约生效时每一次续租;detail = `worker=<w> generation=<g> why=<不可达句 | verdict=unknown>`):中心这一次**没有确认**本实例该不该服务 —— 不可达 / 超时 / 非 2xx / 答复读不成判决,或答 `unknown`(此刻没有本实例的生命周期对象)。按设计**什么都不改**(规则 2 第三臂):到期之前照常 `granted`,过期后 `expired` 照常接单,直到过期满 `FLEET_LEASE_OUTAGE_MAX_MS` 才拒新工作。放行的最坏后果 = 一只其实已被取代的实例在中心失联期间多服务至多「租约剩余 + `T_max`」(中心的代际下限持久,恢复后第一次续租即拒);不放行的代价是中心抖一下整队停服。归 P 类:围栏轴,最坏后果是作业完整性(被取代的实例多服务),不是难看;归 DEBT:容忍规则本身(`T_max` 的取值与去抖)待裁定,收口条件 = 那次裁定落地。计数持续非零 = 中心长时间没确认本实例,去看 detail 的 why(连不上 / 中心不认识这只实例)。";
|
|
404
|
+
};
|
|
405
|
+
readonly "server.fleet.serving-lease-capability-absent": {
|
|
406
|
+
readonly cls: "P-DEBT";
|
|
407
|
+
readonly note: "7.121.0 修复轮(`fleet/serving-lease.ts` 的 `noteCenterCapabilities`;fleet 成员每一次中心能力读数;detail = `worker=<w> generation=<g> was=<enabled|none>`):中心答复体读得出、却**没有宣告** `servingLease`(老中心 `{ ok: true }` / 降级中心 / 位撤掉)⇒ 规则 1:租约不生效,`leaseState: none`,零续租、零 503 —— 这只 fleet 成员此刻**不受围栏**。答复体读不出不进这一臂(那是「没有读数」,上一次的位与钟原样,`fleet-client.ts` `capabilityReadingOf`);已被拒的实例不进(拒是黏的,位消失不复活)。放行的最坏后果 = 中心不执行围栏期间,一只被取代的实例照常服务;不放行的代价是老中心 / 降级中心下所有成员在第一次续租就失败。归 P 类:围栏轴,成员此刻不受围栏;归 DEBT:能力门控是契约规则(中心不宣告 = 不执行),收口条件 = 租约容忍规则的那次裁定(含「位从有到无再到有」的去抖)落地。计数非零 = 这台中心没宣告租约能力。";
|
|
408
|
+
};
|
|
409
|
+
readonly "server.fleet.announce-reply-unreadable": {
|
|
410
|
+
readonly cls: "P-DEBT";
|
|
411
|
+
readonly note: "7.121.0 修复验证轮(`fleet-client.ts` 的公告环,fleet 成员每一拍心跳 2xx 答复;detail = `why=<body|not-an-object|invalid-capabilities> leaseState=<此刻的租约态>`):答复不是能力读数(体读不出 / 不是对象 / `capabilities` 键在场而形坏 —— settings-schema 9.0.0 `readCapabilityReading` 的 `unreadable`,外加本仓的 `body`)⇒ 当作没有这一拍答复,租约的状态与钟原样。**一律**计数(不论租约生效与否):租约还没生效(`leaseState=none`)时这只成员**一直不受围栏**,而修前这一形零计数(中心每拍答 `<html>` ⇒ 成员恒 none、零痕迹);租约已生效时计数说明中心的答复在坏、围栏靠的是上一次读数。归 P 类:围栏轴;归 DEBT:本进程判不出中心为什么答坏体,收口条件 = 中心契约 / 监控(中心侧对心跳答复形的执法)落地。每段连拍另有一行 warn `fleet_announce_reply_unreadable`(带 `why`)。";
|
|
412
|
+
};
|
|
413
|
+
readonly "server.fleet.denied-marker-unpersisted": {
|
|
414
|
+
readonly cls: "P-DEBT";
|
|
415
|
+
readonly note: "7.121.0(`fleet/serving-lease.ts` 的 `enterDenied`,中心以正面证据拒了本实例的服务租约那一拍;detail = `worker=<w> generation=<g>`):拒要落成数据根下一只文件(重启后起步即排空),而这一次**没落下** —— 没有数据根,或写失败(盘满 / 只读挂载 / 目录不在)。本进程照样被拒、照样当拍排空、到期自停;放行的是**重启之后**:同代际重启的实例不记得被拒,在第一次续租得到答复之前会接新工作(中心宣告了能力位时,第一次续租就再被拒一次 —— 中心的代际下限是持久的;中心那时不可达则一直接单到过期满 `FLEET_LEASE_OUTAGE_MAX_MS`)。归 P 类:这是围栏的记忆;归 DEBT:进程内无法把「记不住」变成 fail-closed(起步不接单直到第一次答复,是对租约不可达容忍规则的改动,另案)。denied 行的 `sticky` 同拍说 `process-only`,不声称跨重启记得。计数非零 = 这台机器的数据根写不进,或部署没有数据根。";
|
|
416
|
+
};
|
|
397
417
|
};
|
|
398
418
|
/** 词表键推导的闭集类型——未登记的 tag 传不进 {@link recordFailOpen}(编译期拒)。 */
|
|
399
419
|
export type FailOpenTag = keyof typeof FAIL_OPEN_TAGS;
|
|
@@ -367,6 +367,26 @@ export const FAIL_OPEN_TAGS = {
|
|
|
367
367
|
cls: "F",
|
|
368
368
|
note: "S-1195(7.119.0;`plugins/snapshot-bucket-presence.ts` 的 `ensureSnapshotBucket`,boot 期核一次快照桶 `SESSION_SNAPSHOT_BUCKET`;detail = `bucket=… endpoint=<buildDisplaySafeUrl 的显示形> <HTTP 状态 | request failed: 错类>`):presigned 列举对 `MINIO_ENDPOINT` **连不上 / 403 / 5xx / 没有 NoSuchBucket 的 404**,或探针到窗尾一次都没答过(detail 写 `probe still pending when the window ended`)⇒ **证明不了桶缺**,不拒启、照常起服(只有 404 + `NoSuchBucket` 才是「缺」,那一形窗内重试、到窗拒启,不走本 tag)。三问:①错误落到哪 —— 本 tag 的计数 / 探针行 / 记录器的一次性 warn,detail 带端点显示形与状态或错类;②谁受害 —— 桶其实不存在、或凭据不能列举的部署:之后每一次快照 offload 写与 SendUserFile 私有 presigned 请求会答 404 / 403,孤儿 sweep 每分钟一条失败,运维读到的是一串失败请求而不是「桶没建」(与 7.118.x 的既有形状同形,不是新损失);③缺席倒向哪边 —— 倒向**放行起服**:没有证据既不拒启、也不伪造「桶在场」或「桶缺」,不是门 / 权限 / 凭证。最坏后果 = 缺桶的部署晚一步才暴露;不放行的代价是对象存储一次抖动 / 一次 403 就让整只服务起不来。计数非零 = 这台部署启动时没能确认快照桶,按 detail 的端点与状态去核对网络 / 凭据 / 桶。",
|
|
369
369
|
},
|
|
370
|
+
"server.config-lkg.landed-path-unresolved": {
|
|
371
|
+
cls: "P-DEBT",
|
|
372
|
+
note: "7.120.0(`config-lkg.ts` 的 `landedPathOf`,`lkgReadDenyRows` 每次铸部署姿态座席时调;detail = `segment=<那一段的路径>`):`CONFIG_LKG_PATH` 覆写路径上有一段**目录在、却解不开**(悬空符号链接 / 链接环 / 读不了的目录:`realpath` 失败而 `lstat` 不说「不存在」)⇒ 这一段之下 LKG 真正会落在哪说不出来,读拒行只剩声明的拼写那一行(照旧在),真实落点那一行缺席。段**不存在**(`ENOENT` / `ENOTDIR`:还没建出来)是常态,不进这一臂;末段(文件本身)是悬空链接也不进(落盘的 rename 换掉的是那条目录项,文件就落在声明的拼写上)。归 P 的理由:这一行守的是 LKG 文件(含凭据),链接之后通了、LKG 写到链接目标下,模型按目标路径读得到它,直到重启重铸座席。归 DEBT 的理由:引擎没有「锚定单文件」的读拒行、座席只在 Runner 构造时铸一次,本服务说不出将来的落点;拿文件名在任意深度拒会把用户仓里同名文件一并挡掉。计数非零 = 去看 detail 点名的那一段(链接指向哪、卷挂了没有),修好后重启。",
|
|
373
|
+
},
|
|
374
|
+
"server.fleet.serving-lease-unconfirmed": {
|
|
375
|
+
cls: "P-DEBT",
|
|
376
|
+
note: "7.121.0 修复轮(`fleet/serving-lease.ts` 的 `renewOnce`,租约生效时每一次续租;detail = `worker=<w> generation=<g> why=<不可达句 | verdict=unknown>`):中心这一次**没有确认**本实例该不该服务 —— 不可达 / 超时 / 非 2xx / 答复读不成判决,或答 `unknown`(此刻没有本实例的生命周期对象)。按设计**什么都不改**(规则 2 第三臂):到期之前照常 `granted`,过期后 `expired` 照常接单,直到过期满 `FLEET_LEASE_OUTAGE_MAX_MS` 才拒新工作。放行的最坏后果 = 一只其实已被取代的实例在中心失联期间多服务至多「租约剩余 + `T_max`」(中心的代际下限持久,恢复后第一次续租即拒);不放行的代价是中心抖一下整队停服。归 P 类:围栏轴,最坏后果是作业完整性(被取代的实例多服务),不是难看;归 DEBT:容忍规则本身(`T_max` 的取值与去抖)待裁定,收口条件 = 那次裁定落地。计数持续非零 = 中心长时间没确认本实例,去看 detail 的 why(连不上 / 中心不认识这只实例)。",
|
|
377
|
+
},
|
|
378
|
+
"server.fleet.serving-lease-capability-absent": {
|
|
379
|
+
cls: "P-DEBT",
|
|
380
|
+
note: "7.121.0 修复轮(`fleet/serving-lease.ts` 的 `noteCenterCapabilities`;fleet 成员每一次中心能力读数;detail = `worker=<w> generation=<g> was=<enabled|none>`):中心答复体读得出、却**没有宣告** `servingLease`(老中心 `{ ok: true }` / 降级中心 / 位撤掉)⇒ 规则 1:租约不生效,`leaseState: none`,零续租、零 503 —— 这只 fleet 成员此刻**不受围栏**。答复体读不出不进这一臂(那是「没有读数」,上一次的位与钟原样,`fleet-client.ts` `capabilityReadingOf`);已被拒的实例不进(拒是黏的,位消失不复活)。放行的最坏后果 = 中心不执行围栏期间,一只被取代的实例照常服务;不放行的代价是老中心 / 降级中心下所有成员在第一次续租就失败。归 P 类:围栏轴,成员此刻不受围栏;归 DEBT:能力门控是契约规则(中心不宣告 = 不执行),收口条件 = 租约容忍规则的那次裁定(含「位从有到无再到有」的去抖)落地。计数非零 = 这台中心没宣告租约能力。",
|
|
381
|
+
},
|
|
382
|
+
"server.fleet.announce-reply-unreadable": {
|
|
383
|
+
cls: "P-DEBT",
|
|
384
|
+
note: "7.121.0 修复验证轮(`fleet-client.ts` 的公告环,fleet 成员每一拍心跳 2xx 答复;detail = `why=<body|not-an-object|invalid-capabilities> leaseState=<此刻的租约态>`):答复不是能力读数(体读不出 / 不是对象 / `capabilities` 键在场而形坏 —— settings-schema 9.0.0 `readCapabilityReading` 的 `unreadable`,外加本仓的 `body`)⇒ 当作没有这一拍答复,租约的状态与钟原样。**一律**计数(不论租约生效与否):租约还没生效(`leaseState=none`)时这只成员**一直不受围栏**,而修前这一形零计数(中心每拍答 `<html>` ⇒ 成员恒 none、零痕迹);租约已生效时计数说明中心的答复在坏、围栏靠的是上一次读数。归 P 类:围栏轴;归 DEBT:本进程判不出中心为什么答坏体,收口条件 = 中心契约 / 监控(中心侧对心跳答复形的执法)落地。每段连拍另有一行 warn `fleet_announce_reply_unreadable`(带 `why`)。",
|
|
385
|
+
},
|
|
386
|
+
"server.fleet.denied-marker-unpersisted": {
|
|
387
|
+
cls: "P-DEBT",
|
|
388
|
+
note: "7.121.0(`fleet/serving-lease.ts` 的 `enterDenied`,中心以正面证据拒了本实例的服务租约那一拍;detail = `worker=<w> generation=<g>`):拒要落成数据根下一只文件(重启后起步即排空),而这一次**没落下** —— 没有数据根,或写失败(盘满 / 只读挂载 / 目录不在)。本进程照样被拒、照样当拍排空、到期自停;放行的是**重启之后**:同代际重启的实例不记得被拒,在第一次续租得到答复之前会接新工作(中心宣告了能力位时,第一次续租就再被拒一次 —— 中心的代际下限是持久的;中心那时不可达则一直接单到过期满 `FLEET_LEASE_OUTAGE_MAX_MS`)。归 P 类:这是围栏的记忆;归 DEBT:进程内无法把「记不住」变成 fail-closed(起步不接单直到第一次答复,是对租约不可达容忍规则的改动,另案)。denied 行的 `sticky` 同拍说 `process-only`,不声称跨重启记得。计数非零 = 这台机器的数据根写不进,或部署没有数据根。",
|
|
389
|
+
},
|
|
370
390
|
};
|
|
371
391
|
export function failOpenTagForDroppedFrame(frameType) {
|
|
372
392
|
switch (frameType) {
|
package/dist/operator-ask.d.ts
CHANGED
|
@@ -68,7 +68,7 @@ export declare function operatorAsk(rule: string, message: string): OperatorAskR
|
|
|
68
68
|
export declare function operatorAlwaysHumanAsk(rule: string, message: string): OperatorAskResult;
|
|
69
69
|
/**
|
|
70
70
|
* 🔴 **S-958 —— 治理层的叶策略包装**:`leaf` 产的每一只 ask 盖部署出身(经 {@link withDeploymentAuthorship}),allow / deny 原物交回;
|
|
71
|
-
* 成员(`projection` / `
|
|
71
|
+
* 成员(`projection` / `pathRules` / `writeTargetGuard` / `nameSets`)经 core `wrapToolPolicy` 随行(契约「包装至少拒内层所拒」:
|
|
72
72
|
* 只加一位、从不放宽)。同步叶保持同步返回;叶抛照抛(fail-closed 的属主仍是叶)。
|
|
73
73
|
*
|
|
74
74
|
* **只收叶**:治理层(`runtime-governance.ts` `applyRuntimeGovernance`)对每一片叶各盖一次,再交 core `combinePolicies` 折**一次**
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Pool as MysqlPool } from "mysql2/promise";
|
|
2
2
|
import type { Pool as PgPool } from "pg";
|
|
3
|
+
import { type UndurableScopeRuleRow, type UnreadableRuleBucket } from "./permission-rule-store-sql.js";
|
|
3
4
|
/** 迁移读数(启动日志一行写全;测试按它断言,不靠日志文本)。 */
|
|
4
5
|
export interface OneTimeMigrationReport {
|
|
5
6
|
/** M1:退役快照索引表 —— `dropped` = 本次真 drop 了,`absent` = 早已不在(第二跑/全新库)。 */
|
|
@@ -33,6 +34,13 @@ export interface OneTimeMigrationReport {
|
|
|
33
34
|
readonly workflowParkIndexKey: "reshaped" | "current" | "file-ledger";
|
|
34
35
|
/** M9:改形时原样搬进新表的指针行数(`current` / `file-ledger` 时恒 0)。非零 = 这次迁移真重建了一张表,运维该看见一次。 */
|
|
35
36
|
readonly workflowParkIndexRowsCarried: number;
|
|
37
|
+
/** M11:本次从持久规则桶里删掉的、scope 不被引擎 durable 谓词收的行(一行一条,启动日志逐行 warn;local 车道恒空,见头注 M11 段)。 */
|
|
38
|
+
readonly undurableScopeRuleRows: readonly UndurableScopeRuleRow[];
|
|
39
|
+
/** M11:连续争不过在线写者、留到下一次起服的桶数(那期间坏行照旧由引擎读侧丢并告知;非零 ⇒ warn)。 */
|
|
40
|
+
readonly undurableScopeRuleBucketsDeferred: number;
|
|
41
|
+
/** M11:读腿整桶拒的桶(唯一判官判不过:形 / 指纹 / behavior 闭集 / 墓碑规范文本 / 单一验证器)—— 一个字节没动,一桶一条拒因
|
|
42
|
+
* (不带行内容),启动日志逐桶 warn;local 车道恒空(见头注 M11 段)。 */
|
|
43
|
+
readonly undurableScopeRuleBucketsUnreadable: readonly UnreadableRuleBucket[];
|
|
36
44
|
}
|
|
37
45
|
/**
|
|
38
46
|
* S-590 —— 一条一次性迁移的窗(登记表的一行;纯数据)。字段注写着每个值的取证方式,改一行之前照着重核。
|
|
@@ -5,6 +5,7 @@ import { ensureIndexes } from "./ensure-index.js";
|
|
|
5
5
|
import { decodeJsonColumn, isJsonColumnBodyProblem, jsonColumnText, jsonColumnTextExpr, readNonNullJsonValueColumn } from "./sql-json-column.js";
|
|
6
6
|
import { SESSION_META_INCARNATION_COLUMN, mintSessionIncarnation } from "../session-incarnation.js";
|
|
7
7
|
import { RUN_HOLDER_DEATH_CODES, RUN_ORPHANED_ERROR, RUN_STALLED_ERROR } from "./store-contracts.js";
|
|
8
|
+
import { PERMISSION_RULE_TABLE, purgeUndurableScopeRows } from "./permission-rule-store-sql.js";
|
|
8
9
|
export const ONE_TIME_MIGRATIONS = [
|
|
9
10
|
{
|
|
10
11
|
id: "M1",
|
|
@@ -133,6 +134,21 @@ export const ONE_TIME_MIGRATIONS = [
|
|
|
133
134
|
"已存的规则对写下它们的 principal 的 run 照旧生效;出路 = 删会话(`DELETE /v1/sessions/:id`,规则随之删)",
|
|
134
135
|
reportKeys: [],
|
|
135
136
|
},
|
|
137
|
+
{
|
|
138
|
+
id: "M11",
|
|
139
|
+
introducedIn: "7.120.0",
|
|
140
|
+
releasedOn: "2026-10-08",
|
|
141
|
+
removableAfter: "2026-11-07",
|
|
142
|
+
removal: "删 migrateOn 的 M11 步(`permission_rule` 在场探针 + `purgeUndurableScopeRows` 调用)与 `permission-rule-store-sql.ts` 的 `purgeUndurableScopeRows` / " +
|
|
143
|
+
"`purgeAllBuckets` / `purgeOneBucket` / `contentFreeRefusal` / `PurgeAccumulator` / `UndurableScopeRuleRow` / `UnreadableRuleBucket` / `UndurableScopePurge` / " +
|
|
144
|
+
"`UndurableScopePurgeDb` / `UNDURABLE_PURGE_PAGE` / `UNDURABLE_PURGE_OCC_ATTEMPTS`、本文件的 `removedRuleRowNote`,以及 `BucketRefusedError` 的 `verdict` 位(若届时再无别的读者;" +
|
|
145
|
+
"判官 `decodeBucketRow` 与写法 `writeBucket` 是读腿 / 写臂本身,留);报告、日志行(含逐行的 `permission_rule_row_removed` 与逐桶的 " +
|
|
146
|
+
"`permission_rule_bucket_unreadable`)与 `NO_SQL_MIGRATIONS` 的 `undurableScopeRuleRows` / `undurableScopeRuleBucketsDeferred` / `undurableScopeRuleBucketsUnreadable`",
|
|
147
|
+
afterRemoval: "引擎读侧的既有响亮臂:scope root 不是完整绝对路径的存量行(跳过了带本条的版本直接升的库,或窗关之后别的写者写进来的)在有效视图里被丢、" +
|
|
148
|
+
"每次 run 一条 `permission.rule_row_dropped`(点名规则与 root)—— 那一行不参与任何检查,但撤销口删不动它(引擎的写臂对这种 scope 同样拒);" +
|
|
149
|
+
"出路 = 运维在库里把那只桶的那一行删掉(整桶 OCC 回写,指纹按 `buildRuleBucketChecksum` 重算)",
|
|
150
|
+
reportKeys: ["undurableScopeRuleRows", "undurableScopeRuleBucketsDeferred", "undurableScopeRuleBucketsUnreadable"],
|
|
151
|
+
},
|
|
136
152
|
];
|
|
137
153
|
export function migrationWindowOpen(window, today) {
|
|
138
154
|
return window.removableAfter === null || today <= window.removableAfter;
|
|
@@ -149,6 +165,9 @@ export const NO_SQL_MIGRATIONS = {
|
|
|
149
165
|
allowRuleShadowRowsUnreadable: 0,
|
|
150
166
|
workflowParkIndexKey: "file-ledger",
|
|
151
167
|
workflowParkIndexRowsCarried: 0,
|
|
168
|
+
undurableScopeRuleRows: [],
|
|
169
|
+
undurableScopeRuleBucketsDeferred: 0,
|
|
170
|
+
undurableScopeRuleBucketsUnreadable: [],
|
|
152
171
|
};
|
|
153
172
|
const RETIRED_SNAPSHOT_TABLE = "snapshot_manifest";
|
|
154
173
|
const RETIRED_STEER_SEAT_COLUMN = "pending_steer";
|
|
@@ -293,8 +312,16 @@ async function migrateOn(db, dialect) {
|
|
|
293
312
|
let retiredSnapshotTable = "absent";
|
|
294
313
|
let retiredSnapshotRows = 0;
|
|
295
314
|
let retiredSteerSeatColumn = "absent";
|
|
315
|
+
let undurableScopeRuleRows = [];
|
|
316
|
+
let undurableScopeRuleBucketsDeferred = 0;
|
|
317
|
+
let undurableScopeRuleBucketsUnreadable = [];
|
|
296
318
|
const destroyed = [];
|
|
297
319
|
try {
|
|
320
|
+
if (await tableExists(db, dialect, PERMISSION_RULE_TABLE)) {
|
|
321
|
+
({ removed: undurableScopeRuleRows, bucketsDeferred: undurableScopeRuleBucketsDeferred, bucketsUnreadable: undurableScopeRuleBucketsUnreadable } = await purgeUndurableScopeRows(db, dialect));
|
|
322
|
+
for (const r of undurableScopeRuleRows)
|
|
323
|
+
destroyed.push(`removed permission_rule ${r.kind} (bucket ${r.ownerKey}, ${r.behavior} ${r.tool}, scope root ${JSON.stringify(r.scopeRoot)})`);
|
|
324
|
+
}
|
|
298
325
|
const counted = !snapshotTablePresent
|
|
299
326
|
? undefined
|
|
300
327
|
: await db.rows(`SELECT COUNT(*) AS n FROM ${RETIRED_SNAPSHOT_TABLE}`).then((rows) => Number(rows[0]?.n ?? 0), async (e) => {
|
|
@@ -323,7 +350,7 @@ async function migrateOn(db, dialect) {
|
|
|
323
350
|
return {
|
|
324
351
|
retiredSnapshotTable, retiredSnapshotRows, retiredSteerSeatColumn, retiredSteerSeatStaleRows, askOriginRowsMigrated, auditDetailJsonNullRows,
|
|
325
352
|
sessionIncarnationColumn, sessionIncarnationsBackfilled, holderDeathCodeRowsBackfilled, allowRuleShadowRowsMigrated, allowRuleShadowRowsUnreadable,
|
|
326
|
-
workflowParkIndexKey, workflowParkIndexRowsCarried,
|
|
353
|
+
workflowParkIndexKey, workflowParkIndexRowsCarried, undurableScopeRuleRows, undurableScopeRuleBucketsDeferred, undurableScopeRuleBucketsUnreadable,
|
|
327
354
|
};
|
|
328
355
|
}
|
|
329
356
|
const WORKFLOW_PARK_INDEX_TABLE = "workflow_agent_session";
|
|
@@ -525,7 +552,8 @@ export function migrateRetiredAskOrigin(type, data) {
|
|
|
525
552
|
export function logOneTimeMigrations(log, backend, report) {
|
|
526
553
|
const changed = report.retiredSnapshotTable === "dropped" || report.retiredSteerSeatColumn === "dropped" || report.askOriginRowsMigrated > 0 || report.auditDetailJsonNullRows > 0 ||
|
|
527
554
|
report.sessionIncarnationColumn === "added" || report.sessionIncarnationsBackfilled > 0 || report.holderDeathCodeRowsBackfilled > 0 || report.allowRuleShadowRowsMigrated > 0 || report.allowRuleShadowRowsUnreadable > 0 ||
|
|
528
|
-
report.workflowParkIndexKey === "reshaped"
|
|
555
|
+
report.workflowParkIndexKey === "reshaped" || report.undurableScopeRuleRows.length > 0 || report.undurableScopeRuleBucketsDeferred > 0 ||
|
|
556
|
+
report.undurableScopeRuleBucketsUnreadable.length > 0;
|
|
529
557
|
(changed ? log.warn : log.info).call(log, "one_time_migrations_applied", {
|
|
530
558
|
backend,
|
|
531
559
|
retiredSnapshotTable: report.retiredSnapshotTable,
|
|
@@ -541,6 +569,9 @@ export function logOneTimeMigrations(log, backend, report) {
|
|
|
541
569
|
allowRuleShadowRowsUnreadable: report.allowRuleShadowRowsUnreadable,
|
|
542
570
|
workflowParkIndexKey: report.workflowParkIndexKey,
|
|
543
571
|
workflowParkIndexRowsCarried: report.workflowParkIndexRowsCarried,
|
|
572
|
+
undurableScopeRuleRowsRemoved: report.undurableScopeRuleRows.length,
|
|
573
|
+
undurableScopeRuleBucketsDeferred: report.undurableScopeRuleBucketsDeferred,
|
|
574
|
+
undurableScopeRuleBucketsUnreadable: report.undurableScopeRuleBucketsUnreadable.length,
|
|
544
575
|
removableAfter: Object.fromEntries(ONE_TIME_MIGRATIONS.map((w) => [w.id, w.removableAfter])),
|
|
545
576
|
note: "one-time persisted-data migrations (M1: retired whole-tree snapshot index table dropped; M2: retired single-seat " +
|
|
546
577
|
"steering column dropped; M3: retired ask-origin spelling rewritten in place; M4: audit details written as the JSON " +
|
|
@@ -551,10 +582,48 @@ export function logOneTimeMigrations(log, backend, report) {
|
|
|
551
582
|
"under an earlier reading from resuming under the current one; M8: parked rows' allow-layer disclosure written by an earlier engine " +
|
|
552
583
|
"under its old key shadowedRule rewritten to allowRuleShadowed; M9: the workflow park pointer index rebuilt with the key (session, run), " +
|
|
553
584
|
"its rows carried over; M10 writes nothing at boot — on the local backend, while its window is open, the first task submitted with a principal " +
|
|
554
|
-
"on a session written by an earlier version (which recorded no owner) claims that session for the principal, one session_owner_claimed line per claim
|
|
585
|
+
"on a session written by an earlier version (which recorded no owner) claims that session for the principal, one session_owner_claimed line per claim; " +
|
|
586
|
+
"M11: persisted permission rules whose project scope root is not a complete absolute path — accepted by earlier versions, dropped on every read " +
|
|
587
|
+
"by the current engine and not removable through DELETE /v1/rules — removed, one permission_rule_row_removed line per row; a bucket the read path " +
|
|
588
|
+
"refuses in whole is left untouched, one permission_rule_bucket_unreadable line per bucket). Each step " +
|
|
555
589
|
"runs on every boot until its window closes (the release that introduced it + 30 days; the per-step date is in " +
|
|
556
590
|
"removableAfter, null while that release is not published yet) and is deleted in the first minor after that — start a " +
|
|
557
591
|
"version that still carries a step at least once before upgrading past its deletion.",
|
|
558
592
|
});
|
|
593
|
+
for (const row of report.undurableScopeRuleRows) {
|
|
594
|
+
log.warn("permission_rule_row_removed", {
|
|
595
|
+
backend,
|
|
596
|
+
ownerKey: row.ownerKey,
|
|
597
|
+
kind: row.kind,
|
|
598
|
+
behavior: row.behavior,
|
|
599
|
+
tool: row.tool,
|
|
600
|
+
scopeRoot: row.scopeRoot,
|
|
601
|
+
note: removedRuleRowNote(row.kind),
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
for (const bucket of report.undurableScopeRuleBucketsUnreadable) {
|
|
605
|
+
log.warn("permission_rule_bucket_unreadable", {
|
|
606
|
+
backend,
|
|
607
|
+
ownerKey: bucket.ownerKey,
|
|
608
|
+
why: bucket.why,
|
|
609
|
+
note: "a persisted permission-rule bucket that the read path refuses in whole was left untouched by the one-time removal of rows whose project scope root " +
|
|
610
|
+
"is not a complete absolute path (removing rows from a bucket the read path refuses would turn that refusal into a readable bucket): every read of it " +
|
|
611
|
+
"keeps being refused, as before this release, until someone repairs or deletes that row by hand",
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
function removedRuleRowNote(kind) {
|
|
616
|
+
switch (kind) {
|
|
617
|
+
case "rule":
|
|
618
|
+
return "a persisted permission rule whose project scope root is not a complete absolute path was removed by a one-time migration: the current engine drops such a row on every read (it protected nothing) and refuses any write naming that scope, so it could not be revoked through DELETE /v1/rules; re-add it with the project's full path if it is still wanted";
|
|
619
|
+
case "revoked-rule":
|
|
620
|
+
return "a persisted permission rule row whose project scope root is not a complete absolute path, and which had no live approval left (every approval behind it was already removed, or it never had one), was removed by a one-time migration: the row took effect nowhere before and takes effect nowhere now — no action needed";
|
|
621
|
+
case "tombstone":
|
|
622
|
+
return "a revocation record (tombstone) whose project scope root is not a complete absolute path was removed by a one-time migration: it can only cover rules with that same scope, which were removed with it or were never there, so nothing it revoked comes back — no action needed";
|
|
623
|
+
default: {
|
|
624
|
+
const unknown = kind;
|
|
625
|
+
throw new Error(`M11: no log sentence for removed-row kind ${JSON.stringify(unknown)}`);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
559
628
|
}
|
|
560
629
|
//# sourceMappingURL=one-time-migrations.js.map
|
|
@@ -4,16 +4,12 @@ import { readdirSync } from "node:fs";
|
|
|
4
4
|
import { performance } from "node:perf_hooks";
|
|
5
5
|
import { FileDurableRulePartitionProvider, AppendLog, ensureDir, } from "@sema-agent/core";
|
|
6
6
|
import { z } from "zod";
|
|
7
|
-
import { buildRulePayloadHash } from "./permission-rule-store-sql.js";
|
|
7
|
+
import { DurableRuleScopeSchema, buildRulePayloadHash } from "./permission-rule-store-sql.js";
|
|
8
8
|
import { replayJsonlGuarded } from "../observability/corrupt-read-seat.js";
|
|
9
9
|
import { EDITED_RULE_BREADTH_WARNING_CODES, RULE_APPROVAL_RECORD_SCHEMA, UNCOVERED_SEGMENT_REASONS } from "../permission-rule-vocab.js";
|
|
10
10
|
import { RULE_BEHAVIORS } from "@sema-agent/core";
|
|
11
|
-
const RuleScopeSchema = z.union([
|
|
12
|
-
z.object({ kind: z.literal("global") }).strict(),
|
|
13
|
-
z.object({ kind: z.literal("project"), root: z.string().min(1) }).strict(),
|
|
14
|
-
]);
|
|
15
11
|
const RuleDotSchema = z.object({ actor: z.string().min(1), counter: z.number().int().nonnegative().safe() }).strict();
|
|
16
|
-
const RuleCandidateSchema = z.object({ behavior: z.enum(RULE_BEHAVIORS), rule: z.string(), scope:
|
|
12
|
+
const RuleCandidateSchema = z.object({ behavior: z.enum(RULE_BEHAVIORS), rule: z.string(), scope: DurableRuleScopeSchema }).strict();
|
|
17
13
|
const RuleOffer2Schema = z.union([
|
|
18
14
|
z.object({ kind: z.literal("single"), candidate: z.number().int().nonnegative().safe() }).strict(),
|
|
19
15
|
z
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { type PermissionRuleWriter, type DurableRulePartition, type DurableRulePartitionProvider, type RuleScope, type RuleSyncFrontier, type RuleCandidate, type RuleApprovalRecord, type StaleRuleApprovalRecord, type RuleApprovalRecordStore, type RuleOwner } from "@sema-agent/core";
|
|
2
|
+
import { type PermissionRuleWriter, type DurableRulePartition, type DurableRulePartitionProvider, type RuleScope, type RuleSyncFrontier, type RuleBehavior, type RuleCandidate, type RuleApprovalRecord, type StaleRuleApprovalRecord, type RuleApprovalRecordStore, type RuleOwner } from "@sema-agent/core";
|
|
3
3
|
import type { Pool as MySqlPool } from "mysql2/promise";
|
|
4
4
|
import type { PgQueryFn } from "./pg-query.js";
|
|
5
5
|
import { type SqlDriver } from "./sql-driver.js";
|
|
@@ -16,13 +16,19 @@ import { type IndexSpec } from "./ensure-index.js";
|
|
|
16
16
|
*/
|
|
17
17
|
export declare function writerOfSqlRuleStore(store: DurableRulePartition): PermissionRuleWriter | undefined;
|
|
18
18
|
/**
|
|
19
|
-
* 382P1(core 7.0.0 [ref] §4.3)——
|
|
20
|
-
*
|
|
19
|
+
* 382P1(core 7.0.0 [ref] §4.3)—— DURABLE 二员面的 at-rest **形**:`{kind:"global"}` | `{kind:"project", root:string}`。
|
|
20
|
+
*
|
|
21
|
+
* 🔴 **只判形,不判 root 收不收**。「这个 root 能不能当持久项目 scope」只有一只判据 —— core 的 `isValidDurableScope`
|
|
22
|
+
* (7.46.0 起 root 必须是完整绝对路径):写臂在 {@link SqlDurableRulePartition.apply} 里经 core 的
|
|
23
|
+
* `assertWriteDeltaScopeDurable` 先拒,读侧由 core 的统一店在有效视图里丢掉那一行并发 `permission.rule_row_dropped`
|
|
24
|
+
* (一行的坏只丢那一行)。本 schema 若再判一次 root,就是第二份判据:它与 core 答案不同的那一天(`proj` / `C:`
|
|
25
|
+
* 本仓收、引擎丢;空 root 本仓整桶拒、引擎只丢一行),同一行在两侧是两种命运。core 自己的 File 参照后端在 at-rest
|
|
26
|
+
* 上同样只判形。入口(wire)的判据见 `rules-consent.ts` 的 `parseRuleScope`(调同一只谓词)。
|
|
27
|
+
*
|
|
21
28
|
* `RuleScope` 联合自 v7 起有第三员 `session`,但它**结构上**不许进持久店(家在会话 overlay,随会话死):
|
|
22
29
|
* at-rest 解码不认它 ⇒ 一条被外部写进来的 session 行整桶拒读(fail-closed),写臂另由 core 的
|
|
23
|
-
* `assertWriteDeltaScopeDurable` 在任何 DB 往返之前以典型码拒(见 `apply`)
|
|
24
|
-
*
|
|
25
|
-
* `RuleCandidateSchema`)——那是本仓自己的窄型收口,不是把两张表合成一张。
|
|
30
|
+
* `assertWriteDeltaScopeDurable` 在任何 DB 往返之前以典型码拒(见 `apply`)。审批记录的候选也用这张表
|
|
31
|
+
* (下方 `RuleCandidateSchema`;File 店的审批记录读同一只)。
|
|
26
32
|
*/
|
|
27
33
|
export declare const DurableRuleScopeSchema: z.ZodType<RuleScope>;
|
|
28
34
|
export declare const PERMISSION_RULE_TABLE = "permission_rule";
|
|
@@ -406,6 +412,59 @@ export declare const RULE_PENDING_APPROVAL_RETENTION_MS: number;
|
|
|
406
412
|
* 同口径)。在那之前,MySQL 存量库上这条腿仍是全表扫 —— 但每轮 500 行的上界让它的**单次**代价仍然有界。
|
|
407
413
|
*/
|
|
408
414
|
export declare const RULE_REAP_BATCH = 500;
|
|
415
|
+
/**
|
|
416
|
+
* M11 删掉的一行。只带桶键、态、工具名与 scope 的 root —— 规则正文不出本函数(命令行里可以有任何东西)。
|
|
417
|
+
*
|
|
418
|
+
* `kind` 是闭集(启动日志按它穷尽分句,`one-time-migrations.ts` `logOneTimeMigrations`):
|
|
419
|
+
* · `rule` —— 仍有活 add 的规则行(引擎读侧每跑一次丢它并告知;它从未生效,要的话按项目的完整路径重写);
|
|
420
|
+
* · `revoked-rule` —— 没有活 add 的规则行(判活用 core 的 `applyTombstones`:每个 add 都已被同桶墓碑撤掉,或这一行本来就没有 add):它在任何一侧都已不生效
|
|
421
|
+
* (allow / deny / ask 同判),无需处置;逐行 warn 的句子只说这个事实,不依赖桶里有没有墓碑、也不依赖态;
|
|
422
|
+
* · `tombstone` —— 一条撤销记录。它只盖得住同 (behavior, 文本, scope) 的规则 ⇒ 那些规则的 scope 与它同样不被收,随它一起删了或本来就不在,
|
|
423
|
+
* 删它不会让任何被撤销的东西回来。
|
|
424
|
+
*/
|
|
425
|
+
export interface UndurableScopeRuleRow {
|
|
426
|
+
/** 桶键(sha256,不是 principal 原文)。 */
|
|
427
|
+
readonly ownerKey: string;
|
|
428
|
+
readonly kind: "rule" | "revoked-rule" | "tombstone";
|
|
429
|
+
readonly behavior: RuleBehavior;
|
|
430
|
+
readonly tool: string;
|
|
431
|
+
/** scope 的 root,**过 `redactSecrets`**(本仓唯一脱敏器):坏 root 是最不可信的自由文本,可能夹着凭据形,而本读数进启动日志与拒因。 */
|
|
432
|
+
readonly scopeRoot: string;
|
|
433
|
+
}
|
|
434
|
+
/** M11 判成读不出、一个字节没动的桶。`why` = 唯一判官那次整桶拒**不带任何行内容**的拒因(读腿对这只桶照旧整桶拒,它仍是这只桶的属主)。 */
|
|
435
|
+
export interface UnreadableRuleBucket {
|
|
436
|
+
/** 桶键(sha256,不是 principal 原文)。 */
|
|
437
|
+
readonly ownerKey: string;
|
|
438
|
+
readonly why: string;
|
|
439
|
+
}
|
|
440
|
+
/** M11 一次跑完的读数。 */
|
|
441
|
+
export interface UndurableScopePurge {
|
|
442
|
+
readonly removed: readonly UndurableScopeRuleRow[];
|
|
443
|
+
/** 连续争不过在线写者(OCC 每次都输)而留到下一次起服的桶数。那些桶里的坏行这期间照旧由引擎读侧丢掉并告知。 */
|
|
444
|
+
readonly bucketsDeferred: number;
|
|
445
|
+
/** 读腿整桶拒的桶(形 / 指纹 / behavior 闭集 / 墓碑规范文本 / 单一验证器):一个字节不动,逐桶交出拒因,等人处置。 */
|
|
446
|
+
readonly bucketsUnreadable: readonly UnreadableRuleBucket[];
|
|
447
|
+
}
|
|
448
|
+
/** {@link purgeUndurableScopeRows} 要的最小查询口(一次性迁移段的 `MigrationDb` 同形;占位符按方言由本函数写)。 */
|
|
449
|
+
export interface UndurableScopePurgeDb {
|
|
450
|
+
rows(sql: string, params?: readonly unknown[]): Promise<Record<string, unknown>[]>;
|
|
451
|
+
exec(sql: string, params?: readonly unknown[]): Promise<number>;
|
|
452
|
+
}
|
|
453
|
+
/**
|
|
454
|
+
* **M11** —— 把持久桶里 scope 不被 core `isValidDurableScope` 收的行(规则行与墓碑)删掉,一行一条读数。
|
|
455
|
+
*
|
|
456
|
+
* 这些行从哪来:7.119.0 及更早的本仓入口按「root 非空」收 project scope(`/v1/rules` 写口、两条导入面),引擎 7.45 也收;
|
|
457
|
+
* 引擎 7.46.0 把 project root 收窄成完整绝对路径 ⇒ `project:myproj` 一类存量行在读侧被丢(`permission.rule_row_dropped`,每跑一条),
|
|
458
|
+
* 而撤销口删不动它(引擎的写臂对这种 scope 的墓碑同样拒)。⇒ 一次性迁移,不留「撤销口对坏 scope 另开一条读法」的长期双读。
|
|
459
|
+
*
|
|
460
|
+
* 读法与写法都是**店的那一只**:每只桶先过唯一判官 {@link decodeBucketRow}(读腿用的同一只),判官整桶拒 ⇒ 这只桶**一个字节不动**,
|
|
461
|
+
* 进 `bucketsUnreadable`(桶键 + 不带行内容的拒因;读腿对它的整桶拒仍是它的属主,出路是人);判官放行 ⇒ 在那份合法状态上筛 scope,
|
|
462
|
+
* 经写臂的同一只 {@link writeBucket} 整桶 OCC 回写(`WHERE owner_key = ? AND rev = ?`,rev+1,落盘字节与指纹同写臂)—— 不碰别的桶。
|
|
463
|
+
* **硬删、不铸墓碑**:墓碑的作用是挡同步回灌,而引擎的 sync-join 对 scope 不合法的入站行本来就丢(`applySyncJoin`),
|
|
464
|
+
* 本店也不实现同步(见 `apply` 的 sync-join 臂)。
|
|
465
|
+
* 幂等:第二跑零命中零写(读不出的桶每一跑照报,直到有人处置)。
|
|
466
|
+
*/
|
|
467
|
+
export declare function purgeUndurableScopeRows(db: UndurableScopePurgeDb, dialect: "tidb" | "pg", now?: () => number): Promise<UndurableScopePurge>;
|
|
409
468
|
/** 三个 SQL 面的一次性装配束(one driver, three faces)。`durable` = [ref] 的 durable 分区后端;
|
|
410
469
|
* 引擎接缝上的统一店由 `main.ts` 用 `createPermissionRuleStoreProvider({ durable })` 合成一次。 */
|
|
411
470
|
export interface PermissionRuleStores {
|