Tool-call safety gate + deterministic confirm tokens for agent workflows.
Project description
sdf-plan
Trusted control for every agent tool call.
Most AI agent frameworks make it dangerously easy for the model to call tools that delete files, charge cards, or wipe production data.
The usual safety fixes ("better system prompt" or LangGraph interrupts) haven't felt sufficient for real production agents.
So I built sdf-plan -- a lightweight, deterministic, local-first runtime safety layer that sits right before tool execution.
You get real ToolGate decisions:
ALLOWREQUIRE_CONFIRM(cryptographically signed tokens scoped to workspace + tool + args)WARNBLOCK
Plus automatic idempotency keys, replay protection, tool-mode linting, and PlanSpec preflight.
30-second quickstart
pip install sdf-plan
# Set in production: export SDF_PLAN_TOKEN_SECRET="your-very-strong-secret"
Live Demo
- Demo repo: https://github.com/directiveproto/sdf-plangate-demo
- Run locally:
python -m plangate_demo.main - Flow:
REQUIRE_CONFIRM -> CONFIRM -> ALLOW
30-Second Quickstart (ToolGate-first)
from sdf_plan import GateContext, confirm, propose
ctx = GateContext(workspace_id="demo-ws")
first = propose(
tool_name="filesystem.write",
args={"path": "/tmp/demo.txt", "content": "hello"},
ctx=ctx,
)
print(first.decision.value) # REQUIRE_CONFIRM
token = first.resume.token
_ = confirm(token, user_ok=True)
second = propose(
tool_name="filesystem.write",
args={"path": "/tmp/demo.txt", "content": "hello"},
ctx=ctx,
meta={"confirmed_token": token},
)
print(second.decision.value) # ALLOW
Expected flow: REQUIRE_CONFIRM -> CONFIRM -> ALLOW
Install
pip install sdf-plan
Production note: set a strong SDF_PLAN_TOKEN_SECRET. Development fallback is warning-only and not for deployed environments.
Replay Protection (jti Store)
confirm(...) is stateless in OSS mode. For strict replay protection, store token jti values server-side.
from sdf_plan._internal.token import verify_token
from sdf_plan import confirm
used_jti: set[str] = set()
def confirm_once(token: str):
payload = verify_token(token)
jti = payload.get("jti")
if jti and jti in used_jti:
raise RuntimeError("replay detected")
result = confirm(token, user_ok=True)
if result.confirmed and jti:
used_jti.add(jti)
return result
5-Minute First Success
python examples/tool_gate_quickstart.py
python examples/tool_gate_openai_input.py
python examples/plan_mode_preflight.py
Adapter vs Legacy Integration
| Path | Use case | Status |
|---|---|---|
sdf_plan.adapters.* |
Runtime ToolGate decisions (propose/confirm) |
Recommended |
sdf_plan.integrations.* |
Legacy decomposition-client flow | Legacy (compat only) |
Comparison
| Capability | sdf-plan ToolGate |
Manual checks | LangGraph interrupts only | NVIDIA NeMo guardrails |
|---|---|---|---|---|
Deterministic decision enum (ALLOW/WARN/REQUIRE_CONFIRM/BLOCK) |
Yes | Usually ad hoc | Partial | Policy dependent |
Signed confirm token + resume binding (jti, args, scope) |
Yes | No | No | No |
| Idempotency keying for tool proposals | Yes | No | No | No |
| Built-in thin adapters for agent frameworks | Yes (LangGraph, CrewAI, LangChain) |
No | LangGraph-only | NeMo-native |
| Drop-in local Python package for CI and runtime gates | Yes | N/A | No | Separate stack |
CLI
sdf-plan lint path/to/plan.json
sdf-plan classify --tool filesystem.write
What You Get
- ToolGate runtime decisions (
ALLOW | REQUIRE_CONFIRM | WARN | BLOCK) - Signed confirmation tokens (
jti, expiry, scope/tool/args binding) + resume flow - Idempotency key derivation from scope + tool + canonical args
- Tool-mode lint rules + policy defaults
- PlanSpec lint and preflight (optional mode)
- LangGraph adapter (official thin wrapper for v0.2.x)
Support Matrix (v0.2.8)
- Official maintained adapter:
LangGraph - Thin adapters:
CrewAI,LangChain - Legacy integration path:
sdf_plan.integrations.*(decomposition client, not ToolGate runtime gating) - Direct parser support: OpenAI-style tool calls, generic tool call JSON, PlanSpec
- BYO adapter support: any framework that can pass
(tool_name, args, meta, run_context)intopropose(...) - Deferred official adapters: additional framework-specific variants beyond thin wrappers
Strict Mode Checklist
- Set
SDF_PLAN_TOKEN_SECRET(no fallback in non-development). - Pass
ctx.workspace_idfor write tools. - Enable
strict_args=True. - Optionally set
tool_args_validatorfor deep per-tool schema validation.
Public API Stability
Top-level imports are a stable facade:
from sdf_plan import propose, confirm
The facade remains stable while internals evolve; core logic stays in sdf_plan/gate, not in __init__.py.
Optional PlanSpec Mode
Plan mode remains supported for existing users.
from sdf_plan import lint_plan, policy_annotate, preflight_lint
plan = {
"steps": [
{
"id": "S1",
"type": "ACT",
"title": "send email",
"intent": "send email",
"inputs": [],
"outputs": ["ctx.sent"],
"depends_on": [],
"stop_condition": "Step S1 completed",
"fallback": "reduce_scope",
"idempotency_key": "idem-1",
}
]
}
plan, summary = policy_annotate(plan)
findings = lint_plan(plan, max_steps=12, safety_mode="safe")
preflight_lint(plan, max_steps=12, safety_mode="safe")
Guides
docs/INDEX.md(start here: full docs map)docs/API_REFERENCE.mddocs/ARCHITECTURE.mddocs/SECURITY_MODEL.mddocs/INTEGRATION_RECIPES.mddocs/TROUBLESHOOTING.mddocs/MIGRATION_PLANSPEC_TO_TOOLGATE.mddocs/PRODUCTION_HARDENING.mddocs/ADAPTER_TEMPLATE.mddocs/POLICY_TUNING.mddocs/TOOL_CLASSIFICATION.mddocs/COMPATIBILITY.mddocs/RELEASING.md
Examples
examples/tool_gate_quickstart.pyexamples/tool_gate_openai_input.pyexamples/plan_mode_preflight.pyexamples/adapter_minimal.pyexamples/langgraph_plangate_demo.pyexamples/crewai_plangate_demo.py(community-style example, not an official adapter contract in v0.2.0)examples/langgraph-full/demo.py(full ToolGate-oriented LangGraph node wiring)examples/crewai-thin-wrapper/demo.py(thin CrewAI wrapper integration)
Testing (CI Parity)
Install dev/test dependencies:
pip install -e ".[dev]"
Fast local checks (matches PR path):
pytest -q -m "not slow" tests/unit
pytest -q tests/contract/test_gate_contract.py tests/contract/test_adapter_contract.py
pytest -q -m "not slow" tests/integration/test_openai_variants_normalization.py tests/integration/test_generic_toolcall_normalization.py tests/integration/test_planspec_to_ir.py tests/integration/test_tool_gate_flow.py tests/integration/test_tool_gate_concurrency.py tests/integration/test_plan_and_tool_mode_coexist.py tests/compat/test_planspec_roundtrip_best_effort.py
pytest -q tests/unit/test_token_security.py tests/integration/test_tool_gate_concurrency.py
Coverage gates:
pytest -q --cov=sdf_plan --cov-report=term-missing --cov-fail-under=70 tests/unit tests/integration
pytest -q --cov=sdf_plan.gate --cov-fail-under=70 tests/unit tests/integration
pytest -q --cov=sdf_plan.policy --cov-fail-under=70 tests/unit tests/integration
pytest -q --cov=sdf_plan.inputs --cov-fail-under=70 tests/unit tests/integration
Nightly/slow checks:
pytest -q -m slow tests/integration/test_fuzz_inputs.py tests/integration/test_perf_budget.py
Packaging smoke:
python -m build
twine check dist/*
pip install dist/*.whl
python -c "import sdf_plan; print('sdf_plan import ok')"
Compatibility
Use Cloud schema hash checks to detect contract drift:
from sdf_plan.compat import assert_schema_compat, package_version
assert_schema_compat(package_version(), "schema_hash_from_/v1/schema")
Releases
- Git tags use
vX.Y.Zformat. - GitHub Releases notes mirror
CHANGELOG.md. - PyPI releases are published from tagged workflow runs.
- See
docs/RELEASING.mdfor the exact process.
License
This project is licensed under the MIT License.
See LICENSE for the full text.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file sdf_plan-0.2.9.tar.gz.
File metadata
- Download URL: sdf_plan-0.2.9.tar.gz
- Upload date:
- Size: 31.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1fc1f056b2a707f513ef2117dd417d23b244f439a84c896f0c684f7fb33ae5f9
|
|
| MD5 |
901922195a7c80171241a3d5ff8c6d14
|
|
| BLAKE2b-256 |
e38114ed08ced099a640aaa9b27650e9d9553d48c819d2d01330c91a8635c282
|
Provenance
The following attestation bundles were made for sdf_plan-0.2.9.tar.gz:
Publisher:
release.yml on directiveproto/sdf-plan
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
sdf_plan-0.2.9.tar.gz -
Subject digest:
1fc1f056b2a707f513ef2117dd417d23b244f439a84c896f0c684f7fb33ae5f9 - Sigstore transparency entry: 1000325883
- Sigstore integration time:
-
Permalink:
directiveproto/sdf-plan@d243b39199d01e0afeb5796d1c13423470e8a257 -
Branch / Tag:
refs/tags/v0.2.9 - Owner: https://github.com/directiveproto
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@d243b39199d01e0afeb5796d1c13423470e8a257 -
Trigger Event:
push
-
Statement type:
File details
Details for the file sdf_plan-0.2.9-py3-none-any.whl.
File metadata
- Download URL: sdf_plan-0.2.9-py3-none-any.whl
- Upload date:
- Size: 39.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
cdb9b214cd7ec7aaa2a482b3e72942983ce393a64e3840f3471ed269e249a3bc
|
|
| MD5 |
5c47964d75479cbfc98ecf6c7a8129ac
|
|
| BLAKE2b-256 |
8575b4e8b05a76b09a383b20dcfd2a92975bdc94e34695d18202f472683436e5
|
Provenance
The following attestation bundles were made for sdf_plan-0.2.9-py3-none-any.whl:
Publisher:
release.yml on directiveproto/sdf-plan
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
sdf_plan-0.2.9-py3-none-any.whl -
Subject digest:
cdb9b214cd7ec7aaa2a482b3e72942983ce393a64e3840f3471ed269e249a3bc - Sigstore transparency entry: 1000325936
- Sigstore integration time:
-
Permalink:
directiveproto/sdf-plan@d243b39199d01e0afeb5796d1c13423470e8a257 -
Branch / Tag:
refs/tags/v0.2.9 - Owner: https://github.com/directiveproto
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@d243b39199d01e0afeb5796d1c13423470e8a257 -
Trigger Event:
push
-
Statement type: