@wrenai/genbi 0.0.4 → 0.0.5

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 (230) hide show
  1. package/README.md +64 -2
  2. package/dist/assets/{echarts-LTFC-ysd.js → echarts-DYM4hjvk.js} +2 -2
  3. package/dist/assets/index-A_Wrq7Ey.css +1 -0
  4. package/dist/assets/{index-Bo--QMUN.js → index-DsH129tV.js} +18 -17
  5. package/dist/index.html +2 -2
  6. package/dist-server/harness/bundle/schema.d.ts +38 -0
  7. package/dist-server/harness/bundle/schema.js +18 -10
  8. package/dist-server/harness/bundle/version.js +1 -1
  9. package/dist-server/harness/cli-args.d.ts +10 -2
  10. package/dist-server/harness/cli-args.js +24 -8
  11. package/dist-server/harness/cli.js +75 -9
  12. package/dist-server/harness/compile/cache.js +4 -4
  13. package/dist-server/harness/compile/compose-profile.d.ts +51 -14
  14. package/dist-server/harness/compile/compose-profile.js +85 -19
  15. package/dist-server/harness/compile/context-loader.d.ts +56 -0
  16. package/dist-server/harness/compile/context-loader.js +237 -0
  17. package/dist-server/harness/compile/errors.d.ts +19 -0
  18. package/dist-server/harness/compile/errors.js +35 -0
  19. package/dist-server/harness/compile/index.d.ts +4 -3
  20. package/dist-server/harness/compile/index.js +4 -3
  21. package/dist-server/harness/compile/pipeline.d.ts +15 -7
  22. package/dist-server/harness/compile/pipeline.js +62 -14
  23. package/dist-server/harness/compile/resolve-binary.d.ts +2 -2
  24. package/dist-server/harness/compile/resolve-binary.js +3 -3
  25. package/dist-server/harness/compile/types.d.ts +29 -0
  26. package/dist-server/harness/compile/warble-identity.d.ts +11 -0
  27. package/dist-server/harness/compile/warble-identity.js +13 -0
  28. package/dist-server/harness/components/ai-step.d.ts +4 -0
  29. package/dist-server/harness/components/ai-step.js +40 -0
  30. package/dist-server/harness/components/broker.d.ts +60 -0
  31. package/dist-server/harness/components/broker.js +117 -0
  32. package/dist-server/harness/components/capability-card.d.ts +98 -0
  33. package/dist-server/harness/components/capability-card.js +169 -0
  34. package/dist-server/harness/components/context.d.ts +2 -0
  35. package/dist-server/harness/components/context.js +30 -0
  36. package/dist-server/harness/components/display.d.ts +6 -0
  37. package/dist-server/harness/components/display.js +29 -0
  38. package/dist-server/harness/components/egress-judge.d.ts +12 -0
  39. package/dist-server/harness/components/egress-judge.js +27 -0
  40. package/dist-server/harness/components/egress.d.ts +142 -0
  41. package/dist-server/harness/components/egress.js +469 -0
  42. package/dist-server/harness/components/in-process.d.ts +7 -0
  43. package/dist-server/harness/components/in-process.js +245 -0
  44. package/dist-server/harness/components/normalize.d.ts +35 -0
  45. package/dist-server/harness/components/normalize.js +245 -0
  46. package/dist-server/harness/components/plan.d.ts +31 -0
  47. package/dist-server/harness/components/plan.js +184 -0
  48. package/dist-server/harness/components/report.d.ts +207 -0
  49. package/dist-server/harness/components/report.js +309 -0
  50. package/dist-server/harness/components/runner.d.ts +193 -0
  51. package/dist-server/harness/components/runner.js +492 -0
  52. package/dist-server/harness/components/semantic-guards.d.ts +20 -0
  53. package/dist-server/harness/components/semantic-guards.js +42 -0
  54. package/dist-server/harness/components/terminal-json.d.ts +18 -0
  55. package/dist-server/harness/components/terminal-json.js +82 -0
  56. package/dist-server/harness/components/usage.d.ts +14 -0
  57. package/dist-server/harness/components/usage.js +19 -0
  58. package/dist-server/harness/components/wren-access.d.ts +29 -0
  59. package/dist-server/harness/components/wren-access.js +233 -0
  60. package/dist-server/harness/components/zone-dry-run.d.ts +43 -0
  61. package/dist-server/harness/components/zone-dry-run.js +107 -0
  62. package/dist-server/harness/components/zone-gate.d.ts +76 -0
  63. package/dist-server/harness/components/zone-gate.js +144 -0
  64. package/dist-server/harness/events/index.d.ts +1 -1
  65. package/dist-server/harness/events/types.d.ts +52 -0
  66. package/dist-server/harness/index.d.ts +9 -2
  67. package/dist-server/harness/index.js +5 -1
  68. package/dist-server/harness/loop/executor.d.ts +1 -0
  69. package/dist-server/harness/loop/executor.js +37 -15
  70. package/dist-server/harness/npm-package-resolve.d.ts +2 -0
  71. package/dist-server/harness/npm-package-resolve.js +16 -4
  72. package/dist-server/harness/providers/adapters/openai-compatible.d.ts +13 -0
  73. package/dist-server/harness/providers/adapters/openai-compatible.js +17 -0
  74. package/dist-server/harness/providers/adapters/openai.d.ts +7 -0
  75. package/dist-server/harness/providers/adapters/openai.js +6 -0
  76. package/dist-server/harness/providers/adapters/pi-ai.d.ts +48 -0
  77. package/dist-server/harness/providers/adapters/pi-ai.js +518 -0
  78. package/dist-server/harness/providers/binding.d.ts +45 -4
  79. package/dist-server/harness/providers/binding.js +44 -8
  80. package/dist-server/harness/providers/index.d.ts +9 -3
  81. package/dist-server/harness/providers/index.js +10 -3
  82. package/dist-server/harness/providers/zone.d.ts +96 -0
  83. package/dist-server/harness/providers/zone.js +112 -0
  84. package/dist-server/harness/render/answering-query.d.ts +29 -0
  85. package/dist-server/harness/render/answering-query.js +62 -0
  86. package/dist-server/harness/render/envelope.d.ts +6 -0
  87. package/dist-server/harness/render/envelope.js +10 -3
  88. package/dist-server/harness/route/adapter-spec.d.ts +8 -9
  89. package/dist-server/harness/route/adapter-spec.js +13 -14
  90. package/dist-server/harness/route/codex-ask-events.js +2 -1
  91. package/dist-server/harness/route/codex-ask.d.ts +4 -0
  92. package/dist-server/harness/route/codex-ask.js +69 -30
  93. package/dist-server/harness/route/codex-host-query.d.ts +52 -0
  94. package/dist-server/harness/route/codex-host-query.js +191 -0
  95. package/dist-server/harness/route/codex-local-manifest.d.ts +9 -1
  96. package/dist-server/harness/route/codex-local-manifest.js +96 -15
  97. package/dist-server/harness/route/codex-wren-proxy.d.ts +33 -0
  98. package/dist-server/harness/route/codex-wren-proxy.js +227 -0
  99. package/dist-server/harness/route/describe.js +45 -6
  100. package/dist-server/harness/route/in-process.d.ts +22 -1
  101. package/dist-server/harness/route/in-process.js +54 -4
  102. package/dist-server/harness/route/route.js +5 -2
  103. package/dist-server/harness/route/types.d.ts +30 -1
  104. package/dist-server/harness/session/run.js +30 -10
  105. package/dist-server/harness/setup/runner.d.ts +2 -0
  106. package/dist-server/harness/setup/runner.js +38 -17
  107. package/dist-server/harness/setup/yaml-scalar.d.ts +23 -0
  108. package/dist-server/harness/setup/yaml-scalar.js +62 -0
  109. package/dist-server/server/adopt.js +1 -17
  110. package/dist-server/server/app.d.ts +1 -0
  111. package/dist-server/server/app.js +163 -14
  112. package/dist-server/server/auth-choice.d.ts +13 -1
  113. package/dist-server/server/auth-choice.js +66 -1
  114. package/dist-server/server/bin.js +177 -25
  115. package/dist-server/server/codex-local-grounding.d.ts +18 -0
  116. package/dist-server/server/codex-local-grounding.js +92 -0
  117. package/dist-server/server/codex-native-session.d.ts +25 -0
  118. package/dist-server/server/codex-native-session.js +1 -0
  119. package/dist-server/server/codex-runtime-composition.d.ts +34 -0
  120. package/dist-server/server/codex-runtime-composition.js +250 -0
  121. package/dist-server/server/codex-structured-ask.d.ts +23 -0
  122. package/dist-server/server/codex-structured-ask.js +102 -0
  123. package/dist-server/server/conn-config.js +4 -16
  124. package/dist-server/server/conversation-socket.d.ts +16 -0
  125. package/dist-server/server/conversation-socket.js +65 -0
  126. package/dist-server/server/db.d.ts +46 -0
  127. package/dist-server/server/db.js +88 -9
  128. package/dist-server/server/env-detect.d.ts +6 -0
  129. package/dist-server/server/env-detect.js +7 -0
  130. package/dist-server/server/fold.d.ts +22 -4
  131. package/dist-server/server/fold.js +37 -5
  132. package/dist-server/server/harness.d.ts +2 -2
  133. package/dist-server/server/harness.js +33 -7
  134. package/dist-server/server/interactive-terminal.d.ts +10 -6
  135. package/dist-server/server/interactive-terminal.js +33 -8
  136. package/dist-server/server/managed-wren-runtime.d.ts +116 -0
  137. package/dist-server/server/managed-wren-runtime.js +670 -0
  138. package/dist-server/server/native-artifacts.d.ts +23 -7
  139. package/dist-server/server/native-artifacts.js +43 -12
  140. package/dist-server/server/native-component-context.d.ts +19 -0
  141. package/dist-server/server/native-component-context.js +76 -0
  142. package/dist-server/server/native-component-preparation.d.ts +20 -0
  143. package/dist-server/server/native-component-preparation.js +129 -0
  144. package/dist-server/server/native-component-runtime.d.ts +36 -0
  145. package/dist-server/server/native-component-runtime.js +92 -0
  146. package/dist-server/server/native-components.d.ts +84 -0
  147. package/dist-server/server/native-components.js +160 -0
  148. package/dist-server/server/native-dispatch-process.d.ts +8 -0
  149. package/dist-server/server/native-dispatch-process.js +65 -0
  150. package/dist-server/server/native-runtime-spec.d.ts +89 -0
  151. package/dist-server/server/native-runtime-spec.js +169 -0
  152. package/dist-server/server/native-sessions.d.ts +172 -19
  153. package/dist-server/server/native-sessions.js +839 -244
  154. package/dist-server/server/native-shutdown.d.ts +6 -0
  155. package/dist-server/server/native-shutdown.js +28 -0
  156. package/dist-server/server/native-wren-home.d.ts +18 -0
  157. package/dist-server/server/native-wren-home.js +262 -0
  158. package/dist-server/server/node-pty-host.d.ts +2 -1
  159. package/dist-server/server/node-pty-host.js +3 -2
  160. package/dist-server/server/profile-registry.d.ts +139 -0
  161. package/dist-server/server/profile-registry.js +620 -0
  162. package/dist-server/server/runtime-binding.d.ts +4 -1
  163. package/dist-server/server/runtime-binding.js +50 -2
  164. package/dist-server/server/runtime-host/claude-component-step.d.ts +25 -0
  165. package/dist-server/server/runtime-host/claude-component-step.js +186 -0
  166. package/dist-server/server/runtime-host/codex-app-server.d.ts +75 -0
  167. package/dist-server/server/runtime-host/codex-app-server.js +239 -0
  168. package/dist-server/server/runtime-host/codex-compatibility.d.ts +110 -0
  169. package/dist-server/server/runtime-host/codex-compatibility.js +162 -0
  170. package/dist-server/server/runtime-host/codex-component-step.d.ts +19 -0
  171. package/dist-server/server/runtime-host/codex-component-step.js +308 -0
  172. package/dist-server/server/runtime-host/codex-conversation.d.ts +109 -0
  173. package/dist-server/server/runtime-host/codex-conversation.js +295 -0
  174. package/dist-server/server/runtime-host/codex-direct-tools.d.ts +23 -0
  175. package/dist-server/server/runtime-host/codex-direct-tools.js +59 -0
  176. package/dist-server/server/runtime-host/codex-events.d.ts +647 -0
  177. package/dist-server/server/runtime-host/codex-events.js +66 -0
  178. package/dist-server/server/runtime-host/codex-policy.d.ts +16 -0
  179. package/dist-server/server/runtime-host/codex-policy.js +151 -0
  180. package/dist-server/server/runtime-host/codex-process.d.ts +8 -0
  181. package/dist-server/server/runtime-host/codex-process.js +96 -0
  182. package/dist-server/server/runtime-host/codex-rpc.d.ts +47 -0
  183. package/dist-server/server/runtime-host/codex-rpc.js +182 -0
  184. package/dist-server/server/runtime-host/codex-session.d.ts +74 -0
  185. package/dist-server/server/runtime-host/codex-session.js +492 -0
  186. package/dist-server/server/runtime-host/codex-terminal-protocol.d.ts +35 -0
  187. package/dist-server/server/runtime-host/codex-terminal-protocol.js +243 -0
  188. package/dist-server/server/runtime-host/codex-terminal.d.ts +16 -0
  189. package/dist-server/server/runtime-host/codex-terminal.js +120 -0
  190. package/dist-server/server/runtime-host/index.d.ts +5 -0
  191. package/dist-server/server/runtime-host/index.js +3 -0
  192. package/dist-server/server/runtime-host/local.d.ts +29 -0
  193. package/dist-server/server/runtime-host/local.js +90 -0
  194. package/dist-server/server/runtime-host/policy.d.ts +8 -0
  195. package/dist-server/server/runtime-host/policy.js +78 -0
  196. package/dist-server/server/runtime-host/types.d.ts +52 -0
  197. package/dist-server/server/runtime-host/types.js +62 -0
  198. package/dist-server/server/structured-query-evidence.d.ts +7 -0
  199. package/dist-server/server/structured-query-evidence.js +47 -0
  200. package/dist-server/server/structured-runtime.d.ts +52 -0
  201. package/dist-server/server/structured-runtime.js +130 -0
  202. package/dist-server/server/structured-scope.d.ts +4 -0
  203. package/dist-server/server/structured-scope.js +12 -0
  204. package/dist-server/server/turn.d.ts +6 -1
  205. package/dist-server/server/turn.js +82 -25
  206. package/dist-server/server/wire-types.d.ts +67 -3
  207. package/dist-server/server/wren-profiles.js +1 -1
  208. package/managed-wren/README.md +60 -0
  209. package/managed-wren/dependency-wheels.json +282 -0
  210. package/managed-wren/manifest.json +373 -0
  211. package/managed-wren/release-inputs.json +45 -0
  212. package/managed-wren/runtime-tree.cjs +115 -0
  213. package/managed-wren/source-inputs.json +40 -0
  214. package/package.json +15 -6
  215. package/profiles/genbi-default/context/binding.yml +17 -7
  216. package/profiles/genbi-default/context/context.json +146 -0
  217. package/profiles/genbi-default/ir.golden.json +39 -43
  218. package/profiles/genbi-default/profile.yml +31 -0
  219. package/profiles/genbi-enrich-context/context/binding.yml +14 -3
  220. package/profiles/genbi-enrich-context/context/context.json +146 -0
  221. package/profiles/genbi-enrich-context/ir.golden.json +4 -5
  222. package/profiles/genbi-monitor/context/binding.yml +13 -5
  223. package/profiles/genbi-monitor/context/context.json +146 -0
  224. package/profiles/genbi-monitor/ir.golden.json +13 -7
  225. package/profiles/genbi-report/context/binding.yml +19 -0
  226. package/profiles/genbi-report/context/context.json +223 -0
  227. package/profiles/genbi-report/ir.golden.json +328 -0
  228. package/profiles/genbi-report/profile.yml +38 -0
  229. package/profiles/genbi-setup/ir.golden.json +3 -1
  230. package/dist/assets/index-Xs4q7pNo.css +0 -1
