Compare commits

..

5 Commits

Author SHA1 Message Date
35C4n0r 5be6a38f23 feat: migrate codex installation to use official install script (#908)
## Description

* migrate codex installation to use official install script
* remove npm installation

## Type of Change

- [ ] New module
- [ ] New template
- [ ] Bug fix
- [X] Feature/enhancement
- [ ] Documentation
- [ ] Other

## Module Information

**Path:** `registry/coder-labs/modules/codex` <br>**New version:**
`v5.1.0` <br>**Breaking change:** \[x\] Yes \[ \] No

## Testing & Validation

- [X] Tests pass (`bun test`)
- [X] Code formatted (`bun fmt`)
- [X] Changes tested locally

## Related Issues

Closes: coder/registry#905

---------

Co-authored-by: Atif Ali <atif@coder.com>
2026-06-05 21:56:23 +05:30
Ben Potter f80f36d674 feat: remove agent metadata for incus-nixos 2026-06-04 09:53:49 -05:00
Ben Potter d1d2d5b433 feat(bpmct/templates): add incus-vm and incus-nixos templates (#910)
## Description

Adds two new templates under `registry/bpmct` for running workspaces on
[Incus](https://linuxcontainers.org/incus/) (the open-source LXD fork).

### `incus-vm`

Provisions a full KVM virtual machine on a remote Incus host using
cloud-init. This is a VM variant of the existing `coder/incus` system
container template.

Key differences from `coder/incus`:
- Launches a **virtual machine** (`type = "virtual-machine"`) rather
than a system container
- Supports **remote Incus hosts** via `incus remote add` — the Coder
provisioner does not need to be co-located with the Incus host
- `arch` parameter (`amd64` / `arm64`) threads through the agent, image
fetch, and Incus architecture hint — so the same template works on both
x86-64 and aarch64 hosts
- Token rotation handled on every workspace start via a `null_resource`
provisioner (necessary since cloud-init only runs once on first boot)

### `incus-nixos`

Provisions a NixOS virtual machine on a remote Incus host. NixOS
requires a different provisioning approach from the cloud-init-based
`incus-vm` template.

Key design points:
- Uses Incus's official NixOS images (`nixos/<channel>`)
- Provisions entirely via `nixos-rebuild switch` — no cloud-init
- Writes `/etc/nixos/coder.nix` and `/etc/nixos/configuration.nix` via
`incus exec` on first boot
- Idempotent: subsequent starts skip the rebuild and just refresh the
agent token
- `nixos_channel` is a Terraform `variable` (admin sets at push time);
no user-facing image picker since the whole point of the template is
NixOS

## Type of Change

- [ ] New module
- [x] New template
- [ ] Bug fix
- [ ] Feature/enhancement
- [ ] Documentation
- [ ] Other

## Template Information

**Paths:**
- `registry/bpmct/templates/incus-vm`
- `registry/bpmct/templates/incus-nixos`

## Testing & Validation

- [x] Tests pass (`bun test`)
- [x] Code formatted (`bun fmt`)
- [x] Changes tested locally (both templates deployed and workspaces
verified healthy on aarch64 Raspberry Pi provisioners)

## Related Issues

None

## Video

https://www.loom.com/share/f009972a51af452dbbe9eda9f9760d3e
2026-06-04 09:44:37 -05:00
dependabot[bot] 358ca6804b chore(deps): bump crate-ci/typos from 1.46.3 to 1.47.0 in the github-actions group (#906)
Signed-off-by: dependabot[bot] <support@github.com>
2026-06-02 13:19:02 +05:00
35C4n0r 94203b2c8b fix(coder/modules/dotfiles): allow tilde in DOTFILES_URI shell validation (#904)
Fixes https://github.com/coder/registry/issues/762

## Problem

The shell-side URI validation regex in `run.sh` did not include `~` in
the allowed character set. URLs containing tilde paths (common in
Bitbucket Server personal repositories, e.g.
`ssh://git@bitbucket.example.org:7999/~user/repo.git`) were rejected at
runtime with `ERROR: DOTFILES_URI contains invalid characters`.

The Terraform-side validations in `main.tf` already allowed `~`, so the
inconsistency only surfaced when the script actually ran in a workspace.

## Changes

- **`run.sh`**: Added `~` to the character class in the shell validation
regex, making it consistent with the three Terraform regex patterns in
`main.tf`.
- **`main.test.ts`**: The "accepts valid git URL formats" test now also
executes the rendered shell script and asserts that the shell-side
validation does not reject any of the valid URLs. This closes the
coverage gap that let the Terraform/shell inconsistency go undetected.

> 🤖 Generated by Coder Agents

---------

Authored-by: Jay Kumar <jay.kumar@coder.com>
2026-06-01 17:48:19 +05:30
13 changed files with 802 additions and 48 deletions
+1 -1
View File
@@ -93,7 +93,7 @@ jobs:
- name: Validate formatting
run: bun fmt:ci
- name: Check for typos
uses: crate-ci/typos@7b04f660f4ee4f048d18fd341887cf28dfbedfe2 # v1.46.3
uses: crate-ci/typos@f8a58b6b53f2279f71eb605f03a4ae4d10608f45 # v1.47.0
with:
config: .github/typos.toml
validate-readme-files:
+1 -1
View File
@@ -8,4 +8,4 @@ status: community
# Ben Potter
Tinkerer and Product Manager at Coder. Building modules to make dev environments better.
Tinkerer and Product Manager at Coder.
@@ -0,0 +1,75 @@
---
display_name: Incus NixOS VM
description: Run a NixOS virtual machine on a local Incus host
icon: ../../../../.icons/lxc.svg
verified: false
tags: [local, incus, vm, nixos]
---
# Incus NixOS VM
Provision a NixOS KVM virtual machine on an [Incus](https://linuxcontainers.org/incus/) host. The image is pulled from [images.linuxcontainers.org](https://images.linuxcontainers.org) and cached on the host.
NixOS does not support cloud-init. This template uses `nixos-rebuild switch` via `incus exec` to configure the workspace user and start the Coder agent. The rebuild only runs on first boot; subsequent starts rotate the agent token and restart the service directly.
## Prerequisites
### 1. Install Incus on the VM host
Follow the [Incus installation guide](https://linuxcontainers.org/incus/docs/main/installing/) for your distro. On Debian/Ubuntu:
```sh
sudo apt-get install incus
sudo incus admin init
```
### 2. Run the Coder provisioner on the same machine
This template uses the local Incus socket, so the Coder provisioner must run on the same machine as Incus. See [provisioner daemons](https://coder.com/docs/admin/provisioners).
### 3. Ensure the host has KVM
```sh
ls /dev/kvm
```
If the device is missing, enable virtualisation in your BIOS/UEFI or, in a nested setup, pass through the `kvm` module.
### 4. Create a storage pool (if needed)
```sh
incus storage create default btrfs
incus storage list
```
### 5. Push the template
```sh
# amd64 host:
coder templates push incus-nixos --directory . --variable arch=amd64
# arm64 host:
coder templates push incus-nixos --directory . --variable arch=arm64
```
The `storage_pool` variable defaults to `default`. Override if needed:
```sh
coder templates push incus-nixos --directory . \
--variable arch=arm64 \
--variable storage_pool=fast-nvme
```
The `nixos_channel` variable controls which NixOS channel is used for `nixos-rebuild`. It must match the image version (default: `nixos-25.11`).
## Usage
1. Create a workspace from this template and choose CPU, memory, and disk.
2. Connect via `coder ssh <workspace>` or use VS Code in the browser via the [VS Code extension](https://coder.com/docs/user-guides/workspace-access/vscode).
3. Install packages declaratively by editing `/etc/nixos/coder.nix` and running `sudo nixos-rebuild switch`.
## Notes
- `code-server` is not installed by this template. The Coder agent is started as a plain systemd service. Install editors via nix packages in `coder.nix`.
- The first workspace start takes several minutes while `nixos-rebuild switch` runs. Subsequent starts are fast.
- Advanced Incus remotes (targeting a separate host over the network) are not supported by this template. See the `incus-vm` template for guidance on adding remote support.
@@ -0,0 +1,287 @@
terraform {
required_providers {
coder = {
source = "coder/coder"
version = ">= 2.4.0"
}
incus = {
source = "lxc/incus"
version = "~> 1.0"
}
null = {
source = "hashicorp/null"
version = "~> 3.0"
}
}
}
provider "incus" {}
variable "arch" {
description = "CPU architecture of the VM host. Set this when pushing the template to match your Incus host. Valid values: amd64, arm64."
type = string
default = "amd64"
validation {
condition = contains(["amd64", "arm64"], var.arch)
error_message = "arch must be amd64 or arm64."
}
}
variable "storage_pool" {
description = "Incus storage pool for the root disk. Run `incus storage list` on the host to see available pools."
type = string
default = "default"
}
variable "nixos_channel" {
description = "NixOS channel to use for nixos-rebuild. Must match the image version (e.g. nixos-25.11)."
type = string
default = "nixos-25.11"
}
data "coder_workspace" "me" {}
data "coder_workspace_owner" "me" {}
data "coder_parameter" "cpu" {
name = "cpu"
display_name = "CPU"
description = "Number of vCPUs."
type = "number"
default = 2
icon = "https://raw.githubusercontent.com/matifali/logos/main/cpu-3.svg"
mutable = true
order = 1
validation {
min = 1
max = 16
}
}
data "coder_parameter" "memory" {
name = "memory"
display_name = "Memory (GB)"
type = "number"
default = 4
icon = "/icon/memory.svg"
mutable = true
order = 2
validation {
min = 1
max = 64
}
}
data "coder_parameter" "disk" {
name = "disk"
display_name = "Disk (GB)"
type = "number"
default = 30
icon = "/icon/database.svg"
mutable = true
order = 3
validation {
min = 10
max = 500
}
}
locals {
workspace_user = lower(data.coder_workspace_owner.me.name)
agent_token = data.coder_workspace.me.start_count == 1 ? coder_agent.main[0].token : ""
agent_init_script = data.coder_workspace.me.start_count == 1 ? coder_agent.main[0].init_script : ""
# NixOS images on images.linuxcontainers.org use "nixos/<ver>" with no arch suffix.
# The channel version (e.g. "25.11") is extracted from var.nixos_channel.
nixos_version = replace(var.nixos_channel, "nixos-", "")
image_alias = "nixos/${local.nixos_version}"
# PATH required for incus exec commands on NixOS VMs. The Nix store is not
# on the default system PATH until after the first nixos-rebuild switch.
nix_path = "/nix/var/nix/profiles/system/sw/bin:/run/current-system/sw/bin:/nix/var/nix/profiles/default/bin:/run/wrappers/bin"
}
resource "coder_agent" "main" {
count = data.coder_workspace.me.start_count
arch = var.arch
os = "linux"
}
resource "incus_image" "nixos" {
source_image = {
remote = "images"
name = local.image_alias
type = "virtual-machine"
architecture = var.arch == "amd64" ? "x86_64" : "aarch64"
}
}
resource "incus_instance" "dev" {
running = data.coder_workspace.me.start_count == 1
name = "coder-${lower(data.coder_workspace_owner.me.name)}-${lower(data.coder_workspace.me.name)}"
image = incus_image.nixos.fingerprint
type = "virtual-machine"
config = {
"limits.cpu" = tostring(data.coder_parameter.cpu.value)
"limits.memory" = "${data.coder_parameter.memory.value}GiB"
"security.secureboot" = false
"boot.autostart" = data.coder_workspace.me.start_count == 1
"user.coder-agent-token" = local.agent_token
}
device {
name = "root"
type = "disk"
properties = {
path = "/"
pool = var.storage_pool
size = "${data.coder_parameter.disk.value}GiB"
}
}
lifecycle {
ignore_changes = [
config["user.coder-agent-token"],
image,
]
}
}
# NixOS does not support cloud-init. Provisioning steps:
# 1. Wait for the incus-agent to be ready (virtio serial channel).
# 2. Push the Coder agent binary (/opt/coder/init) and token env file.
# 3. On first boot: write coder.nix and an updated configuration.nix
# that imports the Incus VM module, then run nixos-rebuild switch.
# Leave a marker so subsequent starts skip the rebuild.
# 4. On subsequent starts: overwrite init.env + restart coder-agent.
resource "null_resource" "provision" {
count = data.coder_workspace.me.start_count
triggers = {
agent_token = local.agent_token
instance = incus_instance.dev.name
}
depends_on = [incus_instance.dev]
provisioner "local-exec" {
command = <<-EOT
set -e
INSTANCE="${incus_instance.dev.name}"
WUSER="${local.workspace_user}"
NIX_PATH="${local.nix_path}"
CHANNEL="${var.nixos_channel}"
echo "Waiting for incus-agent..."
for i in $(seq 1 60); do
incus exec "$INSTANCE" -- true 2>/dev/null && break
echo " attempt $i/60..."
sleep 5
done
echo "Pushing Coder agent binary..."
TMPDIR=$(mktemp -d)
echo "${base64encode(local.agent_init_script)}" | base64 -d > "$TMPDIR/init"
chmod 755 "$TMPDIR/init"
incus exec "$INSTANCE" -- env PATH="$NIX_PATH" mkdir -p /opt/coder
incus file push "$TMPDIR/init" "$INSTANCE/opt/coder/init"
incus exec "$INSTANCE" -- env PATH="$NIX_PATH" chmod 755 /opt/coder/init
rm -rf "$TMPDIR"
printf 'CODER_AGENT_TOKEN=${local.agent_token}\nCODER_AGENT_URL=${data.coder_workspace.me.access_url}\n' \
| incus file push - "$INSTANCE/opt/coder/init.env" --mode 0600
# Fast path: already provisioned -- just rotate token and restart.
if incus exec "$INSTANCE" -- test -f /etc/nixos/.coder-provisioned 2>/dev/null; then
echo "Already provisioned; restarting coder-agent..."
incus exec "$INSTANCE" -- env PATH="$NIX_PATH" systemctl restart coder-agent.service
echo "Done."
exit 0
fi
# First boot: write NixOS config and rebuild.
echo "Writing /etc/nixos/coder.nix..."
cat <<'NIXEOF' | incus exec "$INSTANCE" -- env PATH="$NIX_PATH" bash -c 'cat > /etc/nixos/coder.nix'
{ config, pkgs, lib, ... }:
{
users.users."${local.workspace_user}" = {
isNormalUser = true;
uid = 1000;
home = "/home/${local.workspace_user}";
shell = pkgs.bash;
extraGroups = [ "wheel" ];
};
security.sudo.wheelNeedsPassword = false;
nix.settings.trusted-users = [ "root" "${local.workspace_user}" ];
systemd.services.coder-agent = {
description = "Coder Agent";
after = [ "network-online.target" ];
wants = [ "network-online.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
User = "${local.workspace_user}";
EnvironmentFile = "/opt/coder/init.env";
ExecStart = "/opt/coder/init";
Environment = "PATH=/run/current-system/sw/bin:/run/wrappers/bin:/usr/local/bin:/usr/bin:/bin";
Restart = "always";
RestartSec = 10;
TimeoutStopSec = 90;
KillMode = "process";
OOMScoreAdjust = -900;
SyslogIdentifier = "coder-agent";
};
};
}
NIXEOF
echo "Writing /etc/nixos/configuration.nix..."
cat <<'NIXEOF' | incus exec "$INSTANCE" -- env PATH="$NIX_PATH" bash -c 'cat > /etc/nixos/configuration.nix'
{ modulesPath, ... }:
{
imports = [
"$${modulesPath}/virtualisation/incus-virtual-machine.nix"
./incus.nix
./coder.nix
];
systemd.network = {
enable = true;
networks."50-enp5s0" = {
matchConfig.Name = "enp5s0";
networkConfig = {
DHCP = "ipv4";
IPv6AcceptRA = true;
};
linkConfig.RequiredForOnline = "routable";
};
};
system.stateVersion = "${local.nixos_version}";
}
NIXEOF
echo "Restoring nixos channel if needed..."
incus exec "$INSTANCE" -- env PATH="$NIX_PATH" HOME=/root bash -c "
if [ ! -e /nix/var/nix/profiles/per-user/root/channels/nixos ]; then
nix-channel --add https://channels.nixos.org/$CHANNEL nixos
nix-channel --update nixos
fi
"
echo "Running nixos-rebuild switch..."
incus exec "$INSTANCE" -- env PATH="$NIX_PATH" HOME=/root bash -c "
NIXOS_CH=\$(ls -d /nix/var/nix/profiles/per-user/root/channels/nixos 2>/dev/null || echo '')
nixos-rebuild switch -I nixpkgs=\"\$NIXOS_CH\" -I nixos-config=/etc/nixos/configuration.nix \
|| { EC=\$?; [ \$EC -eq 4 ] || exit \$EC; }
"
incus exec "$INSTANCE" -- env PATH="$NIX_PATH" touch /etc/nixos/.coder-provisioned
incus exec "$INSTANCE" -- env PATH="$NIX_PATH" bash -c \
"mkdir -p /home/$WUSER && chown 1000:1000 /home/$WUSER"
echo "NixOS provisioning complete."
EOT
}
}
@@ -0,0 +1,96 @@
---
display_name: Incus VM
description: Run a full virtual machine on a local Incus host
icon: ../../../../.icons/lxc.svg
verified: false
tags: [local, incus, vm, virtual-machine]
---
# Incus VM
Provision a full KVM virtual machine on an [Incus](https://linuxcontainers.org/incus/) host. Unlike the system container template, this creates an isolated VM with its own kernel. Images are pulled from [images.linuxcontainers.org](https://images.linuxcontainers.org) and cached on the host.
## Prerequisites
### 1. Install Incus on the VM host
Follow the [Incus installation guide](https://linuxcontainers.org/incus/docs/main/installing/) for your distro. On Debian/Ubuntu:
```sh
sudo apt-get install incus
sudo incus admin init
```
Verify it's working:
```sh
incus list
```
### 2. Run the Coder provisioner on the same machine
This template uses Incus via the local Unix socket, so the Coder provisioner (or `coderd` itself) must run on the same machine as Incus. The simplest setup is a [provisioner daemon](https://coder.com/docs/admin/provisioners) running directly on the Incus host.
### 3. Set the architecture when pushing the template
The `arch` variable must match your Incus host's CPU architecture. Pass it when pushing:
```sh
# For amd64 (x86-64) hosts:
coder templates push incus-vm --directory . --variable arch=amd64
# For arm64 (aarch64) hosts:
coder templates push incus-vm --directory . --variable arch=arm64
```
### 4. Ensure the VM host has KVM
VMs require hardware virtualisation. Check on the host:
```sh
ls /dev/kvm
```
If the device is missing, enable virtualisation in your BIOS/UEFI or, in a nested setup, pass through the `kvm` module.
### 5. Create a storage pool (if needed)
The template uses an Incus storage pool to back the VM root disk. If you don't already have one:
```sh
incus storage create default btrfs
```
List existing pools:
```sh
incus storage list
```
The pool name defaults to `default` and can be overridden when pushing the template with `--variable storage_pool=<name>`.
## Usage
1. Push this template to your Coder deployment:
```sh
coder templates push incus-vm --directory . --variable arch=amd64
```
2. Create a workspace and select an image and resource sizes.
3. Connect via `coder ssh <workspace>` or open VS Code in the browser.
## Advanced: using a remote Incus host
By default this template connects to the local Incus socket. If you want the provisioner to target a separate Incus host over the network, add a `remote` parameter and use `incus remote add` to register the host on the provisioner machine:
```sh
# On the Incus host — generate a trust token:
incus config trust add coder-provisioner
# On the provisioner — add the remote:
incus remote add my-server https://<host-ip>:8443 --token <paste-token>
```
Then update `main.tf` to accept a `remote` parameter and pass it to the `incus_image` and `incus_instance` resources. See the [Incus remote docs](https://linuxcontainers.org/incus/docs/main/remotes/) for details.
+304
View File
@@ -0,0 +1,304 @@
terraform {
required_providers {
coder = {
source = "coder/coder"
version = ">= 2.4.0"
}
incus = {
source = "lxc/incus"
version = "~> 1.0"
}
null = {
source = "hashicorp/null"
version = "~> 3.0"
}
}
}
provider "incus" {}
variable "arch" {
description = "CPU architecture of the VM host. Set this when pushing the template to match your Incus host. Valid values: amd64, arm64."
type = string
default = "amd64"
validation {
condition = contains(["amd64", "arm64"], var.arch)
error_message = "arch must be amd64 or arm64."
}
}
variable "storage_pool" {
description = "Incus storage pool for the root disk. Run `incus storage list` on the host to see available pools."
type = string
default = "default"
}
data "coder_workspace" "me" {}
data "coder_workspace_owner" "me" {}
data "coder_parameter" "image" {
name = "image"
display_name = "Image"
description = "Base image name from images.linuxcontainers.org (e.g. `ubuntu/noble/cloud`). The template architecture is appended automatically."
type = "string"
default = "ubuntu/noble/cloud"
icon = "/icon/image.svg"
mutable = true
order = 1
option {
name = "Ubuntu 24.04 LTS (Noble)"
value = "ubuntu/noble/cloud"
icon = "/icon/ubuntu.svg"
}
option {
name = "Ubuntu 22.04 LTS (Jammy)"
value = "ubuntu/jammy/cloud"
icon = "/icon/ubuntu.svg"
}
option {
name = "Debian 12 (Bookworm)"
value = "debian/12/cloud"
icon = "/icon/debian.svg"
}
}
data "coder_parameter" "cpu" {
name = "cpu"
display_name = "CPU"
description = "Number of vCPUs."
type = "number"
default = 2
icon = "https://raw.githubusercontent.com/matifali/logos/main/cpu-3.svg"
mutable = true
order = 2
validation {
min = 1
max = 16
}
}
data "coder_parameter" "memory" {
name = "memory"
display_name = "Memory (GB)"
type = "number"
default = 4
icon = "/icon/memory.svg"
mutable = true
order = 3
validation {
min = 1
max = 64
}
}
data "coder_parameter" "disk" {
name = "disk"
display_name = "Disk (GB)"
type = "number"
default = 30
icon = "/icon/database.svg"
mutable = true
order = 4
validation {
min = 10
max = 500
}
}
resource "coder_agent" "main" {
count = data.coder_workspace.me.start_count
arch = var.arch
os = "linux"
metadata {
display_name = "CPU Usage"
key = "0_cpu_usage"
script = "coder stat cpu"
interval = 10
timeout = 1
}
metadata {
display_name = "RAM Usage"
key = "1_ram_usage"
script = "coder stat mem"
interval = 10
timeout = 1
}
metadata {
display_name = "Disk"
key = "2_disk"
script = "coder stat disk --path /"
interval = 60
timeout = 1
}
}
module "code-server" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/code-server/coder"
version = "~> 1.0"
agent_id = coder_agent.main[0].id
}
resource "incus_image" "image" {
source_image = {
remote = "images"
name = "${data.coder_parameter.image.value}/${var.arch}"
type = "virtual-machine"
architecture = var.arch == "amd64" ? "x86_64" : "aarch64"
}
}
resource "incus_instance" "dev" {
running = data.coder_workspace.me.start_count == 1
name = "coder-${lower(data.coder_workspace_owner.me.name)}-${lower(data.coder_workspace.me.name)}"
image = incus_image.image.fingerprint
type = "virtual-machine"
config = {
"limits.cpu" = tostring(data.coder_parameter.cpu.value)
"limits.memory" = "${data.coder_parameter.memory.value}GiB"
"security.secureboot" = false
"boot.autostart" = data.coder_workspace.me.start_count == 1
"user.coder-agent-token" = local.agent_token
"cloud-init.user-data" = <<-EOF
#cloud-config
hostname: ${lower(data.coder_workspace.me.name)}
users:
- name: ${local.workspace_user}
uid: 1000
groups: sudo
shell: /bin/bash
sudo: ALL=(ALL) NOPASSWD:ALL
write_files:
- path: /opt/coder/init
permissions: "0755"
encoding: b64
content: ${base64encode(local.agent_init_script)}
- path: /opt/coder/init.env
permissions: "0600"
content: |
CODER_AGENT_TOKEN=${local.agent_token}
CODER_AGENT_URL=${data.coder_workspace.me.access_url}
- path: /etc/systemd/system/coder-agent.service
permissions: "0644"
content: |
[Unit]
Description=Coder Agent
After=network-online.target
Wants=network-online.target
[Service]
User=${local.workspace_user}
EnvironmentFile=/opt/coder/init.env
ExecStart=/opt/coder/init
Restart=always
RestartSec=10
TimeoutStopSec=90
KillMode=process
OOMScoreAdjust=-900
SyslogIdentifier=coder-agent
[Install]
WantedBy=multi-user.target
runcmd:
- systemctl enable --now coder-agent.service
EOF
}
device {
name = "root"
type = "disk"
properties = {
path = "/"
pool = var.storage_pool
size = "${data.coder_parameter.disk.value}GiB"
}
}
lifecycle {
ignore_changes = [
config["cloud-init.user-data"],
config["user.coder-agent-token"],
image,
]
}
}
resource "null_resource" "token_refresh" {
count = data.coder_workspace.me.start_count
triggers = {
agent_token = local.agent_token
instance = incus_instance.dev.name
}
depends_on = [incus_instance.dev]
provisioner "local-exec" {
command = <<-EOT
INSTANCE="${incus_instance.dev.name}"
echo "Waiting for VM agent..."
for i in $(seq 1 40); do
incus exec "$INSTANCE" -- true 2>/dev/null && break
echo "Attempt $i: not ready, waiting..."
sleep 5
done
echo "Waiting for cloud-init..."
incus exec "$INSTANCE" -- bash -c '
for i in $(seq 1 60); do
[ -f /var/lib/cloud/instance/boot-finished ] && break
sleep 5
done
'
echo "Refreshing agent token..."
printf 'CODER_AGENT_TOKEN=${local.agent_token}\nCODER_AGENT_URL=${data.coder_workspace.me.access_url}\n' \
| incus exec "$INSTANCE" -- bash -c 'cat > /opt/coder/init.env && chmod 600 /opt/coder/init.env'
incus exec "$INSTANCE" -- systemctl restart coder-agent.service
EOT
}
}
resource "coder_metadata" "info" {
count = data.coder_workspace.me.start_count
resource_id = incus_instance.dev.name
item {
key = "instance"
value = incus_instance.dev.name
}
item {
key = "image"
value = "images:${data.coder_parameter.image.value}/${var.arch}"
}
item {
key = "storage_pool"
value = var.storage_pool
}
item {
key = "arch"
value = var.arch
}
item {
key = "cpu"
value = tostring(data.coder_parameter.cpu.value)
}
item {
key = "memory"
value = "${data.coder_parameter.memory.value} GiB"
}
item {
key = "disk"
value = "${data.coder_parameter.disk.value} GiB"
}
}
locals {
workspace_user = lower(data.coder_workspace_owner.me.name)
agent_token = data.coder_workspace.me.start_count == 1 ? coder_agent.main[0].token : ""
agent_init_script = data.coder_workspace.me.start_count == 1 ? coder_agent.main[0].init_script : ""
}
+6 -6
View File
@@ -13,14 +13,14 @@ Install and configure the [Codex CLI](https://github.com/openai/codex) in your w
```tf
module "codex" {
source = "registry.coder.com/coder-labs/codex/coder"
version = "5.0.0"
version = "5.1.0"
agent_id = coder_agent.main.id
openai_api_key = var.openai_api_key
}
```
> [!WARNING]
> If upgrading from v4.x.x of this module: v5 is a major refactor that drops support for [Coder Tasks](https://coder.com/docs/ai-coder/tasks) and [Boundary](https://coder.com/docs/ai-coder/agent-firewall). v5 also assumes npm is pre-installed; it no longer bootstraps Node.js. Keep using v4.x.x if you depend on them. See the [PR description](https://github.com/coder/registry/pull/879) for a full migration guide.
> If upgrading from v4.x.x of this module: v5 is a major refactor that drops support for [Coder Tasks](https://coder.com/docs/ai-coder/tasks) and [Boundary](https://coder.com/docs/ai-coder/agent-firewall). Keep using v4.x.x if you depend on them. See the [PR description](https://github.com/coder/registry/pull/879) for a full migration guide.
## Examples
@@ -33,7 +33,7 @@ locals {
module "codex" {
source = "registry.coder.com/coder-labs/codex/coder"
version = "5.0.0"
version = "5.1.0"
agent_id = coder_agent.main.id
workdir = local.codex_workdir
openai_api_key = var.openai_api_key
@@ -64,7 +64,7 @@ resource "coder_app" "codex" {
```tf
module "codex" {
source = "registry.coder.com/coder-labs/codex/coder"
version = "5.0.0"
version = "5.1.0"
agent_id = coder_agent.main.id
workdir = "/home/coder/project"
enable_ai_gateway = true
@@ -88,7 +88,7 @@ When `enable_ai_gateway = true`, the module configures Codex to use the `aigatew
```tf
module "codex" {
source = "registry.coder.com/coder-labs/codex/coder"
version = "5.0.0"
version = "5.1.0"
agent_id = coder_agent.main.id
workdir = "/home/coder/project"
openai_api_key = var.openai_api_key
@@ -117,7 +117,7 @@ The module exposes the `scripts` output: an ordered list of `coder exp sync` nam
```tf
module "codex" {
source = "registry.coder.com/coder-labs/codex/coder"
version = "5.0.0"
version = "5.1.0"
agent_id = coder_agent.main.id
openai_api_key = var.openai_api_key
}
@@ -189,7 +189,7 @@ describe("codex", async () => {
});
test("install-codex-version", async () => {
const version = "0.10.0";
const version = "0.134.0";
const { id, coderEnvVars, scripts } = await setup({
skipCodexMock: true,
moduleVariables: {
+1 -1
View File
@@ -137,7 +137,7 @@ locals {
EOF
install_script = templatefile("${path.module}/scripts/install.sh.tftpl", {
ARG_INSTALL = tostring(var.install_codex)
ARG_CODEX_VERSION = var.codex_version != "" ? base64encode(var.codex_version) : ""
ARG_CODEX_VERSION = var.codex_version
ARG_WORKDIR = local.workdir != "" ? base64encode(local.workdir) : ""
ARG_BASE_CONFIG_TOML = var.base_config_toml != "" ? base64encode(var.base_config_toml) : ""
ARG_MCP = var.mcp != "" ? base64encode(var.mcp) : ""
@@ -9,7 +9,7 @@ command_exists() {
}
ARG_INSTALL='${ARG_INSTALL}'
ARG_CODEX_VERSION=$(echo -n '${ARG_CODEX_VERSION}' | base64 -d)
ARG_CODEX_VERSION='${ARG_CODEX_VERSION}'
ARG_WORKDIR=$(echo -n '${ARG_WORKDIR}' | base64 -d)
ARG_BASE_CONFIG_TOML=$(echo -n '${ARG_BASE_CONFIG_TOML}' | base64 -d)
ARG_MCP=$(echo -n '${ARG_MCP}' | base64 -d)
@@ -51,8 +51,6 @@ function ensure_codex_in_path() {
local CODEX_BIN=""
if command -v codex > /dev/null 2>&1; then
CODEX_BIN=$(command -v codex)
elif [ -x "$HOME/.npm-global/bin/codex" ]; then
CODEX_BIN="$HOME/.npm-global/bin/codex"
fi
if [ -z "$${CODEX_BIN}" ] || [ ! -x "$${CODEX_BIN}" ]; then
@@ -78,35 +76,9 @@ function install_codex() {
return
fi
if [ -s "$HOME/.nvm/nvm.sh" ]; then
export NVM_DIR="$HOME/.nvm"
. "$NVM_DIR/nvm.sh"
fi
# Detect a package manager for global installs.
if command_exists npm; then
PKG_INSTALL="npm install -g"
if ! command_exists nvm; then
mkdir -p "$HOME/.npm-global"
npm config set prefix "$HOME/.npm-global"
export PATH="$HOME/.npm-global/bin:$PATH"
fi
elif command_exists pnpm; then
PKG_INSTALL="pnpm add -g"
elif command_exists bun; then
PKG_INSTALL="bun add -g"
else
echo "Error: npm, pnpm, or bun is required to install Codex. Install one of them first or set install_codex = false."
exit 1
fi
printf "%s Installing Codex CLI\n" "$${BOLD}"
if [ -n "$${ARG_CODEX_VERSION}" ]; then
$PKG_INSTALL "@openai/codex@$${ARG_CODEX_VERSION}"
else
$PKG_INSTALL "@openai/codex"
fi
curl -fsSL https://chatgpt.com/codex/install.sh | CODEX_RELEASE="$${ARG_CODEX_VERSION}" CODEX_NON_INTERACTIVE=1 sh
export PATH="$HOME/.local/bin:$PATH"
printf "%s Installed Codex CLI: %s\n" "$${BOLD}" "$(codex --version)"
ensure_codex_in_path
}
+6 -6
View File
@@ -18,7 +18,7 @@ Under the hood, this module uses the [coder dotfiles](https://coder.com/docs/v2/
module "dotfiles" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/dotfiles/coder"
version = "1.4.1"
version = "1.4.2"
agent_id = coder_agent.example.id
}
```
@@ -31,7 +31,7 @@ module "dotfiles" {
module "dotfiles" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/dotfiles/coder"
version = "1.4.1"
version = "1.4.2"
agent_id = coder_agent.example.id
}
```
@@ -42,7 +42,7 @@ module "dotfiles" {
module "dotfiles" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/dotfiles/coder"
version = "1.4.1"
version = "1.4.2"
agent_id = coder_agent.example.id
user = "root"
}
@@ -54,14 +54,14 @@ module "dotfiles" {
module "dotfiles" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/dotfiles/coder"
version = "1.4.1"
version = "1.4.2"
agent_id = coder_agent.example.id
}
module "dotfiles-root" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/dotfiles/coder"
version = "1.4.1"
version = "1.4.2"
agent_id = coder_agent.example.id
user = "root"
dotfiles_uri = module.dotfiles.dotfiles_uri
@@ -90,7 +90,7 @@ You can set a default dotfiles repository for all users by setting the `default_
module "dotfiles" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/dotfiles/coder"
version = "1.4.1"
version = "1.4.2"
agent_id = coder_agent.example.id
default_dotfiles_uri = "https://github.com/coder/dotfiles"
}
@@ -1,9 +1,11 @@
import { describe, expect, it } from "bun:test";
import {
findResourceInstance,
runTerraformApply,
runTerraformInit,
testRequiredVariables,
} from "~test";
import { readableStreamToText, spawn } from "bun";
describe("dotfiles", async () => {
await runTerraformInit(import.meta.dir);
@@ -34,6 +36,24 @@ describe("dotfiles", async () => {
dotfiles_uri: url,
});
expect(state.outputs.dotfiles_uri.value).toBe(url);
// Run the rendered shell script to verify the shell-side URI
// validation also accepts the URL. The script will fail later
// (no coder binary available), but it must not fail at the
// URI validation step.
const instance = findResourceInstance(state, "coder_script");
const proc = spawn(["bash", "-c", instance.script], {
stdout: "pipe",
stderr: "pipe",
});
const stderr = await readableStreamToText(proc.stderr);
await proc.exited;
expect(stderr).not.toContain(
"ERROR: DOTFILES_URI contains invalid characters",
);
expect(stderr).not.toContain(
"ERROR: DOTFILES_URI must be a valid repository URL",
);
}
});
+1 -1
View File
@@ -9,7 +9,7 @@ DOTFILES_BRANCH="${DOTFILES_BRANCH}"
# Validate DOTFILES_URI to prevent command injection (defense in depth)
if [ -n "$DOTFILES_URI" ]; then
# shellcheck disable=SC2250
if [[ "$DOTFILES_URI" =~ [^a-zA-Z0-9._/:@-] ]]; then
if [[ "$DOTFILES_URI" =~ [^a-zA-Z0-9._/:@~-] ]]; then
echo "ERROR: DOTFILES_URI contains invalid characters" >&2
exit 1
fi