Add exact-registry Pydantic AI policy toolset - #39
Conversation
Summary by CodeRabbit
WalkthroughChangesThe pull request adds an optional Pydantic AI 2.22.0 adapter. It enforces exact tool registries, policy decisions, deferred approvals, resume-time evidence, and no-network contract coverage. Pydantic AI adapter
Estimated code review effort: 4 (Complex) | ~45 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 8
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs/ARCHITECTURE.md`:
- Around line 23-24: Update the architecture diagram text to use the public
Pydantic AI type name ToolsetTool instead of ToolsetTools, preserving the
existing binding-flow description.
In `@docs/PYDANTIC_AI.md`:
- Around line 96-99: Update the Pydantic AI resume guidance around the
fingerprint description to include the context-contract version and tool context
version as additional fingerprint-bound fields. Specify that both values must be
stored with the pending call and supplied during resume alongside the existing
metadata.
- Around line 25-49: Update the Pydantic AI example’s Agent declaration to
include DeferredToolRequests in output_type, while preserving the existing
toolsets and deps_type configuration so deferred requests are returned through
first.output rather than raising DeferredToolRequestsPending.
In `@integration_tests/test_pydantic_ai_sdk.py`:
- Around line 89-173: Extend the real-agent integration coverage with a
deny-policy test that triggers ToolCallDeniedError through the agent loop and
verifies the tool was not called, then update the approval test flow to
round-trip first.all_messages() through ModelMessagesTypeAdapter before resuming
and confirm the approval still executes. Anchor both changes to the existing
_agent, test_real_agent_approves_exact_deferred_call_once, and
test_real_agent_rejection_never_calls_tool helpers while preserving current
approval and rejection assertions.
In `@requirements-pydantic-ai.txt`:
- Around line 1-2: Regenerate requirements-pydantic-ai.txt from the
pydantic-ai-slim==2.22.0 input using pip-compile --generate-hashes, including
all resolved transitive dependencies and --hash= entries. Update the
corresponding installation flow to pass --require-hashes when consuming this
requirements file.
In `@src/samsarix_ethics/pydantic_ai.py`:
- Around line 207-227: Update the approval enforcement flow around
_verify_approval so stored approval evidence cannot be reused for a later
enforcement run: bind each ToolCallApproval to the current review run or an
expiry, or track and atomically consume each approval as single-use before
returning it. Preserve the existing metadata, approved-state, tool_call_id, and
fingerprint validations, and reject approvals lacking a valid current-run or
single-use binding.
- Around line 359-369: Normalize the pending call arguments in the metadata
validation block before comparing them with the review payload. When the
deferred call exposes JSON-string arguments, use ToolCallPart.args_as_dict()
rather than raw call.args, while preserving already-structured arguments and the
existing call ID and tool name checks.
In `@tests/test_pydantic_ai.py`:
- Around line 67-72: Add a parametrized test case in the
_DeferredCall/build_results test coverage that supplies args as JSON text
representing the same mapping currently used for dict arguments. Invoke
build_results with this deferred call and assert the intended result for pending
ToolCallPart arguments, while retaining the existing dict-args case.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 21267d33-4231-414a-8640-6c03715ecd62
⛔ Files ignored due to path filters (1)
requirements-pydantic-ai.lockis excluded by!**/*.lock
📒 Files selected for processing (20)
.github/workflows/ci.ymlCHANGELOG.mdCONTRIBUTING.mdREADME.mdRELEASING.mdROADMAP.mdSECURITY.mddocs/ADOPTION.mddocs/API.mddocs/ARCHITECTURE.mddocs/PRODUCTIZATION.mddocs/PYDANTIC_AI.mdexamples/pydantic_ai_policy_toolset_demo.pyintegration_tests/test_pydantic_ai_sdk.pypyproject.tomlrequirements-pydantic-ai.txtsrc/samsarix_ethics/__init__.pysrc/samsarix_ethics/pydantic_ai.pytests/test_public_api.pytests/test_pydantic_ai.py
| On approved resume, the adapter recomputes the fingerprint from the current name, validated | ||
| arguments, trusted catalog capabilities, and freshly supplied actor. It then re-evaluates the | ||
| current policy and context facts. Changed arguments or actor, a different call ID, missing or | ||
| malformed evidence, a replay against another call, or a current deny/review never invokes the |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== files =="
git ls-files | rg '(^docs/(ARCHITECTURE|PYDANTIC_AI)\.md$|pydantic|fingerprint|tool_call_id|context-contract|catalog capabilities)' || true
echo
echo "== docs/PYDANTIC_AI.md relevant section =="
if [ -f docs/PYDANTIC_AI.md ]; then
nl -ba docs/PYDANTIC_AI.md | sed -n '70,120p'
else
echo "missing docs/PYDANTIC_AI.md"
fi
echo
echo "== docs/ARCHITECTURE.md fingerprint references =="
if [ -f docs/ARCHITECTURE.md ]; then
rg -n "fingerprint|tool_call_id|context-contract|context contract|capabilities|replay|evidence" docs/ARCHITECTURE.md || true
echo
nl -ba docs/ARCHITECTURE.md | sed -n '1,220p' | rg -n "fingerprint|tool_call_id|context-contract|context contract|capabilities|replay|evidence" -C 3 || true
else
echo "missing docs/ARCHITECTURE.md"
fi
echo
echo "== code search fingerprint-bound terms =="
rg -n "fingerprint|tool_call_id|context-contract|context_contract|contextContract|catalog capabilities|replay|evidence" -S . --glob '!node_modules' --glob '!dist' --glob '!build' | head -300Repository: Deathcharge/samsarix-agent-ethics
Length of output: 575
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== docs/PYDANTIC_AI.md relevant section =="
if [ -f docs/PYDANTIC_AI.md ]; then
awk 'NR>=70 && NR<=120 {printf "%7d\t%s\n", NR, $0}' docs/PYDANTIC_AI.md
else
echo "missing docs/PYDANTIC_AI.md"
fi
echo
echo "== docs/ARCHITECTURE.md fingerprint references =="
if [ -f docs/ARCHITECTURE.md ]; then
awk '/fingerprint|tool_call_id|context-contract|context contract|capabilities|replay|evidence/ {printf "%7d\t%s\n", NR, $0}' docs/ARCHITECTURE.md
else
echo "missing docs/ARCHITECTURE.md"
fi
echo
echo "== code search fingerprint-bound terms =="
rg -n "fingerprint|tool_call_id|context-contract|context_contract|contextContract|catalog capabilities|replay|evidence" -S . --glob '!node_modules' --glob '!dist' --glob '!build' | head -300
echo
echo "== pydantic fingerprints function outline/implementation =="
if [ -f src/samsarix_ethics/pydantic_ai.py ]; then
wc -l src/samsarix_ethics/pydantic_ai.py
rg -n "fingerprint|fingerprint|tool_call_id|context-contract|context_contract|contextContract|catalog_capabilities|capabilities|replay|evidence|evidence" src/samsarix_ethics/pydantic_ai.py -C 4
fiRepository: Deathcharge/samsarix-agent-ethics
Length of output: 44185
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== pydantic fingerprint definitions/usages =="
rg -n "def fingerprint|fingerprint_tool_call|fingerprint\(" src tests | head -200
echo
echo "== approval module =="
fd -i 'approval.*\.(py|json)$' . -t f -d 5 | sort | xargs -r -I{} sh -c 'echo "--- {}"; wc -l "{}"; rg -n "fingerprint|tool_call|context|version|catalog" "{}" -C 3 || true'
echo
echo "== relevant pydantic_ai.py sections =="
awk 'NR>=120 && NR<=285 || NR>=430 && NR<=540 {printf "%7d\t%s\n", NR, $0}' src/samsarix_ethics/pydantic_ai.pyRepository: Deathcharge/samsarix-agent-ethics
Length of output: 25188
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== bound tool gateway fingerprint implementation =="
rg -n "def fingerprint|context_contract|tool_context_version|TOOL_CALL_FINGERPRINT_VERSION|_fingerprint_prepared_tool_call" src/samsarix_ethics/gate.py src/samsarix_ethics -C 4
echo
echo "== pydantic_ai py tests around deferred approvals =="
awk 'NR>=300 && NR<=370 {printf "%7d\t%s\n", NR, $0}' tests/test_pydantic_ai.py
echo
echo "== standalone semantic probe for documented omitted fields =="
python3 - <<'PY'
from pathlib import Path
import re
doc = Path("docs/PYDANTIC_AI.md").read_text()
section_re = re.compile(r"(On approved resume.*?tool\.)(?:\n|$)", re.S)
m = section_re.search(doc)
print("found_resume_paragraph=", bool(m))
if m:
paragraph = m.group(1)
for field in ["tool name", "validated arguments", "trusted catalog capabilities", "freshly supplied actor", "tool_call_id", "context-contract version", "call ID"]:
print(f"{field}: {field in paragraph}")
arch = Path("docs/ARCHITECTURE.md").read_text()
for field in ["tool_call_id", "framework call ID", "context-contract version", "context contract"]:
print(f"arch_mentions_{field}: {bool(re.search(r'[^A-Za-z_]' + re.escape(field) + r'[^A-Za-z_]', arch))}")
PYRepository: Deathcharge/samsarix-agent-ethics
Length of output: 50390
Document the remaining fingerprint-bound fields.
The Pydantic AI resume guidance lists tool name, arguments, catalog capabilities, actor, and call ID; add the context-contract version and tool context version that the v1 fingerprint includes. Store both values with the pending call so resume metadata is not rejected for an omitted, binding field.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/PYDANTIC_AI.md` around lines 96 - 99, Update the Pydantic AI resume
guidance around the fingerprint description to include the context-contract
version and tool context version as additional fingerprint-bound fields. Specify
that both values must be stored with the pending call and supplied during resume
alongside the existing metadata.
| # Exact optional Pydantic AI contract used by CI and the no-network example. | ||
| pydantic-ai-slim==2.22.0 |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Description: Find every consumer of the optional Pydantic AI requirements file and check for hash enforcement.
set -euo pipefail
fd -t f 'requirements-pydantic-ai.txt'
rg -n -C 5 'requirements-pydantic-ai' --glob '!requirements-pydantic-ai.txt'
rg -n 'require-hashes|generate-hashes|--hash=' || echo "no hash enforcement found"Repository: Deathcharge/samsarix-agent-ethics
Length of output: 200
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf 'FILE:\n'
wc -l requirements-pydantic-ai.txt
cat -n requirements-pydantic-ai.txt
printf '\nREPO-WIDE MATCHES EXCLUDING FILE:\n'
rg -n -C 5 'requirements-pydantic-ai|pydantic-ai-slim|require-hashes|generate-hashes|--hash=' --glob '!requirements-pydantic-ai.txt' || true
printf '\nALL HASH-RELATED MATCHES:\n'
rg -n --line-number --no-heading 'require[-_]hashes|generate[-_]hashes|--hash=' . || trueRepository: Deathcharge/samsarix-agent-ethics
Length of output: 492
Generate the hashed Pydantic AI requirements file.
requirements-pydantic-ai.txt currently pins only pydantic-ai-slim==2.22.0 and contains no --hash= entries or transitive pins, so installing from it still resolves dependencies freely. Generate the file with pip-compile --generate-hashes and install with --require-hashes.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@requirements-pydantic-ai.txt` around lines 1 - 2, Regenerate
requirements-pydantic-ai.txt from the pydantic-ai-slim==2.22.0 input using
pip-compile --generate-hashes, including all resolved transitive dependencies
and --hash= entries. Update the corresponding installation flow to pass
--require-hashes when consuming this requirements file.
What changed
WrapperToolsetadapterWhy
Pydantic AI exposes a public toolset execution seam and native deferred approvals, but its approval result is not itself an application authorization boundary. This adapter preserves the framework workflow while keeping current Samsarix policy, exact-call evidence, and one-time consumption authoritative.
Security and compatibility
Validation