Start free

Test & deploy

Before a policy goes live, simulate it against a captured action to see the exact verdict, then pull and deploy it through the control plane.

Everything on this page runs the real engine — the same Rust core that gates your agent and stamps receipts, compiled to WASM and called from the browser. A decision you preview locally is byte-identical to the one the gate makes in production, because there is only one implementation. You can simulate, preview the sentence, and validate the floors with no network calls before you push a policy.

Same engine, every surface

The simulate, preview, and validate functions below are the verify-only WASM surface from @hesohq/verify-wasm. The HESO dashboard policy editor calls these exact functions, so what you see in a script is what you see in the console. Read Policy files for the heso.toml format and Conditions & operators for the operator set.

Simulate a decision

evaluatePolicy(tomlSrc, actionJson) runs the real first-match-wins engine and both pinned floors over an action and returns the PolicyOutcome as JSON. That outcome is the same object a receipt carries: the rule_id that decided, the canonical rule_display sentence, the matched_conditions that fired, and the decision_path — one of allow, block, redact, or require_approval.

Pass the full ActionDetail as the action JSON — the verb, the fields, and the structural facts (tool_name, workflow, account). The conditions match against the fields, and the floors match against the verb and the lane, so an incomplete action will not test the floors correctly.

simulate.ts
import init, { evaluatePolicy } from "@hesohq/verify-wasm" await init() // call once, then the named exports are synchronous const toml = `[[rule]]id = "pay-cap"order = 10enabled = truesubject = { kind = "any" }verb = "payment"scope = "*"conditions = [  { field = "amount_usd", op = "lte", value = 5000, display = "$5,000 or less" },]decision = "allow"` // The action JSON is the full ActionDetail — verb + fields + structural facts —// so the engine and the pinned floors evaluate against the same shape a receipt// would carry.const action = JSON.stringify({  verb: "payment",  tool_name: "stripe.transfers.create",  workflow: "vendor-payouts",  account: "acct_19",  fields: { amount_usd: "4200", payee: "Globex LLC" },}) const outcomeJson = evaluatePolicy(toml, action)const outcome = JSON.parse(outcomeJson) console.log(outcome.rule_id)        // "pay-cap"console.log(outcome.rule_display)   // "Allow a payment of $5,000 or less"console.log(outcome.decision_path)  // "allow"console.log(outcome.matched_conditions)// [{ field: "amount_usd", op: "lte", value: "5000" }]
The floors are part of the verdict

A dangerous lane — a payment, a delete, an account change, a large data export — carries a built-in floor that the engine enforces. Even if your rule says allow, the floor can raise the outcome to require_approval. Simulating with the real action JSON shows you that before it surprises you in production. See Pinned floors.

Preview the sentence

Every rule has one canonical English sentence, and that is the string a receipt stores in policy.rule_display. Render it before you deploy so the words an auditor reads are the words you intended. ruleToSentence(ruleJson) produces that sentence from a single rule; policyRulesFromToml(tomlSrc) parses a whole policy into the structured rule array using the real parser.

preview.ts
import init, { ruleToSentence, policyRulesFromToml } from "@hesohq/verify-wasm" await init() // Parse the whole policy with the real parser. Throws [PARSE] on bad TOML.const rules = JSON.parse(policyRulesFromToml(toml)) // PolicyRule[] // Render the canonical English for one rule — the exact string that lands in a// receipt's policy.rule_display.for (const rule of rules) {  console.log(ruleToSentence(JSON.stringify(rule)))  // "Allow a payment of $5,000 or less"}

ruleToSentence()function

Render the canonical English sentence for one rule — the exact value that appears as rule_display on a receipt.

ts
ruleToSentence(ruleJson: string): string

Parameters

ruleJsonstringrequired
A single PolicyRule serialized as JSON.

Returnsstring

The canonical sentence, e.g. “Allow a payment of $5,000 or less”.

policyRulesFromToml()function

Parse a heso.toml policy into its structured rules with the real parser.

ts
policyRulesFromToml(tomlSrc: string): string

Parameters

tomlSrcstringrequired
The full heso.toml source.

Returnsstring

A JSON array of PolicyRule, in evaluation order.

Throws

[PARSE] if the TOML is malformed.

Validate the floors

Two functions catch a bad policy before it can deploy. parsePolicy(tomlSrc) runs the full parse plus the load-time floor check; validateNoFloorBypass(rulesJson) runs only the floor check over already-parsed rules. Both throw, and the error message starts with the engine reason:

  • [PARSE] — the TOML is malformed and the engine could not load it.
  • [FLOOR_BYPASS] — a rule tries to allow a dangerous lane without approval. The message names the offending rule id and verb so you can fix the exact line. A policy may tighten a floor, but it can never allow-without-approval a dangerous lane.
validate.ts
import init, { parsePolicy, validateNoFloorBypass, policyRulesFromToml } from "@hesohq/verify-wasm" await init() try {  parsePolicy(toml) // full parse + load-time floor check} catch (err) {  // err.message starts with the engine reason, e.g.  // "[PARSE] …"        — malformed TOML  // "[FLOOR_BYPASS] …" — a rule tries to allow a dangerous lane without approval  //                      (the message names the offending rule id + verb)  console.error(err)} // Or check only the floors, given already-parsed rules:const rulesJson = policyRulesFromToml(toml)validateNoFloorBypass(rulesJson) // throws [FLOOR_BYPASS], returns void on success
These checks run in the browser too

The dashboard policy editor calls parsePolicy and validateNoFloorBypass as you type, so a floor-bypassing policy is rejected before deploy — not at runtime. The full set of dangerous lanes and what each floor enforces is on Pinned floors.

Pull the current policy

To see what is live for a team, pull it from the control plane. pullPolicy(teamId) from @hesohq/sdk returns a PolicyBundle with the deployed version, the rules in evaluation order, and the fetchedAt timestamp. The SDK verifies the bundle’s integrity hash before it hands the rules back, so you know the policy you fetched is the policy the server stored.

import { configure, pullPolicy } from "@hesohq/sdk" configure(process.env.HESO_API_KEY!, process.env.HESO_ENDPOINT!) const bundle = await pullPolicy("team_19")// PolicyBundle { version, rules, fetchedAt }// The SDK verifies the bundle's integrity hash before returning it. console.log(bundle.version)    // the deployed policy versionconsole.log(bundle.rules)      // PolicyRule[], in evaluation orderconsole.log(bundle.fetchedAt)  // ISO timestamp of the pull

Call configure(apiKey, endpoint) once at startup before any cloud call. The auth and environment variables are covered on Authentication.

Deploy

Once a policy simulates the way you expect and passes the floor check, deploy it. There are two paths, and both end at the same store:

  • The dashboard policy editor. It uses the same browser functions described above — evaluate, preview, validate — so it refuses to save a policy that would throw [PARSE] or [FLOOR_BYPASS]. This is the path most teams use.
  • Your own control-plane flow. Push the policy through the cloud API and let the backend store it. See the API reference for the endpoints.

Either way, the backend stores the exact bytes you deployed and hashes them into a policy_hash. That hash is what the gate pins a decision to, and it is what pullPolicy verifies on the way back — so the policy that gates an action is provably the policy you deployed, end to end.

What a simulation proves — and what it doesn't

A local simulation proves how the engine will decide a given action under a given policy: the rule, the sentence, the matched conditions, the decision path, and any floor that applies. It is a dry run, not a record that the action happened — that comes from a signed receipt, which runs the same engine and adds the signatures.

Next steps