From e4547a8b28bc7f0a287d93d6833ed422a68c2c97 Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Sat, 15 Aug 2026 11:50:48 +0700 Subject: [PATCH 01/11] spec: add ACB-001 authorization consumption boundary --- ...-001-AUTHORIZATION-CONSUMPTION-BOUNDARY.md | 239 ++++++++++++++++++ 1 file changed, 239 insertions(+) create mode 100644 standards/agent-continuity/authorization-consumption/ACB-001-AUTHORIZATION-CONSUMPTION-BOUNDARY.md diff --git a/standards/agent-continuity/authorization-consumption/ACB-001-AUTHORIZATION-CONSUMPTION-BOUNDARY.md b/standards/agent-continuity/authorization-consumption/ACB-001-AUTHORIZATION-CONSUMPTION-BOUNDARY.md new file mode 100644 index 0000000..b86cc9d --- /dev/null +++ b/standards/agent-continuity/authorization-consumption/ACB-001-AUTHORIZATION-CONSUMPTION-BOUNDARY.md @@ -0,0 +1,239 @@ +# ACB-001 — Authorization Consumption Boundary + +**Version:** 0.1 +**Status:** Draft / executable reference contract +**Scope:** Framework-neutral authorization-to-execution binding + +## Abstract + +A historical `ALLOW` is not by itself proof that a later execution is currently authorized. + +ACB-001 separates: + +```text +semantic decision identity + != +authorization occurrence identity + != +execution occurrence identity +``` + +and defines a fail-closed boundary where a concrete authorization occurrence is checked and consumed by a concrete execution occurrence. + +Core invariant: + +> A consequential execution may cross the Authorization Consumption Boundary only when the exact authorization occurrence is recoverable, its execution scope still matches, all declared freshness conditions still hold, its authority/policy state remains admissible, and its usage policy permits this execution to consume it. + +## Objects + +### AuthorizationOccurrence + +An authorization occurrence is a concrete issued decision event. It carries both the semantic `decision_ref` and the occurrence-specific `decision_event_id`. + +Required fields include: + +- `decision_ref` +- `decision_event_id` +- `status` +- `issuer_ref` +- `policy_version` +- `logical_operation_id` +- `execution_scope_digest` +- `revalidate_if` +- `bound_conditions` +- `usage_policy` +- `use_count` +- `consumed_by_execution_ids` + +A semantic decision may have multiple distinct occurrences. Therefore `decision_ref` alone is insufficient when more than one occurrence exists. + +### ProposedExecution + +A proposed execution is a concrete execution attempt. + +The scope digest is recomputed from a fixed preimage: + +```text +logical_operation_id +tool_name +normalized_args +actor_ref +policy_version +authority_ref +authority_epoch +relevant_state_refs +``` + +`execution_id` is deliberately excluded from the scope digest. Retries may have a new execution occurrence under the same frozen logical operation, while authorization reuse remains governed separately by usage policy. + +### ConsumptionReceipt + +A receipt binds the exact `decision_event_id` to the exact `execution_id` and records either: + +```text +CONSUMED +``` + +or: + +```text +BLOCKED +``` + +with an explicit reason. + +## Boundary algorithm + +A conforming reference decision should fail closed unless all required checks succeed: + +```text +resolve exact authorization occurrence + ↓ +check semantic decision_ref + ↓ +status == resolved_allow + ↓ +logical operation matches + ↓ +recompute execution_scope_digest + ↓ +compare declared freshness conditions + ↓ +check cancellation / supersession + ↓ +check one-shot / reusable usage policy + ↓ +consume authorization + ↓ +emit receipt binding decision_event_id -> execution_id +``` + +## Occurrence resolution + +If an exact `decision_event_id` is supplied, resolution MUST verify that it belongs to the supplied `decision_ref`. + +If no occurrence id is supplied: + +- one matching occurrence may be resolved; +- multiple matching occurrences MUST return `OCCURRENCE_AMBIGUOUS`; +- zero matching occurrences MUST return `OCCURRENCE_NOT_FOUND`. + +Implementations MUST NOT silently choose the first occurrence in storage order. + +## Freshness + +`revalidate_if` names conditions whose values must still equal the values recorded in `bound_conditions`. + +Missing current values fail closed. + +Examples: + +```text +policy_generation +authority_epoch +recipient_binding +account_snapshot +deployment_revision +``` + +This contract does not prescribe which conditions a product must bind. It prescribes how declared conditions are checked. + +## Usage policy + +### One-shot + +For: + +```json +{"mode":"one_shot","max_uses":1} +``` + +the first successful consumption transitions the occurrence to `consumed`. + +Any later execution attempt using the same occurrence MUST be blocked. + +### Reusable + +Reusable authorization is explicit, never inferred from missing consumption state. + +A reusable occurrence may remain `resolved_allow` until `max_uses` is reached. When the limit is reached it transitions to `consumed`. + +## Cancellation and supersession + +A proposed execution carrying: + +```text +cancelled = true +``` + +or: + +```text +superseded = true +``` + +MUST NOT consume authorization. + +Likewise an authorization whose status is `cancelled`, `superseded`, `revoked`, `expired`, `stale`, `denied`, or `consumed` is not currently consumable. + +## Composition with authority causality + +ACB-001 does not replace ACI-001. + +ACI asks: + +> Does this actor currently have authority over the resource/effect? + +ACB asks: + +> Which exact authorization occurrence permits this exact execution to cross the side-effect boundary? + +A system may require both: + +```text +ACI(current authority) == ADMISSIBLE +AND +ACB(current consent) == CONSUMED +``` + +before a consequential effect is released. + +## Required falsification cases + +The executable suite covers at least: + +1. exact scope match succeeds; +2. normalized arguments change after approval; +3. actor changes after approval; +4. authority epoch changes after approval; +5. declared freshness condition changes; +6. declared freshness condition is missing; +7. one-shot authorization is replayed by a retry; +8. reusable authorization respects its explicit use limit; +9. cancellation blocks consumption; +10. supersession blocks consumption; +11. two occurrences share one semantic `decision_ref` and remain independently addressable; +12. semantic-only lookup becomes ambiguous when multiple occurrences exist; +13. wrong `decision_event_id` / `decision_ref` pairing fails closed; +14. an already non-active authorization cannot be consumed. + +## Non-goals + +ACB-001 does not claim: + +- transactional atomicity with arbitrary external systems; +- distributed consensus; +- Byzantine fault tolerance; +- that every approval must be one-shot; +- that a digest alone eliminates all TOCTOU races; +- vendor-native adoption by CrewAI, AG2, or another framework. + +The narrow guarantee is inspectability and fail-closed reference behavior at the authorization-to-execution seam. + +## Operational principle + +> Valid historical consent does not imply current execution authority. + +and: + +> A system should be able to prove not only why an action was allowed, but which exact permission was consumed when the action became real. From 77fe335947c2672d1b8b9cf323f86065464f9617 Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Sat, 15 Aug 2026 11:50:55 +0700 Subject: [PATCH 02/11] schema: add ACB authorization occurrence --- .../authorization-occurrence.schema.json | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 standards/agent-continuity/authorization-consumption/schema/authorization-occurrence.schema.json diff --git a/standards/agent-continuity/authorization-consumption/schema/authorization-occurrence.schema.json b/standards/agent-continuity/authorization-consumption/schema/authorization-occurrence.schema.json new file mode 100644 index 0000000..58ffda0 --- /dev/null +++ b/standards/agent-continuity/authorization-consumption/schema/authorization-occurrence.schema.json @@ -0,0 +1,45 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://safal207.github.io/pythiaLabs/schemas/acb-authorization-occurrence-0.1.json", + "title": "ACB-001 AuthorizationOccurrence", + "type": "object", + "additionalProperties": false, + "required": [ + "schema_version", + "decision_ref", + "decision_event_id", + "status", + "issuer_ref", + "policy_version", + "logical_operation_id", + "execution_scope_digest", + "revalidate_if", + "bound_conditions", + "usage_policy", + "use_count", + "consumed_by_execution_ids" + ], + "properties": { + "schema_version": {"const": "acb-authorization/0.1"}, + "decision_ref": {"type": "string", "minLength": 1}, + "decision_event_id": {"type": "string", "minLength": 1}, + "status": {"enum": ["deferred", "resolved_allow", "denied", "expired", "stale", "revoked", "superseded", "cancelled", "consumed"]}, + "issuer_ref": {"type": "string", "minLength": 1}, + "policy_version": {"type": "string", "minLength": 1}, + "logical_operation_id": {"type": "string", "minLength": 1}, + "execution_scope_digest": {"type": "string", "pattern": "^sha256:[0-9a-f]{64}$"}, + "revalidate_if": {"type": "array", "items": {"type": "string", "minLength": 1}, "uniqueItems": true}, + "bound_conditions": {"type": "object", "additionalProperties": {"type": ["string", "number", "integer", "boolean", "null"]}}, + "usage_policy": { + "type": "object", + "additionalProperties": false, + "required": ["mode", "max_uses"], + "properties": { + "mode": {"enum": ["one_shot", "reusable"]}, + "max_uses": {"type": ["integer", "null"], "minimum": 1} + } + }, + "use_count": {"type": "integer", "minimum": 0}, + "consumed_by_execution_ids": {"type": "array", "items": {"type": "string", "minLength": 1}, "uniqueItems": true} + } +} From ec43db01b1aabb24f3af66450f89c11ffab459d3 Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Sat, 15 Aug 2026 11:51:02 +0700 Subject: [PATCH 03/11] schema: add ACB proposed execution --- .../schema/proposed-execution.schema.json | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 standards/agent-continuity/authorization-consumption/schema/proposed-execution.schema.json diff --git a/standards/agent-continuity/authorization-consumption/schema/proposed-execution.schema.json b/standards/agent-continuity/authorization-consumption/schema/proposed-execution.schema.json new file mode 100644 index 0000000..f9fd264 --- /dev/null +++ b/standards/agent-continuity/authorization-consumption/schema/proposed-execution.schema.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://safal207.github.io/pythiaLabs/schemas/acb-proposed-execution-0.1.json", + "title": "ACB-001 ProposedExecution", + "type": "object", + "additionalProperties": false, + "required": ["schema_version", "logical_operation_id", "execution_id", "tool_name", "normalized_args", "actor_ref", "policy_version", "authority_ref", "authority_epoch", "relevant_state_refs", "current_conditions", "cancelled", "superseded"], + "properties": { + "schema_version": {"const": "acb-execution/0.1"}, + "logical_operation_id": {"type": "string", "minLength": 1}, + "execution_id": {"type": "string", "minLength": 1}, + "tool_name": {"type": "string", "minLength": 1}, + "normalized_args": {"type": "object"}, + "actor_ref": {"type": "string", "minLength": 1}, + "policy_version": {"type": "string", "minLength": 1}, + "authority_ref": {"type": ["string", "null"]}, + "authority_epoch": {"type": ["integer", "null"], "minimum": 0}, + "relevant_state_refs": {"type": "array", "items": {"type": "string", "minLength": 1}, "uniqueItems": true}, + "current_conditions": {"type": "object", "additionalProperties": {"type": ["string", "number", "integer", "boolean", "null"]}}, + "cancelled": {"type": "boolean"}, + "superseded": {"type": "boolean"} + } +} From b88940f5de897d7162795d7b2aa963fe08ec582b Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Sat, 15 Aug 2026 11:51:08 +0700 Subject: [PATCH 04/11] schema: add ACB consumption receipt --- .../schema/consumption-receipt.schema.json | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 standards/agent-continuity/authorization-consumption/schema/consumption-receipt.schema.json diff --git a/standards/agent-continuity/authorization-consumption/schema/consumption-receipt.schema.json b/standards/agent-continuity/authorization-consumption/schema/consumption-receipt.schema.json new file mode 100644 index 0000000..bc2ad21 --- /dev/null +++ b/standards/agent-continuity/authorization-consumption/schema/consumption-receipt.schema.json @@ -0,0 +1,19 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://safal207.github.io/pythiaLabs/schemas/acb-consumption-receipt-0.1.json", + "title": "ACB-001 ConsumptionReceipt", + "type": "object", + "additionalProperties": false, + "required": ["schema_version", "result", "reason", "decision_ref", "decision_event_id", "logical_operation_id", "execution_id", "execution_scope_digest", "consumption_ref"], + "properties": { + "schema_version": {"const": "acb-receipt/0.1"}, + "result": {"enum": ["CONSUMED", "BLOCKED"]}, + "reason": {"type": "string", "minLength": 1}, + "decision_ref": {"type": ["string", "null"]}, + "decision_event_id": {"type": ["string", "null"]}, + "logical_operation_id": {"type": "string", "minLength": 1}, + "execution_id": {"type": "string", "minLength": 1}, + "execution_scope_digest": {"type": "string", "pattern": "^sha256:[0-9a-f]{64}$"}, + "consumption_ref": {"type": "string", "pattern": "^sha256:[0-9a-f]{64}$"} + } +} From 161b6da2e7495b56acc1d1d719e4ee285dc2a042 Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Sat, 15 Aug 2026 11:51:27 +0700 Subject: [PATCH 05/11] test: add ACB reference verifier --- .../conformance/acb_reference.py | 200 ++++++++++++++++++ 1 file changed, 200 insertions(+) create mode 100644 standards/agent-continuity/authorization-consumption/conformance/acb_reference.py diff --git a/standards/agent-continuity/authorization-consumption/conformance/acb_reference.py b/standards/agent-continuity/authorization-consumption/conformance/acb_reference.py new file mode 100644 index 0000000..a673b7d --- /dev/null +++ b/standards/agent-continuity/authorization-consumption/conformance/acb_reference.py @@ -0,0 +1,200 @@ +from __future__ import annotations + +import copy +import hashlib +import json +from pathlib import Path +from typing import Any, Mapping, Sequence + +from jsonschema import Draft202012Validator + +HERE = Path(__file__).resolve().parent +ROOT = HERE.parent +AUTH_SCHEMA = ROOT / "schema" / "authorization-occurrence.schema.json" +EXEC_SCHEMA = ROOT / "schema" / "proposed-execution.schema.json" +RECEIPT_SCHEMA = ROOT / "schema" / "consumption-receipt.schema.json" + + +def load_json(path: Path) -> dict[str, Any]: + value = json.loads(path.read_text(encoding="utf-8")) + if not isinstance(value, dict): + raise ValueError("document root must be object") + return value + + +def schema_errors(document: Mapping[str, Any], schema_path: Path) -> list[str]: + schema = load_json(schema_path) + Draft202012Validator.check_schema(schema) + validator = Draft202012Validator(schema) + return [error.message for error in validator.iter_errors(dict(document))] + + +def _canonical_bytes(value: Any) -> bytes: + return json.dumps( + value, + sort_keys=True, + separators=(",", ":"), + ensure_ascii=False, + allow_nan=False, + ).encode("utf-8") + + +def _sha256_ref(value: Any) -> str: + return "sha256:" + hashlib.sha256(_canonical_bytes(value)).hexdigest() + + +def execution_scope_preimage(execution: Mapping[str, Any]) -> dict[str, Any]: + return { + "logical_operation_id": execution["logical_operation_id"], + "tool_name": execution["tool_name"], + "normalized_args": execution["normalized_args"], + "actor_ref": execution["actor_ref"], + "policy_version": execution["policy_version"], + "authority_ref": execution["authority_ref"], + "authority_epoch": execution["authority_epoch"], + "relevant_state_refs": sorted(execution["relevant_state_refs"]), + } + + +def compute_execution_scope_digest(execution: Mapping[str, Any]) -> str: + return _sha256_ref(execution_scope_preimage(execution)) + + +def resolve_occurrence( + decision_ref: str, + decision_event_id: str | None, + occurrences: Sequence[Mapping[str, Any]], +) -> tuple[str, Mapping[str, Any] | None]: + candidates = [ + item for item in occurrences + if item.get("decision_ref") == decision_ref + ] + + if decision_event_id is None: + if not candidates: + return "OCCURRENCE_NOT_FOUND", None + if len(candidates) > 1: + return "OCCURRENCE_AMBIGUOUS", None + return "RESOLVED", candidates[0] + + exact = [ + item for item in occurrences + if item.get("decision_event_id") == decision_event_id + ] + if not exact: + return "OCCURRENCE_NOT_FOUND", None + if len(exact) > 1: + return "OCCURRENCE_AMBIGUOUS", None + + occurrence = exact[0] + if occurrence.get("decision_ref") != decision_ref: + return "OCCURRENCE_REF_MISMATCH", None + return "RESOLVED", occurrence + + +def _receipt( + result: str, + reason: str, + execution: Mapping[str, Any], + authorization: Mapping[str, Any] | None, +) -> dict[str, Any]: + decision_ref = authorization.get("decision_ref") if authorization else None + decision_event_id = authorization.get("decision_event_id") if authorization else None + scope_digest = compute_execution_scope_digest(execution) + base = { + "schema_version": "acb-receipt/0.1", + "result": result, + "reason": reason, + "decision_ref": decision_ref, + "decision_event_id": decision_event_id, + "logical_operation_id": execution["logical_operation_id"], + "execution_id": execution["execution_id"], + "execution_scope_digest": scope_digest, + } + base["consumption_ref"] = _sha256_ref(base) + return base + + +def attempt_consume( + authorization: Mapping[str, Any], + execution: Mapping[str, Any], +) -> tuple[str, dict[str, Any], dict[str, Any]]: + updated = copy.deepcopy(dict(authorization)) + + if schema_errors(authorization, AUTH_SCHEMA): + receipt = _receipt("BLOCKED", "authorization_schema_invalid", execution, authorization) + return "BLOCKED", updated, receipt + if schema_errors(execution, EXEC_SCHEMA): + receipt = _receipt("BLOCKED", "execution_schema_invalid", execution, authorization) + return "BLOCKED", updated, receipt + + if authorization["status"] != "resolved_allow": + receipt = _receipt("BLOCKED", f"authorization_status_{authorization['status']}", execution, authorization) + return "BLOCKED", updated, receipt + + if execution["cancelled"]: + receipt = _receipt("BLOCKED", "execution_cancelled", execution, authorization) + return "BLOCKED", updated, receipt + if execution["superseded"]: + receipt = _receipt("BLOCKED", "execution_superseded", execution, authorization) + return "BLOCKED", updated, receipt + + if authorization["logical_operation_id"] != execution["logical_operation_id"]: + receipt = _receipt("BLOCKED", "logical_operation_mismatch", execution, authorization) + return "BLOCKED", updated, receipt + + if authorization["policy_version"] != execution["policy_version"]: + receipt = _receipt("BLOCKED", "policy_version_mismatch", execution, authorization) + return "BLOCKED", updated, receipt + + actual_scope = compute_execution_scope_digest(execution) + if authorization["execution_scope_digest"] != actual_scope: + receipt = _receipt("BLOCKED", "execution_scope_mismatch", execution, authorization) + return "BLOCKED", updated, receipt + + bound_conditions = authorization["bound_conditions"] + current_conditions = execution["current_conditions"] + for condition in authorization["revalidate_if"]: + if condition not in bound_conditions: + receipt = _receipt("BLOCKED", f"bound_condition_missing:{condition}", execution, authorization) + return "BLOCKED", updated, receipt + if condition not in current_conditions: + receipt = _receipt("BLOCKED", f"current_condition_missing:{condition}", execution, authorization) + return "BLOCKED", updated, receipt + if current_conditions[condition] != bound_conditions[condition]: + receipt = _receipt("BLOCKED", f"freshness_changed:{condition}", execution, authorization) + return "BLOCKED", updated, receipt + + usage = authorization["usage_policy"] + mode = usage["mode"] + max_uses = usage["max_uses"] + use_count = authorization["use_count"] + + if execution["execution_id"] in authorization["consumed_by_execution_ids"]: + receipt = _receipt("BLOCKED", "execution_already_consumed", execution, authorization) + return "BLOCKED", updated, receipt + + if mode == "one_shot": + if max_uses != 1 or use_count != 0 or authorization["consumed_by_execution_ids"]: + receipt = _receipt("BLOCKED", "one_shot_already_consumed", execution, authorization) + return "BLOCKED", updated, receipt + elif mode == "reusable": + if max_uses is not None and use_count >= max_uses: + receipt = _receipt("BLOCKED", "reusable_limit_exhausted", execution, authorization) + return "BLOCKED", updated, receipt + else: + receipt = _receipt("BLOCKED", "usage_mode_invalid", execution, authorization) + return "BLOCKED", updated, receipt + + updated["use_count"] += 1 + updated["consumed_by_execution_ids"].append(execution["execution_id"]) + + if mode == "one_shot" or ( + max_uses is not None and updated["use_count"] >= max_uses + ): + updated["status"] = "consumed" + + receipt = _receipt("CONSUMED", "authorization_consumed", execution, updated) + if schema_errors(receipt, RECEIPT_SCHEMA): + raise AssertionError("reference implementation produced invalid receipt") + return "CONSUMED", updated, receipt From bfb3a3f78ff4c9c22acd1787cafe834a73a4c2ca Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Sat, 15 Aug 2026 11:51:30 +0700 Subject: [PATCH 06/11] build: add ACB conformance dependency --- .../authorization-consumption/conformance/requirements.txt | 1 + 1 file changed, 1 insertion(+) create mode 100644 standards/agent-continuity/authorization-consumption/conformance/requirements.txt diff --git a/standards/agent-continuity/authorization-consumption/conformance/requirements.txt b/standards/agent-continuity/authorization-consumption/conformance/requirements.txt new file mode 100644 index 0000000..d1cfec3 --- /dev/null +++ b/standards/agent-continuity/authorization-consumption/conformance/requirements.txt @@ -0,0 +1 @@ +jsonschema>=4.23,<5 From d6914259dd269e87fd7b95b7c06d068409dcfdfe Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Sat, 15 Aug 2026 11:51:44 +0700 Subject: [PATCH 07/11] test: add accepted ACB fixture --- .../fixtures/accepted-consumption.json | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 standards/agent-continuity/authorization-consumption/fixtures/accepted-consumption.json diff --git a/standards/agent-continuity/authorization-consumption/fixtures/accepted-consumption.json b/standards/agent-continuity/authorization-consumption/fixtures/accepted-consumption.json new file mode 100644 index 0000000..6d06833 --- /dev/null +++ b/standards/agent-continuity/authorization-consumption/fixtures/accepted-consumption.json @@ -0,0 +1,40 @@ +{ + "authorization": { + "schema_version": "acb-authorization/0.1", + "decision_ref": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "decision_event_id": "event:E1", + "status": "resolved_allow", + "issuer_ref": "human:42", + "policy_version": "policy:v17", + "logical_operation_id": "op:L1", + "execution_scope_digest": "sha256:03048ca7902a12aadefe701b4ba1d3a1678f4ff3da4614156ea03f3479573181", + "revalidate_if": ["authority_epoch", "recipient_binding", "account_snapshot"], + "bound_conditions": { + "authority_epoch": 17, + "recipient_binding": "wallet:B", + "account_snapshot": "acct:v7" + }, + "usage_policy": {"mode": "one_shot", "max_uses": 1}, + "use_count": 0, + "consumed_by_execution_ids": [] + }, + "execution": { + "schema_version": "acb-execution/0.1", + "logical_operation_id": "op:L1", + "execution_id": "exec:X1", + "tool_name": "send_payment", + "normalized_args": {"amount": 100, "currency": "USDC", "recipient": "wallet:B"}, + "actor_ref": "agent:A", + "policy_version": "policy:v17", + "authority_ref": "authority:wallet:primary", + "authority_epoch": 17, + "relevant_state_refs": ["state:wallet:v41", "state:policy:v17"], + "current_conditions": { + "authority_epoch": 17, + "recipient_binding": "wallet:B", + "account_snapshot": "acct:v7" + }, + "cancelled": false, + "superseded": false + } +} From 4af2a35f8f237332d000efe3c615cf4fc74d4395 Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Sat, 15 Aug 2026 11:51:51 +0700 Subject: [PATCH 08/11] test: add rejected ACB fixtures --- .../fixtures/rejected-consumption-cases.json | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 standards/agent-continuity/authorization-consumption/fixtures/rejected-consumption-cases.json diff --git a/standards/agent-continuity/authorization-consumption/fixtures/rejected-consumption-cases.json b/standards/agent-continuity/authorization-consumption/fixtures/rejected-consumption-cases.json new file mode 100644 index 0000000..969a305 --- /dev/null +++ b/standards/agent-continuity/authorization-consumption/fixtures/rejected-consumption-cases.json @@ -0,0 +1,10 @@ +{ + "cases": [ + {"id": "args-mutated", "mutation": "normalized_args.amount=1000", "expected": "BLOCKED:execution_scope_mismatch"}, + {"id": "authority-epoch-changed", "mutation": "authority_epoch=18", "expected": "BLOCKED:execution_scope_mismatch"}, + {"id": "freshness-changed", "mutation": "current_conditions.account_snapshot=acct:v8", "expected": "BLOCKED:freshness_changed:account_snapshot"}, + {"id": "cancelled", "mutation": "cancelled=true", "expected": "BLOCKED:execution_cancelled"}, + {"id": "one-shot-reuse", "mutation": "retry after first consumption", "expected": "BLOCKED:authorization_status_consumed"}, + {"id": "semantic-collision", "mutation": "two event ids share decision_ref", "expected": "OCCURRENCE_AMBIGUOUS without exact event id"} + ] +} From b020f466fafa763dece0492e304227754fd46d49 Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Sat, 15 Aug 2026 11:52:10 +0700 Subject: [PATCH 09/11] test: add ACB conformance suite --- .../conformance/test_acb_conformance.py | 178 ++++++++++++++++++ 1 file changed, 178 insertions(+) create mode 100644 standards/agent-continuity/authorization-consumption/conformance/test_acb_conformance.py diff --git a/standards/agent-continuity/authorization-consumption/conformance/test_acb_conformance.py b/standards/agent-continuity/authorization-consumption/conformance/test_acb_conformance.py new file mode 100644 index 0000000..619e00e --- /dev/null +++ b/standards/agent-continuity/authorization-consumption/conformance/test_acb_conformance.py @@ -0,0 +1,178 @@ +from __future__ import annotations + +import copy +import json +import unittest +from pathlib import Path + +from acb_reference import ( + attempt_consume, + compute_execution_scope_digest, + resolve_occurrence, + schema_errors, + AUTH_SCHEMA, + EXEC_SCHEMA, +) + +HERE = Path(__file__).resolve().parent +ROOT = HERE.parent +FIXTURES = ROOT / "fixtures" + + +def load_fixture(name: str) -> dict: + return json.loads((FIXTURES / name).read_text(encoding="utf-8")) + + +class ACBConformanceTests(unittest.TestCase): + def setUp(self) -> None: + accepted = load_fixture("accepted-consumption.json") + self.authorization = accepted["authorization"] + self.execution = accepted["execution"] + + def test_01_schemas_accept_reference_fixture(self) -> None: + self.assertEqual([], schema_errors(self.authorization, AUTH_SCHEMA)) + self.assertEqual([], schema_errors(self.execution, EXEC_SCHEMA)) + + def test_02_exact_scope_match_consumes(self) -> None: + result, updated, receipt = attempt_consume(self.authorization, self.execution) + self.assertEqual("CONSUMED", result) + self.assertEqual("consumed", updated["status"]) + self.assertEqual(1, updated["use_count"]) + self.assertEqual([self.execution["execution_id"]], updated["consumed_by_execution_ids"]) + self.assertEqual(self.authorization["decision_event_id"], receipt["decision_event_id"]) + self.assertEqual(self.execution["execution_id"], receipt["execution_id"]) + + def test_03_normalized_args_change_blocks(self) -> None: + execution = copy.deepcopy(self.execution) + execution["normalized_args"]["amount"] = 1000 + result, _, receipt = attempt_consume(self.authorization, execution) + self.assertEqual("BLOCKED", result) + self.assertEqual("execution_scope_mismatch", receipt["reason"]) + + def test_04_actor_change_blocks(self) -> None: + execution = copy.deepcopy(self.execution) + execution["actor_ref"] = "agent:B" + result, _, receipt = attempt_consume(self.authorization, execution) + self.assertEqual("BLOCKED", result) + self.assertEqual("execution_scope_mismatch", receipt["reason"]) + + def test_05_authority_epoch_change_blocks(self) -> None: + execution = copy.deepcopy(self.execution) + execution["authority_epoch"] = 18 + execution["current_conditions"]["authority_epoch"] = 18 + result, _, receipt = attempt_consume(self.authorization, execution) + self.assertEqual("BLOCKED", result) + self.assertEqual("execution_scope_mismatch", receipt["reason"]) + + def test_06_freshness_change_blocks(self) -> None: + execution = copy.deepcopy(self.execution) + execution["current_conditions"]["account_snapshot"] = "acct:v8" + result, _, receipt = attempt_consume(self.authorization, execution) + self.assertEqual("BLOCKED", result) + self.assertEqual("freshness_changed:account_snapshot", receipt["reason"]) + + def test_07_missing_current_condition_blocks(self) -> None: + execution = copy.deepcopy(self.execution) + del execution["current_conditions"]["recipient_binding"] + result, _, receipt = attempt_consume(self.authorization, execution) + self.assertEqual("BLOCKED", result) + self.assertEqual("current_condition_missing:recipient_binding", receipt["reason"]) + + def test_08_one_shot_retry_is_blocked(self) -> None: + first_result, consumed, _ = attempt_consume(self.authorization, self.execution) + self.assertEqual("CONSUMED", first_result) + retry = copy.deepcopy(self.execution) + retry["execution_id"] = "exec:X2" + second_result, _, receipt = attempt_consume(consumed, retry) + self.assertEqual("BLOCKED", second_result) + self.assertEqual("authorization_status_consumed", receipt["reason"]) + + def test_09_reusable_authorization_respects_use_limit(self) -> None: + authorization = copy.deepcopy(self.authorization) + authorization["usage_policy"] = {"mode": "reusable", "max_uses": 2} + x1 = copy.deepcopy(self.execution) + x2 = copy.deepcopy(self.execution) + x2["execution_id"] = "exec:X2" + x3 = copy.deepcopy(self.execution) + x3["execution_id"] = "exec:X3" + result1, a1, _ = attempt_consume(authorization, x1) + result2, a2, _ = attempt_consume(a1, x2) + result3, _, receipt3 = attempt_consume(a2, x3) + self.assertEqual("CONSUMED", result1) + self.assertEqual("CONSUMED", result2) + self.assertEqual("consumed", a2["status"]) + self.assertEqual(2, a2["use_count"]) + self.assertEqual("BLOCKED", result3) + self.assertEqual("authorization_status_consumed", receipt3["reason"]) + + def test_10_cancelled_execution_blocks(self) -> None: + execution = copy.deepcopy(self.execution) + execution["cancelled"] = True + result, _, receipt = attempt_consume(self.authorization, execution) + self.assertEqual("BLOCKED", result) + self.assertEqual("execution_cancelled", receipt["reason"]) + + def test_11_superseded_execution_blocks(self) -> None: + execution = copy.deepcopy(self.execution) + execution["superseded"] = True + result, _, receipt = attempt_consume(self.authorization, execution) + self.assertEqual("BLOCKED", result) + self.assertEqual("execution_superseded", receipt["reason"]) + + def test_12_same_semantic_decision_distinct_occurrences(self) -> None: + e1 = copy.deepcopy(self.authorization) + e2 = copy.deepcopy(self.authorization) + e2["decision_event_id"] = "event:E2" + status1, found1 = resolve_occurrence(e1["decision_ref"], "event:E1", [e1, e2]) + status2, found2 = resolve_occurrence(e1["decision_ref"], "event:E2", [e1, e2]) + self.assertEqual("RESOLVED", status1) + self.assertEqual("RESOLVED", status2) + self.assertEqual("event:E1", found1["decision_event_id"]) + self.assertEqual("event:E2", found2["decision_event_id"]) + + def test_13_semantic_only_resolution_is_ambiguous_on_collision(self) -> None: + e1 = copy.deepcopy(self.authorization) + e2 = copy.deepcopy(self.authorization) + e2["decision_event_id"] = "event:E2" + status, found = resolve_occurrence(e1["decision_ref"], None, [e1, e2]) + self.assertEqual("OCCURRENCE_AMBIGUOUS", status) + self.assertIsNone(found) + + def test_14_wrong_event_ref_pairing_fails_closed(self) -> None: + e1 = copy.deepcopy(self.authorization) + e2 = copy.deepcopy(self.authorization) + e2["decision_ref"] = "sha256:" + "b" * 64 + e2["decision_event_id"] = "event:E2" + status, found = resolve_occurrence(e1["decision_ref"], "event:E2", [e1, e2]) + self.assertEqual("OCCURRENCE_REF_MISMATCH", status) + self.assertIsNone(found) + + def test_15_non_active_authorization_blocks(self) -> None: + for status in ["deferred", "denied", "expired", "stale", "revoked", "superseded", "cancelled", "consumed"]: + with self.subTest(status=status): + authorization = copy.deepcopy(self.authorization) + authorization["status"] = status + result, _, receipt = attempt_consume(authorization, self.execution) + self.assertEqual("BLOCKED", result) + self.assertEqual(f"authorization_status_{status}", receipt["reason"]) + + def test_16_policy_version_change_blocks_explicitly(self) -> None: + execution = copy.deepcopy(self.execution) + execution["policy_version"] = "policy:v18" + result, _, receipt = attempt_consume(self.authorization, execution) + self.assertEqual("BLOCKED", result) + self.assertEqual("policy_version_mismatch", receipt["reason"]) + + def test_17_execution_id_is_not_in_scope_digest(self) -> None: + retry = copy.deepcopy(self.execution) + retry["execution_id"] = "exec:X2" + self.assertEqual(compute_execution_scope_digest(self.execution), compute_execution_scope_digest(retry)) + + def test_18_unique_semantic_ref_can_resolve_without_event_id(self) -> None: + status, found = resolve_occurrence(self.authorization["decision_ref"], None, [self.authorization]) + self.assertEqual("RESOLVED", status) + self.assertEqual("event:E1", found["decision_event_id"]) + + +if __name__ == "__main__": + unittest.main() From e6ae8384ea040b0ade0a80d4335636b9a1f9985b Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Sat, 15 Aug 2026 11:52:15 +0700 Subject: [PATCH 10/11] ci: add ACB conformance workflow --- .github/workflows/acb-conformance.yml | 41 +++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 .github/workflows/acb-conformance.yml diff --git a/.github/workflows/acb-conformance.yml b/.github/workflows/acb-conformance.yml new file mode 100644 index 0000000..7a70b4e --- /dev/null +++ b/.github/workflows/acb-conformance.yml @@ -0,0 +1,41 @@ +name: ACB conformance + +on: + pull_request: + paths: + - "standards/agent-continuity/authorization-consumption/**" + - ".github/workflows/acb-conformance.yml" + push: + paths: + - "standards/agent-continuity/authorization-consumption/**" + - ".github/workflows/acb-conformance.yml" + workflow_dispatch: + +permissions: + contents: read + +jobs: + conformance: + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@v4 + with: + persist-credentials: false + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install conformance dependency + run: | + python -m pip install --disable-pip-version-check \ + -r standards/agent-continuity/authorization-consumption/conformance/requirements.txt + + - name: Run ACB conformance suite + run: | + python -m unittest discover \ + -s standards/agent-continuity/authorization-consumption/conformance \ + -p 'test_*.py' \ + -v From c989776bc366673a2b7fddf57c653fe3b914db41 Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Sat, 15 Aug 2026 11:52:29 +0700 Subject: [PATCH 11/11] docs: index ACB-001 beside ACI-001 --- standards/agent-continuity/README.md | 61 ++++++++++++++++++++++++---- 1 file changed, 54 insertions(+), 7 deletions(-) diff --git a/standards/agent-continuity/README.md b/standards/agent-continuity/README.md index 96c5101..7ded5e2 100644 --- a/standards/agent-continuity/README.md +++ b/standards/agent-continuity/README.md @@ -1,6 +1,6 @@ # Agent Continuity & Authority -Vendor-neutral specifications and executable conformance checks for preserving operational continuity across context compaction, session restart, and cross-session handoff. +Vendor-neutral specifications and executable conformance checks for preserving operational continuity across context compaction, session restart, cross-session handoff, authority change, and delayed authorization. ## RFC v0.1 @@ -10,24 +10,55 @@ defines a bounded, structured envelope that carries the active operational tail A separate restore-results document records which required reads and evidence checks were actually completed. The envelope declares the gate; restore results satisfy it. +## Executable sibling contracts + +### ACI-001 — Authority Causality Invariant + +[`authority-causality/`](./authority-causality/) + +makes authority itself causal and versioned. It separates state freshness from current mutation authority and mechanically rejects stale/split authority. + +### ACB-001 — Authorization Consumption Boundary + +[`authorization-consumption/`](./authorization-consumption/) + +separates semantic decision identity, exact authorization occurrence, and concrete execution occurrence. It fails closed when consent is stale, ambiguous, cancelled, superseded, scope-mismatched, or already consumed. + +The two contracts answer different questions: + +```text +ACI: may this actor act now? +ACB: which exact permission may this exact execution consume now? +``` + +For consequential effects a runtime may require both. + ## Problem -Coding agents may lose task continuity after compaction or handoff. They can repeat completed work, violate recent constraints, forget rejected approaches, or confidently reconstruct an execution history that is not supported by durable evidence. +Coding agents may lose task continuity after compaction or handoff. They can repeat completed work, violate recent constraints, forget rejected approaches, or confidently reconstruct an execution history that is not supported by durable evidence. Long-lived or deferred approvals add another failure mode: a historically valid `ALLOW` can be replayed after arguments, authority, policy, state, cancellation status, or execution occurrence has changed. ## Core boundary > Continuity evidence may help an agent resume work, but it must not silently become authority or proof that an action occurred. +And for authorization: + +> Valid historical consent does not imply current execution authority. + ## Package - RFC specification; - JSON Schemas for the envelope and restore results; - example envelope and restore results; - reference validator; -- executable conformance tests. +- executable conformance tests; +- ACI authority-causality contract; +- ACB authorization-consumption contract. ## Quick validation +Base continuation suite: + ```bash python -m pip install -r standards/agent-continuity/conformance/requirements.txt @@ -36,19 +67,35 @@ python -m unittest discover \ -p 'test_*.py' -v ``` -## What the suite verifies +ACB-001: + +```bash +python -m pip install -r standards/agent-continuity/authorization-consumption/conformance/requirements.txt + +python -m unittest discover \ + -s standards/agent-continuity/authorization-consumption/conformance \ + -p 'test_*.py' -v +``` + +## What the suites verify - published JSON Schema enforcement; -- canonical envelope digest integrity; +- canonical digest integrity; - trusted-source authority boundaries; - independent digest/receipt evidence checks; - required-read completion; - fail-closed restore behavior; -- unresolved task verification remaining unresolved. +- unresolved task verification remaining unresolved; +- exact authorization occurrence resolution; +- execution-scope binding; +- freshness revalidation; +- one-shot and explicit reusable consumption semantics; +- cancellation/supersession blocking; +- semantic-decision collision ambiguity rather than silent first-match resolution. ## Intended integrations -The specification is implementation-neutral. Codex, Claude Code, IDE agents, CLI agents, and multi-agent runtimes may store or transport the envelope differently while preserving the same observable guarantees. +The specifications are implementation-neutral. Codex, Claude Code, CrewAI, AG2, IDE agents, CLI agents, and multi-agent runtimes may store or transport these records differently while preserving the same observable guarantees. ## Status