@sema-agent/settings-schema 12.3.0 → 13.0.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.
Files changed (52) hide show
  1. package/CHANGELOG.md +170 -0
  2. package/README.md +32 -4
  3. package/conformance/VERSION +1 -1
  4. package/conformance/cases/c01-entry-salvage-then-typo/expected.json +1 -3
  5. package/conformance/cases/c03-literal-dotted-top-key/expected.json +4 -12
  6. package/conformance/cases/c10-record-key-secret-redacted-twice/expected.json +1 -3
  7. package/conformance/cases/c23-mixed-validity-models/expected.json +2 -6
  8. package/conformance/cases/c24-valid-entries-with-unknown-keys/expected.json +2 -6
  9. package/conformance/cases/c25-published-roundtrip-zero-warnings/expected.json +2 -6
  10. package/conformance/cases/c30-version-triple-a-first/expected.json +2 -6
  11. package/conformance/cases/c31-version-triple-b-changed/expected.json +2 -6
  12. package/conformance/cases/c32-version-triple-a-restored/expected.json +2 -6
  13. package/conformance/cases/c40-s712-identity-only-what-reads-out/expected.json +1 -3
  14. package/conformance/cases/c41-s792-encoded-pem-identity/expected.json +1 -3
  15. package/conformance/cases/c42-s792-unknown-key-secret-redacted/expected.json +2 -6
  16. package/conformance/cases/c43-s792-form-and-deep-encoded-pem/expected.json +1 -3
  17. package/conformance/cases/c44-s792-utf8-escape-glued-token/expected.json +1 -3
  18. package/conformance/cases/c45-s792-identity-bounds-and-decode-depth/expected.json +1 -3
  19. package/conformance/cases/c47-s811-roles-key-url-userinfo-withheld/expected.json +1 -3
  20. package/conformance/cases/c48-s935-model-headers-transported/expected.json +6 -18
  21. package/conformance/cases/c49-s935-model-headers-secret-entry-dropped/expected.json +1 -3
  22. package/conformance/cases/c69-deep-encoded-token-past-every-reading/expected.json +1 -3
  23. package/conformance/cases/c70-rewritten-token-in-front-of-encoded-pem-header/expected.json +1 -3
  24. package/conformance/cases/c72-dangling-reference-write-seat-refuses/expected.json +1 -3
  25. package/conformance/cases/cc03-model-compat-unknown-subkey/expected.json +2 -6
  26. package/conformance/cases/cli01-model-name-nfc-vs-nfd/expected.json +1 -3
  27. package/conformance/cases/cli02-control-and-zero-width-chars/expected.json +1 -3
  28. package/conformance/cases/cli05-long-and-prefix-colliding-names/expected.json +6 -18
  29. package/conformance/cases/t01-absent-domain-key-not-null/expected.json +1 -3
  30. package/conformance/cases/t03-prototype-name-key-not-swallowed/expected.json +2 -6
  31. package/conformance/cases/t04-secret-literal-never-in-warning-value/expected.json +1 -3
  32. package/dist/hooks-config.d.ts +235 -0
  33. package/dist/hooks-config.js +458 -0
  34. package/dist/hooks.d.ts +16 -154
  35. package/dist/hooks.js +17 -111
  36. package/dist/http-hook-allowlists.d.ts +63 -0
  37. package/dist/http-hook-allowlists.js +73 -0
  38. package/dist/index.d.ts +5 -0
  39. package/dist/index.js +6 -1
  40. package/dist/mcp-server-policy.d.ts +75 -0
  41. package/dist/mcp-server-policy.js +84 -0
  42. package/dist/safety-merge-spec.d.ts +13 -3
  43. package/dist/safety-merge-spec.js +10 -3
  44. package/dist/settings-sources.d.ts +91 -0
  45. package/dist/settings-sources.js +107 -0
  46. package/dist/types.d.ts +6 -6
  47. package/dist/types.js +9 -5
  48. package/dist/ui-language.d.ts +40 -0
  49. package/dist/ui-language.js +37 -0
  50. package/dist/url-pattern.d.ts +33 -0
  51. package/dist/url-pattern.js +239 -0
  52. package/package.json +1 -1
@@ -0,0 +1,75 @@
1
+ /**
2
+ * MCP 服务器名单判决(13.0.0,U6 / S-1626;cli [10720] 候包口)—— 托管 / 用户的 `allowedMcpServers` / `deniedMcpServers` 拦下一台 MCP
3
+ * 服务器时「为什么」的**唯一**判官。纯函数,浏览器安全。与 U3 `httpHookUrlVerdict` 同形:放行 ⇒ `undefined`;拦下 ⇒ {@link McpPolicyBlock}
4
+ * —— 判官出结构不出句,壳 / server / web-admin 只渲结构、不各自造句。
5
+ *
6
+ * 判法(锚 = cli main ceff8bc6 `mcpServerPolicyBlock`,它与 CC 2.1.296 名单判法逐支一致;URL 条目按本包 `url-pattern` 两只出口判):
7
+ * - 拒绝名单**绝对优先**,各层都算(用户可以替自己拒):名字条目 → 命令条目(只对 stdio,整数组逐项相等)→ URL 条目(只对远端,拒绝出口)。
8
+ * - 允许名单:各层写的拼成一张(CC「Arrays merge across settings sources」);托管层(源表 `SETTINGS_SOURCE_CONTRACT` 档 = `managed` 的源,
9
+ * 经 `settingsSourceContract` 查 —— 调用方不给档)写了 `allowManagedMcpServersOnly: true` ⇒ 只读托管层那一张(别层写的这个开关不生效)。没有一层写 ⇒ 不限;空 ⇒ 一台都不许;
10
+ * stdio 且名单里有命令条目 ⇒ 必须命中其一;远端且名单里有 URL 条目 ⇒ 必须命中其一(放行出口);否则按名字。
11
+ * - SDK 型等「不由宿主连接」的条目是否豁免,归调用方(cli 的过滤口豁免、盘上加载器不豁免 —— 那是接线,不是判法)。
12
+ *
13
+ * **目标零展开**(cli X-T1 同律):判官**判**的是宿主展开环境变量 / 用户配置之后的连接目标(`server.target` —— 真连的就是它,按写法判会让
14
+ * `${URL}` 绕过拒绝名单),**回显**的只有展开前的写法(`server.written`);stdio 只回显可执行名与参数个数(参数原文常常就是凭据)。
15
+ * 展开值不进 {@link McpPolicyBlock} 的任何字段。
16
+ */
17
+ import { type SettingsSource } from "./settings-sources.js";
18
+ /** 两张名单的键(闭集,声明序)。 */
19
+ export declare const MCP_SERVER_LIST_KEYS: readonly ["allowedMcpServers", "deniedMcpServers"];
20
+ export type McpServerListKey = (typeof MCP_SERVER_LIST_KEYS)[number];
21
+ /** 名单里的一条:恰好一种 —— 按名字、按 stdio 命令数组(整数组逐项相等)、按 URL 模式(`url-pattern`)。 */
22
+ export type McpServerListEntry = {
23
+ readonly serverName: string;
24
+ } | {
25
+ readonly serverCommand: readonly string[];
26
+ } | {
27
+ readonly serverUrl: string;
28
+ };
29
+ /** 一台服务器的连接目标:stdio = `[可执行, ...参数]`,远端 = URL。 */
30
+ export type McpServerTarget = {
31
+ readonly kind: "command";
32
+ readonly command: readonly string[];
33
+ } | {
34
+ readonly kind: "url";
35
+ readonly url: string;
36
+ };
37
+ /** 被判的那一台。`target` 缺席 = 判官只按名字判(类型不明 / 宿主不连接的条目)。`written` 缺席 = 宿主没有展开前的写法(那时只回显种类)。 */
38
+ export interface McpPolicyServer {
39
+ readonly name: string;
40
+ /** 宿主真连的目标(展开之后)—— 只用来判,不回显。 */
41
+ readonly target?: McpServerTarget;
42
+ /** 同一目标展开之前的写法 —— 回显的唯一来源。种类与 `target` 不符 ⇒ 不回显。 */
43
+ readonly written?: McpServerTarget;
44
+ }
45
+ /** 一个设置层的两张名单读数。`source` = 这一层是哪个设置源(闭集 `SETTINGS_SOURCES`,原样回显进 `sources`;托管层由源表认,调用方不给档)。 */
46
+ export interface McpPolicyLayer {
47
+ readonly source: SettingsSource;
48
+ readonly allowedMcpServers?: readonly McpServerListEntry[];
49
+ readonly deniedMcpServers?: readonly McpServerListEntry[];
50
+ readonly allowManagedMcpServersOnly?: boolean;
51
+ }
52
+ /** 没过的那一条的种类(闭集)。拒绝名单:命中的条目种类;允许名单:必须命中而没命中的条目种类,`empty` = 允许名单是空数组。 */
53
+ export declare const MCP_POLICY_RULES: readonly ["name", "command", "url", "empty"];
54
+ export type McpPolicyRule = (typeof MCP_POLICY_RULES)[number];
55
+ /** 一台 MCP 服务器被名单拦下的原因(六字段,零句子)。 */
56
+ export interface McpPolicyBlock {
57
+ /** 配置里的原名。 */
58
+ readonly name: string;
59
+ /** 判下它的那张名单(拒绝名单优先)。 */
60
+ readonly list: McpServerListKey;
61
+ readonly rule: McpPolicyRule;
62
+ /** 判官判的那种身份 —— 只有种类,不带值。 */
63
+ readonly target: McpServerTarget["kind"] | "none";
64
+ /** 展开前的写法:URL 原样;stdio 只给可执行名与参数个数。宿主没给写法(或种类不符)⇒ `undefined`(键恒在)。 */
65
+ readonly written: {
66
+ readonly url: string;
67
+ } | {
68
+ readonly command: string;
69
+ readonly argCount: number;
70
+ } | undefined;
71
+ /** 携带判下它的那张名单的设置层(入参序):拒绝 ⇒ 携带命中条目的层;允许 ⇒ 写了参与判决的允许名单的层。 */
72
+ readonly sources: readonly SettingsSource[];
73
+ }
74
+ /** 名单对这一台的判决:放行 ⇒ `undefined`;拦下 ⇒ 原因(判法见文件头)。 */
75
+ export declare function mcpServerPolicyVerdict(server: McpPolicyServer, layers: readonly McpPolicyLayer[]): McpPolicyBlock | undefined;
@@ -0,0 +1,84 @@
1
+ /**
2
+ * MCP 服务器名单判决(13.0.0,U6 / S-1626;cli [10720] 候包口)—— 托管 / 用户的 `allowedMcpServers` / `deniedMcpServers` 拦下一台 MCP
3
+ * 服务器时「为什么」的**唯一**判官。纯函数,浏览器安全。与 U3 `httpHookUrlVerdict` 同形:放行 ⇒ `undefined`;拦下 ⇒ {@link McpPolicyBlock}
4
+ * —— 判官出结构不出句,壳 / server / web-admin 只渲结构、不各自造句。
5
+ *
6
+ * 判法(锚 = cli main ceff8bc6 `mcpServerPolicyBlock`,它与 CC 2.1.296 名单判法逐支一致;URL 条目按本包 `url-pattern` 两只出口判):
7
+ * - 拒绝名单**绝对优先**,各层都算(用户可以替自己拒):名字条目 → 命令条目(只对 stdio,整数组逐项相等)→ URL 条目(只对远端,拒绝出口)。
8
+ * - 允许名单:各层写的拼成一张(CC「Arrays merge across settings sources」);托管层(源表 `SETTINGS_SOURCE_CONTRACT` 档 = `managed` 的源,
9
+ * 经 `settingsSourceContract` 查 —— 调用方不给档)写了 `allowManagedMcpServersOnly: true` ⇒ 只读托管层那一张(别层写的这个开关不生效)。没有一层写 ⇒ 不限;空 ⇒ 一台都不许;
10
+ * stdio 且名单里有命令条目 ⇒ 必须命中其一;远端且名单里有 URL 条目 ⇒ 必须命中其一(放行出口);否则按名字。
11
+ * - SDK 型等「不由宿主连接」的条目是否豁免,归调用方(cli 的过滤口豁免、盘上加载器不豁免 —— 那是接线,不是判法)。
12
+ *
13
+ * **目标零展开**(cli X-T1 同律):判官**判**的是宿主展开环境变量 / 用户配置之后的连接目标(`server.target` —— 真连的就是它,按写法判会让
14
+ * `${URL}` 绕过拒绝名单),**回显**的只有展开前的写法(`server.written`);stdio 只回显可执行名与参数个数(参数原文常常就是凭据)。
15
+ * 展开值不进 {@link McpPolicyBlock} 的任何字段。
16
+ */
17
+ import { settingsSourceContract } from "./settings-sources.js";
18
+ import { urlAllowPatternMatches, urlDenyPatternMatches } from "./url-pattern.js";
19
+ /** 两张名单的键(闭集,声明序)。 */
20
+ export const MCP_SERVER_LIST_KEYS = ["allowedMcpServers", "deniedMcpServers"];
21
+ /** 没过的那一条的种类(闭集)。拒绝名单:命中的条目种类;允许名单:必须命中而没命中的条目种类,`empty` = 允许名单是空数组。 */
22
+ export const MCP_POLICY_RULES = ["name", "command", "url", "empty"];
23
+ const isName = (e) => "serverName" in e && e.serverName !== undefined;
24
+ const isCommand = (e) => "serverCommand" in e && e.serverCommand !== undefined;
25
+ const isUrl = (e) => "serverUrl" in e && e.serverUrl !== undefined;
26
+ const sameCommand = (a, b) => a.length === b.length && a.every((x, i) => x === b[i]);
27
+ /** 拒绝名单里一条是否按 `rule` 这一种命中这一台。 */
28
+ function denyHit(rule, entry, server) {
29
+ switch (rule) {
30
+ case "name":
31
+ return isName(entry) && entry.serverName === server.name;
32
+ case "command":
33
+ return server.target?.kind === "command" && isCommand(entry) && sameCommand(entry.serverCommand, server.target.command);
34
+ case "url":
35
+ return server.target?.kind === "url" && isUrl(entry) && urlDenyPatternMatches(server.target.url, entry.serverUrl);
36
+ default: {
37
+ const unknown = rule;
38
+ throw new RangeError(`mcpServerPolicyVerdict: unknown deny rule '${String(unknown)}'`);
39
+ }
40
+ }
41
+ }
42
+ /** 拼好的允许名单对这一台:放行 ⇒ `undefined`;否则必须命中而没命中的那一种。 */
43
+ function allowMiss(entries, server) {
44
+ if (entries.length === 0)
45
+ return "empty";
46
+ const target = server.target;
47
+ if (target?.kind === "command" && entries.some(isCommand)) {
48
+ return entries.some((e) => isCommand(e) && sameCommand(e.serverCommand, target.command)) ? undefined : "command";
49
+ }
50
+ if (target?.kind === "url" && entries.some(isUrl)) {
51
+ return entries.some((e) => isUrl(e) && urlAllowPatternMatches(target.url, e.serverUrl)) ? undefined : "url";
52
+ }
53
+ return entries.some((e) => isName(e) && e.serverName === server.name) ? undefined : "name";
54
+ }
55
+ function writtenOf(server) {
56
+ const w = server.written;
57
+ if (w === undefined || w.kind !== server.target?.kind)
58
+ return undefined;
59
+ return w.kind === "url" ? { url: w.url } : { command: w.command[0] ?? "", argCount: Math.max(0, w.command.length - 1) };
60
+ }
61
+ /** 名单对这一台的判决:放行 ⇒ `undefined`;拦下 ⇒ 原因(判法见文件头)。 */
62
+ export function mcpServerPolicyVerdict(server, layers) {
63
+ // 每一层先过源表(闭集外的源在这里响亮抛,哪怕那一层什么都没写);托管 ⇔ 源表档 = managed。
64
+ const managed = new Set(layers.filter((l) => settingsSourceContract(l.source).trust === "managed"));
65
+ const block = (list, rule, sources) => ({
66
+ name: server.name,
67
+ list,
68
+ rule,
69
+ target: server.target?.kind ?? "none",
70
+ written: writtenOf(server),
71
+ sources,
72
+ });
73
+ for (const rule of ["name", "command", "url"]) {
74
+ const sources = layers.filter((l) => (l.deniedMcpServers ?? []).some((e) => denyHit(rule, e, server))).map((l) => l.source);
75
+ if (sources.length > 0)
76
+ return block("deniedMcpServers", rule, sources);
77
+ }
78
+ const managedOnly = layers.some((l) => managed.has(l) && l.allowManagedMcpServersOnly === true);
79
+ const allowLayers = layers.filter((l) => l.allowedMcpServers !== undefined && (!managedOnly || managed.has(l)));
80
+ if (allowLayers.length === 0)
81
+ return undefined;
82
+ const miss = allowMiss(allowLayers.flatMap((l) => l.allowedMcpServers), server);
83
+ return miss === undefined ? undefined : block("allowedMcpServers", miss, allowLayers.map((l) => l.source));
84
+ }
@@ -90,9 +90,9 @@ export declare function isAtLeast(layer: MinTrust, minTrust: MinTrust): boolean;
90
90
  * that would otherwise ask/deny — a privilege, so it needs trust; `tighten` = only ever narrows — restricting is
