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.
Metadata
Release files for sdf-plan 0.2.9
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| sdf_plan-0.2.9.tar.gz | 31.4 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| sdf_plan-0.2.9-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 70.7 kB
Release files / sdf_plan-0.2.9.tar.gz
| Download URL | sdf_plan-0.2.9.tar.gz |
|---|---|
| Size | 31.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
1fc1f056b2a707f513ef2117dd417d23b244f439a84c896f0c684f7fb33ae5f9
|
|
BLAKE2b-256 checksum How to use checksums |
e38114ed08ced099a640aaa9b27650e9d9553d48c819d2d01330c91a8635c282
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.7
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Feb 26, 2026.
Transparency logRelease files / sdf_plan-0.2.9-py3-none-any.whl
| Download URL | sdf_plan-0.2.9-py3-none-any.whl |
|---|---|
| Size | 39.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
cdb9b214cd7ec7aaa2a482b3e72942983ce393a64e3840f3471ed269e249a3bc
|
|
BLAKE2b-256 checksum How to use checksums |
8575b4e8b05a76b09a383b20dcfd2a92975bdc94e34695d18202f472683436e5
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.7
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Feb 26, 2026.
Transparency log