| Field | Value |
|---|---|
| RFC | 0046 |
| Title | host.credentials capability — a portable credential resolution + lifecycle contract (store-at-rest, workspace sharing, two-key-overlap rotation, redaction-everywhere) |
| Status | Accepted |
| Author(s) | David Tufts (@davidscotttufts) |
| Created | 2026-05-24 |
| Updated | 2026-05-25 (Draft → Active → Accepted — MyndHyve workflow-runtime advertises the capability live on https://api.myndhyve.ai/.well-known/openwop (curl-verified 2026-05-25) and the cohort conformance scenarios pass against it: @openwop/openwop-conformance@1.6.0, revision workflow-runtime-00211-69w, commit 85275cdf87972e02c2e588cba481415f3e0edb15, 28 PASS / 0 FAIL across RFCs 0045/0046/0047/0048/0049/0051/0052/0053. Implementation per docs/openwop-adoption/0045-0054-cohort-summary.md; promoted per RFCS/0001 §"Promotion to Accepted".) |
| Affects | schemas/capabilities.schema.json (additive host.credentials block; additive workspace value in secrets.scopes enum) · schemas/credential-reference.schema.json (new — the wire shape of a _reference_, never the secret) · schemas/node-pack-manifest.schema.json (additive requiredCredentials[] node block, sibling to existing requiresSecrets[]) · spec/v1/host-capabilities.md (new §host.credentials) · spec/v1/run-options.md (the ai.credentialRef reserved-key annex is generalized) · SECURITY/invariants.yaml (new credential-payload-redaction invariant) · new conformance scenarios |
| Compatibility | additive |
| Supersedes | — |
| Superseded by | — |
Summary
Add an optional host.credentials capability — a sibling to host.fs / host.kvStorage / host.blobStorage (RFCs 0014–0019) — that defines a portable credential resolution + lifecycle contract: how a credential is stored at rest, shared across the workflows of a workspace, rotated with a grace window, and resolved into a node sandbox without ever putting plaintext on the wire. A pack references a credential by { ref, scope }; the host injects the resolved material into the node sandbox only, never into inputs, persisted variables, events, debug bundles, or replay state. A new SECURITY invariant (credential-payload-redaction) gates the redaction guarantee, mirroring mcp-toolcall-payload-redaction.
Motivation
openwop today has exactly one credential surface: the ai.credentialRef reserved run-option key (spec/v1/run-options.md §"Reserved keys") plus the capabilities.secrets advertisement ({ supported, scopes: ['tenant'|'user'|'run'], resolution: 'host-managed' }). That is enough to _pass an opaque reference into a run_ and have the host's SecretResolver dereference it internally — but it is not a first-class surface for:
- storing/encrypting a credential at rest,
- sharing one credential across many workflows in a workspace,
- rotating a credential with a grace window,
- redacting it consistently across events, debug bundles, and replay state.
MyndHyve hits this directly. It stores per-user secrets at users/{uid}/secrets and runs a workspace-shared BYOK vault. All of that lifecycle logic lives _above_ the protocol as host-private code, so it is uncertifiable, and no vendor.myndhyve.* pack can portably declare "I need credential X." Every host that wants a credential vault reinvents the redaction surface — which is exactly the kind of repeated bug surface (a token leaking into an event payload or a debug bundle) that warrants a single hoisted invariant, the same argument RFC 0014 made for host.fs path-traversal.
This is the foundation of Tier 1 in plans/myndhyve-protocol-extension-rfcs.md: RFC 0047 (OAuth) stores its acquired tokens here, and RFC 0045 (connector manifest) points its requiredCredentials declarations at this contract.
Proposal
§A — capabilities.schema.json: credentials block (additive)
Wire-path note — the advertised path is top-level
capabilities.credentials. The capability is advertised at top level, a sibling ofcapabilities.secrets/capabilities.queueBus, matching the corpus schema convention (the§host.naming is prose; the schema key is top-level, exactly asfs/queueBusare). An earlier draft of this section showed the block nested under ahostkey; the implemented and normative path — used by the §C MUST clauses, the §E invariant, andhost-capabilities.md§host.credentials — iscapabilities.credentials..
Add a top-level credentials block, mirroring the shape of the existing secrets / queueBus / blobStorage blocks:
"properties": {
"secrets": { ... },
+ "credentials": {
+ "type": "object",
+ "description": "RFC 0046. Portable credential resolution + lifecycle contract. A pack references a credential by `{ ref, scope }` (see `credential-reference.schema.json`); the host resolves it into the node sandbox ONLY — never into inputs, persisted variables, events, debug bundles, or replay state (SECURITY invariant `credential-payload-redaction`).",
+ "properties": {
+ "supported": { "type": "boolean" },
+ "scopes": {
+ "type": "array",
+ "items": { "type": "string", "enum": ["user", "workspace", "tenant"] },
+ "uniqueItems": true,
+ "description": "Subset of resolution scopes the host implements. `workspace` is the RFC 0048 sub-tenant; `tenant` and `user` align with the existing `secrets.scopes` vocabulary."
+ },
+ "encryptionAtRest": { "type": "boolean", "description": "Host encrypts stored credential material at rest." },
+ "rotation": {
+ "type": "string",
+ "enum": ["none", "two-key-overlap"],
+ "description": "`two-key-overlap`: old + new credential both resolve as valid during a grace window (mirrors `openwop-auth-api-key-rotation`)."
+ },
+ "sharing": { "type": "boolean", "description": "A single stored credential can be referenced by many workflows within a scope (e.g. a workspace-shared key)." }
+ },
+ "required": ["supported"],
+ "additionalProperties": false
+ }
}
Additionally, extend the existing capabilities.secrets.scopes enum with workspace (additive — no host that omits it changes behavior):
"scopes": {
"type": "array",
- "items": { "type": "string", "enum": ["tenant", "user", "run"] },
+ "items": { "type": "string", "enum": ["tenant", "user", "run", "workspace"] },
"uniqueItems": true
}
§B — credential-reference.schema.json (new — the reference, not the secret)
The only credential artifact that ever crosses the wire is the reference:
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://openwop.dev/schemas/credential-reference.schema.json",
"title": "CredentialReference",
"description": "RFC 0046. An opaque, host-issued handle to a stored credential. NEVER carries key material. The host's resolver dereferences it into the node sandbox at execution time.",
"type": "object",
"required": ["ref"],
"properties": {
"ref": { "type": "string", "minLength": 1, "description": "Opaque host-issued identifier, e.g. `cred_a3b9c2`. Hosts MUST NOT encode secret material in the ref." },
"scope": { "type": "string", "enum": ["user", "workspace", "tenant"], "description": "Resolution scope. Absent ⇒ host default scope." }
},
"additionalProperties": false
}
§C — Resolution contract (added to host-capabilities.md, normative)
A host advertising capabilities.credentials.supported: true MUST:
1. Resolve a { ref, scope } reference at node-execution time and inject the resolved material into the node sandbox only. The resolved value MUST NOT appear in inputs, persisted variables, channels, any run.* event payload, the debug bundle, or replay state. (This mirrors the framework-reserved-name stripping MyndHyve already performs and is gated by §E.) 2. Return a typed error envelope on resolution failure: credential_not_found (unknown ref), credential_forbidden (ref resolvable but out of the caller's scope — fail-closed), credential_scope_unsupported (scope not in the advertised scopes). 3. When sharing: true, resolve the same stored credential for every workflow that references it within the scope, without copying material between references.
§D — Two-key-overlap rotation (normative, when rotation: "two-key-overlap")
Rotation reuses the contract already verified for API keys in openwop-auth-api-key-rotation (and its auth-api-key-rotation.test.ts): during a grace window the old and new credential both resolve as valid; after the window the old MUST fail with credential_not_found. Canary-redaction MUST hold throughout: neither the old nor the new material may surface in any redaction-gated surface from §C.1.
§E — SECURITY invariant credential-payload-redaction
Add to SECURITY/invariants.yaml, sibling to mcp-toolcall-payload-redaction:
- id: credential-payload-redaction
tier: protocol
severity: critical
threat_model: SECURITY/threat-model-secret-leakage.md
tests:
- conformance/src/scenarios/credential-payload-redaction.test.ts
note: |
RFC 0046 §C.1: hosts advertising `capabilities.credentials.supported` MUST resolve a
credential reference into the node sandbox ONLY. Resolved material MUST NOT appear
in run inputs, persisted variables, channels, any run.* event payload, the debug
bundle, or replay state. The reference (`{ ref, scope }`) is the only credential
artifact permitted on the wire.
§F — Pack manifest declaration (additive)
Extend node-pack-manifest.schema.json with a node-level requiredCredentials[] block, a sibling to the existing requiresSecrets[] SecretRequirement array, so a pack declares its credential needs portably:
"PackNode": {
"properties": {
"requiresSecrets": { "type": "array", "items": { "$ref": "#/$defs/SecretRequirement" } },
+ "requiredCredentials": {
+ "type": "array",
+ "items": { "$ref": "#/$defs/CredentialRequirement" },
+ "description": "RFC 0046. Credentials the node needs. Resolved by the host's host.credentials resolver at dispatch time; refuses to register on a host lacking capabilities.credentials.supported."
+ }
}
}
where CredentialRequirement is { key: string, scope?: 'user'|'workspace'|'tenant', displayName?: string }.
Compatibility
Additive. New optional capability block; new optional schema; new optional node-manifest array; one new enum value (workspace) appended to secrets.scopes (appending to an enum is non-breaking — no existing advertisement is invalidated). Hosts without capabilities.credentials.supported ignore the block, and a pack declaring requiredCredentials refuses to register on them (peerDependency credentials: 'supported', the same pattern core.openwop.files uses against host.fs).
This supersedes the informal ai.credentialRef reserved-key annex with a first-class surface; that annex's existing behavior (opaque ref, host-side SecretResolver dereference, credential_forbidden on provider mismatch) stays valid and is now a special case of the general contract. The secrets.scopes advertisement remains valid.
Conformance
credentials-capability-shape.test.ts—host.credentialsblock validates;scopesis a subset of the enum. (Always runs.)credential-resolve-roundtrip.test.ts— aconformance.credential.echofixture node references a seeded credential and the host resolves it in-sandbox; the echo proves resolution without leaking. (Gated oncapabilities.credentials.supported.)credential-payload-redaction.test.ts— adversarial: assert the resolved material is absent from inputs, variables, every event payload, the debug bundle, and replay state. (Gated; backs §E.)credential-rotation-overlap.test.ts— old + new valid during the grace window; old fails after; canary-redaction holds. (Gated onrotation: 'two-key-overlap'.)
New fixtures: a seeded credential entry + the conformance.credential.echo node, catalogued in conformance/fixtures.md.
Alternatives considered
1. Leave credentials as the ai.credentialRef annex + secrets advertisement. Rejected — that surface can pass a reference but cannot express storage, workspace sharing, rotation, or a redaction invariant. MyndHyve's vault stays host-private and uncertifiable, and connector packs (RFC 0045) have nothing portable to point at. 2. Define a credential _store_ API (PUT/GET /v1/credentials). Rejected for v1 — putting credential _write_ operations on the wire enlarges the attack surface and forces every host to expose a CRUD endpoint. The contract here is resolution + lifecycle _advertisement_; how credentials are created (UI, OAuth dance per RFC 0047, admin import) stays host-internal. 3. Fold credentials into host.kvStorage. Rejected — KV has no redaction invariant, no rotation semantics, and no scope model; conflating them would weaken both.
Unresolved questions
1. Per-field rotation granularity. Should rotation be advertised per-scope (e.g. workspace rotates but user does not)? Deferred until an adopter needs split rotation. 2. Credential _metadata_ on the wire. Operators may want non-secret metadata (provider, expiry, last-rotated) visible in a UI. A future RFC could add a redaction-safe GET /v1/credentials/{ref}/meta. Deferred — App-layer concern until pulled.
Implementation notes (non-normative)
- Schema diffs (§A, §B, §F) and the SECURITY invariant (§E) land on
Activepromotion alongside the matching conformance scenarios;scripts/check-security-invariants.shfails if the invariant row lands without its test. - Reference-adopter target: MyndHyve maps its workspace vault to
scope: 'workspace', advertiseshost.credentials.{ rotation: 'two-key-overlap', sharing: true, encryptionAtRest: true }, and re-expressesusers/{uid}/secrets+ the workspace vault as the resolver behind the contract.
Acceptance criteria
- [x] Spec text merged (this file).
- [x]
credentialsblock (top-level, per the schema convention) +workspaceenum value incapabilities.schema.json. - [x]
credential-reference.schema.jsonadded. - [x]
requiredCredentials[]+CredentialRequirementinnode-pack-manifest.schema.json. - [x]
spec/v1/host-capabilities.md§host.credentials section. - [x]
credential-payload-redactioninvariant inSECURITY/invariants.yamlwith its test. - [~] Conformance scenarios — 2 of 4 landed:
credentials-capability-shape.test.ts(shape, always runs) +credential-payload-redaction.test.ts(adversarial redaction, capability-gated, seam soft-skips). The behavioral resolve-roundtrip + rotation-overlap scenarios are deferred until a host wires theconformance.credential.echoseam (same staging pattern asfs-path-traversal). - [x] CHANGELOG entry under
[Unreleased]. - [ ] A non-steward host advertises
host.credentialsand passes the redaction + rotation scenarios (drives the GOVERNANCE.md second-host tripwire).
Implementation note (2026-05-24): Spec + schema + SECURITY invariant + the two shape/redaction scenarios landed on main. Per the schema convention, the capability is advertised at top-level capabilities.credentials (the §A diff in this RFC showed it nested under host for readability; the prose name stays §host.credentials). Status stays Draft pending maintainer promotion + a non-steward host implementation.
References
RFCS/0014-host-fs-capability.md— the host-capability + SECURITY-invariant template this RFC follows.RFCS/0010-auth-profile-conformance.md—openwop-auth-api-key-rotation, the two-key-overlap rotation contract reused in §D.spec/v1/run-options.md§"Reserved keys" — theai.credentialRefannex this RFC generalizes.SECURITY/invariants.yaml—mcp-toolcall-payload-redaction(the sibling redaction invariant).plans/myndhyve-protocol-extension-rfcs.md— Tier 1 context; RFC 0047 + RFC 0045 depend on this.