diff --git a/cli/root.go b/cli/root.go index a40ac7c3c2..ed89a00ddc 100644 --- a/cli/root.go +++ b/cli/root.go @@ -343,10 +343,11 @@ func (r *RootCmd) Command(subcommands []*serpent.Command) (*serpent.Command, err // support links. return } - if cmd.Name() == "boundary" { - // The boundary command is integrated from the boundary package - // and has YAML-only options (e.g., allowlist from config file) - // that don't have flags or env vars. + if cmd.Name() == "agent-firewall" || cmd.Name() == "boundary" { + // The agent-firewall command (and its "boundary" alias) is + // integrated from the boundary package and has YAML-only + // options (e.g., allowlist from config file) that don't + // have flags or env vars. return } merr = errors.Join( diff --git a/docs/ai-coder/agent-firewall/version.md b/docs/ai-coder/agent-firewall/version.md index e8bdef5556..28de4d238c 100644 --- a/docs/ai-coder/agent-firewall/version.md +++ b/docs/ai-coder/agent-firewall/version.md @@ -13,12 +13,12 @@ v4.7.0 or newer**. ### Coder v2.30.0+ Since Coder v2.30.0, Agent Firewall is embedded inside the Coder binary, and -you don't need to install it separately. The `coder boundary` subcommand is +you don't need to install it separately. The `coder agent-firewall` subcommand is available directly from the Coder CLI. ### Claude Code Module v4.7.0+ -Since Claude Code module v4.7.0, the embedded `coder boundary` subcommand is +Since Claude Code module v4.7.0, the embedded `coder agent-firewall` subcommand is used by default. This means you don't need to set `boundary_version`; the boundary version is tied to your Coder version. @@ -27,7 +27,7 @@ boundary version is tied to your Coder version. ### Using Coder Before v2.30.0 with Claude Code Module v4.7.0+ If you're using Coder before v2.30.0 with Claude Code module v4.7.0 or newer, -the `coder boundary` subcommand isn't available in your Coder installation. In +the `coder agent-firewall` subcommand isn't available in your Coder installation. In this case, you need to: 1. Set `use_boundary_directly = true` in your Terraform module configuration diff --git a/docs/manifest.json b/docs/manifest.json index cbbeceefbe..a71fd205da 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -1626,9 +1626,9 @@ "path": "reference/cli/autoupdate.md" }, { - "title": "boundary", + "title": "agent-firewall", "description": "Network isolation tool for monitoring and restricting HTTP/HTTPS requests", - "path": "reference/cli/boundary.md" + "path": "reference/cli/agent-firewall.md" }, { "title": "coder", diff --git a/docs/reference/cli/boundary.md b/docs/reference/cli/agent-firewall.md similarity index 98% rename from docs/reference/cli/boundary.md rename to docs/reference/cli/agent-firewall.md index 79af765679..add4098c6b 100644 --- a/docs/reference/cli/boundary.md +++ b/docs/reference/cli/agent-firewall.md @@ -1,12 +1,12 @@ -# boundary +# agent-firewall Network isolation tool for monitoring and restricting HTTP/HTTPS requests ## Usage ```console -coder boundary [flags] [args...] +coder agent-firewall [flags] [args...] ``` ## Description diff --git a/docs/reference/cli/index.md b/docs/reference/cli/index.md index 211cba86c8..bbb7e85a31 100644 --- a/docs/reference/cli/index.md +++ b/docs/reference/cli/index.md @@ -66,7 +66,7 @@ Coder — A tool for provisioning self-hosted development environments with Terr | [support](./support.md) | Commands for troubleshooting issues with a Coder deployment. | | [server](./server.md) | Start a Coder server | | [provisioner](./provisioner.md) | View and manage provisioner daemons and jobs | -| [boundary](./boundary.md) | Network isolation tool for monitoring and restricting HTTP/HTTPS requests | +| [agent-firewall](./agent-firewall.md) | Network isolation tool for monitoring and restricting HTTP/HTTPS requests | | [features](./features.md) | List Enterprise features | | [licenses](./licenses.md) | Add, delete, and list licenses | | [groups](./groups.md) | Manage groups | diff --git a/enterprise/cli/boundary.go b/enterprise/cli/boundary.go index 104b2c6de2..212fe264ba 100644 --- a/enterprise/cli/boundary.go +++ b/enterprise/cli/boundary.go @@ -1,6 +1,7 @@ package cli import ( + "fmt" "net/http" "os" "runtime/debug" @@ -41,28 +42,31 @@ func (r *RootCmd) verifyLicense(inv *serpent.Invocation) error { entitlements, err := client.Entitlements(inv.Context()) if cerr, ok := codersdk.AsError(err); ok && cerr.StatusCode() == http.StatusNotFound { - return xerrors.Errorf("your deployment appears to be an AGPL deployment, so you cannot use the boundary command") + return xerrors.Errorf("your deployment appears to be an AGPL deployment, so you cannot use the agent-firewall command") } else if err != nil { return xerrors.Errorf("failed to get entitlements: %w", err) } feature := entitlements.Features[codersdk.FeatureBoundary] if feature.Entitlement == codersdk.EntitlementNotEntitled { - return xerrors.Errorf("your license is not entitled to use the boundary feature") + return xerrors.Errorf("your license is not entitled to use the agent-firewall feature") } if !feature.Enabled { // Feature is entitled but disabled (shouldn't happen for FeatureBoundary // since it's in AlwaysEnable(), but handle it gracefully). - return xerrors.Errorf("the boundary feature is disabled in your deployment configuration") + return xerrors.Errorf("the agent-firewall feature is disabled in your deployment configuration") } return nil } -func (r *RootCmd) boundary() *serpent.Command { +// buildAgentFirewallCmd builds the agent-firewall command. The returned command +// uses the boundary base command from the external boundary package, wrapped +// with license verification. +func (r *RootCmd) buildAgentFirewallCmd() *serpent.Command { version := getBoundaryVersion() - cmd := boundarycli.BaseCommand(version) // Package coder/boundary/cli exports a "base command" designed to be integrated as a subcommand. - cmd.Use += " [args...]" // The base command looks like `boundary -- command`. Serpent adds the flags piece, but we need to add the args. + cmd := boundarycli.BaseCommand(version) + cmd.Use = "agent-firewall [args...]" // Wrap the handler to check for FeatureBoundary entitlement. originalHandler := cmd.Handler @@ -78,9 +82,42 @@ func (r *RootCmd) boundary() *serpent.Command { return err } - // Call the original handler if entitlement check passes. return originalHandler(inv) } return cmd } + +// buildBoundaryAliasCmd builds a hidden, deprecated "boundary" command that +// prints a deprecation notice and then runs the same logic as agent-firewall. +func (r *RootCmd) buildBoundaryAliasCmd() *serpent.Command { + version := getBoundaryVersion() + cmd := boundarycli.BaseCommand(version) + cmd.Use = "boundary [args...]" + cmd.Hidden = true + + originalHandler := cmd.Handler + cmd.Handler = func(inv *serpent.Invocation) error { + _, _ = fmt.Fprintln(inv.Stderr, "DEPRECATED: 'coder boundary' is deprecated, use 'coder agent-firewall' instead.") + + if isChild() { + return originalHandler(inv) + } + + if err := r.verifyLicense(inv); err != nil { + return err + } + + return originalHandler(inv) + } + + return cmd +} + +func (r *RootCmd) agentFirewall() *serpent.Command { + return r.buildAgentFirewallCmd() +} + +func (r *RootCmd) boundaryAlias() *serpent.Command { + return r.buildBoundaryAliasCmd() +} diff --git a/enterprise/cli/boundary_test.go b/enterprise/cli/boundary_test.go index 2457f4ca63..0c8f4c7bc3 100644 --- a/enterprise/cli/boundary_test.go +++ b/enterprise/cli/boundary_test.go @@ -24,7 +24,25 @@ import ( // Actually testing the functionality of coder/boundary takes place in the // coder/boundary repo, since it's a dependency of coder. // Here we want to test basically that integrating it as a subcommand doesn't break anything. -func TestBoundarySubcommand(t *testing.T) { +func TestAgentFirewallSubcommand(t *testing.T) { + t.Parallel() + + inv, _ := newCLI(t, "agent-firewall", "--help") + var buf bytes.Buffer + inv.Stdout = &buf + inv.Stderr = &buf + + err := inv.Run() + require.NoError(t, err) + + // Verify help output contains expected information. + // We're simply confirming that `coder agent-firewall --help` ran without a runtime error as + // a good chunk of serpent's self validation logic happens at runtime. + output := buf.String() + assert.Contains(t, output, boundarycli.BaseCommand("dev").Short) +} + +func TestBoundaryAlias(t *testing.T) { t.Parallel() inv, _ := newCLI(t, "boundary", "--help") @@ -35,14 +53,12 @@ func TestBoundarySubcommand(t *testing.T) { err := inv.Run() require.NoError(t, err) - // Verify help output contains expected information. - // We're simply confirming that `coder boundary --help` ran without a runtime error as - // a good chunk of serpents self validation logic happens at runtime. + // The alias should dispatch to the same command and display help. output := buf.String() assert.Contains(t, output, boundarycli.BaseCommand("dev").Short) } -func TestBoundaryLicenseVerification(t *testing.T) { +func TestAgentFirewallLicenseVerification(t *testing.T) { t.Parallel() t.Run("EntitledAndEnabled", func(t *testing.T) { @@ -56,13 +72,13 @@ func TestBoundaryLicenseVerification(t *testing.T) { }, }) - inv, conf := newCLI(t, "boundary", "--version") + inv, conf := newCLI(t, "agent-firewall", "--version") //nolint:gocritic // requires owner clitest.SetupConfig(t, client, conf) ctx := testutil.Context(t, testutil.WaitShort) err := inv.WithContext(ctx).Run() - // Should succeed - boundary --version should work with valid license. + // Should succeed - agent-firewall --version should work with valid license. require.NoError(t, err) }) @@ -122,13 +138,13 @@ func TestBoundaryLicenseVerification(t *testing.T) { proxyClient.SetSessionToken(client.SessionToken()) t.Cleanup(proxyClient.HTTPClient.CloseIdleConnections) - inv, conf := newCLI(t, "boundary", "--version") + inv, conf := newCLI(t, "agent-firewall", "--version") clitest.SetupConfig(t, proxyClient, conf) ctx := testutil.Context(t, testutil.WaitShort) err = inv.WithContext(ctx).Run() require.Error(t, err) - require.ErrorContains(t, err, "your license is not entitled to use the boundary feature") + require.ErrorContains(t, err, "your license is not entitled to use the agent-firewall feature") }) t.Run("FeatureDisabled", func(t *testing.T) { @@ -186,13 +202,13 @@ func TestBoundaryLicenseVerification(t *testing.T) { proxyClient.SetSessionToken(client.SessionToken()) t.Cleanup(proxyClient.HTTPClient.CloseIdleConnections) - inv, conf := newCLI(t, "boundary", "--version") + inv, conf := newCLI(t, "agent-firewall", "--version") clitest.SetupConfig(t, proxyClient, conf) ctx := testutil.Context(t, testutil.WaitShort) err = inv.WithContext(ctx).Run() require.Error(t, err) - require.ErrorContains(t, err, "the boundary feature is disabled in your deployment configuration") + require.ErrorContains(t, err, "the agent-firewall feature is disabled in your deployment configuration") }) t.Run("AGPLDeployment", func(t *testing.T) { @@ -223,7 +239,7 @@ func TestBoundaryLicenseVerification(t *testing.T) { proxyClient.SetSessionToken(client.SessionToken()) t.Cleanup(proxyClient.HTTPClient.CloseIdleConnections) - inv, conf := newCLI(t, "boundary", "--version") + inv, conf := newCLI(t, "agent-firewall", "--version") clitest.SetupConfig(t, proxyClient, conf) ctx := testutil.Context(t, testutil.WaitShort) @@ -233,11 +249,11 @@ func TestBoundaryLicenseVerification(t *testing.T) { }) } -// TestBoundaryChildProcessSkipsCheck verifies that when CHILD=true, the license -// check is skipped. This simulates boundary re-executing itself to run the -// target process. We use a proxy that would fail the license check to verify -// it's skipped. -func TestBoundaryChildProcessSkipsCheck(t *testing.T) { +// TestAgentFirewallChildProcessSkipsCheck verifies that when CHILD=true, the +// license check is skipped. This simulates boundary re-executing itself to run +// the target process. We use a proxy that would fail the license check to +// verify it's skipped. +func TestAgentFirewallChildProcessSkipsCheck(t *testing.T) { // Cannot use t.Parallel() with t.Setenv(). client, _ := coderdenttest.New(t, &coderdenttest.Options{ LicenseOptions: &coderdenttest.LicenseOptions{ @@ -290,7 +306,7 @@ func TestBoundaryChildProcessSkipsCheck(t *testing.T) { proxyClient.SetSessionToken(client.SessionToken()) t.Cleanup(proxyClient.HTTPClient.CloseIdleConnections) - inv, conf := newCLI(t, "boundary", "--version") + inv, conf := newCLI(t, "agent-firewall", "--version") clitest.SetupConfig(t, proxyClient, conf) // Set CHILD=true to simulate boundary re-execution. This should skip the diff --git a/enterprise/cli/root.go b/enterprise/cli/root.go index baba6830e6..b211c0d598 100644 --- a/enterprise/cli/root.go +++ b/enterprise/cli/root.go @@ -18,7 +18,8 @@ func (r *RootCmd) enterpriseOnly() []*serpent.Command { agplcli.ExperimentalCommand(append(r.AGPLExperimental(), r.enterpriseExperimental()...)), // New commands that don't exist in AGPL: - r.boundary(), + r.agentFirewall(), + r.boundaryAlias(), r.workspaceProxy(), r.features(), r.licenses(), diff --git a/enterprise/cli/testdata/coder_--help.golden b/enterprise/cli/testdata/coder_--help.golden index 1db07b1801..373a3609e4 100644 --- a/enterprise/cli/testdata/coder_--help.golden +++ b/enterprise/cli/testdata/coder_--help.golden @@ -14,9 +14,9 @@ USAGE: $ coder templates init SUBCOMMANDS: - aibridge Manage AI Bridge. - boundary Network isolation tool for monitoring and restricting + agent-firewall Network isolation tool for monitoring and restricting HTTP/HTTPS requests + aibridge Manage AI Bridge. external-workspaces Create or manage external workspaces features List Enterprise features groups Manage groups diff --git a/enterprise/cli/testdata/coder_boundary_--help.golden b/enterprise/cli/testdata/coder_agent-firewall_--help.golden similarity index 98% rename from enterprise/cli/testdata/coder_boundary_--help.golden rename to enterprise/cli/testdata/coder_agent-firewall_--help.golden index 74f46947c1..5c6dcf7adb 100644 --- a/enterprise/cli/testdata/coder_boundary_--help.golden +++ b/enterprise/cli/testdata/coder_agent-firewall_--help.golden @@ -1,7 +1,7 @@ coder v0.0.0-devel USAGE: - coder boundary [flags] [args...] + coder agent-firewall [flags] [args...] Network isolation tool for monitoring and restricting HTTP/HTTPS requests