@sema-agent/server 7.98.0 → 7.99.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/MIGRATION.md +6 -0
- package/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/USAGE.md +12 -9
- package/deploy/sema-up/lib/envf.sh +110 -42
- package/deploy/sema-up/sema-up.sh +76 -34
- package/dist/boot/config-center.js +167 -22
- package/dist/boot/org-memory.d.ts +2 -1
- package/dist/boot/org-memory.js +9 -5
- package/dist/boot/runtime-caps.js +4 -4
- package/dist/capabilities/center-prompts.d.ts +3 -0
- package/dist/capabilities/center-prompts.js +1 -1
- package/dist/center-credential.d.ts +112 -0
- package/dist/center-credential.js +131 -0
- package/dist/config-catalog.js +3 -2
- package/dist/config-center/center-request.d.ts +60 -0
- package/dist/config-center/center-request.js +71 -0
- package/dist/config-center/http-client.d.ts +17 -9
- package/dist/config-center/http-client.js +27 -36
- package/dist/config-center/skills-mcp.d.ts +4 -1
- package/dist/config-center/skills-mcp.js +2 -2
- package/dist/config-invariants.d.ts +2 -2
- package/dist/config-invariants.js +0 -8
- package/dist/config-lkg.d.ts +47 -8
- package/dist/config-lkg.js +107 -20
- package/dist/config-provider.d.ts +8 -7
- package/dist/config-provider.js +5 -5
- package/dist/config-types.d.ts +8 -4
- package/dist/config.js +11 -13
- package/dist/fleet-client.d.ts +8 -8
- package/dist/fleet-client.js +7 -7
- package/dist/fleet-lease.d.ts +11 -11
- package/dist/fleet-lease.js +17 -13
- package/dist/hosted-posture.d.ts +49 -93
- package/dist/hosted-posture.js +1 -25
- package/dist/observability/secret-env-scrub.d.ts +2 -26
- package/dist/observability/secret-env-scrub.js +2 -2
- package/dist/plugins/remote-env-host.d.ts +3 -1
- package/dist/plugins/remote-env-host.js +3 -2
- package/dist/project-memory.d.ts +28 -1
- package/dist/project-memory.js +22 -22
- package/dist/run-local.js +0 -2
- package/dist/runtime-caps-resolver.d.ts +8 -8
- package/dist/runtime-caps-resolver.js +16 -14
- package/dist/sealed-key.js +3 -7
- package/dist/server-secret-env.d.ts +41 -0
- package/dist/server-secret-env.js +18 -0
- package/dist/tool-approval.d.ts +48 -1
- package/dist/tool-approval.js +10 -3
- package/package.json +4 -3
package/dist/hosted-posture.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* S-591 / S-564(车LT,7.95.0)—— **「这台部署是不是托管 / 多租」的单一铸点**。
|
|
2
|
+
* S-591 / S-564(车LT,7.95.0)→ S-671 / B-10(车NB,7.99.0)—— **「这台部署是不是托管 / 多租」的单一铸点**。
|
|
3
3
|
*
|
|
4
4
|
* ## 为什么是一处
|
|
5
5
|
*
|
|
@@ -21,60 +21,62 @@
|
|
|
21
21
|
* **`deviceCwdHonored` 这一臂里没有 `requirePrincipal` 项**(`task-cwd.ts`:device 车道上那条路径落在
|
|
22
22
|
* **发起者自己的设备**上,归属由 `device_session` 绑定行验过 ⇒ 跨租户面结构性不存在)。
|
|
23
23
|
*
|
|
24
|
-
*
|
|
25
|
-
* 那台机器上每一条赦免都成立(它自称单用户),而每一道天花板都不成立(它没有 org 分区)。
|
|
26
|
-
* ⇒ 先把这个判定位铸成**一处**,再让两条腿都读它(clay 裁定③,2026-09-22):**不许各自加特判**——
|
|
24
|
+
* ⇒ 先把这个判定位铸成**一处**,再让各条腿都读它(clay 裁定③,2026-09-22):**不许各自加特判**——
|
|
27
25
|
* 每加一条特判,「这台机器算不算托管」在本仓就多一个可以自己漂的答案。
|
|
28
26
|
*
|
|
29
|
-
* ##
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
* (
|
|
33
|
-
* ·
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
* 两者都坏,但假阳会当场打到在跑的部署,所以词表只收**单用户形写不出来**的证据:
|
|
27
|
+
* ## 判据:托管 ⇔ 运维显式声明 `REQUIRE_PRINCIPAL=true`(clay 裁定 [ref] ①,2026-09-23)
|
|
28
|
+
*
|
|
29
|
+
* 只有一条定义式证据,零推断。7.95.0–7.98.x 还收过两条「运维点名了一个外部身份签发方」的推断臂
|
|
30
|
+
* (`PRINCIPAL_JWT_PUBKEYS/ISS/AUD` 任一在场;`AUTH_BRIDGE_ISSUER` 显式在场),[ref] ① 裁 (a) 撤回:
|
|
31
|
+
* · **真假阳**:本机自起引擎、配了 principal-JWT / auth-bridge 签发方的单用户部署(web-admin 8090 真案)
|
|
32
|
+
* 在 7.98.0 被判托管而拒启,只能滚回 7.93.5;
|
|
33
|
+
* · **与回退 issuer 不同判**:auth-bridge 的 `?? configCenter.baseUrl` 回退(`boot/stage-09-leader.ts`)
|
|
34
|
+
* 从来不算证据,而回退桥的引擎同样把单用户授予开给每一个验签通过的 registry JWT 调用者 —— 同一个
|
|
35
|
+
* 租户轴事实,「显式写了 issuer」与「从 center 推出 issuer」是同一形。裁定后两者同判。
|
|
36
|
+
* ⇒ 签发方配置是**身份验签轴**(谁的 JWT 算数),不是租户轴(这台机器有几个主人)。「受控于控制面」
|
|
37
|
+
* (`SEMA_REGISTRY_*`,含 worker 名)同理是 fleet 管理轴,本判据同样不读。
|
|
38
|
+
*
|
|
39
|
+
* 随两臂一起删掉的(按类删干净,规则集变小):S-564 腿①「托管 ∧ 未开 `REQUIRE_PRINCIPAL` ⇒ 拒启」
|
|
40
|
+
* (判据只剩读 `requirePrincipal` 的那一臂之后,那条合取恒假 ⇒ 结构上不可达)及其跨键不变量行;
|
|
41
|
+
* `run-local.ts` 为那两臂清 env 的清除口。
|
|
42
|
+
*
|
|
43
|
+
* ## 被排除的候选(逐条写下,不留「我没想到」的空白)
|
|
44
|
+
*
|
|
45
|
+
* 假阳 = 一台单用户部署被收走一个它一直在用的能力(7.98.0 时还会被拒启)。词表不收任何推断臂:
|
|
46
|
+
* · ❌ `PRINCIPAL_JWT_*` / `AUTH_BRIDGE_ISSUER` 在场 —— 见上一段([ref] ① 撤回;加回来 = 翻案,要新裁定)。
|
|
47
|
+
* · ❌ `configCenter`(含 `worker` 名)/ auth-bridge 的 center 回退 —— fleet 管理轴。初版(车LT)收过
|
|
48
|
+
* `managed-worker` 臂,实测两个反例后撤回:`run-local.ts` 强设 `REQUIRE_PRINCIPAL=false` 的本机 CLI 在一个
|
|
49
|
+
* 带 registry 键的共享 `.env` 上拒启;本仓自己的 `test/config-coverage-blocks.test.ts` configCenter 两格当场红。
|
|
53
50
|
* · ❌ `operatorPrincipals` 非空 —— **真假阳**:`IMAGE_BAKES_ENABLED` 那条既有不变量
|
|
54
|
-
* (`config-invariants.ts` 的 `bake-operator-principals`)
|
|
55
|
-
* 于是一台开了烤门的单用户机器会被判成托管。
|
|
56
|
-
* · ❌ `configCenter`(含 `worker` 名)/ auth-bridge 的 center 回退 —— 见上一段,已实测撤回。
|
|
51
|
+
* (`config-invariants.ts` 的 `bake-operator-principals`)**强制**开烤门的部署把它填非空。
|
|
57
52
|
* · ❌ `SERVICE_AUTH_TOKENS` 条目数 —— 一台机器上同时给 cli 与 web-admin 发一只令牌是单用户常态。
|
|
58
53
|
* · ❌ `DB_BACKEND=mysql|pg` —— 那是**耐久性**轴,不是租户轴(单用户要 durable run 也得开)。
|
|
59
54
|
* · ❌ `BIND_HOST` 非回环 —— 容器里 `0.0.0.0` 是单用户常态(compose/k8s 默认)。
|
|
60
|
-
* · ❌ `COMPLIANCE_PROFILE` / `RETENTION_MAX_AGE_DAYS` / `LOCKED_CONFIG_KEYS` ——
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
* `
|
|
66
|
-
*
|
|
55
|
+
* · ❌ `COMPLIANCE_PROFILE` / `RETENTION_MAX_AGE_DAYS` / `LOCKED_CONFIG_KEYS` —— 数据/治理轴。
|
|
56
|
+
*
|
|
57
|
+
* ## 🔴 召回缺口是**全量**的(显式登记,不用「应该很少」填空)
|
|
58
|
+
*
|
|
59
|
+
* 一台多用户 / 团队机器只要运维**忘设** `REQUIRE_PRINCIPAL`,本判据就**看不见**它 —— 无论它有没有
|
|
60
|
+
* 配签发方、受不受 reconciler 管。那台机器上:`principal` 缺席走单用户 `"default"` 赦免 ⇒ 全服无主会话
|
|
61
|
+
* 被列给每一个调用方;上面四条能力授予面(hooks / `settings.env` / `cwd` / `additionalDirectories`)对每个
|
|
62
|
+
* 调用方打开;写容纳面天花板与 `approverPosture` 留声两条腿(下文)也按单用户放行。
|
|
63
|
+
* `settings.env` 的射程要说准:适配器的 `env` 是**并**到 `process.env` 上的逐键覆盖(`plugins/remote-env-host.ts`
|
|
64
|
+
* 的 `mergeEnv`),而 `task-settings.ts` 的 `cleanEnvMap` 没有任何键名拒集 ⇒ `PATH` / `LD_PRELOAD` /
|
|
65
|
+
* `NODE_OPTIONS` 一视同仁。
|
|
66
|
+
* **补偿**:`REQUIRE_PRINCIPAL=true` 本来就是那台机器该设的键(运维自己的声明;server 不替他推断)——
|
|
67
|
+
* `docs/DEPLOY-PREREQS.md` 的「托管 / 多租部署」段把它写成部署前置,并列出忘设它的后果。
|
|
67
68
|
*/
|
|
68
69
|
import { type ServiceConfigFlat } from "./config-types.js";
|
|
69
|
-
/** 托管/多租的**证据词表**(闭集;加词 ⇒ {@link hostedEvidenceNote} 的穷举 switch 编译红)。
|
|
70
|
-
|
|
71
|
-
|
|
70
|
+
/** 托管/多租的**证据词表**(闭集;加词 ⇒ {@link hostedEvidenceNote} 的穷举 switch 编译红)。
|
|
71
|
+
* 🔴 恰一词是裁定([ref] ①),不是暂时的:加回一条推断臂 = 翻案,先要新裁定。 */
|
|
72
|
+
export declare const HOSTED_EVIDENCE_WORDS: readonly ["require-principal"];
|
|
73
|
+
/** 一条证据(闭集一词)。 */
|
|
72
74
|
export type HostedEvidence = (typeof HOSTED_EVIDENCE_WORDS)[number];
|
|
73
75
|
/**
|
|
74
|
-
* 本判据读的配置键集 —— 取自平铺配置型(键名/类型永不与真配置漂)
|
|
75
|
-
*
|
|
76
|
+
* 本判据读的配置键集 —— 取自平铺配置型(键名/类型永不与真配置漂)。`ServiceConfig` **结构上**满足本形,
|
|
77
|
+
* 所以请求腿(阶段④)与能力面读同一只函数。
|
|
76
78
|
*/
|
|
77
|
-
export type HostedPostureInput = Readonly<Pick<ServiceConfigFlat, "requirePrincipal"
|
|
79
|
+
export type HostedPostureInput = Readonly<Pick<ServiceConfigFlat, "requirePrincipal">>;
|
|
78
80
|
/** 一台 worker 的托管姿态事实(值 + 证据)——纯数据。 */
|
|
79
81
|
export interface HostedPostureFacts {
|
|
80
82
|
/** 这台部署是不是托管 / 多租。 */
|
|
@@ -83,60 +85,14 @@ export interface HostedPostureFacts {
|
|
|
83
85
|
readonly evidence: readonly HostedEvidence[];
|
|
84
86
|
}
|
|
85
87
|
/**
|
|
86
|
-
* 判定位的**唯一**铸点(纯数据 ⇒ `build*`)。
|
|
88
|
+
* 判定位的**唯一**铸点(纯数据 ⇒ `build*`)。托管 ⇔ `requirePrincipal === true`([ref] ①)。
|
|
87
89
|
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
+
* `evidence` 仍是数组(按词表顺序收**全部**命中):拒因逐字点它们,运维才知道改哪儿;词表是闭集,
|
|
91
|
+
* 形状不随词数变。
|
|
90
92
|
*/
|
|
91
93
|
export declare function buildHostedPosture(c: HostedPostureInput): HostedPostureFacts;
|
|
92
|
-
/**
|
|
93
|
-
* 判据里**非定义式**那两条证据背后的 env 名 —— 即「运维显式点名了一个外部身份签发方」的全部拼法。
|
|
94
|
-
*
|
|
95
|
-
* 🔴 **单一属主**:`src/run-local.ts` 那条「强制单用户」的腿按本表清 env(W-新8)。它**结构上**兑现不了
|
|
96
|
-
* 这些键(它把 `REQUIRE_PRINCIPAL` 焊死成 false ⇒ principal 门根本不上场),留着它们只会让判据看见一个
|
|
97
|
-
* 假事实、然后拿一个**它自己会覆盖回去**的旋钮当拒因。名表抄第二份的那天,加一条证据 = run-local 静默漏清。
|
|
98
|
-
* ⚠️ 表里**没有** `REQUIRE_PRINCIPAL`(那是定义式那一条,run-local 自己显式写值)也**没有**
|
|
99
|
-
* `SEMA_REGISTRY_*`(fleet 管理轴,本判据根本不读 —— 本机 CLI 从组织下发面取配置是正当用法)。
|
|
100
|
-
*/
|
|
101
|
-
export declare const HOSTED_EVIDENCE_ENV_KEYS: readonly ["PRINCIPAL_JWT_PUBKEYS", "PRINCIPAL_JWT_ISS", "PRINCIPAL_JWT_AUD", "AUTH_BRIDGE_ISSUER"];
|
|
102
|
-
/**
|
|
103
|
-
* 把上表那几根 env **清掉**(`run-local.ts` 的「强制单用户」腿调它)。
|
|
104
|
-
*
|
|
105
|
-
* 🔴 **为什么是四行字面 `delete` 而不是照表循环**:配置目录的 D1 对账门按文件数**动态 env 读点**并且是
|
|
106
|
-
* **只降不升的棘轮** —— `process.env[k]` 那种计算属性读它一个都不放过(理由是动态读让「这个文件读了哪些
|
|
107
|
-
* 键」变成机器看不见的事)。照表循环会给 `run-local.ts` 顶上第 3 处动态读、当场撞棘轮。⇒ 名字写成字面,
|
|
108
|
-
* **但只写这一处**;上面那张表仍是**声明面**的属主,两者的一致性由
|
|
109
|
-
* `test/hosted-posture.test.ts` 的行为格看着(逐个设上再调一次,断言全没了)——不是靠人眼比对。
|
|
110
|
-
*
|
|
111
|
-
* ⚠️ 表里**没有** `REQUIRE_PRINCIPAL`(定义式那条,调用方自己显式写值)也没有 `SEMA_REGISTRY_*`
|
|
112
|
-
* (fleet 管理轴,本判据根本不读)。
|
|
113
|
-
*/
|
|
114
|
-
export declare function clearHostedEvidenceEnv(): void;
|
|
115
94
|
/** 一条证据的运维事实句 —— **穷举 switch,无缺省臂**(词表加员即编译红)。 */
|
|
116
95
|
export declare function hostedEvidenceNote(e: HostedEvidence): string;
|
|
117
|
-
/**
|
|
118
|
-
* **腿①(S-564)** —— 托管形部署必须显式开 `REQUIRE_PRINCIPAL`。返回拒因;`undefined` = 通过。
|
|
119
|
-
*
|
|
120
|
-
* 判据与文案的**唯一属主**:boot 期的拒启 throw(`config.ts` 的跨域不变量区)与
|
|
121
|
-
* `CONFIG_CROSS_INVARIANTS` 表(`config-invariants.ts`)**调同一只**,不各抄一份([ref] 的纪律 +
|
|
122
|
-
* [ref]「禁手抄投影」)。
|
|
123
|
-
*
|
|
124
|
-
* 🔴 **表里那一行今天是「预置纵深」,不是在岗执法**(主会话裁定⑤,撤回初版那句「热路径同样在岗」):
|
|
125
|
-
* `checkCrossInvariants` 的候选视图(`config-hot` 那条腿)今天只覆写 limits 那 14 员键,本判据读的
|
|
126
|
-
* 5 个键**一个都不在里面** ⇒ 那一行在热路径上**永不返回拒因**。它先在场是纵深(哪天候选视图覆盖到
|
|
127
|
-
* auth 域,它自己就活),不是现在有人在执法。**假覆盖比没覆盖更坏**(7.94.0 自己刚立的反面规矩)。
|
|
128
|
-
*
|
|
129
|
-
* 🔴 **为什么是拒启而不是 warn**:`principal` 缺席在本仓走 `"default"` 赦免 —— 一台没开这根旋钮的
|
|
130
|
-
* 多用户机器会把**全服无主会话**列进 `ListAgents`,并且把 hooks / shellEnv / cwd / 根围栏拓宽四条
|
|
131
|
-
* 能力授予面一并对每个调用方打开(四条面的谓词各不相同,逐条在模块头注 —— 别照「都一样」读)。
|
|
132
|
-
* 安全轴上的 fail-open 不许有安静的兜底臂([ref])。
|
|
133
|
-
*
|
|
134
|
-
* 🔴 **`settings.env` 那一条的射程要说准**(亲读 `plugins/remote-env-host.ts` 的 `mergeEnv`):适配器的
|
|
135
|
-
* `env` 是**并**到 `process.env` 上的逐键覆盖(不是替),而 `task-settings.ts` 的 `cleanEnvMap` 只判
|
|
136
|
-
* 类型/键长/值长/条数、**没有任何键名拒集** ⇒ `PATH` / `LD_PRELOAD` / `NODE_OPTIONS` 一视同仁。
|
|
137
|
-
* 那条面唯一的防线就是上面那道闸;闸一漂,面全开。
|
|
138
|
-
*/
|
|
139
|
-
export declare function hostedPrincipalRefusal(c: HostedPostureInput): string | undefined;
|
|
140
96
|
/** 腿② 读的键集 = 射程判据 + 天花板旋钮。
|
|
141
97
|
* 🔴 档位的**词表属主在 `config-types.ts`**(`TASK_WRITE_FACE_ESCALATIONS`)—— 本模块刻意**不留第二份**:
|
|
142
98
|
* 施工期真犯过一次(两处各写一份 `["allow","refuse"]`,加词的那天两边会分家),[ref] 第一排查点。 */
|
package/dist/hosted-posture.js
CHANGED
|
@@ -1,45 +1,21 @@
|
|
|
1
1
|
import { TASK_WRITE_FACE_ESCALATIONS } from "./config-types.js";
|
|
2
2
|
import { deviceCwdHonored } from "./task-cwd.js";
|
|
3
|
-
export const HOSTED_EVIDENCE_WORDS = ["require-principal"
|
|
3
|
+
export const HOSTED_EVIDENCE_WORDS = ["require-principal"];
|
|
4
4
|
export function buildHostedPosture(c) {
|
|
5
5
|
const evidence = [];
|
|
6
6
|
if (c.requirePrincipal === true)
|
|
7
7
|
evidence.push("require-principal");
|
|
8
|
-
if (c.principalJwtPubkeys.length > 0 || c.principalJwtIss !== undefined || c.principalJwtAud !== undefined)
|
|
9
|
-
evidence.push("principal-jwt");
|
|
10
|
-
if (c.authBridgeIssuer !== undefined)
|
|
11
|
-
evidence.push("auth-bridge-issuer");
|
|
12
8
|
return { hosted: evidence.length > 0, evidence };
|
|
13
9
|
}
|
|
14
|
-
export const HOSTED_EVIDENCE_ENV_KEYS = ["PRINCIPAL_JWT_PUBKEYS", "PRINCIPAL_JWT_ISS", "PRINCIPAL_JWT_AUD", "AUTH_BRIDGE_ISSUER"];
|
|
15
|
-
export function clearHostedEvidenceEnv() {
|
|
16
|
-
delete process.env.PRINCIPAL_JWT_PUBKEYS;
|
|
17
|
-
delete process.env.PRINCIPAL_JWT_ISS;
|
|
18
|
-
delete process.env.PRINCIPAL_JWT_AUD;
|
|
19
|
-
delete process.env.AUTH_BRIDGE_ISSUER;
|
|
20
|
-
}
|
|
21
10
|
export function hostedEvidenceNote(e) {
|
|
22
11
|
switch (e) {
|
|
23
12
|
case "require-principal":
|
|
24
13
|
return "REQUIRE_PRINCIPAL=true (this deployment declares itself multi-tenant)";
|
|
25
|
-
case "principal-jwt":
|
|
26
|
-
return "PRINCIPAL_JWT_PUBKEYS/ISS/AUD is configured (principal identities are verified against an external issuer — a single-user turnkey box has no issuer)";
|
|
27
|
-
case "auth-bridge-issuer":
|
|
28
|
-
return "AUTH_BRIDGE_ISSUER is set explicitly (a second external SSO-JWT issuer; the registry-URL fallback is deliberately NOT read here — that fact is fleet management, not tenancy)";
|
|
29
14
|
}
|
|
30
15
|
}
|
|
31
16
|
function hostedBecause(facts) {
|
|
32
17
|
return facts.evidence.map(hostedEvidenceNote).join("; ");
|
|
33
18
|
}
|
|
34
|
-
export function hostedPrincipalRefusal(c) {
|
|
35
|
-
const facts = buildHostedPosture(c);
|
|
36
|
-
if (!facts.hosted || c.requirePrincipal === true)
|
|
37
|
-
return undefined;
|
|
38
|
-
return (`this deployment looks HOSTED/multi-tenant (${hostedBecause(facts)}) but REQUIRE_PRINCIPAL is not set — ` +
|
|
39
|
-
`an absent principal takes the single-user "default" amnesty, which would list every ownerless session to every caller ` +
|
|
40
|
-
`and open the single-user capability grants (settings.hooks / settings.env / cwd / additionalDirectories) to all of them. ` +
|
|
41
|
-
`Set REQUIRE_PRINCIPAL=true (and OPERATOR_PRINCIPALS for the operator-only routes), or remove the hosted-deployment configuration if this really is a single-user box.`);
|
|
42
|
-
}
|
|
43
19
|
export const HOST_POSTURE_REFUSED_CODE = "request.host_posture_refused";
|
|
44
20
|
function hostPostureDeclarationsHonored(c) {
|
|
45
21
|
if (deviceCwdHonored(c))
|
|
@@ -1,28 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
* [ref]([ref])—— **密钥形 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 { type SecretEnvFinding } from "@sema-agent/core";
|
|
1
|
+
import type { ServerSecretEnvFinding } from "../server-secret-env.js";
|
|
26
2
|
import type { Logger } from "./logger.js";
|
|
27
3
|
import type { Metrics } from "./metrics.js";
|
|
28
4
|
/** 登记过的剥除现场。闭集:未登记的 site 传不进 {@link recordSecretEnvScrub}(编译期拒,同 FAIL_OPEN_TAGS 纪律)。 */
|
|
@@ -40,7 +16,7 @@ interface Sink {
|
|
|
40
16
|
* 记一次剥除。**总不抛** —— 观测本身绝不能变成故障(同 `recordFailOpen` / reapers 的判据)。
|
|
41
17
|
* `findings` 为空(这一次没丢任何 key)时什么也不做:那不是事件,是常态。
|
|
42
18
|
*/
|
|
43
|
-
export declare function recordSecretEnvScrub(site: SecretEnvScrubSite, findings: readonly
|
|
19
|
+
export declare function recordSecretEnvScrub(site: SecretEnvScrubSite, findings: readonly ServerSecretEnvFinding[]): void;
|
|
44
20
|
/** 装配层把 logger/metrics 接上(main.ts,与 `installFailOpenRecorder` 同一处)。装配前累计的次数在此补进 metrics。 */
|
|
45
21
|
export declare function installSecretEnvScrubRecorder(deps: Sink): void;
|
|
46
22
|
/** 已留驻的「现场×键」条数(测试/诊断读面)。恒 ≤ {@link DISCLOSED_CAP}。 */
|
|
@@ -62,7 +62,7 @@ export function recordSecretEnvScrub(site, findings) {
|
|
|
62
62
|
}
|
|
63
63
|
export function installSecretEnvScrubRecorder(deps) {
|
|
64
64
|
sink = { ...deps };
|
|
65
|
-
deps.metrics?.counter(SCRUB_COUNTER, "secret
|
|
65
|
+
deps.metrics?.counter(SCRUB_COUNTER, "secret env entries dropped before spawning a child process, by site/rule/confidence (core scrubSecretEnv shape rule + server config-catalog secret names)");
|
|
66
66
|
drainToMetrics();
|
|
67
67
|
}
|
|
68
68
|
export function secretEnvScrubDisclosedSize() {
|
|
@@ -76,6 +76,6 @@ export function resetSecretEnvScrubRecorderForTest(deps = {}) {
|
|
|
76
76
|
pushed = new Map();
|
|
77
77
|
disclosed = new Set();
|
|
78
78
|
sink = { ...deps };
|
|
79
|
-
deps.metrics?.counter(SCRUB_COUNTER, "secret
|
|
79
|
+
deps.metrics?.counter(SCRUB_COUNTER, "secret env entries dropped before spawning a child process, by site/rule/confidence (core scrubSecretEnv shape rule + server config-catalog secret names)");
|
|
80
80
|
}
|
|
81
81
|
//# sourceMappingURL=secret-env-scrub.js.map
|
|
@@ -33,6 +33,8 @@ export interface HostEnvConfig {
|
|
|
33
33
|
commandTimeoutMs?: number;
|
|
34
34
|
/** Shell-child env policy (D1; mirrors core's `NodeExecutionEnv`). Default `"scrub"`: inherit the worker's
|
|
35
35
|
* `process.env` but DROP secret-pattern keys (core's `scrubSecretEnv` — `*_KEY`/`*_TOKEN`/`*_SECRET`/`*_PASSWORD`…)
|
|
36
|
+
* AND every env name the server's config catalog types as `secret` (`server-secret-env.ts`; 7.99.0 F7 —
|
|
37
|
+
* `SERVICE_AUTH_TOKENS` / `APPROVAL_HMAC_KEYS` slip the shape rule)
|
|
36
38
|
* so a model-driven shell can't `printenv DEEPSEEK_API_KEY` and exfiltrate orchestrator secrets — keeps PATH/HOME/
|
|
37
39
|
* locale/build (low breakage). `"all"` = inherit everything (explicit opt-out, trusted local dev). A `string[]` =
|
|
38
40
|
* strict allowlist. `env`/per-command `options.env` ALWAYS pass through verbatim. */
|
|
@@ -386,7 +388,7 @@ export declare class RemoteHostExecutionEnv implements RemoteExecutionEnv, Sched
|
|
|
386
388
|
private fsReady;
|
|
387
389
|
/** Base env (process.env when inheriting) merged with the adapter's secret env, then per-command overrides win. */
|
|
388
390
|
/** D1 secure default = `"scrub"`: inherit `process.env` minus secret-pattern keys (core's `scrubSecretEnv`, single
|
|
389
|
-
* source of
|
|
391
|
+
* source of the shape rule) minus the config catalog's `secret`-typed names (`scrubServerSecrets`, 7.99.0 F7). `"all"` = full inherit (opt-out); a `string[]` = strict allowlist. Legacy `inheritProcessEnv`
|
|
390
392
|
* maps `true`→`"all"` / `false`→`[]`. */
|
|
391
393
|
private inheritMode;
|
|
392
394
|
private mergeEnv;
|
|
@@ -9,8 +9,9 @@ import { armPipeDestroyGrace, numEnvOr, SYMLINK_CHAIN_MAX_HOPS } from "./remote-
|
|
|
9
9
|
import { hostBackgroundShellEnabled, hostExecSpoolEnabled } from "../config.js";
|
|
10
10
|
import { resolveHostShell, hostShell, spawnGroupOptions, killTreeHard, killTreeSoft, collapseWin32EnvKeys, adoptHostForegroundChild } from "./host-platform.js";
|
|
11
11
|
import { BackgroundShellManager, seedMemStream, feedMemStream, drainMemStream } from "./background-shell-support.js";
|
|
12
|
-
import { FileError, ExecutionError, RemoteExecutionError,
|
|
12
|
+
import { FileError, ExecutionError, RemoteExecutionError, RollingTailBuffer, markTruncated, SchedulerError, BackgroundShellError, } from "@sema-agent/core";
|
|
13
13
|
import { recordSecretEnvScrub } from "../observability/secret-env-scrub.js";
|
|
14
|
+
import { scrubServerSecrets } from "../server-secret-env.js";
|
|
14
15
|
import { trackEphemeralHostWorkspace, untrackEphemeralHostWorkspace } from "./host-workspace-registry.js";
|
|
15
16
|
const PROVIDER = "host";
|
|
16
17
|
const DEFAULT_COMMAND_TIMEOUT_MS = 30 * 60_000;
|
|
@@ -1312,7 +1313,7 @@ export class RemoteHostExecutionEnv {
|
|
|
1312
1313
|
}
|
|
1313
1314
|
else {
|
|
1314
1315
|
const findings = [];
|
|
1315
|
-
base =
|
|
1316
|
+
base = scrubServerSecrets(process.env, findings);
|
|
1316
1317
|
recordSecretEnvScrub("server.remote-env-host.inherit-scrub", findings);
|
|
1317
1318
|
}
|
|
1318
1319
|
if (this.cfg.env)
|
package/dist/project-memory.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type PlatformPath } from "node:path";
|
|
1
2
|
import type { ProjectMemoryLoad } from "@sema-agent/core";
|
|
2
3
|
interface ProjectMemoryCtx {
|
|
3
4
|
cwd: string;
|
|
@@ -7,9 +8,35 @@ interface ProjectMemoryCtx {
|
|
|
7
8
|
sessionId?: string;
|
|
8
9
|
phase?: "fresh" | "resume" | "post-compact" | "post-clear";
|
|
9
10
|
}
|
|
11
|
+
/** The instruction-file name chain: the CLAUDE family first, the AGENTS family only when no CLAUDE file is
|
|
12
|
+
* present. The FIRST hit is the single declared instruction source.
|
|
13
|
+
*
|
|
14
|
+
* Order follows CC 2.1.280 (clay ruling [ref], 2026-09-23, board [ref] RG-2). It replaces the core 1.302 S-1
|
|
15
|
+
* order, where AGENTS.md won over CLAUDE.md; [ref] supersedes that. In CC 2.1.280:
|
|
16
|
+
* - cli280.js:185162 — the default mode `claude-md-or-agents-md`: "a project with no CLAUDE.md of its own gets its
|
|
17
|
+
* AGENTS.md files instead, loaded exactly where and how CLAUDE.md would be";
|
|
18
|
+
* - cli280.js:245920 — the two name families: AGENTS `["AGENTS.md", ".claude/AGENTS.md"]`,
|
|
19
|
+
* CLAUDE `["CLAUDE.md", ".claude/CLAUDE.md", "CLAUDE.local.md"]`;
|
|
20
|
+
* - cli280.js:226831 — the 2.1.277 changelog entry "in a project with no CLAUDE.md, Claude Code reads AGENTS.md
|
|
21
|
+
* instead".
|
|
22
|
+
* Not followed here (CC divergences, registration drafted in the che-MN closing note): `CLAUDE.local.md` is not read;
|
|
23
|
+
* CC's other `instructionFiles` modes (`claude-md-and-agents-md` loads both, `managed-only`) are not offered; CC loads
|
|
24
|
+
* every CLAUDE file it finds, per directory level up the ancestor chain, whereas this loader reads one root and
|
|
25
|
+
* takes the single first hit. */
|
|
26
|
+
declare const INSTRUCTION_FILE_CHAIN: readonly ["CLAUDE.md", ".claude/CLAUDE.md", "AGENTS.md", ".claude/AGENTS.md"];
|
|
27
|
+
type ChainMember = (typeof INSTRUCTION_FILE_CHAIN)[number];
|
|
28
|
+
/** Recover (root, rel) for a path the LOADER declared — every declared path is `root + one chain member`.
|
|
29
|
+
* The probe needs this to re-run the loader's parent-directory symlink gate each round (self-review: the probe
|
|
30
|
+
* checked only the final component, so swapping `.claude` for a symlink out of the repo MID-RUN made the probe
|
|
31
|
+
* fingerprint an outside file). Exported only for its path-matrix pin (posix / win32 through `p`); the probe
|
|
32
|
+
* calls it with the host `node:path`. */
|
|
33
|
+
export declare function instructionChainCoordinates(absPath: string, p?: PlatformPath): {
|
|
34
|
+
root: string;
|
|
35
|
+
rel: ChainMember;
|
|
36
|
+
} | null;
|
|
10
37
|
/**
|
|
11
38
|
* Build a `loadProjectMemory` for the host lane. Returns null (skip) unless hands are enabled AND cwd is a local
|
|
12
|
-
* directory. Reads the instruction-file chain (
|
|
39
|
+
* directory. Reads the instruction-file chain (CLAUDE.md → .claude/CLAUDE.md → AGENTS.md → .claude/AGENTS.md, [ref]) + (non-subagent)
|
|
13
40
|
* `git log -5` / `git status`, redacts secrets, and caps the total. A subagent gets a trimmed instruction file and
|
|
14
41
|
* no git narrative (cost control, H7). When an instruction file is hit the return is a `ProjectMemoryLoad`
|
|
15
42
|
* declaring it in `instructionSources` (path + sha256 over the raw bounded-read window) so core's
|
package/dist/project-memory.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { promises as fs, constants as fsConstants } from "node:fs";
|
|
2
2
|
import { execFile } from "node:child_process";
|
|
3
3
|
import { createHmac, randomBytes } from "node:crypto";
|
|
4
|
-
import { isAbsolute, join
|
|
5
|
-
import { scrubSecretEnv } from "@sema-agent/core";
|
|
4
|
+
import hostPath, { isAbsolute, join } from "node:path";
|
|
6
5
|
import { recordSecretEnvScrub } from "./observability/secret-env-scrub.js";
|
|
6
|
+
import { scrubServerSecrets } from "./server-secret-env.js";
|
|
7
7
|
import { redactSecrets } from "./trace/redact.js";
|
|
8
|
-
const INSTRUCTION_FILE_CHAIN = ["
|
|
8
|
+
const INSTRUCTION_FILE_CHAIN = ["CLAUDE.md", ".claude/CLAUDE.md", "AGENTS.md", ".claude/AGENTS.md"];
|
|
9
9
|
const INSTRUCTION_READ_CAP = 24 * 1024;
|
|
10
10
|
function capUtf8(s, maxBytes) {
|
|
11
11
|
const buf = Buffer.from(s, "utf8");
|
|
@@ -41,15 +41,16 @@ function instructionHash(raw) {
|
|
|
41
41
|
return `hmac-sha256:${createHmac("sha256", INSTRUCTION_HASH_KEY).update(raw).digest("hex")}`;
|
|
42
42
|
}
|
|
43
43
|
const ABSENT_HASH = null;
|
|
44
|
-
function
|
|
45
|
-
|
|
46
|
-
for (const rel of
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
44
|
+
export function instructionChainCoordinates(absPath, p = hostPath) {
|
|
45
|
+
const candidates = INSTRUCTION_FILE_CHAIN.filter((rel) => absPath.endsWith(p.sep + rel.split("/").join(p.sep))).sort((a, b) => b.length - a.length);
|
|
46
|
+
for (const rel of candidates) {
|
|
47
|
+
let root = absPath;
|
|
48
|
+
for (let i = 0; i < rel.split("/").length; i++)
|
|
49
|
+
root = p.dirname(root);
|
|
50
|
+
if (p.join(root, rel) === absPath)
|
|
51
|
+
return { root, rel };
|
|
51
52
|
}
|
|
52
|
-
return
|
|
53
|
+
return null;
|
|
53
54
|
}
|
|
54
55
|
async function hasSymlinkedParent(root, rel) {
|
|
55
56
|
const parts = rel.split("/").slice(0, -1);
|
|
@@ -62,10 +63,15 @@ async function hasSymlinkedParent(root, rel) {
|
|
|
62
63
|
}
|
|
63
64
|
return false;
|
|
64
65
|
}
|
|
66
|
+
async function readChainMember(root, rel, maxBytes, truncatedFlag) {
|
|
67
|
+
if (rel.includes("/") && (await hasSymlinkedParent(root, rel)))
|
|
68
|
+
return null;
|
|
69
|
+
return readRawCapped(join(root, rel), maxBytes, truncatedFlag);
|
|
70
|
+
}
|
|
65
71
|
function git(root, args, maxBytes) {
|
|
66
72
|
const safeArgs = ["-c", "core.fsmonitor=false", "-c", "core.hooksPath=/dev/null", ...args];
|
|
67
73
|
const scrubFindings = [];
|
|
68
|
-
const scrubbedEnv =
|
|
74
|
+
const scrubbedEnv = scrubServerSecrets(process.env, scrubFindings);
|
|
69
75
|
recordSecretEnvScrub("server.project-memory.git-subprocess", scrubFindings);
|
|
70
76
|
return new Promise((res) => {
|
|
71
77
|
execFile("git", safeArgs, { cwd: root, timeout: 5000, maxBuffer: maxBytes, env: { ...scrubbedEnv, GIT_CONFIG_GLOBAL: "/dev/null", GIT_CONFIG_SYSTEM: "/dev/null" } }, (err, stdout, stderr) => {
|
|
@@ -156,9 +162,7 @@ export function makeLoadProjectMemory(opts) {
|
|
|
156
162
|
hitIndex = -1;
|
|
157
163
|
for (let i = 0; i < INSTRUCTION_FILE_CHAIN.length; i++) {
|
|
158
164
|
const rel = INSTRUCTION_FILE_CHAIN[i];
|
|
159
|
-
|
|
160
|
-
continue;
|
|
161
|
-
const raw = await readRawCapped(join(root, rel), claudeCap, claudeTrunc);
|
|
165
|
+
const raw = await readChainMember(root, rel, claudeCap, claudeTrunc);
|
|
162
166
|
if (raw !== null) {
|
|
163
167
|
claudeRaw = capUtf8(raw.toString("utf8"), claudeCap);
|
|
164
168
|
hitFile = rel;
|
|
@@ -171,8 +175,7 @@ export function makeLoadProjectMemory(opts) {
|
|
|
171
175
|
break;
|
|
172
176
|
let appeared = false;
|
|
173
177
|
for (const rel of INSTRUCTION_FILE_CHAIN.slice(0, hitIndex)) {
|
|
174
|
-
|
|
175
|
-
if (higher && !higher.isSymbolicLink()) {
|
|
178
|
+
if ((await readChainMember(root, rel, claudeCap)) !== null) {
|
|
176
179
|
appeared = true;
|
|
177
180
|
break;
|
|
178
181
|
}
|
|
@@ -229,11 +232,8 @@ export function makeProbeInstructionSources() {
|
|
|
229
232
|
return await Promise.all(sources.map(async ({ path }) => {
|
|
230
233
|
if (!isAbsolute(path))
|
|
231
234
|
return { path, contentHash: null };
|
|
232
|
-
const coords =
|
|
233
|
-
|
|
234
|
-
return { path, contentHash: null };
|
|
235
|
-
}
|
|
236
|
-
const raw = await readRawCapped(path, INSTRUCTION_READ_CAP);
|
|
235
|
+
const coords = instructionChainCoordinates(path);
|
|
236
|
+
const raw = coords ? await readChainMember(coords.root, coords.rel, INSTRUCTION_READ_CAP) : await readRawCapped(path, INSTRUCTION_READ_CAP);
|
|
237
237
|
return { path, contentHash: raw === null ? null : instructionHash(raw) };
|
|
238
238
|
}));
|
|
239
239
|
}
|
package/dist/run-local.js
CHANGED
|
@@ -18,7 +18,6 @@ import { taskWallClockSec } from "./task-workflow.js";
|
|
|
18
18
|
import { applyCatalogToSource } from "./capabilities/center-prompts.js";
|
|
19
19
|
import { validatePromptsDomain, CORE_ENGINE_VERSION } from "./prompts-domain-validate.js";
|
|
20
20
|
import { buildUnpricedCeilingNotice, loadConfig, logConfigDiagnostics, parseNumOrFailNonNegative } from "./config.js";
|
|
21
|
-
import { clearHostedEvidenceEnv } from "./hosted-posture.js";
|
|
22
21
|
import { buildStoreOpenRefusalTail } from "./plugins/store-open-refusal.js";
|
|
23
22
|
import { acquireLocalBackendWithWait } from "./plugins/store-lock-wait.js";
|
|
24
23
|
import { createConfigProvider } from "./config-provider.js";
|
|
@@ -176,7 +175,6 @@ export async function runLocal(argv, deps = {}) {
|
|
|
176
175
|
process.env.CONFIG_PROVIDER = "local";
|
|
177
176
|
process.env.SESSION_BACKEND = "memory";
|
|
178
177
|
process.env.REQUIRE_PRINCIPAL = "false";
|
|
179
|
-
clearHostedEvidenceEnv();
|
|
180
178
|
const root = args.root ?? process.env.CONFIG_LOCAL_DIR ?? process.env.AGENT_DATA_DIR ?? join(homedir(), ".ai-agent");
|
|
181
179
|
if (args.root)
|
|
182
180
|
process.env.CONFIG_LOCAL_DIR = args.root;
|
|
@@ -54,6 +54,11 @@
|
|
|
54
54
|
* `allowObservers` 是 `=== true` opt-in,缺席已等于关;`allowMemoryOptOut` 是隐私轴的 FAULT-TRANSIT 契约
|
|
55
55
|
* (core types.d.ts 逐字禁止在 fault 上自造一个显式值)—— 铸 false 就是把故障臂默默换成判决臂。
|
|
56
56
|
*
|
|
57
|
+
* 🔐 [ref] E-15(7.99.0 合并复审 F2):缓存、在飞去重与 ETag 都在**凭证身份的箱**里(`center-credential.ts`
|
|
58
|
+
* `createIdentityBins`,org 记忆目录同一只)——此前只按 principal 键,同机换账号后 TTL 内同一 principal 吃上一个
|
|
59
|
+
* 人的授予、搭上一个人的在飞请求、带上一个人的 etag 去换 304。在飞期间换了人,答复由传输层丢弃
|
|
60
|
+
* (`http-client.ts` `requestPrincipalView`),按这一次取数失败走下面的 fail-closed 臂(降级项落在发起那一箱)。
|
|
61
|
+
*
|
|
57
62
|
* The resolver maps center's `EntitlementRuntimeCaps` → the SUBSET core enforces ({@link RuntimeCaps}): center
|
|
58
63
|
* also carries `allowUltracode` (a shell/UX reasoning-tier cap the engine has no primitive for), which we DROP —
|
|
59
64
|
* this seam feeds the ENGINE, and `allowUltracode` is enforced at the service settings-resolution layer, not here.
|
|
@@ -61,14 +66,15 @@
|
|
|
61
66
|
import type { RuntimeCaps } from "@sema-agent/core";
|
|
62
67
|
import type { EntitlementRuntimeCaps } from "@sema-agent/settings-schema";
|
|
63
68
|
import { fetchPrincipalCaps, type ExecutionRuling } from "./config-center/facade.js";
|
|
69
|
+
import type { CenterEndpoint } from "./config-center/center-request.js";
|
|
64
70
|
import type { ScenarioRuling } from "./capabilities/scenarios.js";
|
|
65
71
|
import type { ServiceConfig } from "./config-types.js";
|
|
66
72
|
import { type Logger } from "./observability/logger.js";
|
|
67
73
|
/** test-only: reset the once-per-process trace so each test observes a fresh "first hit". */
|
|
68
74
|
export declare function resetExecutionLaneDefaultTraceForTest(): void;
|
|
69
75
|
export interface EntitlementsResolverOpts {
|
|
70
|
-
|
|
71
|
-
|
|
76
|
+
/** S-651:中心端点(基址 + 凭证**来源**;每次抓取取一次值,不持有启动快照)。 */
|
|
77
|
+
center: CenterEndpoint;
|
|
72
78
|
/** This worker's name (`SEMA_REGISTRY_WORKER`), passed as `?worker=` so a worker-scoped `wpt_<self>` token can
|
|
73
79
|
* resolve its OWN worker's principal caps (caps-only); harmless on a full token (the caps value
|
|
74
80
|
* is worker-independent). Omitted ⇒ no `?worker=` (a full-token, non-fleet deployment). */
|
|
@@ -136,12 +142,6 @@ export declare function toCoreRuntimeCaps(ent: EntitlementRuntimeCaps | null | u
|
|
|
136
142
|
* 所以本谓词为 false **不再**意味着 auto 不武装;「会不会武装」由 `auto-mode-face.ts` 的三项合取答。
|
|
137
143
|
*/
|
|
138
144
|
export declare function centerEntitlementSourceWired(config: Pick<ServiceConfig, "configCenter">): boolean;
|
|
139
|
-
/** A MISCONFIG diagnostic (review MEDIUM): a worker-scoped `wpt_<self>` pull token REQUIRES `?worker=<self>` to
|
|
140
|
-
* resolve per-principal caps — so a `wpt_` token with NO `SEMA_REGISTRY_WORKER` set will 403
|
|
141
|
-
* on every caps fetch → fail-closed DENY ALL workflows for ALL principals (correct degrade, but otherwise silent
|
|
142
|
-
* bar per-task warns). True ⇒ the caller should log a one-shot BOOT warning so the misconfig is diagnosable.
|
|
143
|
-
* (The orchestrator normally injects SEMA_REGISTRY_WORKER alongside a wpt_ token, so this is a hand-misconfig.) */
|
|
144
|
-
export declare function scopedTokenNeedsWorker(token: string, worker: string | undefined): boolean;
|
|
145
145
|
/** The two per-principal faces served off ONE caps fetch + cache. */
|
|
146
146
|
export interface PrincipalEntitlementsClient {
|
|
147
147
|
/** core's `RunnerDeps.runtimeCapsResolver` seam — FAIL-CLOSED on error (deny workflows/fork/autoMode;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { fetchPrincipalCaps } from "./config-center/facade.js";
|
|
2
|
+
import { createIdentityBins } from "./center-credential.js";
|
|
2
3
|
import { HttpError } from "./security.js";
|
|
3
4
|
import { createLogger } from "./observability/logger.js";
|
|
4
5
|
import { recordFailOpen } from "./observability/fail-open.js";
|
|
@@ -54,9 +55,6 @@ export function toCoreRuntimeCaps(ent) {
|
|
|
54
55
|
export function centerEntitlementSourceWired(config) {
|
|
55
56
|
return config.configCenter !== undefined && !config.configCenter.dryRun;
|
|
56
57
|
}
|
|
57
|
-
export function scopedTokenNeedsWorker(token, worker) {
|
|
58
|
-
return token.startsWith("wpt_") && !worker;
|
|
59
|
-
}
|
|
60
58
|
export function gateExecutionLane(ruling, ownLane, logger = defaultLogger) {
|
|
61
59
|
if (!ruling) {
|
|
62
60
|
traceExecutionLaneDefault("no_ruling", ownLane, logger);
|
|
@@ -80,11 +78,14 @@ export function createPrincipalEntitlementsClient(opts) {
|
|
|
80
78
|
const fetchCaps = opts.fetchCaps ?? fetchPrincipalCaps;
|
|
81
79
|
const now = opts.now ?? (() => Date.now());
|
|
82
80
|
const onError = opts.onError ?? ((err, principal) => defaultLogger.warn("runtime_caps_resolve_failed", { principal, err: String(err) }));
|
|
83
|
-
const
|
|
84
|
-
|
|
81
|
+
const binNow = createIdentityBins(opts.center.credential, (credential) => ({
|
|
82
|
+
center: { baseUrl: opts.center.baseUrl, credential },
|
|
83
|
+
cache: new Map(),
|
|
84
|
+
inflight: new Map(),
|
|
85
|
+
}));
|
|
85
86
|
const readMintedDeny = new WeakSet();
|
|
86
87
|
const MAX_CACHE = 10_000;
|
|
87
|
-
const putCache = (key, entry) => {
|
|
88
|
+
const putCache = (cache, key, entry) => {
|
|
88
89
|
if (cache.size >= MAX_CACHE) {
|
|
89
90
|
const t = now();
|
|
90
91
|
for (const [k, v] of cache)
|
|
@@ -100,15 +101,16 @@ export function createPrincipalEntitlementsClient(opts) {
|
|
|
100
101
|
return entry;
|
|
101
102
|
}
|
|
102
103
|
async function loadRaw(principal, recordDeny) {
|
|
103
|
-
const
|
|
104
|
+
const bin = binNow();
|
|
105
|
+
const cached = bin.cache.get(principal);
|
|
104
106
|
if (cached && cached.expiresAt > now() && !(recordDeny && readMintedDeny.has(cached)))
|
|
105
107
|
return cached;
|
|
106
|
-
const dup = inflight.get(principal);
|
|
108
|
+
const dup = bin.inflight.get(principal);
|
|
107
109
|
if (dup)
|
|
108
110
|
return dup;
|
|
109
111
|
const p = (async () => {
|
|
110
112
|
try {
|
|
111
|
-
const r = await fetchCaps(
|
|
113
|
+
const r = await fetchCaps(bin.center, principal, cached?.etag, fetchImpl, opts.worker);
|
|
112
114
|
if (r === null) {
|
|
113
115
|
const entry = {
|
|
114
116
|
caps: cached?.caps,
|
|
@@ -117,7 +119,7 @@ export function createPrincipalEntitlementsClient(opts) {
|
|
|
117
119
|
...(cached?.etag ? { etag: cached.etag } : {}),
|
|
118
120
|
expiresAt: now() + ttlMs,
|
|
119
121
|
};
|
|
120
|
-
putCache(principal, entry);
|
|
122
|
+
putCache(bin.cache, principal, entry);
|
|
121
123
|
return entry;
|
|
122
124
|
}
|
|
123
125
|
if (r.executionDrift) {
|
|
@@ -133,7 +135,7 @@ export function createPrincipalEntitlementsClient(opts) {
|
|
|
133
135
|
...(r.etag ? { etag: r.etag } : {}),
|
|
134
136
|
expiresAt: now() + ttlMs,
|
|
135
137
|
};
|
|
136
|
-
putCache(principal, entry);
|
|
138
|
+
putCache(bin.cache, principal, entry);
|
|
137
139
|
return entry;
|
|
138
140
|
}
|
|
139
141
|
catch (err) {
|
|
@@ -147,7 +149,7 @@ export function createPrincipalEntitlementsClient(opts) {
|
|
|
147
149
|
const entry = { caps: { allowWorkflows: false, allowFork: false, autoMode: false, autoModeDenySource: "org" }, expiresAt: now() + denyTtlMs };
|
|
148
150
|
if (!recordDeny)
|
|
149
151
|
readMintedDeny.add(entry);
|
|
150
|
-
putCache(principal, entry);
|
|
152
|
+
putCache(bin.cache, principal, entry);
|
|
151
153
|
try {
|
|
152
154
|
onError(e, principal);
|
|
153
155
|
}
|
|
@@ -155,10 +157,10 @@ export function createPrincipalEntitlementsClient(opts) {
|
|
|
155
157
|
return entry;
|
|
156
158
|
}
|
|
157
159
|
finally {
|
|
158
|
-
inflight.delete(principal);
|
|
160
|
+
bin.inflight.delete(principal);
|
|
159
161
|
}
|
|
160
162
|
})();
|
|
161
|
-
inflight.set(principal, p);
|
|
163
|
+
bin.inflight.set(principal, p);
|
|
162
164
|
return p;
|
|
163
165
|
}
|
|
164
166
|
return {
|