agentctrl 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
agentctrl/__init__.py ADDED
@@ -0,0 +1,59 @@
1
+ # Copyright 2026 Mohammad Abu Jafar
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """agentctrl — Institutional governance pipeline for AI agent actions.
16
+
17
+ Usage:
18
+ from agentctrl import RuntimeGateway, ActionProposal
19
+
20
+ gateway = RuntimeGateway()
21
+ result = await gateway.validate(ActionProposal(
22
+ agent_id="my-agent",
23
+ action_type="data.read",
24
+ action_params={"classification": "PII"},
25
+ autonomy_level=2,
26
+ ))
27
+ print(result["decision"]) # ALLOW | ESCALATE | BLOCK
28
+ """
29
+
30
+ from .types import (
31
+ ActionProposal,
32
+ PipelineStageResult,
33
+ PipelineHooks,
34
+ EscalationTarget,
35
+ RuntimeDecisionRecord,
36
+ )
37
+ from .runtime_gateway import RuntimeGateway
38
+ from .policy_engine import PolicyEngine
39
+ from .authority_graph import AuthorityGraphEngine
40
+ from .risk_engine import RiskEngine, RiskScore
41
+ from .conflict_detector import ConflictDetector
42
+ from .decorator import governed, GovernanceBlockedError, GovernanceEscalatedError
43
+
44
+ __all__ = [
45
+ "ActionProposal",
46
+ "PipelineStageResult",
47
+ "PipelineHooks",
48
+ "EscalationTarget",
49
+ "RuntimeDecisionRecord",
50
+ "RuntimeGateway",
51
+ "PolicyEngine",
52
+ "AuthorityGraphEngine",
53
+ "RiskEngine",
54
+ "RiskScore",
55
+ "ConflictDetector",
56
+ "governed",
57
+ "GovernanceBlockedError",
58
+ "GovernanceEscalatedError",
59
+ ]
@@ -0,0 +1,22 @@
1
+ # Copyright 2026 Mohammad Abu Jafar
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """Framework adapters for agentctrl.
16
+
17
+ Each adapter is imported separately to avoid pulling in framework dependencies:
18
+
19
+ from agentctrl.adapters.langchain import govern_tool
20
+ from agentctrl.adapters.openai_agents import govern_tool
21
+ from agentctrl.adapters.crewai import govern_tool
22
+ """
@@ -0,0 +1,135 @@
1
+ # Copyright 2026 Mohammad Abu Jafar
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """CrewAI adapter for agentctrl.
16
+
17
+ Usage:
18
+ from agentctrl import RuntimeGateway
19
+ from agentctrl.adapters.crewai import govern_tool
20
+
21
+ gateway = RuntimeGateway()
22
+ governed = govern_tool(my_crewai_tool, gateway=gateway, agent_id="my-agent")
23
+
24
+ agent = Agent(role="analyst", tools=[governed])
25
+
26
+ Requires: pip install agentctrl[crewai]
27
+ """
28
+
29
+ from __future__ import annotations
30
+
31
+ import asyncio
32
+ from typing import Any, Optional, Type
33
+
34
+ try:
35
+ from crewai.tools import BaseTool as CrewBaseTool
36
+ from pydantic import BaseModel
37
+ except ImportError as e:
38
+ raise ImportError(
39
+ "CrewAI adapter requires crewai. "
40
+ "Install with: pip install agentctrl[crewai]"
41
+ ) from e
42
+
43
+ from ..runtime_gateway import RuntimeGateway
44
+ from ..types import ActionProposal
45
+ from ..decorator import GovernanceBlockedError, GovernanceEscalatedError # noqa: F401 — re-exported
46
+
47
+
48
+ class GovernedCrewTool(CrewBaseTool):
49
+ """CrewAI tool wrapper that runs governance before execution.
50
+
51
+ Wraps any existing CrewAI BaseTool. On BLOCK or ESCALATE, returns an
52
+ error string (CrewAI's standard pattern for tool failures — the agent
53
+ sees the reason and can report it).
54
+ """
55
+
56
+ name: str = ""
57
+ description: str = ""
58
+ args_schema: Optional[Type[BaseModel]] = None
59
+
60
+ _inner_tool: CrewBaseTool
61
+ _gateway: RuntimeGateway
62
+ _agent_id: str
63
+ _autonomy_level: int
64
+
65
+ class Config:
66
+ arbitrary_types_allowed = True
67
+
68
+ def __init__(
69
+ self,
70
+ tool: CrewBaseTool,
71
+ gateway: RuntimeGateway,
72
+ agent_id: str,
73
+ autonomy_level: int = 2,
74
+ **kwargs: Any,
75
+ ):
76
+ super().__init__(
77
+ name=tool.name,
78
+ description=tool.description,
79
+ args_schema=getattr(tool, "args_schema", None),
80
+ **kwargs,
81
+ )
82
+ self._inner_tool = tool
83
+ self._gateway = gateway
84
+ self._agent_id = agent_id
85
+ self._autonomy_level = autonomy_level
86
+
87
+ def _run(self, **kwargs: Any) -> str:
88
+ proposal = ActionProposal(
89
+ agent_id=self._agent_id,
90
+ action_type=self._inner_tool.name,
91
+ action_params=kwargs,
92
+ autonomy_level=self._autonomy_level,
93
+ )
94
+
95
+ try:
96
+ loop = asyncio.get_running_loop()
97
+ except RuntimeError:
98
+ loop = None
99
+
100
+ if loop and loop.is_running():
101
+ import concurrent.futures
102
+ with concurrent.futures.ThreadPoolExecutor() as pool:
103
+ result = pool.submit(
104
+ asyncio.run, self._gateway.validate(proposal)
105
+ ).result()
106
+ else:
107
+ result = asyncio.run(self._gateway.validate(proposal))
108
+
109
+ decision = result.get("decision", "BLOCK")
110
+
111
+ if decision == "BLOCK":
112
+ return f"[GOVERNANCE BLOCKED] {result.get('reason', 'Action not permitted.')}"
113
+ if decision == "ESCALATE":
114
+ return f"[GOVERNANCE ESCALATED] {result.get('reason', 'Human approval required.')}"
115
+
116
+ return self._inner_tool._run(**kwargs)
117
+
118
+
119
+ def govern_tool(
120
+ tool: CrewBaseTool,
121
+ gateway: RuntimeGateway,
122
+ agent_id: str,
123
+ autonomy_level: int = 2,
124
+ ) -> GovernedCrewTool:
125
+ """Wrap a CrewAI tool with governance.
126
+
127
+ Returns a new tool that runs the governance pipeline before every invocation.
128
+ Drop-in replacement — same name, same schema, same interface.
129
+ """
130
+ return GovernedCrewTool(
131
+ tool=tool,
132
+ gateway=gateway,
133
+ agent_id=agent_id,
134
+ autonomy_level=autonomy_level,
135
+ )
@@ -0,0 +1,145 @@
1
+ # Copyright 2026 Mohammad Abu Jafar
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """LangChain / LangGraph adapter for agentctrl.
16
+
17
+ Usage:
18
+ from agentctrl import RuntimeGateway
19
+ from agentctrl.adapters.langchain import govern_tool
20
+
21
+ gateway = RuntimeGateway()
22
+ governed = govern_tool(my_langchain_tool, gateway=gateway, agent_id="my-agent")
23
+
24
+ # Use governed tool in any LangChain agent as usual
25
+ agent = create_react_agent(llm, [governed])
26
+
27
+ Requires: pip install agentctrl[langchain]
28
+ """
29
+
30
+ from __future__ import annotations
31
+
32
+ from typing import Any, Optional, Type
33
+
34
+ try:
35
+ from langchain_core.tools import BaseTool, ToolException
36
+ from langchain_core.callbacks import CallbackManagerForToolRun, AsyncCallbackManagerForToolRun
37
+ from pydantic import BaseModel
38
+ except ImportError as e:
39
+ raise ImportError(
40
+ "LangChain adapter requires langchain-core. "
41
+ "Install with: pip install agentctrl[langchain]"
42
+ ) from e
43
+
44
+ from ..runtime_gateway import RuntimeGateway
45
+ from ..types import ActionProposal
46
+ from ..decorator import GovernanceBlockedError, GovernanceEscalatedError # noqa: F401 — re-exported
47
+
48
+
49
+ class GovernedTool(BaseTool):
50
+ """LangChain tool wrapper that runs governance before execution.
51
+
52
+ Wraps any existing BaseTool. On BLOCK, raises ToolException (LangChain's
53
+ standard error path). On ESCALATE, raises ToolException with the escalation
54
+ reason so the agent can communicate it to the user.
55
+ """
56
+
57
+ name: str = ""
58
+ description: str = ""
59
+ args_schema: Optional[Type[BaseModel]] = None
60
+
61
+ _inner_tool: BaseTool
62
+ _gateway: RuntimeGateway
63
+ _agent_id: str
64
+ _autonomy_level: int
65
+
66
+ class Config:
67
+ arbitrary_types_allowed = True
68
+
69
+ def __init__(
70
+ self,
71
+ tool: BaseTool,
72
+ gateway: RuntimeGateway,
73
+ agent_id: str,
74
+ autonomy_level: int = 2,
75
+ **kwargs: Any,
76
+ ):
77
+ super().__init__(
78
+ name=tool.name,
79
+ description=tool.description,
80
+ args_schema=tool.args_schema,
81
+ **kwargs,
82
+ )
83
+ self._inner_tool = tool
84
+ self._gateway = gateway
85
+ self._agent_id = agent_id
86
+ self._autonomy_level = autonomy_level
87
+
88
+ def _run(
89
+ self,
90
+ *args: Any,
91
+ run_manager: Optional[CallbackManagerForToolRun] = None,
92
+ **kwargs: Any,
93
+ ) -> Any:
94
+ raise ToolException(
95
+ "GovernedTool requires async execution. Use ainvoke() or an async agent."
96
+ )
97
+
98
+ async def _arun(
99
+ self,
100
+ *args: Any,
101
+ run_manager: Optional[AsyncCallbackManagerForToolRun] = None,
102
+ **kwargs: Any,
103
+ ) -> Any:
104
+ proposal = ActionProposal(
105
+ agent_id=self._agent_id,
106
+ action_type=self._inner_tool.name,
107
+ action_params=kwargs if kwargs else {"input": args[0] if args else ""},
108
+ autonomy_level=self._autonomy_level,
109
+ )
110
+
111
+ result = await self._gateway.validate(proposal)
112
+ decision = result.get("decision", "BLOCK")
113
+
114
+ if decision == "BLOCK":
115
+ raise ToolException(
116
+ f"Governance blocked: {result.get('reason', 'Action not permitted.')}"
117
+ )
118
+ if decision == "ESCALATE":
119
+ raise ToolException(
120
+ f"Governance escalated: {result.get('reason', 'Human approval required.')}"
121
+ )
122
+
123
+ return await self._inner_tool.ainvoke(
124
+ kwargs if kwargs else (args[0] if args else ""),
125
+ config={"callbacks": run_manager.get_child() if run_manager else None},
126
+ )
127
+
128
+
129
+ def govern_tool(
130
+ tool: BaseTool,
131
+ gateway: RuntimeGateway,
132
+ agent_id: str,
133
+ autonomy_level: int = 2,
134
+ ) -> GovernedTool:
135
+ """Wrap a LangChain tool with governance.
136
+
137
+ Returns a new tool that runs the governance pipeline before every invocation.
138
+ Drop-in replacement — same name, same schema, same interface.
139
+ """
140
+ return GovernedTool(
141
+ tool=tool,
142
+ gateway=gateway,
143
+ agent_id=agent_id,
144
+ autonomy_level=autonomy_level,
145
+ )
@@ -0,0 +1,141 @@
1
+ # Copyright 2026 Mohammad Abu Jafar
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """OpenAI Agents SDK adapter for agentctrl.
16
+
17
+ Usage:
18
+ from agentctrl import RuntimeGateway
19
+ from agentctrl.adapters.openai_agents import govern_tool
20
+
21
+ gateway = RuntimeGateway()
22
+ governed = govern_tool(my_function_tool, gateway=gateway, agent_id="my-agent")
23
+
24
+ agent = Agent(name="my-agent", tools=[governed])
25
+ result = Runner.run(agent, "do the thing")
26
+
27
+ Requires: pip install agentctrl[openai-agents]
28
+ """
29
+
30
+ from __future__ import annotations
31
+
32
+ import functools
33
+ import inspect
34
+ from typing import Any, Callable
35
+
36
+ try:
37
+ from agents import FunctionTool, RunContext
38
+ except ImportError as e:
39
+ raise ImportError(
40
+ "OpenAI Agents SDK adapter requires the agents package. "
41
+ "Install with: pip install agentctrl[openai-agents]"
42
+ ) from e
43
+
44
+ from ..runtime_gateway import RuntimeGateway
45
+ from ..types import ActionProposal
46
+ from ..decorator import GovernanceBlockedError, GovernanceEscalatedError
47
+
48
+
49
+ def govern_tool(
50
+ tool: FunctionTool,
51
+ gateway: RuntimeGateway,
52
+ agent_id: str,
53
+ autonomy_level: int = 2,
54
+ ) -> FunctionTool:
55
+ """Wrap an OpenAI Agents SDK FunctionTool with governance.
56
+
57
+ Returns a new FunctionTool that runs the governance pipeline before
58
+ every invocation. On BLOCK or ESCALATE, raises an exception that the
59
+ agent framework surfaces as a tool error.
60
+ """
61
+ original_fn = tool.on_invoke_tool
62
+
63
+ @functools.wraps(original_fn)
64
+ async def governed_invoke(ctx: RunContext, input_str: str) -> str:
65
+ proposal = ActionProposal(
66
+ agent_id=agent_id,
67
+ action_type=tool.name,
68
+ action_params={"input": input_str},
69
+ autonomy_level=autonomy_level,
70
+ )
71
+
72
+ result = await gateway.validate(proposal)
73
+ decision = result.get("decision", "BLOCK")
74
+
75
+ if decision == "BLOCK":
76
+ raise GovernanceBlockedError(
77
+ result.get("reason", "Action not permitted."), result
78
+ )
79
+ if decision == "ESCALATE":
80
+ raise GovernanceEscalatedError(
81
+ result.get("reason", "Human approval required."), result
82
+ )
83
+
84
+ return await original_fn(ctx, input_str)
85
+
86
+ governed = FunctionTool(
87
+ name=tool.name,
88
+ description=tool.description,
89
+ params_json_schema=tool.params_json_schema,
90
+ on_invoke_tool=governed_invoke,
91
+ )
92
+ return governed
93
+
94
+
95
+ def governed_function(
96
+ gateway: RuntimeGateway,
97
+ agent_id: str,
98
+ autonomy_level: int = 2,
99
+ action_type: str | None = None,
100
+ ):
101
+ """Decorator for plain functions used as OpenAI agent tools.
102
+
103
+ Usage:
104
+ @governed_function(gateway=gateway, agent_id="my-agent")
105
+ async def send_email(to: str, subject: str, body: str) -> str:
106
+ ...
107
+ """
108
+ def decorator(fn: Callable) -> Callable:
109
+ resolved_action_type = action_type or fn.__name__
110
+
111
+ @functools.wraps(fn)
112
+ async def wrapper(*args: Any, **kwargs: Any) -> Any:
113
+ sig = inspect.signature(fn)
114
+ bound = sig.bind(*args, **kwargs)
115
+ bound.apply_defaults()
116
+ action_params = dict(bound.arguments)
117
+ action_params.pop("ctx", None)
118
+
119
+ proposal = ActionProposal(
120
+ agent_id=agent_id,
121
+ action_type=resolved_action_type,
122
+ action_params=action_params,
123
+ autonomy_level=autonomy_level,
124
+ )
125
+
126
+ result = await gateway.validate(proposal)
127
+ decision = result.get("decision", "BLOCK")
128
+
129
+ if decision == "BLOCK":
130
+ raise GovernanceBlockedError(
131
+ result.get("reason", "Action not permitted."), result
132
+ )
133
+ if decision == "ESCALATE":
134
+ raise GovernanceEscalatedError(
135
+ result.get("reason", "Human approval required."), result
136
+ )
137
+
138
+ return await fn(*args, **kwargs)
139
+
140
+ return wrapper
141
+ return decorator