Air traffic control for AI coding

Nool Gate Authoring Guide: DSL Policies & WASM (Aram)

Enforce architectural rules and governance gates in Nool using declarative DSL invariants and compiled WASM component policies evaluated across every lifecycle stage.

  • Lifecycle stages: pre-tool, pre-propose, post-propose, pre-solidify, pre-push and doctor.
  • Declarative YAML DSL rules with content matching, path regex, and Shannon entropy secret scanning.
  • Sandboxed WASM components via WIT world policy with PolicyContext and deterministic execution.
  • Policies that decide on evidence: evidence_has_attestation, landed_validation_is and EVIDENCE_PENDING at push.
  • Rego (opa build -t wasm) and forbid-only Cedar policies hosted in the same gate.
  • Configurable failure-action cascades and per-stage fail-fast controls in nool.toml.
nool doctor

1. Where gates run — the lifecycle stages

Every stage is a `LifecycleStage` (`crates/nool-core/src/models.rs:380`). Gates fire deterministically from the CLI at these points in the transition lifecycle: | Stage | CLI command | What's evaluated | | --- | --- | --- | | `pre-tool` | an agent's edit or shell call in `nool code` | The call, before it runs, against `[rules] forbidden_paths` and DSL rules that list this stage (since 7.23) | | `pre-propose` | `nool propose` | Candidate knot vs full gate stack (this is where most policies live) | | `post-propose` | after propose | Rarely used; available for audit-style rules | | `pre-solidify` | `nool solidify` | Final check before the knot is signed + appended | | `pre-push` | `nool push` | Last checkpoint before replication | | `doctor` | `nool doctor` | Evaluates current DAG heads for release-readiness, not new work | **Default scoping (no config needed):** - A DSL rule with no `stages:` field runs at every stage except `pre-tool`, which runs only rules that list it. - A WASM policy not named in any hook runs at every stage except `pre-tool`. - A policy or rule listed in a hook runs only at the stages that name it. Hooks are additive — binding one policy to one stage never disables other policies.

  • pre-tool: evaluated before a nool code agent's edit or shell command runs; a blocking rule denies the call, a warning rule lets it run with a notice, and a policy that cannot be read denies.
  • pre-propose: evaluated during nool propose against candidate diffs before stashing.
  • pre-solidify: evaluated before signing and appending a Knot to the DAG.
  • pre-push: evaluated before replicating history to upstream remotes.
  • doctor: evaluates the current DAG head during nool doctor for release readiness.
  • Hooks are purely additive and never disable unmentioned policies.
  • Community loads up to 3 policies, Team up to 11 and Enterprise has no limit; WASM modules and DSL rules count together.

2.1 DSL Policies: File placement & formats

Drop `*.yaml`, `*.yml`, or `*.json` files into `[aram].policies_path` (default `policies/`). Three distinct file shapes are accepted: 1. **Single rule**: A solitary rule object at the root of the file. 2. **Multiple rules in one file**: Rules separated by YAML document delimiters (`---`). 3. **Wrapped array**: Rules placed under a top-level `rules:` list. Rules are sorted by `inv_id` then `title` — evaluation order is deterministic regardless of file system discovery order.

2.2 DSL Rule schema (complete specification)

DSL rules support rich metadata, scoped evaluation, conditional when guards, requires/forbids assertions, and query checks.

  • inv_id: Unique identifier for the invariant rule.
  • scope: Target scope (global, entity, feature, projection, or composite).
  • mode: blocking (blocks operation), warning (flags finding), advisory (logged).
  • reason_code: Machine-readable HTTP/Aram status code returned upon violation.

2.3 All DSL predicates reference

Predicates operate on Mutation, Merge, NodeMutation, Raw, and Message payloads, and scan across every mutation inside a multi-file Synthesis proposal.

2.4 Real example: no_secret_keys.yaml

A production-grade blocking secret scanner with smart scoping that automatically ignores test fixtures and self-referential policy patterns:

2.5 Policies that decide on evidence

