@sema-agent/server 7.65.0 → 7.67.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/USAGE.md +6 -3
- package/dist/approval-card.d.ts +8 -0
- package/dist/approval.d.ts +3 -0
- package/dist/approval.js +2 -1
- package/dist/boot/governance-seams.js +1 -1
- package/dist/boot/resolve-spec.js +12 -5
- package/dist/boot/stores.js +3 -3
- package/dist/capabilities/hands-lane.d.ts +3 -1
- package/dist/capabilities/hands-lane.js +3 -3
- package/dist/config-catalog.js +2 -2
- package/dist/config-center/apply-effective.js +1 -1
- package/dist/config-center/skills-mcp.d.ts +3 -3
- package/dist/config-center/skills-mcp.js +17 -4
- package/dist/config-center/types.d.ts +11 -0
- package/dist/config-provider.js +1 -0
- package/dist/config-types.d.ts +27 -4
- package/dist/config.d.ts +25 -3
- package/dist/config.js +58 -30
- package/dist/deployment-governance.d.ts +8 -0
- package/dist/http/routes/approvals-assistant.js +18 -0
- package/dist/http/routes/diagnostics.d.ts +27 -2
- package/dist/http/routes/diagnostics.js +21 -1
- package/dist/http/routes/rules.js +15 -10
- package/dist/http/routes/tasks.js +9 -9
- package/dist/http/server.js +4 -1
- package/dist/mcp-tool-faces.d.ts +64 -0
- package/dist/mcp-tool-faces.js +93 -0
- package/dist/observability/fail-open.d.ts +8 -0
- package/dist/observability/fail-open.js +8 -0
- package/dist/observability/metrics.js +1 -1
- package/dist/permission-rule-vocab.d.ts +13 -21
- package/dist/permission-rule-vocab.js +6 -5
- package/dist/plugins/checkpoint-store-sql.d.ts +68 -7
- package/dist/plugins/checkpoint-store-sql.js +90 -25
- package/dist/plugins/local-checkpoint-store.d.ts +19 -1
- package/dist/plugins/local-checkpoint-store.js +6 -1
- package/dist/plugins/permission-rule-store-file.d.ts +3 -5
- package/dist/plugins/permission-rule-store-file.js +2 -1
- package/dist/plugins/permission-rule-store-sql.d.ts +10 -10
- package/dist/plugins/permission-rule-store-sql.js +35 -20
- package/dist/plugins/pg-pool.js +5 -0
- package/dist/plugins/tidb-pool.js +11 -0
- package/dist/posture-source.d.ts +57 -0
- package/dist/posture-source.js +27 -0
- package/dist/read-face-posture.d.ts +9 -4
- package/dist/read-face-posture.js +7 -11
- package/dist/rules-consent.d.ts +15 -10
- package/dist/rules-consent.js +41 -21
- package/dist/session-titler.js +3 -12
- package/dist/task-mcp.d.ts +18 -4
- package/dist/task-mcp.js +23 -4
- package/dist/task-settings.d.ts +30 -0
- package/dist/task-settings.js +39 -18
- package/dist/tool-approval.d.ts +5 -4
- package/dist/tool-approval.js +3 -3
- package/dist/trace/core-keyset-guard.d.ts +2 -2
- package/dist/trace/project.d.ts +22 -0
- package/dist/trace/project.js +21 -1
- package/package.json +4 -4
|
@@ -3,7 +3,7 @@ import { createHash } from "node:crypto";
|
|
|
3
3
|
import { existsSync, readFileSync, readdirSync, renameSync, unlinkSync, mkdirSync } from "node:fs";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { isApprovalGateKind } from "../tool-approval.js";
|
|
6
|
-
import { FileCheckpointStore, atomicWriteFile, sanitizePathComponent, } from "@sema-agent/core";
|
|
6
|
+
import { FileCheckpointStore, atomicWriteFile, executionVerdict, sanitizePathComponent, } from "@sema-agent/core";
|
|
7
7
|
import { approvalDecisionOfWinner, approvalPayloadFingerprint, boundedRuleOffers, boundedToolInput, pendingRowPrecedes, TERMINAL_BACKSTOP_MS, TERMINAL_GRACE_MS } from "./checkpoint-store-sql.js";
|
|
8
8
|
import { UNMANAGED_RETENTION } from "./retention-store-sql.js";
|
|
9
9
|
function terminalAtOf(cp) {
|
|
@@ -17,6 +17,7 @@ export class LocalCheckpointStore {
|
|
|
17
17
|
durability = "durable";
|
|
18
18
|
fidelity = "json";
|
|
19
19
|
redecision = { reopen: true };
|
|
20
|
+
execution = { outcome: true };
|
|
20
21
|
inner;
|
|
21
22
|
scopesPath;
|
|
22
23
|
sessionTokensPath;
|
|
@@ -95,6 +96,9 @@ export class LocalCheckpointStore {
|
|
|
95
96
|
reopen(token, scope, reason) {
|
|
96
97
|
return this.inner.reopen(token, scope, reason);
|
|
97
98
|
}
|
|
99
|
+
recordExecutionOutcome(token, scope, gate) {
|
|
100
|
+
return this.inner.recordExecutionOutcome(token, scope, gate);
|
|
101
|
+
}
|
|
98
102
|
setPendingSteer(token, scope, steer) {
|
|
99
103
|
return this.inner.setPendingSteer(token, scope, steer);
|
|
100
104
|
}
|
|
@@ -227,6 +231,7 @@ export class LocalCheckpointStore {
|
|
|
227
231
|
boundInputHash: pa?.boundInputHash ?? null,
|
|
228
232
|
decision: approvalDecisionOfWinner(cp.resolvedOutcome, boundCallId),
|
|
229
233
|
payloadFp: cp.resolvedOutcome === undefined ? null : approvalPayloadFingerprint(cp.resolvedOutcome),
|
|
234
|
+
...((v) => ({ executionOutcome: v.kind === "executed" ? v.gate : null }))(executionVerdict(cp)),
|
|
230
235
|
});
|
|
231
236
|
}
|
|
232
237
|
return out;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { FileDurableRulePartitionProvider, type RuleApprovalRecord, type RuleApprovalRecordStore, type
|
|
1
|
+
import { FileDurableRulePartitionProvider, type RuleApprovalRecord, type RuleApprovalRecordStore, type StaleRuleApprovalRecord } from "@sema-agent/core";
|
|
2
2
|
import { type RuleImportTicket, type RuleTicketPurpose, type RuleTicketReclaimResult, type RuleTicketRedeemResult, type RuleTicketSnapshot } from "./permission-rule-store-sql.js";
|
|
3
3
|
import type { PermissionRuleStoreBundle } from "../rules-consent.js";
|
|
4
|
+
import { type RuleCandidate } from "@sema-agent/core";
|
|
4
5
|
/**
|
|
5
6
|
* `RuleApprovalRecordStore` 的 File 形。
|
|
6
7
|
*
|
|
@@ -62,10 +63,7 @@ export declare class FileRuleImportTicketStore {
|
|
|
62
63
|
principal: string;
|
|
63
64
|
approvalId: string;
|
|
64
65
|
purpose: RuleTicketPurpose;
|
|
65
|
-
candidates: ReadonlyArray<
|
|
66
|
-
rule: string;
|
|
67
|
-
scope: RuleScope;
|
|
68
|
-
}>;
|
|
66
|
+
candidates: ReadonlyArray<RuleCandidate>;
|
|
69
67
|
ttlMs: number;
|
|
70
68
|
}): Promise<RuleImportTicket>;
|
|
71
69
|
/** 一次性**认领**。五条否定项的判序与 SQL 形逐字相同(unknown → wrong-principal → wrong-purpose →
|
|
@@ -6,12 +6,13 @@ import { FileDurableRulePartitionProvider, AppendLog, ensureDir, readJsonlRecord
|
|
|
6
6
|
import { z } from "zod";
|
|
7
7
|
import { buildRulePayloadHash } from "./permission-rule-store-sql.js";
|
|
8
8
|
import { EDITED_RULE_BREADTH_WARNING_CODES, RULE_APPROVAL_RECORD_SCHEMA, UNCOVERED_SEGMENT_REASONS } from "../permission-rule-vocab.js";
|
|
9
|
+
import { RULE_BEHAVIORS } from "@sema-agent/core";
|
|
9
10
|
const RuleScopeSchema = z.union([
|
|
10
11
|
z.object({ kind: z.literal("global") }).strict(),
|
|
11
12
|
z.object({ kind: z.literal("project"), root: z.string().min(1) }).strict(),
|
|
12
13
|
]);
|
|
13
14
|
const RuleDotSchema = z.object({ actor: z.string().min(1), counter: z.number().int().nonnegative().safe() }).strict();
|
|
14
|
-
const RuleCandidateSchema = z.object({ rule: z.string(), scope: RuleScopeSchema }).strict();
|
|
15
|
+
const RuleCandidateSchema = z.object({ behavior: z.enum(RULE_BEHAVIORS), rule: z.string(), scope: RuleScopeSchema }).strict();
|
|
15
16
|
const RuleOffer2Schema = z.union([
|
|
16
17
|
z.object({ kind: z.literal("single"), candidate: z.number().int().nonnegative().safe() }).strict(),
|
|
17
18
|
z
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { type PermissionRuleWriter, type DurableRulePartition, type DurableRulePartitionProvider, type RuleScope, type RuleSyncFrontier, 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 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";
|
|
@@ -274,11 +274,14 @@ export declare const SQL_DB_NOW_MS: {
|
|
|
274
274
|
readonly tidb: "CAST(UNIX_TIMESTAMP(NOW(3)) * 1000 AS SIGNED)";
|
|
275
275
|
readonly pg: "(EXTRACT(EPOCH FROM clock_timestamp()) * 1000)::bigint";
|
|
276
276
|
};
|
|
277
|
-
/** prepare 时刻候选集的规范摘要(载荷绑定 F1 ④ 的判据)。纯数据 ⇒ `build*`。
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
}
|
|
277
|
+
/** prepare 时刻候选集的规范摘要(载荷绑定 F1 ④ 的判据)。纯数据 ⇒ `build*`。
|
|
278
|
+
*
|
|
279
|
+
* 🔴 入参型收到 core 的 `RuleCandidate`(core 7.9.0 [ref] 起含 `behavior`)—— 规则的身份是
|
|
280
|
+
* **(behavior, 文本, 作用域) 三元组**,而这道摘要正是「prepare 那一刻的候选集」与「redeem 那一刻的
|
|
281
|
+
* 候选集」之间的等式。此前的形参写成 `{rule, scope}` 二元组:序列化器吃的是整只对象所以运行期**恰好**
|
|
282
|
+
* 覆盖了 behavior,但那是巧合不是保证 —— 哪天有人从一个只有二元组的地方喂进来,一次把候选从 `deny`
|
|
283
|
+
* 改写成 `allow`(文本一个字节不动)在这道等式下就认不出来了。收到域型让机器替人守这一条。 */
|
|
284
|
+
export declare function buildRulePayloadHash(candidates: ReadonlyArray<RuleCandidate>): string;
|
|
282
285
|
/**
|
|
283
286
|
* 票面三态协议的 SQL 实现。
|
|
284
287
|
*
|
|
@@ -315,10 +318,7 @@ export declare class SqlRuleImportTicketStore {
|
|
|
315
318
|
principal: string;
|
|
316
319
|
approvalId: string;
|
|
317
320
|
purpose: RuleTicketPurpose;
|
|
318
|
-
candidates: ReadonlyArray<
|
|
319
|
-
rule: string;
|
|
320
|
-
scope: RuleScope;
|
|
321
|
-
}>;
|
|
321
|
+
candidates: ReadonlyArray<RuleCandidate>;
|
|
322
322
|
ttlMs: number;
|
|
323
323
|
}): Promise<RuleImportTicket>;
|
|
324
324
|
/**
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
-
import { applyTombstones, assertDeleteDeltaCarriesNoAdd, assertRedemptionNotQuarantined, assertWriteDeltaScopeDurable, foldDelta,
|
|
3
|
+
import { applyTombstones, assertDeleteDeltaCarriesNoAdd, assertRedemptionNotQuarantined, assertWriteDeltaScopeDurable, foldDelta, parseRuleText, readRowBehavior, screenRuleSyncState, PERMISSION_RULE_WRITER, RULE_BEHAVIORS, RULE_SYNC_DROP_CODES, EMPTY_DURABLE_RULE_PARTITION, } from "@sema-agent/core";
|
|
4
4
|
import { dialectProtocolJsonEncoder } from "./sql-driver.js";
|
|
5
5
|
import { isMissingColumnError } from "./sql-errors.js";
|
|
6
6
|
import { canonicalJson } from "../canonical-json.js";
|
|
7
|
-
import { EDITED_RULE_BREADTH_WARNING_CODES, PERSISTED_RULE_MATCHES,
|
|
7
|
+
import { EDITED_RULE_BREADTH_WARNING_CODES, PERSISTED_RULE_MATCHES, isPersistedRuleTool, RULE_APPROVAL_RECORD_SCHEMA, UNCOVERED_SEGMENT_REASONS } from "../permission-rule-vocab.js";
|
|
8
8
|
export function writerOfSqlRuleStore(store) {
|
|
9
9
|
const w = Reflect.get(store, PERMISSION_RULE_WRITER);
|
|
10
10
|
if (w === null || typeof w !== "object")
|
|
@@ -24,10 +24,11 @@ const RuleDotSchema = z.object({ actor: z.string().min(1), counter: z.number().i
|
|
|
24
24
|
const RuleAddSchema = z
|
|
25
25
|
.object({ dot: RuleDotSchema, origin: z.enum(["user", "imported-cc", "starter"]), createdAt: z.string() })
|
|
26
26
|
.strict();
|
|
27
|
-
const
|
|
27
|
+
const PersistedRuleRowSchema = z
|
|
28
28
|
.object({
|
|
29
|
+
behavior: z.string().optional(),
|
|
29
30
|
rule: z.string(),
|
|
30
|
-
tool: z.
|
|
31
|
+
tool: z.string().min(1).refine(isPersistedRuleTool, { message: "not a tool the persisted-rule grammar speaks for" }),
|
|
31
32
|
match: z.enum(PERSISTED_RULE_MATCHES),
|
|
32
33
|
command: z.string(),
|
|
33
34
|
scope: DurableRuleScopeSchema,
|
|
@@ -35,14 +36,15 @@ const PersistedAllowRuleSchema = z
|
|
|
35
36
|
})
|
|
36
37
|
.strict();
|
|
37
38
|
const RuleTombstoneSchema = z
|
|
38
|
-
.object({ rule: z.string(), scope: DurableRuleScopeSchema, removedDots: z.array(RuleDotSchema).min(1), deletedBy: RuleDotSchema })
|
|
39
|
+
.object({ behavior: z.string().optional(), rule: z.string(), scope: DurableRuleScopeSchema, removedDots: z.array(RuleDotSchema).min(1), deletedBy: RuleDotSchema })
|
|
39
40
|
.strict();
|
|
40
41
|
const QUARANTINE_REASONS = new Set(Object.entries(RULE_SYNC_DROP_CODES)
|
|
41
42
|
.filter(([, side]) => side.includes("local-quarantined"))
|
|
42
43
|
.map(([code]) => code));
|
|
43
44
|
const isQuarantineReason = (v) => typeof v === "string" && QUARANTINE_REASONS.has(v);
|
|
44
|
-
const
|
|
45
|
+
const QuarantinedRuleAddRowSchema = z
|
|
45
46
|
.object({
|
|
47
|
+
behavior: z.string().optional(),
|
|
46
48
|
rule: z.string(),
|
|
47
49
|
scope: DurableRuleScopeSchema,
|
|
48
50
|
add: RuleAddSchema,
|
|
@@ -50,8 +52,18 @@ const QuarantinedRuleAddSchema = z
|
|
|
50
52
|
atMs: z.number().int().nonnegative().safe(),
|
|
51
53
|
})
|
|
52
54
|
.strict();
|
|
55
|
+
function readBucketRowBehavior(raw, what, ownerKey) {
|
|
56
|
+
const behavior = readRowBehavior(raw);
|
|
57
|
+
if (behavior === undefined) {
|
|
58
|
+
throw new Error(`permission_rule bucket ${ownerKey} carries a ${what} whose behavior word is outside the closed set (${JSON.stringify(raw)}) — ` +
|
|
59
|
+
`refusing the whole bucket rather than reading a standing deny as "no rule"`);
|
|
60
|
+
}
|
|
61
|
+
return behavior;
|
|
62
|
+
}
|
|
63
|
+
const WritablePersistedRuleSchema = PersistedRuleRowSchema.extend({ behavior: z.enum(RULE_BEHAVIORS) });
|
|
64
|
+
const WritableRuleTombstoneSchema = RuleTombstoneSchema.extend({ behavior: z.enum(RULE_BEHAVIORS) });
|
|
53
65
|
const FrontierSchema = z.record(z.string(), z.number().int().nonnegative().safe());
|
|
54
|
-
const RuleCandidateSchema = z.object({ rule: z.string(), scope: DurableRuleScopeSchema }).strict();
|
|
66
|
+
const RuleCandidateSchema = z.object({ behavior: z.enum(RULE_BEHAVIORS), rule: z.string(), scope: DurableRuleScopeSchema }).strict();
|
|
55
67
|
const RedeemedDotsSchema = z.record(z.string(), RuleDotSchema);
|
|
56
68
|
const RuleEditedSchema = z
|
|
57
69
|
.object({ index: z.number().int().nonnegative().safe(), text: z.string(), at: z.string(), warnings: z.array(z.enum(EDITED_RULE_BREADTH_WARNING_CODES)).optional() })
|
|
@@ -108,9 +120,9 @@ export const TIDB_PERMISSION_RULE_STATEMENTS = [
|
|
|
108
120
|
state VARCHAR(32) NOT NULL,
|
|
109
121
|
rev BIGINT NOT NULL DEFAULT 0,
|
|
110
122
|
candidates_json LONGTEXT NOT NULL,
|
|
111
|
-
-- version:**数据格式版本**(= core \`RuleApprovalRecord.schema\`,本形恒
|
|
123
|
+
-- version:**数据格式版本**(= core \`RuleApprovalRecord.schema\`,本形恒 4 —— core 7.9.0 #625 由 3 升,
|
|
112
124
|
-- 代码里的单点是 RULE_APPROVAL_RECORD_SCHEMA;OCC 守卫另在 rev,两轴禁混,schema-naming 门③ 白名单
|
|
113
|
-
-- 登记)。读侧对非
|
|
125
|
+
-- 登记)。读侧对非 4 的行(含上一形的 3)交还 design/375 §4.5 的
|
|
114
126
|
-- StaleRuleApprovalRecord 信封(identity 键 only)—— 本仓 SCHEMA POLICY 是删表重建,重建后的表
|
|
115
127
|
-- 不该有这种行,这一臂是「未来再换形」的防御位,绝不静默 widen 成一条本形担保不了的记录。
|
|
116
128
|
version INT NOT NULL,
|
|
@@ -263,7 +275,7 @@ export async function ensurePgPermissionRuleSchema(q) {
|
|
|
263
275
|
rev BIGINT NOT NULL DEFAULT 0,
|
|
264
276
|
candidates_json TEXT COLLATE "C" NOT NULL,
|
|
265
277
|
-- version / offers_json / selected_offer:design/375 换形三列(MySQL 孪生的行内注写了理由:
|
|
266
|
-
-- version=数据格式版本恒
|
|
278
|
+
-- version=数据格式版本恒 4(core 7.9.0 #625 由 3 升;单点 RULE_APPROVAL_RECORD_SCHEMA),非 4 的行(含 3)
|
|
267
279
|
-- 读成 Stale 信封;offers_json=按 index 引用的 OPTION 结构,
|
|
268
280
|
-- 禁存文本副本;selected_offer=确认的 OFFER index,与 ticket/redeemed_dots 的 CANDIDATE index
|
|
269
281
|
-- 两空间分离)。
|
|
@@ -356,10 +368,18 @@ class SqlDurableRulePartition {
|
|
|
356
368
|
const row = rows[0];
|
|
357
369
|
if (row === undefined)
|
|
358
370
|
return { ...EMPTY_BUCKET, rules: [], tombstones: [], quarantined: [] };
|
|
359
|
-
const
|
|
360
|
-
const
|
|
361
|
-
const
|
|
371
|
+
const rulesRaw = parseColumn(z.array(PersistedRuleRowSchema), row.rules_json, "rules_json", this.ownerKey);
|
|
372
|
+
const tombstonesRaw = parseColumn(z.array(RuleTombstoneSchema), row.tombstones_json, "tombstones_json", this.ownerKey);
|
|
373
|
+
const quarantinedRaw = parseColumn(z.array(QuarantinedRuleAddRowSchema), row.quarantined_json, "quarantined_json", this.ownerKey);
|
|
362
374
|
const observedVector = row.observed_vector_json === null || row.observed_vector_json === undefined ? undefined : parseColumn(FrontierSchema, row.observed_vector_json, "observed_vector_json", this.ownerKey);
|
|
375
|
+
const expected = buildRuleBucketChecksum({ rules: rulesRaw, tombstones: tombstonesRaw, quarantined: quarantinedRaw, ...(observedVector !== undefined ? { observedVector } : {}) });
|
|
376
|
+
if (String(row.checksum ?? "") !== expected) {
|
|
377
|
+
throw new Error(`permission_rule bucket ${this.ownerKey} failed its integrity check (stored ${String(row.checksum)}, computed ${expected}) — ` +
|
|
378
|
+
`refusing the whole bucket rather than reporting a partial rule set`);
|
|
379
|
+
}
|
|
380
|
+
const rules = rulesRaw.map((r) => ({ ...r, behavior: readBucketRowBehavior(r.behavior, "rule row", this.ownerKey) }));
|
|
381
|
+
const tombstones = tombstonesRaw.map((t) => ({ ...t, behavior: readBucketRowBehavior(t.behavior, "tombstone", this.ownerKey) }));
|
|
382
|
+
const quarantined = quarantinedRaw.map((q) => ({ ...q, behavior: readBucketRowBehavior(q.behavior, "quarantined row", this.ownerKey) }));
|
|
363
383
|
const state = {
|
|
364
384
|
actor: String(row.actor ?? ""),
|
|
365
385
|
counter: Number(row.counter ?? 0),
|
|
@@ -369,13 +389,8 @@ class SqlDurableRulePartition {
|
|
|
369
389
|
quarantined,
|
|
370
390
|
...(observedVector !== undefined ? { observedVector } : {}),
|
|
371
391
|
};
|
|
372
|
-
const expected = buildRuleBucketChecksum({ rules, tombstones, quarantined, ...(observedVector !== undefined ? { observedVector } : {}) });
|
|
373
|
-
if (String(row.checksum ?? "") !== expected) {
|
|
374
|
-
throw new Error(`permission_rule bucket ${this.ownerKey} failed its integrity check (stored ${String(row.checksum)}, computed ${expected}) — ` +
|
|
375
|
-
`refusing the whole bucket rather than reporting a partial rule set`);
|
|
376
|
-
}
|
|
377
392
|
const badTombstone = tombstones.find((t) => {
|
|
378
|
-
const parsed =
|
|
393
|
+
const parsed = parseRuleText(t.rule, t.behavior);
|
|
379
394
|
return "reject" in parsed || parsed.rule.rule !== t.rule;
|
|
380
395
|
});
|
|
381
396
|
if (badTombstone !== undefined) {
|
|
@@ -467,7 +482,7 @@ class SqlDurableRulePartition {
|
|
|
467
482
|
assertDeleteDeltaCarriesNoAdd(delta);
|
|
468
483
|
nextTombstones = [...cur.tombstones, delta.tombstone];
|
|
469
484
|
}
|
|
470
|
-
const writable = z.object({ rules: z.array(
|
|
485
|
+
const writable = z.object({ rules: z.array(WritablePersistedRuleSchema), tombstones: z.array(WritableRuleTombstoneSchema) }).safeParse({ rules: nextRules, tombstones: nextTombstones });
|
|
471
486
|
if (!writable.success) {
|
|
472
487
|
throw new Error(`refusing to write a permission_rule bucket this backend could not read back: ${writable.error.message}`);
|
|
473
488
|
}
|
package/dist/plugins/pg-pool.js
CHANGED
|
@@ -95,6 +95,11 @@ export const PG_SCHEMA_STATEMENTS = [
|
|
|
95
95
|
-- has_bidi_controls(S-30① / #356②):bidi 在场位,语义与「1 或 NULL、恒不写 0」见 MySQL 孪生同名列的
|
|
96
96
|
-- 行内注。clay 批 TINYINT(1) 归一先例:PG 侧统一 SMALLINT。
|
|
97
97
|
has_bidi_controls SMALLINT,
|
|
98
|
+
-- execution_outcome / execution_at_ms(S-160;core 7.7.0 #602):语义、两轴的分工、CAS 与冲突的
|
|
99
|
+
-- 「记录物理不可变」由来见 MySQL 孪生(tidb-pool.ts 同两列的行内注)。载荷是 core 的 GateOutcome
|
|
100
|
+
-- JSON ⇒ 与同表 outcome 列同取 JSONB(协议信封无损,IS NULL 谓词两方言同义)。
|
|
101
|
+
execution_outcome JSONB,
|
|
102
|
+
execution_at_ms BIGINT,
|
|
98
103
|
PRIMARY KEY (token)
|
|
99
104
|
)`,
|
|
100
105
|
`CREATE INDEX IF NOT EXISTS idx_checkpoint_status_deadline ON checkpoint (status, deadline)`,
|
|
@@ -266,6 +266,17 @@ export const SCHEMA_STATEMENTS = [
|
|
|
266
266
|
-- (缺席 = 没检出 ≠ 干净),一个 0 会把「没检出」谎成「已确认干净」。老行(本列诞生前 park 的)
|
|
267
267
|
-- NULL ⇒ 读口省键。clay 批 2026-07-26 TINYINT(1) 归一先例:布尔列 TiDB 侧 TINYINT(1)。
|
|
268
268
|
has_bidi_controls TINYINT(1) NULL,
|
|
269
|
+
-- execution_outcome / execution_at_ms(S-160;core 7.7.0 #602 的耐久半场):**决定被怎么处置了**的
|
|
270
|
+
-- 结算记录 —— resume 腿上那次调用的 tool_end 帧携带的 GateOutcome,与 outcome 列(决定本身,不可变)
|
|
271
|
+
-- 是两个轴:一个人批了(outcome 里 disposition=allowed),部署自己的复核在 post-CAS 否了他改过的参数
|
|
272
|
+
-- (execution_outcome 里 denied/policy)—— 流断了之后从行重建这条 run 的消费者,此前只读得到前半句。
|
|
273
|
+
-- 写口是 recordExecutionOutcome 的 CAS(status='resolved' AND execution_outcome IS NULL),**恰好一次**;
|
|
274
|
+
-- 第二次写同一份记录是幂等,写不同的一份是 checkpoint.execution_outcome_conflict(记录物理不可变 ——
|
|
275
|
+
-- 两份不同的执行记录意味着同一个已决动作被结算了两次,那是引擎缺陷,不是店可以吸收的条件)。
|
|
276
|
+
-- 缺席只有一个含义:**未知**,绝不是「放行过」(读面单口 core executionVerdict(cp))。
|
|
277
|
+
-- 两列同生同灭(stamp 在场 ⇔ 记录在场);reopen 见到记录即拒(已结算的动作不可重开)。
|
|
278
|
+
execution_outcome JSON NULL,
|
|
279
|
+
execution_at_ms BIGINT NULL,
|
|
269
280
|
PRIMARY KEY (token),
|
|
270
281
|
KEY idx_checkpoint_status_deadline (status, deadline),
|
|
271
282
|
KEY idx_checkpoint_session_status (session_id, status)
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* S-167 → S-178 —— **一个部署级旋钮的值是谁定的**,四词闭集 + 一句运维指路。
|
|
3
|
+
*
|
|
4
|
+
* ## 为什么是一个共享件而不是每键一份
|
|
5
|
+
*
|
|
6
|
+
* S-167 给 `READ_FACE` 造了这套东西(值 + 来源 + 指路句),理由写在 `read-face-posture.ts` 的顶注里:
|
|
7
|
+
* 「缺席」这一态里塞着两件不同的事(单机 turnkey 没表态 vs 多租户就是要引擎默认),而把它们分开之后,
|
|
8
|
+
* **env 恒赢的判据**与**运维面「为什么这台机器是这个值」**才有真答案。S-178 把同一条律推到另外三个键
|
|
9
|
+
* (`STREAM_ASK_WINDOW_MS` / `DURABLE_APPROVAL` / `SESSION_AUTO_TITLE`)时,唯一诚实的做法是**抽这一份**
|
|
10
|
+
* ——四词表抄第二遍的那天,两份表会在加词时分家,而「这个值是谁定的」在运维面上是个是非题,不能有两个答案。
|
|
11
|
+
*
|
|
12
|
+
* ## 优先序(与 `config.ts` 的 `postureOn` 同形)
|
|
13
|
+
* `env 显式` > `center 发布` > `posture 派生` > `引擎/服务端默认`。
|
|
14
|
+
*
|
|
15
|
+
* ## 定界
|
|
16
|
+
* 本模块只说**来源**。值本身、以及「这台机器凭什么派生出这个值」那句话,由每个键自己给
|
|
17
|
+
* ({@link PostureKnobFacts.postureMeaning})——那句话是键专属的事实,共享它只会得到一句谁都不认的空话。
|
|
18
|
+
*/
|
|
19
|
+
/** 旋钮取值来源的闭集词表(顺序 = 优先序)。 */
|
|
20
|
+
export declare const POSTURE_SOURCE_WORDS: readonly ["env", "center", "posture", "engine-default"];
|
|
21
|
+
/** 一个旋钮的取值来源(闭集四词)。加词 ⇒ {@link postureSourceNote} 的穷举 switch 编译红。 */
|
|
22
|
+
export type PostureSource = (typeof POSTURE_SOURCE_WORDS)[number];
|
|
23
|
+
/** 一个旋钮报出「我是谁定的」所需要的三件事。 */
|
|
24
|
+
export interface PostureKnobFacts {
|
|
25
|
+
/** 这台机器上占住这个键的 env 变量名(指路句要逐字点它,运维才知道去改哪个)。 */
|
|
26
|
+
readonly knob: string;
|
|
27
|
+
readonly source: PostureSource;
|
|
28
|
+
/**
|
|
29
|
+
* `posture` 那一臂**专属**的一句话:这台机器为什么派生出了这个值,以及怎么把它钉回去。
|
|
30
|
+
* 只在 `source === "posture"` 时被读到 —— 别的臂的句子是通用的(见 {@link postureSourceNote})。
|
|
31
|
+
*/
|
|
32
|
+
readonly postureMeaning: string;
|
|
33
|
+
}
|
|
34
|
+
/** 运维指路句 —— **穷举 switch,无缺省臂**:四词闭集加一个成员这里就编译红。 */
|
|
35
|
+
export declare function postureSourceNote(facts: PostureKnobFacts): string;
|
|
36
|
+
/**
|
|
37
|
+
* 这个值是**运维显式表态**的吗?
|
|
38
|
+
*
|
|
39
|
+
* 🔴 S-178 施工时真踩(本批全量抓出):`hasOperatorGateIntent`(`src/approval.ts`)判「运维表达过门意图
|
|
40
|
+
* 没有」时读的是 `durableApproval` 这个**值** —— 那在本键进 posture 家族之前是对的(它只有显式设过才为
|
|
41
|
+
* 真),之后当场失真:单机 turnkey 上 posture 派生的 `true` 会被读成「运维要门」,于是 run-local 那条
|
|
42
|
+
* **结构上没有 durable 门**的腿在每次 boot 打一条补偿 warn,而它其实正处在「零门意图的既定姿势」里。
|
|
43
|
+
* 这与 S-167 头注①记的是**同一形**病(用一个会被 posture 铸出来的量当「有人表态过」的代理)。
|
|
44
|
+
*
|
|
45
|
+
* ⇒ 判据落到**来源**上,穷举 switch(四词闭集加员即编译红):`env`(这台机器的运维写下的)与 `center`
|
|
46
|
+
* (组织发布的)是表态;`posture`(姿态派生)与 `engine-default`(内建缺省)都**不是**「有人要求过」。
|
|
47
|
+
*/
|
|
48
|
+
export declare function isExplicitPosture(source: PostureSource): boolean;
|
|
49
|
+
/** operator 诊断面的一行读数:值 + 来源 + 指路句。三个 S-178 旋钮与 S-167 的 readFace 同形。 */
|
|
50
|
+
export interface PostureKnobReading<V> {
|
|
51
|
+
value: V;
|
|
52
|
+
source: PostureSource;
|
|
53
|
+
note: string;
|
|
54
|
+
}
|
|
55
|
+
/** 一行读数的铸点(纯数据 ⇒ `build*`)。 */
|
|
56
|
+
export declare function buildPostureKnobReading<V>(value: V, facts: PostureKnobFacts): PostureKnobReading<V>;
|
|
57
|
+
//# sourceMappingURL=posture-source.d.ts.map
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export const POSTURE_SOURCE_WORDS = ["env", "center", "posture", "engine-default"];
|
|
2
|
+
export function postureSourceNote(facts) {
|
|
3
|
+
switch (facts.source) {
|
|
4
|
+
case "env":
|
|
5
|
+
return `pinned by this machine's ${facts.knob} env var (deployment sovereignty — it wins over any published value for this key)`;
|
|
6
|
+
case "center":
|
|
7
|
+
return `applied from the config-center (restart-to-apply); set ${facts.knob} on this machine to take the key back`;
|
|
8
|
+
case "posture":
|
|
9
|
+
return facts.postureMeaning;
|
|
10
|
+
case "engine-default":
|
|
11
|
+
return `not pinned by this deployment — the built-in default for ${facts.knob} is in force`;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export function isExplicitPosture(source) {
|
|
15
|
+
switch (source) {
|
|
16
|
+
case "env":
|
|
17
|
+
case "center":
|
|
18
|
+
return true;
|
|
19
|
+
case "posture":
|
|
20
|
+
case "engine-default":
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export function buildPostureKnobReading(value, facts) {
|
|
25
|
+
return { value, source: facts.source, note: postureSourceNote(facts) };
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=posture-source.js.map
|
|
@@ -27,10 +27,14 @@
|
|
|
27
27
|
* 两者在 core 里是并列机制(deny 表在 open 面上照样拦)。core 的 `config.read_face_deployment_clamped`
|
|
28
28
|
* (只读挂载钳制)同样不受本件影响。
|
|
29
29
|
*/
|
|
30
|
-
|
|
30
|
+
import { type PostureSource } from "./posture-source.js";
|
|
31
|
+
/**
|
|
32
|
+
* 档位来源的闭集词表 —— **S-178 起转 `posture-source.ts` 的共享件**(本模块只是给这一档取个本地名字,
|
|
33
|
+
* 不再持第二份词表:同一张四词表抄两遍的那天,两份会在加词时分家)。
|
|
34
|
+
*/
|
|
31
35
|
export declare const READ_FACE_SOURCE_WORDS: readonly ["env", "center", "posture", "engine-default"];
|
|
32
|
-
/** 档位的来源(闭集四词)。加词 ⇒ {@link
|
|
33
|
-
export type ReadFaceSource =
|
|
36
|
+
/** 档位的来源(闭集四词)。加词 ⇒ {@link postureSourceNote} 的穷举 switch 编译红。 */
|
|
37
|
+
export type ReadFaceSource = PostureSource;
|
|
34
38
|
/** 一台 worker 的 READ 面档位事实(值 + 来源)。`ServiceConfigFlat` 结构上满足本形。 */
|
|
35
39
|
export interface ReadFacePostureFacts {
|
|
36
40
|
readonly readFace?: "open" | "roots" | undefined;
|
|
@@ -41,7 +45,8 @@ export interface ReadFacePostureFacts {
|
|
|
41
45
|
* 单用户 turnkey ⇒ `open`;多租户 ⇒ 不铸(引擎默认 `roots`,server 不复制上游默认)。
|
|
42
46
|
*/
|
|
43
47
|
export declare function readFaceForPosture(singleUserTurnkey: boolean): ReadFacePostureFacts;
|
|
44
|
-
/**
|
|
48
|
+
/** 本档的运维指路句 —— 骨架由共享件 {@link postureSourceNote} 给(四词穷举在那里,加词即编译红);
|
|
49
|
+
* 本函数只提供 `posture` 那一臂的**键专属**事实句。 */
|
|
45
50
|
export declare function readFaceSourceNote(source: ReadFaceSource): string;
|
|
46
51
|
/** operator 诊断面的投影(`GET /v1/diagnostics/wiring`)。`face:null` = 本部署没钉档,引擎默认接管。 */
|
|
47
52
|
export declare function buildReadFaceWiring(facts: ReadFacePostureFacts): {
|
|
@@ -1,18 +1,14 @@
|
|
|
1
|
-
|
|
1
|
+
import { POSTURE_SOURCE_WORDS, postureSourceNote } from "./posture-source.js";
|
|
2
|
+
export const READ_FACE_SOURCE_WORDS = POSTURE_SOURCE_WORDS;
|
|
2
3
|
export function readFaceForPosture(singleUserTurnkey) {
|
|
3
4
|
return singleUserTurnkey ? { readFace: "open", readFaceSource: "posture" } : { readFaceSource: "engine-default" };
|
|
4
5
|
}
|
|
5
6
|
export function readFaceSourceNote(source) {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
case "posture":
|
|
12
|
-
return "derived from the single-user turnkey posture (REQUIRE_PRINCIPAL is not set): reads outside the workspace roots are allowed by default — set READ_FACE=roots to pin the containing posture back";
|
|
13
|
-
case "engine-default":
|
|
14
|
-
return "not pinned by this deployment — the engine's own default (roots) is in force";
|
|
15
|
-
}
|
|
7
|
+
return postureSourceNote({
|
|
8
|
+
knob: "READ_FACE",
|
|
9
|
+
source,
|
|
10
|
+
postureMeaning: "derived from the single-user turnkey posture (REQUIRE_PRINCIPAL is not set): reads outside the workspace roots are allowed by default — set READ_FACE=roots to pin the containing posture back",
|
|
11
|
+
});
|
|
16
12
|
}
|
|
17
13
|
export function buildReadFaceWiring(facts) {
|
|
18
14
|
return { face: facts.readFace ?? null, source: facts.readFaceSource, note: readFaceSourceNote(facts.readFaceSource) };
|
package/dist/rules-consent.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type RuleOwner, type ImportPreview, type RedeemedBatchMember, type ImportedSettingsLayer, type
|
|
1
|
+
import { type RuleOwner, type ImportPreview, type RedeemedBatchMember, type ImportedSettingsLayer, type PersistedRule, type RuleBehavior, type RuleCandidate, type EffectivePermissionRule, type RemoveResult, type EditedRuleTextPrecheck, type RuleScope, type RuleOffer } from "@sema-agent/core";
|
|
2
2
|
import type { DurableRulePartitionProvider, PermissionRuleStoreProvider, RuleApprovalRecordStore } from "@sema-agent/core";
|
|
3
3
|
import { type RuleImportTicket, type RuleTicketPurpose, type RuleTicketReclaimResult, type RuleTicketRedeemResult, type RuleTicketSnapshot } from "./plugins/permission-rule-store-sql.js";
|
|
4
4
|
/**
|
|
@@ -20,10 +20,7 @@ export interface RuleConsentRecordStores {
|
|
|
20
20
|
principal: string;
|
|
21
21
|
approvalId: string;
|
|
22
22
|
purpose: RuleTicketPurpose;
|
|
23
|
-
candidates: ReadonlyArray<
|
|
24
|
-
rule: string;
|
|
25
|
-
scope: RuleScope;
|
|
26
|
-
}>;
|
|
23
|
+
candidates: ReadonlyArray<RuleCandidate>;
|
|
27
24
|
ttlMs: number;
|
|
28
25
|
}): Promise<RuleImportTicket>;
|
|
29
26
|
/** 一次**认领**(不是「已完成」)。语义与「认领之后怎么办」见 {@link RuleConsentStores.tickets.release}。
|
|
@@ -144,9 +141,12 @@ export declare const MAX_LOCAL_IMPORT_BYTES: number;
|
|
|
144
141
|
* 救回来",而不是等下一次 prepare。运维旋钮暂不开(没有部署形要求它可调)。
|
|
145
142
|
*/
|
|
146
143
|
export declare const LOCAL_IMPORT_CLAIM_LEASE_MS = 60000;
|
|
147
|
-
/** 一条规则的 wire 身份键(规则文本 + {@link serializeRuleScope} 的判别式串)。两个面
|
|
148
|
-
* 用**同一把键**,客户端拿它把两张表 join 起来。
|
|
144
|
+
/** 一条规则的 wire 身份键(**态** + 规则文本 + {@link serializeRuleScope} 的判别式串)。两个面
|
|
145
|
+
* (预览 / 兑付回执)用**同一把键**,客户端拿它把两张表 join 起来。
|
|
146
|
+
* 🔴 `behavior` 自 7.67.0 起是这把键的一格(core 7.9.0 [ref]:规则的身份是三元组)—— 少了它,同文本
|
|
147
|
+
* 同作用域的 deny 与 allow 两行会被本口判成 `duplicate_row`,而它们是两条不同的规则。 */
|
|
149
148
|
export interface LocalImportRuleKey {
|
|
149
|
+
behavior: RuleBehavior;
|
|
150
150
|
rule: string;
|
|
151
151
|
scope: string;
|
|
152
152
|
}
|
|
@@ -502,14 +502,18 @@ export declare function parseRuleScope(text: string): DurableRuleScope | undefin
|
|
|
502
502
|
* `adds` **如实上 wire**、不折叠成单值:同一条规则可以被批准过多次(不同 dot、不同来源、不同时刻),
|
|
503
503
|
* 折成「一个 addedAt」会让「这条规则是我导入的还是我点过卡的」在治理面上不可分。 */
|
|
504
504
|
export interface PersistedRuleWireRow {
|
|
505
|
+
/** core 7.9.0([ref])—— 这一行是哪一态的规则(`deny` / `ask` / `allow`)。**身份的一格**:
|
|
506
|
+
* 规则的身份是 (behavior, text, scope) 三元组,同文本不同态是两条不同的行(core `sameRuleIdentity`),
|
|
507
|
+
* 所以它既在列举面上,也在**导入/撤销的输入面**上必填 —— 少了它,一次撤销会瞄准同文本的另一态。 */
|
|
508
|
+
behavior: PersistedRule["behavior"];
|
|
505
509
|
rule: string;
|
|
506
510
|
/** {@link serializeRuleScope} 的判别式串。 */
|
|
507
511
|
scope: string;
|
|
508
|
-
tool:
|
|
509
|
-
match:
|
|
512
|
+
tool: PersistedRule["tool"];
|
|
513
|
+
match: PersistedRule["match"];
|
|
510
514
|
/** 去规范化的命令(core 的匹配器信的就是它 + `match`,不是重新解析 `rule`)。 */
|
|
511
515
|
command: string;
|
|
512
|
-
adds:
|
|
516
|
+
adds: PersistedRule["adds"];
|
|
513
517
|
/** [ref](core 7.5.0):这一行来自哪个源。**由 `scope` 派生**(`global`⟺`user`、`project`⟺
|
|
514
518
|
* `project`、`session`⟺`session`),不是行上另存的第二个字节 —— core 的 `ruleSourceOf` 是整条映射。
|
|
515
519
|
* 今天本部署只接 durable 一格分区 ⇒ 值域实为 `user | project`。
|
|
@@ -600,6 +604,7 @@ export interface RuleConsentLane {
|
|
|
600
604
|
*/
|
|
601
605
|
removeRule(input: {
|
|
602
606
|
principal: RuleBucketRef;
|
|
607
|
+
behavior: RuleBehavior;
|
|
603
608
|
rule: string;
|
|
604
609
|
scope: RuleScope;
|
|
605
610
|
}): Promise<RemoveResult>;
|
package/dist/rules-consent.js
CHANGED
|
@@ -4,7 +4,7 @@ import { recordFailOpen } from "./observability/fail-open.js";
|
|
|
4
4
|
import { createLogger } from "./observability/logger.js";
|
|
5
5
|
import { MAX_CWD_CHARS } from "./task-cwd.js";
|
|
6
6
|
import { CONTROL_AND_BIDI_CHARS, hasBidiControls } from "./text-bidi.js";
|
|
7
|
-
import { confirmRuleApproval,
|
|
7
|
+
import { confirmRuleApproval, RULE_BEHAVIORS, parseRuleText, ruleReachesProgramRun, ruleToolGrammarOf, precheckEditedRuleText, prepareCardApproval, prepareCcImport, redeemRuleBatch, redeemRuleTicket, removePersistedRule, effectiveOrThrow, readRowBehavior, } from "@sema-agent/core";
|
|
8
8
|
import { buildRulePayloadHash } from "./plugins/permission-rule-store-sql.js";
|
|
9
9
|
export const RULE_IMPORT_TICKET_TTL_MS = 10 * 60_000;
|
|
10
10
|
export const RULE_IMPORT_RETRY_AFTER_SEC = 2;
|
|
@@ -18,7 +18,7 @@ const LocalImportOutcomeSchema = z
|
|
|
18
18
|
.object({
|
|
19
19
|
members: z.array(z
|
|
20
20
|
.object({
|
|
21
|
-
key: z.object({ rule: z.string().min(1), scope: z.string().min(1) }).strict(),
|
|
21
|
+
key: z.object({ behavior: z.preprocess((v) => readRowBehavior(v) ?? v, z.enum(RULE_BEHAVIORS)), rule: z.string().min(1), scope: z.string().min(1) }).strict(),
|
|
22
22
|
outcome: z.enum(["persisted", "no-op", "refused", "indeterminate"]),
|
|
23
23
|
reason: z.enum(["store_refused", "walk_failed"]).optional(),
|
|
24
24
|
detail: z.string().optional(),
|
|
@@ -83,6 +83,7 @@ function isSupportedRuleMatch(match) {
|
|
|
83
83
|
case "wildcard":
|
|
84
84
|
return true;
|
|
85
85
|
case "subpath":
|
|
86
|
+
case "path":
|
|
86
87
|
return false;
|
|
87
88
|
default: {
|
|
88
89
|
const unknown = match;
|
|
@@ -177,13 +178,25 @@ function screenLocalImportRow(row) {
|
|
|
177
178
|
`${derived.tool}/${derived.match}/${JSON.stringify(derived.command)} but the row says ${String(row.tool)}/${String(row.match)}/${JSON.stringify(row.command)}`,
|
|
178
179
|
};
|
|
179
180
|
}
|
|
180
|
-
|
|
181
|
+
if (derived.behavior !== row.behavior) {
|
|
182
|
+
return { reason: "row_inconsistent", detail: `this row says behavior ${JSON.stringify(String(row.behavior))} but the parser resolved ${JSON.stringify(derived.behavior)}` };
|
|
183
|
+
}
|
|
184
|
+
return { behavior: derived.behavior, rule: derived.rule, scope };
|
|
181
185
|
};
|
|
182
|
-
const written =
|
|
186
|
+
const written = parseRuleText(row.rule, row.behavior);
|
|
183
187
|
if ("reject" in written)
|
|
184
188
|
return { reason: "rule_rejected", detail: `${written.reject.code}: ${written.reject.message}` };
|
|
185
|
-
if (written.rule.tool
|
|
189
|
+
if (ruleToolGrammarOf(written.rule.tool) !== "command")
|
|
190
|
+
return consistent(written.rule);
|
|
191
|
+
if (written.rule.behavior !== "allow") {
|
|
192
|
+
if (!ruleReachesProgramRun(written.rule, row.command)) {
|
|
193
|
+
return {
|
|
194
|
+
reason: "row_inconsistent",
|
|
195
|
+
detail: `this tightening rule does not speak about the command it names: ${JSON.stringify(written.rule.rule)} vs ${JSON.stringify(row.command)}`,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
186
198
|
return consistent(written.rule);
|
|
199
|
+
}
|
|
187
200
|
const pre = precheckCardRuleText(row.rule, row.command);
|
|
188
201
|
if (pre === undefined) {
|
|
189
202
|
return { reason: "rule_uncheckable", detail: "the rule text gate could not be asked about this row (its command is absent or not a string) — the row itself is malformed" };
|
|
@@ -199,22 +212,29 @@ function buildLocalImportLayers(candidates) {
|
|
|
199
212
|
const byRoot = new Map();
|
|
200
213
|
for (const c of candidates) {
|
|
201
214
|
const key = c.scope.kind === "global" ? "" : c.scope.root;
|
|
202
|
-
|
|
203
|
-
if (bucket === undefined)
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
215
|
+
let bucket = byRoot.get(key);
|
|
216
|
+
if (bucket === undefined) {
|
|
217
|
+
bucket = { deny: [], ask: [], allow: [] };
|
|
218
|
+
byRoot.set(key, bucket);
|
|
219
|
+
}
|
|
220
|
+
bucket[c.behavior].push(c.rule);
|
|
207
221
|
}
|
|
208
|
-
return [...byRoot].map(([root,
|
|
222
|
+
return [...byRoot].map(([root, lists]) => ({
|
|
209
223
|
layer: root === "" ? "userSettings" : "projectSettings",
|
|
210
224
|
path: root === "" ? LOCAL_IMPORT_LAYER_PATH : `${LOCAL_IMPORT_LAYER_PATH}:${root}`,
|
|
211
225
|
root: root === "" ? "/" : root,
|
|
212
|
-
readFile: async () => JSON.stringify({
|
|
226
|
+
readFile: async () => JSON.stringify({
|
|
227
|
+
permissions: {
|
|
228
|
+
...(lists.deny.length > 0 ? { deny: lists.deny } : {}),
|
|
229
|
+
...(lists.ask.length > 0 ? { ask: lists.ask } : {}),
|
|
230
|
+
...(lists.allow.length > 0 ? { allow: lists.allow } : {}),
|
|
231
|
+
},
|
|
232
|
+
}),
|
|
213
233
|
}));
|
|
214
234
|
}
|
|
215
235
|
function buildLocalImportMembers(members) {
|
|
216
236
|
return members.map((m) => {
|
|
217
|
-
const key = { rule: m.rule, scope: serializeRuleScope(m.scope) };
|
|
237
|
+
const key = { behavior: m.behavior, rule: m.rule, scope: serializeRuleScope(m.scope) };
|
|
218
238
|
switch (m.status) {
|
|
219
239
|
case "persisted":
|
|
220
240
|
return { key, outcome: "persisted" };
|
|
@@ -312,7 +332,7 @@ export function createRuleConsentLane(stores, opts) {
|
|
|
312
332
|
if (minted === undefined) {
|
|
313
333
|
return { ok: false, reason: "confirm-refused", detail: "the edited confirmation landed without a minted candidate — nothing to redeem" };
|
|
314
334
|
}
|
|
315
|
-
const canonical =
|
|
335
|
+
const canonical = parseRuleText(minted.rule, "allow");
|
|
316
336
|
if ("reject" in canonical) {
|
|
317
337
|
return { ok: false, reason: "edit-rejected", detail: `the canonical spelling the engine minted does not re-parse: ${canonical.reject.message}` };
|
|
318
338
|
}
|
|
@@ -444,11 +464,11 @@ export function createRuleConsentLane(stores, opts) {
|
|
|
444
464
|
for (const row of rows) {
|
|
445
465
|
const screened = screenLocalImportRow(row);
|
|
446
466
|
if ("reason" in screened) {
|
|
447
|
-
members.push({ key: { rule: row.rule, scope: row.scope }, admitted: false, reason: screened.reason, detail: screened.detail });
|
|
467
|
+
members.push({ key: { behavior: row.behavior, rule: row.rule, scope: row.scope }, admitted: false, reason: screened.reason, detail: screened.detail });
|
|
448
468
|
continue;
|
|
449
469
|
}
|
|
450
|
-
const key = { rule: screened.rule, scope: serializeRuleScope(screened.scope) };
|
|
451
|
-
const dedupKey = `${key.scope}${LOCAL_IMPORT_KEY_SEP}${key.rule}`;
|
|
470
|
+
const key = { behavior: screened.behavior, rule: screened.rule, scope: serializeRuleScope(screened.scope) };
|
|
471
|
+
const dedupKey = `${key.behavior}${LOCAL_IMPORT_KEY_SEP}${key.scope}${LOCAL_IMPORT_KEY_SEP}${key.rule}`;
|
|
452
472
|
if (seen.has(dedupKey)) {
|
|
453
473
|
members.push({ key, admitted: false, reason: "duplicate_row", detail: "this (rule, scope) appears earlier in the same batch — the engine would fold the two silently, so it is reported here instead" });
|
|
454
474
|
continue;
|
|
@@ -460,12 +480,12 @@ export function createRuleConsentLane(stores, opts) {
|
|
|
460
480
|
if (admitted.length === 0)
|
|
461
481
|
return { ok: true, preview: { members } };
|
|
462
482
|
const { preview, approvalId } = await prepareCcImport({ layers: buildLocalImportLayers(admitted), principal, deps });
|
|
463
|
-
const engineKeys = new Set(preview.candidates.map((c) => `${serializeRuleScope(c.scope)}${LOCAL_IMPORT_KEY_SEP}${c.rule}`));
|
|
483
|
+
const engineKeys = new Set(preview.candidates.map((c) => `${c.behavior}${LOCAL_IMPORT_KEY_SEP}${serializeRuleScope(c.scope)}${LOCAL_IMPORT_KEY_SEP}${c.rule}`));
|
|
464
484
|
for (let i = 0; i < members.length; i++) {
|
|
465
485
|
const m = members[i];
|
|
466
486
|
if (m === undefined || !m.admitted)
|
|
467
487
|
continue;
|
|
468
|
-
if (engineKeys.has(`${m.key.scope}${LOCAL_IMPORT_KEY_SEP}${m.key.rule}`))
|
|
488
|
+
if (engineKeys.has(`${m.key.behavior}${LOCAL_IMPORT_KEY_SEP}${m.key.scope}${LOCAL_IMPORT_KEY_SEP}${m.key.rule}`))
|
|
469
489
|
continue;
|
|
470
490
|
const why = preview.skipped.find((s) => s.rule === m.key.rule)?.reason ?? "the engine's import gate did not take it, and said nothing about it";
|
|
471
491
|
localImportLogger.warn("local_import_row_admitted_but_not_importable", {
|
|
@@ -683,12 +703,12 @@ export function createRuleConsentLane(stores, opts) {
|
|
|
683
703
|
},
|
|
684
704
|
async listRules(bucket) {
|
|
685
705
|
const view = await effectiveOrThrow(storeForBucket(stores.provider, bucket));
|
|
686
|
-
const rows = view.rules.map((r) => ({ rule: r.rule, scope: serializeRuleScope(r.scope), tool: r.tool, match: r.match, command: r.command, adds: r.adds, source: r.source, status: r.status }));
|
|
706
|
+
const rows = view.rules.map((r) => ({ behavior: r.behavior, rule: r.rule, scope: serializeRuleScope(r.scope), tool: r.tool, match: r.match, command: r.command, adds: r.adds, source: r.source, status: r.status }));
|
|
687
707
|
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));
|
|
688
708
|
return { rows, rev: view.rev };
|
|
689
709
|
},
|
|
690
710
|
async removeRule(input) {
|
|
691
|
-
return await removePersistedRule({ rule: input.rule, scope: input.scope, principal: input.principal, provider: stores.provider });
|
|
711
|
+
return await removePersistedRule({ behavior: input.behavior, rule: input.rule, scope: input.scope, principal: input.principal, provider: stores.provider });
|
|
692
712
|
},
|
|
693
713
|
};
|
|
694
714
|
}
|