Skip to content

feat(mcp): cover every armed agentic-security and prompt surface - #1

Merged
routeplane-ops merged 1 commit into
mainfrom
feat/armed-agentic-and-prompt-surfaces
Jul 27, 2026
Merged

feat(mcp): cover every armed agentic-security and prompt surface#1
routeplane-ops merged 1 commit into
mainfrom
feat/armed-agentic-and-prompt-surfaces

Conversation

@routeplane-ops

Copy link
Copy Markdown
Collaborator

The prod gateway's agentic-security and prompt surfaces went live, so the SDK needs to actually reach them. It didn't.

The MCP resource was written against invented shapes

Every one of these was verified against the handlers in crates/routeplane/src/mcp_api.rs:

Method Sent Gateway reads
run_step {agent_id, tool, server, args} {agent_id, run_id, cost_micro_usd}
inspect_result {result: {...}} {content: "..."}
hitl.approve / .deny {decision_id, reason} {id, note}
authorize_tool_call {agent_id, tool, server} also argument_urls, arguments, server_manifest, run_id
list_runs expects a bare array {"runs": [...]} envelope

None of these calls could have succeeded against a live gateway. All corrected.

Six routes were missing entirely

sampling/evaluate, security/events, receipt/issue, receipt/verify, anomaly/status/{id}, anomaly/clear — added, grouped into receipts and anomaly sub-namespaces mirroring the URL structure, alongside the existing hitl.

Denials are now values, not exceptions

The gateway is default-deny, so a structured 422 {outcome, reason} is the most common outcome on this surface. The shared _post called raise_for_status() unconditionally, which turned the normal path into the error path.

authorize_tool_call, inspect_result, sampling_evaluate, and run_step now return a typed Decision / RunStep covering both outcomes, including the retry_after_ms / limit / window_ms backoff envelope a 429 quota refusal carries. Both parsers fail closed — anything that is not an explicit allow / continue reads as a deny / stop, so a malformed body can never be mistaken for permission.

A 404 still raises: not-entitled is not a verdict, and these routes 404 rather than 403 precisely so an un-entitled tenant never learns the surface exists. Documented in the module docstring and the README, since it is a confusing failure mode otherwise.

_base._post/_get take an opt-in expect tuple of non-raising statuses; every other resource is unchanged.

Prompts

Adds the missing policy (error/empty) and cohort arguments, and passes arbitrary chat overrides (temperature, max_tokens, user, …) through to the completions body, which the gateway flattens into the chat request.

Fixes provider: it was going into the request body, where the flattened chat request ignores unknown keys — so it silently did nothing. It now travels as x-routeplane-provider, where the gateway reads it. Regression-tested.

Verification

Ran exactly as CI does — ruff check, ruff format --check, mypy (strict), pytest: 76 passed, no lint or type findings.

Test coverage added for the corrected wire shapes, both deny paths, the quota backoff envelope, fail-closed parsing of an empty body, 404-still-raises, envelope unwrapping, and the provider-as-header regression.

Also adds examples/agentic_security.py walking a mediated tool loop end to end.

The MCP resource was written against invented request shapes: run_step sent
{agent_id, tool, server, args} where the gateway reads {agent_id, run_id,
cost_micro_usd}; inspect_result sent {result: {...}} where the gateway reads
{content: "..."}; the HITL legs sent {decision_id, reason} where the gateway
reads {id, note}; and list_runs treated a {"runs": [...]} envelope as a bare
array. None of those calls could have worked against a live gateway.

Correct all of them against the handlers in mcp_api.rs, and add the six routes
that were missing entirely: sampling/evaluate, security/events, receipt/issue,
receipt/verify, anomaly/status, anomaly/clear.

Policy verdicts are now values rather than exceptions. The gateway is
default-deny, so a 422 deny is the single most common outcome on this surface;
raising HTTPStatusError for it made the normal path the error path. authorize,
inspect, sampling, and run_step return a typed Decision/RunStep carrying the
outcome, reason, status, and — on a 429 quota refusal — the backoff envelope.
Both parsers fail closed: anything that is not an explicit allow/continue reads
as a deny/stop. A 404 still raises, since not-entitled is not a verdict.

Prompts gains the missing `missing` policy and `cohort` arguments, and passes
arbitrary chat overrides through to the completions body. `provider` moves from
the body to x-routeplane-provider, where the gateway actually reads it — the
completions body is flattened into a chat request, which ignores an unknown
provider key, so the old argument was a silent no-op.
@routeplane-ops
routeplane-ops merged commit c403124 into main Jul 27, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant