harnery 0.11.0 → 0.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/README.md +1 -1
  2. package/dist/commander.d.ts.map +1 -1
  3. package/dist/commander.js +2 -0
  4. package/dist/commands/browse.js +1 -3
  5. package/dist/commands/eml.d.ts.map +1 -1
  6. package/dist/commands/eml.js +11 -50
  7. package/dist/commands/policy.d.ts +6 -0
  8. package/dist/commands/policy.d.ts.map +1 -0
  9. package/dist/commands/policy.js +42 -0
  10. package/dist/commands/workflow.d.ts.map +1 -1
  11. package/dist/commands/workflow.js +18 -1
  12. package/dist/core/agents/coord-client.d.ts +7 -0
  13. package/dist/core/agents/coord-client.d.ts.map +1 -1
  14. package/dist/core/agents/coord-client.js +27 -1
  15. package/dist/core/policy/evaluate.d.ts +14 -0
  16. package/dist/core/policy/evaluate.d.ts.map +1 -0
  17. package/dist/core/policy/evaluate.js +238 -0
  18. package/dist/core/policy/file.d.ts +3 -0
  19. package/dist/core/policy/file.d.ts.map +1 -0
  20. package/dist/core/policy/file.js +20 -0
  21. package/dist/core/policy/index.d.ts +5 -0
  22. package/dist/core/policy/index.d.ts.map +1 -0
  23. package/dist/core/policy/index.js +3 -0
  24. package/dist/core/policy/types.d.ts +91 -0
  25. package/dist/core/policy/types.d.ts.map +1 -0
  26. package/dist/core/policy/types.js +1 -0
  27. package/dist/core/scratch/index.d.ts.map +1 -1
  28. package/dist/core/scratch/index.js +14 -4
  29. package/dist/core/workflow/engine.d.ts.map +1 -1
  30. package/dist/core/workflow/engine.js +261 -35
  31. package/dist/core/workflow/index.d.ts +1 -1
  32. package/dist/core/workflow/index.d.ts.map +1 -1
  33. package/dist/core/workflow/proof.d.ts +7 -0
  34. package/dist/core/workflow/proof.d.ts.map +1 -1
  35. package/dist/core/workflow/proof.js +33 -0
  36. package/dist/core/workflow/types.d.ts +33 -0
  37. package/dist/core/workflow/types.d.ts.map +1 -1
  38. package/dist/lib/browser/client.d.ts +1 -1
  39. package/dist/lib/browser/client.d.ts.map +1 -1
  40. package/dist/lib/browser/client.js +7 -1
  41. package/package.json +9 -4
  42. package/src/commander.ts +2 -0
  43. package/src/commands/browse.ts +1 -5
  44. package/src/commands/eml.ts +11 -62
  45. package/src/commands/policy.ts +50 -0
  46. package/src/commands/workflow.ts +29 -1
  47. package/src/core/agents/coord-client.ts +28 -1
  48. package/src/core/policy/evaluate.ts +311 -0
  49. package/src/core/policy/file.ts +20 -0
  50. package/src/core/policy/index.ts +26 -0
  51. package/src/core/policy/types.ts +107 -0
  52. package/src/core/scratch/index.ts +20 -4
  53. package/src/core/workflow/engine.ts +283 -41
  54. package/src/core/workflow/index.ts +1 -0
  55. package/src/core/workflow/proof.ts +51 -0
  56. package/src/core/workflow/types.ts +43 -0
  57. package/src/lib/browser/client.ts +6 -1
package/README.md CHANGED
@@ -21,7 +21,7 @@
21
21
 
22
22
  - **Multi-agent coordination:** per-agent heartbeats in `.harnery/active/`, claim-time and commit-time guards, the canonical event stream, councils, a decision docket, per-agent scratchpads, and harness adapters for Claude Code / Cursor / Codex.
23
23
  - **Cross-machine presence:** sessions on every machine that shares a repo see each other, labeled by machine. Zero-config over the repo's own git remote (presence refs), with an optional live-socket upgrade via an end-to-end-encrypted relay — the public `relay.harnery.com`, or self-host with `harn relay serve` / the bundled Cloudflare Worker (`relay/worker/`). The relay sees only ciphertext in opaquely-named rooms.
24
- - **Coordination-aware workflows:** `harn workflow run <script>` executes bounded, schema-gated multi-subagent workflow scripts — plain JS stages fanning out to headless harness-CLI subagents that are born coordination-registered (heartbeats, events, claim guards), with deterministic code deciding the routing between stages.
24
+ - **Coordination-aware workflows:** `harn workflow run <script>` executes bounded, schema-gated multi-subagent workflow scripts — plain JS stages fanning out to headless harness-CLI subagents that are born coordination-registered (heartbeats, events, claim guards), with deterministic code deciding the routing between stages. An optional host-owned policy gates dispatch and declared external mutations with fail-closed `ALLOW`, `ASK`, and `DENY` decisions recorded in the run proof.
25
25
  - **Standalone web UI:** `harn web up` boots a local Next.js dashboard for the coord layer, councils, per-project state, and a local coding-agent status view (Claude Code / Codex / Cursor usage, backed by `harn devtools`). Ships with the git clone, not the npm package (see [Install](#install)).
26
26
  - **Backup + sync:** `harn backup` snapshots `.harnery/` via [restic](https://restic.net/); `harn sync` keeps a curated subset live across machines via [rclone](https://rclone.org/) (Google Drive or any rclone remote).
27
27
 
@@ -1 +1 @@
1
- {"version":3,"file":"commander.d.ts","sourceRoot":"","sources":["../src/commander.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"commander.d.ts","sourceRoot":"","sources":["../src/commander.ts"],"names":[],"mappings":"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"}
package/dist/commander.js CHANGED
@@ -41,6 +41,7 @@ import { registerGrepCommand } from "./commands/grep.js";
41
41
  import { registerHarnessCommand } from "./commands/harness.js";
42
42
  import { registerInitCommand } from "./commands/init.js";
43
43
  import { registerOutlineCommand } from "./commands/outline.js";
44
+ import { registerPolicyCommand } from "./commands/policy.js";
44
45
  import { registerPresenceCommand } from "./commands/presence.js";
45
46
  import { registerReadCommand } from "./commands/read.js";
46
47
  import { registerRelayCommand } from "./commands/relay.js";
@@ -109,6 +110,7 @@ export function createHarneryProgram(opts = {}) {
109
110
  registerEditBatchCommand(program, emit);
110
111
  registerGrepCommand(program, emit, opts.context);
111
112
  registerHarnessCommand(program, emit);
113
+ registerPolicyCommand(program, emit);
112
114
  registerCookiesCommand(program, emit);
113
115
  registerFetchCommand(program, emit, opts.context);
114
116
  registerReadCommand(program, emit);
@@ -396,9 +396,7 @@ async function runPrintMode(browser, navResult, opts, evalResult, visibility, wi
396
396
  body = await browser.htmlContent(opts.selector);
397
397
  }
398
398
  else if (opts.snapshot) {
399
- body = opts.selector
400
- ? await browser.evaluate(`(() => { const el = document.querySelector(${JSON.stringify(opts.selector)}); return el ? el.innerText : ''; })()`)
401
- : await browser.textSnapshot();
399
+ body = await browser.textSnapshot(opts.selector);
402
400
  }
403
401
  if (opts.json) {
404
402
  const result = {
@@ -1 +1 @@
1
- {"version":3,"file":"eml.d.ts","sourceRoot":"","sources":["../../src/commands/eml.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,EAAE,KAAK,UAAU,EAAgB,MAAM,YAAY,CAAC;AAC3D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAEnD;;;;;;;;;GASG;AAEH,UAAU,aAAa;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,IAAI,GAAG,IAAI,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,WAAW,GAAG,IAAI,CAkB5E;AAED,MAAM,WAAW,OAAO;IACtB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAuCD;;;GAGG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,UAAU,GAAG,aAAa,EAAE,CAYjE;AA4VD;;GAEG;AACH,wBAAgB,cAAc,CAC5B,MAAM,EAAE,UAAU,EAClB,QAAQ,EAAE,aAAa,EAAE,EACzB,IAAI,EAAE,OAAO,GACZ,MAAM,CAyDR"}
1
+ {"version":3,"file":"eml.d.ts","sourceRoot":"","sources":["../../src/commands/eml.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEzC,OAAO,EAAE,KAAK,UAAU,EAAgB,MAAM,YAAY,CAAC;AAC3D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAEnD;;;;;;;;;GASG;AAEH,UAAU,aAAa;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,IAAI,GAAG,IAAI,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,WAAW,GAAG,IAAI,CAkB5E;AAED,MAAM,WAAW,OAAO;IACtB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAuCD;;;GAGG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,UAAU,GAAG,aAAa,EAAE,CAYjE;AAwSD;;GAEG;AACH,wBAAgB,cAAc,CAC5B,MAAM,EAAE,UAAU,EAClB,QAAQ,EAAE,aAAa,EAAE,EACzB,IAAI,EAAE,OAAO,GACZ,MAAM,CAyDR"}
@@ -1,4 +1,5 @@
1
1
  import { readFileSync, writeFileSync } from "node:fs";
2
+ import { parseHTML } from "linkedom";
2
3
  import { simpleParser } from "mailparser";
3
4
  export function registerEmlCommand(program, emit) {
4
5
  program
@@ -261,13 +262,13 @@ function parseAttribution(text) {
261
262
  const nameMatch = cleaned.match(/(?:AM|PM)\s+(.+?)(?:\s*<[^>]+>)?\s+wrote:/i);
262
263
  let name = "Unknown";
263
264
  if (nameMatch) {
264
- name = nameMatch[1].replace(/<[^>]+>/g, "").trim();
265
+ name = nameMatch[1].trim();
265
266
  }
266
267
  else {
267
268
  // Try simpler pattern without AM/PM
268
269
  const simpleMatch = cleaned.match(/at\s+[\d:]+\s+(.+?)(?:\s*<[^>]+>)?\s+wrote:/i);
269
270
  if (simpleMatch) {
270
- name = simpleMatch[1].replace(/<[^>]+>/g, "").trim();
271
+ name = simpleMatch[1].trim();
271
272
  }
272
273
  }
273
274
  return { name, date };
@@ -276,18 +277,11 @@ function parseAttribution(text) {
276
277
  * Strip HTML tags, decode entities, and normalize whitespace.
277
278
  */
278
279
  function stripHtml(html) {
279
- let text = html;
280
- // Replace <br>, <br/>, <br /> with newlines
281
- text = text.replace(/<br\s*\/?>/gi, "\n");
282
- // Replace block-level elements with newlines
283
- text = text.replace(/<\/(p|div|tr|li|h[1-6])>/gi, "\n");
284
- text = text.replace(/<(p|div|tr|li|h[1-6])\b[^>]*>/gi, "\n");
285
- // Replace <td> with tab
286
- text = text.replace(/<\/td>/gi, "\t");
287
- // Remove all remaining HTML tags
288
- text = text.replace(/<[^>]+>/g, "");
289
- // Decode HTML entities
290
- text = decodeEntities(text);
280
+ const { document } = parseHTML(`<html><body>${html}</body></html>`);
281
+ for (const unsafe of document.querySelectorAll("script, style, template, noscript")) {
282
+ unsafe.remove();
283
+ }
284
+ let text = document.body?.innerText ?? "";
291
285
  // Normalize whitespace (but preserve newlines)
292
286
  text = text
293
287
  .split("\n")
@@ -295,38 +289,6 @@ function stripHtml(html) {
295
289
  .join("\n");
296
290
  return text;
297
291
  }
298
- /**
299
- * Decode common HTML entities.
300
- */
301
- function decodeEntities(text) {
302
- const entities = {
303
- "&amp;": "&",
304
- "&lt;": "<",
305
- "&gt;": ">",
306
- "&quot;": '"',
307
- "&#39;": "'",
308
- "&apos;": "'",
309
- "&nbsp;": " ",
310
- "&#160;": " ",
311
- "&ndash;": "-",
312
- "&mdash;": "-",
313
- "&laquo;": "<<",
314
- "&raquo;": ">>",
315
- "&bull;": "*",
316
- "&hellip;": "...",
317
- "&copy;": "(c)",
318
- "&reg;": "(R)",
319
- "&trade;": "(TM)",
320
- };
321
- let result = text;
322
- for (const [entity, char] of Object.entries(entities)) {
323
- result = result.replaceAll(entity, char);
324
- }
325
- // Decode numeric entities (&#NNN; and &#xHHH;)
326
- result = result.replace(/&#(\d+);/g, (_, code) => String.fromCharCode(Number.parseInt(code, 10)));
327
- result = result.replace(/&#x([0-9a-fA-F]+);/g, (_, code) => String.fromCharCode(Number.parseInt(code, 16)));
328
- return result;
329
- }
330
292
  /**
331
293
  * Clean up message body text.
332
294
  */
@@ -347,10 +309,9 @@ function cleanBody(text) {
347
309
  }
348
310
  }
349
311
  }
350
- // Remove stray partial HTML tags at the end of the body
351
- body = body.replace(/<\w+\s*$/, "");
352
- // Remove any remaining HTML tags that slipped through
353
- body = body.replace(/<[^>]+>/g, "");
312
+ // The renderer emits Markdown. Neutralize raw HTML in both DOM-derived and
313
+ // plain-text messages so decoded entities cannot become executable markup.
314
+ body = body.replaceAll("<", "&lt;").replaceAll(">", "&gt;");
354
315
  // Collapse 3+ consecutive newlines into 2
355
316
  body = body.replace(/\n{3,}/g, "\n\n");
356
317
  return body.trim();
@@ -0,0 +1,6 @@
1
+ import type { Command } from "commander";
2
+ import type { EmitContext } from "../commander.js";
3
+ import type { NormalizedPolicy } from "../core/policy/index.js";
4
+ export declare function registerPolicyCommand(program: Command, emit: EmitContext): void;
5
+ export declare function renderPolicy(policy: Readonly<NormalizedPolicy>, digest: string): string;
6
+ //# sourceMappingURL=policy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"policy.d.ts","sourceRoot":"","sources":["../../src/commands/policy.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AACnD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAOhE,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,WAAW,GAAG,IAAI,CAwB/E;AAED,wBAAgB,YAAY,CAAC,MAAM,EAAE,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAcvF"}
@@ -0,0 +1,42 @@
1
+ import { loadPolicyFile, policyDigest } from "../core/policy/index.js";
2
+ export function registerPolicyCommand(program, emit) {
3
+ const command = program
4
+ .command("policy")
5
+ .description("Inspect host-enforced workflow policy documents.");
6
+ command
7
+ .command("check <file>")
8
+ .description("Validate and print a normalized policy without running a workflow.")
9
+ .option("--json", "Emit the normalized policy and digest as JSON")
10
+ .action((file, opts) => {
11
+ try {
12
+ const policy = loadPolicyFile(file);
13
+ const result = { policy, sha256: policyDigest(policy) };
14
+ if (opts.json) {
15
+ emit.config({ format: "json" });
16
+ emit.data(result);
17
+ }
18
+ else {
19
+ emit.text(renderPolicy(policy, result.sha256));
20
+ }
21
+ }
22
+ catch (error) {
23
+ emit.error({ code: "policy_invalid", message: error.message });
24
+ emit.setExitCode(1);
25
+ }
26
+ });
27
+ }
28
+ export function renderPolicy(policy, digest) {
29
+ const lines = [
30
+ `${policy.name} (schema ${policy.schema_version})`,
31
+ `sha256: ${digest}`,
32
+ `cost ceiling: ${policy.max_cost_usd === undefined ? "none" : `$${policy.max_cost_usd.toFixed(4)}`}`,
33
+ `unknown cost: ${policy.unknown_cost}`,
34
+ `network: ${policy.network}`,
35
+ `external actions: ${policy.external_actions}`,
36
+ `harnesses: ${policy.allowed_harnesses?.join(", ") ?? "any"}`,
37
+ `models: ${policy.allowed_models?.join(", ") ?? "any"}`,
38
+ `paths: ${policy.allowed_paths?.join(", ") ?? "any"}`,
39
+ `isolation: ${policy.allowed_isolation?.join(", ") ?? "any"}`,
40
+ ];
41
+ return `${lines.join("\n")}\n`;
42
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"workflow.d.ts","sourceRoot":"","sources":["../../src/commands/workflow.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AA8BnD,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,WAAW,GAAG,IAAI,CA+IjF"}
1
+ {"version":3,"file":"workflow.d.ts","sourceRoot":"","sources":["../../src/commands/workflow.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAkCnD,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,WAAW,GAAG,IAAI,CAuKjF"}
@@ -1,6 +1,7 @@
1
1
  import { workflowSubscriptionOnly } from "../core/config.js";
2
2
  import { createBuiltinHarnessRegistry } from "../core/harnesses/index.js";
3
3
  import { findCoordRoot } from "../core/hooks/resolve/coord-root.js";
4
+ import { loadPolicyFile } from "../core/policy/index.js";
4
5
  export function registerWorkflowCommand(program, emit) {
5
6
  const registry = createBuiltinHarnessRegistry();
6
7
  const harnesses = registry.ids();
@@ -9,7 +10,7 @@ export function registerWorkflowCommand(program, emit) {
9
10
  .description("Run bounded, schema-gated multi-subagent workflow scripts.");
10
11
  workflow
11
12
  .command("run <script>")
12
- .description("Execute a workflow script (plain JS: `export default async ({agent, parallel, stage, log, evidence}) => …`). " +
13
+ .description("Execute a workflow script (plain JS: `export default async ({agent, parallel, stage, log, evidence, authorize}) => …`). " +
13
14
  "Subagents spawn as headless harness-CLI subprocesses, coordination-registered.")
14
15
  .option("--max-agents <n>", "Total-agent ceiling for the run (default 50)")
15
16
  .option("--concurrency <n>", "Concurrent-subagent cap (default 4)")
@@ -20,6 +21,8 @@ export function registerWorkflowCommand(program, emit) {
20
21
  "harness has no stored login (repo default via config.jsonc workflow.subscriptionOnly)")
21
22
  .option("--allow-api-billing", "Permit an exported API key to override a stored subscription login (per-token billing); " +
22
23
  "without this the engine refuses that silent-override state")
24
+ .option("--policy <file>", "Host policy JSON/JSONC applied before dispatch and external mutation")
25
+ .option("--isolation <mode>", "Host-created execution boundary: shared | worktree | sandbox | remote (default shared)")
23
26
  .option("--json", "Emit the full RunReport as JSON")
24
27
  .action(async (script, opts) => {
25
28
  const coordRoot = findCoordRoot();
@@ -37,6 +40,14 @@ export function registerWorkflowCommand(program, emit) {
37
40
  });
38
41
  process.exit(1);
39
42
  }
43
+ if (opts.isolation &&
44
+ !["shared", "worktree", "sandbox", "remote"].includes(opts.isolation)) {
45
+ emit.error({
46
+ code: "bad_isolation",
47
+ message: `unknown isolation ${JSON.stringify(opts.isolation)} (expected: shared | worktree | sandbox | remote)`,
48
+ });
49
+ process.exit(1);
50
+ }
40
51
  // Flag beats config; the two flags contradict each other by design.
41
52
  const subscriptionOnly = opts.subscriptionOnly || workflowSubscriptionOnly(coordRoot);
42
53
  if (subscriptionOnly && opts.allowApiBilling) {
@@ -65,6 +76,12 @@ export function registerWorkflowCommand(program, emit) {
65
76
  adapter.profile.id,
66
77
  { toolEvidence: adapter.profile.capabilities.toolEvidence },
67
78
  ])),
79
+ policy: opts.policy ? loadPolicyFile(opts.policy) : undefined,
80
+ isolation: opts.isolation,
81
+ // CLI harness subprocesses inherit the host network. A policy that
82
+ // forbids network must therefore deny dispatch unless a future host
83
+ // adapter creates and declares a network-disabled boundary.
84
+ networkAccess: "enabled",
68
85
  });
69
86
  if (opts.json) {
70
87
  emit.config({ format: "json" });
@@ -6,6 +6,13 @@
6
6
  * ppid map, so a single coord state can be observed and mutated from
7
7
  * either side without divergence.
8
8
  */
9
+ /**
10
+ * Instance IDs become coordination filenames, so only one portable basename
11
+ * alphabet is accepted at every filesystem boundary. UUIDs, test IDs, and
12
+ * legacy hex IDs all fit this contract; separators and dot segments do not.
13
+ */
14
+ export declare function isSafeInstanceId(value: unknown): value is string;
15
+ export declare function assertSafeInstanceId(value: unknown): asserts value is string;
9
16
  export interface Heartbeat {
10
17
  instance_id: string;
11
18
  name?: string;
@@ -1 +1 @@
1
- {"version":3,"file":"coord-client.d.ts","sourceRoot":"","sources":["../../../src/core/agents/coord-client.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAQH,MAAM,WAAW,SAAS;IACxB,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,sEAAsE;IACtE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,oGAAoG;IACpG,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,4EAA4E;IAC5E,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,0IAA0I;IAC1I,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,sFAAsF;IACtF,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,mEAAmE;IACnE,uBAAuB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxC,wEAAwE;IACxE,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;;;GASG;AACH,wBAAgB,YAAY,IAAI,MAAM,GAAG,IAAI,CA0C5C;AAED,qEAAqE;AACrE,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAIvD;AAED,+EAA+E;AAC/E,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAI1D;AAaD;;;;;;;;;;;GAWG;AACH,wBAAgB,YAAY,IAAI,MAAM,GAAG,IAAI,CAE5C;AAED;;;;;;GAMG;AACH,wBAAgB,sBAAsB,IAAI;IACxC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,MAAM,EAAE,KAAK,GAAG,QAAQ,GAAG,iBAAiB,GAAG,aAAa,GAAG,kBAAkB,GAAG,MAAM,CAAC;CAC5F,CAkEA;AA4CD;;;;;;;;;GASG;AACH,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CA2BpE;AAED;;;;;;;;GAQG;AACH,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAyBpE;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAKlE;AAgBD;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAOtD;AAED;;;GAGG;AACH,wBAAgB,eAAe,IAAI,MAAM,CAIxC"}
1
+ {"version":3,"file":"coord-client.d.ts","sourceRoot":"","sources":["../../../src/core/agents/coord-client.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAWH;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAWhE;AAED,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,KAAK,IAAI,MAAM,CAI5E;AAED,MAAM,WAAW,SAAS;IACxB,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,sEAAsE;IACtE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,oGAAoG;IACpG,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,4EAA4E;IAC5E,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,0IAA0I;IAC1I,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,sFAAsF;IACtF,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,mEAAmE;IACnE,uBAAuB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxC,wEAAwE;IACxE,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;;;GASG;AACH,wBAAgB,YAAY,IAAI,MAAM,GAAG,IAAI,CA0C5C;AAED,qEAAqE;AACrE,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAIvD;AAED,+EAA+E;AAC/E,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAI1D;AAaD;;;;;;;;;;;GAWG;AACH,wBAAgB,YAAY,IAAI,MAAM,GAAG,IAAI,CAE5C;AAED;;;;;;GAMG;AACH,wBAAgB,sBAAsB,IAAI;IACxC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,MAAM,EAAE,KAAK,GAAG,QAAQ,GAAG,iBAAiB,GAAG,aAAa,GAAG,kBAAkB,GAAG,MAAM,CAAC;CAC5F,CAkEA;AA4CD;;;;;;;;;GASG;AACH,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CA2BpE;AAED;;;;;;;;GAQG;AACH,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAyBpE;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAKlE;AAgBD;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAOtD;AAED;;;GAGG;AACH,wBAAgB,eAAe,IAAI,MAAM,CAIxC"}
@@ -9,6 +9,32 @@
9
9
  import { spawnSync } from "node:child_process";
10
10
  import { existsSync, readdirSync, readFileSync } from "node:fs";
11
11
  import { resolve } from "node:path";
12
+ // NOTE: kept dependency-free (node builtins only); this file is vendored verbatim into
13
+ // a downstream consumer, so it cannot import the coordEnv helper.
14
+ const MAX_INSTANCE_ID_LENGTH = 128;
15
+ /**
16
+ * Instance IDs become coordination filenames, so only one portable basename
17
+ * alphabet is accepted at every filesystem boundary. UUIDs, test IDs, and
18
+ * legacy hex IDs all fit this contract; separators and dot segments do not.
19
+ */
20
+ export function isSafeInstanceId(value) {
21
+ if (typeof value !== "string" || value.length === 0 || value.length > MAX_INSTANCE_ID_LENGTH) {
22
+ return false;
23
+ }
24
+ for (let index = 0; index < value.length; index++) {
25
+ const code = value.charCodeAt(index);
26
+ const alpha = (code >= 65 && code <= 90) || (code >= 97 && code <= 122);
27
+ const digit = code >= 48 && code <= 57;
28
+ if (!alpha && !digit && code !== 45 && code !== 95)
29
+ return false;
30
+ }
31
+ return true;
32
+ }
33
+ export function assertSafeInstanceId(value) {
34
+ if (!isSafeInstanceId(value)) {
35
+ throw new Error("instance_id must be 1-128 ASCII letters, digits, hyphens, or underscores");
36
+ }
37
+ }
12
38
  /**
13
39
  * Resolve the monorepo root for coord-state purposes.
14
40
  *
@@ -303,7 +329,7 @@ export function resolveSingleActiveOwner(root) {
303
329
  * `.harnery/active/<id>.json` (the canonical location every reader expects).
304
330
  */
305
331
  export function readHeartbeat(instanceId) {
306
- if (!instanceId)
332
+ if (!isSafeInstanceId(instanceId))
307
333
  return null;
308
334
  const root = monorepoRoot();
309
335
  if (!root)
@@ -0,0 +1,14 @@
1
+ import type { NormalizedPolicy, PolicyEvaluation, PolicyIsolation, PolicyRequest, PolicyRequestSummary, PolicySpec } from "./types.js";
2
+ export declare class PolicyDeniedError extends Error {
3
+ readonly decisionId: string;
4
+ constructor(message: string, decisionId: string);
5
+ }
6
+ export declare function normalizePolicy(input: PolicySpec, options?: {
7
+ baseDir?: string;
8
+ }): Readonly<NormalizedPolicy>;
9
+ export declare function policyDigest(policy: NormalizedPolicy): string;
10
+ export declare function evaluatePolicy(policy: Readonly<NormalizedPolicy>, rawRequest: PolicyRequest): PolicyEvaluation;
11
+ /** Bound and normalize the receipt-safe subset before it reaches a journal or proof. */
12
+ export declare function summarizePolicyRequest(input: PolicyRequest): PolicyRequestSummary;
13
+ export type { PolicyIsolation };
14
+ //# sourceMappingURL=evaluate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"evaluate.d.ts","sourceRoot":"","sources":["../../../src/core/policy/evaluate.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,gBAAgB,EAChB,gBAAgB,EAChB,eAAe,EACf,aAAa,EACb,oBAAoB,EAEpB,UAAU,EAEX,MAAM,YAAY,CAAC;AAqBpB,qBAAa,iBAAkB,SAAQ,KAAK;IAC1C,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;gBAEhB,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM;CAKhD;AAED,wBAAgB,eAAe,CAC7B,KAAK,EAAE,UAAU,EACjB,OAAO,GAAE;IAAE,OAAO,CAAC,EAAE,MAAM,CAAA;CAAO,GACjC,QAAQ,CAAC,gBAAgB,CAAC,CAwB5B;AAED,wBAAgB,YAAY,CAAC,MAAM,EAAE,gBAAgB,GAAG,MAAM,CAE7D;AAED,wBAAgB,cAAc,CAC5B,MAAM,EAAE,QAAQ,CAAC,gBAAgB,CAAC,EAClC,UAAU,EAAE,aAAa,GACxB,gBAAgB,CA8FlB;AAED,wFAAwF;AACxF,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,aAAa,GAAG,oBAAoB,CA8BjF;AAyGD,YAAY,EAAE,eAAe,EAAE,CAAC"}
@@ -0,0 +1,238 @@
1
+ import { createHash } from "node:crypto";
2
+ import { isAbsolute, relative, resolve } from "node:path";
3
+ import { POLICY_SCHEMA_VERSION } from "./types.js";
4
+ const VERDICTS = ["allow", "ask", "deny"];
5
+ const ISOLATIONS = ["shared", "worktree", "sandbox", "remote"];
6
+ const MAX_NAME = 200;
7
+ const MAX_FIELD = 500;
8
+ const MAX_LIST = 100;
9
+ const POLICY_FIELDS = new Set([
10
+ "schema_version",
11
+ "name",
12
+ "max_cost_usd",
13
+ "unknown_cost",
14
+ "allowed_harnesses",
15
+ "allowed_models",
16
+ "allowed_paths",
17
+ "network",
18
+ "external_actions",
19
+ "allowed_isolation",
20
+ ]);
21
+ export class PolicyDeniedError extends Error {
22
+ decisionId;
23
+ constructor(message, decisionId) {
24
+ super(message);
25
+ this.name = "PolicyDeniedError";
26
+ this.decisionId = decisionId;
27
+ }
28
+ }
29
+ export function normalizePolicy(input, options = {}) {
30
+ if (!isRecord(input))
31
+ throw new Error("policy must be an object");
32
+ const unknownFields = Object.keys(input).filter((field) => !POLICY_FIELDS.has(field));
33
+ if (unknownFields.length > 0) {
34
+ throw new Error(`policy contains unknown field(s): ${unknownFields.sort().join(", ")}`);
35
+ }
36
+ const schemaVersion = input.schema_version ?? POLICY_SCHEMA_VERSION;
37
+ if (schemaVersion !== POLICY_SCHEMA_VERSION) {
38
+ throw new Error(`unsupported policy schema_version ${JSON.stringify(schemaVersion)}`);
39
+ }
40
+ const baseDir = resolve(options.baseDir ?? process.cwd());
41
+ const normalized = {
42
+ schema_version: POLICY_SCHEMA_VERSION,
43
+ name: boundedString(input.name ?? "workflow policy", "policy name", MAX_NAME),
44
+ max_cost_usd: optionalMoney(input.max_cost_usd, "max_cost_usd"),
45
+ unknown_cost: enumValue(input.unknown_cost ?? "deny", ["ask", "deny"], "unknown_cost"),
46
+ allowed_harnesses: optionalStringList(input.allowed_harnesses, "allowed_harnesses"),
47
+ allowed_models: optionalStringList(input.allowed_models, "allowed_models"),
48
+ allowed_paths: optionalPathList(input.allowed_paths, baseDir),
49
+ network: enumValue(input.network ?? "deny", VERDICTS, "network"),
50
+ external_actions: enumValue(input.external_actions ?? "deny", VERDICTS, "external_actions"),
51
+ allowed_isolation: optionalEnumList(input.allowed_isolation, ISOLATIONS, "allowed_isolation"),
52
+ };
53
+ return deepFreeze(normalized);
54
+ }
55
+ export function policyDigest(policy) {
56
+ return createHash("sha256").update(JSON.stringify(policy)).digest("hex");
57
+ }
58
+ export function evaluatePolicy(policy, rawRequest) {
59
+ const request = summarizePolicyRequest(rawRequest);
60
+ const rules = [];
61
+ const add = (code, verdict, reason) => {
62
+ rules.push({ code, verdict, reason });
63
+ };
64
+ if (policy.allowed_harnesses && request.phase === "dispatch") {
65
+ if (!request.harness || !policy.allowed_harnesses.includes(request.harness)) {
66
+ add("harness_not_allowed", "deny", `harness ${JSON.stringify(request.harness ?? "unknown")} is not allowed`);
67
+ }
68
+ }
69
+ if (policy.allowed_models && request.phase === "dispatch") {
70
+ if (!request.model || !policy.allowed_models.includes(request.model)) {
71
+ add("model_not_allowed", "deny", `model ${JSON.stringify(request.model ?? "unknown")} is not allowed`);
72
+ }
73
+ }
74
+ if (policy.allowed_paths) {
75
+ if (!request.path || !policy.allowed_paths.some((root) => contains(root, request.path))) {
76
+ add("path_not_allowed", "deny", `path ${JSON.stringify(request.path ?? "unknown")} is outside the allowed roots`);
77
+ }
78
+ }
79
+ if (policy.allowed_isolation && !policy.allowed_isolation.includes(request.isolation)) {
80
+ add("isolation_not_allowed", "deny", `isolation ${JSON.stringify(request.isolation)} is not allowed`);
81
+ }
82
+ if (request.network_access !== "disabled" && policy.network !== "allow") {
83
+ add(request.network_access === "unknown" ? "network_unknown" : "network_restricted", policy.network, request.network_access === "unknown"
84
+ ? "network access is unknown"
85
+ : "network access requires policy authorization");
86
+ }
87
+ if (request.phase === "external_mutation" && policy.external_actions !== "allow") {
88
+ add("external_action_restricted", policy.external_actions, "external mutation requires policy authorization");
89
+ }
90
+ if (policy.max_cost_usd !== undefined && request.phase === "dispatch") {
91
+ const current = request.current_cost_usd ?? 0;
92
+ if (current >= policy.max_cost_usd) {
93
+ add("cost_budget_exhausted", "deny", `run cost $${current.toFixed(4)} has reached the $${policy.max_cost_usd.toFixed(4)} ceiling`);
94
+ }
95
+ else if (request.projected_cost_usd === null || request.projected_cost_usd === undefined) {
96
+ add("dispatch_cost_unknown", policy.unknown_cost, `dispatch cost is unknown under a $${policy.max_cost_usd.toFixed(4)} ceiling`);
97
+ }
98
+ else if (current + request.projected_cost_usd > policy.max_cost_usd) {
99
+ add("cost_budget_exceeded", "deny", `projected run cost $${(current + request.projected_cost_usd).toFixed(4)} exceeds the $${policy.max_cost_usd.toFixed(4)} ceiling`);
100
+ }
101
+ }
102
+ if (rules.length === 0) {
103
+ rules.push({ code: "policy_allow", verdict: "allow", reason: "all configured rules allow" });
104
+ }
105
+ const verdict = rules.some((rule) => rule.verdict === "deny")
106
+ ? "deny"
107
+ : rules.some((rule) => rule.verdict === "ask")
108
+ ? "ask"
109
+ : "allow";
110
+ const decisive = rules.filter((rule) => rule.verdict === verdict);
111
+ return {
112
+ verdict,
113
+ reason: decisive.map((rule) => rule.reason).join("; "),
114
+ rules,
115
+ };
116
+ }
117
+ /** Bound and normalize the receipt-safe subset before it reaches a journal or proof. */
118
+ export function summarizePolicyRequest(input) {
119
+ if (!isRecord(input))
120
+ throw new Error("policy request must be an object");
121
+ const phase = enumValue(input.phase, ["dispatch", "external_mutation"], "policy phase");
122
+ const isolation = enumValue(input.isolation, ISOLATIONS, "policy isolation");
123
+ const networkAccess = enumValue(input.network_access, ["enabled", "disabled", "unknown"], "policy network_access");
124
+ return {
125
+ phase,
126
+ action: boundedString(input.action, "policy action", MAX_FIELD),
127
+ path: optionalBounded(input.path, "policy path"),
128
+ harness: optionalBounded(input.harness, "policy harness"),
129
+ model: optionalBounded(input.model, "policy model"),
130
+ effort: optionalBounded(input.effort, "policy effort"),
131
+ max_attempts: optionalWholeNumber(input.max_attempts, "policy max_attempts", 1),
132
+ max_turns: optionalWholeNumber(input.max_turns, "policy max_turns", 1),
133
+ timeout_ms: optionalWholeNumber(input.timeout_ms, "policy timeout_ms", 1),
134
+ prompt_bytes: optionalWholeNumber(input.prompt_bytes, "policy prompt_bytes", 0),
135
+ isolation,
136
+ network_access: networkAccess,
137
+ service: optionalBounded(input.service, "policy service"),
138
+ target: optionalBounded(safeTarget(input.target), "policy target"),
139
+ current_cost_usd: optionalMoney(input.current_cost_usd, "current_cost_usd"),
140
+ projected_cost_usd: input.projected_cost_usd === null
141
+ ? null
142
+ : optionalMoney(input.projected_cost_usd, "projected_cost_usd"),
143
+ };
144
+ }
145
+ function contains(root, candidate) {
146
+ const rel = relative(resolve(root), resolve(candidate));
147
+ return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel));
148
+ }
149
+ function optionalPathList(value, baseDir) {
150
+ const values = optionalStringList(value, "allowed_paths");
151
+ if (!values)
152
+ return undefined;
153
+ return [...new Set(values.map((item) => resolve(baseDir, item)))].sort();
154
+ }
155
+ function optionalStringList(value, field) {
156
+ if (value === undefined)
157
+ return undefined;
158
+ if (!Array.isArray(value) || value.length === 0 || value.length > MAX_LIST) {
159
+ throw new Error(`${field} must be a non-empty array with at most ${MAX_LIST} entries`);
160
+ }
161
+ return [
162
+ ...new Set(value.map((item, index) => boundedString(item, `${field}[${index}]`, MAX_FIELD))),
163
+ ].sort();
164
+ }
165
+ function optionalEnumList(value, allowed, field) {
166
+ if (value === undefined)
167
+ return undefined;
168
+ if (!Array.isArray(value) || value.length === 0 || value.length > MAX_LIST) {
169
+ throw new Error(`${field} must be a non-empty array with at most ${MAX_LIST} entries`);
170
+ }
171
+ return [...new Set(value.map((item) => enumValue(item, allowed, field)))].sort();
172
+ }
173
+ function enumValue(value, values, field) {
174
+ if (typeof value !== "string" || !values.includes(value)) {
175
+ throw new Error(`${field} must be one of: ${values.join(", ")}`);
176
+ }
177
+ return value;
178
+ }
179
+ function optionalMoney(value, field) {
180
+ if (value === undefined)
181
+ return undefined;
182
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
183
+ throw new Error(`${field} must be a finite non-negative number`);
184
+ }
185
+ return value;
186
+ }
187
+ function optionalWholeNumber(value, field, minimum) {
188
+ if (value === undefined)
189
+ return undefined;
190
+ if (!Number.isSafeInteger(value) || value < minimum) {
191
+ throw new Error(`${field} must be a safe integer greater than or equal to ${minimum}`);
192
+ }
193
+ return value;
194
+ }
195
+ function boundedString(value, field, max) {
196
+ if (typeof value !== "string")
197
+ throw new Error(`${field} must be a string`);
198
+ const normalized = withoutControls(value).replace(/\s+/g, " ").trim();
199
+ if (!normalized)
200
+ throw new Error(`${field} must not be empty`);
201
+ if (normalized.length > max)
202
+ throw new Error(`${field} exceeds ${max} characters`);
203
+ return normalized;
204
+ }
205
+ function optionalBounded(value, field) {
206
+ return value === undefined ? undefined : boundedString(value, field, MAX_FIELD);
207
+ }
208
+ function safeTarget(value) {
209
+ if (typeof value !== "string")
210
+ return value;
211
+ try {
212
+ const url = new URL(value);
213
+ return `${url.origin}${url.pathname}`;
214
+ }
215
+ catch {
216
+ const query = value.indexOf("?");
217
+ const fragment = value.indexOf("#");
218
+ const boundary = Math.min(query === -1 ? value.length : query, fragment === -1 ? value.length : fragment);
219
+ return value.slice(0, boundary);
220
+ }
221
+ }
222
+ function isRecord(value) {
223
+ return typeof value === "object" && value !== null && !Array.isArray(value);
224
+ }
225
+ function withoutControls(value) {
226
+ return Array.from(value, (character) => {
227
+ const code = character.charCodeAt(0);
228
+ return code < 32 || code === 127 ? " " : character;
229
+ }).join("");
230
+ }
231
+ function deepFreeze(value) {
232
+ if (value && typeof value === "object") {
233
+ for (const child of Object.values(value))
234
+ deepFreeze(child);
235
+ Object.freeze(value);
236
+ }
237
+ return value;
238
+ }
@@ -0,0 +1,3 @@
1
+ import type { NormalizedPolicy } from "./types.js";
2
+ export declare function loadPolicyFile(path: string): Readonly<NormalizedPolicy>;
3
+ //# sourceMappingURL=file.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"file.d.ts","sourceRoot":"","sources":["../../../src/core/policy/file.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,gBAAgB,EAAc,MAAM,YAAY,CAAC;AAE/D,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,QAAQ,CAAC,gBAAgB,CAAC,CAavE"}
@@ -0,0 +1,20 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { dirname, resolve } from "node:path";
3
+ import { stripJsonComments } from "../config.js";
4
+ import { normalizePolicy } from "./evaluate.js";
5
+ export function loadPolicyFile(path) {
6
+ const absolute = resolve(path);
7
+ let parsed;
8
+ try {
9
+ parsed = JSON.parse(stripJsonComments(readFileSync(absolute, "utf8")));
10
+ }
11
+ catch (error) {
12
+ throw new Error(`cannot parse policy at ${absolute}: ${error.message}`);
13
+ }
14
+ try {
15
+ return normalizePolicy(parsed, { baseDir: dirname(absolute) });
16
+ }
17
+ catch (error) {
18
+ throw new Error(`invalid policy at ${absolute}: ${error.message}`);
19
+ }
20
+ }
@@ -0,0 +1,5 @@
1
+ export { evaluatePolicy, normalizePolicy, PolicyDeniedError, policyDigest, summarizePolicyRequest, } from "./evaluate.js";
2
+ export { loadPolicyFile } from "./file.js";
3
+ export type { DispatchCostEstimator, ExternalMutationRequest, NormalizedPolicy, PolicyAskResolution, PolicyAskResolver, PolicyDecision, PolicyEvaluation, PolicyIsolation, PolicyNetworkAccess, PolicyPhase, PolicyRequest, PolicyRequestSummary, PolicyRuleResult, PolicySpec, PolicyVerdict, } from "./types.js";
4
+ export { POLICY_SCHEMA_VERSION } from "./types.js";
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/core/policy/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,cAAc,EACd,eAAe,EACf,iBAAiB,EACjB,YAAY,EACZ,sBAAsB,GACvB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAC3C,YAAY,EACV,qBAAqB,EACrB,uBAAuB,EACvB,gBAAgB,EAChB,mBAAmB,EACnB,iBAAiB,EACjB,cAAc,EACd,gBAAgB,EAChB,eAAe,EACf,mBAAmB,EACnB,WAAW,EACX,aAAa,EACb,oBAAoB,EACpB,gBAAgB,EAChB,UAAU,EACV,aAAa,GACd,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC"}