mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-10-26 13:16:24 +00:00
* 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>
55 lines
1.9 KiB
Python
55 lines
1.9 KiB
Python
"""ActivationDecisions — the typed async decision carrier (construction hardening).
|
|
|
|
The carrier's contract (see ``deerflow/authz/activation_decisions.py``):
|
|
a covered name returns the batched decision; a MISS resolves per the
|
|
provider-error policy with a loud WARNING and never touches a provider —
|
|
``None`` remains the sync-chain signal where the synchronous authorize() is
|
|
the correct API.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
|
|
from deerflow.authz.activation_decisions import ActivationDecisions
|
|
|
|
|
|
def test_covered_name_returns_batched_decision():
|
|
decisions = ActivationDecisions({"alpha": True, "beta": False}, fail_closed=True)
|
|
|
|
assert decisions.decision_for("alpha") is True
|
|
assert decisions.decision_for("beta") is False
|
|
|
|
|
|
def test_miss_fails_closed_with_loud_log(caplog):
|
|
decisions = ActivationDecisions({}, fail_closed=True)
|
|
|
|
with caplog.at_level(logging.WARNING, logger="deerflow.authz.activation_decisions"):
|
|
assert decisions.decision_for("uncovered") is False
|
|
|
|
assert any("uncovered" in record.message and "denied" in record.message for record in caplog.records)
|
|
|
|
|
|
def test_miss_fails_open_with_loud_log(caplog):
|
|
decisions = ActivationDecisions({}, fail_closed=False)
|
|
|
|
with caplog.at_level(logging.WARNING, logger="deerflow.authz.activation_decisions"):
|
|
assert decisions.decision_for("uncovered") is True
|
|
|
|
assert any("uncovered" in record.message and "allowed" in record.message for record in caplog.records)
|
|
|
|
|
|
def test_get_or_none_distinguishes_miss_from_false():
|
|
decisions = ActivationDecisions({"alpha": False}, fail_closed=True)
|
|
|
|
assert decisions.get_or_none("alpha") is False
|
|
assert decisions.get_or_none("uncovered") is None
|
|
|
|
|
|
def test_container_protocol_and_len():
|
|
decisions = ActivationDecisions({"alpha": True}, fail_closed=True)
|
|
|
|
assert "alpha" in decisions
|
|
assert "uncovered" not in decisions
|
|
assert len(decisions) == 1
|