Internet-Draft Composite Execution Finality September 2026
Das Expires 20 March 2027 [Page]
Workgroup:
Individual Submission
Internet-Draft:
draft-das-composite-execution-finality-01
Published:
Intended Status:
Informational
Expires:
Author:
S. Das
Independent

Partial Commit Is Not Finality: Composite Candidate Acts Across Multiple Finality Sinks

Abstract

An agent, a payment orchestrator, or a control-plane applicator often intends one Candidate Act that would become real only as several consequences: a settlement post, a ledger write, a tool invoke, a notification, a radio enable. [DAS-HANDLE] specifies verify, consume, and receipt at one Finality Sink. It does not say what "the act was permitted" means when sink S1 has consumed and posted and sink S2 has rejected, timed out, or committed a different digest.

Existing distributed-transaction tools already address adjacent problems. Two-phase commit, XA, sagas, TCC, transactional outbox, and workflow engines coordinate steps. OAuth, WIMSE, RATS, and SCITT still name identity, environment, and signed statements. None of them, by themselves, bind N sink-local Execution Handles to one composite Act Digest and require that no child consequence become externally effective unless every required child reaches a defined terminal state.

This document specifies Composite Candidate Acts, child handles, a coordinator that may only prepare, a two-phase consume rule, and the distinction between prevention (no child commits unless the composite closes) and mitigation (compensate after a partial post). Compensation is itself a Candidate Act. It is not a silent undo and not a license to skip verify on the original child.

The join is closed by a durable, linearizable Composite Decision Log holding one value per composite: empty, COMMIT, or ABORT, written only by compare-and-swap so that the first writer wins. The coordinator MUST win the log before sending a decision to any child. A prepared child whose reservation timer expires MUST consult the log: it commits if COMMIT is recorded, releases if ABORT is recorded, attempts its own compare-and-swap of ABORT if the log is empty, and holds its reservation (HOLD) only if the log is unreachable. No child acts on silence alone.

This is not a new consensus protocol and not a claim that XA is obsolete. Profiles that cannot obtain prepare-from-every-sink, or cannot reach a durable linearizable decision log, MUST NOT claim all-or-none prevention.

Three rejections are expected and are treated as design constraints. "Saga plus handle is enough" is accepted when each saga step already reconstructs the live child, consumes a child handle, and cannot log SUCCESS after a required child rejects; this draft then shrinks to join fields. Prepare across N sinks is a latency tax only if consume-state is global; it is per child handle, and a no_prepare mail or MCP child must use the mitigation profile rather than pretend 2PC. Child sinks reconstruct only their own pending effect, not a mesh-wide CAD. Reviewers who would reject on saga, performance, or reconstruction grounds are asked to read those sections before discarding the document. Criticism, including "this should stay a note in the handle draft," remains invited.

Status of This Memo

This Internet-Draft is submitted in full conformance with the provisions of BCP 78 and BCP 79.

Internet-Drafts are working documents of the Internet Engineering Task Force (IETF). Note that other groups may also distribute working documents as Internet-Drafts. The list of current Internet-Drafts is at https://datatracker.ietf.org/drafts/current/.

Internet-Drafts are draft documents valid for a maximum of six months and may be updated, replaced, or obsoleted by other documents at any time. It is inappropriate to use Internet-Drafts as reference material or to cite them other than as "work in progress."

This Internet-Draft will expire on 20 March 2027.

Table of Contents

1. What This Version (-01) Contains

This Internet-Draft is draft-das-composite-execution-finality-01. It specifies how several handle-using sinks join as one Composite Candidate Act: child handles, prepare/commit/abort, ALL_REQUIRED prevention, and compensation as a new act. Single-sink objects remain in [DAS-HANDLE]; code points remain in [DAS-REG].

1.1. Contents of -01

  • Problem of split reality (one child posts, another rejects) after a locally correct handle.
  • Composite CAD, child handles bound to composite_id, coordinator rules.
  • Prepare/commit/abort, join rules, and the mitigation profile for no_prepare sinks.
  • 2PC timeout incompatibility resolved by a compare-and-swap Composite Decision Log: child CAS ABORT on an empty log, HOLD only when the log is unreachable, EF-047 for a discovered split.
  • Formal join identities and pointer to the handle-draft hash / TLA+ embedding.
  • Anticipated criticisms (saga plus handle, latency, reconstruction) and abstract hints.
  • Industrial disclaimer, community questions, and IETF venue mapping.

