Skip to main content

auth.custody/v1alpha1

Specified; runtime pending

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 SecretStore port (crates/connectors-sdk/src/lib.rs:42-44, read only).
  • Family: auth. Sibling documents: connection, profile, acquisition, capability, evidence.
  • Recorded: 2026-09-08.

1. Identity​

FieldValue
Contractauth.custody/v1alpha1
Profilesread_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)
Partieshost (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)

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_new completes durably before publish_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_active carries the expected revision; a concurrent publish loses with Conflict and 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 no write_new, confirm or delete: 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​

ConcernRule
Value size64 KiB default
Versions per connectionbounded (default 8 retained including active)
Read latency budgetpart of the provider deadline; a read exceeding it is Unavailable
Retryreads may be retried by the caller; writes are not retried after an unknown outcome without a new version id

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.

PlatformBindingStoreValue boundLocked or absent store
Linuxsecret_servicethe qualified Secret Service login collection (local Secret Service)64 KiBUnavailable
macOSmacos_keychaingeneric 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 used64 KiBUnavailable, never an unlock prompt
Windowswindows_credential_managergeneric 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 last64 KiBUnavailable

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.

BindingStoreValue boundWritesUnusable directory or item
mounted_secretitems <adapter alias>/<profile> in the configured directory, projected (..data naming one ..-prefixed sibling) or flat64 KiBnone: no write_new, confirm or deleteUnavailable

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, one Conflict.
  • 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 (the mounted module's compile-fail examples).
  • Read-only binding checks, one case each with everything else valid: a symbolic link in place of the item, ..data naming /, ../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 is Unavailable, and no failure text carries the bytes or a path (mounted::tests::refuses_each_check, with the owner case in an_item_owned_by_another_user_is_refused and the group case in group_read_needs_root_and_a_held_group).
  • Read-only binding, projected layout: an item is read through ..data only, and a kubelet-style update makes the pinned version Missing until it is pinned again (mounted::tests::projected_layout_reads_through_data).

7. Compatibility​

  • The read_only profile keeps today's env/file CredentialRef behavior; 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​

ObligationWhere
SecretStore gains write_new, delete, and a scope parametercrates/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.1new host module (with auth.connection metadata)
File binding: one file per version, owner-only, fsync, atomic renamecrates/connectors-host/src/credentials.rs
In-memory binding for testscrates/connectors-conformance

9. ESS entities​

EntityNotes
Immutable custody version / credential setconnectors.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 referenceOptional Connection reference to CustodyVersion alongside distinct active generation/publication fence; publication couples generation, refresh ownership and revocation.
Refresh coordinationRefreshAttempt ESS, owned by acquisition/coordinator; not a custody entity or port
External store layout, Vault pathsUNMAPPED, binding-private
Read-only binding pinsbinding-private (custody-mounted/ in the state directory); store_version stays opaque

10. Open decisions​

DecisionDefault taken
Second binding after fileTaken 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 versionOpen: 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 versions24 h
Value bound64 KiB, per-binding override for certificate chains

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.