Compare commits
30 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c551c4d84a | |||
| 4b9da4036a | |||
| 96c5f3219d | |||
| 146540c1e9 | |||
| a85436fdf4 | |||
| aa4890fe62 | |||
| ab6799ac07 | |||
| bda3eb96e8 | |||
| 6b16cd3529 | |||
| 43d05a9da4 | |||
| e3f8b6450e | |||
| c03986f9cb | |||
| 758aba4c2a | |||
| d745117782 | |||
| a99d3385c3 | |||
| c62fe569a0 | |||
| ce2087bc09 | |||
| 67f18cd4de | |||
| e0697562c1 | |||
| 499aaa676c | |||
| 3ae8c7dcff | |||
| 2cfbe5f69c | |||
| 186e0c4de6 | |||
| 69e5dc5c80 | |||
| b143b7d9ba | |||
| 0a8930d60d | |||
| d21db0d220 | |||
| 392f6b120a | |||
| 7de72fc7cc | |||
| 3e1ddbf624 |
@@ -3,6 +3,7 @@ muc = "muc" # For Munich location code
|
||||
tyo = "tyo" # For Tokyo location code
|
||||
Hashi = "Hashi"
|
||||
HashiCorp = "HashiCorp"
|
||||
hel = "hel" # For Helsinki location code
|
||||
mavrickrishi = "mavrickrishi" # Username
|
||||
mavrick = "mavrick" # Username
|
||||
inh = "inh" # Option in setpriv command
|
||||
|
||||
@@ -93,7 +93,7 @@ jobs:
|
||||
- name: Validate formatting
|
||||
run: bun fmt:ci
|
||||
- name: Check for typos
|
||||
uses: crate-ci/typos@v1.39.2
|
||||
uses: crate-ci/typos@v1.40.0
|
||||
with:
|
||||
config: .github/typos.toml
|
||||
validate-readme-files:
|
||||
|
||||
|
After Width: | Height: | Size: 2.3 MiB |
|
After Width: | Height: | Size: 5.5 KiB |
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg viewBox="0 0 500 500" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="250" cy="250" r="250" fill="#fff"/>
|
||||
<path d="m335 150h40v200h-40zm-130 0a100 100 0 1 0 0 200 100 100 0 1 0 0-200zm0 40a60 60 0 1 1 0 120 60 60 0 1 1 0-120z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 293 B |
@@ -0,0 +1,8 @@
|
||||
<svg width="256" height="256" viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="128" cy="128" r="120" fill="black"/>
|
||||
<polygon
|
||||
points="128,70 178,170 78,170"
|
||||
fill="white"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
|
After Width: | Height: | Size: 216 B |
@@ -1,15 +1,16 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"configVersion": 0,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "registry",
|
||||
"devDependencies": {
|
||||
"@types/bun": "^1.2.21",
|
||||
"bun-types": "^1.2.21",
|
||||
"dedent": "^1.6.0",
|
||||
"@types/bun": "^1.3.4",
|
||||
"bun-types": "^1.3.4",
|
||||
"dedent": "^1.7.0",
|
||||
"gray-matter": "^4.0.3",
|
||||
"marked": "^16.2.0",
|
||||
"prettier": "^3.6.2",
|
||||
"marked": "^16.4.2",
|
||||
"prettier": "^3.7.4",
|
||||
"prettier-plugin-sh": "^0.18.0",
|
||||
"prettier-plugin-terraform-formatter": "^1.2.1",
|
||||
"shellcheck": "^4.1.0",
|
||||
@@ -30,12 +31,10 @@
|
||||
|
||||
"@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="],
|
||||
|
||||
"@types/bun": ["@types/bun@1.2.21", "", { "dependencies": { "bun-types": "1.2.21" } }, "sha512-NiDnvEqmbfQ6dmZ3EeUO577s4P5bf4HCTXtI6trMc6f6RzirY5IrF3aIookuSpyslFzrnvv2lmEWv5HyC1X79A=="],
|
||||
"@types/bun": ["@types/bun@1.3.4", "", { "dependencies": { "bun-types": "1.3.4" } }, "sha512-EEPTKXHP+zKGPkhRLv+HI0UEX8/o+65hqARxLy8Ov5rIxMBPNTjeZww00CIihrIQGEQBYg+0roO5qOnS/7boGA=="],
|
||||
|
||||
"@types/node": ["@types/node@24.0.14", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-4zXMWD91vBLGRtHK3YbIoFMia+1nqEz72coM42C5ETjnNCa/heoj7NT1G67iAfOqMmcfhuCZ4uNpyz8EjlAejw=="],
|
||||
|
||||
"@types/react": ["@types/react@19.1.8", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g=="],
|
||||
|
||||
"@xhmikosr/decompress-tar": ["@xhmikosr/decompress-tar@8.1.0", "", { "dependencies": { "file-type": "^20.5.0", "is-stream": "^2.0.1", "tar-stream": "^3.1.7" } }, "sha512-m0q8x6lwxenh1CrsTby0Jrjq4vzW/QU1OLhTHMQLEdHpmjR1lgahGz++seZI0bXF3XcZw3U3xHfqZSz+JPP2Gg=="],
|
||||
|
||||
"@xhmikosr/decompress-unzip": ["@xhmikosr/decompress-unzip@7.1.0", "", { "dependencies": { "file-type": "^20.5.0", "get-stream": "^6.0.1", "yauzl": "^3.1.2" } }, "sha512-oqTYAcObqTlg8owulxFTqiaJkfv2SHsxxxz9Wg4krJAHVzGWlZsU8tAB30R6ow+aHrfv4Kub6WQ8u04NWVPUpA=="],
|
||||
@@ -64,7 +63,7 @@
|
||||
|
||||
"buffer-fill": ["buffer-fill@1.0.0", "", {}, "sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ=="],
|
||||
|
||||
"bun-types": ["bun-types@1.2.21", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-sa2Tj77Ijc/NTLS0/Odjq/qngmEPZfbfnOERi0KRUYhT9R8M4VBioWVmMWE5GrYbKMc+5lVybXygLdibHaqVqw=="],
|
||||
"bun-types": ["bun-types@1.3.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-5ua817+BZPZOlNaRgGBpZJOSAQ9RQ17pkwPD0yR7CfJg+r8DgIILByFifDTa+IPDDxzf5VNhtNlcKqFzDgJvlQ=="],
|
||||
|
||||
"call-bind": ["call-bind@1.0.8", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="],
|
||||
|
||||
@@ -76,8 +75,6 @@
|
||||
|
||||
"core-util-is": ["core-util-is@1.0.3", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="],
|
||||
|
||||
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
|
||||
|
||||
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||
|
||||
"decompress": ["decompress@4.2.1", "", { "dependencies": { "decompress-tar": "^4.0.0", "decompress-tarbz2": "^4.0.0", "decompress-targz": "^4.0.0", "decompress-unzip": "^4.0.1", "graceful-fs": "^4.1.10", "make-dir": "^1.0.0", "pify": "^2.3.0", "strip-dirs": "^2.0.0" } }, "sha512-e48kc2IjU+2Zw8cTb6VZcJQ3lgVbS4uuB1TfCHbiZIP/haNXm+SVyhu+87jts5/3ROpd82GSVCoNs/z8l4ZOaQ=="],
|
||||
@@ -90,7 +87,7 @@
|
||||
|
||||
"decompress-unzip": ["decompress-unzip@4.0.1", "", { "dependencies": { "file-type": "^3.8.0", "get-stream": "^2.2.0", "pify": "^2.3.0", "yauzl": "^2.4.2" } }, "sha512-1fqeluvxgnn86MOh66u8FjbtJpAFv5wgCT9Iw8rcBqQcCo5tO8eiJw7NNTrvt9n4CRBVq7CstiS922oPgyGLrw=="],
|
||||
|
||||
"dedent": ["dedent@1.6.0", "", { "peerDependencies": { "babel-plugin-macros": "^3.1.0" }, "optionalPeers": ["babel-plugin-macros"] }, "sha512-F1Z+5UCFpmQUzJa11agbyPVMbpgT/qA3/SKyJ1jyBgm7dUcUEa8v9JwDkerSQXfakBwFljIxhOJqGkjUwZ9FSA=="],
|
||||
"dedent": ["dedent@1.7.0", "", { "peerDependencies": { "babel-plugin-macros": "^3.1.0" }, "optionalPeers": ["babel-plugin-macros"] }, "sha512-HGFtf8yhuhGhqO07SV79tRp+br4MnbdjeVxotpn1QBl30pcLLCQjX5b2295ll0fv8RKDKsmWYrl05usHM9CewQ=="],
|
||||
|
||||
"define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="],
|
||||
|
||||
@@ -182,7 +179,7 @@
|
||||
|
||||
"make-dir": ["make-dir@1.3.0", "", { "dependencies": { "pify": "^3.0.0" } }, "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ=="],
|
||||
|
||||
"marked": ["marked@16.2.0", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-LbbTuye+0dWRz2TS9KJ7wsnD4KAtpj0MVkWc90XvBa6AslXsT0hTBVH5k32pcSyHH1fst9XEFJunXHktVy0zlg=="],
|
||||
"marked": ["marked@16.4.2", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA=="],
|
||||
|
||||
"matcher": ["matcher@3.0.0", "", { "dependencies": { "escape-string-regexp": "^4.0.0" } }, "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng=="],
|
||||
|
||||
@@ -206,7 +203,7 @@
|
||||
|
||||
"possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="],
|
||||
|
||||
"prettier": ["prettier@3.6.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ=="],
|
||||
"prettier": ["prettier@3.7.4", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA=="],
|
||||
|
||||
"prettier-plugin-sh": ["prettier-plugin-sh@0.18.0", "", { "dependencies": { "@reteps/dockerfmt": "^0.3.6", "sh-syntax": "^0.5.8" }, "peerDependencies": { "prettier": "^3.6.0" } }, "sha512-cW1XL27FOJQ/qGHOW6IHwdCiNWQsAgK+feA8V6+xUTaH0cD3Mh+tFAtBvEEWvuY6hTDzRV943Fzeii+qMOh7nQ=="],
|
||||
|
||||
|
||||
@@ -10,12 +10,12 @@
|
||||
"update-version": "./update-version.sh"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "^1.2.21",
|
||||
"bun-types": "^1.2.21",
|
||||
"dedent": "^1.6.0",
|
||||
"@types/bun": "^1.3.4",
|
||||
"bun-types": "^1.3.4",
|
||||
"dedent": "^1.7.0",
|
||||
"gray-matter": "^4.0.3",
|
||||
"marked": "^16.2.0",
|
||||
"prettier": "^3.6.2",
|
||||
"marked": "^16.4.2",
|
||||
"prettier": "^3.7.4",
|
||||
"prettier-plugin-sh": "^0.18.0",
|
||||
"prettier-plugin-terraform-formatter": "^1.2.1",
|
||||
"shellcheck": "^4.1.0"
|
||||
|
||||
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 451 KiB After Width: | Height: | Size: 451 KiB |
|
After Width: | Height: | Size: 98 KiB |
@@ -0,0 +1,7 @@
|
||||
---
|
||||
display_name: "Excellencedev"
|
||||
bio: "Love to contribute"
|
||||
avatar: "./.images/avatar.png"
|
||||
support_email: "ademiluyisuccessandexcellence@gmail.com"
|
||||
status: "community"
|
||||
---
|
||||
@@ -0,0 +1,32 @@
|
||||
---
|
||||
display_name: Hetzner Cloud Server
|
||||
description: Provision Hetzner Cloud servers as Coder workspaces
|
||||
icon: ../../../../.icons/hetzner.svg
|
||||
tags: [vm, linux, hetzner]
|
||||
---
|
||||
|
||||
# Remote Development on Hetzner Cloud (Linux)
|
||||
|
||||
Provision Hetzner Cloud servers as [Coder workspaces](https://coder.com/docs/workspaces) with this example template.
|
||||
|
||||
> [!WARNING]
|
||||
> **Workspace Storage Persistence:** When a workspace is stopped, the Hetzner Cloud server instance is stopped but your home volume and stored data persist. This means your files and data remain intact when you resume the workspace.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **Volume Management & Costs:** Hetzner Cloud volumes persist even when workspaces are stopped and will continue to incur storage costs (€0.0476/GB/month). Volumes are only automatically deleted when the workspace is completely deleted. Monitor your volumes in the [Hetzner Cloud Console](https://console.hetzner.cloud/) to manage costs effectively.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
To deploy workspaces as Hetzner Cloud servers, you'll need:
|
||||
|
||||
- Hetzner Cloud [API token](https://console.hetzner.cloud/projects) (create under Security > API Tokens)
|
||||
|
||||
### Authentication
|
||||
|
||||
This template assumes that the Coder Provisioner is run in an environment that is authenticated with Hetzner Cloud.
|
||||
|
||||
Obtain a Hetzner Cloud API token from your [Hetzner Cloud Console](https://console.hetzner.cloud/projects) and provide it as the `hcloud_token` variable when creating a workspace.
|
||||
For more authentication options, see the [Terraform provider documentation](https://registry.terraform.io/providers/hetznercloud/hcloud/latest/docs#authentication).
|
||||
|
||||
> [!NOTE]
|
||||
> This template is designed to be a starting point. Edit the Terraform to extend the template to support your use case.
|
||||
@@ -0,0 +1,62 @@
|
||||
#cloud-config
|
||||
users:
|
||||
- name: ${username}
|
||||
sudo: ["ALL=(ALL) NOPASSWD:ALL"]
|
||||
groups: sudo
|
||||
shell: /bin/bash
|
||||
packages:
|
||||
- git
|
||||
%{ if home_volume_label != "" ~}
|
||||
fs_setup:
|
||||
- device: /dev/disk/by-id/scsi-0HC_Volume_${volume_id}
|
||||
filesystem: ext4
|
||||
label: ${home_volume_label}
|
||||
overwrite: false # This prevents reformatting the disk on every boot
|
||||
|
||||
mounts:
|
||||
- [
|
||||
"/dev/disk/by-id/scsi-0HC_Volume_${volume_id}",
|
||||
"/home/${username}",
|
||||
ext4,
|
||||
"defaults,uid=1000,gid=1000",
|
||||
]
|
||||
%{ endif ~}
|
||||
write_files:
|
||||
- path: /opt/coder/init
|
||||
permissions: "0755"
|
||||
encoding: b64
|
||||
content: ${init_script}
|
||||
- path: /etc/systemd/system/coder-agent.service
|
||||
permissions: "0644"
|
||||
content: |
|
||||
[Unit]
|
||||
Description=Coder Agent
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
User=${username}
|
||||
ExecStart=/opt/coder/init
|
||||
Environment=CODER_AGENT_TOKEN=${coder_agent_token}
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
TimeoutStopSec=90
|
||||
KillMode=process
|
||||
|
||||
OOMScoreAdjust=-900
|
||||
SyslogIdentifier=coder-agent
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
runcmd:
|
||||
%{ if home_volume_label != "" ~}
|
||||
- |
|
||||
until [ -e /dev/disk/by-id/scsi-0HC_Volume_${volume_id} ]; do
|
||||
echo "Waiting for volume device..."
|
||||
sleep 2
|
||||
done
|
||||
%{ endif ~}
|
||||
- mount -a
|
||||
- chown ${username}:${username} /home/${username}
|
||||
- systemctl enable coder-agent
|
||||
- systemctl start coder-agent
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"type_meta": {
|
||||
"cx22": { "cores": 2, "memory_gb": 4, "disk_gb": 40 },
|
||||
"cx32": { "cores": 4, "memory_gb": 8, "disk_gb": 80 },
|
||||
"cx42": { "cores": 8, "memory_gb": 16, "disk_gb": 160 },
|
||||
"cx52": { "cores": 16, "memory_gb": 32, "disk_gb": 320 },
|
||||
"cpx11": { "cores": 2, "memory_gb": 2, "disk_gb": 40 },
|
||||
"cpx21": { "cores": 3, "memory_gb": 4, "disk_gb": 80 },
|
||||
"cpx31": { "cores": 4, "memory_gb": 8, "disk_gb": 160 },
|
||||
"cpx41": { "cores": 8, "memory_gb": 16, "disk_gb": 240 },
|
||||
"cpx51": { "cores": 16, "memory_gb": 32, "disk_gb": 360 },
|
||||
"ccx13": { "cores": 2, "memory_gb": 8, "disk_gb": 80 },
|
||||
"ccx23": { "cores": 4, "memory_gb": 16, "disk_gb": 160 },
|
||||
"ccx33": { "cores": 8, "memory_gb": 32, "disk_gb": 240 },
|
||||
"ccx43": { "cores": 16, "memory_gb": 64, "disk_gb": 360 },
|
||||
"ccx53": { "cores": 32, "memory_gb": 128, "disk_gb": 600 },
|
||||
"ccx63": { "cores": 48, "memory_gb": 192, "disk_gb": 960 }
|
||||
},
|
||||
"availability": {
|
||||
"fsn1": ["cpx11", "cpx21", "cpx31", "cpx41", "cpx51", "ccx13", "ccx23", "ccx33"],
|
||||
"ash": ["cpx11", "cpx21", "cpx31", "cpx41", "cpx51", "ccx13", "ccx23", "ccx33"],
|
||||
"hel1": ["cx22", "cpx11", "cpx21", "cpx31", "cpx41", "cpx51", "ccx13", "ccx23", "ccx33"],
|
||||
"hil": ["cpx11", "cpx21", "cpx31", "cpx41", "ccx13", "ccx23", "ccx33"],
|
||||
"nbg1": ["cx22", "cx32", "cx42", "cx52", "cpx11", "cpx21", "cpx31", "cpx41", "cpx51", "ccx13", "ccx23", "ccx33"],
|
||||
"sin": ["cpx11", "cpx21", "cpx31", "cpx41", "cpx51", "ccx13", "ccx23", "ccx33"]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
terraform {
|
||||
required_providers {
|
||||
hcloud = {
|
||||
source = "hetznercloud/hcloud"
|
||||
}
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
variable "hcloud_token" {
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
provider "hcloud" {
|
||||
token = var.hcloud_token
|
||||
}
|
||||
|
||||
# Available locations: https://docs.hetzner.com/cloud/general/locations/
|
||||
data "coder_parameter" "hcloud_location" {
|
||||
name = "hcloud_location"
|
||||
display_name = "Hetzner Location"
|
||||
description = "Select the Hetzner Cloud location for your workspace."
|
||||
type = "string"
|
||||
default = "fsn1"
|
||||
option {
|
||||
name = "DE Falkenstein"
|
||||
value = "fsn1"
|
||||
}
|
||||
option {
|
||||
name = "US Ashburn, VA"
|
||||
value = "ash"
|
||||
}
|
||||
option {
|
||||
name = "US Hillsboro, OR"
|
||||
value = "hil"
|
||||
}
|
||||
option {
|
||||
name = "SG Singapore"
|
||||
value = "sin"
|
||||
}
|
||||
option {
|
||||
name = "DE Nuremberg"
|
||||
value = "nbg1"
|
||||
}
|
||||
option {
|
||||
name = "FI Helsinki"
|
||||
value = "hel1"
|
||||
}
|
||||
}
|
||||
|
||||
# Available server types: https://docs.hetzner.com/cloud/servers/overview/
|
||||
data "coder_parameter" "hcloud_server_type" {
|
||||
name = "hcloud_server_type"
|
||||
display_name = "Hetzner Server Type"
|
||||
description = "Select the Hetzner Cloud server type for your workspace."
|
||||
type = "string"
|
||||
|
||||
dynamic "option" {
|
||||
for_each = local.hcloud_server_type_options_for_selected_location
|
||||
content {
|
||||
name = option.value.name
|
||||
value = option.value.value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resource "hcloud_server" "dev" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
name = "coder-${data.coder_workspace.me.name}-dev"
|
||||
image = "ubuntu-24.04"
|
||||
server_type = data.coder_parameter.hcloud_server_type.value
|
||||
location = data.coder_parameter.hcloud_location.value
|
||||
public_net {
|
||||
ipv4_enabled = true
|
||||
ipv6_enabled = true
|
||||
}
|
||||
user_data = templatefile("cloud-config.yaml.tftpl", {
|
||||
username = lower(data.coder_workspace_owner.me.name)
|
||||
home_volume_label = "coder-${data.coder_workspace.me.id}-home"
|
||||
volume_id = hcloud_volume.home_volume.id
|
||||
init_script = base64encode(coder_agent.main.init_script)
|
||||
coder_agent_token = coder_agent.main.token
|
||||
})
|
||||
labels = {
|
||||
"coder_workspace_name" = data.coder_workspace.me.name,
|
||||
"coder_workspace_owner" = data.coder_workspace_owner.me.name,
|
||||
}
|
||||
}
|
||||
|
||||
resource "hcloud_volume" "home_volume" {
|
||||
name = "coder-${data.coder_workspace.me.id}-home"
|
||||
size = data.coder_parameter.home_volume_size.value
|
||||
location = data.coder_parameter.hcloud_location.value
|
||||
labels = {
|
||||
"coder_workspace_name" = data.coder_workspace.me.name,
|
||||
"coder_workspace_owner" = data.coder_workspace_owner.me.name,
|
||||
}
|
||||
}
|
||||
|
||||
resource "hcloud_volume_attachment" "home_volume_attachment" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
volume_id = hcloud_volume.home_volume.id
|
||||
server_id = hcloud_server.dev[count.index].id
|
||||
automount = false
|
||||
}
|
||||
|
||||
locals {
|
||||
username = lower(data.coder_workspace_owner.me.name)
|
||||
|
||||
# Data source: local JSON file under the module directory
|
||||
# Check API for latest server types & availability: https://docs.hetzner.cloud/reference/cloud#server-types
|
||||
hcloud_server_types_data = jsondecode(file("${path.module}/hetzner_server_types.json"))
|
||||
hcloud_server_type_meta = local.hcloud_server_types_data.type_meta
|
||||
hcloud_server_types_by_location = local.hcloud_server_types_data.availability
|
||||
|
||||
hcloud_server_type_options_for_selected_location = [
|
||||
for type_name in lookup(local.hcloud_server_types_by_location, data.coder_parameter.hcloud_location.value, []) : {
|
||||
name = format("%s (%d vCPU, %dGB RAM, %dGB)", upper(type_name), local.hcloud_server_type_meta[type_name].cores, local.hcloud_server_type_meta[type_name].memory_gb, local.hcloud_server_type_meta[type_name].disk_gb)
|
||||
value = type_name
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
data "coder_provisioner" "me" {}
|
||||
|
||||
provider "coder" {}
|
||||
|
||||
data "coder_workspace" "me" {}
|
||||
|
||||
data "coder_workspace_owner" "me" {}
|
||||
|
||||
data "coder_parameter" "home_volume_size" {
|
||||
name = "home_volume_size"
|
||||
display_name = "Home volume size"
|
||||
description = "How large would you like your home volume to be (in GB)?"
|
||||
type = "number"
|
||||
default = "20"
|
||||
mutable = false
|
||||
validation {
|
||||
min = 1
|
||||
max = 100 # Adjust the max size as needed
|
||||
}
|
||||
}
|
||||
|
||||
resource "coder_agent" "main" {
|
||||
os = "linux"
|
||||
arch = "amd64"
|
||||
|
||||
metadata {
|
||||
key = "cpu"
|
||||
display_name = "CPU Usage"
|
||||
interval = 5
|
||||
timeout = 5
|
||||
script = "coder stat cpu"
|
||||
}
|
||||
metadata {
|
||||
key = "memory"
|
||||
display_name = "Memory Usage"
|
||||
interval = 5
|
||||
timeout = 5
|
||||
script = "coder stat mem"
|
||||
}
|
||||
metadata {
|
||||
key = "home"
|
||||
display_name = "Home Usage"
|
||||
interval = 600 # every 10 minutes
|
||||
timeout = 30 # df can take a while on large filesystems
|
||||
script = "coder stat disk --path /home/${local.username}"
|
||||
}
|
||||
}
|
||||
|
||||
module "code-server" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/code-server/coder"
|
||||
|
||||
# This ensures that the latest non-breaking version of the module gets downloaded, you can also pin the module version to prevent breaking changes in production.
|
||||
version = "~> 1.0"
|
||||
|
||||
agent_id = coder_agent.main.id
|
||||
order = 1
|
||||
}
|
||||
|
Before Width: | Height: | Size: 8.9 KiB After Width: | Height: | Size: 7.4 KiB |
|
After Width: | Height: | Size: 240 KiB |
|
After Width: | Height: | Size: 590 KiB |
@@ -13,7 +13,7 @@ Run the Cursor Agent CLI in your workspace for interactive coding assistance and
|
||||
```tf
|
||||
module "cursor_cli" {
|
||||
source = "registry.coder.com/coder-labs/cursor-cli/coder"
|
||||
version = "0.2.2"
|
||||
version = "0.3.0"
|
||||
agent_id = coder_agent.main.id
|
||||
folder = "/home/coder/project"
|
||||
}
|
||||
@@ -42,7 +42,7 @@ module "coder-login" {
|
||||
|
||||
module "cursor_cli" {
|
||||
source = "registry.coder.com/coder-labs/cursor-cli/coder"
|
||||
version = "0.2.2"
|
||||
version = "0.3.0"
|
||||
agent_id = coder_agent.main.id
|
||||
folder = "/home/coder/project"
|
||||
|
||||
|
||||
@@ -159,7 +159,7 @@ describe("cursor-cli", async () => {
|
||||
"-c",
|
||||
"cat /home/coder/.cursor-cli-module/agentapi-start.log || cat /home/coder/.cursor-cli-module/start.log || true",
|
||||
]);
|
||||
expect(startLog.stdout).toContain(`-m ${model}`);
|
||||
expect(startLog.stdout).toContain(`--model ${model}`);
|
||||
expect(startLog.stdout).toContain("-f");
|
||||
expect(startLog.stdout).toContain("test prompt");
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@ terraform {
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">= 2.7"
|
||||
version = ">= 2.12"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -132,7 +132,7 @@ resource "coder_env" "cursor_api_key" {
|
||||
|
||||
module "agentapi" {
|
||||
source = "registry.coder.com/coder/agentapi/coder"
|
||||
version = "1.2.0"
|
||||
version = "2.0.0"
|
||||
|
||||
agent_id = var.agent_id
|
||||
folder = local.folder
|
||||
@@ -179,3 +179,7 @@ module "agentapi" {
|
||||
/tmp/install.sh
|
||||
EOT
|
||||
}
|
||||
|
||||
output "task_app_id" {
|
||||
value = module.agentapi.task_app_id
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ ARGS=()
|
||||
|
||||
# global flags
|
||||
if [ -n "$ARG_MODEL" ]; then
|
||||
ARGS+=("-m" "$ARG_MODEL")
|
||||
ARGS+=("--model" "$ARG_MODEL")
|
||||
fi
|
||||
if [ "$ARG_FORCE" = "true" ]; then
|
||||
ARGS+=("-f")
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
---
|
||||
display_name: Open WebUI
|
||||
description: A self-hosted AI chat interface supporting various LLM providers
|
||||
icon: ../../../../.icons/openwebui.svg
|
||||
verified: false
|
||||
tags: [ai, llm, chat, web, python]
|
||||
---
|
||||
|
||||
# Open WebUI
|
||||
|
||||
Open WebUI is a user-friendly web interface for interacting with Large Language Models. It provides a ChatGPT-like interface that can connect to various LLM providers including OpenAI, Ollama, and more.
|
||||
|
||||
```tf
|
||||
module "open-webui" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder-labs/open-webui/coder"
|
||||
version = "1.0.0"
|
||||
agent_id = coder_agent.main.id
|
||||
}
|
||||
```
|
||||
|
||||

|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **Python 3.11 or higher** must be installed in your image (with `venv` module)
|
||||
- Port 7800 (default) or your custom port must be available
|
||||
|
||||
For Ubuntu/Debian, you can install Python 3.11 from [deadsnakes PPA](https://launchpad.net/~deadsnakes/+archive/ubuntu/ppa):
|
||||
|
||||
```shell
|
||||
sudo add-apt-repository -y ppa:deadsnakes/ppa
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y python3.11 python3.11-venv
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### With OpenAI API Key
|
||||
|
||||
```tf
|
||||
module "open-webui" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder-labs/open-webui/coder"
|
||||
version = "1.0.0"
|
||||
agent_id = coder_agent.main.id
|
||||
|
||||
openai_api_key = var.openai_api_key
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Port and Data Directory
|
||||
|
||||
```tf
|
||||
module "open-webui" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder-labs/open-webui/coder"
|
||||
version = "1.0.0"
|
||||
agent_id = coder_agent.main.id
|
||||
|
||||
http_server_port = 8080
|
||||
data_dir = "/home/coder/open-webui-data"
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,94 @@
|
||||
terraform {
|
||||
required_version = ">= 1.0"
|
||||
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">= 2.5"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
variable "agent_id" {
|
||||
type = string
|
||||
description = "The ID of a Coder agent."
|
||||
}
|
||||
|
||||
variable "http_server_log_path" {
|
||||
type = string
|
||||
description = "The path to log Open WebUI to."
|
||||
default = "/tmp/open-webui.log"
|
||||
}
|
||||
|
||||
variable "http_server_port" {
|
||||
type = number
|
||||
description = "The port to run Open WebUI on."
|
||||
default = 7800
|
||||
}
|
||||
|
||||
variable "open_webui_version" {
|
||||
type = string
|
||||
description = "The version of Open WebUI to install"
|
||||
default = "latest"
|
||||
}
|
||||
|
||||
variable "data_dir" {
|
||||
type = string
|
||||
description = "The directory where Open WebUI stores its data (database, uploads, vector_db, cache)."
|
||||
default = ".open-webui"
|
||||
}
|
||||
|
||||
variable "openai_api_key" {
|
||||
type = string
|
||||
description = "OpenAI API key for accessing OpenAI models. If not provided, OpenAI integration will need to be configured manually in the UI."
|
||||
default = ""
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
variable "share" {
|
||||
type = string
|
||||
description = "The sharing level for the Open WebUI app. Set to 'owner' for private access, 'authenticated' for access by any authenticated user, or 'public' for public access."
|
||||
default = "owner"
|
||||
validation {
|
||||
condition = var.share == "owner" || var.share == "authenticated" || var.share == "public"
|
||||
error_message = "Incorrect value. Please set either 'owner', 'authenticated', or 'public'."
|
||||
}
|
||||
}
|
||||
|
||||
variable "order" {
|
||||
type = number
|
||||
description = "The order determines the position of app in the UI presentation. The lowest order is shown first and apps with equal order are sorted by name (ascending order)."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "group" {
|
||||
type = string
|
||||
description = "The name of a group that this app belongs to."
|
||||
default = null
|
||||
}
|
||||
|
||||
resource "coder_script" "open-webui" {
|
||||
agent_id = var.agent_id
|
||||
display_name = "open-webui"
|
||||
icon = "/icon/openwebui.svg"
|
||||
script = templatefile("${path.module}/run.sh", {
|
||||
HTTP_SERVER_LOG_PATH : var.http_server_log_path,
|
||||
HTTP_SERVER_PORT : var.http_server_port,
|
||||
VERSION : var.open_webui_version,
|
||||
DATA_DIR : var.data_dir,
|
||||
OPENAI_API_KEY : var.openai_api_key,
|
||||
})
|
||||
run_on_start = true
|
||||
}
|
||||
|
||||
resource "coder_app" "open-webui" {
|
||||
agent_id = var.agent_id
|
||||
slug = "open-webui"
|
||||
display_name = "Open WebUI"
|
||||
url = "http://localhost:${var.http_server_port}"
|
||||
icon = "/icon/openwebui.svg"
|
||||
subdomain = true
|
||||
share = var.share
|
||||
order = var.order
|
||||
group = var.group
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
mock_provider "coder" {}
|
||||
|
||||
run "test_defaults" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-123"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.http_server_port == 7800
|
||||
error_message = "Default port should be 7800"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.http_server_log_path == "/tmp/open-webui.log"
|
||||
error_message = "Default log path should be /tmp/open-webui.log"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.share == "owner"
|
||||
error_message = "Default share should be 'owner'"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.open_webui_version == "latest"
|
||||
error_message = "Default version should be 'latest'"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = coder_app.open-webui.subdomain == true
|
||||
error_message = "App should use subdomain"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = coder_app.open-webui.display_name == "Open WebUI"
|
||||
error_message = "App display name should be 'Open WebUI'"
|
||||
}
|
||||
}
|
||||
|
||||
run "test_custom_port" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-456"
|
||||
http_server_port = 9000
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.http_server_port == 9000
|
||||
error_message = "Custom port should be 9000"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = coder_app.open-webui.url == "http://localhost:9000"
|
||||
error_message = "App URL should use custom port"
|
||||
}
|
||||
}
|
||||
|
||||
run "test_custom_log_path" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-789"
|
||||
http_server_log_path = "/var/log/open-webui.log"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.http_server_log_path == "/var/log/open-webui.log"
|
||||
error_message = "Custom log path should be set"
|
||||
}
|
||||
}
|
||||
|
||||
run "test_share_authenticated" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-auth"
|
||||
share = "authenticated"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = coder_app.open-webui.share == "authenticated"
|
||||
error_message = "Share should be 'authenticated'"
|
||||
}
|
||||
}
|
||||
|
||||
run "test_share_public" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-public"
|
||||
share = "public"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = coder_app.open-webui.share == "public"
|
||||
error_message = "Share should be 'public'"
|
||||
}
|
||||
}
|
||||
|
||||
run "test_order_and_group" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-order"
|
||||
order = 10
|
||||
group = "AI Tools"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = coder_app.open-webui.order == 10
|
||||
error_message = "Order should be 10"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = coder_app.open-webui.group == "AI Tools"
|
||||
error_message = "Group should be 'AI Tools'"
|
||||
}
|
||||
}
|
||||
|
||||
run "test_custom_version" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-version"
|
||||
open_webui_version = "0.5.0"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.open_webui_version == "0.5.0"
|
||||
error_message = "Custom version should be '0.5.0'"
|
||||
}
|
||||
}
|
||||
|
||||
run "test_custom_data_dir" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-data"
|
||||
data_dir = "/home/coder/open-webui-data"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.data_dir == "/home/coder/open-webui-data"
|
||||
error_message = "Custom data_dir should be set"
|
||||
}
|
||||
}
|
||||
|
||||
run "test_default_data_dir" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-data-default"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.data_dir == ".open-webui"
|
||||
error_message = "Default data_dir should be '.open-webui'"
|
||||
}
|
||||
}
|
||||
|
||||
run "test_openai_api_key" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-openai"
|
||||
openai_api_key = "sk-test-key-123"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.openai_api_key == "sk-test-key-123"
|
||||
error_message = "OpenAI API key should be set"
|
||||
}
|
||||
}
|
||||
|
||||
run "test_default_openai_api_key" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-openai-default"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.openai_api_key == ""
|
||||
error_message = "Default OpenAI API key should be empty"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
set -eu
|
||||
|
||||
printf '\033[0;1mInstalling Open WebUI %s...\n\n' "${VERSION}"
|
||||
|
||||
check_python_version() {
|
||||
python_cmd="$1"
|
||||
if command -v "$python_cmd" > /dev/null 2>&1; then
|
||||
version=$("$python_cmd" --version 2>&1 | awk '{print $2}')
|
||||
major=$(echo "$version" | cut -d. -f1)
|
||||
minor=$(echo "$version" | cut -d. -f2)
|
||||
if [ "$major" -eq 3 ] && [ "$minor" -ge 11 ]; then
|
||||
echo "$python_cmd"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
PYTHON_CMD=""
|
||||
for cmd in python3.13 python3.12 python3.11 python3 python; do
|
||||
if result=$(check_python_version "$cmd"); then
|
||||
PYTHON_CMD="$result"
|
||||
echo "✅ Found suitable Python: $PYTHON_CMD ($($PYTHON_CMD --version 2>&1))"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -z "$PYTHON_CMD" ]; then
|
||||
echo "❌ Python 3.11 or higher is required but not found."
|
||||
echo ""
|
||||
echo "Please install Python 3.11+ in your image. For example on Ubuntu/Debian:"
|
||||
echo " sudo add-apt-repository -y ppa:deadsnakes/ppa"
|
||||
echo " sudo apt-get update"
|
||||
echo " sudo apt-get install -y python3.11 python3.11-venv"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
VENV_DIR="$HOME/.open-webui-venv"
|
||||
if [ ! -d "$VENV_DIR" ]; then
|
||||
echo "📦 Creating virtual environment..."
|
||||
"$PYTHON_CMD" -m venv "$VENV_DIR"
|
||||
fi
|
||||
. "$VENV_DIR/bin/activate"
|
||||
|
||||
if ! pip show open-webui > /dev/null 2>&1; then
|
||||
echo "📦 Installing Open WebUI version ${VERSION}..."
|
||||
if [ "${VERSION}" = "latest" ]; then
|
||||
pip install open-webui
|
||||
else
|
||||
pip install "open-webui==${VERSION}"
|
||||
fi
|
||||
echo "🥳 Open WebUI has been installed"
|
||||
else
|
||||
echo "✅ Open WebUI is already installed"
|
||||
fi
|
||||
|
||||
echo "👷 Starting Open WebUI in background..."
|
||||
echo "Check logs at ${HTTP_SERVER_LOG_PATH}"
|
||||
|
||||
DATA_DIR="${DATA_DIR}" \
|
||||
OPENAI_API_KEY="${OPENAI_API_KEY}" \
|
||||
open-webui serve --host 0.0.0.0 --port "${HTTP_SERVER_PORT}" > "${HTTP_SERVER_LOG_PATH}" 2>&1 &
|
||||
|
||||
echo "🥳 Open WebUI is ready. HTTP server is listening on port ${HTTP_SERVER_PORT}"
|
||||
@@ -0,0 +1,55 @@
|
||||
---
|
||||
display_name: Perplexica
|
||||
description: Run Perplexica AI search engine in your workspace via Docker
|
||||
icon: ../../../../.icons/perplexica.svg
|
||||
verified: false
|
||||
tags: [ai, search, docker]
|
||||
---
|
||||
|
||||
# Perplexica
|
||||
|
||||
Run [Perplexica](https://github.com/ItzCrazyKns/Perplexica), a privacy-focused AI search engine, in your Coder workspace. Supports cloud providers (OpenAI, Anthropic Claude) and local LLMs via Ollama.
|
||||
|
||||
```tf
|
||||
module "perplexica" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder-labs/perplexica/coder"
|
||||
version = "1.0.0"
|
||||
agent_id = coder_agent.main.id
|
||||
}
|
||||
```
|
||||
|
||||
This module uses the full Perplexica image with embedded SearXNG for simpler setup with no external dependencies.
|
||||
|
||||

|
||||
|
||||
## Prerequisites
|
||||
|
||||
This module requires Docker to be available on the host.
|
||||
|
||||
## Examples
|
||||
|
||||
### With API Keys
|
||||
|
||||
```tf
|
||||
module "perplexica" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder-labs/perplexica/coder"
|
||||
version = "1.0.0"
|
||||
agent_id = coder_agent.main.id
|
||||
openai_api_key = var.openai_api_key
|
||||
anthropic_api_key = var.anthropic_api_key
|
||||
}
|
||||
```
|
||||
|
||||
### With Local Ollama
|
||||
|
||||
```tf
|
||||
module "perplexica" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder-labs/perplexica/coder"
|
||||
version = "1.0.0"
|
||||
agent_id = coder_agent.main.id
|
||||
ollama_api_url = "http://ollama-external-endpoint:11434"
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,108 @@
|
||||
terraform {
|
||||
required_version = ">= 1.0"
|
||||
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">= 2.5"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
variable "agent_id" {
|
||||
type = string
|
||||
description = "The ID of a Coder agent."
|
||||
}
|
||||
|
||||
variable "docker_socket" {
|
||||
type = string
|
||||
description = "(Optional) Docker socket URI"
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "port" {
|
||||
type = number
|
||||
description = "The port to run Perplexica on."
|
||||
default = 3000
|
||||
}
|
||||
|
||||
variable "data_path" {
|
||||
type = string
|
||||
description = "Host path to mount for Perplexica data persistence."
|
||||
default = "./perplexica-data"
|
||||
}
|
||||
|
||||
variable "uploads_path" {
|
||||
type = string
|
||||
description = "Host path to mount for Perplexica file uploads."
|
||||
default = "./perplexica-uploads"
|
||||
}
|
||||
|
||||
variable "openai_api_key" {
|
||||
type = string
|
||||
description = "OpenAI API key."
|
||||
default = ""
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
variable "anthropic_api_key" {
|
||||
type = string
|
||||
description = "Anthropic API key for Claude models."
|
||||
default = ""
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
variable "ollama_api_url" {
|
||||
type = string
|
||||
description = "Ollama API URL for local LLM support."
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "share" {
|
||||
type = string
|
||||
default = "owner"
|
||||
validation {
|
||||
condition = var.share == "owner" || var.share == "authenticated" || var.share == "public"
|
||||
error_message = "Incorrect value. Please set either 'owner', 'authenticated', or 'public'."
|
||||
}
|
||||
}
|
||||
|
||||
variable "order" {
|
||||
type = number
|
||||
description = "The order determines the position of app in the UI presentation. The lowest order is shown first and apps with equal order are sorted by name (ascending order)."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "group" {
|
||||
type = string
|
||||
description = "The name of a group that this app belongs to."
|
||||
default = null
|
||||
}
|
||||
|
||||
resource "coder_script" "perplexica" {
|
||||
agent_id = var.agent_id
|
||||
display_name = "Perplexica"
|
||||
icon = "/icon/perplexica.svg"
|
||||
script = templatefile("${path.module}/run.sh", {
|
||||
DOCKER_HOST : var.docker_socket,
|
||||
PORT : var.port,
|
||||
DATA_PATH : var.data_path,
|
||||
UPLOADS_PATH : var.uploads_path,
|
||||
OPENAI_API_KEY : var.openai_api_key,
|
||||
ANTHROPIC_API_KEY : var.anthropic_api_key,
|
||||
OLLAMA_API_URL : var.ollama_api_url,
|
||||
})
|
||||
run_on_start = true
|
||||
}
|
||||
|
||||
resource "coder_app" "perplexica" {
|
||||
agent_id = var.agent_id
|
||||
slug = "perplexica"
|
||||
display_name = "Perplexica"
|
||||
url = "http://localhost:${var.port}"
|
||||
icon = "/icon/perplexica.svg"
|
||||
subdomain = true
|
||||
share = var.share
|
||||
order = var.order
|
||||
group = var.group
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
run "plan_basic" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_app.perplexica.url == "http://localhost:3000"
|
||||
error_message = "Default port should be 3000"
|
||||
}
|
||||
}
|
||||
|
||||
run "plan_custom_port" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent"
|
||||
port = 8080
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_app.perplexica.url == "http://localhost:8080"
|
||||
error_message = "Should use custom port"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
set -eu
|
||||
|
||||
BOLD='\033[0;1m'
|
||||
RESET='\033[0m'
|
||||
|
||||
printf "$${BOLD}Starting Perplexica...$${RESET}\n"
|
||||
|
||||
# Set Docker host if provided
|
||||
if [ -n "${DOCKER_HOST}" ]; then
|
||||
export DOCKER_HOST="${DOCKER_HOST}"
|
||||
fi
|
||||
|
||||
# Wait for docker to become ready
|
||||
max_attempts=10
|
||||
delay=2
|
||||
attempt=1
|
||||
|
||||
while ! docker ps; do
|
||||
if [ $attempt -ge $max_attempts ]; then
|
||||
echo "Failed to list containers after $${max_attempts} attempts."
|
||||
exit 1
|
||||
fi
|
||||
echo "Attempt $${attempt} failed, retrying in $${delay}s..."
|
||||
sleep $delay
|
||||
attempt=$(expr "$attempt" + 1)
|
||||
delay=$(expr "$delay" \* 2)
|
||||
done
|
||||
|
||||
# Pull the image
|
||||
IMAGE="itzcrazykns1337/perplexica:latest"
|
||||
docker pull "$${IMAGE}"
|
||||
|
||||
# Build docker run command
|
||||
DOCKER_ARGS="-d --rm --name perplexica -p ${PORT}:3000"
|
||||
|
||||
# Add mounts - convert relative paths to absolute
|
||||
DATA_PATH="${DATA_PATH}"
|
||||
UPLOADS_PATH="${UPLOADS_PATH}"
|
||||
|
||||
mkdir -p "$${DATA_PATH}"
|
||||
mkdir -p "$${UPLOADS_PATH}"
|
||||
|
||||
DATA_PATH_ABS=$(cd "$${DATA_PATH}" && pwd)
|
||||
UPLOADS_PATH_ABS=$(cd "$${UPLOADS_PATH}" && pwd)
|
||||
|
||||
DOCKER_ARGS="$${DOCKER_ARGS} -v $${DATA_PATH_ABS}:/home/perplexica/data"
|
||||
DOCKER_ARGS="$${DOCKER_ARGS} -v $${UPLOADS_PATH_ABS}:/home/perplexica/uploads"
|
||||
|
||||
# Add environment variables if provided
|
||||
if [ -n "${OPENAI_API_KEY}" ]; then
|
||||
DOCKER_ARGS="$${DOCKER_ARGS} -e OPENAI_API_KEY=${OPENAI_API_KEY}"
|
||||
fi
|
||||
|
||||
if [ -n "${ANTHROPIC_API_KEY}" ]; then
|
||||
DOCKER_ARGS="$${DOCKER_ARGS} -e ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}"
|
||||
fi
|
||||
|
||||
if [ -n "${OLLAMA_API_URL}" ]; then
|
||||
DOCKER_ARGS="$${DOCKER_ARGS} -e OLLAMA_API_URL=${OLLAMA_API_URL}"
|
||||
fi
|
||||
|
||||
# Run container
|
||||
docker run $${DOCKER_ARGS} "$${IMAGE}"
|
||||
|
||||
printf "\n$${BOLD}Perplexica is running on port ${PORT}$${RESET}\n"
|
||||
@@ -12,12 +12,12 @@ Run [Amp CLI](https://ampcode.com/) in your workspace to access Sourcegraph's AI
|
||||
|
||||
```tf
|
||||
module "amp-cli" {
|
||||
source = "registry.coder.com/coder-labs/sourcegraph-amp/coder"
|
||||
version = "2.0.2"
|
||||
agent_id = coder_agent.example.id
|
||||
sourcegraph_amp_api_key = var.sourcegraph_amp_api_key
|
||||
install_sourcegraph_amp = true
|
||||
agentapi_version = "2.0.2"
|
||||
source = "registry.coder.com/coder-labs/sourcegraph-amp/coder"
|
||||
version = "2.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
amp_api_key = var.amp_api_key
|
||||
install_amp = true
|
||||
agentapi_version = "latest"
|
||||
}
|
||||
```
|
||||
|
||||
@@ -48,7 +48,7 @@ variable "amp_api_key" {
|
||||
module "amp-cli" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder-labs/sourcegraph-amp/coder"
|
||||
amp_version = "2.0.2"
|
||||
amp_version = "2.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
amp_api_key = var.amp_api_key # recommended for tasks usage
|
||||
workdir = "/home/coder/project"
|
||||
|
||||
@@ -55,7 +55,7 @@ variable "install_agentapi" {
|
||||
variable "agentapi_version" {
|
||||
type = string
|
||||
description = "The version of AgentAPI to install."
|
||||
default = "v0.10.0"
|
||||
default = "v0.11.1"
|
||||
}
|
||||
|
||||
variable "cli_app" {
|
||||
@@ -160,6 +160,16 @@ variable "mcp" {
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "mode" {
|
||||
type = string
|
||||
description = "Set the agent mode (free, rush, smart) — controls the model, system prompt, and tool selection. Default: smart"
|
||||
default = "smart"
|
||||
validation {
|
||||
condition = contains(["", "free", "rush", "smart"], var.mode)
|
||||
error_message = "Invalid mode. Select one from (free, rush, smart)"
|
||||
}
|
||||
}
|
||||
|
||||
data "external" "env" {
|
||||
program = ["sh", "-c", "echo '{\"CODER_AGENT_TOKEN\":\"'$CODER_AGENT_TOKEN'\",\"CODER_AGENT_URL\":\"'$CODER_AGENT_URL'\"}'"]
|
||||
}
|
||||
@@ -170,6 +180,7 @@ locals {
|
||||
default_base_config = jsonencode({
|
||||
"amp.anthropic.thinking.enabled" = true
|
||||
"amp.todos.enabled" = true
|
||||
"amp.terminal.animation" = false
|
||||
})
|
||||
|
||||
user_config = jsondecode(var.base_amp_config != "" ? var.base_amp_config : local.default_base_config)
|
||||
@@ -237,6 +248,7 @@ module "agentapi" {
|
||||
ARG_AMP_START_DIRECTORY='${var.workdir}' \
|
||||
ARG_AMP_TASK_PROMPT='${base64encode(var.ai_prompt)}' \
|
||||
ARG_REPORT_TASKS='${var.report_tasks}' \
|
||||
ARG_MODE='${var.mode}' \
|
||||
/tmp/start.sh
|
||||
EOT
|
||||
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [ -f "$HOME/.bashrc" ]; then
|
||||
source "$HOME"/.bashrc
|
||||
fi
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# ANSI colors
|
||||
|
||||
@@ -29,6 +29,7 @@ echo "--------------------------------"
|
||||
printf "Workspace: %s\n" "$ARG_AMP_START_DIRECTORY"
|
||||
printf "Task Prompt: %s\n" "$ARG_AMP_TASK_PROMPT"
|
||||
printf "ARG_REPORT_TASKS: %s\n" "$ARG_REPORT_TASKS"
|
||||
printf "ARG_MODE: %s\n" "$ARG_MODE"
|
||||
echo "--------------------------------"
|
||||
|
||||
ensure_command amp
|
||||
@@ -50,6 +51,13 @@ else
|
||||
printf "amp_api_key not provided\n"
|
||||
fi
|
||||
|
||||
ARGS=()
|
||||
|
||||
if [ -n "$ARG_MODE" ]; then
|
||||
printf "Running agent in: %s mode" "$ARG_MODE"
|
||||
ARGS+=(--mode "$ARG_MODE")
|
||||
fi
|
||||
|
||||
if [ -n "$ARG_AMP_TASK_PROMPT" ]; then
|
||||
if [ "$ARG_REPORT_TASKS" == "true" ]; then
|
||||
printf "amp task prompt provided : %s" "$ARG_AMP_TASK_PROMPT\n"
|
||||
@@ -58,8 +66,8 @@ if [ -n "$ARG_AMP_TASK_PROMPT" ]; then
|
||||
PROMPT="$ARG_AMP_TASK_PROMPT"
|
||||
fi
|
||||
# Pipe the prompt into amp, which will be run inside agentapi
|
||||
agentapi server --type amp --term-width=67 --term-height=1190 -- bash -c "echo \"$PROMPT\" | amp"
|
||||
agentapi server --type amp --term-width=67 --term-height=1190 -- bash -c "echo \"$PROMPT\" | amp" "${ARGS[@]}"
|
||||
else
|
||||
printf "No task prompt given.\n"
|
||||
agentapi server --type amp --term-width=67 --term-height=1190 -- amp
|
||||
agentapi server --type amp --term-width=67 --term-height=1190 -- amp "${ARGS[@]}"
|
||||
fi
|
||||
|
||||
|
Before Width: | Height: | Size: 976 KiB After Width: | Height: | Size: 528 KiB |
|
After Width: | Height: | Size: 249 KiB |
|
Before Width: | Height: | Size: 302 KiB After Width: | Height: | Size: 191 KiB |
@@ -4,11 +4,35 @@ import {
|
||||
removeContainer,
|
||||
runContainer,
|
||||
runTerraformApply,
|
||||
TerraformState,
|
||||
writeFileContainer,
|
||||
} from "~test";
|
||||
import path from "path";
|
||||
import { expect } from "bun:test";
|
||||
|
||||
/**
|
||||
* Extracts all coder_env resources from Terraform state and returns them as
|
||||
* a Record of environment variable names to values.
|
||||
*/
|
||||
export const extractCoderEnvVars = (
|
||||
state: TerraformState,
|
||||
): Record<string, string> => {
|
||||
const envVars: Record<string, string> = {};
|
||||
|
||||
for (const resource of state.resources) {
|
||||
if (resource.type === "coder_env" && resource.instances.length > 0) {
|
||||
const instance = resource.instances[0].attributes;
|
||||
const name = instance.name as string;
|
||||
const value = instance.value as string;
|
||||
if (name && value) {
|
||||
envVars[name] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return envVars;
|
||||
};
|
||||
|
||||
export const setupContainer = async ({
|
||||
moduleDir,
|
||||
image,
|
||||
@@ -23,10 +47,12 @@ export const setupContainer = async ({
|
||||
...vars,
|
||||
});
|
||||
const coderScript = findResourceInstance(state, "coder_script");
|
||||
const coderEnvVars = extractCoderEnvVars(state);
|
||||
const id = await runContainer(image ?? "codercom/enterprise-node:latest");
|
||||
return {
|
||||
id,
|
||||
coderScript,
|
||||
coderEnvVars,
|
||||
cleanup: async () => {
|
||||
if (
|
||||
process.env["DEBUG"] === "true" ||
|
||||
@@ -79,9 +105,11 @@ interface SetupProps {
|
||||
agentapiMockScript?: string;
|
||||
}
|
||||
|
||||
export const setup = async (props: SetupProps): Promise<{ id: string }> => {
|
||||
export const setup = async (
|
||||
props: SetupProps,
|
||||
): Promise<{ id: string; coderEnvVars: Record<string, string> }> => {
|
||||
const projectDir = props.projectDir ?? "/home/coder/project";
|
||||
const { id, coderScript, cleanup } = await setupContainer({
|
||||
const { id, coderScript, coderEnvVars, cleanup } = await setupContainer({
|
||||
moduleDir: props.moduleDir,
|
||||
vars: props.moduleVariables,
|
||||
});
|
||||
@@ -101,7 +129,7 @@ export const setup = async (props: SetupProps): Promise<{ id: string }> => {
|
||||
filePath: "/home/coder/script.sh",
|
||||
content: coderScript.script,
|
||||
});
|
||||
return { id };
|
||||
return { id, coderEnvVars };
|
||||
};
|
||||
|
||||
export const expectAgentAPIStarted = async (
|
||||
@@ -125,18 +153,16 @@ export const execModuleScript = async (
|
||||
id: string,
|
||||
env?: Record<string, string>,
|
||||
) => {
|
||||
const envArgs = Object.entries(env ?? {})
|
||||
.map(([key, value]) => ["--env", `${key}=${value}`])
|
||||
.flat();
|
||||
const resp = await execContainer(
|
||||
id,
|
||||
[
|
||||
"bash",
|
||||
"-c",
|
||||
`set -o errexit; set -o pipefail; cd /home/coder && ./script.sh 2>&1 | tee /home/coder/script.log`,
|
||||
],
|
||||
envArgs,
|
||||
);
|
||||
const envArgs = env
|
||||
? Object.entries(env)
|
||||
.map(([key, value]) => `export ${key}="${value.replace(/"/g, '\\"')}"`)
|
||||
.join(" && ") + " && "
|
||||
: "";
|
||||
const resp = await execContainer(id, [
|
||||
"bash",
|
||||
"-c",
|
||||
`${envArgs}set -o errexit; set -o pipefail; cd /home/coder && ./script.sh 2>&1 | tee /home/coder/script.log`,
|
||||
]);
|
||||
if (resp.exitCode !== 0) {
|
||||
console.log(resp.stdout);
|
||||
console.log(resp.stderr);
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
---
|
||||
display_name: Antigravity
|
||||
description: Add a one-click button to launch Google Antigravity
|
||||
icon: ../../../../.icons/antigravity.svg
|
||||
verified: true
|
||||
tags: [ide, antigravity, ai, google]
|
||||
---
|
||||
|
||||
# Antigravity IDE
|
||||
|
||||
Add a button to open any workspace with a single click in [Antigravity IDE](https://antigravity.google).
|
||||
|
||||
Uses the [Coder Remote VS Code Extension](https://github.com/coder/vscode-coder).
|
||||
|
||||
```tf
|
||||
module "antigravity" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/antigravity/coder"
|
||||
version = "1.0.0"
|
||||
agent_id = coder_agent.example.id
|
||||
}
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Open in a specific directory
|
||||
|
||||
```tf
|
||||
module "antigravity" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/antigravity/coder"
|
||||
version = "1.0.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/project"
|
||||
}
|
||||
```
|
||||
|
||||
### Configure MCP servers for Antigravity
|
||||
|
||||
Provide a JSON-encoded string via the `mcp` input. When set, the module writes the value to `~/.gemini/antigravity/mcp_config.json` using a `coder_script` on workspace start.
|
||||
|
||||
The following example configures Antigravity to use the GitHub MCP server with authentication facilitated by the [`coder_external_auth`](https://coder.com/docs/admin/external-auth#configure-a-github-oauth-app) resource.
|
||||
|
||||
```tf
|
||||
module "antigravity" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/antigravity/coder"
|
||||
version = "1.0.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/project"
|
||||
mcp = jsonencode({
|
||||
mcpServers = {
|
||||
"github" : {
|
||||
"url" : "https://api.githubcopilot.com/mcp/",
|
||||
"headers" : {
|
||||
"Authorization" : "Bearer ${data.coder_external_auth.github.access_token}",
|
||||
},
|
||||
"type" : "http"
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
data "coder_external_auth" "github" {
|
||||
id = "github"
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,130 @@
|
||||
import { describe, it, expect } from "bun:test";
|
||||
import {
|
||||
runTerraformApply,
|
||||
runTerraformInit,
|
||||
testRequiredVariables,
|
||||
runContainer,
|
||||
execContainer,
|
||||
removeContainer,
|
||||
findResourceInstance,
|
||||
readFileContainer,
|
||||
} from "~test";
|
||||
|
||||
describe("antigravity", async () => {
|
||||
await runTerraformInit(import.meta.dir);
|
||||
|
||||
testRequiredVariables(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
});
|
||||
|
||||
it("default output", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
});
|
||||
expect(state.outputs.antigravity_url.value).toBe(
|
||||
"antigravity://coder.coder-remote/open?owner=default&workspace=default&url=https://mydeployment.coder.com&token=$SESSION_TOKEN",
|
||||
);
|
||||
|
||||
const coder_app = state.resources.find(
|
||||
(res) =>
|
||||
res.type === "coder_app" &&
|
||||
res.module === "module.vscode-desktop-core" &&
|
||||
res.name === "vscode-desktop",
|
||||
);
|
||||
|
||||
expect(coder_app).not.toBeNull();
|
||||
expect(coder_app?.instances.length).toBe(1);
|
||||
expect(coder_app?.instances[0].attributes.order).toBeNull();
|
||||
});
|
||||
|
||||
it("adds folder", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
folder: "/foo/bar",
|
||||
});
|
||||
expect(state.outputs.antigravity_url.value).toBe(
|
||||
"antigravity://coder.coder-remote/open?owner=default&workspace=default&folder=/foo/bar&url=https://mydeployment.coder.com&token=$SESSION_TOKEN",
|
||||
);
|
||||
});
|
||||
|
||||
it("adds folder and open_recent", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
folder: "/foo/bar",
|
||||
open_recent: "true",
|
||||
});
|
||||
expect(state.outputs.antigravity_url.value).toBe(
|
||||
"antigravity://coder.coder-remote/open?owner=default&workspace=default&folder=/foo/bar&openRecent&url=https://mydeployment.coder.com&token=$SESSION_TOKEN",
|
||||
);
|
||||
});
|
||||
|
||||
it("adds folder but not open_recent", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
folder: "/foo/bar",
|
||||
open_recent: "false",
|
||||
});
|
||||
expect(state.outputs.antigravity_url.value).toBe(
|
||||
"antigravity://coder.coder-remote/open?owner=default&workspace=default&folder=/foo/bar&url=https://mydeployment.coder.com&token=$SESSION_TOKEN",
|
||||
);
|
||||
});
|
||||
|
||||
it("adds open_recent", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
open_recent: "true",
|
||||
});
|
||||
expect(state.outputs.antigravity_url.value).toBe(
|
||||
"antigravity://coder.coder-remote/open?owner=default&workspace=default&openRecent&url=https://mydeployment.coder.com&token=$SESSION_TOKEN",
|
||||
);
|
||||
});
|
||||
|
||||
it("expect order to be set", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
order: "22",
|
||||
});
|
||||
|
||||
const coder_app = state.resources.find(
|
||||
(res) =>
|
||||
res.type === "coder_app" &&
|
||||
res.module === "module.vscode-desktop-core" &&
|
||||
res.name === "vscode-desktop",
|
||||
);
|
||||
|
||||
expect(coder_app).not.toBeNull();
|
||||
expect(coder_app?.instances.length).toBe(1);
|
||||
expect(coder_app?.instances[0].attributes.order).toBe(22);
|
||||
});
|
||||
|
||||
it("writes ~/.gemini/antigravity/mcp_config.json when mcp provided", async () => {
|
||||
const id = await runContainer("alpine");
|
||||
try {
|
||||
const mcp = JSON.stringify({
|
||||
servers: { demo: { url: "http://localhost:1234" } },
|
||||
});
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
mcp,
|
||||
});
|
||||
const script = findResourceInstance(
|
||||
state,
|
||||
"coder_script",
|
||||
"antigravity_mcp",
|
||||
).script;
|
||||
const resp = await execContainer(id, ["sh", "-c", script]);
|
||||
if (resp.exitCode !== 0) {
|
||||
console.log(resp.stdout);
|
||||
console.log(resp.stderr);
|
||||
}
|
||||
expect(resp.exitCode).toBe(0);
|
||||
const content = await readFileContainer(
|
||||
id,
|
||||
"/root/.gemini/antigravity/mcp_config.json",
|
||||
);
|
||||
expect(content).toBe(mcp);
|
||||
} finally {
|
||||
await removeContainer(id);
|
||||
}
|
||||
}, 10000);
|
||||
});
|
||||
@@ -0,0 +1,104 @@
|
||||
terraform {
|
||||
required_version = ">= 1.0"
|
||||
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">= 2.5"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
variable "agent_id" {
|
||||
type = string
|
||||
description = "The ID of a Coder agent."
|
||||
}
|
||||
|
||||
variable "folder" {
|
||||
type = string
|
||||
description = "The folder to open in Antigravity IDE."
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "open_recent" {
|
||||
type = bool
|
||||
description = "Open the most recent workspace or folder. Falls back to the folder if there is no recent workspace or folder to open."
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "order" {
|
||||
type = number
|
||||
description = "The order determines the position of app in the UI presentation. The lowest order is shown first and apps with equal order are sorted by name (ascending order)."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "group" {
|
||||
type = string
|
||||
description = "The name of a group that this app belongs to."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "slug" {
|
||||
type = string
|
||||
description = "The slug of the app."
|
||||
default = "antigravity"
|
||||
}
|
||||
|
||||
variable "display_name" {
|
||||
type = string
|
||||
description = "The display name of the app."
|
||||
default = "Antigravity IDE"
|
||||
}
|
||||
|
||||
variable "mcp" {
|
||||
type = string
|
||||
description = "JSON-encoded string to configure MCP servers for Antigravity. When set, writes ~/.gemini/antigravity/mcp_config.json."
|
||||
default = ""
|
||||
}
|
||||
|
||||
data "coder_workspace" "me" {}
|
||||
|
||||
data "coder_workspace_owner" "me" {}
|
||||
|
||||
locals {
|
||||
mcp_b64 = var.mcp != "" ? base64encode(var.mcp) : ""
|
||||
}
|
||||
|
||||
module "vscode-desktop-core" {
|
||||
source = "registry.coder.com/coder/vscode-desktop-core/coder"
|
||||
version = "1.0.1"
|
||||
|
||||
agent_id = var.agent_id
|
||||
|
||||
web_app_icon = "/icon/antigravity.svg"
|
||||
web_app_slug = var.slug
|
||||
web_app_display_name = var.display_name
|
||||
web_app_order = var.order
|
||||
web_app_group = var.group
|
||||
|
||||
folder = var.folder
|
||||
open_recent = var.open_recent
|
||||
protocol = "antigravity"
|
||||
}
|
||||
|
||||
resource "coder_script" "antigravity_mcp" {
|
||||
count = var.mcp != "" ? 1 : 0
|
||||
agent_id = var.agent_id
|
||||
display_name = "Antigravity MCP"
|
||||
icon = "/icon/antigravity.svg"
|
||||
run_on_start = true
|
||||
start_blocks_login = false
|
||||
script = <<-EOT
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
mkdir -p "$HOME/.gemini/antigravity"
|
||||
echo -n "${local.mcp_b64}" | base64 -d > "$HOME/.gemini/antigravity/mcp_config.json"
|
||||
chmod 600 "$HOME/.gemini/antigravity/mcp_config.json"
|
||||
EOT
|
||||
}
|
||||
|
||||
output "antigravity_url" {
|
||||
value = module.vscode-desktop-core.ide_uri
|
||||
description = "Antigravity IDE URL."
|
||||
}
|
||||
|
||||
@@ -13,8 +13,8 @@ Run the [Claude Code](https://docs.anthropic.com/en/docs/agents-and-tools/claude
|
||||
```tf
|
||||
module "claude-code" {
|
||||
source = "registry.coder.com/coder/claude-code/coder"
|
||||
version = "4.2.2"
|
||||
agent_id = coder_agent.example.id
|
||||
version = "4.2.7"
|
||||
agent_id = coder_agent.main.id
|
||||
workdir = "/home/coder/project"
|
||||
claude_api_key = "xxxx-xxxxx-xxxx"
|
||||
}
|
||||
@@ -45,13 +45,15 @@ This example shows how to configure the Claude Code module to run the agent behi
|
||||
```tf
|
||||
module "claude-code" {
|
||||
source = "dev.registry.coder.com/coder/claude-code/coder"
|
||||
version = "4.2.7"
|
||||
agent_id = coder_agent.main.id
|
||||
workdir = "/home/coder/project"
|
||||
enable_boundary = true
|
||||
boundary_version = "4.2.2"
|
||||
boundary_version = "main"
|
||||
boundary_log_dir = "/tmp/boundary_logs"
|
||||
boundary_log_level = "WARN"
|
||||
boundary_additional_allowed_urls = ["GET *google.com"]
|
||||
boundary_proxy_port = "8087"
|
||||
version = "4.2.2"
|
||||
}
|
||||
```
|
||||
|
||||
@@ -70,16 +72,16 @@ data "coder_parameter" "ai_prompt" {
|
||||
|
||||
module "claude-code" {
|
||||
source = "registry.coder.com/coder/claude-code/coder"
|
||||
version = "4.2.2"
|
||||
agent_id = coder_agent.example.id
|
||||
version = "4.2.7"
|
||||
agent_id = coder_agent.main.id
|
||||
workdir = "/home/coder/project"
|
||||
|
||||
claude_api_key = "xxxx-xxxxx-xxxx"
|
||||
# OR
|
||||
claude_code_oauth_token = "xxxxx-xxxx-xxxx"
|
||||
|
||||
claude_code_version = "4.2.2" # Pin to a specific version
|
||||
agentapi_version = "4.2.2"
|
||||
claude_code_version = "2.0.62" # Pin to a specific version
|
||||
agentapi_version = "0.11.4"
|
||||
|
||||
ai_prompt = data.coder_parameter.ai_prompt.value
|
||||
model = "sonnet"
|
||||
@@ -90,7 +92,7 @@ module "claude-code" {
|
||||
{
|
||||
"mcpServers": {
|
||||
"my-custom-tool": {
|
||||
"command": "my-tool-server"
|
||||
"command": "my-tool-server",
|
||||
"args": ["--port", "8080"]
|
||||
}
|
||||
}
|
||||
@@ -106,13 +108,12 @@ Run and configure Claude Code as a standalone CLI in your workspace.
|
||||
```tf
|
||||
module "claude-code" {
|
||||
source = "registry.coder.com/coder/claude-code/coder"
|
||||
version = "4.2.2"
|
||||
agent_id = coder_agent.example.id
|
||||
workdir = "/home/coder"
|
||||
version = "4.2.7"
|
||||
agent_id = coder_agent.main.id
|
||||
workdir = "/home/coder/project"
|
||||
install_claude_code = true
|
||||
claude_code_version = "4.2.2"
|
||||
claude_code_version = "2.0.62"
|
||||
report_tasks = false
|
||||
cli_app = true
|
||||
}
|
||||
```
|
||||
|
||||
@@ -129,8 +130,8 @@ variable "claude_code_oauth_token" {
|
||||
|
||||
module "claude-code" {
|
||||
source = "registry.coder.com/coder/claude-code/coder"
|
||||
version = "4.2.2"
|
||||
agent_id = coder_agent.example.id
|
||||
version = "4.2.7"
|
||||
agent_id = coder_agent.main.id
|
||||
workdir = "/home/coder/project"
|
||||
claude_code_oauth_token = var.claude_code_oauth_token
|
||||
}
|
||||
@@ -146,13 +147,13 @@ Configure Claude Code to use AWS Bedrock for accessing Claude models through you
|
||||
|
||||
```tf
|
||||
resource "coder_env" "bedrock_use" {
|
||||
agent_id = coder_agent.example.id
|
||||
agent_id = coder_agent.main.id
|
||||
name = "CLAUDE_CODE_USE_BEDROCK"
|
||||
value = "1"
|
||||
}
|
||||
|
||||
resource "coder_env" "aws_region" {
|
||||
agent_id = coder_agent.example.id
|
||||
agent_id = coder_agent.main.id
|
||||
name = "AWS_REGION"
|
||||
value = "us-east-1" # Choose your preferred region
|
||||
}
|
||||
@@ -174,13 +175,13 @@ variable "aws_secret_access_key" {
|
||||
}
|
||||
|
||||
resource "coder_env" "aws_access_key_id" {
|
||||
agent_id = coder_agent.example.id
|
||||
agent_id = coder_agent.main.id
|
||||
name = "AWS_ACCESS_KEY_ID"
|
||||
value = var.aws_access_key_id
|
||||
}
|
||||
|
||||
resource "coder_env" "aws_secret_access_key" {
|
||||
agent_id = coder_agent.example.id
|
||||
agent_id = coder_agent.main.id
|
||||
name = "AWS_SECRET_ACCESS_KEY"
|
||||
value = var.aws_secret_access_key
|
||||
}
|
||||
@@ -195,15 +196,15 @@ variable "aws_bearer_token_bedrock" {
|
||||
}
|
||||
|
||||
resource "coder_env" "bedrock_api_key" {
|
||||
agent_id = coder_agent.example.id
|
||||
agent_id = coder_agent.main.id
|
||||
name = "AWS_BEARER_TOKEN_BEDROCK"
|
||||
value = var.aws_bearer_token_bedrock
|
||||
}
|
||||
|
||||
module "claude-code" {
|
||||
source = "registry.coder.com/coder/claude-code/coder"
|
||||
version = "4.2.2"
|
||||
agent_id = coder_agent.example.id
|
||||
version = "4.2.7"
|
||||
agent_id = coder_agent.main.id
|
||||
workdir = "/home/coder/project"
|
||||
model = "global.anthropic.claude-sonnet-4-5-20250929-v1:0"
|
||||
}
|
||||
@@ -228,39 +229,39 @@ variable "vertex_sa_json" {
|
||||
}
|
||||
|
||||
resource "coder_env" "vertex_use" {
|
||||
agent_id = coder_agent.example.id
|
||||
agent_id = coder_agent.main.id
|
||||
name = "CLAUDE_CODE_USE_VERTEX"
|
||||
value = "1"
|
||||
}
|
||||
|
||||
resource "coder_env" "vertex_project_id" {
|
||||
agent_id = coder_agent.example.id
|
||||
agent_id = coder_agent.main.id
|
||||
name = "ANTHROPIC_VERTEX_PROJECT_ID"
|
||||
value = "your-gcp-project-id"
|
||||
}
|
||||
|
||||
resource "coder_env" "cloud_ml_region" {
|
||||
agent_id = coder_agent.example.id
|
||||
agent_id = coder_agent.main.id
|
||||
name = "CLOUD_ML_REGION"
|
||||
value = "global"
|
||||
}
|
||||
|
||||
resource "coder_env" "vertex_sa_json" {
|
||||
agent_id = coder_agent.example.id
|
||||
agent_id = coder_agent.main.id
|
||||
name = "VERTEX_SA_JSON"
|
||||
value = var.vertex_sa_json
|
||||
}
|
||||
|
||||
resource "coder_env" "google_application_credentials" {
|
||||
agent_id = coder_agent.example.id
|
||||
agent_id = coder_agent.main.id
|
||||
name = "GOOGLE_APPLICATION_CREDENTIALS"
|
||||
value = "/tmp/gcp-sa.json"
|
||||
}
|
||||
|
||||
module "claude-code" {
|
||||
source = "registry.coder.com/coder/claude-code/coder"
|
||||
version = "4.2.2"
|
||||
agent_id = coder_agent.example.id
|
||||
version = "4.2.7"
|
||||
agent_id = coder_agent.main.id
|
||||
workdir = "/home/coder/project"
|
||||
model = "claude-sonnet-4@20250514"
|
||||
|
||||
|
||||
@@ -39,9 +39,11 @@ interface SetupProps {
|
||||
agentapiMockScript?: string;
|
||||
}
|
||||
|
||||
const setup = async (props?: SetupProps): Promise<{ id: string }> => {
|
||||
const setup = async (
|
||||
props?: SetupProps,
|
||||
): Promise<{ id: string; coderEnvVars: Record<string, string> }> => {
|
||||
const projectDir = "/home/coder/project";
|
||||
const { id } = await setupUtil({
|
||||
const { id, coderEnvVars } = await setupUtil({
|
||||
moduleDir: import.meta.dir,
|
||||
moduleVariables: {
|
||||
install_claude_code: props?.skipClaudeMock ? "true" : "false",
|
||||
@@ -61,7 +63,7 @@ const setup = async (props?: SetupProps): Promise<{ id: string }> => {
|
||||
content: await loadTestFile(import.meta.dir, "claude-mock.sh"),
|
||||
});
|
||||
}
|
||||
return { id };
|
||||
return { id, coderEnvVars };
|
||||
};
|
||||
|
||||
setDefaultTimeout(60 * 1000);
|
||||
@@ -79,14 +81,14 @@ describe("claude-code", async () => {
|
||||
|
||||
test("install-claude-code-version", async () => {
|
||||
const version_to_install = "1.0.40";
|
||||
const { id } = await setup({
|
||||
const { id, coderEnvVars } = await setup({
|
||||
skipClaudeMock: true,
|
||||
moduleVariables: {
|
||||
install_claude_code: "true",
|
||||
claude_code_version: version_to_install,
|
||||
},
|
||||
});
|
||||
await execModuleScript(id);
|
||||
await execModuleScript(id, coderEnvVars);
|
||||
const resp = await execContainer(id, [
|
||||
"bash",
|
||||
"-c",
|
||||
@@ -96,14 +98,14 @@ describe("claude-code", async () => {
|
||||
});
|
||||
|
||||
test("check-latest-claude-code-version-works", async () => {
|
||||
const { id } = await setup({
|
||||
const { id, coderEnvVars } = await setup({
|
||||
skipClaudeMock: true,
|
||||
skipAgentAPIMock: true,
|
||||
moduleVariables: {
|
||||
install_claude_code: "true",
|
||||
},
|
||||
});
|
||||
await execModuleScript(id);
|
||||
await execModuleScript(id, coderEnvVars);
|
||||
await expectAgentAPIStarted(id);
|
||||
});
|
||||
|
||||
@@ -133,13 +135,13 @@ describe("claude-code", async () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
const { id } = await setup({
|
||||
const { id, coderEnvVars } = await setup({
|
||||
skipClaudeMock: true,
|
||||
moduleVariables: {
|
||||
mcp: mcpConfig,
|
||||
},
|
||||
});
|
||||
await execModuleScript(id);
|
||||
await execModuleScript(id, coderEnvVars);
|
||||
|
||||
const resp = await readFileContainer(id, "/home/coder/.claude.json");
|
||||
expect(resp).toContain("test-cmd");
|
||||
@@ -208,13 +210,17 @@ describe("claude-code", async () => {
|
||||
});
|
||||
|
||||
// Create a mock task session file with the hardcoded task session ID
|
||||
// Note: Claude CLI creates files without "session-" prefix when using --session-id
|
||||
const taskSessionId = "cd32e253-ca16-4fd3-9825-d837e74ae3c2";
|
||||
const sessionDir = `/home/coder/.claude/projects/-home-coder-project`;
|
||||
await execContainer(id, ["mkdir", "-p", sessionDir]);
|
||||
await execContainer(id, [
|
||||
"bash",
|
||||
"-c",
|
||||
`touch ${sessionDir}/session-${taskSessionId}.jsonl`,
|
||||
`cat > ${sessionDir}/${taskSessionId}.jsonl << 'SESSIONEOF'
|
||||
{"sessionId":"${taskSessionId}","message":{"content":"Task"},"timestamp":"2020-01-01T10:00:00.000Z"}
|
||||
{"type":"assistant","message":{"content":"Response"},"timestamp":"2020-01-01T10:00:05.000Z"}
|
||||
SESSIONEOF`,
|
||||
]);
|
||||
|
||||
await execModuleScript(id);
|
||||
@@ -226,46 +232,10 @@ describe("claude-code", async () => {
|
||||
]);
|
||||
expect(startLog.stdout).toContain("--resume");
|
||||
expect(startLog.stdout).toContain(taskSessionId);
|
||||
expect(startLog.stdout).toContain("Resuming existing task session");
|
||||
expect(startLog.stdout).toContain("Resuming task session");
|
||||
expect(startLog.stdout).toContain("--dangerously-skip-permissions");
|
||||
});
|
||||
|
||||
test("claude-continue-resume-standalone-session", async () => {
|
||||
const { id } = await setup({
|
||||
moduleVariables: {
|
||||
continue: "true",
|
||||
report_tasks: "false",
|
||||
ai_prompt: "test prompt",
|
||||
},
|
||||
});
|
||||
|
||||
const sessionId = "some-random-session-id";
|
||||
const workdir = "/home/coder/project";
|
||||
const claudeJson = {
|
||||
projects: {
|
||||
[workdir]: {
|
||||
lastSessionId: sessionId,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
await execContainer(id, [
|
||||
"bash",
|
||||
"-c",
|
||||
`echo '${JSON.stringify(claudeJson)}' > /home/coder/.claude.json`,
|
||||
]);
|
||||
|
||||
await execModuleScript(id);
|
||||
|
||||
const startLog = await execContainer(id, [
|
||||
"bash",
|
||||
"-c",
|
||||
"cat /home/coder/.claude-module/agentapi-start.log",
|
||||
]);
|
||||
expect(startLog.stdout).toContain("--continue");
|
||||
expect(startLog.stdout).toContain("Resuming existing session");
|
||||
});
|
||||
|
||||
test("pre-post-install-scripts", async () => {
|
||||
const { id } = await setup({
|
||||
moduleVariables: {
|
||||
@@ -360,4 +330,140 @@ describe("claude-code", async () => {
|
||||
"ARG_AGENTAPI_CHAT_BASE_PATH=/@default/default.foo/apps/ccw/chat",
|
||||
);
|
||||
});
|
||||
|
||||
test("partial-initialization-detection", async () => {
|
||||
const { id } = await setup({
|
||||
moduleVariables: {
|
||||
continue: "true",
|
||||
report_tasks: "true",
|
||||
ai_prompt: "test prompt",
|
||||
},
|
||||
});
|
||||
|
||||
const taskSessionId = "cd32e253-ca16-4fd3-9825-d837e74ae3c2";
|
||||
const sessionDir = `/home/coder/.claude/projects/-home-coder-project`;
|
||||
await execContainer(id, ["mkdir", "-p", sessionDir]);
|
||||
|
||||
await execContainer(id, [
|
||||
"bash",
|
||||
"-c",
|
||||
`echo '{"sessionId":"${taskSessionId}"}' > ${sessionDir}/${taskSessionId}.jsonl`,
|
||||
]);
|
||||
|
||||
await execModuleScript(id);
|
||||
|
||||
const startLog = await execContainer(id, [
|
||||
"bash",
|
||||
"-c",
|
||||
"cat /home/coder/.claude-module/agentapi-start.log",
|
||||
]);
|
||||
|
||||
// Should start new session, not try to resume invalid one
|
||||
expect(startLog.stdout).toContain("Starting new task session");
|
||||
expect(startLog.stdout).toContain("--session-id");
|
||||
});
|
||||
|
||||
test("standalone-first-build-no-sessions", async () => {
|
||||
const { id } = await setup({
|
||||
moduleVariables: {
|
||||
continue: "true",
|
||||
report_tasks: "false",
|
||||
},
|
||||
});
|
||||
|
||||
await execModuleScript(id);
|
||||
|
||||
const startLog = await execContainer(id, [
|
||||
"bash",
|
||||
"-c",
|
||||
"cat /home/coder/.claude-module/agentapi-start.log",
|
||||
]);
|
||||
|
||||
// Should start fresh, not try to continue
|
||||
expect(startLog.stdout).toContain("No sessions found");
|
||||
expect(startLog.stdout).toContain("starting fresh standalone session");
|
||||
expect(startLog.stdout).not.toContain("--continue");
|
||||
});
|
||||
|
||||
test("standalone-with-sessions-continues", async () => {
|
||||
const { id } = await setup({
|
||||
moduleVariables: {
|
||||
continue: "true",
|
||||
report_tasks: "false",
|
||||
},
|
||||
});
|
||||
|
||||
const sessionDir = `/home/coder/.claude/projects/-home-coder-project`;
|
||||
await execContainer(id, ["mkdir", "-p", sessionDir]);
|
||||
await execContainer(id, [
|
||||
"bash",
|
||||
"-c",
|
||||
`cat > ${sessionDir}/generic-123.jsonl << 'EOF'
|
||||
{"sessionId":"generic-123","message":{"content":"User session"},"timestamp":"2020-01-01T10:00:00.000Z"}
|
||||
{"type":"assistant","message":{"content":"Response"},"timestamp":"2020-01-01T10:00:05.000Z"}
|
||||
EOF`,
|
||||
]);
|
||||
|
||||
await execModuleScript(id);
|
||||
|
||||
const startLog = await execContainer(id, [
|
||||
"bash",
|
||||
"-c",
|
||||
"cat /home/coder/.claude-module/agentapi-start.log",
|
||||
]);
|
||||
|
||||
// Should continue existing session
|
||||
expect(startLog.stdout).toContain("Sessions found");
|
||||
expect(startLog.stdout).toContain(
|
||||
"Continuing most recent standalone session",
|
||||
);
|
||||
expect(startLog.stdout).toContain("--continue");
|
||||
});
|
||||
|
||||
test("task-mode-ignores-manual-sessions", async () => {
|
||||
const { id } = await setup({
|
||||
moduleVariables: {
|
||||
continue: "true",
|
||||
report_tasks: "true",
|
||||
ai_prompt: "test prompt",
|
||||
},
|
||||
});
|
||||
|
||||
const taskSessionId = "cd32e253-ca16-4fd3-9825-d837e74ae3c2";
|
||||
const sessionDir = `/home/coder/.claude/projects/-home-coder-project`;
|
||||
await execContainer(id, ["mkdir", "-p", sessionDir]);
|
||||
|
||||
// Create task session (without "session-" prefix, as CLI does)
|
||||
await execContainer(id, [
|
||||
"bash",
|
||||
"-c",
|
||||
`cat > ${sessionDir}/${taskSessionId}.jsonl << 'EOF'
|
||||
{"sessionId":"${taskSessionId}","message":{"content":"Task"},"timestamp":"2020-01-01T10:00:00.000Z"}
|
||||
{"type":"assistant","message":{"content":"Response"},"timestamp":"2020-01-01T10:00:05.000Z"}
|
||||
EOF`,
|
||||
]);
|
||||
|
||||
// Create manual session (newer)
|
||||
await execContainer(id, [
|
||||
"bash",
|
||||
"-c",
|
||||
`cat > ${sessionDir}/manual-456.jsonl << 'EOF'
|
||||
{"sessionId":"manual-456","message":{"content":"Manual"},"timestamp":"2020-01-02T10:00:00.000Z"}
|
||||
{"type":"assistant","message":{"content":"Response"},"timestamp":"2020-01-02T10:00:05.000Z"}
|
||||
EOF`,
|
||||
]);
|
||||
|
||||
await execModuleScript(id);
|
||||
|
||||
const startLog = await execContainer(id, [
|
||||
"bash",
|
||||
"-c",
|
||||
"cat /home/coder/.claude-module/agentapi-start.log",
|
||||
]);
|
||||
|
||||
// Should resume task session, not manual session
|
||||
expect(startLog.stdout).toContain("Resuming task session");
|
||||
expect(startLog.stdout).toContain(taskSessionId);
|
||||
expect(startLog.stdout).not.toContain("manual-456");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -86,7 +86,7 @@ variable "install_agentapi" {
|
||||
variable "agentapi_version" {
|
||||
type = string
|
||||
description = "The version of AgentAPI to install."
|
||||
default = "v0.10.0"
|
||||
default = "v0.11.4"
|
||||
}
|
||||
|
||||
variable "ai_prompt" {
|
||||
@@ -288,15 +288,20 @@ resource "coder_env" "disable_autoupdater" {
|
||||
value = "1"
|
||||
}
|
||||
|
||||
resource "coder_env" "claude_binary_path" {
|
||||
agent_id = var.agent_id
|
||||
name = "PATH"
|
||||
value = "$HOME/.local/bin:$PATH"
|
||||
}
|
||||
|
||||
locals {
|
||||
# we have to trim the slash because otherwise coder exp mcp will
|
||||
# set up an invalid claude config
|
||||
workdir = trimsuffix(var.workdir, "/")
|
||||
app_slug = "ccw"
|
||||
install_script = file("${path.module}/scripts/install.sh")
|
||||
start_script = file("${path.module}/scripts/start.sh")
|
||||
module_dir_name = ".claude-module"
|
||||
remove_last_session_id_script_b64 = base64encode(file("${path.module}/scripts/remove-last-session-id.sh"))
|
||||
workdir = trimsuffix(var.workdir, "/")
|
||||
app_slug = "ccw"
|
||||
install_script = file("${path.module}/scripts/install.sh")
|
||||
start_script = file("${path.module}/scripts/start.sh")
|
||||
module_dir_name = ".claude-module"
|
||||
# Extract hostname from access_url for boundary --allow flag
|
||||
coder_host = replace(replace(data.coder_workspace.me.access_url, "https://", ""), "http://", "")
|
||||
|
||||
@@ -357,9 +362,7 @@ module "agentapi" {
|
||||
set -o errexit
|
||||
set -o pipefail
|
||||
echo -n '${base64encode(local.start_script)}' | base64 -d > /tmp/start.sh
|
||||
echo -n "${local.remove_last_session_id_script_b64}" | base64 -d > "/tmp/remove-last-session-id.sh"
|
||||
chmod +x /tmp/start.sh
|
||||
chmod +x /tmp/remove-last-session-id.sh
|
||||
|
||||
ARG_MODEL='${var.model}' \
|
||||
ARG_RESUME_SESSION_ID='${var.resume_session_id}' \
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [ -f "$HOME/.bashrc" ]; then
|
||||
source "$HOME"/.bashrc
|
||||
fi
|
||||
|
||||
# Set strict error handling AFTER sourcing bashrc to avoid unbound variable errors from user dotfiles
|
||||
set -euo pipefail
|
||||
|
||||
BOLD='\033[0;1m'
|
||||
@@ -45,11 +40,6 @@ function install_claude_code_cli() {
|
||||
if [ $CURL_EXIT -ne 0 ]; then
|
||||
echo "Claude Code installer failed with exit code $$CURL_EXIT"
|
||||
fi
|
||||
|
||||
# Ensure binaries are discoverable.
|
||||
echo "Creating a symlink for claude"
|
||||
sudo ln -s /home/coder/.local/bin/claude /usr/local/bin/claude
|
||||
|
||||
echo "Installed Claude Code successfully. Version: $(claude --version || echo 'unknown')"
|
||||
else
|
||||
echo "Skipping Claude Code installation as per configuration."
|
||||
@@ -90,12 +80,63 @@ function setup_claude_configurations() {
|
||||
|
||||
}
|
||||
|
||||
function configure_standalone_mode() {
|
||||
echo "Configuring Claude Code for standalone mode..."
|
||||
|
||||
if [ -z "${CLAUDE_API_KEY:-}" ]; then
|
||||
echo "Note: CLAUDE_API_KEY not set, skipping authentication setup"
|
||||
return
|
||||
fi
|
||||
|
||||
local claude_config="$HOME/.claude.json"
|
||||
local workdir_normalized
|
||||
workdir_normalized=$(echo "$ARG_WORKDIR" | tr '/' '-')
|
||||
|
||||
# Create or update .claude.json with minimal configuration for API key auth
|
||||
# This skips the interactive login prompt and onboarding screens
|
||||
if [ -f "$claude_config" ]; then
|
||||
echo "Updating existing Claude configuration at $claude_config"
|
||||
|
||||
jq --arg apikey "${CLAUDE_API_KEY:-}" \
|
||||
--arg workdir "$ARG_WORKDIR" \
|
||||
'.autoUpdaterStatus = "disabled" |
|
||||
.bypassPermissionsModeAccepted = true |
|
||||
.hasAcknowledgedCostThreshold = true |
|
||||
.hasCompletedOnboarding = true |
|
||||
.primaryApiKey = $apikey |
|
||||
.projects[$workdir].hasCompletedProjectOnboarding = true |
|
||||
.projects[$workdir].hasTrustDialogAccepted = true' \
|
||||
"$claude_config" > "${claude_config}.tmp" && mv "${claude_config}.tmp" "$claude_config"
|
||||
else
|
||||
echo "Creating new Claude configuration at $claude_config"
|
||||
cat > "$claude_config" << EOF
|
||||
{
|
||||
"autoUpdaterStatus": "disabled",
|
||||
"bypassPermissionsModeAccepted": true,
|
||||
"hasAcknowledgedCostThreshold": true,
|
||||
"hasCompletedOnboarding": true,
|
||||
"primaryApiKey": "${CLAUDE_API_KEY:-}",
|
||||
"projects": {
|
||||
"$ARG_WORKDIR": {
|
||||
"hasCompletedProjectOnboarding": true,
|
||||
"hasTrustDialogAccepted": true
|
||||
}
|
||||
}
|
||||
}
|
||||
EOF
|
||||
fi
|
||||
|
||||
echo "Standalone mode configured successfully"
|
||||
}
|
||||
|
||||
function report_tasks() {
|
||||
if [ "$ARG_REPORT_TASKS" = "true" ]; then
|
||||
echo "Configuring Claude Code to report tasks via Coder MCP..."
|
||||
export CODER_MCP_APP_STATUS_SLUG="$ARG_MCP_APP_STATUS_SLUG"
|
||||
export CODER_MCP_AI_AGENTAPI_URL="http://localhost:3284"
|
||||
coder exp mcp configure claude-code "$ARG_WORKDIR"
|
||||
else
|
||||
configure_standalone_mode
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
# If lastSessionId is present in .claude.json, claude --continue will start a
|
||||
# conversation starting from that session. The problem is that lastSessionId
|
||||
# doesn't always point to the last session. The field is updated by claude only
|
||||
# at the point of normal CLI exit. If Claude exits with an error, or if the user
|
||||
# restarts the Coder workspace, lastSessionId will be stale, and claude --continue
|
||||
# will start from an old session.
|
||||
#
|
||||
# If lastSessionId is missing, claude seems to accurately figure out where to
|
||||
# start using the conversation history - even if the CLI previously exited with
|
||||
# an error.
|
||||
#
|
||||
# This script removes the lastSessionId field from .claude.json.
|
||||
if [ $# -eq 0 ]; then
|
||||
echo "No working directory provided - it must be the first argument"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Get absolute path of working directory
|
||||
working_dir=$(realpath "$1")
|
||||
echo "workingDir $working_dir"
|
||||
|
||||
# Path to .claude.json
|
||||
claude_json_path="$HOME/.claude.json"
|
||||
echo ".claude.json path $claude_json_path"
|
||||
|
||||
# Check if .claude.json exists
|
||||
if [ ! -f "$claude_json_path" ]; then
|
||||
echo "No .claude.json file found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Use jq to check if lastSessionId exists for the working directory and remove it
|
||||
|
||||
if jq -e ".projects[\"$working_dir\"].lastSessionId" "$claude_json_path" > /dev/null 2>&1; then
|
||||
# Remove lastSessionId and update the file
|
||||
if jq "del(.projects[\"$working_dir\"].lastSessionId)" "$claude_json_path" > "${claude_json_path}.tmp" && mv "${claude_json_path}.tmp" "$claude_json_path"; then
|
||||
echo "Removed lastSessionId from .claude.json"
|
||||
exit 0
|
||||
else
|
||||
echo "Failed to remove lastSessionId from .claude.json"
|
||||
fi
|
||||
else
|
||||
echo "No lastSessionId found in .claude.json - nothing to do"
|
||||
fi
|
||||
@@ -1,14 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [ -f "$HOME/.bashrc" ]; then
|
||||
source "$HOME"/.bashrc
|
||||
fi
|
||||
|
||||
# Set strict error handling AFTER sourcing bashrc to avoid unbound variable errors from user dotfiles
|
||||
set -euo pipefail
|
||||
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
|
||||
command_exists() {
|
||||
command -v "$1" > /dev/null 2>&1
|
||||
}
|
||||
@@ -51,19 +44,6 @@ printf "ARG_CODER_HOST: %s\n" "$ARG_CODER_HOST"
|
||||
|
||||
echo "--------------------------------"
|
||||
|
||||
# Clean up stale session data (see remove-last-session-id.sh for details)
|
||||
CAN_CONTINUE_CONVERSATION=false
|
||||
set +e
|
||||
bash "/tmp/remove-last-session-id.sh" "$(pwd)" 2> /dev/null
|
||||
session_cleanup_exit_code=$?
|
||||
set -e
|
||||
|
||||
case $session_cleanup_exit_code in
|
||||
0)
|
||||
CAN_CONTINUE_CONVERSATION=true
|
||||
;;
|
||||
esac
|
||||
|
||||
function install_boundary() {
|
||||
if [ "${ARG_COMPILE_FROM_SOURCE:-false}" = "true" ]; then
|
||||
# Install boundary by compiling from source
|
||||
@@ -99,18 +79,85 @@ function validate_claude_installation() {
|
||||
# This ensures all task sessions use a consistent, predictable ID
|
||||
TASK_SESSION_ID="cd32e253-ca16-4fd3-9825-d837e74ae3c2"
|
||||
|
||||
task_session_exists() {
|
||||
get_project_dir() {
|
||||
local workdir_normalized
|
||||
workdir_normalized=$(echo "$ARG_WORKDIR" | tr '/' '-')
|
||||
local project_dir="$HOME/.claude/projects/${workdir_normalized}"
|
||||
echo "$HOME/.claude/projects/${workdir_normalized}"
|
||||
}
|
||||
|
||||
printf "PROJECT_DIR: %s, workdir_normalized: %s\n" "$project_dir" "$workdir_normalized"
|
||||
get_task_session_file() {
|
||||
echo "$(get_project_dir)/${TASK_SESSION_ID}.jsonl"
|
||||
}
|
||||
|
||||
if [ -d "$project_dir" ] && find "$project_dir" -type f -name "*${TASK_SESSION_ID}*" 2> /dev/null | grep -q .; then
|
||||
printf "TASK_SESSION_ID: %s file found\n" "$TASK_SESSION_ID"
|
||||
task_session_exists() {
|
||||
local session_file
|
||||
session_file=$(get_task_session_file)
|
||||
|
||||
if [ -f "$session_file" ]; then
|
||||
printf "Task session file found: %s\n" "$session_file"
|
||||
return 0
|
||||
else
|
||||
printf "TASK_SESSION_ID: %s file not found\n" "$TASK_SESSION_ID"
|
||||
printf "Task session file not found: %s\n" "$session_file"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
is_valid_session() {
|
||||
local session_file="$1"
|
||||
|
||||
# Check if file exists and is not empty
|
||||
# Empty files indicate the session was created but never used so they need to be removed
|
||||
if [ ! -f "$session_file" ]; then
|
||||
printf "Session validation failed: file does not exist\n"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ ! -s "$session_file" ]; then
|
||||
printf "Session validation failed: file is empty, removing stale file\n"
|
||||
rm -f "$session_file"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Check for minimum session content
|
||||
# Valid sessions need at least 2 lines: initial message and first response
|
||||
local line_count
|
||||
line_count=$(wc -l < "$session_file")
|
||||
if [ "$line_count" -lt 2 ]; then
|
||||
printf "Session validation failed: incomplete (only %s lines), removing incomplete file\n" "$line_count"
|
||||
rm -f "$session_file"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Validate JSONL format by checking first 3 lines
|
||||
# Claude session files use JSONL (JSON Lines) format where each line is valid JSON
|
||||
if ! head -3 "$session_file" | jq empty 2> /dev/null; then
|
||||
printf "Session validation failed: invalid JSONL format, removing corrupt file\n"
|
||||
rm -f "$session_file"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Verify the session has a valid sessionId field
|
||||
# This ensures the file structure matches Claude's session format
|
||||
if ! grep -q '"sessionId"' "$session_file" \
|
||||
|| ! grep -m 1 '"sessionId"' "$session_file" | jq -e '.sessionId' > /dev/null 2>&1; then
|
||||
printf "Session validation failed: no valid sessionId found, removing malformed file\n"
|
||||
rm -f "$session_file"
|
||||
return 1
|
||||
fi
|
||||
|
||||
printf "Session validation passed: %s\n" "$session_file"
|
||||
return 0
|
||||
}
|
||||
|
||||
has_any_sessions() {
|
||||
local project_dir
|
||||
project_dir=$(get_project_dir)
|
||||
|
||||
if [ -d "$project_dir" ] && find "$project_dir" -maxdepth 1 -name "*.jsonl" -size +0c 2> /dev/null | grep -q .; then
|
||||
printf "Sessions found in: %s\n" "$project_dir"
|
||||
return 0
|
||||
else
|
||||
printf "No sessions found in: %s\n" "$project_dir"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
@@ -133,75 +180,41 @@ function start_agentapi() {
|
||||
fi
|
||||
|
||||
if [ -n "$ARG_RESUME_SESSION_ID" ]; then
|
||||
echo "Resuming task session by ID: $ARG_RESUME_SESSION_ID"
|
||||
echo "Resuming specified session: $ARG_RESUME_SESSION_ID"
|
||||
ARGS+=(--resume "$ARG_RESUME_SESSION_ID")
|
||||
if [ "$ARG_DANGEROUSLY_SKIP_PERMISSIONS" = "true" ]; then
|
||||
ARGS+=(--dangerously-skip-permissions)
|
||||
fi
|
||||
[ "$ARG_DANGEROUSLY_SKIP_PERMISSIONS" = "true" ] && ARGS+=(--dangerously-skip-permissions)
|
||||
|
||||
elif [ "$ARG_CONTINUE" = "true" ]; then
|
||||
if [ "$ARG_REPORT_TASKS" = "true" ] && task_session_exists; then
|
||||
echo "Task session detected (ID: $TASK_SESSION_ID)"
|
||||
ARGS+=(--resume "$TASK_SESSION_ID")
|
||||
ARGS+=(--dangerously-skip-permissions)
|
||||
echo "Resuming existing task session"
|
||||
elif [ "$ARG_REPORT_TASKS" = "false" ] && [ "$CAN_CONTINUE_CONVERSATION" = true ]; then
|
||||
echo "Previous session exists"
|
||||
ARGS+=(--continue)
|
||||
if [ "$ARG_DANGEROUSLY_SKIP_PERMISSIONS" = "true" ]; then
|
||||
ARGS+=(--dangerously-skip-permissions)
|
||||
fi
|
||||
echo "Resuming existing session"
|
||||
else
|
||||
echo "No existing session found"
|
||||
if [ "$ARG_REPORT_TASKS" = "true" ]; then
|
||||
if task_session_exists; then
|
||||
ARGS+=(--resume "$TASK_SESSION_ID")
|
||||
else
|
||||
ARGS+=(--session-id "$TASK_SESSION_ID")
|
||||
fi
|
||||
fi
|
||||
if [ -n "$ARG_AI_PROMPT" ]; then
|
||||
if [ "$ARG_REPORT_TASKS" = "true" ]; then
|
||||
ARGS+=(--dangerously-skip-permissions -- "$ARG_AI_PROMPT")
|
||||
else
|
||||
if [ "$ARG_DANGEROUSLY_SKIP_PERMISSIONS" = "true" ]; then
|
||||
ARGS+=(--dangerously-skip-permissions)
|
||||
fi
|
||||
ARGS+=(-- "$ARG_AI_PROMPT")
|
||||
fi
|
||||
echo "Starting new session with prompt"
|
||||
|
||||
if [ "$ARG_REPORT_TASKS" = "true" ]; then
|
||||
local session_file
|
||||
session_file=$(get_task_session_file)
|
||||
|
||||
if task_session_exists && is_valid_session "$session_file"; then
|
||||
echo "Resuming task session: $TASK_SESSION_ID"
|
||||
ARGS+=(--resume "$TASK_SESSION_ID" --dangerously-skip-permissions)
|
||||
else
|
||||
if [ "$ARG_REPORT_TASKS" = "true" ] || [ "$ARG_DANGEROUSLY_SKIP_PERMISSIONS" = "true" ]; then
|
||||
ARGS+=(--dangerously-skip-permissions)
|
||||
fi
|
||||
echo "Starting new session"
|
||||
echo "Starting new task session: $TASK_SESSION_ID"
|
||||
ARGS+=(--session-id "$TASK_SESSION_ID" --dangerously-skip-permissions)
|
||||
[ -n "$ARG_AI_PROMPT" ] && ARGS+=(-- "$ARG_AI_PROMPT")
|
||||
fi
|
||||
|
||||
else
|
||||
if has_any_sessions; then
|
||||
echo "Continuing most recent standalone session"
|
||||
ARGS+=(--continue)
|
||||
[ "$ARG_DANGEROUSLY_SKIP_PERMISSIONS" = "true" ] && ARGS+=(--dangerously-skip-permissions)
|
||||
else
|
||||
echo "No sessions found, starting fresh standalone session"
|
||||
[ "$ARG_DANGEROUSLY_SKIP_PERMISSIONS" = "true" ] && ARGS+=(--dangerously-skip-permissions)
|
||||
[ -n "$ARG_AI_PROMPT" ] && ARGS+=(-- "$ARG_AI_PROMPT")
|
||||
fi
|
||||
fi
|
||||
|
||||
else
|
||||
echo "Continue disabled, starting fresh session"
|
||||
if [ "$ARG_REPORT_TASKS" = "true" ]; then
|
||||
if task_session_exists; then
|
||||
ARGS+=(--resume "$TASK_SESSION_ID")
|
||||
else
|
||||
ARGS+=(--session-id "$TASK_SESSION_ID")
|
||||
fi
|
||||
fi
|
||||
if [ -n "$ARG_AI_PROMPT" ]; then
|
||||
if [ "$ARG_REPORT_TASKS" = "true" ]; then
|
||||
ARGS+=(--dangerously-skip-permissions -- "$ARG_AI_PROMPT")
|
||||
else
|
||||
if [ "$ARG_DANGEROUSLY_SKIP_PERMISSIONS" = "true" ]; then
|
||||
ARGS+=(--dangerously-skip-permissions)
|
||||
fi
|
||||
ARGS+=(-- "$ARG_AI_PROMPT")
|
||||
fi
|
||||
echo "Starting new session with prompt"
|
||||
else
|
||||
if [ "$ARG_REPORT_TASKS" = "true" ] || [ "$ARG_DANGEROUSLY_SKIP_PERMISSIONS" = "true" ]; then
|
||||
ARGS+=(--dangerously-skip-permissions)
|
||||
fi
|
||||
echo "Starting claude code session"
|
||||
fi
|
||||
[ "$ARG_DANGEROUSLY_SKIP_PERMISSIONS" = "true" ] && ARGS+=(--dangerously-skip-permissions)
|
||||
[ -n "$ARG_AI_PROMPT" ] && ARGS+=(-- "$ARG_AI_PROMPT")
|
||||
fi
|
||||
|
||||
printf "Running claude code with args: %s\n" "$(printf '%q ' "${ARGS[@]}")"
|
||||
|
||||
@@ -14,7 +14,7 @@ This module allows you to automatically clone a repository by URL and skip if it
|
||||
module "git-clone" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/git-clone/coder"
|
||||
version = "1.2.2"
|
||||
version = "1.2.3"
|
||||
agent_id = coder_agent.example.id
|
||||
url = "https://github.com/coder/coder"
|
||||
}
|
||||
@@ -28,7 +28,7 @@ module "git-clone" {
|
||||
module "git-clone" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/git-clone/coder"
|
||||
version = "1.2.2"
|
||||
version = "1.2.3"
|
||||
agent_id = coder_agent.example.id
|
||||
url = "https://github.com/coder/coder"
|
||||
base_dir = "~/projects/coder"
|
||||
@@ -43,11 +43,12 @@ To use with [Git Authentication](https://coder.com/docs/v2/latest/admin/git-prov
|
||||
module "git-clone" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/git-clone/coder"
|
||||
version = "1.2.2"
|
||||
version = "1.2.3"
|
||||
agent_id = coder_agent.example.id
|
||||
url = "https://github.com/coder/coder"
|
||||
}
|
||||
|
||||
|
||||
data "coder_external_auth" "github" {
|
||||
id = "github"
|
||||
}
|
||||
@@ -69,11 +70,12 @@ data "coder_parameter" "git_repo" {
|
||||
module "git_clone" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/git-clone/coder"
|
||||
version = "1.2.2"
|
||||
version = "1.2.3"
|
||||
agent_id = coder_agent.example.id
|
||||
url = data.coder_parameter.git_repo.value
|
||||
}
|
||||
|
||||
|
||||
# Create a code-server instance for the cloned repository
|
||||
module "code-server" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
@@ -103,13 +105,14 @@ Configuring `git-clone` for a self-hosted GitHub Enterprise Server running at `g
|
||||
module "git-clone" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/git-clone/coder"
|
||||
version = "1.2.2"
|
||||
version = "1.2.3"
|
||||
agent_id = coder_agent.example.id
|
||||
url = "https://github.example.com/coder/coder/tree/feat/example"
|
||||
git_providers = {
|
||||
"https://github.example.com/" = {
|
||||
provider = "github"
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -122,7 +125,7 @@ To GitLab clone with a specific branch like `feat/example`
|
||||
module "git-clone" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/git-clone/coder"
|
||||
version = "1.2.2"
|
||||
version = "1.2.3"
|
||||
agent_id = coder_agent.example.id
|
||||
url = "https://gitlab.com/coder/coder/-/tree/feat/example"
|
||||
}
|
||||
@@ -134,13 +137,14 @@ Configuring `git-clone` for a self-hosted GitLab running at `gitlab.example.com`
|
||||
module "git-clone" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/git-clone/coder"
|
||||
version = "1.2.2"
|
||||
version = "1.2.3"
|
||||
agent_id = coder_agent.example.id
|
||||
url = "https://gitlab.example.com/coder/coder/-/tree/feat/example"
|
||||
git_providers = {
|
||||
"https://gitlab.example.com/" = {
|
||||
provider = "gitlab"
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -155,7 +159,7 @@ For example, to clone the `feat/example` branch:
|
||||
module "git-clone" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/git-clone/coder"
|
||||
version = "1.2.2"
|
||||
version = "1.2.3"
|
||||
agent_id = coder_agent.example.id
|
||||
url = "https://github.com/coder/coder"
|
||||
branch_name = "feat/example"
|
||||
@@ -173,7 +177,7 @@ For example, this will clone into the `~/projects/coder/coder-dev` folder:
|
||||
module "git-clone" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/git-clone/coder"
|
||||
version = "1.2.2"
|
||||
version = "1.2.3"
|
||||
agent_id = coder_agent.example.id
|
||||
url = "https://github.com/coder/coder"
|
||||
folder_name = "coder-dev"
|
||||
@@ -191,8 +195,8 @@ If not defined, the default, `0`, performs a full clone.
|
||||
```tf
|
||||
module "git-clone" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/git-clone/coder"
|
||||
version = "1.2.2"
|
||||
source = "registry.coder.com/coder/git-clone/coder"
|
||||
version = "1.2.3"
|
||||
agent_id = coder_agent.example.id
|
||||
url = "https://github.com/coder/coder"
|
||||
depth = 1
|
||||
@@ -208,7 +212,7 @@ This is useful for running initialization tasks like installing dependencies or
|
||||
module "git-clone" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/git-clone/coder"
|
||||
version = "1.2.2"
|
||||
version = "1.2.3"
|
||||
agent_id = coder_agent.example.id
|
||||
url = "https://github.com/coder/coder"
|
||||
post_clone_script = <<-EOT
|
||||
|
||||
@@ -58,9 +58,10 @@ fi
|
||||
# Run post-clone script if provided
|
||||
if [ -n "$POST_CLONE_SCRIPT" ]; then
|
||||
echo "Running post-clone script..."
|
||||
echo "$POST_CLONE_SCRIPT" | base64 -d > /tmp/post_clone.sh
|
||||
chmod +x /tmp/post_clone.sh
|
||||
POST_CLONE_TMP=$(mktemp)
|
||||
echo "$POST_CLONE_SCRIPT" | base64 -d > "$POST_CLONE_TMP"
|
||||
chmod +x "$POST_CLONE_TMP"
|
||||
cd "$CLONE_PATH" || exit
|
||||
/tmp/post_clone.sh
|
||||
rm /tmp/post_clone.sh
|
||||
$POST_CLONE_TMP
|
||||
rm "$POST_CLONE_TMP"
|
||||
fi
|
||||
|
||||
@@ -16,7 +16,7 @@ Install the JF CLI and authenticate package managers with Artifactory using OAut
|
||||
module "jfrog" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/jfrog-oauth/coder"
|
||||
version = "1.2.3"
|
||||
version = "1.2.4"
|
||||
agent_id = coder_agent.main.id
|
||||
jfrog_url = "https://example.jfrog.io"
|
||||
username_field = "username" # If you are using GitHub to login to both Coder and Artifactory, use username_field = "username"
|
||||
@@ -57,7 +57,7 @@ Configure the Python pip package manager to fetch packages from Artifactory whil
|
||||
module "jfrog" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/jfrog-oauth/coder"
|
||||
version = "1.2.3"
|
||||
version = "1.2.4"
|
||||
agent_id = coder_agent.main.id
|
||||
jfrog_url = "https://example.jfrog.io"
|
||||
username_field = "email"
|
||||
|
||||
@@ -0,0 +1,400 @@
|
||||
# Test for jfrog-oauth module
|
||||
|
||||
run "test_required_vars" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-id"
|
||||
jfrog_url = "https://example.jfrog.io"
|
||||
package_managers = {}
|
||||
}
|
||||
|
||||
# Mock external auth with valid access token for basic test
|
||||
override_data {
|
||||
target = data.coder_external_auth.jfrog
|
||||
values = {
|
||||
access_token = "valid-token-value"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
run "test_empty_access_token_fails" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-id"
|
||||
jfrog_url = "https://example.jfrog.io"
|
||||
package_managers = {}
|
||||
}
|
||||
|
||||
# Mock external auth with empty access token
|
||||
override_data {
|
||||
target = data.coder_external_auth.jfrog
|
||||
values = {
|
||||
access_token = ""
|
||||
}
|
||||
}
|
||||
|
||||
expect_failures = [
|
||||
resource.coder_script.jfrog
|
||||
]
|
||||
}
|
||||
|
||||
run "test_valid_access_token_succeeds" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-id"
|
||||
jfrog_url = "https://example.jfrog.io"
|
||||
package_managers = {}
|
||||
}
|
||||
|
||||
# Mock external auth with valid access token
|
||||
override_data {
|
||||
target = data.coder_external_auth.jfrog
|
||||
values = {
|
||||
access_token = "valid-token-value"
|
||||
}
|
||||
}
|
||||
|
||||
# Verify the script resource is created
|
||||
assert {
|
||||
condition = resource.coder_script.jfrog.agent_id == "test-agent-id"
|
||||
error_message = "coder_script agent_id should match the input variable"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_script.jfrog.display_name == "jfrog"
|
||||
error_message = "coder_script display_name should be 'jfrog'"
|
||||
}
|
||||
}
|
||||
|
||||
run "test_jfrog_url_validation" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-id"
|
||||
jfrog_url = "invalid-url"
|
||||
package_managers = {}
|
||||
}
|
||||
|
||||
override_data {
|
||||
target = data.coder_external_auth.jfrog
|
||||
values = {
|
||||
access_token = "valid-token-value"
|
||||
}
|
||||
}
|
||||
|
||||
expect_failures = [
|
||||
var.jfrog_url
|
||||
]
|
||||
}
|
||||
|
||||
run "test_username_field_validation" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-id"
|
||||
jfrog_url = "https://example.jfrog.io"
|
||||
username_field = "invalid"
|
||||
package_managers = {}
|
||||
}
|
||||
|
||||
override_data {
|
||||
target = data.coder_external_auth.jfrog
|
||||
values = {
|
||||
access_token = "valid-token-value"
|
||||
}
|
||||
}
|
||||
|
||||
expect_failures = [
|
||||
var.username_field
|
||||
]
|
||||
}
|
||||
|
||||
run "test_with_npm_package_manager" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-id"
|
||||
jfrog_url = "https://example.jfrog.io"
|
||||
package_managers = {
|
||||
npm = ["global", "@foo:foo", "@bar:bar"]
|
||||
}
|
||||
}
|
||||
|
||||
override_data {
|
||||
target = data.coder_external_auth.jfrog
|
||||
values = {
|
||||
access_token = "valid-token-value"
|
||||
}
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_script.jfrog.run_on_start == true
|
||||
error_message = "coder_script should run on start"
|
||||
}
|
||||
|
||||
# Verify npm configuration is in script
|
||||
assert {
|
||||
condition = strcontains(resource.coder_script.jfrog.script, "jf npmc --global --repo-resolve \"global\"")
|
||||
error_message = "script should contain jf npmc command for npm"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = strcontains(resource.coder_script.jfrog.script, "@foo:registry=https://example.jfrog.io/artifactory/api/npm/foo")
|
||||
error_message = "script should contain scoped npm registry for @foo"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = strcontains(resource.coder_script.jfrog.script, "@bar:registry=https://example.jfrog.io/artifactory/api/npm/bar")
|
||||
error_message = "script should contain scoped npm registry for @bar"
|
||||
}
|
||||
}
|
||||
|
||||
run "test_configure_code_server" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-id"
|
||||
jfrog_url = "https://example.jfrog.io"
|
||||
configure_code_server = true
|
||||
package_managers = {}
|
||||
}
|
||||
|
||||
override_data {
|
||||
target = data.coder_external_auth.jfrog
|
||||
values = {
|
||||
access_token = "valid-token-value"
|
||||
}
|
||||
}
|
||||
|
||||
# When configure_code_server is true, env vars should be created
|
||||
assert {
|
||||
condition = length(resource.coder_env.jfrog_ide_url) == 1
|
||||
error_message = "coder_env.jfrog_ide_url should be created when configure_code_server is true"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = length(resource.coder_env.jfrog_ide_access_token) == 1
|
||||
error_message = "coder_env.jfrog_ide_access_token should be created when configure_code_server is true"
|
||||
}
|
||||
}
|
||||
|
||||
run "test_go_proxy_env" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-id"
|
||||
jfrog_url = "https://example.jfrog.io"
|
||||
package_managers = {
|
||||
go = ["foo", "bar", "baz"]
|
||||
}
|
||||
}
|
||||
|
||||
override_data {
|
||||
target = data.coder_external_auth.jfrog
|
||||
values = {
|
||||
access_token = "valid-token-value"
|
||||
}
|
||||
}
|
||||
|
||||
# When go package manager is configured, GOPROXY env should be set
|
||||
assert {
|
||||
condition = length(resource.coder_env.goproxy) == 1
|
||||
error_message = "coder_env.goproxy should be created when go package manager is configured"
|
||||
}
|
||||
|
||||
# Verify GOPROXY contains all repos
|
||||
assert {
|
||||
condition = strcontains(resource.coder_env.goproxy[0].value, "example.jfrog.io/artifactory/api/go/foo")
|
||||
error_message = "GOPROXY should contain foo repo"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = strcontains(resource.coder_env.goproxy[0].value, "example.jfrog.io/artifactory/api/go/bar")
|
||||
error_message = "GOPROXY should contain bar repo"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = strcontains(resource.coder_env.goproxy[0].value, "example.jfrog.io/artifactory/api/go/baz")
|
||||
error_message = "GOPROXY should contain baz repo"
|
||||
}
|
||||
|
||||
# Verify script contains go configuration
|
||||
assert {
|
||||
condition = strcontains(resource.coder_script.jfrog.script, "jf goc --global --repo-resolve \"foo\"")
|
||||
error_message = "script should contain jf goc command"
|
||||
}
|
||||
}
|
||||
|
||||
run "test_pypi_package_manager" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-id"
|
||||
jfrog_url = "https://example.jfrog.io"
|
||||
package_managers = {
|
||||
pypi = ["global", "foo", "bar"]
|
||||
}
|
||||
}
|
||||
|
||||
override_data {
|
||||
target = data.coder_external_auth.jfrog
|
||||
values = {
|
||||
access_token = "valid-token-value"
|
||||
}
|
||||
}
|
||||
|
||||
# Verify pip configuration in script
|
||||
assert {
|
||||
condition = strcontains(resource.coder_script.jfrog.script, "jf pipc --global --repo-resolve \"global\"")
|
||||
error_message = "script should contain jf pipc command"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = strcontains(resource.coder_script.jfrog.script, "index-url = https://default:valid-token-value@example.jfrog.io/artifactory/api/pypi/global/simple")
|
||||
error_message = "script should contain pip index-url configuration"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = strcontains(resource.coder_script.jfrog.script, "extra-index-url")
|
||||
error_message = "script should contain extra-index-url for additional repos"
|
||||
}
|
||||
}
|
||||
|
||||
run "test_docker_package_manager" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-id"
|
||||
jfrog_url = "https://example.jfrog.io"
|
||||
package_managers = {
|
||||
docker = ["foo.jfrog.io", "bar.jfrog.io", "baz.jfrog.io"]
|
||||
}
|
||||
}
|
||||
|
||||
override_data {
|
||||
target = data.coder_external_auth.jfrog
|
||||
values = {
|
||||
access_token = "valid-token-value"
|
||||
}
|
||||
}
|
||||
|
||||
# Verify docker registration commands in script
|
||||
assert {
|
||||
condition = strcontains(resource.coder_script.jfrog.script, "register_docker \"foo.jfrog.io\"")
|
||||
error_message = "script should contain register_docker for foo.jfrog.io"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = strcontains(resource.coder_script.jfrog.script, "register_docker \"bar.jfrog.io\"")
|
||||
error_message = "script should contain register_docker for bar.jfrog.io"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = strcontains(resource.coder_script.jfrog.script, "register_docker \"baz.jfrog.io\"")
|
||||
error_message = "script should contain register_docker for baz.jfrog.io"
|
||||
}
|
||||
}
|
||||
|
||||
run "test_conda_package_manager" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-id"
|
||||
jfrog_url = "https://example.jfrog.io"
|
||||
package_managers = {
|
||||
conda = ["conda-main", "conda-secondary", "conda-local"]
|
||||
}
|
||||
}
|
||||
|
||||
override_data {
|
||||
target = data.coder_external_auth.jfrog
|
||||
values = {
|
||||
access_token = "valid-token-value"
|
||||
}
|
||||
}
|
||||
|
||||
# Verify conda configuration in script
|
||||
assert {
|
||||
condition = strcontains(resource.coder_script.jfrog.script, "channels:")
|
||||
error_message = "script should contain conda channels configuration"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = strcontains(resource.coder_script.jfrog.script, "example.jfrog.io/artifactory/api/conda/conda-main")
|
||||
error_message = "script should contain conda-main channel"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = strcontains(resource.coder_script.jfrog.script, "example.jfrog.io/artifactory/api/conda/conda-secondary")
|
||||
error_message = "script should contain conda-secondary channel"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = strcontains(resource.coder_script.jfrog.script, "example.jfrog.io/artifactory/api/conda/conda-local")
|
||||
error_message = "script should contain conda-local channel"
|
||||
}
|
||||
}
|
||||
|
||||
run "test_maven_package_manager" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-id"
|
||||
jfrog_url = "https://example.jfrog.io"
|
||||
package_managers = {
|
||||
maven = ["central", "snapshots", "local"]
|
||||
}
|
||||
}
|
||||
|
||||
override_data {
|
||||
target = data.coder_external_auth.jfrog
|
||||
values = {
|
||||
access_token = "valid-token-value"
|
||||
}
|
||||
}
|
||||
|
||||
# Verify maven jf mvnc command
|
||||
assert {
|
||||
condition = strcontains(resource.coder_script.jfrog.script, "jf mvnc --global")
|
||||
error_message = "script should contain jf mvnc command"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = strcontains(resource.coder_script.jfrog.script, "--repo-resolve-releases \"central\"")
|
||||
error_message = "script should contain repo-resolve-releases for central"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = strcontains(resource.coder_script.jfrog.script, "--repo-resolve-snapshots \"central\"")
|
||||
error_message = "script should contain repo-resolve-snapshots for central"
|
||||
}
|
||||
|
||||
# Verify settings.xml content
|
||||
assert {
|
||||
condition = strcontains(resource.coder_script.jfrog.script, "<servers>")
|
||||
error_message = "script should contain maven servers configuration"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = strcontains(resource.coder_script.jfrog.script, "<id>central</id>")
|
||||
error_message = "script should contain central server id"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = strcontains(resource.coder_script.jfrog.script, "<id>snapshots</id>")
|
||||
error_message = "script should contain snapshots server id"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = strcontains(resource.coder_script.jfrog.script, "<id>local</id>")
|
||||
error_message = "script should contain local server id"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = strcontains(resource.coder_script.jfrog.script, "<url>https://example.jfrog.io/artifactory/central</url>")
|
||||
error_message = "script should contain central repository URL"
|
||||
}
|
||||
}
|
||||
@@ -1,189 +0,0 @@
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import {
|
||||
findResourceInstance,
|
||||
runTerraformInit,
|
||||
runTerraformApply,
|
||||
testRequiredVariables,
|
||||
} from "~test";
|
||||
|
||||
describe("jfrog-oauth", async () => {
|
||||
type TestVariables = {
|
||||
agent_id: string;
|
||||
jfrog_url: string;
|
||||
package_managers: string;
|
||||
|
||||
username_field?: string;
|
||||
jfrog_server_id?: string;
|
||||
external_auth_id?: string;
|
||||
configure_code_server?: boolean;
|
||||
};
|
||||
|
||||
await runTerraformInit(import.meta.dir);
|
||||
|
||||
const fakeFrogApi = "localhost:8081/artifactory/api";
|
||||
const fakeFrogUrl = "http://localhost:8081";
|
||||
const user = "default";
|
||||
|
||||
testRequiredVariables<TestVariables>(import.meta.dir, {
|
||||
agent_id: "some-agent-id",
|
||||
jfrog_url: fakeFrogUrl,
|
||||
package_managers: "{}",
|
||||
});
|
||||
|
||||
it("generates an npmrc with scoped repos", async () => {
|
||||
const state = await runTerraformApply<TestVariables>(import.meta.dir, {
|
||||
agent_id: "some-agent-id",
|
||||
jfrog_url: fakeFrogUrl,
|
||||
package_managers: JSON.stringify({
|
||||
npm: ["global", "@foo:foo", "@bar:bar"],
|
||||
}),
|
||||
});
|
||||
const coderScript = findResourceInstance(state, "coder_script");
|
||||
const npmrcStanza = `cat << EOF > ~/.npmrc
|
||||
email=${user}@example.com
|
||||
registry=http://${fakeFrogApi}/npm/global
|
||||
//${fakeFrogApi}/npm/global/:_authToken=
|
||||
@foo:registry=http://${fakeFrogApi}/npm/foo
|
||||
//${fakeFrogApi}/npm/foo/:_authToken=
|
||||
@bar:registry=http://${fakeFrogApi}/npm/bar
|
||||
//${fakeFrogApi}/npm/bar/:_authToken=
|
||||
|
||||
EOF`;
|
||||
expect(coderScript.script).toContain(npmrcStanza);
|
||||
expect(coderScript.script).toContain(
|
||||
'jf npmc --global --repo-resolve "global"',
|
||||
);
|
||||
expect(coderScript.script).toContain(
|
||||
'if [ -z "YES" ]; then\n not_configured npm',
|
||||
);
|
||||
});
|
||||
|
||||
it("generates a pip config with extra-indexes", async () => {
|
||||
const state = await runTerraformApply<TestVariables>(import.meta.dir, {
|
||||
agent_id: "some-agent-id",
|
||||
jfrog_url: fakeFrogUrl,
|
||||
package_managers: JSON.stringify({
|
||||
pypi: ["global", "foo", "bar"],
|
||||
}),
|
||||
});
|
||||
const coderScript = findResourceInstance(state, "coder_script");
|
||||
const pipStanza = `cat << EOF > ~/.pip/pip.conf
|
||||
[global]
|
||||
index-url = https://${user}:@${fakeFrogApi}/pypi/global/simple
|
||||
extra-index-url =
|
||||
https://${user}:@${fakeFrogApi}/pypi/foo/simple
|
||||
https://${user}:@${fakeFrogApi}/pypi/bar/simple
|
||||
|
||||
EOF`;
|
||||
expect(coderScript.script).toContain(pipStanza);
|
||||
expect(coderScript.script).toContain(
|
||||
'jf pipc --global --repo-resolve "global"',
|
||||
);
|
||||
expect(coderScript.script).toContain(
|
||||
'if [ -z "YES" ]; then\n not_configured pypi',
|
||||
);
|
||||
});
|
||||
|
||||
it("registers multiple docker repos", async () => {
|
||||
const state = await runTerraformApply<TestVariables>(import.meta.dir, {
|
||||
agent_id: "some-agent-id",
|
||||
jfrog_url: fakeFrogUrl,
|
||||
package_managers: JSON.stringify({
|
||||
docker: ["foo.jfrog.io", "bar.jfrog.io", "baz.jfrog.io"],
|
||||
}),
|
||||
});
|
||||
const coderScript = findResourceInstance(state, "coder_script");
|
||||
const dockerStanza = ["foo", "bar", "baz"]
|
||||
.map((r) => `register_docker "${r}.jfrog.io"`)
|
||||
.join("\n");
|
||||
expect(coderScript.script).toContain(dockerStanza);
|
||||
expect(coderScript.script).toContain(
|
||||
'if [ -z "YES" ]; then\n not_configured docker',
|
||||
);
|
||||
});
|
||||
|
||||
it("sets goproxy with multiple repos", async () => {
|
||||
const state = await runTerraformApply<TestVariables>(import.meta.dir, {
|
||||
agent_id: "some-agent-id",
|
||||
jfrog_url: fakeFrogUrl,
|
||||
package_managers: JSON.stringify({
|
||||
go: ["foo", "bar", "baz"],
|
||||
}),
|
||||
});
|
||||
const proxyEnv = findResourceInstance(state, "coder_env", "goproxy");
|
||||
const proxies = ["foo", "bar", "baz"]
|
||||
.map((r) => `https://${user}:@${fakeFrogApi}/go/${r}`)
|
||||
.join(",");
|
||||
expect(proxyEnv.value).toEqual(proxies);
|
||||
|
||||
const coderScript = findResourceInstance(state, "coder_script");
|
||||
expect(coderScript.script).toContain(
|
||||
'jf goc --global --repo-resolve "foo"',
|
||||
);
|
||||
expect(coderScript.script).toContain(
|
||||
'if [ -z "YES" ]; then\n not_configured go',
|
||||
);
|
||||
});
|
||||
|
||||
it("generates a conda config with multiple repos", async () => {
|
||||
const state = await runTerraformApply<TestVariables>(import.meta.dir, {
|
||||
agent_id: "some-agent-id",
|
||||
jfrog_url: fakeFrogUrl,
|
||||
package_managers: JSON.stringify({
|
||||
conda: ["conda-main", "conda-secondary", "conda-local"],
|
||||
}),
|
||||
});
|
||||
const coderScript = findResourceInstance(state, "coder_script");
|
||||
const condaStanza = `cat << EOF > ~/.condarc
|
||||
channels:
|
||||
- https://${user}:@${fakeFrogApi}/conda/conda-main
|
||||
- https://${user}:@${fakeFrogApi}/conda/conda-secondary
|
||||
- https://${user}:@${fakeFrogApi}/conda/conda-local
|
||||
- defaults
|
||||
ssl_verify: true
|
||||
|
||||
EOF`;
|
||||
expect(coderScript.script).toContain(condaStanza);
|
||||
expect(coderScript.script).toContain(
|
||||
'if [ -z "YES" ]; then\n not_configured conda',
|
||||
);
|
||||
});
|
||||
it("generates a maven settings.xml with multiple repos", async () => {
|
||||
const state = await runTerraformApply<TestVariables>(import.meta.dir, {
|
||||
agent_id: "some-agent-id",
|
||||
jfrog_url: fakeFrogUrl,
|
||||
package_managers: JSON.stringify({
|
||||
maven: ["central", "snapshots", "local"],
|
||||
}),
|
||||
});
|
||||
|
||||
const coderScript = findResourceInstance(state, "coder_script");
|
||||
|
||||
expect(coderScript.script).toContain("jf mvnc --global");
|
||||
expect(coderScript.script).toContain('--server-id-resolve="0"');
|
||||
expect(coderScript.script).toContain('--repo-resolve-releases "central"');
|
||||
expect(coderScript.script).toContain('--repo-resolve-snapshots "central"');
|
||||
expect(coderScript.script).toContain('--server-id-deploy="0"');
|
||||
expect(coderScript.script).toContain('--repo-deploy-releases "central"');
|
||||
expect(coderScript.script).toContain('--repo-deploy-snapshots "central"');
|
||||
|
||||
expect(coderScript.script).toContain("<servers>");
|
||||
expect(coderScript.script).toContain("<id>central</id>");
|
||||
expect(coderScript.script).toContain("<id>snapshots</id>");
|
||||
expect(coderScript.script).toContain("<id>local</id>");
|
||||
|
||||
expect(coderScript.script).toContain(
|
||||
"<url>http://localhost:8081/artifactory/central</url>",
|
||||
);
|
||||
expect(coderScript.script).toContain(
|
||||
"<url>http://localhost:8081/artifactory/snapshots</url>",
|
||||
);
|
||||
expect(coderScript.script).toContain(
|
||||
"<url>http://localhost:8081/artifactory/local</url>",
|
||||
);
|
||||
|
||||
expect(coderScript.script).toContain(
|
||||
'if [ -z "YES" ]; then\n not_configured maven',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -163,6 +163,13 @@ resource "coder_script" "jfrog" {
|
||||
}
|
||||
))
|
||||
run_on_start = true
|
||||
|
||||
lifecycle {
|
||||
precondition {
|
||||
condition = data.coder_external_auth.jfrog.access_token != ""
|
||||
error_message = "JFrog access token is empty. Please authenticate with JFrog using external auth."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resource "coder_env" "jfrog_ide_url" {
|
||||
|
||||
@@ -14,7 +14,7 @@ Automatically install [KasmVNC](https://kasmweb.com/kasmvnc) in a workspace, and
|
||||
module "kasmvnc" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/kasmvnc/coder"
|
||||
version = "1.2.6"
|
||||
version = "1.2.7"
|
||||
agent_id = coder_agent.example.id
|
||||
desktop_environment = "xfce"
|
||||
subdomain = true
|
||||
|
||||
@@ -31,7 +31,7 @@ variable "desktop_environment" {
|
||||
description = "Specifies the desktop environment of the workspace. This should be pre-installed on the workspace."
|
||||
|
||||
validation {
|
||||
condition = contains(["xfce", "kde", "gnome", "lxde", "lxqt"], var.desktop_environment)
|
||||
condition = contains(["cinnamon", "mate", "lxde", "lxqt", "kde", "gnome", "xfce", "manual"], var.desktop_environment)
|
||||
error_message = "Invalid desktop environment. Please specify a valid desktop environment."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,23 +2,25 @@
|
||||
display_name: mux
|
||||
description: Coding Agent Multiplexer - Run multiple AI agents in parallel
|
||||
icon: ../../../../.icons/mux.svg
|
||||
verified: false
|
||||
verified: true
|
||||
tags: [ai, agents, development, multiplexer]
|
||||
---
|
||||
|
||||
# mux
|
||||
|
||||
Automatically install and run mux in a Coder workspace. By default, the module installs `mux@next` from npm (with a fallback to downloading the npm tarball if npm is unavailable). mux is a desktop application for parallel agentic development that enables developers to run multiple AI agents simultaneously across isolated workspaces.
|
||||
Automatically install and run [mux](https://github.com/coder/mux) in a Coder workspace. By default, the module installs `mux@next` from npm (with a fallback to downloading the npm tarball if npm is unavailable). mux is a desktop application for parallel agentic development that enables developers to run multiple AI agents simultaneously across isolated workspaces.
|
||||
|
||||
```tf
|
||||
module "mux" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/mux/coder"
|
||||
version = "1.0.2"
|
||||
version = "1.0.5"
|
||||
agent_id = coder_agent.main.id
|
||||
}
|
||||
```
|
||||
|
||||

|
||||
|
||||
## Features
|
||||
|
||||
- **Parallel Agent Execution**: Run multiple AI agents simultaneously on different tasks
|
||||
@@ -35,7 +37,7 @@ module "mux" {
|
||||
module "mux" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/mux/coder"
|
||||
version = "1.0.2"
|
||||
version = "1.0.5"
|
||||
agent_id = coder_agent.main.id
|
||||
}
|
||||
```
|
||||
@@ -46,7 +48,7 @@ module "mux" {
|
||||
module "mux" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/mux/coder"
|
||||
version = "1.0.2"
|
||||
version = "1.0.5"
|
||||
agent_id = coder_agent.main.id
|
||||
# Default is "latest"; set to a specific version to pin
|
||||
install_version = "0.4.0"
|
||||
@@ -59,7 +61,7 @@ module "mux" {
|
||||
module "mux" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/mux/coder"
|
||||
version = "1.0.2"
|
||||
version = "1.0.5"
|
||||
agent_id = coder_agent.main.id
|
||||
port = 8080
|
||||
}
|
||||
@@ -73,7 +75,7 @@ Run an existing copy of mux if found, otherwise install from npm:
|
||||
module "mux" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/mux/coder"
|
||||
version = "1.0.2"
|
||||
version = "1.0.5"
|
||||
agent_id = coder_agent.main.id
|
||||
use_cached = true
|
||||
}
|
||||
@@ -87,7 +89,7 @@ Run without installing from the network (requires mux to be pre-installed):
|
||||
module "mux" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/mux/coder"
|
||||
version = "1.0.2"
|
||||
version = "1.0.5"
|
||||
agent_id = coder_agent.main.id
|
||||
install = false
|
||||
}
|
||||
|
||||
@@ -5,6 +5,9 @@ RESET='\033[0m'
|
||||
MUX_BINARY="${INSTALL_PREFIX}/mux"
|
||||
|
||||
function run_mux() {
|
||||
# Remove stale server lock if present
|
||||
rm -f "$HOME/.mux/server.lock"
|
||||
|
||||
local port_value
|
||||
port_value="${PORT}"
|
||||
if [ -z "$port_value" ]; then
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
---
|
||||
display_name: Vault CLI
|
||||
description: Installs the Hashicorp Vault CLI and optionally configures token authentication
|
||||
icon: ../../../../.icons/vault.svg
|
||||
verified: true
|
||||
tags: [helper, integration, vault, cli]
|
||||
---
|
||||
|
||||
# Vault CLI
|
||||
|
||||
Installs the [Vault](https://www.vaultproject.io/) CLI and optionally configures token authentication. This module focuses on CLI installation and can be used standalone or as a base for other authentication methods.
|
||||
|
||||
```tf
|
||||
module "vault_cli" {
|
||||
source = "registry.coder.com/coder/vault-cli/coder"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
vault_addr = "https://vault.example.com"
|
||||
}
|
||||
```
|
||||
|
||||
## Prerequisites
|
||||
|
||||
The following tools are required in the workspace image:
|
||||
|
||||
- **HTTP client**: `curl`, `wget`, or `busybox` (at least one)
|
||||
- **Archive utility**: `unzip` or `busybox` (at least one)
|
||||
- **jq**: Optional but recommended for reliable JSON parsing (falls back to sed if not available)
|
||||
|
||||
## With Token Authentication
|
||||
|
||||
If you have a Vault token, you can provide it to automatically configure authentication:
|
||||
|
||||
```tf
|
||||
module "vault_cli" {
|
||||
source = "registry.coder.com/coder/vault-cli/coder"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
vault_addr = "https://vault.example.com"
|
||||
vault_token = var.vault_token # Optional
|
||||
}
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Basic Installation (CLI Only)
|
||||
|
||||
Install the Vault CLI without any authentication:
|
||||
|
||||
```tf
|
||||
module "vault_cli" {
|
||||
source = "registry.coder.com/coder/vault-cli/coder"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
vault_addr = "https://vault.example.com"
|
||||
}
|
||||
```
|
||||
|
||||
### With Specific Version
|
||||
|
||||
```tf
|
||||
module "vault_cli" {
|
||||
source = "registry.coder.com/coder/vault-cli/coder"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
vault_addr = "https://vault.example.com"
|
||||
vault_cli_version = "1.15.0"
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Installation Directory
|
||||
|
||||
```tf
|
||||
module "vault_cli" {
|
||||
source = "registry.coder.com/coder/vault-cli/coder"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
vault_addr = "https://vault.example.com"
|
||||
install_dir = "/home/coder/bin"
|
||||
}
|
||||
```
|
||||
|
||||
### With Vault Enterprise Namespace
|
||||
|
||||
For Vault Enterprise users who need to specify a namespace:
|
||||
|
||||
```tf
|
||||
module "vault_cli" {
|
||||
source = "registry.coder.com/coder/vault-cli/coder"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
vault_addr = "https://vault.example.com"
|
||||
vault_token = var.vault_token
|
||||
vault_namespace = "admin/my-namespace"
|
||||
}
|
||||
```
|
||||
|
||||
### Vault Enterprise Binary
|
||||
|
||||
Install the Vault Enterprise binary. This is required if using SAML authentication to Vault:
|
||||
|
||||
```tf
|
||||
module "vault_cli" {
|
||||
source = "registry.coder.com/coder/vault-cli/coder"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
vault_addr = "https://vault.example.com"
|
||||
enterprise = true
|
||||
}
|
||||
```
|
||||
|
||||
## Related Modules
|
||||
|
||||
For more advanced authentication methods, see:
|
||||
|
||||
- [vault-github](https://registry.coder.com/modules/coder/vault-github) - Authenticate with Vault using GitHub tokens
|
||||
- [vault-jwt](https://registry.coder.com/modules/coder/vault-jwt) - Authenticate with Vault using OIDC/JWT
|
||||
|
||||
For simple token-based authentication, see:
|
||||
|
||||
- [vault-token](https://registry.coder.com/modules/coder/vault-token) - Authenticate with Vault using a token
|
||||
@@ -0,0 +1,97 @@
|
||||
terraform {
|
||||
required_version = ">= 1.0"
|
||||
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">= 0.17"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
variable "agent_id" {
|
||||
type = string
|
||||
description = "The ID of a Coder agent."
|
||||
}
|
||||
|
||||
variable "vault_addr" {
|
||||
type = string
|
||||
description = "The address of the Vault server."
|
||||
}
|
||||
|
||||
variable "vault_token" {
|
||||
type = string
|
||||
description = "The Vault token to use for authentication. If not provided, only the CLI will be installed."
|
||||
default = ""
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
variable "install_dir" {
|
||||
type = string
|
||||
description = "The directory to install the Vault CLI to."
|
||||
default = "/usr/local/bin"
|
||||
}
|
||||
|
||||
variable "vault_cli_version" {
|
||||
type = string
|
||||
description = "The version of the Vault CLI to install."
|
||||
default = "latest"
|
||||
validation {
|
||||
condition = var.vault_cli_version == "latest" || can(regex("^[0-9]+\\.[0-9]+\\.[0-9]+$", var.vault_cli_version))
|
||||
error_message = "vault_cli_version must be either 'latest' or a semantic version (e.g., '1.15.0')."
|
||||
}
|
||||
}
|
||||
|
||||
variable "vault_namespace" {
|
||||
type = string
|
||||
description = "The Vault Enterprise namespace to use. If not provided, no namespace will be configured."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "enterprise" {
|
||||
type = bool
|
||||
description = "Whether to install the enterprise version of the Vault CLI. Required if using SAML authentication to Vault."
|
||||
default = false
|
||||
}
|
||||
|
||||
data "coder_workspace" "me" {}
|
||||
|
||||
resource "coder_script" "vault_cli" {
|
||||
agent_id = var.agent_id
|
||||
display_name = "Vault CLI"
|
||||
icon = "/icon/vault.svg"
|
||||
script = templatefile("${path.module}/run.sh", {
|
||||
VAULT_ADDR = var.vault_addr
|
||||
VAULT_TOKEN = var.vault_token
|
||||
INSTALL_DIR = var.install_dir
|
||||
VAULT_CLI_VERSION = var.vault_cli_version
|
||||
ENTERPRISE = var.enterprise
|
||||
})
|
||||
run_on_start = true
|
||||
start_blocks_login = true
|
||||
}
|
||||
|
||||
resource "coder_env" "vault_addr" {
|
||||
agent_id = var.agent_id
|
||||
name = "VAULT_ADDR"
|
||||
value = var.vault_addr
|
||||
}
|
||||
|
||||
resource "coder_env" "vault_token" {
|
||||
count = var.vault_token != "" ? 1 : 0
|
||||
agent_id = var.agent_id
|
||||
name = "VAULT_TOKEN"
|
||||
value = var.vault_token
|
||||
}
|
||||
|
||||
resource "coder_env" "vault_namespace" {
|
||||
count = var.vault_namespace != null ? 1 : 0
|
||||
agent_id = var.agent_id
|
||||
name = "VAULT_NAMESPACE"
|
||||
value = var.vault_namespace
|
||||
}
|
||||
|
||||
output "vault_cli_version" {
|
||||
description = "The version of the Vault CLI that was installed."
|
||||
value = var.vault_cli_version
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
mock_provider "coder" {}
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-id"
|
||||
vault_addr = "https://vault.example.com"
|
||||
}
|
||||
|
||||
run "test_vault_cli_without_token" {
|
||||
assert {
|
||||
condition = resource.coder_script.vault_cli.display_name == "Vault CLI"
|
||||
error_message = "Display name should be 'Vault CLI'"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_env.vault_addr.name == "VAULT_ADDR"
|
||||
error_message = "VAULT_ADDR environment variable should be set"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_env.vault_addr.value == "https://vault.example.com"
|
||||
error_message = "VAULT_ADDR should match the provided vault_addr"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = length(resource.coder_env.vault_token) == 0
|
||||
error_message = "VAULT_TOKEN should not be set when vault_token is not provided"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = length(resource.coder_env.vault_namespace) == 0
|
||||
error_message = "VAULT_NAMESPACE should not be set when vault_namespace is not provided"
|
||||
}
|
||||
}
|
||||
|
||||
run "test_vault_cli_with_token" {
|
||||
variables {
|
||||
vault_token = "test-vault-token"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_script.vault_cli.display_name == "Vault CLI"
|
||||
error_message = "Display name should be 'Vault CLI'"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_env.vault_addr.name == "VAULT_ADDR"
|
||||
error_message = "VAULT_ADDR environment variable should be set"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = length(resource.coder_env.vault_token) == 1
|
||||
error_message = "VAULT_TOKEN should be set when vault_token is provided"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_env.vault_token[0].name == "VAULT_TOKEN"
|
||||
error_message = "VAULT_TOKEN environment variable name should be correct"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_env.vault_token[0].value == "test-vault-token"
|
||||
error_message = "VAULT_TOKEN should match the provided vault_token"
|
||||
}
|
||||
}
|
||||
|
||||
run "test_vault_cli_custom_version" {
|
||||
variables {
|
||||
vault_cli_version = "1.15.0"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = output.vault_cli_version == "1.15.0"
|
||||
error_message = "Vault CLI version output should match the provided version"
|
||||
}
|
||||
}
|
||||
|
||||
run "test_vault_cli_custom_install_dir" {
|
||||
variables {
|
||||
install_dir = "/custom/install/dir"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_script.vault_cli.display_name == "Vault CLI"
|
||||
error_message = "Display name should be 'Vault CLI'"
|
||||
}
|
||||
}
|
||||
|
||||
run "test_vault_cli_invalid_version" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
vault_cli_version = "invalid-version"
|
||||
}
|
||||
|
||||
expect_failures = [var.vault_cli_version]
|
||||
}
|
||||
|
||||
run "test_vault_cli_valid_semver" {
|
||||
variables {
|
||||
vault_cli_version = "1.18.3"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = output.vault_cli_version == "1.18.3"
|
||||
error_message = "Vault CLI version output should match the provided version"
|
||||
}
|
||||
}
|
||||
|
||||
run "test_vault_cli_rejects_v_prefix" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
vault_cli_version = "v1.18.3"
|
||||
}
|
||||
|
||||
expect_failures = [var.vault_cli_version]
|
||||
}
|
||||
|
||||
run "test_vault_cli_with_namespace" {
|
||||
variables {
|
||||
vault_namespace = "admin/my-namespace"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = length(resource.coder_env.vault_namespace) == 1
|
||||
error_message = "VAULT_NAMESPACE should be set when vault_namespace is provided"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_env.vault_namespace[0].name == "VAULT_NAMESPACE"
|
||||
error_message = "VAULT_NAMESPACE environment variable name should be correct"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_env.vault_namespace[0].value == "admin/my-namespace"
|
||||
error_message = "VAULT_NAMESPACE should match the provided vault_namespace"
|
||||
}
|
||||
}
|
||||
|
||||
run "test_vault_cli_with_token_and_namespace" {
|
||||
variables {
|
||||
vault_token = "test-vault-token"
|
||||
vault_namespace = "admin/my-namespace"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = length(resource.coder_env.vault_token) == 1
|
||||
error_message = "VAULT_TOKEN should be set when vault_token is provided"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = length(resource.coder_env.vault_namespace) == 1
|
||||
error_message = "VAULT_NAMESPACE should be set when vault_namespace is provided"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_env.vault_token[0].value == "test-vault-token"
|
||||
error_message = "VAULT_TOKEN should match the provided vault_token"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_env.vault_namespace[0].value == "admin/my-namespace"
|
||||
error_message = "VAULT_NAMESPACE should match the provided vault_namespace"
|
||||
}
|
||||
}
|
||||
|
||||
run "test_vault_cli_enterprise" {
|
||||
variables {
|
||||
enterprise = true
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_script.vault_cli.display_name == "Vault CLI"
|
||||
error_message = "Display name should be 'Vault CLI'"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Convert all templated variables to shell variables
|
||||
VAULT_ADDR=${VAULT_ADDR}
|
||||
VAULT_TOKEN=${VAULT_TOKEN}
|
||||
INSTALL_DIR=${INSTALL_DIR}
|
||||
VAULT_CLI_VERSION=${VAULT_CLI_VERSION}
|
||||
ENTERPRISE=${ENTERPRISE}
|
||||
|
||||
# Fetch URL content. If dest is provided, write to file; otherwise output to stdout.
|
||||
# Usage: fetch <url> [dest]
|
||||
fetch() {
|
||||
url="$1"
|
||||
dest="$${2:-}"
|
||||
|
||||
# Detect HTTP client on first run
|
||||
if [ -z "$${HTTP_CLIENT:-}" ]; then
|
||||
if command -v curl > /dev/null 2>&1; then
|
||||
HTTP_CLIENT="curl"
|
||||
elif command -v wget > /dev/null 2>&1; then
|
||||
HTTP_CLIENT="wget"
|
||||
elif command -v busybox > /dev/null 2>&1; then
|
||||
HTTP_CLIENT="busybox"
|
||||
else
|
||||
printf "curl, wget, or busybox is not installed. Please install curl or wget in your image.\n"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -n "$${dest}" ]; then
|
||||
# shellcheck disable=SC2195
|
||||
case "$${HTTP_CLIENT}" in
|
||||
curl) curl -sSL --fail "$${url}" -o "$${dest}" ;;
|
||||
wget) wget -O "$${dest}" "$${url}" ;;
|
||||
busybox) busybox wget -O "$${dest}" "$${url}" ;;
|
||||
esac
|
||||
else
|
||||
# shellcheck disable=SC2195
|
||||
case "$${HTTP_CLIENT}" in
|
||||
curl) curl -sSL --fail "$${url}" ;;
|
||||
wget) wget -qO- "$${url}" ;;
|
||||
busybox) busybox wget -qO- "$${url}" ;;
|
||||
esac
|
||||
fi
|
||||
}
|
||||
|
||||
unzip_safe() {
|
||||
if command -v unzip > /dev/null 2>&1; then
|
||||
command unzip "$@"
|
||||
elif command -v busybox > /dev/null 2>&1; then
|
||||
busybox unzip "$@"
|
||||
else
|
||||
printf "unzip or busybox is not installed. Please install unzip in your image.\n"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
install() {
|
||||
# Get the architecture of the system
|
||||
ARCH=$(uname -m)
|
||||
if [ "$${ARCH}" = "x86_64" ]; then
|
||||
ARCH="amd64"
|
||||
elif [ "$${ARCH}" = "aarch64" ]; then
|
||||
ARCH="arm64"
|
||||
else
|
||||
printf "Unsupported architecture: %s\n" "$${ARCH}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Determine OS and validate
|
||||
OS=$(uname -s | tr '[:upper:]' '[:lower:]')
|
||||
if [ "$${OS}" != "linux" ] && [ "$${OS}" != "darwin" ]; then
|
||||
printf "Unsupported OS: %s. Only linux and darwin are supported.\n" "$${OS}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Fetch release information from HashiCorp API
|
||||
if [ "$${VAULT_CLI_VERSION}" = "latest" ]; then
|
||||
if [ "$${ENTERPRISE}" = "true" ]; then
|
||||
API_URL="https://api.releases.hashicorp.com/v1/releases/vault/latest?license_class=enterprise"
|
||||
else
|
||||
API_URL="https://api.releases.hashicorp.com/v1/releases/vault/latest"
|
||||
fi
|
||||
else
|
||||
# For specific version, append +ent suffix for enterprise
|
||||
if [ "$${ENTERPRISE}" = "true" ]; then
|
||||
API_URL="https://api.releases.hashicorp.com/v1/releases/vault/$${VAULT_CLI_VERSION}+ent"
|
||||
else
|
||||
API_URL="https://api.releases.hashicorp.com/v1/releases/vault/$${VAULT_CLI_VERSION}"
|
||||
fi
|
||||
fi
|
||||
|
||||
API_RESPONSE=$(fetch "$${API_URL}")
|
||||
if [ -z "$${API_RESPONSE}" ]; then
|
||||
printf "Failed to fetch release information from HashiCorp API.\n"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Parse version and download URL from API response
|
||||
if command -v jq > /dev/null 2>&1; then
|
||||
VAULT_CLI_VERSION=$(printf '%s' "$${API_RESPONSE}" | jq -r '.version')
|
||||
DOWNLOAD_URL=$(printf '%s' "$${API_RESPONSE}" | jq -r --arg os "$${OS}" --arg arch "$${ARCH}" '.builds[] | select(.os == $os and .arch == $arch) | .url')
|
||||
else
|
||||
VAULT_CLI_VERSION=$(printf '%s' "$${API_RESPONSE}" | sed -n 's/.*"version":"\([^"]*\)".*/\1/p')
|
||||
# Fallback: construct URL manually if jq not available
|
||||
DOWNLOAD_URL="https://releases.hashicorp.com/vault/$${VAULT_CLI_VERSION}/vault_$${VAULT_CLI_VERSION}_$${OS}_$${ARCH}.zip"
|
||||
fi
|
||||
|
||||
if [ -z "$${VAULT_CLI_VERSION}" ]; then
|
||||
printf "Failed to determine Vault version.\n"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ -z "$${DOWNLOAD_URL}" ]; then
|
||||
printf "Failed to determine download URL for Vault %s (%s/%s).\n" "$${VAULT_CLI_VERSION}" "$${OS}" "$${ARCH}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
printf "Vault version: %s\n" "$${VAULT_CLI_VERSION}"
|
||||
|
||||
# Check if the vault CLI is installed and has the correct version
|
||||
installation_needed=1
|
||||
if command -v vault > /dev/null 2>&1; then
|
||||
CURRENT_VERSION=$(vault version | grep -oE '[0-9]+\.[0-9]+\.[0-9]+')
|
||||
if [ "$${CURRENT_VERSION}" = "$${VAULT_CLI_VERSION}" ]; then
|
||||
printf "Vault version %s is already installed and up-to-date.\n\n" "$${CURRENT_VERSION}"
|
||||
installation_needed=0
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$${installation_needed}" = "1" ]; then
|
||||
# Download and install Vault
|
||||
if [ -z "$${CURRENT_VERSION}" ]; then
|
||||
printf "Installing Vault CLI ...\n\n"
|
||||
else
|
||||
printf "Upgrading Vault CLI from version %s to %s ...\n\n" "$${CURRENT_VERSION}" "$${VAULT_CLI_VERSION}"
|
||||
fi
|
||||
|
||||
# Create temporary directory for download
|
||||
TEMP_DIR=$(mktemp -d)
|
||||
cd "$${TEMP_DIR}" || return 1
|
||||
|
||||
printf "Downloading from %s\n" "$${DOWNLOAD_URL}"
|
||||
if ! fetch "$${DOWNLOAD_URL}" vault.zip; then
|
||||
printf "Failed to download Vault.\n"
|
||||
rm -rf "$${TEMP_DIR}"
|
||||
return 1
|
||||
fi
|
||||
if ! unzip_safe vault.zip; then
|
||||
printf "Failed to unzip Vault.\n"
|
||||
rm -rf "$${TEMP_DIR}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Install to the specified directory
|
||||
if [ -n "$${INSTALL_DIR}" ] && [ -w "$${INSTALL_DIR}" ]; then
|
||||
mv vault "$${INSTALL_DIR}/vault"
|
||||
printf "Vault installed to %s successfully!\n\n" "$${INSTALL_DIR}"
|
||||
elif [ -n "$${INSTALL_DIR}" ] && [ ! -w "$${INSTALL_DIR}" ]; then
|
||||
# Try with sudo if install dir specified but not writable
|
||||
if sudo mv vault "$${INSTALL_DIR}/vault" 2> /dev/null; then
|
||||
printf "Vault installed to %s successfully!\n\n" "$${INSTALL_DIR}"
|
||||
else
|
||||
printf "Warning: Cannot write to %s. " "$${INSTALL_DIR}"
|
||||
mkdir -p ~/.local/bin
|
||||
if mv vault ~/.local/bin/vault; then
|
||||
printf "Installed to ~/.local/bin instead.\n"
|
||||
printf "Please add ~/.local/bin to your PATH to use vault CLI.\n"
|
||||
else
|
||||
printf "Failed to install Vault.\n"
|
||||
rm -rf "$${TEMP_DIR}"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
elif sudo mv vault /usr/local/bin/vault 2> /dev/null; then
|
||||
printf "Vault installed successfully!\n\n"
|
||||
else
|
||||
mkdir -p ~/.local/bin
|
||||
if ! mv vault ~/.local/bin/vault; then
|
||||
printf "Failed to move Vault to local bin.\n"
|
||||
rm -rf "$${TEMP_DIR}"
|
||||
return 1
|
||||
fi
|
||||
printf "Please add ~/.local/bin to your PATH to use vault CLI.\n"
|
||||
fi
|
||||
|
||||
# Clean up temp directory
|
||||
rm -rf "$${TEMP_DIR}"
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# Run installation
|
||||
if ! install; then
|
||||
printf "Failed to install Vault CLI.\n"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Indicate token configuration status
|
||||
if [ -n "$${VAULT_TOKEN}" ]; then
|
||||
printf "Vault token has been configured via VAULT_TOKEN environment variable.\n"
|
||||
else
|
||||
printf "No Vault token provided. Use 'vault login' or set VAULT_TOKEN to authenticate.\n"
|
||||
fi
|
||||
@@ -19,7 +19,7 @@ Zed is a high-performance, multiplayer code editor from the creators of Atom and
|
||||
module "zed" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/zed/coder"
|
||||
version = "1.1.2"
|
||||
version = "1.1.3"
|
||||
agent_id = coder_agent.main.id
|
||||
}
|
||||
```
|
||||
@@ -32,7 +32,7 @@ module "zed" {
|
||||
module "zed" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/zed/coder"
|
||||
version = "1.1.2"
|
||||
version = "1.1.3"
|
||||
agent_id = coder_agent.main.id
|
||||
folder = "/home/coder/project"
|
||||
}
|
||||
@@ -44,7 +44,7 @@ module "zed" {
|
||||
module "zed" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/zed/coder"
|
||||
version = "1.1.2"
|
||||
version = "1.1.3"
|
||||
agent_id = coder_agent.main.id
|
||||
display_name = "Zed Editor"
|
||||
order = 1
|
||||
@@ -57,7 +57,7 @@ module "zed" {
|
||||
module "zed" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/zed/coder"
|
||||
version = "1.1.2"
|
||||
version = "1.1.3"
|
||||
agent_id = coder_agent.main.id
|
||||
agent_name = coder_agent.example.name
|
||||
}
|
||||
@@ -73,7 +73,7 @@ You can declaratively set/merge settings with the `settings` input. Provide a JS
|
||||
module "zed" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/zed/coder"
|
||||
version = "1.1.2"
|
||||
version = "1.1.3"
|
||||
agent_id = coder_agent.main.id
|
||||
|
||||
settings = jsonencode({
|
||||
|
||||
@@ -73,7 +73,7 @@ resource "coder_script" "zed_settings" {
|
||||
icon = "/icon/zed.svg"
|
||||
run_on_start = true
|
||||
script = <<-EOT
|
||||
#!/bin/sh
|
||||
#!/usr/bin/env bash
|
||||
set -eu
|
||||
SETTINGS_JSON='${replace(var.settings, "\"", "\\\"")}'
|
||||
if [ -z "$${SETTINGS_JSON}" ] || [ "$${SETTINGS_JSON}" = "{}" ]; then
|
||||
|
||||
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 42 KiB |
@@ -2,7 +2,7 @@
|
||||
display_name: Positron Desktop
|
||||
description: Add a one-click button to launch Positron Desktop
|
||||
icon: ../../../../.icons/positron.svg
|
||||
verified: true
|
||||
verified: false
|
||||
tags: [ide, positron]
|
||||
---
|
||||
|
||||
@@ -16,7 +16,7 @@ Uses the [Coder Remote VS Code Extension](https://github.com/coder/vscode-coder)
|
||||
module "positron" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/cytoshahar/positron/coder"
|
||||
version = "1.0.0"
|
||||
version = "1.0.1"
|
||||
agent_id = coder_agent.main.id
|
||||
}
|
||||
```
|
||||
|
||||
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 460 B |
|
After Width: | Height: | Size: 181 KiB |
|
After Width: | Height: | Size: 330 KiB |
@@ -0,0 +1,22 @@
|
||||
---
|
||||
display_name: "Michael Orlov"
|
||||
bio: "Platform Engineer specializing in cloud infrastructure, DevOps automation, and developer experience tools"
|
||||
avatar: "./.images/avatar.png"
|
||||
github: "harleylrn"
|
||||
support_email: "michael.orlov@gmail.com"
|
||||
status: "community"
|
||||
---
|
||||
|
||||
# Michael Orlov
|
||||
|
||||
Platform Engineer specializing in cloud infrastructure, DevOps automation, and developer experience tools. Contributing modules and templates to enhance developer productivity in Coder workspaces.
|
||||
|
||||
## Modules
|
||||
|
||||
### kiro-cli
|
||||
|
||||
AI-powered coding assistant integration for Coder workspaces with MCP (Model Context Protocol) support and task reporting capabilities.
|
||||
|
||||
## About
|
||||
|
||||
I focus on creating tools and integrations that improve developer experience and productivity. My contributions to the Coder Registry aim to provide seamless integration of modern development tools and AI assistants into cloud development environments.
|
||||
@@ -0,0 +1,396 @@
|
||||
---
|
||||
display_name: Kiro CLI
|
||||
description: Run Kiro CLI in your workspace to access AI coding assistant with MCP integration and task reporting.
|
||||
icon: ../../../../.icons/kiro.svg
|
||||
verified: true
|
||||
tags: [agent, ai, kiro, kiro-cli, tasks]
|
||||
---
|
||||
|
||||
# Kiro CLI
|
||||
|
||||
Run [Kiro CLI](https://kiro.dev/) in your workspace to access AI coding assistant. This module provides a complete integration with Coder workspaces, including automatic installation, MCP (Model Context Protocol) integration for task reporting, and support for custom pre/post install scripts.
|
||||
|
||||
```tf
|
||||
module "kiro-cli" {
|
||||
source = "registry.coder.com/harleylrn/kiro-cli/coder"
|
||||
version = "1.0.1"
|
||||
agent_id = coder_agent.example.id
|
||||
workdir = "/home/coder"
|
||||
|
||||
# Required: Authentication tarball (see below for generation)
|
||||
auth_tarball = <<-EOF
|
||||
base64encoded-tarball
|
||||
EOF
|
||||
}
|
||||
```
|
||||
|
||||

|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **zstd** - Required for compressing the authentication tarball
|
||||
- **Ubuntu/Debian**: `sudo apt-get install zstd`
|
||||
- **RHEL/CentOS/Fedora**: `sudo yum install zstd` or `sudo dnf install zstd`
|
||||
- **auth_tarball** - Required for installation and authentication
|
||||
|
||||
### Authentication Tarball
|
||||
|
||||
You must generate an authenticated Kiro CLI tarball on another machine where you have successfully logged in:
|
||||
|
||||
```bash
|
||||
# 1. Install Kiro CLI and login on your local machine
|
||||
kiro-cli login
|
||||
|
||||
# 2. Generate the authentication tarball
|
||||
cd ~/.local/share/kiro-cli
|
||||
tar -c . | zstd | base64 -w 0
|
||||
```
|
||||
|
||||
Copy the output and use it as the `auth_tarball` variable.
|
||||
|
||||
## Detailed Authentication Setup
|
||||
|
||||
**Step 1: Install Kiro CLI locally**
|
||||
|
||||
- Download from [Kiro CLI](https://kiro.dev/)
|
||||
- Follow the installation instructions for your platform
|
||||
|
||||
**Step 2: Authenticate**
|
||||
|
||||
```bash
|
||||
kiro-cli login
|
||||
```
|
||||
|
||||
Complete the authentication process in your browser.
|
||||
|
||||
**Step 3: Generate tarball**
|
||||
|
||||
```bash
|
||||
cd ~/.local/share/kiro-cli
|
||||
tar -c . | zstd | base64 -w 0 > /tmp/kiro-cli-auth.txt
|
||||
```
|
||||
|
||||
**Step 4: Use in Terraform**
|
||||
|
||||
```tf
|
||||
variable "kiro_cli_auth_tarball" {
|
||||
type = string
|
||||
sensitive = true
|
||||
default = "PASTE_YOUR_TARBALL_HERE"
|
||||
}
|
||||
```
|
||||
|
||||
> [!IMPORTANT]
|
||||
>
|
||||
> - Regenerate the tarball if you logout or re-authenticate
|
||||
> - Each user needs their own authentication tarball
|
||||
> - Keep the tarball secure as it contains authentication credentials
|
||||
|
||||
### Coder Tasks Integration
|
||||
|
||||
To enable integration with [Coder Tasks](https://coder.com/docs/ai-coder/tasks), you need to define the `coder_task` data source, create the `coder_ai_task` resource, and configure the module with the task prompt.
|
||||
|
||||
```tf
|
||||
data "coder_task" "me" {}
|
||||
|
||||
module "kiro-cli" {
|
||||
count = data.coder_task.me.enabled ? data.coder_workspace.me.start_count : 0
|
||||
source = "registry.coder.com/harleylrn/kiro-cli/coder"
|
||||
version = "1.0.1"
|
||||
agent_id = coder_agent.example.id
|
||||
workdir = "/home/coder"
|
||||
auth_tarball = var.kiro_cli_auth_tarball
|
||||
ai_prompt = data.coder_task.me.prompt
|
||||
trust_all_tools = true
|
||||
|
||||
# Task reporting configuration
|
||||
report_tasks = true
|
||||
|
||||
# Enable CLI app alongside web app
|
||||
cli_app = true
|
||||
web_app_display_name = "Kiro CLI"
|
||||
cli_app_display_name = "Kiro CLI"
|
||||
}
|
||||
|
||||
resource "coder_ai_task" "task" {
|
||||
count = data.coder_task.me.enabled ? data.coder_workspace.me.start_count : 0
|
||||
app_id = module.kiro-cli[count.index].task_app_id
|
||||
}
|
||||
```
|
||||
|
||||
> [!IMPORTANT]
|
||||
>
|
||||
> - The `data "coder_task" "me" {}` data source provides the task prompt and enabled state
|
||||
> - The module count is controlled by `data.coder_task.me.enabled` to only create when a task is active
|
||||
> - The `coder_ai_task` resource links the module's task reporting to Coder's task system
|
||||
> - The `ai_prompt` is passed from `data.coder_task.me.prompt`
|
||||
> - Without this configuration, `coder_ai_task` resources will not function properly
|
||||
>
|
||||
> **_Security Notice_**
|
||||
> In order to allow the tasks flow non-interactively all the tools are trusted
|
||||
> This flag bypasses standard permission checks and allows Kiro CLI broader access to your system than normally permitted.
|
||||
> While this enables more functionality, it also means Kiro CLI can potentially execute commands with the same privileges as the user running it.
|
||||
> Use this module only in trusted environments and be aware of the security implications.
|
||||
|
||||
### Default System Prompt
|
||||
|
||||
The module includes a simple system prompt that instructs Kiro CLI:
|
||||
|
||||
```
|
||||
You are a helpful Coding assistant. Aim to autonomously investigate
|
||||
and solve issues the user gives you and test your work, whenever possible.
|
||||
Avoid shortcuts like mocking tests. When you get stuck, you can ask the user
|
||||
but opt for autonomy.
|
||||
```
|
||||
|
||||
You can customize this behavior by providing your own system prompt via the `system_prompt` variable.
|
||||
|
||||
### Default Coder MCP Instructions
|
||||
|
||||
The module includes specific instructions for the Coder MCP server integration that are separate from the system prompt:
|
||||
|
||||
```
|
||||
YOU MUST REPORT ALL TASKS TO CODER.
|
||||
When reporting tasks you MUST follow these EXACT instructions:
|
||||
- IMMEDIATELY report status after receiving ANY user message
|
||||
- Be granular If you are investigating with multiple steps report each step to coder.
|
||||
|
||||
Task state MUST be one of the following:
|
||||
- Use "state": "working" when actively processing WITHOUT needing additional user input
|
||||
- Use "state": "complete" only when finished with a task
|
||||
- Use "state": "failure" when you need ANY user input lack sufficient details or encounter blockers.
|
||||
|
||||
Task summaries MUST:
|
||||
- Include specifics about what you're doing
|
||||
- Include clear and actionable steps for the user
|
||||
- Be less than 160 characters in length
|
||||
```
|
||||
|
||||
You can customize these instructions by providing your own via the `coder_mcp_instructions` variable.
|
||||
|
||||
## Default Agent Configuration
|
||||
|
||||
The module includes a default agent configuration template that provides a comprehensive setup for Kiro CLI integration:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "agent",
|
||||
"description": "This is an default agent config",
|
||||
"prompt": "${system_prompt}",
|
||||
"mcpServers": {},
|
||||
"tools": ["read", "write", "shell", "aws", "@coder", "knowledge"],
|
||||
"toolAliases": {},
|
||||
"allowedTools": ["read", "@coder"],
|
||||
"resources": [
|
||||
"file://KiroQ.md",
|
||||
"file://README.md",
|
||||
"file://.kiro/steering/**/*.md"
|
||||
],
|
||||
"hooks": {},
|
||||
"toolsSettings": {},
|
||||
"useLegacyMcpJson": true
|
||||
}
|
||||
```
|
||||
|
||||
### Configuration Details:
|
||||
|
||||
- **Tools Available:** File operations (`read`, `write`), shell execution (`shell`), AWS CLI (`aws`), Coder MCP integration (`@coder`), and knowledge base access (`knowledge`)
|
||||
- **@coder Tool:** Enables Coder MCP integration for task reporting (`coder_report_task` and related tools)
|
||||
- **Allowed Tools:** By default, only `read` and `@coder` are allowed (can be customized for security)
|
||||
- **Resources:** Access to documentation and rule files in the workspace
|
||||
- **MCP Servers:** Empty by default, can be configured via `agent_config` variable
|
||||
- **System Prompt:** Dynamically populated from the `system_prompt` variable
|
||||
- **Legacy MCP:** Uses legacy MCP JSON format for compatibility
|
||||
|
||||
You can override this configuration by providing your own JSON via the `agent_config` variable.
|
||||
|
||||
### Agent Name Configuration
|
||||
|
||||
The module automatically extracts the agent name from the `"name"` field in the `agent_config` JSON and uses it for:
|
||||
|
||||
- **Configuration File:** Saves the agent config as `~/.kiro/agents/{agent_name}.json`
|
||||
- **Default Agent:** Sets the agent as the default using `q settings chat.defaultAgent {agent_name}`
|
||||
- **MCP Integration:** Associates the Coder MCP server with the specified agent name
|
||||
|
||||
If no custom `agent_config` is provided, the default agent name "agent" is used.
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```tf
|
||||
module "kiro-cli" {
|
||||
source = "registry.coder.com/harleylrn/kiro-cli/coder"
|
||||
version = "1.0.1"
|
||||
agent_id = coder_agent.example.id
|
||||
workdir = "/home/coder"
|
||||
auth_tarball = var.kiro_cli_auth_tarball
|
||||
}
|
||||
```
|
||||
|
||||
This example will:
|
||||
|
||||
1. Download and install Kiro CLI latest version
|
||||
2. Extract authentication tarball to ~/.local/share/kiro-cli
|
||||
3. Configure Coder MCP integration for task reporting
|
||||
4. Create default agent configuration file
|
||||
5. Start Kiro CLI in /home/coder directory
|
||||
6. Provide web interface through AgentAPI
|
||||
|
||||
> [!IMPORTANT]
|
||||
> By default `write` tool is not allowed, which will pause the task execution
|
||||
> and will wait for the prompt to approve its usage.
|
||||
> To avoid this, and allow the normal task flow, user has two options:
|
||||
>
|
||||
> - Change the parameter `trust_all_tools` value to `true` (default to `false`)
|
||||
> OR
|
||||
> - Provide your own agent configuration with the tools of your choice allowed
|
||||
|
||||
### With Custom AI Prompt
|
||||
|
||||
```tf
|
||||
module "kiro-cli" {
|
||||
source = "registry.coder.com/harleylrn/kiro-cli/coder"
|
||||
version = "1.0.1"
|
||||
agent_id = coder_agent.example.id
|
||||
workdir = "/home/coder"
|
||||
auth_tarball = var.kiro_cli_auth_tarball
|
||||
ai_prompt = "Help me set up a Python FastAPI project with proper testing structure"
|
||||
trust_all_tools = true
|
||||
}
|
||||
```
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **_Security Notice_**
|
||||
> In order to allow the tasks flow non-interactively all the tools are trusted
|
||||
> This flag bypasses standard permission checks and allows Kiro CLI broader access to your system than normally permitted.
|
||||
> While this enables more functionality, it also means Kiro CLI can potentially execute commands with the same privileges as the user running it.
|
||||
> Use this module only in trusted environments and be aware of the security implications.
|
||||
|
||||
### With Custom Pre/Post Install Scripts
|
||||
|
||||
```tf
|
||||
module "kiro-cli" {
|
||||
source = "registry.coder.com/harleylrn/kiro-cli/coder"
|
||||
version = "1.0.1"
|
||||
agent_id = coder_agent.example.id
|
||||
workdir = "/home/coder"
|
||||
auth_tarball = var.kiro_cli_auth_tarball
|
||||
|
||||
pre_install_script = <<-EOT
|
||||
#!/bin/bash
|
||||
echo "Setting up custom environment..."
|
||||
# Install additional dependencies
|
||||
sudo apt-get update && sudo apt-get install -y zstd
|
||||
EOT
|
||||
|
||||
post_install_script = <<-EOT
|
||||
#!/bin/bash
|
||||
echo "Configuring Kiro CLI settings..."
|
||||
# Custom configuration commands
|
||||
kiro-cli settings chat.model claude-3-sonnet
|
||||
EOT
|
||||
}
|
||||
```
|
||||
|
||||
### Specific Version Installation
|
||||
|
||||
```tf
|
||||
module "kiro-cli" {
|
||||
source = "registry.coder.com/harleylrn/kiro-cli/coder"
|
||||
version = "1.0.1"
|
||||
agent_id = coder_agent.example.id
|
||||
workdir = "/home/coder"
|
||||
auth_tarball = var.kiro_cli_auth_tarball
|
||||
kiro_cli_version = "1.14.0" # Specific version
|
||||
install_kiro_cli = true
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Agent Configuration
|
||||
|
||||
```tf
|
||||
module "kiro-cli" {
|
||||
source = "registry.coder.com/harleylrn/kiro-cli/coder"
|
||||
version = "1.0.1"
|
||||
agent_id = coder_agent.example.id
|
||||
workdir = "/home/coder"
|
||||
auth_tarball = var.kiro_cli_auth_tarball
|
||||
|
||||
agent_config = <<-EOT
|
||||
{
|
||||
"name": "custom-agent",
|
||||
"description": "Custom Kiro CLI agent for my workspace",
|
||||
"prompt": "You are a specialized DevOps assistant...",
|
||||
"tools": ["read", "write", "shell", "aws"]
|
||||
}
|
||||
EOT
|
||||
}
|
||||
```
|
||||
|
||||
### With Custom AgentAPI Configuration
|
||||
|
||||
```tf
|
||||
module "kiro-cli" {
|
||||
source = "registry.coder.com/harleylrn/kiro-cli/coder"
|
||||
version = "1.0.1"
|
||||
agent_id = coder_agent.example.id
|
||||
workdir = "/home/coder"
|
||||
auth_tarball = var.kiro_cli_auth_tarball
|
||||
|
||||
# AgentAPI configuration for environments without wildcard access url. https://coder.com/docs/admin/setup#wildcard-access-url
|
||||
agentapi_chat_based_path = true
|
||||
agentapi_version = "v0.10.0"
|
||||
}
|
||||
```
|
||||
|
||||
### Air-Gapped Installation
|
||||
|
||||
For environments without direct internet access, you can host Kiro CLI installation files internally and configure the module to use your internal repository:
|
||||
|
||||
```tf
|
||||
module "kiro-cli" {
|
||||
source = "registry.coder.com/harleylrn/kiro-cli/coder"
|
||||
version = "1.0.1"
|
||||
agent_id = coder_agent.example.id
|
||||
workdir = "/home/coder"
|
||||
auth_tarball = var.kiro_cli_auth_tarball
|
||||
|
||||
# Point to internal artifact repository
|
||||
kiro_install_url = "https://artifacts.internal.corp/kiro-cli-releases"
|
||||
|
||||
# Use specific version available in your repository
|
||||
kiro_cli_version = "latest"
|
||||
}
|
||||
```
|
||||
|
||||
**Prerequisites for Air-Gapped Setup:**
|
||||
|
||||
1. Download Kiro CLI installation files from the official source and host them internally
|
||||
2. Maintain the same directory structure: `{base_url}/{version}/kirocli-{arch}-linux.zip`
|
||||
3. Ensure both architectures are available:
|
||||
- `kirocli-x86_64-linux.zip` for Intel/AMD systems
|
||||
- `kirocli-aarch64-linux.zip` for ARM systems
|
||||
4. Configure network access from Coder workspaces to your internal repository
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
**Authentication issues:**
|
||||
|
||||
- Regenerate the auth tarball on your local machine
|
||||
- Ensure the tarball is properly base64 encoded
|
||||
- Check that the original authentication is still valid
|
||||
|
||||
**MCP integration not working:**
|
||||
|
||||
- Verify that AgentAPI is installed (`install_agentapi = true`)
|
||||
- Check that the Coder agent is properly configured
|
||||
- Review the system prompt configuration
|
||||
|
||||
## Outputs
|
||||
|
||||
| Name | Description |
|
||||
| -------------------------------------------------------------------- | ----------- |
|
||||
| <a name="output_task_app_id"></a> [task_app_id](#output_task_app_id) | n/a |
|
||||
@@ -0,0 +1,372 @@
|
||||
run "required_variables" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-id"
|
||||
workdir = "/tmp/test-workdir"
|
||||
}
|
||||
}
|
||||
|
||||
run "minimal_config" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-id"
|
||||
workdir = "/tmp/test-workdir"
|
||||
auth_tarball = "dGVzdA==" # base64 "test"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_env.status_slug[0].name == "CODER_MCP_APP_STATUS_SLUG"
|
||||
error_message = "Status slug environment variable not configured correctly"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_env.status_slug[0].value == "kiro-cli"
|
||||
error_message = "Status slug value should be 'kiro-cli'"
|
||||
}
|
||||
}
|
||||
|
||||
# Test Case 1: Basic Usage – No Autonomous Use of Q
|
||||
# Using vanilla Kubernetes Deployment Template configuration
|
||||
run "test_case_1_basic_usage" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-id"
|
||||
workdir = "/tmp/test-workdir"
|
||||
auth_tarball = "dGVzdEF1dGhUYXJiYWxs" # base64 "testAuthTarball"
|
||||
}
|
||||
|
||||
# Q is installed and authenticated
|
||||
assert {
|
||||
condition = resource.coder_env.status_slug[0].name == "CODER_MCP_APP_STATUS_SLUG"
|
||||
error_message = "Status slug environment variable should be configured for basic usage"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_env.status_slug[0].value == "kiro-cli"
|
||||
error_message = "Status slug value should be 'kiro-cli' for basic usage"
|
||||
}
|
||||
|
||||
# AgentAPI is installed and configured (default behavior)
|
||||
assert {
|
||||
condition = length(resource.coder_env.auth_tarball) == 1
|
||||
error_message = "Auth tarball environment variable should be created for authentication"
|
||||
}
|
||||
|
||||
# Foundational configuration applied
|
||||
assert {
|
||||
condition = length(local.agent_config) > 0
|
||||
error_message = "Agent config should be generated with foundational configuration"
|
||||
}
|
||||
|
||||
# No additional parameters required (using defaults)
|
||||
assert {
|
||||
condition = local.agent_name == "agent"
|
||||
error_message = "Default agent name should be 'agent' when no custom config provided"
|
||||
}
|
||||
}
|
||||
|
||||
# Test Case 2: Autonomous Usage – Autonomous Use of Q
|
||||
# AI prompt passed through from external source (Tasks interface or Issue Tracker CI)
|
||||
run "test_case_2_autonomous_usage" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-id"
|
||||
workdir = "/tmp/test-workdir"
|
||||
auth_tarball = "dGVzdEF1dGhUYXJiYWxs" # base64 "testAuthTarball"
|
||||
ai_prompt = "Help me set up a Python FastAPI project with proper testing structure"
|
||||
}
|
||||
|
||||
# Q is installed and authenticated
|
||||
assert {
|
||||
condition = resource.coder_env.status_slug[0].name == "CODER_MCP_APP_STATUS_SLUG"
|
||||
error_message = "Status slug environment variable should be configured for autonomous usage"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_env.status_slug[0].value == "kiro-cli"
|
||||
error_message = "Status slug value should be 'kiro-cli' for autonomous usage"
|
||||
}
|
||||
|
||||
# AgentAPI is installed and configured
|
||||
assert {
|
||||
condition = length(resource.coder_env.auth_tarball) == 1
|
||||
error_message = "Auth tarball environment variable should be created for autonomous usage"
|
||||
}
|
||||
|
||||
# Foundational configuration for all components applied
|
||||
assert {
|
||||
condition = length(local.agent_config) > 0
|
||||
error_message = "Agent config should be generated for autonomous usage"
|
||||
}
|
||||
|
||||
# AI prompt is configured
|
||||
assert {
|
||||
condition = local.full_prompt == "Help me set up a Python FastAPI project with proper testing structure"
|
||||
error_message = "AI prompt should be configured correctly for autonomous usage"
|
||||
}
|
||||
|
||||
# Default agent name when no custom config
|
||||
assert {
|
||||
condition = local.agent_name == "agent"
|
||||
error_message = "Default agent name should be 'agent' for autonomous usage"
|
||||
}
|
||||
}
|
||||
|
||||
# Test Case 3: Extended Configuration – Parameter Validation and File Rendering
|
||||
# Validates extended configuration options and parameter application
|
||||
run "test_case_3_extended_configuration" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-id"
|
||||
workdir = "/tmp/test-workdir"
|
||||
auth_tarball = "dGVzdEF1dGhUYXJiYWxs" # base64 "testAuthTarball"
|
||||
kiro_cli_version = "1.14.1"
|
||||
kiro_install_url = "https://desktop-release.q.us-east-1.amazonaws.com"
|
||||
install_kiro_cli = true
|
||||
install_agentapi = true
|
||||
agentapi_version = "v0.6.0"
|
||||
trust_all_tools = true
|
||||
ai_prompt = "Help me create a production-grade TypeScript monorepo with testing and deployment"
|
||||
system_prompt = "You are a helpful software assistant working in a secure enterprise environment"
|
||||
pre_install_script = "echo 'Pre-install setup'"
|
||||
post_install_script = "echo 'Post-install cleanup'"
|
||||
agent_config = jsonencode({
|
||||
name = "production-agent"
|
||||
description = "Production Kiro CLI agent for enterprise environment"
|
||||
prompt = "You are a helpful software assistant working in a secure enterprise environment"
|
||||
mcpServers = {}
|
||||
tools = ["fs_read", "fs_write", "execute_bash", "use_aws", "knowledge"]
|
||||
toolAliases = {}
|
||||
allowedTools = ["fs_read"]
|
||||
resources = ["file://KiroQ.md", "file://README.md", "file://.kiro/steering/**/*.md"]
|
||||
hooks = {}
|
||||
toolsSettings = {}
|
||||
useLegacyMcpJson = true
|
||||
})
|
||||
}
|
||||
|
||||
# All installation parameters are applied correctly
|
||||
assert {
|
||||
condition = resource.coder_env.status_slug[0].value == "kiro-cli"
|
||||
error_message = "Status slug should be configured correctly with extended parameters"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_env.auth_tarball[0].value == "dGVzdEF1dGhUYXJiYWxs"
|
||||
error_message = "Auth tarball should be configured correctly with extended parameters"
|
||||
}
|
||||
|
||||
# Custom agent configuration is loaded and referenced correctly
|
||||
assert {
|
||||
condition = local.agent_name == "production-agent"
|
||||
error_message = "Agent name should be extracted from custom agent config"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = length(local.agent_config) > 0
|
||||
error_message = "Custom agent config should be processed correctly"
|
||||
}
|
||||
|
||||
# AI prompt and system prompt are configured
|
||||
assert {
|
||||
condition = local.full_prompt == "Help me create a production-grade TypeScript monorepo with testing and deployment"
|
||||
error_message = "AI prompt should be configured correctly in extended configuration"
|
||||
}
|
||||
|
||||
# Pre-install and post-install scripts are provided
|
||||
assert {
|
||||
condition = length(local.agent_config) > 0
|
||||
error_message = "Agent config should be generated correctly for extended configuration"
|
||||
}
|
||||
}
|
||||
|
||||
run "full_config" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-id"
|
||||
workdir = "/tmp/test-workdir"
|
||||
install_kiro_cli = true
|
||||
install_agentapi = true
|
||||
agentapi_version = "v0.5.0"
|
||||
kiro_cli_version = "latest"
|
||||
trust_all_tools = true
|
||||
ai_prompt = "Build a web application"
|
||||
auth_tarball = "dGVzdA=="
|
||||
order = 1
|
||||
group = "AI Tools"
|
||||
icon = "/icon/custom-kiro-cli.svg"
|
||||
pre_install_script = "echo 'pre-install'"
|
||||
post_install_script = "echo 'post-install'"
|
||||
agent_config = jsonencode({
|
||||
name = "test-agent"
|
||||
description = "Test agent configuration"
|
||||
prompt = "You are a helpful AI assistant for testing."
|
||||
mcpServers = {}
|
||||
tools = ["fs_read", "fs_write", "execute_bash", "use_aws", "knowledge"]
|
||||
toolAliases = {}
|
||||
allowedTools = ["fs_read"]
|
||||
resources = ["file://KiroQ.md", "file://README.md", "file://.kiro/steering/**/*.md"]
|
||||
hooks = {}
|
||||
toolsSettings = {}
|
||||
useLegacyMcpJson = true
|
||||
})
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_env.status_slug[0].name == "CODER_MCP_APP_STATUS_SLUG"
|
||||
error_message = "Status slug environment variable not configured correctly"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_env.status_slug[0].value == "kiro-cli"
|
||||
error_message = "Status slug value should be 'kiro-cli'"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = length(resource.coder_env.auth_tarball) == 1
|
||||
error_message = "Auth tarball environment variable should be created when provided"
|
||||
}
|
||||
}
|
||||
|
||||
run "auth_tarball_environment" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-id"
|
||||
workdir = "/tmp/test-workdir"
|
||||
auth_tarball = "dGVzdEF1dGhUYXJiYWxs" # base64 "testAuthTarball"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_env.auth_tarball[0].name == "KIRO_CLI_AUTH_TARBALL"
|
||||
error_message = "Auth tarball environment variable name should be 'KIRO_CLI_AUTH_TARBALL'"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_env.auth_tarball[0].value == "dGVzdEF1dGhUYXJiYWxs"
|
||||
error_message = "Auth tarball environment variable value should match input"
|
||||
}
|
||||
}
|
||||
|
||||
run "empty_auth_tarball" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-id"
|
||||
workdir = "/tmp/test-workdir"
|
||||
auth_tarball = ""
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = length(resource.coder_env.auth_tarball) == 0
|
||||
error_message = "Auth tarball environment variable should not be created when empty"
|
||||
}
|
||||
}
|
||||
|
||||
run "custom_system_prompt" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-id"
|
||||
workdir = "/tmp/test-workdir"
|
||||
system_prompt = "Custom system prompt for testing"
|
||||
}
|
||||
|
||||
# Test that the system prompt is used in the agent config template
|
||||
assert {
|
||||
condition = length(local.agent_config) > 0
|
||||
error_message = "Agent config should be generated with custom system prompt"
|
||||
}
|
||||
}
|
||||
|
||||
run "install_options" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-id"
|
||||
workdir = "/tmp/test-workdir"
|
||||
install_kiro_cli = false
|
||||
install_agentapi = false
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_env.status_slug[0].name == "CODER_MCP_APP_STATUS_SLUG"
|
||||
error_message = "Status slug should still be configured even when install options are disabled"
|
||||
}
|
||||
}
|
||||
|
||||
run "version_configuration" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-id"
|
||||
workdir = "/tmp/test-workdir"
|
||||
kiro_cli_version = "2.15.0"
|
||||
agentapi_version = "v0.4.0"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_env.status_slug[0].value == "kiro-cli"
|
||||
error_message = "Status slug value should remain 'kiro-cli' regardless of version"
|
||||
}
|
||||
}
|
||||
|
||||
# Additional test for agent name extraction
|
||||
run "agent_name_extraction" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-id"
|
||||
workdir = "/tmp/test-workdir"
|
||||
agent_config = jsonencode({
|
||||
name = "custom-enterprise-agent"
|
||||
description = "Custom enterprise agent configuration"
|
||||
prompt = "You are a custom enterprise AI assistant."
|
||||
mcpServers = {}
|
||||
tools = ["fs_read", "fs_write", "execute_bash", "use_aws", "knowledge"]
|
||||
toolAliases = {}
|
||||
allowedTools = ["fs_read", "fs_write"]
|
||||
resources = ["file://README.md"]
|
||||
hooks = {}
|
||||
toolsSettings = {}
|
||||
useLegacyMcpJson = true
|
||||
})
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = local.agent_name == "custom-enterprise-agent"
|
||||
error_message = "Agent name should be extracted correctly from custom agent config"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = length(local.agent_config) > 0
|
||||
error_message = "Agent config should be processed correctly"
|
||||
}
|
||||
}
|
||||
|
||||
# Test for JSON encoding validation
|
||||
run "json_encoding_validation" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-id"
|
||||
workdir = "/tmp/test-workdir"
|
||||
system_prompt = "Multi-line\nsystem prompt\nwith newlines"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = length(local.system_prompt) > 0
|
||||
error_message = "System prompt should be JSON encoded correctly"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = length(local.agent_config) > 0
|
||||
error_message = "Agent config should be generated correctly with multi-line system prompt"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,531 @@
|
||||
import { describe, it, expect } from "bun:test";
|
||||
import {
|
||||
runTerraformApply,
|
||||
runTerraformInit,
|
||||
findResourceInstance,
|
||||
} from "~test";
|
||||
import path from "path";
|
||||
|
||||
const moduleDir = path.resolve(__dirname);
|
||||
|
||||
// Always provide agent_config to bypass template parsing issues
|
||||
const baseAgentConfig = JSON.stringify({
|
||||
name: "test-agent",
|
||||
description: "Test agent configuration",
|
||||
prompt: "You are a helpful AI assistant.",
|
||||
mcpServers: {},
|
||||
tools: ["fs_read", "fs_write", "execute_bash", "use_aws", "knowledge"],
|
||||
toolAliases: {},
|
||||
allowedTools: ["fs_read"],
|
||||
resources: ["file://README.md", "file://.kiro/steering/**/*.md"],
|
||||
hooks: {},
|
||||
toolsSettings: {},
|
||||
useLegacyMcpJson: true,
|
||||
});
|
||||
|
||||
const requiredVars = {
|
||||
agent_id: "dummy-agent-id",
|
||||
agent_config: baseAgentConfig,
|
||||
workdir: "/tmp/test-workdir",
|
||||
};
|
||||
|
||||
const fullConfigVars = {
|
||||
agent_id: "dummy-agent-id",
|
||||
workdir: "/tmp/test-workdir",
|
||||
install_kiro_cli: true,
|
||||
install_agentapi: true,
|
||||
agentapi_version: "v0.6.0",
|
||||
kiro_cli_version: "1.14.1",
|
||||
kiro_install_url: "https://desktop-release.q.us-east-1.amazonaws.com",
|
||||
trust_all_tools: false,
|
||||
ai_prompt: "Build a comprehensive test suite",
|
||||
auth_tarball: "dGVzdEF1dGhUYXJiYWxs", // base64 "testAuthTarball"
|
||||
order: 1,
|
||||
group: "AI Tools",
|
||||
icon: "/icon/custom-kiro-cli.svg",
|
||||
pre_install_script: "echo 'Starting pre-install'",
|
||||
post_install_script: "echo 'Completed post-install'",
|
||||
agent_config: baseAgentConfig,
|
||||
};
|
||||
|
||||
describe("kiro-cli module v1.0.0", async () => {
|
||||
await runTerraformInit(moduleDir);
|
||||
|
||||
// Test Case 1: Basic Usage – No Autonomous Use of Q
|
||||
// Matches CDES-203 Test Case #1: Basic Usage
|
||||
it("Test Case 1: Basic Usage - No Autonomous Use of Q", async () => {
|
||||
const basicUsageVars = {
|
||||
agent_id: "dummy-agent-id",
|
||||
workdir: "/tmp/test-workdir",
|
||||
auth_tarball: "dGVzdEF1dGhUYXJiYWxs", // base64 "testAuthTarball"
|
||||
};
|
||||
|
||||
const state = await runTerraformApply(moduleDir, basicUsageVars);
|
||||
|
||||
// Q is installed and authenticated
|
||||
const statusSlugEnv = findResourceInstance(
|
||||
state,
|
||||
"coder_env",
|
||||
"status_slug",
|
||||
);
|
||||
expect(statusSlugEnv).toBeDefined();
|
||||
expect(statusSlugEnv.name).toBe("CODER_MCP_APP_STATUS_SLUG");
|
||||
expect(statusSlugEnv.value).toBe("kiro-cli");
|
||||
|
||||
// AgentAPI is installed and configured (default behavior)
|
||||
const authTarballEnv = findResourceInstance(
|
||||
state,
|
||||
"coder_env",
|
||||
"auth_tarball",
|
||||
);
|
||||
expect(authTarballEnv).toBeDefined();
|
||||
expect(authTarballEnv.name).toBe("KIRO_CLI_AUTH_TARBALL");
|
||||
expect(authTarballEnv.value).toBe("dGVzdEF1dGhUYXJiYWxs");
|
||||
|
||||
// Foundational configuration for all components is applied
|
||||
// No additional parameters are required for the module to work
|
||||
// Using the terminal application and Q chat returns a functional interface
|
||||
});
|
||||
|
||||
// Test Case 2: Autonomous Usage – Autonomous Use of Q
|
||||
// Matches CDES-203 Test Case 2: Autonomous Usage
|
||||
it("Test Case 2: Autonomous Usage - Autonomous Use of Q", async () => {
|
||||
const autonomousUsageVars = {
|
||||
agent_id: "dummy-agent-id",
|
||||
workdir: "/tmp/test-workdir",
|
||||
auth_tarball: "dGVzdEF1dGhUYXJiYWxs", // base64 "testAuthTarball"
|
||||
ai_prompt:
|
||||
"Help me set up a Python FastAPI project with proper testing structure",
|
||||
};
|
||||
|
||||
const state = await runTerraformApply(moduleDir, autonomousUsageVars);
|
||||
|
||||
// Q is installed and authenticated
|
||||
const statusSlugEnv = findResourceInstance(
|
||||
state,
|
||||
"coder_env",
|
||||
"status_slug",
|
||||
);
|
||||
expect(statusSlugEnv).toBeDefined();
|
||||
expect(statusSlugEnv.name).toBe("CODER_MCP_APP_STATUS_SLUG");
|
||||
expect(statusSlugEnv.value).toBe("kiro-cli");
|
||||
|
||||
// AgentAPI is installed and configured
|
||||
const authTarballEnv = findResourceInstance(
|
||||
state,
|
||||
"coder_env",
|
||||
"auth_tarball",
|
||||
);
|
||||
expect(authTarballEnv).toBeDefined();
|
||||
expect(authTarballEnv.name).toBe("KIRO_CLI_AUTH_TARBALL");
|
||||
|
||||
// AI prompt is passed through from external source
|
||||
// The Chat interface functions as required
|
||||
// The Tasks interface functions as required
|
||||
// The template can be invoked from GitHub integration as expected
|
||||
});
|
||||
|
||||
// Test Case 3: Extended Configuration – Parameter Validation and File Rendering
|
||||
// Matches CDES-203 Test Case 3: Extended Configuration
|
||||
it("Test Case 3: Extended Configuration - Parameter Validation and File Rendering", async () => {
|
||||
const extendedConfigVars = {
|
||||
agent_id: "dummy-agent-id",
|
||||
workdir: "/tmp/test-workdir",
|
||||
auth_tarball: "dGVzdEF1dGhUYXJiYWxs", // base64 "testAuthTarball"
|
||||
kiro_cli_version: "1.14.1",
|
||||
kiro_install_url: "https://desktop-release.q.us-east-1.amazonaws.com",
|
||||
install_kiro_cli: true,
|
||||
install_agentapi: true,
|
||||
agentapi_version: "v0.6.0",
|
||||
trust_all_tools: true,
|
||||
ai_prompt:
|
||||
"Help me create a production-grade TypeScript monorepo with testing and deployment",
|
||||
system_prompt:
|
||||
"You are a helpful software assistant working in a secure enterprise environment",
|
||||
pre_install_script: "echo 'Pre-install setup'",
|
||||
post_install_script: "echo 'Post-install cleanup'",
|
||||
agent_config: JSON.stringify({
|
||||
name: "production-agent",
|
||||
description: "Production Kiro CLI agent for enterprise environment",
|
||||
prompt:
|
||||
"You are a helpful software assistant working in a secure enterprise environment",
|
||||
mcpServers: {},
|
||||
tools: ["fs_read", "fs_write", "execute_bash", "use_aws", "knowledge"],
|
||||
toolAliases: {},
|
||||
allowedTools: ["fs_read"],
|
||||
resources: [
|
||||
"file://KiroQ.md",
|
||||
"file://README.md",
|
||||
"file://.kiro/steering/**/*.md",
|
||||
],
|
||||
hooks: {},
|
||||
toolsSettings: {},
|
||||
useLegacyMcpJson: true,
|
||||
}),
|
||||
};
|
||||
|
||||
const state = await runTerraformApply(moduleDir, extendedConfigVars);
|
||||
|
||||
// All installation steps execute in the correct order
|
||||
const statusSlugEnv = findResourceInstance(
|
||||
state,
|
||||
"coder_env",
|
||||
"status_slug",
|
||||
);
|
||||
expect(statusSlugEnv).toBeDefined();
|
||||
expect(statusSlugEnv.name).toBe("CODER_MCP_APP_STATUS_SLUG");
|
||||
expect(statusSlugEnv.value).toBe("kiro-cli");
|
||||
|
||||
// auth_tarball is unpacked and used as expected
|
||||
const authTarballEnv = findResourceInstance(
|
||||
state,
|
||||
"coder_env",
|
||||
"auth_tarball",
|
||||
);
|
||||
expect(authTarballEnv).toBeDefined();
|
||||
expect(authTarballEnv.value).toBe("dGVzdEF1dGhUYXJiYWxs");
|
||||
|
||||
// agent_config is rendered correctly, and the name field is used as the agent's name
|
||||
// The specified ai_prompt and system_prompt are respected by the Q agent
|
||||
// Tools are trusted globally if trust_all_tools = true
|
||||
// Files and scripts execute in proper sequence
|
||||
});
|
||||
|
||||
// 1. Basic functionality test (replaces testRequiredVariables)
|
||||
it("works with required variables", async () => {
|
||||
const state = await runTerraformApply(moduleDir, requiredVars);
|
||||
|
||||
// Should create the basic resources
|
||||
const statusSlugEnv = findResourceInstance(
|
||||
state,
|
||||
"coder_env",
|
||||
"status_slug",
|
||||
);
|
||||
expect(statusSlugEnv).toBeDefined();
|
||||
expect(statusSlugEnv.name).toBe("CODER_MCP_APP_STATUS_SLUG");
|
||||
expect(statusSlugEnv.value).toBe("kiro-cli");
|
||||
});
|
||||
|
||||
// 2. Environment variables are created correctly
|
||||
it("creates required environment variables", async () => {
|
||||
const state = await runTerraformApply(moduleDir, fullConfigVars);
|
||||
|
||||
// Check status slug environment variable
|
||||
const statusSlugEnv = findResourceInstance(
|
||||
state,
|
||||
"coder_env",
|
||||
"status_slug",
|
||||
);
|
||||
expect(statusSlugEnv).toBeDefined();
|
||||
expect(statusSlugEnv.name).toBe("CODER_MCP_APP_STATUS_SLUG");
|
||||
expect(statusSlugEnv.value).toBe("kiro-cli");
|
||||
|
||||
// Check auth tarball environment variable
|
||||
const authTarballEnv = findResourceInstance(
|
||||
state,
|
||||
"coder_env",
|
||||
"auth_tarball",
|
||||
);
|
||||
expect(authTarballEnv).toBeDefined();
|
||||
expect(authTarballEnv.name).toBe("KIRO_CLI_AUTH_TARBALL");
|
||||
expect(authTarballEnv.value).toBe("dGVzdEF1dGhUYXJiYWxs");
|
||||
});
|
||||
|
||||
// 3. Empty auth tarball handling
|
||||
it("handles empty auth tarball correctly", async () => {
|
||||
const noAuthVars = {
|
||||
...requiredVars,
|
||||
auth_tarball: "",
|
||||
};
|
||||
|
||||
const state = await runTerraformApply(moduleDir, noAuthVars);
|
||||
|
||||
// Auth tarball environment variable should not be created when empty
|
||||
const authTarballEnv = state.resources?.find(
|
||||
(r) => r.type === "coder_env" && r.name === "auth_tarball",
|
||||
);
|
||||
expect(authTarballEnv).toBeUndefined();
|
||||
});
|
||||
|
||||
// 4. Status slug is always created
|
||||
it("creates status slug environment variable", async () => {
|
||||
const state = await runTerraformApply(moduleDir, requiredVars);
|
||||
|
||||
// Status slug should always be configured
|
||||
const statusSlugEnv = findResourceInstance(
|
||||
state,
|
||||
"coder_env",
|
||||
"status_slug",
|
||||
);
|
||||
expect(statusSlugEnv).toBeDefined();
|
||||
expect(statusSlugEnv.name).toBe("CODER_MCP_APP_STATUS_SLUG");
|
||||
expect(statusSlugEnv.value).toBe("kiro-cli");
|
||||
});
|
||||
|
||||
// 5. Install options configuration
|
||||
it("respects install option flags", async () => {
|
||||
const noInstallVars = {
|
||||
...requiredVars,
|
||||
install_kiro_cli: false,
|
||||
install_agentapi: false,
|
||||
};
|
||||
|
||||
const state = await runTerraformApply(moduleDir, noInstallVars);
|
||||
|
||||
// Status slug should still be configured even when install options are disabled
|
||||
const statusSlugEnv = findResourceInstance(
|
||||
state,
|
||||
"coder_env",
|
||||
"status_slug",
|
||||
);
|
||||
expect(statusSlugEnv).toBeDefined();
|
||||
expect(statusSlugEnv.value).toBe("kiro-cli");
|
||||
});
|
||||
|
||||
// 6. Configurable installation URL
|
||||
it("uses configurable kiro_install_url parameter", async () => {
|
||||
const customUrlVars = {
|
||||
...requiredVars,
|
||||
kiro_install_url: "https://internal-mirror.company.com/kiro-cli",
|
||||
};
|
||||
|
||||
const state = await runTerraformApply(moduleDir, customUrlVars);
|
||||
|
||||
// Should create the basic resources
|
||||
const statusSlugEnv = findResourceInstance(
|
||||
state,
|
||||
"coder_env",
|
||||
"status_slug",
|
||||
);
|
||||
expect(statusSlugEnv).toBeDefined();
|
||||
});
|
||||
|
||||
// 7. Version configuration
|
||||
it("uses specified versions", async () => {
|
||||
const versionVars = {
|
||||
...requiredVars,
|
||||
kiro_cli_version: "1.14.1",
|
||||
agentapi_version: "v0.6.0",
|
||||
};
|
||||
|
||||
const state = await runTerraformApply(moduleDir, versionVars);
|
||||
|
||||
// Should create the basic resources
|
||||
const statusSlugEnv = findResourceInstance(
|
||||
state,
|
||||
"coder_env",
|
||||
"status_slug",
|
||||
);
|
||||
expect(statusSlugEnv).toBeDefined();
|
||||
});
|
||||
|
||||
// 8. UI configuration options
|
||||
it("supports UI customization options", async () => {
|
||||
const uiCustomVars = {
|
||||
...requiredVars,
|
||||
order: 5,
|
||||
group: "Custom AI Tools",
|
||||
icon: "/icon/custom-kiro-cli-icon.svg",
|
||||
};
|
||||
|
||||
const state = await runTerraformApply(moduleDir, uiCustomVars);
|
||||
|
||||
// Should create the basic resources
|
||||
const statusSlugEnv = findResourceInstance(
|
||||
state,
|
||||
"coder_env",
|
||||
"status_slug",
|
||||
);
|
||||
expect(statusSlugEnv).toBeDefined();
|
||||
});
|
||||
|
||||
// 9. Pre and post install scripts
|
||||
it("supports pre and post install scripts", async () => {
|
||||
const scriptVars = {
|
||||
...requiredVars,
|
||||
pre_install_script: "echo 'Pre-install setup'",
|
||||
post_install_script: "echo 'Post-install cleanup'",
|
||||
};
|
||||
|
||||
const state = await runTerraformApply(moduleDir, scriptVars);
|
||||
|
||||
// Should create the basic resources
|
||||
const statusSlugEnv = findResourceInstance(
|
||||
state,
|
||||
"coder_env",
|
||||
"status_slug",
|
||||
);
|
||||
expect(statusSlugEnv).toBeDefined();
|
||||
});
|
||||
|
||||
// 10. Valid agent_config JSON with different agent name
|
||||
it("handles valid agent_config JSON with custom agent name", async () => {
|
||||
const customAgentConfig = JSON.stringify({
|
||||
name: "production-agent",
|
||||
description: "Production Kiro CLI agent",
|
||||
prompt: "You are a production AI assistant.",
|
||||
mcpServers: {},
|
||||
tools: ["fs_read", "fs_write"],
|
||||
toolAliases: {},
|
||||
allowedTools: ["fs_read"],
|
||||
resources: ["file://README.md"],
|
||||
hooks: {},
|
||||
toolsSettings: {},
|
||||
useLegacyMcpJson: true,
|
||||
});
|
||||
|
||||
const validAgentConfigVars = {
|
||||
...requiredVars,
|
||||
agent_config: customAgentConfig,
|
||||
};
|
||||
|
||||
const state = await runTerraformApply(moduleDir, validAgentConfigVars);
|
||||
|
||||
// Should create the basic resources
|
||||
const statusSlugEnv = findResourceInstance(
|
||||
state,
|
||||
"coder_env",
|
||||
"status_slug",
|
||||
);
|
||||
expect(statusSlugEnv).toBeDefined();
|
||||
});
|
||||
|
||||
// 11. Air-gapped installation support
|
||||
it("supports air-gapped installation with custom URL", async () => {
|
||||
const airGappedVars = {
|
||||
...requiredVars,
|
||||
kiro_install_url: "https://artifacts.internal.corp/kiro-cli-releases",
|
||||
kiro_cli_version: "1.14.1",
|
||||
};
|
||||
|
||||
const state = await runTerraformApply(moduleDir, airGappedVars);
|
||||
|
||||
// Should create the basic resources
|
||||
const statusSlugEnv = findResourceInstance(
|
||||
state,
|
||||
"coder_env",
|
||||
"status_slug",
|
||||
);
|
||||
expect(statusSlugEnv).toBeDefined();
|
||||
});
|
||||
|
||||
// 12. Trust all tools configuration
|
||||
it("handles trust_all_tools configuration", async () => {
|
||||
const trustVars = {
|
||||
...requiredVars,
|
||||
trust_all_tools: true,
|
||||
};
|
||||
|
||||
const state = await runTerraformApply(moduleDir, trustVars);
|
||||
|
||||
// Should create the basic resources
|
||||
const statusSlugEnv = findResourceInstance(
|
||||
state,
|
||||
"coder_env",
|
||||
"status_slug",
|
||||
);
|
||||
expect(statusSlugEnv).toBeDefined();
|
||||
});
|
||||
|
||||
// 13. AI prompt configuration
|
||||
it("handles AI prompt configuration", async () => {
|
||||
const promptVars = {
|
||||
...requiredVars,
|
||||
ai_prompt: "Create a comprehensive test suite for the application",
|
||||
};
|
||||
|
||||
const state = await runTerraformApply(moduleDir, promptVars);
|
||||
|
||||
// Should create the basic resources
|
||||
const statusSlugEnv = findResourceInstance(
|
||||
state,
|
||||
"coder_env",
|
||||
"status_slug",
|
||||
);
|
||||
expect(statusSlugEnv).toBeDefined();
|
||||
});
|
||||
|
||||
// 14. Agent config with minimal structure
|
||||
it("handles minimal agent config structure", async () => {
|
||||
const minimalAgentConfig = JSON.stringify({
|
||||
name: "minimal-agent",
|
||||
description: "Minimal agent config",
|
||||
prompt: "You are a minimal AI assistant.",
|
||||
mcpServers: {},
|
||||
tools: ["fs_read", "fs_write", "execute_bash", "use_aws", "knowledge"],
|
||||
toolAliases: {},
|
||||
allowedTools: ["fs_read"],
|
||||
resources: ["file://README.md"],
|
||||
hooks: {},
|
||||
toolsSettings: {},
|
||||
useLegacyMcpJson: true,
|
||||
});
|
||||
|
||||
const minimalVars = {
|
||||
...requiredVars,
|
||||
agent_config: minimalAgentConfig,
|
||||
};
|
||||
|
||||
const state = await runTerraformApply(moduleDir, minimalVars);
|
||||
|
||||
// Should create the basic resources
|
||||
const statusSlugEnv = findResourceInstance(
|
||||
state,
|
||||
"coder_env",
|
||||
"status_slug",
|
||||
);
|
||||
expect(statusSlugEnv).toBeDefined();
|
||||
});
|
||||
|
||||
// 15. JSON encoding validation for system prompts with newlines
|
||||
it("handles system prompts with newlines correctly", async () => {
|
||||
const multilinePromptVars = {
|
||||
...requiredVars,
|
||||
system_prompt: "Multi-line\nsystem prompt\nwith newlines",
|
||||
};
|
||||
|
||||
const state = await runTerraformApply(moduleDir, multilinePromptVars);
|
||||
|
||||
// Should create the basic resources without JSON parsing errors
|
||||
const statusSlugEnv = findResourceInstance(
|
||||
state,
|
||||
"coder_env",
|
||||
"status_slug",
|
||||
);
|
||||
expect(statusSlugEnv).toBeDefined();
|
||||
expect(statusSlugEnv.value).toBe("kiro-cli");
|
||||
});
|
||||
|
||||
// 16. Agent name extraction from custom config
|
||||
it("extracts agent name from custom configuration correctly", async () => {
|
||||
const customNameConfig = JSON.stringify({
|
||||
name: "enterprise-production-agent",
|
||||
description: "Enterprise production agent configuration",
|
||||
prompt: "You are an enterprise production AI assistant.",
|
||||
mcpServers: {},
|
||||
tools: ["fs_read", "fs_write", "execute_bash", "use_aws", "knowledge"],
|
||||
toolAliases: {},
|
||||
allowedTools: ["fs_read", "fs_write", "execute_bash"],
|
||||
resources: ["file://README.md", "file://.kiro/steering/**/*.md"],
|
||||
hooks: {},
|
||||
toolsSettings: {},
|
||||
useLegacyMcpJson: true,
|
||||
});
|
||||
|
||||
const customNameVars = {
|
||||
...requiredVars,
|
||||
agent_config: customNameConfig,
|
||||
};
|
||||
|
||||
const state = await runTerraformApply(moduleDir, customNameVars);
|
||||
|
||||
// Should create the basic resources
|
||||
const statusSlugEnv = findResourceInstance(
|
||||
state,
|
||||
"coder_env",
|
||||
"status_slug",
|
||||
);
|
||||
expect(statusSlugEnv).toBeDefined();
|
||||
expect(statusSlugEnv.value).toBe("kiro-cli");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,275 @@
|
||||
# Improved kiro-cli module main.tf
|
||||
|
||||
terraform {
|
||||
required_version = ">= 1.0"
|
||||
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">= 2.12"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
variable "agent_id" {
|
||||
type = string
|
||||
description = "The ID of a Coder agent."
|
||||
}
|
||||
|
||||
data "coder_workspace" "me" {}
|
||||
data "coder_workspace_owner" "me" {}
|
||||
|
||||
variable "order" {
|
||||
type = number
|
||||
description = "The order determines the position of app in the UI presentation. The lowest order is shown first and apps with equal order are sorted by name (ascending order)."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "group" {
|
||||
type = string
|
||||
description = "The name of a group that this app belongs to."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "icon" {
|
||||
type = string
|
||||
description = "The icon to use for the app."
|
||||
default = "/icon/kiro.svg"
|
||||
}
|
||||
|
||||
variable "report_tasks" {
|
||||
type = bool
|
||||
description = "Whether to enable task reporting to Coder UI via AgentAPI"
|
||||
default = true
|
||||
}
|
||||
|
||||
variable "cli_app" {
|
||||
type = bool
|
||||
description = "Whether to create a CLI app for Kiro CLI"
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "web_app_display_name" {
|
||||
type = string
|
||||
description = "Display name for the web app"
|
||||
default = "Kiro CLI"
|
||||
}
|
||||
|
||||
variable "cli_app_display_name" {
|
||||
type = string
|
||||
description = "Display name for the CLI app"
|
||||
default = "Kiro CLI"
|
||||
}
|
||||
|
||||
variable "install_agentapi" {
|
||||
type = bool
|
||||
description = "Whether to install AgentAPI."
|
||||
default = true
|
||||
}
|
||||
|
||||
variable "ai_prompt" {
|
||||
type = string
|
||||
description = "The initial task prompt to send to Kiro CLI."
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "pre_install_script" {
|
||||
type = string
|
||||
description = "Optional script to run before installing Kiro CLI."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "post_install_script" {
|
||||
type = string
|
||||
description = "Optional script to run after installing Kiro CLI."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "agentapi_version" {
|
||||
type = string
|
||||
description = "The version of AgentAPI to install."
|
||||
default = "v0.10.0"
|
||||
}
|
||||
|
||||
variable "workdir" {
|
||||
type = string
|
||||
description = "The folder to run Kiro CLI in."
|
||||
}
|
||||
|
||||
variable "install_kiro_cli" {
|
||||
type = bool
|
||||
description = "Whether to install Kiro CLI."
|
||||
default = true
|
||||
}
|
||||
|
||||
variable "kiro_cli_version" {
|
||||
type = string
|
||||
description = "The version of Kiro CLI to install."
|
||||
default = "latest"
|
||||
}
|
||||
|
||||
variable "kiro_install_url" {
|
||||
type = string
|
||||
description = "Base URL for Kiro CLI installation downloads."
|
||||
default = "https://desktop-release.q.us-east-1.amazonaws.com"
|
||||
}
|
||||
|
||||
variable "trust_all_tools" {
|
||||
type = bool
|
||||
description = "Whether to trust all tools in Kiro CLI."
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "system_prompt" {
|
||||
type = string
|
||||
description = "The system prompt to use for Kiro CLI. This should instruct the agent how to do task reporting."
|
||||
default = <<-EOT
|
||||
You are a helpful Coding assistant. Aim to autonomously investigate
|
||||
and solve issues the user gives you and test your work, whenever possible.
|
||||
Avoid shortcuts like mocking tests. When you get stuck, you can ask the user
|
||||
but opt for autonomy.
|
||||
EOT
|
||||
}
|
||||
|
||||
variable "coder_mcp_instructions" {
|
||||
type = string
|
||||
description = "Instructions for the Coder MCP server integration. This defines how the agent should report tasks to Coder."
|
||||
default = <<-EOT
|
||||
YOU MUST REPORT ALL TASKS TO CODER.
|
||||
When reporting tasks you MUST follow these EXACT instructions:
|
||||
- IMMEDIATELY report status after receiving ANY user message
|
||||
- Be granular If you are investigating with multiple steps report each step to coder.
|
||||
|
||||
Task state MUST be one of the following:
|
||||
- Use "state": "working" when actively processing WITHOUT needing additional user input
|
||||
- Use "state": "complete" only when finished with a task
|
||||
- Use "state": "failure" when you need ANY user input lack sufficient details or encounter blockers.
|
||||
|
||||
Task summaries MUST:
|
||||
- Include specifics about what you're doing
|
||||
- Include clear and actionable steps for the user
|
||||
- Be less than 160 characters in length
|
||||
EOT
|
||||
}
|
||||
|
||||
variable "auth_tarball" {
|
||||
type = string
|
||||
description = "Base64 encoded, zstd compressed tarball of a pre-authenticated ~/.local/share/kiro-cli directory."
|
||||
default = ""
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
variable "agent_config" {
|
||||
type = string
|
||||
description = "Optional Agent configuration JSON for Kiro CLI."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "agentapi_chat_based_path" {
|
||||
type = bool
|
||||
description = "Whether to use chat-based path for AgentAPI.Required if CODER_WILDCARD_ACCESS_URL is not defined in coder deployment"
|
||||
default = false
|
||||
}
|
||||
|
||||
# Expose status slug to the agent environment
|
||||
resource "coder_env" "status_slug" {
|
||||
agent_id = var.agent_id
|
||||
name = "CODER_MCP_APP_STATUS_SLUG"
|
||||
value = local.app_slug
|
||||
count = var.report_tasks ? 1 : 0
|
||||
}
|
||||
|
||||
# Expose auth tarball as environment variable for install script
|
||||
resource "coder_env" "auth_tarball" {
|
||||
count = var.auth_tarball != "" ? 1 : 0
|
||||
agent_id = var.agent_id
|
||||
name = "KIRO_CLI_AUTH_TARBALL"
|
||||
value = var.auth_tarball
|
||||
}
|
||||
|
||||
locals {
|
||||
app_slug = "kiro-cli"
|
||||
workdir = trimsuffix(var.workdir, "/")
|
||||
install_script = file("${path.module}/scripts/install.sh")
|
||||
start_script = file("${path.module}/scripts/start.sh")
|
||||
module_dir_name = ".kiro"
|
||||
system_prompt = jsonencode(replace(var.system_prompt, "/[\r\n]/", ""))
|
||||
coder_mcp_instructions = jsonencode(replace(var.coder_mcp_instructions, "/[\r\n]/", ""))
|
||||
|
||||
# Create default agent config structure
|
||||
default_agent_config = templatefile("${path.module}/templates/agent-config.json.tpl", {
|
||||
system_prompt = local.system_prompt
|
||||
})
|
||||
|
||||
# Choose the JSON string: use var.agent_config if provided, otherwise encode default
|
||||
agent_config = var.agent_config != null ? var.agent_config : local.default_agent_config
|
||||
|
||||
# Extract agent name from the selected config
|
||||
agent_name = try(jsondecode(local.agent_config).name, "agent")
|
||||
|
||||
full_prompt = var.ai_prompt != null ? var.ai_prompt : ""
|
||||
|
||||
server_chat_parameters = var.agentapi_chat_based_path ? "--chat-base-path /@${data.coder_workspace_owner.me.name}/${data.coder_workspace.me.name}.${var.agent_id}/apps/${local.app_slug}/chat" : ""
|
||||
}
|
||||
|
||||
|
||||
module "agentapi" {
|
||||
source = "registry.coder.com/coder/agentapi/coder"
|
||||
version = "2.0.0"
|
||||
|
||||
agent_id = var.agent_id
|
||||
folder = local.workdir
|
||||
web_app_slug = local.app_slug
|
||||
web_app_order = var.order
|
||||
web_app_group = var.group
|
||||
web_app_icon = var.icon
|
||||
web_app_display_name = var.web_app_display_name
|
||||
cli_app = var.cli_app
|
||||
cli_app_slug = var.cli_app ? "${local.app_slug}-cli" : null
|
||||
cli_app_display_name = var.cli_app ? var.cli_app_display_name : null
|
||||
module_dir_name = local.module_dir_name
|
||||
install_agentapi = var.install_agentapi
|
||||
agentapi_version = var.agentapi_version
|
||||
pre_install_script = var.pre_install_script
|
||||
post_install_script = var.post_install_script
|
||||
|
||||
start_script = <<-EOT
|
||||
#!/bin/bash
|
||||
set -o errexit
|
||||
set -o pipefail
|
||||
|
||||
echo -n '${base64encode(local.start_script)}' | base64 -d > /tmp/start.sh
|
||||
chmod +x /tmp/start.sh
|
||||
ARG_TRUST_ALL_TOOLS='${var.trust_all_tools}' \
|
||||
ARG_AI_PROMPT='${base64encode(local.full_prompt)}' \
|
||||
ARG_MODULE_DIR_NAME='${local.module_dir_name}' \
|
||||
ARG_WORKDIR='${var.workdir}' \
|
||||
ARG_SERVER_PARAMETERS="${local.server_chat_parameters}" \
|
||||
ARG_REPORT_TASKS='${var.report_tasks}' \
|
||||
/tmp/start.sh
|
||||
EOT
|
||||
|
||||
install_script = <<-EOT
|
||||
#!/bin/bash
|
||||
set -o errexit
|
||||
set -o pipefail
|
||||
|
||||
echo -n '${base64encode(local.install_script)}' | base64 -d > /tmp/install.sh
|
||||
chmod +x /tmp/install.sh
|
||||
ARG_INSTALL='${var.install_kiro_cli}' \
|
||||
ARG_VERSION='${var.kiro_cli_version}' \
|
||||
ARG_KIRO_INSTALL_URL='${var.kiro_install_url}' \
|
||||
ARG_AUTH_TARBALL='${var.auth_tarball}' \
|
||||
ARG_AGENT_CONFIG='${local.agent_config != null ? base64encode(local.agent_config) : ""}' \
|
||||
ARG_AGENT_NAME='${local.agent_name}' \
|
||||
ARG_MODULE_DIR_NAME='${local.module_dir_name}' \
|
||||
ARG_CODER_MCP_APP_STATUS_SLUG='${local.app_slug}' \
|
||||
ARG_CODER_MCP_INSTRUCTIONS='${base64encode(local.coder_mcp_instructions)}' \
|
||||
ARG_REPORT_TASKS='${var.report_tasks}' \
|
||||
/tmp/install.sh
|
||||
EOT
|
||||
}
|
||||
|
||||
output "task_app_id" {
|
||||
value = module.agentapi.task_app_id
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
#!/bin/bash
|
||||
# Install script for kiro-cli module
|
||||
|
||||
set -o errexit
|
||||
set -o pipefail
|
||||
|
||||
command_exists() {
|
||||
command -v "$1" > /dev/null 2>&1
|
||||
}
|
||||
|
||||
# Inputs
|
||||
ARG_INSTALL=${ARG_INSTALL:-true}
|
||||
ARG_VERSION=${ARG_VERSION:-latest}
|
||||
ARG_KIRO_INSTALL_URL=${ARG_KIRO_INSTALL_URL:-https://desktop-release.q.us-east-1.amazonaws.com}
|
||||
ARG_AUTH_TARBALL=${ARG_AUTH_TARBALL:-}
|
||||
ARG_AGENT_CONFIG=${ARG_AGENT_CONFIG:-}
|
||||
ARG_AGENT_NAME=${ARG_AGENT_NAME:-default-agent}
|
||||
ARG_MODULE_DIR_NAME=${ARG_MODULE_DIR_NAME:-.kiro}
|
||||
ARG_CODER_MCP_APP_STATUS_SLUG=${ARG_CODER_MCP_APP_STATUS_SLUG:-}
|
||||
ARG_CODER_MCP_INSTRUCTIONS=${ARG_CODER_MCP_INSTRUCTIONS:-}
|
||||
ARG_REPORT_TASKS=${ARG_REPORT_TASKS:-true}
|
||||
|
||||
mkdir -p "$HOME/$ARG_MODULE_DIR_NAME"
|
||||
|
||||
# Decode base64 inputs
|
||||
ARG_AGENT_CONFIG_DECODED=""
|
||||
if [ -n "$ARG_AGENT_CONFIG" ]; then
|
||||
ARG_AGENT_CONFIG_DECODED=$(echo -n "$ARG_AGENT_CONFIG" | base64 -d)
|
||||
fi
|
||||
|
||||
ARG_CODER_MCP_INSTRUCTIONS_DECODED=""
|
||||
if [ -n "$ARG_CODER_MCP_INSTRUCTIONS" ]; then
|
||||
ARG_CODER_MCP_INSTRUCTIONS_DECODED=$(echo -n "$ARG_CODER_MCP_INSTRUCTIONS" | base64 -d)
|
||||
fi
|
||||
|
||||
echo "--------------------------------"
|
||||
echo "install: $ARG_INSTALL"
|
||||
echo "version: $ARG_VERSION"
|
||||
echo "kiro_install_url: $ARG_KIRO_INSTALL_URL"
|
||||
echo "agent_name: $ARG_AGENT_NAME"
|
||||
echo "coder_mcp_app_status_slug: $ARG_CODER_MCP_APP_STATUS_SLUG"
|
||||
echo "module_dir_name: $ARG_MODULE_DIR_NAME"
|
||||
echo "auth_tarball_provided: ${ARG_AUTH_TARBALL}"
|
||||
echo "report_tasks: ${ARG_REPORT_TASKS}"
|
||||
echo "--------------------------------"
|
||||
|
||||
# Install Kiro CLI if requested
|
||||
function install_kiro_cli() {
|
||||
if [ "$ARG_INSTALL" = "true" ]; then
|
||||
echo "Installing Kiro CLI..."
|
||||
PREV_DIR="$PWD"
|
||||
TMP_DIR="$(mktemp -d)"
|
||||
cd "$TMP_DIR"
|
||||
|
||||
ARCH="$(uname -m)"
|
||||
case "$ARCH" in
|
||||
"x86_64")
|
||||
KIRO_URL="${ARG_KIRO_INSTALL_URL}/${ARG_VERSION}/kirocli-x86_64-linux.zip"
|
||||
;;
|
||||
"aarch64" | "arm64")
|
||||
KIRO_URL="${ARG_KIRO_INSTALL_URL}/${ARG_VERSION}/kirocli-aarch64-linux.zip"
|
||||
;;
|
||||
*)
|
||||
echo "Error: Unsupported architecture: $ARCH. Kiro CLI only supports x86_64 and arm64."
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "Downloading Kiro CLI for $ARCH from $KIRO_URL..."
|
||||
curl --proto '=https' --tlsv1.2 -sSf "$KIRO_URL" -o "kirocli.zip"
|
||||
unzip kirocli.zip
|
||||
./kirocli/install.sh --no-confirm
|
||||
cd "$PREV_DIR"
|
||||
rm -rf "$TMP_DIR"
|
||||
|
||||
# Ensure binaries are discoverable; create stable symlink to kiro-cli
|
||||
CANDIDATES=(
|
||||
"$(command -v kiro-cli || true)"
|
||||
"$HOME/.local/bin/kiro-cli"
|
||||
)
|
||||
FOUND_BIN=""
|
||||
for c in "${CANDIDATES[@]}"; do
|
||||
if [ -n "$c" ] && [ -x "$c" ]; then
|
||||
FOUND_BIN="$c"
|
||||
break
|
||||
fi
|
||||
done
|
||||
export PATH="$PATH:$HOME/.local/bin"
|
||||
echo "Installed Kiro CLI at: $(command -v kiro-cli || true) (resolved: $FOUND_BIN)"
|
||||
fi
|
||||
}
|
||||
|
||||
# Extract authentication tarball
|
||||
function extract_auth_tarball() {
|
||||
if [ -n "$ARG_AUTH_TARBALL" ]; then
|
||||
echo "Extracting auth tarball..."
|
||||
|
||||
if ! command_exists zstd; then
|
||||
echo "Error: zstd is required to extract the authentication tarball but is not installed."
|
||||
echo "Please install zstd using the pre_install_script parameter."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PREV_DIR="$PWD"
|
||||
echo "$ARG_AUTH_TARBALL" | base64 -d > /tmp/auth.tar.zst
|
||||
rm -rf ~/.local/share/kiro-cli
|
||||
mkdir -p ~/.local/share/kiro-cli
|
||||
cd ~/.local/share/kiro-cli
|
||||
tar -I zstd -xf /tmp/auth.tar.zst
|
||||
rm /tmp/auth.tar.zst
|
||||
cd "$PREV_DIR"
|
||||
echo "Extracted auth tarball to ~/.local/share/kiro-cli"
|
||||
else
|
||||
echo "Warning: No auth tarball provided. Kiro CLI may require manual authentication."
|
||||
fi
|
||||
}
|
||||
|
||||
# Configure MCP integration and create agent
|
||||
function configure_agent() {
|
||||
# Create Kiro CLI agent configuration directory
|
||||
AGENT_CONFIG_DIR="$HOME/.kiro/agents"
|
||||
mkdir -p "$AGENT_CONFIG_DIR"
|
||||
ALLOWED_TOOLS="coder_get_workspace\,coder_create_workspace\,coder_list_workspaces\,coder_list_templates\,coder_template_version_parameters\,coder_get_authenticated_user\,coder_create_workspace_build\,coder_create_template_version\,coder_get_workspace_agent_logs\,coder_get_workspace_build_logs\,coder_get_template_version_logs\,coder_update_template_active_version\,coder_upload_tar_file\,coder_create_template\,coder_delete_template\,coder_workspace_bash"
|
||||
if [ -n "$ARG_AGENT_CONFIG_DECODED" ]; then
|
||||
echo "Applying custom MCP configuration..."
|
||||
# Use agent name as filename for the configuration
|
||||
echo "$ARG_AGENT_CONFIG_DECODED" > "$AGENT_CONFIG_DIR/${ARG_AGENT_NAME}.json"
|
||||
echo "Custom configuration saved to $AGENT_CONFIG_DIR/${ARG_AGENT_NAME}.json"
|
||||
fi
|
||||
if [ "$ARG_REPORT_TASKS" = "true" ]; then
|
||||
echo "Configuring Kiro CLI to report tasks via Coder MCP..."
|
||||
kiro-cli mcp add --name coder \
|
||||
--command "coder" \
|
||||
--agent "$ARG_AGENT_NAME" \
|
||||
--args "exp,mcp,server,--allowed-tools,coder_report_task,--instructions,'$ARG_CODER_MCP_INSTRUCTIONS_DECODED'" \
|
||||
--env "CODER_MCP_APP_STATUS_SLUG=${ARG_CODER_MCP_APP_STATUS_SLUG}" \
|
||||
--env "CODER_MCP_AI_AGENTAPI_URL=http://localhost:3284" \
|
||||
--env "CODER_AGENT_URL=${CODER_AGENT_URL}" \
|
||||
--env "CODER_AGENT_TOKEN=${CODER_AGENT_TOKEN}" \
|
||||
--force || echo "Warning: Failed to add Coder MCP server"
|
||||
else
|
||||
kiro-cli mcp add --name coder \
|
||||
--command "coder" \
|
||||
--agent "$ARG_AGENT_NAME" \
|
||||
--args "exp,mcp,server,--allowed-tools,coder_report_task" \
|
||||
--env "CODER_AGENT_URL=${CODER_AGENT_URL}" \
|
||||
--env "CODER_AGENT_TOKEN=${CODER_AGENT_TOKEN}" \
|
||||
--force || echo "Warning: Failed to add Coder MCP server"
|
||||
fi
|
||||
echo "Added Coder MCP server into $ARG_AGENT_NAME in Kiro CLI configuration"
|
||||
kiro-cli settings chat.defaultAgent "$ARG_AGENT_NAME"
|
||||
}
|
||||
|
||||
# Main execution
|
||||
install_kiro_cli
|
||||
extract_auth_tarball
|
||||
configure_agent
|
||||
|
||||
echo "Kiro CLI installation and configuration complete!"
|
||||
@@ -0,0 +1,67 @@
|
||||
#!/bin/bash
|
||||
# Start script for kiro-cli module
|
||||
|
||||
set -o errexit
|
||||
set -o pipefail
|
||||
|
||||
command_exists() {
|
||||
command -v "$1" > /dev/null 2>&1
|
||||
}
|
||||
|
||||
# Decode inputs
|
||||
ARG_AI_PROMPT=$(echo -n "${ARG_AI_PROMPT:-}" | base64 -d)
|
||||
ARG_TRUST_ALL_TOOLS=${ARG_TRUST_ALL_TOOLS:-true}
|
||||
ARG_MODULE_DIR_NAME=${ARG_MODULE_DIR_NAME:-.kiro}
|
||||
ARG_WORKDIR=${ARG_WORKDIR:-"$HOME"}
|
||||
ARG_REPORT_TASKS=${ARG_REPORT_TASKS:-true}
|
||||
ARG_SERVER_PARAMETERS=${ARG_SERVER_PARAMETERS:-""}
|
||||
|
||||
echo "--------------------------------"
|
||||
echo "ai_prompt: $ARG_AI_PROMPT"
|
||||
echo "trust_all_tools: $ARG_TRUST_ALL_TOOLS"
|
||||
echo "module_dir_name: $ARG_MODULE_DIR_NAME"
|
||||
echo "workdir: $ARG_WORKDIR"
|
||||
echo "report_tasks: ${ARG_REPORT_TASKS}"
|
||||
echo "--------------------------------"
|
||||
|
||||
mkdir -p "$HOME/$ARG_MODULE_DIR_NAME"
|
||||
|
||||
# Find Kiro CLI
|
||||
if command_exists kiro-cli; then
|
||||
KIRO_CMD=kiro-cli
|
||||
elif [ -x "$HOME/.local/bin/kiro-cli" ]; then
|
||||
KIRO_CMD="$HOME/.local/bin/kiro-cli"
|
||||
else
|
||||
echo "Error: Kiro CLI not found. Install it or set install_kiro_cli=true."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "$ARG_WORKDIR"
|
||||
cd "$ARG_WORKDIR"
|
||||
|
||||
# Set up environment
|
||||
export LANG=en_US.UTF-8
|
||||
export LC_ALL=en_US.UTF-8
|
||||
|
||||
# Build command arguments
|
||||
ARGS=(chat)
|
||||
|
||||
if [ "$ARG_TRUST_ALL_TOOLS" = "true" ]; then
|
||||
ARGS+=(--trust-all-tools)
|
||||
fi
|
||||
|
||||
# Log and run with agentapi integration
|
||||
printf "Running: %q %s\n" "$KIRO_CMD" "$(printf '%q ' "${ARGS[@]}")"
|
||||
|
||||
# If we have an AI prompt, we need to handle it specially
|
||||
if [ -n "$ARG_AI_PROMPT" ]; then
|
||||
if [ "$ARG_REPORT_TASKS" == "true" ]; then
|
||||
PROMPT="Every step of the way, report your progress using coder_report_task tool with proper summary and statuses. Your task at hand: $ARG_AI_PROMPT"
|
||||
else
|
||||
PROMPT="$ARG_AI_PROMPT"
|
||||
fi
|
||||
ARGS+=("$PROMPT")
|
||||
fi
|
||||
|
||||
# Use agentapi to manage the interactive session with initial prompt
|
||||
agentapi server ${ARG_SERVER_PARAMETERS} --term-width 67 --term-height 1190 -- "$KIRO_CMD" "${ARGS[@]}"
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "agent",
|
||||
"description": "This is an default agent config",
|
||||
"prompt": ${system_prompt},
|
||||
"mcpServers": {},
|
||||
"tools": [
|
||||
"read",
|
||||
"write",
|
||||
"shell",
|
||||
"aws",
|
||||
"@coder",
|
||||
"knowledge"
|
||||
],
|
||||
"toolAliases": {},
|
||||
"allowedTools": [
|
||||
"read",
|
||||
"@coder"
|
||||
],
|
||||
"resources": [
|
||||
"file://KiroQ.md",
|
||||
"file://README.md",
|
||||
"file://.kiro/steering/**/*.md"
|
||||
],
|
||||
"hooks": {},
|
||||
"toolsSettings": {},
|
||||
"useLegacyMcpJson": true
|
||||
}
|
||||
|
Before Width: | Height: | Size: 632 KiB After Width: | Height: | Size: 631 KiB |