diff --git a/api-reference/error-codes.mdx b/api-reference/error-codes.mdx
index b1bb4ae..107da3c 100644
--- a/api-reference/error-codes.mdx
+++ b/api-reference/error-codes.mdx
@@ -176,6 +176,16 @@ Import + snapshot of a Fleet Bundle (the SKILL.md/TRIGGER.md package behind a Fl
| `UZ-GRANT-001` | 403 | No integration grant for service | This fleet has no approved grant for the target service. Request one with `POST /v1/workspaces/{workspace_id}/fleets/{fleet_id}/integration-requests`. |
| `UZ-GRANT-002` | 404 | Integration grant not found | No grant with that id exists for this fleet, or it was already revoked. List current grants with `GET /v1/workspaces/{workspace_id}/fleets/{fleet_id}/integration-grants`. |
+## Integration connect & token mint
+
+| Code | HTTP | Title | Common Causes |
+|---|---|---|---|
+| `UZ-CONN-001` | 503 | Connector not configured | GitHub connect is unavailable on this deployment — the platform App slug or signing secret is unset. An operator must register the GitHub App and populate the admin vault before fleets can connect. |
+| `UZ-CONN-002` | 400 | Invalid connect state | The connect callback's `state` was missing, forged, expired, or already used. Start the connect again from the dashboard — each attempt issues a fresh single-use state. |
+| `UZ-CRED-001` | 404 | Integration not connected | No connected integration matches this id for the fleet's workspace. Connect it first (e.g. GitHub via the dashboard **Connect** flow) before a fleet can mint a token for it. |
+| `UZ-GH-001` | 409 | GitHub App reconnect required | The GitHub App installation is gone (uninstalled or revoked), so no token can be minted. Reconnect GitHub from the dashboard — the fleet stays blocked until the App is reinstalled. |
+| `UZ-GH-002` | 502 | GitHub token mint failed | GitHub did not return an installation token (upstream 5xx, network, or a malformed exchange response). Transient — retry shortly; if it persists, check GitHub status and the App configuration. |
+
## Approval gate
| Code | HTTP | Title | Common Causes |
@@ -193,6 +203,7 @@ Import + snapshot of a Fleet Bundle (the SKILL.md/TRIGGER.md package behind a Fl
|---|---|---|---|
| `UZ-VAULT-001` | 400 | Credential data must be a non-empty JSON object | `POST /credentials` body must include a `data` field that is a JSON object with at least one key. Bare strings, arrays, scalars, and `{}` are rejected. |
| `UZ-VAULT-002` | 400 | Credential data too large | Stringified credential data exceeds 4 KB. Compose the secret from fewer or shorter fields. |
+| `UZ-VAULT-003` | 404 | Credential not found | No credential matches this name in the workspace. Returned by `PATCH /v1/workspaces/{workspace_id}/credentials/{name}` (key rotation); list the workspace credentials to find a valid name, or create it first. |
## Memory
diff --git a/changelog.mdx b/changelog.mdx
index 64badde..749e478 100644
--- a/changelog.mdx
+++ b/changelog.mdx
@@ -22,6 +22,29 @@ export const STAGE_SELF_MANAGED_M66 = "$0.0001";
agentsfleet is in **stealth-mode testing** and pre-production. APIs and agent behavior may change between releases without long deprecation windows. Email [agentsfleet@agentmail.to](mailto:agentsfleet@agentmail.to) if you want a hand calibrating an agent or to join as a design partner.
+
+ ## Models and Keys is one page, and credentials say what they are
+
+ Model and key management is now a single **Models & Keys** page. The active model reads as a hero you can take in at a glance, every stored provider key is one click from live, and the credentials list now tells the dashboard what each credential *is* instead of guessing from its name. The standalone Credentials page is gone — `/credentials` redirects here.
+
+ ## What's new
+
+ - **One Models & Keys page** — the active model shows as a hero (`model · via · LIVE`) with every stored provider key one click away on a switch list; switching sets that credential's saved model with no key re-entry. This replaces the separate Models and Credentials destinations and the two-option-card screen.
+ - **Credentials are classified server-side** — the list carries each credential's `kind` (provider key, custom endpoint, or custom secret) plus its non-secret provider, model, and base URL, so a stored-but-inactive provider key no longer misfiles as a custom secret. The API key is still never returned.
+ - **Replace a key in place** — rotating a provider key now updates only the secret and keeps the model, provider, and endpoint, so a custom-endpoint key rotation can't corrupt the saved base URL.
+ - **Custom secrets keep their own section** — `NAME=value` secrets a SKILL reads by name stay listed apart from model-provider keys on the same page.
+
+ ## API reference
+
+ ```
+ GET /v1/workspaces/{ws}/credentials each row carries kind + non-secret provider/model/base_url; never api_key
+ PATCH /v1/workspaces/{ws}/credentials/{name} body {api_key} — rotate the key only, preserving provider/model/base_url
+ ```
+
+ - **`kind` is derived server-side** from the stored provider field — `provider_key`, `custom_endpoint`, or `custom_secret` — never from the user-chosen name. An unreadable legacy body lists as a custom secret and the call still returns 200.
+ - **Rotate errors** — a missing credential returns `UZ-VAULT-003` (404); an empty key returns `UZ-REQ-001` (400). The list stays operator-gated and the API key is never in the response.
+
+
## The sandboxed agent runner is hardened for general availability
diff --git a/cli/agentsfleet.mdx b/cli/agentsfleet.mdx
index e75cc15..bc7dbf2 100644
--- a/cli/agentsfleet.mdx
+++ b/cli/agentsfleet.mdx
@@ -291,6 +291,8 @@ List credential names and creation timestamps in the current workspace. Values a
agentsfleet credential list
```
+`--json` additionally carries each credential's server-derived `kind` (`provider_key`, `custom_endpoint`, or `custom_secret`) and its non-secret `provider` / `model` / `base_url` — the `api_key` is never among them.
+
---
## Workspaces
diff --git a/fleets/credentials.mdx b/fleets/credentials.mdx
index 97d8f5c..6af829c 100644
--- a/fleets/credentials.mdx
+++ b/fleets/credentials.mdx
@@ -77,7 +77,7 @@ agentsfleet credential add my-gateway \
The base URL is validated server-side before any run: it must be `https` and resolve to a **public** host. A loopback, private, link-local, or cloud-metadata target — or a `base_url` set on a named (non-`openai-compatible`) provider — is rejected with [`UZ-PROVIDER-005`](/api-reference/error-codes) and never dialed. The runner's egress allowlist is derived from that host, so requests reach only it.
-Activate the credential as the workspace's model with `agentsfleet tenant provider add --credential ` (or the dashboard's **Models → Custom — OpenAI-compatible** option); `agentsfleet tenant provider delete` resets to the platform default. The URL rides inside the stored credential, so activation references it by name — the `PUT /v1/tenants/me/provider` body is identical to a named provider.
+Activate the credential as the workspace's model with `agentsfleet tenant provider add --credential ` (or the dashboard's **Models & Keys** page — add a custom endpoint, then Switch to it); `agentsfleet tenant provider delete` resets to the platform default. The URL rides inside the stored credential, so activation references it by name — the `PUT /v1/tenants/me/provider` body is identical to a named provider.
A self-managed endpoint bills against **your** provider account, not agentsfleet's per-model rates — so its model does not need to appear in the platform catalogue.
@@ -97,7 +97,7 @@ agentsfleet credential list
fly 2026-04-17T09:03:44Z
```
-Pass `--json` for machine-readable output.
+Pass `--json` for machine-readable output. The JSON carries each credential's server-derived `kind` (`provider_key`, `custom_endpoint`, or `custom_secret`) and its non-secret `provider` / `model` / `base_url` alongside the name and timestamp — the value is still never returned. The dashboard's **Models & Keys** page uses these fields to classify and label each credential instead of guessing from its name.
## Referencing credentials from `TRIGGER.md`
@@ -138,6 +138,8 @@ JSON
`--force` is required to overwrite an existing credential — without it, the default skip-if-exists behaviour leaves the old value in place.
+For a model-provider key, the dashboard's **Models & Keys** page offers an in-place **Replace key** that rotates only the secret and preserves the saved provider, model, and base URL — the same key-only rotation the API exposes as `PATCH /v1/workspaces/{workspace_id}/credentials/{name}`.
+
## Deletion
```bash