command_registry.py's stated purpose is that every command resolves "to exactly
ONE honest status", and not_configured is documented as "wired, but required
secrets are missing". Each command declares exactly which secrets it needs, e.g.
{"id": "notion.add_page", "provider": "notion",
"requires": ["NOTION_TOKEN", "NOTION_DATABASE_ID"], ...}
{"id": "notion.export_session", "provider": "notion",
"requires": ["NOTION_TOKEN"], ...}
resolve_status() never reads the contents of that list. It uses it only as a
boolean — "does this command need anything at all?" — and then checks whether
the PROVIDER is configured:
if cmd.get("requires"):
provider = cmd.get("provider")
if isinstance(configured, dict):
state = configured.get(provider)
if state != "activated":
return "credential_present_untested" if state == "credential_only" \
else "not_configured"
Provider activation is per-provider, not per-key: configured_providers() marks a
provider activated when its integration row reaches status key_present/tested.
So two commands sharing a provider but declaring different requires lists are
indistinguishable, and the one with the longer list reports ready as soon as the
shorter one's credential is saved.
Reproduction steps:
- Simulate an operator who has saved and tested a Notion token, with no
database id configured:
integrations = {"crm": [{"id": "notion", "status": "tested"}]}
configured = command_registry.configured_providers(
integrations, vault_present=["notion"])
- Resolve the status of the command that needs two keys:
cmd = command_registry.CMD_BY_ID["notion.add_page"]
command_registry.resolve_status(cmd, configured)
Expected:
not_configured (or credential_present_untested) — the command declares
NOTION_DATABASE_ID as required and it is not present, which is exactly the
condition not_configured is documented to represent.
Actual:
configured map: {'railcall': 'activated', 'notion': 'activated'}
notion.add_page requires=['NOTION_TOKEN', 'NOTION_DATABASE_ID']
-> available_write_requires_approval exec=write_approved
The command is presented as configured and ready for approval. The operator
approves it in the Airlock, and it fails at handler time on the missing key —
surfacing as failed_with_receipt rather than as the honest not_configured the
catalog is designed to report up front.
Root cause:
command_registry.py, resolve_status() — cmd.get("requires") is evaluated for
truthiness only, and the subsequent check is against provider activation state.
The declared key names are never compared against what is actually present, and
configured_providers() only ever produces provider-level granularity
("activated" / "credential_only"), so the per-key information the catalog
carries has no consumer.
Suggested fix:
Resolve against the declared keys rather than the provider. Have the caller pass
the set of credential keys actually present per provider, and checkset(cmd["requires"]) <= present[provider], returning not_configured (naming
the missing keys, which the UI can then show) when it is not. Provider
activation remains the gate for credential_present_untested. Failing that, at
minimum surface the missing key names in the approval preview so an operator is
not asked to approve a command that cannot run.
Honest scope:
This is a status-honesty defect, not a security bypass. It fails safe: the
command still requires Airlock approval, the handler still sources credentials
from the vault, and a missing key produces an error and a receipt rather than an
unintended action. Nothing executes that should not. The cost is that the one
guarantee this module is built around — an honest terminal status per command —
is wrong for any command declaring more keys than a sibling command on the same
provider, and the operator is asked to approve an action that cannot succeed.
The stock catalog contains several such pairs (notion.export_session vs
notion.add_page is the clearest, both shipped and wired). Severity is left to
the maintainer; I would rate it minor.
Counter-evidence checked:
- Confirmed against the real, unmodified module from a clean tarball extraction,
driving the real configured_providers() and resolve_status().
- Confirmed resolve_status() contains no comparison of the
requiresentries
themselves — the list is reached only by if cmd.get("requires"):.
- Confirmed configured_providers() produces provider-level states only, in both
its legacy set form and its v0.40+ dict form, so no caller could supply the
per-key detail even if resolve_status wanted it.
- Checked that the gap is not rescued downstream: the status maps through
EXEC_CLASS to write_approved, which is the normal approvable path.
Distinctness:
This is the requires list being unused, not the mode field's fall-through.
It is distinct from the reported finding that resolve_status() defaults an
unrecognized mode token to available_read_only — that concerns the mode
branch and is fixed by defaulting unknown modes to gated; this concerns the
credential branch above it and is fixed by checking the declared keys. The two
fixes do not overlap. I did not find a community thread about the requires
list being evaluated only for truthiness.