ERC-8312: Bounded Agent Actions

Pulled orbmis/headroom and recomputed it rather than take the config, and it checks out. Your cap invariant isn’t reserved+confirmed ≤ cap on a single cursor like the StatefulBound reference; it’s cumulativeTurnover ≤ maxCumulativeTurnover (CURSOR_HEADROOM, HeadroomPoC.sol L298/534), read via cursorSummary(envelopeId) on the EnvelopeRegistry 0x091f…07d1. Ran it against envelope 0x48e972a2…bcb041 on Base Sepolia (84532):

portfolioValue        10000
cumulativeTurnover        0
maxCumulativeTurnover  8000
remainingTurnover      8000
vaultAllocations   [2500,2500,2500,2500,0,0]  = 10000 bps
riskAllocations    [5000,5000,0]              = 10000 bps
cursorRoot 0x2f19c6e6…  (= slot-5 of getEnvelope)   active true


VERIFIED-GOOD at Base-Sepolia block 43697267, stateRoot 0xbb4fbc638e657de5e9fbdefcc290054e251087cf9948b471c15677b09e3a89ff:

  • cumulativeTurnover 0 ≤ maxCumulativeTurnover 8000 — the headroom bound holds,

  • I re-derived remaining = 8000 − 0 = 8000, matching the contract’s own remainingTurnover (not read on faith),

  • allocations sum to 10000 bps on both the vault and risk-bucket axes — fully allocated, in bounds.

Two things worth flagging for the thread:

  1. This is a second, independently-recomputable instance of the same ERC-8312 guarantee — a turnover-cursor headroom model, distinct from the reserved+confirmed aggregate of the zero-human-loop StatefulBound, both reducing to “aggregate ≤ cap, provable from public state.” Two different implementations converging on one checkable invariant is a good sign for the spec.

  2. Our recompute-kit 8312/cap-conservation recipe is tuned to the StatefulBound storage layout, so it doesn’t drop onto your accessor shape — I recomputed via cursorSummary here. I’ll generalize the recipe to take an accessor form so it covers the headroom model too; happy to fold your case in as a second conformance vector if you’re up for it.

Clean on my side, the bound holds live, re-runnable by anyone from that one cursorSummary call.

Made good on it, pulled orbmis/headroom, mapped the storage, and generalized the recompute-kit recipe so it covers your model as a first-class layout, not a one-off:

bin/recompute-step 8312/cap-conservation \
  https://base-sepolia-rpc.publicnode.com \
  0x091f665B9914b2295861F3e9c05D79eF9b2A07d1 \
  0x48e972a25a6ecc83b0dd9c2943b63155ec899b53e10c1f1eb34ebc1d57bcb041 \
  struct:2:2:1
→ ✓ VERIFIED-GOOD — aggregate 0 ≤ cap 8000, proven from storage


struct:S:A:C = cursorValues at mapping-slot 2, aggregate = cumulativeTurnover (+2), cap = maxCumulativeTurnover (+1) — both read from storage via eth_getProof against the stateRoot, so the cap isn’t taken from cursorSummary’s view either. The recipe now takes either a literal cap (StatefulBound’s reserved+confirmed) or that struct layout, one invariant behind both. Your envelope is in the shared suite as cap-conservation-holds-headroom (trustless-ai/recompute-kit 1ff793e), so anyone can re-run the above and get the green themselves.

The nice part: your turnover-cursor headroom model and the zero-human-loop StatefulBound reserved+confirmed model are two independent implementations reducing to the same checkable thing — aggregate ≤ cap, provable from public state. Two impls converging on one recomputable invariant is exactly the signal an ERC wants. Happy to keep your case pinned as the second conformance vector; ping me if the deployment moves and I’ll re-point it.

