diff --git a/registry/coder/modules/agent-relay-cursor/README.md b/registry/coder/modules/agent-relay-cursor/README.md index 90f6b9e7f..df11f68f7 100644 --- a/registry/coder/modules/agent-relay-cursor/README.md +++ b/registry/coder/modules/agent-relay-cursor/README.md @@ -16,7 +16,7 @@ parameters the relay stamps on each build and runs the Cursor CLI worker. ```tf module "cursor_worker" { source = "registry.coder.com/coder/agent-relay-cursor/coder" - version = "0.6.0" + version = "0.7.0" agent_id = coder_agent.main.id # Downloads the Cursor CLI at start when it is not in the image. Bake @@ -88,26 +88,19 @@ session limit. ## Additional worker arguments -Use `additional_args` to pass extra flags to `agent worker`, as a map of -flag to value. They are appended after the flags the module sets and before -the `start` subcommand. Each flag is passed exactly as written, so use `--name` -or `-n` as the CLI expects, followed by its value as the next argument; an -empty value passes the flag alone. Values are passed verbatim, so spaces and -quotes need no escaping. Terraform orders map keys alphabetically. Flags the module already sets -(`--pool`, `--idle-release-timeout`, `--auth-token-file`, `--auth-token`, -`--computer-use`, `--share-desktop`) are rejected. Give a flag that takes an optional value an explicit -one, or it consumes `start`. +Use `additional_args` to pass extra arguments to `agent worker`, as a +list with one element per argument. They are appended after the flags the module sets and before the `start` subcommand, in the order written. +Each element is passed exactly as written, so a flag can repeat and a value +can contain spaces or quotes without escaping. Flags the module already sets +(`--pool`, `--idle-release-timeout`, `--auth-token-file`, `--auth-token`, `--computer-use`, `--share-desktop`) are rejected, with or without `=value`. Give a flag that takes an +optional value an explicit one (`--flag=value`), or it consumes `start`. ```tf module "cursor_worker" { - source = "registry.coder.com/coder/agent-relay-cursor/coder" - version = "0.6.0" - agent_id = coder_agent.main.id - # Passed as: --another-flag --some-flag value - additional_args = { - "--another-flag" = "" - "--some-flag" = "value" - } + source = "registry.coder.com/coder/agent-relay-cursor/coder" + version = "0.7.0" + agent_id = coder_agent.main.id + additional_args = ["--some-flag", "value", "--some-flag", "another value", "--another-flag"] } ``` diff --git a/registry/coder/modules/agent-relay-cursor/main.test.ts b/registry/coder/modules/agent-relay-cursor/main.test.ts index fe04634f6..96d155264 100644 --- a/registry/coder/modules/agent-relay-cursor/main.test.ts +++ b/registry/coder/modules/agent-relay-cursor/main.test.ts @@ -419,25 +419,33 @@ describe("agent-relay-cursor", () => { expect(args).toContain("start"); }); - it("appends additional_args before start, one argument per flag", async () => { + it("appends additional_args before start, in order, one argument per element", async () => { const { id, scripts } = await setup({ - additional_args: JSON.stringify({ - "--verbose": "", - "--name": "two words $HOME", - "-t": `it"s`, - }), + additional_args: JSON.stringify([ + "--name", + "two words $HOME", + "--name", + `it"s`, + "--verbose", + ]), }); await stubAgent(id, 'printf "%s\\n" "$@" >/tmp/agent-args; sleep 30'); const { start } = await runDispatched(id, scripts); expect(start.exitCode).toBe(0); const args = (await readFileContainer(id, "/tmp/agent-args")).split("\n"); expect(args[0]).toBe("worker"); - expect(args).toContain("--verbose"); - // Values reach the CLI verbatim: no word splitting or expansion. - expect(args[args.indexOf("--name") + 1]).toBe("two words $HOME"); - expect(args[args.indexOf("-t") + 1]).toBe('it"s'); + // A flag can repeat, and arguments reach the CLI verbatim and in order: + // no word splitting or expansion. + const at = args.indexOf("--name"); + expect(args.slice(at, at + 5)).toEqual([ + "--name", + "two words $HOME", + "--name", + 'it"s', + "--verbose", + ]); // The subcommand stays last, after every flag. - expect(args.indexOf("start")).toBeGreaterThan(args.indexOf("-t") + 1); + expect(args.indexOf("start")).toBe(at + 5); expect(args[args.indexOf("--pool") + 1]).toBe(POOL_NAME); }); diff --git a/registry/coder/modules/agent-relay-cursor/main.tf b/registry/coder/modules/agent-relay-cursor/main.tf index d60e33c13..e2284feb8 100644 --- a/registry/coder/modules/agent-relay-cursor/main.tf +++ b/registry/coder/modules/agent-relay-cursor/main.tf @@ -105,13 +105,13 @@ variable "serving_log_pattern" { } variable "additional_args" { - description = "Additional flags to pass to `agent worker`, as a map of flag to value. Each flag is passed as written (e.g. `--name` or `-n`) followed by its value as the next argument; an empty value passes the flag alone. Appended after the flags the module sets and before `start`." - type = map(string) - default = {} + description = "Additional arguments to pass to `agent worker`, one list element per argument, in order (e.g. `[\"--header\", \"a: 1\", \"--header\", \"b: 2\", \"--verbose\"]`). Each element is passed as written, so a flag can repeat and a value may contain spaces." + type = list(string) + default = [] validation { - condition = alltrue([for k, v in var.additional_args : can(regex("^--?[A-Za-z0-9][A-Za-z0-9._-]*$", k)) && !can(regex("\n", v))]) - error_message = "additional_args keys must be flags such as --name or -n (letters, digits, '.', '_', '-'), and values must not contain newlines." + condition = alltrue([for a in var.additional_args : a != "" && !can(regex("\n", a))]) + error_message = "additional_args elements must be non-empty and must not contain newlines." } # The module already sets these: the pool and idle timeout come from Agent @@ -119,7 +119,7 @@ variable "additional_args" { # command line, and desktop flags have their own inputs. Keep this list in # sync with the `worker` flags in start.sh.tftpl. validation { - condition = length(setintersection(keys(var.additional_args), ["--pool", "--idle-release-timeout", "--auth-token-file", "--auth-token", "--computer-use", "--share-desktop"])) == 0 + condition = !anytrue([for a in var.additional_args : contains(["--pool", "--idle-release-timeout", "--auth-token-file", "--auth-token", "--computer-use", "--share-desktop"], split("=", a)[0])]) error_message = "additional_args must not set --pool, --idle-release-timeout, --auth-token-file, --auth-token, --computer-use, or --share-desktop; the module sets them (use computer_use and share_desktop for the desktop flags)." } } @@ -262,10 +262,6 @@ locals { # tree so one directory holds everything a debugger needs. module_directory = "$HOME/.coder-modules/coder/agent-relay-cursor" - # Render additional_args as argv: each key as written, followed by its - # value as a separate argument unless the value is empty. - additional_args = flatten([for k, v in var.additional_args : v == "" ? [k] : [k, v]]) - install_script = templatefile("${path.module}/install.sh.tftpl", { cli_binary = var.cli_binary install_cli = var.install_cli @@ -279,7 +275,7 @@ locals { share_desktop = var.share_desktop state_file = var.state_file log_file = var.log_file - additional_args = base64encode(join("\n", local.additional_args)) + additional_args = base64encode(join("\n", var.additional_args)) }) } diff --git a/registry/coder/modules/agent-relay-cursor/main.tftest.hcl b/registry/coder/modules/agent-relay-cursor/main.tftest.hcl index be7cc6ba3..4217cf9b2 100644 --- a/registry/coder/modules/agent-relay-cursor/main.tftest.hcl +++ b/registry/coder/modules/agent-relay-cursor/main.tftest.hcl @@ -239,7 +239,7 @@ run "additional_args_default_empty" { command = plan assert { - condition = local.additional_args == [] && strcontains(local.start_script, "done < <(echo -n '' | base64 -d)") + condition = strcontains(local.start_script, "done < <(echo -n '' | base64 -d)") error_message = "additional_args must default to empty" } } @@ -248,31 +248,21 @@ run "additional_args_encoded" { command = plan variables { - additional_args = { - "--verbose" = "" - "--name" = "it's here" - "-t" = "x" - } + additional_args = ["--header", "it's here", "--header", "b", "--verbose"] } - # Map keys sort byte-wise, so --flags come before -f flags. + # Base64 keeps quotes in an argument from breaking the rendered script. assert { - condition = local.additional_args == ["--name", "it's here", "--verbose", "-t", "x"] - error_message = "each flag must be passed as written, followed by its value unless the value is empty" - } - - # Base64 keeps quotes in a value from breaking the rendered script. - assert { - condition = strcontains(local.start_script, base64encode(join("\n", local.additional_args))) && !strcontains(local.start_script, "it's here") - error_message = "additional_args must be base64-encoded into the start script" + condition = strcontains(local.start_script, base64encode("--header\nit's here\n--header\nb\n--verbose")) && !strcontains(local.start_script, "it's here") + error_message = "additional_args must be base64-encoded into the start script, one argument per line, in order" } } -run "additional_args_rejects_bad_flag" { +run "additional_args_rejects_newline" { command = plan variables { - additional_args = { "verbose; rm -rf /" = "" } + additional_args = ["--verbose\n--pool"] } expect_failures = [var.additional_args] @@ -282,7 +272,7 @@ run "additional_args_rejects_module_flags" { command = plan variables { - additional_args = { "--auth-token" = "secret" } + additional_args = ["--pool=2"] } expect_failures = [var.additional_args]