@sema-agent/settings-schema 12.0.1 → 12.1.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/CHANGELOG.md CHANGED
@@ -6,6 +6,33 @@
6
6
  > `@sema-agent/registry-core`(更早还曾是 `@sema-ai/registry-core`)。本档 `0.19.0` 以下各节里的
7
7
  > 包名是**历史记录**,如实保留不追改。
8
8
 
9
+ ## 12.1.1 — 2026-10-12 — patch:`isAbsoluteHostPath` 认三族绝对形(posix `/…` · Windows 盘符 `X:\\…` / `X:/…` · UNC `\\\\server\\share\\…`),同一条律(零 `..` 段、零 NUL、长度上界);拒句点名三族
10
+
11
+ - **行为修复(宿主路径形判)**。`isAbsoluteHostPath` / `AbsoluteHostPath` 此前只认 `/` 开头(posix 口径),Windows 壳按 CC 同形送 `C:\…` 的任务 `cwd` / `additionalDirectories` / `skill.baseDir` 全被判「非绝对」—— 读它的 server 7.125.0 据此把 Windows 宿主的**每一条提交** 400 掉。
12
+ **Now.** 三族绝对形同一只判官:posix(`/` 开头)· Windows 盘符(`X:\` / `X:/`;裸 `X:` 与 `X:rel` 不绝对)· UNC(`\\server\share`;`\\server` 不够;`\\?\` 设备路径不收);三族同一条律 —— 零 `..` 段(两种分隔符都算)、零 NUL、≤ `ABSOLUTE_HOST_PATH_MAX`。只判形、不碰文件系统、不做族内归一(怎么解析归宿主那一族)。拒句改成点名三族。
13
+ 三问 —— **谁需要**:大脑在云、手在 Windows 本机的用户(壳禁止侧补偿,路径按宿主原形送);**谁受伤**:无 —— posix 读数逐字不变;把「`C:\…` 应被拒」当事实的消费方(没有);**补偿**:无旋钮(形判不是策略)。
14
+ 钉:`test/host-path-11.3.1.test.ts`(盘符 / UNC 真;裸盘符 / `C:rel` / 单反斜杠 / 无 share / 设备路径假;`..` / NUL / 超长跨三族同拒;zod 拒句点名三族)· `test/projects-11.3.0.test.ts` 参照式随律翻(三族)。与 11.3.1(11.x 线热修,dist-tag `line-11`)同一处改。
15
+
16
+ ## 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
17
+
18
+ > minor:新增一只纯函数导出 `interpolateHookEnvVars(headers, allowedEnvVars, env)`(主 barrel 与 `./hooks` 两处),外加它的两个结果类型 `HookEnvInterpolation` / `HookEnvBlank`。
19
+ > additive:零删、零改名、零改形;导出面登记表 +3 名 × 2 条子路径。消费方:服务器的钩子执行器(引擎跑的事件)与 cli 的 `execHttpHook`(壳本端跑的事件)改调这一只,零别的改动。
20
+
21
+ ### 新导出:http 钩子 header 里 `$VAR` / `${VAR}` 的展开只有一只读法 —— `interpolateHookEnvVars`
22
+
23
+ 此前壳(cli 的 `execHttpHook`)与服务器的钩子执行器各自带一份展开读法,同一份钩子定义发出的字节并不相同(例:`${TOKEN}` 花括号形壳里展开、服务器里原样;
24
+ 名单外的引用壳里置空、服务器里留字面;名字形、控制字符的处置也各不同)。现在两边调同一只函数,到桩的字节相同。读法照 CC 2.1.296 逐字节:
25
+ - **只读 header 值**,header 名不动;引用是 `$NAME` 或 `${NAME}`,`NAME` = `[A-Z_][A-Z0-9_]*`(只认大写;小写、数字开头、`${}`、未闭合的 `${` 都是普通文字)。
26
+ - **名单内**(`allowedEnvVars`)⇒ 展开成 `env` 里的值;`env` 没有这个名字 ⇒ 展开成空串(不进 `blanked`)。
27
+ - **名单外** ⇒ **空串**(不留字面),进返回值的 `blanked` 清单(`{ header, name, reason: "not-allowed" }`);该变量的 `env` 值不被读取。
28
+ - **凭据名变量**(CC 2.1.296 的闭集名字表:27 个名字 × 裸名与 `INPUT_` 前缀两种拼法,共 54 个)⇒ 即使名单点了名也是空串,进 `blanked`(`reason: "credential"`)。是名字表,不是后缀规则 —— `MY_TOKEN` 是普通变量。
29
+ - **结果剥 CR / LF / NUL**(防 header 注入),对每个 header 值都做,不论有没有引用。
30
+ - 纯函数:零 I/O、零日志、不读全局环境(`env` 是调用方给的视图)、不改入参。`blanked` 给调用方留痕(CC 对每条写一行 debug 日志),header 本身两种情形下都是定稿。
31
+ `blanked` 按 header 序、再按首次出现序;同一 header 里重复引用的同一个名字只列一次。
32
+
33
+ 与 cli 今天的展开函数(`execHttpHook.ts` 的 `interpolateEnvVars`)逐字节同 —— 名单内 / 名单外置空 / 无值 / 名字形 / 剥除五项读数相同;唯一不同是凭据名那一臂:cli 尚无,名单点了名的凭据名它会照常展开,
34
+ 改调本导出后收敛。CC 2.1.296 另有一族按 `env` 值与运行状态判的置空(远端汇点名、代理 / 包索引 URL 等),需要本函数不取的入参,不在这一读法里。
35
+
9
36
  ## 12.0.1 — 2026-10-12 — patch:文件系统失败句一只铸点(errno × 落点 × 真实路径)覆盖写 / 删 / 改名 / 迁移拒句 —— 删除失败点名目录 · 拒句用真实路径不写死 config.d/ · 不可变目录提示 · ENAMETOOLONG 一句;零型面
10
37
 
11
38
  > 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.1
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
@@ -5,13 +5,6 @@
5
5
  import { z } from "zod";
6
6
  /** 一条主机绝对路径的长度上界(字符数)。 */
7
7
  export declare const ABSOLUTE_HOST_PATH_MAX = 4096;
8
- /**
9
- * `p` 是不是一条形上合法的**主机绝对路径**:字符串、非空、≤ {@link ABSOLUTE_HOST_PATH_MAX}、不含 NUL、以 `/` 开头(posix 绝对)、没有 `..` 段
10
- * (`/` 与 `\` 都算分隔符 —— `path.isAbsolute` 是语法判,`/a/../../etc` 也「绝对」,而读方 `path.resolve` 会把它解到别处;拒掉它,读到的就是写下的那一条)。
11
- *
12
- * posix 口径是有意的:读这些路径的主机车道今天跑 posix 树,而这一律在 posix 宿主上与 Node 的 `path.isAbsolute` 逐字同判(`C:\x`、`\\host\share`
13
- * 在 posix 上不绝对)。哪天主机车道要接 win 树,换的是这一只(按树的家族判),读方跟着同一只走。
14
- */
15
8
  export declare function isAbsoluteHostPath(p: unknown): p is string;
16
9
  /** {@link isAbsoluteHostPath} 的 zod 座位(一条 issue,定句)。 */
17
10
  export declare const AbsoluteHostPath: z.ZodString & z.ZodType<string, string, z.core.$ZodTypeInternals<string, string>>;
package/dist/host-path.js CHANGED
@@ -7,16 +7,21 @@ import { buildQuotedIssue, fixed } from "./quoted-issue.js";
7
7
  /** 一条主机绝对路径的长度上界(字符数)。 */
8
8
  export const ABSOLUTE_HOST_PATH_MAX = 4096;
9
9
  /**
10
- * `p` 是不是一条形上合法的**主机绝对路径**:字符串、非空、≤ {@link ABSOLUTE_HOST_PATH_MAX}、不含 NUL、以 `/` 开头(posix 绝对)、没有 `..` 段
10
+ * `p` 是不是一条形上合法的**主机绝对路径**:字符串、非空、≤ {@link ABSOLUTE_HOST_PATH_MAX}、不含 NUL、三族绝对形之一(posix `/` 开头 · Windows 盘符 · UNC)、没有 `..` 段
11
11
  * (`/` 与 `\` 都算分隔符 —— `path.isAbsolute` 是语法判,`/a/../../etc` 也「绝对」,而读方 `path.resolve` 会把它解到别处;拒掉它,读到的就是写下的那一条)。
12
12
  *
13
- * posix 口径是有意的:读这些路径的主机车道今天跑 posix 树,而这一律在 posix 宿主上与 Node 的 `path.isAbsolute` 逐字同判(`C:\x`、`\\host\share`
14
- * 在 posix 上不绝对)。哪天主机车道要接 win 树,换的是这一只(按树的家族判),读方跟着同一只走。
13
+ * 宿主是哪一族由壳决定(大脑在云、手在本机:Windows 壳按 CC 同形送 `C:\…`),所以三族绝对形同一只判官、同一条律:
14
+ * posix(`/` 开头)· Windows 盘符(`X:\` / `X:/`;裸 `X:` 与 `X:rel` 不绝对)· UNC(`\\server\share`;`\\server` 不够)。
15
+ * 读方只拿「形合法」这一个事实,怎么解析归宿主那一族 —— 这里不碰文件系统、不做族内归一。
15
16
  */
17
+ const WIN_DRIVE_ABSOLUTE = /^[A-Za-z]:[\\/]/;
18
+ const WIN_UNC_ABSOLUTE = /^\\\\[^\\/?*]+[\\/][^\\/]+/;
16
19
  export function isAbsoluteHostPath(p) {
17
- return typeof p === "string" && p.length > 0 && p.length <= ABSOLUTE_HOST_PATH_MAX && !p.includes("\0") && p.startsWith("/") && !p.split(/[/\\]/).includes("..");
20
+ return (typeof p === "string" && p.length > 0 && p.length <= ABSOLUTE_HOST_PATH_MAX && !p.includes("\0") &&
21
+ (p.startsWith("/") || WIN_DRIVE_ABSOLUTE.test(p) || WIN_UNC_ABSOLUTE.test(p)) &&
22
+ !p.split(/[/\\]/).includes(".."));
18
23
  }
19
24
  /** {@link isAbsoluteHostPath} 的 zod 座位(一条 issue,定句)。 */
20
25
  export const AbsoluteHostPath = z
21
26
  .string()
22
- .refine(isAbsoluteHostPath, buildQuotedIssue `must be an absolute host path: starts with "/", no ".." segment, no NUL, at most ${fixed(String(ABSOLUTE_HOST_PATH_MAX))} characters`);
27
+ .refine(isAbsoluteHostPath, buildQuotedIssue `must be an absolute host path: posix ("/..."), Windows drive ("C:\\..." or "C:/...") or UNC ("\\\\server\\share\\..."), no ".." segment, no NUL, at most ${fixed(String(ABSOLUTE_HOST_PATH_MAX))} characters`);
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.1",
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",