Compare commits

..

15 Commits

Author SHA1 Message Date
Marcin Tojek 9525e2d4d1 cleanup 2025-12-03 15:49:43 +01:00
Marcin Tojek e07e289698 test 2025-12-03 15:28:23 +01:00
Marcin Tojek 1be384252e test 2025-12-03 15:26:01 +01:00
Marcin Tojek 8fa6be0edf test 2025-12-03 15:14:04 +01:00
Marcin Tojek 82c86db196 test 2025-12-03 15:12:00 +01:00
Marcin Tojek 019d735fdd fix: rename variables to HTTP_SERVER_PORT and HTTP_SERVER_LOG_PATH 2025-12-03 14:07:29 +00:00
Marcin Tojek 520a2947e1 Revert "fixes"
This reverts commit abe133afe8.
2025-12-03 15:01:42 +01:00
Marcin Tojek abe133afe8 fixes 2025-12-03 14:56:35 +01:00
Marcin Tojek 5335c85495 fixes 2025-12-03 14:55:09 +01:00
Marcin Tojek d6a18d8419 fix: restore LOG_PATH and PORT variable assignments from templatefile 2025-12-03 13:52:40 +00:00
Marcin Tojek d03588356e fix: remove running process check 2025-12-03 13:22:04 +00:00
Marcin Tojek 6e94ecc165 fix: use venv instead of pip install --user
- Remove automatic Python installation - user must have Python 3.11+ in image
- Use virtual environment (~/.open-webui-venv) to avoid externally-managed-environment error
- Add set -e to fail script on errors
- Update README with installation instructions
2025-12-03 11:42:17 +00:00
Marcin Tojek 25636524d3 fixes 2 2025-12-03 12:34:01 +01:00
Marcin Tojek 504be92cf4 fixes 2025-12-03 12:19:25 +01:00
Marcin Tojek 35f93c663f feat: add open-webui module for coder-labs
- Installs Open WebUI via pip (Python 3.11+ required)
- Auto-installs Python 3.11 from deadsnakes PPA if not available
- Configurable port, share level, logging
- Subdomain support for clean URLs
2025-12-03 09:37:54 +00:00
22 changed files with 248 additions and 1231 deletions
+14 -11
View File
@@ -1,16 +1,15 @@
{
"lockfileVersion": 1,
"configVersion": 0,
"workspaces": {
"": {
"name": "registry",
"devDependencies": {
"@types/bun": "^1.3.4",
"bun-types": "^1.3.4",
"dedent": "^1.7.0",
"@types/bun": "^1.2.21",
"bun-types": "^1.2.21",
"dedent": "^1.6.0",
"gray-matter": "^4.0.3",
"marked": "^16.4.2",
"prettier": "^3.7.4",
"marked": "^16.2.0",
"prettier": "^3.6.2",
"prettier-plugin-sh": "^0.18.0",
"prettier-plugin-terraform-formatter": "^1.2.1",
"shellcheck": "^4.1.0",
@@ -31,10 +30,12 @@
"@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="],
"@types/bun": ["@types/bun@1.3.4", "", { "dependencies": { "bun-types": "1.3.4" } }, "sha512-EEPTKXHP+zKGPkhRLv+HI0UEX8/o+65hqARxLy8Ov5rIxMBPNTjeZww00CIihrIQGEQBYg+0roO5qOnS/7boGA=="],
"@types/bun": ["@types/bun@1.2.21", "", { "dependencies": { "bun-types": "1.2.21" } }, "sha512-NiDnvEqmbfQ6dmZ3EeUO577s4P5bf4HCTXtI6trMc6f6RzirY5IrF3aIookuSpyslFzrnvv2lmEWv5HyC1X79A=="],
"@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=="],
@@ -63,7 +64,7 @@
"buffer-fill": ["buffer-fill@1.0.0", "", {}, "sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ=="],
"bun-types": ["bun-types@1.3.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-5ua817+BZPZOlNaRgGBpZJOSAQ9RQ17pkwPD0yR7CfJg+r8DgIILByFifDTa+IPDDxzf5VNhtNlcKqFzDgJvlQ=="],
"bun-types": ["bun-types@1.2.21", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-sa2Tj77Ijc/NTLS0/Odjq/qngmEPZfbfnOERi0KRUYhT9R8M4VBioWVmMWE5GrYbKMc+5lVybXygLdibHaqVqw=="],
"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=="],
@@ -75,6 +76,8 @@
"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=="],
@@ -87,7 +90,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.7.0", "", { "peerDependencies": { "babel-plugin-macros": "^3.1.0" }, "optionalPeers": ["babel-plugin-macros"] }, "sha512-HGFtf8yhuhGhqO07SV79tRp+br4MnbdjeVxotpn1QBl30pcLLCQjX5b2295ll0fv8RKDKsmWYrl05usHM9CewQ=="],
"dedent": ["dedent@1.6.0", "", { "peerDependencies": { "babel-plugin-macros": "^3.1.0" }, "optionalPeers": ["babel-plugin-macros"] }, "sha512-F1Z+5UCFpmQUzJa11agbyPVMbpgT/qA3/SKyJ1jyBgm7dUcUEa8v9JwDkerSQXfakBwFljIxhOJqGkjUwZ9FSA=="],
"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=="],
@@ -179,7 +182,7 @@
"make-dir": ["make-dir@1.3.0", "", { "dependencies": { "pify": "^3.0.0" } }, "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ=="],
"marked": ["marked@16.4.2", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA=="],
"marked": ["marked@16.2.0", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-LbbTuye+0dWRz2TS9KJ7wsnD4KAtpj0MVkWc90XvBa6AslXsT0hTBVH5k32pcSyHH1fst9XEFJunXHktVy0zlg=="],
"matcher": ["matcher@3.0.0", "", { "dependencies": { "escape-string-regexp": "^4.0.0" } }, "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng=="],
@@ -203,7 +206,7 @@
"possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="],
"prettier": ["prettier@3.7.4", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA=="],
"prettier": ["prettier@3.6.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ=="],
"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=="],
+5 -5
View File
@@ -10,12 +10,12 @@
"update-version": "./update-version.sh"
},
"devDependencies": {
"@types/bun": "^1.3.4",
"bun-types": "^1.3.4",
"dedent": "^1.7.0",
"@types/bun": "^1.2.21",
"bun-types": "^1.2.21",
"dedent": "^1.6.0",
"gray-matter": "^4.0.3",
"marked": "^16.4.2",
"prettier": "^3.7.4",
"marked": "^16.2.0",
"prettier": "^3.6.2",
"prettier-plugin-sh": "^0.18.0",
"prettier-plugin-terraform-formatter": "^1.2.1",
"shellcheck": "^4.1.0"
Binary file not shown.

Before

Width:  |  Height:  |  Size: 407 KiB

@@ -2,24 +2,15 @@
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]
verified: true
tags: [ai, llm, chat, web-ui, 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
}
```
![Open WebUI](../../.images/openwebui.png)
This module installs and runs Open WebUI using Python and pip within your Coder workspace.
## Prerequisites
@@ -34,31 +25,13 @@ sudo apt-get update
sudo apt-get install -y python3.11 python3.11-venv
```
## Examples
### With OpenAI API Key
## Basic Usage
```tf
module "open-webui" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder-labs/open-webui/coder"
version = "1.0.0"
version = "0.9.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"
}
```
```
+3 -25
View File
@@ -9,6 +9,7 @@ terraform {
}
}
# Add required variables for your modules and remove any unneeded variables
variable "agent_id" {
type = string
description = "The ID of a Coder agent."
@@ -26,29 +27,9 @@ variable "http_server_port" {
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"
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'."
@@ -74,9 +55,6 @@ resource "coder_script" "open-webui" {
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
}
@@ -1,188 +0,0 @@
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"
}
}
+4 -10
View File
@@ -2,7 +2,7 @@
set -eu
printf '\033[0;1mInstalling Open WebUI %s...\n\n' "${VERSION}"
printf '\033[0;1mInstalling Open WebUI...\n\n'
check_python_version() {
python_cmd="$1"
@@ -45,12 +45,8 @@ 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 "📦 Installing Open WebUI..."
pip install open-webui
echo "🥳 Open WebUI has been installed"
else
echo "✅ Open WebUI is already installed"
@@ -59,8 +55,6 @@ 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 &
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}"
@@ -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.1.0"
agent_id = coder_agent.example.id
amp_api_key = var.amp_api_key
install_amp = true
agentapi_version = "latest"
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"
}
```
@@ -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.1.0"
amp_version = "2.0.2"
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.11.1"
default = "v0.10.0"
}
variable "cli_app" {
@@ -160,16 +160,6 @@ 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'\"}'"]
}
@@ -180,7 +170,6 @@ 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)
@@ -248,7 +237,6 @@ 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,4 +1,9 @@
#!/bin/bash
if [ -f "$HOME/.bashrc" ]; then
source "$HOME"/.bashrc
fi
set -euo pipefail
# ANSI colors
@@ -29,7 +29,6 @@ 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
@@ -51,13 +50,6 @@ 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"
@@ -66,8 +58,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" "${ARGS[@]}"
agentapi server --type amp --term-width=67 --term-height=1190 -- bash -c "echo \"$PROMPT\" | amp"
else
printf "No task prompt given.\n"
agentapi server --type amp --term-width=67 --term-height=1190 -- amp "${ARGS[@]}"
agentapi server --type amp --term-width=67 --term-height=1190 -- amp
fi
+29 -31
View File
@@ -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.6"
agent_id = coder_agent.main.id
version = "4.2.2"
agent_id = coder_agent.example.id
workdir = "/home/coder/project"
claude_api_key = "xxxx-xxxxx-xxxx"
}
@@ -45,15 +45,13 @@ 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.6"
agent_id = coder_agent.main.id
workdir = "/home/coder/project"
enable_boundary = true
boundary_version = "main"
boundary_version = "4.2.2"
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"
}
```
@@ -72,16 +70,16 @@ data "coder_parameter" "ai_prompt" {
module "claude-code" {
source = "registry.coder.com/coder/claude-code/coder"
version = "4.2.6"
agent_id = coder_agent.main.id
version = "4.2.2"
agent_id = coder_agent.example.id
workdir = "/home/coder/project"
claude_api_key = "xxxx-xxxxx-xxxx"
# OR
claude_code_oauth_token = "xxxxx-xxxx-xxxx"
claude_code_version = "2.0.62" # Pin to a specific version
agentapi_version = "0.11.4"
claude_code_version = "4.2.2" # Pin to a specific version
agentapi_version = "4.2.2"
ai_prompt = data.coder_parameter.ai_prompt.value
model = "sonnet"
@@ -95,7 +93,6 @@ module "claude-code" {
"command": "my-tool-server"
"args": ["--port", "8080"]
}
}
}
EOF
@@ -109,12 +106,13 @@ 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.6"
agent_id = coder_agent.main.id
workdir = "/home/coder/project"
version = "4.2.2"
agent_id = coder_agent.example.id
workdir = "/home/coder"
install_claude_code = true
claude_code_version = "2.0.62"
claude_code_version = "4.2.2"
report_tasks = false
cli_app = true
}
```
@@ -131,8 +129,8 @@ variable "claude_code_oauth_token" {
module "claude-code" {
source = "registry.coder.com/coder/claude-code/coder"
version = "4.2.6"
agent_id = coder_agent.main.id
version = "4.2.2"
agent_id = coder_agent.example.id
workdir = "/home/coder/project"
claude_code_oauth_token = var.claude_code_oauth_token
}
@@ -148,13 +146,13 @@ Configure Claude Code to use AWS Bedrock for accessing Claude models through you
```tf
resource "coder_env" "bedrock_use" {
agent_id = coder_agent.main.id
agent_id = coder_agent.example.id
name = "CLAUDE_CODE_USE_BEDROCK"
value = "1"
}
resource "coder_env" "aws_region" {
agent_id = coder_agent.main.id
agent_id = coder_agent.example.id
name = "AWS_REGION"
value = "us-east-1" # Choose your preferred region
}
@@ -176,13 +174,13 @@ variable "aws_secret_access_key" {
}
resource "coder_env" "aws_access_key_id" {
agent_id = coder_agent.main.id
agent_id = coder_agent.example.id
name = "AWS_ACCESS_KEY_ID"
value = var.aws_access_key_id
}
resource "coder_env" "aws_secret_access_key" {
agent_id = coder_agent.main.id
agent_id = coder_agent.example.id
name = "AWS_SECRET_ACCESS_KEY"
value = var.aws_secret_access_key
}
@@ -197,15 +195,15 @@ variable "aws_bearer_token_bedrock" {
}
resource "coder_env" "bedrock_api_key" {
agent_id = coder_agent.main.id
agent_id = coder_agent.example.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.6"
agent_id = coder_agent.main.id
version = "4.2.2"
agent_id = coder_agent.example.id
workdir = "/home/coder/project"
model = "global.anthropic.claude-sonnet-4-5-20250929-v1:0"
}
@@ -230,39 +228,39 @@ variable "vertex_sa_json" {
}
resource "coder_env" "vertex_use" {
agent_id = coder_agent.main.id
agent_id = coder_agent.example.id
name = "CLAUDE_CODE_USE_VERTEX"
value = "1"
}
resource "coder_env" "vertex_project_id" {
agent_id = coder_agent.main.id
agent_id = coder_agent.example.id
name = "ANTHROPIC_VERTEX_PROJECT_ID"
value = "your-gcp-project-id"
}
resource "coder_env" "cloud_ml_region" {
agent_id = coder_agent.main.id
agent_id = coder_agent.example.id
name = "CLOUD_ML_REGION"
value = "global"
}
resource "coder_env" "vertex_sa_json" {
agent_id = coder_agent.main.id
agent_id = coder_agent.example.id
name = "VERTEX_SA_JSON"
value = var.vertex_sa_json
}
resource "coder_env" "google_application_credentials" {
agent_id = coder_agent.main.id
agent_id = coder_agent.example.id
name = "GOOGLE_APPLICATION_CREDENTIALS"
value = "/tmp/gcp-sa.json"
}
module "claude-code" {
source = "registry.coder.com/coder/claude-code/coder"
version = "4.2.6"
agent_id = coder_agent.main.id
version = "4.2.2"
agent_id = coder_agent.example.id
workdir = "/home/coder/project"
model = "claude-sonnet-4@20250514"
+38 -142
View File
@@ -208,17 +208,13 @@ 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",
`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`,
`touch ${sessionDir}/session-${taskSessionId}.jsonl`,
]);
await execModuleScript(id);
@@ -230,10 +226,46 @@ SESSIONEOF`,
]);
expect(startLog.stdout).toContain("--resume");
expect(startLog.stdout).toContain(taskSessionId);
expect(startLog.stdout).toContain("Resuming task session");
expect(startLog.stdout).toContain("Resuming existing 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: {
@@ -328,140 +360,4 @@ SESSIONEOF`,
"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");
});
});
+9 -6
View File
@@ -86,7 +86,7 @@ variable "install_agentapi" {
variable "agentapi_version" {
type = string
description = "The version of AgentAPI to install."
default = "v0.11.4"
default = "v0.10.0"
}
variable "ai_prompt" {
@@ -291,11 +291,12 @@ resource "coder_env" "disable_autoupdater" {
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"
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"))
# Extract hostname from access_url for boundary --allow flag
coder_host = replace(replace(data.coder_workspace.me.access_url, "https://", ""), "http://", "")
@@ -356,7 +357,9 @@ 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}' \
@@ -90,63 +90,12 @@ 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
}
@@ -0,0 +1,44 @@
# 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
@@ -51,6 +51,19 @@ 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
@@ -86,85 +99,18 @@ function validate_claude_installation() {
# This ensures all task sessions use a consistent, predictable ID
TASK_SESSION_ID="cd32e253-ca16-4fd3-9825-d837e74ae3c2"
get_project_dir() {
task_session_exists() {
local workdir_normalized
workdir_normalized=$(echo "$ARG_WORKDIR" | tr '/' '-')
echo "$HOME/.claude/projects/${workdir_normalized}"
}
local project_dir="$HOME/.claude/projects/${workdir_normalized}"
get_task_session_file() {
echo "$(get_project_dir)/${TASK_SESSION_ID}.jsonl"
}
printf "PROJECT_DIR: %s, workdir_normalized: %s\n" "$project_dir" "$workdir_normalized"
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"
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"
return 0
else
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"
printf "TASK_SESSION_ID: %s file not found\n" "$TASK_SESSION_ID"
return 1
fi
}
@@ -187,41 +133,75 @@ function start_agentapi() {
fi
if [ -n "$ARG_RESUME_SESSION_ID" ]; then
echo "Resuming specified session: $ARG_RESUME_SESSION_ID"
echo "Resuming task session by ID: $ARG_RESUME_SESSION_ID"
ARGS+=(--resume "$ARG_RESUME_SESSION_ID")
[ "$ARG_DANGEROUSLY_SKIP_PERMISSIONS" = "true" ] && ARGS+=(--dangerously-skip-permissions)
if [ "$ARG_DANGEROUSLY_SKIP_PERMISSIONS" = "true" ]; then
ARGS+=(--dangerously-skip-permissions)
fi
elif [ "$ARG_CONTINUE" = "true" ]; then
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
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")
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
if has_any_sessions; then
echo "Continuing most recent standalone session"
ARGS+=(--continue)
[ "$ARG_DANGEROUSLY_SKIP_PERMISSIONS" = "true" ] && ARGS+=(--dangerously-skip-permissions)
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"
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")
if [ "$ARG_REPORT_TASKS" = "true" ] || [ "$ARG_DANGEROUSLY_SKIP_PERMISSIONS" = "true" ]; then
ARGS+=(--dangerously-skip-permissions)
fi
echo "Starting new session"
fi
fi
else
echo "Continue disabled, starting fresh session"
[ "$ARG_DANGEROUSLY_SKIP_PERMISSIONS" = "true" ] && ARGS+=(--dangerously-skip-permissions)
[ -n "$ARG_AI_PROMPT" ] && ARGS+=(-- "$ARG_AI_PROMPT")
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
fi
printf "Running claude code with args: %s\n" "$(printf '%q ' "${ARGS[@]}")"
-121
View File
@@ -1,121 +0,0 @@
---
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
-97
View File
@@ -1,97 +0,0 @@
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
}
@@ -1,176 +0,0 @@
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'"
}
}
-204
View File
@@ -1,204 +0,0 @@
#!/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
@@ -2,7 +2,7 @@
display_name: Positron Desktop
description: Add a one-click button to launch Positron Desktop
icon: ../../../../.icons/positron.svg
verified: false
verified: true
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.1"
version = "1.0.0"
agent_id = coder_agent.main.id
}
```