91
91
  * safe from anyone, so any settings-file layer may add it. The word is what a consumer branches on for the
92
92
  * NOT-admitted outlet: the ruling REQUIRES a consumer to turn a `loosen` member below its floor into a pending
93
- * suggestion (consent flow) rather than drop it silently. This package returns that metadata only — it performs
94
- * no admission and no degradation; whether a given consumer version implements the outlet is that consumer's
95
- * contract. */
93
+ * suggestion (consent flow) rather than drop it silently. This package returns that metadata, and `layerAdmission`
94
+ * (`settings-sources.ts`) reads it per settings layer; it performs no degradation — whether a given consumer version
95
+ * implements the outlet is that consumer's contract. */
96
96
  export declare const MemberDirection: z.ZodEnum<{
97
97
  loosen: "loosen";
98
98
  tighten: "tighten";
@@ -524,6 +524,16 @@ export declare const SAFETY_MERGE_SPEC: {
524
524
  readonly fail: "closed";
525
525
  readonly seam: "web-admin → governanceBaseline (SSO/SCIM/audit/budget, TOB-only)";
526
526
  };
527
+ readonly httpHookAllowlists: {
528
+ readonly mergeShape: "denyFirst";
529
+ readonly minTrust: "global";
530
+ readonly tighten: {
531
+ readonly rule: "setNarrower";
532
+ };
533
+ readonly enforcement: "resolver";
534
+ readonly fail: "closed";
535
+ readonly seam: "http hook URL / header-env allowlists (httpHookUrlVerdict / httpHookEnvVarsAllowed)";
536
+ };
527
537
  };
528
538
  export type SafetyMergeCategory = keyof typeof SAFETY_MERGE_SPEC;
529
539
  /** The members a category declares, as a CLOSED SET TYPE: `"allow" | "deny" | "ask"` for `permissions`, `never` for
@@ -90,9 +90,9 @@ export function isAtLeast(layer, minTrust) {
90
90
  * that would otherwise ask/deny — a privilege, so it needs trust; `tighten` = only ever narrows — restricting is
91
91
  * safe from anyone, so any settings-file layer may add it. The word is what a consumer branches on for the
92
92
  * NOT-admitted outlet: the ruling REQUIRES a consumer to turn a `loosen` member below its floor into a pending
93
- * suggestion (consent flow) rather than drop it silently. This package returns that metadata only — it performs
94
- * no admission and no degradation; whether a given consumer version implements the outlet is that consumer's
95
- * contract. */
93
+ * suggestion (consent flow) rather than drop it silently. This package returns that metadata, and `layerAdmission`
94
+ * (`settings-sources.ts`) reads it per settings layer; it performs no degradation — whether a given consumer version
95
+ * implements the outlet is that consumer's contract. */
96
96
  export const MemberDirection = z.enum(["loosen", "tighten"]);
97
97
  /** One member's admission row. `minTrust` ABSENT ⇒ the category's `minTrust` applies: a row can refine the floor
98
98
  * only by SAYING so; omission never lowers it (fail-closed by construction).
@@ -232,6 +232,13 @@ export const SAFETY_MERGE_SPEC = {
232
232
  autoCompaction: { mergeShape: "replace", minTrust: "session", tighten: { rule: "none" }, enforcement: "client", fail: "open", seam: "compaction config" },
233
233
  ui: { mergeShape: "replace", minTrust: "local", tighten: { rule: "none" }, enforcement: "client", fail: "open", seam: "shell-only (theme/keybindings/statusLine) — never touches the engine" },
234
234
  org: { mergeShape: "replace", minTrust: "managed", tighten: { rule: "none" }, enforcement: "server", fail: "closed", seam: "web-admin → governanceBaseline (SSO/SCIM/audit/budget, TOB-only)" },
235
+ // 13.0.0 (U3 [10538]) — the http hook allowlists `allowedHttpHookUrls` / `httpHookAllowedEnvVars`. Every ADMITTED layer's list must
236
+ // pass (layers INTERSECT: a lower layer can only narrow, never add a URL the managed list lacks — CC concatenates, which lets it);
237
+ // admitted = `layerAdmission(source, workspace, "httpHookAllowlists") === "admitted"` — the source's tier (SETTINGS_SOURCE_CONTRACT) reaches
238
+ // this floor, global (user / flag / managed) — because a repo-controllable `[]` would silence the user's own http GUARD hooks — a
239
+ // tightening word with a loosening consequence, the same boundary as sandboxNetwork.failIfUnavailable. Read and judged by
240
+ // ./http-hook-allowlists (`httpHookUrlVerdict` / `httpHookEnvVarsAllowed`); not `sandboxNetwork` (that row is enforcement:"gate").
241
+ httpHookAllowlists: { mergeShape: "denyFirst", minTrust: "global", tighten: { rule: "setNarrower" }, enforcement: "resolver", fail: "closed", seam: "http hook URL / header-env allowlists (httpHookUrlVerdict / httpHookEnvVarsAllowed)" },
235
242
  };
236
243
  /** Own-property table lookup. The category union is compile-time only; a JavaScript or deserialized caller can
237
244
  * hand in `"__proto__"` / `"constructor"` / `"toString"`, which a bare index resolves through Object.prototype
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Settings SOURCES and the ONE layer-admission function.
3
+ *
4
+ * `SAFETY_MERGE_SPEC` answers "how trusted must a layer be to set this category / member, and which way does it move the unattended
5
+ * surface" (floor + direction); `trustRank` / `isAtLeast` are the ladder. What was missing is the LAYER side: which settings sources
6
+ * there are, which trust tier each one carries, which of them a repository controls, and what an untrusted workspace does to them —
7
+ * every host kept its own table for that. This file is that table, plus the one function a resolver asks per layer × key.
8
+ *
9
+ * THE RULE (one, no per-key special case): a repository-controllable source (`projectSettings` / `localSettings` — a clone brings or the
10
+ * repository's tooling writes them) in a workspace whose trust reading is not `accepted` admits only the members whose published
11
+ * direction is `"tighten"`; everything it may otherwise set is `trust_withheld`. In every case the source's tier must reach the floor
12
+ * (`isAtLeast(tier, floor)`), checked first — a write below its floor is `below_trust_floor` whether or not the workspace is trusted, which
13
+ * also keeps the loose pole of a value-directional (`boolPole`) member out of an untrusted layer. The `direction` data already says which
14
+ * keys an untrusted repository may still set (restricting needs no trust); there is no per-row workspace column (that would copy a
15
+ * layer fact into every category row).
16
+ *
17
+ * CC 2.1.296 (the reference): trust is one per-session boolean (a non-interactive session counts as trusted, `cli296.js:238351`); keys split
18
+ * into the operator tier `policy / flag / user` and the repository-controllable tier `project / local` ("projectSettings and
19
+ * localSettings are repo-controllable", `:237757`); merge order user < project < local < flag < policy (`:237339`).
20
+ * The local layer sits in the project tier here too: a `.local` settings file is gitignored, so a clone cannot bring one, and whoever can
21
+ * write it can write the project file next to it — "local below project" was never a written rule (it arrived with the first import of the
22
+ * table) and holds no threat model. The category rows (`hooks` / `env` / `mcpServers` / `rulesInstructions` at `minTrust: "project"`) and
23
+ * the ladder numbers are untouched: the change is on the source's tier, not on any row.
24
+ *
25
+ * NOT here (each has its own place): the hooks EXECUTION gate (`declined` ⇒ no hook from any layer runs) is contract text in `./hooks`'
26
+ * header and executes in the client core; the workspace trust RECORD (where "this folder was accepted" is stored) is host state.
27
+ */
28
+ import { type MinTrust, type SafetyMergeCategory, type SafetyMergeMember } from "./safety-merge-spec.js";
29
+ /** The settings sources, in MERGE order (a later source overrides an earlier one where a category's merge shape lets it):
30
+ * user < project < local < flag < policy. `flagSettings` = settings handed in at launch (`--settings <file|json>`, an SDK embedder's inline
31
+ * settings); `policySettings` = managed. */
32
+ export declare const SETTINGS_SOURCES: readonly ["userSettings", "projectSettings", "localSettings", "flagSettings", "policySettings"];
33
+ export type SettingsSource = (typeof SETTINGS_SOURCES)[number];
34
+ /** One source's admission facts: its trust tier (a {@link MinTrust} word, compared with `isAtLeast` against a floor) and whether a
35
+ * repository controls it (⇒ subject to the workspace trust reading). */
36
+ export interface SettingsSourceContract {
37
+ readonly trust: MinTrust;
38
+ readonly repoControllable: boolean;
39
+ }
40
+ /**
41
+ * The source table. `flagSettings` is in the operator tier (`global`, not repository-controllable) like CC's: settings handed in at launch are
42
+ * the launcher's own act — an SDK embedder or a wrapper script — not something a repository brings (a wrapper that turns repository content
43
+ * into `--settings` makes that choice itself).
44
+ */
45
+ export declare const SETTINGS_SOURCE_CONTRACT: {
46
+ readonly userSettings: {
47
+ readonly trust: "global";
48
+ readonly repoControllable: false;
49
+ };
50
+ readonly projectSettings: {
51
+ readonly trust: "project";
52
+ readonly repoControllable: true;
53
+ };
54
+ readonly localSettings: {
55
+ readonly trust: "project";
56
+ readonly repoControllable: true;
57
+ };
58
+ readonly flagSettings: {
59
+ readonly trust: "global";
60
+ readonly repoControllable: false;
61
+ };
62
+ readonly policySettings: {
63
+ readonly trust: "managed";
64
+ readonly repoControllable: false;
65
+ };
66
+ };
67
+ /**
68
+ * The ONE lookup of {@link SETTINGS_SOURCE_CONTRACT}: a source's tier and whether a repository controls it. Every function in this package
69
+ * that needs a layer's tier reads it here ({@link layerAdmission}, the http hook allowlist judge, the MCP server list verdict) — none takes a
70
+ * tier from its caller. An unknown source (prototype names included) ⇒ RangeError, never a fallback tier.
71
+ */
72
+ export declare function settingsSourceContract(source: SettingsSource): SettingsSourceContract;
73
+ /**
74
+ * The host's reading of the workspace trust decision: `accepted` (the user accepted this folder), `declined` (the user did not, or the
75
+ * host must assume so), `no_trust_step` (the host has no trust step at all). A host whose reading throws or yields a word outside this set
76
+ * passes `declined`; this package's functions refuse an unknown word loudly rather than reading it as any tier.
77
+ */
78
+ export declare const WORKSPACE_TRUST_STATES: readonly ["accepted", "declined", "no_trust_step"];
79
+ export type WorkspaceTrustState = (typeof WORKSPACE_TRUST_STATES)[number];
80
+ /** What {@link layerAdmission} answers (closed): `admitted`; `trust_withheld` = would be admitted once the workspace is `accepted`;
81
+ * `below_trust_floor` = this source can never set it (its tier is below the floor). */
82
+ export declare const LAYER_ADMISSION_VERDICTS: readonly ["admitted", "trust_withheld", "below_trust_floor"];
83
+ export type LayerAdmissionVerdict = (typeof LAYER_ADMISSION_VERDICTS)[number];
84
+ /**
85
+ * Does a value from `source` for `category` (or one of its declared `member`s; `value` = the candidate value, for a value-directional member)
86
+ * enter the effective settings, given the workspace trust reading? The rule is in the file header; the data is {@link SETTINGS_SOURCE_CONTRACT}
87
+ * and `SAFETY_MERGE_SPEC` (floors and directions through `memberAdmission`). Omitting `member` asks about the category as a whole key (a
88
+ * memberized category's undeclared sub-keys are asked this way: category floor, no direction).
89
+ * Unknown source / trust reading / category, a member named on a whole-key category, or an undeclared member ⇒ RangeError (never a fallback).
90
+ */
91
+ export declare function layerAdmission<C extends SafetyMergeCategory>(source: SettingsSource, workspace: WorkspaceTrustState, category: C, member?: SafetyMergeMember<C>, value?: unknown): LayerAdmissionVerdict;
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Settings SOURCES and the ONE layer-admission function.
3
+ *
4
+ * `SAFETY_MERGE_SPEC` answers "how trusted must a layer be to set this category / member, and which way does it move the unattended
5
+ * surface" (floor + direction); `trustRank` / `isAtLeast` are the ladder. What was missing is the LAYER side: which settings sources
6
+ * there are, which trust tier each one carries, which of them a repository controls, and what an untrusted workspace does to them —
7
+ * every host kept its own table for that. This file is that table, plus the one function a resolver asks per layer × key.
8
+ *
9
+ * THE RULE (one, no per-key special case): a repository-controllable source (`projectSettings` / `localSettings` — a clone brings or the
10
+ * repository's tooling writes them) in a workspace whose trust reading is not `accepted` admits only the members whose published
11
+ * direction is `"tighten"`; everything it may otherwise set is `trust_withheld`. In every case the source's tier must reach the floor
12
+ * (`isAtLeast(tier, floor)`), checked first — a write below its floor is `below_trust_floor` whether or not the workspace is trusted, which
13
+ * also keeps the loose pole of a value-directional (`boolPole`) member out of an untrusted layer. The `direction` data already says which
14
+ * keys an untrusted repository may still set (restricting needs no trust); there is no per-row workspace column (that would copy a
15
+ * layer fact into every category row).
16
+ *
17
+ * CC 2.1.296 (the reference): trust is one per-session boolean (a non-interactive session counts as trusted, `cli296.js:238351`); keys split
18
+ * into the operator tier `policy / flag / user` and the repository-controllable tier `project / local` ("projectSettings and
19
+ * localSettings are repo-controllable", `:237757`); merge order user < project < local < flag < policy (`:237339`).
20
+ * The local layer sits in the project tier here too: a `.local` settings file is gitignored, so a clone cannot bring one, and whoever can
21
+ * write it can write the project file next to it — "local below project" was never a written rule (it arrived with the first import of the
22
+ * table) and holds no threat model. The category rows (`hooks` / `env` / `mcpServers` / `rulesInstructions` at `minTrust: "project"`) and
23
+ * the ladder numbers are untouched: the change is on the source's tier, not on any row.
24
+ *
25
+ * NOT here (each has its own place): the hooks EXECUTION gate (`declined` ⇒ no hook from any layer runs) is contract text in `./hooks`'
26
+ * header and executes in the client core; the workspace trust RECORD (where "this folder was accepted" is stored) is host state.
27
+ */
28
+ import { SAFETY_MERGE_SPEC, isAtLeast, memberAdmission } from "./safety-merge-spec.js";
29
+ /** The settings sources, in MERGE order (a later source overrides an earlier one where a category's merge shape lets it):
30
+ * user < project < local < flag < policy. `flagSettings` = settings handed in at launch (`--settings <file|json>`, an SDK embedder's inline
31
+ * settings); `policySettings` = managed. */
32
+ export const SETTINGS_SOURCES = ["userSettings", "projectSettings", "localSettings", "flagSettings", "policySettings"];
33
+ /**
34
+ * The source table. `flagSettings` is in the operator tier (`global`, not repository-controllable) like CC's: settings handed in at launch are
35
+ * the launcher's own act — an SDK embedder or a wrapper script — not something a repository brings (a wrapper that turns repository content
36
+ * into `--settings` makes that choice itself).
37
+ */
38
+ export const SETTINGS_SOURCE_CONTRACT = {
39
+ userSettings: { trust: "global", repoControllable: false },
40
+ projectSettings: { trust: "project", repoControllable: true },
41
+ localSettings: { trust: "project", repoControllable: true },
42
+ flagSettings: { trust: "global", repoControllable: false },
43
+ policySettings: { trust: "managed", repoControllable: false },
44
+ };
45
+ /**
46
+ * The ONE lookup of {@link SETTINGS_SOURCE_CONTRACT}: a source's tier and whether a repository controls it. Every function in this package
47
+ * that needs a layer's tier reads it here ({@link layerAdmission}, the http hook allowlist judge, the MCP server list verdict) — none takes a
48
+ * tier from its caller. An unknown source (prototype names included) ⇒ RangeError, never a fallback tier.
49
+ */
50
+ export function settingsSourceContract(source) {
51
+ if (!Object.hasOwn(SETTINGS_SOURCE_CONTRACT, source)) {
52
+ throw new RangeError(`SETTINGS_SOURCE_CONTRACT: unknown settings source '${String(source)}' (sources: ${SETTINGS_SOURCES.join(", ")})`);
53
+ }
54
+ return SETTINGS_SOURCE_CONTRACT[source];
55
+ }
56
+ /**
57
+ * The host's reading of the workspace trust decision: `accepted` (the user accepted this folder), `declined` (the user did not, or the
58
+ * host must assume so), `no_trust_step` (the host has no trust step at all). A host whose reading throws or yields a word outside this set
59
+ * passes `declined`; this package's functions refuse an unknown word loudly rather than reading it as any tier.
60
+ */
61
+ export const WORKSPACE_TRUST_STATES = ["accepted", "declined", "no_trust_step"];
62
+ /** What {@link layerAdmission} answers (closed): `admitted`; `trust_withheld` = would be admitted once the workspace is `accepted`;
63
+ * `below_trust_floor` = this source can never set it (its tier is below the floor). */
64
+ export const LAYER_ADMISSION_VERDICTS = ["admitted", "trust_withheld", "below_trust_floor"];
65
+ /**
66
+ * Does a value from `source` for `category` (or one of its declared `member`s; `value` = the candidate value, for a value-directional member)
67
+ * enter the effective settings, given the workspace trust reading? The rule is in the file header; the data is {@link SETTINGS_SOURCE_CONTRACT}
68
+ * and `SAFETY_MERGE_SPEC` (floors and directions through `memberAdmission`). Omitting `member` asks about the category as a whole key (a
69
+ * memberized category's undeclared sub-keys are asked this way: category floor, no direction).
70
+ * Unknown source / trust reading / category, a member named on a whole-key category, or an undeclared member ⇒ RangeError (never a fallback).
71
+ */
72
+ export function layerAdmission(source, workspace, category, member, value) {
73
+ const layer = settingsSourceContract(source);
74
+ const trusted = workspaceAccepted(workspace);
75
+ let floor;
76
+ let direction;
77
+ if (member === undefined) {
78
+ if (!Object.hasOwn(SAFETY_MERGE_SPEC, category)) {
79
+ throw new RangeError(`SAFETY_MERGE_SPEC: unknown category '${String(category)}' (categories: ${Object.keys(SAFETY_MERGE_SPEC).join(", ")})`);
80
+ }
81
+ floor = SAFETY_MERGE_SPEC[category].minTrust;
82
+ }
83
+ else {
84
+ const resolved = memberAdmission(category, member, value);
85
+ floor = resolved.minTrust;
86
+ direction = resolved.direction;
87
+ }
88
+ if (!isAtLeast(layer.trust, floor))
89
+ return "below_trust_floor";
90
+ if (layer.repoControllable && !trusted && direction !== "tighten")
91
+ return "trust_withheld";
92
+ return "admitted";
93
+ }
94
+ /** `accepted` ⇒ true; `declined` / `no_trust_step` ⇒ false (they differ only at the hooks execution gate, not here); anything else ⇒ RangeError. */
95
+ function workspaceAccepted(workspace) {
96
+ switch (workspace) {
97
+ case "accepted":
98
+ return true;
99
+ case "declined":
100
+ case "no_trust_step":
101
+ return false;
102
+ default: {
103
+ const unknown = workspace;
104
+ throw new RangeError(`WORKSPACE_TRUST_STATES: unknown workspace trust reading '${String(unknown)}' (readings: ${WORKSPACE_TRUST_STATES.join(", ")})`);
105
+ }
106
+ }
107
+ }
package/dist/types.d.ts CHANGED
@@ -179,8 +179,8 @@ export declare const ModelEntry: z.ZodObject<{
179
179
  standard: "standard";
180
180
  premium: "premium";
181
181
  }>>;
182
- reasoning: z.ZodDefault<z.ZodBoolean>;
183
- vision: z.ZodDefault<z.ZodBoolean>;
182
+ reasoning: z.ZodOptional<z.ZodBoolean>;
183
+ vision: z.ZodOptional<z.ZodBoolean>;
184
184
  contextWindow: z.ZodOptional<z.ZodNumber>;
185
185
  maxTokens: z.ZodOptional<z.ZodNumber>;
186
186
  autoCompactTokens: z.ZodOptional<z.ZodNumber>;
@@ -292,8 +292,8 @@ export declare const ModelsConfig: z.ZodObject<{
292
292
  standard: "standard";
293
293
  premium: "premium";
294
294
  }>>;
295
- reasoning: z.ZodDefault<z.ZodBoolean>;
296
- vision: z.ZodDefault<z.ZodBoolean>;
295
+ reasoning: z.ZodOptional<z.ZodBoolean>;
296
+ vision: z.ZodOptional<z.ZodBoolean>;
297
297
  contextWindow: z.ZodOptional<z.ZodNumber>;
298
298
  maxTokens: z.ZodOptional<z.ZodNumber>;
299
299
  autoCompactTokens: z.ZodOptional<z.ZodNumber>;
@@ -2311,8 +2311,8 @@ export declare const DOMAIN_SCHEMAS: {
2311
2311
  standard: "standard";
2312
2312
  premium: "premium";
2313
2313
  }>>;
2314
- reasoning: z.ZodDefault<z.ZodBoolean>;
2315
- vision: z.ZodDefault<z.ZodBoolean>;
2314
+ reasoning: z.ZodOptional<z.ZodBoolean>;
2315
+ vision: z.ZodOptional<z.ZodBoolean>;
2316
2316
  contextWindow: z.ZodOptional<z.ZodNumber>;
2317
2317
  maxTokens: z.ZodOptional<z.ZodNumber>;
2318
2318
  autoCompactTokens: z.ZodOptional<z.ZodNumber>;
package/dist/types.js CHANGED
@@ -422,8 +422,8 @@ export const PARSE_TRANSPARENT_KEYS = [
422
422
  domain: "mcp",
423
423
  path: "servers[].toolAxes",
424
424
  since: "12.2.0",
425
- validatedBy: "consumer",
426
- why: "连接器条目声明的按工具轴覆盖层;轴的词表与形归投影它的消费方(server 两腿进 core spec)与写它的连接器,本包只承载,复述那张表必漂。",
425
+ validatedBy: "engine-ingest",
426
+ why: "连接器条目声明的按工具轴覆盖层;轴的词表与形只有一只判官 = 引擎根导出 `readMcpToolAxes`(整声明一坏全拒、不默认),投影它的消费方(server 两腿)与写它的连接器都调它,本包只承载 —— 这里再写一份 zod 就是第二只判官,必漂。",
427
427
  },
428
428
  {
429
429
  domain: "mcp",
@@ -474,8 +474,11 @@ export const ModelEntry = z.object({
474
474
  sealedApiKey: SealedApiKey.optional(),
475
475
  /** Price/capability tier — fuels core 1.25 `selectModel({ tier })` role resolution. */
476
476
  tier: ModelTier.default("standard"),
477
- reasoning: z.boolean().default(false),
478
- vision: z.boolean().default(false),
477
+ /** Capability bits are THREE-STATE (S-1625 / C-R484): `true` = declared capable, `false` = declared not capable, ABSENT =
478
+ * not declared — the reader applies its own default (the engine reads it through one predicate). This package never fills an
479
+ * absent bit in: reading "not written" as "not capable" is a default only a reader may choose. */
480
+ reasoning: z.boolean().optional(),
481
+ vision: z.boolean().optional(),
479
482
  contextWindow: INTEGER.positive().optional(),
480
483
  maxTokens: INTEGER.positive().optional(),
481
484
  /** Auto-compaction trigger threshold (tokens) for this model — web-admin 显式下发时本地 FileConfigStore
@@ -2253,7 +2256,8 @@ export const HostEntry = z.object({
2253
2256
  // .max(HOST_NAME_MAX=63) = DNS-label bound (E2 F3③ 挂账收账, 0.8.0): web-admin-side probe clamp stays as双保险.
2254
2257
  name: z.string().min(1).max(HOST_NAME_MAX, `host name longer than ${HOST_NAME_MAX} characters (a DNS label) — ${HOSTS_BOUND_SIGNPOST}`).regex(/^[a-z0-9][a-z0-9-]*$/, "lowercase letters/digits/-, no spaces"),
2255
2258
  /** 待批准入编状态机(§2-3):自注册/新登记默认 false = 待批,不参与自动排布;admin 批准(走 publish)→
2256
- * true 参排(enforcement 随 E2)。既有 worker 不受影响(reconciler 仍按具体 host 认领)。 */
2259
+ * true 参排(enforcement 随 E2)。只管**自动排布**:`placement.auto:false` 的 worker 的 `host` 是钉,钉在哪台由运维写定、与这里无关;
2260
+ * `auto:true` 的 worker 只会被排到已批准的 host 上(钉 / 提示按 {@link WorkerSpec} 的 `placement.auto` 判)。 */
2257
2261
  approved: z.boolean().default(false),
2258
2262
  /** 调度标签(如 "gpu"/"region-us"),worker 的 `placement.requires` 据此匹配。 */
2259
2263
  labels: z.array(z.string().min(1)).prefault([]),
@@ -0,0 +1,40 @@
1
+ /**
2
+ * 界面语言设置片段(13.0.0,S-1592;client-core [10575] §三′ 请托)—— 壳 / 桌面 / web 自己界面(chrome)的语言,**不是**模型回答用的语言
3
+ * (CC 的 `language` 键是后者;两个事实不共用一个键)。形照 `cross-session.ts` 的片段契约:导出值闭集 + 一层的读器。纯、浏览器安全。
4
+ *
5
+ * - 键 `semaUiLanguage`(沿用终端已写的键,cli `src/sema/i18n/languageStore.ts`,已存值零迁移);作用源 = 用户设置;合并类 = `ui`
6
+ * (`SAFETY_MERGE_SPEC.ui`,`enforcement: "client"`,壳侧、不进引擎)。
7
+ * - 合法值闭集 {@link UI_LANGUAGE_VALUES} = `en` / `zh`。
8
+ * - 缺席或不合法 ⇒ 跟宿主 locale。读器**不猜**:不把 `zh-CN` 折成 `zh`、不按大小写折 —— locale 标签到这两个词的映射是宿主读 locale 时的事,
9
+ * 不是读设置时的事。
10
+ */
11
+ import { z } from "zod";
12
+ import type { SettingLayerRead } from "./cross-session.js";
13
+ /** 合法值闭集(声明序)。 */
14
+ export declare const UI_LANGUAGE_VALUES: readonly ["en", "zh"];
15
+ export type UiLanguageValue = (typeof UI_LANGUAGE_VALUES)[number];
16
+ /** 这把键的声明:键名 / 值域 / 合并类 / 作用源 —— 三端读写键与值域都取这里。 */
17
+ export declare const UI_LANGUAGE_SETTING: {
18
+ readonly key: "semaUiLanguage";
19
+ readonly values: readonly ["en", "zh"];
20
+ readonly category: "ui";
21
+ readonly source: "userSettings";
22
+ };
23
+ /** 值的形门(写侧用:写进设置文件的只能是这两个词之一)。 */
24
+ export declare const UiLanguageSetting: z.ZodEnum<{
25
+ en: "en";
26
+ zh: "zh";
27
+ }>;
28
+ /** 整份 `settings.json` 里本片段负责的那一键;刻意 `passthrough`(本片段不是整份设置的闭集门)。 */
29
+ export declare const UiLanguageSettings: z.ZodObject<{
30
+ semaUiLanguage: z.ZodOptional<z.ZodEnum<{
31
+ en: "en";
32
+ zh: "zh";
33
+ }>>;
34
+ }, z.core.$loose>;
35
+ export type UiLanguageSettings = z.infer<typeof UiLanguageSettings>;
36
+ /**
37
+ * 读一层的 `semaUiLanguage` 原始值:合法 ⇒ `{ ok: true, value }`;缺席 / `null` ⇒ `{ ok: true }`(未设);其余一切 ⇒ `{ ok: false }`(存的值
38
+ * 不是两种之一,设置面可据此说一句)。两种「无值」消费方都落到宿主 locale;读器从不猜一个值。
39
+ */
40
+ export declare function readUiLanguage(raw: unknown): SettingLayerRead<UiLanguageValue>;
@@ -0,0 +1,37 @@
1
+ /**
2
+ * 界面语言设置片段(13.0.0,S-1592;client-core [10575] §三′ 请托)—— 壳 / 桌面 / web 自己界面(chrome)的语言,**不是**模型回答用的语言
3
+ * (CC 的 `language` 键是后者;两个事实不共用一个键)。形照 `cross-session.ts` 的片段契约:导出值闭集 + 一层的读器。纯、浏览器安全。
4
+ *
5
+ * - 键 `semaUiLanguage`(沿用终端已写的键,cli `src/sema/i18n/languageStore.ts`,已存值零迁移);作用源 = 用户设置;合并类 = `ui`
6
+ * (`SAFETY_MERGE_SPEC.ui`,`enforcement: "client"`,壳侧、不进引擎)。
7
+ * - 合法值闭集 {@link UI_LANGUAGE_VALUES} = `en` / `zh`。
8
+ * - 缺席或不合法 ⇒ 跟宿主 locale。读器**不猜**:不把 `zh-CN` 折成 `zh`、不按大小写折 —— locale 标签到这两个词的映射是宿主读 locale 时的事,
9
+ * 不是读设置时的事。
10
+ */
11
+ import { z } from "zod";
12
+ /** 合法值闭集(声明序)。 */
13
+ export const UI_LANGUAGE_VALUES = ["en", "zh"];
14
+ /** 这把键的声明:键名 / 值域 / 合并类 / 作用源 —— 三端读写键与值域都取这里。 */
15
+ export const UI_LANGUAGE_SETTING = {
16
+ key: "semaUiLanguage",
17
+ values: UI_LANGUAGE_VALUES,
18
+ category: "ui",
19
+ source: "userSettings",
20
+ };
21
+ /** 值的形门(写侧用:写进设置文件的只能是这两个词之一)。 */
22
+ export const UiLanguageSetting = z.enum(UI_LANGUAGE_VALUES);
23
+ /** 整份 `settings.json` 里本片段负责的那一键;刻意 `passthrough`(本片段不是整份设置的闭集门)。 */
24
+ export const UiLanguageSettings = z
25
+ .object({
26
+ [UI_LANGUAGE_SETTING.key]: UiLanguageSetting.optional(),
27
+ })
28
+ .passthrough();
29
+ /**
30
+ * 读一层的 `semaUiLanguage` 原始值:合法 ⇒ `{ ok: true, value }`;缺席 / `null` ⇒ `{ ok: true }`(未设);其余一切 ⇒ `{ ok: false }`(存的值
31
+ * 不是两种之一,设置面可据此说一句)。两种「无值」消费方都落到宿主 locale;读器从不猜一个值。
32
+ */
33
+ export function readUiLanguage(raw) {
34
+ if (raw === undefined || raw === null)
35
+ return { ok: true };
36
+ return typeof raw === "string" && UI_LANGUAGE_VALUES.includes(raw) ? { ok: true, value: raw } : { ok: false };
37
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * URL 名单里一条模式对一个 URL 的**唯一判法**(13.0.0,U3 [10538] / S-1597)。管理员写的 URL 名单 —— http hook `allowedHttpHookUrls`
3
+ * ({@link ./http-hook-allowlists})、MCP `allowedMcpServers` / `deniedMcpServers` 的 `serverUrl` 条目({@link ./mcp-server-policy})——
4
+ * 都读这一只。纯函数,全局只用 `URL`,浏览器安全。
5
+ *
6
+ * 一条律,两只出口:**名单读不准的地方一律判向「拦」**。放行出口在含糊处不命中(命中面 ⊆ 上游),拒绝出口在含糊处命中(命中面 ⊇ 上游)。
7
+ *
8
+ * 组件判法(上游同形:CC 2.1.296 `g9`,cli296.js:249195;上游两张名单都调它):
9
+ * - 模式恰为 `*` ⇒ 一切命中;URL 自身解析不了 ⇒ 不命中。
10
+ * - 模式里的 `*` 换成占位记号后按 URL 解析;解析不了且端口位是 `*` ⇒ 端口位换 0 再解析,并记「端口不判」。
11
+ * - 协议:相等,或模式协议整段是 `*`。主机名:两边去尾点、小写后按 glob 比。
12
+ * - 端口:模式没写端口且主机名带 `*` ⇒ 不判;否则逐字相等(协议缺省端口两边都读成空)。
13
+ * - 路径:模式路径是 `/` 或空、没有查询串、且模式原文不以 `/` 结尾 ⇒ 任意路径命中;否则「路径 + 查询串」按 glob 比。片段、用户信息不比。
14
+ * - 模式解析不出 URL(或特殊协议的主机名里出现带通配的 punycode 标签)⇒ 回落成按 `/` 分段的 glob(段数相等、逐段比,`*` 跨不过 `/`)。
15
+ * glob 只认 `*`(任意长度任意字符,含空),其余逐字、区分大小写。
16
+ *
17
+ * 放行出口比上游严(上游四洞;登本包 docs/CC-DIVERGENCE.md,与 cli 偏离登记 D-025 / D-026 / D-028 / D-029 同形):
18
+ * - D-025 模式写了端口(哪怕是协议缺省端口,解析后读成空)⇒ 主机名带 `*` 也照判端口(上游把写明的 `:443` 当没写、放行任意端口)。
19
+ * 「写没写」由解析器自己答:同一串换成缺省端口不同的同族协议再解析,端口不再读成空 ⇒ 写了。
20
+ * - D-026 目标协议不在「主机即连接目标」的四种(http / https / ws / wss)⇒ 模式必须写出那个协议,`*` 协议不通配它;`ws+unix:` 的连接目标
21
+ * 是路径里第一个 `:` 之前的套接字路径 ⇒ 套接字段只认逐字相等(带 `*` 即不放行任何套接字 —— 套接字路径到连接层还会被点段 / 反斜杠 /
22
+ * 长度截断改写,glob 判不住真实目标);`:` 之后的请求路径照 glob;没写路径的模式、套接字段为空或含 `..` 段的目标一律不放行。
23
+ * - D-028 模式解析不出 URL 形(走分段回落)⇒ 不放行:回落把主机与端口拼成一段比,`*` 能跨过两者的边界。
24
+ * - D-029 解析后主机名里出现不是占位来的 `*`(`%2a` / 全角 `*` 被解析器解码、归一成的字面 `*`)⇒ 不放行:那是字面字符,不是通配。
25
+ * 拒绝出口比上游宽一处(D-027):另按整串 glob 再判一次,既有的无协议 / 跨组件写法不因按组件判而失效。
26
+ *
27
+ * 占位记号(与上游 / cli 不同的一处实现):上游用每进程随机串;本包要纯、要确定 ⇒ 按输入选一个记号 —— 它在模式与目标的任何解析读法里
28
+ * (原文 / 百分号解码 / IDNA 近似映射)都不出现,放进模式后也只在放进去的那几处出现(不与相邻字面拼出第二份)。
29
+ */
30
+ /** 放行名单:`url` 是否被模式 `pattern` 放行(判法见文件头;命中面 ⊆ 上游)。 */
31
+ export declare function urlAllowPatternMatches(url: string, pattern: string): boolean;
32
+ /** 拒绝名单:`url` 是否被模式 `pattern` 拦下(上游组件判法,或整串 glob;命中面 ⊇ 上游)。 */
33
+ export declare function urlDenyPatternMatches(url: string, pattern: string): boolean;