amanai 0.1.0__tar.gz

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.
@@ -0,0 +1,16 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .venv/
4
+ venv/
5
+ .env
6
+ *.sqlite3
7
+ *.db
8
+ node_modules/
9
+ dist/
10
+ .DS_Store
11
+ .pytest_cache/
12
+ .mypy_cache/
13
+ .ruff_cache/
14
+ .fida
15
+ release.sh
16
+ uv.lock
amanai-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,142 @@
1
+ Metadata-Version: 2.4
2
+ Name: amanai
3
+ Version: 0.1.0
4
+ Summary: Action Policy Engine for AI agents: policy-as-code for tool-calls — evaluate, enforce (block/warn/approve), and trace agent actions, with the same policy asserted in CI. Plus local guardrails and an offline judge. Pure-Python, zero dependencies.
5
+ Author-email: Aji Purnomo <hi@aji.is-a.dev>
6
+ Keywords: agent,ai,guardrails,llm,owasp,pii,policy-as-code,prompt-injection,security,tool-call
7
+ Classifier: Development Status :: 4 - Beta
8
+ Classifier: Intended Audience :: Developers
9
+ Classifier: License :: OSI Approved :: Apache Software License
10
+ Classifier: Operating System :: OS Independent
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Topic :: Security
13
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
14
+ Requires-Python: >=3.10
15
+ Description-Content-Type: text/markdown
16
+
17
+ # amanai
18
+
19
+ **Action Policy Engine for AI agents — policy-as-code for tool-calls. Pure-Python, zero dependencies.**
20
+
21
+ Write **one policy** for risky agent actions. Amanai evaluates every tool-call
22
+ against it before the tool runs, enforces the decision, and records a structured
23
+ trace as evidence. The same policy file asserts in CI — what you test is what you
24
+ enforce. Decisions are deterministic: no LLM decides whether a high-risk tool runs.
25
+
26
+ ```bash
27
+ pip install amanai
28
+ ```
29
+
30
+ ## Action Policy Engine
31
+
32
+ Five verbs: **load** a policy, **evaluate** an action, **protect** a tool,
33
+ **collect** a trace, **assert** it in tests.
34
+
35
+ ```python
36
+ from amanai import set_policy, tool, ToolBlocked, ApprovalRequired, collect_trace
37
+
38
+ set_policy("amanai.policy.json") # load + validate (raises PolicyError if bad)
39
+
40
+ @tool(name="billing.refund", capability="money_movement", risk="high")
41
+ def refund_payment(amount): ...
42
+
43
+ refund_payment(amount=500) # ToolBlocked / ApprovalRequired per policy
44
+ trace = collect_trace() # canonical evidence: action + decision + status
45
+ ```
46
+
47
+ A **policy** is a list of rules. A rule matches on `tool` and/or `capability`,
48
+ with optional `args` and `context` predicates, and carries an `action`:
49
+
50
+ ```json
51
+ [
52
+ { "id": "discount-cap", "tool": "apply_discount",
53
+ "args": [{ "arg": "pct", "op": ">=", "value": 50 }],
54
+ "action": "block", "reason": "discount of 50% or more is never allowed" },
55
+
56
+ { "id": "refund-approval", "tool": "refund_payment",
57
+ "args": [{ "arg": "amount", "op": ">", "value": 100 }],
58
+ "action": "require_approval" },
59
+
60
+ { "id": "external-email", "capability": "external_comms",
61
+ "args": [{ "arg": "to", "op": "email_external", "value": ["acme.com"] }],
62
+ "action": "block" }
63
+ ]
64
+ ```
65
+
66
+ - **Outcomes:** `allow` · `block` · `warn` · `require_approval`.
67
+ - **Operators:** `>= > <= < == !=` · `contains` · `regex` · `in` / `not_in` ·
68
+ `email_external` · `domain_in` / `domain_not_in`.
69
+ - **Modes** (per request, never global): `enforce` blocks before execution ·
70
+ `shadow` records what it *would* block but lets the call run · `test`
71
+ evaluates with no side effects.
72
+ - The legacy flat rule `{tool, arg, op, value}` still loads (it defaults to
73
+ `block`). Rules without an `id` get a deterministic generated one.
74
+
75
+ ```python
76
+ from amanai import set_mode, set_context, evaluate, ActionRequest
77
+
78
+ set_mode("shadow") # observe before enforcing
79
+ set_context(role="support", tenant="t1", environment="prod") # authz context for rules
80
+ evaluate(ActionRequest("apply_discount", {"pct": 90})) # -> PolicyDecision(outcome="block", ...)
81
+ ```
82
+
83
+ ## Assert the same policy in CI
84
+
85
+ ```python
86
+ from amanai.testing import assert_blocked, assert_no_violations
87
+
88
+ def test_excessive_discount_is_blocked():
89
+ set_policy("amanai.policy.json")
90
+ assert_blocked(apply_discount, pct=90)
91
+
92
+ def test_attack_trace_is_clean():
93
+ run_agent(attack_prompt) # produces a trace
94
+ assert_no_violations(collect_trace()) # replays actions against the active policy
95
+ ```
96
+
97
+ `amanai.testing` also ships `assert_requires_approval`,
98
+ `using_mode`, and `replay` (re-evaluate a recorded trace against a policy).
99
+
100
+ ## Runtime guardrails (input / output, supporting)
101
+
102
+ ```python
103
+ from amanai import guard_input, guard_output, GuardrailBlocked
104
+
105
+ try:
106
+ msg = guard_input(user_msg) # raises GuardrailBlocked on injection
107
+ except GuardrailBlocked:
108
+ reply = "I can't help with that."
109
+ else:
110
+ reply = guard_output(run_agent(msg)) # redacts emails, cards, secrets
111
+ ```
112
+
113
+ ## Offline judge & MCP checks (supporting)
114
+
115
+ Score an agent's input / response / tool-calls against detectors (string match,
116
+ tool assertions, MCP checks; LLM-as-judge when you pass a gateway). The
117
+ `tool_assertion` detector reuses the engine's operators, so the test side can't
118
+ drift from runtime — a contract test guarantees it.
119
+
120
+ ## Monitoring (optional — needs an Amanai server)
121
+
122
+ ```python
123
+ from amanai import Monitor, collect_trace
124
+
125
+ mon = Monitor("http://localhost:8000", PUBLIC_KEY, SECRET_KEY)
126
+ mon.log_trace(collect_trace(), user_id="u123") # canonical events, PII redacted
127
+ ```
128
+
129
+ ## API
130
+
131
+ - **Engine:** `load_policy` `set_policy` `get_policy` `evaluate` `Policy` `Rule`
132
+ `ActionRequest` `PolicyDecision` `TraceEvent` `PendingAction` `PolicyError`
133
+ - **Modes / context:** `set_mode` `get_mode` `set_context` `get_context` `clear_context`
134
+ - **Protect / collect:** `tool` `collect_trace` `collect_tool_calls` `record_tool_call`
135
+ `reset` `registered_tools` `uncovered_tools` `ToolBlocked` `ApprovalRequired`
136
+ - **Test:** `amanai.testing` — `assert_blocked`
137
+ `assert_requires_approval` `assert_no_violations` `using_mode` `replay`
138
+ - **Policy lifecycle:** `clear_tool_policy` (deactivate the active policy)
139
+ - **Guardrails:** `guard_input` `guard_output` `redact_pii` `detect_injection` `detect_pii` `GuardResult` `GuardrailBlocked`
140
+ - **Judge / monitor:** `judge` `Monitor`
141
+
142
+ Apache-2.0. Zero runtime dependencies.
amanai-0.1.0/README.md ADDED
@@ -0,0 +1,126 @@
1
+ # amanai
2
+
3
+ **Action Policy Engine for AI agents — policy-as-code for tool-calls. Pure-Python, zero dependencies.**
4
+
5
+ Write **one policy** for risky agent actions. Amanai evaluates every tool-call
6
+ against it before the tool runs, enforces the decision, and records a structured
7
+ trace as evidence. The same policy file asserts in CI — what you test is what you
8
+ enforce. Decisions are deterministic: no LLM decides whether a high-risk tool runs.
9
+
10
+ ```bash
11
+ pip install amanai
12
+ ```
13
+
14
+ ## Action Policy Engine
15
+
16
+ Five verbs: **load** a policy, **evaluate** an action, **protect** a tool,
17
+ **collect** a trace, **assert** it in tests.
18
+
19
+ ```python
20
+ from amanai import set_policy, tool, ToolBlocked, ApprovalRequired, collect_trace
21
+
22
+ set_policy("amanai.policy.json") # load + validate (raises PolicyError if bad)
23
+
24
+ @tool(name="billing.refund", capability="money_movement", risk="high")
25
+ def refund_payment(amount): ...
26
+
27
+ refund_payment(amount=500) # ToolBlocked / ApprovalRequired per policy
28
+ trace = collect_trace() # canonical evidence: action + decision + status
29
+ ```
30
+
31
+ A **policy** is a list of rules. A rule matches on `tool` and/or `capability`,
32
+ with optional `args` and `context` predicates, and carries an `action`:
33
+
34
+ ```json
35
+ [
36
+ { "id": "discount-cap", "tool": "apply_discount",
37
+ "args": [{ "arg": "pct", "op": ">=", "value": 50 }],
38
+ "action": "block", "reason": "discount of 50% or more is never allowed" },
39
+
40
+ { "id": "refund-approval", "tool": "refund_payment",
41
+ "args": [{ "arg": "amount", "op": ">", "value": 100 }],
42
+ "action": "require_approval" },
43
+
44
+ { "id": "external-email", "capability": "external_comms",
45
+ "args": [{ "arg": "to", "op": "email_external", "value": ["acme.com"] }],
46
+ "action": "block" }
47
+ ]
48
+ ```
49
+
50
+ - **Outcomes:** `allow` · `block` · `warn` · `require_approval`.
51
+ - **Operators:** `>= > <= < == !=` · `contains` · `regex` · `in` / `not_in` ·
52
+ `email_external` · `domain_in` / `domain_not_in`.
53
+ - **Modes** (per request, never global): `enforce` blocks before execution ·
54
+ `shadow` records what it *would* block but lets the call run · `test`
55
+ evaluates with no side effects.
56
+ - The legacy flat rule `{tool, arg, op, value}` still loads (it defaults to
57
+ `block`). Rules without an `id` get a deterministic generated one.
58
+
59
+ ```python
60
+ from amanai import set_mode, set_context, evaluate, ActionRequest
61
+
62
+ set_mode("shadow") # observe before enforcing
63
+ set_context(role="support", tenant="t1", environment="prod") # authz context for rules
64
+ evaluate(ActionRequest("apply_discount", {"pct": 90})) # -> PolicyDecision(outcome="block", ...)
65
+ ```
66
+
67
+ ## Assert the same policy in CI
68
+
69
+ ```python
70
+ from amanai.testing import assert_blocked, assert_no_violations
71
+
72
+ def test_excessive_discount_is_blocked():
73
+ set_policy("amanai.policy.json")
74
+ assert_blocked(apply_discount, pct=90)
75
+
76
+ def test_attack_trace_is_clean():
77
+ run_agent(attack_prompt) # produces a trace
78
+ assert_no_violations(collect_trace()) # replays actions against the active policy
79
+ ```
80
+
81
+ `amanai.testing` also ships `assert_requires_approval`,
82
+ `using_mode`, and `replay` (re-evaluate a recorded trace against a policy).
83
+
84
+ ## Runtime guardrails (input / output, supporting)
85
+
86
+ ```python
87
+ from amanai import guard_input, guard_output, GuardrailBlocked
88
+
89
+ try:
90
+ msg = guard_input(user_msg) # raises GuardrailBlocked on injection
91
+ except GuardrailBlocked:
92
+ reply = "I can't help with that."
93
+ else:
94
+ reply = guard_output(run_agent(msg)) # redacts emails, cards, secrets
95
+ ```
96
+
97
+ ## Offline judge & MCP checks (supporting)
98
+
99
+ Score an agent's input / response / tool-calls against detectors (string match,
100
+ tool assertions, MCP checks; LLM-as-judge when you pass a gateway). The
101
+ `tool_assertion` detector reuses the engine's operators, so the test side can't
102
+ drift from runtime — a contract test guarantees it.
103
+
104
+ ## Monitoring (optional — needs an Amanai server)
105
+
106
+ ```python
107
+ from amanai import Monitor, collect_trace
108
+
109
+ mon = Monitor("http://localhost:8000", PUBLIC_KEY, SECRET_KEY)
110
+ mon.log_trace(collect_trace(), user_id="u123") # canonical events, PII redacted
111
+ ```
112
+
113
+ ## API
114
+
115
+ - **Engine:** `load_policy` `set_policy` `get_policy` `evaluate` `Policy` `Rule`
116
+ `ActionRequest` `PolicyDecision` `TraceEvent` `PendingAction` `PolicyError`
117
+ - **Modes / context:** `set_mode` `get_mode` `set_context` `get_context` `clear_context`
118
+ - **Protect / collect:** `tool` `collect_trace` `collect_tool_calls` `record_tool_call`
119
+ `reset` `registered_tools` `uncovered_tools` `ToolBlocked` `ApprovalRequired`
120
+ - **Test:** `amanai.testing` — `assert_blocked`
121
+ `assert_requires_approval` `assert_no_violations` `using_mode` `replay`
122
+ - **Policy lifecycle:** `clear_tool_policy` (deactivate the active policy)
123
+ - **Guardrails:** `guard_input` `guard_output` `redact_pii` `detect_injection` `detect_pii` `GuardResult` `GuardrailBlocked`
124
+ - **Judge / monitor:** `judge` `Monitor`
125
+
126
+ Apache-2.0. Zero runtime dependencies.
@@ -0,0 +1,121 @@
1
+ """Amanai SDK — policy-as-code for AI agent actions.
2
+
3
+ Test the policy in CI. Enforce the same policy at runtime. Keep evidence for audit.
4
+
5
+ The Action Policy Engine is the product center. Five verbs:
6
+
7
+ from amanai import load_policy, set_policy, evaluate, tool, collect_trace
8
+
9
+ set_policy(load_policy("amanai.policy.json")) # 1. load policy
10
+
11
+ @tool(capability="money_movement", risk="high") # 3. protect tool
12
+ def refund_payment(amount): ...
13
+
14
+ evaluate(ActionRequest("refund_payment", {"amount": 500})) # 2. evaluate action
15
+ trace = collect_trace() # 4. collect trace
16
+ # 5. assert in tests → amanai.testing
17
+
18
+ Decisions are deterministic — no LLM decides whether a high-risk tool runs.
19
+ Prompt-injection and PII guardrails remain supporting modules, not the center.
20
+ """
21
+
22
+ from amanai.client import (
23
+ collect_tool_calls,
24
+ collect_trace,
25
+ record_tool_call,
26
+ registered_tools,
27
+ reset,
28
+ tool,
29
+ uncovered_tools,
30
+ )
31
+ from amanai.coverage import COVERAGE, coverage_for
32
+ from amanai.guardrails import (
33
+ GuardrailBlocked,
34
+ GuardResult,
35
+ detect_injection,
36
+ detect_pii,
37
+ guard_input,
38
+ guard_output,
39
+ redact_pii,
40
+ scan,
41
+ )
42
+ from amanai.judge import run_detector, run_mcp_check
43
+ from amanai.mcp_adapter import guard_mcp_call
44
+ from amanai.monitor import Monitor
45
+ from amanai.policy import (
46
+ MODES,
47
+ OUTCOMES,
48
+ ActionRequest,
49
+ ApprovalRequired,
50
+ PendingAction,
51
+ Policy,
52
+ PolicyDecision,
53
+ PolicyError,
54
+ Rule,
55
+ ToolBlocked,
56
+ TraceEvent,
57
+ clear_context,
58
+ clear_tool_policy,
59
+ evaluate,
60
+ get_context,
61
+ get_mode,
62
+ get_policy,
63
+ load_policy,
64
+ set_context,
65
+ set_mode,
66
+ set_policy,
67
+ )
68
+
69
+ __all__ = [
70
+ # --- Action Policy Engine ---
71
+ "load_policy",
72
+ "set_policy",
73
+ "get_policy",
74
+ "evaluate",
75
+ "Policy",
76
+ "Rule",
77
+ "ActionRequest",
78
+ "PolicyDecision",
79
+ "TraceEvent",
80
+ "PendingAction",
81
+ "ToolBlocked",
82
+ "ApprovalRequired",
83
+ "PolicyError",
84
+ "OUTCOMES",
85
+ "MODES",
86
+ # modes & context
87
+ "set_mode",
88
+ "get_mode",
89
+ "set_context",
90
+ "get_context",
91
+ "clear_context",
92
+ # protect & collect
93
+ "tool",
94
+ "record_tool_call",
95
+ "collect_tool_calls",
96
+ "collect_trace",
97
+ "reset",
98
+ "registered_tools",
99
+ "uncovered_tools",
100
+ # MCP adapter
101
+ "guard_mcp_call",
102
+ # --- supporting: offline judge & MCP static checks ---
103
+ "run_detector",
104
+ "run_mcp_check",
105
+ # clear active policy
106
+ "clear_tool_policy",
107
+ # --- supporting: guardrails ---
108
+ "scan",
109
+ "guard_input",
110
+ "guard_output",
111
+ "redact_pii",
112
+ "detect_injection",
113
+ "detect_pii",
114
+ "GuardResult",
115
+ "GuardrailBlocked",
116
+ # --- supporting: monitoring & coverage ---
117
+ "Monitor",
118
+ "COVERAGE",
119
+ "coverage_for",
120
+ ]
121
+ __version__ = "0.1.0"
@@ -0,0 +1,229 @@
1
+ """Protect tools and collect traces — the runtime entry point into the engine.
2
+
3
+ `@tool` is a thin adapter: it normalizes the call into an `ActionRequest`, asks
4
+ the Action Policy Engine for a decision, enforces it according to the current
5
+ mode, and records a `TraceEvent` as evidence. The policy logic lives in
6
+ `policy.py`; this module only wires real Python calls into it.
7
+
8
+ from amanai import tool, set_policy, collect_trace
9
+
10
+ @tool(capability="money_movement", risk="high")
11
+ def refund_payment(amount): ...
12
+
13
+ set_policy("amanai.policy.json")
14
+ refund_payment(amount=500) # ToolBlocked / ApprovalRequired per policy
15
+ trace = collect_trace() # canonical evidence for CI / monitoring
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import contextvars
21
+ import functools
22
+ import inspect
23
+ from typing import Any, Callable
24
+
25
+ from amanai.policy import (
26
+ ActionRequest,
27
+ ApprovalRequired,
28
+ PendingAction,
29
+ Policy,
30
+ ToolBlocked,
31
+ TraceEvent,
32
+ evaluate,
33
+ get_context,
34
+ get_mode,
35
+ get_policy,
36
+ )
37
+
38
+ # Context-local trace buffer — concurrent requests never see each other's calls.
39
+ _trace: contextvars.ContextVar[list | None] = contextvars.ContextVar("amanai_trace", default=None)
40
+
41
+ # Static, declaration-time inventory of every protected tool (for security review).
42
+ _REGISTRY: dict[str, dict] = {}
43
+
44
+
45
+ def _buffer() -> list:
46
+ buf = _trace.get()
47
+ if buf is None:
48
+ buf = []
49
+ _trace.set(buf)
50
+ return buf
51
+
52
+
53
+ def record_event(event: TraceEvent) -> None:
54
+ _buffer().append(event)
55
+
56
+
57
+ def _normalize_args(fn: Callable, args: tuple, kwargs: dict) -> dict:
58
+ """Map a call to {param_name: value} so policies match regardless of whether
59
+ the tool was called positionally or by keyword (`apply_discount(50)` ==
60
+ `apply_discount(pct=50)`)."""
61
+ try:
62
+ sig = inspect.signature(fn)
63
+ bound = sig.bind_partial(*args, **kwargs)
64
+ out: dict[str, Any] = {}
65
+ for name, param in sig.parameters.items():
66
+ if name not in bound.arguments:
67
+ continue
68
+ val = bound.arguments[name]
69
+ if param.kind is inspect.Parameter.VAR_KEYWORD:
70
+ out.update(val) # **kwargs → flatten to top level
71
+ elif param.kind is inspect.Parameter.VAR_POSITIONAL:
72
+ out[name] = list(val) # *args → keep as a list
73
+ else:
74
+ out[name] = val
75
+ return out
76
+ except (TypeError, ValueError):
77
+ # Builtins / C-functions without an introspectable signature.
78
+ out = dict(kwargs)
79
+ if args:
80
+ out["_args"] = list(args)
81
+ return out
82
+
83
+
84
+ def record_tool_call(
85
+ tool_name: str,
86
+ tool_input: dict,
87
+ tool_output: Any,
88
+ *,
89
+ capability: str | None = None,
90
+ context: dict | None = None,
91
+ metadata: dict | None = None,
92
+ ) -> None:
93
+ """Manually record an executed tool-call.
94
+
95
+ Use this when you can't decorate the function. Because the tool has already
96
+ run, Amanai cannot prevent it here; it still evaluates the active policy and
97
+ marks policy violations as `shadowed` so CI/monitoring can catch bypasses.
98
+ """
99
+ action = ActionRequest(
100
+ tool_name,
101
+ dict(tool_input),
102
+ capability=capability,
103
+ context=context if context is not None else get_context(),
104
+ metadata=metadata or {},
105
+ )
106
+ decision = evaluate(action)
107
+ status = "shadowed" if decision.outcome in ("block", "require_approval") else "executed"
108
+ record_event(TraceEvent(action, decision, status=status, output=tool_output))
109
+
110
+
111
+ def tool(
112
+ fn: Callable | None = None,
113
+ *,
114
+ name: str | None = None,
115
+ capability: str | None = None,
116
+ risk: str | None = None,
117
+ input_schema: dict | None = None,
118
+ ):
119
+ """Protect a function with the Action Policy Engine.
120
+
121
+ Usage: `@tool` or `@tool(name="billing.refund", capability="money_movement")`.
122
+
123
+ Behavior depends on the current mode (`enforce` by default):
124
+ * enforce — `block` raises `ToolBlocked`, `require_approval` raises
125
+ `ApprovalRequired`; neither runs the function.
126
+ * shadow — violations execute anyway but are recorded as evidence.
127
+ * test — nothing executes (no side effects); the decision is recorded.
128
+ """
129
+
130
+ def decorate(fn: Callable) -> Callable:
131
+ tool_name = name or fn.__name__
132
+ meta = {
133
+ "capability": capability,
134
+ "risk": risk,
135
+ "input_schema": input_schema,
136
+ "python_name": fn.__name__,
137
+ }
138
+ _REGISTRY[tool_name] = meta
139
+
140
+ @functools.wraps(fn)
141
+ def wrapper(*args, **kwargs):
142
+ inp = _normalize_args(fn, args, kwargs)
143
+ action = ActionRequest(
144
+ tool_name, inp, capability=capability, context=get_context(), metadata=meta
145
+ )
146
+ decision = evaluate(action)
147
+ mode = get_mode()
148
+
149
+ if mode == "test":
150
+ record_event(TraceEvent(action, decision, status="evaluated"))
151
+ return None
152
+
153
+ if decision.outcome == "block" and mode == "enforce":
154
+ record_event(TraceEvent(action, decision, status="blocked"))
155
+ raise ToolBlocked(decision.reason or f"{tool_name} blocked by policy")
156
+ if decision.outcome == "require_approval" and mode == "enforce":
157
+ pending = PendingAction(action, decision)
158
+ record_event(TraceEvent(action, decision, status="pending"))
159
+ raise ApprovalRequired(pending)
160
+
161
+ try:
162
+ result = fn(*args, **kwargs)
163
+ except Exception as e:
164
+ record_event(TraceEvent(action, decision, status="error", error=str(e)))
165
+ raise
166
+
167
+ # In shadow mode a would-be-blocked call still ran — mark it.
168
+ shadowed = decision.outcome in ("block", "require_approval")
169
+ record_event(
170
+ TraceEvent(
171
+ action, decision, status="shadowed" if shadowed else "executed", output=result
172
+ )
173
+ )
174
+ return result
175
+
176
+ return wrapper
177
+
178
+ return decorate(fn) if callable(fn) else decorate
179
+
180
+
181
+ def collect_trace() -> list[TraceEvent]:
182
+ """Return the canonical trace events recorded so far and clear the buffer."""
183
+ buf = _trace.get() or []
184
+ _trace.set([])
185
+ return list(buf)
186
+
187
+
188
+ def collect_tool_calls() -> list:
189
+ """Legacy view: the executed tool-calls as `{tool, input, output}` dicts.
190
+
191
+ Derived from the trace — blocked/pending/evaluated actions are omitted, so a
192
+ test can prove a dangerous function did not run. Drains the buffer (use
193
+ `collect_trace` if you want the full evidence instead)."""
194
+ out = []
195
+ for e in collect_trace():
196
+ if e.status in ("executed", "shadowed"):
197
+ rec = {"tool": e.action.tool, "input": dict(e.action.input), "output": e.output}
198
+ if e.decision.outcome == "warn":
199
+ rec["policy_warning"] = True
200
+ out.append(rec)
201
+ return out
202
+
203
+
204
+ def reset() -> None:
205
+ _trace.set([])
206
+
207
+
208
+ def registered_tools() -> dict[str, dict]:
209
+ """Every `@tool`-protected function and its declared capability/risk/schema —
210
+ the inventory a security engineer reviews for high-risk actions."""
211
+ return dict(_REGISTRY)
212
+
213
+
214
+ def uncovered_tools(policy: Policy | None = None) -> list[str]:
215
+ """Names of registered tools that no rule covers (by tool name or capability)
216
+ in the given (or active) policy — risky actions left silently unprotected."""
217
+ pol = policy if policy is not None else get_policy()
218
+ names, caps = set(), set()
219
+ if pol is not None:
220
+ for r in pol.rules:
221
+ if r.tool:
222
+ names.add(r.tool)
223
+ if r.capability:
224
+ caps.add(r.capability)
225
+ return [
226
+ name
227
+ for name, meta in _REGISTRY.items()
228
+ if name not in names and meta.get("capability") not in caps
229
+ ]