@sema-agent/settings-schema 12.0.1 → 12.1.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/CHANGELOG.md CHANGED
@@ -6,6 +6,26 @@
6
6
  > `@sema-agent/registry-core`(更早还曾是 `@sema-ai/registry-core`)。本档 `0.19.0` 以下各节里的
7
7
  > 包名是**历史记录**,如实保留不追改。
8
8
 
9
+ ## 12.1.0 — 2026-10-12 — minor:新导出 interpolateHookEnvVars(http 钩子 header 的 $VAR / ${VAR} 展开只有一只读法,照 CC 2.1.296:名单内展开、名单外与凭据名置空并回报 blanked 清单、大写名字形、剥 CR / LF / NUL);消费方 server hook runner / cli execHttpHook
10
+
11
+ > minor:新增一只纯函数导出 `interpolateHookEnvVars(headers, allowedEnvVars, env)`(主 barrel 与 `./hooks` 两处),外加它的两个结果类型 `HookEnvInterpolation` / `HookEnvBlank`。
12
+ > additive:零删、零改名、零改形;导出面登记表 +3 名 × 2 条子路径。消费方:服务器的钩子执行器(引擎跑的事件)与 cli 的 `execHttpHook`(壳本端跑的事件)改调这一只,零别的改动。
13
+
14
+ ### 新导出:http 钩子 header 里 `$VAR` / `${VAR}` 的展开只有一只读法 —— `interpolateHookEnvVars`
15
+
16
+ 此前壳(cli 的 `execHttpHook`)与服务器的钩子执行器各自带一份展开读法,同一份钩子定义发出的字节并不相同(例:`${TOKEN}` 花括号形壳里展开、服务器里原样;
17
+ 名单外的引用壳里置空、服务器里留字面;名字形、控制字符的处置也各不同)。现在两边调同一只函数,到桩的字节相同。读法照 CC 2.1.296 逐字节:
18
+ - **只读 header 值**,header 名不动;引用是 `$NAME` 或 `${NAME}`,`NAME` = `[A-Z_][A-Z0-9_]*`(只认大写;小写、数字开头、`${}`、未闭合的 `${` 都是普通文字)。
19
+ - **名单内**(`allowedEnvVars`)⇒ 展开成 `env` 里的值;`env` 没有这个名字 ⇒ 展开成空串(不进 `blanked`)。
20
+ - **名单外** ⇒ **空串**(不留字面),进返回值的 `blanked` 清单(`{ header, name, reason: "not-allowed" }`);该变量的 `env` 值不被读取。
21
+ - **凭据名变量**(CC 2.1.296 的闭集名字表:27 个名字 × 裸名与 `INPUT_` 前缀两种拼法,共 54 个)⇒ 即使名单点了名也是空串,进 `blanked`(`reason: "credential"`)。是名字表,不是后缀规则 —— `MY_TOKEN` 是普通变量。
22
+ - **结果剥 CR / LF / NUL**(防 header 注入),对每个 header 值都做,不论有没有引用。
23
+ - 纯函数:零 I/O、零日志、不读全局环境(`env` 是调用方给的视图)、不改入参。`blanked` 给调用方留痕(CC 对每条写一行 debug 日志),header 本身两种情形下都是定稿。
24
+ `blanked` 按 header 序、再按首次出现序;同一 header 里重复引用的同一个名字只列一次。
25
+
26
+ 与 cli 今天的展开函数(`execHttpHook.ts` 的 `interpolateEnvVars`)逐字节同 —— 名单内 / 名单外置空 / 无值 / 名字形 / 剥除五项读数相同;唯一不同是凭据名那一臂:cli 尚无,名单点了名的凭据名它会照常展开,
27
+ 改调本导出后收敛。CC 2.1.296 另有一族按 `env` 值与运行状态判的置空(远端汇点名、代理 / 包索引 URL 等),需要本函数不取的入参,不在这一读法里。
28
+
9
29
  ## 12.0.1 — 2026-10-12 — patch:文件系统失败句一只铸点(errno × 落点 × 真实路径)覆盖写 / 删 / 改名 / 迁移拒句 —— 删除失败点名目录 · 拒句用真实路径不写死 config.d/ · 不可变目录提示 · ENAMETOOLONG 一句;零型面
