Reproduction steps:
- Extract a clean station-v0.73 tarball, sys.path.insert(0, "workbench").
- import routes.llm as L
- Confirm (by reading the file or via inspect.getsource(L)) that
"station_llm", "egress_classifier", "egress_policy", "egress_tokens",
and "phi_guard" appear nowhere in it.
- Stub L._model_post to capture its payload argument instead of making a
real HTTP call; stub L.groq_key to return a fake key (BYOK path).
- Call L.groq_chat([{"role": "user", "content": "My SSN is 219-09-9999
and my email is bob@example.com, card 4111 1111 1111 1111 -- please
build a workflow that emails me."}]).
- Inspect the captured payload.
Expected: per the platform's own documented threat model ("Exfiltration
via the model. station_llm already probes/redacts/tokenizes egress; the
agent's prompts go through the same path" -- docs/agent_nodes_spec.md),
any LLM call carrying user-typed content should have PHI/PII classified
and either redacted or tokenized before it reaches a third-party provider.
Actual: the captured outbound payload contains the SSN, email, and card
number verbatim -- no [REDACTED:...] placeholder, no [TOK_...] token,
nothing. The content reaches Groq (or OpenAI, or the hosted RailCall
gateway) exactly as the user typed it.
Root cause: routes/llm.py's groq_chat()/groq_raw()/_hosted_compose() (the
functions actually behind Studio chat and the Workflow Builder) were never
wired to primitives/egress_classifier.py + egress_policy.py +
egress_tokens.py the way primitives/agent_gate.py's LLM path is via
station_llm.complete() -- and docs/agent_nodes_spec.md's description of the
Builder's model path does not match the code that ships.
Suggested fix: route Studio chat, the Workflow Builder, and the MCP compose
path through station_llm.complete() (or apply the same
classify->redact/tokenize step directly inside routes/llm.py before every
outbound call), so every LLM egress path -- not only the new agent-node
one -- gets the same PHI/PII protection the platform already built and
documents as standard.