ERC-8468: Agent Safety Framework (ASF)

Pull request: Add ERC-8468: Agent Safety Framework (ASF) by garyyang-finchip · Pull Request #2113 · ethereum/ERCs · GitHub (ERC-8468, number assigned on 2026-10-27)
Repository: GitHub - garyyang-finchip/agent-safety-framework: AI Agent Safety Framework · GitHub (reference implementation, schemas, vectors, worked examples, two closed-loop consumers, design memo)

Current text: revision 7 (see the revision note below and the replies that introduce each revision). The revision-7 contracts are deployed and verified on Sepolia (second evidence run, 2026-10-27; see “Closing the loop”).

The gap

Agents now hold credentials, operate accounts and move funds for people and organizations, and the mechanisms that bound them are maturing in several places at once: permission grants and delegations in accounts, regulated mandates, action-bound policy verdicts, identity and reputation registries, decision mechanisms, task escrows. What none of them records is the relation between them: in this task, on these resources, in this execution environment, which participants have accepted which safety requirements, in which version, relying on which evidence, enforced by which mechanism, and whether that acceptance can still be relied upon now.

Without that relation a counterparty either trusts a label or asks for nothing; a requirement written into a document cannot be told apart from one enforced at an execution boundary; an accepted requirement is silently reinterpreted when its publisher releases a new version; and when an arrangement is paused or breached there is no shared way to say what stops, what continues, and who was entitled to intervene.

What the draft standardizes

Two objects, one check, one composition rule. Everything else is consumed from the standards that already provide it; the text requires only ERC-165.

Safety Specification. A versioned, content-addressed document: protected interests (five domains — assets and economic position, information and privacy, control and rules, credit and eligibility, human and public interests — each naming whose interest is protected: principal, operator, counterparty, bystander, public), applicability with permitted and prohibited uses, constraints under named profiles (evaluated over an action, a state, a trajectory, a group or a relation between trajectories), who may adopt and enforce and on what external authority, expected evidence and what it binds, enforcement channels with their location (self, account, runtime, custodian, contract, collective), coverage and known bypasses, failure handling for unknown / denied / dependency failure, pre-accepted remedies (reversibility, intervention window, service continuity, incident reporting), and the profiles a consumer must understand. Versions are immutable; publishing v2 changes nothing for adopters of v1; deprecation is a signal.

Safety Binding. The on-chain statement that an adopter (always msg.sender) adopts one pinned specification version for one subject within one scope, under a named external authority basis, with declared enforcement locations, a validity window, named interveners with explicit rights (suspend / resume / terminate) and a lifecycle: Active, Suspended, Terminated. A suspension records who made it, and a suspension recorded by an intervener is not the adopter’s to lift: only an intervener holding the resume right can lift it, while the adopter keeps the right to terminate (a terminated record keeps showing the stop it carried). An intervener’s suspension is also a standing stop against the adopter for the subject and the specification, keyed by the intervener, and the stop is the intervener’s: it outlives the termination of the binding it was recorded through, widens by union when the intervener suspends another binding of the adopter, is lifted only by that intervener or by the one lifter it named (by no right of the adopter, and not by the resume holders of a binding), and no binding of that adopter for that subject and specification satisfies a clause that names the intervener while it stands — a sibling binding held in advance and a fresh adoption are refused alike. A suspension carries a combinable set of response requirements (no new risk, shrink, essential only, de-risk only, full stop; full stop dominates) and the hash of a structured intervention response document that states who started the response and under which right, what it covers, what it prohibits, allows and keeps running, how in-flight actions are handled, how long it lasts and who may end it. The adopter is part of every identifier and index key, so the registry can never attribute an arrangement to an account that did not accept it; whether the adopter was entitled to adopt for the subject is verified by consumers against the permission source the binding references, never by the registry. Bindings can be derived from a parent (same pinned version, validity inside the parent’s, ancestors must hold, parameters never wider under the profile’s declared order).

The check. check(requirement, bindingIds) evaluates a requirement set (clauses with explicit version lists, adopter, subject, scope, required enforcement locations and required interveners — a resource can insist that someone it trusts is able to stop the subject, and a clause that names interveners also closes the resume right to the accounts it names, so the subject cannot lift a stop through an intervener of its own choosing — combined by all-of / any-of / k-of) and returns Satisfied, NotSatisfied or Unknown with a reason and the response set in force per clause. That set is the union over the binding and its suspended ancestors (empty when the chain is terminated or expired), so a parent’s de-risk-only response reaches derived bindings intact and a full stop anywhere in the chain is visible below it. Pure registry logic — existence, state, validity, pin, adopter, subject, scope, enforcement, required interveners, standing stops by the accounts the clause names, ancestors — no policy is evaluated, Unknown never counts as satisfied, and clause verdicts are never combined into a score. The verdicts and reasons are the registry’s own; when a consumer also reads a permission, policy or validation mechanism, that mechanism’s native result is kept as it is, never normalized into the registry’s vocabulary. Which bindings a resource requires is decided by the resource’s own authority rules; a subject cannot substitute a looser one.

Closing the loop

Two reference consumers show that the relation bites at an execution boundary. ASFGuardedAccount: the operator may act only while the principal’s binding is satisfied; a guardian’s suspension with “no new risk” and “de-risk only” set blocks new exposure while repayment and cancellation still go through; once “full stop” is in the set, everything stops; there is no path that skips the check. ASFTaskTenderAdapter: an acceptance authority for token-bound task tenders that admits only fulfillers holding their own binding with the task publisher as a required intervener, rejects an unbound fulfiller and releases the reservation, rejects a fulfiller whose binding the publisher suspended (the fulfiller cannot lift it), and on Unknown neither accepts nor rejects — the kernel’s own judgment clock stays in force. Both are tested against negative scenarios (93 tests, 67 negative), including derived bindings on the wallet path, the closed resume right and the standing stops on both paths.

