Selection without revelation — a flow chooses which borrowed authority to spend, never…
The canonical adopter shape: one axon-tenant, N business sub-tenants
multiplexed by a tenant_id that travels as a flow parameter (a SaaS
that runs as a single deployment while each of its customers connects
their own CRM). v2.48.0 gave this adopter enumeration and rotation of N
borrowed credentials under one class (crm.hubspot.<sub> — the class
sweep does not care about the suffix). What it could not give was
consumption: tool { secret: crm.hubspot } is a STATIC key
(axon-T902), resolved against the axon-tenant with no access to the
runtime discriminator. One tenant's tool could serve exactly one
sub-tenant.
The law. A flow may select which borrowed authority to spend — by a discriminator it already holds — but selection is not revelation. Choosing WHICH custody entry to inject never evaluates to WHAT the entry is, and never widens the class the tool may reach. The credential value remains, as in v2.48.0, a thing no term of the language can name.
The surface
tool CrmCrearContacto {
parameters: { tenant_id: String, email: String, name: String }
secret: crm.hubspot # the class key — a compile-time literal
secret_partition: tenant_id # one of THIS tool's own String parameters
}
flow CreateContact {
use CrmCrearContacto(tenant_id = "acme", email = e, name = n)
# dispatch resolves custody key `crm.hubspot.acme`, injects its value
# under `axon_secret`; the flow never touches the credential.
}
The secret: names the class; secret_partition: names a parameter
whose value becomes exactly ONE appended key segment. The resolved
key is secret + "." + segment. Because the class prefix is fixed at
compile time and the segment is validated to a single run of
[a-z0-9_-] (no . to widen the prefix, no //: to reach a URL),
the resolved key is provably inside the tool's declared class — the
dynamic part is a discriminator, never a new class.
Two properties, both structural
- No revelation (rotation_without_revelation, unbroken). The
partition selects; it does not read. The value still travels only
custody → tool exchange, injected under
axon_secretat dispatch. Nolet, envelope, prompt, orpersistcan bind it.secret_partitionis a parameter name, not a value — the IR carries the name, never a credential. - Class containment. The segment cannot contain a
., so it cannot extend the class prefix past its declared boundary.secret: crm.hubspotwith any partition value can only ever addresscrm.hubspot.<seg>— nevercrm.*at large, neverllm.openai. The confused-deputy reach into another class is unrepresentable, not merely discouraged.
Three layers, all fail-closed
- Compile (
axon-T903). Asecret_partition:must name a requiredStringparameter of the SAME tool (the discriminator is an argument the CALLER binds at theusesite — never ambient state, never an LLM output, never a free identifier), must accompany asecret:(a partition with no class key selects nothing), and is forbidden on atarget:-bound technician tool (argv dispatch has no request body to inject into — theaxon-T902exclusion applied to its selector). - Verify/deploy.
SecretCustodySoundnessre-derives the partition laws from the compiled IR: a hand-edited artifact that points a partition at a ghost parameter, a non-Stringone, or a tool with nosecret:is REFUTED before it mounts. - Dispatch. A partition whose bound value is missing, non-string,
empty, or contains a character outside
[a-z0-9_-]refuses the dispatch with a witness — the program never spends the wrong tenant's credential and never reaches for a key outside its class. No custody port ⇒ the same loud missing-dependency error as every v2.48.0 surface.
The honest perimeter
The law guarantees the segment is a bounded, in-class discriminator.
It does NOT guarantee the segment is the right sub-tenant — because
axon does not model the adopter's sub-tenancy (the same purity rule that
keeps HubSpot out of the language keeps business tenant out of it).
Binding tenant_id to the authenticated end-user is the adopter's
authorization, resolved at its io-layer from the API key or JWT — the
same trust boundary that already decides which tenant_id a request
carries. What v2.49.0 adds is narrow and real: whatever discriminator the
adopter chooses, the language makes it impossible for that discriminator
to reveal a value or escape its class.
Relation to the other laws
- The parametric extension of
rotation_without_revelation(v2.48.0): v2.48.0 made the credential value irrepresentable; v2.49.0 makes the selection of one credential among N a bounded, class-contained operation — without reintroducing a way to read the value. The static key of v2.48.0 is thesecret_partition-less special case. - The inbound mirror of
authority_only_attenuates(v2.46.0): v2.46.0'smintselects an audience to hand authority DOWN to; v2.49.0 selects which borrowed authority, lent by one of N third parties, to spend — and both refuse to let the selected authority become data. dispatch_vs_cognition(v2.9.0): the flow decides WHICH credential (cognition, by a discriminator it holds); the runtime resolves and injects the value (dispatch). Thesecret_partitionfield exists precisely so that split survives multiplexing.
The honest test: if any expressible program can, by choosing a partition, print a custodied secret OR inject a credential from outside the tool's declared class, your selection surface is a covert read. AXON rejects that program at compile time, refutes it at deploy time, and refuses it at dispatch.