Since 7.21 a policy can decide on what is known *about* a sealed knot: its attestations (each marked `trusted` when its signer is a landed checker key or holds a `checker:` grant), its gate receipt, its validation state (`full`, `deferred` for a `--fast` seal not yet validated, `settled`, `unknown`) and the verdict fingerprint the seal recorded. The host builds this evidence projection; the policy never looks anything up. A task completion (`task finish`, `task verify-done`) is judged on the evidence about the knot it names as landed. At push and in doctor every other knot is judged on its own evidence. On `nool pull` there is no evidence, so an evidence rule is recorded `unverifiable` there, never a refusal. A rule that reads evidence fails with reason `428` (`EVIDENCE_PENDING`) unless it sets its own `reason_code`. `nool push` runs the `pre-push` stage over every outgoing knot and, for that code, refuses with exit 3 and names the policy and the knot: retry once the attestation lands. Every governed seal leaves a signed gate receipt that records the evidence's hash, so `nool audit verify` re-runs the rule on exactly what it saw.

3. WASM hooks — [aram.hooks] and failure cascades

Hooks in `nool.toml` bind specific `.wasm` policies to stages and tune their sandbox constraints: ```toml [[aram.hooks]] stage = "pre-solidify" # pre-propose | post-propose | pre-solidify | pre-push | doctor policies = [ "license-guard", # bare name → loads policies/license-guard.wasm { policy = "heavy-scanner", on_failure = "warn" } # per-policy override ] on_failure = "block" # hook-level default: block | warn | allow memory_limit = 67108864 # optional: sandbox memory ceiling (bytes) fuel_limit = 5000000000 # optional: Wasmtime fuel budget for this hook [gating] fail_fast = true wasm_policy_failure = "block" [gating.fail_fast_per_stage] doctor = false # allow doctor to collect all findings without halting ``` **Failure-action cascade (WASM only; DSL uses its own mode):** `per-policy on_failure` → `hook on_failure` → `[gating] wasm_policy_failure` → `built-in Block`. **Crucial distinction:** - **Deny**: The policy ran successfully and returned a denial verdict → always blocks, cascade does not apply. - **ExecutionError**: A sandbox trap, out-of-fuel, memory limit, or instantiation error occurred (no verdict) → governed by the cascade. Setting `warn` allows work to proceed with a loud warning.

  • Per-stage fail-fast: [gating.fail_fast_per_stage] map overrides [gating] fail_fast.
  • Sandbox memory limit default: 16 MiB (16,777,216 bytes).
  • WASM policies outside any hooks run unconditionally at every stage except pre-tool.

3.1 Rego and Cedar policies

Aram also hosts policies you already write in Rego or Cedar, from the same `[aram].policies_path`: - **Rego**: a module built with `opa build -t wasm` is recognised as an OPA module and runs in the same sandbox, under the same fuel and memory ceilings as every other policy. - **Cedar**: a `.cedar` file is evaluated forbid-only. Nool appends `permit(principal, action, resource);`, so a policy denies only what it explicitly forbids. Both follow the WASM fail-direction table: a policy that will not load or evaluate refuses under `block`; under `warn` the knot lands with the policy recorded `unverifiable`, never `pass`, and `nool doctor --strict` then fails with `POLICY_UNVERIFIED`. Each result goes into the gate receipt like any other policy's.

  • `[gating] acs_manifest` binds a forbid-only Cedar seal policy from an Agent Control Specification manifest; its hash and verdict are recorded in the gate receipt, and a `permit` clause is refused rather than run.
  • `nool evidence knot <id> --format acs` exports a knot's gate decision as ACS v0.1.0 audit records.

4.1 WASM policies SDK: Writing and building

Two module flavors are supported by the Aram loader: 1. **Core modules**: Export `memory`, `alloc(i32) -> i32`, `validate(ptr, len) -> i32` (1 = allow, other positive int = deny reason code). Legacy/simple path. 2. **WASM components (recommended)**: Implement the WIT world `policy` in `crates/nool-aram-policy-sdk/wit/aram.wit`. Export: `validate(knot-bytes, knot, lifecycle-stage) -> result<_, denial>`. **Scaffolding a policy:** ```bash nool admin plugin init my-policy --plugin-type policy ``` **Rust Implementation (`src/lib.rs`):**

cargo build --target wasm32-unknown-unknown --release -p my-policy
cp target/wasm32-unknown-unknown/release/my_policy.wasm policies/

