contract authority · 50-authority.md · 51-enforcement.md · guide · card
Admission and authority
A caller reaches Contextful holding a capability credential. This file states what the
credential says, who mints it, what a holder derives from it offline, what a checkpoint
decides about it, and the admitted-authority value that decision hands to every effect.
The relation a grant compiles into is stated in spec/51-enforcement.md.
The path of a credential from issuance to the effects it authorizes:
identify
The subject tuple, its per-member attestation, hygiene at the mint, and the identity links connecting a subject to a source’s principals.
subject-tupletestA subject is a tuple of agent, host,on_behalf_ofprincipal, task scope, inference zone and incognito flag. A credential binds the subset it names and admits under that combination alone.attestationtestEach member carries an attestation,verifiedorasserted. Agent, host, task and zone are asserted and render labeled as such; no surface presents an asserted member as identity.on-behalf-ofteston_behalf_ofis verified at the mint against the issuing identity provider, and admission carries it forward as the reader’s identity.value-hygienetestThe mint checks every subject value: non-empty, at most 256 B, no control character, no leading or trailing whitespace.malformed-valuetestA value failingvalue-hygieneraisesAuthoritySubjectMalformed, except that the command-line mint trims a padded value where the automated exchange refuses it.normalizationtestThe verified tuple is normalized once, each value trimmed and each blank member dropped, before any consumer reads it. No point of use re-normalizes a value or interprets a scheme prefix.subject-missingtestA credential carrying no subject member raisesAuthoritySubjectMissingat admission, naming the refusing surface and echoing no credential value.Why
because an unauthenticated face mints a credential naming a public subject; admitting none reads as the owner’s ambient authorityidentity-linktestAn identity link maps a source principal onto a subject over fields the identity provider verified, recordingmethodandconfidence. Confidence relaxes no check.unverified-linktestOnlyscim_emailandoidc_sublinks authorize. An authorization join consuming anoperator_assertedLinkMethodraisesAuthorityLinkUnverified.directory-linktestDirectory provisioning from the identity provider writes source-account links. A value a source’s profile interface returns at request time is data, not a link.unlinked-principaltestA subject whose source principal resolves to no link reads from that source only the resourcespublic-grantopens.Why
because a source principal is the source’s spelling; theuserprincipalsubject-principalsspells is the subject’s own and needs no linkagent-ceilingtestA sub-agent’s reachable set equals or narrows its parent’s; no agent reads beyond the principal it acts for.incognitotestAdmission carries the caller’s incognito flag unchanged, and a derivation turns it on and never off.subject-reboundtestA derivation naming anon_behalf_ofdifferent from its parent’s raisesAuthoritySubjectRebound.
unsettled: Does a subject with no verified principal need a case distinct from the project owner presenting nothing? owner: authority affects: authority.identify
profile
The versioned delegation profile over the attenuable-credential library: admitted elements, reserved facts, the bounded evaluator, the scoped session.
delegation-profiletestDelegated authority travels in one attenuable, chain-signed library format. The library owns serialization, signatures, block chaining and evaluation; a versioned profile names every fact, check and restriction the engine admits.owner-claimtestProfile 2 permits a signed owner store identity only in the root authority block; profile 1 and attenuation blocks reject it, and activation requires an unrestricted read grant over the selected store.unrecognized-elementtestA credential carrying a block version, predicate, rule or restriction the profile does not name raisesProfileElementUnrecognized.Why
P1reserved-facttestCurrent time, audience, resolved resources and authenticated request identity are reserved facts the engine supplies. A token block introducing one raisesProfileReservedFact.evaluator-boundtestThe evaluator runs with no third-party block, external function, recursion or regular-expression predicate. Input pastfact-ceilingoriteration-ceilingraisesProfileEvaluationBudget.fact-ceilingtestOne authorization holds at most 1000 entries in its fact set.iteration-ceilingtestOne authorization runs at most 100 iterations of the evaluator.appended-blocktestA block after the first contributes no authority fact to an allow decision; appending narrows a credential or adds nothing.unevaluated-restrictiontestA grant carrying a row restriction or aggregate bound with no read evaluator raisesProfileRestrictionUnevaluatedat the mint, at derivation on parent and child, and at admission.Why
P1declared-fieldtestA restriction field the engine refuses stays declared in the profile and is parsed at every admission.Why
P1declared-scopetestIntrospection reports the scope a credential declares without evaluating it. Table policy, source access lists, time bounds and revocation settle effective authority afterwards.scoped-sessiontestAuthorization yields a scoped session carrying every restriction the credential expressed. A statement over two tables needs both accesses in one coherent grant; actions and tables never flatten into independent allowlists.version-unsupportedtestA checkpoint reading a profile version outside its supported set raisesProfileVersionUnsupported. Widening the profile mints a new version.Why
P1introspection-surfacetestIntrospection runs on the command line alone, ascontextful token introspect; no serving face answers an introspection request.Why
because an introspection route on a serving face tells any presenter which credentials verify and what each declares
grant
What a credential says a holder does: actions, table patterns, tenant scope, the template allowlist, ceilings, and resources answering by name.
fieldstestA grant names actions, table patterns, and optionally a tenant scope, aggregate constraints, a template allowlist and a row ceiling. An absent constraint leaves its dimension unconstrained; an absent allowlist confers no template.actionstestA grant’s actions areAction:readfor every row-returning surface,writeto land rows,executeto fire a run,adminto mint,forgetto erase subject data, andread-individualforper-individual-row.unknown-actiontestAn action outside the vocabulary raisesGrantActionUnknownat the mint and at admission.Why
P1pattern-formstesttesttheoremA table pattern is*, covering every table; a prefix ending in*, covering every name beginning with that prefix; or any other string, matched exactly. A concrete pattern never covers*.malformed-patterntestA pattern with*anywhere but its final position raisesGrantPatternMalformed.one-matchertestOne pattern matcher serves derivation legality, view registration, tool visibility and replica object selection.Why
P5startest*is an ordinary grant, bounded by the credential’s actions, audience and expiry and visible in the audit record.tenant-scopetestA tenant scope pairs a table with an opaque byte string bound to that table’s outermost partition column.tenant-bytestestThe grant’s tenant value, the partition key on disk and the consumer’s tenant identifier are identical bytes, compared by bound-parameter equality with no trimming, case folding, collation or Unicode normalization.tenant-unbindabletestA tenant-scoped grant on a table declaring no bare outermost partition column raisesGrantTenantUnbindableat admission.unpartitionedtestA consumer needing a per-tenant scope over a table declaring no partition key declares a bare outermost partition column; no other grant dimension scopes rows by tenant.Why
because a tenant value matched against an arbitrary column is a row restriction, and a scoped-looking grant over an unbound table reads every tenanttenant-child-lifetimetestA tenant-scoped child credential is derived per query with a lifetime of 15 min.template-allowlisttesttesttheoremA template allowlist denies by default: absent authorizes none,*authorizes every template the manifest declares, and a child names only identifiers its parent named or covered.template-tablestestA covered template reads the tables its statement names inside that template’s session, under every other layer; its coverage adds no table to a raw read.template-not-allowedtestInvoking a template outside the allowlist raisesGrantTemplateNotAllowed, and an uncovered template is absent from the projected tool listing.pipeline-resourcetestA pipeline identifier is a read resource: listing it, describing it and tracing its runs each need a read grant covering it. A grant over its landed tables confers none.Why
P2pipeline-not-coveredtestDescribing an uncovered pipeline raisesGrantPipelineNotCovered, naming it. Listing filters to covered identifiers and raises it when none is covered; a project declaring no pipelines lists empty.Why
P2run-trace-deniedtestTracing a run gates on the pipeline resolved from the run record and raisesGrantRunTraceDeniedwithout naming that pipeline.Why
P2write-not-coveredtestA credentialed table write whose grants carry nowriteover a destination table raisesGrantWriteNotCovered, naming the table, before its first effect.Why
because a write grant names the tables a principal authors, and a row landed outside them carries a principal who never authorized itrow-ceilingtestA read’s row ceiling is the least of the grant’s, the request’s, the template’s, each touched table’s published ceiling andface-ceiling; an undeclared component imposes none, and the face ceiling is always declared.duration-ceilingtestA statement’smax_duration_msis the least of its grant, touched-table and request limits; an undeclared component imposes none, and no component declares a default.byte-ceilingtestA response’smax_response_bytesis the least of its grant, touched-table and request limits; an undeclared component imposes none, and no component declares a default.horizontestA grant may carryhorizon, an instant bounding the availability time andsinceof every read its holder makes, underinherited-boundandexceeds-horizon; an absent horizon leaves availability unbounded.aggregatetestAn aggregate grant carries a minimum group size, a maximum single-contributor share, permitted functions, a groups-per-query ceiling and a row ceiling. A write-only or aggregate-only grant contributes no table to a raw read.group-ceilingtestAn aggregate grant’s groups-per-query ceiling is at least 1 entries; a query producing more groups than the ceiling is cut at it.
unsettled: Does a credentialed run start or pipeline run also need execute over its pipeline beside write over its tables? owner: authority affects: authority.grant
attenuate
Deriving a narrower child offline, and the narrowing legality of each grant dimension.
offlinetestA holder derives a narrower child with no issuer round trip by appending a signed block. The parent’s bytes stay unchanged, independently verifiable and independently withdrawable.narrowingtesttesttheoremEvery dimension of a child obeys the narrowing table in Shapes, checked by the deriving holder and again across the whole chain at admission.widenstesttesttesttheoremA child broader than its parent on actions, tables, templates, aggregate constraints, or amax_rows,max_duration_msormax_response_bytesceiling raisesAttenuationWidens, naming the dimension.horizon-widenstestA child whose horizon falls later than its parent’s is broader onhorizonand refuses aswidens, naming that dimension; a child naming no horizon inherits its parent’s.expiry-extendedtestA child whose expiry falls past its parent’s raisesAttenuationExpiryExtended.tenant-droppedtestA child dropping its parent’s tenant scope or naming another tenant raisesAttenuationTenantDropped. Adding a scope to an unscoped parent narrows.truncationtestThe chain-final proof advances an ephemeral key per block; a chain truncated to a broader prefix verifies as nothing.bearertestDerivation proves the chain and not the new holder’s identity. A hop authenticating its new actor runs through the exchange.per-sub-agenttestAn agent derives one child per sub-agent, each bound by its confirmation claim to that sub-agent’s own key pair.
unsettled: What delegation depth does the library format support, and what verification cost does a chain carry at that depth? owner: authority affects: authority.attenuate
issue
Minting: the persisted lifetime ceiling, the principal a row-landing grant needs, signing custody, and the authoring posture.
ceilingtestA project persists in version control an issuance policy naming the default audience and the longest lifetime any mint produces, at most 24 h.above-ceilingtestAn explicitly requested lifetime above the persisted ceiling raisesIssuanceLifetimeAboveCeiling; an exchange policy’s lifetime clamps down to the ceiling instead.ceiling-loweringtestLowering the ceiling records the previous value and its instant; rotation-grace validation uses the recorded value until the last credential minted under it lapses.policy-inittestcontextful token policy initwrites the issuance policy naming its audience with a ceiling of thebearer-lifetimebound; over an existing policy it raisesIssuancePolicyExistsand writes nothing.Why
because a fresh project mints under a policy it commits, and the served faces admit nothing longer-lived by defaultlower-onlytestcontextful token policy lower-ceilingnaming a value at or above the current ceiling raisesIssuanceCeilingNotLoweredand leaves the policy unchanged.Why
because a raised ceiling mints credentials outliving the grace windows validated against the lower oneout-of-tree-minttestCeiling enforcement binds a mint run inside the project tree; a mint run outside it is bounded at the checkpoint by rotation-grace validation.principal-requiredtestA mint grantingwrite,executeorforgetwhose subject names noon_behalf_ofraisesIssuancePrincipalRequired.default-readtestA mint’s default action set isreadalone.absent-authortestAn absent authorship value on a landed row means no credential authorized the write: the uncredentialed owner or a timer-fired run.authoring-posturetestEvery table write verbwrite-verbsruns under its manifest’s authoring posture.sessionauthors every write by one verified ambient credential’s principal;per_requestholds no ambient principal, leaving an unaccompanied write unauthored.posture-keytestA manifest declares its authoring posture as a top-levelauthoring_postureofsessionorper_request. A table write verb reading a manifest whose key is absent or names another value raisesAuthoringPostureUndeclaredbefore its first effect.session-credentialtestUndersession, a table write verb run with no credential raisesAuthoringCredentialMissingbefore its first effect.one-credentialtestA served face admits a verified capability credential, presented as a bearer or bound to a holder key, and nothing else: no static bearer, no gateway shared secret, no unauthenticated owner path.missing-keytestA face configured with no issuer key raisesIssuerKeyMissing, prints the command that fixes it, and binds no port.Why
P3default-keytestA mint naming no issuer key reads.contextful/issuer.seed; no file there raisesmissing-key.Why
because the seedcontextful token keygenwrites by default is the one a mint in the same tree readsproject-roottestThe issuance policy, the default issuer seed and the key-set ledger sit under the directorydiscoverybases project paths on; with nocontextful.tomlupward, under the working directory.Why
because a ledger read from a subdirectory hides every retirement and epoch bump the root recordsunresolvable-keytestA set issuer key reference that resolves to no material raisesIssuerKeyUnresolvable; no local key is fabricated.Why
P3unauthorized-minttestA mint request presenting no grant carryingadminraisesIssuanceUnauthorized.Why
because issuance authorizes itself with the same primitive as every other action, and a separate mint secret is a second perimeter to guardreplica-minttestA replica holds the issuer’s public key and no signing material; a mint there raisesReplicaCannotIssue.Why
because a replica verifies every credential the primary mints and needs no power to produce onezone-wildcardtestA mint whose subject declares a wildcard inference zone raisesIssuanceZoneWildcard.bearer-cap-fixedtestNo issuance policy, face or command setting raises the capbearer-lifetimesets; a credential living longer admits over a network only bound to a holder key.
signing keys
One signing port signs every credential, and its key rotates on a recorded cadence.
signing-porttestOne signing port signs every credential’s authority block and every audit root and tip; no mint path reads a private key. Seed files, secret references resolved at mint time and remote signing oracles are its adapters.oracle-custodytestUnder the oracle adapter the seed stays inside the custodian, which records each mint as a signing call.signature-encodingtestA signing port names its encoding:ed25519, a 32-byte key and 64-byte signature;es256-derores256-raw, a SEC1 P-256 point signing ECDSA over SHA-256 of the message as ASN.1 DER or 64-byter ‖ s.Why
because an enclave, a cloud KMS and a browser key each answer ES256 in one encoding, and the tag lets every adapter answer in its ownder-at-the-edgetestAnes256-rawsignature converts to DER where it leaves the port; a credential and an audit root or tip carry ES256 only as DER.Why
because the credential format verifies ES256 only in DER, and one stored encoding holds a verifier to one decoderencoding-invalidtestA port signature that does not decode under its encoding tag, or does not verify under the port’s public key, raisesSignatureEncodingInvalid; nothing is minted and no root or tip is written.Why
because a mislabelled signature yields a credential every checkpoint rejects, and refusing at the port names the adapter at faultpublic-key-texttestA port’s public key prints ased25519:<hex>ores256:<hex>of its SEC1 point; either form, with a compressed or uncompressed point, parses back to the key it names.algorithmtestA credential names Ed25519, the default, or ECDSA over P-256; the pinned key’s scheme is authoritative.algorithm-mismatchtestA credential naming a scheme other than its pinned key’s raisesSignatureAlgorithmMismatch.key-rotationtestThe issuer signing key rotates every 90 d, and at once on suspected compromise.rotation-not-duetestA rotation without--compromisebeforekey-rotationfalls due raisesIssuerKeyRotationNotDue, naming the instant it falls due.Why
because a scheduled rotation ahead of its cadence churns every verifier’s pins with nothing withdrawnunrecorded-keytestA rotation without--compromiseof an issuer key the key-set ledger does not date raisesIssuerKeyUnrecorded;contextful token record --sincedates it.Why
because an undated key otherwise rotates at once, skipping the cadence the ledger enforces
unsettled: Does a read face — query, mcp, a served face — refuse a manifest declaring no authoring_posture, or does the authoring posture bind table write verbs alone? owner: authority affects: authority.issue
verify
Admission at a checkpoint: signatures, audience, timestamps, possession proof, key sets, and the admitted-authority value.
admitted-authoritytestVerification yields an admitted-authority value carrying the normalized subject tuple and its grants. Every read surface and row-landing effect takes that value as an argument; nothing downstream re-parses a credential or reads ambient state.write-verbstestcontext land,run startandpipeline runare the table write verbs. Each admits its accompanying credential before its first effect and stamps that credential’son_behalf_ofas every landed row’s_authored_by.write-committestA credentialed table write reads its denylist afresh and checks expiry immediately before each commit; a credential revoked or lapsed since admission commits nothing, asrevokedandexpired.no-bypass-constructoritemThe admitted-authority type has no constructor that produces a value without a verification.effect-boundarytestA statement’s start and each commit are effect boundaries. Each boundary re-reads expiry, revocation and policy version against the carried value; a lapse between boundaries stops the effect at the next one.format-interfacetestThe credential format sits behind one interface of issue, attenuate, verify and introspect; no enforcement call site names a credential type.bad-signaturetestA credential with any block signature failing against a pinned key raisesSignatureInvalidand admits nothing, no verified prefix included.Why
because a partially verified chain is an unauthenticated credential, and admitting its prefix hands out authority nobody signed as presentedaudience-mismatchtestA checkpoint declaring an expected audience raisesAudienceMismatchfor a credential with a different audience or none; a checkpoint declaring none performs no audience check.Why
because a credential minted for one store is otherwise replayable against another; the undeclared case is local developmenttimestampstestEvery checkpoint decodes timestamps into UTC instants under one grammar and compares instants.malformed-timestamptestA timestamp that does not decode under that grammar raisesTimestampMalformedat every checkpoint alike.expiredtestA credential whose expiry precedes the evaluation instant raisesAuthorityExpiredat admission and at each later effect boundary.possession-bindingtestA confirmation claim holds a client public-key thumbprint. Each request under a credential carrying one bears a proof signed by the matching private key over method, target, body digest, issue instant and nonce; a credential carrying none requires no proof.possession-invalidtestA request whose proof does not verify against the confirmation thumbprint raisesPossessionProofInvalid.replay-windowtestA proof issued more than 300 s before the checkpoint clock is refused aspossession-invalid; the nonce cache retains each nonce for that window.clock-skewtestA proof issued more than 30 s after the checkpoint clock is refused aspossession-invalid.replayed-noncetestA proof whose nonce repeats inside the replay window raisesPossessionProofReplayed.network-bearertestA network checkpoint admits a credential with no confirmation claim as a bearer when it names the checkpoint’s declared audience and meetsbearer-lifetime; the peer fallback never applies over a network.bearer-lifetimetestA bearer whose expiry falls more than 3600 s after its issue instant raisesBearerLifetimeExceededat a network checkpoint and admits nothing; its holder refreshes throughsurface.nonce-cachetestA checkpoint’s nonce cache holds at most 100000 entries; a proof arriving while it is full answers503and admits nothing.Why
because a proof the cache cannot record is a replay it cannot detect
local transport
A pipe or Unix socket admits a credential through a holder proof or the peer’s uid.
local-transporttestA local transport is a stdio pipe the checkpoint inherited from the process that spawned it, or a Unix socket; every other transport is a network transport.local-holder-prooftestOn a local transport, a credential whose confirmation claim holds a thumbprint admits only with a proof aspossession-binding; the peer fallback never applies to it, and a missing or failing proof is refused aspossession-invalid.local-proof-channeltestA credentialed command-line verb and the stdio tool server sign one holder proof at admission with the Ed25519 seed in--holder-key, else take the proof inCONTEXTFUL_DPOP, else sign with the seed inCONTEXTFUL_HOLDER_KEY; a blank variable is unset.Why
because a bound credential is otherwise unusable off the network face, and a proof on the argument list is readable through the process tablelocal-proof-requesttestA local proof covers methodCLI, the verb’s command path as target, such ascontext land,memory writeormcp, and an empty body.Why
because binding the verb keeps a proof minted for a read from admitting a writelocal-nonce-storetestA command-line verb checks its holder proof against the nonce store.contextful/proof-noncesunder the project root, shared by every invocation on that project under an exclusive lock, so a repeated nonce raisesreplayed-nonce.Why
because each invocation is a fresh process, and a per-process cache lets a proof copied from the environment replay for the whole windowholder-keygentesttoken keygen --holderwrites an Ed25519 holder seed and prints its thumbprint, the valuetoken mint --holderbinds as the confirmation claim.local-peer-fallbacktestOn a local transport, a credential with no confirmation claim admits only through the operating system’s peer authentication: an inherited stdio pipe, or a socket peer whose kernel-reported uid equals the checkpoint process’s uid.peer-mismatchtestA socket peer presenting a credential with no confirmation claim, whose kernel-reported uid differs from the checkpoint process’s uid or which the platform cannot report, raisesTransportPeerMismatchand admits nothing.connection-scopedtestA local admission binds to the pipe or socket connection that presented the credential; a request on any other connection is refused aspeer-mismatchuntil that connection presents the credential and admits itself.
key sets
A checkpoint verifies against pinned or published issuer keys and calls no issuer.
public-key-onlytestA checkpoint holds public key material and no signing secret, and admits with no call to an issuer, identity provider or policy service.key-settestA checkpoint accepts a set of issuer keys: comma-separated static pins, or a project-published unauthenticated key route returning the current and non-retired versions, opted into per key.pin-sourcetestA command-line checkpoint reads its pins from--public-key, elseCONTEXTFUL_ISSUER_PUBKEY, and its audience from--audience, elseCONTEXTFUL_AUDIENCE. A bare 64-hex pin reads ased25519/<hex>; an unknown scheme prefix is refused askey-set-unavailable, naming the accepted forms.Why
because an operator exports the issuer key once per shell, and key tools commonly print a bare Ed25519 key with no scheme prefixkey-set-refreshtestA published key set refreshes every 300 s, single-flight, plus one refresh on a signature failure, with failure-forced refreshes throttled.key-set-staletestAfter a failed fetch the last-known-good set serves while its age since the last successful fetch stays under 1 h; past that the checkpoint raisesKeySetStale.
unsettled: Does a proof replayed against a sibling checkpoint need a shared nonce store or a checkpoint-issued nonce? owner: authority affects: authority.verify
revoke
Ending a credential’s usefulness ahead of expiry: short lifetimes, the denylist, the scoped epoch, key rotation, format withdrawal.
short-lifetimetestA short lifetime is the primary withdrawal: a credential’s remaining lifetime bounds its exposure after a provider sign-out or before a denylist entry propagates.denylisttestA checkpoint reads a denylist keyed on credential identifier. An entry ages out once its identifier verifies under no live key version.revocation-idtestEvery derivation carries its own revocation identifier, withdrawing one subtree without ending the root it descends from.epochtestA revocation epoch scoped on project, tenant and principal class invalidates that slice of outstanding authority.epoch-storetestA project persists each scope’s current epoch and each issuer key version’s retirement in.contextful/keyset.tomlbeside the issuer seed; a mint stamps its scope’s current epoch, and a checkpoint re-reads the file at each admission.Why
because a file the checkpoint reads locally keeps a policy service off the admission path, and the seed’s directory already holds the issuer’s custodyunknown-principal-classtestcontextful token revoke --principal-classnaming neitherdelegatednorunattributedraisesRevocationPrincipalClassUnknownand bumps nothing.Why
because an epoch under a class no credential carries withdraws nothing while reporting successrevokedtestA credential on the denylist, or carrying an epoch below the current scoped epoch, raisesAuthorityRevokedat the next effect boundary.compromisetestSuspected compromise of signing material runs a project-wide epoch bump together with immediate retirement of the key version. Waiting out a grace window withdraws nothing.rotation-gracetestA rotation policy declares a cadence and a grace window, validated against the persisted issuance policy, during which the retiring key version still verifies.short-gracetestA declared or overridden grace window shorter than the effective issuance ceiling raisesRotationGraceTooShort.immediate-retiretestImmediate retirement removes a key version from the published set and every static pin; a checkpoint drops it at its next refresh.format-withdrawntestFrom a declared cutover instant, every admission path raisesCredentialFormatWithdrawnfor a credential in a withdrawn format. Only an explicit declaration restores acceptance.
The life of one issuer key version:
unsettled: How does a bumped epoch reach a checkpoint on another node before the credentials it supersedes lapse? owner: authority affects: authority.revoke
exchange
Exchanging a verified external assertion for a scoped credential under injected verifying material and a declared policy.
surfacetestcontextful token exchangeandPOST /auth/exchangetrade a verified external assertion for a capability credential, configured per project by.contextful/exchange/policy.tomland.contextful/exchange/verify.key. The command reads the assertion from--jwtor standard input, and a holder proof from--dpop.wiretestAn exchange request body is the JSON object{"jwt": "<assertion>"}, which the command line builds from its assertion; a body of any other shape raisesExchangeRequestMalformedand mints nothing. A served face answers a mint{"token": "<credential>"}.Why
because the command line and every served face parse one body through one function, so a refusal reads the same on bothbody-ceilingtestAn exchange request body holds at most 64 KiB; a longer body raisesExchangeBodyTooLarge, mints nothing and is not parsed.Why
because an assertion is a few KiB, and parsing an unbounded body is work an unauthenticated caller spendsunconfiguredtestAn exchange in a project declaring no exchange policy raisesExchangeUnconfigured, naming.contextful/exchange/policy.toml, before the body is read.Why
because an unconfigured project exposes no exchange, whatever a caller sendsstatus-maptestOver HTTP,unconfiguredanswers 404,wireandholder-proof-invalid400,body-ceiling413,material-missing503,assertion-invalid401, and any other refusal 403; every refusal body is{"error": {"http", "identifier", "message"}}.Why
because a caller repairs a 400 or 413, retries a 503, re-authenticates on a 401, and repairs nothing on a 403signing-faulttestOver HTTP,missing-key,unresolvable-key,algorithm-mismatchorreplica-mintduring an exchange answers 500, the body naming the identifier and withholding the refusal’s detail.Why
because the face’s operator repairs its configuration, and the detail describes the face’s keys to an unauthenticated callerpolicytestAn exchange policy declaresRawPolicy; onlyexpected_issis required.injected-materialtestVerifying material is operator-injected: a shared secret, an RS256 public key in PEM form, or a key-set document selected by the assertion’skid. The exchange makes no network call.minted-grantstestMinted grants come fromrole_grantsanddefault_grantsalone. A verified role matching no entry earnsdefault_grants, which is empty unless declared.tenanttestThe assertion claimtenant_claimnames becomes the tenant scope of every minted grant; no subject member carries tenancy.lifetime-defaulttestA minted credential lives 900 s where the policy declares nottl_secs.lifetime-ceilingtestA configuredttl_secsclamps down to 3600 s and never extends.audiencetestA minted credential carriesminted_aud, falling back to the project’s persisted default audience.assertion-invalidtestA bad signature, a lapsed assertion, an untrusted issuer or audience, or a mapped claim absent from the assertion raisesExchangeAssertionInvalidand mints nothing.Why
because no credential exists whose subject the assertion did not verifymaterial-missingtestAn exchange configured with no verifying material raisesExchangeMaterialMissingand mints nothing.per-readertestAn embedding application holds no project-wide credential; per request it exchanges its signed-in reader’s assertion for that reader’s short-lived credential and reads as them.no-cross-readertestAn exchanged credential serves only the reader it was minted for.holder-bindingtestAn exchange request carrying aDPoPproof overPOST /auth/exchangeand its body mints a credential whosecnf.jktis the proof key’s thumbprint, the proof checked aspossession-binding.bearer-minttestAn exchange request carrying no proof mints a credential with no confirmation claim, its lifetime clamped bylifetime-ceiling, so a network face admits it underbearer-lifetime.holder-proof-invalidtestAn exchange request whose proof fails any check ofholder-binding, a replayed nonce included, raisesExchangeHolderProofInvalidand mints nothing.Why
because a proof the face cannot verify proves no key, and a bearer minted in its place hands replayable bytes to a caller asking for a bound credentialrefreshtestA holder renews a served-face credential by exchanging a fresh assertion for a new credential with its own identifier and a full lifetime from the exchange instant.no-extensiontestNo surface extends a minted credential’s expiry: a minted credential presented as the exchange assertion raisesassertion-invalid, andexpiry-extendedbounds every derived child.
Shapes
The authority block’s content, as the profile maps it:
{
"iss": "contextful://acme-research",
"aud": "contextful://acme-research",
"jti": "01JBQ7K4F3S9W2X6",
"iat": 1770000000,
"exp": 1770000900,
"alg": "Ed25519",
"cnf": { "jkt": "NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9Xs" },
"sub": {
"on_behalf_of": "user://dana@acme.example",
"agent": "agent://research-loop",
"host": "host://dana-laptop",
"task": "task://q4-review",
"zone": "local:device",
"incognito": false
},
"att": { "on_behalf_of": "verified", "agent": "asserted", "host": "asserted", "task": "asserted", "zone": "asserted" },
"grants": [
{
"actions": ["read"],
"tables": ["workspace/*"],
"tenant": { "table": "workspace/documents", "value": "acme-eu" },
"templates": ["quarterly_rollup"],
"max_rows": 5000
}
],
"rev": { "id": "rev://01JBQ7K4F3S9W2X6", "epoch": 7 }
}
Table-pattern coverage:
name workspace/* workspace/documents *
workspace no no yes
workspace/documents yes yes yes
workspace/documents/eu yes no yes
other/records no no yes
Narrowing legality, parent to proposed child:
dimension broader equal narrower absent on child
actions refused admitted admitted admitted
tables refused admitted admitted admitted
tenant refused admitted admitted refused
templates refused admitted admitted admitted
aggregate refused admitted admitted inherits parent
max_rows refused admitted admitted inherits parent
max_duration_ms refused admitted admitted inherits parent
max_response_bytes refused admitted admitted inherits parent
expiry refused admitted admitted inherits parent
horizon refused admitted admitted inherits parent
on_behalf_of refused admitted refused inherits parent
incognito refused admitted admitted inherits parent
An exchange policy:
expected_iss = "https://login.acme.example/"
expected_aud = "contextful-console"
tenant_claim = "org_id"
role_claim = "roles"
default_grants = []
ttl_secs = 900
minted_iss = "contextful://acme-research"
minted_aud = "contextful://acme-research"
[subject_map]
on_behalf_of = { claim = "sub", template = "user://{}" }
[[role_grants.reader]]
actions = ["read"]
tables = ["workspace/*"]
templates = ["quarterly_rollup"]
[[role_grants.loader]]
actions = ["write"]
tables = ["workspace/documents"]