Skip to content

feat(agent): enable devcontainers by default #18533

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 our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jun 24, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
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
25 changes: 12 additions & 13 deletions agent/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,9 +89,8 @@ type Options struct {
ServiceBannerRefreshInterval time.Duration
BlockFileTransfer bool
Execer agentexec.Execer

ExperimentalDevcontainersEnabled bool
ContainerAPIOptions []agentcontainers.Option // Enable ExperimentalDevcontainersEnabled for these to be effective.
Devcontainers bool
DevcontainerAPIOptions []agentcontainers.Option // Enable Devcontainers for these to be effective.
}

type Client interface {
Expand Down Expand Up @@ -190,8 +189,8 @@ func New(options Options) Agent {
metrics: newAgentMetrics(prometheusRegistry),
execer: options.Execer,

experimentalDevcontainersEnabled: options.ExperimentalDevcontainersEnabled,
containerAPIOptions: options.ContainerAPIOptions,
devcontainers: options.Devcontainers,
containerAPIOptions: options.DevcontainerAPIOptions,
}
// Initially, we have a closed channel, reflecting the fact that we are not initially connected.
// Each time we connect we replace the channel (while holding the closeMutex) with a new one
Expand Down Expand Up @@ -272,9 +271,9 @@ type agent struct {
metrics *agentMetrics
execer agentexec.Execer

experimentalDevcontainersEnabled bool
containerAPIOptions []agentcontainers.Option
containerAPI atomic.Pointer[agentcontainers.API] // Set by apiHandler.
devcontainers bool
containerAPIOptions []agentcontainers.Option
containerAPI atomic.Pointer[agentcontainers.API] // Set by apiHandler.
}

func (a *agent) TailnetConn() *tailnet.Conn {
Expand Down Expand Up @@ -311,7 +310,7 @@ func (a *agent) init() {
return a.reportConnection(id, connectionType, ip)
},

ExperimentalDevContainersEnabled: a.experimentalDevcontainersEnabled,
ExperimentalContainers: a.devcontainers,
})
if err != nil {
panic(err)
Expand Down Expand Up @@ -340,7 +339,7 @@ func (a *agent) init() {
a.metrics.connectionsTotal, a.metrics.reconnectingPTYErrors,
a.reconnectingPTYTimeout,
func(s *reconnectingpty.Server) {
s.ExperimentalDevcontainersEnabled = a.experimentalDevcontainersEnabled
s.ExperimentalContainers = a.devcontainers
},
)
go a.runLoop()
Expand Down Expand Up @@ -1087,9 +1086,9 @@ func (a *agent) handleManifest(manifestOK *checkpoint) func(ctx context.Context,
slog.F("parent_id", manifest.ParentID),
slog.F("agent_id", manifest.AgentID),
)
if a.experimentalDevcontainersEnabled {
if a.devcontainers {
a.logger.Info(ctx, "devcontainers are not supported on sub agents, disabling feature")
a.experimentalDevcontainersEnabled = false
a.devcontainers = false
}
}
a.client.RewriteDERPMap(manifest.DERPMap)
Expand Down Expand Up @@ -1145,7 +1144,7 @@ func (a *agent) handleManifest(manifestOK *checkpoint) func(ctx context.Context,
scripts = manifest.Scripts
scriptRunnerOpts []agentscripts.InitOption
)
if a.experimentalDevcontainersEnabled {
if a.devcontainers {
var dcScripts []codersdk.WorkspaceAgentScript
scripts, dcScripts = agentcontainers.ExtractAndInitializeDevcontainerScripts(manifest.Devcontainers, scripts)
// See ExtractAndInitializeDevcontainerScripts for motivation
Expand Down
17 changes: 8 additions & 9 deletions agent/agent_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1954,8 +1954,8 @@ func TestAgent_ReconnectingPTYContainer(t *testing.T) {

// nolint: dogsled
conn, _, _, _, _ := setupAgent(t, agentsdk.Manifest{}, 0, func(_ *agenttest.Client, o *agent.Options) {
o.ExperimentalDevcontainersEnabled = true
o.ContainerAPIOptions = append(o.ContainerAPIOptions,
o.Devcontainers = true
o.DevcontainerAPIOptions = append(o.DevcontainerAPIOptions,
agentcontainers.WithContainerLabelIncludeFilter("this.label.does.not.exist.ignore.devcontainers", "true"),
)
})
Expand Down Expand Up @@ -2161,9 +2161,9 @@ func TestAgent_DevcontainerAutostart(t *testing.T) {

//nolint:dogsled
_, agentClient, _, _, _ := setupAgent(t, manifest, 0, func(_ *agenttest.Client, o *agent.Options) {
o.ExperimentalDevcontainersEnabled = true
o.ContainerAPIOptions = append(
o.ContainerAPIOptions,
o.Devcontainers = true
o.DevcontainerAPIOptions = append(
o.DevcontainerAPIOptions,
// Only match this specific dev container.
agentcontainers.WithClock(mClock),
agentcontainers.WithContainerLabelIncludeFilter("devcontainer.local_folder", tempWorkspaceFolder),
Expand Down Expand Up @@ -2312,8 +2312,8 @@ func TestAgent_DevcontainerRecreate(t *testing.T) {

//nolint:dogsled
conn, client, _, _, _ := setupAgent(t, manifest, 0, func(_ *agenttest.Client, o *agent.Options) {
o.ExperimentalDevcontainersEnabled = true
o.ContainerAPIOptions = append(o.ContainerAPIOptions,
o.Devcontainers = true
o.DevcontainerAPIOptions = append(o.DevcontainerAPIOptions,
agentcontainers.WithContainerLabelIncludeFilter("devcontainer.local_folder", workspaceFolder),
)
})
Expand Down Expand Up @@ -2438,8 +2438,7 @@ func TestAgent_DevcontainersDisabledForSubAgent(t *testing.T) {

// Setup the agent with devcontainers enabled initially.
//nolint:dogsled
conn, _, _, _, _ := setupAgent(t, manifest, 0, func(_ *agenttest.Client, o *agent.Options) {
o.ExperimentalDevcontainersEnabled = true
conn, _, _, _, _ := setupAgent(t, manifest, 0, func(*agenttest.Client, *agent.Options) {
})

// Query the containers API endpoint. This should fail because
Expand Down
11 changes: 6 additions & 5 deletions agent/agentssh/agentssh.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,9 +113,10 @@ type Config struct {
BlockFileTransfer bool
// ReportConnection.
ReportConnection reportConnectionFunc
// Experimental: allow connecting to running containers if
// CODER_AGENT_DEVCONTAINERS_ENABLE=true.
ExperimentalDevContainersEnabled bool
// Experimental: allow connecting to running containers via Docker exec.
// Note that this is different from the devcontainers feature, which uses
// subagents.
ExperimentalContainers bool
}

type Server struct {
Expand Down Expand Up @@ -435,7 +436,7 @@ func (s *Server) sessionHandler(session ssh.Session) {
switch ss := session.Subsystem(); ss {
case "":
case "sftp":
if s.config.ExperimentalDevContainersEnabled && container != "" {
if s.config.ExperimentalContainers && container != "" {
closeCause("sftp not yet supported with containers")
_ = session.Exit(1)
return
Expand Down Expand Up @@ -549,7 +550,7 @@ func (s *Server) sessionStart(logger slog.Logger, session ssh.Session, env []str

var ei usershell.EnvInfoer
var err error
if s.config.ExperimentalDevContainersEnabled && container != "" {
if s.config.ExperimentalContainers && container != "" {
ei, err = agentcontainers.EnvInfo(ctx, s.Execer, container, containerUser)
if err != nil {
s.metrics.sessionErrors.WithLabelValues(magicTypeLabel, ptyLabel, "container_env_info").Add(1)
Expand Down
2 changes: 1 addition & 1 deletion agent/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ func (a *agent) apiHandler(aAPI proto.DRPCAgentClient26) (http.Handler, func() e
cacheDuration: cacheDuration,
}

if a.experimentalDevcontainersEnabled {
if a.devcontainers {
containerAPIOpts := []agentcontainers.Option{
agentcontainers.WithExecer(a.execer),
agentcontainers.WithCommandEnv(a.sshServer.CommandEnv),
Expand Down
8 changes: 5 additions & 3 deletions agent/reconnectingpty/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,10 @@ type Server struct {
connCount atomic.Int64
reconnectingPTYs sync.Map
timeout time.Duration

ExperimentalDevcontainersEnabled bool
// Experimental: allow connecting to running containers via Docker exec.
// Note that this is different from the devcontainers feature, which uses
// subagents.
ExperimentalContainers bool
}

// NewServer returns a new ReconnectingPTY server
Expand Down Expand Up @@ -187,7 +189,7 @@ func (s *Server) handleConn(ctx context.Context, logger slog.Logger, conn net.Co
}()

var ei usershell.EnvInfoer
if s.ExperimentalDevcontainersEnabled && msg.Container != "" {
if s.ExperimentalContainers && msg.Container != "" {
dei, err := agentcontainers.EnvInfo(ctx, s.commandCreator.Execer, msg.Container, msg.ContainerUser)
if err != nil {
return xerrors.Errorf("get container env info: %w", err)
Expand Down
19 changes: 9 additions & 10 deletions cli/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,7 @@ func (r *RootCmd) workspaceAgent() *serpent.Command {
blockFileTransfer bool
agentHeaderCommand string
agentHeader []string

experimentalDevcontainersEnabled bool
devcontainers bool
)
cmd := &serpent.Command{
Use: "agent",
Expand Down Expand Up @@ -321,7 +320,7 @@ func (r *RootCmd) workspaceAgent() *serpent.Command {
return xerrors.Errorf("create agent execer: %w", err)
}

if experimentalDevcontainersEnabled {
if devcontainers {
logger.Info(ctx, "agent devcontainer detection enabled")
} else {
logger.Info(ctx, "agent devcontainer detection not enabled")
Expand Down Expand Up @@ -359,11 +358,11 @@ func (r *RootCmd) workspaceAgent() *serpent.Command {
SSHMaxTimeout: sshMaxTimeout,
Subsystems: subsystems,

PrometheusRegistry: prometheusRegistry,
BlockFileTransfer: blockFileTransfer,
Execer: execer,
ExperimentalDevcontainersEnabled: experimentalDevcontainersEnabled,
ContainerAPIOptions: []agentcontainers.Option{
PrometheusRegistry: prometheusRegistry,
BlockFileTransfer: blockFileTransfer,
Execer: execer,
Devcontainers: devcontainers,
DevcontainerAPIOptions: []agentcontainers.Option{
agentcontainers.WithSubAgentURL(r.agentURL.String()),
},
})
Expand Down Expand Up @@ -506,10 +505,10 @@ func (r *RootCmd) workspaceAgent() *serpent.Command {
},
{
Flag: "devcontainers-enable",
Default: "false",
Default: "true",
Env: "CODER_AGENT_DEVCONTAINERS_ENABLE",
Description: "Allow the agent to automatically detect running devcontainers.",
Value: serpent.BoolOf(&experimentalDevcontainersEnabled),
Value: serpent.BoolOf(&devcontainers),
},
}

Expand Down
4 changes: 2 additions & 2 deletions cli/exp_rpty_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,8 +116,8 @@ func TestExpRpty(t *testing.T) {
})

_ = agenttest.New(t, client.URL, agentToken, func(o *agent.Options) {
o.ExperimentalDevcontainersEnabled = true
o.ContainerAPIOptions = append(o.ContainerAPIOptions,
o.Devcontainers = true
o.DevcontainerAPIOptions = append(o.DevcontainerAPIOptions,
agentcontainers.WithContainerLabelIncludeFilter(wantLabel, "true"),
)
})
Expand Down
8 changes: 4 additions & 4 deletions cli/open_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -334,8 +334,8 @@ func TestOpenVSCodeDevContainer(t *testing.T) {
})

_ = agenttest.New(t, client.URL, agentToken, func(o *agent.Options) {
o.ExperimentalDevcontainersEnabled = true
o.ContainerAPIOptions = append(o.ContainerAPIOptions,
o.Devcontainers = true
o.DevcontainerAPIOptions = append(o.DevcontainerAPIOptions,
agentcontainers.WithContainerCLI(mccli),
agentcontainers.WithContainerLabelIncludeFilter("this.label.does.not.exist.ignore.devcontainers", "true"),
)
Expand Down Expand Up @@ -509,8 +509,8 @@ func TestOpenVSCodeDevContainer_NoAgentDirectory(t *testing.T) {
})

_ = agenttest.New(t, client.URL, agentToken, func(o *agent.Options) {
o.ExperimentalDevcontainersEnabled = true
o.ContainerAPIOptions = append(o.ContainerAPIOptions,
o.Devcontainers = true
o.DevcontainerAPIOptions = append(o.DevcontainerAPIOptions,
agentcontainers.WithContainerCLI(mccli),
agentcontainers.WithContainerLabelIncludeFilter("this.label.does.not.exist.ignore.devcontainers", "true"),
)
Expand Down
8 changes: 4 additions & 4 deletions cli/ssh_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2029,8 +2029,8 @@ func TestSSH_Container(t *testing.T) {
})

_ = agenttest.New(t, client.URL, agentToken, func(o *agent.Options) {
o.ExperimentalDevcontainersEnabled = true
o.ContainerAPIOptions = append(o.ContainerAPIOptions,
o.Devcontainers = true
o.DevcontainerAPIOptions = append(o.DevcontainerAPIOptions,
agentcontainers.WithContainerLabelIncludeFilter("this.label.does.not.exist.ignore.devcontainers", "true"),
)
})
Expand Down Expand Up @@ -2069,8 +2069,8 @@ func TestSSH_Container(t *testing.T) {
Warnings: nil,
}, nil).AnyTimes()
_ = agenttest.New(t, client.URL, agentToken, func(o *agent.Options) {
o.ExperimentalDevcontainersEnabled = true
o.ContainerAPIOptions = append(o.ContainerAPIOptions,
o.Devcontainers = true
o.DevcontainerAPIOptions = append(o.DevcontainerAPIOptions,
agentcontainers.WithContainerCLI(mLister),
agentcontainers.WithContainerLabelIncludeFilter("this.label.does.not.exist.ignore.devcontainers", "true"),
)
Expand Down
2 changes: 1 addition & 1 deletion cli/testdata/coder_agent_--help.golden
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ OPTIONS:
--debug-address string, $CODER_AGENT_DEBUG_ADDRESS (default: 127.0.0.1:2113)
The bind address to serve a debug HTTP server.

--devcontainers-enable bool, $CODER_AGENT_DEVCONTAINERS_ENABLE (default: false)
--devcontainers-enable bool, $CODER_AGENT_DEVCONTAINERS_ENABLE (default: true)
Allow the agent to automatically detect running devcontainers.

--log-dir string, $CODER_AGENT_LOG_DIR (default: /tmp)
Expand Down
14 changes: 7 additions & 7 deletions coderd/workspaceagents_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1250,8 +1250,8 @@ func TestWorkspaceAgentContainers(t *testing.T) {
return agents
}).Do()
_ = agenttest.New(t, client.URL, r.AgentToken, func(o *agent.Options) {
o.ExperimentalDevcontainersEnabled = true
o.ContainerAPIOptions = append(o.ContainerAPIOptions,
o.Devcontainers = true
o.DevcontainerAPIOptions = append(o.DevcontainerAPIOptions,
agentcontainers.WithContainerLabelIncludeFilter("this.label.does.not.exist.ignore.devcontainers", "true"),
)
})
Expand Down Expand Up @@ -1358,8 +1358,8 @@ func TestWorkspaceAgentContainers(t *testing.T) {
}).Do()
_ = agenttest.New(t, client.URL, r.AgentToken, func(o *agent.Options) {
o.Logger = logger.Named("agent")
o.ExperimentalDevcontainersEnabled = true
o.ContainerAPIOptions = append(o.ContainerAPIOptions,
o.Devcontainers = true
o.DevcontainerAPIOptions = append(o.DevcontainerAPIOptions,
agentcontainers.WithContainerCLI(mcl),
agentcontainers.WithContainerLabelIncludeFilter("this.label.does.not.exist.ignore.devcontainers", "true"),
)
Expand Down Expand Up @@ -1473,9 +1473,9 @@ func TestWorkspaceAgentRecreateDevcontainer(t *testing.T) {
}).Do()
_ = agenttest.New(t, client.URL, r.AgentToken, func(o *agent.Options) {
o.Logger = logger.Named("agent")
o.ExperimentalDevcontainersEnabled = true
o.ContainerAPIOptions = append(
o.ContainerAPIOptions,
o.Devcontainers = true
o.DevcontainerAPIOptions = append(
o.DevcontainerAPIOptions,
agentcontainers.WithContainerCLI(mccli),
agentcontainers.WithDevcontainerCLI(mdccli),
agentcontainers.WithWatcher(watcher.NewNoop()),
Expand Down
Loading