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

feat: add aibridged package#19797

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Merged
dannykopping merged 4 commits intomainfromdk/aibridged-pkg
Sep 25, 2025
Merged
Show file tree
Hide file tree
Changes from1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
NextNext commit
feat: aibridge package
  • Loading branch information
@dannykopping
dannykopping committedSep 25, 2025
commit6adceb1924417fd468f1331347f7373b48bc70d6
12 changes: 11 additions & 1 deletionMakefile
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -635,6 +635,10 @@ TAILNETTEST_MOCKS := \
tailnet/tailnettest/workspaceupdatesprovidermock.go \
tailnet/tailnettest/subscriptionmock.go

AIBRIDGED_MOCKS := \
enterprise/x/aibridged/aibridgedmock/clientmock.go \
enterprise/x/aibridged/aibridgedmock/poolmock.go

GEN_FILES := \
tailnet/proto/tailnet.pb.go \
agent/proto/agent.pb.go \
Expand All@@ -660,7 +664,8 @@ GEN_FILES := \
agent/agentcontainers/acmock/acmock.go \
agent/agentcontainers/dcspec/dcspec_gen.go \
coderd/httpmw/loggermw/loggermock/loggermock.go \
codersdk/workspacesdk/agentconnmock/agentconnmock.go
codersdk/workspacesdk/agentconnmock/agentconnmock.go \
$(AIBRIDGED_MOCKS)

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

for file in $$files; do
Expand DownExpand Up@@ -760,6 +766,10 @@ codersdk/workspacesdk/agentconnmock/agentconnmock.go: codersdk/workspacesdk/agen
go generate ./codersdk/workspacesdk/agentconnmock/
touch "$@"

$(AIBRIDGED_MOCKS): enterprise/x/aibridged/client.go enterprise/x/aibridged/pool.go
go generate ./enterprise/x/aibridged/aibridgedmock/
touch "$@"