1.2. Changes from -00

  • Document name and series info updated to -01.
  • Abstract names saga, performance, and reconstruction objections, and states the decision-log rule.
  • Added the Composite Decision Log: durable, linearizable, written only by compare-and-swap (first writer wins). The coordinator wins the log before sending any decision.
  • A prepared child whose reservation timer expires consults the log: COMMIT commits, ABORT releases, an empty log allows the child to CAS ABORT, and an unreachable log leads to HOLD (EF-048). This removes the -00 partial-commit path and prevents indefinite blocking when the coordinator dies before deciding.
  • Coordinator abort that loses the CAS to COMMIT re-drives commit instead of aborting.
  • Coordinator re-checks EH_C currentness immediately before writing COMMIT.
  • After COMMIT is recorded, child commits are retried to a terminal state; only a child that cannot post yields COMPENSATING with EF-047.
  • The same rule is applied consistently in the state machine, Phase 2, coordinator rules, timeouts, formal join, PreventionWellFormed, pseudocode, profiles, questions, venues, security, and IANA sections.
  • ABORTED includes a deliberate coordinator abort, always via the log.
  • Partial commit uses EF-047, not EF-044.
  • Security Considerations adds decision-log integrity and availability as trusted computing base, and child-forced abort as a liveness (not safety) attack.
  • Family references aligned to current titles, revisions, and Datatracker URLs; handle and registries cited as -01.
  • Split(C,t) refined so that the normal in-progress window after COMMIT is Closing(C,t) and COMMIT_DECIDED, not COMPENSATING.
  • Child handles bind composite_digest = d(C), a digest over the sorted child list, not only composite_id.
  • Added Safety, Closure, and Termination: decision-log axioms, child action invariants, No-Split theorem with proof, Closure theorem with its separate assumptions, termination bounds, the finality point and currentness condition, and compensation identities.

2. Introduction

2.1. Vulnerability: One Intent, Split Reality

A composite intent C is authorized as a unit: pay vendor X AND write the invoice AND notify finance. If the settlement sink consumes its handle and posts, and the notification sink times out, operators still say "the agent paid." The Candidate Act that was reviewed was not the world that occurred. No child token was forged. The failure is split effectuation of one intended act.

Intended C = { pay X, book invoice, notify finance }

        +-- S1 SETTLEMENT   consume + post     EFFECTUATED
   C ---+-- S2 CONTROL      timeout            NON_EFFECTIVE
        +-- S3 TOOL         never invoked      NON_EFFECTIVE

Reviewed act:   C
Occurred world: pay X only

Partial commit
      !=
composite finality
Figure 1: Child S1 commits; child S2 never becomes effective

2.2. Existing Mechanisms Already Coordinate Steps

Two-phase commit and XA serialize prepare/commit across resource managers. Sagas and TCC specify forward actions plus compensations. Outbox and workflow engines order side effects. Those tools should be used. They do not define an Execution Handle per child sink, do not reconstruct each live child act, and do not make "compensation" a second handled Candidate Act.

2.3. Residual Gap After a Single-Sink Handle

[DAS-HANDLE] closes possession-versus-authority at one boundary. After that draft, each child can be locally correct and the composite still wrong. Registries reserve COMPOSITE as a class and a sink type [DAS-REG]. They do not specify the protocol.

2.4. What This Document Introduces

  • Composite CAD with an ordered, explicit child list and a composite Act Digest.
  • One child Execution Handle per required sink, all bound to the same composite id.
  • Prepare / commit / abort on consume-state, with fail-closed defaults.
  • A coordinator role that cannot substitute for child sinks.
  • Compensation as a new Candidate Act, not a hidden reverse of the original handle.

3. Conventions and Requirements Language

The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in BCP 14 [RFC2119] [RFC8174] when, and only when, they appear in all capitals.

4. Direct Question

If C is the act that was authorized, and only a proper subset of C's required children have become externally effective, was C effectuated?

Prevention profile:
    C is EFFECTUATED
        iff every required child is EFFECTUATED
        under handles bound to C
        and no extra child effect is attributed to C.

Otherwise C remains NON_EFFECTIVE
    even if some child already moved money or iron.

That last sentence is the operational cost. If a profile cannot prevent the child post, it must not advertise all-or-none prevention.

5. Motivating Scenario: Pay, Book, Notify

An enterprise agent is allowed to settle a vendor invoice only as a unit with the ERP booking and an audit mail. Settlement posts in 80 ms. ERP is in a change window. Mail is a third-party API with no prepare. A coordinator that "retries notify later" has not effectuated C. It has effectuated a different act: pay-without-book.

Authorized C17 = pay + book + notify   [all required]

After S1 post, S2 reject:
    world = paid, not booked
    C17   = still NON_EFFECTIVE

C18 = "notify later about a payment already posted"
    is a new CAD
    and needs its own handle
Figure 2: Why retry-later is a different Candidate Act

6. Threat Model

The attacker need not forge handles. Relevant failures:

7. Goals and Non-Goals

Goal: define when a multi-sink Candidate Act may be called EFFECTUATED, PREPARED, ABORTED, or COMPENSATING, using the objects in [DAS-HANDLE].

Non-goals: a new Paxos/Raft; replacing XA or workflow engines; requiring every SaaS to implement prepare; claiming exactly-once delivery across the open Internet; automatic legal netting of a posted payment.

8. Terminology

Composite Candidate Act (C): A Candidate Act whose intended consequence is the conjunction of two or more child consequences.

Child Act C_i: The component that one sink would effectuate.

Required child: A child that MUST reach EFFECTUATED for C to reach EFFECTUATED.

Optional child: A child that MUST NOT block C if it fails, and MUST NOT be advertised as part of C's authorized unit if it is skipped. Optional children SHOULD be separate acts.

Coordinator: The component that records composite state. It is not a Finality Sink for a child effect unless it is also that child's sink.

Prepare: Child sink has verified the live child act, reserved consume-state, and promises not to post until the Composite Decision Log records COMMIT, and not to release until it records ABORT.

