@sema-agent/server 7.102.0 → 7.103.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/MIGRATION.md +91 -0
- package/README.md +3 -3
- package/README.zh-CN.md +3 -3
- package/USAGE.md +107 -6
- package/dist/background-session-reap.d.ts +4 -1
- package/dist/bake-runner/main.d.ts +21 -3
- package/dist/bake-runner/main.js +18 -11
- package/dist/boot/budget-tracing.d.ts +1 -0
- package/dist/boot/budget-tracing.js +3 -2
- package/dist/boot/config-center.d.ts +12 -2
- package/dist/boot/config-center.js +33 -21
- package/dist/boot/stage-01-config.js +2 -0
- package/dist/boot/stage-04-stores.d.ts +1 -0
- package/dist/boot/stage-04-stores.js +2 -2
- package/dist/boot/stage-05-execution-env.d.ts +1 -0
- package/dist/boot/stage-06-runners.d.ts +1 -0
- package/dist/boot/stage-07-capability-layer.d.ts +1 -0
- package/dist/boot/stage-08-reapers.d.ts +1 -0
- package/dist/boot/stage-08-reapers.js +2 -2
- package/dist/boot/stage-09-leader.d.ts +1 -0
- package/dist/boot/stage-10-http-server.d.ts +1 -0
- package/dist/boot/stage-10-http-server.js +6 -1
- package/dist/brain.d.ts +60 -9
- package/dist/brain.js +44 -8
- package/dist/capabilities/center-plugins.d.ts +9 -3
- package/dist/capabilities/center-plugins.js +5 -10
- package/dist/config-catalog.js +9 -5
- package/dist/config-center/apply-effective.d.ts +40 -2
- package/dist/config-center/apply-effective.js +32 -12
- package/dist/config-center/facade.d.ts +1 -1
- package/dist/config-center/facade.js +1 -1
- package/dist/config-center/mcp-revocation.d.ts +2 -2
- package/dist/config-center/mcp-revocation.js +1 -1
- package/dist/config-center/restart-signal.d.ts +39 -12
- package/dist/config-center/restart-signal.js +13 -24
- package/dist/config-types.d.ts +35 -17
- package/dist/config-types.js +3 -3
- package/dist/config.d.ts +7 -0
- package/dist/config.js +43 -17
- package/dist/env-name-allowlist-knobs.d.ts +21 -7
- package/dist/env-name-allowlist-knobs.js +7 -2
- package/dist/fleet-client.d.ts +9 -3
- package/dist/fleet-client.js +2 -0
- package/dist/host-lsp-manager.d.ts +4 -3
- package/dist/host-lsp-manager.js +2 -6
- package/dist/http/dispatch.js +4 -0
- package/dist/http/server.d.ts +4 -0
- package/dist/leader/wire.d.ts +8 -1
- package/dist/leader/wire.js +4 -3
- package/dist/model-entry-refusal.d.ts +32 -9
- package/dist/model-entry-refusal.js +22 -10
- package/dist/model-route-endpoint.d.ts +10 -0
- package/dist/model-route-endpoint.js +6 -2
- package/dist/observability/secret-env-scrub.d.ts +13 -1
- package/dist/observability/secret-env-scrub.js +15 -2
- package/dist/outbound-dispatcher.d.ts +125 -0
- package/dist/outbound-dispatcher.js +211 -0
- package/dist/plugins/host-platform.d.ts +6 -1
- package/dist/plugins/remote-env-adb.d.ts +10 -3
- package/dist/plugins/remote-env-adb.js +24 -20
- package/dist/plugins/remote-env-host.js +3 -11
- package/dist/plugins/remote-env-k8s.d.ts +8 -1
- package/dist/plugins/remote-env-k8s.js +31 -24
- package/dist/plugins/remote-env-ssh.d.ts +8 -1
- package/dist/plugins/remote-env-ssh.js +24 -18
- package/dist/plugins/remote-shell.d.ts +109 -9
- package/dist/plugins/remote-shell.js +83 -0
- package/dist/project-memory.js +3 -6
- package/dist/run-local.js +2 -0
- package/dist/server-secret-env.d.ts +89 -24
- package/dist/server-secret-env.js +21 -1
- package/dist/trace/core-keyset-guard.d.ts +6 -1
- package/dist/trace/project.js +13 -0
- package/dist/trace/redact.d.ts +11 -0
- package/dist/trace/redact.js +1 -0
- package/package.json +4 -3
- package/skills/update-config.md +23 -5
|
@@ -1,40 +1,48 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 7.99.0 合并复审 F7(web-admin [ref] 实测;安全轴;DEBTS S-684)—— 子进程 env
|
|
2
|
+
* 7.99.0 合并复审 F7(web-admin [ref] 实测;安全轴;DEBTS S-684)—— 子进程 env 交出去之前的**本仓**密钥剥除;7.103.0(车OT,
|
|
3
|
+
* S-684 残余按类收口)起也是本包**宿主子进程 env 的唯一组合点**。
|
|
3
4
|
*
|
|
4
5
|
* ## 病
|
|
5
|
-
*
|
|
6
|
+
* 本仓的子进程腿 —— 模型可驱动的 host shell(`inheritEnv:"scrub"` 缺省腿,`plugins/remote-env-host.ts`)与
|
|
6
7
|
* project-memory 的 git 子进程(`project-memory.ts`)—— 此前只调 core 的 `scrubSecretEnv`。它按键名**形状**判
|
|
7
8
|
* (`*_KEY` / `*_TOKEN` / `*_SECRET` …),而本仓自己的凭据键里有形状规则认不出的:`SERVICE_AUTH_TOKENS`(复数,
|
|
8
9
|
* per-system 凭据表)、`APPROVAL_HMAC_KEYS`(直连门 HMAC 密钥集)、`OTEL_EXPORTER_OTLP_HEADERS`(常载鉴权头)⇒
|
|
9
|
-
* 原样进子进程,模型选的一句 `printenv` 就读得到。
|
|
10
|
+
* 原样进子进程,模型选的一句 `printenv` 就读得到。S-684 残余(7.103.0):另外三条宿主腿连 core 的形状剥都没过 —— leader 推送的
|
|
11
|
+
* git(`leader/wire.ts`)、center-plugins 克隆的 git(`capabilities/center-plugins.ts`)、bake-runner 交给 build.sh 与主机命令的
|
|
12
|
+
* env(`bake-runner/main.ts`,连 runner 自己的 `BAKE_RUNNER_TOKEN`)。
|
|
10
13
|
*
|
|
11
14
|
* ## 规则(一句)
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
+
* 本包任何进程在宿主上 spawn 的子进程:env = {@link hostSubprocessEnv} = {@link scrubServerSecrets}(process.env) ∪ 部署方点名放回的键
|
|
16
|
+
* (只在运维真可能要传东西的腿上有旋钮),剥掉的键经一个登记过的 `SecretEnvScrubSite` 留痕;git 腿再叠 git 自己的两件
|
|
17
|
+
* ({@link gitSubprocessEnv})。各腿只调这两只 —— 不许再有内联拼装(类级门在 `test/secret-env-scrub-host-legs.test.ts`)。
|
|
18
|
+
*
|
|
19
|
+
* {@link scrubServerSecrets} = core `scrubSecretEnv`(按形)∘ 本仓按名两臂:
|
|
20
|
+
* · 目录臂 —— 配置目录(`config-catalog.ts`)里 `type:"secret"` 的每一行,从目录机器派生,不手抄第二份名单;
|
|
21
|
+
* · 名单型旋钮臂 —— `ENV_NAME_ALLOWLIST_KNOBS` 的每一只(`env-name-allowlist-knobs.ts`,表键派生):名单**原文**可能夹着一枚被错展开进来
|
|
22
|
+
* 的令牌,按凭据类对待。服务侧三只(`LSP_ENV_ALLOW` S-699 / `E2B_SANDBOX_ENV` / `DOCKER_SANDBOX_ENV` S-713)同时是目录 secret 行,
|
|
23
|
+
* 目录臂先认;本臂接住的是**不在服务目录里**的那一只 —— bake-runner 的 `BAKE_ENV_ALLOW`(它不是服务进程的配置)。
|
|
15
24
|
* core 形状规则的缺口由 core 源头修 —— 7.30.1 #1072 已补 `KEYS|TOKENS|PASSWORDS`(S-616 提货),目录臂独担的只剩
|
|
16
|
-
* `OTEL_EXPORTER_OTLP_HEADERS`(`*_HEADERS` 不在形状规则射程,是边界不是缺陷票)
|
|
17
|
-
*
|
|
18
|
-
* 认它们)。本层不改 core 的判据,也不自造
|
|
19
|
-
* 第二份形状规则。目录里加一个 secret 型键,三条腿当场就剥它。
|
|
25
|
+
* `OTEL_EXPORTER_OTLP_HEADERS`(`*_HEADERS` 不在形状规则射程,是边界不是缺陷票)与名单型旋钮(形状规则按名判,本来就不该认它们)。
|
|
26
|
+
* 本层不改 core 的判据,也不自造第二份形状规则。目录里加一个 secret 型键 / 叶表加一只名单型旋钮,每条腿当场就剥它。
|
|
20
27
|
*
|
|
21
28
|
* ## 不变的面
|
|
22
29
|
* `inheritEnv:"all"`(显式全继承)与显式 allowlist 两形不经本函数,行为逐字不变;调用方显式传入的 env
|
|
23
30
|
* (`cfg.env` / 单次命令的 `env`)照旧原样覆盖在剥除结果之上。
|
|
24
31
|
*
|
|
25
|
-
* ## 点名放行(S-699 / S-713
|
|
26
|
-
* {@link allowlistedEnv} 是本仓「一张名单点到哪些 env 键,就把它们从源 env
|
|
27
|
-
* host shell 的 `inheritEnv:[names]`(`plugins/remote-env-host.ts
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
32
|
+
* ## 点名放行(S-699 / S-713,一只谓词各处用)
|
|
33
|
+
* {@link allowlistedEnv} 是本仓「一张名单点到哪些 env 键,就把它们从源 env **原样**交给子进程」的唯一实现。调用:
|
|
34
|
+
* host shell 的 `inheritEnv:[names]`(`plugins/remote-env-host.ts`,hermetic 形)、{@link hostSubprocessEnv}(剥后放回形:本地语言服务器的
|
|
35
|
+
* `LSP_ENV_ALLOW`、bake-runner 的 `BAKE_ENV_ALLOW`)、沙箱带外 env 的 `E2B_SANDBOX_ENV` / `DOCKER_SANDBOX_ENV`(`config.ts`,S-713 起)。
|
|
36
|
+
* 名单型旋钮的**名单**另由 `config.ts` 的同一只解析器 `parseEnvNameAllowlist` 判形(形错拒启 / 源里没有的名按序号 warn),本函数只管取值。
|
|
37
|
+
* 「点名」在各处同义:点到的键原样带值(空串值照带),密钥形也照过(点名 = 部署方显式承担);源里没有的名不产生条目。
|
|
31
38
|
* 沙箱两腿不继承本进程环境:进沙箱的本进程值只有点到的键(与 host shell 的 hermetic 形同一侧)。
|
|
32
|
-
* host shell
|
|
33
|
-
* `NodeExecutionEnv` 同名选项同义,没点到的一律不继承)
|
|
34
|
-
* (没点到的照缺省剥,`PATH` / `HOME` 这类非密钥键照常在 —— hermetic
|
|
35
|
-
*
|
|
39
|
+
* host shell 与「剥后放回」形**只在没点到的键上**不同,这是有意保留的差别:host shell 的 `string[]` 形是 hermetic 白名单(与 core
|
|
40
|
+
* `NodeExecutionEnv` 同名选项同义,没点到的一律不继承);语言服务器 / bake-runner 在 {@link scrubServerSecrets} 的结果之上放回点名键
|
|
41
|
+
* (没点到的照缺省剥,`PATH` / `HOME` 这类非密钥键照常在 —— hermetic 形会让子进程当场找不到自己的工具链)。
|
|
42
|
+
* 两形都不放宽对方:把 host shell 改成「剥后放回」= 在模型可驱动的 shell 上放宽,把后者改成 hermetic = 功能当场失灵。
|
|
36
43
|
*/
|
|
37
44
|
import { type SecretEnvFinding } from "@sema-agent/core";
|
|
45
|
+
import { type SecretEnvScrubSite } from "./observability/secret-env-scrub.js";
|
|
38
46
|
/** 目录派生臂的一条剥除记录(形同 core 的 `SecretEnvFinding`:键名 / 规则 / 置信度 / 来源;值永不携带)。
|
|
39
47
|
* `source` 用本仓自己的词 `server-config-catalog`,与 core 的 `secret-env` 分开 —— 读观测面的人要分得清
|
|
40
48
|
* 「形状规则剥的」与「本仓目录点名剥的」(后者多出来的那部分正是 core 形状规则的缺口)。 */
|
|
@@ -44,16 +52,73 @@ export interface CatalogSecretEnvFinding {
|
|
|
44
52
|
readonly confidence: "high";
|
|
45
53
|
readonly source: "server-config-catalog";
|
|
46
54
|
}
|
|
47
|
-
/**
|
|
48
|
-
export
|
|
55
|
+
/** 名单型旋钮臂的一条剥除记录(形同上;`source` 用自己的词 —— 读观测面的人要分得清「目录点名剥的」与「名单型旋钮原文按凭据类剥的」)。 */
|
|
56
|
+
export interface NameListKnobEnvFinding {
|
|
57
|
+
readonly key: string;
|
|
58
|
+
readonly kind: "name-list-knob";
|
|
59
|
+
readonly confidence: "high";
|
|
60
|
+
readonly source: "server-name-list-knob";
|
|
61
|
+
}
|
|
62
|
+
/** 本仓子进程腿的扣下记录:core 的形状臂 ∪ 本仓的目录臂 ∪ 名单型旋钮臂。 */
|
|
63
|
+
export type ServerSecretEnvFinding = SecretEnvFinding | CatalogSecretEnvFinding | NameListKnobEnvFinding;
|
|
49
64
|
/** 配置目录里 `type:"secret"` 的 env 名(大写 —— Windows 的 env 名不分大小写,按大写比才不会漏)。 */
|
|
50
65
|
export declare const CATALOG_SECRET_ENV_NAMES: ReadonlySet<string>;
|
|
51
66
|
/**
|
|
52
|
-
* 先交 core
|
|
67
|
+
* 先交 core 按形剥,再按目录名、名单型旋钮名剥它漏下的。`findings` 是调用方持有的收集器(交给 `recordSecretEnvScrub` 留痕):
|
|
53
68
|
* 形状臂剥掉的记 core 的 finding;目录臂只记**形状臂漏下、被目录点名剥掉**的键 —— 于是既有的形状臂计数不变,
|
|
54
|
-
* 目录臂的计数恰好就是 core
|
|
69
|
+
* 目录臂的计数恰好就是 core 形状规则的缺口读数;名单型旋钮臂只记前两臂都漏下的(今天 = 不在服务目录里的 `BAKE_ENV_ALLOW`)。
|
|
70
|
+
* 各腿不直接调它 —— 经 {@link hostSubprocessEnv}(剥 + 放回 + 留痕一只口)。
|
|
71
|
+
*
|
|
72
|
+
* **只为宿主子进程组合**:子进程与本进程同平台 ⇒ core `scrubSecretEnv` 的平台尾参(core 7.33.6 #1159:git 的 env 配置记录按子进程的平台读 ——
|
|
73
|
+
* POSIX 只认大写名,win32 大小写折叠)取缺省 `process.platform` 即对。本包跑在别的平台上的子进程(ssh / k8s / adb / e2b / docker 车道)的 env
|
|
74
|
+
* 不经本函数:那些车道不继承本进程 env,只交调用方显式给的 env 与部署方点名的键(`E2B_SANDBOX_ENV` / `DOCKER_SANDBOX_ENV`,经 {@link allowlistedEnv})。
|
|
55
75
|
*/
|
|
56
76
|
export declare function scrubServerSecrets(env: NodeJS.ProcessEnv, findings: ServerSecretEnvFinding[]): NodeJS.ProcessEnv;
|
|
77
|
+
/**
|
|
78
|
+
* 🔴 **宿主子进程 env 的唯一组合**(S-684 残余按类收口,7.103.0):{@link scrubServerSecrets}(env) ∪ `allow` 点到的键
|
|
79
|
+
* (经 {@link allowlistedEnv} 从同一份源 env 原样放回),**没有按原样交给子进程**的键(剥掉 / 换成 core 占位半条)经 `site` 留痕(点名放回的键取回源值,
|
|
80
|
+
* 不是剥除,不计数、不披露)。源 env 只在入参 `sourceEnv` 绑定一次(缺省 `process.env`):剥除源、放回源、谓词回读三处读的是同一份映射。`sourceEnv` 是测试缝 ——
|
|
81
|
+
* `src/` 里的调用一律缺省(只带一个实参;钉:`test/secret-env-scrub-host-legs.test.ts` 类级门「opus 验修 L-1」格),平台尾参取缺省的推理只在源 = 本进程 env 时成立。
|
|
82
|
+
* (入参不叫 `env`:D1 对账门按标识符名在全文件收 `= process.env` 缺省参数的别名,叫 `env` 会把 {@link allowlistedEnv} 的同名形参一并认成别名、多记一处动态读。)
|
|
83
|
+
*
|
|
84
|
+
* 六条宿主腿都只经它(直接或经 {@link gitSubprocessEnv}):host shell 缺省 scrub 腿、project-memory / center-plugins / leader 三条 git 腿、
|
|
85
|
+
* 本地语言服务器、bake-runner 的每一只子进程组。`allow` 只在**有部署级出路**的现场非空(今天 = `LSP_ENV_ALLOW` / `BAKE_ENV_ALLOW`,
|
|
86
|
+
* 它们的现场在 `SECRET_ENV_SCRUB_SITES` 里登记了 `remedy`,披露行因此是 warn + 放行名单 + 出路句);其余现场传 `[]`。
|
|
87
|
+
* 返回 `NodeJS.ProcessEnv` 形(值可能是 `undefined` 的键本来就不会进子进程);调用方显式要交的键(`LC_ALL`、`cfg.env` …)叠在它之上。
|
|
88
|
+
* 调用时机 = 每次要交 env 时现算一次(`process.env` 在本仓是 boot 期输入,各次同值;计数因此是「每组合一次」)。
|
|
89
|
+
*
|
|
90
|
+
* 🔴 **git 的 env 配置组(`GIT_CONFIG_COUNT` + `GIT_CONFIG_KEY_<i>` / `GIT_CONFIG_VALUE_<i>`)只有 core 一只写手**(7.103.1:车PG 退役本层,车PK 提 core 7.33.6):
|
|
91
|
+
* 一条配置(KEY + VALUE)一次判定;COUNT 以内被扣的一条**原位**换占位(`GIT_CONFIG_KEY_<i>=sema.withheld` / `GIT_CONFIG_VALUE_<i>=""`,git 读成一条没人用的
|
|
92
|
+
* 配置)、不重排、COUNT 原样(#1160);COUNT 之外 / 没有 COUNT 的被扣条目整条不交;COUNT 按 git 的 `strtoul` 读(#1162);记录按子进程的平台读(#1159,见
|
|
93
|
+
* {@link scrubServerSecrets})。本层 7.103.0 的「整组扣」与 7.103.1 中间态的「按 pair 扣留 + 重编号」(D-7)都已退役,本层不重判(同一语义面上不留第二只写手)。
|
|
94
|
+
* 本仓按名两臂(目录 secret 名 / 名单型旋钮)不含这一族的名字(钉:`test/secret-env-git-config-pair.test.ts`「一只写手」格)。
|
|
95
|
+
* 点名放回按名叠在 scrub 结果之上 ⇒ 放回一条被扣配置要把 KEY 与 VALUE 两只一并点名(只点一只 = 与另一只的占位半条配成一对)。
|
|
96
|
+
* 🔴 **披露谓词 = 「这个名字没有按原样交给子进程」**(输入值 vs 输出值;core 7.33.6 pickup 注:「not handed on as written」)。被扣的记录成员在输出里
|
|
97
|
+
* **仍在**(装着占位半条)⇒「组合结果里有没有这个名字」恒为有;点名放回的名字取回源值 ⇒ 与输入同值 ⇒ 不是剥除;占位半条恰与输入同值的成员(KEY 里
|
|
98
|
+
* 带凭据、VALUE 本就是空串)子进程拿到的正是原值 ⇒ 也不披露。一只谓词盖住「剥掉」「换占位」「点名放回」三形,不按形分臂。
|
|
99
|
+
*/
|
|
100
|
+
export declare function hostSubprocessEnv(o: {
|
|
101
|
+
site: SecretEnvScrubSite;
|
|
102
|
+
allow: readonly string[];
|
|
103
|
+
}, sourceEnv?: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
|
|
104
|
+
/** git 子进程腿的现场(闭集里名字带 `.git-` 的那几只 —— 编译期筛;它们都没有点名放行面)。 */
|
|
105
|
+
export type GitSubprocessSite = Extract<SecretEnvScrubSite, `${string}.git-${string}`>;
|
|
106
|
+
/**
|
|
107
|
+
* 宿主 **git 子进程**的 env:{@link hostSubprocessEnv}(site, 不放回) + `GIT_TERMINAL_PROMPT=0`(恒:凭据缺席 ⇒ 响亮失败,绝不挂在交互
|
|
108
|
+
* prompt 上 —— 服务进程没有终端)+(`isolateConfig` ⇒ `GIT_CONFIG_GLOBAL` / `GIT_CONFIG_SYSTEM` = `/dev/null`:宿主全局 / 系统 git 配置
|
|
109
|
+
* (insteadOf / credential helper / hooksPath / fsmonitor …)一概不继承)。
|
|
110
|
+
*
|
|
111
|
+
* `isolateConfig` 按腿定(三条腿各自的理由在调用点):project-memory 与 center-plugins 隔离(读仓 / 克隆不需要宿主身份);leader 推送
|
|
112
|
+
* **不**隔离 —— durable remote 的写凭据属于控制面宿主(「Coordinator's creds」),可能由宿主 git 配置里的 credential helper / insteadOf
|
|
113
|
+
* 提供,隔离会把它们一起切掉。不隔离 ≠ 不剥:helper 同样只看得到剥后的 env(读 `*_TOKEN` 形 env 的 helper 因此读不到,出路见 CHANGELOG)。
|
|
114
|
+
*
|
|
115
|
+
* ⚠️ 已知边界(不在本函数修):Apple Git 另有一层 Xcode 自带配置(`…/Xcode.app/…/git-core/gitconfig`,内含 `credential.helper=osxkeychain`),
|
|
116
|
+
* `GIT_CONFIG_SYSTEM` 盖不住、`GIT_CONFIG_NOSYSTEM=1` 才盖得住 —— 只在 macOS 开发机上,文件属 root(不可被模型种),见 [ref] 登记。
|
|
117
|
+
*/
|
|
118
|
+
export declare function gitSubprocessEnv(o: {
|
|
119
|
+
site: GitSubprocessSite;
|
|
120
|
+
isolateConfig: boolean;
|
|
121
|
+
}): NodeJS.ProcessEnv;
|
|
57
122
|
/**
|
|
58
123
|
* S-699 —— **点名放行**的唯一谓词:从 `env` 里把 `names` 点到的键**原样**取出(值逐字,空串照带;源里没有的名不产生
|
|
59
124
|
* 条目)。密钥形的键点到了也照取 —— 点名即部署方显式承担;要不要在这之上再叠别的基底,是调用方的事(见文件头「点名放行」节)。
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { scrubSecretEnv } from "@sema-agent/core";
|
|
2
2
|
import { CONFIG_CATALOG } from "./config-catalog.js";
|
|
3
|
+
import { ENV_NAME_ALLOWLIST_KNOB_NAMES } from "./env-name-allowlist-knobs.js";
|
|
4
|
+
import { recordSecretEnvScrub } from "./observability/secret-env-scrub.js";
|
|
3
5
|
export const CATALOG_SECRET_ENV_NAMES = new Set(CONFIG_CATALOG.filter((spec) => spec.type === "secret").map((spec) => spec.name.toUpperCase()));
|
|
4
6
|
export function scrubServerSecrets(env, findings) {
|
|
5
7
|
const shapeFindings = [];
|
|
@@ -7,14 +9,32 @@ export function scrubServerSecrets(env, findings) {
|
|
|
7
9
|
findings.push(...shapeFindings);
|
|
8
10
|
const out = {};
|
|
9
11
|
for (const key of Object.keys(shaped)) {
|
|
10
|
-
|
|
12
|
+
const upper = key.toUpperCase();
|
|
13
|
+
if (CATALOG_SECRET_ENV_NAMES.has(upper)) {
|
|
11
14
|
findings.push({ key, kind: "config-catalog", confidence: "high", source: "server-config-catalog" });
|
|
12
15
|
continue;
|
|
13
16
|
}
|
|
17
|
+
if (ENV_NAME_ALLOWLIST_KNOB_NAMES.has(upper)) {
|
|
18
|
+
findings.push({ key, kind: "name-list-knob", confidence: "high", source: "server-name-list-knob" });
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
14
21
|
out[key] = shaped[key];
|
|
15
22
|
}
|
|
16
23
|
return out;
|
|
17
24
|
}
|
|
25
|
+
export function hostSubprocessEnv(o, sourceEnv = process.env) {
|
|
26
|
+
const scrubFindings = [];
|
|
27
|
+
const composed = { ...scrubServerSecrets(sourceEnv, scrubFindings), ...allowlistedEnv(o.allow, sourceEnv) };
|
|
28
|
+
recordSecretEnvScrub(o.site, scrubFindings.filter((f) => composed[f.key] !== sourceEnv[f.key]), o.allow);
|
|
29
|
+
return composed;
|
|
30
|
+
}
|
|
31
|
+
export function gitSubprocessEnv(o) {
|
|
32
|
+
return {
|
|
33
|
+
...hostSubprocessEnv({ site: o.site, allow: [] }),
|
|
34
|
+
GIT_TERMINAL_PROMPT: "0",
|
|
35
|
+
...(o.isolateConfig ? { GIT_CONFIG_GLOBAL: "/dev/null", GIT_CONFIG_SYSTEM: "/dev/null" } : {}),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
18
38
|
export function allowlistedEnv(names, env) {
|
|
19
39
|
const out = {};
|
|
20
40
|
for (const name of names) {
|
|
@@ -59,7 +59,7 @@ type _GuardMailboxReverse = AssertAllKeysHandled<Exclude<MailboxProjected, keyof
|
|
|
59
59
|
type MailboxAppendProjected = "from" | "content" | "sentAt" | "hopChain" | "peerMeta";
|
|
60
60
|
type _GuardMailboxAppend = AssertAllKeysHandled<Exclude<keyof MailboxAppendMessage, MailboxAppendProjected>>;
|
|
61
61
|
type _GuardMailboxAppendReverse = AssertAllKeysHandled<Exclude<MailboxAppendProjected, keyof MailboxAppendMessage>>;
|
|
62
|
-
type WiringSectionProjected = "schemaVersion" | "leg" | "ask" | "question" | "interaction" | "elicit" | "parkLane" | "session" | "fleet" | "permissionRules" | "modelGate" | "autoMode" | "mcp" | "tools" | "hooks" | "lsp" | "writeProtection" | "teamRole";
|
|
62
|
+
type WiringSectionProjected = "schemaVersion" | "leg" | "ask" | "question" | "interaction" | "elicit" | "parkLane" | "session" | "fleet" | "permissionRules" | "modelGate" | "autoMode" | "mcp" | "tools" | "hooks" | "lsp" | "hands" | "writeProtection" | "teamRole";
|
|
63
63
|
type WiringSectionExcluded = "governance" | "configFingerprint" | "autoConsolidation" | "readDeny" | "peerPresence" | "fileHistory";
|
|
64
64
|
type _GuardWiringSections = AssertAllKeysHandled<Exclude<keyof WiringManifest, WiringSectionProjected | WiringSectionExcluded>>;
|
|
65
65
|
type _GuardWiringSectionsReverse = AssertAllKeysHandled<Exclude<WiringSectionProjected | WiringSectionExcluded, keyof WiringManifest>>;
|
|
@@ -104,6 +104,9 @@ type _GuardWiringMcpLivenessReverse = AssertAllKeysHandled<Exclude<WiringMcpLive
|
|
|
104
104
|
type WiringRulesHandled = "storeWired" | "syncWired" | "orgGoverned";
|
|
105
105
|
type _GuardWiringRules = AssertAllKeysHandled<Exclude<keyof WiringSection<"permissionRules">, WiringRulesHandled>>;
|
|
106
106
|
type _GuardWiringRulesReverse = AssertAllKeysHandled<Exclude<WiringRulesHandled, keyof WiringSection<"permissionRules">>>;
|
|
107
|
+
type WiringHandsHandled = "mounted" | "reason";
|
|
108
|
+
type _GuardWiringHands = AssertAllKeysHandled<Exclude<keyof WiringSection<"hands">, WiringHandsHandled>>;
|
|
109
|
+
type _GuardWiringHandsReverse = AssertAllKeysHandled<Exclude<WiringHandsHandled, keyof WiringSection<"hands">>>;
|
|
107
110
|
type WiringWriteProtectionHandled = "targetView";
|
|
108
111
|
type _GuardWiringWriteProtection = AssertAllKeysHandled<Exclude<keyof WiringSection<"writeProtection">, WiringWriteProtectionHandled>>;
|
|
109
112
|
type _GuardWiringWriteProtectionReverse = AssertAllKeysHandled<Exclude<WiringWriteProtectionHandled, keyof WiringSection<"writeProtection">>>;
|
|
@@ -236,6 +239,8 @@ export type CoreKeysetGuards = [
|
|
|
236
239
|
_GuardWiringSections,
|
|
237
240
|
_GuardWiringWriteProtection,
|
|
238
241
|
_GuardWiringWriteProtectionReverse,
|
|
242
|
+
_GuardWiringHands,
|
|
243
|
+
_GuardWiringHandsReverse,
|
|
239
244
|
_GuardWiringReadDeny,
|
|
240
245
|
_GuardWiringReadDenyReverse,
|
|
241
246
|
_GuardWiringAutoConsolidation,
|
package/dist/trace/project.js
CHANGED
|
@@ -521,6 +521,18 @@ export function wiringManifestEventData(ev) {
|
|
|
521
521
|
const lane = str(lspIn.lane);
|
|
522
522
|
return { mounted, ...(lane === undefined ? {} : { lane }) };
|
|
523
523
|
})();
|
|
524
|
+
const handsIn = sec(m.hands);
|
|
525
|
+
const hands = (() => {
|
|
526
|
+
if (m.hands === undefined)
|
|
527
|
+
return undefined;
|
|
528
|
+
if (handsIn === undefined)
|
|
529
|
+
return dropProjectedFace("wiring_manifest.hands is not an object");
|
|
530
|
+
const mounted = bool(handsIn.mounted);
|
|
531
|
+
if (mounted === undefined)
|
|
532
|
+
return dropProjectedFace("wiring_manifest.hands.mounted is not a boolean");
|
|
533
|
+
const reason = str(handsIn.reason);
|
|
534
|
+
return { mounted, ...(reason === undefined ? {} : { reason }) };
|
|
535
|
+
})();
|
|
524
536
|
const wpIn = sec(m.writeProtection);
|
|
525
537
|
const writeProtection = (() => {
|
|
526
538
|
if (m.writeProtection === undefined)
|
|
@@ -560,6 +572,7 @@ export function wiringManifestEventData(ev) {
|
|
|
560
572
|
...(mcp !== undefined ? { mcp } : {}),
|
|
561
573
|
...(hooks !== undefined ? { hooks } : {}),
|
|
562
574
|
...(lsp !== undefined ? { lsp } : {}),
|
|
575
|
+
...(hands !== undefined ? { hands } : {}),
|
|
563
576
|
...(writeProtection !== undefined ? { writeProtection } : {}),
|
|
564
577
|
...identityFields(ev),
|
|
565
578
|
};
|
package/dist/trace/redact.d.ts
CHANGED
|
@@ -61,6 +61,17 @@ export declare function armPoolBudgetForTest(): {
|
|
|
61
61
|
ssh2ScanBytes: number;
|
|
62
62
|
};
|
|
63
63
|
export declare function resetArmPoolBudgetForTest(): void;
|
|
64
|
+
/** 本模块会写进文本的全部替换 token(`«…»` 字面),从三张表现算 —— 表改这里自动跟,不另抄一份。
|
|
65
|
+
* 🔴 **S-223 v2 修复轮起它只剩一个语义:闭形闭集的加载期校验**。运行期「这一段是不是一枚记号」已经由
|
|
66
|
+
* {@link credentialSpans} 的闭**形**那一支一处答完({@link opaqueUnitEnd}),按闭**集**再认一遍的
|
|
67
|
+
* `redactionTokenEndAt` 与它同义而更窄,已删 —— 两只「什么算记号」的判别器就是两个定义。 */
|
|
68
|
+
/** {@link buildDisplaySafeUrl} 解析不动一个配置型 URL 时的**占位**(S-475 件B)——fail-closed:输出**不含原串的
|
|
69
|
+
* 任何字节**(连 host 都不留),不许回落成原值。它不是任何一条臂的产物,所以要在这里**显式**进闭集
|
|
70
|
+
* {@link REDACTION_TOKENS}(闭形的加载期校验对它同样成立),消费端按契约 §5.1 的闭形逐枚解析时认得它。 */
|
|
71
|
+
declare const DISPLAY_SAFE_URL_TOKEN = "\u00ABredacted:url\u00BB";
|
|
72
|
+
/** S-776:出网代理 URL 的回显尺(`outbound-dispatcher.ts` `displayProxyUrl`)形不干净时整只用**同一枚**占位 —— 单独一行导出,
|
|
73
|
+
* 上面那行字面量保持 `const NAME = "…";` 形(`test/api-notice-text-freeze.test.ts` §6 的冻结扫描按这一形认替换 token)。 */
|
|
74
|
+
export { DISPLAY_SAFE_URL_TOKEN };
|
|
64
75
|
/** 测试可见:扫描器 indexOf 实际扫过的字节数(摊还线性 ⇒ ≤ 常数 × 输入长)。 */
|
|
65
76
|
export declare function ppkScanBytesForTest(): number;
|
|
66
77
|
export declare function resetPpkScanBytesForTest(): void;
|
package/dist/trace/redact.js
CHANGED
|
@@ -1091,6 +1091,7 @@ const FRAGMENT_SECRET_PATTERNS = [
|
|
|
1091
1091
|
[valueArm("xapp-\\d-", "[A-Za-z0-9-]", 4, "needs-left-boundary"), "«redacted:slack-token:partial»"],
|
|
1092
1092
|
];
|
|
1093
1093
|
const DISPLAY_SAFE_URL_TOKEN = "«redacted:url»";
|
|
1094
|
+
export { DISPLAY_SAFE_URL_TOKEN };
|
|
1094
1095
|
const WITHHELD_VALUE_TOKEN = "«redacted»";
|
|
1095
1096
|
const REDACTION_TOKENS = new Set([...FULL_ARM_PATTERNS.map(([, r]) => r), PEM_FRAGMENT_TOKEN, DISPLAY_SAFE_URL_TOKEN, ...FRAGMENT_SECRET_PATTERNS.map(([, r]) => r)]
|
|
1096
1097
|
.flatMap((r) => r.match(/«[^»]*»/g) ?? []));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sema-agent/server",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.103.1",
|
|
4
4
|
"description": "Sema Server — the server/API implementation layer for Sema, wiring core, registry, model providers, and cloud agent execution. Built on @sema-agent/core.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "BUSL-1.1",
|
|
@@ -61,14 +61,15 @@
|
|
|
61
61
|
"build:binary:run-local:darwin-arm64": "bun build --compile --target=bun-darwin-arm64 src/run-local.ts --outfile dist/run-local-darwin-arm64"
|
|
62
62
|
},
|
|
63
63
|
"dependencies": {
|
|
64
|
-
"@sema-agent/core": "7.
|
|
65
|
-
"@sema-agent/settings-schema": "6.
|
|
64
|
+
"@sema-agent/core": "7.33.6",
|
|
65
|
+
"@sema-agent/settings-schema": "6.3.0",
|
|
66
66
|
"e2b": "^2.28.0",
|
|
67
67
|
"libsodium-wrappers": "^0.8.4",
|
|
68
68
|
"mysql2": "^3.22.4",
|
|
69
69
|
"pg": "^8.22.0",
|
|
70
70
|
"ssh2": "^1.17.0",
|
|
71
71
|
"typebox": "1.1.39",
|
|
72
|
+
"undici": "7.30.0",
|
|
72
73
|
"ws": "^8.21.0",
|
|
73
74
|
"zod": "^4.4.3"
|
|
74
75
|
},
|
package/skills/update-config.md
CHANGED
|
@@ -65,7 +65,7 @@ The core resolved units (`SemaSettings`):
|
|
|
65
65
|
"env": { "DEBUG": "true" },
|
|
66
66
|
"model": "model-name",
|
|
67
67
|
"outputStyle": "style-name",
|
|
68
|
-
"webSearch": { "provider": "
|
|
68
|
+
"webSearch": { "provider": "searxng", "endpoint": "https://search.example.com", "maxResults": 5 },
|
|
69
69
|
"ultracode": true
|
|
70
70
|
}
|
|
71
71
|
```
|
|
@@ -74,20 +74,38 @@ Notes:
|
|
|
74
74
|
|
|
75
75
|
- **Permission rule syntax** is upstream-compatible: exact match `"Bash(npm run test)"`, prefix wildcard `"Bash(git:*)"`, tool-only `"Read"`.
|
|
76
76
|
- `webSearch` and `ultracode` are sema-only keys (no upstream equivalent).
|
|
77
|
+
- `webSearch.provider` is `"brave"`, `"tavily"` or `"searxng"`. The block also accepts an `apiKey`, but that field holds the key itself (not the name of an env var), so this skill never writes it: for a keyed provider (brave / tavily), leave `webSearch` out of the settings files and have the human export `WEB_SEARCH_PROVIDER` and `WEB_SEARCH_API_KEY` in the environment the engine starts from. A `webSearch` block in settings replaces that environment backend for the request; it does not borrow its key.
|
|
77
78
|
- Settings files accept the wider upstream settings.json superset; unknown keys are tolerated, but the units above are what the sema resolver folds across layers.
|
|
78
79
|
|
|
79
80
|
## Model configuration (sema-specific)
|
|
80
81
|
|
|
81
82
|
Models are NOT configured via a bare `"model": "sonnet"` alias. Two channels, both under the user layer:
|
|
82
83
|
|
|
83
|
-
1. **Model catalog** — `<config-home>/config.d/models.json`: a multi-slot catalog of entries `{ name, id, api, baseUrl, apiKeyEnv, contextWindow, maxTokens }` where `api` is `"anthropic-messages"` or `"openai-completions"`. A catalog with enabled models REPLACES the env catalog, and the FIRST enabled entry is the default model. Secrets never go in this file: `apiKeyEnv` names an env var (convention `SEMA_MODEL_KEY_<ENTRY>`)
|
|
84
|
-
2. **Env two-slot fallback** — when no catalog exists, the settings `env` block drives models directly: `MODEL_ID` (main), `MODEL_CHEAP_ID` (cheap/summarize slot), `
|
|
84
|
+
1. **Model catalog** — `<config-home>/config.d/models.json`: a multi-slot catalog of entries `{ name, id, api, baseUrl, apiKeyEnv, contextWindow, maxTokens }` where `api` is `"anthropic-messages"` or `"openai-completions"`. A catalog with enabled models REPLACES the env catalog, and the FIRST enabled entry is the default model. Secrets never go in this file: `apiKeyEnv` names an env var (convention `SEMA_MODEL_KEY_<ENTRY>`); the variable's **value** is set by the human out of band (the shell or service environment the engine starts from), never by this skill.
|
|
85
|
+
2. **Env two-slot fallback** — when no catalog exists, the settings `env` block drives models directly: `MODEL_ID` (main), `MODEL_CHEAP_ID` (cheap/summarize slot), `ANTHROPIC_BASE_URL` (anthropic-messages routes) or `MODEL_GATEWAY_BASEURL` (openai-completions routes), plus optional `MODEL_CONTEXT_WINDOW` / `MODEL_MAX_TOKENS`. The credential for this route (`MODEL_API_KEY` or `ANTHROPIC_AUTH_TOKEN`) is **not** one of the slots this skill edits — see the credentials rule below.
|
|
85
86
|
|
|
86
|
-
Prefer editing the catalog when it exists; only touch the env slots for catalog-less installs.
|
|
87
|
+
Prefer editing the catalog when it exists (it never carries secrets); only touch the env slots for catalog-less installs.
|
|
88
|
+
|
|
89
|
+
## Credentials — hard rule
|
|
90
|
+
|
|
91
|
+
This skill never handles credential values (API keys, tokens, anything shaped like `*_KEY` / `*_TOKEN` / `apiKey`). Whatever a tool shows you from a settings file enters the model context and the session transcript, and a settings file may already hold credentials the user put there (an `env` block, a `webSearch.apiKey`). A line-oriented read does not keep them out: `grep` for one key prints its whole line, and a one-line or minified JSON file is a single line. So:
|
|
92
|
+
|
|
93
|
+
- Never write a credential value into any settings file, and never read one back or restate it. If the user asks you to set an API key or token, decline to write it and tell them to export it in the environment the engine starts from (shell profile / service unit), out of band.
|
|
94
|
+
- For model changes prefer `config.d/models.json`: it names credentials by env var only and is safe to read and edit.
|
|
95
|
+
- Never open a settings file that may hold credentials (`settings.json` / `settings.local.json` at any layer) with any tool that reads file contents — Read, Grep, Edit, Write, or Bash `cat` / `head` / `tail` / `less` / `jq` / `grep` / `sed` / `awk`. Change it with one short script that parses the JSON, touches only the key you were asked to change, writes the file back, and prints nothing but the names of the keys it changed. The script must never print file contents or exception messages: on any failure it prints a fixed error code and exits non-zero (a runtime's own error for a malformed file can quote the offending line, credentials included). For example, to set the main model slot:
|
|
96
|
+
|
|
97
|
+
```sh
|
|
98
|
+
node -e 'const fs=require("fs");const f=process.argv[1];let j;try{j=JSON.parse(fs.readFileSync(f,"utf8"))}catch{console.error("settings-json-unparseable");process.exit(2)}if(!j||typeof j!=="object"||Array.isArray(j)){console.error("settings-json-not-an-object");process.exit(2)}j.env??={};if(!j.env||typeof j.env!=="object"||Array.isArray(j.env)){console.error("settings-env-not-an-object");process.exit(2)}j.env.MODEL_ID="new-model";try{fs.writeFileSync(f,JSON.stringify(j,null,2)+"\n")}catch{console.error("settings-json-write-failed");process.exit(3)}console.log("changed: env.MODEL_ID")' "<file>"
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
To check a key before changing it, print that one key the same way — and only when it is a scalar leaf (a string, number or boolean) that is not a credential slot. Never print an object or an array: it can carry credentials nested inside.
|
|
102
|
+
- If a change cannot be made that way — the file does not parse as JSON, deciding the change would need a credential's value, or no script runtime is available — stop. Tell the user to move the credentials out of the file first (into the environment the engine starts from), then make the change.
|
|
103
|
+
- Do not quote, summarize or re-emit any `env` value in your reply or notes.
|
|
104
|
+
- A settings file that already holds credentials is a risk worth naming once to the user: recommend moving them to the process environment.
|
|
87
105
|
|
|
88
106
|
## How to apply a change
|
|
89
107
|
|
|
90
108
|
1. Pick the narrowest file that satisfies the scope and trust rules above.
|
|
91
|
-
2.
|
|
109
|
+
2. Merge your key in — never clobber unrelated keys. A settings file is changed only through the JSON projection script from the credentials rule (never through a tool that reads its contents); `config.d/models.json` carries no secrets and can be edited normally.
|
|
92
110
|
3. Settings files are watched: edits hot-reload without a restart. If a change appears ignored, check for a resolver rejection (trust/tighten violation) before assuming a bug.
|
|
93
111
|
4. For "do X automatically every time Y" requests, configure a hook — do not promise behavior from memory.
|