The evidence run has been executed on Sepolia twice, against the live TASK-KERNEL v3.0 TaskToken: with the revision-5 contracts on 2026-10-26 (36 transactions) and with the revision-7 contracts on 2026-10-27 (blocks 12003345 to 12003478, 43 transactions, all successful, eight contracts verified on Sourcify with a runtime and a creation match). The second run adds the standing-stop loop of Section 8.5 to the phases of the first: the task publisher stops the worker’s binding (StopRecorded); the worker terminates it, adopts a fresh binding, submits and calls judge itself and is rejected with StoppedByIntervener; the publisher lifts its stop through liftStop (StopLifted, the terminated record untouched); the worker submits again and is accepted and paid as the tender’s second completion. The guard loop shows the revision-7 rule from the other side: the guardian’s suspension of the principal’s binding records the guardian’s stop, and the guardian’s own resume lifts it. Revision-7 addresses: ASFSpecificationRegistry 0x9110b622a17F83065f1e9773D961EB10672932e5, ASFBindingRegistry 0x136E2a88e1B518342CD997423BDa69Dd2c68e919 (libraries ASFEvaluation 0x9b12f62D69EAfc9a240b9AB889cC1647Bf9C56db, ASFLifecycle 0x6a6F645eC9F161735Cc22D441aFD1f513f3b143b), ASFGuardedAccount 0xc88A3FdbC5a4934D335b24416cC9fbF4046e88b0, ASFTaskTenderAdapter 0x015393ECe2E3072055A3Fbe8Fc7f2979B7C7F3Ed. The transaction records, the decoded events and the state read back after each run are in docs/sepolia-evidence-rev7.md and docs/sepolia-evidence.md of the repository; the revision-5 deployment (ASFBindingRegistry 0xc3e1A1Cb021Ab96EBBeD3279018840016114A5c4) stays as it was.

What it deliberately does not do

It does not decide whether an action is permitted, does not evaluate policies, does not certify that an agent is aligned, and does not prove that an arrangement is enforced: declared locations are attributable commitments, evidence and the mechanisms carry the verification. Incident reports, claims and labels have no effect of their own in the framework; only the rules of the consumer that reads them give them one, and the text fixes the consumer rules that keep such records from being used as weapons. It records who accepted what, where, in which version, enforced where, and whether that is in force now — and makes that answer available in one call at the moment of effect.

Analysis basis

The design memo in the repository maps the problem space on three axes used as a multi-label index rather than as an exclusive classification: the seven links of an agent system (substrate, identity, authority, information flow, decision, accountability, collective), the five protected domains above, and six sources of failure. Eight requirement families group the profiles. The memo carries the object-and-authority attribution table, a scenario walkthrough against the test suite, and the overlap and adaptation review against the neighbouring proposals (permissions and delegations, policy verdicts, mandates, identity and reputation, validation, swarm charters, task tenders).

Questions for the thread

  1. Derived bindings: is the split between the on-chain structural rule and the off-chain profile-order rule acceptable, or should profiles with a declared order be checkable on chain?
  2. Response requirements: are the five combinable bits plus the structured response document the right shape, or should the response document’s inFlight rule (what happens to actions already authorized or sent) also be reflected on chain?
  3. Protected domains: the fifth domain, human and public interests with the roles principal / operator / counterparty / bystander / public, replaced a narrower “third parties and the public” domain. Does anyone see an interest that falls outside the five?
  4. Incident reports and claims stay outside the kernel and have effect only through consumer rules (and, if wanted, an optional profile). Is that the right boundary for a first version?
  5. Standing stops: an intervener’s stop applies to the adopter for the subject and the specification, keyed by the intervener, and is the intervener’s to lift (itself, or the one lifter it names with setStopLifter). Is one lifter per intervener, for all its stops, the right granularity, or should a lifter be named per stop? And should a “created before” clause member still be offered for the replacement bindings that no stop concerns?

Feedback on the overlap review with the mandate, verdict, delegation and identity proposals is especially welcome.

