Article ID: I001-RN-CCL Deck: Самая опасная кнопка в автономной системе — не Execute, а Approve, если runtime не может доказать, что именно это разрешение было выдано для именно этого действия, осталось актуальным к моменту исполнения и было потреблено ровно тем execution occurrence, который реально совершил side effect. By: RESONANCE Editorial Status: Published Last verified: 2026-08-15 Languages: RU Canonical identity: Issue 001 · The Age of Agents · Causal Consent / Authorization Occurrence / Consumption Boundary
Самая опасная кнопка — Approve
Представим простой интерфейс.
AI-агент хочет выполнить действие:
send_payment(amount=100, recipient=B)
Система показывает человеку запрос.
Человек нажимает:
APPROVE
На первый взгляд всё закончено.
Есть человек. Есть разрешение. Есть действие.
Но между нажатием кнопки и реальным side effect может пройти минута, час или сутки.
За это время:
- изменились аргументы;
- изменился recipient;
- изменилась policy;
- изменился account state;
- authority была revoked;
- операция была retried;
- старый worker восстановился после crash;
- появился новый execution attempt;
- первоначальное разрешение уже было использовано;
- тот же semantic decision был выпущен повторно как отдельное signed occurrence.
И вот здесь возникает вопрос, который обычная модель ALLOW | DENY почти не задаёт:
Что именно человек разрешил — смысл действия, конкретный authorization event или конкретное исполнение?
Это три разных объекта.
Если система их не различает, кнопка Approve может незаметно превратиться из разрешения на одно действие в переносимую фразу:
«Когда-то действие такого типа было разрешено».
Для автономной системы этого недостаточно.
Consent is not a timeless boolean. Consent has a causal lifetime.
Signal
В публичном треде crewAIInc/crewAI#4877 изначально обсуждался компактный provider-agnostic контракт для pre-tool-call authorization.
Базовая форма была простой:
before_tool_call
↓
ALLOW | DENY
Затем обсуждение упёрлось в human approval.
Если решение требует человека или внешнего policy engine, нельзя держать coroutine или процесс открытым часами только ради ожидания.
atomicdjt предложил более durable модель:
before_tool_call
-> ALLOW | DENY | DEFER(decision_id, continuation_descriptor)
DEFER
-> durable pending authorization
-> typed tool_deferred outcome
-> dependent work remains unsatisfied
external resolver
-> resolve(decision_id, allow|deny)
-> enqueue fresh continuation/replay
Это важный архитектурный сдвиг.
Human approval перестаёт быть паузой процесса и становится durable workflow fact.
Но тот же комментарий добавил более опасный failure mode:
approve action A
↓
world changes
↓
framework later executes materially different A'
↓
stale consent reused
Предлагаемая защита — bind pending decision к digest как минимум от:
tool_name
normalized_args
agent / crew identity
policy version
relevant state / version
и повторно проверять binding при resolve.
Почти одновременно babyblueviper1 проследил другой seam в реальном ledger implementation, обсуждаемом в том же треде: semantic decision_ref мог соответствовать нескольким различным signed decision events.
По его публичному отчёту, один semantic decision identifier не позволял независимо доказать, какое именно occurrence было тем authorization event, к которому относится outcome.
Отсюда появился второй принцип:
semantic decision identity
!=
signed decision occurrence
А существующий AG2 proposal ag2ai/ag2#3156 уже отделял:
logical_operation_id
!=
execution_id
и содержал revalidate_if: явные условия, изменение которых делает preflight verdict непригодным для исполнения даже до формального expiry.
Эти три линии сходятся в одну более общую модель.
The missing object: authorization consumption
Большинство authorization systems умеют ответить хотя бы на часть следующих вопросов:
1. Что было разрешено?
2. Кто разрешил?
3. Когда разрешил?
4. По какой policy?
Но для autonomous execution появляется пятый вопрос:
Какое конкретное execution occurrence потребило это разрешение?
Это не bookkeeping detail.
Это последняя causal edge между permission и side effect.
Рассмотрим:
decision_ref = D
D может означать:
ALLOW tool=X args=Y under policy=P
Но один и тот же semantic decision может быть выпущен дважды:
E1 -> decision_ref D
E2 -> decision_ref D
Одинаковый смысл.
Разные signed events.
Теперь появляется execution:
X7 -> side effect
Если audit record хранит только:
X7 cites D
независимый verifier не всегда может доказать:
E1 authorized X7
а не:
E2 authorized X7
или вообще:
D merely describes a compatible semantic decision
Поэтому одного semantic ref недостаточно.
Нужен отдельный causal edge:
authorization occurrence
↓
consumed by
↓
execution occurrence
Three identities that must not collapse
Минимальная модель должна различать три identity classes.
1. Semantic decision identity
Отвечает:
Что означает решение?
Например:
decision_ref = hash(
intent,
policy_version,
verdict,
bounded context
)
Это удобно для deduplication, recomputation и сравнения одинаковых logical decisions.
Но semantic identity не обязана быть уникальной во времени.
2. Authorization occurrence identity
Отвечает:
Какое конкретное решение было фактически выпущено?
Например:
decision_event_id
issued_at
issuer / signer
signature
precedence anchor
Два occurrence могут иметь один decision_ref.
E1.semantic_ref = D
E2.semantic_ref = D
E1 != E2
Это не collision в смысле ошибки hash function.
Это нормальное различие между:
same meaning
и:
same historical event
3. Execution occurrence identity
Отвечает:
Какое конкретное исполнение произошло?
logical_operation_id = L
execution_id = X1
Retry может породить:
logical_operation_id = L
execution_id = X2
То есть:
logical operation identity
!=
execution occurrence identity
И теперь полный invariant становится:
Semantic identity != authorization occurrence != execution consumption.
Approval is not execution authority forever
Самая простая implementation mistake выглядит так:
if approval.status == ALLOW:
execute()
Но ALLOW — это исторический факт.
Execution требует текущего admissibility proof.
Между ними существует время.
А значит, существует drift.
T0 proposal created
T1 authorization issued
T2 authorization deferred
T3 human resolves allow
T4 freshness revalidated
T5 tool executes
T6 side effect becomes observable
T7 outcome recorded
На любом участке может измениться мир.
Поэтому вопрос:
Was it approved?
слабее вопроса:
Was this exact execution still authorized at the moment its effect crossed the execution boundary?
The hidden TOCTOU seam
DEFER + resolve убирает необходимость держать long-lived suspension.
execution_scope_digest может защитить от изменения tool name / arguments / actor / policy / relevant state.
revalidate_if может потребовать повторную проверку freshness-sensitive predicates.
Но даже после этого остаётся классический TOCTOU window:
resolve approval
↓
verify digest
↓
revalidate state
↓
WORLD CHANGES
↓
execute tool
Это означает:
revalidation is not automatically equivalent to execution-time authorization.
Если revalidation и effect crossing — разные несвязанные операции, stale consent всё ещё может просочиться через последний промежуток.
Поэтому появляется отдельная boundary.
Authorization Consumption Boundary
Назовём её Authorization Consumption Boundary (ACB).
Это точка, в которой runtime должен fail-close подтвердить одновременно:
1. exact authorization occurrence is known
2. semantic decision still matches proposed action
3. execution scope digest still matches
4. freshness predicates still hold
5. authority/policy state is still admissible
6. authorization has not already been consumed/revoked/superseded
7. this execution occurrence is the one consuming it
Концептуально:
resolve approval
↓
resolve exact decision_event_id
↓
recompute execution_scope_digest
↓
revalidate freshness predicates
↓
consume authorization fail-closed
↓
execute tool
↓
record execution occurrence
В идеальной implementation consumption и начало irreversible effect находятся в одной transactional / compare-and-swap / capability-use boundary настолько близко, насколько позволяет конкретный runtime.
Это не означает, что все frameworks обязаны реализовать distributed transaction.
Это означает, что разрыв должен быть явным и проверяемым, а не скрытым за одним boolean allow=True.
Consent as a state machine
Approval удобнее моделировать не boolean, а state transition system.
Например:
PROPOSED
↓
DEFERRED
↓
RESOLVED_ALLOW
↓
REVALIDATED
↓
CONSUMED
Terminal / competing states:
DENIED
EXPIRED
STALE
REVOKED
SUPERSEDED
CANCELLED
CONSUMED
Ключевое свойство:
CONSUMED
не равно:
ALLOW forever
Для one-shot authorization повторное consumption должно fail-close.
E1 consumed by X1
retry X2 attempts to use E1
↓
BLOCKED
Если product действительно хочет reusable authorization, это должно быть отдельным явным contract:
usage_policy:
mode: reusable
max_uses: 5
scope: ...
expires_at: ...
А не случайным свойством отсутствия поля consumed_by.
Consent is a capability, not a memory
Полезная mental model:
approval record
не должен вести себя как passive memory.
Он ближе к bounded capability.
Capability описывает не просто прошлую мысль человека или policy engine.
Она отвечает:
who may do what
under which state
until when
how many times
for which execution scope
Тогда human approval становится не строкой:
"approved": true
а machine-checkable object.
Например:
decision_ref: sha256:...
decision_event_id: evt_...
status: resolved_allow
issuer_ref: human:42
policy_version: policy:v17
execution_scope_digest: sha256:...
logical_operation_id: op_...
revalidate_if:
- account_balance_changed
- recipient_binding_changed
- policy_version_changed
- authority_epoch_changed
usage_policy:
mode: one_shot
consumed_by_execution_id: null
После execution:
status: consumed
consumed_by_execution_id: exec_019
consumed_at: ...
Теперь audit trail может отвечать не только:
Был ли approve?
но и:
Какой approve был использован каким execution, при каких still-valid conditions?
DEFER becomes ordinary workflow state
Это особенно хорошо сочетается с идеей atomicdjt.
Вместо special-case pause:
agent coroutine waiting for human
получаем:
ToolNode
↓
DEFERRED authorization
↓
durable unsatisfied dependency
Зависимый branch:
requires(tool_result)
↓
NOT RUNNABLE
Независимый branch:
no dependency on deferred result
↓
CAN CONTINUE
После deny:
DENIED outcome
↓
dependency resolved explicitly as denied
После allow:
fresh continuation
↓
ACB verification
↓
execution or fail-close
Human approval перестаёт быть необычной магией внутри guardrail hook.
Он становится нормальным stateful edge workflow graph.
The stale-consent matrix
Чтобы контракт был полезным, его надо пытаться сломать.
Case 1 — arguments changed after approval
approved:
transfer(100, B)
execute:
transfer(1000, B)
Expected:
execution_scope_digest mismatch
→ BLOCKED
Case 2 — recipient changed
approved:
recipient=B
execute:
recipient=C
Expected:
BLOCKED
Case 3 — same semantic decision, different signed occurrence
E1 -> D
E2 -> D
outcome cites only D
Expected read-side behavior:
occurrence ambiguous
not:
pick first event silently
A consumable authorization should name the exact occurrence used.
Case 4 — policy changed after human approval
approved under policy v17
current policy v18
Expected depends on policy contract, but must be explicit:
revalidate / supersede / block
never accidental reuse.
Case 5 — authority revoked
human approved
agent authority epoch = 17
later:
authority epoch = 18
owner changed
Even unchanged arguments are insufficient.
From Article 08:
Correct knowledge does not imply current authority.
Here:
Valid historical consent does not imply current execution authority.
Case 6 — approval consumed once, retry occurs
E1 consumed by execution X1
X1 acknowledgement lost
runtime retries as X2
The worst implementation does:
approval == allow
→ execute again
A safer implementation asks independently:
Did X1 already cross the side-effect boundary?
Was E1 already consumed?
Is retry authorized as a new execution occurrence?
This connects directly to the broader invariant:
commit != acknowledgement != retry permission
Case 7 — cancellation after approval, before execution
approval resolved
↓
run cancelled
↓
old continuation wakes up
Article 09 established cancellation as a state transition rather than mere transport disappearance.
Therefore cancel may supersede pending execution authority.
Recovery must not silently resurrect the old approval path.
A minimal framework-neutral contract
The goal is not to prescribe CrewAI internals.
A compact neutral model could look like:
@dataclass
class AuthorizationOccurrence:
decision_ref: str
decision_event_id: str
status: Literal[
"deferred",
"resolved_allow",
"denied",
"expired",
"stale",
"revoked",
"superseded",
"consumed",
]
issuer_ref: str
policy_version: str
logical_operation_id: str
execution_scope_digest: str
revalidate_if: list[str]
usage_mode: Literal["one_shot", "reusable"] = "one_shot"
max_uses: int | None = 1
consumed_by_execution_id: str | None = None
Execution proposal:
@dataclass
class ProposedExecution:
logical_operation_id: str
execution_id: str
tool_name: str
normalized_args: dict
actor_ref: str
authority_ref: str | None
relevant_state_refs: list[str]
Then the boundary can be expressed as:
scope(execution) == authorization.execution_scope_digest
AND
freshness predicates hold
AND
authorization occurrence is current
AND
usage policy permits consumption
AND
execution has not been cancelled/superseded
Only after that:
consume -> execute
Why decision_id belongs even on synchronous allow/deny
The deferred case makes this obvious, but the principle generalizes.
A synchronous deny should be a fact.
A synchronous allow should be a fact.
A defer should be a fact.
no execution happened
не означает автоматически:
deny occurred
Точно так же:
execution happened
не должно означать автоматически:
some valid authorization must have existed
Поэтому GuardrailDecision выигрывает от stable:
decision_id
status
policy / provenance metadata
даже когда всё произошло за миллисекунды.
Это делает authorization inspectable after the fact и не привязывает framework к конкретному policy engine или approval UI.
The PLF-shaped shift: from permission to provenance
В начале кажется, что задача — просто добавить human approval.
Потом кажется, что задача — научиться ждать его без busy-wait.
Потом — bind arguments digest.
Но настоящая смена модели происходит позже.
Проблема не в том, чтобы получить разрешение.
Проблема в том, чтобы сохранить причинную цепочку от разрешения до effect:
intent
↓
semantic decision
↓
authorization occurrence
↓
deferred/resolved state
↓
revalidation
↓
authorization consumption
↓
execution occurrence
↓
observed outcome
Именно тогда approval перестаёт быть UI feature.
Он становится частью verifiable execution protocol.
Relationship to the previous RESONANCE articles
Article 07 сформулировал:
recovered state
!=
recovered responsibility topology
Article 08 добавил:
correct knowledge
!=
current authority
Article 09 добавил:
older durable state
!=
automatic resume authority
Article 10 добавляет:
historically valid consent
!=
current execution authorization
Вместе:
recover state
↓
recover responsibility boundaries
↓
recover current authority
↓
resolve exact authorization occurrence
↓
revalidate execution scope + freshness
↓
consume authorization
↓
execute
↓
observe outcome independently
Это уже можно рассматривать как causal permission graph.
Causal Permission Graph
Обычный audit log часто выглядит как список:
12:01 approval
12:04 execution
12:05 success
Но timestamp proximity не доказывает causality.
Более сильная модель:
Intent(I7)
↓ evaluated as
DecisionSemantic(D3)
↓ issued as
DecisionEvent(E11)
↓ resolves
DeferredAuthorization(A8)
↓ revalidated against
State(S19) + AuthorityEpoch(22)
↓ consumed by
Execution(X41)
↓ produced
Outcome(O9)
↓ observed by
Observer(V4)
Каждая edge отвечает на отдельный вопрос.
Если две identity случайно объединить, audit становится короче — и слабее.
What should fail closed
Минимальный contract должен блокировать execution, если:
decision_event_idотсутствует там, где semantic ref ambiguous;- semantic decision не соответствует current execution scope;
- normalized args изменились;
- actor / crew / agent identity изменились вне разрешённой policy;
- policy version требует revalidation;
- relevant state version изменилась;
- authority была revoked / transferred / superseded;
- authorization expired;
- one-shot authorization уже consumed;
- cancellation superseded continuation;
- required provenance cannot be recovered;
- resolver говорит
allow, но runtime не может доказать, что это тот pending decision, который относится к текущему execution.
Fail-close здесь не означает «всегда навсегда deny».
Он означает:
cannot prove current authorization
↓
do not silently execute
Runtime может:
re-request approval
revalidate
create new decision occurrence
repair provenance
surface ambiguity
Но не должен подменять отсутствие proof предположением.
What this does not require
Этот model не требует:
- держать process suspended до human response;
- встроить конкретный policy engine в CrewAI;
- использовать blockchain;
- подписывать каждую локальную allow decision;
- делать distributed transaction вокруг каждого tool call;
- превращать every approval в one-shot независимо от product semantics;
- запрещать retries;
- запрещать reusable delegated capabilities.
Он требует только одного:
Если authorization используется для оправдания consequential execution, связь между конкретным authorization occurrence и конкретным execution occurrence должна оставаться проверяемой.
Smallest falsifiable conformance test
1. Freeze action A with normalized args and relevant state refs.
2. Produce semantic decision D.
3. Issue concrete authorization occurrence E1 for D.
4. Defer it.
5. Resolve E1 = allow.
6. Revalidate all declared freshness conditions.
7. Consume E1 with execution X1.
8. Record outcome O1.
9. Attempt to reuse E1 with execution X2.
10. Mutate one bound field and attempt X3.
11. Issue E2 with the same semantic D and verify E1/E2 remain distinguishable.
PASS if:
X1 admissible
X2 blocked for one-shot reuse
X3 blocked for scope mismatch
E1 and E2 independently addressable
O1 can prove which authorization occurrence X1 consumed
FAIL if:
semantic D alone is treated as sufficient occurrence identity
or
approval remains replayable merely because decision == allow
or
freshness is checked but the consumed authorization is not bound to X1
The larger consequence
Human-in-the-loop is often described as a safety mechanism.
Но human approval без causal binding может создать ложное чувство безопасности.
Человек действительно нажал Approve.
Лог действительно содержит ALLOW.
Tool действительно был вызван.
И всё это может быть правдой одновременно — при том, что система не умеет доказать, что разрешение относилось именно к этому execution occurrence в момент side effect.
Поэтому следующий уровень agent governance — не больше approval dialogs.
Он выглядит так:
less implicit permission
more causal permission
Core invariants
Consent has a causal lifetime.
Semantic identity != authorization occurrence != execution consumption.
Valid historical consent does not imply current execution authority.
An approval should authorize a bounded execution occurrence under bound conditions, not become an indefinitely replayable statement that an action was once approved.
И наконец:
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.
What comes next
Следующий естественный шаг — executable conformance layer, который проверяет связку:
DEFER
→ exact authorization occurrence
→ scope digest
→ freshness revalidation
→ one-shot consumption
→ execution occurrence
→ outcome provenance
Если такая цепочка становится portable между frameworks, human approval перестаёт быть framework-specific UI mechanic.
Он становится interoperable evidence primitive для autonomous systems.
И тогда вопрос будущей agent infrastructure меняется ещё раз.
Не:
Кто нажал Approve?
А:
Можем ли мы независимо доказать, какое разрешение пересекло execution boundary вместе с этим действием?
Именно с этого момента consent становится не обещанием безопасности, а проверяемой частью системы.
Исходник и доказательства