refactor(agents): unify permissions into one vertical-slice package

Per-file verification of the slice-3 candidates showed receipts/ and
date_filters.py are shared contracts (consumed by shared/state + shared
middleware + subagents), so they correctly stay put.

permissions was the real misfit: the rule *model* lived at shared/permissions.py
while its enforcement lived at shared/middleware/permissions/. Unify them into a
single self-contained subsystem:

  shared/permissions.py                 -> shared/permissions/model.py
  shared/middleware/permissions/{deny,ask,middleware}
                                        -> shared/permissions/{deny,ask,middleware}

The package __init__ re-exports the model API + build_permission_mw, so the 32
external model consumers keep importing `from ...shared.permissions import Rule`
unchanged; only the 8 internal files redirect to `.model` (cycle-safe, model
loaded before middleware).
This commit is contained in:
CREDO23 2026-06-05 13:29:48 +02:00
parent f2a61bc0ef
commit 84b775c0ac
27 changed files with 75 additions and 42 deletions

View file

@ -35,13 +35,13 @@ from app.agents.chat.multi_agent_chat.shared.middleware.memory import build_memo
from app.agents.chat.multi_agent_chat.shared.middleware.patch_tool_calls import (
build_patch_tool_calls_mw,
)
from app.agents.chat.multi_agent_chat.shared.middleware.permissions import (
build_permission_mw,
)
from app.agents.chat.multi_agent_chat.shared.middleware.resilience import (
build_resilience_middlewares,
)
from app.agents.chat.multi_agent_chat.shared.middleware.todos import build_todos_mw
from app.agents.chat.multi_agent_chat.shared.permissions import (
build_permission_mw,
)
from app.agents.chat.multi_agent_chat.subagents import (
build_subagents,
get_subagents_to_exclude,

View file

@ -1,11 +0,0 @@
"""Pattern-based allow/deny/ask middleware with HITL fallback (vertical slice).
Public surface (one entry point only every other symbol is an internal of
the rule engine and stays inside ``middleware/``, ``ask/``, or ``deny.py``):
- :func:`build_permission_mw` construction recipe shared by every stack.
"""
from .middleware.factory import build_permission_mw
__all__ = ["build_permission_mw"]

View file

@ -0,0 +1,41 @@
"""Permissions vertical slice: rule model + allow/deny/ask enforcement.
Self-contained subsystem combining the permission rule engine (:mod:`.model`)
with the pattern-based allow/deny/ask middleware and its HITL fallback
(:mod:`.middleware`, :mod:`.ask`, :mod:`.deny`).
Public surface:
- rule model: ``Rule``, ``Ruleset``, ``RuleAction`` and the ``evaluate`` /
``evaluate_many`` / ``aggregate_action`` / ``wildcard_match`` helpers.
- middleware: ``build_permission_mw`` the construction recipe shared by
every agent stack.
"""
# isort: off
# Import order matters: the rule model must be bound on this package before the
# middleware loads, because the middleware transitively imports consumers (e.g.
# app.services.user_tool_allowlist) that re-import ``Rule``/``Ruleset`` from this
# package root. Loading ``.model`` first avoids a partially-initialized cycle.
from .model import (
Rule,
RuleAction,
Ruleset,
aggregate_action,
evaluate,
evaluate_many,
wildcard_match,
)
from .middleware.factory import build_permission_mw
# isort: on
__all__ = [
"Rule",
"RuleAction",
"Ruleset",
"aggregate_action",
"build_permission_mw",
"evaluate",
"evaluate_many",
"wildcard_match",
]

View file

@ -6,7 +6,7 @@ from typing import Any
from langchain_core.tools import BaseTool
from app.agents.chat.multi_agent_chat.shared.permissions import Rule
from app.agents.chat.multi_agent_chat.shared.permissions.model import Rule
from app.agents.chat.multi_agent_chat.subagents.shared.hitl.wire import (
LC_DECISION_APPROVE,
LC_DECISION_EDIT,

View file

@ -16,7 +16,7 @@ from typing import Any
from langchain_core.tools import BaseTool
from langgraph.types import interrupt
from app.agents.chat.multi_agent_chat.shared.permissions import Rule
from app.agents.chat.multi_agent_chat.shared.permissions.model import Rule
from app.observability import metrics as ot_metrics, otel as ot
from .decision import normalize_permission_decision

View file

@ -11,7 +11,7 @@ from typing import Any
from langchain_core.messages import ToolMessage
from app.agents.chat.multi_agent_chat.shared.permissions import Rule
from app.agents.chat.multi_agent_chat.shared.permissions.model import Rule
from app.agents.chat.runtime.errors import StreamingError

View file

@ -26,7 +26,7 @@ from langchain_core.messages import AIMessage, ToolMessage
from langchain_core.tools import BaseTool
from langgraph.runtime import Runtime
from app.agents.chat.multi_agent_chat.shared.permissions import Ruleset
from app.agents.chat.multi_agent_chat.shared.permissions.model import Ruleset
from app.agents.chat.runtime.errors import CorrectedError, RejectedError
from app.services.user_tool_allowlist import TrustedToolSaver

View file

@ -16,7 +16,7 @@ from __future__ import annotations
import logging
from typing import Any
from app.agents.chat.multi_agent_chat.shared.permissions import (
from app.agents.chat.multi_agent_chat.shared.permissions.model import (
Rule,
RuleAction,
Ruleset,

View file

@ -28,7 +28,7 @@ from collections.abc import Sequence
from langchain_core.tools import BaseTool
from app.agents.chat.multi_agent_chat.shared.feature_flags import AgentFeatureFlags
from app.agents.chat.multi_agent_chat.shared.permissions import Rule, Ruleset
from app.agents.chat.multi_agent_chat.shared.permissions.model import Rule, Ruleset
from app.services.user_tool_allowlist import TrustedToolSaver
from .core import PermissionMiddleware

View file

@ -9,7 +9,7 @@ newly-promoted rules apply to subsequent calls.
from __future__ import annotations
from app.agents.chat.multi_agent_chat.shared.permissions import (
from app.agents.chat.multi_agent_chat.shared.permissions.model import (
Ruleset,
aggregate_action,
evaluate_many,

View file

@ -7,7 +7,7 @@ is the streaming layer's job — this module keeps the in-memory copy only.
from __future__ import annotations
from app.agents.chat.multi_agent_chat.shared.permissions import Rule, Ruleset
from app.agents.chat.multi_agent_chat.shared.permissions.model import Rule, Ruleset
def persist_always(

View file

@ -27,10 +27,10 @@ from app.agents.chat.multi_agent_chat.shared.middleware.kb_context_projection im
from app.agents.chat.multi_agent_chat.shared.middleware.patch_tool_calls import (
build_patch_tool_calls_mw,
)
from app.agents.chat.multi_agent_chat.shared.middleware.permissions import (
from app.agents.chat.multi_agent_chat.shared.permissions import (
Ruleset,
build_permission_mw,
)
from app.agents.chat.multi_agent_chat.shared.permissions import Ruleset
def _kb_user_allowlist(

View file

@ -15,13 +15,13 @@ from __future__ import annotations
from typing import Any
from app.agents.chat.multi_agent_chat.shared.feature_flags import AgentFeatureFlags
from app.agents.chat.multi_agent_chat.shared.middleware.permissions import (
build_permission_mw,
)
from app.agents.chat.multi_agent_chat.shared.middleware.resilience import (
ResilienceMiddlewares,
)
from app.agents.chat.multi_agent_chat.shared.middleware.todos import build_todos_mw
from app.agents.chat.multi_agent_chat.shared.permissions import (
build_permission_mw,
)
def build_subagent_middleware_stack(

View file

@ -11,10 +11,10 @@ from deepagents.middleware.patch_tool_calls import PatchToolCallsMiddleware
from langchain_core.language_models import BaseChatModel
from langchain_core.tools import BaseTool
from app.agents.chat.multi_agent_chat.shared.middleware.permissions import (
from app.agents.chat.multi_agent_chat.shared.permissions import (
Ruleset,
build_permission_mw,
)
from app.agents.chat.multi_agent_chat.shared.permissions import Ruleset
from app.agents.chat.multi_agent_chat.subagents.shared.md_file_reader import (
read_shared_snippet,
)

View file

@ -43,10 +43,10 @@ from app.agents.chat.multi_agent_chat.main_agent.middleware.checkpointed_subagen
from app.agents.chat.multi_agent_chat.main_agent.middleware.checkpointed_subagent_middleware.task_tool import (
build_task_tool_with_parent_config,
)
from app.agents.chat.multi_agent_chat.shared.middleware.permissions.ask.request import (
from app.agents.chat.multi_agent_chat.shared.permissions import Rule
from app.agents.chat.multi_agent_chat.shared.permissions.ask.request import (
request_permission_decision,
)
from app.agents.chat.multi_agent_chat.shared.permissions import Rule
from app.agents.chat.multi_agent_chat.subagents.shared.hitl.approvals.self_gated import (
request_approval,
)

View file

@ -16,10 +16,10 @@ from langgraph.graph import END, START, StateGraph
from langgraph.types import Command
from typing_extensions import TypedDict
from app.agents.chat.multi_agent_chat.shared.middleware.permissions.ask.request import (
from app.agents.chat.multi_agent_chat.shared.permissions import Rule
from app.agents.chat.multi_agent_chat.shared.permissions.ask.request import (
request_permission_decision,
)
from app.agents.chat.multi_agent_chat.shared.permissions import Rule
class _State(TypedDict, total=False):

View file

@ -14,13 +14,14 @@ from pydantic import BaseModel
from typing_extensions import TypedDict
from app.agents.chat.multi_agent_chat.shared.feature_flags import AgentFeatureFlags
from app.agents.chat.multi_agent_chat.shared.middleware.permissions import (
from app.agents.chat.multi_agent_chat.shared.permissions import (
Rule,
Ruleset,
build_permission_mw,
)
from app.agents.chat.multi_agent_chat.shared.middleware.permissions.ask.payload import (
from app.agents.chat.multi_agent_chat.shared.permissions.ask.payload import (
build_permission_ask_payload,
)
from app.agents.chat.multi_agent_chat.shared.permissions import Rule, Ruleset
class _NoArgs(BaseModel):

View file

@ -24,10 +24,11 @@ from langgraph.types import Command
from typing_extensions import TypedDict
from app.agents.chat.multi_agent_chat.shared.feature_flags import AgentFeatureFlags
from app.agents.chat.multi_agent_chat.shared.middleware.permissions import (
from app.agents.chat.multi_agent_chat.shared.permissions import (
Rule,
Ruleset,
build_permission_mw,
)
from app.agents.chat.multi_agent_chat.shared.permissions import Rule, Ruleset
def _kb_style_ruleset() -> Ruleset:

View file

@ -15,10 +15,11 @@ from pydantic import BaseModel
from typing_extensions import TypedDict
from app.agents.chat.multi_agent_chat.shared.feature_flags import AgentFeatureFlags
from app.agents.chat.multi_agent_chat.shared.middleware.permissions import (
from app.agents.chat.multi_agent_chat.shared.permissions import (
Rule,
Ruleset,
build_permission_mw,
)
from app.agents.chat.multi_agent_chat.shared.permissions import Rule, Ruleset
class _NoArgs(BaseModel):

View file

@ -20,10 +20,10 @@ from langchain_core.messages import AIMessage, BaseMessage, HumanMessage
from langchain_core.outputs import ChatGeneration, ChatResult
from app.agents.chat.multi_agent_chat.shared.feature_flags import AgentFeatureFlags
from app.agents.chat.multi_agent_chat.shared.middleware.permissions.middleware.core import (
from app.agents.chat.multi_agent_chat.shared.permissions import Rule, Ruleset, evaluate
from app.agents.chat.multi_agent_chat.shared.permissions.middleware.core import (
PermissionMiddleware,
)
from app.agents.chat.multi_agent_chat.shared.permissions import Rule, Ruleset, evaluate
from app.agents.chat.multi_agent_chat.subagents.shared.subagent_builder import (
pack_subagent,
)