← Community
bugfixed

A module rejected on reload keeps its commands registered and executable — unregistration is keyed only on the directory disappearing

ShwetaShweta#114d ago · 56 views
affected: station-v1.0.0fixed in: station-v1.3.0

routes/modules._load_modules unregisters a module's commands in exactly one
situation — when its directory is gone from disk:

disk_slugs = {e for e in os.listdir(_MODULES_DIR) if os.path.isdir(...)}
for slug in list(_LOADED_MODULES.keys()):
if slug not in disk_slugs:
for cid in _LOADED_MODULES[slug].get("command_ids", []):
LOCAL_HANDLERS.pop(cid, None)
cmdreg.COMMANDS[:] = [c for c in cmdreg.COMMANDS if c.get("id") != cid]
cmdreg.CMD_BY_ID.pop(cid, None)
_unregister_module_integrations(slug)
_LOADED_MODULES.pop(slug, None)

Every gate that refuses a module does rejected.append({...}); continue, which
leaves the registry exactly as the previous successful load left it. The handler
object from the earlier exec stays in LOCAL_HANDLERS and the command stays in
command_registry, so /api/commands/execute still dispatches to it and the
workflow palette still offers it — while the Modules tab reports the module as
rejected.

The gates this applies to are the whole ladder:

invalid signature bundle edited after signing
trust: <reason> publisher no longer trusted / not attested
publisher pin: <reason> bundle presents a different publisher key
license: <reason> license missing, foreign, expired past grace
trust: <reason> marketplace reports refunded / canceled

Reload is the designated mechanism for acting on a trust decision — the
publisher-trust primitive describes the flow as "the reason surfaces in the
Modules tab with the publisher fingerprint so the operator can trust-then-reload"
— and it is the mechanism that does not revoke. Concretely:

· railcall trust remove <fp>, or flipping to a stricter trust_mode, followed
by a reload does not stop the module. The operator is shown the refusal and
the code keeps running until the station process is restarted.
· a canceled or refunded subscription is caught by the DRM gate on reload and
the paid module's commands keep working anyway.
· a bundle edited after signing is refused as tampered while the already-loaded
copy continues to serve the same command ids.

Reproduction steps:

  1. Extract the station-v1.0.0 tarball to a clean directory. Put workbench/ on

sys.path, import studio_server then routes.modules, warm its _LATE names, and
point routes.modules.WS and routes.modules._MODULES_DIR at scratch dirs.

  1. Write publisher_trust.json with trust_mode "any".
  2. Build a signed v1 module in <modules>/acme-crm/ exposing one command, and

call routes.modules._load_modules(). Confirm LOCAL_HANDLERS holds the command
and that calling it runs the module's code.

  1. publisher_trust.set_mode(ws, "allowlist") — the publisher is not on the

allowlist — and reload. Check LOCAL_HANDLERS again and call the command.

  1. Restore trust_mode "any", overwrite module.sig with 64 zero bytes, and

reload. Check LOCAL_HANDLERS again and call the command.

  1. Delete the module directory and reload. Check LOCAL_HANDLERS again.

Expected: a module the loader refuses is not serving commands after the reload
that refused it. Step 6 shows the station already knows how to do this.

Actual:
step 3 loaded handler present, returns {'ok': True, 'ran': 'MODULE CODE'}
step 4 rejected "trust: publisher <fp>… not in the trust allowlist"
handler STILL present, still returns 'MODULE CODE'
step 5 rejected "invalid signature"
handler STILL present, still returns 'MODULE CODE'
step 6 not present handler removed, command_registry entry removed

Root cause: the unregister sweep asks "is this module still on disk?" when the
question that matters is "did this module load successfully this pass?". The
loader already computes that second answer — it is the loaded list — but never
reconciles the registry against it.

Suggested fix: reconcile after the scan rather than before it. Collect the slugs
that loaded successfully in this pass, then unregister every previously-loaded
slug not in that set, reusing the existing unregister block. That covers disk
removal (a directory that is gone cannot load) and every refusal with one rule,
and it removes the need for the registry to ever hold a module the current gate
ladder would not admit.

5 pts

1 reply

Fixed in station-v1.3.0. _unregister_slug() now runs both on disk-removal and on per-entry pre-validation, so a module rejected on reload has its commands unregistered instead of staying executable.

Thanks for the report — credited.

Sign in to reply.