Risk OS Red Try Stoa

Threshold — a generic voice-agent stress test

Threshold is a fictional voice-based screening/intake platform — the kind of thing that could sit under hiring pre-screens, research-participant recruitment, or program eligibility checks. It's deliberately horizontal: the point isn't the business, it's the risk shape a live, unsupervised conversation with a real person creates, and whether Stoa's detectors hold up against it. Eleven agents, MCP client + server + two deliberate MCP detection gaps, one finding planted per Stoa risk dimension, and a genuine participant-incentive payout agent so all 14 assurance-packet areas — including Economic authority — have real content, not an empty box. The full source is in examples/threshold-voice.

Architecture

caller (voice) → orchestrator → conversation_agent → intake MCP server
                     ↓                                     ↑
                extraction_agent                  escalation_agent
                     ↓                                     ↓
                 (database)                          (database, no gate)
                                                            ↓
                                                       notifier → analytics

The agents

AgentFrameworkPlanted to exercise
orchestratorhand-rolledAI001 — voice transcript → prompt
conversation_agentLangGraphAI005 floating-alias; declares a data class Stoa can't scan for
extraction_agenthand-rolledAI002/sql, CTRL005 — lands indeterminate on the autonomy ladder
escalation_agentCrewAIAI002/sql, AI003 — inferred unrestricted_autonomous, declared human_approved → DECL001
incentive_agentLangChainpays participants via Stripe (move_funds), a real spend ceiling → inferred bounded_autonomous — undeclared → DECL003
consent_managerLangChainthe well-controlled baseline — zero contradictions
notifierhand-rolled (not agentic)AI006 — participant email → third-party analytics
intake_toolsMCP servera genuine agentic surface in its own right
calendar_clientpure MCP client (py)invisible on purpose — no other agentic signal
widget_clientpure MCP client (ts)misclassified on purpose — labeled "MCP server" despite being a client

All four autonomy-ladder levels, one fixture

LevelAgentWhy
recommend_onlyconsent_manager, conversation_agent, orchestratorno side-effecting path from model output, or no other correlating signal
bounded_autonomousincentive_agenta real hardcoded spend ceiling, no approval gate
unrestricted_autonomousescalation_agenthigh-impact database write, no approval, no bounding
indeterminateextraction_agenta real sink and a real high-impact capability, but no tool-binding evidence — the classifier won't guess

human_approved is the one level not demonstrated here — Meridian's payments.py already covers it.

Two discovered detection gaps

Building this fixture surfaced two real gaps worth knowing about — the point of a stress-test fixture is to make gaps concrete, not paper over them.

MCP client vs. server isn't disambiguated. intake_tools.py is a genuine server; widget_client.ts only imports the SDK's client subpath. Both get frameworks=["mcp"] and render identically as an MCP-server node, because the JS/TS trigger is a bare substring match on @modelcontextprotocol/sdk with no path awareness. calendar_client.py shows the inverse on the Python side: pure client glue with no other agentic signal never clears the agentic-signal floor at all.

A declared data class can be scan-blind. conversation_agent accurately declares data_classes = ["personal"]. DECL004 (undeclared data class) only cross-checks the "authentication" class today, tied to a leaked-secret signal — there's no scan-side pattern for "this code touches personal data" at all. The declaration is accurate and will never be contradicted, correct or not.

Run it yourself: stoa scan examples/threshold-voice, or read the full README for the dimension-coverage map. The run-e2e.sh driver asserts 26 checks, including both MCP gaps and the DECL001/DECL003 contradictions.