mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
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:
@@ -325,6 +325,7 @@ type Options struct {
|
||||
AppSigningKeyCache cryptokeys.SigningKeycache
|
||||
AppEncryptionKeyCache cryptokeys.EncryptionKeycache
|
||||
OIDCConvertKeyCache cryptokeys.SigningKeycache
|
||||
ChatFileTokenKeyCache cryptokeys.SigningKeycache
|
||||
// NATSCACache serves the NATS cluster mTLS CA via the generic signing key
|
||||
// cache for the nats_ca feature. SigningKey returns the active CA
|
||||
// (a *NATSCA); VerifyingKey returns a specific CA by sequence. The key
|
||||
@@ -595,6 +596,17 @@ func New(options *Options) *API {
|
||||
}
|
||||
}
|
||||
|
||||
if options.ChatFileTokenKeyCache == nil {
|
||||
options.ChatFileTokenKeyCache, err = cryptokeys.NewSigningCache(ctx,
|
||||
options.Logger.Named("chat_file_token_keycache"),
|
||||
fetcher,
|
||||
codersdk.CryptoKeyFeatureChatFilesToken,
|
||||
)
|
||||
if err != nil {
|
||||
options.Logger.Fatal(ctx, "failed to properly instantiate chat file token signing cache", slog.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
if options.AppSigningKeyCache == nil {
|
||||
options.AppSigningKeyCache, err = cryptokeys.NewSigningCache(ctx,
|
||||
options.Logger.Named("app_signing_keycache"),
|
||||
@@ -1362,6 +1374,10 @@ func New(options *Options) *API {
|
||||
r.Delete("/", api.deleteUserAIProviderKey)
|
||||
})
|
||||
})
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(httpmw.RateLimit(options.FilesRateLimit, time.Minute))
|
||||
r.Get("/chats/files/{file}/download", api.downloadChatFile)
|
||||
})
|
||||
r.Route("/chats", func(r chi.Router) {
|
||||
r.Use(
|
||||
apiKeyMiddleware,
|
||||
@@ -1374,6 +1390,7 @@ func New(options *Options) *API {
|
||||
r.Route("/files", func(r chi.Router) {
|
||||
r.Use(httpmw.RateLimit(options.FilesRateLimit, time.Minute))
|
||||
r.Post("/", api.postChatFile)
|
||||
r.Post("/{file}/download-url", api.postChatFileDownloadURL)
|
||||
r.Get("/{file}", api.chatFileByID)
|
||||
})
|
||||
r.Route("/config", func(r chi.Router) {
|
||||
@@ -2496,6 +2513,7 @@ func (api *API) Close() error {
|
||||
}
|
||||
_ = api.NetworkTelemetryBatcher.Close()
|
||||
_ = api.OIDCConvertKeyCache.Close()
|
||||
_ = api.ChatFileTokenKeyCache.Close()
|
||||
_ = api.AppSigningKeyCache.Close()
|
||||
_ = api.AppEncryptionKeyCache.Close()
|
||||
if api.NATSCACache != nil {
|
||||
|
||||
Reference in New Issue
Block a user