Revision note

  • Revision 1 (initial filing): two objects, one check, five exclusive suspension restrictions, four protected domains plus “third parties and the public”.
  • Revision 2: suspension restrictions became a combinable bit set with a structured intervention response document (responseRef); the fifth protected domain became human and public interests with protected roles; specifications gained permitted and prohibited uses and the remedies reversibility, intervention window, service continuity and incident reporting; the text now states that native results of external mechanisms are preserved rather than normalized, that identity, representation and legitimate use are established separately, and the consumer rules for incident reports, claims and labels; the IASFBindingRegistry interface identifier changed accordingly (0xb99bdc2a); the design memo with the three-axis map, the attribution table, the scenario walkthrough and the overlap review was added to the repository.
  • Revision 3 (after the first review in this thread): check returns the response set in force per clause, defined as the union over the binding and its suspended ancestors and empty when the chain is terminated or expired, and the reference account applies it for Suspended and ParentNotSatisfied alike (a parent’s de-risk-only response no longer becomes a full stop on derived bindings); clauses gained required interveners (IntervenerMismatch); the registry records suspendedBy, and a suspension recorded by an intervener can be lifted only by an intervener holding the resume right, never by the adopter; the tender example requires the task publisher as an intervener; the IASFBindingRegistry interface identifier is now 0x1ff33bef and requirementId values changed with the clause encoding; 82 tests, 56 negative.
  • Revision 4 (after the second review): a clause that names interveners also closes the resume right — the binding must not grant RIGHT_RESUME to any account the clause does not name, otherwise the adopter, who chooses the interveners, could lift any intervener’s suspension through an account of its own; the zero-account wildcard was removed for the same reason; additional suspend-only or terminate-only interveners remain allowed. No interface or encoding change; 84 tests, 58 negative.
  • Revision 5 (Sepolia): the evidence run was executed on Sepolia against the live TASK-KERNEL v3.0 TaskToken (36 transactions, all successful, seven contracts verified on Sourcify) and recorded in the repository. One reference-implementation change, made so that the registry could be deployed under Sepolia’s current gas schedule and its per-transaction gas cap: the binding registry’s check now runs in the externally linked library ASFEvaluation, called with the registry’s storage. No change to the text’s normative content, the interfaces, the identifiers, the encodings or the test results. The title of the text is Agent Safety Framework (ASF), as in this thread’s title; revisions 2 to 4 of the pull request had carried “Agent Safety and Security Framework”, and the rationale paragraph on the name now explains the broad sense in which “safety” is used.
  • Revision 6 (after the third review): an intervener’s suspension is recorded as a standing stop against the adopter for the subject and the specification, keyed by the intervener; it outlives the termination of the binding it was recorded through, is lifted only through a resume by an account holding the resume right (also on the terminated record), and a clause that names the intervener is met by no binding of that adopter for that subject and specification while it stands (StoppedByIntervener, with the stop’s response set); terminated records keep their suspension fields; stopOf was added and the IASFBindingRegistry interface identifier is now 0x2db6eb8a (the clause encoding and requirementId values are unchanged); the tender example names the publisher with the suspend and the resume right; the registry’s writes moved into a second linked library (ASFLifecycle); 90 tests, 64 negative. The Sepolia deployment remains revision 5’s.
  • Revision 7 (after the fourth review): the standing stop is the intervener’s. It is lifted only by the intervener that recorded it or by the one lifter the intervener named (setStopLifter), through liftStop or through that account’s own resume of a binding the intervener suspended; a resume holder on a binding reopens the record and nothing more, and a Terminated record accepts no resume. A later suspension by the same intervener widens the stop by union (set, latest binding, latest response document supplied) instead of replacing it. StopRecorded, StopLifterSet and stopLifterOf were added, StopLifted is keyed by the stop, and the IASFBindingRegistry interface identifier is now 0xcff7f826 (the clause encoding and requirementId values are unchanged). 93 tests, 67 negative. The evidence run was then executed on Sepolia a second time with the revision-7 contracts (2026-10-27, 43 transactions including the stop loop, all successful, eight contracts verified on Sourcify; docs/sepolia-evidence-rev7.md); the revision-5 deployment stays as it was.
3 Likes

Nice to see the two consumers ship with the spec, it makes the check easy to reason about at the point where it bites. Two things from reading them against the text.

A DeriskOnly suspension on a parent turns into a full stop on derived bindings. _evaluate returns ParentNotSatisfied with the ancestor’s restriction, but check only hands back the reasons, so _onlyDeriskSuspensions in ASFGuardedAccount sees ParentNotSatisfied instead of Suspended, returns false, and the call reverts (the child’s own restriction, the only one it could read through bindingOf, is still None). The listed de-risking calls (repay, cancel) revert exactly when they should stay open, and the wallet path tests don’t use a derived binding. Returning the restriction per clause from check and accepting ParentNotSatisfied under a DeriskOnly ancestor would fix it, and it’s part of your derived bindings question too.

Who can stop a fulfiller. In the tender path the fulfiller is the adopter, so they choose the interveners and rightsOf gives them RIGHT_ALL. A clause has no field for interveners, so a task can’t require that its guardian holds suspend rights, and when one does, the fulfiller can resume right after the suspension and judge accepts them again. On the wallet path that’s fine, the adopter is the principal being protected, but where the adopter is the party being constrained the suspension is only advisory. Letting a clause require interveners with given rights, and making resume after an intervener’s suspension need that intervener, would make it bite.

workijg on something similiar for cross chain wallets for agents.

