← Community
bugfixed

Studio chat, Workflow Builder, and MCP compose bypass ALL PHI/PII redaction -- contradicts the platform's own spec

ShwetaShweta#117d ago · 67 views
affected: Station version: v0.73fixed in: station-v0.74

Reproduction steps:

  1. Extract a clean station-v0.73 tarball, sys.path.insert(0, "workbench").
  2. import routes.llm as L
  3. 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.

  1. 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).

  1. 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."}]).

  1. 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.

5 pts

2 replies

This station has built substantial, carefully-documented
infrastructure specifically to classify, redact, and tokenize PHI/PII
before it leaves the box on an LLM call -- primitives/egress_classifier.py,
egress_policy.py, egress_tokens.py. Studio's terminal chat, the Workflow
Builder, and the MCP compose path never go through any of it. A message
containing a real SSN, email, and card number reaches the outbound
provider payload completely unmodified.

This station's own docs/agent_nodes_spec.md states, describing the
governed LLM entrypoint added for the new agent-node feature: "station_llm.
complete(messages, ...) exists as the single, already-governed LLM
entrypoint (PHI probe, tokenize/redact, trust-tier routing, signed egress
receipt) -- but it is used at compose time (the Builder) and for policy
decisions, never as a runtime node." This explicitly claims the Workflow
Builder's compose calls go through the governed, PHI-probing entrypoint.

They do not. The Builder's actual model callable
(studio_server.compose_raw(), "The model callable the BUILD path hands to
compose_spec") calls routes/llm.py's groq_raw()/_byok_raw()/_hosted_compose()
directly. Studio's terminal chat (studio_server.guarded_chat(), used by
both /api/chat and /api/chat/local) calls routes/llm.py's groq_chat() the
same way. None of these functions, nor anything else in routes/llm.py,
references station_llm, egress_classifier, egress_policy, egress_tokens, or
phi_guard anywhere -- confirmed by reading the full file and by source
inspection. The ONLY real caller of the actual governed entrypoint
(station_llm.complete()) anywhere in the shipped code is the new
agent-node path in workflow_engine.py -- not the Builder, not chat, not
the MCP compose path, despite routes/llm.py's own docstring listing all of
those as its callers.

The chat-side protection that DOES run (chat_guard.pre_gate/post_gate) is a
different, narrower control: it stops a user from extracting the station's
OWN secrets/provider-identity via chat, and scrubs leaked keys out of the
model's reply. It does not classify or redact PII/PHI in the OUTBOUND user
message at all.

ShwetaShweta#117d ago

Verified: module LLM calls (station_llm.complete) classify → policy → tokenize PHI/PII before the provider call, but the interactive surfaces reached the model with the user's raw text — Studio chat, the Workflow Builder /build path, and the BYOK/cap-off compose. Extracted the redaction into a shared guard_egress_messages() and ran it on all three: denied → refuse the call; redacted → send the tokenized copy and detokenize the reply so the operator still sees their data but the provider never did. Fails CLOSED — sensitive data present but policy unevaluable → deny. Confirmed against the code and fixed on the v0.74 batch (verified + regression-tested); ships in station-v0.74. Thanks shweta — important one.

Sign in to reply.