Repository navigation
canonical(enums): SecretType and SecretRequirement duplicate the secret taxonomy — agents cannot express an OAuth requirement #13846
Description
Activity
Delivered in #14957 (branch
issue-13845-enum-union, head17b58390). Member sets, deliberate-fork findings and the per-fork union tables are in the PR body. Not merged — CI in flight.Delivered and merged as
342eeb05e(PR #14957).- CI: all 10 required contexts present and green, checked by name. 59 success / 2 skipped / 0 failures. All 12
python-suiteshards green, including shard 10 — the one that went red on the pin tests. - Review: independent
code-reviewerpass. Every fork's member set was reconstructed from the merge-base tree and diffed against the union rather than taken from the PR body.
Nothing was dropped.
FORBIDDEN,DANGEROUS,OAUTH_REFRESH_TOKENandANYall have a real home.FORBIDDENandDANGEROUSwere kept as separate members, and the reasoning is now in the source: they are distinct reasons reaching the same verdict —DANGEROUSmeans the command matched a destructive pattern (RISKY_COMMAND_PATTERNS),FORBIDDENmeans the base command is on the executor's deny list (FORBIDDEN_COMMANDS)."dangerous"is also already serialized by two live endpoints. Folding either away would have broken a wire contract or erased a distinction the executor actually makes.The union created a fail-open, and the PR closed it. With two blocking members, an equality check against one let the other producer's blocking verdict through. The enum gained
.blocks, and review confirmed all three production comparison sites migrated with none missed. AHIGH_RISK_COMMAND_LEVELSset that previously drew only fromCommandRiskLevelnow picks upFORBIDDENautomatically.map_risk_to_levelwas fail-open — it defaulted unmapped members toMEDIUM, downgrading a blocking verdict to mid-scale. Now total, with the fallback changed toCRITICAL, and a guard that reads the table via AST and asserts totality against the live enum so a future member without an entry fails loudly.A third
SecretTypethe issues never named was found inapi/schemas_system.py— also missingOAUTH_REFRESH_TOKEN, soPOST /secretscould not accept the kind the database row already stored. And the real OAuth producer was persisting"connector_oauth_token"as a bare string, commented as aSecretTypelabel while belonging to no enum at all.Four deliberate forks preserved with evidence, none collapsed: the #7258 legacy-wire
RiskLevel, the standalone host-sideSecretType,InjectionRisk(grades text, not commands — discovered by the new scan and documented rather than folded), and the three role vocabularies whose own issue states they are correctly separate.The guard caught itself failing open. Mutation M9 re-declared a fork behind
from enum import Enum as _Eand the scan stayed green, because it matched the literal wordEnum. Fixed by resolving the import to its local names. A two-hop re-export remains a known limit, now stated in the docstring and pinned by a test that instructs the next person to update it rather than delete it.A regression this PR caused was found and fixed before merge:
models/schemas.pygained an import thattest_apply_secrets.pycould not satisfy — that test replacesmodels.databasewith aMagicMockand hand-installs one real stand-in, because pydantic needs a genuine enum type. The result was a collection error, so all 9 tests in the file stopped running rather than one failing. The blast radius was then settled by collecting both trees (1697 each) rather than grepping, since collection is exactly where the failure manifests.Remaining and filed: #14956 (169 severity literals — the honest measured count, not the issue's 119, with 43 already out of vocabulary; ratcheted so it can only fall), #14974 (
ANYadvertised on two API surfaces that reject it), #14955 (CRITICALrendering as unclassified grey).- CI: all 10 required contexts present and green, checked by name. 59 success / 2 skipped / 0 failures. All 12
Discovered while auditing the secrets vault for umbrella #13707 (company mailbox).
Design doc: docs/research/email-reading-capability-audit.md section 4.2
Problem
Two enums classify the same thing — what kind of credential a secret is — under
different names, in different layers:
SecretTypeSecretRequirementmodels/secret.py:33services/agent_secrets_integration.py:28SecretrowSSH_KEYAPI_KEYPASSWORDTOKENCERTIFICATEDATABASE_URLOAUTH_REFRESH_TOKENINFRASTRUCTURE_HOSTOTHERANYSix members are duplicated verbatim — down to the identical
# nosec B105and# nosemgrepsuppression comments, which is a strong indication of copy-paste rather thanindependent design.
This is not merely untidy — it is a functional gap
SecretRequirementhas noOAUTH_REFRESH_TOKEN.AgentSecretMappingis expressedpurely in
SecretRequirementterms, so an agent mapping cannot state that an agent needsOAuth credentials — the vocabulary has no word for it. Meanwhile
SecretTypepersistsexactly that kind on the row.
Any agent whose credential is an OAuth refresh token can only be described as
ANY, whichis the blanket "all available secrets" grant — the broadest possible request standing in
for the most specific one.
Why this matters now
Child #13711 of umbrella #13707 adds the agent write path to the vault
(
store_secret_for_agent). Mail connectors authenticate via OAuth. An agent depositing amail credential must classify it — and the two layers will disagree at exactly that point,
with the requirement vocabulary unable to name the type being stored.
Fixing this is cheap now and gets progressively more expensive once agent-written secrets
exist in the database.
Why a name-based sweep missed this
#12771 triages same-name clusters. These carry different names for the same concept —
the blind spot of a name-keyed scan. Same shape as #13597.
Acceptance criteria
than restating it
ANYpreserved — it is a genuine wildcard, not a duplicate member, and must not belost in consolidation
to
ANYSecretTypevalues migrate with no data lossnosec/nosemgrepsuppressions survive on whichever definition remainsFiles
autobot-backend/models/secret.py:33autobot-backend/services/agent_secrets_integration.py:28