@sema-agent/server 7.22.0-rc.1 → 7.23.0-rc.1
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 +2 -2
- package/README.zh-CN.md +2 -2
- package/USAGE.md +6 -4
- package/deploy/sema-up/chart/values.yaml +1 -1
- package/dist/approval-card.d.ts +53 -0
- package/dist/approval-card.js +104 -1
- package/dist/bench/s1/run-firm.js +1 -1
- package/dist/boot/budget-tracing.d.ts +0 -1
- package/dist/boot/resolve-spec.js +3 -1
- package/dist/budget.d.ts +0 -11
- package/dist/budget.js +10 -22
- package/dist/capabilities/scenarios.js +14 -8
- package/dist/config.d.ts +1 -1
- package/dist/config.js +12 -1
- package/dist/fleet/fleet-bus.d.ts +8 -3
- package/dist/fleet/fleet-bus.js +10 -5
- package/dist/http/route-ctx.d.ts +11 -0
- package/dist/http/route-ctx.js +16 -1
- package/dist/http/routes/runs.d.ts +3 -0
- package/dist/http/routes/runs.js +28 -24
- package/dist/http/routes/side-query.js +4 -13
- package/dist/http/routes/tasks.js +15 -3
- package/dist/http/server.d.ts +4 -3
- package/dist/http/server.js +8 -4
- package/dist/http/wire-types.d.ts +3 -1
- package/dist/lsp/manager.d.ts +9 -0
- package/dist/lsp/manager.js +8 -26
- package/dist/main.js +4 -0
- package/dist/observability/fail-open.d.ts +4 -0
- package/dist/observability/fail-open.js +4 -0
- package/dist/observability/otel-exporter.js +7 -1
- package/dist/observability/secret-env-scrub.d.ts +53 -0
- package/dist/observability/secret-env-scrub.js +135 -0
- package/dist/plugins/image-bake-store-sql.d.ts +8 -1
- package/dist/plugins/image-bake-store-sql.js +8 -1
- package/dist/plugins/image-index-sql.d.ts +13 -3
- package/dist/plugins/image-index-sql.js +13 -3
- package/dist/plugins/permission-rule-store-sql.d.ts +9 -72
- package/dist/plugins/permission-rule-store-sql.js +36 -64
- package/dist/plugins/remote-env-host.js +10 -2
- package/dist/project-memory.js +7 -1
- package/dist/run-local.js +7 -0
- package/dist/runtime-governance.d.ts +1 -1
- package/dist/runtime-governance.js +12 -15
- package/dist/security.d.ts +3 -1
- package/dist/security.js +15 -7
- package/dist/task-settings.js +10 -5
- package/dist/tool-approval.d.ts +20 -1
- package/dist/tool-approval.js +4 -1
- package/dist/trace/core-keyset-guard.d.ts +1 -1
- package/dist/trace/project.d.ts +2 -0
- package/dist/trace/project.js +11 -0
- package/package.json +3 -3
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A-033.3(#256)—— **密钥形 env 剥除的留痕面**(core D1 `scrubSecretEnv` 的可观测半场)。
|
|
3
|
+
*
|
|
4
|
+
* ## 它解决的问题
|
|
5
|
+
* `scrubSecretEnv` 是本仓两条 shell/子进程腿的**安全默认**:模型可驱动的 shell(`inheritEnv:"scrub"`)与
|
|
6
|
+
* git 子进程(钩子/凭证助手)拿到的 env 里,`*_KEY`/`*_TOKEN`/`*_SECRET`/`*_PASSWORD`… 一律先丢掉。
|
|
7
|
+
* 两处都以**单参形**调用,于是「这一次到底丢了哪些 key、按哪条规则(suffix-rule / exact-name)、置信度多少」
|
|
8
|
+
* **零留痕** —— 部署方既证明不了保护生效过,也发现不了它悄悄不生效了(词表漂移 / 一个新的密钥命名法从
|
|
9
|
+
* 规则的缝里穿过去 / 某条腿被改成 `inheritEnv:"all"`)。core 早就把收集口开好了(`findings?:
|
|
10
|
+
* SecretEnvFinding[]`,d.ts 原话:the VALUE is never carried — this record is meant to be loggable),
|
|
11
|
+
* 缺的只是本仓这半场。本模块就是那半场,**不是**一个新的执法面:它一个字节也不改变剥除行为。
|
|
12
|
+
*
|
|
13
|
+
* ## 形(照 `fail-open.ts` 的仓内先例)
|
|
14
|
+
* 1. **计数(逐次)** —— `secret_env_scrubbed_total{site,kind,confidence}`;
|
|
15
|
+
* 2. **结构化披露(每 site×key 一次)** —— 首次在某条腿上丢掉某个 key 时一条 `info`,之后同 key 只走计数。
|
|
16
|
+
* 不是「每 site 一次」:一个**新**的密钥形 key 后来才出现(部署加了一个凭证)时,那才是最该看见的一行。
|
|
17
|
+
* 值永不进任何一件(core 的 finding 本身就不携带值)。
|
|
18
|
+
*
|
|
19
|
+
* ## 为什么是进程级自由函数而不是注入
|
|
20
|
+
* 与 `recordFailOpen` 同因:两个调用点都在**没有 logger/metrics 句柄**的深处
|
|
21
|
+
* (`plugins/remote-env-host.ts` 的 `mergeEnv` 全文零观测通道;`project-memory.ts` 的 `git()` 是个自由函数)。
|
|
22
|
+
* 装配层用 {@link installSecretEnvScrubRecorder} 把句柄接上,装配之前命中的次数不会丢(计数照常累加,
|
|
23
|
+
* install 时一次性补进 metrics),披露行则在下一次命中时补发(未成功披露的 key 不进 `disclosed` 集)。
|
|
24
|
+
*/
|
|
25
|
+
import { summarizeRedactions } from "@sema-agent/core";
|
|
26
|
+
/** 登记过的剥除现场。闭集:未登记的 site 传不进 {@link recordSecretEnvScrub}(编译期拒,同 FAIL_OPEN_TAGS 纪律)。 */
|
|
27
|
+
export const SECRET_ENV_SCRUB_SITES = {
|
|
28
|
+
"server.remote-env-host.inherit-scrub": "模型可驱动的 host shell 继承 env(`inheritEnv:\"scrub\"` 默认腿)——这条腿上的一次泄漏等于把编排器的脑钥/仓库令牌交给模型选的命令",
|
|
29
|
+
"server.project-memory.git-subprocess": "project-memory 读仓叙事时的 git 子进程 env —— 仓内的钩子/凭证助手会以本进程身份运行",
|
|
30
|
+
};
|
|
31
|
+
/** 计数器名。 */
|
|
32
|
+
const SCRUB_COUNTER = "secret_env_scrubbed_total";
|
|
33
|
+
/** 已披露集的上限。key 集在一个进程里实际上是常量(恒扫同一份 `process.env`),这个帽只是防病态输入
|
|
34
|
+
* 把集合撑爆;越帽之后只停止**新增披露**,计数照常(缺席的是行,不是账)。 */
|
|
35
|
+
const DISCLOSED_CAP = 512;
|
|
36
|
+
/** 桶键与披露键的分隔符。用 `=` 是有据的:POSIX 环境变量名里唯二不可能出现的字节就是 `=` 与 NUL
|
|
37
|
+
* (NUL 不写进源码 —— 裸控制字节会让 grep 对整个文件失明,本仓有机械门盯它)。用空格/冒号分隔则会让
|
|
38
|
+
* `A B`+`C` 与 `A`+`B C` 撞成同一个键 —— 标签串台 = 遥测说谎。 */
|
|
39
|
+
const SEP = "=";
|
|
40
|
+
let sink = {};
|
|
41
|
+
/** `site=kind=confidence` → 累计次数(逐次,进程级)。 */
|
|
42
|
+
let counts = new Map();
|
|
43
|
+
/** 已经推进 metrics 的部分 —— 差额即欠账,install 时一次性补。 */
|
|
44
|
+
let pushed = new Map();
|
|
45
|
+
/** `site=key` —— 已经出过披露行的现场×键。 */
|
|
46
|
+
let disclosed = new Set();
|
|
47
|
+
function bucketKey(site, f) {
|
|
48
|
+
return `${site}${SEP}${f.kind}${SEP}${f.confidence}`;
|
|
49
|
+
}
|
|
50
|
+
/** 把 counts 与 pushed 的差额推进 metrics。metrics 缺席时什么也不做(差额留着,下次补)。 */
|
|
51
|
+
function drainToMetrics() {
|
|
52
|
+
const m = sink.metrics;
|
|
53
|
+
if (m === undefined)
|
|
54
|
+
return;
|
|
55
|
+
for (const [k, n] of counts) {
|
|
56
|
+
const already = pushed.get(k) ?? 0;
|
|
57
|
+
if (n <= already)
|
|
58
|
+
continue;
|
|
59
|
+
const [site, kind, confidence] = k.split(SEP);
|
|
60
|
+
try {
|
|
61
|
+
m.inc(SCRUB_COUNTER, { site: site, kind: kind, confidence: confidence }, n - already);
|
|
62
|
+
pushed.set(k, n); // 真进了汇才销账(投递失败 ⇒ 差额留着重放)
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
/* 下一次命中/下一次装配再补 */
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* 记一次剥除。**总不抛** —— 观测本身绝不能变成故障(同 `recordFailOpen` / reapers 的判据)。
|
|
71
|
+
* `findings` 为空(这一次没丢任何 key)时什么也不做:那不是事件,是常态。
|
|
72
|
+
*/
|
|
73
|
+
export function recordSecretEnvScrub(site, findings) {
|
|
74
|
+
if (findings.length === 0)
|
|
75
|
+
return;
|
|
76
|
+
try {
|
|
77
|
+
for (const f of findings) {
|
|
78
|
+
const k = bucketKey(site, f);
|
|
79
|
+
counts.set(k, (counts.get(k) ?? 0) + 1);
|
|
80
|
+
}
|
|
81
|
+
drainToMetrics();
|
|
82
|
+
const logger = sink.logger;
|
|
83
|
+
if (logger === undefined)
|
|
84
|
+
return; // 披露行留到有 logger 的下一次命中(未进 disclosed ⇒ 会补发)
|
|
85
|
+
// 🔴 帽是**对这一次的批也生效**的(codex 复审 round2 medium 真修):先按剩余容量截断,再用截断后的
|
|
86
|
+
// 那一批去发行与登记。两条都靠它:
|
|
87
|
+
// ① 帽满 ⇒ 停止披露,而不是「喊了但记不下」—— 后者会把「每 site×key 一次」在 key 最多的时候
|
|
88
|
+
// 反转成「每次调用一行」(mergeEnv 是逐命令调用的,那是刷屏);
|
|
89
|
+
// ② 单批越界 ⇒ 也得截断 —— 只在批**之前**判帽,一次 513 条的病态 env 会一次性把 513 条全发出去
|
|
90
|
+
// (一行超大日志)并全部留驻,帽等于没有。计数不受截断影响:账永远是全的,缺席的只是行。
|
|
91
|
+
const room = DISCLOSED_CAP - disclosed.size;
|
|
92
|
+
if (room <= 0)
|
|
93
|
+
return;
|
|
94
|
+
const fresh = findings.filter((f) => !disclosed.has(`${site}${SEP}${f.key}`)).slice(0, room);
|
|
95
|
+
if (fresh.length === 0)
|
|
96
|
+
return;
|
|
97
|
+
logger.info("secret_env_scrubbed", {
|
|
98
|
+
site,
|
|
99
|
+
note: SECRET_ENV_SCRUB_SITES[site],
|
|
100
|
+
// 只报**本次新见**的 key(值永不携带 —— core 的 finding 按契约只带键名/规则/置信度/来源)。
|
|
101
|
+
keys: fresh.map((f) => f.key),
|
|
102
|
+
// 一行人读摘要走 core 的 `summarizeRedactions`(SecretEnvFinding 满足 SummarizableFinding),
|
|
103
|
+
// 不在本仓另写一份分组渲染 —— 那正是本票要消灭的那种孪生。
|
|
104
|
+
summary: summarizeRedactions(fresh),
|
|
105
|
+
});
|
|
106
|
+
for (const f of fresh)
|
|
107
|
+
disclosed.add(`${site}${SEP}${f.key}`);
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
/* 观测绝不抛 */
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/** 装配层把 logger/metrics 接上(main.ts,与 `installFailOpenRecorder` 同一处)。装配前累计的次数在此补进 metrics。 */
|
|
114
|
+
export function installSecretEnvScrubRecorder(deps) {
|
|
115
|
+
sink = { ...deps };
|
|
116
|
+
deps.metrics?.counter(SCRUB_COUNTER, "secret-pattern env entries dropped before spawning a child process, by site/rule/confidence (core scrubSecretEnv findings)");
|
|
117
|
+
drainToMetrics();
|
|
118
|
+
}
|
|
119
|
+
/** 已留驻的「现场×键」条数(测试/诊断读面)。恒 ≤ {@link DISCLOSED_CAP}。 */
|
|
120
|
+
export function secretEnvScrubDisclosedSize() {
|
|
121
|
+
return disclosed.size;
|
|
122
|
+
}
|
|
123
|
+
/** 本进程各桶的累计次数(测试/诊断读面)。 */
|
|
124
|
+
export function secretEnvScrubCounts() {
|
|
125
|
+
return new Map(counts);
|
|
126
|
+
}
|
|
127
|
+
/** 测试用重置(进程级状态在同一个 vitest worker 里跨用例存活)。 */
|
|
128
|
+
export function resetSecretEnvScrubRecorderForTest(deps = {}) {
|
|
129
|
+
counts = new Map();
|
|
130
|
+
pushed = new Map();
|
|
131
|
+
disclosed = new Set();
|
|
132
|
+
sink = { ...deps };
|
|
133
|
+
deps.metrics?.counter(SCRUB_COUNTER, "secret-pattern env entries dropped before spawning a child process, by site/rule/confidence (core scrubSecretEnv findings)");
|
|
134
|
+
}
|
|
135
|
+
//# sourceMappingURL=secret-env-scrub.js.map
|
|
@@ -123,7 +123,14 @@ export declare class SqlImageBake {
|
|
|
123
123
|
retainedFrom(bakeId: string): Promise<number>;
|
|
124
124
|
/** Events with seq strictly greater than `afterSeq` (0 for the whole stream), ascending. */
|
|
125
125
|
getEvents(bakeId: string, afterSeq: number): Promise<BakeEvent[]>;
|
|
126
|
-
/**
|
|
126
|
+
/**
|
|
127
|
+
* Coarse 4-value lifecycle transition (the column the SSE reader polls for terminality).
|
|
128
|
+
*
|
|
129
|
+
* @deprecated #193 车7(staleness-sweep P2-12):**test-only**——生产零调用。生产唯一写入路径是
|
|
130
|
+
* `ingestBakeLine`(routes/images.ts),它只经 `setState`(denormalize)与 `setTerminal` 驱动状态;
|
|
131
|
+
* 本方法是绕开「status 只由 state/done 驱动」不变量的唯一直接写口,**禁止新增生产调用**。留而不删
|
|
132
|
+
* 的唯一理由=三个集成测试用它铸中间态,整删随该套件下次翻新一并做。
|
|
133
|
+
*/
|
|
127
134
|
setStatus(bakeId: string, status: BakeStatus): Promise<void>;
|
|
128
135
|
/** Denormalize build.sh's 8-value `state` for the UI progress bar (NEVER drives terminality, P2.3). */
|
|
129
136
|
setState(bakeId: string, state: BakeState): Promise<void>;
|
|
@@ -494,7 +494,14 @@ export class SqlImageBake {
|
|
|
494
494
|
ts: iso(r.ts),
|
|
495
495
|
}));
|
|
496
496
|
}
|
|
497
|
-
/**
|
|
497
|
+
/**
|
|
498
|
+
* Coarse 4-value lifecycle transition (the column the SSE reader polls for terminality).
|
|
499
|
+
*
|
|
500
|
+
* @deprecated #193 车7(staleness-sweep P2-12):**test-only**——生产零调用。生产唯一写入路径是
|
|
501
|
+
* `ingestBakeLine`(routes/images.ts),它只经 `setState`(denormalize)与 `setTerminal` 驱动状态;
|
|
502
|
+
* 本方法是绕开「status 只由 state/done 驱动」不变量的唯一直接写口,**禁止新增生产调用**。留而不删
|
|
503
|
+
* 的唯一理由=三个集成测试用它铸中间态,整删随该套件下次翻新一并做。
|
|
504
|
+
*/
|
|
498
505
|
async setStatus(bakeId, status) {
|
|
499
506
|
await this.db.query(this.q("UPDATE image_bake SET status = ?, updated_at = ? WHERE bake_id = ?", "UPDATE image_bake SET status = $1, updated_at = $2 WHERE bake_id = $3"), [status, new Date(), bakeId]);
|
|
500
507
|
}
|
|
@@ -17,7 +17,16 @@ export declare class SqlImageIndex {
|
|
|
17
17
|
private q;
|
|
18
18
|
/** Stable index id = sha256(repo@digest) — identity is the digest, not the mutable tag. */
|
|
19
19
|
static idFor(repo: string, digest: string): string;
|
|
20
|
-
/**
|
|
20
|
+
/**
|
|
21
|
+
* Register or update an index row (idempotent on repo+digest). Returns the row id.
|
|
22
|
+
*
|
|
23
|
+
* @deprecated #193 车7(staleness-sweep P2-11):**生产零调用**——曾声称的 operator
|
|
24
|
+
* `/v1/images/register` 路径实际走 `registerBuilding`(routes/images.ts),本方法只剩集成测试当
|
|
25
|
+
* seed 口在用。危险语义原样保留着:UNCONDITIONAL upsert 会把 `published` 行打回 `building`
|
|
26
|
+
* (正是 §P2.10 静默 de-publish 病的成因,`registerBuilding` 因此才带 STATUS-MONOTONIC 守卫)。
|
|
27
|
+
* **禁止新增生产调用**;测试 seed 请优先走 `registerBuilding`(不需要覆盖语义时)。留而不删的
|
|
28
|
+
* 唯一理由=16 处 pg 集成测试 seed 依赖,整删随该套件下次翻新一并做。
|
|
29
|
+
*/
|
|
21
30
|
upsert(e: ImageIndexUpsert): Promise<string>;
|
|
22
31
|
/**
|
|
23
32
|
* STATUS-MONOTONIC register (IMAGE-API-DESIGN.md §P2.10 MUST-FIX) — the auto-register path the bake runner uses.
|
|
@@ -30,8 +39,9 @@ export declare class SqlImageIndex {
|
|
|
30
39
|
* isn't already `published` — a late/duplicate `done` for an already-promoted digest is a no-op on the
|
|
31
40
|
* lifecycle, never a regression. Returns the row id.
|
|
32
41
|
*
|
|
33
|
-
* Distinct from the unconditional `upsert` (
|
|
34
|
-
*
|
|
42
|
+
* Distinct from the unconditional `upsert` (#193 车7 更正:那条 operator `/v1/images/register` 路径
|
|
43
|
+
* **也走本方法**——routes/images.ts:446 `idx.registerBuilding(...)`;`upsert` 已 @deprecated,只剩
|
|
44
|
+
* 测试 seed 在用)。This is the bake auto-register's ONLY write into the index. Default status is `building` (P2 registers
|
|
35
45
|
* node-local/quarantine `building` artifacts; P3's verify gate is what flips `building → published`).
|
|
36
46
|
*
|
|
37
47
|
* The `IF(...published...)` (TiDB) / `CASE WHEN...published...` (PG) guards are the monotonic de-publish
|
|
@@ -180,7 +180,16 @@ export class SqlImageIndex {
|
|
|
180
180
|
static idFor(repo, digest) {
|
|
181
181
|
return createHash("sha256").update(`${repo}@${digest}`).digest("hex").slice(0, 64);
|
|
182
182
|
}
|
|
183
|
-
/**
|
|
183
|
+
/**
|
|
184
|
+
* Register or update an index row (idempotent on repo+digest). Returns the row id.
|
|
185
|
+
*
|
|
186
|
+
* @deprecated #193 车7(staleness-sweep P2-11):**生产零调用**——曾声称的 operator
|
|
187
|
+
* `/v1/images/register` 路径实际走 `registerBuilding`(routes/images.ts),本方法只剩集成测试当
|
|
188
|
+
* seed 口在用。危险语义原样保留着:UNCONDITIONAL upsert 会把 `published` 行打回 `building`
|
|
189
|
+
* (正是 §P2.10 静默 de-publish 病的成因,`registerBuilding` 因此才带 STATUS-MONOTONIC 守卫)。
|
|
190
|
+
* **禁止新增生产调用**;测试 seed 请优先走 `registerBuilding`(不需要覆盖语义时)。留而不删的
|
|
191
|
+
* 唯一理由=16 处 pg 集成测试 seed 依赖,整删随该套件下次翻新一并做。
|
|
192
|
+
*/
|
|
184
193
|
async upsert(e) {
|
|
185
194
|
// 🔴 PG-only identity-axis validation (codex R17-H1/R19) — TiDB has no NUL/lone-surrogate column
|
|
186
195
|
// constraint, so it stores repo/tenantId verbatim; PG rejects up front (see file-header dialect delta).
|
|
@@ -226,8 +235,9 @@ export class SqlImageIndex {
|
|
|
226
235
|
* isn't already `published` — a late/duplicate `done` for an already-promoted digest is a no-op on the
|
|
227
236
|
* lifecycle, never a regression. Returns the row id.
|
|
228
237
|
*
|
|
229
|
-
* Distinct from the unconditional `upsert` (
|
|
230
|
-
*
|
|
238
|
+
* Distinct from the unconditional `upsert` (#193 车7 更正:那条 operator `/v1/images/register` 路径
|
|
239
|
+
* **也走本方法**——routes/images.ts:446 `idx.registerBuilding(...)`;`upsert` 已 @deprecated,只剩
|
|
240
|
+
* 测试 seed 在用)。This is the bake auto-register's ONLY write into the index. Default status is `building` (P2 registers
|
|
231
241
|
* node-local/quarantine `building` artifacts; P3's verify gate is what flips `building → published`).
|
|
232
242
|
*
|
|
233
243
|
* The `IF(...published...)` (TiDB) / `CASE WHEN...published...` (PG) guards are the monotonic de-publish
|
|
@@ -1,81 +1,18 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type PermissionRuleWriter, type PermissionRuleStore, type PermissionRuleStoreProvider, type RuleScope, type RuleSyncFrontier, type RuleApprovalRecord, type RuleApprovalRecordStore, type RuleOwner } from "@sema-agent/core";
|
|
2
2
|
import type { Pool as MySqlPool } from "mysql2/promise";
|
|
3
3
|
import type { PgQueryFn } from "./pg-query.js";
|
|
4
4
|
import { type SqlDriver } from "./sql-driver.js";
|
|
5
|
-
/** core `permission-rule-store.ts` 的 `PERMISSION_RULE_WRITER` 字面值(逐字)。`writerOf(store)` 读的
|
|
6
|
-
* 就是这把键——它是 core 与 backend 之间**事实上的**协议名,只是没被导出。 */
|
|
7
|
-
export declare const PERMISSION_RULE_WRITER_KEY = "__semaPermissionRuleWriter";
|
|
8
|
-
/** core `RedemptionAuthorization` 的镜像。 */
|
|
9
|
-
export interface RedemptionAuthorizationMirror {
|
|
10
|
-
recordId: string;
|
|
11
|
-
principal?: string;
|
|
12
|
-
owner?: RuleOwner;
|
|
13
|
-
}
|
|
14
|
-
/** core `RuleAddDelta` 的镜像。 */
|
|
15
|
-
export interface RuleAddDeltaMirror {
|
|
16
|
-
kind: "redemption-add";
|
|
17
|
-
rule: string;
|
|
18
|
-
scope: RuleScope;
|
|
19
|
-
tool: PersistedAllowRule["tool"];
|
|
20
|
-
match: PersistedAllowRule["match"];
|
|
21
|
-
command: string;
|
|
22
|
-
add: RuleAdd;
|
|
23
|
-
redemption: RedemptionAuthorizationMirror;
|
|
24
|
-
}
|
|
25
|
-
/** core `RuleDeleteDelta` 的镜像。 */
|
|
26
|
-
export interface RuleDeleteDeltaMirror {
|
|
27
|
-
kind: "tighten-delete";
|
|
28
|
-
tombstone: RuleTombstone;
|
|
29
|
-
}
|
|
30
|
-
/** core `RuleSyncJoinDelta` 的镜像(**本 backend 不实现**,只为判别式完整——见顶注 ②)。 */
|
|
31
|
-
export interface RuleSyncJoinDeltaMirror {
|
|
32
|
-
kind: "sync-join";
|
|
33
|
-
inbound: {
|
|
34
|
-
rules: PersistedAllowRule[];
|
|
35
|
-
tombstones: RuleTombstone[];
|
|
36
|
-
};
|
|
37
|
-
gcFrontier?: RuleSyncFrontier;
|
|
38
|
-
observedVector?: RuleSyncFrontier;
|
|
39
|
-
quarantine?: Array<{
|
|
40
|
-
rule: string;
|
|
41
|
-
scope: RuleScope;
|
|
42
|
-
dots: RuleDot[];
|
|
43
|
-
reason: QuarantinedRuleAdd["reason"];
|
|
44
|
-
}>;
|
|
45
|
-
}
|
|
46
|
-
export type RuleWriteDeltaMirror = RuleAddDeltaMirror | RuleDeleteDeltaMirror | RuleSyncJoinDeltaMirror;
|
|
47
|
-
/** core `RawRuleSyncState` 的镜像。 */
|
|
48
|
-
export interface RawRuleSyncStateMirror {
|
|
49
|
-
actor: string;
|
|
50
|
-
counter: number;
|
|
51
|
-
rev: number;
|
|
52
|
-
rules: PersistedAllowRule[];
|
|
53
|
-
tombstones: RuleTombstone[];
|
|
54
|
-
observedVector?: RuleSyncFrontier;
|
|
55
|
-
quarantined?: QuarantinedRuleAdd[];
|
|
56
|
-
}
|
|
57
|
-
/** core `PermissionRuleWriter` 的镜像。 */
|
|
58
|
-
export interface PermissionRuleWriterMirror {
|
|
59
|
-
nextDot(): Promise<RuleDot>;
|
|
60
|
-
apply(delta: RuleWriteDeltaMirror, opts: {
|
|
61
|
-
expectedRev: number;
|
|
62
|
-
}): Promise<PutResult | {
|
|
63
|
-
conflict: true;
|
|
64
|
-
rev: number;
|
|
65
|
-
}>;
|
|
66
|
-
readRaw(): Promise<RawRuleSyncStateMirror>;
|
|
67
|
-
}
|
|
68
|
-
/** core `WritablePermissionRuleStore` 的镜像(属性名 = {@link PERMISSION_RULE_WRITER_KEY})。 */
|
|
69
|
-
export interface WritablePermissionRuleStoreMirror extends PermissionRuleStore {
|
|
70
|
-
readonly __semaPermissionRuleWriter: PermissionRuleWriterMirror;
|
|
71
|
-
}
|
|
72
5
|
/**
|
|
73
|
-
*
|
|
6
|
+
* 一只店的写面,或 `undefined`(该 backend 从引擎侧看是只读的)。
|
|
7
|
+
*
|
|
8
|
+
* 与 core 的 `writerOf` 的**唯一**区别是判据更严:core 只看 `apply`/`nextDot` 两个方法在不在(它服务的是
|
|
9
|
+
* 引擎自己的兑付腿),本函数还要求 `readRaw` —— 本仓的消费点(测试/取证)读的是**完整**写面,少一个方法
|
|
10
|
+
* 就该当场判 `undefined`,而不是在调用 `readRaw()` 时炸一个 `not a function`。
|
|
74
11
|
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
12
|
+
* 结构检查而不是断言:没有这个函数,每个调用点都会各写一次 `as unknown as {…}`,那既是三处宽松断言,
|
|
13
|
+
* 也是三份会各自漂的形状假设。
|
|
77
14
|
*/
|
|
78
|
-
export declare function writerOfSqlRuleStore(store: PermissionRuleStore):
|
|
15
|
+
export declare function writerOfSqlRuleStore(store: PermissionRuleStore): PermissionRuleWriter | undefined;
|
|
79
16
|
export declare const PERMISSION_RULE_TABLE = "permission_rule";
|
|
80
17
|
export declare const PERMISSION_RULE_APPROVAL_TABLE = "permission_rule_approval";
|
|
81
18
|
export declare const PERMISSION_RULE_TICKET_TABLE = "permission_rule_ticket";
|
|
@@ -23,23 +23,28 @@
|
|
|
23
23
|
* 天然原子,不需要事务、不需要 `FOR UPDATE`、不进本仓在案的悲观锁死锁族。
|
|
24
24
|
*
|
|
25
25
|
* ─────────────────────────────────────────────────────────────────────────────────────────────────
|
|
26
|
-
* 🔴
|
|
26
|
+
* 🔴 写面 = **core 的本尊**(A-033.2 / #256 销账,交接件③已兑现)
|
|
27
27
|
* ─────────────────────────────────────────────────────────────────────────────────────────────────
|
|
28
|
-
* core
|
|
29
|
-
* `RuleWriteDelta`/`RawRuleSyncState` 类型、`foldDelta`/`
|
|
30
|
-
* `assertRedemptionNotQuarantined`/`assertDeleteDeltaCarriesNoAdd` 函数)
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
* consent protocol」——那条纪律针对的是「宿主自己往店里写」,而**一个宿主提供的 backend** 恰恰必须持有
|
|
34
|
-
* 写面才能被 `redeemRuleTicket` 使用(core 的 `writerOf(store)` 读的就是这个属性)。
|
|
28
|
+
* 本文件曾整段登记一个上游契约缺口:core 的写面(`PERMISSION_RULE_WRITER` 常量、`PermissionRuleWriter`/
|
|
29
|
+
* `WritablePermissionRuleStore`/`RuleWriteDelta`/`RawRuleSyncState` 类型、`foldDelta`/`writerOf`/
|
|
30
|
+
* `assertRedemptionNotQuarantined`/`assertDeleteDeltaCarriesNoAdd` 函数)当时**不在包的公开导出面上**,
|
|
31
|
+
* 于是本文件按结构满足它:属性名用字面常量、类型在下方一份份**本地镜像**(`RuleWriteDeltaMirror` 等),
|
|
32
|
+
* 并把「求 core 导出写面」列为交接件③。
|
|
35
33
|
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
34
|
+
* core 2026-08-10 裁定**导出后端契约**(d.ts 原话:「Exported as part of the BACKEND CONTRACT … so an
|
|
35
|
+
* out-of-repo store implementation hangs the same face the file backend does, instead of mirroring the
|
|
36
|
+
* types」),5.33.0 起全部根导出 ⇒ 镜像与手写孪生一并摘除,本文件直接消费 core 的类型与三个共享判据。
|
|
37
|
+
* 这不是洁癖:`foldDelta` 的 d.ts 自陈「Pure — the backends share it so the file and in-memory forms
|
|
38
|
+
* cannot drift in what a delta MEANS」,而本 backend 此前**没有共享它**——一份 delta 在 file 后端与
|
|
39
|
+
* SQL 后端上「意味着什么」可以各自漂,漂的落点是放行面。换装同时把两条判据换成 core 的严格版:
|
|
40
|
+
* · `assertDeleteDeltaCarriesNoAdd`:core 查 **6 个** add-only 键(add/redemption/rule/command/match/tool)
|
|
41
|
+
* 并要求 `removedDots` 是**非空数组**;本地孪生只查过一个 `rules` 键(那还是 sync-join 的字段名,
|
|
42
|
+
* 打偏了)且对空 `removedDots` 放行 —— 一条删不掉任何 add 的墓碑会让本该已删的规则继续放行。
|
|
43
|
+
* · `assertRedemptionNotQuarantined`:语义等价(同 dot 即拒),换本尊后报文也与 file 后端一致。
|
|
44
|
+
*
|
|
45
|
+
* 仍然**不实现**的只剩 `sync-join` 那一臂:它的落地管线(`applySyncJoin`)至今是 core 私有的一整条
|
|
46
|
+
* 流水线,复制它才是真的会漂;本 backend 对该 delta **响亮拒绝**(fail-closed,不是静默 no-op),
|
|
47
|
+
* 同步客户端本仓尚未接线,所以这条臂今天零调用点。
|
|
43
48
|
*
|
|
44
49
|
* ─────────────────────────────────────────────────────────────────────────────────────────────────
|
|
45
50
|
* 边界纪律:列回读一律 zod `safeParse`(宪法 [2704] 禁裸 as-cast)。JSON 列是驱动交回的 `unknown`,
|
|
@@ -49,22 +54,23 @@
|
|
|
49
54
|
*/
|
|
50
55
|
import { createHash, randomBytes } from "node:crypto";
|
|
51
56
|
import { z } from "zod";
|
|
52
|
-
import { applyTombstones,
|
|
57
|
+
import { applyTombstones, assertDeleteDeltaCarriesNoAdd, assertRedemptionNotQuarantined, foldDelta, parseAllowRuleText, screenRuleSyncState, PERMISSION_RULE_WRITER, RULE_SYNC_DROP_CODES, } from "@sema-agent/core";
|
|
53
58
|
import { dialectProtocolJsonEncoder } from "./sql-driver.js";
|
|
54
59
|
// ─────────────────────────────────────────────────────────────────────────────────────────────────
|
|
55
|
-
// core
|
|
60
|
+
// 写面(core 的后端契约,2026-08-10 裁定导出;本仓 A-033.2 已摘除全部本地镜像)
|
|
56
61
|
// ─────────────────────────────────────────────────────────────────────────────────────────────────
|
|
57
|
-
/** core `permission-rule-store.ts` 的 `PERMISSION_RULE_WRITER` 字面值(逐字)。`writerOf(store)` 读的
|
|
58
|
-
* 就是这把键——它是 core 与 backend 之间**事实上的**协议名,只是没被导出。 */
|
|
59
|
-
export const PERMISSION_RULE_WRITER_KEY = "__semaPermissionRuleWriter";
|
|
60
62
|
/**
|
|
61
|
-
*
|
|
63
|
+
* 一只店的写面,或 `undefined`(该 backend 从引擎侧看是只读的)。
|
|
64
|
+
*
|
|
65
|
+
* 与 core 的 `writerOf` 的**唯一**区别是判据更严:core 只看 `apply`/`nextDot` 两个方法在不在(它服务的是
|
|
66
|
+
* 引擎自己的兑付腿),本函数还要求 `readRaw` —— 本仓的消费点(测试/取证)读的是**完整**写面,少一个方法
|
|
67
|
+
* 就该当场判 `undefined`,而不是在调用 `readRaw()` 时炸一个 `not a function`。
|
|
62
68
|
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
69
|
+
* 结构检查而不是断言:没有这个函数,每个调用点都会各写一次 `as unknown as {…}`,那既是三处宽松断言,
|
|
70
|
+
* 也是三份会各自漂的形状假设。
|
|
65
71
|
*/
|
|
66
72
|
export function writerOfSqlRuleStore(store) {
|
|
67
|
-
const w = Reflect.get(store,
|
|
73
|
+
const w = Reflect.get(store, PERMISSION_RULE_WRITER);
|
|
68
74
|
if (w === null || typeof w !== "object")
|
|
69
75
|
return undefined;
|
|
70
76
|
const nextDot = Reflect.get(w, "nextDot");
|
|
@@ -315,42 +321,6 @@ function canonicalJson(v) {
|
|
|
315
321
|
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
|
|
316
322
|
return `{${entries.map(([k, val]) => `${JSON.stringify(k)}:${canonicalJson(val)}`).join(",")}}`;
|
|
317
323
|
}
|
|
318
|
-
/**
|
|
319
|
-
* core `foldDelta` 的**语义镜像**:把一次 redemption-add 折进原始规则集。
|
|
320
|
-
*
|
|
321
|
-
* 逐字对着 core 的 JSDoc 与 in-memory 参照物写:同 (rule, scope) 归一组,组内 adds 按 **dot 身份**并集
|
|
322
|
-
* (同 dot 重放 ⇒ 恒等,这正是 core「一次兑付恒铸一个 dot,重放多少次都只有一个 add」的落点)。
|
|
323
|
-
*/
|
|
324
|
-
function foldAdd(rules, delta) {
|
|
325
|
-
const next = rules.map((r) => ({ ...r, adds: [...r.adds] }));
|
|
326
|
-
const hit = next.find((r) => r.rule === delta.rule && sameScope(r.scope, delta.scope));
|
|
327
|
-
if (hit === undefined) {
|
|
328
|
-
next.push({ rule: delta.rule, tool: delta.tool, match: delta.match, command: delta.command, scope: delta.scope, adds: [delta.add] });
|
|
329
|
-
return next;
|
|
330
|
-
}
|
|
331
|
-
if (!hit.adds.some((a) => a.dot.actor === delta.add.dot.actor && a.dot.counter === delta.add.dot.counter))
|
|
332
|
-
hit.adds.push(delta.add);
|
|
333
|
-
return next;
|
|
334
|
-
}
|
|
335
|
-
/** core `assertRedemptionNotQuarantined` 的语义镜像:**任何** (rule, scope) 下与本 delta 同 dot 的
|
|
336
|
-
* 隔离行都让这次写响亮失败——dot 只铸一次,被隔离钉住的 dot 再出现就是「复活」形。 */
|
|
337
|
-
function assertNotQuarantined(quarantined, delta) {
|
|
338
|
-
const hit = quarantined.find((q) => q.add.dot.actor === delta.add.dot.actor && q.add.dot.counter === delta.add.dot.counter);
|
|
339
|
-
if (hit !== undefined) {
|
|
340
|
-
throw new Error(`refusing to fold a redemption whose dot (${hit.add.dot.actor}#${hit.add.dot.counter}) is quarantined (${hit.reason}) — ` +
|
|
341
|
-
`re-entry requires a NEW consent (a new ask, a new record, a new dot)`);
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
/** core `assertDeleteDeltaCarriesNoAdd` 的语义镜像(联合已让它不可拼写,但 JS 调用方不受联合约束 ⇒ 执行它)。 */
|
|
345
|
-
function assertDeleteCarriesNoAdd(delta) {
|
|
346
|
-
// `Reflect.get` 而不是 `as unknown as {…}`:这一行的**全部目的**就是看一个联合类型里不存在的键在不在
|
|
347
|
-
// (JS 调用方不受联合约束),用断言去表达「我知道它可能有个类型上没有的键」既绕过类型面又触类型卫生门。
|
|
348
|
-
const smuggled = Reflect.get(delta, "rules");
|
|
349
|
-
if (smuggled !== undefined)
|
|
350
|
-
throw new Error("a tighten-delete delta must carry a tombstone and nothing else");
|
|
351
|
-
if (delta.tombstone === undefined || delta.tombstone.removedDots === undefined)
|
|
352
|
-
throw new Error("a tighten-delete delta must carry a tombstone");
|
|
353
|
-
}
|
|
354
324
|
/** {@link SqlPermissionRuleStore} 的 dot 铸造 CAS 重试上限。有界:一个无界循环只会把「争不过」变成
|
|
355
325
|
* 挂死;用尽 ⇒ 响亮抛错,由 core 的兑付腿(它自己也有有界重试)如实回 refused。 */
|
|
356
326
|
const NEXT_DOT_MAX_ATTEMPTS = 8;
|
|
@@ -475,7 +445,7 @@ class SqlPermissionRuleStore {
|
|
|
475
445
|
async quarantined() {
|
|
476
446
|
return (await this.read()).quarantined;
|
|
477
447
|
}
|
|
478
|
-
get [
|
|
448
|
+
get [PERMISSION_RULE_WRITER]() {
|
|
479
449
|
return this.writer;
|
|
480
450
|
}
|
|
481
451
|
writer = {
|
|
@@ -535,12 +505,14 @@ class SqlPermissionRuleStore {
|
|
|
535
505
|
return { conflict: true, rev: cur.rev };
|
|
536
506
|
let nextRules = cur.rules;
|
|
537
507
|
let nextTombstones = cur.tombstones;
|
|
508
|
+
// A-033.2(#256):两条判据 + 折叠本体全部走 core 的共享件(`foldDelta` 的 d.ts:「the backends share
|
|
509
|
+
// it so the file and in-memory forms cannot drift in what a delta MEANS」)。此前这三处是本地孪生。
|
|
538
510
|
if (delta.kind === "redemption-add") {
|
|
539
|
-
|
|
540
|
-
nextRules =
|
|
511
|
+
assertRedemptionNotQuarantined(cur.quarantined, delta);
|
|
512
|
+
nextRules = foldDelta(cur.rules, delta);
|
|
541
513
|
}
|
|
542
514
|
else {
|
|
543
|
-
|
|
515
|
+
assertDeleteDeltaCarriesNoAdd(delta);
|
|
544
516
|
nextTombstones = [...cur.tombstones, delta.tombstone];
|
|
545
517
|
}
|
|
546
518
|
// 🔴 codex 交叉复审 round5 [high] 一(验真后修)——**只写读得回来的东西**。
|
|
@@ -48,6 +48,7 @@ import { hostBackgroundShellEnabled, hostExecSpoolEnabled } from "../config.js";
|
|
|
48
48
|
import { resolveHostShell, hostShell, spawnGroupOptions, killTreeHard, killTreeSoft, collapseWin32EnvKeys } from "./host-platform.js";
|
|
49
49
|
import { BackgroundShellManager, seedMemStream, feedMemStream, drainMemStream } from "./background-shell-support.js";
|
|
50
50
|
import { FileError, ExecutionError, RemoteExecutionError, scrubSecretEnv, RollingTailBuffer, markTruncated, SchedulerError, BackgroundShellError, } from "@sema-agent/core";
|
|
51
|
+
import { recordSecretEnvScrub } from "../observability/secret-env-scrub.js";
|
|
51
52
|
const PROVIDER = "host";
|
|
52
53
|
/** Default per-command wall-clock when the caller passes none (NOT a hang detector — host commands rarely hang).
|
|
53
54
|
* ⚠️ 同判据三腿之一(另两条:`remote-env-local-docker.ts` 同值 30min、`remote-env-ssh.ts` **5min**)——
|
|
@@ -1526,8 +1527,15 @@ export class RemoteHostExecutionEnv {
|
|
|
1526
1527
|
if (process.env[k] !== undefined)
|
|
1527
1528
|
base[k] = process.env[k];
|
|
1528
1529
|
}
|
|
1529
|
-
else
|
|
1530
|
-
|
|
1530
|
+
else {
|
|
1531
|
+
// "scrub" — core's first-party D1 secret-env filter. A-033.3(#256):带 `findings` 收集器调用,
|
|
1532
|
+
// 把「这一次丢了哪些密钥形 key、按哪条规则、置信度多少」交给既有观测面留痕。零留痕的保护证明不了
|
|
1533
|
+
// 自己生效过,也发现不了自己不再生效(词表漂/新命名法穿缝/这条腿被改成 inheritEnv:"all")。
|
|
1534
|
+
// 收集器**不改变**剥除行为一个字节(core: 省略它 ⇒ byte-for-byte 与之前相同)。
|
|
1535
|
+
const findings = [];
|
|
1536
|
+
base = scrubSecretEnv(process.env, findings);
|
|
1537
|
+
recordSecretEnvScrub("server.remote-env-host.inherit-scrub", findings);
|
|
1538
|
+
}
|
|
1531
1539
|
if (this.cfg.env)
|
|
1532
1540
|
Object.assign(base, this.cfg.env); // caller-supplied (control-plane secrets) verbatim
|
|
1533
1541
|
if (perCommand)
|
package/dist/project-memory.js
CHANGED
|
@@ -17,6 +17,7 @@ import { execFile } from "node:child_process";
|
|
|
17
17
|
import { createHmac, randomBytes } from "node:crypto";
|
|
18
18
|
import { isAbsolute, join, sep } from "node:path";
|
|
19
19
|
import { scrubSecretEnv } from "@sema-agent/core";
|
|
20
|
+
import { recordSecretEnvScrub } from "./observability/secret-env-scrub.js";
|
|
20
21
|
import { redactSecrets } from "./trace/redact.js";
|
|
21
22
|
/** The instruction-file name chain (core 1.302 S-1): AGENTS.md wins, CLAUDE.md is the fallback, then the
|
|
22
23
|
* `.claude/` nested form. The FIRST hit is the single declared instruction source. */
|
|
@@ -122,11 +123,16 @@ async function hasSymlinkedParent(root, rel) {
|
|
|
122
123
|
* and GIT_CONFIG_GLOBAL/SYSTEM=/dev/null drop the global/system config so a planted one can't inject either. */
|
|
123
124
|
function git(root, args, maxBytes) {
|
|
124
125
|
const safeArgs = ["-c", "core.fsmonitor=false", "-c", "core.hooksPath=/dev/null", ...args];
|
|
126
|
+
// A-033.3(#256):带 `findings` 收集器 —— 见下方 env 行的注释,这条腿丢掉的是「仓内的钩子/凭证助手
|
|
127
|
+
// 本来会看到的」那批 key,零留痕的话保护生效与否在遥测里读不出来。收集器不改变剥除行为。
|
|
128
|
+
const scrubFindings = [];
|
|
129
|
+
const scrubbedEnv = scrubSecretEnv(process.env, scrubFindings);
|
|
130
|
+
recordSecretEnvScrub("server.project-memory.git-subprocess", scrubFindings);
|
|
125
131
|
return new Promise((res) => {
|
|
126
132
|
execFile("git", safeArgs,
|
|
127
133
|
// scrubSecretEnv: a git hook / credential helper must not see the orchestrator's *_KEY/*_TOKEN/*_SECRET env.
|
|
128
134
|
// GIT_CONFIG_GLOBAL/SYSTEM=/dev/null: ignore ~/.gitconfig + /etc/gitconfig so a planted exec-config can't steer us.
|
|
129
|
-
{ cwd: root, timeout: 5000, maxBuffer: maxBytes, env: { ...
|
|
135
|
+
{ cwd: root, timeout: 5000, maxBuffer: maxBytes, env: { ...scrubbedEnv, GIT_CONFIG_GLOBAL: "/dev/null", GIT_CONFIG_SYSTEM: "/dev/null" } }, (err, stdout, stderr) => {
|
|
130
136
|
if (!err)
|
|
131
137
|
return res({ out: stdout.trim() });
|
|
132
138
|
// S23: the git narrative used to vanish with zero fingerprint — classify why (→ gitDropReason).
|
package/dist/run-local.js
CHANGED
|
@@ -78,6 +78,7 @@ import { createPermissionDeniedMeter } from "./observability/tool-trace.js";
|
|
|
78
78
|
import { createKeyResolver } from "./key-resolver.js";
|
|
79
79
|
import { createLogger } from "./observability/logger.js";
|
|
80
80
|
import { createMetrics } from "./observability/metrics.js";
|
|
81
|
+
import { installSecretEnvScrubRecorder } from "./observability/secret-env-scrub.js";
|
|
81
82
|
/** Parse argv (everything AFTER `node run-local.js`). Throws a clear Error on a missing objective. */
|
|
82
83
|
export function parseArgs(argv) {
|
|
83
84
|
let objective;
|
|
@@ -308,6 +309,12 @@ export async function runLocal(argv, deps = {}) {
|
|
|
308
309
|
const config = loadConfig();
|
|
309
310
|
const logger = deps.logger ?? createLogger(config.logLevel);
|
|
310
311
|
const metrics = createMetrics();
|
|
312
|
+
// A-033.3(#256,codex 复审 medium 真修):run-local **也**要装密钥剥除的留痕面。这不是 main.ts 的
|
|
313
|
+
// 复制粘贴 —— 它恰恰是最该有的那条腿:run-local 是 TOC 本地形,模型可驱动的 shell 跑在**用户自己的
|
|
314
|
+
// 机器**上、继承的是用户**真实的** env(脑钥/仓库令牌/DB 凭据),而这两条剥除腿(hostExecutionEnvFactory
|
|
315
|
+
// 的 mergeEnv 与 makeLoadProjectMemory 的 git 子进程)在本入口全都接线了。不装 ⇒ 收集器有了、sink 是空的,
|
|
316
|
+
// 这条腿保留 A-033.3 要消灭的那个「零披露」原状(而且进程一退,连计数都没了)。
|
|
317
|
+
installSecretEnvScrubRecorder({ logger, metrics });
|
|
311
318
|
// F10 (codex audit): run-local used to drop the pre-logger config diagnostics on the floor (S20 soft-knob
|
|
312
319
|
// warnings + [792]④ boot notices) — main.ts drained them, this entrypoint didn't. Shared helper, no drift.
|
|
313
320
|
logConfigDiagnostics(logger);
|
|
@@ -52,7 +52,7 @@ export declare function stripDelegationTools<T extends {
|
|
|
52
52
|
}>(tools: readonly T[]): T[];
|
|
53
53
|
/** The operator autonomy mode (registry `runtime.autonomy`). */
|
|
54
54
|
export type Autonomy = "read-only" | "ask" | "plan" | "auto";
|
|
55
|
-
/** 导出给
|
|
55
|
+
/** 导出给 `test/coarse-shell-tools-mirror.test.ts` 的同源钉用(见那里的顶注)。 */
|
|
56
56
|
export declare const COARSE_SHELL_TOOLS_MIRROR: readonly string[];
|
|
57
57
|
/** Validate `commandPolicy` rules — the shape (array of {command, decision}), the `command` (a bare argv[0] name,
|
|
58
58
|
* see {@link VALID_COMMAND_NAME}), AND the `decision` enum. Returns the list of human-readable errors (empty =
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
* path-prefixed commands all bypass the argv[0] match (they route to `ask`, fail-closed, but are not blocked by
|
|
24
24
|
* name). Real isolation is the `executionEnv` sandbox. Same caveat core documents on `createCoarseCommandNamePolicy`.
|
|
25
25
|
*/
|
|
26
|
-
import { combinePolicies, createCoarseCommandNamePolicy, DEFAULT_SUBAGENT_TOOL_NAME, parseLeadingCommandName, tightenTaskSpec, } from "@sema-agent/core";
|
|
26
|
+
import { combinePolicies, COARSE_SHELL_TOOLS as CORE_COARSE_SHELL_TOOLS, createCoarseCommandNamePolicy, DEFAULT_SUBAGENT_TOOL_NAME, parseLeadingCommandName, tightenTaskSpec, } from "@sema-agent/core";
|
|
27
27
|
import { currentGovernanceAskMarks } from "./governance-ask-marks.js";
|
|
28
28
|
/** True when `name` names a DELEGATION tool — the subagent tool(CC-187 canonical `"Agent"`,core
|
|
29
29
|
* `DEFAULT_SUBAGENT_TOOL_NAME`)。5.0.0 RB-476:折叠面退役,RAW 比对 core 单源常量——旧名("Task")
|
|
@@ -48,23 +48,20 @@ export function stripDelegationTools(tools) {
|
|
|
48
48
|
const kept = tools.filter((t) => !isDelegationToolName(t.name));
|
|
49
49
|
return kept.length === tools.length ? tools : kept;
|
|
50
50
|
}
|
|
51
|
-
/** The shell tool names a command-name gate parses
|
|
52
|
-
*
|
|
53
|
-
* (a `Bash` ask-command asks) so a core rename surfaces as a red test, not silent drift.
|
|
51
|
+
/** The shell tool names a command-name gate parses — **core 的 {@link COARSE_SHELL_TOOLS} 本尊**,不再是镜像。
|
|
52
|
+
* 本仓的 ask-list 策略因此恒门住与 `createCoarseCommandNamePolicy` **同一批**工具。
|
|
54
53
|
* Q6 (core 1.161): tool names align to CC — `bash`→`Bash`; the old `bash_readonly` MERGED into `Bash` (read-only is
|
|
55
54
|
* now a toolEffect, not a separate tool).
|
|
56
55
|
*
|
|
57
|
-
* 🔴
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
* `
|
|
63
|
-
*
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
const COARSE_SHELL_TOOLS = ["Bash", "Monitor"];
|
|
67
|
-
/** 导出给 的枚举式行为钉用(见那里的顶注:从 core 行为反推再比对)。 */
|
|
56
|
+
* 🔴 A-033.1(#256)换装记:本常量此前是**手抄镜像** `["Bash","Monitor"] as const`,注释自称「core 未导出」。
|
|
57
|
+
* 那句话在 core RB-153(server [1708] 请托)之后就过期了 —— core 现在**公开导出**这份名单(index.d.ts:134),
|
|
58
|
+
* 它自己的原话是「The list itself is the fact; publishing it removes the guess」。镜像的真实代价有前科:
|
|
59
|
+
* core 1.414 把默认从 `["Bash"]` 扩到 `["Bash","Monitor"]`(Monitor 走同一条执行 seam),镜像漏跟一轮 ⇒
|
|
60
|
+
* **blocklist 模式下本仓的 ask-list 不覆盖 Monitor 而 core 的 deny 覆盖**(core 端到端实证:同一条
|
|
61
|
+
* `rm -rf $(cat targets)`,Bash 被拒、Monitor 零门执行)。换真 import 之后这一整类漏跟在结构上消失。
|
|
62
|
+
* `test/coarse-shell-tools-mirror.test.ts` 的钉相应从「行为反推 + 值比对」改成**同源断言**(见那里顶注)。 */
|
|
63
|
+
const COARSE_SHELL_TOOLS = CORE_COARSE_SHELL_TOOLS;
|
|
64
|
+
/** 导出给 `test/coarse-shell-tools-mirror.test.ts` 的同源钉用(见那里的顶注)。 */
|
|
68
65
|
export const COARSE_SHELL_TOOLS_MIRROR = COARSE_SHELL_TOOLS;
|
|
69
66
|
/**
|
|
70
67
|
* A per-command ask-list ToolPolicy: a parsed shell command whose `argv[0]` is in `askCommands` tightens to
|
package/dist/security.d.ts
CHANGED
|
@@ -138,7 +138,9 @@ export interface AuthContext {
|
|
|
138
138
|
sessionId: string;
|
|
139
139
|
/** Long-term memory scope, derived from the principal (not the body). Undefined disables memory. */
|
|
140
140
|
memoryScope?: string;
|
|
141
|
-
/** 142-S4:the request's SHAPE-VALIDATED projectId
|
|
141
|
+
/** 142-S4:the request's SHAPE-VALIDATED **且已归一(小写)** 的 projectId(body.projectId 过 core
|
|
142
|
+
* `PROJECT_ID_REGEX` 门后折小写回传;A-033.1/#256 宽读严写 —— 词法带 /i 收下大写形,但派生 scope 键
|
|
143
|
+
* 与 config.projects 查表都只认这一个归一形,否则同一个项目会按大小写分成两只记忆盘)。
|
|
142
144
|
* 身份半场(scope 的 tenant 段)永远来自 verified principal — projectId 只选「哪个项目盘/哪条登记」
|
|
143
145
|
* (config.projects 查表键 + proj: 键的 projectId 段),不是 capability。main.ts 拿它查 defaultScopes。 */
|
|
144
146
|
resolvedProjectId?: string;
|