@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
package/dist/adoption/runner.js
CHANGED
|
@@ -23,8 +23,10 @@
|
|
|
23
23
|
import { randomUUID } from "node:crypto";
|
|
24
24
|
import { ADOPTION_PHASE, adoptionLockName, } from "../plugins/adoption-log-sql.js";
|
|
25
25
|
import { policyKeyFor } from "../plugins/session-policy-store-sql.js";
|
|
26
|
+
import { buildRuleOwnerKey } from "../plugins/permission-rule-store-sql.js";
|
|
27
|
+
import { withAdoptionQuiesce } from "./quiesce.js";
|
|
26
28
|
import { AFFECTED_CONFIG_TEMPLATES, MEMORY_SCOPE_MAX_CHARS, NOT_MIGRATED_BY_DESIGN, REBIND_LEGS, buildVerbatimPair, rewriteMemoryScope, } from "./plan.js";
|
|
27
|
-
import { buildBlobScanSql, buildBlobWriteSql, buildConflictProbeSql, buildMemoryScopeScanSql, buildResidualCountSql, buildRebindSql, buildSessionPolicyRewriteSql, buildSessionPolicyScanSql, } from "./sql.js";
|
|
29
|
+
import { buildBlobScanSql, buildBlobWriteSql, buildConflictProbeSql, buildMemoryScopeScanSql, buildResidualCountSql, buildRebindSql, buildRuleOwnerRekeySql, buildSessionPolicyRewriteSql, buildSessionPolicyScanSql, } from "./sql.js";
|
|
28
30
|
import { AdoptionConfigsSchema, AdoptionLegsSchema, AdoptionReceiptSchema, AdoptionRejectDetailSchema, AdoptionReportSchema, } from "./wire.js";
|
|
29
31
|
import { formatUserScope, encodeScopeSegment } from "@sema-agent/core";
|
|
30
32
|
/** MySQL `ER_DUP_ENTRY` / PG `unique_violation` —— 跨轴组合冲突的最后一道 fail-closed 门。 */
|
|
@@ -42,6 +44,18 @@ function msgOf(e) {
|
|
|
42
44
|
async function resolveValuePairs(exec, dialect, leg, fromPrincipal, toPrincipal) {
|
|
43
45
|
if (leg.encoding === "verbatim")
|
|
44
46
|
return [buildVerbatimPair(fromPrincipal, toPrincipal)];
|
|
47
|
+
// 规则桶键:恒一对,**算**出来而不是扫出来(原像里只有 principal,没有逐行变量)。
|
|
48
|
+
// 🔴 这一支同时服务 preflight 的冲突探针与 `countResidual` 的残留计数 —— 少了它,那两处会拿**明文**
|
|
49
|
+
// principal 去比 `owner_key`,而那个谓词在这张表上永远匹配不到任何一行:preflight 恒说「无冲突」、
|
|
50
|
+
// 残留恒报 0(A-010.16 / A-010.22 点名的正是后者)。
|
|
51
|
+
if (leg.encoding === "rule-owner-key") {
|
|
52
|
+
return [
|
|
53
|
+
{
|
|
54
|
+
from: buildRuleOwnerKey({ kind: "principal", principal: fromPrincipal }),
|
|
55
|
+
to: buildRuleOwnerKey({ kind: "principal", principal: toPrincipal }),
|
|
56
|
+
},
|
|
57
|
+
];
|
|
58
|
+
}
|
|
45
59
|
const seg = encodeScopeSegment(fromPrincipal);
|
|
46
60
|
const projHead = `proj:${seg}/`;
|
|
47
61
|
const userProjHead = `userproj:${seg}/`;
|
|
@@ -183,6 +197,15 @@ export function createAdoptionRunner(deps) {
|
|
|
183
197
|
out.push({ store: leg.leg, action: leg.action, rows });
|
|
184
198
|
continue;
|
|
185
199
|
}
|
|
200
|
+
if (leg.kind === "rule-owner-rekey") {
|
|
201
|
+
// 一对键算一次(见 buildRuleOwnerRekeySql 的注),一条等值 UPDATE 走完;`principal` 明文列同改。
|
|
202
|
+
const [pair] = await resolveValuePairs(exec, dialect, leg, fromPrincipal, toPrincipal);
|
|
203
|
+
if (pair === undefined)
|
|
204
|
+
throw new Error(`runLegs: leg ${leg.leg} resolved no identity pair`);
|
|
205
|
+
const res = await exec.query(buildRuleOwnerRekeySql(dialect, leg), [pair.to, toPrincipal, pair.from]);
|
|
206
|
+
out.push({ store: leg.leg, action: leg.action, rows: res.affected });
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
186
209
|
const pairs = await resolveValuePairs(exec, dialect, leg, fromPrincipal, toPrincipal);
|
|
187
210
|
let rows = 0;
|
|
188
211
|
for (const pair of pairs) {
|
|
@@ -230,11 +253,26 @@ export function createAdoptionRunner(deps) {
|
|
|
230
253
|
slot.blob = leg;
|
|
231
254
|
else {
|
|
232
255
|
slot.match.add(leg.matchColumn);
|
|
233
|
-
|
|
234
|
-
|
|
256
|
+
// 🔴 A-010.16(验真后修):这里此前只认 `memory-scope` 一种**非明文**编码,别的编码一律
|
|
257
|
+
// 落到下面那条 `[{ from: fromPrincipal, … }]` 兜底 —— 也就是拿**明文 principal** 去比一根存
|
|
258
|
+
// 派生键的列。对 `permission_rule.owner_key`(sha256 原像里带身份)那个谓词**永远匹配不到
|
|
259
|
+
// 任何一行**,于是残留计数在这张表上结构性恒 0:回执一边说「adopted」、一边把一整桶还挂在
|
|
260
|
+
// 旧身份名下的活规则报成「零残留」。判据改成**编码非 verbatim 即需解析**,新编码天然进对臂。
|
|
261
|
+
if (leg.encoding !== "verbatim")
|
|
262
|
+
slot.codedLeg = leg;
|
|
235
263
|
}
|
|
236
264
|
byTable.set(leg.table, slot);
|
|
237
265
|
}
|
|
266
|
+
// 🔴 一张表内**混编码**会让下面那条「一条 COUNT + 一个值对」的形悄悄算错(OR 起来的每一列都会被
|
|
267
|
+
// 绑上同一个值,而它们的编码不同 ⇒ 至少一列的谓词恒假)。今天没有这样的表;真出现时必须响亮拦下,
|
|
268
|
+
// 而不是报一个偏小的残留数(偏小 = 说「没落下」,正是本条要消灭的谎)。
|
|
269
|
+
for (const [table, slot] of byTable) {
|
|
270
|
+
const encodings = new Set(REBIND_LEGS.filter((l) => l.table === table && l.kind !== "blob-rewrite").map((l) => l.encoding));
|
|
271
|
+
if (encodings.size > 1) {
|
|
272
|
+
throw new Error(`adoption: table ${table} carries identity axes in more than one encoding (${[...encodings].sort().join(", ")}) — the residual count binds ONE value pair across the OR'd columns, so it would silently under-report. Split the count per encoding before adding such a leg.`);
|
|
273
|
+
}
|
|
274
|
+
void slot;
|
|
275
|
+
}
|
|
238
276
|
let n = 0;
|
|
239
277
|
for (const [table, slot] of byTable) {
|
|
240
278
|
if (slot.blob !== undefined) {
|
|
@@ -252,8 +290,8 @@ export function createAdoptionRunner(deps) {
|
|
|
252
290
|
// 普通表:一条 COUNT,谓词是该表全部身份轴的并。memory 族的值对逐对各来一次(不同 scope 键 =
|
|
253
291
|
// 不同的行集合,不会重复计数同一行)。
|
|
254
292
|
const cols = [...slot.match];
|
|
255
|
-
const pairs = slot.
|
|
256
|
-
? await resolveValuePairs(exec, dialect, slot.
|
|
293
|
+
const pairs = slot.codedLeg !== undefined
|
|
294
|
+
? await resolveValuePairs(exec, dialect, slot.codedLeg, fromPrincipal, toPrincipal)
|
|
257
295
|
: [{ from: fromPrincipal, to: toPrincipal }];
|
|
258
296
|
for (const pair of pairs) {
|
|
259
297
|
const res = await exec.query(buildResidualCountSql(dialect, table, cols), cols.map(() => pair.from));
|
|
@@ -421,7 +459,27 @@ export function createAdoptionRunner(deps) {
|
|
|
421
459
|
return 1;
|
|
422
460
|
}
|
|
423
461
|
}
|
|
462
|
+
/**
|
|
463
|
+
* 🔴 A-010.18:**唯一**的静默闸挂点。三条入口(`start` / `get` 的在飞续跑 / boot 的 `resumeInFlight`)
|
|
464
|
+
* 全部经由本函数动数据,所以闸挂在这里一处就覆盖全部会写被迁表的路径 —— 挂在 `start` 上会漏掉后两条,
|
|
465
|
+
* 而那两条恰恰是「没人按重试、副本自己续跑」的场景,与维护 tick 撞车的概率并不更低。
|
|
466
|
+
* 闸的语义、为什么让开**整轮** tick、以及它只挡本副本这条射程边界,逐字见 `adoption/quiesce.ts`。
|
|
467
|
+
*/
|
|
424
468
|
async function driveUnderLock(row, sweep) {
|
|
469
|
+
return withAdoptionQuiesce(() => driveUnderLockInner(row, sweep), {
|
|
470
|
+
// 排空没等到 ⇒ **响亮**(codex R1 [high] 的第二半):等不到不等于可以装作等到了。这条 warn 是
|
|
471
|
+
// 运维判断「那次收编期间有没有腿在跟它抢同一批行」的唯一读数。
|
|
472
|
+
onDrainTimeout: (reading) => deps.logger?.warn?.("adoption_maintenance_drain_timeout", {
|
|
473
|
+
adoptionId: row.adoptionId,
|
|
474
|
+
pendingLegs: reading.pending,
|
|
475
|
+
note: "the adoption arc stopped waiting for this replica's already-airborne maintenance legs to land. " +
|
|
476
|
+
"New ticks are held off for the arc's duration, but a leg that was ALREADY running may still write a " +
|
|
477
|
+
"table this arc rebinds (the approval reconciler is the sharp one). design/183 I1 (stop the engine " +
|
|
478
|
+
"first) remains the operator premise; this reading says the in-process half could not fully cover it.",
|
|
479
|
+
}),
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
async function driveUnderLockInner(row, sweep) {
|
|
425
483
|
const out = await store.withLock(adoptionLockName(row.fromPrincipal),
|
|
426
484
|
// 🔴 **拿到锁之后重读**(codex R2-F3):`row` 是排队**之前**读到的快照,而排队期间别的副本可能已经
|
|
427
485
|
// 把这条弧推完或拒掉了。拿旧快照进状态机 = 按一个已经不成立的世界做决定(实测可达的形:一条已被
|
package/dist/adoption/sql.d.ts
CHANGED
|
@@ -65,6 +65,21 @@ export declare function buildBlobScanSql(dialect: SqlDialect, leg: RebindLegSpec
|
|
|
65
65
|
* (行数可能仍报 1,故回执的行数按「**内容真变了**才计」在调用侧收窄)。
|
|
66
66
|
*/
|
|
67
67
|
export declare function buildBlobWriteSql(dialect: SqlDialect, leg: RebindLegSpec): string;
|
|
68
|
+
/**
|
|
69
|
+
* 规则桶的**整桶改键**(A-010.16)。参数序:`[newOwnerKey, toPrincipal, oldOwnerKey]`。
|
|
70
|
+
*
|
|
71
|
+
* `owner_key = sha256("principal:" + principal)` —— 身份在**键的 hash 原像**里,与 session_policy
|
|
72
|
+
* 同族。差别在于这把键**不含逐行变量**(session_policy 的原像里还有 session_id),所以不必逐行扫出来
|
|
73
|
+
* 重算:一对 (旧键, 新键) 算一次,一条等值 UPDATE 走完全表。
|
|
74
|
+
*
|
|
75
|
+
* `principal` 列跟着同改:它是 owner_key 的**明文原像列**(店按前者寻址、`GET /v1/rules` 的回执与运维
|
|
76
|
+
* 的 `WHERE principal = …` 排查按后者读)。只改一列,库里就有两份互相矛盾的身份说法。
|
|
77
|
+
*
|
|
78
|
+
* 幂等性来自谓词:重跑时旧桶键已经不在库里 ⇒ 零行匹配 ⇒ 零行变化。
|
|
79
|
+
* `owner_kind = 'local-owner'` 的那只桶天然不受影响 —— 它的 owner_key 是 `sha256("local-owner")`,
|
|
80
|
+
* 与任何 principal 派生的键都不相等,所以**匹配不到**(不需要额外的 owner_kind 谓词来护住它)。
|
|
81
|
+
*/
|
|
82
|
+
export declare function buildRuleOwnerRekeySql(dialect: SqlDialect, leg: RebindLegSpec): string;
|
|
68
83
|
/** session_policy 重键腿的扫描:拿到 from 侧每一行的 `(policy_key, session_id)`。参数序:`[fromPrincipal]`。 */
|
|
69
84
|
export declare function buildSessionPolicyScanSql(dialect: SqlDialect): string;
|
|
70
85
|
/**
|
package/dist/adoption/sql.js
CHANGED
|
@@ -90,6 +90,24 @@ export function buildBlobWriteSql(dialect, leg) {
|
|
|
90
90
|
// —— 收编那时已经报 adopted。推进 rev 才让那些「站在过去」的写者按 OCC 的本意输掉并重试。
|
|
91
91
|
return `UPDATE ${leg.table} SET ${blob.column} = ${ph(dialect, 1)}, ${blob.revColumn} = ${blob.revColumn} + 1 WHERE ${where} AND ${blob.revColumn} = ${ph(dialect, blob.pk.length + 2)}`;
|
|
92
92
|
}
|
|
93
|
+
/**
|
|
94
|
+
* 规则桶的**整桶改键**(A-010.16)。参数序:`[newOwnerKey, toPrincipal, oldOwnerKey]`。
|
|
95
|
+
*
|
|
96
|
+
* `owner_key = sha256("principal:" + principal)` —— 身份在**键的 hash 原像**里,与 session_policy
|
|
97
|
+
* 同族。差别在于这把键**不含逐行变量**(session_policy 的原像里还有 session_id),所以不必逐行扫出来
|
|
98
|
+
* 重算:一对 (旧键, 新键) 算一次,一条等值 UPDATE 走完全表。
|
|
99
|
+
*
|
|
100
|
+
* `principal` 列跟着同改:它是 owner_key 的**明文原像列**(店按前者寻址、`GET /v1/rules` 的回执与运维
|
|
101
|
+
* 的 `WHERE principal = …` 排查按后者读)。只改一列,库里就有两份互相矛盾的身份说法。
|
|
102
|
+
*
|
|
103
|
+
* 幂等性来自谓词:重跑时旧桶键已经不在库里 ⇒ 零行匹配 ⇒ 零行变化。
|
|
104
|
+
* `owner_kind = 'local-owner'` 的那只桶天然不受影响 —— 它的 owner_key 是 `sha256("local-owner")`,
|
|
105
|
+
* 与任何 principal 派生的键都不相等,所以**匹配不到**(不需要额外的 owner_kind 谓词来护住它)。
|
|
106
|
+
*/
|
|
107
|
+
export function buildRuleOwnerRekeySql(dialect, leg) {
|
|
108
|
+
return (`UPDATE ${leg.table} SET owner_key = ${ph(dialect, 1)}, principal = ${ph(dialect, 2)}` +
|
|
109
|
+
` WHERE owner_key = ${ph(dialect, 3)}`);
|
|
110
|
+
}
|
|
93
111
|
/** session_policy 重键腿的扫描:拿到 from 侧每一行的 `(policy_key, session_id)`。参数序:`[fromPrincipal]`。 */
|
|
94
112
|
export function buildSessionPolicyScanSql(dialect) {
|
|
95
113
|
return `SELECT policy_key, session_id FROM session_policy WHERE principal = ${ph(dialect, 1)}`;
|
package/dist/adoption/wire.d.ts
CHANGED
|
@@ -68,7 +68,7 @@ export type AdoptionConfigEntry = z.infer<typeof AdoptionConfigEntrySchema>;
|
|
|
68
68
|
* 🔴 这是**冻结的闭集**(下游按成员名写正向断言)。改名/删员 = wire 破坏;新增 = 新行为面,要先过三问。
|
|
69
69
|
* 判据形见 183 §10.5:「按设计不迁」与「漏了没迁」在黑盒上必须可分,所以它是**正向声明**而不是沉默。
|
|
70
70
|
*/
|
|
71
|
-
export declare const NOT_MIGRATED_FACES: readonly ["usage-window", "cost-quota", "rate-limit", "approval-exemption-grantor", "approval-ask-decision-actor", "image-bake-requested-by"];
|
|
71
|
+
export declare const NOT_MIGRATED_FACES: readonly ["usage-window", "cost-quota", "rate-limit", "approval-exemption-grantor", "approval-ask-decision-actor", "image-bake-requested-by", "permission-rule-approval-history", "permission-rule-import-ticket"];
|
|
72
72
|
export type NotMigratedFace = (typeof NOT_MIGRATED_FACES)[number];
|
|
73
73
|
export declare const AdoptionNotMigratedSchema: z.ZodObject<{
|
|
74
74
|
face: z.ZodEnum<{
|
|
@@ -78,6 +78,8 @@ export declare const AdoptionNotMigratedSchema: z.ZodObject<{
|
|
|
78
78
|
"approval-exemption-grantor": "approval-exemption-grantor";
|
|
79
79
|
"approval-ask-decision-actor": "approval-ask-decision-actor";
|
|
80
80
|
"image-bake-requested-by": "image-bake-requested-by";
|
|
81
|
+
"permission-rule-approval-history": "permission-rule-approval-history";
|
|
82
|
+
"permission-rule-import-ticket": "permission-rule-import-ticket";
|
|
81
83
|
}>;
|
|
82
84
|
ruling: z.ZodEnum<{
|
|
83
85
|
D1: "D1";
|
|
@@ -124,6 +126,8 @@ export declare const AdoptionReportSchema: z.ZodObject<{
|
|
|
124
126
|
"approval-exemption-grantor": "approval-exemption-grantor";
|
|
125
127
|
"approval-ask-decision-actor": "approval-ask-decision-actor";
|
|
126
128
|
"image-bake-requested-by": "image-bake-requested-by";
|
|
129
|
+
"permission-rule-approval-history": "permission-rule-approval-history";
|
|
130
|
+
"permission-rule-import-ticket": "permission-rule-import-ticket";
|
|
127
131
|
}>;
|
|
128
132
|
ruling: z.ZodEnum<{
|
|
129
133
|
D1: "D1";
|
|
@@ -183,6 +187,8 @@ export declare const AdoptionReceiptSchema: z.ZodObject<{
|
|
|
183
187
|
"approval-exemption-grantor": "approval-exemption-grantor";
|
|
184
188
|
"approval-ask-decision-actor": "approval-ask-decision-actor";
|
|
185
189
|
"image-bake-requested-by": "image-bake-requested-by";
|
|
190
|
+
"permission-rule-approval-history": "permission-rule-approval-history";
|
|
191
|
+
"permission-rule-import-ticket": "permission-rule-import-ticket";
|
|
186
192
|
}>;
|
|
187
193
|
ruling: z.ZodEnum<{
|
|
188
194
|
D1: "D1";
|
package/dist/adoption/wire.js
CHANGED
|
@@ -64,6 +64,12 @@ export const NOT_MIGRATED_FACES = [
|
|
|
64
64
|
"approval-exemption-grantor",
|
|
65
65
|
"approval-ask-decision-actor",
|
|
66
66
|
"image-bake-requested-by",
|
|
67
|
+
// A-010.22(台账 P1)—— 规则店族此前对回执**零表态**:`permission_rule` 那只**活桶**现在真迁了
|
|
68
|
+
// (`permission_rule#owner_key` 腿),但同族另外两张表按 D2/D1 刻意不迁,而「刻意不迁」与「漏了没迁」
|
|
69
|
+
// 在黑盒上必须可分(§10.5)。少了这两员,运维读到的回执是:`notMigratedByDesign` 里没有规则店的影子、
|
|
70
|
+
// `residualSourceRows` 又把它们数成 0 —— 一份「什么都没落下」的**假声明**。
|
|
71
|
+
"permission-rule-approval-history",
|
|
72
|
+
"permission-rule-import-ticket",
|
|
67
73
|
];
|
|
68
74
|
export const AdoptionNotMigratedSchema = z
|
|
69
75
|
.object({
|
package/dist/approval-card.d.ts
CHANGED
|
@@ -50,6 +50,7 @@ export declare const ApprovalCardSchema: z.ZodObject<{
|
|
|
50
50
|
requiresRealApproval: z.ZodBoolean;
|
|
51
51
|
}, z.core.$strict>;
|
|
52
52
|
governanceForced: z.ZodOptional<z.ZodLiteral<true>>;
|
|
53
|
+
persistedRuleShadowed: z.ZodOptional<z.ZodString>;
|
|
53
54
|
ruleSuggestions: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
54
55
|
rule: z.ZodString;
|
|
55
56
|
match: z.ZodEnum<{
|
|
@@ -89,6 +90,7 @@ export declare const ApprovalCardEnvelopeSchema: z.ZodObject<{
|
|
|
89
90
|
requiresRealApproval: z.ZodBoolean;
|
|
90
91
|
}, z.core.$strict>;
|
|
91
92
|
governanceForced: z.ZodOptional<z.ZodLiteral<true>>;
|
|
93
|
+
persistedRuleShadowed: z.ZodOptional<z.ZodString>;
|
|
92
94
|
ruleSuggestions: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
93
95
|
rule: z.ZodString;
|
|
94
96
|
match: z.ZodEnum<{
|
|
@@ -132,6 +134,9 @@ export interface ApprovalCardSource {
|
|
|
132
134
|
/** [2942]/[2943]:治理来源标 —— 与 wire 帧**同一份素材**(`ToolApprovalFrame` 结构上满足本接口),
|
|
133
135
|
* 于是 live 帧 / `card_json` / 重放帧三面同源,不是三处各判一遍。 */
|
|
134
136
|
governanceForced?: true;
|
|
137
|
+
/** #144(core 5.25.0):被越级的持久规则原文 —— **已 redactSecrets**(发帧点做,本模块只 clip)。
|
|
138
|
+
* 与 wire 帧**同一份素材**(`ToolApprovalFrame` 结构上满足本接口)⇒ live 帧 / `card_json` / 重放帧三面同源。 */
|
|
139
|
+
persistedRuleShadowed?: string;
|
|
135
140
|
/** #154 车二:引擎铸的规则候选(**只在规则店装配时**由发帧点填;语义与在场性契约见
|
|
136
141
|
* `ApprovalCardSchema.ruleSuggestions`)。与 wire 帧**同一份素材** ⇒ live 帧 / `card_json` / 重放帧
|
|
137
142
|
* 三面同源。 */
|
package/dist/approval-card.js
CHANGED
|
@@ -79,6 +79,24 @@ export const ApprovalCardSchema = z
|
|
|
79
79
|
* park(fail-safe),回滚窗结束即自愈。
|
|
80
80
|
*/
|
|
81
81
|
governanceForced: z.literal(true).optional(),
|
|
82
|
+
/**
|
|
83
|
+
* #144(core 5.25.0 [3438]/[3443],**ADDITIVE**):这次 ask **命中了**一条用户的持久 allow 规则,
|
|
84
|
+
* 但规则**清不掉**它 —— 值 = 那条被越级的规则**原文**(语义、消音边界与「缺席 ≠ 没有规则」的
|
|
85
|
+
* 硬条款逐字见 `tool-approval.ts` 的 `ToolApprovalFrame.persistedRuleShadowed`)。
|
|
86
|
+
*
|
|
87
|
+
* 放在**卡的顶层**、与 `governanceForced` 并列而不是塞进 `risk`:`risk` 是引擎对这次**操作**的风险
|
|
88
|
+
* 判定,这一格讲的是「你那条规则怎么了」——两件事。与 `governanceForced` 也**刻意分列**:那个说
|
|
89
|
+
* 「门是运维下的」,这个说「有一条你的规则在场但被越级」,两者可以同时在场也可以各自单独在场
|
|
90
|
+
* (governance 是不可消音的三个来源之一,doctrine `always` 与工具自带 egress/irreversible mark 是另两个)。
|
|
91
|
+
*
|
|
92
|
+
* 值域 = **用户内容族**(规则原文是人写的文本,core 侧已过 `inlineUntrusted` 200 cap):
|
|
93
|
+
* 写侧在**上游**(`tool-approval.ts` 发帧点)过 `redactSecrets` 再交进来,本层只截长(`clip`)。
|
|
94
|
+
* UNTRUSTED-for-display,同 `message`/`args` 待遇。
|
|
95
|
+
*
|
|
96
|
+
* 回滚窗代价与 `governanceForced` **逐字同族**(见上一段那条 ⚠️):`.strict()` 下旧二进制读不动带本键的
|
|
97
|
+
* 新行 ⇒ 重放跳过 + 幂等重入走 park(fail-safe),`schemaVersion` 不动。
|
|
98
|
+
*/
|
|
99
|
+
persistedRuleShadowed: z.string().max(MAX_RULE_TEXT_CHARS).optional(),
|
|
82
100
|
/**
|
|
83
101
|
* #154 车二(core 5.18.0 design/179):引擎为这次 ask 铸的**规则候选**(`Bash(git status)` 精确 /
|
|
84
102
|
* `Bash(git status:*)` 词界前缀)—— 呈卡面据它渲「不再询问」。
|
|
@@ -183,6 +201,7 @@ export function buildApprovalCard(source, req, requiresRealApproval) {
|
|
|
183
201
|
const toolCallId = clip(source.toolCallId, MAX_IDENT);
|
|
184
202
|
const sourceTaskId = clip(source.sourceTaskId, MAX_IDENT);
|
|
185
203
|
const sourceAgentName = clip(source.sourceAgentName, MAX_AGENT_NAME);
|
|
204
|
+
const shadowedRule = clip(source.persistedRuleShadowed, MAX_RULE_TEXT_CHARS);
|
|
186
205
|
return {
|
|
187
206
|
toolName: clip(source.toolName, MAX_IDENT) ?? "",
|
|
188
207
|
message: clip(source.message, MAX_MESSAGE) ?? "",
|
|
@@ -195,6 +214,9 @@ export function buildApprovalCard(source, req, requiresRealApproval) {
|
|
|
195
214
|
},
|
|
196
215
|
// [2942]/[2943]:只在为真时投影(`=== true` 严判:非布尔真值不得把一张普通卡染成治理卡)。
|
|
197
216
|
...(source.governanceForced === true ? { governanceForced: true } : {}),
|
|
217
|
+
// #144:被越级的规则原文。**空串不投**(core 的四个 mint 点只在真有命中规则时带这个键 ⇒ 空串不是
|
|
218
|
+
// 「有一条空规则」而是坏值;投一个空串会让壳渲一行「你的规则 ⟨⟩ 仍在」)。截长同 `message` 待遇。
|
|
219
|
+
...(shadowedRule !== undefined && shadowedRule !== "" ? { persistedRuleShadowed: shadowedRule } : {}),
|
|
198
220
|
// #154 车二:候选**逐字**投影(空数组 ⇒ 不投键 —— 「没有候选」与「本部署不供候选」在卡面上都读作
|
|
199
221
|
// 「本卡无此选项」,铸一个空数组只会让消费端多一条无意义的分支)。拷贝一份:卡不与调用方共享可变引用。
|
|
200
222
|
...(source.ruleSuggestions !== undefined && source.ruleSuggestions.length > 0
|
package/dist/auth-keys.d.ts
CHANGED
|
@@ -15,9 +15,31 @@ export interface ApprovalHmacKey {
|
|
|
15
15
|
key: string;
|
|
16
16
|
status?: "active" | "retiring";
|
|
17
17
|
}
|
|
18
|
+
/**
|
|
19
|
+
* #210(黑板 [3425]③「坏值禁静默回默认」)—— 一条**被丢弃**的 key-set 条目的诊断。
|
|
20
|
+
*
|
|
21
|
+
* 为什么两个解析器要新长一个必填出口:它们的 fail-closed 方向是对的(坏形 ⇒ `[]` ⇒ 门关着),但**丢弃
|
|
22
|
+
* 本身此前零留痕**。真场景:轮换新钥时手滑把 `key` 写成 `secret`,这一条被 `filter` 悄悄滤掉,进程照常
|
|
23
|
+
* 起、旧钥还在、验签照常过 —— 直到旧钥退役那天,全部审批回决在生产上一起开始 401,而配置早在几周前就
|
|
24
|
+
* 已经坏了。诊断出口让「我加的那把钥没进来」在 boot 当场可见。
|
|
25
|
+
*
|
|
26
|
+
* `index` 是**原数组下标**(operator 拿它直接定位是 JSON 里第几条);`"-"` = 整体形坏(不是数组 / 不是
|
|
27
|
+
* JSON),没有可指的条目。`reason` 只说**缺陷字段名**,永不带值 —— HMAC 的 `key` 是对称密钥,诊断行会
|
|
28
|
+
* 进日志。
|
|
29
|
+
*/
|
|
30
|
+
export interface KeySetRejection {
|
|
31
|
+
index: number | "-";
|
|
32
|
+
reason: string;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* 诊断出口**必填**(不是 `onReject?:`)。可选回调 = 「没人接就等于错误蒸发」,正是本仓 #191 静默降级
|
|
36
|
+
* 门数的 SHAPE B;这两条钥又都在凭据轴上,所以由**类型**逼每个调用点当场表态,而不是靠人记得去接。
|
|
37
|
+
*/
|
|
38
|
+
export type OnKeySetRejection = (rejection: KeySetRejection) => void;
|
|
18
39
|
/** Parse APPROVAL_HMAC_KEYS — a sema-registry-managed, rotating key-SET, as a JSON array of {kid,key,status}.
|
|
19
|
-
* Empty / unset / unparseable ⇒ `[]` ⇒ HMAC verification is OFF (D-G inactive; back-compat, BFF-only door).
|
|
20
|
-
|
|
40
|
+
* Empty / unset / unparseable ⇒ `[]` ⇒ HMAC verification is OFF (D-G inactive; back-compat, BFF-only door).
|
|
41
|
+
* fail-closed 语义与改前**逐字相同**;新增的只有 `onReject`(#210:每一条被丢弃的条目都要说出来)。 */
|
|
42
|
+
export declare function parseApprovalHmacKeys(raw: string | undefined, onReject: OnKeySetRejection): ApprovalHmacKey[];
|
|
21
43
|
/** One issuer public key in the rotating principal JWKS (mirror of the HMAC key-set rotation discipline). The
|
|
22
44
|
* `alg` is PINNED per key — the JWT header's alg MUST match it, killing alg-confusion (`none`, HS-with-the-pubkey).
|
|
23
45
|
* `key` is a PEM (SPKI) public key. PUBLIC, non-secret. */
|
|
@@ -28,6 +50,8 @@ export interface PrincipalJwtKey {
|
|
|
28
50
|
status?: "active" | "retiring";
|
|
29
51
|
}
|
|
30
52
|
/** Parse PRINCIPAL_JWT_PUBKEYS — a JSON array of {kid,alg,key,status}. Empty/unset/malformed ⇒ [] ⇒ no trust
|
|
31
|
-
* anchor ⇒ the direct door cannot open (D-G inactive). Each entry must have kid + a pinned alg + a PEM key.
|
|
32
|
-
|
|
53
|
+
* anchor ⇒ the direct door cannot open (D-G inactive). Each entry must have kid + a pinned alg + a PEM key.
|
|
54
|
+
* #210: fail-closed 语义逐字不变,但每条被丢弃的条目经 `onReject` 报出(序号 + 缺陷字段名,不带值)——
|
|
55
|
+
* 这里的 key 是**公**钥,但 reason 仍只说字段名,与 HMAC 那条同形(两个出口的口径不该分岔)。 */
|
|
56
|
+
export declare function parsePrincipalJwks(raw: string | undefined, onReject: OnKeySetRejection): PrincipalJwtKey[];
|
|
33
57
|
//# sourceMappingURL=auth-keys.d.ts.map
|
package/dist/auth-keys.js
CHANGED
|
@@ -8,37 +8,82 @@
|
|
|
8
8
|
* (verifyApprovalHmac / verifyPrincipalJwt) stay in ./approval-hmac.ts and ./principal-jwt.ts; consumers import these four names from HERE ([2354] 兼容面全清).
|
|
9
9
|
*/
|
|
10
10
|
/** Parse APPROVAL_HMAC_KEYS — a sema-registry-managed, rotating key-SET, as a JSON array of {kid,key,status}.
|
|
11
|
-
* Empty / unset / unparseable ⇒ `[]` ⇒ HMAC verification is OFF (D-G inactive; back-compat, BFF-only door).
|
|
12
|
-
|
|
11
|
+
* Empty / unset / unparseable ⇒ `[]` ⇒ HMAC verification is OFF (D-G inactive; back-compat, BFF-only door).
|
|
12
|
+
* fail-closed 语义与改前**逐字相同**;新增的只有 `onReject`(#210:每一条被丢弃的条目都要说出来)。 */
|
|
13
|
+
export function parseApprovalHmacKeys(raw, onReject) {
|
|
13
14
|
if (!raw)
|
|
14
15
|
return [];
|
|
16
|
+
let v;
|
|
15
17
|
try {
|
|
16
|
-
|
|
17
|
-
if (!Array.isArray(v))
|
|
18
|
-
return [];
|
|
19
|
-
return v.filter((k) => !!k && typeof k.kid === "string" && typeof k.key === "string");
|
|
18
|
+
v = JSON.parse(raw);
|
|
20
19
|
}
|
|
21
20
|
catch {
|
|
21
|
+
// ⚠️ 解析器自带的 message **不能**转述:V8 的 `JSON.parse` 报错里嵌了输入片段
|
|
22
|
+
// (`Unexpected token 'T', "TOP_SECRET"... is not valid JSON`),而这条 env 的内容就是对称密钥 ——
|
|
23
|
+
// 把它写进 boot 日志等于把密钥前缀印出来。只说「不是合法 JSON」,定位靠 env 名本身。
|
|
24
|
+
onReject({ index: "-", reason: "not valid JSON (value withheld: this env holds secrets)" });
|
|
22
25
|
return [];
|
|
23
26
|
}
|
|
27
|
+
if (!Array.isArray(v)) {
|
|
28
|
+
onReject({ index: "-", reason: "not a JSON array of {kid,key,status} entries" });
|
|
29
|
+
return [];
|
|
30
|
+
}
|
|
31
|
+
const out = [];
|
|
32
|
+
v.forEach((raw, i) => {
|
|
33
|
+
const e = raw;
|
|
34
|
+
if (!e || typeof e !== "object") {
|
|
35
|
+
onReject({ index: i, reason: "entry is not an object" });
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const bad = ["kid", "key"].filter((f) => typeof e[f] !== "string");
|
|
39
|
+
if (bad.length > 0)
|
|
40
|
+
onReject({ index: i, reason: `missing/non-string field(s): ${bad.join(", ")}` });
|
|
41
|
+
else
|
|
42
|
+
out.push(e);
|
|
43
|
+
});
|
|
44
|
+
return out;
|
|
24
45
|
}
|
|
25
46
|
/** Parse PRINCIPAL_JWT_PUBKEYS — a JSON array of {kid,alg,key,status}. Empty/unset/malformed ⇒ [] ⇒ no trust
|
|
26
|
-
* anchor ⇒ the direct door cannot open (D-G inactive). Each entry must have kid + a pinned alg + a PEM key.
|
|
27
|
-
|
|
47
|
+
* anchor ⇒ the direct door cannot open (D-G inactive). Each entry must have kid + a pinned alg + a PEM key.
|
|
48
|
+
* #210: fail-closed 语义逐字不变,但每条被丢弃的条目经 `onReject` 报出(序号 + 缺陷字段名,不带值)——
|
|
49
|
+
* 这里的 key 是**公**钥,但 reason 仍只说字段名,与 HMAC 那条同形(两个出口的口径不该分岔)。 */
|
|
50
|
+
export function parsePrincipalJwks(raw, onReject) {
|
|
28
51
|
if (!raw)
|
|
29
52
|
return [];
|
|
30
53
|
const ALGS = new Set(["EdDSA", "RS256", "ES256"]);
|
|
54
|
+
let v;
|
|
31
55
|
try {
|
|
32
|
-
|
|
33
|
-
if (!Array.isArray(v))
|
|
34
|
-
return [];
|
|
35
|
-
return v.filter((k) => {
|
|
36
|
-
const e = k;
|
|
37
|
-
return !!e && typeof e.kid === "string" && typeof e.key === "string" && typeof e.alg === "string" && ALGS.has(e.alg);
|
|
38
|
-
});
|
|
56
|
+
v = JSON.parse(raw);
|
|
39
57
|
}
|
|
40
58
|
catch {
|
|
59
|
+
// 同 parseApprovalHmacKeys:不转述 V8 的 message(它嵌输入片段)。这一口装的是**公**钥,但两个出口
|
|
60
|
+
// 的口径不该分岔 —— 一个「视情况才脱敏」的规矩,迟早会在错的那一侧被抄走。
|
|
61
|
+
onReject({ index: "-", reason: "not valid JSON (value withheld)" });
|
|
62
|
+
return [];
|
|
63
|
+
}
|
|
64
|
+
if (!Array.isArray(v)) {
|
|
65
|
+
onReject({ index: "-", reason: "not a JSON array of {kid,alg,key,status} entries" });
|
|
41
66
|
return [];
|
|
42
67
|
}
|
|
68
|
+
const out = [];
|
|
69
|
+
v.forEach((row, i) => {
|
|
70
|
+
const e = row;
|
|
71
|
+
if (!e || typeof e !== "object") {
|
|
72
|
+
onReject({ index: i, reason: "entry is not an object" });
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const bad = ["kid", "key"].filter((f) => typeof e[f] !== "string");
|
|
76
|
+
// alg 单列:缺席与「不在闭集」是两种手滑,但都必须点名 `alg` —— 运维看到字段名才知道去改哪一格
|
|
77
|
+
// (alg-confusion 防线就挂在这条钉上,静默滤掉等于防线自己消失了没人知道)。
|
|
78
|
+
if (typeof e.alg !== "string")
|
|
79
|
+
bad.push("alg");
|
|
80
|
+
else if (!ALGS.has(e.alg))
|
|
81
|
+
bad.push(`alg (must be one of: ${[...ALGS].join(", ")})`);
|
|
82
|
+
if (bad.length > 0)
|
|
83
|
+
onReject({ index: i, reason: `missing/invalid field(s): ${bad.join(", ")}` });
|
|
84
|
+
else
|
|
85
|
+
out.push(e);
|
|
86
|
+
});
|
|
87
|
+
return out;
|
|
43
88
|
}
|
|
44
89
|
//# sourceMappingURL=auth-keys.js.map
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type BackgroundAgentRecord, type ToolExecuteContext } from "@sema-agent/core";
|
|
1
|
+
import { type BackgroundAgentRecord, type RunnerDeps, type ToolExecuteContext } from "@sema-agent/core";
|
|
2
2
|
import type { ServiceConfig } from "../config.js";
|
|
3
3
|
import type { RunnerDepsOnAsk } from "./runner-deps.js";
|
|
4
4
|
import { type ApprovalBaselineConfigView, type DeploymentGovernanceConfigView, type LiveQuestionFace } from "../deployment-governance.js";
|
|
@@ -46,6 +46,20 @@ export interface ParkedReviveGateDeps {
|
|
|
46
46
|
*/
|
|
47
47
|
readonly approverSeat: RunnerDepsOnAsk | undefined;
|
|
48
48
|
readonly approvalExemptionStore: ApprovalExemptionProbe | undefined;
|
|
49
|
+
/**
|
|
50
|
+
* A-010.1 —— core `RunnerDeps.runtimeCapsResolver` 的**同一只**(main.ts 的 `runtimeCapsResolver`,
|
|
51
|
+
* 单一属主同 `boot/runtime-caps.ts`)。重建条目的第三位决议链元数据 `autoModeArmed` 的唯一判据是
|
|
52
|
+
* per-principal 的 `RuntimeCaps.autoMode`,而它只有 center 解得出 —— 见 {@link autoModePostureOf}。
|
|
53
|
+
* 缺席(无 center 的部署 / dry-run)⇒ core 侧 `runtimeCaps` 也恒 undefined ⇒ 该位在 park 时也从不置位,
|
|
54
|
+
* 两侧同为「不供」,逐字零行为差。
|
|
55
|
+
*
|
|
56
|
+
* 类型从 **core 的载体位**推导(同 `boot/runner-deps.ts:94` 的先例,理由同 {@link RebuiltInheritedGate}:
|
|
57
|
+
* 手抄一个等价签名会让 core 改这条 seam 的那天变成静默漂移而不是编译期事件)。
|
|
58
|
+
*/
|
|
59
|
+
readonly resolveRuntimeCaps?: RunnerDeps["runtimeCapsResolver"];
|
|
60
|
+
/** `RunnerDeps.autoMode` 分类器面(信任门半场)在不在场 —— core 的武装条件是
|
|
61
|
+
* `runtimeCaps.autoMode === true` **∧** 这一面在场,两个半场缺一不武装(prepare-task 原式)。 */
|
|
62
|
+
readonly autoModeSeatMounted?: boolean;
|
|
49
63
|
readonly logger: {
|
|
50
64
|
info(event: string, fields?: Record<string, unknown>): void;
|
|
51
65
|
warn(event: string, fields?: Record<string, unknown>): void;
|
|
@@ -97,6 +111,8 @@ export type RebuiltParentConstraint = NonNullable<RebuiltInheritedGate["parentCo
|
|
|
97
111
|
* 时点语义(成文进 docs/ASSISTANT-WIRE-CONTRACT.md,免运维当 bug 报):
|
|
98
112
|
* · toolPolicy 轴 = **当前** config(与 resume 重折一致,收紧与放松都被兑现——放松是一次显式运维动作);
|
|
99
113
|
* · shellGate 轴 = core 取 max(live, park 时 seed)—— 收紧兑现、放松**不**兑现(fail-safe)。
|
|
114
|
+
* · `autoModeArmed` 轴(A-010.1)= 按**当前** per-principal entitlement 现解(见 {@link autoModePostureOf});
|
|
115
|
+
* 与 toolPolicy 轴同族:挂起期间权益被撤/被授 ⇒ 摘要对不上 ⇒ core pre-CAS 响亮拒,不静默换姿势跑。
|
|
100
116
|
* · `onAsk` 轴 = 部署的活体审批席(`deps.approverSeat`),**在场即供**:park 时那一层冻的就是它。
|
|
101
117
|
* 席位缺席的部署上不供,而**这不等于 auto-deny**(codex R3-中1 验真纠正):此形恒带
|
|
102
118
|
* `durableMandate`(见 {@link mandatePostureOf}),继承层判 `ask` 时 core 只要还看得见 checkpoint 店
|
|
@@ -107,5 +123,5 @@ export type RebuiltParentConstraint = NonNullable<RebuiltInheritedGate["parentCo
|
|
|
107
123
|
* ask 门、scratchpad 豁免……整类)park 时未持久化 ⇒ 重建不出。§2.3 已披露的残余,机制面有特征化钉
|
|
108
124
|
* (test/parked-revive-e2e.test.ts);真 settings→park→赎回的端到端钉未建(候件,随收口件一起)。
|
|
109
125
|
*/
|
|
110
|
-
export declare function createParkedReviveInheritedGate(deps: ParkedReviveGateDeps): (row: BackgroundAgentRecord) => RebuiltInheritedGate
|
|
126
|
+
export declare function createParkedReviveInheritedGate(deps: ParkedReviveGateDeps): (row: BackgroundAgentRecord) => Promise<RebuiltInheritedGate>;
|
|
111
127
|
//# sourceMappingURL=parked-revive-gate.d.ts.map
|