- dsh-authorization's manifest becomes a release member on current
master's terms: version matches the root 0.1.0-rc.6 and
publishConfig.access is public, which the constraints gate and the
tarball pack's version verify both enforce after the master merge.
- Regenerate docs/module-graph (zh mirror included) for the
authorization package and the llm-pi-ai -> authorization edge; the
graph gate lives outside doc-sync and was never regenerated when the
package was added.
- The built-bin smoke seeds the versioned credentials document; this
branch's provider refuses the pre-release flat layout by design, and
the master-side test still wrote the old shape.
Review findings on #2509, all confirmed:
- Every writer of .credentials.yaml now waits out the record-mutation
lock (DOCUMENT_LOCK_WAIT_MS): refs and records share one file and one
lock, so a reference write or record delete contending with an OAuth
refresh must not fail at the 2s file-work default.
- api-key records are admitted before they are rendered: an empty key,
a non-POSIX env name, or an empty env value is refused at the write
instead of persisting a document the next boot rejects wholesale.
- llm-pi-ai no longer lets the credential-key grammar reject legal
route ids: reads answer "nothing stored" via isCredentialKeySegment
(new dsh-credentials export), deletes have nothing to remove, and only
a write refuses, as LlmError UNSTORABLE_PROVIDER_ID; flow registration
skips a future catalog id outside the grammar instead of failing the
mount.
- authorization/settled fans out with contained listener failures on
the credentials seam's terms (INVARIANT still rethrows), so a broken
watcher can never turn a finished attempt into a failure.
- notify() is fire-and-forget at the seam: a surface that cannot render
a notice loses the notice, never the attempt.
- A declined prompt is an outcome: interactions reject with the new
AuthorizationDeclinedError and the attempt settles cancelled instead
of failed.
- NOT_COMMITTED now confirms a commit observed during the attempt
(credentials/record-updated for the flow's key), so a re-auth cannot
pass a stale record off as fresh; a flow that deletes its record is
refused on the same code.
READMEs, the subsystem/event/config catalogs, and the Agent Note follow
the shipped behavior; memory.ts carries the dedup TODO.
pi-ai's auth model reaches this adapter through three translations, all
of which live here: a CredentialStore over the harness credential
records, an AuthContext over the credential plane and the host
filesystem, and one authorization flow per installed provider that
ships a login. The seams they consume name nothing from pi-ai, so a
second adapter family can arrive with a different auth model and share
them.
Every collection is now built with the store and the context rather
than with nothing, which is what makes a signed-in provider stay signed
in across the collection rebuild a configuration change causes. With a
posture that works, the configurable-provider directory no longer
withholds OAuth-only routes and `openai-codex` is offered again; the
predicate that withheld it is gone.
The credential plane stays optional. Reads answer "nothing stored"
without a credentials service because such a composition genuinely
holds no credential, while writes refuse by name — a login whose grant
evaporated would report success and then fail every request. Flow
registration is scoped to the authorization seam, so a headless or ACP
composition mounts with no sign-in and everything else unchanged.
Two fixes found while wiring this up: pre-release credential fixtures
in the llm suites still used the flat document the record work
replaced, and a flow that ignores its cancellation signal would have
held its key for the life of the process — withdrawal now settles the
attempt either way.
Some credentials cannot be configured, only obtained: getting one means
a conversation — open this page, paste that code, pick an account. The
new seam owns that conversation and the one-attempt-per-key lifecycle,
and never the protocol, so a second authorization protocol arrives as
another flow rather than as another seam.
A flow is registered under the CredentialKey it writes, which is also
how the seam knows which plugin answers for the format inside that
record. The flow owns the write: run() resolving means the record is
already committed through ctx.credentials, and the seam confirms it.
That keeps a library persisting through its own store adapter the
single writer instead of being copied back out and written twice.
The interaction travels with the request rather than a registry,
because whoever starts an authorization is the one who can talk to the
human about it. A request already withdrawn never claims the key and
never starts the flow — relying on each flow to check its signal before
the first await would let one that does not hang holding the key.