feat: extend agent chat MCP tools for remote UAT evidence loops (#28233)

Extends the Agent-chat MCP tools so an unattended UAT evidence loop can
fetch artifacts, monitor long runs, and find prior runs without burning
model context.

## Backend

- New `chat_files_token` crypto key feature (migration 000571) with
rotator support and a dedicated signing keycache on coderd.
- `POST /api/experimental/chats/files/{file}/download-url`
(authenticated) mints a short-lived (5 min) signed URL and returns it
with `sha256`, `size_bytes`, `name`, `mime_type`, and `expires_at`.
- `GET /api/experimental/chats/files/{file}/download?token=` (no session
token) redeems the signed URL: verifies the JWS, requires the token's
`file_id` to match the path, and re-checks the minting user's RBAC
access live at redemption. Clients can `curl -o` artifacts with zero
credentials in the URL consumer.
- `ChatFileMetadata` gains `size_bytes` (via `octet_length`, no bytes
fetched).

## MCP tools (`codersdk/toolsdk`)

- `coder_download_chat_file`: by `file_id` or `chat_id`+`file_name`;
returns the signed URL plus checksum and size instead of base64.
- `coder_await_chat`: blocks (bounded `wait_secs`, 1-120) until a chat
leaves `running`/`interrupting`, using the existing watch stream with
subscribe-before-read.
- `coder_list_chats`: label, query, and limit filtering; chat
projections now include labels.
- `coder_get_chat_messages`: `after_id` forward cursor with
`next_after_id` (exact incremental reads), plus per-message `files`
metadata so artifact-bearing messages are identifiable.
- `coder_get_chat`: file listings now include `size_bytes` and
`created_at`.
- `coder_list_templates`: exposes `agents_allowed` for pre-flight
checks.

## Testing

- coderd: mint/redeem happy path with an unauthenticated client,
expired/tampered/file-mismatched tokens, auth still required on the
plain file endpoint, non-owner mint rejection.
- toolsdk: harness + integration coverage for all new/changed tools,
including signed-URL redemption with checksum verification,
forward-cursor exactness, await transition/timeout paths, and label
filtering.
- Remote dogfood UAT (dev.coder.com Coder Agent) passed all six
acceptance scenarios end to end over both MCP transports.

Note: `go test ./codersdk/toolsdk/` has a pre-existing goleak flake on
main (leaked `agentssh` non-PTY session goroutines from SSH exec tests;
reproduced 3/3 on clean `b4971bc49f1`). It is unrelated to this diff.

> Mux acted on Mike's behalf to create this PR.

<!-- mux-attribution: model=claude-sonnet-4-6 thinking=high -->
This commit is contained in:
Michael Suchacz
2026-08-18 19:15:30 +02:00
committed by GitHub
parent 7724ee281a
commit affeeaf9c8
30 changed files with 1840 additions and 91 deletions
+29 -14
View File
@@ -97,7 +97,8 @@ Experimental: this endpoint is subject to change.
"mime_type": "string",
"name": "string",
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
"owner_id": "8826ee2e-7933-4665-aef2-2393f84a0d05"
"owner_id": "8826ee2e-7933-4665-aef2-2393f84a0d05",
"size_bytes": 0
}
],
"has_unread": true,
@@ -203,6 +204,7 @@ Status Code **200**
| `»» name` | string | false | | |
| `»» organization_id` | string(uuid) | false | | |
| `»» owner_id` | string(uuid) | false | | |
| `»» size_bytes` | integer | false | | |
| `» has_unread` | boolean | false | | Has unread is true when assistant messages exist beyond the owner's read cursor, which updates on stream connect and disconnect. |
| `» id` | string(uuid) | false | | |
| `» labels` | object | false | | |
@@ -376,7 +378,8 @@ Experimental: this endpoint is subject to change.
"mime_type": "string",
"name": "string",
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
"owner_id": "8826ee2e-7933-4665-aef2-2393f84a0d05"
"owner_id": "8826ee2e-7933-4665-aef2-2393f84a0d05",
"size_bytes": 0
}
],
"has_unread": true,
@@ -471,7 +474,8 @@ Experimental: this endpoint is subject to change.
"mime_type": "string",
"name": "string",
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
"owner_id": "8826ee2e-7933-4665-aef2-2393f84a0d05"
"owner_id": "8826ee2e-7933-4665-aef2-2393f84a0d05",
"size_bytes": 0
}
],
"has_unread": true,
@@ -723,7 +727,8 @@ Experimental: this endpoint is subject to change.
"mime_type": "string",
"name": "string",
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
"owner_id": "8826ee2e-7933-4665-aef2-2393f84a0d05"
"owner_id": "8826ee2e-7933-4665-aef2-2393f84a0d05",
"size_bytes": 0
}
],
"has_unread": true,
@@ -872,7 +877,8 @@ Experimental: this endpoint is subject to change.
"mime_type": "string",
"name": "string",
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
"owner_id": "8826ee2e-7933-4665-aef2-2393f84a0d05"
"owner_id": "8826ee2e-7933-4665-aef2-2393f84a0d05",
"size_bytes": 0
}
],
"has_unread": true,
@@ -967,7 +973,8 @@ Experimental: this endpoint is subject to change.
"mime_type": "string",
"name": "string",
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
"owner_id": "8826ee2e-7933-4665-aef2-2393f84a0d05"
"owner_id": "8826ee2e-7933-4665-aef2-2393f84a0d05",
"size_bytes": 0
}
],
"has_unread": true,
@@ -1153,7 +1160,8 @@ Experimental: this endpoint is subject to change.
"mime_type": "string",
"name": "string",
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
"owner_id": "8826ee2e-7933-4665-aef2-2393f84a0d05"
"owner_id": "8826ee2e-7933-4665-aef2-2393f84a0d05",
"size_bytes": 0
}
],
"has_unread": true,
@@ -1248,7 +1256,8 @@ Experimental: this endpoint is subject to change.
"mime_type": "string",
"name": "string",
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
"owner_id": "8826ee2e-7933-4665-aef2-2393f84a0d05"
"owner_id": "8826ee2e-7933-4665-aef2-2393f84a0d05",
"size_bytes": 0
}
],
"has_unread": true,
@@ -1484,7 +1493,8 @@ Experimental: this endpoint is subject to change.
"mime_type": "string",
"name": "string",
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
"owner_id": "8826ee2e-7933-4665-aef2-2393f84a0d05"
"owner_id": "8826ee2e-7933-4665-aef2-2393f84a0d05",
"size_bytes": 0
}
],
"has_unread": true,
@@ -1579,7 +1589,8 @@ Experimental: this endpoint is subject to change.
"mime_type": "string",
"name": "string",
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
"owner_id": "8826ee2e-7933-4665-aef2-2393f84a0d05"
"owner_id": "8826ee2e-7933-4665-aef2-2393f84a0d05",
"size_bytes": 0
}
],
"has_unread": true,
@@ -2501,7 +2512,8 @@ Experimental: this endpoint is subject to change.
"mime_type": "string",
"name": "string",
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
"owner_id": "8826ee2e-7933-4665-aef2-2393f84a0d05"
"owner_id": "8826ee2e-7933-4665-aef2-2393f84a0d05",
"size_bytes": 0
}
],
"has_unread": true,
@@ -2596,7 +2608,8 @@ Experimental: this endpoint is subject to change.
"mime_type": "string",
"name": "string",
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
"owner_id": "8826ee2e-7933-4665-aef2-2393f84a0d05"
"owner_id": "8826ee2e-7933-4665-aef2-2393f84a0d05",
"size_bytes": 0
}
],
"has_unread": true,
@@ -3105,7 +3118,8 @@ Experimental: this endpoint is subject to change.
"mime_type": "string",
"name": "string",
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
"owner_id": "8826ee2e-7933-4665-aef2-2393f84a0d05"
"owner_id": "8826ee2e-7933-4665-aef2-2393f84a0d05",
"size_bytes": 0
}
],
"has_unread": true,
@@ -3200,7 +3214,8 @@ Experimental: this endpoint is subject to change.
"mime_type": "string",
"name": "string",
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
"owner_id": "8826ee2e-7933-4665-aef2-2393f84a0d05"
"owner_id": "8826ee2e-7933-4665-aef2-2393f84a0d05",
"size_bytes": 0
}
],
"has_unread": true,
+44 -15
View File
@@ -2250,7 +2250,8 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
"mime_type": "string",
"name": "string",
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
"owner_id": "8826ee2e-7933-4665-aef2-2393f84a0d05"
"owner_id": "8826ee2e-7933-4665-aef2-2393f84a0d05",
"size_bytes": 0
}
],
"has_unread": true,
@@ -2345,7 +2346,8 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
"mime_type": "string",
"name": "string",
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
"owner_id": "8826ee2e-7933-4665-aef2-2393f84a0d05"
"owner_id": "8826ee2e-7933-4665-aef2-2393f84a0d05",
"size_bytes": 0
}
],
"has_unread": true,
@@ -2793,6 +2795,30 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `auth`, `config`, `content_filter`, `generic`, `hook_denied`, `hook_dispatch_failed`, `missing_key`, `overloaded`, `provider_disabled`, `rate_limit`, `stream_silence_timeout`, `timeout`, `usage_limit` |
## codersdk.ChatFileDownloadURLResponse
```json
{
"expires_at": "2019-08-24T14:15:22Z",
"mime_type": "string",
"name": "string",
"sha256": "string",
"size_bytes": 0,
"url": "http://example.com"
}
```
### Properties
| Name | Type | Required | Restrictions | Description |
|--------------|---------|----------|--------------|-------------|
| `expires_at` | string | false | | |
| `mime_type` | string | false | | |
| `name` | string | false | | |
| `sha256` | string | false | | |
| `size_bytes` | integer | false | | |
| `url` | string | false | | |
## codersdk.ChatFileMetadata
```json
@@ -2802,20 +2828,22 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
"mime_type": "string",
"name": "string",
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
"owner_id": "8826ee2e-7933-4665-aef2-2393f84a0d05"
"owner_id": "8826ee2e-7933-4665-aef2-2393f84a0d05",
"size_bytes": 0
}
```
### Properties
| Name | Type | Required | Restrictions | Description |
|-------------------|--------|----------|--------------|-------------|
| `created_at` | string | false | | |
| `id` | string | false | | |
| `mime_type` | string | false | | |
| `name` | string | false | | |
| `organization_id` | string | false | | |
| `owner_id` | string | false | | |
| Name | Type | Required | Restrictions | Description |
|-------------------|---------|----------|--------------|-------------|
| `created_at` | string | false | | |
| `id` | string | false | | |
| `mime_type` | string | false | | |
| `name` | string | false | | |
| `organization_id` | string | false | | |
| `owner_id` | string | false | | |
| `size_bytes` | integer | false | | |
## codersdk.ChatGroup
@@ -4176,7 +4204,8 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
"mime_type": "string",
"name": "string",
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
"owner_id": "8826ee2e-7933-4665-aef2-2393f84a0d05"
"owner_id": "8826ee2e-7933-4665-aef2-2393f84a0d05",
"size_bytes": 0
}
],
"has_unread": true,
@@ -5580,9 +5609,9 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o
#### Enumerated Values
| Value(s) |
|-----------------------------------------------------------------------------------------------|
| `nats_ca`, `oidc_convert`, `tailnet_resume`, `workspace_apps_api_key`, `workspace_apps_token` |
| Value(s) |
|-------------------------------------------------------------------------------------------------------------------|
| `chat_files_token`, `nats_ca`, `oidc_convert`, `tailnet_resume`, `workspace_apps_api_key`, `workspace_apps_token` |
## codersdk.CustomNotificationContent