Composite Decision Log (CDL): A durable, linearizable register holding one value per composite identifier: empty, COMMIT, or ABORT. Its only write operation is CAS(C, empty -> v). The first successful write is final; every later write fails and returns the recorded value. The coordinator and every prepared required child can read it and attempt the write. "Queryable" is not sufficient: an eventually consistent store cannot provide first-writer-wins.

HOLD: The child-local state of a prepared child that cannot read or write the CDL. A child in HOLD produces no external effect and keeps its reservation.

Compensation act: A new Candidate Act that would reverse or contain a child effect that already escaped. It uses a new handle.

9. Composite Objects

9.1. Composite CAD

A composite CAD is a CAD whose consequence_class and coordinator sink_type are COMPOSITE [DAS-REG]. It MUST list children explicitly.

composite_cad = {
  candidate_act_id,
  composite_act_digest,     // d(C), see below
  consequence_class: "COMPOSITE",
  children: [
    { child_id, cad_i, sink_i, required: true|false },
    ...
  ],
  join_rule: "ALL_REQUIRED",
  expires_at,
  generations
}

ALL_REQUIRED:
    every child with required=true must EFFECTUATE
    for C to EFFECTUATE.

QUORUM / BEST_EFFORT:
    MUST NOT be used in a prevention profile
    for FINANCIAL, PHYSICAL, RF, CLINICAL, MODEL_RELEASE.

The composite Act Digest binds the whole child list, not only an identifier. Children are sorted by child_id before canonicalization so that the digest does not depend on presentation order.

d(C) = Hash( Canon( join_rule,
         sort_by_id[ (id_i, d(C_i), S_i, required_i) ]
         for all children i ) )

Hash and Canon are those of [DAS-HANDLE]. Adding, removing, reordering-as-required, or substituting a child changes d(C).

9.2. Child Handles

Each required child has its own Execution Handle EH_i issued by the PED. EH_i MUST bind:

  • act_digest of child CAD_i
  • sink_id of S_i
  • composite_id = C.candidate_act_id
  • composite_digest = d(C), the composite Act Digest
  • reuse_policy compatible with the child class

A child handle MUST NOT be accepted at a sink whose consume would not serve that composite_id and composite_digest. Binding only composite_id is insufficient: an attacker who can alter the child list while keeping the identifier would otherwise obtain child handles that still match. Presenting EH_i for a standalone payment that is not C is EF-023 or EF-040 [DAS-REG].

9.3. Composite Handle

The coordinator MAY hold a composite handle EH_C whose digest is the composite Act Digest. EH_C authorizes the coordinator to drive prepare/commit/abort. It does not authorize S_i to post.

10. Composite State Machine

                 issue
                   |
                   v
              NON_EFFECTIVE
                   |
              children verify
                   v
                PREPARED --- CDL CAS ABORT wins ---> ABORTED
                   |       (coordinator or child)
              CDL CAS COMMIT wins
                   v
              COMMIT_DECIDED
                   |
              every required child driven to commit
                   v
              EFFECTUATED

If any required child already posted
and others cannot:
              EFFECTUATED is forbidden
                   |
                   v
              COMPENSATING -- compensation C' --> CONTAINED
                   |
                   +--> UNKNOWN (compensation also split)

Prevention profile forbids entering COMPENSATING
because a required child posted before join.

Child-local, on reservation timer:
  CDL = COMMIT        --> commit        (EFFECTUATED)
  CDL = ABORT         --> release       (RELEASED)
  CDL = empty         --> CAS ABORT
                           won  --> release
                           lost --> commit (COMMIT won)
  CDL unreachable     --> HOLD, no post, EF-048
  HOLD, CDL readable  --> re-evaluate as above
Figure 3: States of C, distinct from child sink states

Child sinks use the handle draft states locally: verify, consume (here: reserve / commit / abort), receipt. C's state is a join of those receipts, not a vote by the model that proposed C.

11. Prepare, Commit, Abort

11.1. Phase 0: Reconstruct Each Child

Each S_i MUST reconstruct live child CAD_i from the pending local effect, not from a coordinator-supplied blob alone [DAS-HANDLE]. Coordinator-supplied CAD_i is a hint. Digest mismatch is EF-023.

11.2. Phase 1: Prepare

Prepare is consume-state reservation without external effect.

S_i.prepare(EH_i, live_cad_i):
    verify handle and reconstruct digest
    if fail: receipt REJECTED, do not reserve
    if consume-store down: EF-081, fail closed
    reserve EH_i  (not yet posted)
    return PREPARED

Coordinator:
    if any required child not PREPARED
        before deadline:
            Coordinator.abort(C)   // via the CDL

A sink that cannot reserve without posting (typical third-party mail or card API) MUST be declared no_prepare=true. A composite that includes a required no_prepare sink MUST NOT use the prevention profile. It MAY use mitigation: post last, or accept COMPENSATING.

11.3. Phase 2: Commit or Abort

Coordinator.commit(C):
    require all required children PREPARED
    require EH_C still current          // checked immediately
                                        // before the CAS
    v = CDL.CAS(C, empty -> COMMIT)
    if v == ABORT:                      // a child TTL or an
        release_all_prepared(C)         // earlier abort won
        C = ABORTED
        return
    C = COMMIT_DECIDED                  // point of no return
    for each required S_i:
        r = retry_until_terminal(S_i.commit, EH_i)
        // a PREPARED child promised to post; transient
        // failure is retried, never treated as abort
    if all required EFFECTUATED:
        C = EFFECTUATED
        emit composite receipt
    else:
        // reachable only if a prepared sink cannot post
        // (broken promise, sink destroyed, legal stop)
        C = COMPENSATING                // EF-047