@@ -0,0 +1,14 @@
1
+ import type { LanguageModel } from "ai";
2
+ import type { UsageRecord, UsageTrace } from "../events/types.js";
3
+ export interface StepUsage {
4
+ readonly inputTokens: number;
5
+ readonly outputTokens: number;
6
+ }
7
+ /** Provider-reported counts; a missing or malformed count is recorded as zero, never omitted. */
8
+ export declare function reportedUsage(usage: {
9
+ readonly inputTokens?: number | undefined;
10
+ readonly outputTokens?: number | undefined;
11
+ } | undefined): StepUsage;
12
+ export declare function modelIdOf(model: LanguageModel): string;
13
+ /** Per-step records plus their totals per provider, in first-seen order. */
14
+ export declare function summariseUsage(records: readonly UsageRecord[]): UsageTrace;
@@ -0,0 +1,19 @@
1
+ /** Provider-reported counts; a missing or malformed count is recorded as zero, never omitted. */
2
+ export function reportedUsage(usage) {
3
+ const count = (value) => value !== undefined && Number.isSafeInteger(value) && value >= 0 ? value : 0;
4
+ return { inputTokens: count(usage?.inputTokens), outputTokens: count(usage?.outputTokens) };
5
+ }
6
+ export function modelIdOf(model) {
7
+ return typeof model === "string" ? model : model.modelId;
8
+ }
9
+ /** Per-step records plus their totals per provider, in first-seen order. */
10
+ export function summariseUsage(records) {
11
+ const providers = new Map();
12
+ for (const record of records) {
13
+ const key = JSON.stringify([record.zone, record.adapter, record.model]);
14
+ const total = providers.get(key) ?? { zone: record.zone, adapter: record.adapter, model: record.model, calls: 0, inputTokens: 0, outputTokens: 0 };
15
+ providers.set(key, { ...total, calls: total.calls + 1,
16
+ inputTokens: total.inputTokens + record.inputTokens, outputTokens: total.outputTokens + record.outputTokens });
17
+ }
18
+ return { steps: [...records], providers: [...providers.values()] };
19
+ }
@@ -0,0 +1,29 @@
1
+ import type { ComponentAccess } from "./broker.js";
2
+ /**
3
+ * The closed vocabulary of governed query failures a model may see. The wire carries the
4
+ * class; the sentence shown to the model is owned here, so nothing a `wren` process writes
5
+ * (exception text, SQL, paths, credentials) can reach a model even if that process is stale
6
+ * or compromised. `transport` is host-only: the channel itself failed or closed.
7
+ */
8
+ export declare const GOVERNED_ERROR_CLASSES: readonly ["invalid_request", "model_not_found", "policy_rejected", "invalid_sql", "execution_failed", "timeout", "datasource_unavailable", "result_too_large", "internal", "transport"];
9
+ export type GovernedErrorClass = (typeof GOVERNED_ERROR_CLASSES)[number];
10
+ export declare class GovernedWrenError extends Error {
11
+ readonly errorClass: GovernedErrorClass;
12
+ constructor(errorClass: GovernedErrorClass);
13
+ }
14
+ export interface WrenAccessIdentity {
15
+ readonly environment: Readonly<NodeJS.ProcessEnv>;
16
+ readonly digest: string;
17
+ assertCurrent(): Promise<void>;
18
+ }
19
+ /** Capture once per root, before preparing any component. Secrets never leave the host. */
20
+ export declare function captureWrenAccessIdentity(project: string, source?: NodeJS.ProcessEnv): Promise<WrenAccessIdentity>;
21
+ /** One owned process captures the project model, policy and credentials once. */
22
+ export declare function openWrenComponentAccess(options: {
23
+ readonly executable: string;
24
+ readonly project: string;
25
+ readonly fingerprint: string;
26
+ readonly signal: AbortSignal;
27
+ readonly environment?: NodeJS.ProcessEnv;
28
+ readonly identity?: WrenAccessIdentity;
29
+ }): Promise<ComponentAccess>;
@@ -0,0 +1,233 @@
1
+ import { spawn } from "node:child_process";
2
+ import { createHash } from "node:crypto";
3
+ import { readFile, realpath } from "node:fs/promises";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import { z } from "zod";
7
+ import { hashDirectory } from "../compile/fingerprint.js";
8
+ /**
9
+ * The closed vocabulary of governed query failures a model may see. The wire carries the
10
+ * class; the sentence shown to the model is owned here, so nothing a `wren` process writes
11
+ * (exception text, SQL, paths, credentials) can reach a model even if that process is stale
12
+ * or compromised. `transport` is host-only: the channel itself failed or closed.
13
+ */
14
+ export const GOVERNED_ERROR_CLASSES = ["invalid_request", "model_not_found", "policy_rejected", "invalid_sql", "execution_failed",
15
+ "timeout", "datasource_unavailable", "result_too_large", "internal", "transport"];
16
+ const GOVERNED_ERROR_TEXT = Object.freeze({
17
+ invalid_request: "The request did not match the governed operation contract.",
18
+ model_not_found: "The query references a table that is not a model in the bound semantic context; query only the models it defines.",
19
+ policy_rejected: "The read-only analytical policy rejected the query: use one SELECT over the bound models with standard analytical functions only.",
20
+ invalid_sql: "The SQL could not be parsed or planned against the semantic context.",
21
+ execution_failed: "The data source rejected the query at execution time, for example an unknown column or a type mismatch.",
22
+ timeout: "The query exceeded the statement time limit.",
23
+ datasource_unavailable: "The bound data source could not be reached.",
24
+ result_too_large: "The result exceeded the governed byte limit; narrow the query.",
25
+ internal: "The governed operation failed for an unclassified reason.",
26
+ transport: "Governed Wren operation is unavailable.",
27
+ });
28
+ export class GovernedWrenError extends Error {
29
+ errorClass;
30
+ constructor(errorClass) {
31
+ super(`Governed Wren query failed [${errorClass}]: ${GOVERNED_ERROR_TEXT[errorClass]}`);
32
+ this.errorClass = errorClass;
33
+ this.name = "GovernedWrenError";
34
+ }
35
+ }
36
+ /** Only the class crosses; a message from the wire is never forwarded, and an unknown class is `internal`. */
37
+ function governedError(wire) {
38
+ const named = typeof wire === "string" ? undefined : wire.class;
39
+ const known = GOVERNED_ERROR_CLASSES.find((candidate) => candidate === named && candidate !== "transport");
40
+ return new GovernedWrenError(known ?? "internal");
41
+ }
42
+ const PROTOCOLS = ["wren-governed/1", "wren-governed/2"];
43
+ const reply = z.object({ id: z.number().int().nonnegative(), protocol: z.enum(PROTOCOLS).optional(), result: z.unknown().optional(),
44
+ error: z.union([z.string(), z.object({ class: z.string().min(1).max(64), message: z.string().max(1024) }).strict()]).optional() }).strict();
45
+ /** Capture once per root, before preparing any component. Secrets never leave the host. */
46
+ export async function captureWrenAccessIdentity(project, source = process.env) {
47
+ const environment = Object.freeze({ ...source });
48
+ const home = path.resolve(environment.WREN_HOME ?? path.join(environment.HOME ?? os.homedir(), ".wren"));
49
+ const files = [path.join(project, ".env"), ...[".env", "profiles.yml", "config.json"].map((name) => path.join(home, name))];
50
+ const fingerprint = async () => {
51
+ const hash = createHash("sha256").update(JSON.stringify(environment));
52
+ for (const file of files) {
53
+ hash.update(file).update("\0");
54
+ try {
55
+ hash.update(await realpath(file)).update("\0").update(await readFile(file));
56
+ }
57
+ catch (error) {
58
+ if (error.code !== "ENOENT")
59
+ throw new Error("Wren configuration is unavailable");
60
+ hash.update("absent");
61
+ }
62
+ }
63
+ return hash.digest("hex");
64
+ };
65
+ const digest = await fingerprint();
66
+ return Object.freeze({ environment, digest, async assertCurrent() {
67
+ if (await fingerprint() !== digest)
68
+ throw new Error("Wren configuration changed");
69
+ } });
70
+ }
71
+ /** One owned process captures the project model, policy and credentials once. */
72
+ export async function openWrenComponentAccess(options) {
73
+ options.signal.throwIfAborted();
74
+ const identity = options.identity ?? await captureWrenAccessIdentity(options.project, options.environment);
75
+ const assertProject = async () => {
76
+ await identity.assertCurrent();
77
+ if (await hashDirectory(options.project) !== options.fingerprint)
78
+ throw new Error("Component project changed");
79
+ options.signal.throwIfAborted();
80
+ };
81
+ await assertProject();
82
+ if (process.platform === "win32")
83
+ throw new Error("Governed process cleanup is unavailable on this platform");
84
+ const child = spawn(options.executable, ["governed-stdio", "--project", options.project], {
85
+ cwd: options.project, env: { ...identity.environment }, detached: true, stdio: ["pipe", "pipe", "pipe"],
86
+ });
87
+ child.stderr.resume();
88
+ let closed = false;
89
+ let closing;
90
+ let buffer = Buffer.alloc(0);
91
+ let nextId = 1;
92
+ let pending;
93
+ const unavailable = () => new GovernedWrenError("transport");
94
+ const ready = new Promise((resolve, reject) => { pending = { id: 0, resolve: () => resolve(), reject }; });
95
+ const signalGroup = (value) => {
96
+ if (!child.pid)
97
+ return;
98
+ try {
99
+ process.kill(-child.pid, value);
100
+ }
101
+ catch (error) {
102
+ if (error.code !== "ESRCH")
103
+ throw unavailable();
104
+ }
105
+ };
106
+ const alive = () => {
107
+ if (!child.pid)
108
+ return false;
109
+ try {
110
+ process.kill(-child.pid, 0);
111
+ return true;
112
+ }
113
+ catch (error) {
114
+ return error.code !== "ESRCH";
115
+ }
116
+ };
117
+ const waitGone = async (ms) => {
118
+ const deadline = performance.now() + ms;
119
+ while (alive() && performance.now() < deadline)
120
+ await new Promise((resolve) => setTimeout(resolve, 20));
121
+ return !alive();
122
+ };
123
+ const close = () => {
124
+ if (closing)
125
+ return closing;
126
+ closed = true;
127
+ pending?.reject(unavailable());
128
+ pending = undefined;
129
+ closing = (async () => {
130
+ try {
131
+ child.stdin.end();
132
+ if (await waitGone(100))
133
+ return;
134
+ signalGroup("SIGTERM");
135
+ if (await waitGone(300))
136
+ return;
137
+ signalGroup("SIGKILL");
138
+ if (!(await waitGone(2000)))
139
+ throw new Error("Governed Wren cleanup failed");
140
+ }
141
+ finally {
142
+ options.signal.removeEventListener("abort", abort);
143
+ child.stdin.destroy();
144
+ child.stdout.destroy();
145
+ child.stderr.destroy();
146
+ }
147
+ })();
148
+ return closing;
149
+ };
150
+ const abort = () => { void close().catch(() => { }); };
151
+ options.signal.addEventListener("abort", abort, { once: true });
152
+ child.on("error", abort);
153
+ child.on("exit", abort);
154
+ child.stdin.on("error", abort);
155
+ child.stdout.on("error", abort);
156
+ child.stdout.on("data", (chunk) => {
157
+ if (closed)
158
+ return;
159
+ buffer = Buffer.concat([buffer, chunk]);
160
+ if (buffer.length > 1_048_577) {
161
+ abort();
162
+ return;
163
+ }
164
+ const newline = buffer.indexOf(10);
165
+ if (newline < 0)
166
+ return;
167
+ try {
168
+ const value = reply.parse(JSON.parse(buffer.subarray(0, newline).toString("utf8")));
169
+ buffer = buffer.subarray(newline + 1);
170
+ if (buffer.length || !pending || value.id !== pending.id)
171
+ throw unavailable();
172
+ if (value.id === 0 ? value.protocol === undefined || Object.keys(value).length !== 2
173
+ : value.protocol !== undefined || (Object.hasOwn(value, "result") === Object.hasOwn(value, "error")))
174
+ throw unavailable();
175
+ const waiter = pending;
176
+ pending = undefined;
177
+ if (value.error !== undefined)
178
+ waiter.reject(governedError(value.error));
179
+ else
180
+ waiter.resolve(value.result);
181
+ }
182
+ catch {
183
+ abort();
184
+ }
185
+ });
186
+ if (options.signal.aborted)
187
+ abort();
188
+ const timer = setTimeout(abort, 10_000);
189
+ try {
190
+ await ready;
191
+ await assertProject();
192
+ }
193
+ catch (error) {
194
+ await close();
195
+ throw error;
196
+ }
197
+ finally {
198
+ clearTimeout(timer);
199
+ }
200
+ let tail = Promise.resolve();
201
+ const execute = (payload, signal) => {
202
+ const operation = tail.then(async () => {
203
+ signal.throwIfAborted();
204
+ if (closed)
205
+ throw unavailable();
206
+ await assertProject();
207
+ const input = JSON.stringify({ id: nextId, ...payload }) + "\n";
208
+ if (Buffer.byteLength(input) > 65_536)
209
+ throw unavailable();
210
+ const result = new Promise((resolve, reject) => { pending = { id: nextId++, resolve, reject }; });
211
+ signal.addEventListener("abort", abort, { once: true });
212
+ if (signal.aborted)
213
+ abort();
214
+ else
215
+ child.stdin.write(input);
216
+ try {
217
+ const value = await result;
218
+ signal.throwIfAborted();
219
+ await assertProject();
220
+ return value;
221
+ }
222
+ finally {
223
+ signal.removeEventListener("abort", abort);
224
+ }
225
+ });
226
+ tail = operation.then(() => { }, () => { });
227
+ return operation;
228
+ };
229
+ return {
230
+ query: (input, signal) => execute({ operation: "query", ...input }, signal),
231
+ inspect: (signal) => execute({ operation: "inspect" }, signal), close,
232
+ };
233
+ }
@@ -0,0 +1,43 @@
1
+ import type { AdapterSpec, TierBinding } from "../providers/binding.js";
2
+ import type { ExecutionPlan } from "./runner.js";
3
+ import { type ZoneGateResult } from "./zone-gate.js";
4
+ export interface AdapterAudit {
5
+ readonly adapter: string;
6
+ readonly model: string;
7
+ readonly endpointHost: string;
8
+ readonly thinking: "on" | "off" | "unset";
9
+ }
10
+ export interface ZoneDryRunEdge {
11
+ readonly from: string;
12
+ readonly alias: string;
13
+ readonly to: string;
14
+ }
15
+ export interface ZoneDryRunOptions {
16
+ /** The capability card that public-zone steps would receive, when the project is known. */
17
+ readonly card?: {
18
+ readonly digest: string;
19
+ readonly bytes: number;
20
+ readonly truncated: boolean;
21
+ };
22
+ }
23
+ export interface ZoneDryRun {
24
+ readonly gate: ZoneGateResult;
25
+ readonly card?: ZoneDryRunOptions["card"];
26
+ /** Which host context each step would receive: the card on public steps, the snapshot elsewhere. */
27
+ readonly contexts: Readonly<Record<string, "card" | "snapshot">>;
28
+ readonly audits: Readonly<Record<string, AdapterAudit>>;
29
+ readonly edges: readonly ZoneDryRunEdge[];
30
+ /** 0 when the gate is not armed or found nothing; 1 on any violation. */
31
+ readonly exitCode: 0 | 1;
32
+ }
33
+ /**
34
+ * What an audit needs to know about the serving runtime behind a spec,
35
+ * read from the spec's configuration alone. No adapter is constructed, no
36
+ * request is made: the dry-run must stay runnable with no network and no
37
+ * credentials.
38
+ */
39
+ export declare function auditAdapterSpec(spec: AdapterSpec): AdapterAudit;
40
+ /** Evaluates the gate over the plan and collects everything an audit prints; pure, offline. */
41
+ export declare function describeZoneDryRun(plan: ExecutionPlan, entry: string, binding: TierBinding, options?: ZoneDryRunOptions): ZoneDryRun;
42
+ /** Renders a dry-run as the fixed-width text the CLI prints. */
43
+ export declare function formatZoneDryRun(dryRun: ZoneDryRun): string;
@@ -0,0 +1,107 @@
1
+ import { evaluateZoneGate } from "./zone-gate.js";
2
+ function readString(record, key) {
3
+ if (typeof record !== "object" || record === null)
4
+ return undefined;
5
+ const value = record[key];
6
+ return typeof value === "string" && value.length > 0 ? value : undefined;
7
+ }
8
+ /**
9
+ * What an audit needs to know about the serving runtime behind a spec,
10
+ * read from the spec's configuration alone. No adapter is constructed, no
11
+ * request is made: the dry-run must stay runnable with no network and no
12
+ * credentials.
13
+ */
14
+ export function auditAdapterSpec(spec) {
15
+ const config = spec.config;
16
+ const baseURL = readString(config, "baseURL");
17
+ let endpointHost = "(adapter default)";
18
+ if (baseURL !== undefined) {
19
+ try {
20
+ endpointHost = new URL(baseURL).host;
21
+ }
22
+ catch {
23
+ endpointHost = `(invalid baseURL: ${baseURL})`;
24
+ }
25
+ }
26
+ else if (spec.adapter === "anthropic") {
27
+ endpointHost = "api.anthropic.com (adapter default)";
28
+ }
29
+ else if (spec.adapter === "openai") {
30
+ endpointHost = "api.openai.com (adapter default)";
31
+ }
32
+ else if (spec.adapter === "mock") {
33
+ endpointHost = "(none: mock)";
34
+ }
35
+ const extraBody = config?.["extraBody"];
36
+ const kwargs = typeof extraBody === "object" && extraBody !== null ? extraBody["chat_template_kwargs"] : undefined;
37
+ const enable = typeof kwargs === "object" && kwargs !== null ? kwargs["enable_thinking"] : undefined;
38
+ return {
39
+ adapter: spec.adapter,
40
+ model: readString(config, "model") ?? "(adapter default)",
41
+ endpointHost,
42
+ thinking: enable === true ? "on" : enable === false ? "off" : "unset",
43
+ };
44
+ }
45
+ /** Evaluates the gate over the plan and collects everything an audit prints; pure, offline. */
46
+ export function describeZoneDryRun(plan, entry, binding, options = {}) {
47
+ const gate = evaluateZoneGate(plan, entry, binding);
48
+ const contexts = {};
49
+ for (const report of gate.steps)
50
+ contexts[`${report.component}.${report.step}`] = gate.armed && report.zone === "public" ? "card" : "snapshot";
51
+ const audits = {};
52
+ for (const report of gate.steps)
53
+ if (report.key !== undefined && report.spec)
54
+ audits[report.key] = auditAdapterSpec(report.spec);
55
+ for (const role of gate.roles)
56
+ if (role.spec)
57
+ audits[role.key] = auditAdapterSpec(role.spec);
58
+ const edges = [];
59
+ for (const report of gate.steps)
60
+ for (const edge of report.calls)
61
+ edges.push({ from: `${report.component}.${report.step}`, alias: edge.alias, to: edge.component });
62
+ return { gate, ...(options.card ? { card: options.card } : {}), contexts, audits, edges, exitCode: gate.armed && gate.violations.length > 0 ? 1 : 0 };
63
+ }
64
+ /** Renders a dry-run as the fixed-width text the CLI prints. */
65
+ export function formatZoneDryRun(dryRun) {
66
+ const lines = [];
67
+ const { gate } = dryRun;
68
+ lines.push(`zone dry-run · entry ${gate.entry} · gate ${gate.armed ? "armed" : "NOT armed (legacy single-zone binding: no zone, disclosure_policy or roles declared)"}`);
69
+ lines.push("");
70
+ lines.push("steps");
71
+ for (const step of gate.steps) {
72
+ const audit = step.key !== undefined ? dryRun.audits[step.key] : undefined;
73
+ const where = audit ? `${audit.adapter} model=${audit.model} host=${audit.endpointHost} thinking=${audit.thinking}` : "(unbound)";
74
+ const tools = step.dataTools.length ? ` tools=${step.dataTools.join(",")}` : "";
75
+ const context = dryRun.contexts[`${step.component}.${step.step}`] ?? "snapshot";
76
+ const contextText = context === "card" ? `context=card${dryRun.card ? ` sha256:${dryRun.card.digest}` : ""}` : "context=snapshot";
77
+ lines.push(` ${step.component}.${step.step} role=${step.role} tier=${step.tier} key=${step.key ?? "-"} zone=${step.zone} ${contextText} ${where}${tools}`);
78
+ }
79
+ if (dryRun.card)
80
+ lines.push(` capability card: sha256:${dryRun.card.digest} (${dryRun.card.bytes} bytes${dryRun.card.truncated ? ", truncated" : ""}) — sent to public-zone steps only`);
81
+ lines.push("");
82
+ lines.push("call edges");
83
+ if (dryRun.edges.length === 0)
84
+ lines.push(" (none)");
85
+ for (const edge of dryRun.edges)
86
+ lines.push(` ${edge.from} --[${edge.alias}]--> ${edge.to}`);
87
+ lines.push("");
88
+ lines.push("roles");
89
+ if (gate.roles.length === 0)
90
+ lines.push(" (none declared)");
91
+ for (const role of gate.roles) {
92
+ const audit = dryRun.audits[role.key];
93
+ const where = audit ? `${audit.adapter} model=${audit.model} host=${audit.endpointHost} thinking=${audit.thinking}` : "(unbound)";
94
+ lines.push(` ${role.role} key=${role.key} zone=${role.zone} ${where}`);
95
+ }
96
+ lines.push("");
97
+ if (!gate.armed)
98
+ lines.push("result: gate not armed; nothing was checked");
99
+ else if (gate.violations.length === 0)
100
+ lines.push("result: ok (no zone violations)");
101
+ else {
102
+ lines.push(`result: REJECTED (${gate.violations.length} violation${gate.violations.length === 1 ? "" : "s"})`);
103
+ for (const violation of gate.violations)
104
+ lines.push(` - [${violation.code}] ${violation.message}`);
105
+ }
106
+ return `${lines.join("\n")}\n`;
107
+ }
@@ -0,0 +1,76 @@
1
+ import { type AdapterSpec, type TierBinding } from "../providers/binding.js";
2
+ import type { Zone } from "../providers/zone.js";
3
+ import type { ExecutionPlan } from "./runner.js";
4
+ /** Where a step sits in the composition as seen from one entry. */
5
+ export type ZoneStepRole = "caller" | "callee";
6
+ export interface ZoneStepReport {
7
+ readonly component: string;
8
+ readonly step: string;
9
+ readonly tier: string;
10
+ readonly role: ZoneStepRole;
11
+ /** The binding key that bound this (mount, tier); undefined when nothing did. */
12
+ readonly key?: string;
13
+ readonly spec?: AdapterSpec;
14
+ readonly zone: Zone | "unbound";
15
+ /** Data-bearing tool grants on the step (query / semantic_introspect). */
16
+ readonly dataTools: readonly string[];
17
+ readonly calls: readonly {
18
+ readonly alias: string;
19
+ readonly component: string;
20
+ }[];
21
+ }
22
+ export interface ZoneRoleReport {
23
+ readonly role: "judge" | "render";
24
+ readonly key: string;
25
+ readonly spec?: AdapterSpec;
26
+ readonly zone: Zone | "unbound";
27
+ }
28
+ export type ZoneGateViolationCode = "callee_public" | "data_step_public" | "missing_zone" | "unbound_tier" | "judge_public" | "render_public" | "missing_judge" | "missing_policy" | "render_unbound";
29
+ export interface ZoneGateViolation {
30
+ readonly code: ZoneGateViolationCode;
31
+ readonly component?: string;
32
+ readonly step?: string;
33
+ readonly tier: string;
34
+ readonly zone: Zone | "unbound";
35
+ readonly message: string;
36
+ }
37
+ export interface ZoneGateResult {
38
+ /** False for a legacy single-zone binding: nothing was checked and nothing is claimed. */
39
+ readonly armed: boolean;
40
+ readonly entry: string;
41
+ readonly steps: readonly ZoneStepReport[];
42
+ readonly roles: readonly ZoneRoleReport[];
43
+ readonly violations: readonly ZoneGateViolation[];
44
+ }
45
+ export declare class ZoneGateError extends Error {
46
+ readonly violations: readonly ZoneGateViolation[];
47
+ constructor(violations: readonly ZoneGateViolation[]);
48
+ }
49
+ /** The mounts reachable from `entry`, in first-visit order; the entry itself is first. */
50
+ export declare function reachableMounts(plan: ExecutionPlan, entry: string): string[];
51
+ /**
52
+ * The zone gate: a pure function over (plan, binding) that decides whether a
53
+ * binding may run a composition at all. Enforced at binding time, before any
54
+ * model, tool or child session starts; a violation is a loud failure, never a
55
+ * degrade.
56
+ *
57
+ * Rules, keyed on (mount, tier):
58
+ * 1. every step of a callee mount (reached through a call edge) resolves to `zone: private`;
59
+ * 2. every step that holds a data tool grant resolves to `zone: private`, whatever its mount;
60
+ * 3. the `judge` role tier, when declared, is private; a disclosure policy without a judge fails;
61
+ * 4. the `render` role tier, when declared, is private;
62
+ * 5. a tier referenced by any of the above without a `zone` fails — no default zone;
63
+ * 6. a caller step without data tools may be public, private, or leave zone unset;
64
+ * 7. a public (or zone-unset) caller step that calls a callee needs a `disclosure_policy`,
65
+ * because that is the only way child results are verified before they cross;
66
+ * 8. an entry that renders a contract must name `roles.render` explicitly whenever the stage's
67
+ * default tier (the entry's last independent step, which is where `render/envelope.ts`
68
+ * would run a render model) is not private: rows reach the render stage, so leaving it on an
69
+ * inherited public tier is rejected rather than assumed.
70
+ *
71
+ * A binding with no zone information anywhere is not zone-aware: the gate is
72
+ * not armed and the result carries no violations, only the step report.
73
+ */
74
+ export declare function evaluateZoneGate(plan: ExecutionPlan, entry: string, binding: TierBinding): ZoneGateResult;
75
+ /** Runs the gate and throws {@link ZoneGateError} when an armed gate finds any violation. */
76
+ export declare function assertZoneGate(plan: ExecutionPlan, entry: string, binding: TierBinding): ZoneGateResult;
@@ -0,0 +1,144 @@
1
+ import { isZoneAwareBinding, resolveTierSpec } from "../providers/binding.js";
2
+ export class ZoneGateError extends Error {
3
+ violations;
4
+ constructor(violations) {
5
+ super(`zone gate rejected the binding:\n${violations.map((violation) => ` - ${violation.message}`).join("\n")}`);
6
+ this.violations = violations;
7
+ this.name = "ZoneGateError";
8
+ }
9
+ }
10
+ /** The mounts reachable from `entry`, in first-visit order; the entry itself is first. */
11
+ export function reachableMounts(plan, entry) {
12
+ const closure = [];
13
+ const seen = new Set();
14
+ const visit = (id) => {
15
+ if (seen.has(id))
16
+ return;
17
+ seen.add(id);
18
+ closure.push(id);
19
+ const node = plan.components[id];
20
+ if (!node)
21
+ throw new Error(`zone gate: plan has no component "${id}"`);
22
+ for (const step of node.steps)
23
+ for (const edge of step.calls)
24
+ visit(edge.component);
25
+ };
26
+ visit(entry);
27
+ return closure;
28
+ }
29
+ function dataToolNames(step) {
30
+ return step.tools.map((tool) => tool.name);
31
+ }
32
+ /**
33
+ * The zone gate: a pure function over (plan, binding) that decides whether a
34
+ * binding may run a composition at all. Enforced at binding time, before any
35
+ * model, tool or child session starts; a violation is a loud failure, never a
36
+ * degrade.
37
+ *
38
+ * Rules, keyed on (mount, tier):
39
+ * 1. every step of a callee mount (reached through a call edge) resolves to `zone: private`;
40
+ * 2. every step that holds a data tool grant resolves to `zone: private`, whatever its mount;
41
+ * 3. the `judge` role tier, when declared, is private; a disclosure policy without a judge fails;
42
+ * 4. the `render` role tier, when declared, is private;
43
+ * 5. a tier referenced by any of the above without a `zone` fails — no default zone;
44
+ * 6. a caller step without data tools may be public, private, or leave zone unset;
45
+ * 7. a public (or zone-unset) caller step that calls a callee needs a `disclosure_policy`,
46
+ * because that is the only way child results are verified before they cross;
47
+ * 8. an entry that renders a contract must name `roles.render` explicitly whenever the stage's
48
+ * default tier (the entry's last independent step, which is where `render/envelope.ts`
49
+ * would run a render model) is not private: rows reach the render stage, so leaving it on an
50
+ * inherited public tier is rejected rather than assumed.
51
+ *
52
+ * A binding with no zone information anywhere is not zone-aware: the gate is
53
+ * not armed and the result carries no violations, only the step report.
54
+ */
55
+ export function evaluateZoneGate(plan, entry, binding) {
56
+ const armed = isZoneAwareBinding(binding);
57
+ const mounts = reachableMounts(plan, entry);
58
+ const steps = [];
59
+ const violations = [];
60
+ for (const mount of mounts) {
61
+ const node = plan.components[mount];
62
+ const role = mount === entry ? "caller" : "callee";
63
+ for (const step of node.steps) {
64
+ const resolved = resolveTierSpec(binding, step.tier, mount);
65
+ const zone = resolved?.spec.zone ?? "unbound";
66
+ const report = { component: mount, step: step.name, tier: step.tier, role,
67
+ ...(resolved ? { key: resolved.key, spec: resolved.spec } : {}), zone, dataTools: dataToolNames(step), calls: step.calls };
68
+ steps.push(report);
69
+ if (!armed)
70
+ continue;
71
+ const where = `step ${mount}.${step.name} (tier ${step.tier})`;
72
+ if (!resolved) {
73
+ violations.push({ code: "unbound_tier", component: mount, step: step.name, tier: step.tier, zone,
74
+ message: `${where} has no binding entry (looked up ${mount}/${step.tier}, then ${step.tier})` });
75
+ continue;
76
+ }
77
+ const relevant = role === "callee" || report.dataTools.length > 0;
78
+ if (!relevant)
79
+ continue;
80
+ const why = role === "callee" ? "runs inside the callee" : `holds data tools ${report.dataTools.join(", ")}`;
81
+ if (zone === "unbound") {
82
+ violations.push({ code: "missing_zone", component: mount, step: step.name, tier: step.tier, zone,
83
+ message: `${where} ${why} but binding entry "${resolved.key}" declares no zone` });
84
+ }
85
+ else if (zone !== "private") {
86
+ violations.push({ code: role === "callee" ? "callee_public" : "data_step_public", component: mount, step: step.name, tier: step.tier, zone,
87
+ message: `${where} ${why} but binding entry "${resolved.key}" is zone: ${zone}; it must be private` });
88
+ }
89
+ }
90
+ }
91
+ const roles = [];
92
+ for (const role of ["judge", "render"]) {
93
+ const key = binding.roles?.[role];
94
+ if (key === undefined)
95
+ continue;
96
+ const spec = binding.tiers[key];
97
+ const zone = spec?.zone ?? "unbound";
98
+ roles.push({ role, key, ...(spec ? { spec } : {}), zone });
99
+ if (!armed)
100
+ continue;
101
+ if (!spec) {
102
+ violations.push({ code: "unbound_tier", tier: key, zone, message: `${role} role names binding entry "${key}", which does not exist` });
103
+ }
104
+ else if (zone === "unbound") {
105
+ violations.push({ code: "missing_zone", tier: key, zone, message: `${role} tier "${key}" declares no zone; it must be private` });
106
+ }
107
+ else if (zone !== "private") {
108
+ violations.push({ code: role === "judge" ? "judge_public" : "render_public", tier: key, zone,
109
+ message: `${role} tier "${key}" is zone: ${zone}; it ${role === "judge" ? "sees unfiltered answers" : "sees rows"} and must be private` });
110
+ }
111
+ }
112
+ if (armed && binding.disclosurePolicy === undefined) {
113
+ for (const report of steps) {
114
+ if (report.role === "caller" && report.calls.length > 0 && report.zone !== "private") {
115
+ violations.push({ code: "missing_policy", component: report.component, step: report.step, tier: report.tier, zone: report.zone,
116
+ message: `step ${report.component}.${report.step} (tier ${report.tier}, zone ${report.zone}) calls ${report.calls.map((edge) => edge.component).join(", ")} but the binding has no disclosure_policy; child results would cross unverified` });
117
+ }
118
+ }
119
+ }
120
+ if (armed && binding.roles?.render === undefined) {
121
+ const node = plan.components[entry];
122
+ const render = node.declaration.effect?.render_blocks;
123
+ const terminal = node.steps.filter((step) => !step.repairOf).at(-1);
124
+ if (Array.isArray(render) && render.length > 0 && terminal) {
125
+ const zone = resolveTierSpec(binding, terminal.tier, entry)?.spec.zone ?? "unbound";
126
+ if (zone !== "private") {
127
+ violations.push({ code: "render_unbound", component: entry, step: terminal.name, tier: terminal.tier, zone,
128
+ message: `entry ${entry} renders a contract but roles.render names no tier; the render stage sees rows and would inherit step ${entry}.${terminal.name} (tier ${terminal.tier}, zone ${zone}); bind it to a private tier explicitly` });
129
+ }
130
+ }
131
+ }
132
+ if (armed && binding.disclosurePolicy !== undefined && binding.roles?.judge === undefined) {
133
+ violations.push({ code: "missing_judge", tier: "(none)", zone: "unbound",
134
+ message: "disclosure_policy is set but roles.judge names no tier; the egress verification step needs a private judge" });
135
+ }
136
+ return { armed, entry, steps, roles, violations };
137
+ }
138
+ /** Runs the gate and throws {@link ZoneGateError} when an armed gate finds any violation. */
139
+ export function assertZoneGate(plan, entry, binding) {
140
+ const result = evaluateZoneGate(plan, entry, binding);
141
+ if (result.armed && result.violations.length > 0)
142
+ throw new ZoneGateError(result.violations);
143
+ return result;
144
+ }