deepseek-harness/docs/subsystems/credentials.md

289 lines
14 KiB
Markdown
Raw Normal View History

# User Credentials
English | [中文](credentials.zh.md)
The credential seam of [dsh-credentials](../../packages/credentials/credentials) keeps secrets out of configuration: settings sections and `cordis.yml` entries carry *references* (environment-variable names), providers such as [dsh-credentials-local](../../packages/credentials/credentials-local) own the values, and consumers resolve a reference once per operation — the LLM adapters resolve once per model request, so a rotated credential reaches the very next request without any restart. One seam-wide rule binds every provider: an empty stored value is absent everywhere.
Source: [`packages/credentials/credentials/src/index.ts`](../../packages/credentials/credentials/src/index.ts)
## Identity
2026-08-09 15:27:21 +08:00
A reference names one credential as a POSIX-style environment-variable name. The brand prevents callers from mixing credential references with other strings passed between packages or processes; construction validates the shell-identifier syntax.
```ts type-equiv
/** Nominal reference to one credential: a POSIX-style environment-variable name. */
type CredentialRef = Branded<'CredentialRef'>
```
## Resolution
`resolve(ref)` returns the value with the provider-defined source layer that supplied it, or `undefined` while unconfigured. Consumers re-resolve at each operation and never cache across operations — that per-operation read is the hot-update mechanism.
```ts type-equiv
/** One resolved credential value and the source layer that supplied it. */
interface ResolvedCredential {
/** The non-empty secret value. */
value: string
fix(config): close the review findings on configuration source ownership Two had real security consequences: The bootstrap rejection ran on npm dotenv's parser while process.loadEnvFile applied the file with Node's own. Two independently maintained dialects meant the check and the thing it guards could disagree: a name Node accepts but the checker misses would reach process.env unchecked, and BASH_ENV there runs a file of the project's choosing on every `bash -c` the bash tool issues. Parse once with node:util's parseEnv — the same engine loadEnvFile uses — and assign the entries already checked, which also drops the dotenv dependency. llm-pi-ai still returned a literal profile.apiKey ahead of everything, and it registers a settings namespace, so the defect removed from llm-deepseek survived intact in its design twin. The field is gone from the profile schema, the resolution path, and the tests. The rest are consistency and documentation defects the review named: - verify-config-source-ownership did not scan the Python runtime's bundled cordis.yml, which still inlined apiKey and baseURL. Both are covered now, and the line-anchored INLINE_DENY documents that it is a tripwire, not a parser. - The deny list missed NODE_TLS_REJECT_UNAUTHORIZED, the askpass hooks, the GIT_CONFIG_* redirections, and PYTHONHOME — all implied by its own stated rule about what a variable does. - Snapshot lookups folded case on Windows, where environment names are case-insensitive and an exact-match Map could miss a higher-ranked layer. - The credentials note claimed a read-time permission check was "not taken" while this PR implemented it; the credentials-local README still described two layers, live process.env reads, dotenv-era limitations, and a renamed anchor; the llm-deepseek README still advertised the removed literal apiKey; and web.ts and base.cordis.yml kept personal-overlay wording. - The ownership note's literal-apiKey claim now names its scope: the web-search providers keep a literal field but register no settings namespace, so nothing can shadow a stored credential through them.
2026-08-05 11:18:06 +08:00
/** Provider-defined source layer id (the local provider uses `env`, `file`, `project-env`, and `user-env`). */
source: string
}
```
## Description
`describe(ref)` answers configuration surfaces without ever exposing a value: whether the reference resolves, from which layer, and whether `set` would currently succeed. The local provider reports a reference supplied by the live process environment as `writable: false` — a write would appear to succeed while resolution kept returning the shadowing value, so the seam rejects it and the UI can render the reference read-only up front.
```ts type-equiv
/** Source and writability facts for one reference, safe for configuration UIs — never the value. */
interface CredentialInfo {
/** Whether {@link CredentialProvider.resolve} would currently return a value. */
configured: boolean
/** Source layer currently supplying the value; absent while unconfigured. */
source?: string
/** Whether {@link CredentialProvider.set} would currently succeed for this reference. */
writable: boolean
}
```
## Change commits
`credentials/reference-updated (ref)` fires after a committed change to a provider-managed source — a `set`, an `unset`, or an external edit observed in storage. Ambient process-environment changes are not observable and never emit. Consumers do not need the event (they re-resolve per operation); it exists for configuration surfaces refreshing a "configured" badge.
<!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
<a id="cordis-surface"></a>
## Cordis API
Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — the language sides differ only in locale-specific paired document paths. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
<a id="ctxauthorization--authorizationservice"></a>
### `ctx.authorization` — `AuthorizationService`
`ctx.authorization`: a registry of credential-obtaining flows, one attempt at a time per key.
```ts cordis-catalog
/**
* Offer a way to obtain one credential. One flow per key: two plugins
* claiming the same key would each write a record in their own format, and
* whichever ran last would leave the other reading a payload it cannot parse.
*
* @param flow - the key it writes, its label, its methods, and its runner.
* @returns Disposer that withdraws this flow.
* @throws {AuthorizationError} code `DUPLICATE_FLOW` when the key is already claimed.
*/
registerFlow(flow: AuthorizationFlow): () => void
/**
* Every registered flow, for a surface listing what can be authorized.
* @returns one entry per flow, in registration order.
*/
list(): readonly AuthorizationEntry[]
/**
* One registered flow.
* @param key - the credential record to ask about.
* @returns the entry, or undefined when no flow claims that key.
*/
describe(key: CredentialKey): AuthorizationEntry | undefined
/**
* Withdraw the attempt running for a key, if any. Separate from the
* request's own signal because a request/response transport answers a Cancel
* button on a second call, with no handle on the first one's signal.
* @param key - the credential record whose attempt should stop.
*/
cancel(key: CredentialKey): void
/**
* Run one attempt to authorize a key, and report how it ended.
*
* One attempt per key at a time. A second caller is refused rather than
* joined: the two would be prompting different humans through the same flow,
* and the second would answer questions the first was asked.
*
* @param request - the key, the method, the surface, and the cancel signal.
fix(credentials,authorization,llm-pi-ai): harden the auth seams per review 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.
2026-08-17 13:46:13 +08:00
* @returns `authorized` once the flow's record is committed during this
* attempt and observed, or `cancelled` when the human declined or the
* caller withdrew.
* @throws {AuthorizationError} code `NO_FLOW` when nothing claims the key,
* `UNKNOWN_METHOD` when the named method is not one the flow offers,
* `ALREADY_IN_FLIGHT` when an attempt is already running for the key, or
fix(credentials,authorization,llm-pi-ai): harden the auth seams per review 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.
2026-08-17 13:46:13 +08:00
* `NOT_COMMITTED` when the flow resolved without committing a record
* during the attempt.
*/
async begin(request: AuthorizationRequest): Promise<AuthorizationOutcome>
```
Merge remote-tracking branch 'origin/master' into codex/remove-cordis-catalog-line-numbers # Conflicts: # docs/subsystems/agent-team.i18n.yaml # docs/subsystems/approval.i18n.yaml # docs/subsystems/client-modules.i18n.yaml # docs/subsystems/client-modules.md # docs/subsystems/client-modules.zh.md # docs/subsystems/code-runtime.i18n.yaml # docs/subsystems/commands.i18n.yaml # docs/subsystems/commands.md # docs/subsystems/commands.zh.md # docs/subsystems/compaction.i18n.yaml # docs/subsystems/compaction.md # docs/subsystems/compaction.zh.md # docs/subsystems/core.i18n.yaml # docs/subsystems/core.md # docs/subsystems/core.zh.md # docs/subsystems/credentials.i18n.yaml # docs/subsystems/credentials.md # docs/subsystems/credentials.zh.md # docs/subsystems/feedback.i18n.yaml # docs/subsystems/filesystem.i18n.yaml # docs/subsystems/goal.i18n.yaml # docs/subsystems/http-server.md # docs/subsystems/http-server.zh.md # docs/subsystems/invariants.i18n.yaml # docs/subsystems/invariants.md # docs/subsystems/invariants.zh.md # docs/subsystems/jobs.i18n.yaml # docs/subsystems/llm-streaming.i18n.yaml # docs/subsystems/llm-streaming.md # docs/subsystems/llm-streaming.zh.md # docs/subsystems/permission-presets.md # docs/subsystems/permission-presets.zh.md # docs/subsystems/persistence.i18n.yaml # docs/subsystems/persistence.md # docs/subsystems/persistence.zh.md # docs/subsystems/plan.i18n.yaml # docs/subsystems/plan.md # docs/subsystems/plan.zh.md # docs/subsystems/sandbox.i18n.yaml # docs/subsystems/sandbox.md # docs/subsystems/sandbox.zh.md # docs/subsystems/schedule.i18n.yaml # docs/subsystems/session-projection.i18n.yaml # docs/subsystems/session-projection.md # docs/subsystems/session-projection.zh.md # docs/subsystems/session-query.i18n.yaml # docs/subsystems/session-reference.i18n.yaml # docs/subsystems/session-reference.md # docs/subsystems/session-reference.zh.md # docs/subsystems/session-telemetry.md # docs/subsystems/session-telemetry.zh.md # docs/subsystems/session-title.i18n.yaml # docs/subsystems/session.i18n.yaml # docs/subsystems/session.md # docs/subsystems/session.zh.md # docs/subsystems/settings.i18n.yaml # docs/subsystems/settings.md # docs/subsystems/settings.zh.md # docs/subsystems/shell.i18n.yaml # docs/subsystems/shell.md # docs/subsystems/shell.zh.md # docs/subsystems/skills.i18n.yaml # docs/subsystems/skills.md # docs/subsystems/skills.zh.md # docs/subsystems/spill.i18n.yaml # docs/subsystems/storage.i18n.yaml # docs/subsystems/subagent.i18n.yaml # docs/subsystems/subagent.md # docs/subsystems/subagent.zh.md # docs/subsystems/subprocess.i18n.yaml # docs/subsystems/system-prompt.i18n.yaml # docs/subsystems/system-prompt.md # docs/subsystems/system-prompt.zh.md # docs/subsystems/tasks.md # docs/subsystems/tasks.zh.md # docs/subsystems/terminal.md # docs/subsystems/terminal.zh.md # docs/subsystems/token-meter.i18n.yaml # docs/subsystems/tools.i18n.yaml # docs/subsystems/tools.md # docs/subsystems/tools.zh.md # docs/subsystems/typert.i18n.yaml # docs/subsystems/typert.md # docs/subsystems/typert.zh.md # docs/subsystems/user-interaction.i18n.yaml # docs/subsystems/user-questions.i18n.yaml # docs/subsystems/user-questions.md # docs/subsystems/user-questions.zh.md # docs/subsystems/web.i18n.yaml # docs/subsystems/workflow.i18n.yaml # docs/subsystems/workflow.md # docs/subsystems/workflow.zh.md # docs/subsystems/workspace.i18n.yaml # docs/subsystems/workspace.md # docs/subsystems/workspace.zh.md # packages/typert/generator/tests/cordis-catalog.spec.ts
2026-08-20 19:48:43 +08:00
Source: [`packages/credentials/authorization/src/index.ts`](../../packages/credentials/authorization/src/index.ts)
<a id="ctxcredentials--credentialprovider-abstract-seam"></a>
### `ctx.credentials` — `CredentialProvider` (abstract seam)
feat(credentials): store durable credential records beside references The seam answered one question — what is behind this environment-variable name — and that shape cannot hold what an authorization grant is: a multi-field, rotating value keyed by a provider id rather than by a POSIX identifier. The Models page already works around the gap by inventing a synthetic environment name (`MINIMAX_CN_API_KEY`) for a route the user added by hand, because the store's key must look like one. `CredentialKey` is `<scope>/<id>`, where the scope is the owning plugin's registered name. The owner is in the key because a `grant` payload is written in its owner's format: two plugins serving the same provider name would otherwise read each other's payload, and a record left by an uninstalled plugin could not be told from a live one. The `/` also keeps the grammar disjoint from `CredentialRef`, so the key spaces cannot collide. `CredentialRecord` is `api-key` (key and/or provider environment values) or `grant` (an opaque, owner-owned payload). The asymmetry is deliberate: an api key is the harness's own data, a grant is a package it carries for someone else. `modifyRecord` is the only write path because a correct write depends on the current value — a token refresh is read-decide-replace under one cross-process lock, without which two processes rotating one refresh token lose whichever wrote first. `.credentials.yaml` becomes a versioned two-section document. The pre-release flat layout is refused by name, with the entry count and the one edit needed, rather than read as an empty store — which would surface as an authentication failure on the first request instead of at load. A grant payload is admitted in both directions, so a value the document could not read back exactly as written is refused rather than stored lossily.
2026-08-13 15:00:09 +08:00
Abstract credential service over two key spaces that answer two questions.
A CredentialRef answers "what is behind this environment-variable name", layered over the process environment, the provider-managed store, and `.env` files. One seam-wide rule binds that half: an empty stored value is absent everywhere — `resolve` skips it, `describe` reports it unconfigured — so a blank never masquerades as a configured secret.
A CredentialKey answers "what credential does this plugin hold for this id". Nothing can layer here — an authorization grant has no environment to be read from — so presence of the record is the whole fact, and modifyRecord is the only write path because a correct write depends on the current value (a token refresh is read-decide-replace under one lock).
```ts cordis-catalog
/**
* Resolve one reference to its current value. Resolution is per call:
* consumers re-resolve at each operation and must not cache across
* operations — that per-operation read is what makes a changed credential
* reach the next operation without a restart.
* @param ref - the reference to resolve.
* @returns the value and its source, or `undefined` while unconfigured.
*/
abstract resolve(ref: CredentialRef): Promise<ResolvedCredential | undefined>
/**
* Describe one reference for configuration surfaces without exposing the
* value.
* @param ref - the reference to describe.
* @returns configured state, supplying source, and writability.
*/
abstract describe(ref: CredentialRef): Promise<CredentialInfo>
/**
* Durably store one value in the provider-managed writable source. Rejects
* while a read-only source shadows the reference — the write would appear
* to succeed while resolution keeps returning the shadowing value — and
* rejects an empty value (use {@link unset}).
* @param ref - the reference to store.
* @param value - the non-empty secret value.
*/
abstract set(ref: CredentialRef, value: string): Promise<void>
/**
* Remove one reference from the provider-managed writable source; removing
* an absent reference is a no-op. Rejects while a read-only source shadows
* the reference, like {@link set}.
* @param ref - the reference to remove.
*/
abstract unset(ref: CredentialRef): Promise<void>
feat(credentials): store durable credential records beside references The seam answered one question — what is behind this environment-variable name — and that shape cannot hold what an authorization grant is: a multi-field, rotating value keyed by a provider id rather than by a POSIX identifier. The Models page already works around the gap by inventing a synthetic environment name (`MINIMAX_CN_API_KEY`) for a route the user added by hand, because the store's key must look like one. `CredentialKey` is `<scope>/<id>`, where the scope is the owning plugin's registered name. The owner is in the key because a `grant` payload is written in its owner's format: two plugins serving the same provider name would otherwise read each other's payload, and a record left by an uninstalled plugin could not be told from a live one. The `/` also keeps the grammar disjoint from `CredentialRef`, so the key spaces cannot collide. `CredentialRecord` is `api-key` (key and/or provider environment values) or `grant` (an opaque, owner-owned payload). The asymmetry is deliberate: an api key is the harness's own data, a grant is a package it carries for someone else. `modifyRecord` is the only write path because a correct write depends on the current value — a token refresh is read-decide-replace under one cross-process lock, without which two processes rotating one refresh token lose whichever wrote first. `.credentials.yaml` becomes a versioned two-section document. The pre-release flat layout is refused by name, with the entry count and the one edit needed, rather than read as an empty store — which would surface as an authentication failure on the first request instead of at load. A grant payload is admitted in both directions, so a value the document could not read back exactly as written is refused rather than stored lossily.
2026-08-13 15:00:09 +08:00
/**
* Read one stored record. The value is returned as its owner wrote it; a
* {@link GrantRecord} payload is not interpreted on the way out.
* @param key - the record to read.
* @returns the record, or `undefined` while none is stored.
*/
abstract readRecord(key: CredentialKey): Promise<CredentialRecord | undefined>
/**
* Describe one record for configuration surfaces without exposing its value.
* @param key - the record to describe.
* @returns presence, discriminant, and writability.
*/
abstract describeRecord(key: CredentialKey): Promise<CredentialRecordInfo>
/**
* Enumerate every stored record's address and tag. Unlike the reference
* half, which has no enumeration because configuration surfaces learn which
* references exist from settings schemas, records have no such discovery
* path: a surface that cannot list them cannot show what a user is
* authorized for, nor find an orphan left by an uninstalled plugin.
* @returns every stored record, values excluded.
*/
abstract listRecords(): Promise<readonly CredentialRecordEntry[]>
/**
* Serialized read-modify-write over one record — the only write path.
* `mutate` sees the record as it stands at the moment the write is
* exclusive, and returning `undefined` leaves the entry untouched. Exclusion
* holds across processes where the backing store supports it, which is what
* makes a token refresh safe: two processes rotating one refresh token
* concurrently would otherwise lose whichever wrote first.
* @param key - the record to modify.
* @param mutate - receives the current record and returns its replacement, or `undefined` to leave it.
* @returns the record after the write, or the current one when `mutate` declined.
*/
abstract modifyRecord( key: CredentialKey, mutate: (current: CredentialRecord | undefined) => Promise<CredentialRecord | undefined>, ): Promise<CredentialRecord | undefined>
/**
* Remove one record; removing an absent record is a no-op.
* @param key - the record to remove.
*/
abstract deleteRecord(key: CredentialKey): Promise<void>
```
Source: [`packages/credentials/credentials/src/index.ts`](../../packages/credentials/credentials/src/index.ts)
<a id="authorization-events"></a>
### `authorization/*` events
<a id="authorizationsettled--emit"></a>
#### `authorization/settled` — emit
One authorization attempt has finished and released its key. Fires for every terminal outcome, failures included, so a surface watching a key it did not start (a second browser tab) learns the attempt is over.
```ts cordis-catalog
/**
* One authorization attempt has finished and released its key. Fires for
* every terminal outcome, failures included, so a surface watching a key it
* did not start (a second browser tab) learns the attempt is over.
* @mode emit
* @param key - the credential record the finished attempt was authorizing.
* @param settlement - how it ended, including the `failed` case its caller sees as a thrown error.
*/
'authorization/settled'(key: CredentialKey, settlement: AuthorizationSettlement): void
```
Merge remote-tracking branch 'origin/master' into codex/remove-cordis-catalog-line-numbers # Conflicts: # docs/subsystems/agent-team.i18n.yaml # docs/subsystems/approval.i18n.yaml # docs/subsystems/client-modules.i18n.yaml # docs/subsystems/client-modules.md # docs/subsystems/client-modules.zh.md # docs/subsystems/code-runtime.i18n.yaml # docs/subsystems/commands.i18n.yaml # docs/subsystems/commands.md # docs/subsystems/commands.zh.md # docs/subsystems/compaction.i18n.yaml # docs/subsystems/compaction.md # docs/subsystems/compaction.zh.md # docs/subsystems/core.i18n.yaml # docs/subsystems/core.md # docs/subsystems/core.zh.md # docs/subsystems/credentials.i18n.yaml # docs/subsystems/credentials.md # docs/subsystems/credentials.zh.md # docs/subsystems/feedback.i18n.yaml # docs/subsystems/filesystem.i18n.yaml # docs/subsystems/goal.i18n.yaml # docs/subsystems/http-server.md # docs/subsystems/http-server.zh.md # docs/subsystems/invariants.i18n.yaml # docs/subsystems/invariants.md # docs/subsystems/invariants.zh.md # docs/subsystems/jobs.i18n.yaml # docs/subsystems/llm-streaming.i18n.yaml # docs/subsystems/llm-streaming.md # docs/subsystems/llm-streaming.zh.md # docs/subsystems/permission-presets.md # docs/subsystems/permission-presets.zh.md # docs/subsystems/persistence.i18n.yaml # docs/subsystems/persistence.md # docs/subsystems/persistence.zh.md # docs/subsystems/plan.i18n.yaml # docs/subsystems/plan.md # docs/subsystems/plan.zh.md # docs/subsystems/sandbox.i18n.yaml # docs/subsystems/sandbox.md # docs/subsystems/sandbox.zh.md # docs/subsystems/schedule.i18n.yaml # docs/subsystems/session-projection.i18n.yaml # docs/subsystems/session-projection.md # docs/subsystems/session-projection.zh.md # docs/subsystems/session-query.i18n.yaml # docs/subsystems/session-reference.i18n.yaml # docs/subsystems/session-reference.md # docs/subsystems/session-reference.zh.md # docs/subsystems/session-telemetry.md # docs/subsystems/session-telemetry.zh.md # docs/subsystems/session-title.i18n.yaml # docs/subsystems/session.i18n.yaml # docs/subsystems/session.md # docs/subsystems/session.zh.md # docs/subsystems/settings.i18n.yaml # docs/subsystems/settings.md # docs/subsystems/settings.zh.md # docs/subsystems/shell.i18n.yaml # docs/subsystems/shell.md # docs/subsystems/shell.zh.md # docs/subsystems/skills.i18n.yaml # docs/subsystems/skills.md # docs/subsystems/skills.zh.md # docs/subsystems/spill.i18n.yaml # docs/subsystems/storage.i18n.yaml # docs/subsystems/subagent.i18n.yaml # docs/subsystems/subagent.md # docs/subsystems/subagent.zh.md # docs/subsystems/subprocess.i18n.yaml # docs/subsystems/system-prompt.i18n.yaml # docs/subsystems/system-prompt.md # docs/subsystems/system-prompt.zh.md # docs/subsystems/tasks.md # docs/subsystems/tasks.zh.md # docs/subsystems/terminal.md # docs/subsystems/terminal.zh.md # docs/subsystems/token-meter.i18n.yaml # docs/subsystems/tools.i18n.yaml # docs/subsystems/tools.md # docs/subsystems/tools.zh.md # docs/subsystems/typert.i18n.yaml # docs/subsystems/typert.md # docs/subsystems/typert.zh.md # docs/subsystems/user-interaction.i18n.yaml # docs/subsystems/user-questions.i18n.yaml # docs/subsystems/user-questions.md # docs/subsystems/user-questions.zh.md # docs/subsystems/web.i18n.yaml # docs/subsystems/workflow.i18n.yaml # docs/subsystems/workflow.md # docs/subsystems/workflow.zh.md # docs/subsystems/workspace.i18n.yaml # docs/subsystems/workspace.md # docs/subsystems/workspace.zh.md # packages/typert/generator/tests/cordis-catalog.spec.ts
2026-08-20 19:48:43 +08:00
Source: [`packages/credentials/authorization/src/index.ts`](../../packages/credentials/authorization/src/index.ts)
<a id="credentials-events"></a>
### `credentials/*` events
feat(credentials): store durable credential records beside references The seam answered one question — what is behind this environment-variable name — and that shape cannot hold what an authorization grant is: a multi-field, rotating value keyed by a provider id rather than by a POSIX identifier. The Models page already works around the gap by inventing a synthetic environment name (`MINIMAX_CN_API_KEY`) for a route the user added by hand, because the store's key must look like one. `CredentialKey` is `<scope>/<id>`, where the scope is the owning plugin's registered name. The owner is in the key because a `grant` payload is written in its owner's format: two plugins serving the same provider name would otherwise read each other's payload, and a record left by an uninstalled plugin could not be told from a live one. The `/` also keeps the grammar disjoint from `CredentialRef`, so the key spaces cannot collide. `CredentialRecord` is `api-key` (key and/or provider environment values) or `grant` (an opaque, owner-owned payload). The asymmetry is deliberate: an api key is the harness's own data, a grant is a package it carries for someone else. `modifyRecord` is the only write path because a correct write depends on the current value — a token refresh is read-decide-replace under one cross-process lock, without which two processes rotating one refresh token lose whichever wrote first. `.credentials.yaml` becomes a versioned two-section document. The pre-release flat layout is refused by name, with the entry count and the one edit needed, rather than read as an empty store — which would surface as an authentication failure on the first request instead of at load. A grant payload is admitted in both directions, so a value the document could not read back exactly as written is refused rather than stored lossily.
2026-08-13 15:00:09 +08:00
<a id="credentialsrecord-updated--emit"></a>
feat(credentials): store durable credential records beside references The seam answered one question — what is behind this environment-variable name — and that shape cannot hold what an authorization grant is: a multi-field, rotating value keyed by a provider id rather than by a POSIX identifier. The Models page already works around the gap by inventing a synthetic environment name (`MINIMAX_CN_API_KEY`) for a route the user added by hand, because the store's key must look like one. `CredentialKey` is `<scope>/<id>`, where the scope is the owning plugin's registered name. The owner is in the key because a `grant` payload is written in its owner's format: two plugins serving the same provider name would otherwise read each other's payload, and a record left by an uninstalled plugin could not be told from a live one. The `/` also keeps the grammar disjoint from `CredentialRef`, so the key spaces cannot collide. `CredentialRecord` is `api-key` (key and/or provider environment values) or `grant` (an opaque, owner-owned payload). The asymmetry is deliberate: an api key is the harness's own data, a grant is a package it carries for someone else. `modifyRecord` is the only write path because a correct write depends on the current value — a token refresh is read-decide-replace under one cross-process lock, without which two processes rotating one refresh token lose whichever wrote first. `.credentials.yaml` becomes a versioned two-section document. The pre-release flat layout is refused by name, with the entry count and the one edit needed, rather than read as an empty store — which would surface as an authentication failure on the first request instead of at load. A grant payload is admitted in both directions, so a value the document could not read back exactly as written is refused rather than stored lossily.
2026-08-13 15:00:09 +08:00
#### `credentials/record-updated` — emit
Committed change to a stored credential record: a `modifyRecord` that wrote, a `deleteRecord` that removed, or an external edit observed in storage. Separate from `credentials/reference-updated` because the two key grammars are disjoint — a listener that received both on one event could not tell which space a subject belongs to. Listener failures are contained on the same terms as `credentials/reference-updated`.
feat(credentials): store durable credential records beside references The seam answered one question — what is behind this environment-variable name — and that shape cannot hold what an authorization grant is: a multi-field, rotating value keyed by a provider id rather than by a POSIX identifier. The Models page already works around the gap by inventing a synthetic environment name (`MINIMAX_CN_API_KEY`) for a route the user added by hand, because the store's key must look like one. `CredentialKey` is `<scope>/<id>`, where the scope is the owning plugin's registered name. The owner is in the key because a `grant` payload is written in its owner's format: two plugins serving the same provider name would otherwise read each other's payload, and a record left by an uninstalled plugin could not be told from a live one. The `/` also keeps the grammar disjoint from `CredentialRef`, so the key spaces cannot collide. `CredentialRecord` is `api-key` (key and/or provider environment values) or `grant` (an opaque, owner-owned payload). The asymmetry is deliberate: an api key is the harness's own data, a grant is a package it carries for someone else. `modifyRecord` is the only write path because a correct write depends on the current value — a token refresh is read-decide-replace under one cross-process lock, without which two processes rotating one refresh token lose whichever wrote first. `.credentials.yaml` becomes a versioned two-section document. The pre-release flat layout is refused by name, with the entry count and the one edit needed, rather than read as an empty store — which would surface as an authentication failure on the first request instead of at load. A grant payload is admitted in both directions, so a value the document could not read back exactly as written is refused rather than stored lossily.
2026-08-13 15:00:09 +08:00
```ts cordis-catalog
/**
* Committed change to a stored credential record: a `modifyRecord` that
* wrote, a `deleteRecord` that removed, or an external edit observed in
* storage. Separate from `credentials/reference-updated` because the two key
feat(credentials): store durable credential records beside references The seam answered one question — what is behind this environment-variable name — and that shape cannot hold what an authorization grant is: a multi-field, rotating value keyed by a provider id rather than by a POSIX identifier. The Models page already works around the gap by inventing a synthetic environment name (`MINIMAX_CN_API_KEY`) for a route the user added by hand, because the store's key must look like one. `CredentialKey` is `<scope>/<id>`, where the scope is the owning plugin's registered name. The owner is in the key because a `grant` payload is written in its owner's format: two plugins serving the same provider name would otherwise read each other's payload, and a record left by an uninstalled plugin could not be told from a live one. The `/` also keeps the grammar disjoint from `CredentialRef`, so the key spaces cannot collide. `CredentialRecord` is `api-key` (key and/or provider environment values) or `grant` (an opaque, owner-owned payload). The asymmetry is deliberate: an api key is the harness's own data, a grant is a package it carries for someone else. `modifyRecord` is the only write path because a correct write depends on the current value — a token refresh is read-decide-replace under one cross-process lock, without which two processes rotating one refresh token lose whichever wrote first. `.credentials.yaml` becomes a versioned two-section document. The pre-release flat layout is refused by name, with the entry count and the one edit needed, rather than read as an empty store — which would surface as an authentication failure on the first request instead of at load. A grant payload is admitted in both directions, so a value the document could not read back exactly as written is refused rather than stored lossily.
2026-08-13 15:00:09 +08:00
* grammars are disjoint — a listener that received both on one event could
* not tell which space a subject belongs to. Listener failures are
* contained on the same terms as `credentials/reference-updated`.
feat(credentials): store durable credential records beside references The seam answered one question — what is behind this environment-variable name — and that shape cannot hold what an authorization grant is: a multi-field, rotating value keyed by a provider id rather than by a POSIX identifier. The Models page already works around the gap by inventing a synthetic environment name (`MINIMAX_CN_API_KEY`) for a route the user added by hand, because the store's key must look like one. `CredentialKey` is `<scope>/<id>`, where the scope is the owning plugin's registered name. The owner is in the key because a `grant` payload is written in its owner's format: two plugins serving the same provider name would otherwise read each other's payload, and a record left by an uninstalled plugin could not be told from a live one. The `/` also keeps the grammar disjoint from `CredentialRef`, so the key spaces cannot collide. `CredentialRecord` is `api-key` (key and/or provider environment values) or `grant` (an opaque, owner-owned payload). The asymmetry is deliberate: an api key is the harness's own data, a grant is a package it carries for someone else. `modifyRecord` is the only write path because a correct write depends on the current value — a token refresh is read-decide-replace under one cross-process lock, without which two processes rotating one refresh token lose whichever wrote first. `.credentials.yaml` becomes a versioned two-section document. The pre-release flat layout is refused by name, with the entry count and the one edit needed, rather than read as an empty store — which would surface as an authentication failure on the first request instead of at load. A grant payload is admitted in both directions, so a value the document could not read back exactly as written is refused rather than stored lossily.
2026-08-13 15:00:09 +08:00
* @param key - the record whose stored value changed.
* @mode emit
*/
'credentials/record-updated'(key: CredentialKey): void
```
Merge remote-tracking branch 'origin/master' into codex/remove-cordis-catalog-line-numbers # Conflicts: # docs/subsystems/agent-team.i18n.yaml # docs/subsystems/approval.i18n.yaml # docs/subsystems/client-modules.i18n.yaml # docs/subsystems/client-modules.md # docs/subsystems/client-modules.zh.md # docs/subsystems/code-runtime.i18n.yaml # docs/subsystems/commands.i18n.yaml # docs/subsystems/commands.md # docs/subsystems/commands.zh.md # docs/subsystems/compaction.i18n.yaml # docs/subsystems/compaction.md # docs/subsystems/compaction.zh.md # docs/subsystems/core.i18n.yaml # docs/subsystems/core.md # docs/subsystems/core.zh.md # docs/subsystems/credentials.i18n.yaml # docs/subsystems/credentials.md # docs/subsystems/credentials.zh.md # docs/subsystems/feedback.i18n.yaml # docs/subsystems/filesystem.i18n.yaml # docs/subsystems/goal.i18n.yaml # docs/subsystems/http-server.md # docs/subsystems/http-server.zh.md # docs/subsystems/invariants.i18n.yaml # docs/subsystems/invariants.md # docs/subsystems/invariants.zh.md # docs/subsystems/jobs.i18n.yaml # docs/subsystems/llm-streaming.i18n.yaml # docs/subsystems/llm-streaming.md # docs/subsystems/llm-streaming.zh.md # docs/subsystems/permission-presets.md # docs/subsystems/permission-presets.zh.md # docs/subsystems/persistence.i18n.yaml # docs/subsystems/persistence.md # docs/subsystems/persistence.zh.md # docs/subsystems/plan.i18n.yaml # docs/subsystems/plan.md # docs/subsystems/plan.zh.md # docs/subsystems/sandbox.i18n.yaml # docs/subsystems/sandbox.md # docs/subsystems/sandbox.zh.md # docs/subsystems/schedule.i18n.yaml # docs/subsystems/session-projection.i18n.yaml # docs/subsystems/session-projection.md # docs/subsystems/session-projection.zh.md # docs/subsystems/session-query.i18n.yaml # docs/subsystems/session-reference.i18n.yaml # docs/subsystems/session-reference.md # docs/subsystems/session-reference.zh.md # docs/subsystems/session-telemetry.md # docs/subsystems/session-telemetry.zh.md # docs/subsystems/session-title.i18n.yaml # docs/subsystems/session.i18n.yaml # docs/subsystems/session.md # docs/subsystems/session.zh.md # docs/subsystems/settings.i18n.yaml # docs/subsystems/settings.md # docs/subsystems/settings.zh.md # docs/subsystems/shell.i18n.yaml # docs/subsystems/shell.md # docs/subsystems/shell.zh.md # docs/subsystems/skills.i18n.yaml # docs/subsystems/skills.md # docs/subsystems/skills.zh.md # docs/subsystems/spill.i18n.yaml # docs/subsystems/storage.i18n.yaml # docs/subsystems/subagent.i18n.yaml # docs/subsystems/subagent.md # docs/subsystems/subagent.zh.md # docs/subsystems/subprocess.i18n.yaml # docs/subsystems/system-prompt.i18n.yaml # docs/subsystems/system-prompt.md # docs/subsystems/system-prompt.zh.md # docs/subsystems/tasks.md # docs/subsystems/tasks.zh.md # docs/subsystems/terminal.md # docs/subsystems/terminal.zh.md # docs/subsystems/token-meter.i18n.yaml # docs/subsystems/tools.i18n.yaml # docs/subsystems/tools.md # docs/subsystems/tools.zh.md # docs/subsystems/typert.i18n.yaml # docs/subsystems/typert.md # docs/subsystems/typert.zh.md # docs/subsystems/user-interaction.i18n.yaml # docs/subsystems/user-questions.i18n.yaml # docs/subsystems/user-questions.md # docs/subsystems/user-questions.zh.md # docs/subsystems/web.i18n.yaml # docs/subsystems/workflow.i18n.yaml # docs/subsystems/workflow.md # docs/subsystems/workflow.zh.md # docs/subsystems/workspace.i18n.yaml # docs/subsystems/workspace.md # docs/subsystems/workspace.zh.md # packages/typert/generator/tests/cordis-catalog.spec.ts
2026-08-20 19:48:43 +08:00
Source: [`packages/credentials/credentials/src/types.ts`](../../packages/credentials/credentials/src/types.ts)
feat(credentials): store durable credential records beside references The seam answered one question — what is behind this environment-variable name — and that shape cannot hold what an authorization grant is: a multi-field, rotating value keyed by a provider id rather than by a POSIX identifier. The Models page already works around the gap by inventing a synthetic environment name (`MINIMAX_CN_API_KEY`) for a route the user added by hand, because the store's key must look like one. `CredentialKey` is `<scope>/<id>`, where the scope is the owning plugin's registered name. The owner is in the key because a `grant` payload is written in its owner's format: two plugins serving the same provider name would otherwise read each other's payload, and a record left by an uninstalled plugin could not be told from a live one. The `/` also keeps the grammar disjoint from `CredentialRef`, so the key spaces cannot collide. `CredentialRecord` is `api-key` (key and/or provider environment values) or `grant` (an opaque, owner-owned payload). The asymmetry is deliberate: an api key is the harness's own data, a grant is a package it carries for someone else. `modifyRecord` is the only write path because a correct write depends on the current value — a token refresh is read-decide-replace under one cross-process lock, without which two processes rotating one refresh token lose whichever wrote first. `.credentials.yaml` becomes a versioned two-section document. The pre-release flat layout is refused by name, with the entry count and the one edit needed, rather than read as an empty store — which would surface as an authentication failure on the first request instead of at load. A grant payload is admitted in both directions, so a value the document could not read back exactly as written is refused rather than stored lossily.
2026-08-13 15:00:09 +08:00
<a id="credentialsreference-updated--emit"></a>
#### `credentials/reference-updated` — emit
Committed change to a provider-managed credential source: a `set`, an `unset`, or an external edit observed in storage. Ambient process-environment changes are not observable and never emit. Listener failures are contained and logged — a sync throw and an async rejection alike — without changing the committed operation's outcome, except `INVARIANT`-coded failures, which rethrow after every listener ran; that rethrow reaches the emitter only from synchronous listeners, so invariant checks on this event must not be async functions.
```ts cordis-catalog
/**
* Committed change to a provider-managed credential source: a `set`, an
* `unset`, or an external edit observed in storage. Ambient
* process-environment changes are not observable and never emit. Listener
* failures are contained and logged — a sync throw and an async rejection
* alike — without changing the committed operation's outcome, except
* `INVARIANT`-coded failures, which rethrow after every listener ran;
* that rethrow reaches the emitter only from synchronous listeners, so
* invariant checks on this event must not be async functions.
* @param ref - the reference whose stored value changed.
* @mode emit
*/
'credentials/reference-updated'(ref: CredentialRef): void
```
Merge remote-tracking branch 'origin/master' into codex/remove-cordis-catalog-line-numbers # Conflicts: # docs/subsystems/agent-team.i18n.yaml # docs/subsystems/approval.i18n.yaml # docs/subsystems/client-modules.i18n.yaml # docs/subsystems/client-modules.md # docs/subsystems/client-modules.zh.md # docs/subsystems/code-runtime.i18n.yaml # docs/subsystems/commands.i18n.yaml # docs/subsystems/commands.md # docs/subsystems/commands.zh.md # docs/subsystems/compaction.i18n.yaml # docs/subsystems/compaction.md # docs/subsystems/compaction.zh.md # docs/subsystems/core.i18n.yaml # docs/subsystems/core.md # docs/subsystems/core.zh.md # docs/subsystems/credentials.i18n.yaml # docs/subsystems/credentials.md # docs/subsystems/credentials.zh.md # docs/subsystems/feedback.i18n.yaml # docs/subsystems/filesystem.i18n.yaml # docs/subsystems/goal.i18n.yaml # docs/subsystems/http-server.md # docs/subsystems/http-server.zh.md # docs/subsystems/invariants.i18n.yaml # docs/subsystems/invariants.md # docs/subsystems/invariants.zh.md # docs/subsystems/jobs.i18n.yaml # docs/subsystems/llm-streaming.i18n.yaml # docs/subsystems/llm-streaming.md # docs/subsystems/llm-streaming.zh.md # docs/subsystems/permission-presets.md # docs/subsystems/permission-presets.zh.md # docs/subsystems/persistence.i18n.yaml # docs/subsystems/persistence.md # docs/subsystems/persistence.zh.md # docs/subsystems/plan.i18n.yaml # docs/subsystems/plan.md # docs/subsystems/plan.zh.md # docs/subsystems/sandbox.i18n.yaml # docs/subsystems/sandbox.md # docs/subsystems/sandbox.zh.md # docs/subsystems/schedule.i18n.yaml # docs/subsystems/session-projection.i18n.yaml # docs/subsystems/session-projection.md # docs/subsystems/session-projection.zh.md # docs/subsystems/session-query.i18n.yaml # docs/subsystems/session-reference.i18n.yaml # docs/subsystems/session-reference.md # docs/subsystems/session-reference.zh.md # docs/subsystems/session-telemetry.md # docs/subsystems/session-telemetry.zh.md # docs/subsystems/session-title.i18n.yaml # docs/subsystems/session.i18n.yaml # docs/subsystems/session.md # docs/subsystems/session.zh.md # docs/subsystems/settings.i18n.yaml # docs/subsystems/settings.md # docs/subsystems/settings.zh.md # docs/subsystems/shell.i18n.yaml # docs/subsystems/shell.md # docs/subsystems/shell.zh.md # docs/subsystems/skills.i18n.yaml # docs/subsystems/skills.md # docs/subsystems/skills.zh.md # docs/subsystems/spill.i18n.yaml # docs/subsystems/storage.i18n.yaml # docs/subsystems/subagent.i18n.yaml # docs/subsystems/subagent.md # docs/subsystems/subagent.zh.md # docs/subsystems/subprocess.i18n.yaml # docs/subsystems/system-prompt.i18n.yaml # docs/subsystems/system-prompt.md # docs/subsystems/system-prompt.zh.md # docs/subsystems/tasks.md # docs/subsystems/tasks.zh.md # docs/subsystems/terminal.md # docs/subsystems/terminal.zh.md # docs/subsystems/token-meter.i18n.yaml # docs/subsystems/tools.i18n.yaml # docs/subsystems/tools.md # docs/subsystems/tools.zh.md # docs/subsystems/typert.i18n.yaml # docs/subsystems/typert.md # docs/subsystems/typert.zh.md # docs/subsystems/user-interaction.i18n.yaml # docs/subsystems/user-questions.i18n.yaml # docs/subsystems/user-questions.md # docs/subsystems/user-questions.zh.md # docs/subsystems/web.i18n.yaml # docs/subsystems/workflow.i18n.yaml # docs/subsystems/workflow.md # docs/subsystems/workflow.zh.md # docs/subsystems/workspace.i18n.yaml # docs/subsystems/workspace.md # docs/subsystems/workspace.zh.md # packages/typert/generator/tests/cordis-catalog.spec.ts
2026-08-20 19:48:43 +08:00
Source: [`packages/credentials/credentials/src/types.ts`](../../packages/credentials/credentials/src/types.ts)
<!-- END GENERATED cordis-surface -->