fix: clamp template port sharing level in SubAgentAPI (#26061)

Fixes an issue where sub-agent apps created via CreateSubAgent would
bypass the check for the template's max port sharing level:

- Clamps dynamically inserted `workspace_apps` to the template max
sharing level in `coderd.agentapi.SubAgentAPI`.
- Emits a warning when clamping occurs.
- Adds unit test coverage for the max sharing level matrix.
- Adds an integration-ish test through the devcontainer sub-agent client
path.

> 🤖 Generated by Coder Agents with guidance from a human.
This commit is contained in:
Cian Johnston
2026-06-05 16:30:15 +01:00
committed by GitHub
parent 5d8cd2ea7c
commit 63cd8a8c01
5 changed files with 565 additions and 3 deletions
+3
View File
@@ -26,6 +26,7 @@ import (
"github.com/coder/coder/v2/coderd/database/pubsub"
"github.com/coder/coder/v2/coderd/externalauth"
"github.com/coder/coder/v2/coderd/notifications"
"github.com/coder/coder/v2/coderd/portsharing"
"github.com/coder/coder/v2/coderd/prometheusmetrics"
"github.com/coder/coder/v2/coderd/tracing"
"github.com/coder/coder/v2/coderd/workspacestats"
@@ -90,6 +91,7 @@ type Options struct {
NetworkTelemetryHandler func(batch []*tailnetproto.TelemetryEvent)
BoundaryUsageTracker *boundaryusage.Tracker
LifecycleMetrics *LifecycleMetrics
PortSharer *atomic.Pointer[portsharing.PortSharer]
AccessURL *url.URL
AppHostname string
@@ -230,6 +232,7 @@ func New(opts Options, workspace database.Workspace, agent database.WorkspaceAge
Log: opts.Log,
Clock: opts.Clock,
Database: opts.Database,
PortSharer: opts.PortSharer,
}
api.BoundaryLogsAPI = &BoundaryLogsAPI{
+41 -3
View File
@@ -8,6 +8,7 @@ import (
"errors"
"fmt"
"strings"
"sync/atomic"
"github.com/google/uuid"
"github.com/sqlc-dev/pqtype"
@@ -17,6 +18,7 @@ import (
agentproto "github.com/coder/coder/v2/agent/proto"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/dbauthz"
"github.com/coder/coder/v2/coderd/portsharing"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/provisioner"
"github.com/coder/quartz"
@@ -27,9 +29,10 @@ type SubAgentAPI struct {
OrganizationID uuid.UUID
AgentFn func(context.Context) (database.WorkspaceAgent, error)
Log slog.Logger
Clock quartz.Clock
Database database.Store
Log slog.Logger
Clock quartz.Clock
Database database.Store
PortSharer *atomic.Pointer[portsharing.PortSharer]
}
func (a *SubAgentAPI) CreateSubAgent(ctx context.Context, req *agentproto.CreateSubAgentRequest) (*agentproto.CreateSubAgentResponse, error) {
@@ -129,6 +132,21 @@ func (a *SubAgentAPI) CreateSubAgent(ctx context.Context, req *agentproto.Create
Detail: fmt.Sprintf("agent name %q does not match regex %q", agentName, provisioner.AgentNameRegex),
}
}
var template database.Template
if len(req.Apps) > 0 {
workspace, err := a.Database.GetWorkspaceByAgentID(ctx, parentAgent.ID)
if err != nil {
return nil, xerrors.Errorf("get workspace by agent id: %w", err)
}
// Intentional: SubAgentAPI auth context enforces template ACL.
// Normal workspace operations depend on this.
template, err = a.Database.GetTemplateByID(ctx, workspace.TemplateID)
if err != nil {
return nil, xerrors.Errorf("get template policy: %w. If template access was recently changed, restart the workspace to refresh agent permissions", err)
}
}
subAgent, err := a.Database.InsertWorkspaceAgent(ctx, database.InsertWorkspaceAgentParams{
ID: uuid.New(),
ParentID: uuid.NullUUID{Valid: true, UUID: parentAgent.ID},
@@ -155,6 +173,14 @@ func (a *SubAgentAPI) CreateSubAgent(ctx context.Context, req *agentproto.Create
return nil, xerrors.Errorf("insert sub agent: %w", err)
}
// A nil PortSharer uses the AGPL default, which permits all share levels.
portSharer := portsharing.DefaultPortSharer
if a.PortSharer != nil {
if loaded := a.PortSharer.Load(); loaded != nil {
portSharer = *loaded
}
}
var appCreationErrors []*agentproto.CreateSubAgentResponse_AppCreationError
appSlugs := make(map[string]struct{})
@@ -198,6 +224,18 @@ func (a *SubAgentAPI) CreateSubAgent(ctx context.Context, req *agentproto.Create
}
}
sharingLevel := database.AppSharingLevel(strings.ToLower(protoSharingLevel))
// Clamp instead of rejecting so a too-permissive app share level does
// not block the sub-agent from starting.
if err := portSharer.AuthorizedLevel(template, codersdk.WorkspaceAgentPortShareLevel(sharingLevel)); err != nil {
a.Log.Warn(ctx, "clamping sub-agent app sharing level to template max port sharing level",
slog.F("sub_agent_name", subAgent.Name),
slog.F("sub_agent_id", subAgent.ID),
slog.F("app_slug", slug),
slog.F("requested_share_level", sharingLevel),
slog.F("max_port_share_level", template.MaxPortSharingLevel),
slog.Error(err))
sharingLevel = template.MaxPortSharingLevel
}
var openIn database.WorkspaceAppOpenIn
switch app.GetOpenIn() {
+5
View File
@@ -412,6 +412,11 @@ var (
User: []rbac.Permission{},
ByOrgID: map[string]rbac.OrgPermissions{
orgID.String(): {
Org: rbac.Permissions(map[string][]policy.Action{
// SubAgentAPI needs to check metadata of templates
// potentially shared via group_acl.
rbac.ResourceTemplate.Type: {policy.ActionRead},
}),
Member: rbac.Permissions(map[string][]policy.Action{
rbac.ResourceWorkspace.Type: {policy.ActionRead, policy.ActionUpdate, policy.ActionCreateAgent, policy.ActionDeleteAgent, policy.ActionUpdateAgent},
}),
+1
View File
@@ -166,6 +166,7 @@ func (api *API) workspaceAgentRPC(rw http.ResponseWriter, r *http.Request) {
PublishWorkspaceAgentLogsUpdateFn: api.publishWorkspaceAgentLogsUpdate,
NetworkTelemetryHandler: api.NetworkTelemetryBatcher.Handler,
BoundaryUsageTracker: api.BoundaryUsageTracker,
PortSharer: &api.PortSharer,
AccessURL: api.AccessURL,
AppHostname: api.AppHostname,
+515
View File
@@ -0,0 +1,515 @@
package coderd_test
import (
"cmp"
"context"
"slices"
"strings"
"sync/atomic"
"testing"
"github.com/google/uuid"
"github.com/prometheus/client_golang/prometheus"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
"github.com/coder/coder/v2/agent/agentcontainers"
"github.com/coder/coder/v2/agent/proto"
"github.com/coder/coder/v2/coderd/agentapi"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/db2sdk"
agpldbauthz "github.com/coder/coder/v2/coderd/database/dbauthz"
"github.com/coder/coder/v2/coderd/database/dbgen"
"github.com/coder/coder/v2/coderd/database/dbmock"
"github.com/coder/coder/v2/coderd/database/dbtestutil"
agplportsharing "github.com/coder/coder/v2/coderd/portsharing"
"github.com/coder/coder/v2/coderd/rbac"
"github.com/coder/coder/v2/coderd/util/ptr"
"github.com/coder/coder/v2/codersdk"
entdbauthz "github.com/coder/coder/v2/enterprise/coderd/dbauthz"
entportsharing "github.com/coder/coder/v2/enterprise/coderd/portsharing"
"github.com/coder/coder/v2/testutil"
"github.com/coder/quartz"
)
func TestSubAgentAPICreateSubAgentAppShareRespectsEnterpriseMaxPortShareLevel(t *testing.T) {
t.Parallel()
type expectedApp struct {
slugSuffix string
sharingLevel database.AppSharingLevel
}
tests := []struct {
name string
maxPortShareLevel database.AppSharingLevel
apps []*proto.CreateSubAgentRequest_App
expectedStoredApps []expectedApp
}{
{
name: "AuthenticatedClampsPublicOnly",
maxPortShareLevel: database.AppSharingLevelAuthenticated,
apps: []*proto.CreateSubAgentRequest_App{
{
Slug: "public-app",
Share: proto.CreateSubAgentRequest_App_PUBLIC.Enum(),
Url: ptr.Ref("http://localhost:8080"),
},
{
Slug: "authenticated-app",
Share: proto.CreateSubAgentRequest_App_AUTHENTICATED.Enum(),
Url: ptr.Ref("http://localhost:8081"),
},
{
Slug: "owner-app",
Share: proto.CreateSubAgentRequest_App_OWNER.Enum(),
Url: ptr.Ref("http://localhost:8082"),
},
{
Slug: "organization-app",
Share: proto.CreateSubAgentRequest_App_ORGANIZATION.Enum(),
Url: ptr.Ref("http://localhost:8083"),
},
},
expectedStoredApps: []expectedApp{
{
slugSuffix: "-authenticated-app",
sharingLevel: database.AppSharingLevelAuthenticated,
},
{
slugSuffix: "-organization-app",
sharingLevel: database.AppSharingLevelOrganization,
},
{
slugSuffix: "-owner-app",
sharingLevel: database.AppSharingLevelOwner,
},
{
slugSuffix: "-public-app",
sharingLevel: database.AppSharingLevelAuthenticated,
},
},
},
{
name: "PublicAllowsPublicAuthenticatedOrganizationAndOwner",
maxPortShareLevel: database.AppSharingLevelPublic,
apps: []*proto.CreateSubAgentRequest_App{
{
Slug: "public-app",
Share: proto.CreateSubAgentRequest_App_PUBLIC.Enum(),
Url: ptr.Ref("http://localhost:8080"),
},
{
Slug: "authenticated-app",
Share: proto.CreateSubAgentRequest_App_AUTHENTICATED.Enum(),
Url: ptr.Ref("http://localhost:8081"),
},
{
Slug: "owner-app",
Share: proto.CreateSubAgentRequest_App_OWNER.Enum(),
Url: ptr.Ref("http://localhost:8082"),
},
{
Slug: "organization-app",
Share: proto.CreateSubAgentRequest_App_ORGANIZATION.Enum(),
Url: ptr.Ref("http://localhost:8083"),
},
},
expectedStoredApps: []expectedApp{
{
slugSuffix: "-authenticated-app",
sharingLevel: database.AppSharingLevelAuthenticated,
},
{
slugSuffix: "-organization-app",
sharingLevel: database.AppSharingLevelOrganization,
},
{
slugSuffix: "-owner-app",
sharingLevel: database.AppSharingLevelOwner,
},
{
slugSuffix: "-public-app",
sharingLevel: database.AppSharingLevelPublic,
},
},
},
{
name: "OrganizationClampsAuthenticatedAndPublic",
maxPortShareLevel: database.AppSharingLevelOrganization,
apps: []*proto.CreateSubAgentRequest_App{
{
Slug: "authenticated-app",
Share: proto.CreateSubAgentRequest_App_AUTHENTICATED.Enum(),
Url: ptr.Ref("http://localhost:8080"),
},
{
Slug: "public-app",
Share: proto.CreateSubAgentRequest_App_PUBLIC.Enum(),
Url: ptr.Ref("http://localhost:8081"),
},
{
Slug: "owner-app",
Share: proto.CreateSubAgentRequest_App_OWNER.Enum(),
Url: ptr.Ref("http://localhost:8082"),
},
{
Slug: "organization-app",
Share: proto.CreateSubAgentRequest_App_ORGANIZATION.Enum(),
Url: ptr.Ref("http://localhost:8083"),
},
},
expectedStoredApps: []expectedApp{
{
slugSuffix: "-authenticated-app",
sharingLevel: database.AppSharingLevelOrganization,
},
{
slugSuffix: "-organization-app",
sharingLevel: database.AppSharingLevelOrganization,
},
{
slugSuffix: "-owner-app",
sharingLevel: database.AppSharingLevelOwner,
},
{
slugSuffix: "-public-app",
sharingLevel: database.AppSharingLevelOrganization,
},
},
},
{
name: "OwnerClampsOrganizationAuthenticatedAndPublic",
maxPortShareLevel: database.AppSharingLevelOwner,
apps: []*proto.CreateSubAgentRequest_App{
{
Slug: "authenticated-app",
Share: proto.CreateSubAgentRequest_App_AUTHENTICATED.Enum(),
Url: ptr.Ref("http://localhost:8080"),
},
{
Slug: "public-app",
Share: proto.CreateSubAgentRequest_App_PUBLIC.Enum(),
Url: ptr.Ref("http://localhost:8081"),
},
{
Slug: "owner-app",
Share: proto.CreateSubAgentRequest_App_OWNER.Enum(),
Url: ptr.Ref("http://localhost:8082"),
},
{
Slug: "organization-app",
Share: proto.CreateSubAgentRequest_App_ORGANIZATION.Enum(),
Url: ptr.Ref("http://localhost:8083"),
},
},
expectedStoredApps: []expectedApp{
{
slugSuffix: "-authenticated-app",
sharingLevel: database.AppSharingLevelOwner,
},
{
slugSuffix: "-organization-app",
sharingLevel: database.AppSharingLevelOwner,
},
{
slugSuffix: "-owner-app",
sharingLevel: database.AppSharingLevelOwner,
},
{
slugSuffix: "-public-app",
sharingLevel: database.AppSharingLevelOwner,
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
ctx, api, upsertedApps := newMockSubAgentAPIWithMaxPortShareLevel(t, tt.maxPortShareLevel, len(tt.apps))
resp, err := api.CreateSubAgent(ctx, &proto.CreateSubAgentRequest{
Name: "child-agent",
Directory: "/workspaces/coder",
Architecture: "amd64",
OperatingSystem: "linux",
Apps: tt.apps,
})
require.NoError(t, err)
require.NotNil(t, resp.Agent)
require.Empty(t, resp.AppCreationErrors)
require.Len(t, *upsertedApps, len(tt.expectedStoredApps))
slices.SortFunc(*upsertedApps, func(a, b database.UpsertWorkspaceAppParams) int {
return cmp.Compare(appSlugSuffix(a.Slug), appSlugSuffix(b.Slug))
})
slices.SortFunc(tt.expectedStoredApps, func(a, b expectedApp) int {
return cmp.Compare(a.slugSuffix, b.slugSuffix)
})
for i, expectedApp := range tt.expectedStoredApps {
require.Equal(t, expectedApp.slugSuffix, appSlugSuffix((*upsertedApps)[i].Slug))
require.Equal(t, expectedApp.sharingLevel, (*upsertedApps)[i].SharingLevel)
}
})
}
}
func appSlugSuffix(slug string) string {
_, suffix, ok := strings.Cut(slug, "-")
if !ok {
return slug
}
return "-" + suffix
}
func newMockSubAgentAPIWithMaxPortShareLevel(
t *testing.T,
maxPortShareLevel database.AppSharingLevel,
appCount int,
) (context.Context, *agentapi.SubAgentAPI, *[]database.UpsertWorkspaceAppParams) {
t.Helper()
ctx := testutil.Context(t, testutil.WaitShort)
log := testutil.Logger(t)
clock := quartz.NewMock(t)
ownerID := uuid.New()
organizationID := uuid.New()
templateID := uuid.New()
parentAgent := database.WorkspaceAgent{
ID: uuid.New(),
ResourceID: uuid.New(),
}
workspace := database.Workspace{
ID: uuid.New(),
OwnerID: ownerID,
OrganizationID: organizationID,
TemplateID: templateID,
}
template := database.Template{
ID: templateID,
MaxPortSharingLevel: maxPortShareLevel,
}
upsertedApps := []database.UpsertWorkspaceAppParams{}
db := dbmock.NewMockStore(gomock.NewController(t))
db.EXPECT().GetWorkspaceByAgentID(gomock.Any(), parentAgent.ID).Return(workspace, nil)
db.EXPECT().GetTemplateByID(gomock.Any(), templateID).Return(template, nil)
db.EXPECT().InsertWorkspaceAgent(gomock.Any(), gomock.Any()).DoAndReturn(
func(_ context.Context, params database.InsertWorkspaceAgentParams) (database.WorkspaceAgent, error) {
require.True(t, params.ParentID.Valid)
require.Equal(t, parentAgent.ID, params.ParentID.UUID)
return database.WorkspaceAgent{
ID: params.ID,
Name: params.Name,
AuthToken: params.AuthToken,
}, nil
},
)
db.EXPECT().UpsertWorkspaceApp(gomock.Any(), gomock.Any()).DoAndReturn(
func(_ context.Context, params database.UpsertWorkspaceAppParams) (database.WorkspaceApp, error) {
upsertedApps = append(upsertedApps, params)
return database.WorkspaceApp{
ID: params.ID,
AgentID: params.AgentID,
Slug: params.Slug,
SharingLevel: params.SharingLevel,
}, nil
},
).Times(appCount)
portSharer := &atomic.Pointer[agplportsharing.PortSharer]{}
var ps agplportsharing.PortSharer = entportsharing.NewEnterprisePortSharer()
portSharer.Store(&ps)
api := &agentapi.SubAgentAPI{
OwnerID: ownerID,
OrganizationID: organizationID,
AgentFn: func(context.Context) (database.WorkspaceAgent, error) {
return parentAgent, nil
},
Log: log,
Clock: clock,
Database: db,
PortSharer: portSharer,
}
return ctx, api, &upsertedApps
}
func TestDevcontainerSubAgentAppShareClampedByEnterpriseTemplateMaxPortShareLevel(t *testing.T) {
t.Parallel()
ctx, db, client := newDevcontainerSubAgentClientWithMaxPortShareLevel(t, database.AppSharingLevelAuthenticated)
subAgent, err := client.Create(ctx, agentcontainers.SubAgent{
Name: "devcontainer",
Directory: "/workspaces/coder",
Architecture: "amd64",
OperatingSystem: "linux",
Apps: []agentcontainers.SubAgentApp{
{
Slug: "public-app",
URL: "http://localhost:8080",
Share: codersdk.WorkspaceAppSharingLevelPublic,
},
{
Slug: "owner-app",
URL: "http://localhost:8081",
Share: codersdk.WorkspaceAppSharingLevelOwner,
},
},
})
require.NoError(t, err)
require.NotEqual(t, uuid.Nil, subAgent.ID)
apps, err := db.GetWorkspaceAppsByAgentID(ctx, subAgent.ID)
require.NoError(t, err)
require.Len(t, apps, 2)
slices.SortFunc(apps, func(a, b database.WorkspaceApp) int {
return cmp.Compare(appSlugSuffix(a.Slug), appSlugSuffix(b.Slug))
})
require.Equal(t, "-owner-app", appSlugSuffix(apps[0].Slug))
require.Equal(t, database.AppSharingLevelOwner, apps[0].SharingLevel)
require.Equal(t, "-public-app", appSlugSuffix(apps[1].Slug))
require.Equal(t, database.AppSharingLevelAuthenticated, apps[1].SharingLevel)
}
func TestDevcontainerCoderAppShareClampedWithGroupRestrictedEnterpriseTemplateACL(t *testing.T) {
t.Parallel()
ctx, db, client := newDevcontainerSubAgentClientWithMaxPortShareLevel(t,
database.AppSharingLevelAuthenticated,
withGroupRestrictedTemplateACL,
)
subAgent, err := client.Create(ctx, agentcontainers.SubAgent{
Name: "devcontainer",
Directory: "/workspaces/coder",
Architecture: "amd64",
OperatingSystem: "linux",
Apps: []agentcontainers.SubAgentApp{
{
Slug: "public-app",
URL: "http://localhost:8080",
Share: codersdk.WorkspaceAppSharingLevelPublic,
},
},
})
require.NoError(t, err)
apps, err := db.GetWorkspaceAppsByAgentID(ctx, subAgent.ID)
require.NoError(t, err)
require.Len(t, apps, 1)
require.Equal(t, "-public-app", appSlugSuffix(apps[0].Slug))
require.Equal(t, database.AppSharingLevelAuthenticated, apps[0].SharingLevel)
}
type devcontainerSubAgentClientOption func(testing.TB, database.Store, database.Organization, database.User, *database.Template)
func newDevcontainerSubAgentClientWithMaxPortShareLevel(
t *testing.T,
maxPortShareLevel database.AppSharingLevel,
options ...devcontainerSubAgentClientOption,
) (context.Context, database.Store, agentcontainers.SubAgentClient) {
t.Helper()
ctx := testutil.Context(t, testutil.WaitShort)
log := testutil.Logger(t)
clock := quartz.NewMock(t)
rawDB, _ := dbtestutil.NewDB(t)
org := dbgen.Organization(t, rawDB, database.Organization{})
user := dbgen.User(t, rawDB, database.User{})
template := dbgen.Template(t, rawDB, database.Template{
OrganizationID: org.ID,
CreatedBy: user.ID,
MaxPortSharingLevel: maxPortShareLevel,
})
for _, option := range options {
option(t, rawDB, org, user, &template)
}
templateVersion := dbgen.TemplateVersion(t, rawDB, database.TemplateVersion{
TemplateID: uuid.NullUUID{Valid: true, UUID: template.ID},
OrganizationID: org.ID,
CreatedBy: user.ID,
})
workspace := dbgen.Workspace(t, rawDB, database.WorkspaceTable{
OrganizationID: org.ID,
TemplateID: template.ID,
OwnerID: user.ID,
})
job := dbgen.ProvisionerJob(t, rawDB, nil, database.ProvisionerJob{
Type: database.ProvisionerJobTypeWorkspaceBuild,
OrganizationID: org.ID,
})
build := dbgen.WorkspaceBuild(t, rawDB, database.WorkspaceBuild{
JobID: job.ID,
WorkspaceID: workspace.ID,
TemplateVersionID: templateVersion.ID,
})
resource := dbgen.WorkspaceResource(t, rawDB, database.WorkspaceResource{
JobID: build.JobID,
})
parentAgent := dbgen.WorkspaceAgent(t, rawDB, database.WorkspaceAgent{
ResourceID: resource.ID,
})
auth := rbac.NewStrictCachingAuthorizer(prometheus.NewRegistry())
accessControlStore := &atomic.Pointer[agpldbauthz.AccessControlStore]{}
var acs agpldbauthz.AccessControlStore = entdbauthz.EnterpriseTemplateAccessControlStore{}
accessControlStore.Store(&acs)
db := agpldbauthz.New(rawDB, auth, log, accessControlStore)
portSharer := &atomic.Pointer[agplportsharing.PortSharer]{}
var ps agplportsharing.PortSharer = entportsharing.NewEnterprisePortSharer()
portSharer.Store(&ps)
api := &agentapi.SubAgentAPI{
OwnerID: user.ID,
OrganizationID: org.ID,
AgentFn: func(context.Context) (database.WorkspaceAgent, error) {
return parentAgent, nil
},
Log: log,
Clock: clock,
Database: db,
PortSharer: portSharer,
}
client := agentcontainers.NewSubAgentClientFromAPI(log, devcontainerSubAgentDRPCClient{api: api})
return ctx, rawDB, client
}
func withGroupRestrictedTemplateACL(t testing.TB, db database.Store, org database.Organization, user database.User, template *database.Template) {
t.Helper()
group := dbgen.Group(t, db, database.Group{OrganizationID: org.ID})
dbgen.GroupMember(t, db, database.GroupMemberTable{
GroupID: group.ID,
UserID: user.ID,
})
template.GroupACL = database.TemplateACL{
group.ID.String(): db2sdk.TemplateRoleActions(codersdk.TemplateRoleUse),
}
template.UserACL = database.TemplateACL{}
require.NoError(t, db.UpdateTemplateACLByID(context.Background(), database.UpdateTemplateACLByIDParams{
ID: template.ID,
GroupACL: template.GroupACL,
UserACL: template.UserACL,
}))
}
type devcontainerSubAgentDRPCClient struct {
proto.DRPCAgentClient28
api *agentapi.SubAgentAPI
}
func (c devcontainerSubAgentDRPCClient) CreateSubAgent(ctx context.Context, req *proto.CreateSubAgentRequest) (*proto.CreateSubAgentResponse, error) {
return c.api.CreateSubAgent(ctx, req)
}
func (c devcontainerSubAgentDRPCClient) DeleteSubAgent(ctx context.Context, req *proto.DeleteSubAgentRequest) (*proto.DeleteSubAgentResponse, error) {
return c.api.DeleteSubAgent(ctx, req)
}
func (c devcontainerSubAgentDRPCClient) ListSubAgents(ctx context.Context, req *proto.ListSubAgentsRequest) (*proto.ListSubAgentsResponse, error) {
return c.api.ListSubAgents(ctx, req)
}