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

Commitfc9bff7

Browse files
authored
feat: add aibridged package (#19797)
Addressescoder/internal#987
1 parent289f021 commitfc9bff7

File tree

19 files changed

+1377
-43
lines changed

19 files changed

+1377
-43
lines changed

‎Makefile‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -635,6 +635,10 @@ TAILNETTEST_MOCKS := \
635635
tailnet/tailnettest/workspaceupdatesprovidermock.go\
636636
tailnet/tailnettest/subscriptionmock.go
637637

638+
AIBRIDGED_MOCKS :=\
639+
enterprise/x/aibridged/aibridgedmock/clientmock.go\
640+
enterprise/x/aibridged/aibridgedmock/poolmock.go
641+
638642
GEN_FILES :=\
639643
tailnet/proto/tailnet.pb.go\
640644
agent/proto/agent.pb.go\
@@ -660,7 +664,8 @@ GEN_FILES := \
660664
agent/agentcontainers/acmock/acmock.go\
661665
agent/agentcontainers/dcspec/dcspec_gen.go\
662666
coderd/httpmw/loggermw/loggermock/loggermock.go\
663-
codersdk/workspacesdk/agentconnmock/agentconnmock.go
667+
codersdk/workspacesdk/agentconnmock/agentconnmock.go\
668+
$(AIBRIDGED_MOCKS)
664669

665670
# all gen targets should be added here and to gen/mark-fresh
666671
gen: gen/db gen/golden-files$(GEN_FILES)
@@ -713,6 +718,7 @@ gen/mark-fresh:
713718
agent/agentcontainers/dcspec/dcspec_gen.go\
714719
coderd/httpmw/loggermw/loggermock/loggermock.go\
715720
codersdk/workspacesdk/agentconnmock/agentconnmock.go\
721+
$(AIBRIDGED_MOCKS)\
716722
"
717723

718724
for file in $$files; do
@@ -760,6 +766,10 @@ codersdk/workspacesdk/agentconnmock/agentconnmock.go: codersdk/workspacesdk/agen
760766
go generate ./codersdk/workspacesdk/agentconnmock/
761767
touch"$@"
762768

769+
$(AIBRIDGED_MOCKS): enterprise/x/aibridged/client.go enterprise/x/aibridged/pool.go
770+
go generate ./enterprise/x/aibridged/aibridgedmock/
771+
touch"$@"
772+
763773
agent/agentcontainers/dcspec/dcspec_gen.go:\
764774
node_modules/.installed\
765775
agent/agentcontainers/dcspec/devContainer.base.schema.json\

‎coderd/coderd.go‎

Lines changed: 22 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -999,29 +999,31 @@ func New(options *Options) *API {
999999

10001000
// Experimental routes are not guaranteed to be stable and may change at any time.
10011001
r.Route("/api/experimental",func(r chi.Router) {
1002-
r.Use(apiKeyMiddleware)
1003-
r.Route("/aitasks",func(r chi.Router) {
1004-
r.Get("/prompts",api.aiTasksPrompts)
1005-
})
1006-
r.Route("/tasks",func(r chi.Router) {
1007-
r.Use(apiRateLimiter)
1002+
r.Group(func(r chi.Router) {
1003+
r.Use(apiKeyMiddleware)
1004+
r.Route("/aitasks",func(r chi.Router) {
1005+
r.Get("/prompts",api.aiTasksPrompts)
1006+
})
1007+
r.Route("/tasks",func(r chi.Router) {
1008+
r.Use(apiRateLimiter)
10081009

1009-
r.Get("/",api.tasksList)
1010+
r.Get("/",api.tasksList)
10101011

1011-
r.Route("/{user}",func(r chi.Router) {
1012-
r.Use(httpmw.ExtractOrganizationMembersParam(options.Database,api.HTTPAuth.Authorize))
1013-
r.Get("/{id}",api.taskGet)
1014-
r.Delete("/{id}",api.taskDelete)
1015-
r.Post("/{id}/send",api.taskSend)
1016-
r.Post("/",api.tasksCreate)
1012+
r.Route("/{user}",func(r chi.Router) {
1013+
r.Use(httpmw.ExtractOrganizationMembersParam(options.Database,api.HTTPAuth.Authorize))
1014+
r.Get("/{id}",api.taskGet)
1015+
r.Delete("/{id}",api.taskDelete)
1016+
r.Post("/{id}/send",api.taskSend)
1017+
r.Post("/",api.tasksCreate)
1018+
})
1019+
})
1020+
r.Route("/mcp",func(r chi.Router) {
1021+
r.Use(
1022+
httpmw.RequireExperimentWithDevBypass(api.Experiments,codersdk.ExperimentOAuth2,codersdk.ExperimentMCPServerHTTP),
1023+
)
1024+
// MCP HTTP transport endpoint with mandatory authentication
1025+
r.Mount("/http",api.mcpHTTPHandler())
10171026
})
1018-
})
1019-
r.Route("/mcp",func(r chi.Router) {
1020-
r.Use(
1021-
httpmw.RequireExperimentWithDevBypass(api.Experiments,codersdk.ExperimentOAuth2,codersdk.ExperimentMCPServerHTTP),
1022-
)
1023-
// MCP HTTP transport endpoint with mandatory authentication
1024-
r.Mount("/http",api.mcpHTTPHandler())
10251027
})
10261028
})
10271029

‎coderd/database/dbauthz/dbauthz.go‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -570,6 +570,7 @@ var (
570570
DisplayName:"AIBridge Daemon",
571571
Site:rbac.Permissions(map[string][]policy.Action{
572572
rbac.ResourceUser.Type: {
573+
policy.ActionRead,// Required to validate API key owner is active.
573574
policy.ActionReadPersonal,// Required to read users' external auth links. // TODO: this is too broad; reduce scope to just external_auth_links by creating separate resource.
574575
},
575576
rbac.ResourceApiKey.Type: {policy.ActionRead},// Validate API keys.
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
package aibridged
2+
3+
import (
4+
"context"
5+
"errors"
6+
"net/http"
7+
"sync"
8+
"time"
9+
10+
"golang.org/x/xerrors"
11+
12+
"cdr.dev/slog"
13+
"github.com/coder/coder/v2/codersdk"
14+
"github.com/coder/retry"
15+
)
16+
17+
// Server provides the AI Bridge functionality.
18+
// It is responsible for:
19+
// - receiving requests on /api/experimental/aibridged/* // TODO: update endpoint once out of experimental
20+
// - manipulating the requests
21+
// - relaying requests to upstream AI services and relaying responses to caller
22+
//
23+
// It requires a [Dialer] to provide a [DRPCClient] implementation to
24+
// communicate with a [DRPCServer] implementation, to persist state and perform other functions.
25+
typeServerstruct {
26+
clientDialerDialer
27+
clientChchanDRPCClient
28+
29+
// A pool of [aibridge.RequestBridge] instances, which service incoming requests.
30+
requestBridgePoolPooler
31+
32+
logger slog.Logger
33+
wg sync.WaitGroup
34+
35+
// initConnectionCh will receive when the daemon connects to coderd for the
36+
// first time.
37+
initConnectionChchanstruct{}
38+
initConnectionOnce sync.Once
39+
40+
// lifecycleCtx is canceled when we start closing.
41+
lifecycleCtx context.Context
42+
// cancelFn closes the lifecycleCtx.
43+
cancelFnfunc()
44+
45+
shutdownOnce sync.Once
46+
}
47+
48+
funcNew(ctx context.Context,poolPooler,rpcDialerDialer,logger slog.Logger) (*Server,error) {
49+
ifrpcDialer==nil {
50+
returnnil,xerrors.Errorf("nil rpcDialer given")
51+
}
52+
53+
ctx,cancel:=context.WithCancel(ctx)
54+
daemon:=&Server{
55+
logger:logger,
56+
clientDialer:rpcDialer,
57+
requestBridgePool:pool,
58+
clientCh:make(chanDRPCClient),
59+
lifecycleCtx:ctx,
60+
cancelFn:cancel,
61+
initConnectionCh:make(chanstruct{}),
62+
}
63+
64+
daemon.wg.Add(1)
65+
godaemon.connect()
66+
67+
returndaemon,nil
68+
}
69+
70+
// Connect establishes a connection to coderd.
71+
func (s*Server)connect() {
72+
defers.logger.Debug(s.lifecycleCtx,"connect loop exited")
73+
defers.wg.Done()
74+
75+
logConnect:=s.logger.With(slog.F("context","aibridged.server")).Debug
76+
// An exponential back-off occurs when the connection is failing to dial.
77+
// This is to prevent server spam in case of a coderd outage.
78+
connectLoop:
79+
forretrier:=retry.New(50*time.Millisecond,10*time.Second);retrier.Wait(s.lifecycleCtx); {
80+
// It's possible for the aibridge daemon to be shut down
81+
// before the wait is complete!
82+
ifs.isShutdown() {
83+
return
84+
}
85+
s.logger.Debug(s.lifecycleCtx,"dialing coderd")
86+
client,err:=s.clientDialer(s.lifecycleCtx)
87+
iferr!=nil {
88+
iferrors.Is(err,context.Canceled) {
89+
return
90+
}
91+
varsdkErr*codersdk.Error
92+
// If something is wrong with our auth, stop trying to connect.
93+
iferrors.As(err,&sdkErr)&&sdkErr.StatusCode()==http.StatusForbidden {
94+
s.logger.Error(s.lifecycleCtx,"not authorized to dial coderd",slog.Error(err))
95+
return
96+
}
97+
ifs.isShutdown() {
98+
return
99+
}
100+
s.logger.Warn(s.lifecycleCtx,"coderd client failed to dial",slog.Error(err))
101+
continue
102+
}
103+
104+
// TODO: log this with INFO level when we implement external aibridge daemons.
105+
logConnect(s.lifecycleCtx,"successfully connected to coderd")
106+
retrier.Reset()
107+
s.initConnectionOnce.Do(func() {
108+
close(s.initConnectionCh)
109+
})
110+
111+
// Serve the client until we are closed or it disconnects.
112+
for {
113+
select {
114+
case<-s.lifecycleCtx.Done():
115+
client.DRPCConn().Close()
116+
return
117+
case<-client.DRPCConn().Closed():
118+
logConnect(s.lifecycleCtx,"connection to coderd closed")
119+
continue connectLoop
120+
cases.clientCh<-client:
121+
continue
122+
}
123+
}
124+
}
125+
}
126+
127+
func (s*Server)Client() (DRPCClient,error) {
128+
select {
129+
case<-s.lifecycleCtx.Done():
130+
returnnil,xerrors.New("context closed")
131+
caseclient:=<-s.clientCh:
132+
returnclient,nil
133+
}
134+
}
135+
136+
// GetRequestHandler retrieves a (possibly reused) [*aibridge.RequestBridge] from the pool, for the given user.
137+
func (s*Server)GetRequestHandler(ctx context.Context,reqRequest) (http.Handler,error) {
138+
ifs.requestBridgePool==nil {
139+
returnnil,xerrors.New("nil requestBridgePool")
140+
}
141+
142+
reqBridge,err:=s.requestBridgePool.Acquire(ctx,req,s.Client)
143+
iferr!=nil {
144+
returnnil,xerrors.Errorf("acquire request bridge: %w",err)
145+
}
146+
147+
returnreqBridge,nil
148+
}
149+
150+
// isShutdown returns whether the Server is shutdown or not.
151+
func (s*Server)isShutdown()bool {
152+
select {
153+
case<-s.lifecycleCtx.Done():
154+
returntrue
155+
default:
156+
returnfalse
157+
}
158+
}
159+
160+
// Shutdown waits for all exiting in-flight requests to complete, or the context to expire, whichever comes first.
161+
func (s*Server)Shutdown(ctx context.Context)error {
162+
varerrerror
163+
s.shutdownOnce.Do(func() {
164+
s.cancelFn()
165+
166+
// Wait for any outstanding connections to terminate.
167+
s.wg.Wait()
168+
169+
select {
170+
case<-ctx.Done():
171+
s.logger.Warn(ctx,"graceful shutdown failed",slog.Error(ctx.Err()))
172+
err=ctx.Err()
173+
return
174+
default:
175+
}
176+
177+
s.logger.Info(ctx,"shutting down request pool")
178+
iferr=s.requestBridgePool.Shutdown(ctx);err!=nil {
179+
s.logger.Error(ctx,"request pool shutdown failed with error",slog.Error(err))
180+
}
181+
182+
s.logger.Info(ctx,"gracefully shutdown")
183+
})
184+
returnerr
185+
}

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp