@sema-agent/settings-schema 12.2.1 → 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 (57) hide show
  1. package/CHANGELOG.md +223 -0
  2. package/README.md +41 -6
  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/c76-projects-entry-salvage-nested/expected.json +142 -0
  26. package/conformance/cases/c76-projects-entry-salvage-nested/input.json +54 -0
  27. package/conformance/cases/c76-projects-entry-salvage-nested/meta.json +6 -0
  28. package/conformance/cases/cc03-model-compat-unknown-subkey/expected.json +2 -6
  29. package/conformance/cases/cli01-model-name-nfc-vs-nfd/expected.json +1 -3
  30. package/conformance/cases/cli02-control-and-zero-width-chars/expected.json +1 -3
  31. package/conformance/cases/cli05-long-and-prefix-colliding-names/expected.json +6 -18
  32. package/conformance/cases/t01-absent-domain-key-not-null/expected.json +1 -3
  33. package/conformance/cases/t03-prototype-name-key-not-swallowed/expected.json +2 -6
  34. package/conformance/cases/t04-secret-literal-never-in-warning-value/expected.json +1 -3
  35. package/dist/config-fns.d.ts +94 -8
  36. package/dist/config-fns.js +254 -116
  37. package/dist/hooks-config.d.ts +235 -0
  38. package/dist/hooks-config.js +458 -0
  39. package/dist/hooks.d.ts +16 -154
  40. package/dist/hooks.js +17 -111
  41. package/dist/http-hook-allowlists.d.ts +63 -0
  42. package/dist/http-hook-allowlists.js +73 -0
  43. package/dist/index.d.ts +5 -0
  44. package/dist/index.js +6 -1
  45. package/dist/mcp-server-policy.d.ts +75 -0
  46. package/dist/mcp-server-policy.js +84 -0
  47. package/dist/safety-merge-spec.d.ts +13 -3
  48. package/dist/safety-merge-spec.js +10 -3
  49. package/dist/settings-sources.d.ts +91 -0
  50. package/dist/settings-sources.js +107 -0
  51. package/dist/types.d.ts +6 -6
  52. package/dist/types.js +9 -5
  53. package/dist/ui-language.d.ts +40 -0
  54. package/dist/ui-language.js +37 -0
  55. package/dist/url-pattern.d.ts +33 -0
  56. package/dist/url-pattern.js +239 -0
  57. package/package.json +1 -1
package/dist/hooks.d.ts CHANGED
@@ -13,7 +13,7 @@
13
13
  * discovering it the hard way (the 1.3.0 / 1.6.0 / 1.8.0 / 2.0.0 pattern).
14
14
  *
15
15
  * CONSUMERS
16
- * - service (the worker server): `TaskRequest.settings.hooks` wire face = `HooksConfig` below; the worker-side
16
+ * - service (the worker server): `TaskRequest.settings.hooks` wire face = `HooksConfig` (`./hooks-config`); the worker-side
17
17
  * hook-runner translates matching entries into core `TaskSpec.hooks` in-process callbacks. Single-user
18
18
  * gate first (requirePrincipal semantics, same as MCP injection ).
19
19
  * - core (engine): per-event input/output shapes for the engine-owned events; `stoppedBy` on settle.
@@ -26,11 +26,21 @@
26
26
  * only from their owner; the shell must NOT double-fire engine-owned events. Configuring an event whose
27
27
  * owner hasn't shipped its runner yet is LEGAL (no validation error) — it fires when that half ships.
28
28
  *
29
+ * CONFIG SURFACE: what a settings document's `hooks` key may carry — the event vocabulary {@link HOOK_EVENTS}, the entry types, the ONE
30
+ * classifier `classifyHooksConfig` and the zod faces that read it (`HooksConfig` / `HookMatcherEntry` / `HookEntry` …) — is defined in
31
+ * `./hooks-config` and re-exported here whole; import it from here (or the main barrel) as before.
32
+ *
33
+ * WORKSPACE TRUST AND THE HOOKS EXECUTION GATE (contract; the gate executes in the client core, in one place — not in this package):
34
+ * when the host reports the workspace `declined` (`WORKSPACE_TRUST_STATES`), NO hook runs from ANY settings layer — the user, managed
35
+ * and flag layers included (CC 2.1.296: an untrusted session runs no hooks at all, `cli296.js:249597`). `no_trust_step` (the host has no
36
+ * trust step) differs from `declined` at this gate only: the gate stays open, and what applies is layer admission alone
37
+ * (`layerAdmission`: a repository-controllable layer's hooks are `trust_withheld` until the workspace is `accepted`). The gate is not
38
+ * layer admission: it lives in neither `SAFETY_MERGE_SPEC` nor `SETTINGS_SOURCE_CONTRACT`.
39
+ *
29
40
  * Pure zod contract — zero runtime deps beyond zod, browser-safe, same as the rest of this package.
30
41
  */
31
42
  import { z } from "zod";
32
- /** The full 33-event contract set = CC 198's 27 + the 6 post-198 events — the one exported closed set of hook event names. */
33
- export declare const HOOK_EVENTS: readonly ["PreToolUse", "PostToolUse", "PostToolUseFailure", "PostToolBatch", "Notification", "UserPromptSubmit", "UserPromptExpansion", "SessionStart", "SessionEnd", "Stop", "StopFailure", "SubagentStart", "SubagentStop", "PreCompact", "PostCompact", "PermissionRequest", "PermissionDenied", "Setup", "TeammateIdle", "TaskCreated", "TaskCompleted", "Elicitation", "ElicitationResult", "ConfigChange", "WorktreeCreate", "WorktreeRemove", "InstructionsLoaded", "CwdChanged", "FileChanged", "MessageDisplay", "DirectoryAdded", "PreModelSwitch", "PostModelSwitch"];
43
+ export * from "./hooks-config.js";
34
44
  export declare const HookEventName: z.ZodEnum<{
35
45
  PreToolUse: "PreToolUse";
36
46
  PostToolUse: "PostToolUse";
@@ -223,11 +233,11 @@ export type AssistantTurnError = z.infer<typeof AssistantTurnError>;
223
233
  /** SessionEnd `reason` values (CC EXIT_REASONS). */
224
234
  export declare const SESSION_EXIT_REASONS: readonly ["clear", "resume", "logout", "prompt_input_exit", "other", "bypass_permissions_disabled"];
225
235
  export declare const SessionExitReason: z.ZodEnum<{
236
+ other: "other";
226
237
  clear: "clear";
227
238
  resume: "resume";
228
239
  logout: "logout";
229
240
  prompt_input_exit: "prompt_input_exit";
230
- other: "other";
231
241
  bypass_permissions_disabled: "bypass_permissions_disabled";
232
242
  }>;
233
243
  export type SessionExitReason = z.infer<typeof SessionExitReason>;
@@ -372,11 +382,11 @@ export declare const SessionEndHookInput: z.ZodObject<{
372
382
  agent_type: z.ZodOptional<z.ZodString>;
373
383
  hook_event_name: z.ZodLiteral<"SessionEnd">;
374
384
  reason: z.ZodEnum<{
385
+ other: "other";
375
386
  clear: "clear";
376
387
  resume: "resume";
377
388
  logout: "logout";
378
389
  prompt_input_exit: "prompt_input_exit";
379
- other: "other";
380
390
  bypass_permissions_disabled: "bypass_permissions_disabled";
381
391
  }>;
382
392
  }, z.core.$strip>;
@@ -982,11 +992,11 @@ export declare const HookInput: z.ZodDiscriminatedUnion<[z.ZodObject<{
982
992
  agent_type: z.ZodOptional<z.ZodString>;
983
993
  hook_event_name: z.ZodLiteral<"SessionEnd">;
984
994
  reason: z.ZodEnum<{
995
+ other: "other";
985
996
  clear: "clear";
986
997
  resume: "resume";
987
998
  logout: "logout";
988
999
  prompt_input_exit: "prompt_input_exit";
989
- other: "other";
990
1000
  bypass_permissions_disabled: "bypass_permissions_disabled";
991
1001
  }>;
992
1002
  }, z.core.$strip>, z.ZodObject<{
@@ -2111,143 +2121,6 @@ export type HookOutput = z.infer<typeof HookOutput>;
2111
2121
  export declare const HOOK_INJECTED_ENV: readonly ["CLAUDE_PROJECT_DIR", "CLAUDE_ENV_FILE"];
2112
2122
  /** Default per-hook timeout when an entry does not set one (seconds; CC default). */
2113
2123
  export declare const DEFAULT_HOOK_TIMEOUT_SECONDS = 60;
2114
- /** `command` — shell command on the worker host (DESIGN POINT ②). First runner phase. */
2115
- export declare const CommandHookEntry: z.ZodObject<{
2116
- if: z.ZodOptional<z.ZodString>;
2117
- timeout: z.ZodOptional<z.ZodNumber>;
2118
- statusMessage: z.ZodOptional<z.ZodString>;
2119
- once: z.ZodOptional<z.ZodBoolean>;
2120
- type: z.ZodLiteral<"command">;
2121
- command: z.ZodString;
2122
- shell: z.ZodOptional<z.ZodEnum<{
2123
- bash: "bash";
2124
- powershell: "powershell";
2125
- }>>;
2126
- async: z.ZodOptional<z.ZodBoolean>;
2127
- asyncRewake: z.ZodOptional<z.ZodBoolean>;
2128
- }, z.core.$strip>;
2129
- /** `prompt` — one-shot LLM evaluation of the hook input ($ARGUMENTS = input JSON). */
2130
- export declare const PromptHookEntry: z.ZodObject<{
2131
- if: z.ZodOptional<z.ZodString>;
2132
- timeout: z.ZodOptional<z.ZodNumber>;
2133
- statusMessage: z.ZodOptional<z.ZodString>;
2134
- once: z.ZodOptional<z.ZodBoolean>;
2135
- type: z.ZodLiteral<"prompt">;
2136
- prompt: z.ZodString;
2137
- model: z.ZodOptional<z.ZodString>;
2138
- }, z.core.$strip>;
2139
- /** `agent` — agentic verifier loop over the hook input ($ARGUMENTS = input JSON). */
2140
- export declare const AgentHookEntry: z.ZodObject<{
2141
- if: z.ZodOptional<z.ZodString>;
2142
- timeout: z.ZodOptional<z.ZodNumber>;
2143
- statusMessage: z.ZodOptional<z.ZodString>;
2144
- once: z.ZodOptional<z.ZodBoolean>;
2145
- type: z.ZodLiteral<"agent">;
2146
- prompt: z.ZodString;
2147
- model: z.ZodOptional<z.ZodString>;
2148
- }, z.core.$strip>;
2149
- /** `http` — POST the hook input JSON to a URL. Headers may reference $NAMEs listed in allowedEnvVars
2150
- * (env-NAME interpolation only — the config NEVER carries a secret value; same boundary as the rest
2151
- * of this package). */
2152
- export declare const HttpHookEntry: z.ZodObject<{
2153
- if: z.ZodOptional<z.ZodString>;
2154
- timeout: z.ZodOptional<z.ZodNumber>;
2155
- statusMessage: z.ZodOptional<z.ZodString>;
2156
- once: z.ZodOptional<z.ZodBoolean>;
2157
- type: z.ZodLiteral<"http">;
2158
- url: z.ZodString;
2159
- headers: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
2160
- allowedEnvVars: z.ZodOptional<z.ZodArray<z.ZodString>>;
2161
- }, z.core.$strip>;
2162
- /** One configured hook (CC settings shape + shell superset fields; discriminated on `type`). */
2163
- export declare const HookEntry: z.ZodDiscriminatedUnion<[z.ZodObject<{
2164
- if: z.ZodOptional<z.ZodString>;
2165
- timeout: z.ZodOptional<z.ZodNumber>;
2166
- statusMessage: z.ZodOptional<z.ZodString>;
2167
- once: z.ZodOptional<z.ZodBoolean>;
2168
- type: z.ZodLiteral<"command">;
2169
- command: z.ZodString;
2170
- shell: z.ZodOptional<z.ZodEnum<{
2171
- bash: "bash";
2172
- powershell: "powershell";
2173
- }>>;
2174
- async: z.ZodOptional<z.ZodBoolean>;
2175
- asyncRewake: z.ZodOptional<z.ZodBoolean>;
2176
- }, z.core.$strip>, z.ZodObject<{
2177
- if: z.ZodOptional<z.ZodString>;
2178
- timeout: z.ZodOptional<z.ZodNumber>;
2179
- statusMessage: z.ZodOptional<z.ZodString>;
2180
- once: z.ZodOptional<z.ZodBoolean>;
2181
- type: z.ZodLiteral<"prompt">;
2182
- prompt: z.ZodString;
2183
- model: z.ZodOptional<z.ZodString>;
2184
- }, z.core.$strip>, z.ZodObject<{
2185
- if: z.ZodOptional<z.ZodString>;
2186
- timeout: z.ZodOptional<z.ZodNumber>;
2187
- statusMessage: z.ZodOptional<z.ZodString>;
2188
- once: z.ZodOptional<z.ZodBoolean>;
2189
- type: z.ZodLiteral<"agent">;
2190
- prompt: z.ZodString;
2191
- model: z.ZodOptional<z.ZodString>;
2192
- }, z.core.$strip>, z.ZodObject<{
2193
- if: z.ZodOptional<z.ZodString>;
2194
- timeout: z.ZodOptional<z.ZodNumber>;
2195
- statusMessage: z.ZodOptional<z.ZodString>;
2196
- once: z.ZodOptional<z.ZodBoolean>;
2197
- type: z.ZodLiteral<"http">;
2198
- url: z.ZodString;
2199
- headers: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
2200
- allowedEnvVars: z.ZodOptional<z.ZodArray<z.ZodString>>;
2201
- }, z.core.$strip>], "type">;
2202
- export type HookEntry = z.infer<typeof HookEntry>;
2203
- export type CommandHookEntry = z.infer<typeof CommandHookEntry>;
2204
- export type PromptHookEntry = z.infer<typeof PromptHookEntry>;
2205
- export type AgentHookEntry = z.infer<typeof AgentHookEntry>;
2206
- export type HttpHookEntry = z.infer<typeof HttpHookEntry>;
2207
- /** One matcher group: `matcher` (see HOOK_EVENT_MATCHER_FIELD) + the hooks it fires. */
2208
- export declare const HookMatcherEntry: z.ZodObject<{
2209
- matcher: z.ZodOptional<z.ZodString>;
2210
- hooks: z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
2211
- if: z.ZodOptional<z.ZodString>;
2212
- timeout: z.ZodOptional<z.ZodNumber>;
2213
- statusMessage: z.ZodOptional<z.ZodString>;
2214
- once: z.ZodOptional<z.ZodBoolean>;
2215
- type: z.ZodLiteral<"command">;
2216
- command: z.ZodString;
2217
- shell: z.ZodOptional<z.ZodEnum<{
2218
- bash: "bash";
2219
- powershell: "powershell";
2220
- }>>;
2221
- async: z.ZodOptional<z.ZodBoolean>;
2222
- asyncRewake: z.ZodOptional<z.ZodBoolean>;
2223
- }, z.core.$strip>, z.ZodObject<{
2224
- if: z.ZodOptional<z.ZodString>;
2225
- timeout: z.ZodOptional<z.ZodNumber>;
2226
- statusMessage: z.ZodOptional<z.ZodString>;
2227
- once: z.ZodOptional<z.ZodBoolean>;
2228
- type: z.ZodLiteral<"prompt">;
2229
- prompt: z.ZodString;
2230
- model: z.ZodOptional<z.ZodString>;
2231
- }, z.core.$strip>, z.ZodObject<{
2232
- if: z.ZodOptional<z.ZodString>;
2233
- timeout: z.ZodOptional<z.ZodNumber>;
2234
- statusMessage: z.ZodOptional<z.ZodString>;
2235
- once: z.ZodOptional<z.ZodBoolean>;
2236
- type: z.ZodLiteral<"agent">;
2237
- prompt: z.ZodString;
2238
- model: z.ZodOptional<z.ZodString>;
2239
- }, z.core.$strip>, z.ZodObject<{
2240
- if: z.ZodOptional<z.ZodString>;
2241
- timeout: z.ZodOptional<z.ZodNumber>;
2242
- statusMessage: z.ZodOptional<z.ZodString>;
2243
- once: z.ZodOptional<z.ZodBoolean>;
2244
- type: z.ZodLiteral<"http">;
2245
- url: z.ZodString;
2246
- headers: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
2247
- allowedEnvVars: z.ZodOptional<z.ZodArray<z.ZodString>>;
2248
- }, z.core.$strip>], "type">>;
2249
- }, z.core.$strip>;
2250
- export type HookMatcherEntry = z.infer<typeof HookMatcherEntry>;
2251
2124
  /**
2252
2125
  * Does `matcher` select `name`? —— **the one reading** of a hooks matcher (11.0.0). The shell and the server both ask this
2253
2126
  * function; neither keeps a copy.
@@ -2313,17 +2186,6 @@ export interface HookEnvInterpolation {
2313
2186
  * 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
2187
  */
2315
2188
  export declare function interpolateHookEnvVars(headers: Readonly<Record<string, string>>, allowedEnvVars: readonly string[], env: Readonly<Record<string, string | undefined>>): HookEnvInterpolation;
2316
- /**
2317
- * The hooks config record — the shape `TaskRequest.settings.hooks` carries on the wire and the shell
2318
- * reads from settings JSON: `{ [eventName]: [{ matcher?, hooks: [...] }] }`. Keys limited to the 33
2319
- * contract events (an unknown event name fails parse — catch typos at the wire); all keys optional.
2320
- *
2321
- * 🔴 The closed set is what makes a typo loud, and it is ALSO what makes falling behind CC expensive: an
2322
- * event this package has not adopted yet takes the WHOLE settings file down with it, not just its own key.
2323
- * That blast radius is why {@link HOOK_EVENTS} now has an upstream drift gate rather than a human promise.
2324
- */
2325
- export type HooksConfig = Partial<Record<HookEventName, HookMatcherEntry[]>>;
2326
- export declare const HooksConfig: z.ZodType<HooksConfig>;
2327
2189
  /**
2328
2190
  * DESIGN POINT ③ (slot semantics — RESERVED, core to finalize): how `spec.hooks` composes with the
2329
2191
  * engine's own `deps.hooks` (e.g. the service's process-level toolTracer). "override" = the spec slot
package/dist/hooks.js CHANGED
@@ -13,7 +13,7 @@
13
13
  * discovering it the hard way (the 1.3.0 / 1.6.0 / 1.8.0 / 2.0.0 pattern).
14
14
  *
15
15
  * CONSUMERS
16
- * - service (the worker server): `TaskRequest.settings.hooks` wire face = `HooksConfig` below; the worker-side
16
+ * - service (the worker server): `TaskRequest.settings.hooks` wire face = `HooksConfig` (`./hooks-config`); the worker-side
17
17
  * hook-runner translates matching entries into core `TaskSpec.hooks` in-process callbacks. Single-user
18
18
  * gate first (requirePrincipal semantics, same as MCP injection ).
19
19
  * - core (engine): per-event input/output shapes for the engine-owned events; `stoppedBy` on settle.
@@ -26,58 +26,25 @@
26
26
  * only from their owner; the shell must NOT double-fire engine-owned events. Configuring an event whose
27
27
  * owner hasn't shipped its runner yet is LEGAL (no validation error) — it fires when that half ships.
28
28
  *
29
+ * CONFIG SURFACE: what a settings document's `hooks` key may carry — the event vocabulary {@link HOOK_EVENTS}, the entry types, the ONE
30
+ * classifier `classifyHooksConfig` and the zod faces that read it (`HooksConfig` / `HookMatcherEntry` / `HookEntry` …) — is defined in
31
+ * `./hooks-config` and re-exported here whole; import it from here (or the main barrel) as before.
32
+ *
33
+ * WORKSPACE TRUST AND THE HOOKS EXECUTION GATE (contract; the gate executes in the client core, in one place — not in this package):
34
+ * when the host reports the workspace `declined` (`WORKSPACE_TRUST_STATES`), NO hook runs from ANY settings layer — the user, managed
35
+ * and flag layers included (CC 2.1.296: an untrusted session runs no hooks at all, `cli296.js:249597`). `no_trust_step` (the host has no
36
+ * trust step) differs from `declined` at this gate only: the gate stays open, and what applies is layer admission alone
37
+ * (`layerAdmission`: a repository-controllable layer's hooks are `trust_withheld` until the workspace is `accepted`). The gate is not
38
+ * layer admission: it lives in neither `SAFETY_MERGE_SPEC` nor `SETTINGS_SOURCE_CONTRACT`.
39
+ *
29
40
  * Pure zod contract — zero runtime deps beyond zod, browser-safe, same as the rest of this package.
30
41
  */
31
42
  import { z } from "zod";
43
+ import { HOOK_EVENTS } from "./hooks-config.js";
32
44
  import { INTEGER } from "./integer.js";
33
- import { buildQuotedIssue } from "./quoted-issue.js";
45
+ export * from "./hooks-config.js";
34
46
  // ── event set ──────────────────────────────────────────────────────────────────────────────────────────
35
- // The two halves below are where each word came from (CC 2.1.198's own table, then the later additions) — provenance for the
36
- // reader of this file, not a contract: they are module-private, and the ONE exported closed set is {@link HOOK_EVENTS}.
37
- /** CC 2.1.198 full hook-event set (jP array) — 27 events. Order matches the shell's HOOK_EVENTS. */
38
- const CC_198_HOOK_EVENTS = [
39
- "PreToolUse",
40
- "PostToolUse",
41
- "PostToolUseFailure",
42
- "PostToolBatch",
43
- "Notification",
44
- "UserPromptSubmit",
45
- "UserPromptExpansion",
46
- "SessionStart",
47
- "SessionEnd",
48
- "Stop",
49
- "StopFailure",
50
- "SubagentStart",
51
- "SubagentStop",
52
- "PreCompact",
53
- "PostCompact",
54
- "PermissionRequest",
55
- "PermissionDenied",
56
- "Setup",
57
- "TeammateIdle",
58
- "TaskCreated",
59
- "TaskCompleted",
60
- "Elicitation",
61
- "ElicitationResult",
62
- "ConfigChange",
63
- "WorktreeCreate",
64
- "WorktreeRemove",
65
- "InstructionsLoaded",
66
- ];
67
- /**
68
- * Post-CC-2.1.198 additions (sema superset; CC added them after 198).
69
- * APPEND-ONLY: new members go at the tail (1.3.0 `DirectoryAdded`, CC 2.1.219 — CC's own tuple places it
70
- * between FileChanged and MessageDisplay; the declaration order here is not normative, the enum is a set).
71
- * 2.0.0 appends the model-switch pair (`PreModelSwitch` / `PostModelSwitch`, CC 2.1.259 — absent from the
72
- * 2.1.250 corpus, so 250→259 is when they landed upstream).
73
- *
74
- * 🔴 This list is no longer maintained by hand-reading a CLI bundle: `test/upstream-drift.test.ts` fails
75
- * (naming the missing words) whenever the CC anchor snapshot in `test/fixtures/cc-hook-events.json` holds an
76
- * event this set does not, and that snapshot is produced by `scripts/extract-cc-hook-events.mjs`, never typed.
77
- */
78
- const POST_198_HOOK_EVENTS = ["CwdChanged", "FileChanged", "MessageDisplay", "DirectoryAdded", "PreModelSwitch", "PostModelSwitch"];
79
- /** The full 33-event contract set = CC 198's 27 + the 6 post-198 events — the one exported closed set of hook event names. */
80
- export const HOOK_EVENTS = [...CC_198_HOOK_EVENTS, ...POST_198_HOOK_EVENTS];
47
+ // The ONE closed set of hook event names is {@link HOOK_EVENTS} (`./hooks-config`, re-exported above; drift-gated against CC).
81
48
  export const HookEventName = z.enum(HOOK_EVENTS);
82
49
  // ── ownership (owner-single) ─────────────────────────────────────────────────────────────────────
83
50
  export const HookOwner = z.enum(["engine", "shell"]);
@@ -612,8 +579,8 @@ export const HookOutput = z.union([AsyncHookOutput, SyncHookOutput]);
612
579
  // - env = the worker host process env, PLUS the injected names in HOOK_INJECTED_ENV below
613
580
  // - timeout = per-entry `timeout` (seconds), else DEFAULT_HOOK_TIMEOUT_SECONDS
614
581
  // - stdin = the HookInput JSON; stdout parsed as HookOutput (see exit-code semantics above)
615
- // `prompt` / `agent` / `http` hook types are part of the config contract (the shell already parses
616
- // them); the service runner may phase them in after `command` — config validity is not phased.
582
+ // `prompt` / `agent` / `http` / `mcp_tool` hook types are part of the config contract (`HOOK_ENTRY_TYPES`); a runner
583
+ // may phase them in after `command` — config validity is not phased (`classifyHooksConfig` is the one reading of it).
617
584
  /** Env names the hook runner injects into a command hook's environment (CC-parity). */
618
585
  export const HOOK_INJECTED_ENV = [
619
586
  /** Absolute path of the project/workspace root. */
@@ -623,66 +590,6 @@ export const HOOK_INJECTED_ENV = [
623
590
  ];
624
591
  /** Default per-hook timeout when an entry does not set one (seconds; CC default). */
625
592
  export const DEFAULT_HOOK_TIMEOUT_SECONDS = 60;
626
- /** A non-empty string with no NUL byte (spawn()/JSON-safe). */
627
- const hookStr = z.string().min(1).refine((s) => !s.includes("\0"), buildQuotedIssue `must not contain a NUL byte`);
628
- /**
629
- * `if` condition — permission-rule syntax (e.g. "Bash(git *)") evaluated against tool_name/tool_input
630
- * BEFORE spawning the hook (only meaningful on tool events). Shell superset field, CC-compatible.
631
- */
632
- const IfCondition = z.string().optional();
633
- const HookEntryCommon = {
634
- if: IfCondition,
635
- /** Timeout in seconds for this entry (default DEFAULT_HOOK_TIMEOUT_SECONDS). */
636
- timeout: z.number().positive().optional(),
637
- /** Spinner status message (shell superset). */
638
- statusMessage: z.string().optional(),
639
- /** Run once, then remove (shell superset). */
640
- once: z.boolean().optional(),
641
- };
642
- /** `command` — shell command on the worker host (DESIGN POINT ②). First runner phase. */
643
- export const CommandHookEntry = z.object({
644
- type: z.literal("command"),
645
- command: hookStr,
646
- /** 'bash' = the user's $SHELL family; 'powershell' = pwsh. Default bash. */
647
- shell: z.enum(["bash", "powershell"]).optional(),
648
- /** Run in background without blocking (shell superset). */
649
- async: z.boolean().optional(),
650
- /** Background + wake the model on exit 2 (implies async; shell superset). */
651
- asyncRewake: z.boolean().optional(),
652
- ...HookEntryCommon,
653
- });
654
- /** `prompt` — one-shot LLM evaluation of the hook input ($ARGUMENTS = input JSON). */
655
- export const PromptHookEntry = z.object({
656
- type: z.literal("prompt"),
657
- prompt: hookStr,
658
- model: z.string().optional(),
659
- ...HookEntryCommon,
660
- });
661
- /** `agent` — agentic verifier loop over the hook input ($ARGUMENTS = input JSON). */
662
- export const AgentHookEntry = z.object({
663
- type: z.literal("agent"),
664
- prompt: hookStr,
665
- model: z.string().optional(),
666
- ...HookEntryCommon,
667
- });
668
- /** `http` — POST the hook input JSON to a URL. Headers may reference $NAMEs listed in allowedEnvVars
669
- * (env-NAME interpolation only — the config NEVER carries a secret value; same boundary as the rest
670
- * of this package). */
671
- export const HttpHookEntry = z.object({
672
- type: z.literal("http"),
673
- url: hookStr,
674
- headers: z.record(z.string(), z.string()).optional(),
675
- allowedEnvVars: z.array(z.string()).optional(),
676
- ...HookEntryCommon,
677
- });
678
- /** One configured hook (CC settings shape + shell superset fields; discriminated on `type`). */
679
- export const HookEntry = z.discriminatedUnion("type", [CommandHookEntry, PromptHookEntry, AgentHookEntry, HttpHookEntry]);
680
- /** One matcher group: `matcher` (see HOOK_EVENT_MATCHER_FIELD) + the hooks it fires. */
681
- export const HookMatcherEntry = z.object({
682
- /** Read by {@link hookMatcherSelects} — the one reading (name list or unanchored JS regex; empty/`*`/absent matches everything). */
683
- matcher: z.string().optional(),
684
- hooks: z.array(HookEntry),
685
- });
686
593
  /** The characters a matcher may consist of to be read as a NAME LIST rather than a regex. */
687
594
  const HOOK_MATCHER_NAME_LIST = /^[a-zA-Z0-9_|, -]+$/;
688
595
  function readHookMatcher(matcher) {
@@ -849,7 +756,6 @@ export function interpolateHookEnvVars(headers, allowedEnvVars, env) {
849
756
  // fromEntries defines own properties: a header keyed `__proto__` stays a header instead of replacing the prototype.
850
757
  return { headers: Object.fromEntries(entries), blanked };
851
758
  }
852
- export const HooksConfig = z.partialRecord(HookEventName, z.array(HookMatcherEntry));
853
759
  /**
854
760
  * DESIGN POINT ③ (slot semantics — RESERVED, core to finalize): how `spec.hooks` composes with the
855
761
  * engine's own `deps.hooks` (e.g. the service's process-level toolTracer). "override" = the spec slot
@@ -0,0 +1,63 @@
1
+ /**
2
+ * http hook 两张名单 `allowedHttpHookUrls` / `httpHookAllowedEnvVars`(13.0.0,U3 定稿 [10538] / S-1597)—— 类目 `httpHookAllowlists`
3
+ * (`SAFETY_MERGE_SPEC.httpHookAllowlists` 那一行是这两键的合并律与准入地板的唯一真源),本模块是它的读法与判决。纯函数,浏览器安全。
4
+ *
5
+ * CC 2.1.296 锚(cli296.js:237460 describe 原句):URL 名单「When set, HTTP hooks with non-matching URLs are blocked. If undefined, all URLs
6
+ * are allowed. If empty array, no HTTP hooks are allowed」;变量名单「each hook's effective allowedEnvVars is the intersection with this list.
7
+ * If undefined, no restriction」;坏形名单 ⇒「no HTTP hooks may run」/「no environment variables may be interpolated」(按空名单读)。
8
+ *
9
+ * 一条律:**每一张准入的名单都必须放行**(跨层取交)。准入只有一个读者:`layerAdmission(层的源, 工作区读数, "httpHookAllowlists") === "admitted"`
10
+ * (`./settings-sources`:层档取自源表 `SETTINGS_SOURCE_CONTRACT`,地板取自本类目行 `minTrust: "global"` ⇒ 用户 / flag / 托管层准入,项目 / 本地层
11
+ * 在地板之下)。本模块不推档,层对象只带源名 —— 调用方给不了档,也就抬不了档。两处与 CC 的差都在「下层不能放宽」方向(登 docs/CC-DIVERGENCE.md):
12
+ * ① CC 各层**拼接**(「Arrays merge across settings sources」)⇒ 下层可往托管名单里加 URL = 绕过管理员 ⇒ 本包取交;
13
+ * ② CC 任意层可设 ⇒ 本包只认准入层:项目 / 本地层(仓库可控)的一个 `[]` 会让用户自己的 http 守卫 hook 不发出 = 守卫失效(放行方向),
14
+ * 与 `sandboxNetwork.failIfUnavailable` 同一条边界;它们写的名单既不收窄也不放宽。
15
+ * 判官出结构不出句:放行 ⇒ `undefined`;拦下 ⇒ {@link HttpHookUrlBlock}(哪张名单 / 为什么 / 出自哪几层),用户面那一句由各宿主按它铸。
16
+ */
17
+ import { z } from "zod";
18
+ import { type SettingsSource, type WorkspaceTrustState } from "./settings-sources.js";
19
+ /** 两键(闭集,声明序)。 */
20
+ export declare const HTTP_HOOK_ALLOWLIST_KEYS: readonly ["allowedHttpHookUrls", "httpHookAllowedEnvVars"];
21
+ export type HttpHookAllowlistKey = (typeof HTTP_HOOK_ALLOWLIST_KEYS)[number];
22
+ /**
23
+ * 整份 `settings.json` 里本片段负责的两键:在场则必须是字符串数组。刻意 `passthrough`(与 `CrossSessionSettings` 同姿势):本片段不是整份
24
+ * 设置的闭集门。它是**写侧**的形门;读侧坏形不拒整份,按 {@link readHttpHookAllowlistLayer} 读成空名单(fail-closed)。
25
+ */
26
+ export declare const HttpHookAllowlistSettings: z.ZodObject<{
27
+ allowedHttpHookUrls: z.ZodOptional<z.ZodArray<z.ZodString>>;
28
+ httpHookAllowedEnvVars: z.ZodOptional<z.ZodArray<z.ZodString>>;
29
+ }, z.core.$loose>;
30
+ export type HttpHookAllowlistSettings = z.infer<typeof HttpHookAllowlistSettings>;
31
+ /**
32
+ * 一个设置层对两键的读数。键缺席 = 这一层不设限;`[]` = 这一层一个都不许。`source` = 这一层是哪个设置源(闭集 `SETTINGS_SOURCES`,原样回显进
33
+ * `sources`);准入由判官按源经 `layerAdmission` 判,调用方不预先筛层、不给档。
34
+ */
35
+ export interface HttpHookAllowlistLayer {
36
+ readonly source: SettingsSource;
37
+ readonly allowedHttpHookUrls?: readonly string[];
38
+ readonly httpHookAllowedEnvVars?: readonly string[];
39
+ }
40
+ /** 一层的读法结果:层读数 + 读成空名单的坏形键(调用方据此告警;读法本身不出句)。 */
41
+ export interface HttpHookAllowlistLayerRead {
42
+ readonly layer: HttpHookAllowlistLayer;
43
+ readonly malformed: readonly HttpHookAllowlistKey[];
44
+ }
45
+ /**
46
+ * 从一层的原始设置对象读出两键(唯一读法)。键缺席(或值为 `undefined`)⇒ 不设;字符串数组 ⇒ 原样;其余一切(非数组、含非串条目、`null`)
47
+ * ⇒ **读成 `[]`**(CC 同向:坏形名单 = 一个都不许)并在 `malformed` 点名。整份设置不是对象 ⇒ 两键都缺。
48
+ */
49
+ export declare function readHttpHookAllowlistLayer(source: SettingsSource, settings: unknown): HttpHookAllowlistLayerRead;
50
+ /** 拦下一只 http hook 的原因。`rule`:`empty` = 有一张准入名单是空的(那一层改对之前一只都不许);`url` = 没命中那几张名单里的任何模式。
51
+ * `sources` = 不放行它的那几张名单所在的层(入参序)。零 URL 回显 —— URL 调用方自己有,按受众给显示形。 */
52
+ export interface HttpHookUrlBlock {
53
+ readonly list: "allowedHttpHookUrls";
54
+ readonly rule: "empty" | "url";
55
+ readonly sources: readonly SettingsSource[];
56
+ }
57
+ /**
58
+ * 一只 http hook 的 URL 过不过名单:每一张准入的 `allowedHttpHookUrls` 都要有一条模式按放行出口({@link urlAllowPatternMatches})命中它。
59
+ * 没有准入层写这张名单 ⇒ 不限(`undefined`)。`workspace` = 宿主对工作区信任的读数(读口抛或闭集外按 `declined` 交),与 `layerAdmission` 同一个入参。
60
+ */
61
+ export declare function httpHookUrlVerdict(url: string, layers: readonly HttpHookAllowlistLayer[], workspace: WorkspaceTrustState): HttpHookUrlBlock | undefined;
62
+ /** 一只 http hook 实际可插值进请求头的环境变量:它自己的 `allowedEnvVars` 与每一张准入的 `httpHookAllowedEnvVars` 取交(保持 hook 自己的序)。 */
63
+ export declare function httpHookEnvVarsAllowed(hookVars: readonly string[], layers: readonly HttpHookAllowlistLayer[], workspace: WorkspaceTrustState): string[];
@@ -0,0 +1,73 @@
1
+ /**
2
+ * http hook 两张名单 `allowedHttpHookUrls` / `httpHookAllowedEnvVars`(13.0.0,U3 定稿 [10538] / S-1597)—— 类目 `httpHookAllowlists`
3
+ * (`SAFETY_MERGE_SPEC.httpHookAllowlists` 那一行是这两键的合并律与准入地板的唯一真源),本模块是它的读法与判决。纯函数,浏览器安全。
4
+ *
5
+ * CC 2.1.296 锚(cli296.js:237460 describe 原句):URL 名单「When set, HTTP hooks with non-matching URLs are blocked. If undefined, all URLs
6
+ * are allowed. If empty array, no HTTP hooks are allowed」;变量名单「each hook's effective allowedEnvVars is the intersection with this list.
7
+ * If undefined, no restriction」;坏形名单 ⇒「no HTTP hooks may run」/「no environment variables may be interpolated」(按空名单读)。
8
+ *
9
+ * 一条律:**每一张准入的名单都必须放行**(跨层取交)。准入只有一个读者:`layerAdmission(层的源, 工作区读数, "httpHookAllowlists") === "admitted"`
10
+ * (`./settings-sources`:层档取自源表 `SETTINGS_SOURCE_CONTRACT`,地板取自本类目行 `minTrust: "global"` ⇒ 用户 / flag / 托管层准入,项目 / 本地层
11
+ * 在地板之下)。本模块不推档,层对象只带源名 —— 调用方给不了档,也就抬不了档。两处与 CC 的差都在「下层不能放宽」方向(登 docs/CC-DIVERGENCE.md):
12
+ * ① CC 各层**拼接**(「Arrays merge across settings sources」)⇒ 下层可往托管名单里加 URL = 绕过管理员 ⇒ 本包取交;
13
+ * ② CC 任意层可设 ⇒ 本包只认准入层:项目 / 本地层(仓库可控)的一个 `[]` 会让用户自己的 http 守卫 hook 不发出 = 守卫失效(放行方向),
14
+ * 与 `sandboxNetwork.failIfUnavailable` 同一条边界;它们写的名单既不收窄也不放宽。
15
+ * 判官出结构不出句:放行 ⇒ `undefined`;拦下 ⇒ {@link HttpHookUrlBlock}(哪张名单 / 为什么 / 出自哪几层),用户面那一句由各宿主按它铸。
16
+ */
17
+ import { z } from "zod";
18
+ import { layerAdmission } from "./settings-sources.js";
19
+ import { urlAllowPatternMatches } from "./url-pattern.js";
20
+ /** 两键(闭集,声明序)。 */
21
+ export const HTTP_HOOK_ALLOWLIST_KEYS = ["allowedHttpHookUrls", "httpHookAllowedEnvVars"];
22
+ /**
23
+ * 整份 `settings.json` 里本片段负责的两键:在场则必须是字符串数组。刻意 `passthrough`(与 `CrossSessionSettings` 同姿势):本片段不是整份
24
+ * 设置的闭集门。它是**写侧**的形门;读侧坏形不拒整份,按 {@link readHttpHookAllowlistLayer} 读成空名单(fail-closed)。
25
+ */
26
+ export const HttpHookAllowlistSettings = z
27
+ .object({
28
+ allowedHttpHookUrls: z.array(z.string()).optional(),
29
+ httpHookAllowedEnvVars: z.array(z.string()).optional(),
30
+ })
31
+ .passthrough();
32
+ /**
33
+ * 从一层的原始设置对象读出两键(唯一读法)。键缺席(或值为 `undefined`)⇒ 不设;字符串数组 ⇒ 原样;其余一切(非数组、含非串条目、`null`)
34
+ * ⇒ **读成 `[]`**(CC 同向:坏形名单 = 一个都不许)并在 `malformed` 点名。整份设置不是对象 ⇒ 两键都缺。
35
+ */
36
+ export function readHttpHookAllowlistLayer(source, settings) {
37
+ const bag = typeof settings === "object" && settings !== null && !Array.isArray(settings) ? settings : {};
38
+ const malformed = [];
39
+ const lists = {};
40
+ for (const key of HTTP_HOOK_ALLOWLIST_KEYS) {
41
+ if (!Object.hasOwn(bag, key) || bag[key] === undefined)
42
+ continue;
43
+ const v = bag[key];
44
+ if (Array.isArray(v) && v.every((x) => typeof x === "string"))
45
+ lists[key] = [...v];
46
+ else {
47
+ lists[key] = [];
48
+ malformed.push(key);
49
+ }
50
+ }
51
+ return { layer: { source, ...lists }, malformed };
52
+ }
53
+ /** 进判决的那几层(入参序):`layerAdmission` 答 `admitted` 的层。每一层都问 —— 闭集外的源 / 工作区读数在这里响亮抛,哪怕那一层没写名单。 */
54
+ function admittedLayers(layers, workspace) {
55
+ return layers.filter((l) => layerAdmission(l.source, workspace, "httpHookAllowlists") === "admitted");
56
+ }
57
+ /**
58
+ * 一只 http hook 的 URL 过不过名单:每一张准入的 `allowedHttpHookUrls` 都要有一条模式按放行出口({@link urlAllowPatternMatches})命中它。
59
+ * 没有准入层写这张名单 ⇒ 不限(`undefined`)。`workspace` = 宿主对工作区信任的读数(读口抛或闭集外按 `declined` 交),与 `layerAdmission` 同一个入参。
60
+ */
61
+ export function httpHookUrlVerdict(url, layers, workspace) {
62
+ const refusing = admittedLayers(layers, workspace).filter((l) => l.allowedHttpHookUrls !== undefined && !l.allowedHttpHookUrls.some((p) => urlAllowPatternMatches(url, p)));
63
+ if (refusing.length === 0)
64
+ return undefined;
65
+ return { list: "allowedHttpHookUrls", rule: refusing.some((l) => l.allowedHttpHookUrls.length === 0) ? "empty" : "url", sources: refusing.map((l) => l.source) };
66
+ }
67
+ /** 一只 http hook 实际可插值进请求头的环境变量:它自己的 `allowedEnvVars` 与每一张准入的 `httpHookAllowedEnvVars` 取交(保持 hook 自己的序)。 */
68
+ export function httpHookEnvVarsAllowed(hookVars, layers, workspace) {
69
+ const lists = admittedLayers(layers, workspace)
70
+ .filter((l) => l.httpHookAllowedEnvVars !== undefined)
71
+ .map((l) => l.httpHookAllowedEnvVars);
72
+ return hookVars.filter((v) => lists.every((list) => list.includes(v)));
73
+ }
package/dist/index.d.ts CHANGED
@@ -31,3 +31,8 @@ export * from "./web-admin-env.js";
31
31
  export * from "./web-admin-credential.js";
32
32
  export * from "./web-admin-session-url.js";
33
33
  export * from "./retired-envs.js";
34
+ export * from "./settings-sources.js";
35
+ export * from "./url-pattern.js";
36
+ export * from "./http-hook-allowlists.js";
37
+ export * from "./mcp-server-policy.js";
38
+ export * from "./ui-language.js";
package/dist/index.js CHANGED
@@ -19,7 +19,7 @@ export * from "./remote-exec.js";
19
19
  export * from "./safety-merge-spec.js"; // #14 per-category safety merge-spec (settings resolver authority)
20
20
  export * from "./scheduler-store.js"; // R7 self-wake scheduler store CONTRACT (pure: type + parse/serialize + SchedulerStore interface; fs binding in ./node)
21
21
  export * from "./cross-session.js"; // CC-parity `settings.json` cross-session keys (crossSessionInbound / dialogExpiry) — pure/browser-safe
22
- export * from "./hooks.js"; // CC-parity hooks contract (31-event set + owner map + per-event input/output shapes + HooksConfig) & stop attribution (stoppedBy) — pure/browser-safe
22
+ export * from "./hooks.js"; // CC-parity hooks contract (33-event set + owner map + per-event input/output shapes + the config surface: classifyHooksConfig and its zod faces HooksConfig / HookEntry …) & stop attribution (stoppedBy) — pure/browser-safe
23
23
  export * from "./fleet.js"; // scheduling/fleet contract (drain protocol · instance discovery · dispatch policy · quota lease · usage report · quota reset event) — CONTROL-PLANE-SCHEDULING-DESIGN, pure/browser-safe
24
24
  export * from "./workers-rows.js"; // 11.0.0 the workers domain's per-row verdict (readWorkersRows: a row that does not read is held, the rest read on) — permanent read semantics, pure
25
25
  export * from "./api/auth.js"; // /api/v1/auth/* wire contract (RFC 8628 device flow + rotating refresh + logout + approve) — REGISTRY-REBUILD-DESIGN §2, M1 shipped shapes frozen, pure/browser-safe
@@ -31,6 +31,11 @@ export * from "./web-admin-env.js"; // WEB_ADMIN_ENV — every env name an engin
31
31
  export * from "./web-admin-credential.js"; // S-698 web-admin credential FILE contract: WebAdminCredentialEnvelope (user-session envelope, strict) + parseWebAdminCredentialFile (bare / envelope / unreadable — the one discrimination law) + judgeWebAdminCredentialDeclaration(env, credential) (pick-exactly-one over the credential's WEB_ADMIN_ENV pair; WEB_ADMIN_CREDENTIALS = access | assertion), pure/browser-safe
32
32
  export * from "./web-admin-session-url.js"; // webAdminSessionUrl(webBaseUrl, { worker, sessionId }) — THE web-admin session link template (<webBaseUrl>/chat?agent=<worker>&session=<sessionId>) + parseWebAdminSessionUrl(link) its paired reader, pure/browser-safe
33
33
  export * from "./retired-envs.js"; // the retired-env TOMBSTONE table (renamed / replaced / removed, closed set) + retiredEnvRefusal(env) (the one refusal sentence) + buildRetiredEnvWire() — engine boot, catalog wire, shell and web-admin read this one table, pure/browser-safe
34
+ export * from "./settings-sources.js"; // settings SOURCES (merge order) + SETTINGS_SOURCE_CONTRACT (trust tier, repository-controllable) + WORKSPACE_TRUST_STATES + layerAdmission(source, workspace, category, member?, value?) — the one layer × key admission reading, pure/browser-safe
35
+ export * from "./url-pattern.js"; // 13.0.0 (S-1597): THE URL-list pattern judge, two exits — urlAllowPatternMatches (⊆ upstream g9, four holes closed) / urlDenyPatternMatches (⊇ upstream ∪ whole-string glob), pure/browser-safe
36
+ export * from "./http-hook-allowlists.js"; // 13.0.0 (U3): allowedHttpHookUrls / httpHookAllowedEnvVars — one layer reader + httpHookUrlVerdict / httpHookEnvVarsAllowed (admitted layers INTERSECT; SAFETY_MERGE_SPEC.httpHookAllowlists), pure/browser-safe
37
+ export * from "./mcp-server-policy.js"; // 13.0.0 (U6): mcpServerPolicyVerdict → McpPolicyBlock{name, list, rule, target, written, sources} (structure, no sentence; target judged expanded, echoed only as written), pure/browser-safe
38
+ export * from "./ui-language.js"; // 13.0.0 (S-1592): the UI-language settings fragment (semaUiLanguage, en | zh, category ui, user settings) + readUiLanguage, pure/browser-safe
34
39
  // NOTE: the Node-fs-backed helpers (FileConfigStore + the file editor) are NOT in this barrel — they pull
35
40
  // `node:fs`/`node:path` and would break a browser/client bundle that only wants the contract (e.g.
36
41
  // web-admin's client components import the zod types through here). Import them from the Node-only