4.2 PolicyContext API surface & Reason codes

The `PolicyContext` exposes structured access to the Knot, host graph state, and host logging helpers: | Helper | Purpose | | --- | --- | | `ctx.knot` | Full deserialized Knot (header + payload + signature) | | `ctx.as_mutation()` | `(kind, semantic_path, diff)` for Mutation knots | | `ctx.as_task()` / `ctx.as_crast()` | Typed accessors for Task / CRAST knots | | `PolicyContext::get_active_contracts()` | Active contract payloads via host | | `PolicyContext::get_active_announcements()` | Active announcements (lease & conflict checks) | | `PolicyContext::get_current_invariants()` | Current invariant payloads | | `PolicyContext::fact_projection()` | Pre-built bounded architecture facts (host walked the graph before guest started — O(1) fuel cost to guest). `None` = absent; never treat absence as a violation | | `PolicyContext::evidence()` | The evidence projection about the knot being judged (attestations, gate receipt, validation state); `None` where the stage has none | | `PolicyContext::stage` | Which lifecycle stage is running | | `PolicyContext::log_{error,warn,info,debug}` | Structured logging to host stdout/stderr | **Standard Reason codes (`reason_codes` module):** - `400 MALFORMED_KNOT`: structurally invalid Knot - `401 EMPTY_DIFF`: change carries no diff content - `402 INSUFFICIENT_INTENT`: missing or too-short intent description - `403 UNAUTHORIZED`: authorship or identity scope violation - `404 SCOPE_VIOLATION`: change exceeds declared blast radius - `406 INVARIANT_VIOLATION`: structural or relational invariant broken - `407 BYZANTINE`: replayed Knot integrity check failed - `410 TEMPORAL_VIOLATION`: HLC clock skew exceeds tolerance - `428 EVIDENCE_PENDING`: the evidence a policy needs has not landed yet (push exits 3; retry) - `499 CUSTOM_VIOLATION`: policy-specific custom denial code

  • PolicyResult variants: Allow, deny(reason_code, msg), deny_with_evidence(reason_code, msg, path).
  • Fact projections are computed prior to guest execution to ensure zero WASM fuel consumption on graph traversals.

4.3 Sandbox guarantees (deterministic enforcement)

The determinism validator runs on every policy load and strictly enforces: - **Strict Import Allowlist**: Only `nool:aram/host` functions (`log`, `get-active-contracts`, `get-active-announcements`, `get-current-invariants`, `get-fact-projection`, `get-evidence-projection`). No WASI, no system clock, no filesystem or network I/O. - **Bit-exact Floats**: Non-bit-exact float arithmetic is rejected under `strict_floats = true` (loads, stores, constants, and comparisons are permitted since they are spec-determined; arithmetic operations with potential NaN-payload divergence are rejected). - **Fuel & Memory Budgets**: Fuel (`[aram].fuel_limit`) and linear memory (`[aram].memory_limit`, default 16 MiB) enforced per invocation via Wasmtime `StoreLimits` with per-hook overrides. - **Recursion Limits**: Nested components/modules scanned recursively (depth ≤ 16). - **Deterministic Chain Ordering**: Sorted strictly by policy filename stem.

  • Zero-I/O isolation: WASM policies cannot access network, filesystem, or env vars.
  • Cross-platform execution reproducibility guaranteed across architectures.

4.4 When to use DSL vs WASM

DSL and WASM policies complement each other and compose freely in the same `policies/` directory: | Use DSL when… | Use WASM when… | | --- | --- | | Regex, substring, or Shannon entropy checks on diffs | You need real parsing, control flow, or AST cross-references | | Metadata gating (author pubkey, tag, intent present, test heuristics) | You must consult contracts, announcements, or fact projections | | You want policies editable instantly without recompilation | Logic exceeds what declarative regex/predicates can express | | Blocking + warning severity suffices | You need structured evidence and custom diagnostics attached to denials | *Both compose freely in the same `policies/` directory — the first denial in deterministic chain order wins.*

  • DSL: Fast iteration, declarative YAML, ideal for diff pattern scanning and test coverage rules.
  • WASM: High expressiveness, Rust SDK, ideal for complex semantic analysis, AST validation, and contract gating.