Chio/Docs
LOGIN · JOIN

Shared resource ownership

Make the resource that owns an effect refuse a write from a worker whose assignment has moved to another.

Concept chio@5f63e6e

On this page

When a worker loses its assignment and then rereads the newest version, the shared resource still refuses its write. The resource compares the kernel-selected caller with its current owner inside the same transaction that changes it.

A mailbox lease orders deliveries, and a document version stops an overwrite based on an old read. Neither stops a superseded worker that rereads the latest version and proposes a fresh write. The running example is one report document, two workers A and B, and the downloadable resource project.

A version check alone lets a superseded worker write again

Two workers race for one document. The comparison sets an owner-bound transaction beside a version-only one.

Fresh data does not restore ownership
Fresh data does not restore ownershipTwo independent scenarios start at owner B, generation 2, document version 2. Caller A proposes a new operation expecting version 2. Version-only checking permits the write. Owner-bound checking refuses A before the version check and leaves B's report unchanged.Same starting snapshotOwner B · generation 2Document version 2Version-only transactionCounterfactual: no owner checkNew operation · caller AExpected 2 · document 2Owner: not checkedNo ownership gateVersion: 2 = 2Version check passesCommit A · version 3A’s value retainedResult: committedOwner-bound transactionCurrent exampleNew operation · caller AExpected 2 · document 2Owner: A ≠ BSTOP · supersededVersion: not reachedFresh data; write refusedNo mutation · version 2B’s report remainsResult: supersededFresh data does not restore ownershipTwo independent scenarios start at owner B, generation 2, document version 2. Caller A proposes a new operation expecting version 2. Version-only checking permits the write. Owner-bound checking refuses A before the version check and leaves B's report unchanged.Same starting snapshotOwner B · generation 2Document version 2Version-only transactionCounterfactual: no owner checkNew operation · caller AExpected 2 · document 2Owner: not checkedNo ownership gateVersion: 2 = 2Version check passesCommit A · version 3A’s value retainedResult: committedOwner-bound transactionCurrent exampleNew operation · caller AExpected 2 · document 2Owner: A ≠ BSTOP · supersededVersion: not reachedFresh data; write refusedNo mutation · version 2B’s report remainsResult: superseded

Owner-bound result: superseded. No mutation; the document remains at version 2.

A reread can give a superseded worker the latest document version. The resource still refuses a new mutation when the authenticated caller differs from its current owner.
Source
  1. Assign A. The document is at generation 1 with owner A, and A begins a report.
  2. Replace A with B. Generation 2 names owner B while A is paused.
  3. B commits a correction. Document version 2 holds the correct result.
  4. A resumes and rereads version 2. The owner is still B, yet A passes the version compare-and-swap and overwrites B unless the resource checks ownership.

Cancelling A stops its new Chio admissions, and an effect the kernel already admitted can still run. The resource’s assignment check closes that mutation race.

The kernel supplies the caller, so a payload never chooses the owner

On the native MCP path, the kernel-selected invocation context carries the caller’s capability digest in trusted transport metadata. The resource server reads that binding from its host-owned connection, apart from the tool’s JSON arguments, so a field named owner in a model-generated payload has no say.

IdentitySelected byWhat it controls
Logical operation IDThe application and kernel bindingDeduplication of this exact request and its known result
Caller capability digestThe kernel-selected authenticated contextWho is asking the resource to mutate
Assignment generation and ownerThe resource’s authorized assignment transitionWhich caller may make a new mutation now

An operation key answers whether this exact effect was already attempted. An assignment answers whether this caller may mutate the resource now, and a mailbox claim says who may complete a message under its current lease. Replacing a worker leaves its old receipt in place, and reading the current version gives a superseded worker no current authority.

In a local test, a preconfigured connection can select the caller, which exercises the resource contract alone. A public server authenticates its transport and prevents clients from forging the forwarded metadata.

An exact replay returns its retained result and nothing more

The store retains each request and its known result under an operation ID, in the resource’s own database and inside the mutation transaction.

Replay boundary
same request: replaynew operationmutatechanged request, same identity: conflictroperation + requestslookupρoriginal outcomeoownervversionfresourcereplaynew operationconflictroperation + requestslookupρoriginal outcomeoownervversionfresource mutated

An old result is not permission for a new write

The store looks up the operation identity with its exact request. An identical retained request returns its original outcome, even when ownership has moved since. Anything new takes the mutation path, which checks the current owner and then the expected version before it writes.

An exact replay returns its retained outcome before the current-owner check. A fresh mutation takes the separate ownership and version path; changed arguments under an existing operation identity conflict.

Drawn fromresource/store.py

BEGIN IMMEDIATE serializes SQLite writers before the store reads the operation or the assignment. The operation binding covers the trusted caller, the tool name, and the exact arguments. A previous matching request returns its original result, and a changed request under that operation ID fails with an identity conflict. A superseded refusal is itself a retained known result, so returning the old owner later replays the refusal, never a fresh write.

