From bf789662563924f010148accc5444f66277f1ad7 Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Tue, 29 Jul 2025 22:30:17 +1000 Subject: [PATCH 1/5] chore: remove soft isolation configurability (#19069) Undoes a lot of the changes in 5319d47dfa5c619951fbcda67b82ae7c7e0c9efc Keeps the `netns.SetCoderSoftIsolation()` call, but always sets it to `true` when using a TUN device. --- tailnet/conn.go | 20 ++--- vpn/client.go | 16 ++-- vpn/speaker_internal_test.go | 2 +- vpn/tunnel.go | 15 ++-- vpn/tunnel_internal_test.go | 99 ++++++--------------- vpn/version.go | 4 +- vpn/vpn.pb.go | 166 ++++++++++++++++------------------- vpn/vpn.proto | 1 - 8 files changed, 127 insertions(+), 196 deletions(-) diff --git a/tailnet/conn.go b/tailnet/conn.go index e23e0ae04b0d5..709d5b2958453 100644 --- a/tailnet/conn.go +++ b/tailnet/conn.go @@ -102,17 +102,6 @@ type Options struct { BlockEndpoints bool Logger slog.Logger ListenPort uint16 - // UseSoftNetIsolation enables our homemade soft isolation feature in the - // netns package. This option will only be considered if TUNDev is set. - // - // The Coder soft isolation mode is a workaround to allow Coder Connect to - // connect to Coder servers behind corporate VPNs, and relaxes some of the - // loop protections that come with Tailscale. - // - // When soft isolation is disabled, the netns package will function as - // normal and route all traffic through the default interface (and block all - // traffic to other VPN interfaces) on macOS and Windows. - UseSoftNetIsolation bool // CaptureHook is a callback that captures Disco packets and packets sent // into the tailnet tunnel. @@ -169,10 +158,13 @@ func NewConn(options *Options) (conn *Conn, err error) { } useNetNS := options.TUNDev != nil - useSoftIsolation := useNetNS && options.UseSoftNetIsolation - options.Logger.Debug(context.Background(), "network isolation configuration", slog.F("use_netns", useNetNS), slog.F("use_soft_isolation", useSoftIsolation)) + options.Logger.Debug(context.Background(), "network isolation configuration", slog.F("use_netns", useNetNS)) netns.SetEnabled(useNetNS) - netns.SetCoderSoftIsolation(useSoftIsolation) + // The Coder soft isolation mode is a workaround to allow Coder Connect to + // connect to Coder servers behind corporate VPNs, and relaxes some of the + // loop protections that come with Tailscale. + // See the comment above the netns function for more details. + netns.SetCoderSoftIsolation(useNetNS) var telemetryStore *TelemetryStore if options.TelemetrySink != nil { diff --git a/vpn/client.go b/vpn/client.go index 8d2115ec2839a..0411b209c24a8 100644 --- a/vpn/client.go +++ b/vpn/client.go @@ -69,14 +69,13 @@ func NewClient() Client { } type Options struct { - Headers http.Header - Logger slog.Logger - UseSoftNetIsolation bool - DNSConfigurator dns.OSConfigurator - Router router.Router - TUNDevice tun.Device - WireguardMonitor *netmon.Monitor - UpdateHandler tailnet.UpdatesHandler + Headers http.Header + Logger slog.Logger + DNSConfigurator dns.OSConfigurator + Router router.Router + TUNDevice tun.Device + WireguardMonitor *netmon.Monitor + UpdateHandler tailnet.UpdatesHandler } type derpMapRewriter struct { @@ -164,7 +163,6 @@ func (*client) NewConn(initCtx context.Context, serverURL *url.URL, token string DERPForceWebSockets: connInfo.DERPForceWebSockets, Logger: options.Logger, BlockEndpoints: connInfo.DisableDirectConnections, - UseSoftNetIsolation: options.UseSoftNetIsolation, DNSConfigurator: options.DNSConfigurator, Router: options.Router, TUNDev: options.TUNDevice, diff --git a/vpn/speaker_internal_test.go b/vpn/speaker_internal_test.go index 5ec5de4a3bf59..433868851a5bc 100644 --- a/vpn/speaker_internal_test.go +++ b/vpn/speaker_internal_test.go @@ -23,7 +23,7 @@ func TestMain(m *testing.M) { goleak.VerifyTestMain(m, testutil.GoleakOptions...) } -const expectedHandshake = "codervpn tunnel 1.3\n" +const expectedHandshake = "codervpn tunnel 1.2\n" // TestSpeaker_RawPeer tests the speaker with a peer that we simulate by directly making reads and // writes to the other end of the pipe. There should be at least one test that does this, rather diff --git a/vpn/tunnel.go b/vpn/tunnel.go index 38d474c33206b..30ee56c2396fa 100644 --- a/vpn/tunnel.go +++ b/vpn/tunnel.go @@ -265,14 +265,13 @@ func (t *Tunnel) start(req *StartRequest) error { svrURL, apiToken, &Options{ - Headers: header, - Logger: t.clientLogger, - UseSoftNetIsolation: req.GetTunnelUseSoftNetIsolation(), - DNSConfigurator: networkingStack.DNSConfigurator, - Router: networkingStack.Router, - TUNDevice: networkingStack.TUNDevice, - WireguardMonitor: networkingStack.WireguardMonitor, - UpdateHandler: t, + Headers: header, + Logger: t.clientLogger, + DNSConfigurator: networkingStack.DNSConfigurator, + Router: networkingStack.Router, + TUNDevice: networkingStack.TUNDevice, + WireguardMonitor: networkingStack.WireguardMonitor, + UpdateHandler: t, }, ) if err != nil { diff --git a/vpn/tunnel_internal_test.go b/vpn/tunnel_internal_test.go index b93b679de332c..c21fd20251282 100644 --- a/vpn/tunnel_internal_test.go +++ b/vpn/tunnel_internal_test.go @@ -2,10 +2,8 @@ package vpn import ( "context" - "encoding/json" "maps" "net" - "net/http" "net/netip" "net/url" "slices" @@ -24,7 +22,6 @@ import ( "github.com/coder/quartz" maputil "github.com/coder/coder/v2/coderd/util/maps" - "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/tailnet" "github.com/coder/coder/v2/tailnet/proto" "github.com/coder/coder/v2/testutil" @@ -32,43 +29,25 @@ import ( func newFakeClient(ctx context.Context, t *testing.T) *fakeClient { return &fakeClient{ - t: t, - ctx: ctx, - connCh: make(chan *fakeConn, 1), - } -} - -func newFakeClientWithOptsCh(ctx context.Context, t *testing.T) *fakeClient { - return &fakeClient{ - t: t, - ctx: ctx, - connCh: make(chan *fakeConn, 1), - optsCh: make(chan *Options, 1), + t: t, + ctx: ctx, + ch: make(chan *fakeConn, 1), } } type fakeClient struct { - t *testing.T - ctx context.Context - connCh chan *fakeConn - optsCh chan *Options // options will be written to this channel if it's not nil + t *testing.T + ctx context.Context + ch chan *fakeConn } var _ Client = (*fakeClient)(nil) -func (f *fakeClient) NewConn(_ context.Context, _ *url.URL, _ string, opts *Options) (Conn, error) { - if f.optsCh != nil { - select { - case <-f.ctx.Done(): - return nil, f.ctx.Err() - case f.optsCh <- opts: - } - } - +func (f *fakeClient) NewConn(context.Context, *url.URL, string, *Options) (Conn, error) { select { case <-f.ctx.Done(): return nil, f.ctx.Err() - case conn := <-f.connCh: + case conn := <-f.ch: return conn, nil } } @@ -155,7 +134,7 @@ func TestTunnel_StartStop(t *testing.T) { t.Parallel() ctx := testutil.Context(t, testutil.WaitShort) - client := newFakeClientWithOptsCh(ctx, t) + client := newFakeClient(ctx, t) conn := newFakeConn(tailnet.WorkspaceUpdate{}, time.Time{}) _, mgr := setupTunnel(t, ctx, client, quartz.NewMock(t)) @@ -163,45 +142,29 @@ func TestTunnel_StartStop(t *testing.T) { errCh := make(chan error, 1) var resp *TunnelMessage // When: we start the tunnel - telemetry := codersdk.CoderDesktopTelemetry{ - DeviceID: "device001", - DeviceOS: "macOS", - CoderDesktopVersion: "0.24.8", - } - telemetryJSON, err := json.Marshal(telemetry) - require.NoError(t, err) go func() { r, err := mgr.unaryRPC(ctx, &ManagerMessage{ Msg: &ManagerMessage_Start{ Start: &StartRequest{ TunnelFileDescriptor: 2, - // Use default value for TunnelUseSoftNetIsolation - CoderUrl: "https://coder.example.com", - ApiToken: "fakeToken", + CoderUrl: "https://coder.example.com", + ApiToken: "fakeToken", Headers: []*StartRequest_Header{ {Name: "X-Test-Header", Value: "test"}, }, - DeviceOs: telemetry.DeviceOS, - DeviceId: telemetry.DeviceID, - CoderDesktopVersion: telemetry.CoderDesktopVersion, + DeviceOs: "macOS", + DeviceId: "device001", + CoderDesktopVersion: "0.24.8", }, }, }) resp = r errCh <- err }() - - // Then: `NewConn` is called - opts := testutil.RequireReceive(ctx, t, client.optsCh) - require.Equal(t, http.Header{ - "X-Test-Header": {"test"}, - codersdk.CoderDesktopTelemetryHeader: {string(telemetryJSON)}, - }, opts.Headers) - require.False(t, opts.UseSoftNetIsolation) // the default is false - testutil.RequireSend(ctx, t, client.connCh, conn) - + // Then: `NewConn` is called, + testutil.RequireSend(ctx, t, client.ch, conn) // And: a response is received - err = testutil.TryReceive(ctx, t, errCh) + err := testutil.TryReceive(ctx, t, errCh) require.NoError(t, err) _, ok := resp.Msg.(*TunnelMessage_Start) require.True(t, ok) @@ -234,7 +197,7 @@ func TestTunnel_PeerUpdate(t *testing.T) { wsID1 := uuid.UUID{1} wsID2 := uuid.UUID{2} - client := newFakeClientWithOptsCh(ctx, t) + client := newFakeClient(ctx, t) conn := newFakeConn(tailnet.WorkspaceUpdate{ UpsertedWorkspaces: []*tailnet.Workspace{ { @@ -248,28 +211,22 @@ func TestTunnel_PeerUpdate(t *testing.T) { tun, mgr := setupTunnel(t, ctx, client, quartz.NewMock(t)) - // When: we start the tunnel errCh := make(chan error, 1) var resp *TunnelMessage go func() { r, err := mgr.unaryRPC(ctx, &ManagerMessage{ Msg: &ManagerMessage_Start{ Start: &StartRequest{ - TunnelFileDescriptor: 2, - TunnelUseSoftNetIsolation: true, - CoderUrl: "https://coder.example.com", - ApiToken: "fakeToken", + TunnelFileDescriptor: 2, + CoderUrl: "https://coder.example.com", + ApiToken: "fakeToken", }, }, }) resp = r errCh <- err }() - - // Then: `NewConn` is called - opts := testutil.RequireReceive(ctx, t, client.optsCh) - require.True(t, opts.UseSoftNetIsolation) - testutil.RequireSend(ctx, t, client.connCh, conn) + testutil.RequireSend(ctx, t, client.ch, conn) err := testutil.TryReceive(ctx, t, errCh) require.NoError(t, err) _, ok := resp.Msg.(*TunnelMessage_Start) @@ -334,7 +291,7 @@ func TestTunnel_NetworkSettings(t *testing.T) { resp = r errCh <- err }() - testutil.RequireSend(ctx, t, client.connCh, conn) + testutil.RequireSend(ctx, t, client.ch, conn) err := testutil.TryReceive(ctx, t, errCh) require.NoError(t, err) _, ok := resp.Msg.(*TunnelMessage_Start) @@ -475,7 +432,7 @@ func TestTunnel_sendAgentUpdate(t *testing.T) { resp = r errCh <- err }() - testutil.RequireSend(ctx, t, client.connCh, conn) + testutil.RequireSend(ctx, t, client.ch, conn) err := testutil.TryReceive(ctx, t, errCh) require.NoError(t, err) _, ok := resp.Msg.(*TunnelMessage_Start) @@ -646,7 +603,7 @@ func TestTunnel_sendAgentUpdateReconnect(t *testing.T) { resp = r errCh <- err }() - testutil.RequireSend(ctx, t, client.connCh, conn) + testutil.RequireSend(ctx, t, client.ch, conn) err := testutil.TryReceive(ctx, t, errCh) require.NoError(t, err) _, ok := resp.Msg.(*TunnelMessage_Start) @@ -746,7 +703,7 @@ func TestTunnel_sendAgentUpdateWorkspaceReconnect(t *testing.T) { resp = r errCh <- err }() - testutil.RequireSend(ctx, t, client.connCh, conn) + testutil.RequireSend(ctx, t, client.ch, conn) err := testutil.TryReceive(ctx, t, errCh) require.NoError(t, err) _, ok := resp.Msg.(*TunnelMessage_Start) @@ -849,7 +806,7 @@ func TestTunnel_slowPing(t *testing.T) { resp = r errCh <- err }() - testutil.RequireSend(ctx, t, client.connCh, conn) + testutil.RequireSend(ctx, t, client.ch, conn) err := testutil.TryReceive(ctx, t, errCh) require.NoError(t, err) _, ok := resp.Msg.(*TunnelMessage_Start) @@ -938,7 +895,7 @@ func TestTunnel_stopMidPing(t *testing.T) { resp = r errCh <- err }() - testutil.RequireSend(ctx, t, client.connCh, conn) + testutil.RequireSend(ctx, t, client.ch, conn) err := testutil.TryReceive(ctx, t, errCh) require.NoError(t, err) _, ok := resp.Msg.(*TunnelMessage_Start) diff --git a/vpn/version.go b/vpn/version.go index b7bf1448a2c2e..2bf815e903e29 100644 --- a/vpn/version.go +++ b/vpn/version.go @@ -23,9 +23,7 @@ var CurrentSupportedVersions = RPCVersionList{ // - preferred_derp: The server that DERP relayed connections are // using, if they're not using P2P. // - preferred_derp_latency: The latency to the preferred DERP - // 1.3 adds: - // - tunnel_use_soft_net_isolation to the StartRequest - {Major: 1, Minor: 3}, + {Major: 1, Minor: 2}, }, } diff --git a/vpn/vpn.pb.go b/vpn/vpn.pb.go index 8e08a453acdc3..fbf5ce303fa35 100644 --- a/vpn/vpn.pb.go +++ b/vpn/vpn.pb.go @@ -1375,11 +1375,10 @@ type StartRequest struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - TunnelFileDescriptor int32 `protobuf:"varint,1,opt,name=tunnel_file_descriptor,json=tunnelFileDescriptor,proto3" json:"tunnel_file_descriptor,omitempty"` - TunnelUseSoftNetIsolation bool `protobuf:"varint,8,opt,name=tunnel_use_soft_net_isolation,json=tunnelUseSoftNetIsolation,proto3" json:"tunnel_use_soft_net_isolation,omitempty"` - CoderUrl string `protobuf:"bytes,2,opt,name=coder_url,json=coderUrl,proto3" json:"coder_url,omitempty"` - ApiToken string `protobuf:"bytes,3,opt,name=api_token,json=apiToken,proto3" json:"api_token,omitempty"` - Headers []*StartRequest_Header `protobuf:"bytes,4,rep,name=headers,proto3" json:"headers,omitempty"` + TunnelFileDescriptor int32 `protobuf:"varint,1,opt,name=tunnel_file_descriptor,json=tunnelFileDescriptor,proto3" json:"tunnel_file_descriptor,omitempty"` + CoderUrl string `protobuf:"bytes,2,opt,name=coder_url,json=coderUrl,proto3" json:"coder_url,omitempty"` + ApiToken string `protobuf:"bytes,3,opt,name=api_token,json=apiToken,proto3" json:"api_token,omitempty"` + Headers []*StartRequest_Header `protobuf:"bytes,4,rep,name=headers,proto3" json:"headers,omitempty"` // Device ID from Coder Desktop DeviceId string `protobuf:"bytes,5,opt,name=device_id,json=deviceId,proto3" json:"device_id,omitempty"` // Device OS from Coder Desktop @@ -1427,13 +1426,6 @@ func (x *StartRequest) GetTunnelFileDescriptor() int32 { return 0 } -func (x *StartRequest) GetTunnelUseSoftNetIsolation() bool { - if x != nil { - return x.TunnelUseSoftNetIsolation - } - return false -} - func (x *StartRequest) GetCoderUrl() string { if x != nil { return x.CoderUrl @@ -2562,86 +2554,82 @@ var file_vpn_vpn_proto_rawDesc = []byte{ 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x12, 0x23, 0x0a, 0x0d, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x4d, 0x65, 0x73, 0x73, 0x61, - 0x67, 0x65, 0x22, 0x96, 0x03, 0x0a, 0x0c, 0x53, 0x74, 0x61, 0x72, 0x74, 0x52, 0x65, 0x71, 0x75, + 0x67, 0x65, 0x22, 0xd4, 0x02, 0x0a, 0x0c, 0x53, 0x74, 0x61, 0x72, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x34, 0x0a, 0x16, 0x74, 0x75, 0x6e, 0x6e, 0x65, 0x6c, 0x5f, 0x66, 0x69, 0x6c, 0x65, 0x5f, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x14, 0x74, 0x75, 0x6e, 0x6e, 0x65, 0x6c, 0x46, 0x69, 0x6c, 0x65, 0x44, - 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x6f, 0x72, 0x12, 0x40, 0x0a, 0x1d, 0x74, 0x75, 0x6e, - 0x6e, 0x65, 0x6c, 0x5f, 0x75, 0x73, 0x65, 0x5f, 0x73, 0x6f, 0x66, 0x74, 0x5f, 0x6e, 0x65, 0x74, - 0x5f, 0x69, 0x73, 0x6f, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, - 0x52, 0x19, 0x74, 0x75, 0x6e, 0x6e, 0x65, 0x6c, 0x55, 0x73, 0x65, 0x53, 0x6f, 0x66, 0x74, 0x4e, - 0x65, 0x74, 0x49, 0x73, 0x6f, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1b, 0x0a, 0x09, 0x63, - 0x6f, 0x64, 0x65, 0x72, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, - 0x63, 0x6f, 0x64, 0x65, 0x72, 0x55, 0x72, 0x6c, 0x12, 0x1b, 0x0a, 0x09, 0x61, 0x70, 0x69, 0x5f, - 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x61, 0x70, 0x69, - 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x32, 0x0a, 0x07, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, - 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x76, 0x70, 0x6e, 0x2e, 0x53, 0x74, 0x61, - 0x72, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, - 0x52, 0x07, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x12, 0x1b, 0x0a, 0x09, 0x64, 0x65, 0x76, - 0x69, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x64, 0x65, - 0x76, 0x69, 0x63, 0x65, 0x49, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, - 0x5f, 0x6f, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x64, 0x65, 0x76, 0x69, 0x63, - 0x65, 0x4f, 0x73, 0x12, 0x32, 0x0a, 0x15, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x5f, 0x64, 0x65, 0x73, - 0x6b, 0x74, 0x6f, 0x70, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x07, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x13, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x44, 0x65, 0x73, 0x6b, 0x74, 0x6f, 0x70, - 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x1a, 0x32, 0x0a, 0x06, 0x48, 0x65, 0x61, 0x64, 0x65, - 0x72, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x4e, 0x0a, 0x0d, 0x53, - 0x74, 0x61, 0x72, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x18, 0x0a, 0x07, - 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x73, - 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x12, 0x23, 0x0a, 0x0d, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, - 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x65, - 0x72, 0x72, 0x6f, 0x72, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x7a, 0x0a, 0x1d, 0x53, - 0x74, 0x61, 0x72, 0x74, 0x50, 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, 0x44, 0x6f, 0x77, 0x6e, - 0x6c, 0x6f, 0x61, 0x64, 0x50, 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, 0x12, 0x23, 0x0a, 0x0d, - 0x62, 0x79, 0x74, 0x65, 0x73, 0x5f, 0x77, 0x72, 0x69, 0x74, 0x74, 0x65, 0x6e, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x04, 0x52, 0x0c, 0x62, 0x79, 0x74, 0x65, 0x73, 0x57, 0x72, 0x69, 0x74, 0x74, 0x65, - 0x6e, 0x12, 0x24, 0x0a, 0x0b, 0x62, 0x79, 0x74, 0x65, 0x73, 0x5f, 0x74, 0x6f, 0x74, 0x61, 0x6c, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x48, 0x00, 0x52, 0x0a, 0x62, 0x79, 0x74, 0x65, 0x73, 0x54, - 0x6f, 0x74, 0x61, 0x6c, 0x88, 0x01, 0x01, 0x42, 0x0e, 0x0a, 0x0c, 0x5f, 0x62, 0x79, 0x74, 0x65, - 0x73, 0x5f, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x22, 0xaa, 0x01, 0x0a, 0x0d, 0x53, 0x74, 0x61, 0x72, - 0x74, 0x50, 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, 0x12, 0x2d, 0x0a, 0x05, 0x73, 0x74, 0x61, - 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x17, 0x2e, 0x76, 0x70, 0x6e, 0x2e, 0x53, - 0x74, 0x61, 0x72, 0x74, 0x50, 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, 0x53, 0x74, 0x61, 0x67, - 0x65, 0x52, 0x05, 0x73, 0x74, 0x61, 0x67, 0x65, 0x12, 0x54, 0x0a, 0x11, 0x64, 0x6f, 0x77, 0x6e, - 0x6c, 0x6f, 0x61, 0x64, 0x5f, 0x70, 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x76, 0x70, 0x6e, 0x2e, 0x53, 0x74, 0x61, 0x72, 0x74, 0x50, - 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, 0x44, 0x6f, 0x77, 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x50, - 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, 0x48, 0x00, 0x52, 0x10, 0x64, 0x6f, 0x77, 0x6e, 0x6c, - 0x6f, 0x61, 0x64, 0x50, 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, 0x88, 0x01, 0x01, 0x42, 0x14, - 0x0a, 0x12, 0x5f, 0x64, 0x6f, 0x77, 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x5f, 0x70, 0x72, 0x6f, 0x67, - 0x72, 0x65, 0x73, 0x73, 0x22, 0x0d, 0x0a, 0x0b, 0x53, 0x74, 0x6f, 0x70, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x22, 0x4d, 0x0a, 0x0c, 0x53, 0x74, 0x6f, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x12, 0x23, 0x0a, - 0x0d, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x4d, 0x65, 0x73, 0x73, 0x61, - 0x67, 0x65, 0x22, 0x0f, 0x0a, 0x0d, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x22, 0xe4, 0x01, 0x0a, 0x06, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x33, - 0x0a, 0x09, 0x6c, 0x69, 0x66, 0x65, 0x63, 0x79, 0x63, 0x6c, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x0e, 0x32, 0x15, 0x2e, 0x76, 0x70, 0x6e, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x2e, 0x4c, - 0x69, 0x66, 0x65, 0x63, 0x79, 0x63, 0x6c, 0x65, 0x52, 0x09, 0x6c, 0x69, 0x66, 0x65, 0x63, 0x79, - 0x63, 0x6c, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x6d, 0x65, 0x73, - 0x73, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x65, 0x72, 0x72, 0x6f, - 0x72, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x30, 0x0a, 0x0b, 0x70, 0x65, 0x65, 0x72, - 0x5f, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0f, 0x2e, - 0x76, 0x70, 0x6e, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x0a, - 0x70, 0x65, 0x65, 0x72, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x22, 0x4e, 0x0a, 0x09, 0x4c, 0x69, - 0x66, 0x65, 0x63, 0x79, 0x63, 0x6c, 0x65, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, - 0x57, 0x4e, 0x10, 0x00, 0x12, 0x0c, 0x0a, 0x08, 0x53, 0x54, 0x41, 0x52, 0x54, 0x49, 0x4e, 0x47, - 0x10, 0x01, 0x12, 0x0b, 0x0a, 0x07, 0x53, 0x54, 0x41, 0x52, 0x54, 0x45, 0x44, 0x10, 0x02, 0x12, - 0x0c, 0x0a, 0x08, 0x53, 0x54, 0x4f, 0x50, 0x50, 0x49, 0x4e, 0x47, 0x10, 0x03, 0x12, 0x0b, 0x0a, - 0x07, 0x53, 0x54, 0x4f, 0x50, 0x50, 0x45, 0x44, 0x10, 0x04, 0x2a, 0x47, 0x0a, 0x12, 0x53, 0x74, - 0x61, 0x72, 0x74, 0x50, 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, 0x53, 0x74, 0x61, 0x67, 0x65, - 0x12, 0x10, 0x0a, 0x0c, 0x49, 0x6e, 0x69, 0x74, 0x69, 0x61, 0x6c, 0x69, 0x7a, 0x69, 0x6e, 0x67, - 0x10, 0x00, 0x12, 0x0f, 0x0a, 0x0b, 0x44, 0x6f, 0x77, 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x69, 0x6e, - 0x67, 0x10, 0x01, 0x12, 0x0e, 0x0a, 0x0a, 0x46, 0x69, 0x6e, 0x61, 0x6c, 0x69, 0x7a, 0x69, 0x6e, - 0x67, 0x10, 0x02, 0x42, 0x39, 0x5a, 0x1d, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, - 0x6d, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x76, 0x32, - 0x2f, 0x76, 0x70, 0x6e, 0xaa, 0x02, 0x17, 0x43, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x44, 0x65, 0x73, - 0x6b, 0x74, 0x6f, 0x70, 0x2e, 0x56, 0x70, 0x6e, 0x2e, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x6f, 0x72, 0x12, 0x1b, 0x0a, 0x09, 0x63, 0x6f, 0x64, + 0x65, 0x72, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x63, 0x6f, + 0x64, 0x65, 0x72, 0x55, 0x72, 0x6c, 0x12, 0x1b, 0x0a, 0x09, 0x61, 0x70, 0x69, 0x5f, 0x74, 0x6f, + 0x6b, 0x65, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x61, 0x70, 0x69, 0x54, 0x6f, + 0x6b, 0x65, 0x6e, 0x12, 0x32, 0x0a, 0x07, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x18, 0x04, + 0x20, 0x03, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x76, 0x70, 0x6e, 0x2e, 0x53, 0x74, 0x61, 0x72, 0x74, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x52, 0x07, + 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x12, 0x1b, 0x0a, 0x09, 0x64, 0x65, 0x76, 0x69, 0x63, + 0x65, 0x5f, 0x69, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x64, 0x65, 0x76, 0x69, + 0x63, 0x65, 0x49, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x6f, + 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x4f, + 0x73, 0x12, 0x32, 0x0a, 0x15, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x5f, 0x64, 0x65, 0x73, 0x6b, 0x74, + 0x6f, 0x70, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x13, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x44, 0x65, 0x73, 0x6b, 0x74, 0x6f, 0x70, 0x56, 0x65, + 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x1a, 0x32, 0x0a, 0x06, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x12, + 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, + 0x61, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x4e, 0x0a, 0x0d, 0x53, 0x74, 0x61, + 0x72, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x75, + 0x63, 0x63, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x73, 0x75, 0x63, + 0x63, 0x65, 0x73, 0x73, 0x12, 0x23, 0x0a, 0x0d, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x6d, 0x65, + 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x65, 0x72, 0x72, + 0x6f, 0x72, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x7a, 0x0a, 0x1d, 0x53, 0x74, 0x61, + 0x72, 0x74, 0x50, 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, 0x44, 0x6f, 0x77, 0x6e, 0x6c, 0x6f, + 0x61, 0x64, 0x50, 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, 0x12, 0x23, 0x0a, 0x0d, 0x62, 0x79, + 0x74, 0x65, 0x73, 0x5f, 0x77, 0x72, 0x69, 0x74, 0x74, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x04, 0x52, 0x0c, 0x62, 0x79, 0x74, 0x65, 0x73, 0x57, 0x72, 0x69, 0x74, 0x74, 0x65, 0x6e, 0x12, + 0x24, 0x0a, 0x0b, 0x62, 0x79, 0x74, 0x65, 0x73, 0x5f, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x04, 0x48, 0x00, 0x52, 0x0a, 0x62, 0x79, 0x74, 0x65, 0x73, 0x54, 0x6f, 0x74, + 0x61, 0x6c, 0x88, 0x01, 0x01, 0x42, 0x0e, 0x0a, 0x0c, 0x5f, 0x62, 0x79, 0x74, 0x65, 0x73, 0x5f, + 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x22, 0xaa, 0x01, 0x0a, 0x0d, 0x53, 0x74, 0x61, 0x72, 0x74, 0x50, + 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, 0x12, 0x2d, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x67, 0x65, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x17, 0x2e, 0x76, 0x70, 0x6e, 0x2e, 0x53, 0x74, 0x61, + 0x72, 0x74, 0x50, 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, 0x53, 0x74, 0x61, 0x67, 0x65, 0x52, + 0x05, 0x73, 0x74, 0x61, 0x67, 0x65, 0x12, 0x54, 0x0a, 0x11, 0x64, 0x6f, 0x77, 0x6e, 0x6c, 0x6f, + 0x61, 0x64, 0x5f, 0x70, 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x22, 0x2e, 0x76, 0x70, 0x6e, 0x2e, 0x53, 0x74, 0x61, 0x72, 0x74, 0x50, 0x72, 0x6f, + 0x67, 0x72, 0x65, 0x73, 0x73, 0x44, 0x6f, 0x77, 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x50, 0x72, 0x6f, + 0x67, 0x72, 0x65, 0x73, 0x73, 0x48, 0x00, 0x52, 0x10, 0x64, 0x6f, 0x77, 0x6e, 0x6c, 0x6f, 0x61, + 0x64, 0x50, 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, 0x88, 0x01, 0x01, 0x42, 0x14, 0x0a, 0x12, + 0x5f, 0x64, 0x6f, 0x77, 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x5f, 0x70, 0x72, 0x6f, 0x67, 0x72, 0x65, + 0x73, 0x73, 0x22, 0x0d, 0x0a, 0x0b, 0x53, 0x74, 0x6f, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x22, 0x4d, 0x0a, 0x0c, 0x53, 0x74, 0x6f, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x08, 0x52, 0x07, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x12, 0x23, 0x0a, 0x0d, 0x65, + 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x0c, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, + 0x22, 0x0f, 0x0a, 0x0d, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x22, 0xe4, 0x01, 0x0a, 0x06, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x33, 0x0a, 0x09, + 0x6c, 0x69, 0x66, 0x65, 0x63, 0x79, 0x63, 0x6c, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, + 0x15, 0x2e, 0x76, 0x70, 0x6e, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x2e, 0x4c, 0x69, 0x66, + 0x65, 0x63, 0x79, 0x63, 0x6c, 0x65, 0x52, 0x09, 0x6c, 0x69, 0x66, 0x65, 0x63, 0x79, 0x63, 0x6c, + 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, + 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x4d, + 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x30, 0x0a, 0x0b, 0x70, 0x65, 0x65, 0x72, 0x5f, 0x75, + 0x70, 0x64, 0x61, 0x74, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x76, 0x70, + 0x6e, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x0a, 0x70, 0x65, + 0x65, 0x72, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x22, 0x4e, 0x0a, 0x09, 0x4c, 0x69, 0x66, 0x65, + 0x63, 0x79, 0x63, 0x6c, 0x65, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, + 0x10, 0x00, 0x12, 0x0c, 0x0a, 0x08, 0x53, 0x54, 0x41, 0x52, 0x54, 0x49, 0x4e, 0x47, 0x10, 0x01, + 0x12, 0x0b, 0x0a, 0x07, 0x53, 0x54, 0x41, 0x52, 0x54, 0x45, 0x44, 0x10, 0x02, 0x12, 0x0c, 0x0a, + 0x08, 0x53, 0x54, 0x4f, 0x50, 0x50, 0x49, 0x4e, 0x47, 0x10, 0x03, 0x12, 0x0b, 0x0a, 0x07, 0x53, + 0x54, 0x4f, 0x50, 0x50, 0x45, 0x44, 0x10, 0x04, 0x2a, 0x47, 0x0a, 0x12, 0x53, 0x74, 0x61, 0x72, + 0x74, 0x50, 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, 0x53, 0x74, 0x61, 0x67, 0x65, 0x12, 0x10, + 0x0a, 0x0c, 0x49, 0x6e, 0x69, 0x74, 0x69, 0x61, 0x6c, 0x69, 0x7a, 0x69, 0x6e, 0x67, 0x10, 0x00, + 0x12, 0x0f, 0x0a, 0x0b, 0x44, 0x6f, 0x77, 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x69, 0x6e, 0x67, 0x10, + 0x01, 0x12, 0x0e, 0x0a, 0x0a, 0x46, 0x69, 0x6e, 0x61, 0x6c, 0x69, 0x7a, 0x69, 0x6e, 0x67, 0x10, + 0x02, 0x42, 0x39, 0x5a, 0x1d, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, + 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x76, 0x32, 0x2f, 0x76, + 0x70, 0x6e, 0xaa, 0x02, 0x17, 0x43, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x44, 0x65, 0x73, 0x6b, 0x74, + 0x6f, 0x70, 0x2e, 0x56, 0x70, 0x6e, 0x2e, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/vpn/vpn.proto b/vpn/vpn.proto index 61c9978cdcad6..357a2b91b12fb 100644 --- a/vpn/vpn.proto +++ b/vpn/vpn.proto @@ -214,7 +214,6 @@ message NetworkSettingsResponse { // StartResponse. message StartRequest { int32 tunnel_file_descriptor = 1; - bool tunnel_use_soft_net_isolation = 8; string coder_url = 2; string api_token = 3; // Additional HTTP headers added to all requests From b666d52171ddd29e41b09c1d3a6f91685843b425 Mon Sep 17 00:00:00 2001 From: Hugo Dutka Date: Tue, 29 Jul 2025 16:20:02 +0200 Subject: [PATCH 2/5] feat(codersdk/toolsdk): add MCP workspace bash background parameter (#19034) Addresses coder/internal#820 --------- Signed-off-by: Thomas Kosiewski Co-authored-by: Thomas Kosiewski --- codersdk/toolsdk/bash.go | 63 +++++++++---- codersdk/toolsdk/bash_test.go | 150 +++++++++++++++++++++++++++++-- codersdk/toolsdk/toolsdk_test.go | 24 ++--- 3 files changed, 202 insertions(+), 35 deletions(-) diff --git a/codersdk/toolsdk/bash.go b/codersdk/toolsdk/bash.go index 5fb15843f1bf1..037227337bfc9 100644 --- a/codersdk/toolsdk/bash.go +++ b/codersdk/toolsdk/bash.go @@ -21,9 +21,10 @@ import ( ) type WorkspaceBashArgs struct { - Workspace string `json:"workspace"` - Command string `json:"command"` - TimeoutMs int `json:"timeout_ms,omitempty"` + Workspace string `json:"workspace"` + Command string `json:"command"` + TimeoutMs int `json:"timeout_ms,omitempty"` + Background bool `json:"background,omitempty"` } type WorkspaceBashResult struct { @@ -50,9 +51,13 @@ The workspace parameter supports various formats: The timeout_ms parameter specifies the command timeout in milliseconds (defaults to 60000ms, maximum of 300000ms). If the command times out, all output captured up to that point is returned with a cancellation message. +For background commands (background: true), output is captured until the timeout is reached, then the command +continues running in the background. The captured output is returned as the result. + Examples: - workspace: "my-workspace", command: "ls -la" - workspace: "john/dev-env", command: "git status", timeout_ms: 30000 +- workspace: "my-workspace", command: "npm run dev", background: true, timeout_ms: 10000 - workspace: "my-workspace.main", command: "docker ps"`, Schema: aisdk.Schema{ Properties: map[string]any{ @@ -70,6 +75,10 @@ Examples: "default": 60000, "minimum": 1, }, + "background": map[string]any{ + "type": "boolean", + "description": "Whether to run the command in the background. Output is captured until timeout, then the command continues running in the background.", + }, }, Required: []string{"workspace", "command"}, }, @@ -137,23 +146,35 @@ Examples: // Set default timeout if not specified (60 seconds) timeoutMs := args.TimeoutMs + defaultTimeoutMs := 60000 if timeoutMs <= 0 { - timeoutMs = 60000 + timeoutMs = defaultTimeoutMs + } + command := args.Command + if args.Background { + // For background commands, use nohup directly to ensure they survive SSH session + // termination. This captures output normally but allows the process to continue + // running even after the SSH connection closes. + command = fmt.Sprintf("nohup %s &1", args.Command) } - // Create context with timeout - ctx, cancel = context.WithTimeout(ctx, time.Duration(timeoutMs)*time.Millisecond) - defer cancel() + // Create context with command timeout (replace the broader MCP timeout) + commandCtx, commandCancel := context.WithTimeout(ctx, time.Duration(timeoutMs)*time.Millisecond) + defer commandCancel() // Execute command with timeout handling - output, err := executeCommandWithTimeout(ctx, session, args.Command) + output, err := executeCommandWithTimeout(commandCtx, session, command) outputStr := strings.TrimSpace(string(output)) // Handle command execution results if err != nil { // Check if the command timed out - if errors.Is(context.Cause(ctx), context.DeadlineExceeded) { - outputStr += "\nCommand canceled due to timeout" + if errors.Is(context.Cause(commandCtx), context.DeadlineExceeded) { + if args.Background { + outputStr += "\nCommand continues running in background" + } else { + outputStr += "\nCommand canceled due to timeout" + } return WorkspaceBashResult{ Output: outputStr, ExitCode: 124, @@ -387,21 +408,27 @@ func executeCommandWithTimeout(ctx context.Context, session *gossh.Session, comm return safeWriter.Bytes(), err case <-ctx.Done(): // Context was canceled (timeout or other cancellation) - // Close the session to stop the command - _ = session.Close() + // Close the session to stop the command, but handle errors gracefully + closeErr := session.Close() - // Give a brief moment to collect any remaining output - timer := time.NewTimer(50 * time.Millisecond) + // Give a brief moment to collect any remaining output and for goroutines to finish + timer := time.NewTimer(100 * time.Millisecond) defer timer.Stop() select { case <-timer.C: // Timer expired, return what we have + break case err := <-done: // Command finished during grace period - return safeWriter.Bytes(), err + if closeErr == nil { + return safeWriter.Bytes(), err + } + // If session close failed, prioritize the context error + break } + // Return the collected output with the context error return safeWriter.Bytes(), context.Cause(ctx) } } @@ -421,5 +448,9 @@ func (sw *syncWriter) Write(p []byte) (n int, err error) { func (sw *syncWriter) Bytes() []byte { sw.mu.Lock() defer sw.mu.Unlock() - return sw.w.Bytes() + // Return a copy to prevent race conditions with the underlying buffer + b := sw.w.Bytes() + result := make([]byte, len(b)) + copy(result, b) + return result } diff --git a/codersdk/toolsdk/bash_test.go b/codersdk/toolsdk/bash_test.go index 53ac480039278..0656b2d8786e6 100644 --- a/codersdk/toolsdk/bash_test.go +++ b/codersdk/toolsdk/bash_test.go @@ -9,6 +9,7 @@ import ( "github.com/coder/coder/v2/agent/agenttest" "github.com/coder/coder/v2/coderd/coderdtest" "github.com/coder/coder/v2/codersdk/toolsdk" + "github.com/coder/coder/v2/testutil" ) func TestWorkspaceBash(t *testing.T) { @@ -174,8 +175,6 @@ func TestWorkspaceBashTimeout(t *testing.T) { // Test that the TimeoutMs field can be set and read correctly args := toolsdk.WorkspaceBashArgs{ - Workspace: "test-workspace", - Command: "echo test", TimeoutMs: 0, // Should default to 60000 in handler } @@ -192,8 +191,6 @@ func TestWorkspaceBashTimeout(t *testing.T) { // Test that negative values can be set and will be handled by the default logic args := toolsdk.WorkspaceBashArgs{ - Workspace: "test-workspace", - Command: "echo test", TimeoutMs: -100, } @@ -279,7 +276,7 @@ func TestWorkspaceBashTimeoutIntegration(t *testing.T) { TimeoutMs: 2000, // 2 seconds timeout - should timeout after first echo } - result, err := toolsdk.WorkspaceBash.Handler(t.Context(), deps, args) + result, err := testTool(t, toolsdk.WorkspaceBash, deps, args) // Should not error (timeout is handled gracefully) require.NoError(t, err) @@ -313,7 +310,6 @@ func TestWorkspaceBashTimeoutIntegration(t *testing.T) { deps, err := toolsdk.NewDeps(client) require.NoError(t, err) - ctx := context.Background() args := toolsdk.WorkspaceBashArgs{ Workspace: workspace.Name, @@ -321,7 +317,8 @@ func TestWorkspaceBashTimeoutIntegration(t *testing.T) { TimeoutMs: 5000, // 5 second timeout - plenty of time } - result, err := toolsdk.WorkspaceBash.Handler(ctx, deps, args) + // Use testTool to register the tool as tested and satisfy coverage validation + result, err := testTool(t, toolsdk.WorkspaceBash, deps, args) // Should not error require.NoError(t, err) @@ -338,3 +335,142 @@ func TestWorkspaceBashTimeoutIntegration(t *testing.T) { require.NotContains(t, result.Output, "Command canceled due to timeout") }) } + +func TestWorkspaceBashBackgroundIntegration(t *testing.T) { + t.Parallel() + + t.Run("BackgroundCommandCapturesOutput", func(t *testing.T) { + t.Parallel() + + client, workspace, agentToken := setupWorkspaceForAgent(t) + + // Start the agent and wait for it to be fully ready + _ = agenttest.New(t, client.URL, agentToken) + + // Wait for workspace agents to be ready + coderdtest.NewWorkspaceAgentWaiter(t, client, workspace.ID).Wait() + + deps, err := toolsdk.NewDeps(client) + require.NoError(t, err) + + args := toolsdk.WorkspaceBashArgs{ + Workspace: workspace.Name, + Command: `echo "started" && sleep 60 && echo "completed"`, // Command that would take 60+ seconds + Background: true, // Run in background + TimeoutMs: 2000, // 2 second timeout + } + + result, err := testTool(t, toolsdk.WorkspaceBash, deps, args) + + // Should not error + require.NoError(t, err) + + t.Logf("Background result: exitCode=%d, output=%q", result.ExitCode, result.Output) + + // Should have exit code 124 (timeout) since command times out + require.Equal(t, 124, result.ExitCode) + + // Should capture output up to timeout point + require.Contains(t, result.Output, "started", "Should contain output captured before timeout") + + // Should NOT contain the second echo (it never executed due to timeout) + require.NotContains(t, result.Output, "completed", "Should not contain output after timeout") + + // Should contain background continuation message + require.Contains(t, result.Output, "Command continues running in background") + }) + + t.Run("BackgroundVsNormalExecution", func(t *testing.T) { + t.Parallel() + + client, workspace, agentToken := setupWorkspaceForAgent(t) + + // Start the agent and wait for it to be fully ready + _ = agenttest.New(t, client.URL, agentToken) + + // Wait for workspace agents to be ready + coderdtest.NewWorkspaceAgentWaiter(t, client, workspace.ID).Wait() + + deps, err := toolsdk.NewDeps(client) + require.NoError(t, err) + + // First run the same command in normal mode + normalArgs := toolsdk.WorkspaceBashArgs{ + Workspace: workspace.Name, + Command: `echo "hello world"`, + Background: false, + } + + normalResult, err := toolsdk.WorkspaceBash.Handler(t.Context(), deps, normalArgs) + require.NoError(t, err) + + // Normal mode should return the actual output + require.Equal(t, 0, normalResult.ExitCode) + require.Equal(t, "hello world", normalResult.Output) + + // Now run the same command in background mode + backgroundArgs := toolsdk.WorkspaceBashArgs{ + Workspace: workspace.Name, + Command: `echo "hello world"`, + Background: true, + } + + backgroundResult, err := testTool(t, toolsdk.WorkspaceBash, deps, backgroundArgs) + require.NoError(t, err) + + t.Logf("Normal result: %q", normalResult.Output) + t.Logf("Background result: %q", backgroundResult.Output) + + // Background mode should also return the actual output since command completes quickly + require.Equal(t, 0, backgroundResult.ExitCode) + require.Equal(t, "hello world", backgroundResult.Output) + }) + + t.Run("BackgroundCommandContinuesAfterTimeout", func(t *testing.T) { + t.Parallel() + + client, workspace, agentToken := setupWorkspaceForAgent(t) + + // Start the agent and wait for it to be fully ready + _ = agenttest.New(t, client.URL, agentToken) + + // Wait for workspace agents to be ready + coderdtest.NewWorkspaceAgentWaiter(t, client, workspace.ID).Wait() + + deps, err := toolsdk.NewDeps(client) + require.NoError(t, err) + + args := toolsdk.WorkspaceBashArgs{ + Workspace: workspace.Name, + Command: `echo "started" && sleep 4 && echo "done" > /tmp/bg-test-done`, // Command that will timeout but continue + TimeoutMs: 2000, // 2000ms timeout (shorter than command duration) + Background: true, // Run in background + } + + result, err := testTool(t, toolsdk.WorkspaceBash, deps, args) + + // Should not error but should timeout + require.NoError(t, err) + + t.Logf("Background with timeout result: exitCode=%d, output=%q", result.ExitCode, result.Output) + + // Should have timeout exit code + require.Equal(t, 124, result.ExitCode) + + // Should capture output before timeout + require.Contains(t, result.Output, "started", "Should contain output captured before timeout") + + // Should contain background continuation message + require.Contains(t, result.Output, "Command continues running in background") + + // Wait for the background command to complete (even though SSH session timed out) + require.Eventually(t, func() bool { + checkArgs := toolsdk.WorkspaceBashArgs{ + Workspace: workspace.Name, + Command: `cat /tmp/bg-test-done 2>/dev/null || echo "not found"`, + } + checkResult, err := toolsdk.WorkspaceBash.Handler(t.Context(), deps, checkArgs) + return err == nil && checkResult.Output == "done" + }, testutil.WaitMedium, testutil.IntervalMedium, "Background command should continue running and complete after timeout") + }) +} diff --git a/codersdk/toolsdk/toolsdk_test.go b/codersdk/toolsdk/toolsdk_test.go index c201190bd3456..13e475c80609a 100644 --- a/codersdk/toolsdk/toolsdk_test.go +++ b/codersdk/toolsdk/toolsdk_test.go @@ -456,7 +456,7 @@ var testedTools sync.Map // This is to mimic how we expect external callers to use the tool. func testTool[Arg, Ret any](t *testing.T, tool toolsdk.Tool[Arg, Ret], tb toolsdk.Deps, args Arg) (Ret, error) { t.Helper() - defer func() { testedTools.Store(tool.Tool.Name, true) }() + defer func() { testedTools.Store(tool.Name, true) }() toolArgs, err := json.Marshal(args) require.NoError(t, err, "failed to marshal args") result, err := tool.Generic().Handler(t.Context(), tb, toolArgs) @@ -625,23 +625,23 @@ func TestToolSchemaFields(t *testing.T) { // Test that all tools have the required Schema fields (Properties and Required) for _, tool := range toolsdk.All { - t.Run(tool.Tool.Name, func(t *testing.T) { + t.Run(tool.Name, func(t *testing.T) { t.Parallel() // Check that Properties is not nil - require.NotNil(t, tool.Tool.Schema.Properties, - "Tool %q missing Schema.Properties", tool.Tool.Name) + require.NotNil(t, tool.Schema.Properties, + "Tool %q missing Schema.Properties", tool.Name) // Check that Required is not nil - require.NotNil(t, tool.Tool.Schema.Required, - "Tool %q missing Schema.Required", tool.Tool.Name) + require.NotNil(t, tool.Schema.Required, + "Tool %q missing Schema.Required", tool.Name) // Ensure Properties has entries for all required fields - for _, requiredField := range tool.Tool.Schema.Required { - _, exists := tool.Tool.Schema.Properties[requiredField] + for _, requiredField := range tool.Schema.Required { + _, exists := tool.Schema.Properties[requiredField] require.True(t, exists, "Tool %q requires field %q but it is not defined in Properties", - tool.Tool.Name, requiredField) + tool.Name, requiredField) } }) } @@ -652,7 +652,7 @@ func TestToolSchemaFields(t *testing.T) { func TestMain(m *testing.M) { // Initialize testedTools for _, tool := range toolsdk.All { - testedTools.Store(tool.Tool.Name, false) + testedTools.Store(tool.Name, false) } code := m.Run() @@ -660,8 +660,8 @@ func TestMain(m *testing.M) { // Ensure all tools have been tested var untested []string for _, tool := range toolsdk.All { - if tested, ok := testedTools.Load(tool.Tool.Name); !ok || !tested.(bool) { - untested = append(untested, tool.Tool.Name) + if tested, ok := testedTools.Load(tool.Name); !ok || !tested.(bool) { + untested = append(untested, tool.Name) } } From 29486f9d4e146df282fabeca291867691a109310 Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Tue, 29 Jul 2025 16:23:57 +0200 Subject: [PATCH 3/5] fix: fix e2e tests (#19076) Closes https://github.com/coder/internal/issues/824 --- site/e2e/helpers.ts | 51 ++++++++++++++++++++++++++++++--------------- 1 file changed, 34 insertions(+), 17 deletions(-) diff --git a/site/e2e/helpers.ts b/site/e2e/helpers.ts index 768a7d477f992..e771adeab3813 100644 --- a/site/e2e/helpers.ts +++ b/site/e2e/helpers.ts @@ -127,6 +127,10 @@ export const createWorkspace = async ( const name = randomName(); await page.getByLabel("name").fill(name); + if (buildParameters.length > 0) { + await page.waitForSelector("form", { state: "visible" }); + } + await fillParameters(page, richParameters, buildParameters); if (useExternalAuth) { @@ -898,28 +902,29 @@ const fillParameters = async ( ); } - const parameterLabel = await page.waitForSelector( - `[data-testid='parameter-field-${richParameter.name}']`, - { state: "visible" }, + // Use modern locator approach instead of waitForSelector + const parameterLabel = page.getByTestId( + `parameter-field-${richParameter.name}`, ); + await expect(parameterLabel).toBeVisible(); if (richParameter.type === "bool") { - const parameterField = await parameterLabel.waitForSelector( - `[data-testid='parameter-field-bool'] .MuiRadio-root input[value='${buildParameter.value}']`, - ); + const parameterField = parameterLabel + .getByTestId("parameter-field-bool") + .locator(`.MuiRadio-root input[value='${buildParameter.value}']`); await parameterField.click(); } else if (richParameter.options.length > 0) { - const parameterField = await parameterLabel.waitForSelector( - `[data-testid='parameter-field-options'] .MuiRadio-root input[value='${buildParameter.value}']`, - ); + const parameterField = parameterLabel + .getByTestId("parameter-field-options") + .locator(`.MuiRadio-root input[value='${buildParameter.value}']`); await parameterField.click(); } else if (richParameter.type === "list(string)") { throw new Error("not implemented yet"); // FIXME } else { // text or number - const parameterField = await parameterLabel.waitForSelector( - "[data-testid='parameter-field-text'] input", - ); + const parameterField = parameterLabel + .getByTestId("parameter-field-text") + .locator("input"); await parameterField.fill(buildParameter.value); } } @@ -1217,22 +1222,34 @@ export const disableDynamicParameters = async ( waitUntil: "domcontentloaded", }); + await page.waitForSelector("form", { state: "visible" }); + // Find and uncheck the "Enable dynamic parameters" checkbox const dynamicParamsCheckbox = page.getByRole("checkbox", { name: /Enable dynamic parameters for workspace creation/, }); + await dynamicParamsCheckbox.waitFor({ state: "visible" }); + // If the checkbox is checked, uncheck it if (await dynamicParamsCheckbox.isChecked()) { await dynamicParamsCheckbox.click(); } // Save the changes - await page.getByRole("button", { name: /save/i }).click(); + const saveButton = page.getByRole("button", { name: /save/i }); + await saveButton.waitFor({ state: "visible" }); + await saveButton.click(); // Wait for the success message or page to update - await page.waitForSelector("text=Template updated successfully", { - state: "visible", - timeout: 10000, - }); + await page + .locator("[role='alert']:has-text('Template updated successfully')") + .first() + .waitFor({ + state: "visible", + timeout: 15000, + }); + + // Additional wait to ensure the changes are persisted + await page.waitForTimeout(500); }; From 812d72c5bb40965106c7f4c90bb2fd8204b6e8a0 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Tue, 29 Jul 2025 15:24:11 +0100 Subject: [PATCH 4/5] fix: sanitize app status summary (#19075) Fixes https://github.com/coder/coder/issues/18875 --- coderd/util/strings/strings.go | 40 +++++++++++++++++++++++++++++ coderd/util/strings/strings_test.go | 39 ++++++++++++++++++++++++++++ coderd/workspaceagents.go | 6 ++++- codersdk/toolsdk/toolsdk.go | 2 +- go.mod | 2 +- 5 files changed, 86 insertions(+), 3 deletions(-) diff --git a/coderd/util/strings/strings.go b/coderd/util/strings/strings.go index f416bba463bbf..49aad579e83f5 100644 --- a/coderd/util/strings/strings.go +++ b/coderd/util/strings/strings.go @@ -2,7 +2,12 @@ package strings import ( "fmt" + "strconv" "strings" + "unicode" + + "github.com/acarl005/stripansi" + "github.com/microcosm-cc/bluemonday" ) // JoinWithConjunction joins a slice of strings with commas except for the last @@ -28,3 +33,38 @@ func Truncate(s string, n int) string { } return s[:n] } + +var bmPolicy = bluemonday.StrictPolicy() + +// UISanitize sanitizes a string for display in the UI. +// The following transformations are applied, in order: +// - HTML tags are removed using bluemonday's strict policy. +// - ANSI escape codes are stripped using stripansi. +// - Consecutive backslashes are replaced with a single backslash. +// - Non-printable characters are removed. +// - Whitespace characters are replaced with spaces. +// - Multiple spaces are collapsed into a single space. +// - Leading and trailing whitespace is trimmed. +func UISanitize(in string) string { + if unq, err := strconv.Unquote(`"` + in + `"`); err == nil { + in = unq + } + in = bmPolicy.Sanitize(in) + in = stripansi.Strip(in) + var b strings.Builder + var spaceSeen bool + for _, r := range in { + if unicode.IsSpace(r) { + if !spaceSeen { + _, _ = b.WriteRune(' ') + spaceSeen = true + } + continue + } + spaceSeen = false + if unicode.IsPrint(r) { + _, _ = b.WriteRune(r) + } + } + return strings.TrimSpace(b.String()) +} diff --git a/coderd/util/strings/strings_test.go b/coderd/util/strings/strings_test.go index 5172fb08e1e69..7a20a06a25f28 100644 --- a/coderd/util/strings/strings_test.go +++ b/coderd/util/strings/strings_test.go @@ -3,6 +3,7 @@ package strings_test import ( "testing" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/coder/coder/v2/coderd/util/strings" @@ -37,3 +38,41 @@ func TestTruncate(t *testing.T) { }) } } + +func TestUISanitize(t *testing.T) { + t.Parallel() + + for _, tt := range []struct { + s string + expected string + }{ + {"normal text", "normal text"}, + {"\tfoo \r\\nbar ", "foo bar"}, + {"通常のテキスト", "通常のテキスト"}, + {"foo\nbar", "foo bar"}, + {"foo\tbar", "foo bar"}, + {"foo\rbar", "foo bar"}, + {"foo\x00bar", "foobar"}, + {"\u202Eabc", "abc"}, + {"\u200Bzero width", "zero width"}, + {"foo\x1b[31mred\x1b[0mbar", "fooredbar"}, + {"foo\u0008bar", "foobar"}, + {"foo\x07bar", "foobar"}, + {"foo\uFEFFbar", "foobar"}, + {"link", "link"}, + {"", ""}, + {"HTML", "HTML"}, + {"
line break", "line break"}, + {"", ""}, + {"", ""}, + {"visible", "visible"}, + {"", ""}, + {"", ""}, + } { + t.Run(tt.expected, func(t *testing.T) { + t.Parallel() + actual := strings.UISanitize(tt.s) + assert.Equal(t, tt.expected, actual) + }) + } +} diff --git a/coderd/workspaceagents.go b/coderd/workspaceagents.go index 3ae57d8394d43..d600eff6ecfec 100644 --- a/coderd/workspaceagents.go +++ b/coderd/workspaceagents.go @@ -41,6 +41,7 @@ import ( "github.com/coder/coder/v2/coderd/rbac/policy" "github.com/coder/coder/v2/coderd/telemetry" maputil "github.com/coder/coder/v2/coderd/util/maps" + strutil "github.com/coder/coder/v2/coderd/util/strings" "github.com/coder/coder/v2/coderd/wspubsub" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/codersdk/agentsdk" @@ -383,6 +384,9 @@ func (api *API) patchWorkspaceAgentAppStatus(rw http.ResponseWriter, r *http.Req return } + // Treat the message as untrusted input. + cleaned := strutil.UISanitize(req.Message) + // nolint:gocritic // This is a system restricted operation. _, err = api.Database.InsertWorkspaceAppStatus(dbauthz.AsSystemRestricted(ctx), database.InsertWorkspaceAppStatusParams{ ID: uuid.New(), @@ -391,7 +395,7 @@ func (api *API) patchWorkspaceAgentAppStatus(rw http.ResponseWriter, r *http.Req AgentID: workspaceAgent.ID, AppID: app.ID, State: database.WorkspaceAppStatusState(req.State), - Message: req.Message, + Message: cleaned, Uri: sql.NullString{ String: req.URI, Valid: req.URI != "", diff --git a/codersdk/toolsdk/toolsdk.go b/codersdk/toolsdk/toolsdk.go index 862d0c34a5316..c6c37821e5234 100644 --- a/codersdk/toolsdk/toolsdk.go +++ b/codersdk/toolsdk/toolsdk.go @@ -229,7 +229,7 @@ ONLY report an "idle" or "failure" state if you have FULLY completed the task. Properties: map[string]any{ "summary": map[string]any{ "type": "string", - "description": "A concise summary of your current progress on the task. This must be less than 160 characters in length.", + "description": "A concise summary of your current progress on the task. This must be less than 160 characters in length and must not include newlines or other control characters.", }, "link": map[string]any{ "type": "string", diff --git a/go.mod b/go.mod index 8e48f67f65885..fcd76e07987e2 100644 --- a/go.mod +++ b/go.mod @@ -365,7 +365,7 @@ require ( github.com/mdlayher/netlink v1.7.2 // indirect github.com/mdlayher/sdnotify v1.0.0 // indirect github.com/mdlayher/socket v0.5.0 // indirect - github.com/microcosm-cc/bluemonday v1.0.27 // indirect + github.com/microcosm-cc/bluemonday v1.0.27 github.com/miekg/dns v1.1.57 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect From 0ef7720f8c7d828563fc93295783f1b498bd180a Mon Sep 17 00:00:00 2001 From: 35C4n0r <70096901+35C4n0r@users.noreply.github.com> Date: Tue, 29 Jul 2025 20:19:17 +0530 Subject: [PATCH 5/5] feat: add tmux and gemini icons (#19031) Related PRs: #246 #229 --------- Co-authored-by: DevCats --- site/src/theme/icons.json | 2 ++ site/static/icon/gemini.svg | 1 + site/static/icon/tmux.svg | 1 + 3 files changed, 4 insertions(+) create mode 100644 site/static/icon/gemini.svg create mode 100644 site/static/icon/tmux.svg diff --git a/site/src/theme/icons.json b/site/src/theme/icons.json index ec79f1193040e..c3f6b1aac6b36 100644 --- a/site/src/theme/icons.json +++ b/site/src/theme/icons.json @@ -48,6 +48,7 @@ "folder.svg", "gateway.svg", "gcp.png", + "gemini.svg", "git.svg", "gitea.svg", "github.svg", @@ -104,6 +105,7 @@ "tensorflow.svg", "terminal.svg", "theia.svg", + "tmux.svg", "typescript.svg", "ubuntu.svg", "vault.svg", diff --git a/site/static/icon/gemini.svg b/site/static/icon/gemini.svg new file mode 100644 index 0000000000000..f1cf357573df0 --- /dev/null +++ b/site/static/icon/gemini.svg @@ -0,0 +1 @@ +Gemini \ No newline at end of file diff --git a/site/static/icon/tmux.svg b/site/static/icon/tmux.svg new file mode 100644 index 0000000000000..ac0174ed0784a --- /dev/null +++ b/site/static/icon/tmux.svg @@ -0,0 +1 @@ + \ No newline at end of file