All credential endpoints require authentication and workspace context unless otherwise noted.
Endpoints
Credential endpoints
Endpoint contract
Each endpoint identifies Auth, Request, Response, and Status. Unless noted, requests and responses are JSON, workspace routes require a session/CLI token plus membership, and secret values are never returned. The shared failures are401 unauthenticated, 403 unauthorized, 400 invalid
input, 404 missing or invisible resource, 409 uniqueness/lifecycle
conflict, and 500 unexpected handler/database failure. Test and reveal routes
may also return 429. Route gates and status writes are verified in
internal/api/router_crews.go, internal/api/credentials_mutate.go,
internal/api/credential_rotation.go, and internal/api/credential_fields.go.
Auth: Use the role/capability written on the endpoint; read visibility is
also enforced for crew-scoped credentials.
Request: JSON body and query parameters are shown on the endpoint. Secret
values may be sent only to create, rotate, test, or reveal operations.
Response: Credential metadata, rotation records, bindings, and field
metadata never contain the encrypted credential value; reveal is the sole
explicit exception and is separately audited.
Status: Success is 200 for reads/updates, 201 for creates, and 204
where a delete section says so. Invalid input is 400, authorization is 401
or 403, invisible resources are 404, conflicts are 409, and rate limits
are 429.
Managing Credentials
Explain your credential access
workspace_id query; no body.
Response: credential_id and actions, each with state (allowed,
conditional, or denied) and reason. reveal can only be conditional:
fresh interactive login, a written reason and a successful audit write are
checked by the reveal action. A CLI token receives non_interactive_auth.
No secret value is read or returned by this endpoint.
Status: 200 for a visible credential, 401 without authentication,
404 for a missing or hidden credential, and 500 if a policy or capability
lookup fails. The response is not cached.
Routine dependents
GET /api/v1/credentials/{credentialId}/dependents returns visible, saved
routines that statically declare the credential’s type through
credentials_required, credential_ref.type, or {{ secrets.<type> }} in
supported steps and hooks. Each row has a routine slug, name, type, and
resolution: would_resolve if the runtime’s current ACTIVE selection points
to this credential, another_credential if a different matching row wins, or
unavailable if no row qualifies. The response also includes
visibility_limited, dynamic_uses_untracked, and
recorded_use: "not_attributed". It never returns secret values.
These are configured references and a present-time selection estimate, not a
run-level usage record. Agent and script lookups performed dynamically cannot
be inferred from the routine definition. Hidden routines are excluded for
non-management roles. Selection follows the runtime’s credential_crews
grants for any live crew, with an unlinked workspace credential as fallback;
legacy crew_id alone does not grant access.
crewship credential dependents <name-or-id> shows
the same view in the CLI.
Core CRUD for the vault — create, read, update, and soft-delete credential records. Secret values are encrypted on write and omitted from every response.
List Credentials
PROVIDER_LOGIN row, and an AI_CLI_TOKEN / API_KEY
row whose provider is a model provider, is dropped from the list for every
lower role (credentialVisibilityFilter in internal/api/credentials_loaders.go),
and any per-id route answers 404 for it below OWNER/ADMIN
(providerAccountPolicy in internal/api/provider_login_policy.go) — a
capability grant for ordinary secrets does not cross this boundary. Delivery to
agents is unchanged.
Query Parameters: (parseListPagination in internal/api/credentials.go:81)
The response carries
X-Total-Count, X-Limit and X-Offset describing the
window (see Paging under List Crews); the cursor form below carries
X-Total-Count and X-Limit too.
Ordering is type ASC, created_at DESC, id ASC, so paging with limit/offset is stable across requests.
Pass paginate=true to use the opt-in cursor form instead of the legacy array.
That form defaults to limit=50, caps limit at 500, orders by
created_at DESC, id DESC, and returns { "credentials": [...], "next_cursor": "...", "limit": 50 }; next_cursor is null on the last
page. A malformed cursor or a non-positive limit in this mode returns 400.
Response: 200 OK
Response Fields
Create Credential
MANAGER+ role, or a member with the credential.create capability
for this workspace. A provider account (type: PROVIDER_LOGIN, or
AI_CLI_TOKEN / API_KEY with a model provider) needs OWNER or ADMIN;
below that the request is 403 Provider accounts require OWNER or ADMIN, and
the credential.create capability does not lift it.
Request Body:
value is encrypted with AES-256-GCM before storage using the format v1:{base64(IV||AuthTag||Ciphertext)}.
Response: 201 Created — credential object (without the encrypted value).
Get Credential
200 OK — credential object with crew_ids, agent_names, and mcp_used.
Update Credential
OWNER, ADMIN, or MANAGER role (canRole(role, "update") in internal/api/credentials_mutate.go). A MANAGER cannot reach a provider account at all — the row is 404 for them (see List).
Updatable Fields:
Note: The
status field cannot be updated via this endpoint. Status changes are managed by the credential monitor and OAuth refresh worker.
Re-login (PROVIDER_LOGIN with value). Sending a value to a provider
login replaces the login in place, keeping the credential’s id and every
assignment and binding: the value is split into its parts again (access token,
refresh token, id token, account, plan, expiry) the way create splits it, the
previous parts and the refresh state (provider_login_refresh) are discarded,
and token_expires_at is taken from the new token. type and provider must
stay what they are — a request that changes either alongside value is
400 re-login must keep the existing provider and type. This is how a login
that reached needs_relogin is brought back without re-granting it.
Response: 200 OK — updated credential object.
Delete Credential
OWNER or ADMIN role
Response: 200 OK
Validating Credentials
Probe a value against the provider’s live API — either an unsaved value before storing it, or an existing stored credential.Test Credential
internal/api/credentials_test_endpoint.go:229)
200 OK with { "valid": bool, "status": int, "error": string | null, "supported": bool }.
error carries omitempty, so on a clean probe the key is absent rather than empty or null. The generated spec declares it nullable: true, so a generated client may also admit null; treat absent, "" and null as the same “no error”. Read supported and valid, not the presence of error.
supported reports whether this credential shape has a probe, not whether an upstream request was dialled on this route. For OPENAI_COMPAT and for any ENDPOINT_URL it answers true while this route validates only that the URL is well-formed — the reachability dial is withheld here on purpose. See the note under Test Stored Credential below.
For type: PROVIDER_LOGIN, an optional mode (api_key or subscription) selects the credential shape; when omitted, Crewship infers it from the value. Only api_key mode uses the provider’s API-key probe.
Test Stored Credential
internal/api/credentials_test_endpoint.go:253)
Auth: OWNER, ADMIN, or MANAGER role (canRole(role, "update")). Rate-limited to 60 requests/minute per IP.
Response: 200 OK with { "valid": bool, "status": int, "error": string | null, "supported": bool }.
supported reports whether this stored credential has a real upstream probe. Crewship maintains probes for API-key credentials from ANTHROPIC, OPENAI, OPENAI_COMPAT, GOOGLE, OPENROUTER, CURSOR, FACTORY, GITHUB, GITLAB and VERCEL (probeSupportedProviders in internal/api/credentials_test_endpoint.go), plus any credential of type ENDPOINT_URL whatever its provider — for that type the stored value is the target being dialled. A PROVIDER_LOGIN in api_key mode uses its provider’s API-key probe; an Anthropic subscription setup-token retains its OAuth probe. An OpenAI PROVIDER_LOGIN in subscription mode does not: Crewship stores the OAuth access token extracted from a Codex auth.json and reconstructs the login file for the CLI; sending that token as an OpenAI API key tests the wrong credential. Older OpenAI AI_CLI_TOKEN auth blobs are likewise unprobeable. These return supported: false, valid: true under the existing unchecked-result convention, and an explanation in error. This is not proof that the login works; use a controlled agent run. Every other unprobeable provider likewise answers valid: true with supported: false — nothing failed because nothing was attempted.
OPENAI_COMPAT and ENDPOINT_URL are actually dialled only here. Their endpoint is operator-supplied, so the dial is withheld on POST /api/v1/credentials/test above — that route is authenticated but carries no workspace or role floor, and dialling a caller-supplied host from it would make the server a blind-SSRF and internal-port-scan oracle. It answers “the endpoint URL is well-formed” and stops. This route is gated on canRole(role, "update") plus workspace visibility, and the URL was already vetted when the credential was created, so the reachability dial happens: the dialer resolves DNS first and refuses link-local and metadata addresses on the concrete IP, redirects are never followed so a 3xx cannot walk the probe onto a host the guard refused, and the probe offers no credential at all — not the stored key, not the custom headers. It asks for a model list, because reachability is the thing an operator cannot otherwise find out, and authentication is not worth turning the Test button into a way to post the stored secret at a baseURL that anyone holding “update” can repoint.
Read supported before valid: supported: false means the credential was not checked, which is neither a pass nor a failure. valid retains its original meaning for existing clients.
Refresh Provider Login
docs/prd/provider-logins.md §10.4). Only a
PROVIDER_LOGIN in subscription mode with a stored refresh_token part —
a ChatGPT login — can be refreshed. On success the new access, refresh and id
tokens and the expiry are stored, a REFRESH audit event is recorded, and the
file is re-rendered into the running containers of every Codex agent the
login pays for. On a provider failure the login’s refresh state carries the
error and the backoff; after three consecutive failures, or one the provider
calls permanent (invalid_grant), refresh.status is needs_relogin and
current workspace administrators get an inbox message, even if the original
creator has since been demoted. The server also refreshes on its own: 24 h
before expiry from the credential monitor, and before a run start with less
than 48 h left.
Auth: OWNER, ADMIN, or MANAGER role (canRole(role, "update")) plus workspace visibility.
Request: No body.
Response: 200 OK with { "login": { … } } — the login object after the refresh. 502 Bad Gateway with { "error", "login" } when the provider refused; the login’s refresh.status and refresh.error say why.
Rotation & Audit
Rotate secrets without downtime via an overlap grace window, inspect the append-only audit trail, and manage rotation history.Audit Timeline
USE, ROTATE, TEST, REVOKE, DETECTED, CREATED events). Backs the Audit tab in the detail Sheet. (internal/api/credential_audit.go:276)
Auth: OWNER, ADMIN, or MANAGER role (canRole(role, "update")). Audit reveals admin-action IPs, so VIEWER/MEMBER are blocked.
Query Parameters:
Response:
200 OK — JSON array (newest first).
Rotate Credential
credential_rotations row for the grace window, then scrubbed when status transitions to EXPIRED or CANCELLED. (internal/api/credential_rotation.go:77)
While the window is open, the previous value is consumed on a 401. A crew sidecar that boots inside the window receives it beside the new value (never in the agent’s environment), and when the provider answers 401 to the new value the sidecar replays the request exactly once with the previous one; the replay’s answer is what the agent sees, so a second 401 surfaces. Nothing else triggers it — a 403, a 429 or a 5xx is not retried, and only the rotated credential’s own previous value is used. The replay is recorded as a network.egress journal entry naming the credential and the rotation. A sidecar that was already running before the rotation is still on the previous value and needs no fallback.
Auth: OWNER or ADMIN role, or any member holding the credential.rotate capability (layered gate requireRoleOrCapabilityOrForbid — lets an oncall MANAGER/MEMBER rotate a leaked token without blanket vault access)
Request Body:
200 OK
EXPIRED immediately when grace_seconds is 0; otherwise
it starts as ACTIVE. cancelled_at is nullable and is set when a rotation
is cancelled.
List Rotations
internal/api/credential_rotation.go:209)
Auth: Session or CLI token + workspace membership. The same visibility filter as GET /credentials/{id} applies: MANAGER+ see every credential’s history; MEMBER/VIEWER only see WORKSPACE-scoped credentials or CREW-scoped ones belonging to a crew they are a member of.
Response: 200 OK — JSON array. Each entry also includes nullable
cancelled_at when a rotation was cancelled.
old_value_gone is true once status is EXPIRED or CANCELLED (the old encrypted value has been scrubbed from the row).
Rotation Statuses
Cancel Rotation (End Grace Early)
ACTIVE grace overlap immediately, scrubbing the stored old value. Idempotent: already-terminal rotations return 200 with the existing status. (internal/api/credential_rotation.go:258)
Auth: OWNER or ADMIN role, or any member holding the credential.rotate capability (same layered gate as rotate — ending a grace window early is part of the rotation lifecycle)
Response: 200 OK
Bindings
A credential’sname is the account (github-acme). The slot is the environment variable the agent reads (GH_TOKEN). A binding connects them in one scope:
- One slot, one credential, per scope. Enforced by a unique index; a conflicting write is refused with
409, never applied as a replacement. - Resolution is agent > crew > workspace. The most specific scope that binds a slot wins, and only that one is delivered.
name — the pre-binding behaviour, unchanged.
List Bindings
scope, crew_id, agent_id, credential_id, slot.
Auth: Session or CLI token, workspace context required.
Response: 200 OK
Create Binding
OWNER / ADMIN (manage).
Response:
201 Created with the binding.
Delete Binding
OWNER / ADMIN (manage). Response: 204 No Content, or 404 if the binding is not in the caller’s workspace.
The row is really deleted — it is the slot claim, so anything that merely hid it would keep the slot occupied and 409 every later write.
Bindings also cascade with their scope: deleting a crew, an agent, or the credential itself removes the bindings that pointed at it.
Resolve Slots for an Agent
200 OK
source, most specific first: agent_grant (an explicit per-agent
assignment), agent_binding, crew_binding, workspace_binding, crew_link
(no binding — delivered under the credential’s own name). The response may
also contain a warnings array when a credential was renamed, normalised, or
could not be assigned a deliverable slot; warnings never contain secret values.
Custom Fields
Custom fields extend a credential with named parts such as a non-secret region or an additional secret. Field keys are lower snake case, start with a letter, and are limited to 64 characters. Secret field values are encrypted and are never returned; non-secret values are returned asvalue.
List Custom Fields
200 OK
Add Custom Field
MANAGER+ role.
is_secret defaults to true when omitted. value is required and is capped
at 64 KiB. The response is 201 Created with the field object. A missing or
invisible credential returns 404; invalid keys/values or a duplicate key
returns 400.
Update Custom Field
PUT /api/v1/credentials/{credentialId}/fields/{fieldKey}
Auth: Authenticated workspace member with the create capability (the
same write tier as updating the credential). Credential visibility is enforced
against the current workspace; an invisible credential is reported as 404.
Request: Path parameters are credentialId and fieldKey. JSON body:
value is required and capped at 64 KiB. is_secret and ordinal are
optional; omitted is_secret preserves the field’s current classification,
and the path key cannot be renamed.
Response: 200 OK with the updated field object. Secret field values are
omitted (value is null); non-secret values are returned.
Statuses: 400 invalid JSON, key, value, or ordinal; 401 unauthenticated;
403 insufficient role/capability; 404 credential or field not found; 500
unexpected database or encryption failure.
Delete Custom Field
DELETE /api/v1/credentials/{credentialId}/fields/{fieldKey}
Auth: Authenticated workspace member with the create capability.
Credential visibility is enforced against the current workspace.
Request: No body. credentialId and fieldKey are required path
parameters.
Response: 200 OK with { "success": true }.
Statuses: 401 unauthenticated; 403 insufficient role/capability; 404
credential or field not found; 500 unexpected database failure.
Lookups & Assignment
Helper lookups and the cross-reference to per-agent credential assignment.Default Environment Variable
GH_TOKEN for GITHUB), and whether that provider can be probed. Recognised providers: GITHUB → GH_TOKEN, GITLAB → GITLAB_TOKEN, VERCEL → VERCEL_TOKEN, AWS → AWS_ACCESS_KEY_ID, KUBERNETES → KUBECONFIG; any other provider returns an empty string.
The optional type parameter affects testable only: a credential of type ENDPOINT_URL is probeable whatever its provider, because the stored value is itself the target being dialled. env_var is derived from provider alone.
Auth: Session or CLI token (no workspace context needed)
Response: 200 OK
testable lets a client decide whether to offer a “Test” action without keeping its own list of probeable providers — the dashboard’s Add Credential form reads it for exactly that. The same flag rides on each credential in List and Get responses, so a detail view needs no extra call.
Agent Credential Assignment
See the Agents page for credential assignment endpoints:Provider logins — sign in with a device code
A subscription login for a model provider can be created without pasting a file: the server drives the provider’s device-code authorization, the person enters a short code in the provider’s browser page, and the resulting login is stored as aPROVIDER_LOGIN credential owned by the caller. The server —
not the client — polls the provider, so a closed tab or a CLI timeout does
not lose the sign-in, and a pending sign-in survives a server restart.
Only OPENAI (ChatGPT plans, delivered to the Codex adapter as a rendered
auth.json) is offered today. CLI: crewship credential login --provider OPENAI.
Start a device-code sign-in
OWNER or ADMIN. Provider-account administration has
this role floor even when a lower role has the credential.create capability.
Request body:
201 Created
user_code and verification_url to the person, then poll the status
endpoint every interval_s seconds until status leaves pending. The code
is valid for 15 minutes.
Device-code sign-in status
404.
Response: 200 OK
The created credential is named
"ChatGPT <plan> · <email>" from the token’s
plan claim and the account’s email, carries the whole login as its value
(the refresh token is sealed on the server and replaced by a placeholder in
every delivered file), and is otherwise exactly the row Create
Credential would have produced — including its
CREATED audit event.
Provider pools (account groups)
A pool — “account group” in the Credentials page — is a named set of provider accounts of one provider and one billing mode, in priority order, so that when one seat is unavailable another can take its turn. The API and CLI call it a pool; the UI calls it an account group; they are the same object (internal/api/provider_pool.go, internal/providerpool).
A pool is a definition only. Creating one does not bind it to any agent,
grant its members to anyone, decrypt a token or contact a provider; runtime
selection is not wired to it yet. Every pool route, reads included, requires
OWNER or ADMIN; a scoped CLI token additionally needs credentials:write
for create, update and delete (reads need no scope). Capabilities for ordinary
secrets do not grant pool access. Secret values are
never returned. The CLI is crewship credential pool …
(reference).
Revisions and ETag. Each pool carries a revision that starts at 1
and increases by one on every PUT. GET one pool returns it as a strong
ETag ("2"); PUT and DELETE require it back as If-Match and answer
412 when the pool has moved on, 428 when the header is missing, and 400
when it is not one quoted integer (wildcards, weak tags and lists are refused).
A pool member must be a credential of this workspace, and members must all
carry the pool’s provider and mode:
PROVIDER_LOGIN rows, or legacy API_KEY
rows (read as api_key). Unknown and foreign-tenant ids fail the same way
(400), so the existence of a credential in another tenant is not disclosed.
Accounts owned by different people can only be pooled with
allow_cross_owner: true; a member with no recorded owner cannot be pooled at
all.List provider pools
Response:
200 OK — up to 100 pools without their member lists.
next_cursor is null on the last page. Retired pools are not listed.
Create provider pool
400); the body limit is 64 KiB.
201 Created with ETag: "1" — the pool as Get returns it, revision 1, members echoed.
An unavailable member (a seat waiting on re-login) does not fail creation; a
malformed or mixed-owner set does. The create is audited
(
provider_pool.create) with the member count and consent flag only.
Get provider pool
200 OK with ETag: "<revision>" — the definition read in one
snapshot, including members whose seat is revoked or expired, so an
administrator can diagnose the set. It is not an availability probe.
Update provider pool
PUT /api/v1/provider-logins/pools/{poolId}
Authentication: workspace OWNER/ADMIN; scoped CLI tokens also require
credentials:write. Ordinary-secret capabilities do not grant pool access.
Request headers: If-Match: "1" using the current ETag from
Get. Wildcards, weak tags and tag lists are not accepted. Request body: complete
replacement definition, not a patch:
{"error":"…"}. Inspect the new
definition before retrying 412. Audit uses the existing best-effort settings
audit; it is not transactionally coupled to the definition update.
Delete provider pool
DELETE /api/v1/provider-logins/pools/{poolId}
Authentication: workspace OWNER/ADMIN, with credentials:write for scoped
CLI tokens. Request headers: current If-Match: "2"; no request body.
Response: 204, no body. Soft deletion hides the pool from list/detail and
rejects future selection. Provider accounts and membership history remain;
the name stays reserved. No upstream account is revoked.
Status codes: 400 invalid revision, 401 unauthenticated, 403 forbidden, 404
missing/foreign/already-retired pool, 412 stale revision, 428 missing If-Match,
500 internal failure. Errors use {"error":"…"}. Retirement is audited through
the same best-effort administrative audit. This increment manages definitions
only; runtime bindings and active-execution protection are not enabled yet.
Reference
Enums and value sets used across the credential endpoints above.Credential Types
The closed type enum (internal/api/credentials_types.go, enforced by both Create and Update):
Providers
provider is a free-form string. The values below are the ones with a
live validation probe in the Test / Test Stored endpoints
(internal/api/credentials_test_endpoint.go); any other provider value
(including NONE) is accepted and stored, but Test returns
valid: true with "No validation available for this provider".
Statuses
Revoking an LLM/provider token stops proxy injection within ~60s. A revoked credential is filtered out of every fresh delivery path immediately, and each agent’s sidecar also holds an in-memory snapshot of its provider tokens (loaded at container start). The sidecar reconciles that snapshot against the live set on a ~60s interval and drops any credential the server no longer lists (revoked or deleted), so a revoked provider key stops being injected into new outbound LLM requests within one interval — without waiting for the container to restart.File-based secrets are removed on revoke, too. File-based secrets written to
/secrets/{agent-slug}/ (DB passwords, GH_TOKEN, SSH keys, certs) are materialized on disk at container start. On revoke (deleting a credential), the server reaches into every running crew container that mounted the credential as a file and removes the secret file(s) — exec’d inside the container as the agent UID (1001), the only principal that can unlink inside the agent’s 0700 secrets directory (the sidecar, UID 1002, cannot). This runs synchronously with the delete, so a successful revoke has already removed the files from live containers.Removal is best-effort: a container that isn’t running is skipped (nothing to remove, and the revoked credential won’t be re-materialized on the next boot since it’s filtered from every delivery path). The
.env hint-map keeps a now-inert reference to the removed file until the next container boot — harmless, since the file it points at is gone. If an exec fails transiently, recreate the crew container to force a clean rewrite.Reveal
Reveal is the only endpoint in Crewship that returns a stored secret in plaintext. Every other read path omits values entirely. A reveal succeeds only when all of the following hold. Each is checked independently and each fails closed, so breaking one does not get you past the others.Agents, API tokens and CI can never reveal
The endpoint requires an interactive, revocable user session. A CLI token (crewship_cli_… / crewship_admin_…), an X-Internal-Token, a sidecar request, and any future auth method that does not explicitly opt in are all refused with 403.
This is a gate on the credential shape, not on identity. An agent’s request carries a real user’s id and role — that is how delegation works — so “who is this?” cannot separate a person from a container. “What did they present?” can. An agent that needs a secret gets it through the normal delivery paths (env var, /secrets mount) or asks a human via Keeper escalation; it never reads one back out of the vault.
Because of this, crewship credential reveal asks for your password and signs you in interactively rather than using the stored CLI token.
Capability, not role
credentials:reveal is granted per person on the membership and is what the
gate checks; the role floor only says who may hold it. Since #2462 the
admin preset includes it, and an OWNER or ADMIN whose membership has no
explicit capability set holds it by default (BundleAdmin in
internal/api/capabilities.go, the role fallback in capabilities_check.go).
An explicit set is always authoritative: revoking credentials:reveal from an
ADMIN removes it, and a MANAGER holds it only when granted. See
Member capabilities.
Recommended for corporate deployments: revoke it from the admin team and grant
it to two people.
A fresh sign-in, on your own password
The value is disclosed only to a session created in the last 5 minutes (internal/api/credentials_reveal_freshness.go). The clock is the server-side
session creation time, not the JWT refresh or last activity, so a stolen
session that is kept alive does not earn a reveal window; the CLI satisfies
this by signing you in for the call. An account still on the seed default
password is refused as well (default_password) — change it in account
settings and sign in again.
Reveal Credential
http.cred_reveal_per_min, default 3/min) — far tighter than the general API bucket, and unlike it, authenticated CLI callers are not exempt.
Request Body:
200 OK with Cache-Control: no-store
journal_entry_id anchors the disclosure to the tamper-evident audit chain — quote it in an incident report and it can be verified.
Errors:
The audit write is a precondition
The reveal is recorded ininternal/journal, which is an HMAC hash-chain with signed compaction checkpoints, before the value is read out of the row. If that write fails, the request returns 500 and no value is returned. There is no “return it now, audit later” path.
The flat credential_audit timeline also gets a REVEAL row so the disclosure appears in the credential’s detail Sheet next to every other event on it. That table has no hash chain — a row can be deleted without trace — which is exactly why it is not the authoritative record.
What the journal payload contains: actor, credential id and name, classification, reason, actor role, client IP.
What it never contains: the value, or any hash or fingerprint of it. A digest looks like a harmless identifier and is not — journal rows are readable by anyone with journal access and travel in every backup, and a digest of a short secret is crackable offline in seconds. Recording one would turn the audit log into a second copy of the vault.
Classification
Every credential carries asensitivity. It is the first thing the reveal gate consults and the only one that no role can override.
SEALED has no escape hatch by design. Break-glass for a sealed credential is rotation: mint a new value, let the old one drain through its grace window. An escape hatch that exists is an escape hatch that gets used.
Existing credentials default to STANDARD, which is safe only because the workspace reveal switch defaults to off.
RESTRICTED behaves identically to STANDARD today. The four-eyes second-approver flow that will distinguish them is deferred; classifying now still pays off, because that gate will key off this field.Set Classification
200 OK
200 and is not journaled.
Errors: 400 unknown class · 403 lowering without OWNER/ADMIN · 404 not in this workspace · 500 audit write failed (the classification is unchanged)
Workspace Reveal Policy
Reveal is off for every workspace until an OWNER turns it on, including freshly created ones. A default of “on” would make a new tenant’s security posture depend on someone remembering to lock it.Read Reveal Policy
403 — whether a tenant has reveal enabled is target-selection information. A MANAGER reads it because they need to know the rules they work under, but cannot change it.
Response: 200 OK
Set Reveal Policy
200 OK
credentials:reveal on their membership.
Errors: 400 body is not {"enabled": bool} · 403 caller is not OWNER · 500 audit write failed (switch unchanged)