The complete execute function in store.py
store.py · complete execute functionpython
def execute(path, operation_id, name, args, caller=None, *, operator=False):
    identifier(operation_id)
    validate(name, args, operator=operator)
    if operator:
        caller_identity(caller)
    binding = {"name": name, "arguments": args}
    if caller is not None:
        binding["caller_capability_sha256"] = caller_identity(caller)
    request = encoded(binding)
    with closing(connect(path)) as db, db:
        if db.execute("PRAGMA user_version").fetchone()[0] != SCHEMA:
            raise ValueError("unsupported or incomplete resource state")
        db.execute("BEGIN IMMEDIATE")
        previous = db.execute(
            "SELECT request, result FROM operations WHERE id=?",
            (operation_id,),
        ).fetchone()
        if previous:
            if previous[0] != request:
                raise ValueError("operation identity conflict")
            db.execute(
                "UPDATE operations SET deliveries=deliveries+1 WHERE id=?",
                (operation_id,),
            )
            return json.loads(previous[1])
        result = transition(db, operation_id, name, args, caller)
        db.execute(
            "INSERT INTO operations VALUES(?, ?, ?, 1)",
            (operation_id, request, encoded(result)),
        )
        return result

The owner check and the version check share one write transaction

For a new request, transition checks the current assignment, then the expected version, then mutates the document and records the result before the surrounding transaction commits.

store.py · mutation branch within the same transactionpython
    row = db.execute(
        "SELECT version, body FROM documents WHERE id=?", (args["document"],)
    ).fetchone()
    if row is None:
        return {"status": "not_found"}
    version, body = row
    if name == "snapshot":
        return {"status": "snapshot", "version": version, "value": json.loads(body)}
    owner = db.execute(
        "SELECT generation, owner FROM assignments WHERE document=? ORDER BY generation DESC LIMIT 1",
        (args["document"],),
    ).fetchone()
    if owner:
        if caller is None:
            raise ValueError("owned resource requires an authenticated caller binding")
        if caller != owner[1]:
            return {"status": "superseded", "assignment_generation": owner[0]}
    if args["expected_version"] != version:
        return {"status": "version_conflict", "version": version}
    db.execute(
        "UPDATE documents SET version=version+1, body=? WHERE id=?",
        (encoded(args["value"]), args["document"]),
    )
    db.execute(
        "INSERT INTO mutations VALUES(?, ?, ?)",
        (version + 1, args["document"], operation_id),
    )
    return {"status": "committed", "version": version + 1}

Publish assignments through the same serialization boundary as mutations. An owner check moved outside the transaction reopens the race, even when the final UPDATE compares a document version. The downloaded store permits unassigned documents as an explicit application choice, and the report exercise assigns the document before any worker writes; a service that requires ownership for every mutation refuses unassigned documents too.

Ownership holds where the resource participates. Chio delivers the caller binding; the service that owns the effect makes the comparison.

PostgreSQL stores fence the same race with their own transition functions

The PostgreSQL job store applies the principle through tenant-scoped transition functions. renew_job_lease passes tenant, job, lease worker, lease fence, and duration. complete_job validates the canonical result bytes and their SHA-256, then calls chio_finding_market_complete_job inside the tenant transaction.

Existing PostgreSQL transition · call shape, application-bound parameterssql
-- Execute in the tenant transaction established by the store.
SELECT chio_finding_market_complete_job(
  $1, -- authenticated tenant
  $2, -- job ID
  $3, -- retained lease worker ID
  $4, -- retained lease fence
  $5, -- SHA-256 of canonical result bytes
  $6  -- canonical result bytes
);
-- 0 inserted; 1 exact replay; 2 conflict; 3 not found; 4 lease lost.

The function locks the job row FOR UPDATE and checks state, owner, fence, and database-clock expiry before changing it, returning distinct inserted, exact-replay, conflict, not-found, and lease-lost outcomes. The store’s tenant context, role privileges, and trusted lease object belong to the contract, so keep the call behind them and away from any SQL endpoint where a model could choose the tenant, worker, or fence.

To port the report resource to PostgreSQL, lock its assignment and document in one transaction, keep request and result deduplication, and serialize reassignment through the same locked row. A document-version compare-and-swap stays useful as an additional check.

The stale-writer exercise checks the database and the kernel separately

Download the complete resource store and ownership exercise, extract it, and change into resource/. The exercise.py script calls store.py directly against temporary SQLite databases, so it needs Python 3.11 or newer and no process host.

Inside the extracted resource directorybash
python3 exercise.py

The deterministic exercise creates one report, commits an original write, and transfers the assignment. The superseded caller then rereads the newest version and attempts a fresh write, and the test compares documents and mutation history before and after the refusal. Each scenario asserts the database and the kernel independently:

  • Superseded worker reads the latest version. The fresh write returns superseded, with the document and mutation count unchanged. Verify the refusal result and the original caller binding.
  • Original write replays after reassignment. The store returns the same retained result with no new mutation, and the known operation returns its original receipt.
  • A changed caller reuses an operation ID. The store reports an operation identity conflict, and worker arguments have no way to select another process.
  • The host dies after the resource commits. The outcome lookup finds one mutation, and the original call stays known or unknown according to the retained kernel evidence.

That last case has two commits. The resource can commit its operation record before the kernel retains the reply, and its outcome lookup then shows an operator what the database did. The kernel receipt is a separate record: the lookup cannot stand in for a missing receipt or make a second Chio dispatch safe. Keep the original process operation unknown when its recovery policy requires, retain the resource observation beside it, and decide what application work remains.

A replacement worker continues under the original operation key; moving the assignment is no reason to replay the old task under a new one. Verify the original signed receipts separately, including any unknown operation after host death, as Dispatch commitments describes for admission evidence.

Next: build the same resource in Rust