Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 11 additions & 18 deletions registry/coder/modules/agent-relay-cursor/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"]
}
```

Expand Down
30 changes: 19 additions & 11 deletions registry/coder/modules/agent-relay-cursor/main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});

Expand Down
18 changes: 7 additions & 11 deletions registry/coder/modules/agent-relay-cursor/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -105,21 +105,21 @@ 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
# Relay's parameters, the token is passed by file so it stays off the
# 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)."
}
}
Expand Down Expand Up @@ -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
Expand All @@ -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))
})
}

Expand Down
26 changes: 8 additions & 18 deletions registry/coder/modules/agent-relay-cursor/main.tftest.hcl
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
Expand All @@ -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]
Expand All @@ -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]
Expand Down
Loading