10
30
 
11
31
  > patch:文件系统失败句收成一只铸点,覆盖写 / 删 / 改名 / 读与 `config.d` 文档拒句四处。零型面(零新导出、零删、零改名;导出面登记表不动),退出码、报告行、库写口抛的错的 `code` / `cause` 都不变;
package/README.md CHANGED
@@ -411,7 +411,8 @@ therefore shows only `zod`. Node built-ins only (`node:crypto`, `node:fs`).
411
411
  `HOOK_EVENT_OWNER` (engine | shell), the matcher-field table, a per-event input schema (`HookInput`),
412
412
  the output/decision shapes (`SyncHookOutput` / `HookSpecificOutput` / `HookPermissionRequestDecision`),
413
413
  `HooksConfig` (the `settings.json` `hooks` shape — a CLOSED key set, so a typo is caught at the wire),
414
- and stop attribution (`StoppedBy` / `stopAttributionText`).
414
+ stop attribution (`StoppedBy` / `stopAttributionText`), and the one reading of `$VAR` / `${VAR}` in an `http` hook's header values
415
+ (`interpolateHookEnvVars` — allow-listed names expand, everything else is blanked and reported, credential variables are never released, CR / LF / NUL stripped).
415
416
  - **Cross-session settings** (`cross-session.ts`): the two `settings.json` keys `crossSessionInbound`
416
417
  (wake/next-turn/hold/refuse — the core 7.30.0 vocabulary, `accept` retired in 6.0.0; `default` = unset) and `dialogExpiry` (60s/5m/10m/never), each with a reader that
417
418
  answers `ok:false` on a bad value instead of silently folding to the default.
@@ -1 +1 @@
1
- 12.0.1
1
+ 12.1.0
package/dist/hooks.d.ts CHANGED
@@ -2283,6 +2283,36 @@ export type HookMatcherForm = "literal" | "regex" | "invalid";
2283
2283
  * For a caller that must route matchers before evaluating them (run the literal ones inline, give the regex ones a deadline).
2284
2284
  */
2285
2285
  export declare function hookMatcherForm(matcher: string | undefined): HookMatcherForm;
2286
+ /** One reference in a header value that {@link interpolateHookEnvVars} replaced with the empty string —— what the caller records. */
2287
+ export interface HookEnvBlank {
2288
+ /** The header (its name, as keyed in the input) whose value held the reference. */
2289
+ readonly header: string;
2290
+ /** The variable name as written in the reference. The value is never part of this record. */
2291
+ readonly name: string;
2292
+ /** `"not-allowed"` —— the name is not in the hook's `allowedEnvVars`. `"credential"` —— the name is a credential variable, which no list releases. */
2293
+ readonly reason: "not-allowed" | "credential";
2294
+ }
2295
+ /** What {@link interpolateHookEnvVars} returns: the headers as they go on the wire + the references it blanked. */
2296
+ export interface HookEnvInterpolation {
2297
+ readonly headers: Record<string, string>;
2298
+ /** In header order, then first-occurrence order within a header; a name that is referenced several times in one header is listed once. */
2299
+ readonly blanked: readonly HookEnvBlank[];
2300
+ }
2301
+ /**
2302
+ * The one reading of `$VAR` / `${VAR}` in an http hook's header values —— the shell and the server both call this; neither keeps a copy,
2303
+ * so one hook definition puts the same bytes on the wire from either. The reading is CC 2.1.296's (`cli296.js:249576`), byte for byte:
2304
+ *
2305
+ * · only header **values** are read (header names are never touched);
2306
+ * · a reference is `$NAME` or `${NAME}` with NAME = `[A-Z_][A-Z0-9_]*`; anything else (lower case, a digit first, `${}`, an unclosed `${`) is plain text;
2307
+ * · a name in `allowedEnvVars` ⇒ the env value (`""` when `env` has none —— expanded to empty, not listed in `blanked`);
2308
+ * · a name **not** in `allowedEnvVars` ⇒ `""`, listed in `blanked` (`reason: "not-allowed"`); its env value is not read;
2309
+ * · a credential variable ({@link HOOK_ENV_CREDENTIAL_NAMES}'s closed set) ⇒ `""` even when the list names it, listed in `blanked` (`reason: "credential"`);
2310
+ * · CR / LF / NUL are removed from every resulting header value, references or not.
2311
+ *
2312
+ * The caller decides what to do with `blanked` (a debug line per entry is what CC writes); the headers are final either way.
2313
+ * Pure: no I/O, no logging, no global env —— `env` is the caller's view (`process.env` at the call site). Inputs are not modified.
2314
+ */
2315
+ export declare function interpolateHookEnvVars(headers: Readonly<Record<string, string>>, allowedEnvVars: readonly string[], env: Readonly<Record<string, string | undefined>>): HookEnvInterpolation;
2286
2316
  /**
2287
2317
  * The hooks config record — the shape `TaskRequest.settings.hooks` carries on the wire and the shell
2288
2318
  * reads from settings JSON: `{ [eventName]: [{ matcher?, hooks: [...] }] }`. Keys limited to the 33
package/dist/hooks.js CHANGED
@@ -768,6 +768,87 @@ export function hookMatcherForm(matcher) {
768
768
  }
769
769
  }
770
770
  }
771
+ /** `${NAME}` or `$NAME`, NAME = `[A-Z_][A-Z0-9_]*` —— upper-case names only (lower-case and digit-led spellings are not references). */
772
+ const HOOK_ENV_REFERENCE = /\$\{([A-Z_][A-Z0-9_]*)\}|\$([A-Z_][A-Z0-9_]*)/g;
773
+ /** CR, LF and NUL never reach a header value (header injection through an env value or a configured template). */
774
+ const HOOK_HEADER_FORBIDDEN = /[\r\n\0]/g;
775
+ /**
776
+ * The credential variables an http hook header never receives, whatever `allowedEnvVars` says. The closed name set of CC 2.1.296
777
+ * (`cli296.js:237450` `_nn` / `ynn`, `:246731` `lGn` / `bft` / `hGn` / `vft`): 27 base names, each also under an `INPUT_` prefix
778
+ * (the spelling a CI action passes an input in). A name set, not a pattern —— `MY_TOKEN` is an ordinary variable.
779
+ * CC 296 also blanks, unless the managed policy lists the name, a second family decided from env values and live state
780
+ * (remote-sink names, proxy / registry URLs …); that family needs inputs this function does not take and is not part of this reading.
781
+ */
782
+ const HOOK_ENV_CREDENTIAL_NAMES = new Set([
783
+ // CC 296 `_nn`: the OAuth token, the five file-descriptor spellings (`ynn`), then the bridge / device / proxy / background-session tokens
784
+ "CLAUDE_CODE_OAUTH_TOKEN",
785
+ "CLAUDE_CODE_OAUTH_TOKEN_FILE_DESCRIPTOR",
786
+ "CLAUDE_CODE_GATEWAY_TOKEN_FILE_DESCRIPTOR",
787
+ "CLAUDE_CODE_API_KEY_FILE_DESCRIPTOR",
788
+ "CLAUDE_CODE_WEBSOCKET_AUTH_FILE_DESCRIPTOR",
789
+ "CCR_AGENT_PROXY_TOKEN_FILE_DESCRIPTOR",
790
+ "CLAUDE_CODE_ARTIFACTS_API_TOKEN",
791
+ "CLAUDE_CODE_SLACK_TAG_TOKEN",
792
+ "CLAUDE_CODE_HFI_BEARER_TOKEN",
793
+ "CLAUDE_BRIDGE_OAUTH_TOKEN",
794
+ "CLAUDE_TRUSTED_DEVICE_TOKEN",
795
+ "AGENT_PROXY_AUTH_TOKEN",
796
+ "CLAUDE_CODE_MCP_SERVE_AUTH_TOKEN",
797
+ "CLAUDE_BG_AUTH_SNAPSHOT_PATH",
798
+ "CLAUDE_BG_SOCKET_TOKENS_PATH",
799
+ "CLAUDE_BG_RV_AUTH",
800
+ "CLAUDE_BG_PTY_AUTH",
801
+ "CLAUDE_BG_CLAIM_AUTH",
802
+ // CC 296 `lGn`: refresh / ingress / host-credential / messaging tokens, MCP client secrets, runner secrets
803
+ "CLAUDE_CODE_OAUTH_REFRESH_TOKEN",
804
+ "CLAUDE_SESSION_INGRESS_TOKEN_FILE",
805
+ "CLAUDE_CODE_HOST_CREDS_FILE",
806
+ "CLAUDE_CODE_MESSAGING_TOKEN",
807
+ "MCP_CLIENT_SECRET",
808
+ "MCP_XAA_IDP_CLIENT_SECRET",
809
+ "ENVIRONMENT_SERVICE_KEY",
810
+ "SELF_HOSTED_RUNNER_POOL_SECRET",
811
+ "SELF_HOSTED_RUNNER_ENVIRONMENT_SECRET",
812
+ ].flatMap((name) => [name, `INPUT_${name}`]));
813
+ /**
814
+ * The one reading of `$VAR` / `${VAR}` in an http hook's header values —— the shell and the server both call this; neither keeps a copy,
815
+ * so one hook definition puts the same bytes on the wire from either. The reading is CC 2.1.296's (`cli296.js:249576`), byte for byte:
816
+ *
817
+ * · only header **values** are read (header names are never touched);
818
+ * · a reference is `$NAME` or `${NAME}` with NAME = `[A-Z_][A-Z0-9_]*`; anything else (lower case, a digit first, `${}`, an unclosed `${`) is plain text;
819
+ * · a name in `allowedEnvVars` ⇒ the env value (`""` when `env` has none —— expanded to empty, not listed in `blanked`);
820
+ * · a name **not** in `allowedEnvVars` ⇒ `""`, listed in `blanked` (`reason: "not-allowed"`); its env value is not read;
821
+ * · a credential variable ({@link HOOK_ENV_CREDENTIAL_NAMES}'s closed set) ⇒ `""` even when the list names it, listed in `blanked` (`reason: "credential"`);
822
+ * · CR / LF / NUL are removed from every resulting header value, references or not.
823
+ *
824
+ * The caller decides what to do with `blanked` (a debug line per entry is what CC writes); the headers are final either way.
825
+ * Pure: no I/O, no logging, no global env —— `env` is the caller's view (`process.env` at the call site). Inputs are not modified.
826
+ */
827
+ export function interpolateHookEnvVars(headers, allowedEnvVars, env) {
828
+ const allowed = new Set(allowedEnvVars);
829
+ const blanked = [];
830
+ const entries = Object.entries(headers).map(([header, value]) => {
831
+ const listed = new Set();
832
+ const blank = (name, reason) => {
833
+ if (!listed.has(name)) {
834
+ listed.add(name);
835
+ blanked.push({ header, name, reason });
836
+ }
837
+ return "";
838
+ };
839
+ const expanded = value.replace(HOOK_ENV_REFERENCE, (reference) => {
840
+ const name = reference.startsWith("${") ? reference.slice(2, -1) : reference.slice(1);
841
+ if (!allowed.has(name))
842
+ return blank(name, "not-allowed");
843
+ if (HOOK_ENV_CREDENTIAL_NAMES.has(name))
844
+ return blank(name, "credential");
845
+ return env[name] ?? "";
846
+ });
847
+ return [header, expanded.replace(HOOK_HEADER_FORBIDDEN, "")];
848
+ });
849
+ // fromEntries defines own properties: a header keyed `__proto__` stays a header instead of replacing the prototype.
850
+ return { headers: Object.fromEntries(entries), blanked };
851
+ }
771
852
  export const HooksConfig = z.partialRecord(HookEventName, z.array(HookMatcherEntry));
772
853
  /**
773
854
  * DESIGN POINT ③ (slot semantics — RESERVED, core to finalize): how `spec.hooks` composes with the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/settings-schema",
3
- "version": "12.0.1",
3
+ "version": "12.1.0",
4
4
  "description": "Sema settings schema — pure config contract (zod domains + effective-config + roster resolution + ref-integrity + remoteExec) shared by sema-web-admin, sema-server, and the TOC desktop/CLI. One source of truth. (Renamed at 1.0.0 from @sema-agent/registry-core — name and docs only, no behavior change; note the range moves 0.19.0 -> 1.0.0, so a dependency must become ^1.0.0. The old names @sema-agent/registry-core <=0.19.0 and @sema-ai/registry-core remain published, frozen, for migration.)",
5
5
  "type": "module",
6
6
  "license": "BUSL-1.1",