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/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.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/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)
}
}
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
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);
};
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 @@
+
\ 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
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