hataa 3e5bd081db
feat(authz): construction-enforced async decision batches (ActivationDecisions) (#6508)
* feat(authz): construction-enforced async decision batches (ActivationDecisions)

The follow-up promised in the #4541 review threads. Four review rounds
traced four instances of one failure family: an async execution path
whose decision-map miss silently fell back to the synchronous
authorize() from a worker thread — the wrong API for loop-affine
providers, and under fail_closed:false a provider error there flips a
denial into an allow. The carrier was a plain dict|None held together by
caller discipline.

ActivationDecisions (deerflow/authz/activation_decisions.py) makes the
contract construction-enforced:
- a covered name returns the batched decision (unchanged semantics);
- a MISS resolves per the configured provider-error policy (fail-closed
  denies, fail-open allows) with a loud WARNING — a miss is a
  prepass/consumer divergence, a construction bug, not a policy
  question — and never touches a provider;
- None remains the sync-chain signal (synchronous authorize() is correct
  there);
- an empty candidate set still yields an empty BATCH, not None, so an
  unexpected consumer name fails per policy instead of silently taking
  the sync path;
- get_or_none lets publishers distinguish 'uncovered' from False;
- fail_closed travels with the batch from the resolved authorization
  context.

Both middlewares (_activation_allowed in SkillActivationMiddleware and
SkillToolPolicyMiddleware) migrate to the type; their collectors return
batches; the render-decision publisher adapts. Tests 65->70+5: type unit
tests (covered/miss both directions with loud-log assertions/
get_or_none/container), middleware-level miss semantics (fail-open
allows with provider.sync_calls == [], production-default fail-closed
denies, None sync chain still synchronous), and mutation M18 (restoring
the miss's sync fallback) verified red. 252 tests green on the affected
surface.

* docs(authz): sync every decision-batch contract description to the new miss semantics

Review round 1 on #6508 — all documentation-level: the docstrings still
described the miss-as-synchronous-fallback contract this PR removes.

- _activation_allowed (both middlewares): a covered name returns the
  batched decision; a miss resolves per the carried provider-error policy
  with a WARNING and never touches a provider; None (sync chain /
  disabled) is the only synchronous case.
- _collect_activation_decisions / _resolve_policy_registry /
  _active_skills_for_paths: the reason not to diverge is now a
  policy-resolved miss (denied under the production fail-closed default;
  allowed without any provider consult under fail-open; logged loudly),
  not a silent sync fallback.
- The two _REGISTRY_LOAD_FAILED sentinel comments, same rewording.
- Drop unused tmp_path/monkeypatch fixtures from the two new miss tests.

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-09 16:09:17 +08:00
..