Coordinator.abort(C):
    v = CDL.CAS(C, empty -> ABORT)
    if v == COMMIT:                     // lost to an earlier
        Coordinator.commit_redrive(C)   // COMMIT: do not abort
        return
    for each PREPARED S_i:
        S_i.abort(EH_i)                 // RELEASED, no post
    C = ABORTED

The composite finality point is the successful CAS of COMMIT, not the first child post. Child currentness (generations, revocation) is evaluated inside each child's serialized prepare section [DAS-HANDLE], and EH_C currentness is re-checked immediately before the CAS. A revocation that arrives after COMMIT is recorded applies to an act that is already final; it is handled by a new compensation act (Section 13), not by releasing a child that the log says must commit.

12. Join Rules

Table 1: Join rules and whether prevention may be claimed
Rule C EFFECTUATED when Prevention claim
ALL_REQUIRED every required child EFFECTUATED Yes, if every required sink supports prepare
ALL_THEN_OPTIONAL required set EFFECTUATED; optional attempted Only over the required set
QUORUM(k) any k children No for high-consequence classes
BEST_EFFORT coordinator tried Never

13. Compensation Is a New Act

If a required child has already posted, C cannot become EFFECTUATED and cannot become cleanly ABORTED. A compensation Candidate Act C' MAY be issued: reverse the post, hold the funds, disable the radio, retract the tool side effect.

EH_pay  CONSUMED + posted
EH_book REJECTED

Forbidden:
    S_pay.undo(EH_pay)     // handle already consumed

Required:
    C' = "return funds for composite C17"
    new CAD', new EH'
    S_pay.verify/consume(EH')
    C  = COMPENSATING then CONTAINED
         only if C' EFFECTUATED
Figure 4: Compensation does not reuse the original child handle

Success of C' is not success of C. Receipts MUST keep the two identifiers distinct. Failure of C' leaves C in UNKNOWN containment and MUST be visible.

14. Coordinator Rules

15. Timeouts, Crash, and Child CAS Abort

Classic two-phase commit is blocking. The following two rules cannot both hold in a prevention profile:

  1. A PREPARED child whose reserve TTL expires aborts locally without asking anyone.
  2. After the coordinator has decided COMMIT, no required child may abort.

If the coordinator writes COMMIT, S1 posts, and then dies before S2 hears COMMIT, S2's local TTL abort produces the partial commit this draft called unreachable. That path is reachable unless children consult a durable decision before acting on a timer.

A log that children can only query is not enough on its own. If the coordinator dies before deciding, every child reads an empty log and cannot safely release, because the coordinator might still write COMMIT. Every child would then block. The CDL removes that case: a child may itself write ABORT into an empty log, and because the write is compare-and-swap, the coordinator can no longer write COMMIT afterwards. HOLD is reserved for the case where the log itself is unreachable.

Forbidden pair (prevention profile):
    coordinator.decision == COMMIT
        AND
    some required child locally ABORTS on TTL

Required instead:
    coordinator wins CDL.CAS before sending
        COMMIT or ABORT to any child
    child reservation timer fires
        -> read CDL[C]
        -> COMMIT:      commit locally
        -> ABORT:       release (RELEASED)
        -> empty:       CAS(C, empty -> ABORT)
                          won:  release
                          lost: commit
        -> unreachable: HOLD, no post, EF-048
                        retry the log; alert operator

Dead coordinator, no decision:
    first child timer writes ABORT
    composite ABORTED, no child posts

Dead coordinator, COMMIT recorded:
    every child timer reads COMMIT
    composite closes

If the deployment cannot provide a durable,
linearizable CDL:
    it MUST NOT claim PreventionWellFormed
    it MAY use hold-until-operator
    it MUST treat a discovered split as
        C = COMPENSATING, reason EF-047

Automatic commit on coordinator silence remains forbidden. Automatic release on a child timer is forbidden unless the child's own CAS of ABORT succeeds or the log already records ABORT. Timers trigger a decision attempt; they never substitute for a decision. An operator resolution procedure, if a profile defines one, MUST itself write through CDL.CAS so that a manual decision cannot contradict an automatic one. A child in HOLD SHOULD raise an operator alert; for sinks where a long-held reservation is itself harmful (for example a held payment authorization), the profile SHOULD document the maximum acceptable HOLD duration and the escalation path.

16. Formal Join

Let C have required index set R. Child i has handle H_i, sink S_i, live act C_i(t), and local state q_i(t). The identities below are the same join as the prepare/commit pseudocode. Child digests use the hash function of [DAS-HANDLE]. A TLA+ model of this join is well-formed only if COMMIT followed by child TTL cannot reach Split under PreventionWellFormed.

Posted(C,t)   = { i in R | q_i(t) = EFFECTUATED }

Effect(C,t)   iff  Posted(C,t) = R
              and  forall i in R. d(C_i(t)) = H_i.d
              and  forall i in R. H_i.composite_id = C.id
              and  forall i in R. H_i.composite_digest = d(C)

Abort(C,t)    iff  Posted(C,t) = empty
              and  CDL(C) = ABORT

Split(C,t)    iff  Posted(C,t) nonempty and Posted(C,t) != R
              and  ( CDL(C) != COMMIT
                     or exists i in R. CannotPost(i,t) )
Split(C,t)    =>   COMPENSATING with EF-047

Closing(C,t)  iff  CDL(C) = COMMIT
              and  Posted(C,t) nonempty and Posted(C,t) != R
              and  forall i in R. not CannotPost(i,t)
Closing(C,t)  =>   COMMIT_DECIDED (transient, not a split)

reject, deadline, operator abort, or child timer
    => attempt CDL.CAS(C, empty -> ABORT)
       (never a direct transition to Abort)

CDL(C) = COMMIT => no PREPARED child is released
Timer:  COMMIT -> commit
        ABORT  -> release
        empty  -> CAS ABORT (won: release; lost: commit)
        unreachable -> HOLD
Required(C)     = { i | child_i.required }
Prepared(C)     = { i in Required(C) | S_i.state == PREPARED }
Posted(C)       = { i in Required(C) | S_i.state == EFFECTUATED }
Rejected(C)     = { i in Required(C) | S_i.state == REJECTED }

C.EFFECTUATED   iff Posted(C) == Required(C)
                 AND no child digest diverged
                 AND join_rule == ALL_REQUIRED
                     or ALL_THEN_OPTIONAL

C.ABORTED       iff Posted(C) empty
                 AND CDL(C) == ABORT

C.COMMIT_DECIDED iff CDL(C) == COMMIT
                 AND Posted(C) != Required(C)
                 // transient: every required child
                 // is being driven to EFFECTUATED

C.COMPENSATING  iff Posted(C) nonempty
                 AND Posted(C) != Required(C)
                 AND some required child can no longer post

PreventionWellFormed(C) iff
    join_rule == ALL_REQUIRED
AND every required S_i supports prepare
AND no required S_i has no_prepare
AND a durable, linearizable CDL is reachable
    by the coordinator and every required child
AND no child releases or posts on a timer
    except as the CDL dictates

PreventionWellFormed(C) == false
    ==> MUST NOT claim all-or-none prevention

17. Safety, Closure, and Termination

This section states the properties the join claims and the assumptions each property needs. Safety (no split) depends only on the Composite Decision Log and child behaviour. Closure (the composite eventually ends) additionally depends on sinks keeping the promise made at prepare and on fair re-drive. The two claims are kept separate so that a deployment can state which one it provides.

17.1. Decision-Log Axioms

Notation: bot is the empty value; CDL(C,t) is the value recorded for C at time t; Read_x(C,t) is a read by party x.

A1 Stability:
     CDL(C,t) != bot
       =>  forall t' >= t.  CDL(C,t') = CDL(C,t)

