@sema-agent/settings-schema 1.2.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/hooks.js CHANGED
@@ -2,13 +2,13 @@
2
2
  * `hooks` — the CC-parity hooks contract (incl. stop attribution).
3
3
  *
4
4
  * ONE definition of the FULL hook-event set — 27 CC 2.1.198 events (REF: the `jP` array in
5
- * claude-cli-2.1.198.pretty.js) + 3 post-198 events the shell already ships (CwdChanged / FileChanged /
6
- * MessageDisplay). Per the product ruling: the CONTRACT is the full set from day one; the engine
7
- * hook-runner (service) and the shell light events up in PHASES — adding an event later is pure
8
- * implementation, never a schema change.
5
+ * claude-cli-2.1.198.pretty.js) + 4 post-198 events the shell already ships (CwdChanged / FileChanged /
6
+ * MessageDisplay, and DirectoryAdded from CC 2.1.219). Per the product ruling: the CONTRACT is the full set
7
+ * from day one; the engine hook-runner (service) and the shell light events up in PHASES — adding an event
8
+ * later is pure implementation, never a schema change.
9
9
  *
10
10
  * CONSUMERS
11
- * - service (sema-server): `TaskRequest.settings.hooks` wire face = `HooksConfig` below; the worker-side
11
+ * - service (the worker server): `TaskRequest.settings.hooks` wire face = `HooksConfig` below; the worker-side
12
12
  * hook-runner translates matching entries into core `TaskSpec.hooks` in-process callbacks. Single-user
13
13
  * gate first (requirePrincipal semantics, same as MCP injection ).
14
14
  * - core (engine): per-event input/output shapes for the engine-owned events; `stoppedBy` on settle.
@@ -55,9 +55,13 @@ export const CC_198_HOOK_EVENTS = [
55
55
  "WorktreeRemove",
56
56
  "InstructionsLoaded",
57
57
  ];
58
- /** Post-CC-2.1.198 additions the shell already ships (sema superset; CC added them after 198). */
59
- export const POST_198_HOOK_EVENTS = ["CwdChanged", "FileChanged", "MessageDisplay"];
60
- /** The full 30-event contract set = CC 198's 27 + the 3 post-198 shell events. */
58
+ /**
59
+ * Post-CC-2.1.198 additions the shell already ships (sema superset; CC added them after 198).
60
+ * APPEND-ONLY: new members go at the tail (1.3.0 `DirectoryAdded`, CC 2.1.219 — CC's own tuple places it
61
+ * between FileChanged and MessageDisplay; the declaration order here is not normative, the enum is a set).
62
+ */
63
+ export const POST_198_HOOK_EVENTS = ["CwdChanged", "FileChanged", "MessageDisplay", "DirectoryAdded"];
64
+ /** The full 31-event contract set = CC 198's 27 + the 4 post-198 shell events. */
61
65
  export const HOOK_EVENTS = [...CC_198_HOOK_EVENTS, ...POST_198_HOOK_EVENTS];
62
66
  export const HookEventName = z.enum(HOOK_EVENTS);
63
67
  // ── ownership (owner-single) ─────────────────────────────────────────────────────────────────────
@@ -119,6 +123,7 @@ export const HOOK_EVENT_MATCHER_FIELD = {
119
123
  CwdChanged: null,
120
124
  FileChanged: "file_path", // matched against basename(file_path)
121
125
  MessageDisplay: null,
126
+ DirectoryAdded: null, // the shell's getMatchingHooks switch has no arm for it → every entry matches (CwdChanged family)
122
127
  };
123
128
  // ── shared enums (CC-verbatim) ─────────────────────────────────────────────────────────────────────────
124
129
  export const PermissionBehavior = z.enum(["allow", "deny", "ask"]);
@@ -382,7 +387,18 @@ export const MessageDisplayHookInput = BaseHookInput.extend({
382
387
  /** Newly completed lines since the prior flush (final flush may end mid-line / be empty). */
383
388
  delta: z.string(),
384
389
  });
