Skip to content

feat(secrets): an unreadable credential fails the call; the retry-state key is the runtime's own - #269

Merged
rami-hatoum merged 3 commits into
mainfrom
feat/the-runtime-without-a-key-in-hand
Oct 20, 2026
Merged

rami-hatoum merged 3 commits into
mainfrom
feat/the-runtime-without-a-key-in-hand

Conversation

@rami-hatoum

Copy link
Copy Markdown
Contributor

Two things a custodian that may refuse or stall needs, and that were wrong before it. A credential the secret store cannot read now, for a row sealed under a key that is neither the current nor the previous one, a custodian that refuses, or a custodian or keeper that does not answer within thirty seconds, failed the connection for good: it was recorded broken and the person had to add it again, so one start with the wrong key broke every connection used while it ran. Now such a call fails as connection_unavailable, in words that say what whoever runs the runtime looks at, with the store's own words after them; nothing is recorded, the connection stays connected, the failure is logged once per cause per connection, and the next call after the store can read again succeeds. A row that is gone, and a value that is not the tokens of a consent, break the connection as before, since nothing mends them. The store's three failures are named and typed, and none carries a value or a key.

The key that signs the state of a retry of an MCP tool was derived from the secrets key, which a key held by a custodian leaves no bytes to derive from. It is now a secret the runtime makes for itself at its first start, kept in the store and named by a fact of the runtime's own in runtime/own-keys, the same for every server sharing the ledger and across restarts and rotations; a server that starts under a key that cannot read it stops in words that name the setting in force and the two ways out, the key the ledger's secrets were sealed under or that key as the previous one beside a new key. The configuration reference, the API and server READMEs and the self-host pages say the new rule and the new words.

rami-hatoum and others added 3 commits October 20, 2026 14:45
…dian failing the call alone

A value the store cannot read now fails the call that asked for it, in words that name no value
and no key, and the row stays as it was for the next call to read.

- secret_key_unknown stays: a sealed row whose kid is neither custodian's.
- CustodianRefused, custodian_refused, { kid, detail }: a custodian whose wrap or unwrap fails,
  "The custodian of the key <kid> refused to unwrap a data key: <the custodian's words>".
  localCustodian refuses a wrapping it cannot open in words of its own, "its wrapped key does not
  open under this key".
- KeeperUnavailable, keeper_unavailable, { detail }: a keeper, or the custodian it asks, that
  does not answer within the bound the store sets, 30 seconds through Effect's timeoutOrElse,
  around each hold and reveal of a keep, a read and a replacement.
- The Custodian and Keeper ports take the error channel these come from: a custodian and a
  keeper's hold fail with a KeepingFailure, the last two, and a keeper's reveal with a
  ReadingFailure, all three. A keep or a replacement that fails keeps nothing.
- A rotation bounds each row's unwrapping and wrapping again as a read is, and fails with the
  custodian's KeepingFailure. The server's start stops on it, as an InvalidSettingsError that
  names the previous key's setting before the custodian's words.
- recordingCustodian is told to refuse(), in the words custodianRefusal, to stall(), whose
  promise settles once it is first asked, and to answer() again.
- storeBehaviour's failures are a suite of their own: secret_key_unknown, custodian_refused and
  keeper_unavailable through read, at 30 seconds of TestClock and not before, read again once
  the custodian answers, and custodian_refused through keep and replace, the row as it was.

The README says the three failures, the bound and what the recording custodian is told.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…all and breaks nothing

A row the secret store cannot read now, for any of its three failures, fails the call that needed
it and leaves the connection connected, so the next call reads it again; only a row that is gone,
and a value that is not the tokens the connection expects, break the connection as before.

- given-credentials answers such a read as ConnectionUnready { because: 'connection_unavailable' },
  "The credential of the connection <name> cannot be read now: <the store's words>", records
  nothing, and logs that sentence as a warning, annotated with the org and the connection's id,
  once for each cause of each connection in the process: again when the store's words change, and
  again after a read that succeeded.
- A value that is no tokens breaks the connection in words of its own, "The credential of the
  connection <name> is not the tokens of a consent the runtime keeps, so the person who added it
  removes it and adds it again".
- connection_unavailable joins the reasons a connection is not ready in @on.auto/tool-servers and
  the rejection's because in @on.auto/operations, with its words and its remedy for an agent. A
  link is not kept for the process on it, as on connection_broken, so the next use reads the
  credential again and calls once the store can read it.
- The descriptions of search, dry_run and a run's because name it, connection_broken is said in
  one wording where search, the operations README and the reference pages name it, and
  docs/assets/openapi.json is written again by docs:openapi. The API README says why
  tool_not_offered still carries no Retry-After.
- Tests: a tool call through a recording custodian told to refuse, refused twice in those words
  with one line logged and the connection connected, then answered once the custodian answers; the
  log of each cause; a custodian that stalls, unavailable at 30 seconds; a connection prepared
  again at each use in tool-servers; a restarted server whose row is sealed under another key,
  refusing an execute and a run as 503 tool_not_offered connection_unavailable without
  Retry-After, then calling once the row is sealed again under its key; a row gone breaking it as
  before, which the test of the providers a search lists now uses for its broken connection.
- The connections README's step 3 says the new rule, and the reference and self-host pages name
  connection_unavailable beside the other reasons.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…, kept in its secret store

The key that signs the state an MCP client sends back when it retries a call is no longer derived
from the secrets key, which a custodian holding its key elsewhere leaves no process to derive from:
the runtime keeps it for itself in its secret store and names it by a fact of its own.

- src/database/own-keys.ts, beside ledger-stores.ts, which opens the store the key is read from,
  keeps the stream runtime/own-keys, of the runtime and of no org, as connections keep
  runtime/client-registrations: one fact, own_key_kept { purpose, secret_id }, by the runtime,
  folded by purpose, the latest last. ownKeysDecider refuses a second record for a purpose that
  has one, conflict taken. The one purpose is retry_state.
- At start, once the store is open and rotated, the server folds the stream; where no key is
  recorded it keeps 32 random bytes in the store and records the fact under the version it read,
  and where another server recorded first it forgets its own row and takes that one. It reads the
  row and gives the API RetryStateKeys { current }, with no previous.
- A row it cannot read stops the start as an OwnKeyUnavailable, in words that name the purpose,
  the key in force, the store's words and the two ways out: start with the key the ledger's
  secrets were sealed under, or give that key as SECRETS_KEY_PREVIOUS or SECRETS_KEY_FILE_PREVIOUS
  beside a new key. A row that is gone, one that holds something other than 32 bytes, and a key
  the store cannot keep stop it too; the store is closed first. The HKDF derivation and
  retryStateKeysOf of secrets-keys.ts go, with their tests.
- @on.auto/connections exports byTheRuntime, which the fact is recorded by.
- Tests: the decider's rules on their own; the same key at the next start with one fact and one
  row; the key another server recorded first taken and the server's own forgotten; the start
  stopped for a row under another key, a row gone, a value that is no key and a key not kept; the
  store closed on a failed start and left open on one that has the key; a retry of add_connection
  answered after a restart on the same ledger, across a rotation, and refused by a server of
  another ledger under the same SECRETS_KEY; two servers starting at once on one PostgreSQL ledger
  keeping one key in one fact and one row, skipped without a database; a server that cannot read
  the key not starting, in process and as a process; nothing of the key in its log over two
  starts.
- The pages: the configuration rows of SECRETS_KEY and SECRETS_KEY_PREVIOUS no longer derive the
  key, the API README says it is the runtime's own and the same for every server sharing the
  ledger, the server README sets out runtime/own-keys beside runtime/client-registrations, and the
  security, production and connections pages say a key that cannot read it stops the start.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@rami-hatoum
rami-hatoum force-pushed the feat/the-runtime-without-a-key-in-hand branch from 8f5771a to ab5c89a Compare October 20, 2026 13:46
@rami-hatoum
rami-hatoum merged commit 53bd32a into main Oct 20, 2026
19 checks passed
@rami-hatoum
rami-hatoum deleted the feat/the-runtime-without-a-key-in-hand branch October 20, 2026 15:39
SamHatoum pushed a commit that referenced this pull request Oct 20, 2026
🤖 I have created a release *beep* *boop*
---


##
[0.15.0](v0.14.0...v0.15.0)
(2026-10-20)


### Features

* **connections:** a runtime with no client of its own takes consent
through Auto's client
([#268](#268))
([b2f38ea](b2f38ea))
* **providers:** github, slack and profound take a sign-in
([#261](#261))
([0655d3c](0655d3c))
* **providers:** microsoft, amazon and adobe's six providers
([#264](#264))
([f536378](f536378))
* **providers:** the thirteen providers of Google, with their documents
translated from Discovery
([#262](#262))
([99f0f05](99f0f05))
* **providers:** zoom takes a sign-in, over its HTTP API
([#263](#263))
([2803ca1](2803ca1))
* **secrets:** an unreadable credential fails the call; the retry-state
key is the runtime's own
([#269](#269))
([53bd32a](53bd32a))
* **secrets:** each secret under its own key, wrapped by a custodian, in
rows the ledger keeps
([#267](#267))
([45e0bc6](45e0bc6))


### Bug Fixes

* **api:** the runtime's page and icon show the Auto mark with its color
bands, beside its name
([#270](#270))
([be7fc82](be7fc82))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

Co-authored-by: beonautobot[bot] <298957281+beonautobot[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant