disinto/nomad/jobs/chat.hcl
dev-qwen2 7f1f8fa01c
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/nomad-validate Pipeline was successful
ci/woodpecker/pr/ci Pipeline was successful
ci/woodpecker/pr/edge-subpath Pipeline was successful
ci/woodpecker/pr/nomad-validate Pipeline was successful
ci/woodpecker/pr/secret-scan Pipeline was successful
ci/woodpecker/pr/smoke-init Pipeline was successful
fix: vision(#623): scope Claude chat working directory to project staging checkout (#1027)
- server.py: add CHAT_WORKSPACE_DIR env var, set cwd to workspace
  and use --permission-mode acceptEdits + append message in Claude invocations
- lib/generators.sh: add workspace bind mount and env var to compose generator
- nomad/jobs/chat.hcl: add workspace host volume (static source "chat-workspace"),
  meta block + NOMAD_META_ env var, volume_mount — Nomad-compatible pattern

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-20 18:11:33 +00:00

188 lines
7.2 KiB
HCL

# =============================================================================
# nomad/jobs/chat.hcl — Claude chat UI (Nomad service job)
#
# Part of the Nomad+Vault migration (S5.2, issue #989). Lightweight service
# job for the Claude chat UI with sandbox hardening (#706).
#
# Build:
# Custom image built from docker/chat/Dockerfile as disinto/chat:local
# (same :local pattern as disinto/agents:local).
#
# Sandbox hardening (#706):
# - Read-only root filesystem (enforced via entrypoint)
# - tmpfs /tmp:size=64m for runtime temp files
# - cap_drop ALL (no Linux capabilities)
# - pids_limit 128 (prevent fork bombs)
# - mem_limit 512m (matches compose sandbox hardening)
#
# Vault integration:
# - vault { role = "service-chat" } at group scope
# - Template stanza renders CHAT_OAUTH_CLIENT_ID, CHAT_OAUTH_CLIENT_SECRET,
# FORWARD_AUTH_SECRET from kv/disinto/shared/chat
# - Seeded on fresh boxes by tools/vault-seed-chat.sh
#
# Host volumes:
# - chat-history → /var/lib/chat/history (persists conversation history)
# - workspace → /var/workspace (project working tree for Claude access, #1027)
#
# Client-side host_volume registration (operator prerequisite):
# In nomad/client.hcl on each Nomad node:
# host_volume "chat-workspace" {
# path = "/var/disinto/chat-workspace"
# read_only = false
# }
# Nodes without the host_volume registered will not schedule the workspace mount.
#
# Not the runtime yet: docker-compose.yml is still the factory's live stack
# until cutover. This file exists so CI can validate it and S5.2 can wire
# `disinto init --backend=nomad --with chat` to `nomad job run` it.
# =============================================================================
job "chat" {
type = "service"
datacenters = ["dc1"]
group "chat" {
count = 1
# ── Vault workload identity (S5.2, issue #989) ───────────────────────────
# Role `service-chat` defined in vault/roles.yaml, policy in
# vault/policies/service-chat.hcl. Bound claim pins nomad_job_id = "chat".
vault {
role = "service-chat"
}
# ── Network ──────────────────────────────────────────────────────────────
# External port 8080 for chat UI access (via edge proxy or direct).
network {
port "http" {
static = 8080
to = 8080
}
}
# ── Host volumes ─────────────────────────────────────────────────────────
# chat-history volume: declared in nomad/client.hcl, path
# /srv/disinto/chat-history on the factory box.
volume "chat-history" {
type = "host"
source = "chat-history"
read_only = false
}
# Workspace volume: bind-mounted project working tree for Claude access (#1027)
# Source is a fixed logical name resolved by client-side host_volume registration.
volume "workspace" {
type = "host"
source = "chat-workspace"
read_only = false
}
# ── Metadata (per-dispatch env var via NOMAD_META_*) ──────────────────────
# CHAT_WORKSPACE_DIR: project working tree path, injected into task env
# as NOMAD_META_CHAT_WORKSPACE_DIR for the workspace volume mount target.
meta {
CHAT_WORKSPACE_DIR = "/var/workspace"
}
# ── Restart policy ───────────────────────────────────────────────────────
restart {
attempts = 3
interval = "5m"
delay = "15s"
mode = "delay"
}
# ── Service registration ─────────────────────────────────────────────────
service {
name = "chat"
port = "http"
provider = "nomad"
check {
type = "http"
path = "/health"
interval = "10s"
timeout = "3s"
}
}
task "chat" {
driver = "docker"
config {
image = "disinto/chat:local"
force_pull = false
# Sandbox hardening (#706): cap_drop ALL, pids_limit 128, tmpfs /tmp
# ReadonlyRootfs enforced via entrypoint script (fails if running as root)
cap_drop = ["ALL"]
pids_limit = 128
mount {
type = "tmpfs"
target = "/tmp"
readonly = false
tmpfs_options {
size = 67108864 # 64MB in bytes
}
}
# Security options for sandbox hardening
# apparmor=unconfined needed for Claude CLI ptrace access
# no-new-privileges prevents privilege escalation
security_opt = ["apparmor=unconfined", "no-new-privileges"]
}
# ── Volume mounts ──────────────────────────────────────────────────────
# Mount chat-history for conversation persistence
volume_mount {
volume = "chat-history"
destination = "/var/lib/chat/history"
read_only = false
}
# Mount workspace directory for Claude code access (#1027)
# Binds project working tree so Claude can inspect/modify code
volume_mount {
volume = "workspace"
destination = "/var/workspace"
read_only = false
}
# ── Environment: secrets from Vault (S5.2) ──────────────────────────────
# CHAT_OAUTH_CLIENT_ID, CHAT_OAUTH_CLIENT_SECRET, FORWARD_AUTH_SECRET
# rendered from kv/disinto/shared/chat via template stanza.
env {
FORGE_URL = "http://forgejo:3000"
CHAT_WORKSPACE_DIR = "${NOMAD_META_CHAT_WORKSPACE_DIR}"
}
# ── Vault-templated secrets (S5.2, issue #989) ─────────────────────────
# Renders chat-secrets.env from Vault KV v2 at kv/disinto/shared/chat.
# Placeholder values kept < 16 chars to avoid secret-scan CI failures.
template {
destination = "secrets/chat-secrets.env"
env = true
change_mode = "restart"
error_on_missing_key = false
data = <<EOT
{{- with secret "kv/data/disinto/shared/chat" -}}
CHAT_OAUTH_CLIENT_ID={{ .Data.data.chat_oauth_client_id }}
CHAT_OAUTH_CLIENT_SECRET={{ .Data.data.chat_oauth_client_secret }}
FORWARD_AUTH_SECRET={{ .Data.data.forward_auth_secret }}
{{- else -}}
# WARNING: run tools/vault-seed-chat.sh
CHAT_OAUTH_CLIENT_ID=seed-me
CHAT_OAUTH_CLIENT_SECRET=seed-me
FORWARD_AUTH_SECRET=seed-me
{{- end -}}
EOT
}
# ── Sandbox hardening (S5.2, #706) ────────────────────────────────────
# Memory = 512MB (matches docker-compose sandbox hardening)
resources {
cpu = 200
memory = 512
}
}
}
}