agent/agentcontainers/dcspec/dcspec_gen.go: \
node_modules/.installed \
agent/agentcontainers/dcspec/devContainer.base.schema.json \
Expand Down
42 changes: 22 additions & 20 deletionscoderd/coderd.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -999,29 +999,31 @@ func New(options *Options) *API {

// Experimental routes are not guaranteed to be stable and may change at any time.
r.Route("/api/experimental",func(r chi.Router) {
r.Use(apiKeyMiddleware)
r.Route("/aitasks",func(r chi.Router) {
r.Get("/prompts",api.aiTasksPrompts)
})
r.Route("/tasks",func(r chi.Router) {
r.Use(apiRateLimiter)
r.Group(func(r chi.Router) {
r.Use(apiKeyMiddleware)
r.Route("/aitasks",func(r chi.Router) {
r.Get("/prompts",api.aiTasksPrompts)
})
r.Route("/tasks",func(r chi.Router) {
r.Use(apiRateLimiter)

r.Get("/",api.tasksList)
r.Get("/",api.tasksList)

r.Route("/{user}",func(r chi.Router) {
r.Use(httpmw.ExtractOrganizationMembersParam(options.Database,api.HTTPAuth.Authorize))
r.Get("/{id}",api.taskGet)
r.Delete("/{id}",api.taskDelete)
r.Post("/{id}/send",api.taskSend)
r.Post("/",api.tasksCreate)
r.Route("/{user}",func(r chi.Router) {
r.Use(httpmw.ExtractOrganizationMembersParam(options.Database,api.HTTPAuth.Authorize))
r.Get("/{id}",api.taskGet)
r.Delete("/{id}",api.taskDelete)
r.Post("/{id}/send",api.taskSend)
r.Post("/",api.tasksCreate)
})
})
r.Route("/mcp",func(r chi.Router) {
r.Use(
httpmw.RequireExperimentWithDevBypass(api.Experiments,codersdk.ExperimentOAuth2,codersdk.ExperimentMCPServerHTTP),
)
// MCP HTTP transport endpoint with mandatory authentication
r.Mount("/http",api.mcpHTTPHandler())
})
})
r.Route("/mcp",func(r chi.Router) {
r.Use(
httpmw.RequireExperimentWithDevBypass(api.Experiments,codersdk.ExperimentOAuth2,codersdk.ExperimentMCPServerHTTP),
)
// MCP HTTP transport endpoint with mandatory authentication
r.Mount("/http",api.mcpHTTPHandler())
})
})

Expand Down
1 change: 1 addition & 0 deletionscoderd/database/dbauthz/dbauthz.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -570,6 +570,7 @@ var (
DisplayName:"AIBridge Daemon",
Site:rbac.Permissions(map[string][]policy.Action{
rbac.ResourceUser.Type: {
policy.ActionRead,// Required to validate API key owner is active.
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.
},
rbac.ResourceApiKey.Type: {policy.ActionRead},// Validate API keys.
Expand Down
185 changes: 185 additions & 0 deletionsenterprise/x/aibridged/aibridged.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
package aibridged

import (
"context"
"errors"
"net/http"
"sync"
"time"

"golang.org/x/xerrors"

"cdr.dev/slog"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/retry"
)

// Server provides the AI Bridge functionality.
// It is responsible for:
// - receiving requests on /api/experimental/aibridged/* // TODO: update endpoint once out of experimental
// - manipulating the requests
// - relaying requests to upstream AI services and relaying responses to caller
//
// It requires a [Dialer] to provide a [DRPCClient] implementation to
// communicate with a [DRPCServer] implementation, to persist state and perform other functions.
type Server struct {
clientDialer Dialer
clientCh chan DRPCClient

// A pool of [aibridge.RequestBridge] instances, which service incoming requests.
requestBridgePool Pooler

logger slog.Logger
wg sync.WaitGroup

// initConnectionCh will receive when the daemon connects to coderd for the
// first time.
initConnectionCh chan struct{}
initConnectionOnce sync.Once

// lifecycleCtx is canceled when we start closing.
lifecycleCtx context.Context
// cancelFn closes the lifecycleCtx.
cancelFn func()

shutdownOnce sync.Once
}

func New(ctx context.Context, pool Pooler, rpcDialer Dialer, logger slog.Logger) (*Server, error) {
if rpcDialer == nil {
return nil, xerrors.Errorf("nil rpcDialer given")
}

ctx, cancel := context.WithCancel(ctx)
daemon := &Server{
logger: logger,
clientDialer: rpcDialer,
requestBridgePool: pool,
clientCh: make(chan DRPCClient),
lifecycleCtx: ctx,
cancelFn: cancel,
initConnectionCh: make(chan struct{}),
}

daemon.wg.Add(1)
go daemon.connect()

return daemon, nil
}

// Connect establishes a connection to coderd.
func (d *Server) connect() {
defer d.logger.Debug(d.lifecycleCtx, "connect loop exited")
defer d.wg.Done()

logConnect := d.logger.With(slog.F("context", "aibridged.server")).Debug
// An exponential back-off occurs when the connection is failing to dial.
// This is to prevent server spam in case of a coderd outage.
connectLoop:
for retrier := retry.New(50*time.Millisecond, 10*time.Second); retrier.Wait(d.lifecycleCtx); {
// It's possible for the aibridge daemon to be shut down
// before the wait is complete!
if d.isShutdown() {
return
}
d.logger.Debug(d.lifecycleCtx, "dialing coderd")
client, err := d.clientDialer(d.lifecycleCtx)
if err != nil {
if errors.Is(err, context.Canceled) {
return
}
var sdkErr *codersdk.Error
// If something is wrong with our auth, stop trying to connect.
if errors.As(err, &sdkErr) && sdkErr.StatusCode() == http.StatusForbidden {
d.logger.Error(d.lifecycleCtx, "not authorized to dial coderd", slog.Error(err))
return
}
if d.isShutdown() {
return
}
d.logger.Warn(d.lifecycleCtx, "coderd client failed to dial", slog.Error(err))
continue
}

// TODO: log this with INFO level when we implement external aibridge daemons.
logConnect(d.lifecycleCtx, "successfully connected to coderd")
retrier.Reset()
d.initConnectionOnce.Do(func() {
close(d.initConnectionCh)
})

// Serve the client until we are closed or it disconnects.
for {
select {
case <-d.lifecycleCtx.Done():
client.DRPCConn().Close()
return
case <-client.DRPCConn().Closed():
logConnect(d.lifecycleCtx, "connection to coderd closed")
continue connectLoop
case d.clientCh <- client:
continue
}
}
}
}

func (d *Server) Client() (DRPCClient, error) {
select {
case <-d.lifecycleCtx.Done():
return nil, xerrors.New("context closed")
case client := <-d.clientCh:
return client, nil
}
}

// GetRequestHandler retrieves a (possibly reused) [*aibridge.RequestBridge] from the pool, for the given user.
func (d *Server) GetRequestHandler(ctx context.Context, req Request) (http.Handler, error) {
if d.requestBridgePool == nil {
return nil, xerrors.New("nil requestBridgePool")
}

reqBridge, err := d.requestBridgePool.Acquire(ctx, req, d.Client)
if err != nil {
return nil, xerrors.Errorf("acquire request bridge: %w", err)
}

return reqBridge, nil
}

// isShutdown returns whether the Server is shutdown or not.
func (d *Server) isShutdown() bool {
select {
case <-d.lifecycleCtx.Done():
return true
default:
return false
}
}

// Shutdown waits for all exiting in-flight requests to complete, or the context to expire, whichever comes first.
func (d *Server) Shutdown(ctx context.Context) error {
var err error
d.shutdownOnce.Do(func() {
d.cancelFn()

// Wait for any outstanding connections to terminate.
d.wg.Wait()

select {
case <-ctx.Done():
d.logger.Warn(ctx, "graceful shutdown failed", slog.Error(ctx.Err()))
err = ctx.Err()
return
default:
}

d.logger.Info(ctx, "shutting down request pool")
if err = d.requestBridgePool.Shutdown(ctx); err != nil {
d.logger.Error(ctx, "request pool shutdown failed with error", slog.Error(err))
}

d.logger.Info(ctx, "gracefully shutdown")
})
return err
}
Loading

[8]ページ先頭

©2009-2025 Movatter.jp