Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit1bd075b

Browse files
committed
feat(coderd): add tasks send endpoint
Fixescoder/internal#902
1 parent4a56a40 commit1bd075b

File tree

4 files changed

+400
-1
lines changed

4 files changed

+400
-1
lines changed

‎coderd/aitasks.go‎

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,19 @@
11
package coderd
22

33
import (
4+
"bytes"
45
"context"
56
"database/sql"
7+
"encoding/json"
68
"errors"
79
"fmt"
10+
"io"
11+
"net"
812
"net/http"
13+
"net/url"
14+
"path"
15+
"time"
16+
917
"slices"
1018
"strings"
1119

@@ -590,3 +598,207 @@ func (api *API) taskDelete(rw http.ResponseWriter, r *http.Request) {
590598
// Delete build created successfully.
591599
rw.WriteHeader(http.StatusAccepted)
592600
}
601+
602+
// taskSend submits task input to the tasks sidebar app by dialing the agent
603+
// directly over the tailnet. We enforce ApplicationConnect RBAC on the
604+
// workspace and validate the sidebar app health.
605+
func (api*API)taskSend(rw http.ResponseWriter,r*http.Request) {
606+
ctx:=r.Context()
607+
608+
idStr:=chi.URLParam(r,"id")
609+
taskID,err:=uuid.Parse(idStr)
610+
iferr!=nil {
611+
httperror.WriteResponseError(ctx,rw,httperror.NewResponseError(http.StatusBadRequest, codersdk.Response{
612+
Message:fmt.Sprintf("Invalid UUID %q for task ID.",idStr),
613+
}))
614+
return
615+
}
616+
617+
varreq codersdk.TaskSendRequest
618+
if!httpapi.Read(ctx,rw,r,&req) {
619+
return
620+
}
621+
ifreq.Input=="" {
622+
httperror.WriteResponseError(ctx,rw,httperror.NewResponseError(http.StatusBadRequest, codersdk.Response{
623+
Message:"Task input is required.",
624+
}))
625+
return
626+
}
627+
628+
typemessagePayloadstruct {
629+
Contentstring`json:"content"`
630+
Typestring`json:"type"`
631+
}
632+
payload,err:=json.Marshal(messagePayload{Content:req.Input,Type:"user"})
633+
iferr!=nil {
634+
httperror.WriteResponseError(ctx,rw,httperror.NewResponseError(http.StatusInternalServerError, codersdk.Response{
635+
Message:"Internal error encoding request payload.",
636+
Detail:err.Error(),
637+
}))
638+
return
639+
}
640+
641+
iferr=api.authAndDoWithTaskSidebarAppClient(r,taskID,func(ctx context.Context,client*http.Client,appURL*url.URL)error {
642+
// Build the request to the agent local app (we expect agentapi on the other side).
643+
appReqURL:=appURL
644+
appReqURL.Path=path.Join(appURL.Path,"message")
645+
646+
newReq,err:=http.NewRequestWithContext(ctx,http.MethodPost,appReqURL.String(),bytes.NewReader(payload))
647+
iferr!=nil {
648+
returnhttperror.NewResponseError(http.StatusInternalServerError, codersdk.Response{
649+
Message:"Internal error creating request to task app.",
650+
Detail:err.Error(),
651+
})
652+
}
653+
newReq.Header.Set("Content-Type","application/json")
654+
655+
resp,err:=client.Do(newReq)
656+
iferr!=nil {
657+
returnhttperror.NewResponseError(http.StatusBadGateway, codersdk.Response{
658+
Message:"Failed to reach task app endpoint.",
659+
Detail:err.Error(),
660+
})
661+
}
662+
deferresp.Body.Close()
663+
664+
ifresp.StatusCode!=http.StatusOK {
665+
body,_:=io.ReadAll(io.LimitReader(resp.Body,128))
666+
returnhttperror.NewResponseError(http.StatusBadGateway, codersdk.Response{
667+
Message:"Task app rejected the message.",
668+
Detail:fmt.Sprintf("Upstream status: %d; Body: %s",resp.StatusCode,body),
669+
})
670+
}
671+
returnnil
672+
});err!=nil {
673+
httperror.WriteResponseError(ctx,rw,err)
674+
}
675+
676+
rw.WriteHeader(http.StatusNoContent)
677+
}
678+
679+
// authAndDoWithTaskSidebarAppClient centralizes the shared logic to:
680+
//
681+
// - Fetch the task workspace
682+
// - Authorize ApplicationConnect on the workspace
683+
// - Validate the AI task and sidebar app health
684+
// - Dial the agent and construct an HTTP client to the apps loopback URL
685+
//
686+
// The provided callback receives the context, an HTTP client that dials via the
687+
// agent, and the base app URL (as a value URL) to perform any request.
688+
func (api*API)authAndDoWithTaskSidebarAppClient(
689+
r*http.Request,
690+
taskID uuid.UUID,
691+
dofunc(ctx context.Context,client*http.Client,appURL*url.URL)error,
692+
)error {
693+
ctx:=r.Context()
694+
695+
workspaceID:=taskID
696+
workspace,err:=api.Database.GetWorkspaceByID(ctx,workspaceID)
697+
iferr!=nil {
698+
ifhttpapi.Is404Error(err) {
699+
returnhttperror.ErrResourceNotFound
700+
}
701+
returnhttperror.NewResponseError(http.StatusInternalServerError, codersdk.Response{
702+
Message:"Internal error fetching workspace.",
703+
Detail:err.Error(),
704+
})
705+
}
706+
707+
// Connecting to applications requires ApplicationConnect on the workspace.
708+
if!api.Authorize(r,policy.ActionApplicationConnect,workspace) {
709+
returnhttperror.ErrResourceNotFound
710+
}
711+
712+
data,err:=api.workspaceData(ctx, []database.Workspace{workspace})
713+
iferr!=nil {
714+
returnhttperror.NewResponseError(http.StatusInternalServerError, codersdk.Response{
715+
Message:"Internal error fetching workspace resources.",
716+
Detail:err.Error(),
717+
})
718+
}
719+
iflen(data.builds)==0||len(data.templates)==0 {
720+
returnhttperror.ErrResourceNotFound
721+
}
722+
build:=data.builds[0]
723+
ifbuild.HasAITask==nil||!*build.HasAITask||build.AITaskSidebarAppID==nil||!(*build.AITaskSidebarAppID!=uuid.Nil) {
724+
returnhttperror.NewResponseError(http.StatusBadRequest, codersdk.Response{
725+
Message:"Task is not configured with a sidebar app.",
726+
})
727+
}
728+
sidebarAppID:=*build.AITaskSidebarAppID
729+
730+
// Find the sidebar app details to get the URL and validate app health.
731+
agentIDs:=make([]uuid.UUID,0,len(build.Resources))
732+
for_,res:=rangebuild.Resources {
733+
for_,agent:=rangeres.Agents {
734+
agentIDs=append(agentIDs,agent.ID)
735+
}
736+
}
737+
apps,err:=api.Database.GetWorkspaceAppsByAgentIDs(ctx,agentIDs)
738+
iferr!=nil&&!errors.Is(err,sql.ErrNoRows) {
739+
returnhttperror.NewResponseError(http.StatusInternalServerError, codersdk.Response{
740+
Message:"Internal error fetching workspace apps.",
741+
Detail:err.Error(),
742+
})
743+
}
744+
sidebarApp,ok:=slice.Find(apps,func(app database.WorkspaceApp)bool {returnapp.ID==sidebarAppID })
745+
if!ok {
746+
returnhttperror.NewResponseError(http.StatusBadRequest, codersdk.Response{
747+
Message:"Task sidebar app not found in latest build.",
748+
})
749+
}
750+
751+
// Return an informative error if the app isn't healthy rather than trying
752+
// and failing.
753+
switchsidebarApp.Health {
754+
casedatabase.WorkspaceAppHealthDisabled:
755+
// No health check, pass through.
756+
casedatabase.WorkspaceAppHealthInitializing:
757+
returnhttperror.NewResponseError(http.StatusServiceUnavailable, codersdk.Response{
758+
Message:"Task sidebar app is initializing. Try again shortly.",
759+
})
760+
casedatabase.WorkspaceAppHealthUnhealthy:
761+
returnhttperror.NewResponseError(http.StatusServiceUnavailable, codersdk.Response{
762+
Message:"Task sidebar app is unhealthy.",
763+
})
764+
}
765+
766+
// Build the direct app URL and dial the agent.
767+
if!sidebarApp.Url.Valid||sidebarApp.Url.String=="" {
768+
returnhttperror.NewResponseError(http.StatusInternalServerError, codersdk.Response{
769+
Message:"Task sidebar app URL is not configured.",
770+
})
771+
}
772+
parsedURL,err:=url.Parse(sidebarApp.Url.String)
773+
iferr!=nil {
774+
returnhttperror.NewResponseError(http.StatusInternalServerError, codersdk.Response{
775+
Message:"Internal error parsing task app URL.",
776+
Detail:err.Error(),
777+
})
778+
}
779+
ifparsedURL.Scheme!="http" {
780+
returnhttperror.NewResponseError(http.StatusBadRequest, codersdk.Response{
781+
Message:"Only http scheme is supported for direct agent-dial.",
782+
})
783+
}
784+
785+
dialCtx,dialCancel:=context.WithTimeout(ctx,time.Second*30)
786+
deferdialCancel()
787+
agentConn,release,err:=api.agentProvider.AgentConn(dialCtx,sidebarApp.AgentID)
788+
iferr!=nil {
789+
returnhttperror.NewResponseError(http.StatusBadGateway, codersdk.Response{
790+
Message:"Failed to reach task app endpoint.",
791+
Detail:err.Error(),
792+
})
793+
}
794+
deferrelease()
795+
796+
client:=&http.Client{
797+
Transport:&http.Transport{
798+
DialContext:func(ctx context.Context,network,addrstring) (net.Conn,error) {
799+
returnagentConn.DialContext(ctx,network,addr)
800+
},
801+
},
802+
}
803+
returndo(ctx,client,parsedURL)
804+
}

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp