summaryrefslogtreecommitdiff
path: root/images/agent-base/tools
diff options
context:
space:
mode:
authorPeter Stone <thepeterstone@gmail.com>2026-03-18 23:56:20 +0000
committerPeter Stone <thepeterstone@gmail.com>2026-03-18 23:56:20 +0000
commit7df4f06ae0e3ae80bd967bf53cbec36e58b4a3bd (patch)
tree740c141c52764604fc8d4c036733e5f47368b26a /images/agent-base/tools
parenta4795d68fc5381f1ff48d043fe7554355e5899fb (diff)
feat: containerized execution with agent tooling and deployment fixes
- ContainerRunner replaces ClaudeRunner/GeminiRunner; all agent types run in Docker containers via claudomator-agent:latest - Writable agentHome staging dir (/home/agent) satisfies home-dir requirements for both claude and gemini CLIs without exposing host creds - Copy .credentials.json and .claude.json into staging dir at run time; GEMINI_API_KEY passed via env file - Fix git clone: remove MkdirTemp-created dir before cloning (git rejects pre-existing dirs even when empty) - Replace localhost with host.docker.internal in APIURL so container can reach host API; add --add-host=host.docker.internal:host-gateway - Run container as --user=$(uid):$(gid) so host-owned workspace files are readable; chmod workspace 0755 and instructions file 0644 after clone - Pre-create .gemini/ in staging dir to avoid atomic-rename ENOENT on first gemini-cli run - Add ct CLI tool to container image: pre-built Bash wrapper for Claudomator API (ct task submit/create/run/wait/status/list) - Document ct tool in CLAUDE.md agent instructions section - Add drain-failed-tasks script: retries failed tasks on a 5-minute interval - Update Dockerfile: Node 22 via NodeSource, Go 1.24, gemini-cli, git safe.directory=*, default ~/.claude.json Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'images/agent-base/tools')
-rw-r--r--images/agent-base/tools/ct210
1 files changed, 210 insertions, 0 deletions
diff --git a/images/agent-base/tools/ct b/images/agent-base/tools/ct
new file mode 100644
index 0000000..46d9613
--- /dev/null
+++ b/images/agent-base/tools/ct
@@ -0,0 +1,210 @@
+#!/bin/bash
+# ct - Claudomator CLI for agents running inside containers
+#
+# Usage:
+# ct task create --name "..." --instructions "..." # create subtask (parent auto-set)
+# ct task run <task-id> # queue a task for execution
+# ct task wait <task-id> [--timeout 300] # poll until done, print status
+# ct task status <task-id> # print current state
+# ct task list # list recent tasks
+#
+# Environment (injected by ContainerRunner):
+# CLAUDOMATOR_API_URL base URL of the Claudomator API
+# CLAUDOMATOR_TASK_ID ID of the currently running task (used as default parent)
+
+set -euo pipefail
+
+API="${CLAUDOMATOR_API_URL:-http://host.docker.internal:8484}"
+PARENT="${CLAUDOMATOR_TASK_ID:-}"
+
+_api() {
+ local method="$1"; shift
+ local path="$1"; shift
+ curl -sf -X "$method" "${API}${path}" \
+ -H "Content-Type: application/json" \
+ "$@"
+}
+
+_require() {
+ if ! command -v "$1" &>/dev/null; then
+ echo "ct: required tool '$1' not found" >&2
+ exit 1
+ fi
+}
+
+_require curl
+_require jq
+
+cmd_task_create() {
+ local name="" instructions="" instructions_file="" model="" budget="" parent="$PARENT"
+
+ while [[ $# -gt 0 ]]; do
+ case "$1" in
+ --name) name="$2"; shift 2 ;;
+ --instructions) instructions="$2"; shift 2 ;;
+ --file) instructions_file="$2"; shift 2 ;;
+ --model) model="$2"; shift 2 ;;
+ --budget) budget="$2"; shift 2 ;;
+ --parent) parent="$2"; shift 2 ;;
+ *) echo "ct task create: unknown flag $1" >&2; exit 1 ;;
+ esac
+ done
+
+ if [[ -z "$name" ]]; then
+ echo "ct task create: --name is required" >&2; exit 1
+ fi
+
+ if [[ -n "$instructions_file" ]]; then
+ instructions=$(cat "$instructions_file")
+ fi
+
+ if [[ -z "$instructions" ]]; then
+ echo "ct task create: --instructions or --file is required" >&2; exit 1
+ fi
+
+ local payload
+ payload=$(jq -n \
+ --arg name "$name" \
+ --arg instructions "$instructions" \
+ --arg parent "$parent" \
+ --arg model "${model:-sonnet}" \
+ --argjson budget "${budget:-3.0}" \
+ '{
+ name: $name,
+ parent_task_id: $parent,
+ agent: {
+ type: "claude",
+ model: $model,
+ instructions: $instructions,
+ max_budget_usd: $budget
+ }
+ }')
+
+ local response
+ response=$(_api POST /api/tasks -d "$payload")
+ local task_id
+ task_id=$(echo "$response" | jq -r '.id // empty')
+
+ if [[ -z "$task_id" ]]; then
+ echo "ct task create: API error: $(echo "$response" | jq -r '.error // .')" >&2
+ exit 1
+ fi
+
+ echo "$task_id"
+}
+
+cmd_task_run() {
+ local task_id="${1:-}"
+ if [[ -z "$task_id" ]]; then
+ echo "ct task run: task-id required" >&2; exit 1
+ fi
+
+ local response
+ response=$(_api POST "/api/tasks/${task_id}/run")
+ echo "$response" | jq -r '.message // .error // .'
+}
+
+cmd_task_wait() {
+ local task_id="${1:-}"
+ local timeout=300
+ shift || true
+
+ while [[ $# -gt 0 ]]; do
+ case "$1" in
+ --timeout) timeout="$2"; shift 2 ;;
+ *) echo "ct task wait: unknown flag $1" >&2; exit 1 ;;
+ esac
+ done
+
+ if [[ -z "$task_id" ]]; then
+ echo "ct task wait: task-id required" >&2; exit 1
+ fi
+
+ local deadline=$(( $(date +%s) + timeout ))
+ local interval=5
+
+ while true; do
+ local response
+ response=$(_api GET "/api/tasks/${task_id}" 2>/dev/null) || true
+
+ local state
+ state=$(echo "$response" | jq -r '.state // "UNKNOWN"')
+
+ case "$state" in
+ COMPLETED|FAILED|TIMED_OUT|CANCELLED|BUDGET_EXCEEDED)
+ echo "$state"
+ [[ "$state" == "COMPLETED" ]] && exit 0 || exit 1
+ ;;
+ BLOCKED)
+ echo "BLOCKED"
+ exit 2
+ ;;
+ esac
+
+ if [[ $(date +%s) -ge $deadline ]]; then
+ echo "ct task wait: timed out after ${timeout}s (state: $state)" >&2
+ exit 1
+ fi
+
+ sleep "$interval"
+ done
+}
+
+cmd_task_status() {
+ local task_id="${1:-}"
+ if [[ -z "$task_id" ]]; then
+ echo "ct task status: task-id required" >&2; exit 1
+ fi
+ _api GET "/api/tasks/${task_id}" | jq -r '.state'
+}
+
+cmd_task_list() {
+ _api GET "/api/tasks" | jq -r '.[] | "\(.state)\t\(.id)\t\(.name)"' | sort
+}
+
+# create-and-run shorthand: create a subtask and immediately queue it, then optionally wait
+cmd_task_submit() {
+ local wait=false
+ local args=()
+
+ while [[ $# -gt 0 ]]; do
+ case "$1" in
+ --wait) wait=true; shift ;;
+ *) args+=("$1"); shift ;;
+ esac
+ done
+
+ local task_id
+ task_id=$(cmd_task_create "${args[@]}")
+ cmd_task_run "$task_id" >/dev/null
+ echo "$task_id"
+
+ if $wait; then
+ cmd_task_wait "$task_id"
+ fi
+}
+
+# Dispatch
+if [[ $# -lt 2 ]]; then
+ echo "Usage: ct <resource> <command> [args...]"
+ echo " ct task create --name NAME --instructions TEXT [--file FILE] [--model MODEL] [--budget N]"
+ echo " ct task submit --name NAME --instructions TEXT [--wait]"
+ echo " ct task run <id>"
+ echo " ct task wait <id> [--timeout 300]"
+ echo " ct task status <id>"
+ echo " ct task list"
+ exit 1
+fi
+
+resource="$1"; shift
+command="$1"; shift
+
+case "${resource}/${command}" in
+ task/create) cmd_task_create "$@" ;;
+ task/run) cmd_task_run "$@" ;;
+ task/wait) cmd_task_wait "$@" ;;
+ task/status) cmd_task_status "$@" ;;
+ task/list) cmd_task_list ;;
+ task/submit) cmd_task_submit "$@" ;;
+ *) echo "ct: unknown command: ${resource} ${command}" >&2; exit 1 ;;
+esac