Skip to content
Integra Agent Guard

Verify before sign

The order the gate runs its steps in, what each one can conclude, and where the signing key becomes reachable.

transact is evaluate plus one rule: invoke the signing key only when the decision is proceed.

import {
  type BuyerPolicy,
  type GatePorts,
  type GateProposal,
  type GuardedSigner,
  evaluate,
  transact,
} from "@integraledger/agent-guard";

declare const proposal: GateProposal;
declare const policy: BuyerPolicy;
declare const ports: GatePorts;
declare const signer: GuardedSigner;

// The decision on its own — the signing key is not involved at all.
const decision = await evaluate(proposal, policy, ports);

// The decision, enforced against your key.
const result = await transact(proposal, policy, ports, signer);

evaluate never touches a key. transact is the only place a signer appears, and it appears after a single kind !== "proceed" early return. That is what makes "before any signing key is invoked" a structural claim rather than a discipline.

The order, and why it is that order

Fetch and retain the terms

Always — even at Level 1. Retaining the evidence is a policy decision, never a silent skip.

The fetch is caught, not allowed to throw. Every failure the shipped fetcher raises is counterparty-reachable: a 404 on a URL the seller chose, a TLS error, a body over the cap, a host that resolves to a private address. Each arrives as a value you can act on — gate/terms-unfetchable, carrying the fetcher's own message verbatim so you can tell which failure it was.

It is classed verification-failure, not policy-rejection: no policy of yours rejected anything. The terms could not be obtained, so the fingerprint could not be recomputed, so the guard cannot say the document is the one that was advertised — the same thing a mismatch says, arrived at one step earlier.

Check the level floor

proposal.level < policy.requiredLevel declines with gate/below-required-level.

Recompute the fingerprint — the halt

SHA-256 over the fetched bytes, compared to proposal.advertisedAtrHash. A mismatch declines with gate/fingerprint-mismatch and the halt class verification-failure.

Every GateProposal carries a validated advertised hash, so this step always runs. There is no "verify if present" path, because skipping a present, verifiable hash is a silent fail-open.

Check the seller-assurance floor

Independent of the terms format, so it gates every proceed path and is checked before the typed/coverage-gap branch. A buyer that tolerates non-machine-readable terms must not thereby lose its assurance floor.

Evaluate policy on the typed envelope

The fetched bytes are parsed into a conformant legal-context record and policy runs on that — never on the prose. See the typed proposal.

If the bytes are not a conformant record, that is a coverage gap, and your onNotAttempted disposition decides it. Never a silent proceed.

Proceed

The decision is recorded and returned. transact calls your signer exactly once, with the fingerprint that was proved.

The three decisions

import type { GateDecision } from "@integraledger/agent-guard";

declare const decision: GateDecision;

switch (decision.kind) {
  case "proceed":
    break;
  case "decline":
    // decision.haltClass, decision.code, decision.detail
    break;
  case "escalate":
    // decision.bytes, decision.hash, decision.reason
    break;
}

Escalate binds display to signature

An escalation hands back the exact bytes that were fetched and their hash, where hash === sha256(bytes). Show those bytes to your approver. What a human reads is then provably what a key would sign — which is the whole point of routing a gap to a human rather than to a default.

Post-settlement verification

Separately from the gate, verifySettled turns a settled receipt into a transaction record: it recovers the on-chain atrHash through an injected binding adapter and runs a mechanical verification.

import { type MechanicalPorts, verifySettled } from "@integraledger/agent-guard";

// `SettlementRef` and the `recover` half of `WeldAdapter` are types from the peer
// `@integraledger/lcp-binding-core`. Taken off the function's own signature here so the
// example needs one import, but you may import them directly instead.
type Args = Parameters<typeof verifySettled>;

declare const ref: Args[0];
declare const adapter: Args[1];
declare const ports: MechanicalPorts;

const report = await verifySettled(ref, adapter, ports);

verified is raised only if the ATR bytes hash to the recovered on-chain hash and the class-required steps hold. A refused recovery leaves the settled hash absent, which makes the fingerprint indeterminate, which keeps verified false. The chain reader is injected, which is what keeps the guard viem-free.

Edit on GitHub

Last updated on

On this page