A2 Single value (linearizability):
     Read_x(C,t) = v  and  Read_y(C,t') = v'
     and  v != bot  and  v' != bot
       =>  v = v'

A3 First writer wins:
     CAS_x(C, bot -> v) succeeds at t
       =>  CDL(C,t) = v  and  every later CAS fails
           and returns v

17.2. Child Action Invariants

I1  Post_i(t)
      =>  exists t0 <= t.  Read_i(C,t0) = COMMIT

I2  Release_i(t)
      =>  exists t0 <= t.  Read_i(C,t0) = ABORT
                        or WonCAS_i(C, ABORT, t0)

I3  Post_i(t)
      =>  Prepared_i(t_p)  with  t_p < t_dec
    where t_dec is the time of the successful
    CAS(C, bot -> COMMIT)

I4  Hold_i(t)
      =>  not Post_i(t)  and  not Release_i(t)

17.3. No-Split Theorem

Theorem 1 (Safety).  Under A1, A2, A3, I1, I2:

    not exists i, j in R.
        Posted_i  and  Released_j

Proof.
    Posted_i    =>  some party read COMMIT      (I1)
    Released_j  =>  some party read ABORT
                    or won CAS ABORT            (I2, A3)
    A1 and A2 forbid the log holding both
    COMMIT and ABORT for C.
    Hence no execution has both.               QED

Theorem 1 does not require the coordinator to be correct, alive, or honest. It requires only that the log satisfies A1 to A3 and that every required child obeys I1 and I2.

17.4. Closure Theorem

A4 Promise kept:
     CDL(C) = COMMIT  =>  forall i in R.  not CannotPost(i)

A5 Weak fairness:
     a party that can re-drive commit, or retry a
     reachable log, eventually does so

A6 Log eventually reachable:
     <>[] Reachable(CDL)

Theorem 2 (Closure).  Under Theorem 1, A4, A5, A6:

    <>( Effect(C)  or  Abort(C) )

Sketch.
    If CDL(C) stays bot, the first expiring child
    timer writes ABORT once the log is reachable
    (A5, A6), so Abort(C).
    If CDL(C) = COMMIT, every required child is
    driven to Post (A4, A5), so Effect(C).
    By A1 exactly one branch applies.           QED

If A4 fails for some child (a prepared sink is destroyed, or a legal stop prevents posting), Theorem 1 still holds for all other children but the composite enters COMPENSATING with EF-047. That case is a broken prepare promise, not a protocol split.

17.5. Termination Bounds

Coordinator dead before deciding, log reachable:
    T_abort  <=  min_i TTL_i  +  delta_cas

HOLD duration for child i:
    T_hold(i)  <=  T_unreach(CDL)  +  TTL_i  +  delta_cas

Commit completion after t_dec, with at most k
retries per child and per-attempt time delta_post:
    T_close  <=  t_dec  +  |R| * k * delta_post
             (sequential; parallel re-drive gives
              max_i instead of the sum)

T_unreach(CDL) is outside the protocol. It is why log availability belongs to the trusted computing base and why a profile SHOULD state a maximum acceptable HOLD duration.

17.6. Finality Point and Currentness

t_prep_i  = time child i entered PREPARED
t_dec     = time of successful CAS(C, bot -> COMMIT)

Effect(C)
    =>  forall i in R.  Current(H_i.g, t_prep_i)
    and Current(EH_C.g, t_dec)
    and t_dec >= max_i t_prep_i

Revoked(H_i, t)  with  t > t_dec
    =>  handled by a compensation act C'
        not by releasing child i

17.7. Compensation Is Distinct from C

Effect(C')  does not imply  Effect(C)

Contained(C)  iff  Split(C)
              and  Effect(C')
              and  C'.parent = C.id
              and  C'.handle != any H_i

Failed containment:
    Split(C) and not Effect(C')  =>  UNKNOWN

18. Illustrative Pseudocode

function effectuate_composite(C, handles, cdl):
    if not PreventionWellFormed(C):
        return mitigation_mode(C, handles)   # different claim

    prepared = []
    for child in required(C):
        live = child.sink.reconstruct()
        r = child.sink.prepare(handles[child], live, C.id)
        if r.decision != "PREPARED":
            return abort_via_log(C, prepared, cdl, r.reason_code)
        prepared.append(child)

    if not ehc_current(C):                   # re-check before CAS
        return abort_via_log(C, prepared, cdl, "EF-041")

    if cdl.cas(C.id, EMPTY, COMMIT) == ABORT:
        release_all(prepared)                # a child timer won
        return receipt(C, "ABORTED", "EF-048")

    for child in required(C):                # point of no return
        r = retry_until_terminal(child.sink.commit, handles[child])
        if r.decision != "EFFECTUATED":
            return receipt(C, "COMPENSATING", "EF-047")

    return receipt(C, "EFFECTUATED", consume_outcome="CONSUMED")


function abort_via_log(C, prepared, cdl, code):
    if cdl.cas(C.id, EMPTY, ABORT) == COMMIT:
        return redrive_commit(C)             # too late to abort
    release_all(prepared)
    return receipt(C, "ABORTED", code)


function child_on_reservation_timer(child, C_id, cdl):
    try:
        v = cdl.read(C_id)
        if v == EMPTY:
            v = cdl.cas(C_id, EMPTY, ABORT)  # returns final value
    except Unreachable:
        child.state = HOLD                   # no post, keep reserve
        alert_operator(C_id)
        return report("EF-048")
    if v == ABORT:
        child.release()                      # RELEASED
    else:
        child.commit()                       # COMMIT is final


function compensate(C, child_posted):
    C2 = new_cad(reverse_of(child_posted), parent=C.id)
    eh2 = PED.issue(C2)
    return child_posted.sink.verify_consume(eh2)

19. Composition with Existing Tools

Table 2: What existing coordinators supply versus what is still required
Existing tool Supplies Still required
XA / 2PC Prepare/commit wire to RMs Child Act Digest + handle bind
Saga / TCC Forward + compensate graph Compensation as new handle; no silent undo
Outbox / workflow Ordered jobs, retries Retries are new attempts, not C success
SCITT Logged composite receipt Log is not child commit
Single-sink handle Local authority This join

20. Profiles

ef-composite-strict: ALL_REQUIRED, every required sink supports prepare, a durable linearizable Composite Decision Log written only by compare-and-swap, child timers act only through that log (CAS ABORT if empty, HOLD if unreachable), compensation is a new act, coordinator receipt insufficient.

ef-composite-mitigation: at least one required sink is no_prepare, or no durable linearizable decision log is available; document the escaped-effect window; MUST NOT claim prevention.

21. Anticipated Criticisms

The invitation in the abstract to treat "saga plus handle is enough" as a successful review outcome is deliberate. The same is true of the performance and reconstruction objections. This section states where those objections are accepted.

21.1. Objection: Saga Plus Handle Is Enough

If each saga step is already a handle-using sink — reconstruct live child, prepare or consume, fail closed — then this draft is a join profile on top of that saga, not a replacement for it. XA resource managers that expose prepare, and PSP authorize/capture pairs, are the same pattern under other names.

The residual cases this draft still names:

  • the saga logs SUCCESS after a required child posts and another required child rejects;
  • compensation reuses the original child handle instead of a new reverse act;
  • a no_prepare child (mail, many MCP tools) is treated as if 2PC applied;
  • the coordinator receipt is taken as proof that settlement occurred.
Accepted:
    saga / XA / workflow
        AS coordinator
    child Execution Handle
        AS per-sink authority

Not accepted as prevention:
    saga success
        without child PREPARED/EFFECTUATED receipts
    compensate(original_token)
        after the original handle is CONSUMED
    BEST_EFFORT notify
        advertised as C EFFECTUATED

If reviewers produce a saga profile that already forbids those four cases, this document should shrink to that profile plus the composite CAD fields. That outcome is welcome.

21.2. Objection: Prepare Across N Sinks Is a Latency Tax

ALL_REQUIRED prepare adds one reservation round to every required child before any child posts. On a payment rail that is already authorize-then-capture, that round already exists. On a three-sink agent path where mail cannot prepare, the honest profile is mitigation: do not claim all-or-none, or split notify into a later act.

Consume-state remains per child handle, not one lock for the whole composite. The coordinator persists join state; it does not serialize unrelated composites. Automatic commit on coordinator silence is still forbidden in the strict profile, because that is how partial posts happen.

21.3. Objection: Each Child Cannot Reconstruct a Unified CAD

Children do not reconstruct the composite CAD. Each sink reconstructs only its child: the payment it would post, the tool it would invoke, the row it would write. The coordinator compares child receipts and the composite digest over the declared child list. A mesh hop that is not a child sink does not reconstruct anything.

If a child sink cannot observe the fields that change its own effect, that child cannot be in a prevention-profile composite. It can be an optional later act. Hiding that limitation inside a caller-supplied composite blob is how split reality occurs.

21.4. What Would Settle the Argument

A minimal demonstration is more useful than another join diagram: one MCP (or payment) middleware that consumes a child handle, one second sink that can prepare, and a coordinator that aborts when the second child rejects — including the case where a prompt-injected destination changes only one child's digest. This document does not ship that code. An implementation that does is invited.

22. Industrial Relevance and Public-Roadmap Alignment

Agent products already chain tools: pay, ticket, mail, CRM write. Cloud workflow services already run sagas. PSPs already split authorize and capture. Those roadmaps are complementary. This document does not assert that any named orchestrator is unsafe, incomplete, or required to implement prepare.

PSP authorize/capture   ~  prepare/commit at SETTLEMENT
Cloud workflow / saga   ~  coordinator + compensation graph
MCP tool chain          ~  children; often no_prepare
Agent runtime           ~  proposer of C, not the join

A PSP capture API that already holds an authorization and voids it on timeout is already a prepare-capable SETTLEMENT sink under another name. An MCP server that cannot reserve a Gmail send is a no_prepare TOOL_DISPATCH sink. Putting both in one "strict" composite would be a mis-profile, not a vendor defect.

Named companies are not claimed as reviewers, implementers, or endorsers. Corrections and requests to remove a framing are invited. Silence is not agreement.

23. Questions to the IETF Community

  1. Is composite finality a separate document, or a section of the handle draft?
  2. Is ALL_REQUIRED the only join rule worth specifying?
  3. Must every prevention-profile child support prepare, or is "post last no_prepare sink" an acceptable prevention trick?
  4. Is child compare-and-swap of ABORT into an empty decision log the right reservation-timer default, with HOLD only when the log is unreachable?
  5. For sinks where a held reservation is itself harmful, should the profile define a maximum HOLD duration that ends in an operator decision written through the log?
  6. Should the Composite Decision Log be a named wire object with a media type, or remain an abstract linearizable register?
  7. Is compensation in scope here or a later draft?
  8. How should composite receipts nest child receipts without becoming a new transparency protocol?
  9. What prior art (XA, saga, TCC, TN, workflow) already closes this if child handles are added as resources?
  10. Should optional children be forbidden in v1 so that notify-later is always a new act?
  11. Which venue: DISPATCH, then none until handle lands?
  12. If a saga profile already forbids success-without-child-receipts and silent reuse of a consumed handle, should this draft shrink to CAD fields only?
  13. Is authorize/capture on existing rails accepted as prepare/commit, or must the wire names change?

24. Potential IETF Discussion Venues

This document does not claim that any named group should adopt the work.

24.1. DISPATCH

Natural first stop: the work spans payments, HTTP APIs, and agents.

24.2. OAuth

Relevant only if child handles are token profiles. OAuth should not own actuation join.

24.3. WIMSE

Relevant if child actors are workloads on different hops. Workload identity is not composite join.

24.4. RATS

Relevant if a child CAD includes environment currentness. Attestation is not prepare.

24.5. HTTPAPI

Relevant to coordinator HTTP resources and problem+json mapping of EF-047 / COMPENSATING and EF-048 / HOLD.

24.6. SCITT

Relevant to logging composite receipts. Not the commit protocol.

24.7. SAAG

Useful for whether all-or-none across administrative domains is in scope for the IETF at all.

24.8. No Presumed Home

Individual -00. No IANA request in this revision beyond codes already reserved in [DAS-REG].

26. Security Considerations

Coordinator compromise is not child-sink compromise. A malicious or failed coordinator can withhold a decision, but it cannot make a child post without a recorded COMMIT, and it cannot hold reservations indefinitely while the log is reachable, because any prepared child can write ABORT into an empty log.

Child sinks MUST NOT release a PREPARED reservation on a timer when COMMIT may already be recorded. They MUST consult the log: COMMIT commits, ABORT releases, an empty log allows a child CAS of ABORT, and an unreachable log leads to HOLD.

The integrity and availability of the Composite Decision Log are part of the trusted computing base for the prevention claim. An attacker who can rewrite a recorded value, or who can make two readers observe different values, can split the composite. An attacker who can make the log unreachable can keep reservations in HOLD. Profiles SHOULD replicate the log with a consensus protocol and SHOULD authenticate writers.

Child CAS of ABORT means a compromised or misconfigured required child can force a composite to abort by writing ABORT early. That is a liveness attack, not a safety break: no child posts, and no split occurs. Profiles MAY restrict early child writes to children whose reservation timer has actually expired and SHOULD log the writer identity of every CAS.

Treating a coordinator signature as a settlement receipt recreates bearer authority at the wrong layer. Quorum join on financial or physical children is a safety bug, not an availability feature.

27. Privacy Considerations

A composite CAD lists every child destination. That list can reveal a payment, a patient-adjacent system, and a mail target together. Coordinators SHOULD store child digests and sink ids, and SHOULD omit raw arguments from composite receipts sent off-path.

28. IANA Considerations

This version does not request IANA action. It uses COMPOSITE, EF-047 PARTIAL_COMMIT, and the composite states proposed in [DAS-REG]. It also uses EF-048 DECISION_LOG_UNREACHABLE, the COMMIT_DECIDED composite state, the HOLD child decision, and the ef-composite-strict and ef-composite-mitigation profile identifiers, which are expected to be added to [DAS-REG] in its next revision.

29. Criticism, Corrections, and Review Invited

The most useful criticism is that XA plus per-resource handles already yields this join, or that mitigation sagas are the only honest Internet profile. Either result should shrink the draft. Named-product corrections are welcome. Silence is not endorsement.

30. Conclusion

Local handle correctness at each sink does not make a multi-sink intent final. C is effectuated only when every required child is effectuated under handles bound to C. If a required child cannot prepare, the honest claim is mitigation plus a later compensation act — not all-or-none prevention.

CHILD POST
      !=
COMPOSITE FINALITY

SAGA COMPENSATION
      !=
THE ORIGINAL ACT SUCCEEDED

PREPARE ALL REQUIRED
  or
DO NOT CLAIM PREVENTION

31. Normative References

[RFC2119]
Bradner, S., "Key words for use in RFCs to Indicate Requirement Levels", BCP 14, , <https://www.rfc-editor.org/rfc/rfc2119>.
[RFC8174]
Leiba, B., "Ambiguity of Uppercase vs Lowercase in RFC 2119 Key Words", BCP 14, , <https://www.rfc-editor.org/rfc/rfc8174>.

32. Informative References

[DAS-AGENTIC]
Das, S., "Tool Selection Is Not Execution: Finality for Agentic Tool Dispatch in High-Risk AI Systems", Work in Progress, Internet-Draft, draft-das-agentic-execution-finality-02, , <https://datatracker.ietf.org/doc/draft-das-agentic-execution-finality/>.
[DAS-HANDLE]
Das, S., "Possession Is Not Authority: Execution Handle, Sink Verification, Atomic Consumption, and Finality Receipt", Work in Progress, Internet-Draft, draft-das-execution-handle-01, , <https://datatracker.ietf.org/doc/draft-das-execution-handle/>.
[DAS-JURISDICTION]
Das, S., "Authorized Here, Not Authorized There: Jurisdiction-Bound Execution Finality for Cross-Border and Sovereign Systems", Work in Progress, Internet-Draft, draft-das-jurisdiction-bound-execution-finality-00, , <https://datatracker.ietf.org/doc/draft-das-jurisdiction-bound-execution-finality/>.
[DAS-PATH]
Das, S., "When the Gate Can Be Bypassed: Consequence-Path Completeness for Execution Finality", Work in Progress, Internet-Draft, draft-das-consequence-path-completeness-00, , <https://datatracker.ietf.org/doc/draft-das-consequence-path-completeness/>.
[DAS-PROTOCOL]
Das, S., "The Missing Protocol Layer for the Agentic Internet: Computation Is Not Authority", Work in Progress, Internet-Draft, draft-das-execution-finality-protocol-layer-01, , <https://datatracker.ietf.org/doc/draft-das-execution-finality-protocol-layer/>.
[DAS-PURPOSE]
Das, S., "Data-Purpose Laundering Prevention: Execution-Finality for Preventing Cross-Domain Data Reuse", Work in Progress, Internet-Draft, draft-das-purpose-execution-finality-03, , <https://datatracker.ietf.org/doc/draft-das-purpose-execution-finality/>.
[DAS-REG]
Das, S., "Illustrative Codes Are Not a Namespace: Registries for Execution-Finality Objects", Work in Progress, Internet-Draft, draft-das-ef-registries-01, , <https://datatracker.ietf.org/doc/draft-das-ef-registries/>.
[DAS-REVOCATION]
Das, S., "Revoked but Still Executable: Closing the Authorization-to-Effect Gap with Finality-Bound Revocation", Work in Progress, Internet-Draft, draft-das-finality-bound-revocation-00, , <https://datatracker.ietf.org/doc/draft-das-finality-bound-revocation/>.
[DAS-STATE]
Das, S., "When Valid Authorization Becomes Stale: State and Policy Continuity at the Execution-Finality Boundary", Work in Progress, Internet-Draft, draft-das-state-policy-continuity-finality-00, , <https://datatracker.ietf.org/doc/draft-das-state-policy-continuity-finality/>.

Author's Address

Sangam Das
Independent
Balasore
Odisha
India