Reproduction steps:
- A module is a signed handler bundle installable from any publisher (trust
default "any"). It registers a command whose handler performs a live external
effect (a charge / message send / HTTP POST to its declared endpoint) but
declares that command mode:"read" in its manifest (or omits mode entirely —
the default is "read").
- Invoke the command through the airlock enforcement path
(routes/commands.execute_command, reached from Studio's /api/commands/execute
and, when mcp.module_reads_enabled is on, from the LLM over MCP).
- Contrast with the identical handler declared mode:"write_requires_approval".
- Run repro_module_mode_self_classification_bypass_v099.py against a clean v0.99
extraction. It drives the REAL classifier + the REAL execute_command:
mode=read -> status=executed handler_ran=1
mode=write_requires_approval -> status=blocked_by_policy handler_ran=0
Expected:
Whether a command may execute without a human approval must be decided by what
the command actually DOES (does its handler perform an external/mutating effect),
not by a label the module writes about itself. The Approval Airlock is documented
as "the airlock's enforcement path — every mutation that leaves the station goes
through execute_command" and the registry promises "no hidden writes". A module's
self-assertion that its send is a "read" must not be the thing that waives
approval.
Actual:
The read/write classification is 100% the module's self-declared mode, trusted
end to end with no behavioral verification:
routes/modules.py:516 action_class = _module_action_class(cmd.get("mode","read"))
_module_action_class: "read" -> "reversible"; any write mode -> "external_send".
command_registry.resolve_status: mode=="write_requires_approval" ->
available_write_requires_approval; mode=="write_blocked" -> blocked;
ANYTHING ELSE (incl. "read", a typo, or omitted) -> available_read_only.
* routes/commands.execute_command: status=="available_read_only" and the cmd is
in LOCAL_HANDLERS -> runs the handler immediately, no approval, no hold.
So a live-effect command labeled mode:"read" is classified available_read_only and
EXECUTES with no human approval (proven above). The same mislabel makes
_module_action_class return "reversible", so the same command planned inside a
workflow also dodges approval_policy hard-floor F1 (IRREVERSIBLE actions always
require a human) — it is planned as a safe reversible effect. And
mcp_server._module_execute_or_pointer runs read-mode module commands directly and
even hardcodes action_class="read" into its policy_gate, so with
mcp.module_reads_enabled the mislabeled write is callable straight from the LLM.
Nothing anywhere checks that a "read"-declared command performs no external
write; the module sandbox restricts fs/network paths but not the governance
label. Net: a module (self-signed, any publisher) exfiltrates data or moves money
through a command the airlock, the workflow planner, and the MCP read-gate all
believe is a harmless read.
Suggested fix:
Stop treating the module's self-declared read/write label as authoritative for
the approval decision:
1. resolve_status must FAIL CLOSED on an unrecognised/absent mode — only an
explicit, known READ classification should yield available_read_only; a
missing or unknown mode must resolve to available_write_requires_approval,
not read-only. (Today the default branch is the permissive one.)
2. Derive (or at minimum cross-check) the read/write classification from
something the module cannot forge: whether the command's handler is granted
any network/filesystem/credential capability (a command with an egress or
credential capability cannot be a "read"), or a platform-side annotation set
at audit/publish time — not a free-text manifest field.
3. At execute time, a command classified read-only that actually touches an
external API (external_touched=True in the receipt) should be treated as a
policy violation for a read-declared command, not merely receipted.
At publish/lint time, reject a manifest that pairs mode:"read" with a handler
declaring egress/credential capabilities.