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 +59 -0
- agentctrl/adapters/__init__.py +22 -0
- agentctrl/adapters/crewai.py +135 -0
- agentctrl/adapters/langchain.py +145 -0
- agentctrl/adapters/openai_agents.py +141 -0
- agentctrl/authority_graph.py +362 -0
- agentctrl/conflict_detector.py +164 -0
- agentctrl/decorator.py +85 -0
- agentctrl/policy_engine.py +421 -0
- agentctrl/py.typed +0 -0
- agentctrl/risk_engine.py +247 -0
- agentctrl/runtime_gateway.py +419 -0
- agentctrl/types.py +92 -0
- agentctrl-0.1.0.dist-info/METADATA +491 -0
- agentctrl-0.1.0.dist-info/RECORD +17 -0
- agentctrl-0.1.0.dist-info/WHEEL +4 -0
- agentctrl-0.1.0.dist-info/licenses/LICENSE +190 -0
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
|