@sema-agent/server 7.10.0 → 7.12.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 +56 -0
- package/dist/adoption/plan.d.ts +152 -0
- package/dist/adoption/plan.js +513 -0
- package/dist/adoption/runner.d.ts +54 -0
- package/dist/adoption/runner.js +505 -0
- package/dist/adoption/sql.d.ts +76 -0
- package/dist/adoption/sql.js +106 -0
- package/dist/adoption/wire.d.ts +250 -0
- package/dist/adoption/wire.js +153 -0
- package/dist/approval-card.d.ts +24 -0
- package/dist/approval-card.js +32 -0
- package/dist/auth-keys.d.ts +28 -4
- package/dist/auth-keys.js +60 -15
- package/dist/boot/adoption.d.ts +30 -0
- package/dist/boot/adoption.js +57 -0
- package/dist/boot/coordinators.d.ts +4 -0
- package/dist/boot/coordinators.js +3 -1
- package/dist/boot/parked-revive-gate.d.ts +56 -7
- package/dist/boot/parked-revive-gate.js +177 -8
- package/dist/boot/permission-rules-audit.d.ts +49 -0
- package/dist/boot/permission-rules-audit.js +57 -0
- package/dist/boot/resolve-spec.js +43 -12
- package/dist/boot/runner-deps.d.ts +10 -2
- package/dist/boot/runner-deps.js +12 -1
- package/dist/budget.js +22 -0
- package/dist/config-types.d.ts +24 -1
- package/dist/config.d.ts +28 -2
- package/dist/config.js +348 -75
- package/dist/governance-ask-marks.js +8 -2
- package/dist/http/route-ctx.d.ts +6 -3
- package/dist/http/routes/adoption.d.ts +26 -0
- package/dist/http/routes/adoption.js +120 -0
- package/dist/http/routes/approvals-assistant.js +2 -1
- package/dist/http/routes/capabilities.js +49 -2
- package/dist/http/routes/rules.d.ts +35 -0
- package/dist/http/routes/rules.js +293 -0
- package/dist/http/routes/shared-memory.d.ts +31 -0
- package/dist/http/routes/shared-memory.js +181 -0
- package/dist/http/routes/trace-usage.js +139 -3
- package/dist/http/server.d.ts +23 -3
- package/dist/http/server.js +123 -3
- package/dist/http/wire-types.d.ts +48 -0
- package/dist/main.js +70 -3
- package/dist/observability/fail-open.d.ts +12 -0
- package/dist/observability/fail-open.js +12 -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 +191 -0
- package/dist/plugins/adoption-log-sql.js +273 -0
- package/dist/plugins/checkpoint-store-sql.d.ts +13 -0
- package/dist/plugins/checkpoint-store-sql.js +11 -0
- package/dist/plugins/local-checkpoint-store.d.ts +10 -0
- package/dist/plugins/local-checkpoint-store.js +8 -0
- 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 +249 -0
- package/dist/plugins/permission-rule-store-sql.js +828 -0
- package/dist/plugins/pg-pool.js +37 -0
- package/dist/plugins/session-policy-store-sql.d.ts +6 -0
- package/dist/plugins/session-policy-store-sql.js +7 -1
- package/dist/plugins/shared-memory-store-sql.d.ts +223 -0
- package/dist/plugins/shared-memory-store-sql.js +516 -0
- package/dist/plugins/store-backend.d.ts +38 -2
- package/dist/plugins/store-backend.js +69 -6
- package/dist/plugins/tidb-pool.js +47 -0
- package/dist/rules-consent.d.ts +194 -0
- package/dist/rules-consent.js +240 -0
- package/dist/run-local.js +120 -13
- package/dist/runtime-governance.js +9 -3
- package/dist/shared-memory-scope-authorizer.d.ts +29 -0
- package/dist/shared-memory-scope-authorizer.js +17 -0
- package/dist/task-settings.d.ts +44 -0
- package/dist/task-settings.js +57 -1
- package/dist/tool-approval.d.ts +60 -0
- package/dist/tool-approval.js +223 -25
- package/dist/trace/core-keyset-guard.d.ts +15 -4
- package/dist/trace/project.d.ts +20 -2
- package/dist/trace/project.js +25 -4
- package/package.json +3 -3
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #154 车二 —— **同意车道的 server 半场**(core 5.18.0 design/179 `permission-rule-consent.ts` 的宿主侧)。
|
|
3
|
+
*
|
|
4
|
+
* core 把一条规则进店的路钉成三步:`durable approval record (pending)` → `authenticated confirmation
|
|
5
|
+
* transfer (approved)` → `principal-bound redemption (redeemed)`,而且**只有第三步**够得着写面。core 明写
|
|
6
|
+
* 「The confirmation transfer is an in-process call. **A deployment must reach it only from its
|
|
7
|
+
* authenticated approval channel**」—— 那句话是把一段责任**交给宿主**,不是一段已经完成的检查。本文件就是
|
|
8
|
+
* 那段责任的落点:
|
|
9
|
+
*
|
|
10
|
+
* · 卡道(兑付口):一次 ask 决议携「不再询问」时,由**这里**去 prepare→confirm→redeem。人是谁 =
|
|
11
|
+
* 回决端点已验明的 principal(不是请求体里的任何字段);规则文本 = **引擎**从被裁决的命令铸的候选
|
|
12
|
+
* (`prepareCardApproval` 不收调用方候选,core 在那条入口上已经把「写自己的规则」这件事变成不可拼写),
|
|
13
|
+
* 客户端只能说「我选第几个」。
|
|
14
|
+
*
|
|
15
|
+
* · 导入道(CC settings):`prepare` 读用户交上来的 settings 层、铸 pending 记录 + **server 自铸的票**;
|
|
16
|
+
* `redeem` 原子消费票、confirm、`redeemRuleBatch`。
|
|
17
|
+
*
|
|
18
|
+
* ─────────────────────────────────────────────────────────────────────────────────────────────────
|
|
19
|
+
* 🔴 票的信任边界四条,**全部是 server 附加的**(不是 core 的语义)
|
|
20
|
+
* ─────────────────────────────────────────────────────────────────────────────────────────────────
|
|
21
|
+
* 亲读 core 的 `mintRuleTicket`(dist,5.22.0):`return \`rt.${candidateIndex}.${recordId}\`` —— 它**没有**
|
|
22
|
+
* TTL、没有签名、没有消费位、没有载荷绑定。core 的票在**进程内**是够的(它的相邻语义是「重放同一次兑付是
|
|
23
|
+
* 幂等的」),但一条跨网络、由客户端保管、可被复制的票需要另外四样:
|
|
24
|
+
* ① **principal 绑定** —— redeem 的已验明 principal 必须 = prepare 铸票的那一位。跨 principal redeem 与
|
|
25
|
+
* unknown ticket **同形 404**:两者返回不同的码就是一台存在性 oracle(拿别人的票撞库能探到「它存在」)。
|
|
26
|
+
* ② **过期** —— `RULE_IMPORT_TICKET_TTL_MS`;过期 redeem 拒。
|
|
27
|
+
* ③ **一次性原子消费** —— 同票二次 redeem 拒,并发双 redeem **恰一成功**。判据由 SQL 的单条条件
|
|
28
|
+
* `UPDATE … WHERE consumed_at_ms IS NULL` 回答,**禁 read-then-write**(见 store 那侧的 🔴 注)。
|
|
29
|
+
* ④ **载荷绑定** —— redeem 应用的规则集 = prepare 时刻的快照。core 已经结构上保证了「规则文本来自记录」,
|
|
30
|
+
* 本层再加一道:票上钉着 prepare 时刻候选集的摘要,redeem 时与记录上的候选集重算比对,不符即拒 ——
|
|
31
|
+
* 于是「篡改记录再拿旧票兑付」这条路上多一道等式,而不是只靠「记录只有我们能写」这一条信念。
|
|
32
|
+
*
|
|
33
|
+
* 命名(CLAUDE.md 工厂律):`build*` = 纯数据;`create*` = 活对象。本文件的 {@link createRuleConsentLane}
|
|
34
|
+
* 返回一个带方法的束 ⇒ `create*`。
|
|
35
|
+
*/
|
|
36
|
+
import { randomUUID } from "node:crypto";
|
|
37
|
+
import { recordFailOpen } from "./observability/fail-open.js";
|
|
38
|
+
import { confirmRuleApproval, prepareCardApproval, prepareCcImport, redeemRuleBatch, redeemRuleTicket, removePersistedRule, } from "@sema-agent/core";
|
|
39
|
+
import { buildRulePayloadHash } from "./plugins/permission-rule-store-sql.js";
|
|
40
|
+
/** 导入票的存活窗。人从「看到预览」到「按下确认」是一次交互,不是一段会话——十分钟宽到不会误伤,
|
|
41
|
+
* 窄到一张泄漏的票不会常驻。运维旋钮暂不开(没有部署形要求它可调;要开时走 config.ts 的既有姿势)。 */
|
|
42
|
+
export const RULE_IMPORT_TICKET_TTL_MS = 10 * 60_000;
|
|
43
|
+
/** 可重试那一支通告给客户端的等待秒数(`state.rule_import_retry` 的 `retryAfterSec` / `Retry-After`)。
|
|
44
|
+
* 短 —— 它对应的是一次瞬时 store 抖动,不是排队。**单一真源在本文件**:放回认领时要用它判「这张票
|
|
45
|
+
* 还能不能撑过这段窗」,HTTP 层要用它填响应,两处用同一个数才谈得上「承诺兑得出来」。 */
|
|
46
|
+
export const RULE_IMPORT_RETRY_AFTER_SEC = 2;
|
|
47
|
+
/**
|
|
48
|
+
* **全部层合计**的候选上限(codex 交叉复审 round7 [high] 二,验真后修)。
|
|
49
|
+
*
|
|
50
|
+
* 单层 16 KiB 只管住了**请求体**,管不住**下游工作量**:紧凑的合法规则(`Bash(ls)` 十个字符)能让三层
|
|
51
|
+
* 塞进上千条候选,而 core 的 `redeemRuleBatch` 是**逐候选**跑一遍「读店 + CAS 写」的串行循环 —— 一个
|
|
52
|
+
* 已鉴权的请求就能把连接池占满、把响应撑到兆字节。全局限流器默认是关的,拦不住它(一次准入就够了)。
|
|
53
|
+
*
|
|
54
|
+
* 帽数的是 **core 已经解析好的候选**(不是我们自己再数一遍 JSON):零重复解析,而且数的正好是那条串行
|
|
55
|
+
* 循环的**真实**长度。位置在**铸票之前** —— 超限就没有票,那条昂贵的循环一次都跑不起来(代价只剩一条
|
|
56
|
+
* 已铸的 pending 记录,与任何一次没去兑付的 prepare 留下的残余同形)。
|
|
57
|
+
* 200 的取值:一份人手写的 CC settings 极少超过几十条;200 给了一个数量级的余量,又把最坏工作量钉在
|
|
58
|
+
* 「几百次 SQL」而不是「几万次」。
|
|
59
|
+
*/
|
|
60
|
+
export const MAX_IMPORT_CANDIDATES = 200;
|
|
61
|
+
// ─────────────────────────────────────────────────────────────────────────────────────────────────
|
|
62
|
+
// #203 §2 —— 撤销面(`GET /v1/rules` + `DELETE /v1/rules`)的车道半场
|
|
63
|
+
// ─────────────────────────────────────────────────────────────────────────────────────────────────
|
|
64
|
+
/**
|
|
65
|
+
* scope 的 **wire 形**:`global` | `project:<root>` 的判别式串(design/203 v2 §6 F5「显式成文」)。
|
|
66
|
+
*
|
|
67
|
+
* 🔴 为什么是一个串而不是把 core 的 `{kind, root}` 对象直接上 wire:这一份表示要同时当**列举结果里的
|
|
68
|
+
* 一列**、**查询参数**、**删除请求体的一个字段**和**分页游标的一部分**。四处若各用各的形,「删掉我刚才
|
|
69
|
+
* 列出来的那一行」就变成一次跨形状翻译 —— 而那正是最容易漂的地方。判别式串在四处逐字相同,而且
|
|
70
|
+
* `project:` 前缀让「这条规则是项目内的」在一眼扫日志时就成立。
|
|
71
|
+
*
|
|
72
|
+
* `root` 里可以含冒号(路径合法),所以只切**第一个**冒号 —— 用 `split(":")` 取 [1] 会把
|
|
73
|
+
* `project:/a:b` 悄悄截成 `/a`,那是一条**放宽面**上的静默改写(更短的 root 覆盖更多 cwd)。
|
|
74
|
+
*/
|
|
75
|
+
export function serializeRuleScope(scope) {
|
|
76
|
+
return scope.kind === "global" ? "global" : `project:${scope.root}`;
|
|
77
|
+
}
|
|
78
|
+
/** {@link serializeRuleScope} 的逆。读不出形 ⇒ `undefined`(调用方 400,绝不猜一个 global 出来 ——
|
|
79
|
+
* 猜 global 会把一次「删项目内规则」的请求变成一次删不掉任何东西的 no-op,或者更糟)。 */
|
|
80
|
+
export function parseRuleScope(text) {
|
|
81
|
+
if (text === "global")
|
|
82
|
+
return { kind: "global" };
|
|
83
|
+
if (!text.startsWith("project:"))
|
|
84
|
+
return undefined;
|
|
85
|
+
const root = text.slice("project:".length);
|
|
86
|
+
// 空 root 硬拒:`{kind:"project", root:""}` 在 core 的 `pathWithinRoot` 下**包含每一个 cwd**
|
|
87
|
+
// (空前缀包含一切)——一条项目内规则就此悄悄升成全局规则(cc-import 那侧同判据,同一条 codex 发现)。
|
|
88
|
+
return root === "" ? undefined : { kind: "project", root };
|
|
89
|
+
}
|
|
90
|
+
export function createRuleConsentLane(stores, opts) {
|
|
91
|
+
const deps = { provider: stores.provider, approvals: stores.approvals };
|
|
92
|
+
const ticketTtlMs = opts?.ticketTtlMs ?? RULE_IMPORT_TICKET_TTL_MS;
|
|
93
|
+
const newTicketId = opts?.newTicketId ?? (() => randomUUID());
|
|
94
|
+
return {
|
|
95
|
+
async persistCardRule(input) {
|
|
96
|
+
// 🔴 候选**由引擎铸**(core `prepareCardApproval` 不收调用方候选;那条入口上「写自己的规则」
|
|
97
|
+
// 结构上不可拼写)。本层只把「人选了哪一个」翻译成下标。
|
|
98
|
+
const prepared = await prepareCardApproval({
|
|
99
|
+
principal: input.principal,
|
|
100
|
+
toolName: input.toolName,
|
|
101
|
+
command: input.command,
|
|
102
|
+
...(input.toolCallId !== undefined ? { toolCallId: input.toolCallId } : {}),
|
|
103
|
+
...(input.boundInputHash !== undefined ? { boundInputHash: input.boundInputHash } : {}),
|
|
104
|
+
...(input.scope !== undefined ? { scope: input.scope } : {}),
|
|
105
|
+
deps,
|
|
106
|
+
});
|
|
107
|
+
if (prepared === undefined) {
|
|
108
|
+
return { ok: false, reason: "no-candidates", detail: "the rule lane cannot speak for this command (compound / redirection / unsupported tool)" };
|
|
109
|
+
}
|
|
110
|
+
const index = prepared.candidates.findIndex((c) => c.rule === input.ruleText);
|
|
111
|
+
const ticket = index < 0 ? undefined : prepared.tickets[index];
|
|
112
|
+
if (index < 0 || ticket === undefined) {
|
|
113
|
+
return { ok: false, reason: "unknown-candidate", detail: "the chosen rule text is not one the engine minted for this command" };
|
|
114
|
+
}
|
|
115
|
+
const confirmed = await confirmRuleApproval({ approvalId: prepared.approvalId, principal: input.principal, selectedCandidate: index, deps });
|
|
116
|
+
if (!confirmed.ok)
|
|
117
|
+
return { ok: false, reason: "confirm-refused", detail: confirmed.reason };
|
|
118
|
+
const redeemed = await redeemRuleTicket({ ticket, principal: input.principal, deps });
|
|
119
|
+
if (redeemed.status !== "redeemed")
|
|
120
|
+
return { ok: false, reason: "redeem-refused", detail: redeemed.reason };
|
|
121
|
+
return { ok: true, rule: redeemed.rule, rev: redeemed.rev, alreadyRedeemed: redeemed.alreadyRedeemed };
|
|
122
|
+
},
|
|
123
|
+
async prepareImport(principal, layers) {
|
|
124
|
+
// core 的 `CcImportLayer.readFile` 是一条**注入的**读口 —— 云端部署没有用户的文件系统,内容由
|
|
125
|
+
// 调用方交上来(这是本仓的部署形,不是绕过:读的仍然只是 allow 桶,拒/问桶是收紧方向、另有通道)。
|
|
126
|
+
const ccLayers = layers.map((l) => ({
|
|
127
|
+
layer: l.layer,
|
|
128
|
+
path: l.path,
|
|
129
|
+
root: l.root,
|
|
130
|
+
readFile: async () => l.content,
|
|
131
|
+
}));
|
|
132
|
+
const { preview, approvalId } = await prepareCcImport({ layers: ccLayers, principal, deps });
|
|
133
|
+
// 帽在**铸票之前**(见 {@link MAX_IMPORT_CANDIDATES}):没有票 ⇒ 那条逐候选的串行 SQL 循环跑不起来。
|
|
134
|
+
if (preview.candidates.length > MAX_IMPORT_CANDIDATES) {
|
|
135
|
+
// core 在**返回预览之前**就把 pending 记录落了盘 ⇒ 这次不做的导入会留一条永远没人要的行。
|
|
136
|
+
// 收掉它(codex round8 [medium]:被拒的 prepare 不该在共享库里长东西)。best-effort:收不掉
|
|
137
|
+
// 也只是留一条 pending 孤儿,不影响任何判决 —— 但**绝不**因此把 413 变成 500。
|
|
138
|
+
try {
|
|
139
|
+
await stores.approvals.discardPendingRecord?.(approvalId);
|
|
140
|
+
}
|
|
141
|
+
catch (err) {
|
|
142
|
+
recordFailOpen("server.rules.rejected-prepare-row-left", err instanceof Error ? err.message : String(err));
|
|
143
|
+
}
|
|
144
|
+
return { ok: false, reason: "too-many-candidates", candidates: preview.candidates.length };
|
|
145
|
+
}
|
|
146
|
+
const minted = await stores.tickets.mint({ ticketId: newTicketId(), principal, approvalId, candidates: preview.candidates, ttlMs: ticketTtlMs });
|
|
147
|
+
return { ok: true, preview, ticket: minted.ticketId, expiresAtMs: minted.expiresAtMs };
|
|
148
|
+
},
|
|
149
|
+
async redeemImport(principal, ticket) {
|
|
150
|
+
// ①②③ 一次原子**认领**同时回答(principal 绑定 / 未过期 / 未认领);四类拒绝在 wire 面同形。
|
|
151
|
+
const consumed = await stores.tickets.consume(ticket, principal);
|
|
152
|
+
if (!consumed.ok)
|
|
153
|
+
return { ok: false, reason: "ticket-unusable", detail: consumed.reason };
|
|
154
|
+
// 🔴 codex round1 [high] 一(验真后修):认领**之后**的每一步都可能失败,而旧形把认领当终局 ⇒
|
|
155
|
+
// 一次 store 抖动就永久烧掉这张票,客户端拿到与「票不存在」同形的 404,重试无门。
|
|
156
|
+
// 处置:裁定性拒绝(载荷被篡改 / 记录不存在)**留着**认领(重试不改判);不确定/瞬时失败**放回**
|
|
157
|
+
// 认领并标 `retryable`。放回不打开双兑付的门(认领仍是单持有者),兑付腿本身按 core 的设计幂等。
|
|
158
|
+
// 🔴 codex 交叉复审 round3 [high] 一(验真后修):返回值是**「认领确实放回去了吗」**,不是 void。
|
|
159
|
+
// 旧形无条件回 `retryable: true`,而释放本身也可能失败(同一场 store 故障往往两跳一起挂)——
|
|
160
|
+
// 那就变成对客户端**撒谎**:它拿着一张其实已经烧掉的票去重试,只会收到 404,人的确认永久丢失。
|
|
161
|
+
// 判据必须是「释放**确认**成功」;没确认就不许承诺重试(如实按不可重试报,四类同形 404)。
|
|
162
|
+
const releaseClaim = async () => {
|
|
163
|
+
try {
|
|
164
|
+
return await stores.tickets.release(ticket, principal, RULE_IMPORT_RETRY_AFTER_SEC * 1000);
|
|
165
|
+
}
|
|
166
|
+
catch (err) {
|
|
167
|
+
// 放不回去 = 回到「认领即烧票」的旧形(属主这一轮不能重试,票随 TTL 消失)。**不**把一次
|
|
168
|
+
// 已经失败的请求再变成一次抛错;但也**不**静默 —— 登记的 F 类兜底,理由逐字见 tag 词表。
|
|
169
|
+
recordFailOpen("server.rules.ticket-claim-release-failed", err instanceof Error ? err.message : String(err));
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
/** 「不确定」那一支的统一收口:**释放确认成功**才承诺可重试。 */
|
|
174
|
+
const indeterminate = async (detail) => {
|
|
175
|
+
// 放回之后这张票至少要能撑过我们通告的等待窗 —— 撑不过就不承诺重试(见 store 侧 `release` 头注)。
|
|
176
|
+
const released = await releaseClaim();
|
|
177
|
+
return { ok: false, reason: "record-unusable", detail, ...(released ? { retryable: true } : {}) };
|
|
178
|
+
};
|
|
179
|
+
try {
|
|
180
|
+
// ④ 载荷绑定:记录上的候选集必须与铸票时刻的快照同摘要。
|
|
181
|
+
const record = await stores.approvals.get(consumed.approvalId);
|
|
182
|
+
if (record === undefined)
|
|
183
|
+
return { ok: false, reason: "record-unusable", detail: "the approval record behind this ticket is gone" };
|
|
184
|
+
if (buildRulePayloadHash(record.candidates) !== consumed.payloadHash) {
|
|
185
|
+
return { ok: false, reason: "payload-mismatch", detail: "the approval record's candidates changed after the ticket was minted" };
|
|
186
|
+
}
|
|
187
|
+
// 🔴 确认是**一次性状态跃迁**,不是每次兑付都要过的门:重试一张**放回过认领**的票时,记录
|
|
188
|
+
// 早已不是 `pending` 了(core 的崩溃序:`redeemRuleTicket` 在调用写面**之前**就把记录推到
|
|
189
|
+
// `redeemed`,所以哪怕一个候选都没落地,状态也已经变了)。对已确认的记录再调一次
|
|
190
|
+
// `confirmRuleApproval` 会拿到 `not_pending` —— 把它当拒绝就等于「放回了认领却永远兑不动」,
|
|
191
|
+
// round1/round3/round5 那一串修复到此全部白做。判据用记录**自己的状态**,不是猜错误串。
|
|
192
|
+
if (record.state === "pending") {
|
|
193
|
+
const confirmed = await confirmRuleApproval({ approvalId: consumed.approvalId, principal, deps });
|
|
194
|
+
if (!confirmed.ok) {
|
|
195
|
+
// `conflict` = CAS 输给了一个并发写者(瞬时);其余是裁定(记录不存在/状态不对)。
|
|
196
|
+
if (confirmed.reason === "conflict")
|
|
197
|
+
return await indeterminate(confirmed.reason);
|
|
198
|
+
return { ok: false, reason: "record-unusable", detail: confirmed.reason };
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
const result = await redeemRuleBatch({ approvalId: consumed.approvalId, principal, deps });
|
|
202
|
+
if ("status" in result) {
|
|
203
|
+
// core 的 refused 里既有裁定也有瞬时(OCC 用尽 / 店读不出来)——分不清就按**可重试**放回:
|
|
204
|
+
// 兑付是幂等的,多放一次认领最坏是多跑一遍同样的重放;烧掉一张真票是不可逆的。
|
|
205
|
+
return await indeterminate(result.reason);
|
|
206
|
+
}
|
|
207
|
+
// 🔴 codex 交叉复审 round5 [high] 二(验真后修):core 的 `redeemRuleBatch` 把**逐候选**的失败
|
|
208
|
+
// 记在 `skippedAtRedeem` 里,顶层照样返回一个 ImportResult —— 只判顶层 `status` 会把「一半没落地」
|
|
209
|
+
// 当成功回 200,而认领已经烧掉,那一半再也补不回来。
|
|
210
|
+
// 为什么这条车道可以把「非空 skippedAtRedeem」一律当**不确定**:候选在 `prepareCcImport` 那一步
|
|
211
|
+
// 就已经全过了同一个验证器(过不了的进 `preview.skipped`,压根不进记录),所以兑付期的逐候选拒绝
|
|
212
|
+
// 实际只剩「店读不出/写不进 / OCC 用尽 / 隔离栅栏」这类**瞬时或异常**成因。重放是幂等的
|
|
213
|
+
// (记录里存着每个候选已铸的 dot),所以放回认领让人重试,代价上界只是再跑一遍同样的重放。
|
|
214
|
+
// 残留(如实登记):真出现一条**永久**被拒的候选时,客户端会一直拿到 503 直到票到期(10 分钟)——
|
|
215
|
+
// 有界、且不破坏任何已落地的规则;要更好得让 core 把逐候选拒绝分成瞬时/裁定两类,已列后续件。
|
|
216
|
+
if (result.skippedAtRedeem.length > 0) {
|
|
217
|
+
return await indeterminate(`partial import: ${result.skippedAtRedeem.length} candidate(s) did not land (${result.skippedAtRedeem.map((x) => x.reason).join("; ")})`);
|
|
218
|
+
}
|
|
219
|
+
return { ok: true, result };
|
|
220
|
+
}
|
|
221
|
+
catch (err) {
|
|
222
|
+
return await indeterminate(err instanceof Error ? err.message : String(err));
|
|
223
|
+
}
|
|
224
|
+
},
|
|
225
|
+
async listRules(principal) {
|
|
226
|
+
// `list()` 已经把墓碑折算掉(`applyTombstones`)⇒ 这里拿到的就是**活着**的规则,治理面看到的
|
|
227
|
+
// 与引擎放行时看到的是同一份。空桶 = 空表 + rev 0,不是错误(「这个人没有规则」是合法状态)。
|
|
228
|
+
const stored = await stores.provider.forPrincipal(principal).list();
|
|
229
|
+
const rows = stored.rules.map((r) => ({ rule: r.rule, scope: serializeRuleScope(r.scope), tool: r.tool, match: r.match, command: r.command, adds: r.adds }));
|
|
230
|
+
rows.sort((a, b) => (a.scope < b.scope ? -1 : a.scope > b.scope ? 1 : a.rule < b.rule ? -1 : a.rule > b.rule ? 1 : 0));
|
|
231
|
+
return { rows, rev: stored.rev };
|
|
232
|
+
},
|
|
233
|
+
async removeRule(input) {
|
|
234
|
+
// 🔴 一行也不自己写:OCC 重试、墓碑铸造、`stillLive` 的读回全在 core 那条原语里,而它的语义
|
|
235
|
+
// (add-wins、observed-remove、失败必须由读回确认「什么都没写」)是 core 的单一属主。
|
|
236
|
+
return await removePersistedRule({ rule: input.rule, scope: input.scope, principal: input.principal, provider: stores.provider });
|
|
237
|
+
},
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
//# sourceMappingURL=rules-consent.js.map
|
package/dist/run-local.js
CHANGED
|
@@ -26,6 +26,16 @@
|
|
|
26
26
|
* allow-all arm and a gated `ask` is answered by {@link createLocalApprover} — the TTY human, or a
|
|
27
27
|
* fail-closed deny — instead of parking a checkpoint nobody will ever redeem.
|
|
28
28
|
*
|
|
29
|
+
* 🔴 **design/201 的 permissionMode → shellGate 翻译表 SCOPED-OUT 本腿**(设计稿 §5,显式裁定而非欠账)。
|
|
30
|
+
* 这条腿的 operator 就是亲手起这个进程的用户,它**没有 permission mode 这个概念**(一次性 CLI 收不到
|
|
31
|
+
* 客户端表态),今昔都落 core 的缺省 `shellGate: off`。给它硬塞一个 classify 档,在没有 HITL 面的形态下
|
|
32
|
+
* 等于把每一次 shell 调用送进 auto-deny —— 那不是加固,是废掉 run-local。部署治理段(上一段)照旧全接:
|
|
33
|
+
* `MANUAL_MODE_SHELL_GATE` 想要门,operator 在自己的 `.env` 里说一句就有。同族的另一条 scoped-out 是
|
|
34
|
+
* parked-revive 赎回腿(body 全盲,core 的 `InheritedGate` 以 `max(seed, live)` 兜底,自洽)。
|
|
35
|
+
* 「三腿不共享翻译点」是设计,不是漏接:名册对账钉在
|
|
36
|
+
* `test/operator-knob-client-posture-matrix.test.ts`(`shellGateForMode` 已并入构造点名册,第四条腿哪天
|
|
37
|
+
* 接上它必须同时挂矩阵腿)。
|
|
38
|
+
*
|
|
29
39
|
* Usage: run-local "<objective>" [--root <dir>] [--json] [--scenario <name>]
|
|
30
40
|
*
|
|
31
41
|
* `main()` is a thin wrapper over the testable {@link runLocal}, which takes argv + injectable
|
|
@@ -38,7 +48,7 @@ import { realpathSync } from "node:fs";
|
|
|
38
48
|
import { createInterface } from "node:readline/promises";
|
|
39
49
|
import { fileURLToPath } from "node:url";
|
|
40
50
|
import { loadRemoteExec } from "@sema-agent/registry-core/node";
|
|
41
|
-
import { Runner, uuidv7, parseModelMention, FileStorageBackend, NodeExecutionEnv, NodeLspManager, TtlSessionStore, CenterPromptSource, FilePromptArtifactStore, FilePromptSourceStateStore, MemoryPromptArtifactStore, MemoryPromptSourceStateStore } from "@sema-agent/core";
|
|
51
|
+
import { Runner, uuidv7, parseModelMention, AdoptionError, FileStorageBackend, NodeExecutionEnv, NodeLspManager, TtlSessionStore, CenterPromptSource, FilePromptArtifactStore, FilePromptSourceStateStore, MemoryPromptArtifactStore, MemoryPromptSourceStateStore } from "@sema-agent/core";
|
|
42
52
|
import { hasOperatorGateIntent } from "./approval.js";
|
|
43
53
|
import { createBrain } from "./brain.js";
|
|
44
54
|
import { assertGuardPatternsUsable, buildOnlySensitiveBaselineWarning, createApprovalBaselinePolicy, createDeploymentGovernanceInputs, } from "./deployment-governance.js";
|
|
@@ -60,7 +70,10 @@ import { pickHandsRunner, withoutExecutionEnv } from "./capabilities/hands-lane.
|
|
|
60
70
|
import { HttpError } from "./security.js";
|
|
61
71
|
import { memoryEngineBackendFor, memorySpecForRequest } from "./memory-scope.js";
|
|
62
72
|
import { createMemorySyncRunner, createMemorySyncTransport } from "./memory-sync-client.js";
|
|
63
|
-
import { buildPricing, cappedCeiling } from "./budget.js";
|
|
73
|
+
import { buildPricing, cappedCeiling, createTracer } from "./budget.js";
|
|
74
|
+
import { createSharedRunnerDeps } from "./boot/runner-deps.js";
|
|
75
|
+
import { createOrgMemoryAdmissionWiring } from "./boot/org-memory.js";
|
|
76
|
+
import { createPermissionDeniedMeter } from "./observability/tool-trace.js";
|
|
64
77
|
import { createKeyResolver } from "./key-resolver.js";
|
|
65
78
|
import { createLogger } from "./observability/logger.js";
|
|
66
79
|
import { createMetrics } from "./observability/metrics.js";
|
|
@@ -389,11 +402,35 @@ export async function runLocal(argv, deps = {}) {
|
|
|
389
402
|
// concurrent instance throws in the constructor — surface a clear message instead of a stack.
|
|
390
403
|
let fileBackend;
|
|
391
404
|
try {
|
|
392
|
-
fileBackend = new FileStorageBackend({
|
|
405
|
+
fileBackend = new FileStorageBackend({
|
|
406
|
+
// 🔴 腐读披露座(codex 复审 R1-medium,#205 件3 连带):本批把 `sessionPolicyStore` 接进了两只
|
|
407
|
+
// Runner,而 core 对**坏掉的**策略文件是 documented fail-open —— 读不出来按「没有规则」处理。
|
|
408
|
+
// 这条 fail-open 本身是 core 的裁定(抛会让整条任务死、也会自锁修复写),但它**不许无声**
|
|
409
|
+
// (#157 安全轴纪律)。一个座覆盖 core 今天转发的三张脸,后果**各不相同**,所以文案只陈述共同事实
|
|
410
|
+
// (「一次持久读被当成缺席」)并把后果按脸分列,不下统一结论(codex R2-medium:原文案把「少了
|
|
411
|
+
// 会话规则这一层」写成「整个任务无约束」——那会误导事故定级,而且对另外两张脸根本不成立)。
|
|
412
|
+
// ENOENT 不触发(那是真缺席)。
|
|
413
|
+
root,
|
|
414
|
+
onCorruptRead: (info) => logger.warn("file_store_corrupt_read", {
|
|
415
|
+
path: info.path,
|
|
416
|
+
reason: info.reason,
|
|
417
|
+
...(info.sessionId !== undefined ? { sessionId: info.sessionId } : {}),
|
|
418
|
+
...(info.principal !== undefined ? { principal: info.principal } : {}),
|
|
419
|
+
note: "a durable read was treated as ABSENT because the bytes were unreadable (never a plain ENOENT). " +
|
|
420
|
+
"Consequence depends on which store read it: session-policy = this run lost its SUBTRACTIVE session-rule " +
|
|
421
|
+
"layer (deployment policy / hooks / shell gate still applied); session repo = a listing silently omitted " +
|
|
422
|
+
"rows; file-snapshot = a snapshot or scope read as missing. Inspect the named path before deciding.",
|
|
423
|
+
}),
|
|
424
|
+
});
|
|
393
425
|
}
|
|
394
426
|
catch (e) {
|
|
395
427
|
printErr(`cannot open local data dir ${root}: ${e instanceof Error ? e.message : String(e)}`);
|
|
396
|
-
|
|
428
|
+
// 🔴 core 5.23.0([3372] 件④):这个构造器的第二个拒绝面是 design/183 的 I6 adoption boot 门。
|
|
429
|
+
// 那一类失败**不能**再补「换一个 --root」这句 —— 换根 = 丢下一个迁移到一半的数据根(与
|
|
430
|
+
// `plugins/store-backend.ts` 的 local 臂同案同判)。core 的拒绝句已自带出路,上面那行已如实打印。
|
|
431
|
+
if (!(e instanceof AdoptionError)) {
|
|
432
|
+
printErr("(another run-local/engine instance may own it; finish it first, or use a different --root)");
|
|
433
|
+
}
|
|
397
434
|
return 2;
|
|
398
435
|
}
|
|
399
436
|
const sessionStore = fileBackend.sessionStore;
|
|
@@ -496,12 +533,74 @@ export async function runLocal(argv, deps = {}) {
|
|
|
496
533
|
...(ctx.classification !== undefined ? { classification: ctx.classification } : {}),
|
|
497
534
|
err: String(err),
|
|
498
535
|
});
|
|
499
|
-
|
|
536
|
+
// ⚠️ `createOrgMemoryAdmissionWiring` 有一条 **throw** 路径:`MEMORY_ORG_DIRECTORY_JSON` 形坏时
|
|
537
|
+
// `parseOrgDirectoryStatic` 启动期炸(config 层按 A4 分层刻意不校验它)。server 那边炸=进程拒启,
|
|
538
|
+
// 正确;但本腿的既定 UX 是「doctor 文案 + 退 2」(同 assertGuardPatternsUsable / remote-exec-file-invalid
|
|
539
|
+
// 两处),把栈抛给在终端里敲命令的人是回归。另一条 throw(多租户 org 键 + 无目录源)在本腿**结构上
|
|
540
|
+
// 不可达**——上面已把 `REQUIRE_PRINCIPAL` 强制成 false。
|
|
541
|
+
let orgMemoryAdmission;
|
|
542
|
+
try {
|
|
543
|
+
orgMemoryAdmission = createOrgMemoryAdmissionWiring({ config, logger, metrics });
|
|
544
|
+
}
|
|
545
|
+
catch (e) {
|
|
546
|
+
printErr(`MEMORY_ORG_DIRECTORY_JSON is invalid: ${e instanceof Error ? e.message : String(e)}`);
|
|
547
|
+
printErr(`(local config root: ${root} — fix it in ${join(root, ".env")} or your shell env, or unset it)`);
|
|
548
|
+
await fileBackend.dispose();
|
|
549
|
+
return 2;
|
|
550
|
+
}
|
|
551
|
+
/**
|
|
552
|
+
* [3397]①②③ / #205 件3 —— run-local 的两份 Runner deps 走 **server 同一只共享基座**
|
|
553
|
+
* (`createSharedRunnerDeps`,main.ts:432 的同款展开)。
|
|
554
|
+
*
|
|
555
|
+
* 为什么必须收编:这两份 deps 此前各自手写同源键,而漏配**没有任何编译期或运行期信号** —— 复扫亲验
|
|
556
|
+
* 到三处真漂移:`toolResultStore` 只在主 runner(委派子代的 `ReadToolResult(ref)` 恒空,正是 core
|
|
557
|
+
* 1.219 修过的那个病在 sub 腿复发)、`config.tiers` **全文件零接线**(而上面的 `applyEffective` 真在
|
|
558
|
+
* 填它 ⇒ tier 词 / CC 别名在这条腿上解不出来)、`hooks` / `tracer` / `sessionPolicyStore` 两处都没有。
|
|
559
|
+
* 基座是一份,新增共享键不可能只挂一半。
|
|
560
|
+
*
|
|
561
|
+
* ── 逐参裁定(local 车道**真差异**保留,不为收编硬造 stub)───────────────────────────────────
|
|
562
|
+
* · `brain` / `pricing` / `promptSource` / `executionEnvFactory` / `lspManager`:本腿真有,直供。
|
|
563
|
+
* · `config`(基座据以取 models / roles / **tiers** / usageWindows):本腿真有 —— tiers 正是本件修的漏。
|
|
564
|
+
* · `tracer`:`createTracer(metrics)` 真形。配额/舰队/prompt-manifest 四个可选跟踪器是 server 面的
|
|
565
|
+
* 设施,本腿没有 ⇒ 不传(不是 stub:那几个参数本就可选,缺席即该轴不记账)。
|
|
566
|
+
* · `deploymentHooks`:`createPermissionDeniedMeter(metrics)` —— main.ts 在**没有** toolTracer 时的
|
|
567
|
+
* 同一只值(tool-trace 落库面是 server 侧设施,一次性 CLI 无处落)。
|
|
568
|
+
* · `sessionPolicyStore`:`fileBackend.sessionPolicyStore`(core 的 File 形,其文档原话就是「本地部署
|
|
569
|
+
* 要让会话规则跨重启存活就接这里」)。E6 规则是 subtract-only ⇒ 方向只会更紧;同一个数据根上的
|
|
570
|
+
* 本地 HTTP 服务写下的规则,这条腿本就该认。
|
|
571
|
+
* · `usageWindowStore`:与 `config.usageWindows` **成对**(main.ts 同规:两键同真同假)—— 配了窗才接
|
|
572
|
+
* File 账本,没配窗时接一个空账本只会让 core 以为这条腿有治理窗。
|
|
573
|
+
* · `toolResultStore`:`fileBackend.toolResultStore`(本件的主修点:现在主/子同源)。
|
|
574
|
+
* · `orgMemoryAdmission`:`createOrgMemoryAdmissionWiring(...)` 真形。本腿 `REQUIRE_PRINCIPAL` 被强制
|
|
575
|
+
* false(见上),所以那条多租户拒启探测结构上不可达;产物在无 center / 无 `MEMORY_ORG_DIRECTORY_JSON`
|
|
576
|
+
* 时是「resolver 缺席 + deployment 自证集」——即 `MEMORY_SCOPE=org:x` 的本地用户从此走得通自证通道。
|
|
577
|
+
* · `backgroundAgentStore` / `mailboxStore` / `rosterStore`:**undefined** —— core 的 `FileStorageBackend`
|
|
578
|
+
* 根本不供这三个店(durable 后台子代 / tier-3 懒复活 / 持久名册都是 server+SQL 面的设施)。造个内存
|
|
579
|
+
* 冒牌货等于让 core 以为「具名子代能跨进程复活」,而一次性 CLI 的进程下一秒就没了。
|
|
580
|
+
* · `sharedMemoryStores`:**undefined** —— 供给面只有 SQL 形(`store-backend.ts` 明写:单机造 file 形
|
|
581
|
+
* 等于给一个人的部署做「团队共享」,能力面会因此说谎)。
|
|
582
|
+
*/
|
|
583
|
+
const sharedRunnerDeps = createSharedRunnerDeps({
|
|
584
|
+
config,
|
|
500
585
|
brain,
|
|
501
|
-
models: config.models,
|
|
502
|
-
roles: config.roles,
|
|
503
586
|
pricing,
|
|
587
|
+
tracer: createTracer(metrics),
|
|
504
588
|
promptSource,
|
|
589
|
+
executionEnvFactory,
|
|
590
|
+
lspManager,
|
|
591
|
+
backgroundAgentStore: undefined,
|
|
592
|
+
mailboxStore: undefined,
|
|
593
|
+
rosterStore: undefined,
|
|
594
|
+
deploymentHooks: createPermissionDeniedMeter(metrics),
|
|
595
|
+
toolResultStore: fileBackend.toolResultStore,
|
|
596
|
+
sessionPolicyStore: fileBackend.sessionPolicyStore,
|
|
597
|
+
usageWindowStore: config.usageWindows ? fileBackend.usageWindowStore : undefined,
|
|
598
|
+
orgMemoryAdmission,
|
|
599
|
+
sharedMemoryStores: undefined,
|
|
600
|
+
});
|
|
601
|
+
const runnerDeps = {
|
|
602
|
+
...sharedRunnerDeps,
|
|
603
|
+
// ── 以下为主 runner 的差异键(不在共享基座;逐个有因)──────────────────────────────────────
|
|
505
604
|
// [931]① clay 拍:run-local=Sema 品牌本地形态,commit 尾注接 Sema 署名(core 1.300 缺省已翻转不署)。
|
|
506
605
|
hands: { commitCoAuthor: "Sema <noreply@vivi-ai.com>" },
|
|
507
606
|
sessionStore,
|
|
@@ -512,11 +611,6 @@ export async function runLocal(argv, deps = {}) {
|
|
|
512
611
|
// stderr)后其余告警——compaction / prompt-cache / mcp / memory / hook 的降级——也不再无声。
|
|
513
612
|
onError: engineOnError,
|
|
514
613
|
onAsk,
|
|
515
|
-
// core 1.219 (dogfood: "ReadToolResult(ref) 恒空"): durable tool-result refs on the TOC CLI
|
|
516
|
-
// lane too — offloaded full text survives a process restart (one file per ref under the data root).
|
|
517
|
-
toolResultStore: fileBackend.toolResultStore,
|
|
518
|
-
...(executionEnvFactory ? { executionEnvFactory } : {}),
|
|
519
|
-
...(lspManager ? { lspManager } : {}),
|
|
520
614
|
// design/113 C4: run-local IS the single-user host lane (cwd = --workspace ?? process.cwd()) — the CLI path where
|
|
521
615
|
// CLAUDE.md project-awareness most belongs. Wire it unless explicitly disabled.
|
|
522
616
|
...(config.projectMemoryEnabled
|
|
@@ -530,7 +624,12 @@ export async function runLocal(argv, deps = {}) {
|
|
|
530
624
|
// [1367]① fork server half(main.ts subRunner 同款):ForkRoutingSessionStore——fork/resume 形
|
|
531
625
|
// (requireExisting)→ host 文件店优先(core 1.350 hostSessionFork 在宿主店 fork,拆店=响亮
|
|
532
626
|
// resume.session_not_found),普通子任务 → 私有 TTL 店(throwaway 姿势保留)。
|
|
533
|
-
|
|
627
|
+
// #205 件3:与主 runner **同一只**共享基座展开(上面那份 `sharedRunnerDeps`)——此前这里是一行手写
|
|
628
|
+
// 字面量,`toolResultStore`/`tiers`/`hooks`/`tracer`/`sessionPolicyStore` 全漏,子代 lane 静默降级。
|
|
629
|
+
// 差异键只剩会话店与两个座位;**checkpointStore 有意不给**(main.ts 的 sub 腿给它是为了子代 durable
|
|
630
|
+
// park,而本腿的 park 设施结构上不存在:一次性 CLI 没有 `/decide` 赎回腿,gated ask 由 TTY 真人当场答
|
|
631
|
+
// ——上面 `buildOnlySensitiveBaselineWarning` 传 `durableEnabled: false` 记的就是同一件事实)。
|
|
632
|
+
const subRunnerDeps = { ...sharedRunnerDeps, sessionStore: new ForkRoutingSessionStore(sessionStore, new TtlSessionStore({ defaultTtlDays: 1 / 24 })), onError: engineOnError, onAsk };
|
|
534
633
|
const subRunner = new Runner(subRunnerDeps);
|
|
535
634
|
// #196:无手孪生一对 —— **mirrors main.ts**(那边是 `handslessRunner` / `handslessSubRunner` 两只,同 deps
|
|
536
635
|
// 摘掉 executionEnvFactory)。run-local 是同形装配点,漏这一对 = 本地 lane 上 scan/council/team 的最终
|
|
@@ -619,6 +718,14 @@ export async function runLocal(argv, deps = {}) {
|
|
|
619
718
|
// design/129: run-local IS the pure TOC lane → session-scoped background children
|
|
620
719
|
// (CC Backgrounded semantics; parity with the server's single-user posture).
|
|
621
720
|
backgroundScope: "session",
|
|
721
|
+
// [1909]⑧(core 5.23.0 `TaskSpec.oneShot`,#205 件1 的**本腿**半场,codex R2-high 抓获):这条腿
|
|
722
|
+
// 是**定义上**的一次性提交 —— 本文件头注第一句就是「跑完一条任务、打印 TaskResult、退出」,
|
|
723
|
+
// `runLocal` 返回后 Runner 全被 dispose、进程即散。缺席这个键时,core 会照默认(交互态)给后台
|
|
724
|
+
// 委派 / run_workflow 发「结束回合,你会被通知」的回执,而这里**没有下一个回合**能接住那条通知 ——
|
|
725
|
+
// 那正是本键被造出来要消灭的丢结果病族(BGB drilldown case 2)。
|
|
726
|
+
// 与 HTTP 腿的差别在**来源**而不在语义:那边是调用方表态(缺席不写键),这边是**进程形态的事实**,
|
|
727
|
+
// 所以无条件为真,不看任何 body/表态(本腿根本收不到客户端表态,同 permissionMode 的 scoped-out 理由)。
|
|
728
|
+
oneShot: true,
|
|
622
729
|
principal: scope, // stable local memory/identity scope (single-user TOC; --user overrides)
|
|
623
730
|
// design/138 S1: enable the memory engine for this run (spec.memory is core's per-task activation half of
|
|
624
731
|
// the deps.memoryBackend switch). Scope = the --user identity (default "local", the single TOC user).
|
|
@@ -319,9 +319,15 @@ export function applyRuntimeGovernance(base, governance) {
|
|
|
319
319
|
const candidate = overrides.shellGate !== undefined && SHELL_GATE_RANK[overrides.shellGate] >= SHELL_GATE_RANK[governance.manualModeShellGate]
|
|
320
320
|
? overrides.shellGate
|
|
321
321
|
: governance.manualModeShellGate;
|
|
322
|
-
// base 已更严 ⇒ 省略,让 base 原样保留(省略=行为等价,不 throw)
|
|
323
|
-
//
|
|
324
|
-
//
|
|
322
|
+
// base 已更严 ⇒ 省略,让 base 原样保留(省略=行为等价,不 throw)。**仍然装配上不可达、纯防御**
|
|
323
|
+
// ——但论证已随 design/201 换了一条:
|
|
324
|
+
// · 前提(改):resolveSpec 的 spec 字面量**现在会写 shellGate** —— 显式 permissionMode 的翻译档
|
|
325
|
+
// (`shellGateForMode`,阶段④,governance 之前),所以 base.shellGate 不再恒缺席。
|
|
326
|
+
// · 结论(不变):这一支照旧不可达。翻译表的**上限是 classify**(rank 1),而能走到这里的 candidate
|
|
327
|
+
// 下限也是 classify(旋钮词表只有 classify/always,且 candidate 已取过与 autonomy 派生值的较大者)
|
|
328
|
+
// ⇒ candidate ≥ base 恒成立。SUP 路由姿态(resolve-spec 的 supPostureOverrides)是 governance 之
|
|
329
|
+
// **后**才叠的,够不到这里的 base。
|
|
330
|
+
// 若将来有人给 base 种上更严的值(例:翻译表新增一个 ⇒ always 的模式词),这里省略而不是让
|
|
325
331
|
// tightenTaskSpec 因「override 更松」throw——candidate 已是 autonomy 派生值与旋钮的较大者,省略它不会
|
|
326
332
|
// 丢掉 autonomy 那一半(autonomy 只产 "always",即最高 rank,永远不会落进这一支)。
|
|
327
333
|
if (SHELL_GATE_RANK[candidate] >= SHELL_GATE_RANK[base.shellGate ?? "off"])
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* design/177 v2/F4 —— shared-memory 读面的**授权折叠**:principal → 可读 org scope 集。
|
|
3
|
+
*
|
|
4
|
+
* 为什么复用 {@link OrgMemoryDirectory} 而不是另立一张表:「谁属于 org:acme」在一个进程里必须只有一个
|
|
5
|
+
* 答案。core 的记忆准入 seam、`/v1/memory/*` 的属主门、以及这里的共享库读面读的是**同一个目录实例**
|
|
6
|
+
* (`createOrgMemoryAdmissionWiring` 的产物),因此共享同一份 TTL 缓存 / 退避窗 / gen 高水位——吊销窗内
|
|
7
|
+
* 三面不会公开分歧。各建各的目录 = 三个答案,那正是 design/170 §7 收编要消灭的东西。
|
|
8
|
+
*
|
|
9
|
+
* 🔴 判别联合原样透传(N3/C5 裁定的下游一致性):目录说 `unavailable` 时**绝不**折成「你不属于任何
|
|
10
|
+
* org」。前者在模型面渲染成「连接不可用,恢复前拒读」(可重试),后者渲染成「本会话没有连接任何记忆库」
|
|
11
|
+
* (终局,模型就此放弃)。两句话是相反的指令,把前者塌进后者是一次静默的 fail-open。
|
|
12
|
+
*
|
|
13
|
+
* 🔴 deployment-origin scope 的多租户切断:operator 在配置里自证的 org scope(`MEMORY_SCOPE=org:x`、
|
|
14
|
+
* 单用户部署的 projects 登记簿)只在**无租户边界**的部署里授予。多租户部署(`requirePrincipal`)下,
|
|
15
|
+
* 一条部署级声明会同时授予每一个 principal —— 那是跨租户读,不是配置便利。装配点因此按部署形态把这个
|
|
16
|
+
* 集合择净后再传进来(见 main.ts 的构造行),本模块只忠实使用它:哪些 scope 算「部署自证」是装配点的
|
|
17
|
+
* 判断,不是这里的。
|
|
18
|
+
*/
|
|
19
|
+
import type { SharedMemoryScopeAuthorizer } from "./plugins/shared-memory-store-sql.js";
|
|
20
|
+
import type { OrgMemoryDirectory } from "./org-memory-admission.js";
|
|
21
|
+
export interface SharedMemoryScopeAuthorizerOptions {
|
|
22
|
+
/** 目录实例。**必须**与 core 准入 seam / memory-policy 面同一只(见头注)。 */
|
|
23
|
+
directory: OrgMemoryDirectory;
|
|
24
|
+
/** 部署自证的 org scope(装配点已按部署形态择净;多租户下应为空)。 */
|
|
25
|
+
deploymentScopes?: readonly string[];
|
|
26
|
+
}
|
|
27
|
+
/** 活对象(闭包持有目录与集合)⇒ `create*`(CLAUDE.md 工厂命名律)。 */
|
|
28
|
+
export declare function createSharedMemoryScopeAuthorizer(opts: SharedMemoryScopeAuthorizerOptions): SharedMemoryScopeAuthorizer;
|
|
29
|
+
//# sourceMappingURL=shared-memory-scope-authorizer.d.ts.map
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** 活对象(闭包持有目录与集合)⇒ `create*`(CLAUDE.md 工厂命名律)。 */
|
|
2
|
+
export function createSharedMemoryScopeAuthorizer(opts) {
|
|
3
|
+
const deploymentScopes = [...new Set(opts.deploymentScopes ?? [])];
|
|
4
|
+
return {
|
|
5
|
+
async resolve(principal) {
|
|
6
|
+
// principal 缺席 = 单用户部署(多租户下 core 与 HTTP 门都先要求身份)。此时唯一的授权事实就是
|
|
7
|
+
// operator 自证的那一组;目录无从查起,但这不是「不可用」——它是一个**已知的**答案。
|
|
8
|
+
if (principal === undefined)
|
|
9
|
+
return { kind: "granted", scopes: deploymentScopes };
|
|
10
|
+
const lookup = await opts.directory.lookup(principal);
|
|
11
|
+
if (lookup.kind === "unavailable")
|
|
12
|
+
return { kind: "unavailable", reason: lookup.reason };
|
|
13
|
+
return { kind: "granted", scopes: [...new Set([...deploymentScopes, ...Object.keys(lookup.scopes)])] };
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=shared-memory-scope-authorizer.js.map
|
package/dist/task-settings.d.ts
CHANGED
|
@@ -70,6 +70,50 @@ export declare function effectiveThinking(reasoningEffort: unknown, ultracode: b
|
|
|
70
70
|
* field is the explicit per-turn intent → it WINS over a bundle defaultMode). Creates a minimal settings object when
|
|
71
71
|
* no `body.settings` bundle was sent. The result flows through {@link applyTaskSettings} (tighten-only). */
|
|
72
72
|
export declare function withPermissionMode(settings: ParsedTaskSettings | undefined, mode: SettingsPermissionMode): ParsedTaskSettings;
|
|
73
|
+
/**
|
|
74
|
+
* design/201 §6-1 —— 本请求的**生效** permission mode,单点。优先序逐字同 {@link withPermissionMode}:
|
|
75
|
+
* 顶层 `body.permissionMode`(本轮显式表态)赢过 bundle 的 `settings.permissions.defaultMode`;两处
|
|
76
|
+
* 都没有 ⇒ `undefined` = **无表态**(调用点据此不写键,而不是替调用方选一个默认值)。
|
|
77
|
+
*
|
|
78
|
+
* 🔴 为什么必须是一只函数:同一个「生效模式」此前在 resolve-spec 里被算了两次(hooks 腿的
|
|
79
|
+
* `permission_mode` 载荷、settings 折叠腿的 `withPermissionMode`),三审同点判定翻译表**不得**成为
|
|
80
|
+
* 第三个算点 —— 三处各算各的,任何一次优先序修订都会让三面分家,而分家在这条轴上的形态是
|
|
81
|
+
* 「壳发的 bundle bypass 在 A 面生效、在 B 面没生效」这种最难被外部发现的静默偏差。
|
|
82
|
+
*/
|
|
83
|
+
export declare function effectivePermissionMode(body: {
|
|
84
|
+
permissionMode?: unknown;
|
|
85
|
+
}, settings: ParsedTaskSettings | undefined): SettingsPermissionMode | undefined;
|
|
86
|
+
/**
|
|
87
|
+
* design/201 §2 —— 显式 permission mode → `TaskSpec.shellGate` 档位的**翻译表(单一真源)**。
|
|
88
|
+
*
|
|
89
|
+
* · `bypassPermissions` ⇒ `"off"` —— CC `--dangerously-skip-permissions` 的对位裁定
|
|
90
|
+
* (与 fs-write 面的 bypass 臂两面归一,见 {@link deriveSettingsPolicy})。
|
|
91
|
+
* · `auto` ⇒ `"classify"` —— **design/201 §8 开口按施工期新披露亲裁改译**(原裁定字面是 off,
|
|
92
|
+
* §8 保留「core auto 分类器覆盖面有新披露时可独立再裁,表驱动一行」——新披露见下段):
|
|
93
|
+
*
|
|
94
|
+
* ⚠️ **`auto` 这一行有一条待裁的开口(design/201 §8 明列「core auto 分类器覆盖面有新披露时可独立再裁,
|
|
95
|
+
* 表驱动一行」)。施工期对抗复审给出了那条新披露,已亲读安装包核实**:
|
|
96
|
+
* · core 只在**已经产生 `ask`** 之后才咨询分类器(`dist/core/hooks.js`:`if (input.autoMode &&
|
|
97
|
+
* decision.action === "ask" …)`);
|
|
98
|
+
* · 而 `shellGate:"off"` 下 core **不铸任何 shell 面的门**(`dist/core/runner/prepare-task.js` 的
|
|
99
|
+
* `shellGate === "off"` 臂反而发一条 `classification:"shell-gate-off"` 的 onError:「真可写 Bash 挂着
|
|
100
|
+
* 却没有 shell 安全轴折叠」)。
|
|
101
|
+
* 两条合起来:`auto` × off ⇒ Bash 既不产 ask、分类器也就永不被咨询 —— off 档下「筛选权交给分类器」
|
|
102
|
+
* 不成立。故 auto 归 classify 组:分类器坐在 classify 产的 ask 之上,语义才真是「交给分类器」
|
|
103
|
+
* (良性只读命令由分类器自动放行,其余 ask;这正是 auto 模式的本义)。发车帖向 [3378] 裁定链披露此
|
|
104
|
+
* 一行偏离;core auto 分类器若来日在 off 档下也有咨询点,可再裁回(表驱动一行)。
|
|
105
|
+
* · `default` / `acceptEdits` / `plan` ⇒ `"classify"` —— 这一档原先由壳无条件注入 `MANUAL_MODE_SHELL_GATE`
|
|
106
|
+
* 供给(车道缺省走了 operator 通道),现在归位到表态轴。`plan` 本身 handsReadOnly(core 明写此档下
|
|
107
|
+
* shellGate 无效),给它 classify 纯为一致性。
|
|
108
|
+
*
|
|
109
|
+
* 🔴 **入参非可选**:缺席(无表态)不是这张表的一行 —— 它的语义是「不写这个键」,只能在调用点判。
|
|
110
|
+
* 把它折进来会逼出一个 `undefined` 返回值,而那正是「写 off」与「不写」被混同的入口(core 的
|
|
111
|
+
* 缺席默认是 off,但**写**一个 off 会成为 governance 的 base,与缺席不是同一件事)。
|
|
112
|
+
*
|
|
113
|
+
* 🔴 **闭集 exhaustive switch,无 default 臂**:五个模式词是封闭词表,新增一个模式而不更新本表
|
|
114
|
+
* 是**编译错误**(#157 的安全轴纪律:词表的未知项不许有静默兜底臂)。
|
|
115
|
+
*/
|
|
116
|
+
export declare function shellGateForMode(effMode: SettingsPermissionMode): "off" | "classify";
|
|
73
117
|
/** The validated, service-trusted subset of `SemaSettings` we project onto the spec. `env` is parsed only to REPORT
|
|
74
118
|
* it as received-but-deferred when off the host lane (never silently dropped); a MALFORMED `hooks` likewise reports
|
|
75
119
|
* deferred (submit 路径另有 400 fail-loud),valid `hooks` 进 applied shape(hook-runner 阶段一)。 */
|