Co-author of ERC-8226 (Regulated Agent Mandate) here, with a prior art note. This thread is converging on constructs that ERC-8226 has specified since April 12 (PR #1679, now PR #1844 with a reference implementation including end to end enforcement through the ERC-7943 transfer hook). Since the spec text here is being written right now, the record should show where the overlap sits. We reached the same shape from the regulated asset side, so mapping it rather than restating it.

The counter. A RAMS mandate is keyed (agent, principal) and holds one authoritative cumulativeUsed slot in the registry. Every execution path writes to that same slot through recordExecution: the token compliance hook, an EIP-7702 account, or an executor. There is no per surface state to reconstruct, and canExecute reads the total back in O(1). A monotonic, registry held, cross venue counter against a committed bound is specified and implemented today in the regulated domain.

The aggregate. On the observation upthread that no prior construct holds one cap across relationships and venues: within the regulated perimeter, ERC-8226 does. Every venue there is compliance gated by construction, so all draws are forced through the registry and the aggregate holds without cooperation from the surfaces. What that perimeter provides for free, an open agent economy has to build, which is presumably where the reservation and witness machinery in this spec earns its complexity.

Counting and enforcing, fused by design. RAMS deliberately does not separate them. canExecute bundles existence, validity window, revocation, per action enablement, enforcer freeze, per transaction cap and cumulative cap into one read, and recordExecution re-applies the identical checks before advancing. The meter and the gate are the same read, evaluated in the same frame as the transfer, on the asset’s own non custodial path under ERC-7943: the principal holds the tokens, but the asset reverts a non compliant transfer, so nothing, not even the principal’s key, advances the counter without passing the gate. A reverted transfer reverts its own advance, so the reservation problem never arises in this frame. A meter that can drift from the enforcement it feeds is the gap a regulator asks about, which is why fusion is a requirement for securities rather than a design preference.

The layer the meter does not carry. What makes ERC-8226 a distinct standard is the regulated surface around the counter: a KYC verified principal, a compliance provider checked at grant, an enforcer role that can freeze an agent independently of the principal, and an EIP-712 / EIP-1271 signed lifecycle so a smart wallet principal can grant and revoke. That legal surface is out of scope for a general metering object, and it is where mandate authority for regulated assets actually lives.

One question on scope, since it affects how implementers in the regulated domain will read this. ERC-8312 treats enforcement as substrate defined and stays deliberately agnostic about it. For regulated instruments that substrate is not a free choice: enforcement has to live in the asset’s own transfer path under ERC-7943, in the same frame as the compliance gate, or it is not enforcement a regulator accepts. Does ERC-8312 intend to specify how the cursor binds to enforcement, or does it stop at the counting layer and leave binding to the substrate, in which case the regulated substrate is already specified in ERC-8226 and the two are orthogonal by design?

Spec and discussion: ERC-8226: Regulated Agent Mandate

1 Like

Thanks for bringing up ERC-8226. I actually think it’s a useful comparison because it highlights what I see as the architectural boundary ERC-8312 is trying to establish.

From my perspective, the question isn’t whether ERC-8226 can express cumulative caps or bounded mandates. It certainly can. The question is whether every authorization or mandate standard should also define its own cumulative-consumption semantics, or whether those semantics should be factored into a reusable primitive that any authorization model can adopt.

ERC-8312 argues for the latter.

The purpose of ERC-8312 is to standardize bounded authority as a shared state machine, independent of how that authority is issued or enforced.

That distinction becomes important once the same authority can be exercised across multiple execution venues. A principal may authorize actions through a smart account module, a vault, a workflow engine, a settlement contract, a token hook, or a regulated-asset mandate. Each of those systems can define who is authorized and under what conditions. What they should not all have to reinvent is the logic for representing and consuming the same bounded authority over time.

That’s what the cursor is for.

I don’t view the cursor as bookkeeping. I view it as the canonical state that serializes bounded authority across heterogeneous execution environments. Without a shared state object, every implementation ends up defining its own notion of cumulative consumption, making interoperability much harder.

On enforcement, I agree this should be explicit.

ERC-8312 should not prescribe a single enforcement venue, because that would unnecessarily constrain implementations. However, it absolutely should specify the enforcement invariants that every conforming substrate must satisfy:

  • The action must be authorized under the committed capability.
  • The transition must remain within the represented bound.
  • Cursor advancement and the represented state transition must be atomic, or the substrate must provide an equivalent synchronization guarantee.
  • Unauthorized parties must not be able to advance a cursor and consume another party’s authority.
  • Implementations claiming non-bypassable bounded authority must ensure that all valid execution paths are subject to those checks.

In other words, ERC-8312 specifies the enforcement contract without mandating a single enforcement mechanism.

Viewed through that lens, I don’t see ERC-8226 and ERC-8312 as competing standards.

ERC-8226 defines a regulated authorization model. ERC-8312 defines a reusable bounded-authority primitive that regulated authorization models, account abstraction systems, vaults, workflow engines, settlement protocols, and future standards can all compose with.

To me, this is similar to many other successful ERC boundaries: we generally standardize reusable protocol semantics once, rather than embedding the same state transition logic independently into every higher-level standard that could benefit from it.

If ERC-8226 (or any future authorization standard) chooses to use an ERC-8312 envelope as its shared consumption object, I would consider that a success for both standards. It means authorization and bounded-consumption semantics remain independently evolvable while interoperating through a common interface, rather than each authorization framework defining its own incompatible model for cumulative authority.

1 Like

This is the right framing, the cursor as canonical state, factored out of both issuance and enforcement, is what lets “bounded authority” mean the same thing in a smart account, a vault, or a workflow engine. I’d add the corollary that turns it from an architectural nicety into something enforceable-by-anyone: if the cursor is the canonical state, then the bound is checkable from that state, independently of whichever venue advanced it.

That’s the part we’ve been making concrete. The aggregate ≤ cap invariant recomputes straight from the cursor’s storage (eth_getProof against the state root), and we now run it over two independent cursor serializations with one recipe:

  • your StatefulBound cursor reserved + confirmed

  • a turnover cursor cumulativeTurnover ≤ maxCumulativeTurnover (orbmis’s headroom)

Same invariant, two layouts, both read from public state rather than from a view function’s say-so. So “bounded authority as a shared state machine” isn’t only asserted at the interface — the state machine’s core invariant is reproducible by anyone from the chain, whatever venue did the enforcing.

That also speaks directly to your enforcement list, in particular no unauthorized cursor advancement: a storage-proof recompute witnesses the cursor value at a block for exactly what it is, so you can read whether the bound still holds without trusting the enforcer. That’s precisely the property you want if the same cursor is going to be honored across heterogeneous venues, the venue-agnosticism of the primitive and the venue-agnosticism of the check are the same move. A cursor that any authorization model can adopt is only as strong as a bound anyone can verify, and the check has to be as model-agnostic as the primitive: it reads the cursor’s storage, not the issuance path.

1 Like

Appreciated @blockbird @TMerlini, and I think we agree on the main point: these are orthogonal, one is a regulated authorization model, the other a general bounded-authority primitive. Two clarifications for the record.

On recompute: it already runs against RAMS. cumulativeUsed and maxCumulativeValue are public storage, so aggregate ≤ cap verifies from the state root by eth_getProof, the same recipe @TMerlini describes, just a different layout. canExecute was never the source of truth, so there is no view function to take on faith.

And it cuts the other way. A storage proof of a split cursor shows what the meter claims. The same proof against RAMS shows what actually settled, because the advance is atomic with the ERC-7943 transfer and a reverted transfer reverts its own advance. Reserve, confirm, cancel is an elegant way to keep a meter in sync with enforcement across frames; atomicity removes that gap before it appears, which is why RAMS does not carry the machinery. Same invariant, proven at settlement rather than asserted at the meter.

On adopting an 8312 envelope: I would put it exactly as you did, independently evolvable and interoperating through shared, recomputable state. The difference is which side holds the state, and RAMS already holds it, with the enforcement, freeze, and compliance around it in one contract. For a regulated asset the counter has to sit where the enforcer and compliance operator govern it, not factored out, and moving it out would pull RAMS toward the split model that fusion avoids. If someone wants the shared recipe later, RAMS can expose a compatible read view over its existing slots, no envelope adopted.
One framing point. The shared layer is not any single standard’s interface, it is the state root. Both counters recompute from it by eth_getProof with no shared envelope, which is why RAMS interoperates today without adopting one. Which format, if any, becomes the common one looks like a community call, not one draft’s to decide.

On prior art, the related-work section is thorough, so it stood out that ERC-8226 is not in it, a merged and directly adjacent standard with cumulative value caps. ERC-8118 (Agent Authorization) is in the same space too, usage-limited delegation with automatic revocation, though as an unmerged draft it is harder to reference. One correction on the aggregate point: a RAMS cap already holds across a principal’s venues, since the token hook, the account, and the executor all write to the same mandate slot, and the registry is many to many, keyed (agent, principal). It caps each mandate on its own rather than pooling them, which for regulated delegation is the point: each mandate is a separate authority and is capped separately.
Worth a look at ERC-8118 regardless, it helps mark where the primitive ends and each authorization model begins.

Good thread. Useful to have a fused settlement-time design and a split meter-time design against the same invariant in public.

1 Like

@thamerdridi agree, and thanks for the precision.

On recompute, yes, and that’s exactly how the shared recipe is already built. 8312/cap-conservation proves aggregate ≤ cap from the storage slots via eth_getProof against the block’s stateRoot, explicitly not a trusted meter/view read. So “canExecute was never the source of truth” isn’t a gap between us, it’s the premise. RAMS’ cumulativeUsed / maxCumulativeValue verify the same way, different layout, same one line of proof.

Your framing is the one I’d keep: the shared layer is not any standard’s interface, it’s the state root. Interop is two counters recomputing from the same root by eth_getProof, no envelope adopted. That’s the whole thesis, you compose through recomputable state, not a shared ABI.

On settled vs claimed, fair, and I’d draw it the same way. RAMS proves at settlement because the advance is atomic with the ERC-7943 transfer; reserve/confirm/cancel proves what’s reserved and reconciles after. The meter’s trajectory is itself non-forgeable (confirm/cancel bound to the reserver, no third party can move it), which is what keeps it recomputable, but you’re right that atomicity removes the reserve→settle gap rather than tracking it, and for a regulated asset the counter belongs where the enforcer and compliance operator govern it, not factored out. Fused settlement-time and split meter-time, same invariant, both provable from the root, exactly the useful thing to have side by side in public.

And agreed: which format, if any, becomes common is a community call, not one draft’s to make. A compatible read view over RAMS’ existing slots, no envelope, just the slots, is the right shape if/when someone wants the shared recipe. (The 8226 / 8118 prior-art note is one for @blockbird related-work, worth folding in.)

1 Like

Agreed on orthogonality, and the recompute result is the part I care most about: cumulativeUsed and maxCumulativeValue proving from the state root by eth_getProof means the invariant verifies against RAMS today, no view taken on faith. Two independently designed counters landing in the same one-line proof is the thesis working.

One precision on the shared layer, because there are two planes in play. For audit you are right, and it is the frame I would keep: the shared layer is the state root, two layouts recompute from it, no shared envelope needed. Ordering is the plane where the envelope earns its place. A root lets anyone read every counter; a pooled aggregate needs every surface writing one counter. When a principal wants one cap across mandates and venues, that sum has to exist as a single written object every draw advances, which no per-layout read can assemble at check time. RAMS does not need that object because it caps each mandate on its own, by design. The general primitive exists for the case where someone wants the pool.

On settled versus reserved, conceded, with one note on where that lands architecturally. Fusing the advance into the ERC-7943 transfer, with freeze and compliance in the same contract, is exactly the binding the spec’s enforcement clause asks a substrate to provide: value cannot move without the counter moving. So I read the fusion as a conformant home for a cap rather than a rival to the metering model. Reserve, confirm, cancel exists for the frames RAMS deliberately does not have, deferred and cross-surface settlement, where a reserve-to-settle window has to be kept honest; reservation binding keeps that trajectory non-forgeable rather than asserted, though you are right that atomicity removes the window instead of tracking it. For a regulated asset the counter belongs where the enforcer and compliance operator govern it, and I would not factor it out either.

Thank you for the aggregate correction, it is a real one. A RAMS cap does hold across a principal’s venues, hook, account, and executor writing the same mandate slot. What remains is intent, not deficiency: per-mandate caps are the point of regulated delegation, and the pooled cross-mandate aggregate is the point of the general primitive.

On adoption we agree, no envelope, and a compatible read view over existing slots is the right interop shape. For what it is worth, the budget profile’s read surface (bound, spent, remaining) is one ready-made shape for such a view if it is ever useful. Which format becomes common is the community’s call, agreed. The case a draft can make is what gets built against it.

On prior art, accepted, and it is fair to flag your own adjacent work: ERC-8226 is now in the related-work section as the directly adjacent regulated design, linked since it is merged, and ERC-8118 is noted as text while it is unmerged. Both are in PR #1833 now.

1 Like

Aligned, and thanks for engaging on the detail @blockbird @TMerlini. So we have it: the shared layer is the state root, two counters recompute from it by eth_getProof, no envelope adopted, and RAMS keeps its counter where the enforcer and compliance govern it. A read view over the slots is the shape if anyone ever wants the shared recipe.

I appreciate the 8226 / 8118 note.

Happy to compare notes as both move forward.

3 Likes

Agreed, and the summary I would pin is the one you opened with: orthogonal, a regulated authorization model and a general bounded-authority primitive. On the shared layer, both halves for the record: the state root is where any counter verifies, and the envelope is where a pooled aggregate exists. One cap across a principal’s mandates, venues, and settlement frames has to live as a single written object every draw advances, and that is the case the general primitive is for. RAMS’s fused counter is a conformant home for the per-mandate case and does not need it. Happy to compare notes as both move forward.

The single written object the pooled-aggregate case needs now exists: an aggregate-budget profile for delegation trees, specified and tested. An agent spawns sub-agents, each under its own key, and one cap B holds across all of them. Per-edge budgets cannot do this, every child-cap check passes and k sibling grants of B still realize kB, and a shared counter in the delegator’s own account state can be reset between paths. So the meter is keyed on the root: one slot per (root, period), every draw at any depth check-then-increments it, and the conserved quantity is the sum itself.

That slot also lands squarely in the #45/#47 recompute frame: a single eth_getProof witnesses the aggregate of the entire tree, whichever venues advanced it. Easiest layout the recipe has met yet.

At commit 59ff0f4: IAggregateBudget (id 0xc7cabe86), reference cursor, interface-typed conformance suite, the kB counterexample as a test, and a stateful invariant (meter == sum of admitted draws <= cap under random trees, draws, revocations, rollovers). https://github.com/ERC8312/bounded-agent-actions

Notes. A capped node can’t delegate (else it mints an uncapped child past its own cap); capped leaves summing to at most B give every leaf a guaranteed allocation. This complements @TMerlini’s hierarchical profile, independent scopes inside a conserved envelope. Meters only; single chain (the #16 question: cross-chain aggregation is out of scope); revocation never refunds. Separately, the Section 2.3 witness binding in the PR now accepts prevCursor or a per-draw nonce, so the live (id, nonce) implementations are conforming as deployed.

Open question: optional profile inside 8312, or sibling ERC? A tree is not an envelope (it does not extend IBoundedAgentAction), which argues sibling; it is clearly the same metering layer, which argues profile. Base interface and frozen ids untouched either way. Preferences? (For now I drafted it as Section 5 in the PR, so there is concrete text to evaluate either way; it lifts out cleanly to a sibling if that’s the preference.)

Clean design – keying the meter on (root, period) rather than per-edge is the right fix for the kB counterexample, and the eth_getProof-witnesses-the-whole-tree property is a real win, not incidental.

One question worth pinning before this settles as a profile or sibling: is meter == sum(admitted draws) always evaluated against a specific historical block/cursor, or can it be read live? Same trap just surfaced on a different ERC-8312-adjacent thread tonight – an entitlement-audit design that read a live timestamp instead of the historical grant event, so the identical audit over identical actions returned different verdicts depending on when it ran. “Rollovers” is the phrase in your post that makes me ask: a period boundary is exactly the kind of moment where two honest verifiers checking a few seconds apart on either side of it can legitimately disagree unless the recompute is pinned to a specific (root, period, block) triple rather than “current state.” If it’s already pinned that way the eth_getProof framing implies it, this is moot – just worth stating explicitly given how easy this exact trap is to reintroduce at the profile layer even when the base interface gets it right.

On profile vs sibling: leaning sibling, for the same reason contested got questioned earlier in the thread (#18) – keeping the base interface’s state machine lean is worth more than saving one ERC number, and “does not extend IBoundedAgentAction” is itself the argument.

Catching up on the thread’s more recent turn (the contested-state and profile-composition discussion) – on the “prove-every-advance versus materialize-once” question specifically: I’d lean toward prove-every-advance, and the reasoning maps directly onto something we’ve hit repeatedly building WYRIWE’s recompute chain. Materialize-once collapses history into a single derived state you attest to – cheap, but it means a challenger can only check the endpoint, not the path. If an intermediate advance was invalid but later advances happen to land back on a “plausible” state, that’s invisible after collapse. Prove-every-advance keeps every step independently checkable, which is the same property our L4 judgment-attestation section leans on (a verdict has to be checkable against the specific input/output pair it was issued for, not just a final rolled-up claim). The cost tradeoff is real, but for a system whose whole point is bounded/revocable authority, I’d want the audit trail to survive an adversarial intermediate step, not just agree at the finish line. Happy to sketch what a minimal per-advance commitment would look like if useful.

On pinning: yes, by the storage key. spentRoot is rootId => periodIndex => uint256, so the period is a mapping key and rollover writes a new slot rather than resetting one. cap, periodLength and periodAnchor are write-once, so the period index cannot be redefined under an audit that already ran. Name the period and the answer is fixed once that period closes.

You are right that a live surface exists: remainingRoot() and currentPeriod() derive the period from block.timestamp, and an audit must not use them. That was never written down. 5.1 requires the meter to sit outside the write-domain of every key in the tree, but it does not spell out that the period addressing is part of the meter, and it should. So this goes into Section 5 as normative text, and travels with the section wherever it ends up: the period index MUST be a stored key rather than derived at read time, cap, periodLength and periodAnchor MUST NOT change after root creation, and conformance is asserted against (root, period) with the block chosen only to observe it.

On prove-every-advance versus materialize-once: in #17 I meant the gate’s membership cost, a merkle proof per draw versus materializing a leaf’s subCap once. Your reading is a different question, and a fair one. The profile carries both halves and requires them to agree. Endpoint is spentRoot[rootId][p], one eth_getProof. Path is Drawn(rootId, nodeId, periodIndex, amount) on every admitted draw. The stateful invariant asserts equality between the slot and a ghost ledger of admitted draws, per period, across warps that cross boundaries. An invalid intermediate that lands on a plausible endpoint does not survive that: either it emitted a Drawn and sits in the sum, or the slot contradicts the log set.

The collapse does lose attribution and order, since the slot is a scalar and Drawn carries nodeId. The tradeoff is durability rather than cost: a storage proof verifies from any node that kept the state, the log set needs one that retained receipts.

Worth pricing, as a follow-on rather than in this draft. The question I would want a sketch to answer: does a per-advance commitment buy anything the slot-plus-logs pair does not, beyond checking the path with no archive node at all? If that is the case, a hash chain folded into the meter’s own slot costs one word of state and one hash per draw.

On profile versus sibling. @orbmis, Section 5 landed after your last post here so you have not seen it: it adds no states, no enum members, no transitions, and IAggregateBudget extends IERC165, not IBoundedAgentAction. That makes it a placement call, and I want yours before I move anything. If you would rather I just call it, say so.

@TMerlini, a technical question rather than a venue one: does the root-keyed conservation clause in 5.1 conflict with anything in the per-leaf profile? It rules edge-keyed accounting non-conformant to that section, which is not a claim about the base or the flat profile. You are the most likely to find the seam if there is one.

Separately, I owe #18 and #22 an answer. Short version: I am keeping the Contested enum member on backwards-compatibility grounds, since it is index 3 and removing it renumbers Revoked and Expired for every consumer decoding status from a deployed registry. Most of the structural ask is already in the document, optional extension, arbitration out of scope, 8183 untouched. But the Rationale overstates the Specification on this and I will fix that.

1 Like

Looked for the seam @blockbird — and §5.1 is right as it stands, so the answer is “no conflict,” with one place worth a second look.

The key distinction: the conserved carrier is one root-keyed, admin-free meter across all
edge-attributed draws for the period. Edge attribution is for the breakdown, which node/edge a draw came from, never for the conservation itself. A per-edge counter treated as the conserved aggregate is the non-conformant counterexample, and correctly so: that’s exactly what a fan-out / re-delegation can spin up fresh copies of to walk past the root cap. So §5.1 ruling it out isn’t a conflict with the per-leaf profile it’s the leak the profile exists to close.

Where the per-leaf profile and §5.1 compose cleanly: the per-leaf edges are attribution, and the one root meter carries the cap. The only real seam would be per-leaf language that treats an edge counter as the cap-bearing budget itself (rather than as an attributed draw against the root meter) if any such wording exists, that’s the line to fix, by deferring conservation to the root meter. That’s the spot I’d point your eye at; everywhere the edges stay attribution, they slot under the root meter with nothing to reconcile.

The conformance vector then recomputes it directly from the log:

  1. sum all admitted Drawn amounts for (rootId, periodIndex)

  2. compare against the pinned root cap

  3. preserve node/edge attribution for the breakdown (labels on the draws, not the conserved quantity)

  4. never treat a per-edge counter as the conserved aggregate

Two honest bounds on the vector, so it doesn’t overclaim: it asserts Σ metered draws ≤ cap only over draws routed through the meter — non-bypassability is the substrate obligation the profile states, not something the recompute proves. And your period-index-as-stored-key rule is what makes it auditable: a pinned period means the (rootId, periodIndex) predicate is assertable at any observing block, no timestamp replay. Keep it.

I’ll pin the aggregate-budget vector against that (rootId, periodIndex) → root-cap predicate; once the profile + vector bytes are fixed, the ReceiptOS external/conformance seam is the right home for it. Happy to review the §5.1 wording once you draft it.

No conflict confirmed, and 5.1 stays as it stands: one root-keyed, admin-free
meter is the conserved carrier, edge attribution is the breakdown, and a
per-edge counter treated as the aggregate is the counterexample, not an
alternative representation. My earlier draft leaned on the cross-chain partition
as proof that per-edge counters can conserve. The analogy fails exactly where it
matters: cross-chain slices are minted by the principal alone and fixed from
then on, while a delegation tree lets a delegate mint fresh edges at runtime, so
partition only holds if issuance is bounded at the root. Bounding issuance at
the root is root-keyed accounting moved from draws to allocations, which
concedes the point rather than escaping it.

On the seam you pointed at: the line exists, and it is in the per-leaf direction
rather than in 5.1. The granularity paragraph in Section 4 describes per-leaf
consumption committed in cursorRoot, and the hierarchical sketch gives each leaf
its own subCap and leafSpent. As sketched, conservation there rests on a static
partition: the leaf set is frozen in capabilityRoot at registration, so no
fresh counter can be minted at runtime, and the sum is conserved when the leaf
caps sum to at most the envelope cap. The moment the caps overbook, per-leaf
checks alone conserve nothing. The aggregate profile can afford overbooked leaf
caps because spentRoot backstops them; the per-leaf sketch has no backstop yet,
which is the difference your correction names. So the follow-on defers
conservation to an envelope-level meter over the sum, with subCap as allocation
and leafSpent as attribution, and I will write that in rather than leave it
implicit.

One hazard from my withdrawn framing survives, relocated to where it actually
lives: clocks. Per-leaf time windows reset on their own epochs, and an epoch
unaligned to the root period can realize more than its allocation inside one
root period while every window-local check passes. The discipline is that leaf
windows nest inside the root period or bound their draws per it. Worth a line in
the follow-on next to the conservation deferral.

The vector as you and Pavlo specced it is right, and both bounds match the
section as written: sum admitted Drawn for (rootId, periodIndex) against the
pinned root cap, attribution as labels on the draws, never a per-edge counter as
the aggregate, and the claim scoped to draws routed through the meter, with
non-bypassability staying the substrate obligation per 5.3. Drawn carries rootId
with an indexed periodIndex, so the log side needs nothing added. Keeping the
period-index-as-stored-key rule is agreed; it is what makes (root, period) a
fixed target for the recompute.

Circulating the bytes before the ReceiptOS seam lands is the right order. I will
review the vector against the reference cursor when you do.

2 Likes

Converged, @blockbird, and the vector’s landed so the agreement is testable, not just stated. aggregate-budget-v0 is in trustless-ai/recompute-kit (conformance/aggregate-budget-v0, on main): Σ admitted Drawn for (rootId, periodIndex) ≤ pinned root cap, attribution as labels on the draws, never a per-edge counter as the aggregate — exactly your spec. 7 hash-pinned vectors(sha256 ac6f6efd…); the load-bearing one is fanout-exceeds-root-cap — three edges each under cap, root sum over it, and the adapter’s --tamper mode computes by the per-edge-as aggregate method and the suite fails 5/7, so the vectors pin the method, not the numbers.

Allocations = the same conservation moved up a level, and it composes cleanly. Bounding root issuance subCap as allocation, leafSpent as attribution, conservation deferred to an envelope-level meter over the sum, is the per-leaf backstop, and it’s the same rule as §5.1: leaves stay attribution, the one root/envelope meter carries the cap. Σ subCap ≤ rootCap at issuance and Σ leafSpent ≤ subCap per leaf just factor the single root-keyed invariant through the allocation tree; no second conserved carrier appears.

On the epoch/period-alignment hazard, that’s the real one, and it’s a keying rule, not a new meter. An epoch unaligned to the root period realizes more than its allocation inside one root period only if the conserved meter is keyed to the leaf-local window. Key the conserved meter to the root period, (rootId, rootPeriodIndex) and every draw in that root period hits the same meter no matter how the leaf’s local epochs tile across it, so the window-local checks can’t sum past the root cap. Leaf epochs stay attribution (which local window a draw fell in); they are never the conservation key. It’s the same “attribution ≠ conserved quantity” line, now applied to the time axis: attribution may be leaf-epoch-local, the meter is root-period-keyed. The current suite already pins(rootId, periodIndex) as the stored meter key (period-index-isolation); I’ll add an explicitepoch-unaligned-to-root-period counterexample in a v0.1 bump, a leaf drawing its full allocation in two overlapping local windows within one root period, root meter still sums them → caught, so the guard is a vector, not an assertion.

Happy to review the §5.1 allocation wording once you draft it. Good thread, I think this closes it.

1 Like

I should have read all the way to the end before opening my mouth — your #51 already puts an aggregate-budget profile for delegation trees on the table, tested, with the exact escape I was going to raise already closed (“a capped node can’t delegate, else it mints an uncapped child past its own cap”). So let me re-aim at what I think is still a different axis, and at the open question you end #51 with (“optional profile inside 8312, or a sibling ERC?”).

Your aggregate-budget governs a conserved quantity: one cap B metered across the whole tree, and a capped node simply can’t delegate. What I’ve been building governs a different thing — the whole mandate, inherited and welded to identity, not just spend. A child can exist (unlike the can’t-delegate rule), but only strictly tighter than its parent on every non-spend axis at once: payees ⊆ parent, expiry ≤ parent, a generation counter that only decrements, and a freeze that cascades down the subtree. The mandate is part of the identity hash, so editing it changes who the agent is (non-strippable), with a soulbound binding so it can’t be shed by transferring the token.

So the two feel complementary: yours meters what a lineage may spend in aggregate; this bounds what a spawned child is allowed to be across the identity-bound, non-spend clauses. On your open question, my instinct leans sibling ERC that composes with 8312 rather than a profile inside it — because it’s identity-welded and governs more than the budget — but I’d genuinely defer to you on where the seam belongs.

Draft spec + a runnable prototype (child ⊆ parent enforced, escape attempts rejected) if it’s useful: GitHub - helmymekaoui-web/inheritable-agent-mandates: Inheritable Agent Mandates — identity-bound, non-strippable limits inherited by spawned on-chain AI agents (testnet). White paper + draft EIP + reference contract + prototype. · GitHub — and thanks; #51 is a sharper treatment of the spend axis than I’d have managed.

Once a registry implements IContestableEnvelope, Contested isn’t merely descriptive. Section 2.3 rejects advanceCursor on any stored status other than Active, and isActive is true only for Active, so entering Contested suspends further cursor advancement. An integrator can adopt what looks like a review flag and inherit a de facto freeze of the envelope. It’s the same non-dispositive versus dispositive line you drew in your ERC-8319 comment, pointed back at this one.

That’s the piece I’d put into the fix you flagged in #55, where the Rationale overstates the Specification. Say in the Specification that Contested is operationally suspensive, and keep a purely informational review signal off lifecycle status altogether so it changes neither isActive nor advanceCursor. The enum stays for compatibility, and the concerns in #18 and #22 get an answer without anything being removed.

One liveness edge remains. Contested -> Active and Contested -> Revoked both run through resolver-restricted resolve, and there is no stored Contested -> Expired transition. Since expiresAt may be zero, resolver silence can leave a non-expiring envelope contested indefinitely. For an expiring envelope, consumers must treat it as inactive after the timestamp, but the state machine still defines no stored transition out of Contested. The principal’s explicit Active -> Revoked route is also no longer available unless the registry makes the principal a resolver. A bonded challenger doesn’t necessarily have to win; resolution stalling may be enough.

The reservation interaction should be pinned too. Where an implementation models an advance as a reservation, Section 2.3 blocks new advances after contest, but the Specification doesn’t say whether already-open reservations may still be confirmed or cancelled while the stored status is non-Active. Reservation Binding only pins who may perform those operations. Whether those reservations remain locked, settle, or release changes the pooled headroom other venues rely on, so an IContestableEnvelope implementation should be required to define that behavior.

@TMerlini both landed. 2f861948 pins period addressing, d625dcb2 carries the epoch
keying and the allocation wording, plus one clause past yours: leaf caps summing to at
most cap hold guaranteed allocations, above that they overbook by choice. CI green, so
5.1 is yours to shoot at. Your seven vectors also replay green through the reference
cursor, afab44c in ERC8312/bounded-agent-actions, fanout-exceeds-root-cap included.
Same for the epoch-unaligned one at v0.1.

@helmymekaoui-web sibling. 8312 never looks inside capabilityRoot, which is opaque by
design, so an identity-welded mandate is the structure behind it while the cursor
meters what the lineage spends against it. Your child-tighter-on-every-non-spend-axis
and my capped-node-cannot-delegate are one discipline on two axes: constraints
propagate down, capabilities never do. The #51 placement question is separate, still
parked with @orbmis. Seam test: point your prototype’s mandate at a capabilityRoot in a
conforming registry and open whatever breaks as an issue on the reference repo.

@jay-oraclizer all three hold, so all three become spec changes.

Suspensive: yes, and it belongs in the Specification, not derived from 2.3. The
Rationale half of the #55 fix landed in 2f861948; yours is the Specification half it
left open. With it: an informational review signal MUST NOT ride on lifecycle status,
since anything in the enum inherits the freeze. That answers #18 and #22 without
removing the member.

Liveness: real, and smaller than it looks. Contested → Revoked is already valid in 2.5
and 2.4 leaves its authorization implementation-defined, so a registry can give the
principal that route through setStatus without making it a resolver. Two edits close
it: extend the anti-escape clause from “out of Active” to “out of Active or Contested”,
and put the Active → Revoked authorization floor on Contested → Revoked. That covers
principal and metered party being one address. Not Contested → Expired: it is
terminal, so an any-caller edge lets the accused run out the clock and foreclose the
verdict. For expiresAt zero, IContestableEnvelope gains a resolution deadline set at
contest time, after which any caller may resolve to a documented default. Default
Active, since the stall you name is the challenger’s.

Reservations: unpinned, and your rule goes in as written: an IContestableEnvelope
implementation MUST define the behavior. Mine: cancel stays with the reserver, since it
only returns headroom and 2.3 blocks the redraw while suspended. Confirm stays too, but
not for a safety reason: an open reservation already holds the headroom down, so
blocking it costs liveness and protects nothing.

Separately, the 28917 draft you accepted in #4 goes to that thread, as you asked.

1 Like