The shape of the protocol
The privileged side builds the list of options for each case and publishes it. The model gets the case and the list, and hands back three things: which option, how confident, and why. No target field. No command field. No address field. There is nowhere to put one.
If a response comes back carrying a target anyway, that is not a malformed message. That is a compromised worker, or an injection that worked, and it pages as tampering, because there is no innocent way for that field to exist.
The privileged side then looks up what that option ID means in a file it wrote itself, minutes earlier, and never showed anyone. An allowlist would still mean receiving an attacker-influenced value and checking it. Here there is nothing to check, because there is nothing to tamper with.
ALLOWED_CHOICE_KEYS = {"schema", "case_id", "option_id", "option_nonce",
"confidence", "rationale", "worker"}
OPTION_ID_RE = re.compile(r"^O(?:[1-9]|1[0-9])$")
# a complete, valid response:
{
"schema": "soar-choice/v1",
"case_id": "auto-…-1782930902",
"option_id": "O3",
"option_nonce": "… 32 hex …",
"confidence": 0.82,
"rationale": "repeat auth failures from one source, no successful login",
"worker": "w-07"
}