From 0e4ed340e3ffacd14b974f76decbd04af5bb0142 Mon Sep 17 00:00:00 2001 From: "coder[bot]" <137810501+coder[bot]@users.noreply.github.com> Date: Tue, 20 Oct 2026 14:33:31 +0000 Subject: [PATCH 01/10] feat(registry/coder-labs): add kiro-crew module and template Port the Kiro Crew prototype into a coder-labs module that installs Kiro CLI and Kiro Crew through coder-utils, starts the gateway and a token-minting redirector, and exposes a self-authenticating dashboard app. Add a Kubernetes template that uses the module. Generated with Coder Agents. --- .../coder-labs/modules/kiro-crew/README.md | 127 +++++ .../modules/kiro-crew/kiro-crew.tftest.hcl | 290 +++++++++++ .../coder-labs/modules/kiro-crew/main.test.ts | 453 ++++++++++++++++++ registry/coder-labs/modules/kiro-crew/main.tf | 261 ++++++++++ .../kiro-crew/scripts/install.sh.tftpl | 177 +++++++ .../modules/kiro-crew/scripts/start.sh.tftpl | 160 +++++++ .../kiro-crew/testdata/fake-gateway.py | 36 ++ .../kiro-crew/testdata/kiro-cli-mock.sh | 9 + .../kiro-crew/testdata/kirocrew-mock.sh | 27 ++ .../coder-labs/templates/kiro-crew/README.md | 62 +++ .../templates/kiro-crew/kiro-crew-prereqs.sh | 34 ++ .../coder-labs/templates/kiro-crew/main.tf | 379 +++++++++++++++ .../coder-labs/templates/kiro-crew/startup.sh | 74 +++ 13 files changed, 2089 insertions(+) create mode 100644 registry/coder-labs/modules/kiro-crew/README.md create mode 100644 registry/coder-labs/modules/kiro-crew/kiro-crew.tftest.hcl create mode 100644 registry/coder-labs/modules/kiro-crew/main.test.ts create mode 100644 registry/coder-labs/modules/kiro-crew/main.tf create mode 100644 registry/coder-labs/modules/kiro-crew/scripts/install.sh.tftpl create mode 100644 registry/coder-labs/modules/kiro-crew/scripts/start.sh.tftpl create mode 100644 registry/coder-labs/modules/kiro-crew/testdata/fake-gateway.py create mode 100644 registry/coder-labs/modules/kiro-crew/testdata/kiro-cli-mock.sh create mode 100644 registry/coder-labs/modules/kiro-crew/testdata/kirocrew-mock.sh create mode 100644 registry/coder-labs/templates/kiro-crew/README.md create mode 100644 registry/coder-labs/templates/kiro-crew/kiro-crew-prereqs.sh create mode 100644 registry/coder-labs/templates/kiro-crew/main.tf create mode 100644 registry/coder-labs/templates/kiro-crew/startup.sh diff --git a/registry/coder-labs/modules/kiro-crew/README.md b/registry/coder-labs/modules/kiro-crew/README.md new file mode 100644 index 000000000..da7338f39 --- /dev/null +++ b/registry/coder-labs/modules/kiro-crew/README.md @@ -0,0 +1,127 @@ +--- +display_name: Kiro Crew +description: Run the Kiro Crew gateway in your workspace and open its web dashboard from a self-authenticating Coder app. +icon: ../../../../.icons/kiro.svg +verified: false +tags: [agent, ai, kiro, aws, mcp] +--- + +# Kiro Crew + +Install [Kiro CLI](https://kiro.dev/cli/) and [Kiro Crew](https://github.com/kirodotdev/KiroCrew), run the `kirocrew gateway` in the workspace, and open the Kiro Crew web dashboard from a Coder app. + +```tf +module "kiro-crew" { + source = "registry.coder.com/coder-labs/kiro-crew/coder" + version = "1.0.0" + agent_id = coder_agent.main.id +} +``` + +The module adds two subdomain apps: + +- A visible **Kiro Crew** app that points at a loopback redirector. Each time you open it, the redirector mints a short-lived gateway token and redirects your browser to the dashboard with that token, so you land on an authenticated dashboard without running `kirocrew token`. +- A hidden `-dashboard` app that serves the dashboard and its websocket through Coder's subdomain proxy. + +The gateway rejects requests whose `Host` is not loopback (`Host header not allowed.`). The module derives the dashboard app's origin from the Coder access URL, passes it to the gateway in `KIROCREW_CORS_ORIGINS`, and sets `dashboard.url` so token cookies are scoped to it. + +## Prerequisites + +- Your Coder deployment must serve [wildcard subdomain apps](https://coder.com/docs/admin/setup#wildcard-access-url). Both apps are subdomain apps. +- The workspace image needs `bash`, `curl`, `python3` with the `venv` module, `openssl`, and `sha256sum`. The Kiro Crew installer uses Python 3.10 or newer and prefers 3.12 or 3.11. Use `pre_install_script` to install missing packages. +- The Coder agent must support `coder exp sync`, which orders the install and start scripts. +- A Kiro account. See [Authentication](#authentication). + +## Examples + +### Install system prerequisites first + +```tf +module "kiro-crew" { + source = "registry.coder.com/coder-labs/kiro-crew/coder" + version = "1.0.0" + agent_id = coder_agent.main.id + + pre_install_script = <<-EOT + #!/usr/bin/env bash + sudo apt-get update -qq + sudo apt-get install -y curl openssl python3-venv + EOT +} +``` + +### Add MCP servers + +`extra_mcp_json` is deep-merged into `~/.kiro/settings/mcp.json`. Keys you set replace existing keys with the same name; other existing servers are kept. + +```tf +module "kiro-crew" { + source = "registry.coder.com/coder-labs/kiro-crew/coder" + version = "1.0.0" + agent_id = coder_agent.main.id + + extra_mcp_json = jsonencode({ + mcpServers = { + fetch = { + command = "uvx" + args = ["mcp-server-fetch"] + } + } + }) +} +``` + +### Custom wildcard domain + +If your deployment's wildcard access URL uses a different host than its access URL, set the dashboard origin explicitly. The hidden app's slug is `-dashboard`. + +```tf +module "kiro-crew" { + source = "registry.coder.com/coder-labs/kiro-crew/coder" + version = "1.0.0" + agent_id = coder_agent.main.id + dashboard_origin = "https://kirocrew-dashboard--${data.coder_workspace.me.name}--${data.coder_workspace_owner.me.name}.apps.example.com" +} +``` + +Use `allowed_origins` to trust additional origins, such as the Coder access URL, in the gateway's allowlist. + +### AWS Builder ID token from external auth + +```tf +data "coder_external_auth" "aws_builder_id" { + id = "aws-builder-id" +} + +module "kiro-crew" { + source = "registry.coder.com/coder-labs/kiro-crew/coder" + version = "1.0.0" + agent_id = coder_agent.main.id + aws_builder_id_token = data.coder_external_auth.aws_builder_id.access_token +} +``` + +The token is exported to the workspace as `KIRO_AWS_BUILDER_ID_TOKEN` and is not rendered into any script. + +## Authentication + +The gateway starts with the workspace, but agents cannot call models until Kiro CLI is authenticated. Kiro credentials are stored under `$HOME`, so use a persistent home directory to keep them across restarts. + +- **Headless:** create a Coder user secret that sets `KIRO_API_KEY` before you start the workspace, for example `coder secret create kiro-api-key --env KIRO_API_KEY`. The gateway inherits it, and Kiro CLI authenticates without a browser. Generating API keys requires a Kiro subscription that supports them. +- **Interactive:** open a terminal, run `kiro-cli login`, and complete the device-code sign-in with AWS Builder ID or IAM Identity Center. Check the result with `kiro-cli whoami`. + +## Troubleshooting + +Logs are written to `$HOME/.coder-modules/coder-labs/kiro-crew/logs/`: + +- `install.log` and `start.log` for the module scripts. +- `gateway.log` for the `kirocrew gateway` process. +- `redirector.log` for the token-minting redirector. + +The start script launches the gateway in a detached session and returns immediately, because a slow cold start could otherwise exceed the agent's script timeout. Readiness is reported by the app healthchecks, so the app can show as unhealthy for a short time after the workspace starts. While the gateway is starting, the Kiro Crew app responds with `503`; retry after a few seconds. + +- **`Host header not allowed.`:** the browser's origin is not in the gateway's allowlist. Set `dashboard_origin` or `allowed_origins` to the origin shown in the browser. +- **`ImportError: No module named 'aiohttp'` on gateway start:** the managed venv at `~/.kiro/crew-venv` was built with a foreign `PYTHONPATH`. The install script unsets `PYTHONPATH` and `PYTHONHOME`; remove `~/.kiro/crew-venv`, set `use_cached = false`, and restart the workspace. +- **Kiro Crew installer fails in `pipx` with `JSONDecodeError`:** distro `pipx` on Python 3.13 or newer crashes. The install script builds its own `pipx` with `python3.12` or `python3.11` when available, so install one of them, or use an image based on Ubuntu 24.04. +- **Sandbox warnings:** Kiro Crew sandboxes agent subprocesses with a Linux user namespace. When the workspace cannot create one, which is common in Kubernetes pods, the gateway is configured with `agent.sandbox_allow_unsandboxed_exec` and the workspace is the isolation boundary. +- **Expected warnings:** `Vendored llama-cpp-python failed to import` (the gateway falls back to API-based embeddings) and `cgroup v2 scope enforcement unavailable` (no systemd user session in containers) do not affect the gateway. diff --git a/registry/coder-labs/modules/kiro-crew/kiro-crew.tftest.hcl b/registry/coder-labs/modules/kiro-crew/kiro-crew.tftest.hcl new file mode 100644 index 000000000..31641d915 --- /dev/null +++ b/registry/coder-labs/modules/kiro-crew/kiro-crew.tftest.hcl @@ -0,0 +1,290 @@ +override_data { + target = data.coder_workspace.me + values = { + name = "dev" + access_url = "https://coder.example.com/" + } +} + +override_data { + target = data.coder_workspace_owner.me + values = { + name = "alice" + } +} + +run "defaults" { + command = plan + + variables { + agent_id = "test-agent" + } + + assert { + condition = local.module_directory == "$HOME/.coder-modules/coder-labs/kiro-crew" + error_message = "module_directory must follow the $HOME/.coder-modules// layout" + } + + assert { + condition = resource.coder_app.kiro_crew.url == "http://localhost:8898/" + error_message = "Visible app must point at the redirector on the default port" + } + + assert { + condition = resource.coder_app.kiro_crew.slug == "kirocrew" + error_message = "Visible app slug should default to 'kirocrew'" + } + + assert { + condition = resource.coder_app.kiro_crew.subdomain == true + error_message = "Visible app must be a subdomain app" + } + + assert { + condition = resource.coder_app.kiro_crew_dashboard.url == "http://localhost:8899/" + error_message = "Dashboard app must point at the gateway on the default port" + } + + assert { + condition = resource.coder_app.kiro_crew_dashboard.slug == "kirocrew-dashboard" + error_message = "Dashboard app slug should be '-dashboard'" + } + + assert { + condition = resource.coder_app.kiro_crew_dashboard.hidden == true + error_message = "Dashboard app must be hidden" + } + + assert { + condition = resource.coder_app.kiro_crew_dashboard.subdomain == true + error_message = "Dashboard app must be a subdomain app" + } + + assert { + condition = local.dashboard_origin == "https://kirocrew-dashboard--dev--alice.coder.example.com" + error_message = "Dashboard origin should be derived from the access URL, workspace, and owner" + } + + assert { + condition = local.gateway_allowed_origins == local.dashboard_origin + error_message = "Gateway allowlist should contain only the dashboard origin by default" + } + + assert { + condition = output.kirocrew_url == "http://localhost:8899/" + error_message = "kirocrew_url output should use the gateway port" + } + + assert { + condition = length(resource.coder_env.aws_builder_id_token) == 0 + error_message = "No Builder ID env var should be created without a token" + } + + assert { + condition = strcontains(local.start_script, "ARG_ALLOWED_ORIGINS=$(echo -n '${base64encode(local.dashboard_origin)}' | base64 -d)") + error_message = "Start script should receive the allowlist base64 encoded" + } + + assert { + condition = strcontains(local.install_script, "ARG_USE_CACHED='true'") + error_message = "Install script should use cached binaries by default" + } + + assert { + condition = strcontains(local.install_script, "ARG_EXTRA_MCP_JSON=$(echo -n '' | base64 -d)") + error_message = "Install script should receive an empty extra MCP value by default" + } +} + +run "custom_ports_and_slug" { + command = plan + + variables { + agent_id = "test-agent" + port = 19899 + redirect_port = 19898 + slug = "crew" + } + + assert { + condition = resource.coder_app.kiro_crew.url == "http://localhost:19898/" + error_message = "Visible app must use redirect_port" + } + + assert { + condition = resource.coder_app.kiro_crew_dashboard.slug == "crew-dashboard" + error_message = "Dashboard app slug should follow the custom slug" + } + + assert { + condition = local.dashboard_origin == "https://crew-dashboard--dev--alice.coder.example.com" + error_message = "Dashboard origin should follow the custom slug" + } + + assert { + condition = strcontains(local.start_script, "ARG_PORT='19899'") && strcontains(local.start_script, "ARG_REDIRECT_PORT='19898'") + error_message = "Start script should receive the configured ports" + } +} + +run "healthchecks_use_configured_ports" { + command = apply + + variables { + agent_id = "test-agent" + port = 19899 + redirect_port = 19898 + } + + assert { + condition = one([for h in resource.coder_app.kiro_crew.healthcheck : h.url]) == "http://localhost:19898/healthz" + error_message = "Visible app healthcheck must use the redirector /healthz endpoint" + } + + assert { + condition = one([for h in resource.coder_app.kiro_crew_dashboard.healthcheck : h.url]) == "http://localhost:19899/health" + error_message = "Dashboard app healthcheck must use the gateway /health endpoint" + } +} + +run "allowed_origins_are_appended" { + command = plan + + variables { + agent_id = "test-agent" + allowed_origins = " https://coder.example.com,https://proxy.example.com " + } + + assert { + condition = local.gateway_allowed_origins == "https://kirocrew-dashboard--dev--alice.coder.example.com,https://coder.example.com,https://proxy.example.com" + error_message = "Extra allowed origins should follow the dashboard origin" + } +} + +run "dashboard_origin_override" { + command = plan + + variables { + agent_id = "test-agent" + dashboard_origin = "https://kirocrew-dashboard--dev--alice.apps.example.com" + } + + assert { + condition = local.dashboard_origin == "https://kirocrew-dashboard--dev--alice.apps.example.com" + error_message = "dashboard_origin should override the derived origin" + } + + assert { + condition = output.dashboard_origin == "https://kirocrew-dashboard--dev--alice.apps.example.com" + error_message = "dashboard_origin output should reflect the override" + } +} + +run "invalid_dashboard_origin_is_rejected" { + command = plan + + variables { + agent_id = "test-agent" + dashboard_origin = "https://apps.example.com/path" + } + + expect_failures = [var.dashboard_origin] +} + +run "invalid_extra_mcp_json_is_rejected" { + command = plan + + variables { + agent_id = "test-agent" + extra_mcp_json = "{not json" + } + + expect_failures = [var.extra_mcp_json] +} + +run "extra_mcp_json_is_base64_encoded" { + command = plan + + variables { + agent_id = "test-agent" + extra_mcp_json = "{\"mcpServers\":{\"demo\":{\"command\":\"echo\",\"args\":[\"it's\"]}}}" + } + + assert { + condition = strcontains(local.install_script, base64encode("{\"mcpServers\":{\"demo\":{\"command\":\"echo\",\"args\":[\"it's\"]}}}")) + error_message = "extra_mcp_json should be passed to the install script base64 encoded" + } +} + +run "http_access_url_keeps_scheme" { + command = plan + + override_data { + target = data.coder_workspace.me + values = { + name = "dev" + access_url = "http://localhost:3000" + } + } + + variables { + agent_id = "test-agent" + } + + assert { + condition = local.dashboard_origin == "http://kirocrew-dashboard--dev--alice.localhost:3000" + error_message = "Dashboard origin should keep the access URL scheme" + } +} + +run "builder_id_token_sets_env" { + command = apply + + variables { + agent_id = "test-agent" + aws_builder_id_token = "test-token" + } + + assert { + condition = length(resource.coder_env.aws_builder_id_token) == 1 + error_message = "Builder ID env var should be created when a token is set" + } + + assert { + condition = resource.coder_env.aws_builder_id_token[0].name == "KIRO_AWS_BUILDER_ID_TOKEN" + error_message = "Builder ID token should be exported as KIRO_AWS_BUILDER_ID_TOKEN" + } + + assert { + condition = !strcontains(local.install_script, "test-token") && !strcontains(local.start_script, "test-token") + error_message = "Builder ID token must not be rendered into scripts" + } +} + +run "scripts_output_includes_start" { + command = apply + + variables { + agent_id = "test-agent" + } + + assert { + condition = output.scripts == ["coder-labs-kiro-crew-install_script", "coder-labs-kiro-crew-start_script"] + error_message = "scripts output should list install and start scripts in run order" + } +} + +run "scripts_output_with_pre_and_post_install" { + command = apply + + variables { + agent_id = "test-agent" + pre_install_script = "echo pre" + post_install_script = "echo post" + } + + assert { + condition = output.scripts == ["coder-labs-kiro-crew-pre_install_script", "coder-labs-kiro-crew-install_script", "coder-labs-kiro-crew-post_install_script", "coder-labs-kiro-crew-start_script"] + error_message = "scripts output should list every configured script in run order" + } +} diff --git a/registry/coder-labs/modules/kiro-crew/main.test.ts b/registry/coder-labs/modules/kiro-crew/main.test.ts new file mode 100644 index 000000000..3a32f1382 --- /dev/null +++ b/registry/coder-labs/modules/kiro-crew/main.test.ts @@ -0,0 +1,453 @@ +import { + afterEach, + beforeAll, + describe, + expect, + setDefaultTimeout, + test, +} from "bun:test"; +import { + execContainer, + readFileContainer, + removeContainer, + runContainer, + runTerraformApply, + runTerraformInit, + TerraformState, +} from "~test"; +import { writeExecutable } from "../../../coder/modules/agentapi/test-util"; +import path from "path"; + +interface ModuleScripts { + pre_install?: string; + install: string; + post_install?: string; + start: string; +} + +const collectScripts = (state: TerraformState): ModuleScripts => { + const byDisplayName: Record = {}; + for (const resource of state.resources) { + if (resource.type !== "coder_script") continue; + for (const instance of resource.instances) { + const attrs = instance.attributes as Record; + const displayName = attrs.display_name as string | undefined; + const script = attrs.script as string | undefined; + if (displayName && script) { + byDisplayName[displayName] = script; + } + } + } + const install = byDisplayName["Kiro Crew: Install Script"]; + const start = byDisplayName["Kiro Crew: Start Script"]; + if (!install || !start) { + throw new Error("install or start script not found in terraform state"); + } + return { + pre_install: byDisplayName["Kiro Crew: Pre-Install Script"], + install, + post_install: byDisplayName["Kiro Crew: Post-Install Script"], + start, + }; +}; + +let cleanupFunctions: (() => Promise)[] = []; +const registerCleanup = (cleanup: () => Promise) => { + cleanupFunctions.push(cleanup); +}; +afterEach(async () => { + const cleanupFnsCopy = cleanupFunctions.slice().reverse(); + cleanupFunctions = []; + for (const cleanup of cleanupFnsCopy) { + try { + await cleanup(); + } catch (error) { + console.error("Error during cleanup:", error); + } + } +}); + +const moduleDir = path.resolve(import.meta.dir); +const moduleLogDir = "/home/coder/.coder-modules/coder-labs/kiro-crew/logs"; + +// Containers share the host network, so use ports unlikely to be in use. +const gatewayPort = "19899"; +const redirectPort = "19898"; + +interface SetupProps { + // Install mock kiro-cli and kirocrew binaries on PATH. + withMocks?: boolean; + moduleVariables?: Record; +} + +const setup = async ( + props?: SetupProps, +): Promise<{ id: string; scripts: ModuleScripts; state: TerraformState }> => { + const state = await runTerraformApply(moduleDir, { + agent_id: "foo", + port: gatewayPort, + redirect_port: redirectPort, + ...props?.moduleVariables, + }); + const scripts = collectScripts(state); + + const id = await runContainer("codercom/enterprise-node:latest"); + registerCleanup(async () => { + if (process.env["DEBUG"] === "true" || process.env["DEBUG"] === "1") { + console.log(`Not removing container ${id} in debug mode`); + return; + } + await removeContainer(id); + }); + + // `coder exp sync` calls from the coder-utils wrappers need a coder binary. + await writeExecutable({ + containerId: id, + filePath: "/usr/bin/coder", + content: "#!/bin/bash\nexit 0\n", + }); + await writeExecutable({ + containerId: id, + filePath: "/tmp/fake-gateway.py", + content: await Bun.file( + path.join(moduleDir, "testdata", "fake-gateway.py"), + ).text(), + }); + if (props?.withMocks) { + await writeExecutable({ + containerId: id, + filePath: "/usr/bin/kiro-cli", + content: await Bun.file( + path.join(moduleDir, "testdata", "kiro-cli-mock.sh"), + ).text(), + }); + await writeExecutable({ + containerId: id, + filePath: "/usr/bin/kirocrew", + content: await Bun.file( + path.join(moduleDir, "testdata", "kirocrew-mock.sh"), + ).text(), + }); + } + return { id, scripts, state }; +}; + +const envPrefix = (env?: Record) => + Object.entries(env ?? {}) + .map(([key, value]) => `export ${key}="${value.replace(/"/g, '\\"')}" && `) + .join(""); + +const runScript = async ( + id: string, + name: string, + script: string, + env?: Record, +) => { + const target = `/tmp/coder-utils-${name}.sh`; + await writeExecutable({ containerId: id, filePath: target, content: script }); + return execContainer(id, ["bash", "-c", `${envPrefix(env)}${target}`]); +}; + +const runOrFail = async ( + id: string, + name: string, + script: string, + env?: Record, +) => { + const resp = await runScript(id, name, script, env); + if (resp.exitCode !== 0) { + console.log(`script ${name} failed:`); + console.log(resp.stdout); + console.log(resp.stderr); + } + expect(resp.exitCode).toBe(0); + return resp; +}; + +// Records curl arguments and copies a per-URL fixture to --output. Exits with +// failExitCode instead when it is non-zero. +const writeCurlMock = async (id: string, failExitCode = 0) => { + await writeExecutable({ + containerId: id, + filePath: "/usr/local/bin/curl", + content: `#!/bin/bash +printf '%s\\n' "$*" >> /tmp/curl-args +out="" +url="" +while [ $# -gt 0 ]; do + case "$1" in + --output) out="$2"; shift ;; + http*) url="$1" ;; + esac + shift +done +if [ ${failExitCode} -ne 0 ]; then + exit ${failExitCode} +fi +case "$url" in + https://cli.kiro.dev/install) cp /tmp/fixtures/kiro-cli-install.sh "$out" ;; + https://download.crew.kiro.dev/cli.sh) cp /tmp/fixtures/kiro-crew-install.sh "$out" ;; + *) exit 22 ;; +esac +`, + }); +}; + +// Fake installers that place binaries where the real installers do: kiro-cli +// in ~/.local/bin and kirocrew in the managed venv. +const writeInstallerFixtures = async (id: string) => { + await execContainer(id, ["mkdir", "-p", "/tmp/fixtures"]); + await writeExecutable({ + containerId: id, + filePath: "/tmp/fixtures/kiro-cli-install.sh", + content: `#!/bin/bash +echo "kiro-cli installer ran" >> /tmp/installers-ran +mkdir -p "$HOME/.local/bin" +printf '#!/bin/sh\\necho "kiro-cli 9.9.9"\\n' > "$HOME/.local/bin/kiro-cli" +chmod +x "$HOME/.local/bin/kiro-cli" +`, + }); + await writeExecutable({ + containerId: id, + filePath: "/tmp/fixtures/kiro-crew-install.sh", + content: `#!/bin/sh +echo "kiro-crew installer ran PYTHONPATH=\${PYTHONPATH-unset} pipx=$(command -v pipx)" >> /tmp/installers-ran +mkdir -p "$HOME/.kiro/crew-venv/bin" +cp /tmp/kirocrew-mock.sh "$HOME/.kiro/crew-venv/bin/kirocrew" +chmod +x "$HOME/.kiro/crew-venv/bin/kirocrew" +`, + }); + await writeExecutable({ + containerId: id, + filePath: "/tmp/kirocrew-mock.sh", + content: await Bun.file( + path.join(moduleDir, "testdata", "kirocrew-mock.sh"), + ).text(), + }); + // Pre-create the dedicated pipx so the test does not hit PyPI. + await execContainer(id, [ + "bash", + "-c", + `mkdir -p "$HOME/.kiro/pipx-venv/bin" && printf '#!/bin/sh\\nexit 0\\n' > "$HOME/.kiro/pipx-venv/bin/pipx" && chmod +x "$HOME/.kiro/pipx-venv/bin/pipx"`, + ]); +}; + +// Polls a URL from inside the container until curl reports the expected +// status code, then prints " ". +const waitForStatus = async (id: string, url: string, status: string) => { + const resp = await execContainer(id, [ + "bash", + "-c", + `for i in $(seq 1 100); do + out=$(/usr/bin/curl -s -o /dev/null -w '%{http_code} %{redirect_url}' '${url}') + case "$out" in ${status}*) echo "$out"; exit 0 ;; esac + sleep 0.2 + done + echo "last: $out"; exit 1`, + ]); + if (resp.exitCode !== 0) { + console.log(resp.stdout); + console.log( + await execContainer(id, ["bash", "-c", `cat ${moduleLogDir}/*.log`]), + ); + } + expect(resp.exitCode).toBe(0); + return resp.stdout.trim(); +}; + +setDefaultTimeout(90 * 1000); + +describe("kiro-crew", async () => { + beforeAll(async () => { + await runTerraformInit(import.meta.dir); + }); + + test("install-uses-cached-binaries", async () => { + const { id, scripts } = await setup({ withMocks: true }); + await writeCurlMock(id, 22); + await runOrFail(id, "install", scripts.install); + const log = await readFileContainer(id, `${moduleLogDir}/install.log`); + expect(log).toContain("Kiro CLI already installed: kiro-cli 0.0.0-test"); + expect(log).toContain("Kiro Crew already installed: /usr/bin/kirocrew"); + expect(log).toContain("Kiro Crew install completed."); + const curl = await execContainer(id, ["test", "-e", "/tmp/curl-args"]); + expect(curl.exitCode).not.toBe(0); + }); + + test("install-runs-installers-when-missing", async () => { + const { id, scripts } = await setup(); + await writeInstallerFixtures(id); + await writeCurlMock(id); + await runOrFail(id, "install", scripts.install, { + PYTHONPATH: "/should/be/unset", + }); + const curlArgs = await readFileContainer(id, "/tmp/curl-args"); + expect(curlArgs).toContain("https://cli.kiro.dev/install"); + expect(curlArgs).toContain("https://download.crew.kiro.dev/cli.sh"); + const ran = await readFileContainer(id, "/tmp/installers-ran"); + expect(ran).toContain("kiro-cli installer ran"); + expect(ran).toContain( + "kiro-crew installer ran PYTHONPATH=unset pipx=/home/coder/.local/bin/pipx", + ); + const log = await readFileContainer(id, `${moduleLogDir}/install.log`); + expect(log).toContain("Kiro CLI ready."); + expect(log).toContain("Kiro Crew ready."); + }); + + test("install-reinstalls-when-use-cached-is-false", async () => { + const { id, scripts } = await setup({ + withMocks: true, + moduleVariables: { use_cached: "false" }, + }); + await writeInstallerFixtures(id); + await writeCurlMock(id); + await runOrFail(id, "install", scripts.install); + const ran = await readFileContainer(id, "/tmp/installers-ran"); + expect(ran).toContain("kiro-cli installer ran"); + expect(ran).toContain("kiro-crew installer ran"); + }); + + test("install-fails-when-download-fails", async () => { + const { id, scripts } = await setup(); + await writeCurlMock(id, 22); + const resp = await runScript(id, "install", scripts.install); + expect(resp.exitCode).not.toBe(0); + const log = await readFileContainer(id, `${moduleLogDir}/install.log`); + expect(log).toContain("Kiro CLI installation failed."); + }); + + test("install-writes-extra-mcp-json", async () => { + const extra = JSON.stringify({ + mcpServers: { demo: { command: "echo", args: ["it's"] } }, + }); + const { id, scripts } = await setup({ + withMocks: true, + moduleVariables: { extra_mcp_json: extra }, + }); + await runOrFail(id, "install", scripts.install); + const mcp = await readFileContainer( + id, + "/home/coder/.kiro/settings/mcp.json", + ); + expect(JSON.parse(mcp)).toEqual(JSON.parse(extra)); + }); + + test("install-merges-extra-mcp-json", async () => { + const extra = JSON.stringify({ + mcpServers: { demo: { command: "new" } }, + }); + const { id, scripts } = await setup({ + withMocks: true, + moduleVariables: { extra_mcp_json: extra }, + }); + await execContainer(id, [ + "bash", + "-c", + `mkdir -p ~/.kiro/settings && echo '{"mcpServers":{"coder":{"url":"u"},"demo":{"command":"old"}},"other":1}' > ~/.kiro/settings/mcp.json`, + ]); + await runOrFail(id, "install", scripts.install); + const mcp = JSON.parse( + await readFileContainer(id, "/home/coder/.kiro/settings/mcp.json"), + ); + expect(mcp).toEqual({ + mcpServers: { coder: { url: "u" }, demo: { command: "new" } }, + other: 1, + }); + }); + + test("start-launches-gateway-and-redirector", async () => { + const { id, scripts, state } = await setup({ + withMocks: true, + moduleVariables: { allowed_origins: "https://extra.example.com" }, + }); + const dashboardOrigin = state.outputs.dashboard_origin.value as string; + await runOrFail(id, "install", scripts.install); + const start = await runOrFail(id, "start", scripts.start, { + KIRO_AWS_BUILDER_ID_TOKEN: "builder-token", + }); + expect(start.stdout).toContain("Kiro Crew gateway launch dispatched."); + + await waitForStatus(id, `http://127.0.0.1:${redirectPort}/healthz`, "200"); + const health = await execContainer(id, [ + "/usr/bin/curl", + "-s", + `http://127.0.0.1:${gatewayPort}/health`, + ]); + expect(health.stdout).toBe("fake-gateway"); + + const redirect = await waitForStatus( + id, + `http://127.0.0.1:${redirectPort}/`, + "302", + ); + expect(redirect).toBe(`302 ${dashboardOrigin}/?token=test-token`); + + const gatewayEnv = await readFileContainer(id, "/tmp/kirocrew-gateway-env"); + expect(gatewayEnv).toContain(`KIROCREW_PORT=${gatewayPort}`); + expect(gatewayEnv).toContain( + `KIROCREW_CORS_ORIGINS=${dashboardOrigin},https://extra.example.com`, + ); + expect(gatewayEnv).toContain("KIRO_AWS_BUILDER_ID_TOKEN=builder-token"); + + const calls = await readFileContainer(id, "/tmp/kirocrew-calls"); + expect(calls).toContain(`config set dashboard.url ${dashboardOrigin}`); + + for (const file of ["start.log", "gateway.log", "redirector.log"]) { + const exists = await execContainer(id, [ + "test", + "-f", + `${moduleLogDir}/${file}`, + ]); + expect(exists.exitCode).toBe(0); + } + }); + + test("redirector-returns-503-until-gateway-ready", async () => { + const { id, scripts } = await setup({ withMocks: true }); + await runOrFail(id, "install", scripts.install); + await runOrFail(id, "start", scripts.start, { + KIROCREW_MOCK_GATEWAY: "hang", + }); + await waitForStatus(id, `http://127.0.0.1:${redirectPort}/healthz`, "200"); + const resp = await waitForStatus( + id, + `http://127.0.0.1:${redirectPort}/`, + "503", + ); + expect(resp).toStartWith("503"); + }); + + test("start-replaces-stale-listener-on-gateway-port", async () => { + const { id, scripts } = await setup({ withMocks: true }); + await runOrFail(id, "install", scripts.install); + await execContainer(id, [ + "bash", + "-c", + `setsid nohup python3 -m http.server --bind 127.0.0.1 ${gatewayPort} > /dev/null 2>&1 < /dev/null &`, + ]); + await waitForStatus(id, `http://127.0.0.1:${gatewayPort}/`, "200"); + await runOrFail(id, "start", scripts.start); + await waitForStatus(id, `http://127.0.0.1:${gatewayPort}/health`, "200"); + const health = await execContainer(id, [ + "/usr/bin/curl", + "-s", + `http://127.0.0.1:${gatewayPort}/health`, + ]); + expect(health.stdout).toBe("fake-gateway"); + }); + + test("start-fails-without-kirocrew", async () => { + const { id, scripts } = await setup(); + // The install wrapper normally creates the module directories. + await execContainer(id, [ + "mkdir", + "-p", + "/home/coder/.coder-modules/coder-labs/kiro-crew/scripts", + moduleLogDir, + ]); + const resp = await runScript(id, "start", scripts.start); + expect(resp.exitCode).not.toBe(0); + const log = await readFileContainer(id, `${moduleLogDir}/start.log`); + expect(log).toContain("kirocrew binary not found"); + }); +}); diff --git a/registry/coder-labs/modules/kiro-crew/main.tf b/registry/coder-labs/modules/kiro-crew/main.tf new file mode 100644 index 000000000..1f01fadea --- /dev/null +++ b/registry/coder-labs/modules/kiro-crew/main.tf @@ -0,0 +1,261 @@ +terraform { + required_version = ">= 1.9" + + required_providers { + coder = { + source = "coder/coder" + version = ">= 2.13" + } + } +} + +variable "agent_id" { + description = "The ID of a Coder agent." + type = string +} + +variable "icon" { + description = "The icon to use for the Kiro Crew apps and scripts." + type = string + default = "/icon/kiro.svg" +} + +variable "port" { + description = "Port the Kiro Crew gateway listens on inside the workspace." + type = number + default = 8899 + + validation { + condition = var.port >= 1 && var.port <= 65535 + error_message = "port must be between 1 and 65535." + } +} + +variable "redirect_port" { + description = "Loopback port for the token-minting redirector that fronts the dashboard so the Kiro Crew app tile opens an authenticated dashboard." + type = number + default = 8898 + + validation { + condition = var.redirect_port >= 1 && var.redirect_port <= 65535 + error_message = "redirect_port must be between 1 and 65535." + } +} + +variable "slug" { + description = "Slug for the visible Kiro Crew app. The hidden dashboard app uses \"-dashboard\"." + type = string + default = "kirocrew" + + validation { + condition = can(regex("^[a-z0-9]+(-[a-z0-9]+)*$", var.slug)) + error_message = "slug must contain only lowercase letters, numbers, and single hyphens between them." + } +} + +variable "order" { + description = "The order determines the position of the app in the UI presentation. The lowest order is shown first and apps with equal order are sorted by name (ascending order)." + type = number + default = null +} + +variable "group" { + description = "The name of a group that this app belongs to." + type = string + default = null +} + +variable "share" { + description = "The sharing level for the Kiro Crew apps: 'owner', 'authenticated', or 'public'." + type = string + default = "owner" + + validation { + condition = contains(["owner", "authenticated", "public"], var.share) + error_message = "share must be 'owner', 'authenticated', or 'public'." + } +} + +variable "open_in" { + description = "Where the Kiro Crew app opens: 'tab' or 'slim-window'." + type = string + default = "slim-window" + + validation { + condition = contains(["tab", "slim-window"], var.open_in) + error_message = "open_in must be 'tab' or 'slim-window'." + } +} + +variable "use_cached" { + description = "Skip running the Kiro CLI and Kiro Crew installers when the binaries are already present." + type = bool + default = true +} + +variable "extra_mcp_json" { + description = "Optional MCP configuration JSON to deep-merge into ~/.kiro/settings/mcp.json. Keys from this value replace existing keys with the same name; other existing keys are kept." + type = string + default = "" + + validation { + condition = var.extra_mcp_json == "" || can(jsondecode(var.extra_mcp_json)) + error_message = "extra_mcp_json must be empty or valid JSON." + } +} + +variable "aws_builder_id_token" { + description = "Optional AWS Builder ID access token, for example from a coder_external_auth data source. Exported to the workspace as KIRO_AWS_BUILDER_ID_TOKEN." + type = string + default = "" + sensitive = true +} + +variable "allowed_origins" { + description = "Comma-separated extra origins that the Kiro Crew gateway trusts in its Host/Origin allowlist, for example the Coder access URL origin. The dashboard app origin is always included. Passed to the gateway as KIROCREW_CORS_ORIGINS." + type = string + default = "" +} + +variable "dashboard_origin" { + description = "Override for the external origin of the hidden dashboard app, for example when the deployment's wildcard access URL differs from its access URL. When empty, the module derives \"://-dashboard----.\" from the access URL." + type = string + default = "" + + validation { + condition = var.dashboard_origin == "" || can(regex("^https?://[^/]+$", var.dashboard_origin)) + error_message = "dashboard_origin must be empty or an origin such as 'https://app.example.com' with no path." + } +} + +variable "pre_install_script" { + description = "Custom script to run before installing Kiro CLI and Kiro Crew, for example to install system packages." + type = string + default = null +} + +variable "post_install_script" { + description = "Custom script to run after installing Kiro CLI and Kiro Crew, before the gateway starts." + type = string + default = null +} + +data "coder_workspace" "me" {} + +data "coder_workspace_owner" "me" {} + +locals { + module_dir_name = ".coder-modules/coder-labs/kiro-crew" + module_directory = "$HOME/${local.module_dir_name}" + + dashboard_slug = "${var.slug}-dashboard" + access_scheme = startswith(data.coder_workspace.me.access_url, "http://") ? "http" : "https" + access_host = trimsuffix(replace(replace(data.coder_workspace.me.access_url, "https://", ""), "http://", ""), "/") + + # Coder serves subdomain apps at ----.. + # The redirector sends the browser here with a freshly minted token, and the + # gateway must trust this origin or it rejects proxied requests with + # "Host header not allowed.". + dashboard_origin = ( + var.dashboard_origin != "" + ? var.dashboard_origin + : "${local.access_scheme}://${local.dashboard_slug}--${data.coder_workspace.me.name}--${data.coder_workspace_owner.me.name}.${local.access_host}" + ) + + gateway_allowed_origins = ( + trimspace(var.allowed_origins) != "" + ? "${local.dashboard_origin},${trimspace(var.allowed_origins)}" + : local.dashboard_origin + ) + + install_script = templatefile("${path.module}/scripts/install.sh.tftpl", { + ARG_USE_CACHED = tostring(var.use_cached) + ARG_EXTRA_MCP_JSON = var.extra_mcp_json != "" ? base64encode(var.extra_mcp_json) : "" + }) + + start_script = templatefile("${path.module}/scripts/start.sh.tftpl", { + ARG_MODULE_DIRECTORY = local.module_directory + ARG_PORT = tostring(var.port) + ARG_REDIRECT_PORT = tostring(var.redirect_port) + ARG_ALLOWED_ORIGINS = base64encode(local.gateway_allowed_origins) + ARG_DASHBOARD_URL = base64encode(local.dashboard_origin) + }) +} + +# The token is exported through the agent environment so it is never rendered +# into a script. +resource "coder_env" "aws_builder_id_token" { + count = var.aws_builder_id_token != "" ? 1 : 0 + agent_id = var.agent_id + name = "KIRO_AWS_BUILDER_ID_TOKEN" + value = var.aws_builder_id_token +} + +module "coder_utils" { + source = "registry.coder.com/coder/coder-utils/coder" + version = "0.0.2" + + agent_id = var.agent_id + module_directory = local.module_directory + display_name_prefix = "Kiro Crew" + icon = var.icon + pre_install_script = var.pre_install_script + install_script = local.install_script + post_install_script = var.post_install_script + start_script = local.start_script +} + +# The visible tile points at a loopback redirector that mints a short-lived +# gateway token and redirects to the hidden dashboard app with ?token=. Kiro +# Crew requires a token for every browser request, and a static coder_app URL +# cannot embed a token that only exists at runtime. +resource "coder_app" "kiro_crew" { + agent_id = var.agent_id + slug = var.slug + display_name = "Kiro Crew" + url = "http://localhost:${var.redirect_port}/" + icon = var.icon + subdomain = true + share = var.share + order = var.order + group = var.group + open_in = var.open_in + + healthcheck { + url = "http://localhost:${var.redirect_port}/healthz" + interval = 5 + threshold = 6 + } +} + +# Serves the dashboard SPA and its websocket through Coder's subdomain proxy. +resource "coder_app" "kiro_crew_dashboard" { + agent_id = var.agent_id + slug = local.dashboard_slug + url = "http://localhost:${var.port}/" + subdomain = true + share = var.share + hidden = true + open_in = var.open_in + + healthcheck { + url = "http://localhost:${var.port}/health" + interval = 5 + threshold = 6 + } +} + +output "kirocrew_url" { + description = "Local URL of the Kiro Crew gateway inside the workspace." + value = "http://localhost:${var.port}/" +} + +output "dashboard_origin" { + description = "External origin of the hidden Kiro Crew dashboard app that the redirector sends the browser to." + value = local.dashboard_origin +} + +output "scripts" { + description = "Ordered list of coder exp sync names for the coder_script resources this module creates, in run order (pre_install, install, post_install, start). Scripts that were not configured are absent from the list." + value = module.coder_utils.scripts +} diff --git a/registry/coder-labs/modules/kiro-crew/scripts/install.sh.tftpl b/registry/coder-labs/modules/kiro-crew/scripts/install.sh.tftpl new file mode 100644 index 000000000..f1c2fbe20 --- /dev/null +++ b/registry/coder-labs/modules/kiro-crew/scripts/install.sh.tftpl @@ -0,0 +1,177 @@ +#!/usr/bin/env bash + +set -euo pipefail + +command_exists() { + command -v "$1" > /dev/null 2>&1 +} + +ARG_USE_CACHED='${ARG_USE_CACHED}' +ARG_EXTRA_MCP_JSON=$(echo -n '${ARG_EXTRA_MCP_JSON}' | base64 -d) + +KIRO_CREW_VENV_BIN="$HOME/.kiro/crew-venv/bin/kirocrew" + +mkdir -p "$HOME/.local/bin" +export PATH="$HOME/.local/bin:$HOME/bin:$PATH" + +echo "--------------------------------" +printf "use_cached: %s\n" "$${ARG_USE_CACHED}" +printf "extra_mcp_json: %s\n" "$${ARG_EXTRA_MCP_JSON:+(set)}" +echo "--------------------------------" + +# Downloads an installer to a temporary file so a failed or partial download +# is never executed. Runs in a subshell to scope the cleanup trap. +run_installer() ( + local url="$1" + local interpreter="$2" + local installer_file + installer_file=$(mktemp) + trap 'rm -f "$${installer_file}"' EXIT + + if ! curl --fail --silent --show-error --location \ + --retry 2 --retry-delay 1 --retry-all-errors \ + --connect-timeout 10 --max-time 300 \ + --output "$${installer_file}" "$${url}"; then + echo "ERROR: Installer could not be downloaded from $${url}." >&2 + return 1 + fi + + if ! "$${interpreter}" -n "$${installer_file}"; then + echo "ERROR: Installer downloaded from $${url} is not a valid script." >&2 + return 1 + fi + + "$${interpreter}" "$${installer_file}" < /dev/null +) + +link_into_script_bin_dir() { + local name="$1" + local target="$2" + if [ -n "$${CODER_SCRIPT_BIN_DIR:-}" ] && [ ! -e "$${CODER_SCRIPT_BIN_DIR}/$${name}" ]; then + ln -s "$${target}" "$${CODER_SCRIPT_BIN_DIR}/$${name}" + echo "Created symlink: $${CODER_SCRIPT_BIN_DIR}/$${name} -> $${target}" + fi +} + +install_kiro_cli() { + if [ "$${ARG_USE_CACHED}" = "true" ] && command_exists kiro-cli; then + echo "Kiro CLI already installed: $(kiro-cli --version 2> /dev/null || echo 'unknown version')" + else + echo "Installing Kiro CLI..." + # The installer prompts on /dev/tty when kiro-cli already exists in + # ~/.local/bin, so remove the old binaries first as it would on confirm. + rm -f "$HOME/.local/bin/kiro-cli" "$HOME/.local/bin/kiro-cli-chat" + if ! run_installer "https://cli.kiro.dev/install" bash; then + echo "ERROR: Kiro CLI installation failed." >&2 + exit 1 + fi + hash -r + fi + + if ! command_exists kiro-cli; then + echo "ERROR: kiro-cli was not found on PATH after installation." >&2 + exit 1 + fi + link_into_script_bin_dir kiro-cli "$(command -v kiro-cli)" + echo "Kiro CLI ready." +} + +# The Kiro Crew installer shells out to the ambient pipx. On images whose +# system Python is 3.13 or newer, the distro pipx crashes while parsing an +# empty `pip list --format=json`. Build a pipx on a supported interpreter +# (python3.12, then python3.11) and put it first on PATH. +prepare_pipx() { + local pipx_python=python3 + if command_exists python3.12; then + pipx_python=python3.12 + elif command_exists python3.11; then + pipx_python=python3.11 + fi + + if [ ! -x "$HOME/.kiro/pipx-venv/bin/pipx" ]; then + mkdir -p "$HOME/.kiro" + "$${pipx_python}" -m venv "$HOME/.kiro/pipx-venv" \ + && "$HOME/.kiro/pipx-venv/bin/pip" install -q --upgrade pip pipx \ + || echo "WARN: Dedicated pipx venv build failed; using the system pipx." + fi + if [ -x "$HOME/.kiro/pipx-venv/bin/pipx" ]; then + ln -sf "$HOME/.kiro/pipx-venv/bin/pipx" "$HOME/.local/bin/pipx" + fi + hash -r +} + +install_kiro_crew() { + if [ "$${ARG_USE_CACHED}" = "true" ] && command_exists kirocrew; then + echo "Kiro Crew already installed: $(command -v kirocrew)" + elif [ "$${ARG_USE_CACHED}" = "true" ] && [ -x "$${KIRO_CREW_VENV_BIN}" ]; then + echo "Kiro Crew already installed: $${KIRO_CREW_VENV_BIN}" + else + echo "Installing Kiro Crew..." + # An inherited PYTHONPATH or PYTHONHOME makes pip treat foreign packages + # as already satisfied, which leaves the managed venv without its + # dependencies (ImportError on the first gateway start). + unset PYTHONPATH PYTHONHOME + prepare_pipx + if ! run_installer "https://download.crew.kiro.dev/cli.sh" sh; then + echo "ERROR: Kiro Crew installation failed." >&2 + exit 1 + fi + hash -r + fi + + local kirocrew_bin="" + if command_exists kirocrew; then + kirocrew_bin="$(command -v kirocrew)" + elif [ -x "$${KIRO_CREW_VENV_BIN}" ]; then + kirocrew_bin="$${KIRO_CREW_VENV_BIN}" + else + echo "ERROR: kirocrew was not found after installation." >&2 + exit 1 + fi + link_into_script_bin_dir kirocrew "$${kirocrew_bin}" + echo "Kiro Crew ready." +} + +apply_extra_mcp() { + if [ -z "$${ARG_EXTRA_MCP_JSON}" ]; then + return + fi + + local mcp_file="$HOME/.kiro/settings/mcp.json" + echo "Applying extra MCP configuration to $${mcp_file}..." + mkdir -p "$(dirname "$${mcp_file}")" + + if [ -f "$${mcp_file}" ] && command_exists python3; then + python3 - "$${mcp_file}" "$${ARG_EXTRA_MCP_JSON}" << 'PYEOF' +import json +import sys + +with open(sys.argv[1]) as f: + existing = json.load(f) +new = json.loads(sys.argv[2]) + + +def merge(a, b): + for k, v in b.items(): + if k in a and isinstance(a[k], dict) and isinstance(v, dict): + merge(a[k], v) + else: + a[k] = v + + +merge(existing, new) +with open(sys.argv[1], "w") as f: + f.write(json.dumps(existing, indent=2)) +PYEOF + echo "MCP configuration merged." + else + printf '%s' "$${ARG_EXTRA_MCP_JSON}" > "$${mcp_file}" + echo "MCP configuration written." + fi +} + +install_kiro_cli +install_kiro_crew +apply_extra_mcp + +echo "Kiro Crew install completed." diff --git a/registry/coder-labs/modules/kiro-crew/scripts/start.sh.tftpl b/registry/coder-labs/modules/kiro-crew/scripts/start.sh.tftpl new file mode 100644 index 000000000..bc988d938 --- /dev/null +++ b/registry/coder-labs/modules/kiro-crew/scripts/start.sh.tftpl @@ -0,0 +1,160 @@ +#!/usr/bin/env bash + +set -euo pipefail + +# module_directory is a constant set in main.tf, so double-quoting it to +# expand its literal "$HOME" at runtime is safe. +ARG_MODULE_DIRECTORY="${ARG_MODULE_DIRECTORY}" +ARG_PORT='${ARG_PORT}' +ARG_REDIRECT_PORT='${ARG_REDIRECT_PORT}' +ARG_ALLOWED_ORIGINS=$(echo -n '${ARG_ALLOWED_ORIGINS}' | base64 -d) +ARG_DASHBOARD_URL=$(echo -n '${ARG_DASHBOARD_URL}' | base64 -d) + +LOG_DIR="$${ARG_MODULE_DIRECTORY}/logs" +GATEWAY_LOG="$${LOG_DIR}/gateway.log" +REDIRECTOR_LOG="$${LOG_DIR}/redirector.log" +CREW_DIR="$HOME/.kiro/crew" +LAUNCHER_PATH="$${CREW_DIR}/start-gateway.sh" +REDIRECTOR_PATH="$${CREW_DIR}/redirect-gateway.py" + +mkdir -p "$${LOG_DIR}" "$${CREW_DIR}" +export PATH="$HOME/.local/bin:$HOME/bin:$PATH" + +# Everything below is best effort and must reach the launch step, so disable +# errexit for the remainder of the script. +set +e + +# Stops whatever listens on a port. Never match by process name: the agent +# runs scripts with their full text in argv, so a pattern such as "kirocrew" +# would also match, and terminate, this script. +free_port() { + local port="$1" + if command -v fuser > /dev/null 2>&1; then + fuser -k "$${port}/tcp" > /dev/null 2>&1 || true + else + local port_pid + port_pid="$(ss -ltnHp "sport = :$${port}" 2> /dev/null | grep -oE 'pid=[0-9]+' | head -1 | cut -d= -f2)" + if [ -n "$${port_pid}" ] && [ "$${port_pid}" != "$$" ] && [ "$${port_pid}" != "$PPID" ]; then + kill "$${port_pid}" > /dev/null 2>&1 || true + fi + fi +} + +KIROCREW_CMD="" +if command -v kirocrew > /dev/null 2>&1; then + KIROCREW_CMD="$(command -v kirocrew)" +elif [ -x "$HOME/.kiro/crew-venv/bin/kirocrew" ]; then + KIROCREW_CMD="$HOME/.kiro/crew-venv/bin/kirocrew" +else + echo "ERROR: kirocrew binary not found. Check the install log in $${LOG_DIR}/install.log." >&2 + exit 1 +fi + +free_port "$${ARG_PORT}" +free_port "$${ARG_REDIRECT_PORT}" + +echo "Using kirocrew binary: $${KIROCREW_CMD}" +echo "Gateway port: $${ARG_PORT}" +echo "Gateway log: $${GATEWAY_LOG}" + +if [ -n "$${KIRO_API_KEY:-}" ]; then + echo "Headless Kiro auth: KIRO_API_KEY detected; kiro-cli authenticates non-interactively." +else + echo "Headless Kiro auth: KIRO_API_KEY not set; sign in with 'kiro-cli login', or create a Coder user secret that sets KIRO_API_KEY." +fi + +# The launcher is written with a quoted heredoc so nothing expands at write +# time. Values reach it through the environment at launch. +# +# Kiro Crew sandboxes agent subprocesses with a Linux user namespace. Many +# Kubernetes pods set user.max_user_namespaces=0, so when unshare fails the +# launcher allows unsandboxed exec and the pod is the isolation boundary. +cat > "$${LAUNCHER_PATH}" << 'LAUNCHER' +#!/usr/bin/env bash +# Trust the Coder proxy origins in the gateway's Host/Origin allowlist. +[ -n "$KC_ORIGINS" ] && export KIROCREW_CORS_ORIGINS="$KC_ORIGINS" +# Scope token cookies to the dashboard app's public origin. +[ -n "$KC_DASHBOARD_URL" ] && "$KC_CMD" config set dashboard.url "$KC_DASHBOARD_URL" > /dev/null 2>&1 || true +if ! unshare --user --map-root-user true > /dev/null 2>&1; then + "$KC_CMD" config set agent.sandbox_allow_unsandboxed_exec true > /dev/null 2>&1 || true +fi +exec env KIROCREW_PORT="$KC_PORT" "$KC_CMD" gateway +LAUNCHER +chmod +x "$${LAUNCHER_PATH}" + +# The gateway can take a while to cold start, and the agent terminates start +# scripts that run too long, so the gateway runs in a detached session and +# readiness is reported by the dashboard app healthcheck. +: > "$${GATEWAY_LOG}" 2> /dev/null || true +setsid nohup env \ + KC_CMD="$${KIROCREW_CMD}" \ + KC_PORT="$${ARG_PORT}" \ + KC_ORIGINS="$${ARG_ALLOWED_ORIGINS}" \ + KC_DASHBOARD_URL="$${ARG_DASHBOARD_URL}" \ + bash "$${LAUNCHER_PATH}" >> "$${GATEWAY_LOG}" 2>&1 < /dev/null & +disown 2> /dev/null || true +echo "Kiro Crew gateway launch dispatched." + +# The visible app tile points at this loopback redirector. Each request mints +# a short-lived token from the gateway's loopback secret and redirects to the +# dashboard app with ?token=. /healthz answers without minting a token so the +# app healthcheck does not create tokens. +cat > "$${REDIRECTOR_PATH}" << 'PYEOF' +import http.server +import json +import os +import urllib.request + +REDIR_PORT = int(os.environ["REDIR_PORT"]) +GW_BASE = "http://127.0.0.1:" + os.environ["GW_PORT"] +DASH = os.environ["DASH_ORIGIN"].rstrip("/") +SECRET = os.path.expanduser("~/.kiro/crew/.local_secret") + + +class Handler(http.server.BaseHTTPRequestHandler): + def _mint(self): + with open(SECRET) as f: + secret = f.read().strip() + req = urllib.request.Request( + GW_BASE + "/api/token/local?ttl=24h", + headers={"X-Local-Secret": secret}, + ) + with urllib.request.urlopen(req, timeout=5) as resp: + return json.load(resp).get("token", "") + + def do_GET(self): + if self.path.startswith("/healthz"): + self.send_response(200) + self.end_headers() + self.wfile.write(b"ok") + return + try: + token = self._mint() + except Exception: + token = "" + if not token: + self.send_response(503) + self.end_headers() + self.wfile.write(b"Kiro Crew gateway still starting - retry shortly") + return + self.send_response(302) + self.send_header("Location", DASH + "/?token=" + token) + self.end_headers() + + def log_message(self, *args): + pass + + +http.server.HTTPServer(("127.0.0.1", REDIR_PORT), Handler).serve_forever() +PYEOF + +: > "$${REDIRECTOR_LOG}" 2> /dev/null || true +setsid nohup env \ + REDIR_PORT="$${ARG_REDIRECT_PORT}" \ + GW_PORT="$${ARG_PORT}" \ + DASH_ORIGIN="$${ARG_DASHBOARD_URL}" \ + python3 "$${REDIRECTOR_PATH}" >> "$${REDIRECTOR_LOG}" 2>&1 < /dev/null & +disown 2> /dev/null || true +echo "Kiro Crew redirector dispatched on port $${ARG_REDIRECT_PORT} -> $${ARG_DASHBOARD_URL}" + +exit 0 diff --git a/registry/coder-labs/modules/kiro-crew/testdata/fake-gateway.py b/registry/coder-labs/modules/kiro-crew/testdata/fake-gateway.py new file mode 100644 index 000000000..c4f81e244 --- /dev/null +++ b/registry/coder-labs/modules/kiro-crew/testdata/fake-gateway.py @@ -0,0 +1,36 @@ +import http.server +import json +import os + +PORT = int(os.environ["KIROCREW_PORT"]) +SECRET_PATH = os.path.expanduser("~/.kiro/crew/.local_secret") + + +class Handler(http.server.BaseHTTPRequestHandler): + def do_GET(self): + if self.path == "/health": + self.send_response(200) + self.end_headers() + self.wfile.write(b"fake-gateway") + return + if self.path.startswith("/api/token/local"): + with open(SECRET_PATH) as f: + secret = f.read().strip() + if self.headers.get("X-Local-Secret") != secret: + self.send_response(401) + self.end_headers() + return + body = json.dumps({"token": "test-token"}).encode() + self.send_response(200) + self.send_header("Content-Type", "application/json") + self.end_headers() + self.wfile.write(body) + return + self.send_response(404) + self.end_headers() + + def log_message(self, *args): + pass + + +http.server.HTTPServer(("127.0.0.1", PORT), Handler).serve_forever() diff --git a/registry/coder-labs/modules/kiro-crew/testdata/kiro-cli-mock.sh b/registry/coder-labs/modules/kiro-crew/testdata/kiro-cli-mock.sh new file mode 100644 index 000000000..af2c62899 --- /dev/null +++ b/registry/coder-labs/modules/kiro-crew/testdata/kiro-cli-mock.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +if [[ "$1" == "--version" ]]; then + echo "kiro-cli 0.0.0-test" + exit 0 +fi + +echo "kiro-cli invoked with: $*" +exit 0 diff --git a/registry/coder-labs/modules/kiro-crew/testdata/kirocrew-mock.sh b/registry/coder-labs/modules/kiro-crew/testdata/kirocrew-mock.sh new file mode 100644 index 000000000..b2eb11639 --- /dev/null +++ b/registry/coder-labs/modules/kiro-crew/testdata/kirocrew-mock.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash + +# Stand-in for the kirocrew CLI. Records every invocation and, for +# `kirocrew gateway`, records the environment it was launched with and starts +# a fake gateway that serves /health and /api/token/local. + +echo "$*" >> /tmp/kirocrew-calls + +case "$1" in + --version) + echo "kirocrew 0.0.0-test" + ;; + gateway) + { + echo "KIROCREW_PORT=$KIROCREW_PORT" + echo "KIROCREW_CORS_ORIGINS=${KIROCREW_CORS_ORIGINS:-}" + echo "KIRO_AWS_BUILDER_ID_TOKEN=${KIRO_AWS_BUILDER_ID_TOKEN:-}" + } > /tmp/kirocrew-gateway-env + if [ "${KIROCREW_MOCK_GATEWAY:-serve}" = "hang" ]; then + exec sleep infinity + fi + mkdir -p "$HOME/.kiro/crew" + echo "test-secret" > "$HOME/.kiro/crew/.local_secret" + exec python3 /tmp/fake-gateway.py + ;; +esac +exit 0 diff --git a/registry/coder-labs/templates/kiro-crew/README.md b/registry/coder-labs/templates/kiro-crew/README.md new file mode 100644 index 000000000..0e42b5e88 --- /dev/null +++ b/registry/coder-labs/templates/kiro-crew/README.md @@ -0,0 +1,62 @@ +--- +display_name: Kiro Crew on Kubernetes +description: Kubernetes workspaces that run the Kiro Crew gateway and web dashboard, with Kiro CLI, code-server, and a persistent home volume. +icon: ../../../../.icons/kiro.svg +verified: false +tags: [kubernetes, container, kiro, ai, agent, mcp] +--- + +# Kiro Crew on Kubernetes + +Provision Kubernetes pods as [Coder workspaces](https://coder.com/docs/workspaces) that run [Kiro Crew](https://github.com/kirodotdev/KiroCrew). Kiro Crew is a persistent development workspace built on Kiro CLI: a long-running gateway owns memory, transcripts, and artifacts, and a web dashboard lets you chat with agents, run multi-step tasks, schedule jobs, and extend agents with skills and MCP servers. + +The template uses the [kiro-crew](../../modules/kiro-crew/README.md) module to install Kiro CLI and Kiro Crew, start the gateway, and publish the dashboard through Coder's authenticated subdomain proxy. The **Kiro Crew** app opens an authenticated dashboard without a separate token step. + +Each workspace also includes: + +- A **Kiro CLI** terminal app for interactive sign-in. +- code-server and the Kiro IDE app. +- The Coder MCP server in `~/.kiro/settings/mcp.json`, so agents can work with Coder workspaces and templates. +- An optional repository clone on start. +- A persistent volume at `/home/coder` that keeps installs, Kiro credentials, and Kiro Crew state across restarts. +- CPU, memory, and disk usage on the workspace page. + +## Prerequisites + +### Infrastructure + +- A Kubernetes cluster that Coder can provision into. The namespace must exist before you create workspaces. If the Coder host runs outside the cluster, set `use_kubeconfig = true` and provide `~/.kube/config` on the Coder host. +- A storage class that can provision `ReadWriteOnce` persistent volumes. +- [Wildcard subdomain apps](https://coder.com/docs/admin/setup#wildcard-access-url). The dashboard is a subdomain app, and the dashboard app tile does not resolve without a wildcard access URL. +- Outbound access from workspace pods to the Ubuntu package mirrors, the deadsnakes PPA, PyPI, and the Kiro download hosts. + +The default image pins `codercom/enterprise-base` to Ubuntu 24.04 (Python 3.12). Images based on newer Ubuntu releases ship a `pipx` that breaks the Kiro Crew installer. + +### Kiro account + +Agents cannot call models until Kiro CLI is authenticated. Kiro credentials are stored in the persistent home volume, so you only sign in once per workspace. + +- **Headless:** before you start the workspace, create a Coder user secret that sets `KIRO_API_KEY`: + + ```sh + coder secret create kiro-api-key --description "Kiro CLI headless auth" --env KIRO_API_KEY + ``` + + The gateway inherits the variable and Kiro CLI authenticates without a browser. Generating API keys requires a Kiro subscription that supports them; on enterprise-managed accounts an administrator must enable it. Rotating or deleting the Coder secret does not revoke the key in Kiro. + +- **Interactive:** open the **Kiro CLI** app, run `kiro-cli login`, and complete the device-code sign-in with AWS Builder ID or IAM Identity Center. Check the result with `kiro-cli whoami`. + +If agents need AWS credentials, set `service_account_name` to a service account that is mapped to an IAM role, or run `aws configure` in the workspace. + +## Use the dashboard + +1. Authenticate Kiro CLI as described above. +1. Open the **Kiro Crew** app. The gateway starts in the background, so the app can report `503` or show as unhealthy for a short time after the workspace starts. +1. Start a session. If model calls fail with an authentication error, complete Kiro sign-in and retry. + +Gateway, redirector, and script logs are in `~/.coder-modules/coder-labs/kiro-crew/logs/`. + +## Notes + +- Kiro Crew sandboxes agent subprocesses with Linux user namespaces. Many Kubernetes nodes disable them (`user.max_user_namespaces=0`); in that case the module allows unsandboxed execution and the pod is the isolation boundary. +- The Coder MCP configuration stores your Coder session token in `~/.kiro/settings/mcp.json` on the workspace volume. Remove the `coder` entry from that file if you do not want agents to act on your behalf in Coder. diff --git a/registry/coder-labs/templates/kiro-crew/kiro-crew-prereqs.sh b/registry/coder-labs/templates/kiro-crew/kiro-crew-prereqs.sh new file mode 100644 index 000000000..d95cb797f --- /dev/null +++ b/registry/coder-labs/templates/kiro-crew/kiro-crew-prereqs.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +# Pre-install script for the kiro-crew module. Installs the system packages +# that the Kiro CLI and Kiro Crew installers need. +set -euo pipefail + +sudo apt-get update -qq + +# curl, unzip, openssl, and sha256sum (coreutils) are used by the installers; +# Kiro Crew verifies its signed manifest with openssl and sha256sum. +sudo apt-get install -y -qq \ + curl \ + unzip \ + gnupg \ + dirmngr \ + openssl \ + ca-certificates \ + python3-venv \ + python3-pip + +# Kiro Crew creates its managed venv at ~/.kiro/crew-venv with the newest of +# python3.12, python3.11, and python3.10. Install python3.11 and its venv +# module as well so the installer has a tested interpreter. +if ! python3.11 --version > /dev/null 2>&1; then + echo "Installing Python 3.11 from the deadsnakes PPA..." + sudo apt-get install -y -qq software-properties-common + sudo add-apt-repository -y ppa:deadsnakes/ppa + sudo apt-get update -qq +fi +sudo apt-get install -y -qq \ + python3.11 \ + python3.11-venv \ + python3.11-dev + +echo "Kiro Crew prerequisites installed." diff --git a/registry/coder-labs/templates/kiro-crew/main.tf b/registry/coder-labs/templates/kiro-crew/main.tf new file mode 100644 index 000000000..06284989d --- /dev/null +++ b/registry/coder-labs/templates/kiro-crew/main.tf @@ -0,0 +1,379 @@ +terraform { + required_providers { + coder = { + source = "coder/coder" + version = ">= 2.13" + } + kubernetes = { + source = "hashicorp/kubernetes" + version = ">= 2.37" + } + } +} + +variable "use_kubeconfig" { + type = bool + description = <<-EOF + Use host kubeconfig? (true/false) + + Set this to false if the Coder host is itself running as a Pod on the same + Kubernetes cluster as you are deploying workspaces to. + + Set this to true if the Coder host is running outside the Kubernetes cluster + for workspaces. A valid "~/.kube/config" must be present on the Coder host. + EOF + default = false +} + +variable "namespace" { + type = string + description = "Kubernetes namespace to create workspaces in. The namespace must exist before workspaces are created." + default = "coder" +} + +variable "service_account_name" { + type = string + description = "Optional Kubernetes service account for workspace pods, for example one mapped to an IAM role. Leave empty to use the namespace's default service account." + default = "" +} + +variable "image" { + type = string + description = <<-EOF + Container image for workspaces. The default pins codercom/enterprise-base + to Ubuntu 24.04 (Python 3.12). Images whose system Python is 3.13 or newer + ship a pipx that breaks the Kiro Crew installer. + EOF + default = "codercom/enterprise-base@sha256:16031b4de2e099884ad6b4057bffeaf03380a76eddf46c193b5d0767e738ba96" +} + +provider "kubernetes" { + # Authenticate via ~/.kube/config or a Coder-specific ServiceAccount, depending on admin preferences. + config_path = var.use_kubeconfig == true ? "~/.kube/config" : null +} + +locals { + home_dir = "/home/coder" +} + +data "coder_parameter" "cpu" { + name = "cpu" + display_name = "CPU cores" + description = "CPU core limit for the workspace." + type = "number" + form_type = "input" + icon = "/icon/memory.svg" + default = 2 + mutable = true + order = 1 + validation { + min = 2 + max = 8 + } +} + +data "coder_parameter" "memory" { + name = "memory" + display_name = "Memory (GB)" + description = "Memory limit for the workspace, in GB. The Kiro Crew gateway and its agents can use several GB." + type = "number" + form_type = "input" + icon = "/icon/memory.svg" + default = 8 + mutable = true + order = 2 + validation { + min = 4 + max = 16 + } +} + +data "coder_parameter" "home_disk_size" { + name = "home_disk_size" + display_name = "Home disk size (GB)" + description = "Size of the persistent volume mounted at ${local.home_dir}, in GB. It can only grow." + type = "number" + form_type = "slider" + icon = "/icon/database.svg" + default = 30 + mutable = true + order = 3 + validation { + min = 10 + max = 50 + monotonic = "increasing" + } +} + +data "coder_parameter" "git_repo" { + name = "git_repo" + display_name = "Git repository" + description = "Optional repository to clone into the workspace on start. Leave empty to skip." + type = "string" + icon = "/icon/git.svg" + default = "" + mutable = true + order = 4 +} + +data "coder_workspace" "me" {} +data "coder_workspace_owner" "me" {} + +resource "coder_agent" "main" { + arch = "amd64" + os = "linux" + + # The agent expands $PATH, so the agent's own directory (and the coder + # binary that module scripts call) stays on PATH. + env = { + PATH = "${local.home_dir}/.local/bin:${local.home_dir}/bin:$PATH" + } + + display_apps { + vscode = false + vscode_insiders = false + web_terminal = true + ssh_helper = false + } + + # The Kiro Crew gateway and its agent subprocesses can be CPU, memory, and + # disk intensive, so surface usage on the workspace page. + metadata { + display_name = "CPU Usage" + key = "0_cpu_usage" + script = "coder stat cpu" + interval = 10 + timeout = 1 + } + + metadata { + display_name = "RAM Usage" + key = "1_ram_usage" + script = "coder stat mem" + interval = 10 + timeout = 1 + } + + metadata { + display_name = "Home Disk" + key = "2_home_disk" + script = "coder stat disk --path ${local.home_dir}" + interval = 60 + timeout = 1 + } + + startup_script_behavior = "blocking" + startup_script = file("${path.module}/startup.sh") +} + +module "coder-login" { + source = "registry.coder.com/coder/coder-login/coder" + version = "1.1.1" + agent_id = coder_agent.main.id +} + +module "code-server" { + source = "registry.coder.com/coder/code-server/coder" + version = "1.6.0" + agent_id = coder_agent.main.id + folder = local.home_dir + subdomain = false + order = 0 +} + +module "kiro" { + source = "registry.coder.com/coder/kiro/coder" + version = "1.2.2" + agent_id = coder_agent.main.id + order = 1 +} + +module "kiro-crew" { + source = "registry.coder.com/coder-labs/kiro-crew/coder" + version = "1.0.0" + agent_id = coder_agent.main.id + order = 3 + + # Installs the system packages the Kiro CLI and Kiro Crew installers need + # before the module's install script runs. + pre_install_script = file("${path.module}/kiro-crew-prereqs.sh") +} + +# Terminal app for interactive Kiro CLI sign-in (`kiro-cli login`). +resource "coder_app" "kiro_cli" { + agent_id = coder_agent.main.id + slug = "kiro-cli" + display_name = "Kiro CLI" + icon = "/icon/kiro.svg" + command = "kiro-cli" + share = "owner" + order = 2 +} + +module "git-config" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/coder/git-config/coder" + version = "1.0.35" + agent_id = coder_agent.main.id +} + +module "git-clone" { + count = data.coder_parameter.git_repo.value != "" ? data.coder_workspace.me.start_count : 0 + source = "registry.coder.com/coder/git-clone/coder" + version = "2.0.5" + agent_id = coder_agent.main.id + url = data.coder_parameter.git_repo.value +} + +resource "kubernetes_persistent_volume_claim_v1" "home" { + metadata { + name = "coder-${data.coder_workspace.me.id}-home" + namespace = var.namespace + labels = { + "app.kubernetes.io/name" = "coder-pvc" + "app.kubernetes.io/instance" = "coder-pvc-${data.coder_workspace.me.id}" + "app.kubernetes.io/part-of" = "coder" + "com.coder.resource" = "true" + "com.coder.workspace.id" = data.coder_workspace.me.id + "com.coder.workspace.name" = data.coder_workspace.me.name + "com.coder.user.id" = data.coder_workspace_owner.me.id + "com.coder.user.username" = data.coder_workspace_owner.me.name + } + annotations = { + "com.coder.user.email" = data.coder_workspace_owner.me.email + } + } + wait_until_bound = false + spec { + access_modes = ["ReadWriteOnce"] + resources { + requests = { + storage = "${data.coder_parameter.home_disk_size.value}Gi" + } + } + } +} + +resource "kubernetes_deployment_v1" "main" { + count = data.coder_workspace.me.start_count + depends_on = [kubernetes_persistent_volume_claim_v1.home] + wait_for_rollout = false + + metadata { + name = "coder-${data.coder_workspace.me.id}" + namespace = var.namespace + labels = { + "app.kubernetes.io/name" = "coder-workspace" + "app.kubernetes.io/instance" = "coder-workspace-${data.coder_workspace.me.id}" + "app.kubernetes.io/part-of" = "coder" + "com.coder.resource" = "true" + "com.coder.workspace.id" = data.coder_workspace.me.id + "com.coder.workspace.name" = data.coder_workspace.me.name + "com.coder.user.id" = data.coder_workspace_owner.me.id + "com.coder.user.username" = data.coder_workspace_owner.me.name + } + annotations = { + "com.coder.user.email" = data.coder_workspace_owner.me.email + } + } + + spec { + replicas = 1 + selector { + match_labels = { + "app.kubernetes.io/name" = "coder-workspace" + "app.kubernetes.io/instance" = "coder-workspace-${data.coder_workspace.me.id}" + "app.kubernetes.io/part-of" = "coder" + "com.coder.resource" = "true" + "com.coder.workspace.id" = data.coder_workspace.me.id + "com.coder.workspace.name" = data.coder_workspace.me.name + "com.coder.user.id" = data.coder_workspace_owner.me.id + "com.coder.user.username" = data.coder_workspace_owner.me.name + } + } + strategy { + type = "Recreate" + } + + template { + metadata { + labels = { + "app.kubernetes.io/name" = "coder-workspace" + "app.kubernetes.io/instance" = "coder-workspace-${data.coder_workspace.me.id}" + "app.kubernetes.io/part-of" = "coder" + "com.coder.resource" = "true" + "com.coder.workspace.id" = data.coder_workspace.me.id + "com.coder.workspace.name" = data.coder_workspace.me.name + "com.coder.user.id" = data.coder_workspace_owner.me.id + "com.coder.user.username" = data.coder_workspace_owner.me.name + } + } + spec { + security_context { + run_as_user = 1000 + fs_group = 1000 + } + service_account_name = var.service_account_name != "" ? var.service_account_name : null + + container { + name = "dev" + image = var.image + image_pull_policy = "IfNotPresent" + command = ["sh", "-c", coder_agent.main.init_script] + security_context { + run_as_user = "1000" + } + + env { + name = "CODER_AGENT_TOKEN" + value = coder_agent.main.token + } + + resources { + requests = { + "cpu" = "250m" + "memory" = "512Mi" + } + limits = { + "cpu" = "${data.coder_parameter.cpu.value}" + "memory" = "${data.coder_parameter.memory.value}Gi" + } + } + + volume_mount { + mount_path = local.home_dir + name = "home" + read_only = false + } + } + + volume { + name = "home" + persistent_volume_claim { + claim_name = kubernetes_persistent_volume_claim_v1.home.metadata[0].name + read_only = false + } + } + + affinity { + # Spread workspaces across nodes when possible. + pod_anti_affinity { + preferred_during_scheduling_ignored_during_execution { + weight = 1 + pod_affinity_term { + topology_key = "kubernetes.io/hostname" + label_selector { + match_expressions { + key = "app.kubernetes.io/name" + operator = "In" + values = ["coder-workspace"] + } + } + } + } + } + } + } + } + } +} diff --git a/registry/coder-labs/templates/kiro-crew/startup.sh b/registry/coder-labs/templates/kiro-crew/startup.sh new file mode 100644 index 000000000..668c13dc5 --- /dev/null +++ b/registry/coder-labs/templates/kiro-crew/startup.sh @@ -0,0 +1,74 @@ +#!/usr/bin/env bash +# Workspace startup script. Runs as the coder user inside the workspace pod. +set -euo pipefail + +mkdir -p "$HOME/.ssh" "$HOME/bin" "$HOME/.local/bin" +chmod 700 "$HOME/.ssh" +ssh-keyscan -t ed25519,rsa github.com >> "$HOME/.ssh/known_hosts" 2> /dev/null || echo "Warning: could not fetch GitHub host keys." +chmod 600 "$HOME/.ssh/known_hosts" 2> /dev/null || true + +# Point Kiro at the Coder MCP server so agents can work with Coder itself. +# CODER_URL and CODER_SESSION_TOKEN are set by the coder-login module. +if [ -n "${CODER_URL:-}" ] && [ -n "${CODER_SESSION_TOKEN:-}" ]; then + echo "Configuring the Coder MCP server for Kiro..." + mkdir -p "$HOME/.kiro/settings" + CODER_MCP_URL="${CODER_URL%/}/api/experimental/mcp/http" + cat > "$HOME/.kiro/settings/mcp.json" << EOF +{ + "mcpServers": { + "coder": { + "url": "$CODER_MCP_URL", + "headers": { + "Authorization": "Bearer $CODER_SESSION_TOKEN" + }, + "autoApprove": [ + "coder_workspace_edit_file", + "coder_workspace_read_file", + "coder_get_task_status", + "coder_workspace_write_file", + "coder_workspace_ls", + "coder_workspace_bash", + "coder_get_task_logs", + "coder_list_templates", + "coder_create_task", + "coder_get_authenticated_user", + "coder_delete_task", + "coder_send_task_input", + "coder_list_workspaces", + "coder_workspace_edit_files", + "coder_workspace_list_apps", + "coder_workspace_port_forward", + "coder_create_workspace_build", + "coder_template_version_parameters" + ] + } + } +} +EOF + chmod 600 "$HOME/.kiro/settings/mcp.json" +else + echo "CODER_URL or CODER_SESSION_TOKEN is not set; skipping Coder MCP configuration." +fi + +# Trust the home directory in code-server and Kiro so they open without a +# workspace trust prompt. +mkdir -p "$HOME/.local/share/code-server/User" +cat > "$HOME/.local/share/code-server/User/settings.json" << 'EOF' +{ + "security.workspace.trust.enabled": true, + "security.workspace.trust.startupPrompt": "never", + "security.workspace.trust.emptyWindow": false, + "security.workspace.trust.untrustedFiles": "open" +} +EOF + +mkdir -p "$HOME/.kiro/settings" +cat > "$HOME/.kiro/settings/trusted-workspaces.json" << EOF +{ + "trustedFolders": [ + "$HOME" + ] +} +EOF + +echo "Workspace startup completed." From 22a2da473ba431b3a9d3a07c8fc95c06bfa7fea4 Mon Sep 17 00:00:00 2001 From: greg-the-coder Date: Tue, 20 Oct 2026 21:37:33 +0000 Subject: [PATCH 02/10] docs/feat(coder-labs/kiro-crew): mirrorable install URLs, BYO binaries, egress + session continuity docs --- .../coder-labs/modules/kiro-crew/README.md | 61 +++++++++++++++++++ .../modules/kiro-crew/kiro-crew.tftest.hcl | 30 +++++++++ registry/coder-labs/modules/kiro-crew/main.tf | 20 +++++- .../kiro-crew/scripts/install.sh.tftpl | 6 +- 4 files changed, 112 insertions(+), 5 deletions(-) diff --git a/registry/coder-labs/modules/kiro-crew/README.md b/registry/coder-labs/modules/kiro-crew/README.md index da7338f39..a6c54d059 100644 --- a/registry/coder-labs/modules/kiro-crew/README.md +++ b/registry/coder-labs/modules/kiro-crew/README.md @@ -110,6 +110,67 @@ The gateway starts with the workspace, but agents cannot call models until Kiro - **Headless:** create a Coder user secret that sets `KIRO_API_KEY` before you start the workspace, for example `coder secret create kiro-api-key --env KIRO_API_KEY`. The gateway inherits it, and Kiro CLI authenticates without a browser. Generating API keys requires a Kiro subscription that supports them. - **Interactive:** open a terminal, run `kiro-cli login`, and complete the device-code sign-in with AWS Builder ID or IAM Identity Center. Check the result with `kiro-cli whoami`. +## Session continuity + +Kiro Crew is a long-running gateway, not a one-shot CLI. The `kirocrew gateway` process owns your sessions, chat transcripts, task state, and agent memory, so work continues across disconnects and restarts: + +- **Reconnecting resumes where you left off.** Closing the dashboard tab or losing your connection does not end a session. Reopening the **Kiro Crew** app reattaches to the same running gateway and its in-progress sessions and transcripts. +- **Sessions survive workspace restarts.** All gateway state lives under `$HOME` (`~/.kiro`). On a persistent home directory it is preserved across stop/start and rebuilds; the module's start script relaunches the gateway automatically on the next start, and your prior sessions are still there. + +Use a persistent home directory to retain sessions, transcripts, and Kiro credentials across restarts. The companion [template](../../templates/kiro-crew/README.md) mounts a persistent volume at `/home/coder` for this reason. + +## Restricted and air-gapped environments + +### Use an internal mirror for the installers + +By default the module downloads the Kiro CLI and Kiro Crew installers from the public Kiro endpoints. Point `kiro_cli_install_url` and `kirocrew_install_url` at an internal mirror or artifact store so the workspace never reaches the public internet for them: + +```tf +module "kiro-crew" { + source = "registry.coder.com/coder-labs/kiro-crew/coder" + version = "1.0.0" + agent_id = coder_agent.main.id + + kiro_cli_install_url = "https://artifacts.example.com/kiro/cli-install.sh" + kirocrew_install_url = "https://artifacts.example.com/kiro/crew-cli.sh" +} +``` + +### Bring your own binaries (skip all downloads) + +`use_cached` defaults to `true`, which skips both installers when `kiro-cli` and `kirocrew` are already present (on `PATH` or in the managed venv at `~/.kiro/crew-venv`). Bake the binaries into your workspace image to run fully offline with no installer downloads at all: + +```Dockerfile +# In your workspace image +RUN curl -fsSL https://cli.kiro.dev/install | bash \ + && curl -fsSL https://download.crew.kiro.dev/cli.sh | sh +``` + +```tf +module "kiro-crew" { + source = "registry.coder.com/coder-labs/kiro-crew/coder" + version = "1.0.0" + agent_id = coder_agent.main.id + use_cached = true # default; skips the installers when the binaries already exist +} +``` + +With the binaries baked in, neither installer endpoint is contacted. + +### Network access + +The module and the tools it runs contact these external endpoints. Nothing else is required by the module itself. + +| Phase | Endpoint | Purpose | Avoid by | +| ------------------ | ----------------------------------------- | --------------------------------------------------- | --------------------------------------------------- | +| Install | `cli.kiro.dev` | Kiro CLI installer script | Set `kiro_cli_install_url`, or bake in the binary | +| Install | `download.crew.kiro.dev` | Kiro Crew installer script | Set `kirocrew_install_url`, or bake in the binary | +| Install | `pypi.org`, `files.pythonhosted.org` | `pipx` and the Kiro Crew Python dependencies | Mirror PyPI, or bake in the `~/.kiro/crew-venv` | +| Install (optional) | Ubuntu mirrors, deadsnakes PPA | Only if your `pre_install_script` installs packages | Pre-install the packages in the image | +| Runtime | Kiro service and model/embeddings backend | Kiro CLI sign-in and agent model/embeddings calls | Required for agents to call models; not overridable | + +For a fully air-gapped deployment: bake in the binaries and the managed venv, mirror PyPI, and confirm the workspace can still reach the Kiro model backend — agents cannot call models without it. + ## Troubleshooting Logs are written to `$HOME/.coder-modules/coder-labs/kiro-crew/logs/`: diff --git a/registry/coder-labs/modules/kiro-crew/kiro-crew.tftest.hcl b/registry/coder-labs/modules/kiro-crew/kiro-crew.tftest.hcl index 31641d915..27d81f176 100644 --- a/registry/coder-labs/modules/kiro-crew/kiro-crew.tftest.hcl +++ b/registry/coder-labs/modules/kiro-crew/kiro-crew.tftest.hcl @@ -94,6 +94,36 @@ run "defaults" { condition = strcontains(local.install_script, "ARG_EXTRA_MCP_JSON=$(echo -n '' | base64 -d)") error_message = "Install script should receive an empty extra MCP value by default" } + + assert { + condition = strcontains(local.install_script, "ARG_KIRO_CLI_INSTALL_URL='https://cli.kiro.dev/install'") + error_message = "Install script should default to the public Kiro CLI installer URL" + } + + assert { + condition = strcontains(local.install_script, "ARG_KIROCREW_INSTALL_URL='https://download.crew.kiro.dev/cli.sh'") + error_message = "Install script should default to the public Kiro Crew installer URL" + } +} + +run "install_urls_are_configurable" { + command = plan + + variables { + agent_id = "test-agent" + kiro_cli_install_url = "https://artifacts.example.com/kiro/cli-install.sh" + kirocrew_install_url = "https://artifacts.example.com/kiro/crew-cli.sh" + } + + assert { + condition = strcontains(local.install_script, "ARG_KIRO_CLI_INSTALL_URL='https://artifacts.example.com/kiro/cli-install.sh'") + error_message = "Install script should use the configured Kiro CLI installer URL" + } + + assert { + condition = strcontains(local.install_script, "ARG_KIROCREW_INSTALL_URL='https://artifacts.example.com/kiro/crew-cli.sh'") + error_message = "Install script should use the configured Kiro Crew installer URL" + } } run "custom_ports_and_slug" { diff --git a/registry/coder-labs/modules/kiro-crew/main.tf b/registry/coder-labs/modules/kiro-crew/main.tf index 1f01fadea..7eb919c8b 100644 --- a/registry/coder-labs/modules/kiro-crew/main.tf +++ b/registry/coder-labs/modules/kiro-crew/main.tf @@ -88,11 +88,23 @@ variable "open_in" { } variable "use_cached" { - description = "Skip running the Kiro CLI and Kiro Crew installers when the binaries are already present." + description = "Skip running the Kiro CLI and Kiro Crew installers when the binaries are already present. Combined with binaries baked into the workspace image, this lets the module run without contacting any installer endpoint." type = bool default = true } +variable "kiro_cli_install_url" { + description = "URL the Kiro CLI installer script is downloaded from. Point this at an internal mirror or artifact store in restricted or air-gapped environments." + type = string + default = "https://cli.kiro.dev/install" +} + +variable "kirocrew_install_url" { + description = "URL the Kiro Crew installer script is downloaded from. Point this at an internal mirror or artifact store in restricted or air-gapped environments." + type = string + default = "https://download.crew.kiro.dev/cli.sh" +} + variable "extra_mcp_json" { description = "Optional MCP configuration JSON to deep-merge into ~/.kiro/settings/mcp.json. Keys from this value replace existing keys with the same name; other existing keys are kept." type = string @@ -169,8 +181,10 @@ locals { ) install_script = templatefile("${path.module}/scripts/install.sh.tftpl", { - ARG_USE_CACHED = tostring(var.use_cached) - ARG_EXTRA_MCP_JSON = var.extra_mcp_json != "" ? base64encode(var.extra_mcp_json) : "" + ARG_USE_CACHED = tostring(var.use_cached) + ARG_EXTRA_MCP_JSON = var.extra_mcp_json != "" ? base64encode(var.extra_mcp_json) : "" + ARG_KIRO_CLI_INSTALL_URL = var.kiro_cli_install_url + ARG_KIROCREW_INSTALL_URL = var.kirocrew_install_url }) start_script = templatefile("${path.module}/scripts/start.sh.tftpl", { diff --git a/registry/coder-labs/modules/kiro-crew/scripts/install.sh.tftpl b/registry/coder-labs/modules/kiro-crew/scripts/install.sh.tftpl index f1c2fbe20..73ad348c9 100644 --- a/registry/coder-labs/modules/kiro-crew/scripts/install.sh.tftpl +++ b/registry/coder-labs/modules/kiro-crew/scripts/install.sh.tftpl @@ -8,6 +8,8 @@ command_exists() { ARG_USE_CACHED='${ARG_USE_CACHED}' ARG_EXTRA_MCP_JSON=$(echo -n '${ARG_EXTRA_MCP_JSON}' | base64 -d) +ARG_KIRO_CLI_INSTALL_URL='${ARG_KIRO_CLI_INSTALL_URL}' +ARG_KIROCREW_INSTALL_URL='${ARG_KIROCREW_INSTALL_URL}' KIRO_CREW_VENV_BIN="$HOME/.kiro/crew-venv/bin/kirocrew" @@ -61,7 +63,7 @@ install_kiro_cli() { # The installer prompts on /dev/tty when kiro-cli already exists in # ~/.local/bin, so remove the old binaries first as it would on confirm. rm -f "$HOME/.local/bin/kiro-cli" "$HOME/.local/bin/kiro-cli-chat" - if ! run_installer "https://cli.kiro.dev/install" bash; then + if ! run_installer "$${ARG_KIRO_CLI_INSTALL_URL}" bash; then echo "ERROR: Kiro CLI installation failed." >&2 exit 1 fi @@ -112,7 +114,7 @@ install_kiro_crew() { # dependencies (ImportError on the first gateway start). unset PYTHONPATH PYTHONHOME prepare_pipx - if ! run_installer "https://download.crew.kiro.dev/cli.sh" sh; then + if ! run_installer "$${ARG_KIROCREW_INSTALL_URL}" sh; then echo "ERROR: Kiro Crew installation failed." >&2 exit 1 fi From aa2eec3a42e7f92060a422f154a4e9d7cedfe676 Mon Sep 17 00:00:00 2001 From: greg-the-coder Date: Wed, 21 Oct 2026 19:27:39 +0000 Subject: [PATCH 03/10] fix(coder-labs/kiro-crew): drop system pipx/Python, use installer-managed Python via uv The Kiro Crew installer provisions its own managed Python with uv, so the module no longer needs system Python or pipx. Remove prepare_pipx and the python packages from the template pre-req script, and resolve the Python used by the install MCP merge and the start redirector from the installer-managed interpreter (managed venv, uv crew-python, or kirocrew pipx venv), falling back to system python3 only if none is found. Docs updated accordingly. Addresses review feedback from @35C4n0r on #1196. --- .../coder-labs/modules/kiro-crew/README.md | 20 ++++---- .../kiro-crew/scripts/install.sh.tftpl | 51 ++++++++++--------- .../modules/kiro-crew/scripts/start.sh.tftpl | 18 ++++++- .../coder-labs/templates/kiro-crew/README.md | 4 +- .../templates/kiro-crew/kiro-crew-prereqs.sh | 21 ++------ .../coder-labs/templates/kiro-crew/main.tf | 5 +- 6 files changed, 62 insertions(+), 57 deletions(-) diff --git a/registry/coder-labs/modules/kiro-crew/README.md b/registry/coder-labs/modules/kiro-crew/README.md index a6c54d059..d478e33d5 100644 --- a/registry/coder-labs/modules/kiro-crew/README.md +++ b/registry/coder-labs/modules/kiro-crew/README.md @@ -28,7 +28,7 @@ The gateway rejects requests whose `Host` is not loopback (`Host header not allo ## Prerequisites - Your Coder deployment must serve [wildcard subdomain apps](https://coder.com/docs/admin/setup#wildcard-access-url). Both apps are subdomain apps. -- The workspace image needs `bash`, `curl`, `python3` with the `venv` module, `openssl`, and `sha256sum`. The Kiro Crew installer uses Python 3.10 or newer and prefers 3.12 or 3.11. Use `pre_install_script` to install missing packages. +- The workspace image needs `bash`, `curl`, `openssl`, and `sha256sum`. The Kiro Crew installer provisions its own managed Python via `uv`, so no system Python is required. Use `pre_install_script` to install missing packages. - The Coder agent must support `coder exp sync`, which orders the install and start scripts. - A Kiro account. See [Authentication](#authentication). @@ -45,7 +45,7 @@ module "kiro-crew" { pre_install_script = <<-EOT #!/usr/bin/env bash sudo apt-get update -qq - sudo apt-get install -y curl openssl python3-venv + sudo apt-get install -y curl openssl unzip EOT } ``` @@ -161,13 +161,14 @@ With the binaries baked in, neither installer endpoint is contacted. The module and the tools it runs contact these external endpoints. Nothing else is required by the module itself. -| Phase | Endpoint | Purpose | Avoid by | -| ------------------ | ----------------------------------------- | --------------------------------------------------- | --------------------------------------------------- | -| Install | `cli.kiro.dev` | Kiro CLI installer script | Set `kiro_cli_install_url`, or bake in the binary | -| Install | `download.crew.kiro.dev` | Kiro Crew installer script | Set `kirocrew_install_url`, or bake in the binary | -| Install | `pypi.org`, `files.pythonhosted.org` | `pipx` and the Kiro Crew Python dependencies | Mirror PyPI, or bake in the `~/.kiro/crew-venv` | -| Install (optional) | Ubuntu mirrors, deadsnakes PPA | Only if your `pre_install_script` installs packages | Pre-install the packages in the image | -| Runtime | Kiro service and model/embeddings backend | Kiro CLI sign-in and agent model/embeddings calls | Required for agents to call models; not overridable | +| Phase | Endpoint | Purpose | Avoid by | +| ------------------ | ----------------------------------------- | ----------------------------------------------------------------- | ----------------------------------------------------------------- | +| Install | `cli.kiro.dev` | Kiro CLI installer script | Set `kiro_cli_install_url`, or bake in the binary | +| Install | `download.crew.kiro.dev` | Kiro Crew installer script | Set `kirocrew_install_url`, or bake in the binary | +| Install | Astral (`uv` and managed CPython) | The Kiro Crew installer uses `uv` to provision its managed Python | Bake `~/.kiro/crew-python` and `~/.kiro/crew-venv` into the image | +| Install | `pypi.org`, `files.pythonhosted.org` | The Kiro Crew Python dependencies | Mirror PyPI, or bake in the `~/.kiro/crew-venv` | +| Install (optional) | Ubuntu package mirrors | Only if your `pre_install_script` installs packages | Pre-install the packages in the image | +| Runtime | Kiro service and model/embeddings backend | Kiro CLI sign-in and agent model/embeddings calls | Required for agents to call models; not overridable | For a fully air-gapped deployment: bake in the binaries and the managed venv, mirror PyPI, and confirm the workspace can still reach the Kiro model backend — agents cannot call models without it. @@ -183,6 +184,5 @@ The start script launches the gateway in a detached session and returns immediat - **`Host header not allowed.`:** the browser's origin is not in the gateway's allowlist. Set `dashboard_origin` or `allowed_origins` to the origin shown in the browser. - **`ImportError: No module named 'aiohttp'` on gateway start:** the managed venv at `~/.kiro/crew-venv` was built with a foreign `PYTHONPATH`. The install script unsets `PYTHONPATH` and `PYTHONHOME`; remove `~/.kiro/crew-venv`, set `use_cached = false`, and restart the workspace. -- **Kiro Crew installer fails in `pipx` with `JSONDecodeError`:** distro `pipx` on Python 3.13 or newer crashes. The install script builds its own `pipx` with `python3.12` or `python3.11` when available, so install one of them, or use an image based on Ubuntu 24.04. - **Sandbox warnings:** Kiro Crew sandboxes agent subprocesses with a Linux user namespace. When the workspace cannot create one, which is common in Kubernetes pods, the gateway is configured with `agent.sandbox_allow_unsandboxed_exec` and the workspace is the isolation boundary. - **Expected warnings:** `Vendored llama-cpp-python failed to import` (the gateway falls back to API-based embeddings) and `cgroup v2 scope enforcement unavailable` (no systemd user session in containers) do not affect the gateway. diff --git a/registry/coder-labs/modules/kiro-crew/scripts/install.sh.tftpl b/registry/coder-labs/modules/kiro-crew/scripts/install.sh.tftpl index 73ad348c9..ab3ebf7a2 100644 --- a/registry/coder-labs/modules/kiro-crew/scripts/install.sh.tftpl +++ b/registry/coder-labs/modules/kiro-crew/scripts/install.sh.tftpl @@ -78,28 +78,29 @@ install_kiro_cli() { echo "Kiro CLI ready." } -# The Kiro Crew installer shells out to the ambient pipx. On images whose -# system Python is 3.13 or newer, the distro pipx crashes while parsing an -# empty `pip list --format=json`. Build a pipx on a supported interpreter -# (python3.12, then python3.11) and put it first on PATH. -prepare_pipx() { - local pipx_python=python3 - if command_exists python3.12; then - pipx_python=python3.12 - elif command_exists python3.11; then - pipx_python=python3.11 - fi - - if [ ! -x "$HOME/.kiro/pipx-venv/bin/pipx" ]; then - mkdir -p "$HOME/.kiro" - "$${pipx_python}" -m venv "$HOME/.kiro/pipx-venv" \ - && "$HOME/.kiro/pipx-venv/bin/pip" install -q --upgrade pip pipx \ - || echo "WARN: Dedicated pipx venv build failed; using the system pipx." - fi - if [ -x "$HOME/.kiro/pipx-venv/bin/pipx" ]; then - ln -sf "$HOME/.kiro/pipx-venv/bin/pipx" "$HOME/.local/bin/pipx" +# The Kiro Crew installer provisions its own managed Python via uv (a managed +# CPython under ~/.kiro/crew-python, and depending on the installer version a +# managed venv at ~/.kiro/crew-venv or a pipx venv under ~/.local/share/pipx), +# so the module needs neither system Python nor a system pipx. +# resolve_kiro_python returns an interpreter for the module's own helper +# scripts, preferring the installer-provided Python and falling back to any +# system python3 only if none is found. +resolve_kiro_python() { + local candidate + for candidate in \ + "$HOME/.kiro/crew-venv/bin/python" \ + "$HOME"/.kiro/crew-python/*/bin/python3 \ + "$HOME/.local/share/pipx/venvs/kirocrew/bin/python"; do + if [ -x "$${candidate}" ]; then + echo "$${candidate}" + return 0 + fi + done + if command_exists python3; then + command -v python3 + return 0 fi - hash -r + return 1 } install_kiro_crew() { @@ -113,7 +114,6 @@ install_kiro_crew() { # as already satisfied, which leaves the managed venv without its # dependencies (ImportError on the first gateway start). unset PYTHONPATH PYTHONHOME - prepare_pipx if ! run_installer "$${ARG_KIROCREW_INSTALL_URL}" sh; then echo "ERROR: Kiro Crew installation failed." >&2 exit 1 @@ -143,8 +143,11 @@ apply_extra_mcp() { echo "Applying extra MCP configuration to $${mcp_file}..." mkdir -p "$(dirname "$${mcp_file}")" - if [ -f "$${mcp_file}" ] && command_exists python3; then - python3 - "$${mcp_file}" "$${ARG_EXTRA_MCP_JSON}" << 'PYEOF' + local python_bin + python_bin="$(resolve_kiro_python || true)" + + if [ -f "$${mcp_file}" ] && [ -n "$${python_bin}" ]; then + "$${python_bin}" - "$${mcp_file}" "$${ARG_EXTRA_MCP_JSON}" << 'PYEOF' import json import sys diff --git a/registry/coder-labs/modules/kiro-crew/scripts/start.sh.tftpl b/registry/coder-labs/modules/kiro-crew/scripts/start.sh.tftpl index bc988d938..45225daf5 100644 --- a/registry/coder-labs/modules/kiro-crew/scripts/start.sh.tftpl +++ b/registry/coder-labs/modules/kiro-crew/scripts/start.sh.tftpl @@ -57,6 +57,22 @@ echo "Using kirocrew binary: $${KIROCREW_CMD}" echo "Gateway port: $${ARG_PORT}" echo "Gateway log: $${GATEWAY_LOG}" +# Run the loopback redirector with the Python the Kiro Crew installer +# provisions (a managed CPython under ~/.kiro/crew-python, or a managed/pipx +# venv depending on the installer version), falling back to system python3 +# only if none is found, so the module does not depend on a system Python. +PYTHON_BIN="python3" +for candidate in \ + "$HOME/.kiro/crew-venv/bin/python" \ + "$HOME"/.kiro/crew-python/*/bin/python3 \ + "$HOME/.local/share/pipx/venvs/kirocrew/bin/python"; do + if [ -x "$${candidate}" ]; then + PYTHON_BIN="$${candidate}" + break + fi +done +echo "Redirector Python: $${PYTHON_BIN}" + if [ -n "$${KIRO_API_KEY:-}" ]; then echo "Headless Kiro auth: KIRO_API_KEY detected; kiro-cli authenticates non-interactively." else @@ -153,7 +169,7 @@ setsid nohup env \ REDIR_PORT="$${ARG_REDIRECT_PORT}" \ GW_PORT="$${ARG_PORT}" \ DASH_ORIGIN="$${ARG_DASHBOARD_URL}" \ - python3 "$${REDIRECTOR_PATH}" >> "$${REDIRECTOR_LOG}" 2>&1 < /dev/null & + "$${PYTHON_BIN}" "$${REDIRECTOR_PATH}" >> "$${REDIRECTOR_LOG}" 2>&1 < /dev/null & disown 2> /dev/null || true echo "Kiro Crew redirector dispatched on port $${ARG_REDIRECT_PORT} -> $${ARG_DASHBOARD_URL}" diff --git a/registry/coder-labs/templates/kiro-crew/README.md b/registry/coder-labs/templates/kiro-crew/README.md index 0e42b5e88..f1e56c3d9 100644 --- a/registry/coder-labs/templates/kiro-crew/README.md +++ b/registry/coder-labs/templates/kiro-crew/README.md @@ -28,9 +28,9 @@ Each workspace also includes: - A Kubernetes cluster that Coder can provision into. The namespace must exist before you create workspaces. If the Coder host runs outside the cluster, set `use_kubeconfig = true` and provide `~/.kube/config` on the Coder host. - A storage class that can provision `ReadWriteOnce` persistent volumes. - [Wildcard subdomain apps](https://coder.com/docs/admin/setup#wildcard-access-url). The dashboard is a subdomain app, and the dashboard app tile does not resolve without a wildcard access URL. -- Outbound access from workspace pods to the Ubuntu package mirrors, the deadsnakes PPA, PyPI, and the Kiro download hosts. +- Outbound access from workspace pods to the Ubuntu package mirrors, the Kiro download hosts (`cli.kiro.dev`, `download.crew.kiro.dev`), and the hosts the bundled Kiro Crew installer uses to provision its managed Python (`uv`/Astral and PyPI). -The default image pins `codercom/enterprise-base` to Ubuntu 24.04 (Python 3.12). Images based on newer Ubuntu releases ship a `pipx` that breaks the Kiro Crew installer. +The default image pins `codercom/enterprise-base` to a known-good Ubuntu 24.04 build. The Kiro Crew installer provisions its own managed Python via `uv`, so the image does not need a specific system Python. ### Kiro account diff --git a/registry/coder-labs/templates/kiro-crew/kiro-crew-prereqs.sh b/registry/coder-labs/templates/kiro-crew/kiro-crew-prereqs.sh index d95cb797f..9cf6e61a0 100644 --- a/registry/coder-labs/templates/kiro-crew/kiro-crew-prereqs.sh +++ b/registry/coder-labs/templates/kiro-crew/kiro-crew-prereqs.sh @@ -1,6 +1,7 @@ #!/usr/bin/env bash # Pre-install script for the kiro-crew module. Installs the system packages -# that the Kiro CLI and Kiro Crew installers need. +# the Kiro CLI and Kiro Crew installers need. The Kiro Crew installer +# provisions its own managed Python via uv, so no system Python is required. set -euo pipefail sudo apt-get update -qq @@ -13,22 +14,6 @@ sudo apt-get install -y -qq \ gnupg \ dirmngr \ openssl \ - ca-certificates \ - python3-venv \ - python3-pip - -# Kiro Crew creates its managed venv at ~/.kiro/crew-venv with the newest of -# python3.12, python3.11, and python3.10. Install python3.11 and its venv -# module as well so the installer has a tested interpreter. -if ! python3.11 --version > /dev/null 2>&1; then - echo "Installing Python 3.11 from the deadsnakes PPA..." - sudo apt-get install -y -qq software-properties-common - sudo add-apt-repository -y ppa:deadsnakes/ppa - sudo apt-get update -qq -fi -sudo apt-get install -y -qq \ - python3.11 \ - python3.11-venv \ - python3.11-dev + ca-certificates echo "Kiro Crew prerequisites installed." diff --git a/registry/coder-labs/templates/kiro-crew/main.tf b/registry/coder-labs/templates/kiro-crew/main.tf index 06284989d..3af956626 100644 --- a/registry/coder-labs/templates/kiro-crew/main.tf +++ b/registry/coder-labs/templates/kiro-crew/main.tf @@ -41,8 +41,9 @@ variable "image" { type = string description = <<-EOF Container image for workspaces. The default pins codercom/enterprise-base - to Ubuntu 24.04 (Python 3.12). Images whose system Python is 3.13 or newer - ship a pipx that breaks the Kiro Crew installer. + to a known-good Ubuntu 24.04 build. The Kiro Crew installer provisions its + own managed Python via uv, so the image does not need a specific system + Python. EOF default = "codercom/enterprise-base@sha256:16031b4de2e099884ad6b4057bffeaf03380a76eddf46c193b5d0767e738ba96" } From c4ef705db2f7156c5c24893f47fe1a3e20bf4efa Mon Sep 17 00:00:00 2001 From: default Date: Fri, 23 Oct 2026 16:46:05 +0000 Subject: [PATCH 04/10] fix(coder-labs/kiro-crew): derive dashboard origins from the app hostname and support workspace proxies The redirector sent the browser to -dashboard----., which only matches deployments whose wildcard is *.. Deployments with another wildcard (for example *--apps.example.com) or with workspace proxies got a broken redirect or "Host header not allowed." from the gateway. The start script now derives the primary dashboard origin from the agent's VSCODE_PROXY_URI and expands each workspace_proxy_hostnames entry into exact origins. One snapshot feeds both the gateway allowlist and the redirector's host-to-origin routes, so the redirector keeps the browser on the proxy it came through and falls back to the primary origin for unknown hosts. Generated with Coder Agents. --- .../coder-labs/modules/kiro-crew/README.md | 27 ++- .../modules/kiro-crew/kiro-crew.tftest.hcl | 55 +++++- .../coder-labs/modules/kiro-crew/main.test.ts | 151 +++++++++++++- registry/coder-labs/modules/kiro-crew/main.tf | 57 +++--- .../modules/kiro-crew/scripts/start.sh.tftpl | 185 +++++++++++++++++- 5 files changed, 427 insertions(+), 48 deletions(-) diff --git a/registry/coder-labs/modules/kiro-crew/README.md b/registry/coder-labs/modules/kiro-crew/README.md index d478e33d5..c033f9ee5 100644 --- a/registry/coder-labs/modules/kiro-crew/README.md +++ b/registry/coder-labs/modules/kiro-crew/README.md @@ -23,7 +23,7 @@ The module adds two subdomain apps: - A visible **Kiro Crew** app that points at a loopback redirector. Each time you open it, the redirector mints a short-lived gateway token and redirects your browser to the dashboard with that token, so you land on an authenticated dashboard without running `kirocrew token`. - A hidden `-dashboard` app that serves the dashboard and its websocket through Coder's subdomain proxy. -The gateway rejects requests whose `Host` is not loopback (`Host header not allowed.`). The module derives the dashboard app's origin from the Coder access URL, passes it to the gateway in `KIROCREW_CORS_ORIGINS`, and sets `dashboard.url` so token cookies are scoped to it. +The gateway rejects requests whose `Host` is not in its allowlist (`Host header not allowed.`). At startup, the module derives the dashboard app's origin from the agent's `VSCODE_PROXY_URI`, which follows the deployment's wildcard access URL. It also derives an origin for each hostname in `workspace_proxy_hostnames`. The module passes these origins to the gateway in `KIROCREW_CORS_ORIGINS` and sets `dashboard.url` to the primary origin. The redirector sends your browser to the dashboard on the same hostname you opened the app from, primary or proxy, if that origin is in the allowlist. Otherwise, it sends your browser to the primary origin. ## Prerequisites @@ -71,9 +71,28 @@ module "kiro-crew" { } ``` -### Custom wildcard domain +### Workspace proxies -If your deployment's wildcard access URL uses a different host than its access URL, set the dashboard origin explicitly. The hidden app's slug is `-dashboard`. +Without configuration, a browser that opens Kiro Crew through a [workspace proxy](https://coder.com/docs/admin/networking/workspace-proxies) is redirected to the dashboard on the primary deployment's wildcard hostname. To keep dashboard traffic on the proxy, list each proxy's wildcard hostname, as shown in the `wildcard_hostname` field of `/api/v2/regions`: + +```tf +module "kiro-crew" { + source = "registry.coder.com/coder-labs/kiro-crew/coder" + version = "1.0.0" + agent_id = coder_agent.main.id + + workspace_proxy_hostnames = [ + "*.apps.eu.example.com", + "*.apps.us.example.com", + ] +} +``` + +The gateway's allowlist is fixed when the workspace starts, so restart the workspace after you add a proxy. + +### Custom dashboard origin + +The module derives the dashboard origin from the agent's `VSCODE_PROXY_URI`. If that variable is not set, or you serve the dashboard from another origin, set the dashboard origin explicitly. The redirector uses it in place of the primary origin. The hidden app's slug is `-dashboard`. ```tf module "kiro-crew" { @@ -182,7 +201,7 @@ Logs are written to `$HOME/.coder-modules/coder-labs/kiro-crew/logs/`: The start script launches the gateway in a detached session and returns immediately, because a slow cold start could otherwise exceed the agent's script timeout. Readiness is reported by the app healthchecks, so the app can show as unhealthy for a short time after the workspace starts. While the gateway is starting, the Kiro Crew app responds with `503`; retry after a few seconds. -- **`Host header not allowed.`:** the browser's origin is not in the gateway's allowlist. Set `dashboard_origin` or `allowed_origins` to the origin shown in the browser. +- **`Host header not allowed.`:** the browser's origin is not in the gateway's allowlist. `start.log` lists the allowed origins. If you opened the dashboard through a workspace proxy, add its wildcard hostname to `workspace_proxy_hostnames`. Otherwise, set `dashboard_origin` or `allowed_origins` to the origin shown in the browser. - **`ImportError: No module named 'aiohttp'` on gateway start:** the managed venv at `~/.kiro/crew-venv` was built with a foreign `PYTHONPATH`. The install script unsets `PYTHONPATH` and `PYTHONHOME`; remove `~/.kiro/crew-venv`, set `use_cached = false`, and restart the workspace. - **Sandbox warnings:** Kiro Crew sandboxes agent subprocesses with a Linux user namespace. When the workspace cannot create one, which is common in Kubernetes pods, the gateway is configured with `agent.sandbox_allow_unsandboxed_exec` and the workspace is the isolation boundary. - **Expected warnings:** `Vendored llama-cpp-python failed to import` (the gateway falls back to API-based embeddings) and `cgroup v2 scope enforcement unavailable` (no systemd user session in containers) do not affect the gateway. diff --git a/registry/coder-labs/modules/kiro-crew/kiro-crew.tftest.hcl b/registry/coder-labs/modules/kiro-crew/kiro-crew.tftest.hcl index 27d81f176..390f117c9 100644 --- a/registry/coder-labs/modules/kiro-crew/kiro-crew.tftest.hcl +++ b/registry/coder-labs/modules/kiro-crew/kiro-crew.tftest.hcl @@ -62,12 +62,17 @@ run "defaults" { assert { condition = local.dashboard_origin == "https://kirocrew-dashboard--dev--alice.coder.example.com" - error_message = "Dashboard origin should be derived from the access URL, workspace, and owner" + error_message = "Fallback dashboard origin should be derived from the access URL, workspace, and owner" } assert { - condition = local.gateway_allowed_origins == local.dashboard_origin - error_message = "Gateway allowlist should contain only the dashboard origin by default" + condition = strcontains(local.start_script, "ARG_DASHBOARD_ORIGIN=$(echo -n '' | base64 -d)") + error_message = "Start script should receive an empty dashboard_origin override by default" + } + + assert { + condition = strcontains(local.start_script, "ARG_PROXY_HOSTNAMES=$(echo -n '' | base64 -d)") + error_message = "Start script should receive no workspace proxy hostnames by default" } assert { @@ -81,8 +86,8 @@ run "defaults" { } assert { - condition = strcontains(local.start_script, "ARG_ALLOWED_ORIGINS=$(echo -n '${base64encode(local.dashboard_origin)}' | base64 -d)") - error_message = "Start script should receive the allowlist base64 encoded" + condition = strcontains(local.start_script, "ARG_FALLBACK_ORIGIN=$(echo -n '${base64encode(local.dashboard_origin)}' | base64 -d)") + error_message = "Start script should receive the fallback dashboard origin base64 encoded" } assert { @@ -186,9 +191,45 @@ run "allowed_origins_are_appended" { } assert { - condition = local.gateway_allowed_origins == "https://kirocrew-dashboard--dev--alice.coder.example.com,https://coder.example.com,https://proxy.example.com" - error_message = "Extra allowed origins should follow the dashboard origin" + condition = strcontains(local.start_script, "ARG_EXTRA_ALLOWED_ORIGINS=$(echo -n '${base64encode("https://coder.example.com,https://proxy.example.com")}' | base64 -d)") + error_message = "Extra allowed origins should be trimmed and passed to the start script" + } +} + +run "workspace_proxy_hostnames_are_passed" { + command = plan + + variables { + agent_id = "test-agent" + workspace_proxy_hostnames = ["*--apps.eu.example.com", "https://*.apps.us.example.com:8443"] + } + + assert { + condition = strcontains(local.start_script, "ARG_PROXY_HOSTNAMES=$(echo -n '${base64encode("*--apps.eu.example.com\nhttps://*.apps.us.example.com:8443")}' | base64 -d)") + error_message = "Workspace proxy hostnames should be passed to the start script newline separated" + } +} + +run "invalid_workspace_proxy_hostname_is_rejected" { + command = plan + + variables { + agent_id = "test-agent" + workspace_proxy_hostnames = ["apps.example.com"] } + + expect_failures = [var.workspace_proxy_hostnames] +} + +run "workspace_proxy_hostname_with_path_is_rejected" { + command = plan + + variables { + agent_id = "test-agent" + workspace_proxy_hostnames = ["*.apps.example.com/path"] + } + + expect_failures = [var.workspace_proxy_hostnames] } run "dashboard_origin_override" { diff --git a/registry/coder-labs/modules/kiro-crew/main.test.ts b/registry/coder-labs/modules/kiro-crew/main.test.ts index 3a32f1382..2c61a73d5 100644 --- a/registry/coder-labs/modules/kiro-crew/main.test.ts +++ b/registry/coder-labs/modules/kiro-crew/main.test.ts @@ -233,13 +233,20 @@ chmod +x "$HOME/.kiro/crew-venv/bin/kirocrew" }; // Polls a URL from inside the container until curl reports the expected -// status code, then prints " ". -const waitForStatus = async (id: string, url: string, status: string) => { +// status code, then prints " ". A host sets the Host +// header, as Coder's app proxy forwards the browser's Host. +const waitForStatus = async ( + id: string, + url: string, + status: string, + host?: string, +) => { + const hostHeader = host ? `-H 'Host: ${host}'` : ""; const resp = await execContainer(id, [ "bash", "-c", `for i in $(seq 1 100); do - out=$(/usr/bin/curl -s -o /dev/null -w '%{http_code} %{redirect_url}' '${url}') + out=$(/usr/bin/curl -s -o /dev/null ${hostHeader} -w '%{http_code} %{redirect_url}' '${url}') case "$out" in ${status}*) echo "$out"; exit 0 ;; esac sleep 0.2 done @@ -257,6 +264,58 @@ const waitForStatus = async (id: string, url: string, status: string) => { setDefaultTimeout(90 * 1000); +// Names the coder provider resolved for the workspace and owner, which shape +// every app hostname. +const workspaceNames = (state: TerraformState) => { + const nameOf = (type: string) => { + const resource = state.resources.find((r) => r.type === type); + const name = resource?.instances[0]?.attributes.name; + if (typeof name !== "string" || name === "") { + throw new Error(`${type} name not found in terraform state`); + } + return name; + }; + return { + workspace: nameOf("coder_workspace"), + owner: nameOf("coder_workspace_owner"), + }; +}; + +const agentName = "main"; + +// What the agent sets for a deployment whose wildcard is *--apps.coder.example.com. +const proxyURIEnv = (state: TerraformState) => { + const { workspace, owner } = workspaceNames(state); + return { + CODER_WORKSPACE_AGENT_NAME: agentName, + VSCODE_PROXY_URI: `https://{{port}}--${agentName}--${workspace}--${owner}--apps.coder.example.com`, + }; +}; + +const gatewayOrigins = async (id: string) => { + const env = await readFileContainer(id, "/tmp/kirocrew-gateway-env"); + const line = env + .split("\n") + .find((l) => l.startsWith("KIROCREW_CORS_ORIGINS=")); + return (line ?? "").replace("KIROCREW_CORS_ORIGINS=", "").split(","); +}; + +// Requests the redirector with the given Host and returns the redirect +// origin, asserting the gateway trusts it. +const redirectOrigin = async (id: string, host: string) => { + const out = await waitForStatus( + id, + `http://127.0.0.1:${redirectPort}/`, + "302", + host, + ); + const location = out.replace(/^302 /, ""); + expect(location).toEndWith("/?token=test-token"); + const origin = location.replace("/?token=test-token", ""); + expect(await gatewayOrigins(id)).toContain(origin); + return origin; +}; + describe("kiro-crew", async () => { beforeAll(async () => { await runTerraformInit(import.meta.dir); @@ -360,12 +419,17 @@ describe("kiro-crew", async () => { withMocks: true, moduleVariables: { allowed_origins: "https://extra.example.com" }, }); - const dashboardOrigin = state.outputs.dashboard_origin.value as string; + const { workspace, owner } = workspaceNames(state); + const dashboardOrigin = `https://kirocrew-dashboard--${workspace}--${owner}--apps.coder.example.com`; await runOrFail(id, "install", scripts.install); const start = await runOrFail(id, "start", scripts.start, { KIRO_AWS_BUILDER_ID_TOKEN: "builder-token", + ...proxyURIEnv(state), }); expect(start.stdout).toContain("Kiro Crew gateway launch dispatched."); + expect(start.stdout).toContain( + `Primary dashboard origin: ${dashboardOrigin}`, + ); await waitForStatus(id, `http://127.0.0.1:${redirectPort}/healthz`, "200"); const health = await execContainer(id, [ @@ -385,7 +449,7 @@ describe("kiro-crew", async () => { const gatewayEnv = await readFileContainer(id, "/tmp/kirocrew-gateway-env"); expect(gatewayEnv).toContain(`KIROCREW_PORT=${gatewayPort}`); expect(gatewayEnv).toContain( - `KIROCREW_CORS_ORIGINS=${dashboardOrigin},https://extra.example.com`, + `KIROCREW_CORS_ORIGINS=${dashboardOrigin},https://kirocrew-dashboard--${agentName}--${workspace}--${owner}--apps.coder.example.com,https://extra.example.com`, ); expect(gatewayEnv).toContain("KIRO_AWS_BUILDER_ID_TOKEN=builder-token"); @@ -402,6 +466,83 @@ describe("kiro-crew", async () => { } }); + test("redirector-keeps-browser-on-its-workspace-proxy", async () => { + const { id, scripts, state } = await setup({ + withMocks: true, + moduleVariables: { + workspace_proxy_hostnames: JSON.stringify([ + "*--apps.eu.coder.example.com", + "http://*.apps.lab.example.com:8080", + ]), + }, + }); + const { workspace, owner } = workspaceNames(state); + const sub = `${workspace}--${owner}`; + await runOrFail(id, "install", scripts.install); + await runOrFail(id, "start", scripts.start, proxyURIEnv(state)); + + // Primary deployment. + expect( + await redirectOrigin(id, `kirocrew--${sub}--apps.coder.example.com`), + ).toBe(`https://kirocrew-dashboard--${sub}--apps.coder.example.com`); + // Listed proxy, in the form Coder links to and in the form naming the agent. + expect( + await redirectOrigin(id, `kirocrew--${sub}--apps.eu.coder.example.com`), + ).toBe(`https://kirocrew-dashboard--${sub}--apps.eu.coder.example.com`); + expect( + await redirectOrigin( + id, + `KIROCREW--${agentName}--${sub}--apps.eu.coder.example.com`, + ), + ).toBe( + `https://kirocrew-dashboard--${agentName}--${sub}--apps.eu.coder.example.com`, + ); + // Explicit scheme and port. + expect( + await redirectOrigin(id, `kirocrew--${sub}.apps.lab.example.com:8080`), + ).toBe(`http://kirocrew-dashboard--${sub}.apps.lab.example.com:8080`); + // Unlisted proxies and unrelated hosts fall back to the primary origin. + for (const host of [ + `kirocrew--${sub}--apps.ap.coder.example.com`, + `kirocrew--${sub}--apps.attacker.example`, + ]) { + expect(await redirectOrigin(id, host)).toBe( + `https://kirocrew-dashboard--${sub}--apps.coder.example.com`, + ); + } + }); + + test("dashboard-origin-override-is-the-primary-origin", async () => { + const override = "https://crew.apps.example.com"; + const { id, scripts, state } = await setup({ + withMocks: true, + moduleVariables: { dashboard_origin: override }, + }); + const { workspace, owner } = workspaceNames(state); + await runOrFail(id, "install", scripts.install); + await runOrFail(id, "start", scripts.start, proxyURIEnv(state)); + expect( + await redirectOrigin( + id, + `kirocrew--${workspace}--${owner}--apps.coder.example.com`, + ), + ).toBe(override); + expect(await gatewayOrigins(id)).toEqual([override]); + const calls = await readFileContainer(id, "/tmp/kirocrew-calls"); + expect(calls).toContain(`config set dashboard.url ${override}`); + }); + + test("start-falls-back-without-vscode-proxy-uri", async () => { + const { id, scripts, state } = await setup({ withMocks: true }); + const fallback = state.outputs.dashboard_origin.value as string; + await runOrFail(id, "install", scripts.install); + const start = await runOrFail(id, "start", scripts.start); + expect(start.stdout + start.stderr).toContain( + "WARNING: could not determine the wildcard app hostname", + ); + expect(await redirectOrigin(id, "127.0.0.1")).toBe(fallback); + }); + test("redirector-returns-503-until-gateway-ready", async () => { const { id, scripts } = await setup({ withMocks: true }); await runOrFail(id, "install", scripts.install); diff --git a/registry/coder-labs/modules/kiro-crew/main.tf b/registry/coder-labs/modules/kiro-crew/main.tf index 7eb919c8b..9013d0473 100644 --- a/registry/coder-labs/modules/kiro-crew/main.tf +++ b/registry/coder-labs/modules/kiro-crew/main.tf @@ -130,7 +130,7 @@ variable "allowed_origins" { } variable "dashboard_origin" { - description = "Override for the external origin of the hidden dashboard app, for example when the deployment's wildcard access URL differs from its access URL. When empty, the module derives \"://-dashboard----.\" from the access URL." + description = "Override for the external origin of the hidden dashboard app. When empty, the module derives it at startup from the agent's VSCODE_PROXY_URI, which follows the deployment's wildcard access URL." type = string default = "" @@ -140,6 +140,20 @@ variable "dashboard_origin" { } } +variable "workspace_proxy_hostnames" { + description = "Wildcard app hostnames of the deployment's workspace proxies, for example [\"*.apps.eu.example.com\"]. Use the wildcard_hostname that /api/v2/regions reports for each proxy. A scheme prefix is optional and defaults to the access URL scheme. A browser that opens Kiro Crew through a listed proxy stays on that proxy. A browser that opens it through an unlisted proxy is sent to the primary dashboard origin." + type = list(string) + default = [] + + validation { + condition = alltrue([ + for h in var.workspace_proxy_hostnames : + can(regex("^(https?://)?[A-Za-z0-9-]*\\*[A-Za-z0-9-]*(\\.[A-Za-z0-9-]+)+(:[0-9]+)?$", h)) + ]) + error_message = "Each workspace_proxy_hostnames entry must be a wildcard hostname such as '*.apps.example.com' or '*--apps.example.com', with exactly one '*' in the first label and no path." + } +} + variable "pre_install_script" { description = "Custom script to run before installing Kiro CLI and Kiro Crew, for example to install system packages." type = string @@ -164,21 +178,13 @@ locals { access_scheme = startswith(data.coder_workspace.me.access_url, "http://") ? "http" : "https" access_host = trimsuffix(replace(replace(data.coder_workspace.me.access_url, "https://", ""), "http://", ""), "/") - # Coder serves subdomain apps at ----.. - # The redirector sends the browser here with a freshly minted token, and the - # gateway must trust this origin or it rejects proxied requests with - # "Host header not allowed.". - dashboard_origin = ( - var.dashboard_origin != "" - ? var.dashboard_origin - : "${local.access_scheme}://${local.dashboard_slug}--${data.coder_workspace.me.name}--${data.coder_workspace_owner.me.name}.${local.access_host}" - ) - - gateway_allowed_origins = ( - trimspace(var.allowed_origins) != "" - ? "${local.dashboard_origin},${trimspace(var.allowed_origins)}" - : local.dashboard_origin - ) + # The start script derives the real dashboard origins from the agent's + # VSCODE_PROXY_URI and var.workspace_proxy_hostnames, because the wildcard + # app hostname is not available to Terraform. This origin, which assumes + # "*.", is only the last resort when neither is available. + fallback_dashboard_origin = "${local.access_scheme}://${local.dashboard_slug}--${data.coder_workspace.me.name}--${data.coder_workspace_owner.me.name}.${local.access_host}" + + dashboard_origin = var.dashboard_origin != "" ? var.dashboard_origin : local.fallback_dashboard_origin install_script = templatefile("${path.module}/scripts/install.sh.tftpl", { ARG_USE_CACHED = tostring(var.use_cached) @@ -188,11 +194,18 @@ locals { }) start_script = templatefile("${path.module}/scripts/start.sh.tftpl", { - ARG_MODULE_DIRECTORY = local.module_directory - ARG_PORT = tostring(var.port) - ARG_REDIRECT_PORT = tostring(var.redirect_port) - ARG_ALLOWED_ORIGINS = base64encode(local.gateway_allowed_origins) - ARG_DASHBOARD_URL = base64encode(local.dashboard_origin) + ARG_MODULE_DIRECTORY = local.module_directory + ARG_PORT = tostring(var.port) + ARG_REDIRECT_PORT = tostring(var.redirect_port) + ARG_APP_SLUG = var.slug + ARG_DASHBOARD_SLUG = local.dashboard_slug + ARG_WORKSPACE_NAME = data.coder_workspace.me.name + ARG_OWNER_NAME = data.coder_workspace_owner.me.name + ARG_ACCESS_SCHEME = local.access_scheme + ARG_DASHBOARD_ORIGIN = base64encode(var.dashboard_origin) + ARG_FALLBACK_ORIGIN = base64encode(local.fallback_dashboard_origin) + ARG_PROXY_HOSTNAMES = base64encode(join("\n", var.workspace_proxy_hostnames)) + ARG_EXTRA_ALLOWED_ORIGINS = base64encode(trimspace(var.allowed_origins)) }) } @@ -265,7 +278,7 @@ output "kirocrew_url" { } output "dashboard_origin" { - description = "External origin of the hidden Kiro Crew dashboard app that the redirector sends the browser to." + description = "The dashboard_origin override, or the origin assumed when the wildcard app hostname cannot be determined at startup. The start script logs the origins it actually uses." value = local.dashboard_origin } diff --git a/registry/coder-labs/modules/kiro-crew/scripts/start.sh.tftpl b/registry/coder-labs/modules/kiro-crew/scripts/start.sh.tftpl index 45225daf5..296c907d8 100644 --- a/registry/coder-labs/modules/kiro-crew/scripts/start.sh.tftpl +++ b/registry/coder-labs/modules/kiro-crew/scripts/start.sh.tftpl @@ -7,8 +7,15 @@ set -euo pipefail ARG_MODULE_DIRECTORY="${ARG_MODULE_DIRECTORY}" ARG_PORT='${ARG_PORT}' ARG_REDIRECT_PORT='${ARG_REDIRECT_PORT}' -ARG_ALLOWED_ORIGINS=$(echo -n '${ARG_ALLOWED_ORIGINS}' | base64 -d) -ARG_DASHBOARD_URL=$(echo -n '${ARG_DASHBOARD_URL}' | base64 -d) +ARG_APP_SLUG='${ARG_APP_SLUG}' +ARG_DASHBOARD_SLUG='${ARG_DASHBOARD_SLUG}' +ARG_WORKSPACE_NAME='${ARG_WORKSPACE_NAME}' +ARG_OWNER_NAME='${ARG_OWNER_NAME}' +ARG_ACCESS_SCHEME='${ARG_ACCESS_SCHEME}' +ARG_DASHBOARD_ORIGIN=$(echo -n '${ARG_DASHBOARD_ORIGIN}' | base64 -d) +ARG_FALLBACK_ORIGIN=$(echo -n '${ARG_FALLBACK_ORIGIN}' | base64 -d) +ARG_PROXY_HOSTNAMES=$(echo -n '${ARG_PROXY_HOSTNAMES}' | base64 -d) +ARG_EXTRA_ALLOWED_ORIGINS=$(echo -n '${ARG_EXTRA_ALLOWED_ORIGINS}' | base64 -d) LOG_DIR="$${ARG_MODULE_DIRECTORY}/logs" GATEWAY_LOG="$${LOG_DIR}/gateway.log" @@ -16,6 +23,8 @@ REDIRECTOR_LOG="$${LOG_DIR}/redirector.log" CREW_DIR="$HOME/.kiro/crew" LAUNCHER_PATH="$${CREW_DIR}/start-gateway.sh" REDIRECTOR_PATH="$${CREW_DIR}/redirect-gateway.py" +ORIGINS_HELPER_PATH="$${CREW_DIR}/coder-origins.py" +ORIGINS_PATH="$${CREW_DIR}/coder-origins.json" mkdir -p "$${LOG_DIR}" "$${CREW_DIR}" export PATH="$HOME/.local/bin:$HOME/bin:$PATH" @@ -73,6 +82,151 @@ for candidate in \ done echo "Redirector Python: $${PYTHON_BIN}" +# Coder serves subdomain apps at with "*" replaced by +# ----, on the primary deployment and on every +# workspace proxy, each with its own wildcard hostname. The gateway only +# accepts Host/Origin values from an exact allowlist that is fixed at launch, +# so this helper expands every known wildcard into exact origins once and +# writes one snapshot. The gateway allowlist and the redirector routes both +# come from that snapshot, so the redirector never sends a token to an origin +# the gateway rejects or to a host taken from the request. +cat > "$${ORIGINS_HELPER_PATH}" << 'PYEOF' +import json +import os +import sys +from urllib.parse import urlsplit + +env = os.environ +app_slug = env["KC_APP_SLUG"] +dash_slug = env["KC_DASHBOARD_SLUG"] +workspace = env["KC_WORKSPACE_NAME"] +owner = env["KC_OWNER_NAME"] +agent = env.get("CODER_WORKSPACE_AGENT_NAME", "") +access_scheme = env.get("KC_ACCESS_SCHEME", "https") +override = env.get("KC_DASHBOARD_ORIGIN", "").strip().rstrip("/") +fallback = env.get("KC_FALLBACK_ORIGIN", "").strip().rstrip("/") +proxy_uri = env.get("VSCODE_PROXY_URI", "").strip() + + +def log(msg): + print(msg, file=sys.stderr) + + +def split_wildcard(value, default_scheme): + """Returns (scheme, host-pattern) for a wildcard such as '*.apps.example.com'.""" + value = value.strip().rstrip("/") + scheme = default_scheme + for prefix in ("https://", "http://"): + if value.lower().startswith(prefix): + scheme = prefix[:-3] + value = value[len(prefix):] + first_label = value.split(".", 1)[0] + if not value or "/" in value or value.count("*") != 1 or "*" not in first_label: + return None + return scheme, value.lower() + + +def primary_wildcard(): + """Recovers the primary wildcard hostname from VSCODE_PROXY_URI. + + Coder sets it to :// with "*" replaced by + "{{port}}------". + """ + if not proxy_uri: + return None + marker = "{{port}}--%s--%s--%s" % (agent, workspace, owner) + parts = urlsplit(proxy_uri) + host = parts.netloc + idx = host.lower().find(marker.lower()) if agent else -1 + if parts.scheme not in ("http", "https") or idx < 0: + log("WARNING: VSCODE_PROXY_URI has an unexpected format: %s" % proxy_uri) + return None + return split_wildcard( + "%s://%s*%s" % (parts.scheme, host[:idx], host[idx + len(marker):]), + parts.scheme, + ) + + +def expand(wildcard, subdomain): + scheme, pattern = wildcard + return scheme, pattern.replace("*", subdomain.lower(), 1) + + +wildcards = [] +primary_from_proxy_uri = None +if not override: + primary_from_proxy_uri = primary_wildcard() + if primary_from_proxy_uri: + wildcards.append(primary_from_proxy_uri) +for line in env.get("KC_PROXY_HOSTNAMES", "").splitlines(): + if not line.strip(): + continue + parsed = split_wildcard(line, access_scheme) + if parsed is None: + log("WARNING: ignoring invalid workspace proxy hostname: %s" % line) + elif parsed not in wildcards: + wildcards.append(parsed) + +# Coder omits the agent from slug app subdomains, but also routes the form +# that names the agent, so both are trusted. +suffixes = ["--%s--%s" % (workspace, owner)] +if agent: + suffixes.append("--%s--%s--%s" % (agent, workspace, owner)) + +routes = {} +for wildcard in wildcards: + for suffix in suffixes: + _, app_host = expand(wildcard, app_slug + suffix) + scheme, dash_host = expand(wildcard, dash_slug + suffix) + routes[app_host] = "%s://%s" % (scheme, dash_host) + +if override: + primary = override +elif wildcards: + scheme, dash_host = expand(wildcards[0], dash_slug + suffixes[0]) + primary = "%s://%s" % (scheme, dash_host) + if primary_from_proxy_uri is None: + log("WARNING: VSCODE_PROXY_URI is not set; using the first workspace proxy as the primary dashboard origin.") +else: + primary = fallback + log("WARNING: could not determine the wildcard app hostname; assuming %s. Set dashboard_origin if the dashboard is unreachable." % fallback) + +allowed = [primary] +for origin in list(routes.values()) + env.get("KC_EXTRA_ALLOWED_ORIGINS", "").split(","): + origin = origin.strip().rstrip("/") + if origin and origin not in allowed: + allowed.append(origin) + +with open(sys.argv[1], "w") as f: + json.dump({"primary": primary, "routes": routes, "allowed_origins": allowed}, f, indent=2) +print(primary) +print(",".join(allowed)) +PYEOF + +DASHBOARD_URL="" +ALLOWED_ORIGINS="" +{ + read -r DASHBOARD_URL + read -r ALLOWED_ORIGINS +} < <( + KC_APP_SLUG="$${ARG_APP_SLUG}" \ + KC_DASHBOARD_SLUG="$${ARG_DASHBOARD_SLUG}" \ + KC_WORKSPACE_NAME="$${ARG_WORKSPACE_NAME}" \ + KC_OWNER_NAME="$${ARG_OWNER_NAME}" \ + KC_ACCESS_SCHEME="$${ARG_ACCESS_SCHEME}" \ + KC_DASHBOARD_ORIGIN="$${ARG_DASHBOARD_ORIGIN}" \ + KC_FALLBACK_ORIGIN="$${ARG_FALLBACK_ORIGIN}" \ + KC_PROXY_HOSTNAMES="$${ARG_PROXY_HOSTNAMES}" \ + KC_EXTRA_ALLOWED_ORIGINS="$${ARG_EXTRA_ALLOWED_ORIGINS}" \ + "$${PYTHON_BIN}" "$${ORIGINS_HELPER_PATH}" "$${ORIGINS_PATH}" +) +if [ -z "$${DASHBOARD_URL}" ]; then + echo "ERROR: could not compute the Kiro Crew dashboard origins." >&2 + exit 1 +fi +echo "Primary dashboard origin: $${DASHBOARD_URL}" +echo "Gateway allowed origins: $${ALLOWED_ORIGINS}" + if [ -n "$${KIRO_API_KEY:-}" ]; then echo "Headless Kiro auth: KIRO_API_KEY detected; kiro-cli authenticates non-interactively." else @@ -105,16 +259,18 @@ chmod +x "$${LAUNCHER_PATH}" setsid nohup env \ KC_CMD="$${KIROCREW_CMD}" \ KC_PORT="$${ARG_PORT}" \ - KC_ORIGINS="$${ARG_ALLOWED_ORIGINS}" \ - KC_DASHBOARD_URL="$${ARG_DASHBOARD_URL}" \ + KC_ORIGINS="$${ALLOWED_ORIGINS}" \ + KC_DASHBOARD_URL="$${DASHBOARD_URL}" \ bash "$${LAUNCHER_PATH}" >> "$${GATEWAY_LOG}" 2>&1 < /dev/null & disown 2> /dev/null || true echo "Kiro Crew gateway launch dispatched." # The visible app tile points at this loopback redirector. Each request mints # a short-lived token from the gateway's loopback secret and redirects to the -# dashboard app with ?token=. /healthz answers without minting a token so the -# app healthcheck does not create tokens. +# dashboard app with ?token=. Coder forwards the browser's Host, which selects +# the dashboard origin on the same primary or proxy hostname. Unknown hosts go +# to the primary origin. /healthz answers without minting a token so the app +# healthcheck does not create tokens. cat > "$${REDIRECTOR_PATH}" << 'PYEOF' import http.server import json @@ -123,7 +279,10 @@ import urllib.request REDIR_PORT = int(os.environ["REDIR_PORT"]) GW_BASE = "http://127.0.0.1:" + os.environ["GW_PORT"] -DASH = os.environ["DASH_ORIGIN"].rstrip("/") +with open(os.environ["ORIGINS_PATH"]) as f: + ORIGINS = json.load(f) +PRIMARY = ORIGINS["primary"].rstrip("/") +ROUTES = ORIGINS["routes"] SECRET = os.path.expanduser("~/.kiro/crew/.local_secret") @@ -138,6 +297,10 @@ class Handler(http.server.BaseHTTPRequestHandler): with urllib.request.urlopen(req, timeout=5) as resp: return json.load(resp).get("token", "") + def _dashboard_origin(self): + host = (self.headers.get("Host") or "").strip().lower().rstrip(".") + return ROUTES.get(host, PRIMARY).rstrip("/") + def do_GET(self): if self.path.startswith("/healthz"): self.send_response(200) @@ -154,7 +317,9 @@ class Handler(http.server.BaseHTTPRequestHandler): self.wfile.write(b"Kiro Crew gateway still starting - retry shortly") return self.send_response(302) - self.send_header("Location", DASH + "/?token=" + token) + self.send_header("Location", self._dashboard_origin() + "/?token=" + token) + self.send_header("Cache-Control", "no-store") + self.send_header("Referrer-Policy", "no-referrer") self.end_headers() def log_message(self, *args): @@ -168,9 +333,9 @@ PYEOF setsid nohup env \ REDIR_PORT="$${ARG_REDIRECT_PORT}" \ GW_PORT="$${ARG_PORT}" \ - DASH_ORIGIN="$${ARG_DASHBOARD_URL}" \ + ORIGINS_PATH="$${ORIGINS_PATH}" \ "$${PYTHON_BIN}" "$${REDIRECTOR_PATH}" >> "$${REDIRECTOR_LOG}" 2>&1 < /dev/null & disown 2> /dev/null || true -echo "Kiro Crew redirector dispatched on port $${ARG_REDIRECT_PORT} -> $${ARG_DASHBOARD_URL}" +echo "Kiro Crew redirector dispatched on port $${ARG_REDIRECT_PORT} -> $${DASHBOARD_URL}" exit 0 From 6002c25e2ffcd4b5c3780c0595a455651a79cb63 Mon Sep 17 00:00:00 2001 From: default Date: Fri, 23 Oct 2026 16:57:05 +0000 Subject: [PATCH 05/10] fix(coder-labs/kiro-crew): write generated start helpers to the module scripts directory The gateway launcher, redirector, and origins helper are module artifacts, so they now live in $HOME/.coder-modules/coder-labs/kiro-crew/scripts/ next to the coder-utils scripts, and the origins snapshot in the module directory, instead of Kiro Crew's own ~/.kiro/crew data directory. Copies left there by earlier versions are removed. Generated with Coder Agents. --- .../coder-labs/modules/kiro-crew/main.test.ts | 31 ++++++++++++++----- .../modules/kiro-crew/scripts/start.sh.tftpl | 16 +++++----- 2 files changed, 33 insertions(+), 14 deletions(-) diff --git a/registry/coder-labs/modules/kiro-crew/main.test.ts b/registry/coder-labs/modules/kiro-crew/main.test.ts index 2c61a73d5..948e21745 100644 --- a/registry/coder-labs/modules/kiro-crew/main.test.ts +++ b/registry/coder-labs/modules/kiro-crew/main.test.ts @@ -68,7 +68,9 @@ afterEach(async () => { }); const moduleDir = path.resolve(import.meta.dir); -const moduleLogDir = "/home/coder/.coder-modules/coder-labs/kiro-crew/logs"; +const moduleDataDir = "/home/coder/.coder-modules/coder-labs/kiro-crew"; +const moduleLogDir = `${moduleDataDir}/logs`; +const moduleScriptsDir = `${moduleDataDir}/scripts`; // Containers share the host network, so use ports unlikely to be in use. const gatewayPort = "19899"; @@ -422,6 +424,11 @@ describe("kiro-crew", async () => { const { workspace, owner } = workspaceNames(state); const dashboardOrigin = `https://kirocrew-dashboard--${workspace}--${owner}--apps.coder.example.com`; await runOrFail(id, "install", scripts.install); + await execContainer(id, [ + "bash", + "-c", + "mkdir -p ~/.kiro/crew && touch ~/.kiro/crew/start-gateway.sh ~/.kiro/crew/redirect-gateway.py", + ]); const start = await runOrFail(id, "start", scripts.start, { KIRO_AWS_BUILDER_ID_TOKEN: "builder-token", ...proxyURIEnv(state), @@ -456,14 +463,24 @@ describe("kiro-crew", async () => { const calls = await readFileContainer(id, "/tmp/kirocrew-calls"); expect(calls).toContain(`config set dashboard.url ${dashboardOrigin}`); - for (const file of ["start.log", "gateway.log", "redirector.log"]) { - const exists = await execContainer(id, [ - "test", - "-f", - `${moduleLogDir}/${file}`, - ]); + for (const file of [ + `${moduleLogDir}/start.log`, + `${moduleLogDir}/gateway.log`, + `${moduleLogDir}/redirector.log`, + `${moduleScriptsDir}/start-gateway.sh`, + `${moduleScriptsDir}/redirect-gateway.py`, + `${moduleScriptsDir}/coder-origins.py`, + `${moduleDataDir}/coder-origins.json`, + ]) { + const exists = await execContainer(id, ["test", "-f", file]); expect(exists.exitCode).toBe(0); } + const legacy = await execContainer(id, [ + "bash", + "-c", + "ls ~/.kiro/crew/start-gateway.sh ~/.kiro/crew/redirect-gateway.py", + ]); + expect(legacy.exitCode).not.toBe(0); }); test("redirector-keeps-browser-on-its-workspace-proxy", async () => { diff --git a/registry/coder-labs/modules/kiro-crew/scripts/start.sh.tftpl b/registry/coder-labs/modules/kiro-crew/scripts/start.sh.tftpl index 296c907d8..c8acbcc54 100644 --- a/registry/coder-labs/modules/kiro-crew/scripts/start.sh.tftpl +++ b/registry/coder-labs/modules/kiro-crew/scripts/start.sh.tftpl @@ -20,13 +20,15 @@ ARG_EXTRA_ALLOWED_ORIGINS=$(echo -n '${ARG_EXTRA_ALLOWED_ORIGINS}' | base64 -d) LOG_DIR="$${ARG_MODULE_DIRECTORY}/logs" GATEWAY_LOG="$${LOG_DIR}/gateway.log" REDIRECTOR_LOG="$${LOG_DIR}/redirector.log" -CREW_DIR="$HOME/.kiro/crew" -LAUNCHER_PATH="$${CREW_DIR}/start-gateway.sh" -REDIRECTOR_PATH="$${CREW_DIR}/redirect-gateway.py" -ORIGINS_HELPER_PATH="$${CREW_DIR}/coder-origins.py" -ORIGINS_PATH="$${CREW_DIR}/coder-origins.json" - -mkdir -p "$${LOG_DIR}" "$${CREW_DIR}" +SCRIPTS_DIR="$${ARG_MODULE_DIRECTORY}/scripts" +LAUNCHER_PATH="$${SCRIPTS_DIR}/start-gateway.sh" +REDIRECTOR_PATH="$${SCRIPTS_DIR}/redirect-gateway.py" +ORIGINS_HELPER_PATH="$${SCRIPTS_DIR}/coder-origins.py" +ORIGINS_PATH="$${ARG_MODULE_DIRECTORY}/coder-origins.json" + +mkdir -p "$${LOG_DIR}" "$${SCRIPTS_DIR}" +# Earlier versions wrote these into Kiro Crew's own data directory. +rm -f "$HOME/.kiro/crew/start-gateway.sh" "$HOME/.kiro/crew/redirect-gateway.py" export PATH="$HOME/.local/bin:$HOME/bin:$PATH" # Everything below is best effort and must reach the launch step, so disable From 2c480ba622e067aeae792436445f01caa5fae021 Mon Sep 17 00:00:00 2001 From: default Date: Fri, 23 Oct 2026 16:59:30 +0000 Subject: [PATCH 06/10] test(coder-labs/kiro-crew): drop stale pipx expectation from the installer test aa2eec3 removed the module's dedicated pipx, but the test still expected it. Generated with Coder Agents. --- registry/coder-labs/modules/kiro-crew/main.test.ts | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/registry/coder-labs/modules/kiro-crew/main.test.ts b/registry/coder-labs/modules/kiro-crew/main.test.ts index 948e21745..0006787fa 100644 --- a/registry/coder-labs/modules/kiro-crew/main.test.ts +++ b/registry/coder-labs/modules/kiro-crew/main.test.ts @@ -213,7 +213,7 @@ chmod +x "$HOME/.local/bin/kiro-cli" containerId: id, filePath: "/tmp/fixtures/kiro-crew-install.sh", content: `#!/bin/sh -echo "kiro-crew installer ran PYTHONPATH=\${PYTHONPATH-unset} pipx=$(command -v pipx)" >> /tmp/installers-ran +echo "kiro-crew installer ran PYTHONPATH=\${PYTHONPATH-unset}" >> /tmp/installers-ran mkdir -p "$HOME/.kiro/crew-venv/bin" cp /tmp/kirocrew-mock.sh "$HOME/.kiro/crew-venv/bin/kirocrew" chmod +x "$HOME/.kiro/crew-venv/bin/kirocrew" @@ -226,12 +226,6 @@ chmod +x "$HOME/.kiro/crew-venv/bin/kirocrew" path.join(moduleDir, "testdata", "kirocrew-mock.sh"), ).text(), }); - // Pre-create the dedicated pipx so the test does not hit PyPI. - await execContainer(id, [ - "bash", - "-c", - `mkdir -p "$HOME/.kiro/pipx-venv/bin" && printf '#!/bin/sh\\nexit 0\\n' > "$HOME/.kiro/pipx-venv/bin/pipx" && chmod +x "$HOME/.kiro/pipx-venv/bin/pipx"`, - ]); }; // Polls a URL from inside the container until curl reports the expected @@ -347,9 +341,7 @@ describe("kiro-crew", async () => { expect(curlArgs).toContain("https://download.crew.kiro.dev/cli.sh"); const ran = await readFileContainer(id, "/tmp/installers-ran"); expect(ran).toContain("kiro-cli installer ran"); - expect(ran).toContain( - "kiro-crew installer ran PYTHONPATH=unset pipx=/home/coder/.local/bin/pipx", - ); + expect(ran).toContain("kiro-crew installer ran PYTHONPATH=unset"); const log = await readFileContainer(id, `${moduleLogDir}/install.log`); expect(log).toContain("Kiro CLI ready."); expect(log).toContain("Kiro Crew ready."); From afddd21bcadfc79241d8423bc5ba3e8bfd769380 Mon Sep 17 00:00:00 2001 From: default Date: Fri, 23 Oct 2026 17:24:26 +0000 Subject: [PATCH 07/10] feat(coder-labs/kiro-crew): log decoded script inputs at the top of install and start Prints the non-sensitive Terraform inputs after decoding, matching the claude-code module, plus the agent's CODER_WORKSPACE_AGENT_NAME and VSCODE_PROXY_URI that the dashboard origins derive from. extra_mcp_json is shown only as "(set)". Generated with Coder Agents. --- .../coder-labs/modules/kiro-crew/main.test.ts | 24 ++++++++++++++++++- .../kiro-crew/scripts/install.sh.tftpl | 6 +++-- .../modules/kiro-crew/scripts/start.sh.tftpl | 18 ++++++++++++++ 3 files changed, 45 insertions(+), 3 deletions(-) diff --git a/registry/coder-labs/modules/kiro-crew/main.test.ts b/registry/coder-labs/modules/kiro-crew/main.test.ts index 0006787fa..dfe3dbe25 100644 --- a/registry/coder-labs/modules/kiro-crew/main.test.ts +++ b/registry/coder-labs/modules/kiro-crew/main.test.ts @@ -325,6 +325,10 @@ describe("kiro-crew", async () => { expect(log).toContain("Kiro CLI already installed: kiro-cli 0.0.0-test"); expect(log).toContain("Kiro Crew already installed: /usr/bin/kirocrew"); expect(log).toContain("Kiro Crew install completed."); + expect(log).toContain("ARG_USE_CACHED: true"); + expect(log).toContain( + "ARG_KIROCREW_INSTALL_URL: https://download.crew.kiro.dev/cli.sh", + ); const curl = await execContainer(id, ["test", "-e", "/tmp/curl-args"]); expect(curl.exitCode).not.toBe(0); }); @@ -378,6 +382,12 @@ describe("kiro-crew", async () => { moduleVariables: { extra_mcp_json: extra }, }); await runOrFail(id, "install", scripts.install); + const installLog = await readFileContainer( + id, + `${moduleLogDir}/install.log`, + ); + expect(installLog).toContain("ARG_EXTRA_MCP_JSON: (set)"); + expect(installLog).not.toContain("mcpServers"); const mcp = await readFileContainer( id, "/home/coder/.kiro/settings/mcp.json", @@ -488,7 +498,19 @@ describe("kiro-crew", async () => { const { workspace, owner } = workspaceNames(state); const sub = `${workspace}--${owner}`; await runOrFail(id, "install", scripts.install); - await runOrFail(id, "start", scripts.start, proxyURIEnv(state)); + const start = await runOrFail( + id, + "start", + scripts.start, + proxyURIEnv(state), + ); + expect(start.stdout).toContain( + "ARG_PROXY_HOSTNAMES: *--apps.eu.coder.example.com,http://*.apps.lab.example.com:8080", + ); + expect(start.stdout).toContain(`ARG_WORKSPACE_NAME: ${workspace}`); + expect(start.stdout).toContain( + `VSCODE_PROXY_URI: ${proxyURIEnv(state).VSCODE_PROXY_URI}`, + ); // Primary deployment. expect( diff --git a/registry/coder-labs/modules/kiro-crew/scripts/install.sh.tftpl b/registry/coder-labs/modules/kiro-crew/scripts/install.sh.tftpl index ab3ebf7a2..f908a7d5a 100644 --- a/registry/coder-labs/modules/kiro-crew/scripts/install.sh.tftpl +++ b/registry/coder-labs/modules/kiro-crew/scripts/install.sh.tftpl @@ -17,8 +17,10 @@ mkdir -p "$HOME/.local/bin" export PATH="$HOME/.local/bin:$HOME/bin:$PATH" echo "--------------------------------" -printf "use_cached: %s\n" "$${ARG_USE_CACHED}" -printf "extra_mcp_json: %s\n" "$${ARG_EXTRA_MCP_JSON:+(set)}" +printf "ARG_USE_CACHED: %s\n" "$${ARG_USE_CACHED}" +printf "ARG_EXTRA_MCP_JSON: %s\n" "$${ARG_EXTRA_MCP_JSON:+(set)}" +printf "ARG_KIRO_CLI_INSTALL_URL: %s\n" "$${ARG_KIRO_CLI_INSTALL_URL}" +printf "ARG_KIROCREW_INSTALL_URL: %s\n" "$${ARG_KIROCREW_INSTALL_URL}" echo "--------------------------------" # Downloads an installer to a temporary file so a failed or partial download diff --git a/registry/coder-labs/modules/kiro-crew/scripts/start.sh.tftpl b/registry/coder-labs/modules/kiro-crew/scripts/start.sh.tftpl index c8acbcc54..2db96e960 100644 --- a/registry/coder-labs/modules/kiro-crew/scripts/start.sh.tftpl +++ b/registry/coder-labs/modules/kiro-crew/scripts/start.sh.tftpl @@ -17,6 +17,24 @@ ARG_FALLBACK_ORIGIN=$(echo -n '${ARG_FALLBACK_ORIGIN}' | base64 -d) ARG_PROXY_HOSTNAMES=$(echo -n '${ARG_PROXY_HOSTNAMES}' | base64 -d) ARG_EXTRA_ALLOWED_ORIGINS=$(echo -n '${ARG_EXTRA_ALLOWED_ORIGINS}' | base64 -d) +echo "--------------------------------" +printf "ARG_MODULE_DIRECTORY: %s\n" "$${ARG_MODULE_DIRECTORY}" +printf "ARG_PORT: %s\n" "$${ARG_PORT}" +printf "ARG_REDIRECT_PORT: %s\n" "$${ARG_REDIRECT_PORT}" +printf "ARG_APP_SLUG: %s\n" "$${ARG_APP_SLUG}" +printf "ARG_DASHBOARD_SLUG: %s\n" "$${ARG_DASHBOARD_SLUG}" +printf "ARG_WORKSPACE_NAME: %s\n" "$${ARG_WORKSPACE_NAME}" +printf "ARG_OWNER_NAME: %s\n" "$${ARG_OWNER_NAME}" +printf "ARG_ACCESS_SCHEME: %s\n" "$${ARG_ACCESS_SCHEME}" +printf "ARG_DASHBOARD_ORIGIN: %s\n" "$${ARG_DASHBOARD_ORIGIN}" +printf "ARG_FALLBACK_ORIGIN: %s\n" "$${ARG_FALLBACK_ORIGIN}" +printf "ARG_PROXY_HOSTNAMES: %s\n" "$${ARG_PROXY_HOSTNAMES//$'\n'/,}" +printf "ARG_EXTRA_ALLOWED_ORIGINS: %s\n" "$${ARG_EXTRA_ALLOWED_ORIGINS}" +# Runtime inputs from the agent that the dashboard origins are derived from. +printf "CODER_WORKSPACE_AGENT_NAME: %s\n" "$${CODER_WORKSPACE_AGENT_NAME:-}" +printf "VSCODE_PROXY_URI: %s\n" "$${VSCODE_PROXY_URI:-}" +echo "--------------------------------" + LOG_DIR="$${ARG_MODULE_DIRECTORY}/logs" GATEWAY_LOG="$${LOG_DIR}/gateway.log" REDIRECTOR_LOG="$${LOG_DIR}/redirector.log" From c4a95459aa997e10b996023e46d1a8df0a52e22e Mon Sep 17 00:00:00 2001 From: default Date: Fri, 23 Oct 2026 19:53:23 +0000 Subject: [PATCH 08/10] chore(registry/coder-labs/templates/kiro-crew): move template to a separate PR The template validates only after the module is published, so it moves to the superblink/coder-labs-kiro-crew-template branch and lands after this module PR. Generated with Coder Agents. --- .../coder-labs/templates/kiro-crew/README.md | 62 --- .../templates/kiro-crew/kiro-crew-prereqs.sh | 19 - .../coder-labs/templates/kiro-crew/main.tf | 380 ------------------ .../coder-labs/templates/kiro-crew/startup.sh | 74 ---- 4 files changed, 535 deletions(-) delete mode 100644 registry/coder-labs/templates/kiro-crew/README.md delete mode 100644 registry/coder-labs/templates/kiro-crew/kiro-crew-prereqs.sh delete mode 100644 registry/coder-labs/templates/kiro-crew/main.tf delete mode 100644 registry/coder-labs/templates/kiro-crew/startup.sh diff --git a/registry/coder-labs/templates/kiro-crew/README.md b/registry/coder-labs/templates/kiro-crew/README.md deleted file mode 100644 index f1e56c3d9..000000000 --- a/registry/coder-labs/templates/kiro-crew/README.md +++ /dev/null @@ -1,62 +0,0 @@ ---- -display_name: Kiro Crew on Kubernetes -description: Kubernetes workspaces that run the Kiro Crew gateway and web dashboard, with Kiro CLI, code-server, and a persistent home volume. -icon: ../../../../.icons/kiro.svg -verified: false -tags: [kubernetes, container, kiro, ai, agent, mcp] ---- - -# Kiro Crew on Kubernetes - -Provision Kubernetes pods as [Coder workspaces](https://coder.com/docs/workspaces) that run [Kiro Crew](https://github.com/kirodotdev/KiroCrew). Kiro Crew is a persistent development workspace built on Kiro CLI: a long-running gateway owns memory, transcripts, and artifacts, and a web dashboard lets you chat with agents, run multi-step tasks, schedule jobs, and extend agents with skills and MCP servers. - -The template uses the [kiro-crew](../../modules/kiro-crew/README.md) module to install Kiro CLI and Kiro Crew, start the gateway, and publish the dashboard through Coder's authenticated subdomain proxy. The **Kiro Crew** app opens an authenticated dashboard without a separate token step. - -Each workspace also includes: - -- A **Kiro CLI** terminal app for interactive sign-in. -- code-server and the Kiro IDE app. -- The Coder MCP server in `~/.kiro/settings/mcp.json`, so agents can work with Coder workspaces and templates. -- An optional repository clone on start. -- A persistent volume at `/home/coder` that keeps installs, Kiro credentials, and Kiro Crew state across restarts. -- CPU, memory, and disk usage on the workspace page. - -## Prerequisites - -### Infrastructure - -- A Kubernetes cluster that Coder can provision into. The namespace must exist before you create workspaces. If the Coder host runs outside the cluster, set `use_kubeconfig = true` and provide `~/.kube/config` on the Coder host. -- A storage class that can provision `ReadWriteOnce` persistent volumes. -- [Wildcard subdomain apps](https://coder.com/docs/admin/setup#wildcard-access-url). The dashboard is a subdomain app, and the dashboard app tile does not resolve without a wildcard access URL. -- Outbound access from workspace pods to the Ubuntu package mirrors, the Kiro download hosts (`cli.kiro.dev`, `download.crew.kiro.dev`), and the hosts the bundled Kiro Crew installer uses to provision its managed Python (`uv`/Astral and PyPI). - -The default image pins `codercom/enterprise-base` to a known-good Ubuntu 24.04 build. The Kiro Crew installer provisions its own managed Python via `uv`, so the image does not need a specific system Python. - -### Kiro account - -Agents cannot call models until Kiro CLI is authenticated. Kiro credentials are stored in the persistent home volume, so you only sign in once per workspace. - -- **Headless:** before you start the workspace, create a Coder user secret that sets `KIRO_API_KEY`: - - ```sh - coder secret create kiro-api-key --description "Kiro CLI headless auth" --env KIRO_API_KEY - ``` - - The gateway inherits the variable and Kiro CLI authenticates without a browser. Generating API keys requires a Kiro subscription that supports them; on enterprise-managed accounts an administrator must enable it. Rotating or deleting the Coder secret does not revoke the key in Kiro. - -- **Interactive:** open the **Kiro CLI** app, run `kiro-cli login`, and complete the device-code sign-in with AWS Builder ID or IAM Identity Center. Check the result with `kiro-cli whoami`. - -If agents need AWS credentials, set `service_account_name` to a service account that is mapped to an IAM role, or run `aws configure` in the workspace. - -## Use the dashboard - -1. Authenticate Kiro CLI as described above. -1. Open the **Kiro Crew** app. The gateway starts in the background, so the app can report `503` or show as unhealthy for a short time after the workspace starts. -1. Start a session. If model calls fail with an authentication error, complete Kiro sign-in and retry. - -Gateway, redirector, and script logs are in `~/.coder-modules/coder-labs/kiro-crew/logs/`. - -## Notes - -- Kiro Crew sandboxes agent subprocesses with Linux user namespaces. Many Kubernetes nodes disable them (`user.max_user_namespaces=0`); in that case the module allows unsandboxed execution and the pod is the isolation boundary. -- The Coder MCP configuration stores your Coder session token in `~/.kiro/settings/mcp.json` on the workspace volume. Remove the `coder` entry from that file if you do not want agents to act on your behalf in Coder. diff --git a/registry/coder-labs/templates/kiro-crew/kiro-crew-prereqs.sh b/registry/coder-labs/templates/kiro-crew/kiro-crew-prereqs.sh deleted file mode 100644 index 9cf6e61a0..000000000 --- a/registry/coder-labs/templates/kiro-crew/kiro-crew-prereqs.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/env bash -# Pre-install script for the kiro-crew module. Installs the system packages -# the Kiro CLI and Kiro Crew installers need. The Kiro Crew installer -# provisions its own managed Python via uv, so no system Python is required. -set -euo pipefail - -sudo apt-get update -qq - -# curl, unzip, openssl, and sha256sum (coreutils) are used by the installers; -# Kiro Crew verifies its signed manifest with openssl and sha256sum. -sudo apt-get install -y -qq \ - curl \ - unzip \ - gnupg \ - dirmngr \ - openssl \ - ca-certificates - -echo "Kiro Crew prerequisites installed." diff --git a/registry/coder-labs/templates/kiro-crew/main.tf b/registry/coder-labs/templates/kiro-crew/main.tf deleted file mode 100644 index 3af956626..000000000 --- a/registry/coder-labs/templates/kiro-crew/main.tf +++ /dev/null @@ -1,380 +0,0 @@ -terraform { - required_providers { - coder = { - source = "coder/coder" - version = ">= 2.13" - } - kubernetes = { - source = "hashicorp/kubernetes" - version = ">= 2.37" - } - } -} - -variable "use_kubeconfig" { - type = bool - description = <<-EOF - Use host kubeconfig? (true/false) - - Set this to false if the Coder host is itself running as a Pod on the same - Kubernetes cluster as you are deploying workspaces to. - - Set this to true if the Coder host is running outside the Kubernetes cluster - for workspaces. A valid "~/.kube/config" must be present on the Coder host. - EOF - default = false -} - -variable "namespace" { - type = string - description = "Kubernetes namespace to create workspaces in. The namespace must exist before workspaces are created." - default = "coder" -} - -variable "service_account_name" { - type = string - description = "Optional Kubernetes service account for workspace pods, for example one mapped to an IAM role. Leave empty to use the namespace's default service account." - default = "" -} - -variable "image" { - type = string - description = <<-EOF - Container image for workspaces. The default pins codercom/enterprise-base - to a known-good Ubuntu 24.04 build. The Kiro Crew installer provisions its - own managed Python via uv, so the image does not need a specific system - Python. - EOF - default = "codercom/enterprise-base@sha256:16031b4de2e099884ad6b4057bffeaf03380a76eddf46c193b5d0767e738ba96" -} - -provider "kubernetes" { - # Authenticate via ~/.kube/config or a Coder-specific ServiceAccount, depending on admin preferences. - config_path = var.use_kubeconfig == true ? "~/.kube/config" : null -} - -locals { - home_dir = "/home/coder" -} - -data "coder_parameter" "cpu" { - name = "cpu" - display_name = "CPU cores" - description = "CPU core limit for the workspace." - type = "number" - form_type = "input" - icon = "/icon/memory.svg" - default = 2 - mutable = true - order = 1 - validation { - min = 2 - max = 8 - } -} - -data "coder_parameter" "memory" { - name = "memory" - display_name = "Memory (GB)" - description = "Memory limit for the workspace, in GB. The Kiro Crew gateway and its agents can use several GB." - type = "number" - form_type = "input" - icon = "/icon/memory.svg" - default = 8 - mutable = true - order = 2 - validation { - min = 4 - max = 16 - } -} - -data "coder_parameter" "home_disk_size" { - name = "home_disk_size" - display_name = "Home disk size (GB)" - description = "Size of the persistent volume mounted at ${local.home_dir}, in GB. It can only grow." - type = "number" - form_type = "slider" - icon = "/icon/database.svg" - default = 30 - mutable = true - order = 3 - validation { - min = 10 - max = 50 - monotonic = "increasing" - } -} - -data "coder_parameter" "git_repo" { - name = "git_repo" - display_name = "Git repository" - description = "Optional repository to clone into the workspace on start. Leave empty to skip." - type = "string" - icon = "/icon/git.svg" - default = "" - mutable = true - order = 4 -} - -data "coder_workspace" "me" {} -data "coder_workspace_owner" "me" {} - -resource "coder_agent" "main" { - arch = "amd64" - os = "linux" - - # The agent expands $PATH, so the agent's own directory (and the coder - # binary that module scripts call) stays on PATH. - env = { - PATH = "${local.home_dir}/.local/bin:${local.home_dir}/bin:$PATH" - } - - display_apps { - vscode = false - vscode_insiders = false - web_terminal = true - ssh_helper = false - } - - # The Kiro Crew gateway and its agent subprocesses can be CPU, memory, and - # disk intensive, so surface usage on the workspace page. - metadata { - display_name = "CPU Usage" - key = "0_cpu_usage" - script = "coder stat cpu" - interval = 10 - timeout = 1 - } - - metadata { - display_name = "RAM Usage" - key = "1_ram_usage" - script = "coder stat mem" - interval = 10 - timeout = 1 - } - - metadata { - display_name = "Home Disk" - key = "2_home_disk" - script = "coder stat disk --path ${local.home_dir}" - interval = 60 - timeout = 1 - } - - startup_script_behavior = "blocking" - startup_script = file("${path.module}/startup.sh") -} - -module "coder-login" { - source = "registry.coder.com/coder/coder-login/coder" - version = "1.1.1" - agent_id = coder_agent.main.id -} - -module "code-server" { - source = "registry.coder.com/coder/code-server/coder" - version = "1.6.0" - agent_id = coder_agent.main.id - folder = local.home_dir - subdomain = false - order = 0 -} - -module "kiro" { - source = "registry.coder.com/coder/kiro/coder" - version = "1.2.2" - agent_id = coder_agent.main.id - order = 1 -} - -module "kiro-crew" { - source = "registry.coder.com/coder-labs/kiro-crew/coder" - version = "1.0.0" - agent_id = coder_agent.main.id - order = 3 - - # Installs the system packages the Kiro CLI and Kiro Crew installers need - # before the module's install script runs. - pre_install_script = file("${path.module}/kiro-crew-prereqs.sh") -} - -# Terminal app for interactive Kiro CLI sign-in (`kiro-cli login`). -resource "coder_app" "kiro_cli" { - agent_id = coder_agent.main.id - slug = "kiro-cli" - display_name = "Kiro CLI" - icon = "/icon/kiro.svg" - command = "kiro-cli" - share = "owner" - order = 2 -} - -module "git-config" { - count = data.coder_workspace.me.start_count - source = "registry.coder.com/coder/git-config/coder" - version = "1.0.35" - agent_id = coder_agent.main.id -} - -module "git-clone" { - count = data.coder_parameter.git_repo.value != "" ? data.coder_workspace.me.start_count : 0 - source = "registry.coder.com/coder/git-clone/coder" - version = "2.0.5" - agent_id = coder_agent.main.id - url = data.coder_parameter.git_repo.value -} - -resource "kubernetes_persistent_volume_claim_v1" "home" { - metadata { - name = "coder-${data.coder_workspace.me.id}-home" - namespace = var.namespace - labels = { - "app.kubernetes.io/name" = "coder-pvc" - "app.kubernetes.io/instance" = "coder-pvc-${data.coder_workspace.me.id}" - "app.kubernetes.io/part-of" = "coder" - "com.coder.resource" = "true" - "com.coder.workspace.id" = data.coder_workspace.me.id - "com.coder.workspace.name" = data.coder_workspace.me.name - "com.coder.user.id" = data.coder_workspace_owner.me.id - "com.coder.user.username" = data.coder_workspace_owner.me.name - } - annotations = { - "com.coder.user.email" = data.coder_workspace_owner.me.email - } - } - wait_until_bound = false - spec { - access_modes = ["ReadWriteOnce"] - resources { - requests = { - storage = "${data.coder_parameter.home_disk_size.value}Gi" - } - } - } -} - -resource "kubernetes_deployment_v1" "main" { - count = data.coder_workspace.me.start_count - depends_on = [kubernetes_persistent_volume_claim_v1.home] - wait_for_rollout = false - - metadata { - name = "coder-${data.coder_workspace.me.id}" - namespace = var.namespace - labels = { - "app.kubernetes.io/name" = "coder-workspace" - "app.kubernetes.io/instance" = "coder-workspace-${data.coder_workspace.me.id}" - "app.kubernetes.io/part-of" = "coder" - "com.coder.resource" = "true" - "com.coder.workspace.id" = data.coder_workspace.me.id - "com.coder.workspace.name" = data.coder_workspace.me.name - "com.coder.user.id" = data.coder_workspace_owner.me.id - "com.coder.user.username" = data.coder_workspace_owner.me.name - } - annotations = { - "com.coder.user.email" = data.coder_workspace_owner.me.email - } - } - - spec { - replicas = 1 - selector { - match_labels = { - "app.kubernetes.io/name" = "coder-workspace" - "app.kubernetes.io/instance" = "coder-workspace-${data.coder_workspace.me.id}" - "app.kubernetes.io/part-of" = "coder" - "com.coder.resource" = "true" - "com.coder.workspace.id" = data.coder_workspace.me.id - "com.coder.workspace.name" = data.coder_workspace.me.name - "com.coder.user.id" = data.coder_workspace_owner.me.id - "com.coder.user.username" = data.coder_workspace_owner.me.name - } - } - strategy { - type = "Recreate" - } - - template { - metadata { - labels = { - "app.kubernetes.io/name" = "coder-workspace" - "app.kubernetes.io/instance" = "coder-workspace-${data.coder_workspace.me.id}" - "app.kubernetes.io/part-of" = "coder" - "com.coder.resource" = "true" - "com.coder.workspace.id" = data.coder_workspace.me.id - "com.coder.workspace.name" = data.coder_workspace.me.name - "com.coder.user.id" = data.coder_workspace_owner.me.id - "com.coder.user.username" = data.coder_workspace_owner.me.name - } - } - spec { - security_context { - run_as_user = 1000 - fs_group = 1000 - } - service_account_name = var.service_account_name != "" ? var.service_account_name : null - - container { - name = "dev" - image = var.image - image_pull_policy = "IfNotPresent" - command = ["sh", "-c", coder_agent.main.init_script] - security_context { - run_as_user = "1000" - } - - env { - name = "CODER_AGENT_TOKEN" - value = coder_agent.main.token - } - - resources { - requests = { - "cpu" = "250m" - "memory" = "512Mi" - } - limits = { - "cpu" = "${data.coder_parameter.cpu.value}" - "memory" = "${data.coder_parameter.memory.value}Gi" - } - } - - volume_mount { - mount_path = local.home_dir - name = "home" - read_only = false - } - } - - volume { - name = "home" - persistent_volume_claim { - claim_name = kubernetes_persistent_volume_claim_v1.home.metadata[0].name - read_only = false - } - } - - affinity { - # Spread workspaces across nodes when possible. - pod_anti_affinity { - preferred_during_scheduling_ignored_during_execution { - weight = 1 - pod_affinity_term { - topology_key = "kubernetes.io/hostname" - label_selector { - match_expressions { - key = "app.kubernetes.io/name" - operator = "In" - values = ["coder-workspace"] - } - } - } - } - } - } - } - } - } -} diff --git a/registry/coder-labs/templates/kiro-crew/startup.sh b/registry/coder-labs/templates/kiro-crew/startup.sh deleted file mode 100644 index 668c13dc5..000000000 --- a/registry/coder-labs/templates/kiro-crew/startup.sh +++ /dev/null @@ -1,74 +0,0 @@ -#!/usr/bin/env bash -# Workspace startup script. Runs as the coder user inside the workspace pod. -set -euo pipefail - -mkdir -p "$HOME/.ssh" "$HOME/bin" "$HOME/.local/bin" -chmod 700 "$HOME/.ssh" -ssh-keyscan -t ed25519,rsa github.com >> "$HOME/.ssh/known_hosts" 2> /dev/null || echo "Warning: could not fetch GitHub host keys." -chmod 600 "$HOME/.ssh/known_hosts" 2> /dev/null || true - -# Point Kiro at the Coder MCP server so agents can work with Coder itself. -# CODER_URL and CODER_SESSION_TOKEN are set by the coder-login module. -if [ -n "${CODER_URL:-}" ] && [ -n "${CODER_SESSION_TOKEN:-}" ]; then - echo "Configuring the Coder MCP server for Kiro..." - mkdir -p "$HOME/.kiro/settings" - CODER_MCP_URL="${CODER_URL%/}/api/experimental/mcp/http" - cat > "$HOME/.kiro/settings/mcp.json" << EOF -{ - "mcpServers": { - "coder": { - "url": "$CODER_MCP_URL", - "headers": { - "Authorization": "Bearer $CODER_SESSION_TOKEN" - }, - "autoApprove": [ - "coder_workspace_edit_file", - "coder_workspace_read_file", - "coder_get_task_status", - "coder_workspace_write_file", - "coder_workspace_ls", - "coder_workspace_bash", - "coder_get_task_logs", - "coder_list_templates", - "coder_create_task", - "coder_get_authenticated_user", - "coder_delete_task", - "coder_send_task_input", - "coder_list_workspaces", - "coder_workspace_edit_files", - "coder_workspace_list_apps", - "coder_workspace_port_forward", - "coder_create_workspace_build", - "coder_template_version_parameters" - ] - } - } -} -EOF - chmod 600 "$HOME/.kiro/settings/mcp.json" -else - echo "CODER_URL or CODER_SESSION_TOKEN is not set; skipping Coder MCP configuration." -fi - -# Trust the home directory in code-server and Kiro so they open without a -# workspace trust prompt. -mkdir -p "$HOME/.local/share/code-server/User" -cat > "$HOME/.local/share/code-server/User/settings.json" << 'EOF' -{ - "security.workspace.trust.enabled": true, - "security.workspace.trust.startupPrompt": "never", - "security.workspace.trust.emptyWindow": false, - "security.workspace.trust.untrustedFiles": "open" -} -EOF - -mkdir -p "$HOME/.kiro/settings" -cat > "$HOME/.kiro/settings/trusted-workspaces.json" << EOF -{ - "trustedFolders": [ - "$HOME" - ] -} -EOF - -echo "Workspace startup completed." From 36bf530e35456298ac565e4644aa872966df0b0e Mon Sep 17 00:00:00 2001 From: default Date: Fri, 23 Oct 2026 19:53:32 +0000 Subject: [PATCH 09/10] docs(coder-labs/kiro-crew): drop link to the template, which moves to a separate PR Generated with Coder Agents. --- registry/coder-labs/modules/kiro-crew/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/registry/coder-labs/modules/kiro-crew/README.md b/registry/coder-labs/modules/kiro-crew/README.md index c033f9ee5..703c34406 100644 --- a/registry/coder-labs/modules/kiro-crew/README.md +++ b/registry/coder-labs/modules/kiro-crew/README.md @@ -136,7 +136,7 @@ Kiro Crew is a long-running gateway, not a one-shot CLI. The `kirocrew gateway` - **Reconnecting resumes where you left off.** Closing the dashboard tab or losing your connection does not end a session. Reopening the **Kiro Crew** app reattaches to the same running gateway and its in-progress sessions and transcripts. - **Sessions survive workspace restarts.** All gateway state lives under `$HOME` (`~/.kiro`). On a persistent home directory it is preserved across stop/start and rebuilds; the module's start script relaunches the gateway automatically on the next start, and your prior sessions are still there. -Use a persistent home directory to retain sessions, transcripts, and Kiro credentials across restarts. The companion [template](../../templates/kiro-crew/README.md) mounts a persistent volume at `/home/coder` for this reason. +Use a persistent home directory to retain sessions, transcripts, and Kiro credentials across restarts. For example, mount a persistent volume at `/home/coder`. ## Restricted and air-gapped environments From 84c9318866c3e5515d4419d1f64526aba9aec436 Mon Sep 17 00:00:00 2001 From: default Date: Fri, 23 Oct 2026 20:04:27 +0000 Subject: [PATCH 10/10] docs(coder-labs/kiro-crew): use 0.0.1 as the initial module version Generated with Coder Agents. --- registry/coder-labs/modules/kiro-crew/README.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/registry/coder-labs/modules/kiro-crew/README.md b/registry/coder-labs/modules/kiro-crew/README.md index 703c34406..300f6f973 100644 --- a/registry/coder-labs/modules/kiro-crew/README.md +++ b/registry/coder-labs/modules/kiro-crew/README.md @@ -13,7 +13,7 @@ Install [Kiro CLI](https://kiro.dev/cli/) and [Kiro Crew](https://github.com/kir ```tf module "kiro-crew" { source = "registry.coder.com/coder-labs/kiro-crew/coder" - version = "1.0.0" + version = "0.0.1" agent_id = coder_agent.main.id } ``` @@ -39,7 +39,7 @@ The gateway rejects requests whose `Host` is not in its allowlist (`Host header ```tf module "kiro-crew" { source = "registry.coder.com/coder-labs/kiro-crew/coder" - version = "1.0.0" + version = "0.0.1" agent_id = coder_agent.main.id pre_install_script = <<-EOT @@ -57,7 +57,7 @@ module "kiro-crew" { ```tf module "kiro-crew" { source = "registry.coder.com/coder-labs/kiro-crew/coder" - version = "1.0.0" + version = "0.0.1" agent_id = coder_agent.main.id extra_mcp_json = jsonencode({ @@ -78,7 +78,7 @@ Without configuration, a browser that opens Kiro Crew through a [workspace proxy ```tf module "kiro-crew" { source = "registry.coder.com/coder-labs/kiro-crew/coder" - version = "1.0.0" + version = "0.0.1" agent_id = coder_agent.main.id workspace_proxy_hostnames = [ @@ -97,7 +97,7 @@ The module derives the dashboard origin from the agent's `VSCODE_PROXY_URI`. If ```tf module "kiro-crew" { source = "registry.coder.com/coder-labs/kiro-crew/coder" - version = "1.0.0" + version = "0.0.1" agent_id = coder_agent.main.id dashboard_origin = "https://kirocrew-dashboard--${data.coder_workspace.me.name}--${data.coder_workspace_owner.me.name}.apps.example.com" } @@ -114,7 +114,7 @@ data "coder_external_auth" "aws_builder_id" { module "kiro-crew" { source = "registry.coder.com/coder-labs/kiro-crew/coder" - version = "1.0.0" + version = "0.0.1" agent_id = coder_agent.main.id aws_builder_id_token = data.coder_external_auth.aws_builder_id.access_token } @@ -147,7 +147,7 @@ By default the module downloads the Kiro CLI and Kiro Crew installers from the p ```tf module "kiro-crew" { source = "registry.coder.com/coder-labs/kiro-crew/coder" - version = "1.0.0" + version = "0.0.1" agent_id = coder_agent.main.id kiro_cli_install_url = "https://artifacts.example.com/kiro/cli-install.sh" @@ -168,7 +168,7 @@ RUN curl -fsSL https://cli.kiro.dev/install | bash \ ```tf module "kiro-crew" { source = "registry.coder.com/coder-labs/kiro-crew/coder" - version = "1.0.0" + version = "0.0.1" agent_id = coder_agent.main.id use_cached = true # default; skips the installers when the binaries already exist }