Revision 2 of the draft is now on the pull request (Add ERC: Agent Safety and Security Framework by garyyang-finchip · Pull Request #2113 · ethereum/ERCs · GitHub) and in the repository (GitHub - garyyang-finchip/agent-safety-framework: AI Agent Safety Framework · GitHub). The first post has been updated to the new text; this reply lists what changed and why, so that earlier readers can see the delta.

What changed

1. Suspension restrictions are a combinable set, with a structured response document. Revision 1 had five mutually exclusive restrictions. A first round of review made the point that the five are not alternatives: “no new risk” is a prohibition, “shrink” a capability change, “essential only” a duty, “de-risk only” a permitted path and “full stop” a stop target, and a real intervention usually combines them (stop new exposure, keep repayment open, keep the service alive). suspend now takes a uint8 bit set (RESTRICT_NO_NEW_RISK 1, RESTRICT_SHRINK 2, RESTRICT_ESSENTIAL_ONLY 4, RESTRICT_DERISK_ONLY 8, RESTRICT_FULL_STOP 16; full stop dominates; empty and out-of-range sets are rejected) together with a responseRef, the canonical hash of an intervention response document (new schema intervention-response.schema.json) that states who started the response and under which right, what it covers, what it prohibits, allows and keeps running, how actions already authorized or sent are handled, how long it lasts and who may end it. checkBinding returns the bit set with Suspended and with ParentNotSatisfied, so a consumer learns what still may proceed without fetching the document. The reference guarded account keeps de-risking open while RESTRICT_DERISK_ONLY is set and RESTRICT_FULL_STOP is not. This changes the IASFBindingRegistry interface identifier to 0xb99bdc2a; the specification registry’s identifier is unchanged.

2. The fifth protected domain is human and public interests, with protected roles. third-party-public was too narrow: the principal’s own rights and welfare (not only their assets) are a protected interest too, and so are the operator’s. The domain is now human-public, and every protects entry names the roles whose interest it serves: principal, operator, counterparty, bystander, public.

3. Specifications state permitted and prohibited uses and four more remedies. applicability.permittedUses / prohibitedUses and remedies.reversibility, interventionWindow, serviceContinuity, incidentReporting were added as optional members. Section 11.8 now separates three facts that must never be inferred from one another: that an account is who it claims to be (identity), that it may act for a principal or resource (representation), and that a given action serves a permitted use (legitimate use). An action can be authentic and authorized and still outside the permitted uses.

4. Native results of external mechanisms are preserved. The question of revision 1 (“should the kernel normalize denial reasons of external policy mechanisms?”) is closed in the negative. The registry’s Verdict and Reason describe the registry’s own evaluation only; when a consumer also reads a policy verdict, a mandate check or an account validation result, that mechanism’s native result is kept as it is (Section 10.6). Normalizing them would lose information and would couple the framework to each mechanism’s revision cycle.

5. Incident reports, claims and labels have no power of their own. Section 11.7 fixes the consumer rules: a report, a claim or a label changes nothing by itself; only the rules of the consumer that reads it give it an effect; statement, acceptance, finding and action are kept apart; corrections are indexed with accusations; report contents are untrusted input. This also answers the fourth question of revision 1: incident and claim records stay outside the kernel in this version.

6. Analysis basis and design memo. The repository now carries the design memo (docs/design-memo.md) with the three-axis map used as a multi-label index (seven links of an agent system, five protected domains, six sources of failure; eight requirement families as the grouping of profiles), the object-and-authority attribution table (which object settles which question, under whose authority), a fourteen-scenario walkthrough against the registry behaviour and the test suite, and the overlap and adaptation review against the neighbouring proposals. Two earlier statements were withdrawn in that review: the framework does not claim that on-chain enforcement is the only enforcement that counts (the self, runtime and custodian locations exist for that reason), and pricing or deposits are not a condition of the framework’s effectiveness.

7. Smaller corrections. Examples carry two intervention response documents; vectors carry the response requirement bits; the test suite has 74 tests, 48 of them negative conformance scenarios (N01–N48); the evidence script was re-rehearsed on Anvil with the response documents (36 transactions).

What did not change

The kernel: two objects (Safety Specification, Safety Binding), one check, one composition rule; the adopter is always msg.sender; identifiers and their derivations (Section 3) are unchanged; derived bindings never widen their parent; Unknown never satisfies; the text requires only ERC-165.

Next

The Sepolia evidence run (both registries, the two example specifications, the example bindings, the guarded-account loop with the guardian’s de-risk-only response, the tender loop against the live TASK-KERNEL v3.0 TaskToken, and the lifecycle phase with the principal’s full stop) is prepared and will be recorded under deployments/ with a results note. A second closed-loop consumer for group constraints (a shared-budget coordinator with reserve / commit / release) is planned as a companion, outside the ERC text.

Comments on the four questions in the updated first post, and on the overlap review, are welcome.

Thank you for reading the consumers, not only the text. Both findings are correct, and I have reproduced both against revision 2 (the revision note above; the pull request carries the same text): the change from five exclusive restrictions to a combinable bit set did not touch either path. Here is what I found and what revision 3 will do.

1. A parent’s de-risk-only suspension becomes a full stop on derived bindings. Confirmed. checkBinding already returns the suspended ancestor’s response set together with ParentNotSatisfied (bits 9 in the reproduction, for no new risk + de-risk only), but check returns only the reasons, and the reference account then reads the child’s own record, whose set is empty while the ancestor is suspended. So the listed de-risking calls revert exactly when the response was designed to keep them open. The wallet-path tests never presented a derived binding, which is why it was not caught.

Fix in revision 3: check returns, per clause, the response set in force next to the reason. That set is defined as the union of the sets of the clause binding and of every suspended ancestor, not just the nearest one, so that a parent’s “no new risk” is never hidden behind a child’s narrower set. The consumer rule (11.3) will say that a consumer which keeps paths open under a response set must apply the set returned per clause for both Suspended and ParentNotSatisfied, and must not read the clause binding’s own record for that purpose. ASFGuardedAccount is changed accordingly, and the wallet-path tests gain a derived-binding group: parent suspended de-risk only (repay and cancel open, borrow blocked), parent under full stop (everything closed), parent terminated (ParentNotSatisfied with an empty set, closed), parent resumed (open again).

2. Where the adopter is the constrained party, the suspension is advisory. Also confirmed: the fulfiller adopts for itself, names its own interveners, holds RIGHT_ALL as adopter, and can resume in the block after a guardian’s suspend; judge then accepts. A clause has no way to require that anyone else holds a right over the binding. Two changes:

  • Clauses can require interveners. A clause gains an interveners member, a list of (account, rights); the binding must list each named account as an intervener with at least those rights, and address(0) stands for “some intervener other than the adopter” with those rights. Mismatch is a new registry reason, IntervenerMismatch, evaluated after the enforcement check and before the ancestor walk. The tender example will require the task publisher as an intervener with the suspend right, and the worker’s self-adopted binding example names it.
  • An intervener’s suspension is not the adopter’s to lift. The registry records suspendedBy; when that account is not the adopter, resume requires the caller to be an intervener holding RIGHT_RESUME, and the adopter’s call reverts. The adopter’s own suspensions stay liftable by the adopter or by any intervener with the right.

On “the same intervener”: I went with any intervener that holds RIGHT_RESUME rather than the one who suspended, because the escalation shape the examples already use (an organizer may only suspend, a collective decision executor may suspend, resume and terminate) needs a different account to be able to lift the organizer’s stop, and every intervener was named by the adopter at creation, so the set is closed. If you see a case where the same-intervener rule is needed, I would like to hear it.

One residual should be stated plainly: the adopter can always terminate and adopt afresh. A consumer that requires a given intervener then only accepts a new binding that names that intervener again, the terminated record stays readable with its suspendedBy, and a consumer that must not accept a replacement can pin the binding identifier it admitted. The registry does not stop the replacement; it makes it attributable.

Both changes alter the IASFBindingRegistry identifier again and the requirement encoding (a clause gains a member, so requirementId values change). I take that as acceptable while the proposal is a draft; the derivations of specId and bindingId in Section 3 do not change. Revision 3 with the tests will follow in the pull request and here, and I would be glad to have you look at the derived-binding group and the intervener clause once it is up.

Revision 3 is on the pull request (Add ERC: Agent Safety and Security Framework by garyyang-finchip · Pull Request #2113 · ethereum/ERCs · GitHub) and in the repository (GitHub - garyyang-finchip/agent-safety-framework: AI Agent Safety Framework · GitHub); the first post is updated. It carries the two fixes announced above, as implemented and tested, and nothing else of substance.

What changed

1. The response set in force is returned per clause and defined over the whole chain. check now returns (verdict, reasons[], restrictions[]). The set for a clause is the union of the response sets of every suspended record among the clause binding and its ancestors, and it is empty when any record in that chain is terminated or outside its validity window (a chain that is not in force keeps no path open). checkBinding returns the same set with Suspended and with ParentNotSatisfied. Consumer rule 11.3 now says that a consumer applies the set the check returns, for both reasons, and never reads it from the clause binding’s own record, which carries no set while an ancestor is suspended. ASFGuardedAccount follows that rule. Tests: N50 (union over a three-level chain; a full stop anywhere is in the union; a terminated or expired record empties it), N52 (a de-risk-only suspension of the root blocks new exposure through the derived binding and keeps repay and cancel open; the guardian’s resume reopens), N53 (full stop or termination on the root closes the de-risking path), N54 (child and parent sets combine; full stop on either side dominates).

2. Clauses can require interveners. Clause gained Intervener[] interveners: each entry is an account and a rights bit set the binding must grant to that account; a zero account stands for some intervener other than the adopter. The adopter is never an intervener, so a requirement cannot be met by self-control. Mismatch is the new reason IntervenerMismatch, evaluated after the enforcement check and before the ancestor walk; malformed rights (0 or above 7) make check and requirementId revert. The tender example requires the task publisher with the suspend right, and the worker’s self-adopted binding names it. Tests: N51 (named account, rights subset, adopter never counts, wildcard, several required interveners, malformed rights), N55 (a fulfiller whose binding is under its sole control is rejected with IntervenerMismatch; one naming the publisher is accepted and paid through the kernel).

3. An intervener’s suspension is not the adopter’s to lift. The registry records suspendedBy. When it is not the adopter, resume reverts for the adopter and succeeds only for an intervener holding RIGHT_RESUME; the adopter’s own suspensions remain liftable by the adopter or by an intervener with the right, and the adopter keeps RIGHT_TERMINATE in every state. Tests: N16 (updated: the adopter cannot lift a suspend-only guardian’s stop and can only terminate), N49 (guardian suspends, adopter cannot resume, an executor with the right can; the adopter’s own suspension is lifted by either; the executor’s suspension cannot be lifted by the adopter or by a suspend-only guardian), N35 (updated wallet path: the principal cannot lift the guardian’s suspension, the guardian does), N56 (the publisher suspends the fulfiller’s binding; the fulfiller’s resume reverts; judgment rejects; a fresh binding that names the publisher again is admitted and the publisher can suspend it again).

4. Examples and evidence script. The personal-assistant example gives the guardian suspend and resume rights, since a suspension it records can no longer be lifted by the principal; the governance/intervention profile descriptor states the rule; the evidence script reads interveners from the relinked manifest, the guardian resumes in the guard loop, and the adapter template carries the publisher as a required intervener. Re-rehearsed on Anvil: 36 transactions across five phases, all successful.

5. Identifiers. The IASFBindingRegistry interface identifier is 0x1ff33bef. requirementId values changed with the clause encoding, whose ABI type is now tuple(uint8,uint16,tuple(address,bytes32,uint32[],address,bytes32,bytes32,uint16,tuple(address,uint8)[])[]); the vector carries a clause with a named and a wildcard required intervener. The derivations of specId and bindingId in Section 3 are unchanged. Section 13.4 now states that while the proposal is a draft the clause struct and therefore requirementId values and interface identifiers may change between revisions.

Stated residual

The adopter of a suspended binding can still terminate it and adopt afresh. The registry keeps this attributable (terminated record with its suspendedBy, new identifier and createdAt) and a requirement that names an intervener is met only by a binding that names it again, so the intervener’s power returns with the binding; a consumer that must refuse replacements pins the binding identifier it admitted. A “created before” clause member is listed as an open point and as question 5 in the first post.

Suite: 82 tests, 56 negative conformance scenarios (N01–N56). Lint and the clean-copy verification are unchanged.

Thanks, and nice turnaround on revision 3. The derived-binding part reads right to me: the set is the union over the whole chain, a terminated or expired record empties it, and the account only reads the per-clause set, so a terminated parent closes the de-risk path too.

On the same-intervener question, here’s a case. adopt accepts any extra intervener the adopter names, _hasIntervener only checks that the clause’s accounts hold at least the required rights, and resume only turns away the adopter’s own address. So a fulfiller names the publisher with suspend, as the tender asks, plus a second address it controls with RIGHT_RESUME. The publisher suspends after a report, the second address resumes right away, and judge accepts again from then on. The zero-account wildcard doesn’t prove independence for the same reason: any second address the fulfiller controls meets it.

Your escalation shape still works if the clause, not the adopter, decides who may lift a stop, for example IntervenerMismatch when an account the clause doesn’t name holds RIGHT_RESUME. The tender then names the publisher, plus an executor if it wants one.

You are right, and the case is exact: adopt takes any intervener the adopter names, the clause only asked that its own accounts hold at least their rights, and resume only refused the adopter’s address. A fulfiller names the publisher with suspend, adds a key of its own with RIGHT_RESUME, and the publisher’s stop lasts one block. The wildcard had the same flaw for the same reason: nothing an adopter chooses can establish independence from the adopter.

Revision 4 does what you suggest, and nothing else. A clause that names interveners now also closes the resume right: the binding must not grant RIGHT_RESUME to any account the clause does not name, otherwise IntervenerMismatch. Additional interveners holding only suspend or terminate rights remain allowed, since they can only stop. A named account may hold more rights than the clause requires. The zero-account wildcard is gone; a zero account in a clause is a malformed requirement. The escalation shape is expressed where it belongs, in the clause: name the publisher with suspend and, if wanted, an executor with resume, and the binding must grant exactly those resume holders. The registry-level rule from revision 3 stays (an intervener’s suspension is never the adopter’s to lift); the clause now decides who else may lift it. A clause that names no interveners constrains nothing about the lifecycle, and the text now says plainly that such a subject controls its own.

Tests: N57 at the registry level (the puppet-key binding fails the clause before any suspension; unnamed suspend-only or terminate-only interveners pass; the suspender-plus-executor clause is met by a binding granting exactly that, the executor lifts the suspender’s stop and the adopter cannot; a clause without interveners leaves the resume right unconstrained) and N58 on the task path (your scenario verbatim, rejected with IntervenerMismatch at judgment; then the escalation variant, where the executor lifts the publisher’s stop and the fulfiller is accepted). The suite is at 84 tests, 58 negative. No interface or encoding change: IASFBindingRegistry stays 0x1ff33bef and requirementId values are unchanged. Text changes in Sections 6.5, 8.1, 10.1, 10.2, 10.4, 11.2, the rationale and a new security consideration on interveners chosen by the adopter. The pull request and the repository carry revision 4; the first post is updated.

One thing the registry still cannot do is know who controls an account, so the rule is stated as it is: independence comes from the requirement naming accounts, not from any property of the binding. If you see a remaining path by which the adopter lifts a stop under a clause that names its interveners, I would like to hear it.

The evidence run is on Sepolia. On 2026-10-26, between blocks 11994931 and 11995051, the script deployed both registries and the two reference consumers, published the two example specifications, created the four example bindings (the principal’s binding for an assistant with a guardian holding suspend and resume rights, the swarm charter, a member binding derived from it, and a worker’s own binding naming the task publisher as an intervener), and ran the three loops of the text against them: 36 transactions, every one successful.

What the chain shows, read back after the run. On the account path, the operator’s borrow went through; the guardian suspended with “no new risk” and “de-risk only” set and the hash of its response document; the next borrow was refused with BindingNotSatisfied(NotSatisfied, [Suspended]) while repay went through under the de-risk-only set; the guardian lifted its own suspension (the principal could not), and borrow went through again. On the task path, against the live TASK-KERNEL v3.0 TaskToken at 0xA62059A498E40C4Ae4aF926E2B00C1Ff122bDdb7 (task #20), the adapter accepted the bound worker — Judged(Satisfied, [its binding], [None]) — and the kernel’s own acceptFulfillment paid the reward, then rejected the unbound submitter with NoBinding. In the lifecycle phase the principal’s full stop refused even repay; after the charter’s termination the derived member binding checks ParentNotSatisfied with an empty response set while its own record stays Active; version 1 of the swarm specification is deprecated with the bindings unaffected.

Addresses: ASFSpecificationRegistry 0x1939665F5905d0d9135Da4f27e36f1B8c84E014b, ASFBindingRegistry 0xc3e1A1Cb021Ab96EBBeD3279018840016114A5c4 (library ASFEvaluation at 0x663B84f1BfcE37bE4a78358b0301B5d5E5079269), ASFGuardedAccount 0x40AaD5106A7a13B15892E53099f6bD06bCF80aD3, ASFTaskTenderAdapter 0x51772fabFB757Fb5A4186c0435Ec4fa76D75CE4e. All seven deployed contracts are verified on Sourcify (runtime and creation match). The record — every transaction with hash, block and gas, the decoded events, the state read back with eth_call, and the commands to reproduce the checks — is in docs/sepolia-evidence.md and deployments/ of the repository.

One implementation change came out of the run, and I want to state it plainly because it touches the reference implementation in the pull request. Sepolia’s current gas schedule prices code deposit about ten times higher than before, and a transaction may use at most 16,777,216 gas; the revision-4 binding registry (13.3 KB of runtime code) needed about 20.9 million gas to deploy and could not be deployed at all. The registry’s evaluation — Section 10.1, the combinators, the requirement validation and the response-set rule over a derivation chain — now lives in ASFEvaluation, a library linked externally and called with the registry’s storage; the project compiles via IR. The registry’s code fell to 9,660 bytes and deployed at 15.1 million gas. Nothing normative changed: the interfaces, the identifiers (0x1ff33bef, 0x9bddde77), the requirementId encoding, the vectors and all 84 tests are as in revision 4; the first post and the pull request carry revision 5 with the library added to the reference implementation. The first Sepolia attempt, sent with the gas limit of the Anvil rehearsal, ran out of gas; it is recorded as well.

The five questions of the first post stand, and the shared-budget coordinator remains the next closed loop.

Thanks, revision 4 closes it for the record the clause is checked against: an account the clause doesn’t name can’t hold RIGHT_RESUME there anymore.

The path I still see goes around the record. adopt takes a salt and nothing links one adopter’s bindings, so a fulfiller can hold a second binding for the same subject and spec that names the publisher with suspend, exactly as the clause asks. When the publisher suspends one, ASFTaskTenderAdapter.evaluate scans the fulfiller’s bindings and takes the first Satisfied one, so judge accepts on the sibling. With a single binding it works after the fact too: rightsOf gives the adopter RIGHT_ALL, so it can terminate the suspended record and adopt a fresh one, and terminate also clears suspendedBy, so the stored record no longer shows the stop (only the events do).

So in the tender a stop lasts only as long as the fulfiller keeps that one record. Two changes would make it stick: keep who stopped it through termination, and have the adapter (or the clause) refuse when any binding it scans for that adopter, subject and spec carries a stop from an account the clause names. Then the stop applies to the adopter, not to one bindingId.

Both paths are real, and the second one also exposed a contradiction between the text and the code: the security considerations said that a terminated record stays readable with its suspendedBy, while terminate cleared it. The root is the same in both cases: every rule attached to one bindingId, and adopt takes a salt, so the adopter decides how many records exist for the same subject and specification and which of them a consumer sees first. A permissionless judge then runs before the publisher can react.

Revision 6 makes the stop apply to the adopter, as you put it, and does so in the registry rather than in the adapter. A suspension recorded by an intervener is now also a standing stop, keyed by the adopter, the binding’s subject and specification, and the intervener; it carries the binding it was recorded through, the response set and the responseRef. It outlives the termination of that binding. It is lifted only by a resume of that binding under the existing rights — an account holding RIGHT_RESUME on it, never the adopter — and resume of a terminated record whose stop still stands lifts the stop, leaves the record Terminated and emits StopLifted. In the check, a clause that names interveners now fails with a new reason, StoppedByIntervener, for any binding of that adopter for that subject and specification while a stop by an account the clause names stands, whatever the binding’s own state, and returns the stop’s response set; a clause that names nobody is not concerned. terminate keeps suspendedBy, the response set and the responseRef on the record, so the stored state shows who stopped what, not only the events. stopOf exposes the standing stop.

I kept the stop keyed rather than scanned. A rule that scanned the adopter’s index would have to bound the scan, and a bound is a bypass (push the stopped record outside the window); a rule keyed by what a clause can pin — adopter, subject, specification — reaches exactly the bindings the clause would otherwise accept, at one storage read per named intervener. Keying it per intervener keeps the stop within the clauses that name its author and keeps lifting where the binding put it: the author, or whoever the binding granted the resume right. The adapter’s logic is unchanged; it scans the fulfiller’s bindings as before and now finds every sibling and every fresh adoption refused. One consequence is stated in the text: a stop by a suspend-only intervener stands until the requirement itself changes, so a requirement that names a suspender should also name who may lift its stops. The tender example now names the publisher with both the suspend and the resume right.

Tests N59 to N64: at the registry level (the adopter’s own suspension records no stop; the intervener’s does; the stop survives the adopter’s termination, cannot be lifted by the adopter or a stranger, is lifted by the guardian through the terminated record; a sibling binding and a fresh binding fail the naming clause with the stop’s set and pass a clause that names nobody; the key is per intervener, subject and specification; the stop is checked after the required interveners and before the ancestors), on the task path (your two scenarios verbatim — the sibling and the terminate-and-readopt with judge called by the fulfiller — both rejected; with the publisher named with both rights, the publisher lifts through the terminated record and the fresh binding is admitted and paid) and on the wallet path (the principal who terminates the guardian-stopped binding and presents a fresh one is still held to the guardian’s de-risk-only set). The suite is at 90 tests, 64 negative. The interface identifier changes with stopOf (IASFBindingRegistry is now 0x2db6eb8a); the clause encoding and requirementId values are unchanged. Text changes in Sections 6.5, 7, 8.1, a new 8.5, 10.1, 10.2, 11.2, 11.3, the rationale, the security considerations on termination and on intervener key compromise, and the test cases. The registry’s writes moved into a second linked library to stay well under Sepolia’s per-transaction gas cap; the Sepolia deployment recorded in the repository is revision 5’s, and revision 6 will be deployed in a second run. The pull request and the repository carry revision 6; the first post is updated.

Two choices I would like your view on: the key of the stop (adopter, subject, specification, intervener), which makes a stop span scopes and versions but not other subjects of the same adopter; and lifting through the recorded binding’s resume right rather than through a separate right of the intervener who stopped.

On the key, I’d keep it as is. Leaving version and scope out is what stops a v2 adoption or a new scope from walking around the stop, and in the tender the subject is the fulfiller’s own account anyway.

On lifting, the reach and the authority come from different places. A stop counts against every clause that names the intervener, but it is lifted by whoever holds RIGHT_RESUME on the one binding it was recorded through. Say two tenders require the same specification: one names the publisher with suspend and an executor with resume, the other names only the publisher, with suspend and resume. If the publisher suspends the fulfiller’s binding for the first tender, that executor can lift the stop the second tender relies on, though the second never named it.

Related: stops[key] holds one record and suspend overwrites it. A second suspension by the same publisher moves bindingId, restrictions and responseRef to the newer binding, so a milder set can replace a stricter one, resuming the first binding no longer touches the stop, and the newer binding’s resume holders now decide it.

I’d make the stop the intervener’s: lifted by that account or one it names when it stops, and a later suspension keeps the existing stop (OR the restrictions in) instead of replacing it.

1 Like

Our capacity prototype keeps pre-existing buyer refunds outside the provider’s safety binding, even under full stop. Does that boundary fit revision 6’s standing-stop model? Tests are currently pinned to revision 4.

Both points stand, and the second one is the sharper of the two: the reach of a stop came from the clause that named the intervener, while the authority to lift it came from the resume holders of one binding, chosen by the adopter and possibly never named by the clause that relied on the stop. Your two-tender example is now a test (N67), and so is the overwrite: one record per key, replaced on each suspension, let a milder set displace a stricter one and moved the lifting to the newer binding’s resume holders (N65, N66).

Revision 7 makes the stop the intervener’s. It is lifted only by the intervener that recorded it, or by the one account the intervener names as its lifter (setStopLifter, which it may call or change at any time and which applies to all of its stops), either through liftStop or through that account’s own resume of a binding the intervener suspended. Any other holder of the resume right reopens the record and nothing more; no right of the adopter lifts a stop; and a Terminated record accepts no resume at all, which removes the revision-6 oddity of resuming a terminated record to reach the stop behind it. A later suspension by the same intervener under the same key widens the stop instead of replacing it: the response set becomes the union, the binding identifier moves to the latest suspension, the response document is replaced only when the new suspension supplies one, and the time is that of the first recording. StopRecorded states the stop after every recording, StopLifted is keyed by the stop rather than by a binding, and stopLifterOf reports the lifter.

On “an account it names when it stops”: I made the lifter one account per intervener rather than a parameter of each suspension. The intervener makes that choice rarely and should be able to change it while a stop stands, which a value frozen at suspension time would not allow, and a per-suspension parameter would sit on every suspend, including the adopter’s own, which record no stop. If you see a case that needs a different lifter per stop, I would add it as a second, optional path rather than replace this one. The escalation shape survives at both levels: the clause names the executor with the resume right for the record, and the suspender names the executor as its lifter for the stop; N57 and N58 now show the executor’s resumption alone leaving the publisher’s stop in place. One consequence is stated in the security considerations: a compromised intervener key can rename the lifter, so the lifter is a review path, not a recovery path, and a consumer whose intervener key is lost changes its requirement.

The interface identifier of IASFBindingRegistry is now 0xcff7f826; the clause encoding and the requirementId values are unchanged. Text changes in Sections 6.5, 7, 8.1, 8.5, 10.2 and 11.2, a new rationale paragraph on who lifts a stop, the security considerations on intervener key compromise and on termination and sibling bindings, and the test cases. 93 tests, 67 negative. The pull request and the repository carry revision 7; the first post is updated. Nothing of revision 6 was deployed, so the second Sepolia run will deploy revision 7.

Yes, the boundary fits, and the standing-stop model does not move it. A stop is recorded against the subject of the binding and reaches the actions taken under the binding: the provider’s new commitments and its result submissions, in your profile. RESTRICT_FULL_STOP means that nothing under the binding may proceed; it says nothing about rights that other parties hold over their own assets through the order itself. A buyer’s cancellation, a deadline refund, third-party cleanup and the buyer’s acceptance of a result are not actions of the provider under its binding, so a stop on the provider is not a hold on them, and a consumer that let a provider’s stop freeze buyer escrow would be widening the stop beyond its subject. Section 11.3 asks the consumer to stop the actions the response set, the response document and the profiles do not allow and to keep the rest; the “rest” is what the subject does under the arrangement. The response document’s scope, mustContinue and inFlight members exist to say which of the subject’s own actions continue; the order’s deadlines are the order’s. The reference consumers draw the same line: ASFTaskTenderAdapter only judges submissions, and the kernel’s cancellation and refund paths are not gated by it at all; ASFGuardedAccount keeps repayment and cancellation open under de-risk-only because the owner listed them, not because the registry decided it. I will make this explicit in the text at the next revision: a stop applies to the subject’s actions under the binding, not to counterparties’ rights over their own assets.

What revisions 6 and 7 change for a port: a leaf of a stopped provider can now report StoppedByIntervener (reason 15) with the stop’s response set, also for a fresh binding adopted after a termination, and since revision 7 that stop is the guardian’s to lift (liftStop, or the guardian’s own resume); a resumption by any other holder of the resume right reopens the record and leaves the stop standing. Your pinned-leaf design already closes the replacement path on its own, so the visible difference for you is the new reason code and the set that comes with it; your table’s “other suspension, expiry, termination or unknown: denied” column is the right default for it, and the stop’s own responseRef is readable through stopOf if you keep the rule that a non-zero document closes continuation. Your whole-chain check of ancestor resume holders remains a consumer policy stricter than the clause, as your note says, and nothing in the new text contradicts it. If you port, pin revision 7 directly (IASFBindingRegistry 0xcff7f826); nothing in the boundary you describe needs to change. Thank you for building it against the real registries; a consumer that distinguishes stopped admission, narrow continuation and independent escrow recovery is exactly the shape the response requirements were meant to allow.

How are earlier response requirements preserved when a later suspension replaces responseRef?

Suppose the first document prohibits a specific in-flight action. A second suspension adds another restriction but supplies a document that says nothing about that action. The bit sets are combined by union but the earlier document is no longer the current reference.

Must the new document explicitly preserve all earlier requirements? Otherwise the on-chain flags can become stricter while the detailed instructions become weaker. A test with two successive suspensions would help clarify this.

1 Like

Thank you, this is a real gap in the text, not in the recording. What is recorded today, and where:

  • The stop keeps one responseRef: the document supplied with the latest recording into it, unchanged when the later suspension supplies none (so a second suspension with a zero responseRef does not drop the first document). The stop’s response set is the union and only widens. Both are in Section 8.5, and N65 is the test with two successive suspensions you describe: the set widens, the first document stays until a later suspension supplies one, a later non-zero document replaces it, and each binding record keeps its own set.
  • The earlier document is not lost. Each binding record keeps the responseRef of its own suspension (bindingOf(b).responseRef, until that record is resumed), and every recording into the stop emitted StopRecorded with the set and the document after it, so the sequence of documents is reconstructible from the events of the stop’s key.

What the text does not say is which document governs when a reader has only stopOf. Your example is exactly the case: the first document prohibits a specific in-flight action, the second adds a restriction but says nothing about it; the bits are stricter, the current reference is weaker. The on-chain set is what the registry’s own consumers enforce mechanically (the reference account reads the bits, never the document), so the mechanical part only tightens; the detail that lives in the documents, above all inFlight, mustContinue and scope, is where the weakening can happen.

I would close it in the text rather than in storage. A list of documents per stop is unbounded on chain and adds nothing the StopRecorded events do not already carry. The clarification I have in mind for the next revision of Section 8.5: the stop’s responseRef is the document of the latest recording and MUST be read as the current reference for the stop as widened; a response document supplied with a suspension that widens a standing stop MUST therefore state the requirements of the stop as widened, either in full or by naming the earlier document it incorporates (the response document schema already has evidence and scope members where a reference fits; a supersedes member naming the earlier responseRef is the natural addition). An intervener that does not want to restate anything supplies a zero responseRef, and the earlier document stays the reference. On the test side, N65 would gain the assertion that each record keeps its own responseRef through the widening, and an example response document that widens an earlier one.

If you see a case where the on-chain set alone should carry more (for example a bit for “in-flight actions prohibited” so that the most common detail does not depend on the document), that would be a different change and I would rather hear the case first.