auth.custody/v1alpha1
Rendered from contracts/auth/custody/v1alpha1/semantics.md by connectors-docs; the source owns every rule on this page.
- Status: proposed, not implemented. Extends the existing
SecretStoreport (crates/connectors-sdk/src/lib.rs:42-44,readonly). - Family: auth. Sibling documents: connection, profile, acquisition, capability, evidence.
- Recorded: 2026-09-08.
1. Identity
| Field | Value |
|---|---|
| Contract | auth.custody/v1alpha1 |
| Profiles | read_only (today: env/file-backed references resolved at dispatch), versioned (write, read, delete immutable versions; publication and refresh coordination held by the host metadata store) |
| Parties | host (grants a scoped store capability), store binding (in-memory, owner-only file, later Vault or database), coordinator (writer), execution boundary (reader), consumer (receives one delivered version through the host; never a store reader) |
More columns: swipe horizontally, or focus the table and use the arrow keys.
Design responsibility three of four: persist sensitive material through an injected secret-store binding (docs/design.md:528). The store knows nothing about provider identity, OAuth, expiry, or refresh (docs/design.md:576).
3. Types
Port (specializes docs/design.md:568-572):
write_new(scope, SensitiveMaterial) -> SecretVersionRef
read(scope, SecretVersionRef) -> SensitiveMaterial | Missing | Unavailable | Denied
delete(scope, SecretVersionRef) -> Deleted | Missing | Unavailable | Denied
Host metadata (not in the store):
{ "connection": "conn_…", "active_credential": "ver_…", "auth_publication_fence": 12, "superseded": ["ver_…"] }
publish_active(connection, expected_auth_fence, new_version) -> Published(auth_fence) | Conflict(current_auth_fence)
// Baseline host operation. Rotating refresh requires the guarded transaction in
// auth.acquisition §4.1; this three-argument CAS alone is insufficient.
// Private concurrency fence, not public connection/F02 semantic revision.
SensitiveMaterial is an opaque bounded byte string per version. A credential set (access plus refresh token, or user plus secret half) is one version, so the pair is written and read together (docs/design.md:576). Interpretation of the bytes belongs to the provider auth implementation.
scope is host-assigned per adapter instance and connection; the store maps (scope, version) to its own layout. Public invocations never carry a scope or version reference (docs/design.md:574).
Outcomes are distinct: Missing (never written or deleted), Unavailable (backend outage), Denied (scope mismatch). Callers map Unavailable to custody_unavailable in connection status; Denied is an internal invariant failure and is logged without the reference.
4. Rules
- Versions are immutable: a written version is never modified; rotation writes a new version and publishes it.
- Write before publish:
write_newcompletes durably beforepublish_active; a crash between the two leaves an unreferenced version that cleanup reclaims. Dispatch readers use only the current admitted generation and its pinned snapshot; coordinator validation may read a private candidate before publication. An orphan never becomes dispatch authority. - Host publication: a baseline
publish_activecarries the expected revision; a concurrent publish loses withConflictand re-reads. For rotating refresh, the host must instead fulfill acquisition §4.1: per-generation reservation, durable single exchange authorization, fencing and guarded publication serialized with revocation. None is a secret-store lease or an operation on opaque material. - Refresh publication additionally binds the new custody version to a new private credential generation validated under evidence. It atomically updates the active reference and invalidates old-generation dispatch admissions. Custody retention never authorizes dispatch or a second exchange. A material version can remain readable for an already-valid use without remaining refreshable.
- Failure between provider exchange and durable response registration never permits a repeat exchange. An unreferenced candidate version is not proof that refresh did or did not happen. Recovery uses the coordinator ledger; ambiguous candidates are discarded/repair is required. Secret garbage collection must not erase the ledger fact that a source was consumed.
- Reclaim: superseded versions are deleted once no valid use requires them, after a declared retention (first-profile default 24 h; refresh-token grace behavior is provider-specific and declared in the profile).
- Scoped access: a capability handed to an adapter reads only its own scope. There is no list operation over values. A consumer holds no capability at all: the host reads on its behalf and delivers one version (§4.2).
- Bounded values: a version is at most 64 KiB (first-profile default; certificates with chains may need more and set it per binding).
- No private references in diagnostics: read errors, ordinary logs and metrics expose neither values nor custody scope/version/generation ids. Use separately admitted safe correlation and error codes. Access-controlled internal custody state is not a diagnostic export.
- Durability claims per binding: in-memory claims none; file binding claims fsync-before-return on the owner-only file; a database or Vault binding declares its own. A binding declares the guarantees it does not provide instead of emulating them (
docs/design.md:578). - A read-only binding (
mounted_secret, §5.2) provides nowrite_new,confirmordelete: its read capability is the whole of it, and a write against it does not compile. A version is published from a checked, pinned read, not a write. Reclamation deletes nothing it does not own: a retired version of a read-only binding is never deleted, and the mounted item is the platform's. - Startup: the host refuses to start an adapter whose required custody binding is missing rather than falling back to another binding (
docs/design.md:816).
4.1 Scoped version identity and guarded reclamation
CustodyVersion names exact (scope_ref, store_version) through private non-reusable
version_ref; store the tuple or an injective mapping, never a store-local name
alone. It references ServiceConfiguration. Its coherent CredentialSet is an embedded
private descriptor/handle, not separate token entities. Binding allocation may
precede Connection publication and orphans may outlive abandoned allocation, so
scope is not a required live Connection foreign key. Host publication proves the
current admitted binding equality; the opaque store knows no provider identity.
Stored → Deleted records acknowledged durable write and physical deletion.
Stored is a historical acknowledgement, not current availability or dispatch
authority. Unknown deletion leaves that last acknowledgement unchanged and grants
no publication, reuse or claim of absent bytes. Observe the exact owner/version or
refuse. Active/superseded remain Connection reference facts, not custody states.
Before deletion, the host acknowledges a retirement fence against future publication and new valid use of that exact version, proves no existing valid use or admitted completion/refresh recovery needs it, and checks retention with trustworthy time. For superseded material, the default 24-hour interval starts at acknowledged supersession. For never-published material it starts at a definitive abandoned-candidate retirement decision; unknown publication or possible response recovery cannot start the clock. A possibly active version first needs acknowledged deactivation/replacement. Capacity refuses new versions instead of evicting required or retained material.
The retirement fence precedes physical deletion and survives uncertain deletion. Remove active/superseded resolver links only under that guarded host decision; never delete immutable capture identities, consumed refresh history or audit evidence as a custody cascade. Retain private version identity and retirement/deletion fence without automatic expiry in this first binding, but drop the material handle after acknowledged deletion. This does not retain credential bytes indefinitely. Finite metadata capacity refuses new writes. This is ordered acknowledgement, not a new distributed custody/metadata transaction.
auth_bindings.yaml models these identities/references/lifecycle. Actual storage, handle erasure, retirement comparisons and trusted clocks remain binding predicates, not executed schema guarantees.
4.2 Consumer delivery
A consumer is a reader party that holds no store capability. On an admitted
consumer launch the host reads the
connection's current version under the same bounded read use as a dispatch (capture,
custody read, dispatch, release) and hands the consumer that version's material
verbatim and uninterpreted, in a memfd sealed with F_SEAL_WRITE, F_SEAL_GROW,
F_SEAL_SHRINK and F_SEAL_SEAL, on descriptor 3. Admission is the existing bounded
read use: current evidence is required, otherwise the launch is refused at readiness.
The consumer learns no scope, version or reference, and the delivered copy grants no
further read. The use is released once the copy is delivered; retirement of the
version afterwards does not reach the consumer's copy.
4.3 Copy to another connection
invoke --definition's reuse answer (CLI §3,
story:invoke-definition-reuse-credential) makes a new connection of the definition's
instance from the current version of a connection another instance of the same
connector holds. Only the owner holds the material. It reads the source's current
version under the same bounded read use as a dispatch (capture, custody read,
dispatch, release), validates it through the new instance's adapter, and writes it
as a new version under the new connection's own scope (write_new, then publication,
§4), exactly as a connect writes an entered credential; the publishing commit
requires the source still to hold the version that was read, under the same
publication fence, Live and not Stale, so a source revoked or replaced while its
copy was validated is never copied. Nothing is shared: the two
connections hold two versions in two scopes, and revoking, reconnecting, refreshing
or retiring either reaches nothing of the other. The source connection's version,
generation, evidence and revision are not changed by the copy, and a refused copy
marks nothing on it. Only a static entry is copied: a profile whose entry an OAuth
acquisition refreshes is never copied, because one rotation would end the other's
refresh. A read-only binding (§5.2) writes no version and is never a copy's target.
5. Limits
| Concern | Rule |
|---|---|
| Value size | 64 KiB default |
| Versions per connection | bounded (default 8 retained including active) |
| Read latency budget | part of the provider deadline; a read exceeding it is Unavailable |
| Retry | reads may be retried by the caller; writes are not retried after an unknown outcome without a new version id |
More columns: swipe horizontally, or focus the table and use the arrow keys.
5.1 Platform bindings of the local host
The local host's versioned custody has one platform binding per build, fixed when the binary is
built, plus the explicitly selected read-only mounted_secret (§5.2). setup check reports the
binding in use as custody_backend (connectors.cli.CustodyBackend). No binding falls back to
another, to a file or to an environment variable; without a usable binding, custody
answers Unavailable and the CLI refuses with custody_unavailable. None activates, unlocks or
prompts.
| Platform | Binding | Store | Value bound | Locked or absent store |
|---|---|---|---|---|
| Linux | secret_service | the qualified Secret Service login collection (local Secret Service) | 64 KiB | Unavailable |
| macOS | macos_keychain | generic password items in the user's login keychain, user interaction disallowed; each item is created with an access whose decrypt ACL admits any application of the user; a second binary is still refused (OS status -25293, errSecAuthFailed, observed on the CI runner), inferred to be the item's partition list, which names the creating binary, so an upgraded binary does not yet read what an earlier one wrote; the data-protection keychain needs entitlements and is not used | 64 KiB | Unavailable, never an unlock prompt |
| Windows | windows_credential_manager | generic credentials with local-machine persistence, never roaming; a value over one blob (CRED_MAX_CREDENTIAL_BLOB_SIZE, 2,560 bytes) is split over numbered part entries of its version, written first, then one header entry naming the part count and the value's SHA-256, written last | 64 KiB | Unavailable |
More columns: swipe horizontally, or focus the table and use the arrow keys.
On Windows a version exists only once its header exists. A read needs the header and every part and
checks the digest; a missing part or a digest mismatch is InvalidMaterial. A delete removes the
header first, then the parts. Parts without a header, left by a crash during a write or a delete, are
an unacknowledged version and are reclaimed as one.
Every binding keys an item by the scope and version ids alone, so the immutable-version, scoped
read and Conflict rules of §4.1 hold unchanged. One conformance suite, generic over the binding,
runs against an in-memory reference on every platform and against each real binding on its own
platform.
5.2 The read-only mounted-secret binding
mounted_secret (Linux) reads credentials a platform delivers as files in a read-only mounted
directory, such as a Kubernetes Secret volume or a read-only bind mount
(local mounted-secret custody). It is used only
when the owner configuration selects it with a [custody] table (connectors-local/4,
connectors.cli.LocalCustodyBinding); it is never a fallback, and no request selects a binding, a
directory or an item. The directory is an infrastructure binding of the owner configuration.
| Binding | Store | Value bound | Writes | Unusable directory or item |
|---|---|---|---|---|
mounted_secret | items <adapter alias>/<profile> in the configured directory, projected (..data naming one ..-prefixed sibling) or flat | 64 KiB | none: no write_new, confirm or delete | Unavailable |
More columns: swipe horizontally, or focus the table and use the arrow keys.
Every open is relative to an admitted descriptor with O_NOFOLLOW. Every directory above the
configured one is owned by root or the effective user and is not group- or other-writable; the
configured directory is owned by root or the effective user and on a read-only mount; ..data is
the single symbolic link resolved. An item is a regular file with one link, no write bit, no other
bit, owned by the effective user or root, on a read-only mount; group read is admitted only on a
root-owned item whose group the process holds. It is read once from the checked descriptor.
A version pins its bytes with an HMAC-SHA-256 under a random key held owner-only in the state
directory. A read returns the item's bytes only while they match the pin; otherwise it is Missing
and never serves the new bytes, and the host asks for a revalidation, which publishes them as a new
version. Under this binding Missing therefore never invalidates a version for good. A write that
the host would make (a refresh-rotating profile, an approval-signing key) is refused as
custody_read_only before any provider exchange.
6. Conformance scenarios (docs/design.md:986)
- Write, publish, read: read returns the bytes; a second write creates a new version; the old version stays readable until deleted.
- Scoped: a capability for scope A reading a version of scope B →
Denied. - Missing versus outage: deleted version →
Missing; binding fault injected →Unavailable; the caller-facing status differs. - Concurrent baseline publish with the same expected revision → one
Published, oneConflict. - Rotating refresh uses the acquisition failure matrix: concurrent exchanges are prevented before send, stale owners cannot publish, revocation blocks resurrection, and custody outages never reopen consumed authorization.
- Crash between write and publish (file binding): after restart the active reference is unchanged and the orphan is reclaimable.
- Serialize every error and log line produced by the suite; grep for the fixture's secret bytes → no match.
- Same suite runs against the in-memory and file bindings; provider code untouched (
docs/design.md:1014). - Read-only binding (
mounted_secret): the read half of the suite runs against it over a read-only mount (mounted::tests::contract_suite_read_only); a write, confirm or delete against it does not compile (themountedmodule's compile-fail examples). - Read-only binding checks, one case each with everything else valid: a symbolic link in place of the item,
..datanaming/,../x,..or a name without.., a second link, a FIFO, a write bit, an other bit, an owner that is neither root nor the effective user, group read of a group the process does not hold, a writable mount, and a value over the bound. Each isUnavailable, and no failure text carries the bytes or a path (mounted::tests::refuses_each_check, with the owner case inan_item_owned_by_another_user_is_refusedand the group case ingroup_read_needs_root_and_a_held_group). - Read-only binding, projected layout: an item is read through
..dataonly, and a kubelet-style update makes the pinned versionMissinguntil it is pinned again (mounted::tests::projected_layout_reads_through_data).
7. Compatibility
- The
read_onlyprofile keeps today's env/fileCredentialRefbehavior; adapters using it change nothing. - Migration of old credential sets is a scoped custody migration or reauthorization, never a copy through a document or command line (
docs/design.md:1116). - Service compatibility is authoritative for the binding. Private custody values and CAS ports are not public service messages. An unchanged read-only port can retain legacy behavior; versioned custody requires its own selected infrastructure binding.
8. SDK and host obligations
| Obligation | Where |
|---|---|
SecretStore gains write_new, delete, and a scope parameter | crates/connectors-sdk/src/lib.rs:42-44 |
Host metadata store with baseline publish_active CAS and the stronger rotating-refresh coordinator protocol in acquisition §4.1 | new host module (with auth.connection metadata) |
| File binding: one file per version, owner-only, fsync, atomic rename | crates/connectors-host/src/credentials.rs |
| In-memory binding for tests | crates/connectors-conformance |
More columns: swipe horizontally, or focus the table and use the arrow keys.
9. ESS entities
| Entity | Notes |
|---|---|
| Immutable custody version / credential set | connectors.auth_bindings.CustodyVersion: scoped identity and Stored/Deleted acknowledgement lifecycle; CredentialSet is embedded. §4.1 selects retention/retirement without deletion ownership over Connection or generation. |
| Active credential reference | Optional Connection reference to CustodyVersion alongside distinct active generation/publication fence; publication couples generation, refresh ownership and revocation. |
| Refresh coordination | RefreshAttempt ESS, owned by acquisition/coordinator; not a custody entity or port |
| External store layout, Vault paths | UNMAPPED, binding-private |
| Read-only binding pins | binding-private (custody-mounted/ in the state directory); store_version stays opaque |
More columns: swipe horizontally, or focus the table and use the arrow keys.
10. Open decisions
| Decision | Default taken |
|---|---|
| Second binding after file | Taken for read-only: mounted_secret (§5.2), explicitly selected. A writable file, encrypted-file, SQLite or Vault binding remains open |
| End state of a read-only binding's retired version | Open: a distinct Released state ("not owned; nothing deleted") needs a metadata store migration; until then such a version stays retired and is never deleted |
| Retention of superseded versions | 24 h |
| Value bound | 64 KiB, per-binding override for certificate chains |
More columns: swipe horizontally, or focus the table and use the arrow keys.
Persistence ownership is consolidated in design §31. CustodyPort owns immutable sensitive versions; ConnectionAuthorityPort/RefreshCoordinatorPort own active references, publication and consumed-source history. This inventory does not supply a backend or execute its atomicity predicates.