<!-- OpenSmartRoute: osr-security-hardening. Source https://github.com/isathish/OpenSmartRoute/blob/main/.claude/skills/osr-security-hardening/SKILL.md; HTML https://opensmartroute.ai/docs/skills/osr-security-hardening -->
---
name: osr-security-hardening
description: Harden OpenSmartRoute deployments against prompt injection, gadget/steering attacks, PII leakage, runaway agents and untrusted tool arguments - InputGuard and GuardMiddleware, learned gadget detector, PII redaction, ResourceLimits for tools and agents, OriginPolicy for tool-parameter provenance, signed MCP manifests, secret loading and the safety-routing red-team suite. Use when reviewing security of a routing setup, handling PII or private data boundaries, or wiring MCP tools safely.
license: Apache-2.0
compatibility: OpenSmartRoute >= 0.4, Python >= 3.10
metadata:
  author: opensmartroute
  osr-domains: "coding legal general"
  osr-tags: "opensmartroute security injection pii mcp"
  osr-quality-prior: "0.85"
  osr-primary: "false"
---

# OpenSmartRoute security hardening

Threat model: the router sits between untrusted text (users, retrieved documents, tool output) and
expensive or state-changing targets. Defend the input, the catalogue, and the execution budget.

## 1. Input guard (every deployment)

```python
from opensmartroute.security import InputGuard, GuardMiddleware
guard = InputGuard(max_chars=32_000, max_tokens_est=8_000, gadget_threshold=0.6,
                   reject_on_gadget=False, learned=False, strip_steering=True)
app = RouterBuilder(reg).with_defaults().with_middleware(GuardMiddleware(guard, redact=True)).build()
```

- `guard.inspect(text) -> GuardReport(ok, reasons, gadget_suspected, clean_text, score, steering_removed)`.
- `GuardMiddleware` raises `SecurityError` when `not ok`; with `redact=True` PII is replaced before
  routing (original in `request.context["_original_text"]`, map in `context["_pii_map"]`).
- `learned=True` uses `GadgetDetector.default()`; train your own with `osr train --gadget gadget.json`
  and `GadgetDetector.load`.
- Pure functions for ad-hoc checks: `injection_risk(text)`, `inspect_injection(text)`,
  `strip_steering(text)`, `sanitize_for_prompt(text)`, `shannon_entropy(text)`.

## 2. Data boundaries and PII in the catalogue

- Targets declare `constraints.data_boundary: public|private|on_prem` and `pii_allowed`. Requests set
  `RequestConstraints(data_boundary=..., contains_pii=...)`; the `PIISignal` also detects PII.
- Policy rejects mismatches (`DataBoundaryRule`, `pii` rule) - verify in `trace.policy_rejections`.
- Pair with a rule: `when: {contains_pii: true} prefer: [llm-onprem] pin: true`.
- Tenant isolation: `constraints.tenants` on targets + `TenantMiddleware(require=True)`.

## 3. Budgets for tools, agents and workflows

```python
from opensmartroute.security import ResourceLimits, ResourceLimiter, ResourceLimitMiddleware, apply_limits
limiter = ResourceLimiter(ResourceLimits(max_steps=50, max_tool_calls=100, max_depth=4,
                                         max_total_tokens=500_000, max_cost_usd=5.0, max_wall_s=900.0))
apply_limits(registry, limiter, kinds=("tool", "agent", "workflow"))   # wraps handlers per task_id
builder.with_middleware(ResourceLimitMiddleware(limiter))
```
Exceeding a limit raises `ResourceLimitExceeded` (a `SecurityError`). Budgets key on
`request.context["task_id"]`.

## 4. Tool-parameter provenance (indirect injection)

```python
from opensmartroute.security import OriginPolicy, OriginRule, mark_untrusted, apply_origin_policy
mark_untrusted(request, retrieved_text, source="retrieved")         # tag text you did not author
policy = OriginPolicy(rules=[OriginRule(state_changing=["delete_*", "send_*"], allowed_origins=["user"])])
apply_origin_policy(registry, policy)                                # wraps tool handlers
```
A state-changing tool whose arguments were copied from untrusted text raises `OriginViolation`.

## 5. MCP catalogues

- `tools_from_mcp(payload, max_description_risk=0.6, guard=InputGuard())` drops tools whose
  descriptions look like steering ("ignore previous instructions ...").
- Sign catalogues offline and verify at load: `osr mcp-manifest tools.json --key-env OSR_MCP_KEY
  --server name --out manifest.json`; then `tools_from_manifest(manifest, key, require_signature=True,
  max_age_s=86400)`. `--algorithm ed25519` needs the `crypto` extra.

## 6. Secrets

`load_secret("OPENAI_API_KEY")` reads the env var or the file named by `OPENAI_API_KEY_FILE`
(Kubernetes secret mounts). Never put secrets in `targets.yaml`; handlers read them at call time.
Telemetry never logs raw text.

## 7. Red-team suite

`osr -t targets.yaml safety [--learned-guard] [--json]` or `run_safety_suite(app.route)` runs
`SafetyCase`s (jailbreaks, PII exfiltration, boundary hops, cost bombs) and reports `pass_rate` and
`by_category`. Add it next to `osr eval --min-accuracy` in CI.

## Review checklist

- [ ] `GuardMiddleware` first in the chain, `redact=True` when any target is `pii_allowed: false`.
- [ ] Every private/on-prem target has `data_boundary` and `regions` set; requests carry tenant.
- [ ] `apply_limits` on all `tool`/`agent`/`workflow` targets; `max_cost_usd` matches the tenant budget.
- [ ] MCP tools loaded from a signed manifest; `max_description_risk` left at or below 0.6.
- [ ] Secrets via `load_secret`; audit sink enabled; `osr safety` passes in CI.
