Repository navigation
feat(secrets): an unreadable credential fails the call; the retry-state key is the runtime's own - #269
Merged
Conversation
…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
force-pushed
the
feat/the-runtime-without-a-key-in-hand
branch
from
October 20, 2026 13:46
8f5771a to
ab5c89a
Compare
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.