| name | add-backend |
| description | Guide for adding a backend (Rust or Python) to the agent-sec-core security middleware. Use when creating new backends, integrating Rust or Python code into the security middleware, or extending with new backend actions. |
| arguments | [{"name":"backend_name","description":"Name of the new backend (e.g. 'code_verify'). Spaces are converted to underscores for code identifiers.","required":true},{"name":"backend_type","description":"Backend implementation type: 'rust' or 'python'","required":true},{"name":"module_path","description":"For python type: module path (e.g. 'agent_sec_cli.code_verify.verifier'). Required when backend_type=python.","required":false}] |
Adding a Backend to Security Middleware
This skill walks through the complete, end-to-end process of adding a backend
(Rust or Python) to the security middleware, wiring it into the router, and
exposing it through the CLI.
Unified interface: Both Rust and Python backends implement the same
execute(ctx, **kwargs) → ActionResult contract. The middleware doesn't care
about the implementation language.
Backend Type Selection
| Type | Use Case | Pros | Cons |
|---|
| rust | Performance-critical, CPU-intensive tasks | High performance, memory safety | Requires Rust toolchain, compilation |
| python | Rapid development, glue code, existing libraries | Fast iteration, rich ecosystem | Slower execution, GIL limitations |
Naming Convention
Derive all identifiers from the backend_name argument:
| Concept | Rule | Example (backend_name = "code verify") |
|---|
| action_name | lowercase, underscores | code_verify |
| Backend class | PascalCase + Backend | CodeVerifyBackend |
| Python module | {action_name}.py | code_verify.py |
| lifecycle category | same as action_name | code_verify |
Rust-specific (only when backend_type=rust):
| Concept | Rule | Example |
|---|
| Rust function | same as action_name | code_verify |
| Request struct | PascalCase + Request | CodeVerifyRequest |
| Response struct | PascalCase + Response | CodeVerifyResponse |
1. Architecture Overview
Both backend types follow the same execution flow:
agent-sec-cli ──→ security_middleware.invoke("{action_name}", **kwargs)
│
├─ router.get_backend("{action_name}")
│ └─ _REGISTRY["{action_name}"] → "security_middleware.backends.{action_name}"
│ └─ lazy import → {ActionName}Backend()
│
├─ backend.execute(ctx, **kwargs) → ActionResult
│ │
│ ├─ [Rust] from agent_sec_cli._native import {action_name}
│ │ {action_name}(json_in) → json_out
│ │
│ └─ [Python] import {module_path}
│ module.function(**kwargs) → result
│
└─ lifecycle.post_action() → SecurityEvent → JSONL
Key contract: Every backend is a Python class with an execute(ctx, **kwargs) → ActionResult
method. The implementation language (Rust/Python) is an implementation detail — the middleware
never calls Rust or module functions directly.
2. Create the Python Backend Wrapper
The Python backend wrapper is the unified interface that the middleware calls. It delegates
to either Rust or Python implementation based on backend_type.
2.1 Choose Template
- For
backend_type=rust: Use templates/rust_backend.py
- For
backend_type=python: Use templates/python_backend.py
2.2 Create Backend File
Create agent-sec-cli/src/agent_sec_cli/security_middleware/backends/{action_name}.py
Copy the appropriate template and replace placeholders:
{backend_name} → actual backend name (e.g., "code_verify")
{BackendName} → PascalCase class name (e.g., "CodeVerify")
{action_name} → action name for Rust calls (e.g., "code_verify")
{module_path} → Python module path (only for python type, e.g., "agent_sec_cli.code_verify.verifier")
Convention: Class name = PascalCase of module name + Backend.
IMPORTANT — stdout / error contract: The CLI (agent-sec-cli) only
prints result.stdout and result.error. If a backend returns an ActionResult
with both stdout and error empty, the CLI produces no output at all.
Every ActionResult must populate at least one of:
| Field | When to set |
|---|
stdout | Always on success — human-readable text for the terminal |
error | Always on failure — written to stderr by the CLI |
A helper like _format_stdout() keeps formatting in one place and makes it
easy to test independently.
3. Register Backend in Router and Lifecycle
3.1 Register in Router
Edit agent-sec-cli/src/agent_sec_cli/security_middleware/router.py — add to _REGISTRY:
_REGISTRY: Dict[str, str] = {
"{action_name}": "agent_sec_cli.security_middleware.backends.{action_name}",
}
3.2 Add Lifecycle Category Mapping
Edit agent-sec-cli/src/agent_sec_cli/security_middleware/lifecycle.py — add to _ACTION_CATEGORY:
_ACTION_CATEGORY: Dict[str, str] = {
"{action_name}": "{action_name}",
}
3.3 Add CLI Entry Point
Edit src/agent_sec_cli/cli.py — add a new @app.command() function:
@app.command()
def {action_name}(
param1: str = typer.Option("", "--param1", help="Parameter 1"),
):
"""{ActionName} description."""
result = invoke("{action_name}", param1=param1)
if result.stdout:
typer.echo(result.stdout)
if result.error:
typer.echo(result.error, err=True)
raise typer.Exit(code=result.exit_code)
Now callable as:
agent-sec-cli {action_name} --param1 value
4. Rust-Specific Steps (backend_type=rust)
Skip this section if backend_type=python.
Sub-skill available: For complex Rust modules (multiple files, shared types,
or workspace-scale projects), use the add-rust-module sub-skill at
sub-skills/add-rust-module.md. It covers single-file modules, directory modules,
and Cargo workspace layouts.
The steps below cover the simple inline case (adding a function directly to
src/lib.rs). For anything beyond a single function, delegate to the sub-skill:
add-rust-module module_name="{action_name}" functions="{action_name}"
4.1 Add Rust Function to lib.rs
Edit agent-sec-cli/src/lib.rs — add your Rust function above the #[pymodule] block:
#[derive(Deserialize)]
struct {ActionName}Request {
}
#[derive(Serialize)]
struct {ActionName}Response {
}
fn do_{action_name}(req: &{ActionName}Request) -> Result<{ActionName}Response, String> {
todo!("implement {action_name} logic")
}
#[pyfunction]
fn {action_name}(py: Python<'_>, request_json: &str) -> PyResult<String> {
let req: {ActionName}Request = serde_json::from_str(request_json)
.map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(
format!("Invalid JSON: {e}")
))?;
py.allow_threads(|| {
let resp = do_{action_name}(&req)
.map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e))?;
serde_json::to_string(&resp)
.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(
format!("Serialization failed: {e}")
))
})
}
4.2 Register in #[pymodule]
Add this line inside the _native pymodule function in src/lib.rs:
m.add_function(wrap_pyfunction!({action_name}, m)?)?;
4.3 Update Python Backend Wrapper
Edit agent-sec-cli/src/agent_sec_cli/security_middleware/backends/{action_name}.py:
from agent_sec_cli._native import {action_name} as rust_{action_name}
class {BackendName}Backend:
def execute(self, ctx, **kwargs) -> ActionResult:
try:
req = json.dumps(kwargs)
resp_json = rust_{action_name}(req)
resp = json.loads(resp_json)
return ActionResult(
success=True,
data=resp,
stdout=self._format_stdout(resp),
)
except Exception as exc:
return ActionResult(success=False, error=f"Rust error: {exc}", exit_code=1)
Key changes:
- No
RUST_AVAILABLE check needed (Rust code is always available)
- Import directly from
agent_sec_cli._native
- No Python fallback (unless you intentionally keep it)
4.4 Build and Test
4.4.1 Rebuild with maturin
cd agent-sec-cli
uv run maturin develop --release
4.4.2 Test from Python
from agent_sec_cli._native import {action_name}
import json
req = json.dumps({"param": "value"})
resp = {action_name}(req)
print(json.loads(resp))
4.4.3 Run Rust Tests
cd agent-sec-cli
cargo test
4.5 Add Dependencies (if needed)
If your Rust function needs additional crates (e.g., serde, serde_json),
edit agent-sec-cli/Cargo.toml:
[dependencies]
pyo3 = { version = "0.20", features = ["extension-module"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
4.6 Complex Rust Modules
If the Rust logic is too large for a single function in lib.rs, use the
add-rust-module sub-skill to create a dedicated module:
add-rust-module module_name="{action_name}" functions="{action_name}" complex="true"
This will:
- Create
src/{action_name}/mod.rs with proper sub-module layout
- Wire
mod {action_name}; into src/lib.rs