385
- /** Any hook input discriminated on `hook_event_name` (all 30 events). */
390
+ /** How a directory joined the session working set: the `/add-dir` slash command or repo-root registration. */
391
+ export const DIRECTORY_ADDED_SOURCES = ["slash_command", "register_repo_root"];
392
+ /**
393
+ * Fired after a directory joins the session's working set (CC 2.1.219; agent-types hooks.d.ts:582-586 —
394
+ * `directory` is the absolute path). Shell-owned; matcher ignored; no hookSpecificOutput arm upstream.
395
+ */
396
+ export const DirectoryAddedHookInput = BaseHookInput.extend({
397
+ hook_event_name: z.literal("DirectoryAdded"),
398
+ directory: z.string(),
399
+ source: z.enum(DIRECTORY_ADDED_SOURCES),
400
+ });
401
+ /** Any hook input — discriminated on `hook_event_name` (all 31 events). */
386
402
  export const HookInput = z.discriminatedUnion("hook_event_name", [
387
403
  PreToolUseHookInput,
388
404
  PostToolUseHookInput,
@@ -414,6 +430,7 @@ export const HookInput = z.discriminatedUnion("hook_event_name", [
414
430
  CwdChangedHookInput,
415
431
  FileChangedHookInput,
416
432
  MessageDisplayHookInput,
433
+ DirectoryAddedHookInput,
417
434
  ]);
418
435
  // ── hook output / decision semantics (CC-verbatim, with the service-receipt design points pinned) ────────
419
436
  //
@@ -488,8 +505,13 @@ export const HookSpecificOutput = z.union([
488
505
  ]);
489
506
  /**
490
507
  * Synchronous hook JSON output (stdout of a command hook, CC-verbatim). Exit-code semantics ride
491
- * alongside: exit 0 = success (stdout parsed as this shape when JSON), exit 2 = BLOCKING error (stderr
492
- * fed back to the model), any other exit = non-blocking error (stderr shown to the user).
508
+ * alongside: exit 0 = success (stdout parsed as this shape when JSON); exit 2 = BLOCKING error (stderr is fed
509
+ * back to the model as the block reason / additionalContext, per event); any other exit, a timeout, a spawn
510
+ * failure or malformed JSON = NON-BLOCKING failure: the run continues, and the consumer's hook-runner forwards
511
+ * it as a `hook_non_blocking_failure` notice (a `hook_notice` frame on the task/fleet stream carrying hook name,
512
+ * event, reason, exit code and the stderr text redacted-then-bounded to 512 chars), so the failure is visible
513
+ * from the task's point of view instead of only in server logs. There is no separate "shown to the user"
514
+ * surface: what a user sees is whatever their client renders from that notice frame.
493
515
  */
494
516
  export const SyncHookOutput = z.object({
495
517
  /** Whether the run should continue after the hook (default true). */
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * @sema-agent/settings-schema — the public barrel. ONE source of truth for the config contract shared by
3
- * sema-registry, sema-server, and the TOC desktop/CLI.
3
+ * the config center, the worker service, and the TOC desktop/CLI.
4
4
  */
5
5
  export * from "./types.js";
6
6
  export * from "./hash.js";
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * @sema-agent/settings-schema — the public barrel. ONE source of truth for the config contract shared by
3
- * sema-registry, sema-server, and the TOC desktop/CLI.
3
+ * the config center, the worker service, and the TOC desktop/CLI.
4
4
  */
5
5
  export * from "./types.js";
6
6
  export * from "./hash.js";
@@ -14,7 +14,7 @@ export * from "./cross-domain.js"; // pure half only (no fleet placement)
14
14
  export * from "./remote-exec.js";
15
15
  export * from "./safety-merge-spec.js"; // #14 per-category safety merge-spec (settings resolver authority)
16
16
  export * from "./scheduler-store.js"; // R7 self-wake scheduler store CONTRACT (pure: type + parse/serialize + SchedulerStore interface; fs binding in ./node)
17
- export * from "./hooks.js"; // CC-parity hooks contract (30-event set + owner map + per-event input/output shapes + HooksConfig) & stop attribution (stoppedBy) — pure/browser-safe
17
+ 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
18
18
  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
19
19
  export * from "./migrate.js"; // 0.5→0.6 migration tools (hosts grandfathering + worker.model→catalog migration PLAN — report/draft only, E1; EXPERT-REDESIGN §2-2/§3), pure/browser-safe
20
20
  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
@@ -28,7 +28,7 @@ export interface LocalConfig {
28
28
  requiredEnv: RequiredEnvName[];
29
29
  /** 本次读盘的降级/静默告警(0.19.0)。此前本函数**从不**给 store 挂 `onWarning`,于是单机 lane 的
30
30
  * 默认形是「没有通道 = 没有声音」:域文档里被剥的键、`config.d/` 里永远不会被读的文件,一律静默
31
- * (sema-server #322 的病形)。空数组 = 本次读盘无告警。 */
31
+ * (消费方 #322 的病形)。空数组 = 本次读盘无告警。 */
32
32
  warnings: EffectiveReadWarning[];
33
33
  }
34
34
  /** The ONE canonical local load — the service engine and the client CLI/desktop both call this so they cannot
@@ -1,16 +1,16 @@
1
1
  /**
2
2
  * `remoteExec` — the execution-substrate contract (NET-NEW; not part of DOMAIN_SCHEMAS today).
3
3
  *
4
- * Per Clay's decision the exec layer is user-configurable & pluggable. This SCHEMA is a FORWARD-COMPATIBLE
5
- * SUPERSET: it carries both the backends sema-server already implements today AND the TOC single-machine
6
- * backends Clay added (the service implements those in PHASES — host + e2b first, local/remote-docker
7
- * fast-follow). The schema LEADS; runtime support is phased. The discriminator is `provider`:
8
- * FLEET (implemented today, sema-server/src/config.ts):
4
+ * Per the product owner's decision the exec layer is user-configurable & pluggable. This SCHEMA is a
5
+ * FORWARD-COMPATIBLE SUPERSET: it carries both the backends the worker service already implements today AND the
6
+ * TOC single-machine backends the product owner added (the service implements those in PHASES — host + e2b first,
7
+ * local/remote-docker fast-follow). The schema LEADS; runtime support is phased. The discriminator is `provider`:
8
+ * FLEET (implemented today, the worker service's config module):
9
9
  * - `e2b` per-task E2B Firecracker VM (isolated, suspendable). The default code-agent worker.
10
10
  * - `k8s` per-task Kata pod on our k3s (isolated, NOT suspendable). Self-hosted sandbox lane.
11
11
  * - `ssh` a real host over SSH (NOT isolated/suspendable). AI-orchestrated batch deploy.
12
12
  * - `adb` a real device over ADB (NOT isolated/suspendable). On-device APP testing.
13
- * TOC single-machine (Clay; service implements in phases — macOS + Linux first):
13
+ * TOC single-machine (product-owner scope; service implements in phases — macOS + Linux first):
14
14
  * - `host` run tools DIRECTLY on this machine, no container (the TOC default, like a local coding
15
15
  * agent). No isolation. macOS/Linux.
16
16
  * - `local-docker` containers on THIS machine's docker (isolation for parallel/untrusted fan-out).
@@ -25,7 +25,7 @@
25
25
  import { z } from "zod";
26
26
  export declare const RemoteExecSpec: z.ZodDiscriminatedUnion<"provider", [z.ZodObject<{
27
27
  provider: z.ZodLiteral<"host">;
28
- /** Working ROOT for the agent's tools; absent = the workspace/cwd. (≡ sema-server's resolved
28
+ /** Working ROOT for the agent's tools; absent = the workspace/cwd. (≡ the worker service's resolved
29
29
  * `workspaceBase` — the adapter runs each task under `<workdir>/ai-agent-host-<id>`.) */
30
30
  workdir: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
31
31
  /** Optional writable-path allowlist bounding what the host-exec tools may touch (defence for a TOC box). */
@@ -33,7 +33,7 @@ export declare const RemoteExecSpec: z.ZodDiscriminatedUnion<"provider", [z.ZodO
33
33
  /** Shell to spawn (absent = the platform default). */
34
34
  shell: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
35
35
  /** Per-COMMAND timeout (ms) for the host lane — host is long-lived so this bounds a single exec, not the
36
- * task (mirrors e2b/k8s `timeoutMs`; = sema-server's `commandTimeoutMs`). Absent = adapter default. */
36
+ * task (mirrors e2b/k8s `timeoutMs`; = the worker service's `commandTimeoutMs`). Absent = adapter default. */
37
37
  commandTimeoutMs: z.ZodOptional<z.ZodNumber>;
38
38
  }, "strip", z.ZodTypeAny, {
39
39
  provider: "host";
@@ -1,16 +1,16 @@
1
1
  /**
2
2
  * `remoteExec` — the execution-substrate contract (NET-NEW; not part of DOMAIN_SCHEMAS today).
3
3
  *
4
- * Per Clay's decision the exec layer is user-configurable & pluggable. This SCHEMA is a FORWARD-COMPATIBLE
5
- * SUPERSET: it carries both the backends sema-server already implements today AND the TOC single-machine
6
- * backends Clay added (the service implements those in PHASES — host + e2b first, local/remote-docker
7
- * fast-follow). The schema LEADS; runtime support is phased. The discriminator is `provider`:
8
- * FLEET (implemented today, sema-server/src/config.ts):
4
+ * Per the product owner's decision the exec layer is user-configurable & pluggable. This SCHEMA is a
5
+ * FORWARD-COMPATIBLE SUPERSET: it carries both the backends the worker service already implements today AND the
6
+ * TOC single-machine backends the product owner added (the service implements those in PHASES — host + e2b first,
7
+ * local/remote-docker fast-follow). The schema LEADS; runtime support is phased. The discriminator is `provider`:
8
+ * FLEET (implemented today, the worker service's config module):
9
9
  * - `e2b` per-task E2B Firecracker VM (isolated, suspendable). The default code-agent worker.
10
10
  * - `k8s` per-task Kata pod on our k3s (isolated, NOT suspendable). Self-hosted sandbox lane.
11
11
  * - `ssh` a real host over SSH (NOT isolated/suspendable). AI-orchestrated batch deploy.
12
12
  * - `adb` a real device over ADB (NOT isolated/suspendable). On-device APP testing.
13
- * TOC single-machine (Clay; service implements in phases — macOS + Linux first):
13
+ * TOC single-machine (product-owner scope; service implements in phases — macOS + Linux first):
14
14
  * - `host` run tools DIRECTLY on this machine, no container (the TOC default, like a local coding
15
15
  * agent). No isolation. macOS/Linux.
16
16
  * - `local-docker` containers on THIS machine's docker (isolation for parallel/untrusted fan-out).
@@ -44,7 +44,7 @@ export const RemoteExecSpec = z.discriminatedUnion("provider", [
44
44
  // ── host: run on THIS machine, no container (TOC default; macOS/Linux). No isolation, no secrets.
45
45
  z.object({
46
46
  provider: z.literal("host"),
47
- /** Working ROOT for the agent's tools; absent = the workspace/cwd. (≡ sema-server's resolved
47
+ /** Working ROOT for the agent's tools; absent = the workspace/cwd. (≡ the worker service's resolved
48
48
  * `workspaceBase` — the adapter runs each task under `<workdir>/ai-agent-host-<id>`.) */
49
49
  workdir: execStr.optional(),
50
50
  /** Optional writable-path allowlist bounding what the host-exec tools may touch (defence for a TOC box). */
@@ -52,7 +52,7 @@ export const RemoteExecSpec = z.discriminatedUnion("provider", [
52
52
  /** Shell to spawn (absent = the platform default). */
53
53
  shell: execStr.optional(),
54
54
  /** Per-COMMAND timeout (ms) for the host lane — host is long-lived so this bounds a single exec, not the
55
- * task (mirrors e2b/k8s `timeoutMs`; = sema-server's `commandTimeoutMs`). Absent = adapter default. */
55
+ * task (mirrors e2b/k8s `timeoutMs`; = the worker service's `commandTimeoutMs`). Absent = adapter default. */
56
56
  commandTimeoutMs: z.number().int().positive().optional(),
57
57
  }),
58
58
  // ── local-docker: containers on this machine's docker (isolated; enables parallel fan-out). No secrets
@@ -14,7 +14,7 @@
14
14
  * admitted or rejected as one value under its `minTrust`. A resolver reads this table and has NO per-key
15
15
  * special-casing.
16
16
  *
17
- * MEMBER-LEVEL ADMISSION (1.1.0 — the WRITTEN layer of the permission-rule dual-channel ruling, board [5797] ④;
17
+ * MEMBER-LEVEL ADMISSION (1.1.0 — the WRITTEN layer of the permission-rule dual-channel ruling, [ref] ④;
18
18
  * the executing layer is core's `prepareCcImport` door + `RuleAddOrigin`, the resolver/shell halves are cli's):
19
19
  * A category may declare `members`, each with its own admission floor and DIRECTION. Today only `permissions`
20
20
  * does — `allow` / `deny` / `ask`, a CLOSED set ({@link SafetyMergeMember}; an unknown member is a compile error
@@ -39,7 +39,7 @@
39
39
  * requiresRealApproval). Consumers read admission through {@link memberAdmission} / {@link memberMinTrust} /
40
40
  * {@link membersOf} — never through a hardcoded allow/deny/ask table of their own (single source).
41
41
  *
42
- * TRUST ORDERING PRIMITIVES (1.2.0 — board [5796] ordering question → [5805] claim / [5807] consumer statement):
42
+ * TRUST ORDERING PRIMITIVES (1.2.0 — [ref] ordering question → claim / consumer statement):
43
43
  * The floor words are only meaningful against a LADDER, and that ladder is v2-design §1:29, verbatim:
44
44
  * `MANAGED 6 > GLOBAL 5 > PROJECT 4 > LOCAL 3 > SESSION 2 > TASK 1`. {@link trustRank} publishes exactly those
45
45
  * numbers and {@link isAtLeast} the comparison a resolver makes (`rank(layer) ≥ rank(floor)` ⇒ admitted), so no
@@ -83,16 +83,44 @@ export declare function isAtLeast(layer: MinTrust, minTrust: MinTrust): boolean;
83
83
  export declare const MemberDirection: z.ZodEnum<["loosen", "tighten"]>;
84
84
  export type MemberDirection = z.infer<typeof MemberDirection>;
85
85
  /** One member's admission row. `minTrust` ABSENT ⇒ the category's `minTrust` applies: a row can refine the floor
86
- * only by SAYING so; omission never lowers it (fail-closed by construction). */
86
+ * only by SAYING so; omission never lowers it (fail-closed by construction).
87
+ *
88
+ * `boolPole` (S-59 件八, codex r3 [high]) — VALUE-DIRECTIONAL admission for a boolean member whose two poles
89
+ * point opposite ways (`enabled:true` narrows the sandbox, `enabled:false` opens it): a write EQUAL to
90
+ * `boolPole.strict` (strict equality, never truthiness — a string "true" is not the pole) is the tightening
91
+ * write and is admitted at `boolPole.minTrust`; ANY other write — the loose pole, a non-boolean, or a
92
+ * valueless query — is admitted at the ROW floor. 🔴 Vintage-safety law: a boolPole row's OWN `minTrust`
93
+ * must be the LOOSEN floor (the category's), so a consumer that predates this field (its schema strips
94
+ * `boolPole`) reads the row as tighten@<loosen-floor> and withholds BOTH poles from low-trust layers —
95
+ * fail-closed (it loses only the convenience of low-trust tightening, it can never admit a loosening write).
96
+ * Resolution is value-aware: pass the candidate value to {@link memberAdmission} / {@link memberMinTrust}. */
87
97
  export declare const MemberAdmission: z.ZodObject<{
88
98
  direction: z.ZodEnum<["loosen", "tighten"]>;
89
99
  minTrust: z.ZodOptional<z.ZodEnum<["local", "session", "project", "global", "managed", "task"]>>;
100
+ boolPole: z.ZodOptional<z.ZodObject<{
101
+ strict: z.ZodBoolean;
102
+ minTrust: z.ZodEnum<["local", "session", "project", "global", "managed", "task"]>;
103
+ }, "strip", z.ZodTypeAny, {
104
+ strict: boolean;
105
+ minTrust: "local" | "session" | "global" | "project" | "managed" | "task";
106
+ }, {
107
+ strict: boolean;
108
+ minTrust: "local" | "session" | "global" | "project" | "managed" | "task";
109
+ }>>;
90
110
  }, "strip", z.ZodTypeAny, {
91
111
  direction: "loosen" | "tighten";
92
112
  minTrust?: "local" | "session" | "global" | "project" | "managed" | "task" | undefined;
113
+ boolPole?: {
114
+ strict: boolean;
115
+ minTrust: "local" | "session" | "global" | "project" | "managed" | "task";
116
+ } | undefined;
93
117
  }, {
94
118
  direction: "loosen" | "tighten";
95
119
  minTrust?: "local" | "session" | "global" | "project" | "managed" | "task" | undefined;
120
+ boolPole?: {
121
+ strict: boolean;
122
+ minTrust: "local" | "session" | "global" | "project" | "managed" | "task";
123
+ } | undefined;
96
124
  }>;
97
125
  export type MemberAdmission = z.infer<typeof MemberAdmission>;
98
126
  /** The RESOLVED admission a consumer branches on — the category fallback already applied, so `minTrust` is never
@@ -100,6 +128,12 @@ export type MemberAdmission = z.infer<typeof MemberAdmission>;
100
128
  export interface ResolvedMemberAdmission {
101
129
  readonly direction: MemberDirection;
102
130
  readonly minTrust: MinTrust;
131
+ /** Present iff the row is value-directional; `minTrust` above already reflects the VALUE handed in (row floor
132
+ * when no value / not the strict pole). Exposed for presentation faces; enforcement should pass the value. */
133
+ readonly boolPole?: {
134
+ readonly strict: boolean;
135
+ readonly minTrust: MinTrust;
136
+ };
103
137
  }
104
138
  /** WHERE the category is enforced (STRENGTH) — MUST stay in lockstep with `EffectiveKey.enforcement` (types.ts).
105
139
  * gate=data-plane engine, server=control-plane/TOB boundary, resolver=merger correctness, client=UI only. */
@@ -202,12 +236,30 @@ export declare const MergeCategorySpec: z.ZodObject<{
202
236
  members: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
203
237
  direction: z.ZodEnum<["loosen", "tighten"]>;
204
238
  minTrust: z.ZodOptional<z.ZodEnum<["local", "session", "project", "global", "managed", "task"]>>;
239
+ boolPole: z.ZodOptional<z.ZodObject<{
240
+ strict: z.ZodBoolean;
241
+ minTrust: z.ZodEnum<["local", "session", "project", "global", "managed", "task"]>;
242
+ }, "strip", z.ZodTypeAny, {
243
+ strict: boolean;
244
+ minTrust: "local" | "session" | "global" | "project" | "managed" | "task";
245
+ }, {
246
+ strict: boolean;
247
+ minTrust: "local" | "session" | "global" | "project" | "managed" | "task";
248
+ }>>;
205
249
  }, "strip", z.ZodTypeAny, {
206
250
  direction: "loosen" | "tighten";
207
251
  minTrust?: "local" | "session" | "global" | "project" | "managed" | "task" | undefined;
252
+ boolPole?: {
253
+ strict: boolean;
254
+ minTrust: "local" | "session" | "global" | "project" | "managed" | "task";
255
+ } | undefined;
208
256
  }, {
209
257
  direction: "loosen" | "tighten";
210
258
  minTrust?: "local" | "session" | "global" | "project" | "managed" | "task" | undefined;
259
+ boolPole?: {
260
+ strict: boolean;
261
+ minTrust: "local" | "session" | "global" | "project" | "managed" | "task";
262
+ } | undefined;
211
263
  }>>>;
212
264
  }, "strip", z.ZodTypeAny, {
213
265
  enforcement: "gate" | "server" | "resolver" | "client";
@@ -231,6 +283,10 @@ export declare const MergeCategorySpec: z.ZodObject<{
231
283
  members?: Record<string, {
232
284
  direction: "loosen" | "tighten";
233
285
  minTrust?: "local" | "session" | "global" | "project" | "managed" | "task" | undefined;
286
+ boolPole?: {
287
+ strict: boolean;
288
+ minTrust: "local" | "session" | "global" | "project" | "managed" | "task";
289
+ } | undefined;
234
290
  }> | undefined;
235
291
  coreAxis?: "permissionDecision" | "toolEffect" | "egress" | "irreversibility" | "safetyAxis" | "severity" | "shellGate" | undefined;
236
292
  }, {
@@ -255,6 +311,10 @@ export declare const MergeCategorySpec: z.ZodObject<{
255
311
  members?: Record<string, {
256
312
  direction: "loosen" | "tighten";
257
313
  minTrust?: "local" | "session" | "global" | "project" | "managed" | "task" | undefined;
314
+ boolPole?: {
315
+ strict: boolean;
316
+ minTrust: "local" | "session" | "global" | "project" | "managed" | "task";
317
+ } | undefined;
258
318
  }> | undefined;
259
319
  coreAxis?: "permissionDecision" | "toolEffect" | "egress" | "irreversibility" | "safetyAxis" | "severity" | "shellGate" | undefined;
260
320
  }>;
@@ -341,6 +401,88 @@ export declare const SAFETY_MERGE_SPEC: {
341
401
  readonly fail: "closed";
342
402
  readonly seam: "ExecutionEnv (fs+net) + sensitive-path";
343
403
  readonly coreAxis: "egress";
404
+ readonly members: {
405
+ readonly enabled: {
406
+ readonly direction: "tighten";
407
+ readonly minTrust: "global";
408
+ readonly boolPole: {
409
+ readonly strict: true;
410
+ readonly minTrust: "local";
411
+ };
412
+ };
413
+ readonly allowUnsandboxedCommands: {
414
+ readonly direction: "tighten";
415
+ readonly minTrust: "global";
416
+ readonly boolPole: {
417
+ readonly strict: false;
418
+ readonly minTrust: "local";
419
+ };
420
+ };
421
+ readonly autoAllowBashIfSandboxed: {
422
+ readonly direction: "tighten";
423
+ readonly minTrust: "global";
424
+ readonly boolPole: {
425
+ readonly strict: false;
426
+ readonly minTrust: "local";
427
+ };
428
+ };
429
+ readonly enableWeakerNestedSandbox: {
430
+ readonly direction: "tighten";
431
+ readonly minTrust: "global";
432
+ readonly boolPole: {
433
+ readonly strict: false;
434
+ readonly minTrust: "local";
435
+ };
436
+ };
437
+ readonly enableWeakerNetworkIsolation: {
438
+ readonly direction: "tighten";
439
+ readonly minTrust: "global";
440
+ readonly boolPole: {
441
+ readonly strict: false;
442
+ readonly minTrust: "local";
443
+ };
444
+ };
445
+ readonly "network.allowManagedDomainsOnly": {
446
+ readonly direction: "tighten";
447
+ readonly minTrust: "global";
448
+ readonly boolPole: {
449
+ readonly strict: true;
450
+ readonly minTrust: "local";
451
+ };
452
+ };
453
+ readonly "network.allowAllUnixSockets": {
454
+ readonly direction: "tighten";
455
+ readonly minTrust: "global";
456
+ readonly boolPole: {
457
+ readonly strict: false;
458
+ readonly minTrust: "local";
459
+ };
460
+ };
461
+ readonly "network.allowLocalBinding": {
462
+ readonly direction: "tighten";
463
+ readonly minTrust: "global";
464
+ readonly boolPole: {
465
+ readonly strict: false;
466
+ readonly minTrust: "local";
467
+ };
468
+ };
469
+ readonly "filesystem.allowManagedReadPathsOnly": {
470
+ readonly direction: "tighten";
471
+ readonly minTrust: "global";
472
+ readonly boolPole: {
473
+ readonly strict: true;
474
+ readonly minTrust: "local";
475
+ };
476
+ };
477
+ readonly "filesystem.denyWrite": {
478
+ readonly direction: "tighten";
479
+ readonly minTrust: "local";
480
+ };
481
+ readonly "filesystem.denyRead": {
482
+ readonly direction: "tighten";
483
+ readonly minTrust: "local";
484
+ };
485
+ };
344
486
  };
345
487
  readonly hooks: {
346
488
  readonly mergeShape: "runAll";
@@ -462,12 +604,16 @@ export declare function membersOf<C extends SafetyMergeCategory>(category: C): r
462
604
  * Fail-CLOSED and loud on both misuse arms — a category without `members` has no member to name, and a member
463
605
  * outside the declared set is a caller bug, not "use the category floor" (that silent arm is exactly what would
464
606
  * let a typo admit a bucket under the wrong rule).
607
+ * Value-aware (codex r3): pass the CANDIDATE VALUE being written for a value-directional (`boolPole`) member —
608
+ * the strict pole resolves to `boolPole.minTrust`, everything else (loose pole / non-boolean / no value) to the
609
+ * row floor. Rows without `boolPole` ignore the value entirely.
465
610
  * @param categoryLabel only decorates the error message (the spec value carries no name of its own).
466
611
  */
467
- export declare function resolveMemberAdmission(spec: MergeCategorySpec, member: string, categoryLabel?: string): ResolvedMemberAdmission;
612
+ export declare function resolveMemberAdmission(spec: MergeCategorySpec, member: string, categoryLabel?: string, value?: unknown): ResolvedMemberAdmission;
468
613
  /** A member's resolved admission (direction + floor, category fallback applied) from the published table.
469
- * Unknown category or member ⇒ RangeError (never a fallback). */
470
- export declare function memberAdmission<C extends SafetyMergeCategory>(category: C, member: SafetyMergeMember<C>): ResolvedMemberAdmission;
614
+ * Unknown category or member ⇒ RangeError (never a fallback). Value-aware: pass the candidate value for a
615
+ * `boolPole` member (see {@link resolveMemberAdmission}); omitting it answers the fail-closed row floor. */
616
+ export declare function memberAdmission<C extends SafetyMergeCategory>(category: C, member: SafetyMergeMember<C>, value?: unknown): ResolvedMemberAdmission;
471
617
  /** A member's admission FLOOR from the published table — the word a resolver compares the layer's trust against
472
618
  * (`isAtLeast(layer, floor)` ⇒ admitted; the ladder is {@link trustRank}). Convenience over {@link memberAdmission}. */
473
- export declare function memberMinTrust<C extends SafetyMergeCategory>(category: C, member: SafetyMergeMember<C>): MinTrust;
619
+ export declare function memberMinTrust<C extends SafetyMergeCategory>(category: C, member: SafetyMergeMember<C>, value?: unknown): MinTrust;
@@ -14,7 +14,7 @@
14
14
  * admitted or rejected as one value under its `minTrust`. A resolver reads this table and has NO per-key
15
15
  * special-casing.
16
16
  *
17
- * MEMBER-LEVEL ADMISSION (1.1.0 — the WRITTEN layer of the permission-rule dual-channel ruling, board [5797] ④;
17
+ * MEMBER-LEVEL ADMISSION (1.1.0 — the WRITTEN layer of the permission-rule dual-channel ruling, [ref] ④;
18
18
  * the executing layer is core's `prepareCcImport` door + `RuleAddOrigin`, the resolver/shell halves are cli's):
19
19
  * A category may declare `members`, each with its own admission floor and DIRECTION. Today only `permissions`
20
20
  * does — `allow` / `deny` / `ask`, a CLOSED set ({@link SafetyMergeMember}; an unknown member is a compile error
@@ -39,7 +39,7 @@
39
39
  * requiresRealApproval). Consumers read admission through {@link memberAdmission} / {@link memberMinTrust} /
40
40
  * {@link membersOf} — never through a hardcoded allow/deny/ask table of their own (single source).
41
41
  *
42
- * TRUST ORDERING PRIMITIVES (1.2.0 — board [5796] ordering question → [5805] claim / [5807] consumer statement):
42
+ * TRUST ORDERING PRIMITIVES (1.2.0 — [ref] ordering question → claim / consumer statement):
43
43
  * The floor words are only meaningful against a LADDER, and that ladder is v2-design §1:29, verbatim:
44
44
  * `MANAGED 6 > GLOBAL 5 > PROJECT 4 > LOCAL 3 > SESSION 2 > TASK 1`. {@link trustRank} publishes exactly those
45
45
  * numbers and {@link isAtLeast} the comparison a resolver makes (`rank(layer) ≥ rank(floor)` ⇒ admitted), so no
@@ -95,10 +95,21 @@ export function isAtLeast(layer, minTrust) {
95
95
  * 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
- * only by SAYING so; omission never lowers it (fail-closed by construction). */
98
+ * only by SAYING so; omission never lowers it (fail-closed by construction).
99
+ *
100
+ * `boolPole` (S-59 件八, codex r3 [high]) — VALUE-DIRECTIONAL admission for a boolean member whose two poles
101
+ * point opposite ways (`enabled:true` narrows the sandbox, `enabled:false` opens it): a write EQUAL to
102
+ * `boolPole.strict` (strict equality, never truthiness — a string "true" is not the pole) is the tightening
103
+ * write and is admitted at `boolPole.minTrust`; ANY other write — the loose pole, a non-boolean, or a
104
+ * valueless query — is admitted at the ROW floor. 🔴 Vintage-safety law: a boolPole row's OWN `minTrust`
105
+ * must be the LOOSEN floor (the category's), so a consumer that predates this field (its schema strips
106
+ * `boolPole`) reads the row as tighten@<loosen-floor> and withholds BOTH poles from low-trust layers —
107
+ * fail-closed (it loses only the convenience of low-trust tightening, it can never admit a loosening write).
108
+ * Resolution is value-aware: pass the candidate value to {@link memberAdmission} / {@link memberMinTrust}. */
99
109
  export const MemberAdmission = z.object({
100
110
  direction: MemberDirection,
101
111
  minTrust: MinTrust.optional(),
112
+ boolPole: z.object({ strict: z.boolean(), minTrust: MinTrust }).optional(),
102
113
  });
103
114
  /** WHERE the category is enforced (STRENGTH) — MUST stay in lockstep with `EffectiveKey.enforcement` (types.ts).
104
115
  * gate=data-plane engine, server=control-plane/TOB boundary, resolver=merger correctness, client=UI only. */
@@ -157,7 +168,56 @@ export const SAFETY_MERGE_SPEC = {
157
168
  handsReadOnly: { mergeShape: "replace", minTrust: "global", tighten: { rule: "boolStricter", strict: true }, enforcement: "gate", fail: "closed", seam: "handsReadOnly" },
158
169
  shellGate: { mergeShape: "replace", minTrust: "global", tighten: { rule: "enumRank", order: ["off", "classify", "always"] }, enforcement: "gate", fail: "closed", seam: "shellGate", coreAxis: "shellGate" },
159
170
  approver: { mergeShape: "replace", minTrust: "global", tighten: { rule: "none" }, enforcement: "gate", fail: "closed", seam: "onAsk (conflict → reject)" },
160
- sandboxNetwork: { mergeShape: "denyFirst", minTrust: "global", tighten: { rule: "setNarrower" }, enforcement: "gate", fail: "closed", seam: "ExecutionEnv (fs+net) + sensitive-path", coreAxis: "egress" },
171
+ // S-59 件八 member-level admission for the sandbox category (the shell's temporary "which sub-key counts
172
+ // as tightening" table lifts upstream; its gate keeps only the VALUE-pole half as executing detail).
173
+ // The members below are the CLOSED SET a low-trust settings layer may still set — restricting needs no trust,
174
+ // so each is tighten@"local" (the lowest settings-FILE tier, same law as permissions.deny/ask). Direction
175
+ // evidence, per member, from the shell's sandbox schema `.describe()` texts (its `SandboxSettingsSchema`):
176
+ // · enabled the sandbox being ON confines commands ⇒ setting `true` tightens (false = loosen pole);
177
+ // · allowUnsandboxedCommands `false` = the unsandboxed escape hatch is refused ⇒ tighten (true = loosen pole);
178
+ // · autoAllowBashIfSandboxed `false` = no auto-allow just for being sandboxed ⇒ tighten;
179
+ // · enableWeakerNestedSandbox `false` = the weaker nested sandbox stays off ⇒ tighten;
180
+ // · enableWeakerNetworkIsolation `false` = the "Reduces security" switch stays off ⇒ tighten;
181
+ // · network.allowManagedDomainsOnly `true` = only managed-layer domains honoured ⇒ tighten (its CONSUMER
182
+ // additionally reads policy settings only — that is the shell schema's own semantics, not admission law);
183
+ // · network.allowAllUnixSockets / network.allowLocalBinding `false` = the opening stays shut ⇒ tighten;
184
+ // · filesystem.allowManagedReadPathsOnly `true` ⇒ tighten (same managed-only consumer caveat);
185
+ // · filesystem.denyWrite / filesystem.denyRead — deny arrays only ever narrow ⇒ tighten in every write.
186
+ // A boolean member's admission is VALUE-directional and the pole is MODELED (codex r3 [high]): each boolean
187
+ // row carries `boolPole {strict, minTrust:"local"}` and keeps its ROW floor at the loosen floor "global" —
188
+ // value-aware consumers admit the strict pole at "local"; pole-blind consumers (whose schema strips boolPole)
189
+ // read tighten@global and withhold BOTH poles from low-trust layers (fail-closed: convenience lost, never a
190
+ // loosening write admitted). The two deny arrays narrow in every write, so they are plain tighten@"local".
191
+ // DELIBERATELY NOT members (⇒ the 1.1.0 header law: an undeclared member stays under the category minTrust
192
+ // "global" — exactly the consumer's fail-closed withhold arm, so FUTURE sub-keys are withheld by default):
193
+ // · the loosen face — network.allowedDomains / network.allowUnixSockets / filesystem.allowWrite /
194
+ // filesystem.allowRead (re-allow within denyRead = a loosener) / excludedCommands (named commands escape
195
+ // the sandbox) / ignoreViolations / ripgrep (binary override) — widening is a privilege;
196
+ // · failIfUnavailable — tightening on the security axis but its consequence is REFUSING TO START: admitted
197
+ // low, a cloned repo writing {enabled:true, failIfUnavailable:true} turns a "tighten" key into a
198
+ // denial-of-service primitive on any sandbox-less machine. An availability hard-gate is an operator's
199
+ // decision ⇒ it rides the loosen floor. ("Tightening is always safe" holds INSIDE this boundary — the
200
+ // boundary is written down rather than the sentence deleted.)
201
+ // · INDETERMINATE (flagged for the shell to confirm, fail-closed meanwhile): network.httpProxyPort /
202
+ // network.socksProxyPort (strictly routing, but they can steer sandboxed traffic to an arbitrary local
203
+ // port) and enabledPlatforms (platform-set edits cut both ways) — unlisted ⇒ category floor.
204
+ sandboxNetwork: {
205
+ mergeShape: "denyFirst", minTrust: "global", tighten: { rule: "setNarrower" }, enforcement: "gate", fail: "closed",
206
+ seam: "ExecutionEnv (fs+net) + sensitive-path", coreAxis: "egress",
207
+ members: {
208
+ enabled: { direction: "tighten", minTrust: "global", boolPole: { strict: true, minTrust: "local" } },
209
+ allowUnsandboxedCommands: { direction: "tighten", minTrust: "global", boolPole: { strict: false, minTrust: "local" } },
210
+ autoAllowBashIfSandboxed: { direction: "tighten", minTrust: "global", boolPole: { strict: false, minTrust: "local" } },
211
+ enableWeakerNestedSandbox: { direction: "tighten", minTrust: "global", boolPole: { strict: false, minTrust: "local" } },
212
+ enableWeakerNetworkIsolation: { direction: "tighten", minTrust: "global", boolPole: { strict: false, minTrust: "local" } },
213
+ "network.allowManagedDomainsOnly": { direction: "tighten", minTrust: "global", boolPole: { strict: true, minTrust: "local" } },
214
+ "network.allowAllUnixSockets": { direction: "tighten", minTrust: "global", boolPole: { strict: false, minTrust: "local" } },
215
+ "network.allowLocalBinding": { direction: "tighten", minTrust: "global", boolPole: { strict: false, minTrust: "local" } },
216
+ "filesystem.allowManagedReadPathsOnly": { direction: "tighten", minTrust: "global", boolPole: { strict: true, minTrust: "local" } },
217
+ "filesystem.denyWrite": { direction: "tighten", minTrust: "local" },
218
+ "filesystem.denyRead": { direction: "tighten", minTrust: "local" },
219
+ },
220
+ },
161
221
  hooks: { mergeShape: "runAll", minTrust: "project", tighten: { rule: "none" }, enforcement: "gate", fail: "closed", seam: "hooks (single source or adapter-composed)" },
162
222
  mcpServers: { mergeShape: "overrideByName", minTrust: "project", tighten: { rule: "none" }, enforcement: "resolver", fail: "open", seam: "spec.mcp + allowTools" },
163
223
  rulesInstructions: { mergeShape: "concat", minTrust: "project", tighten: { rule: "none" }, enforcement: "resolver", fail: "open", seam: "appendSystemPrompt / memory (GLOBAL→PROJECT→SESSION)" },
@@ -191,9 +251,12 @@ export function membersOf(category) {
191
251
  * Fail-CLOSED and loud on both misuse arms — a category without `members` has no member to name, and a member
192
252
  * outside the declared set is a caller bug, not "use the category floor" (that silent arm is exactly what would
193
253
  * let a typo admit a bucket under the wrong rule).
254
+ * Value-aware (codex r3): pass the CANDIDATE VALUE being written for a value-directional (`boolPole`) member —
255
+ * the strict pole resolves to `boolPole.minTrust`, everything else (loose pole / non-boolean / no value) to the
256
+ * row floor. Rows without `boolPole` ignore the value entirely.
194
257
  * @param categoryLabel only decorates the error message (the spec value carries no name of its own).
195
258
  */
196
- export function resolveMemberAdmission(spec, member, categoryLabel = "(unnamed category)") {
259
+ export function resolveMemberAdmission(spec, member, categoryLabel = "(unnamed category)", value) {
197
260
  if (spec.members === undefined) {
198
261
  throw new RangeError(`SAFETY_MERGE_SPEC: category '${categoryLabel}' has no member-level admission — its minTrust applies to the whole key`);
199
262
  }
@@ -201,15 +264,21 @@ export function resolveMemberAdmission(spec, member, categoryLabel = "(unnamed c
201
264
  throw new RangeError(`SAFETY_MERGE_SPEC: unknown member '${member}' of category '${categoryLabel}' (members: ${Object.keys(spec.members).join(", ")})`);
202
265
  }
203
266
  const row = spec.members[member];
204
- return { direction: row.direction, minTrust: row.minTrust ?? spec.minTrust };
267
+ const rowFloor = row.minTrust ?? spec.minTrust;
268
+ if (row.boolPole !== undefined) {
269
+ const floor = value === row.boolPole.strict ? row.boolPole.minTrust : rowFloor;
270
+ return { direction: row.direction, minTrust: floor, boolPole: row.boolPole };
271
+ }
272
+ return { direction: row.direction, minTrust: rowFloor };
205
273
  }
206
274
  /** A member's resolved admission (direction + floor, category fallback applied) from the published table.
207
- * Unknown category or member ⇒ RangeError (never a fallback). */
208
- export function memberAdmission(category, member) {
209
- return resolveMemberAdmission(categorySpec(category), member, category);
275
+ * Unknown category or member ⇒ RangeError (never a fallback). Value-aware: pass the candidate value for a
276
+ * `boolPole` member (see {@link resolveMemberAdmission}); omitting it answers the fail-closed row floor. */
277
+ export function memberAdmission(category, member, value) {
278
+ return resolveMemberAdmission(categorySpec(category), member, category, value);
210
279
  }
211
280
  /** A member's admission FLOOR from the published table — the word a resolver compares the layer's trust against
212
281
  * (`isAtLeast(layer, floor)` ⇒ admitted; the ladder is {@link trustRank}). Convenience over {@link memberAdmission}. */
213
- export function memberMinTrust(category, member) {
214
- return memberAdmission(category, member).minTrust;
282
+ export function memberMinTrust(category, member, value) {
283
+ return memberAdmission(category, member, value).minTrust;
215
284
  }
@@ -30,7 +30,7 @@ export type SchedulerStoreMutation = {
30
30
  reason: 'aborted';
31
31
  };
32
32
  /**
33
- * The ONE cross-process mutation primitive for the shared scheduler store (board [1001]①/[1003]②
33
+ * The ONE cross-process mutation primitive for the shared scheduler store ([ref]①/②
34
34
  * server L1-2 + shell 对抗评审 F1 双向合流后的契约层统一原语). Every WRITER (engine backend, resident
35
35
  * daemon fire/reap, session reap, sweeps) must go through this instead of hand-rolled
36
36
  * load→filter→save — the naked sequence is a lost-update race: two writers load the same base
@@ -47,7 +47,7 @@ export function fileSchedulerStore(path) {
47
47
  };
48
48
  }
49
49
  /**
50
- * The ONE cross-process mutation primitive for the shared scheduler store (board [1001]①/[1003]②
50
+ * The ONE cross-process mutation primitive for the shared scheduler store ([ref]①/②
51
51
  * server L1-2 + shell 对抗评审 F1 双向合流后的契约层统一原语). Every WRITER (engine backend, resident
52
52
  * daemon fire/reap, session reap, sweeps) must go through this instead of hand-rolled
53
53
  * load→filter→save — the naked sequence is a lost-update race: two writers load the same base
@@ -44,7 +44,7 @@ export function isSchedulerRecord(v) {
44
44
  * record must leave a trace reachable by whoever can act on it).
45
45
  */
46
46
  /**
47
- * core 5.5.0 durable 键分叉的迁移半场(board [2439]/[2440]/[2444];server 裁 (a) 一次性迁移,拒读侧
47
+ * core 5.5.0 durable 键分叉的迁移半场([ref];server 裁 (a) 一次性迁移,拒读侧
48
48
  * 双键容忍窗)。旧铸键链 `sessionId ?? principal ?? taskId ?? "default"` 让 durable 行的隔离键随会话
49
49
  * 走——新会话新 sessionId ⇒ 旧 durable job 永远 not_found。core 5.5.0 起 durable 铸键=
50
50
  * `principal ?? "default"`;存量旧键行由**本层**在 parse 时重铸(纯函数,幂等:重写后判据自不满足)。