Compare commits
55 commits
fix/issue-
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 3fb2de4a8a | |||
|
|
c24d204b0f | ||
| 58a4ce4e0c | |||
|
|
b475f99873 | ||
| b05a31197c | |||
|
|
e6dcad143d | ||
| a35d6e7848 | |||
|
|
38b55e1855 | ||
|
|
4f5e546c42 | ||
| 85969ad42d | |||
|
|
31e2f63f1b | ||
| f98338cec7 | |||
|
|
fa7fb60415 | ||
| 6f21582ffa | |||
| cfe526b481 | |||
|
|
ec8791787d | ||
|
|
d8f2be1c4f | ||
|
|
78a19a8add | ||
| 1eac6d63e2 | |||
|
|
f2bafbc190 | ||
| dfb1a45295 | |||
|
|
832d6bb851 | ||
| 8fc3ba5b59 | |||
|
|
3b82f8e3a1 | ||
|
|
8381f88491 | ||
|
|
0c85339285 | ||
|
|
acd6240ec4 | ||
| 16474a1800 | |||
|
|
8b1857e83f | ||
|
|
da93748fee | ||
| 30bc21c650 | |||
|
|
9806ed40df | ||
|
|
9f94b818a3 | ||
|
|
9f9abdee82 | ||
| 90831d3347 | |||
|
|
72aecff8d8 | ||
| 84d63d49b5 | |||
|
|
e17e9604c1 | ||
| daaaf70d34 | |||
|
|
4a07049383 | ||
| 8c7b26f916 | |||
|
|
deda192d60 | ||
|
|
4a3c8e16db | ||
| 450e2a09c8 | |||
|
|
f2b175e49b | ||
| c872f28242 | |||
|
|
386f9a1bc0 | ||
| 71e770b8ae | |||
|
|
ffd1f41b33 | ||
| 05e57478ad | |||
|
|
5185cc720a | ||
| 93c26ef037 | |||
|
|
98bb5a3fee | ||
| 3cb76d571b | |||
|
|
0c767d9fee |
35 changed files with 1182 additions and 474 deletions
|
|
@ -1,4 +1,4 @@
|
||||||
<!-- last-reviewed: edf7a28bd3c85d4f72d28fd986fd2af3dcb885c1 -->
|
<!-- last-reviewed: b05a31197cc78aa28f3c3e6365e782032bfb25af -->
|
||||||
# Disinto — Agent Instructions
|
# Disinto — Agent Instructions
|
||||||
|
|
||||||
## What this repo is
|
## What this repo is
|
||||||
|
|
@ -39,7 +39,7 @@ disinto/ (code repo)
|
||||||
│ hooks/ — Claude Code session hooks (on-compact-reinject, on-idle-stop, on-phase-change, on-pretooluse-guard, on-session-end, on-stop-failure)
|
│ hooks/ — Claude Code session hooks (on-compact-reinject, on-idle-stop, on-phase-change, on-pretooluse-guard, on-session-end, on-stop-failure)
|
||||||
│ init/nomad/ — cluster-up.sh, install.sh, vault-init.sh, lib-systemd.sh (Nomad+Vault Step 0 installers, #821-#825); wp-oauth-register.sh (Forgejo OAuth2 app + Vault KV seeder for Woodpecker, S3.3); deploy.sh (dependency-ordered Nomad job deploy + health-wait, S4)
|
│ init/nomad/ — cluster-up.sh, install.sh, vault-init.sh, lib-systemd.sh (Nomad+Vault Step 0 installers, #821-#825); wp-oauth-register.sh (Forgejo OAuth2 app + Vault KV seeder for Woodpecker, S3.3); deploy.sh (dependency-ordered Nomad job deploy + health-wait, S4)
|
||||||
├── nomad/ server.hcl, client.hcl (allow_privileged for woodpecker-agent, S3-fix-5), vault.hcl — HCL configs deployed to /etc/nomad.d/ and /etc/vault.d/ by lib/init/nomad/cluster-up.sh
|
├── nomad/ server.hcl, client.hcl (allow_privileged for woodpecker-agent, S3-fix-5), vault.hcl — HCL configs deployed to /etc/nomad.d/ and /etc/vault.d/ by lib/init/nomad/cluster-up.sh
|
||||||
│ jobs/ — Nomad jobspecs: forgejo.hcl (Vault secrets via template, S2.4); woodpecker-server.hcl + woodpecker-agent.hcl (host-net, docker.sock, Vault KV, S3.1-S3.2); agents.hcl (7 roles, llama, Vault-templated bot tokens, S4.1)
|
│ jobs/ — Nomad jobspecs: forgejo.hcl (Vault secrets via template, S2.4); woodpecker-server.hcl + woodpecker-agent.hcl (host-net, docker.sock, Vault KV, S3.1-S3.2); agents.hcl (7 roles, llama, Vault-templated bot tokens, S4.1); vault-runner.hcl (parameterized batch dispatch, S5.3); staging.hcl (Caddy file-server, dynamic port — edge discovers via service registration, S5.2); chat.hcl (Claude chat UI, tmpfs via mount block, Vault OAuth secrets, S5.2); edge.hcl (Caddy proxy + dispatcher sidecar, S5.1)
|
||||||
├── projects/ *.toml.example — templates; *.toml — local per-box config (gitignored)
|
├── projects/ *.toml.example — templates; *.toml — local per-box config (gitignored)
|
||||||
├── formulas/ Issue templates (TOML specs for multi-step agent tasks)
|
├── formulas/ Issue templates (TOML specs for multi-step agent tasks)
|
||||||
├── docker/ Dockerfiles and entrypoints: reproduce, triage, edge dispatcher, chat (server.py, entrypoint-chat.sh, Dockerfile, ui/)
|
├── docker/ Dockerfiles and entrypoints: reproduce, triage, edge dispatcher, chat (server.py, entrypoint-chat.sh, Dockerfile, ui/)
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
<!-- last-reviewed: edf7a28bd3c85d4f72d28fd986fd2af3dcb885c1 -->
|
<!-- last-reviewed: b05a31197cc78aa28f3c3e6365e782032bfb25af -->
|
||||||
# Architect — Agent Instructions
|
# Architect — Agent Instructions
|
||||||
|
|
||||||
## What this agent is
|
## What this agent is
|
||||||
|
|
|
||||||
118
bin/disinto
118
bin/disinto
|
|
@ -82,7 +82,7 @@ Init options:
|
||||||
--ci-id <n> Woodpecker CI repo ID (default: 0 = no CI)
|
--ci-id <n> Woodpecker CI repo ID (default: 0 = no CI)
|
||||||
--forge-url <url> Forge base URL (default: http://localhost:3000)
|
--forge-url <url> Forge base URL (default: http://localhost:3000)
|
||||||
--backend <value> Orchestration backend: docker (default) | nomad
|
--backend <value> Orchestration backend: docker (default) | nomad
|
||||||
--with <services> (nomad) Deploy services: forgejo,woodpecker,agents[,...] (S1.3, S3.4, S4.2)
|
--with <services> (nomad) Deploy services: forgejo,woodpecker,agents,staging,chat,edge[,...] (S1.3, S3.4, S4.2, S5.2, S5.5)
|
||||||
--empty (nomad) Bring up cluster only, no jobs (S0.4)
|
--empty (nomad) Bring up cluster only, no jobs (S0.4)
|
||||||
--bare Skip compose generation (bare-metal setup)
|
--bare Skip compose generation (bare-metal setup)
|
||||||
--build Use local docker build instead of registry images (dev mode)
|
--build Use local docker build instead of registry images (dev mode)
|
||||||
|
|
@ -787,7 +787,7 @@ _disinto_init_nomad() {
|
||||||
# real-run path so dry-run output accurately represents execution order.
|
# real-run path so dry-run output accurately represents execution order.
|
||||||
# Build ordered deploy list: only include services present in with_services
|
# Build ordered deploy list: only include services present in with_services
|
||||||
local DEPLOY_ORDER=""
|
local DEPLOY_ORDER=""
|
||||||
for ordered_svc in forgejo woodpecker-server woodpecker-agent agents; do
|
for ordered_svc in forgejo woodpecker-server woodpecker-agent agents staging chat edge; do
|
||||||
if echo ",$with_services," | grep -q ",$ordered_svc,"; then
|
if echo ",$with_services," | grep -q ",$ordered_svc,"; then
|
||||||
DEPLOY_ORDER="${DEPLOY_ORDER:+${DEPLOY_ORDER} }${ordered_svc}"
|
DEPLOY_ORDER="${DEPLOY_ORDER:+${DEPLOY_ORDER} }${ordered_svc}"
|
||||||
fi
|
fi
|
||||||
|
|
@ -801,6 +801,7 @@ _disinto_init_nomad() {
|
||||||
case "$svc" in
|
case "$svc" in
|
||||||
woodpecker-server|woodpecker-agent) seed_name="woodpecker" ;;
|
woodpecker-server|woodpecker-agent) seed_name="woodpecker" ;;
|
||||||
agents) seed_name="agents" ;;
|
agents) seed_name="agents" ;;
|
||||||
|
chat) seed_name="chat" ;;
|
||||||
esac
|
esac
|
||||||
local seed_script="${FACTORY_ROOT}/tools/vault-seed-${seed_name}.sh"
|
local seed_script="${FACTORY_ROOT}/tools/vault-seed-${seed_name}.sh"
|
||||||
if [ -x "$seed_script" ]; then
|
if [ -x "$seed_script" ]; then
|
||||||
|
|
@ -822,6 +823,32 @@ _disinto_init_nomad() {
|
||||||
done
|
done
|
||||||
echo "[deploy] dry-run complete"
|
echo "[deploy] dry-run complete"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Dry-run vault-runner (unconditionally, not gated by --with)
|
||||||
|
echo ""
|
||||||
|
echo "── Vault-runner dry-run ───────────────────────────────────"
|
||||||
|
local vault_runner_path="${FACTORY_ROOT}/nomad/jobs/vault-runner.hcl"
|
||||||
|
if [ -f "$vault_runner_path" ]; then
|
||||||
|
echo "[deploy] vault-runner: [dry-run] nomad job validate ${vault_runner_path}"
|
||||||
|
echo "[deploy] vault-runner: [dry-run] nomad job run -detach ${vault_runner_path}"
|
||||||
|
else
|
||||||
|
echo "[deploy] vault-runner: jobspec not found, skipping"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Build custom images dry-run (if agents, chat, or edge services are included)
|
||||||
|
if echo ",$with_services," | grep -qE ",(agents|chat|edge),"; then
|
||||||
|
echo ""
|
||||||
|
echo "── Build images dry-run ──────────────────────────────"
|
||||||
|
if echo ",$with_services," | grep -q ",agents,"; then
|
||||||
|
echo "[build] [dry-run] docker build -t disinto/agents:local -f ${FACTORY_ROOT}/docker/agents/Dockerfile ${FACTORY_ROOT}"
|
||||||
|
fi
|
||||||
|
if echo ",$with_services," | grep -q ",chat,"; then
|
||||||
|
echo "[build] [dry-run] docker build -t disinto/chat:local -f ${FACTORY_ROOT}/docker/chat/Dockerfile ${FACTORY_ROOT}/docker/chat"
|
||||||
|
fi
|
||||||
|
if echo ",$with_services," | grep -q ",edge,"; then
|
||||||
|
echo "[build] [dry-run] docker build -t disinto/edge:local -f ${FACTORY_ROOT}/docker/edge/Dockerfile ${FACTORY_ROOT}/docker/edge"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
@ -909,6 +936,29 @@ _disinto_init_nomad() {
|
||||||
echo "[import] no --import-env/--import-sops — skipping; set them or seed kv/disinto/* manually before deploying secret-dependent services"
|
echo "[import] no --import-env/--import-sops — skipping; set them or seed kv/disinto/* manually before deploying secret-dependent services"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Build custom images required by Nomad jobs (S4.2, S5.2, S5.5) — before deploy.
|
||||||
|
# Single-node factory dev box: no multi-node pull needed, no registry auth.
|
||||||
|
# Can upgrade to approach B (registry push/pull) later if multi-node.
|
||||||
|
if echo ",$with_services," | grep -qE ",(agents|chat|edge),"; then
|
||||||
|
echo ""
|
||||||
|
echo "── Building custom images ─────────────────────────────"
|
||||||
|
if echo ",$with_services," | grep -q ",agents,"; then
|
||||||
|
local tag="disinto/agents:local"
|
||||||
|
echo "── Building $tag ─────────────────────────────"
|
||||||
|
docker build -t "$tag" -f "${FACTORY_ROOT}/docker/agents/Dockerfile" "${FACTORY_ROOT}" 2>&1 | tail -5
|
||||||
|
fi
|
||||||
|
if echo ",$with_services," | grep -q ",chat,"; then
|
||||||
|
local tag="disinto/chat:local"
|
||||||
|
echo "── Building $tag ─────────────────────────────"
|
||||||
|
docker build -t "$tag" -f "${FACTORY_ROOT}/docker/chat/Dockerfile" "${FACTORY_ROOT}/docker/chat" 2>&1 | tail -5
|
||||||
|
fi
|
||||||
|
if echo ",$with_services," | grep -q ",edge,"; then
|
||||||
|
local tag="disinto/edge:local"
|
||||||
|
echo "── Building $tag ─────────────────────────────"
|
||||||
|
docker build -t "$tag" -f "${FACTORY_ROOT}/docker/edge/Dockerfile" "${FACTORY_ROOT}/docker/edge" 2>&1 | tail -5
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
# Interleaved seed/deploy per service (S2.6, #928, #948).
|
# Interleaved seed/deploy per service (S2.6, #928, #948).
|
||||||
# We interleave seed + deploy per service (not batch all seeds then all deploys)
|
# We interleave seed + deploy per service (not batch all seeds then all deploys)
|
||||||
# so that OAuth-dependent services can reach their dependencies during seeding.
|
# so that OAuth-dependent services can reach their dependencies during seeding.
|
||||||
|
|
@ -917,9 +967,9 @@ _disinto_init_nomad() {
|
||||||
if [ -n "$with_services" ]; then
|
if [ -n "$with_services" ]; then
|
||||||
local vault_addr="${VAULT_ADDR:-http://127.0.0.1:8200}"
|
local vault_addr="${VAULT_ADDR:-http://127.0.0.1:8200}"
|
||||||
|
|
||||||
# Build ordered deploy list (S3.4, S4.2): forgejo → woodpecker-server → woodpecker-agent → agents
|
# Build ordered deploy list (S3.4, S4.2, S5.2, S5.5): forgejo → woodpecker-server → woodpecker-agent → agents → staging → chat → edge
|
||||||
local DEPLOY_ORDER=""
|
local DEPLOY_ORDER=""
|
||||||
for ordered_svc in forgejo woodpecker-server woodpecker-agent agents; do
|
for ordered_svc in forgejo woodpecker-server woodpecker-agent agents staging chat edge; do
|
||||||
if echo ",$with_services," | grep -q ",$ordered_svc,"; then
|
if echo ",$with_services," | grep -q ",$ordered_svc,"; then
|
||||||
DEPLOY_ORDER="${DEPLOY_ORDER:+${DEPLOY_ORDER} }${ordered_svc}"
|
DEPLOY_ORDER="${DEPLOY_ORDER:+${DEPLOY_ORDER} }${ordered_svc}"
|
||||||
fi
|
fi
|
||||||
|
|
@ -932,6 +982,7 @@ _disinto_init_nomad() {
|
||||||
case "$svc" in
|
case "$svc" in
|
||||||
woodpecker-server|woodpecker-agent) seed_name="woodpecker" ;;
|
woodpecker-server|woodpecker-agent) seed_name="woodpecker" ;;
|
||||||
agents) seed_name="agents" ;;
|
agents) seed_name="agents" ;;
|
||||||
|
chat) seed_name="chat" ;;
|
||||||
esac
|
esac
|
||||||
local seed_script="${FACTORY_ROOT}/tools/vault-seed-${seed_name}.sh"
|
local seed_script="${FACTORY_ROOT}/tools/vault-seed-${seed_name}.sh"
|
||||||
if [ -x "$seed_script" ]; then
|
if [ -x "$seed_script" ]; then
|
||||||
|
|
@ -951,6 +1002,23 @@ _disinto_init_nomad() {
|
||||||
# Deploy this service
|
# Deploy this service
|
||||||
echo ""
|
echo ""
|
||||||
echo "── Deploying ${svc} ───────────────────────────────────────"
|
echo "── Deploying ${svc} ───────────────────────────────────────"
|
||||||
|
|
||||||
|
# Seed host volumes before deployment (if needed)
|
||||||
|
case "$svc" in
|
||||||
|
staging)
|
||||||
|
# Seed site-content host volume (/srv/disinto/docker) with static content
|
||||||
|
# The staging jobspec mounts this volume read-only to /srv/site
|
||||||
|
local site_content_src="${FACTORY_ROOT}/docker/index.html"
|
||||||
|
local site_content_dst="/srv/disinto/docker"
|
||||||
|
if [ -f "$site_content_src" ] && [ -d "$site_content_dst" ]; then
|
||||||
|
if ! cmp -s "$site_content_src" "${site_content_dst}/index.html" 2>/dev/null; then
|
||||||
|
echo "[staging] seeding site-content volume..."
|
||||||
|
cp "$site_content_src" "${site_content_dst}/index.html"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
local jobspec_path="${FACTORY_ROOT}/nomad/jobs/${svc}.hcl"
|
local jobspec_path="${FACTORY_ROOT}/nomad/jobs/${svc}.hcl"
|
||||||
if [ ! -f "$jobspec_path" ]; then
|
if [ ! -f "$jobspec_path" ]; then
|
||||||
echo "Error: jobspec not found: ${jobspec_path}" >&2
|
echo "Error: jobspec not found: ${jobspec_path}" >&2
|
||||||
|
|
@ -969,6 +1037,27 @@ _disinto_init_nomad() {
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
|
# Run vault-runner (unconditionally, not gated by --with) — infrastructure job
|
||||||
|
# vault-runner is always present since it's needed for vault action dispatch
|
||||||
|
echo ""
|
||||||
|
echo "── Running vault-runner ────────────────────────────────────"
|
||||||
|
local vault_runner_path="${FACTORY_ROOT}/nomad/jobs/vault-runner.hcl"
|
||||||
|
if [ -f "$vault_runner_path" ]; then
|
||||||
|
echo "[deploy] vault-runner: running Nomad job (infrastructure)"
|
||||||
|
local -a vault_runner_cmd=("$deploy_sh" "vault-runner")
|
||||||
|
if [ "$(id -u)" -eq 0 ]; then
|
||||||
|
"${vault_runner_cmd[@]}" || exit $?
|
||||||
|
else
|
||||||
|
if ! command -v sudo >/dev/null 2>&1; then
|
||||||
|
echo "Error: deploy.sh must run as root and sudo is not installed" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
sudo -n -- "${vault_runner_cmd[@]}" || exit $?
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "[deploy] vault-runner: jobspec not found, skipping"
|
||||||
|
fi
|
||||||
|
|
||||||
# Print final summary
|
# Print final summary
|
||||||
echo ""
|
echo ""
|
||||||
echo "── Summary ────────────────────────────────────────────"
|
echo "── Summary ────────────────────────────────────────────"
|
||||||
|
|
@ -996,6 +1085,12 @@ _disinto_init_nomad() {
|
||||||
if echo ",$with_services," | grep -q ",agents,"; then
|
if echo ",$with_services," | grep -q ",agents,"; then
|
||||||
echo " agents: (polling loop running)"
|
echo " agents: (polling loop running)"
|
||||||
fi
|
fi
|
||||||
|
if echo ",$with_services," | grep -q ",staging,"; then
|
||||||
|
echo " staging: (internal, no external port)"
|
||||||
|
fi
|
||||||
|
if echo ",$with_services," | grep -q ",chat,"; then
|
||||||
|
echo " chat: 8080"
|
||||||
|
fi
|
||||||
echo "────────────────────────────────────────────────────────"
|
echo "────────────────────────────────────────────────────────"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
@ -1119,14 +1214,25 @@ disinto_init() {
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Auto-include all dependencies when edge is requested (S5.5)
|
||||||
|
if echo ",$with_services," | grep -q ",edge,"; then
|
||||||
|
# Edge depends on all backend services
|
||||||
|
for dep in forgejo woodpecker-server woodpecker-agent agents staging chat; do
|
||||||
|
if ! echo ",$with_services," | grep -q ",${dep},"; then
|
||||||
|
echo "Note: --with edge implies --with ${dep} (edge depends on all backend services)"
|
||||||
|
with_services="${with_services},${dep}"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
# Validate all service names are known
|
# Validate all service names are known
|
||||||
local IFS=','
|
local IFS=','
|
||||||
for _svc in $with_services; do
|
for _svc in $with_services; do
|
||||||
_svc=$(echo "$_svc" | xargs)
|
_svc=$(echo "$_svc" | xargs)
|
||||||
case "$_svc" in
|
case "$_svc" in
|
||||||
forgejo|woodpecker-server|woodpecker-agent|agents) ;;
|
forgejo|woodpecker-server|woodpecker-agent|agents|staging|chat|edge) ;;
|
||||||
*)
|
*)
|
||||||
echo "Error: unknown service '${_svc}' — known: forgejo, woodpecker-server, woodpecker-agent, agents" >&2
|
echo "Error: unknown service '${_svc}' — known: forgejo, woodpecker-server, woodpecker-agent, agents, staging, chat, edge" >&2
|
||||||
exit 1
|
exit 1
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
<!-- last-reviewed: edf7a28bd3c85d4f72d28fd986fd2af3dcb885c1 -->
|
<!-- last-reviewed: b05a31197cc78aa28f3c3e6365e782032bfb25af -->
|
||||||
# Dev Agent
|
# Dev Agent
|
||||||
|
|
||||||
**Role**: Implement issues autonomously — write code, push branches, address
|
**Role**: Implement issues autonomously — write code, push branches, address
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,6 @@ services:
|
||||||
- project-repos:/home/agent/repos
|
- project-repos:/home/agent/repos
|
||||||
- ${CLAUDE_SHARED_DIR:-/var/lib/disinto/claude-shared}:${CLAUDE_SHARED_DIR:-/var/lib/disinto/claude-shared}
|
- ${CLAUDE_SHARED_DIR:-/var/lib/disinto/claude-shared}:${CLAUDE_SHARED_DIR:-/var/lib/disinto/claude-shared}
|
||||||
- ${CLAUDE_CONFIG_FILE:-${HOME}/.claude.json}:/home/agent/.claude.json:ro
|
- ${CLAUDE_CONFIG_FILE:-${HOME}/.claude.json}:/home/agent/.claude.json:ro
|
||||||
- ${CLAUDE_BIN_DIR}:/usr/local/bin/claude:ro
|
|
||||||
- ${AGENT_SSH_DIR:-${HOME}/.ssh}:/home/agent/.ssh:ro
|
- ${AGENT_SSH_DIR:-${HOME}/.ssh}:/home/agent/.ssh:ro
|
||||||
- ${SOPS_AGE_DIR:-${HOME}/.config/sops/age}:/home/agent/.config/sops/age:ro
|
- ${SOPS_AGE_DIR:-${HOME}/.config/sops/age}:/home/agent/.config/sops/age:ro
|
||||||
- woodpecker-data:/woodpecker-data:ro
|
- woodpecker-data:/woodpecker-data:ro
|
||||||
|
|
@ -78,7 +77,6 @@ services:
|
||||||
- project-repos:/home/agent/repos
|
- project-repos:/home/agent/repos
|
||||||
- ${CLAUDE_SHARED_DIR:-/var/lib/disinto/claude-shared}:${CLAUDE_SHARED_DIR:-/var/lib/disinto/claude-shared}
|
- ${CLAUDE_SHARED_DIR:-/var/lib/disinto/claude-shared}:${CLAUDE_SHARED_DIR:-/var/lib/disinto/claude-shared}
|
||||||
- ${CLAUDE_CONFIG_FILE:-${HOME}/.claude.json}:/home/agent/.claude.json:ro
|
- ${CLAUDE_CONFIG_FILE:-${HOME}/.claude.json}:/home/agent/.claude.json:ro
|
||||||
- ${CLAUDE_BIN_DIR}:/usr/local/bin/claude:ro
|
|
||||||
- ${AGENT_SSH_DIR:-${HOME}/.ssh}:/home/agent/.ssh:ro
|
- ${AGENT_SSH_DIR:-${HOME}/.ssh}:/home/agent/.ssh:ro
|
||||||
- ${SOPS_AGE_DIR:-${HOME}/.config/sops/age}:/home/agent/.config/sops/age:ro
|
- ${SOPS_AGE_DIR:-${HOME}/.config/sops/age}:/home/agent/.config/sops/age:ro
|
||||||
- woodpecker-data:/woodpecker-data:ro
|
- woodpecker-data:/woodpecker-data:ro
|
||||||
|
|
@ -139,7 +137,6 @@ services:
|
||||||
- project-repos:/home/agent/repos
|
- project-repos:/home/agent/repos
|
||||||
- ${CLAUDE_SHARED_DIR:-/var/lib/disinto/claude-shared}:${CLAUDE_SHARED_DIR:-/var/lib/disinto/claude-shared}
|
- ${CLAUDE_SHARED_DIR:-/var/lib/disinto/claude-shared}:${CLAUDE_SHARED_DIR:-/var/lib/disinto/claude-shared}
|
||||||
- ${CLAUDE_CONFIG_FILE:-${HOME}/.claude.json}:/home/agent/.claude.json:ro
|
- ${CLAUDE_CONFIG_FILE:-${HOME}/.claude.json}:/home/agent/.claude.json:ro
|
||||||
- ${CLAUDE_BIN_DIR}:/usr/local/bin/claude:ro
|
|
||||||
- ${AGENT_SSH_DIR:-${HOME}/.ssh}:/home/agent/.ssh:ro
|
- ${AGENT_SSH_DIR:-${HOME}/.ssh}:/home/agent/.ssh:ro
|
||||||
- ${SOPS_AGE_DIR:-${HOME}/.config/sops/age}:/home/agent/.config/sops/age:ro
|
- ${SOPS_AGE_DIR:-${HOME}/.config/sops/age}:/home/agent/.config/sops/age:ro
|
||||||
- woodpecker-data:/woodpecker-data:ro
|
- woodpecker-data:/woodpecker-data:ro
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,26 @@
|
||||||
FROM debian:bookworm-slim
|
FROM debian:bookworm-slim
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
bash curl git jq tmux python3 python3-pip openssh-client ca-certificates age shellcheck procps gosu \
|
bash curl git jq tmux nodejs npm python3 python3-pip openssh-client ca-certificates age shellcheck procps gosu \
|
||||||
&& pip3 install --break-system-packages networkx tomlkit \
|
&& pip3 install --break-system-packages networkx tomlkit \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Pre-built binaries (copied from docker/agents/bin/)
|
# Pre-built binaries (copied from docker/agents/bin/)
|
||||||
# SOPS — encrypted data decryption tool
|
# SOPS — encrypted data decryption tool
|
||||||
COPY docker/agents/bin/sops /usr/local/bin/sops
|
# Download sops binary (replaces manual COPY of vendored binary)
|
||||||
RUN chmod +x /usr/local/bin/sops
|
ARG SOPS_VERSION=3.9.4
|
||||||
|
RUN curl -fsSL "https://github.com/getsops/sops/releases/download/v${SOPS_VERSION}/sops-v${SOPS_VERSION}.linux.amd64" \
|
||||||
|
-o /usr/local/bin/sops && chmod +x /usr/local/bin/sops
|
||||||
|
|
||||||
# tea CLI — official Gitea/Forgejo CLI for issue/label/comment operations
|
# tea CLI — official Gitea/Forgejo CLI for issue/label/comment operations
|
||||||
COPY docker/agents/bin/tea /usr/local/bin/tea
|
# Download tea binary (replaces manual COPY of vendored binary)
|
||||||
RUN chmod +x /usr/local/bin/tea
|
ARG TEA_VERSION=0.9.2
|
||||||
|
RUN curl -fsSL "https://dl.gitea.com/tea/${TEA_VERSION}/tea-${TEA_VERSION}-linux-amd64" \
|
||||||
|
-o /usr/local/bin/tea && chmod +x /usr/local/bin/tea
|
||||||
|
|
||||||
# Claude CLI is mounted from the host via docker-compose volume.
|
# Install Claude Code CLI — agent runtime for all LLM backends (llama, Claude API).
|
||||||
# No internet access to cli.anthropic.com required at build time.
|
# The CLI is the execution environment; ANTHROPIC_BASE_URL selects the model provider.
|
||||||
|
RUN npm install -g @anthropic-ai/claude-code@2.1.84
|
||||||
|
|
||||||
# Non-root user
|
# Non-root user
|
||||||
RUN useradd -m -u 1000 -s /bin/bash agent
|
RUN useradd -m -u 1000 -s /bin/bash agent
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,22 @@
|
||||||
# disinto-chat — minimal HTTP backend for Claude chat UI
|
# disinto-chat — minimal HTTP backend for Claude chat UI
|
||||||
#
|
#
|
||||||
# Small Debian slim base with Python runtime.
|
# Small Debian slim base with Python runtime and Node.js.
|
||||||
# Chosen for simplicity and small image size (~100MB).
|
# Chosen for simplicity and small image size (~100MB).
|
||||||
#
|
#
|
||||||
# Image size: ~100MB (well under the 200MB ceiling)
|
# Image size: ~100MB (well under the 200MB ceiling)
|
||||||
#
|
#
|
||||||
# The claude binary is mounted from the host at runtime via docker-compose,
|
# Claude CLI is baked into the image — same pattern as the agents container.
|
||||||
# not baked into the image — same pattern as the agents container.
|
|
||||||
|
|
||||||
FROM debian:bookworm-slim
|
FROM debian:bookworm-slim
|
||||||
|
|
||||||
# Install Python (no build-time network access needed)
|
# Install Node.js (required for Claude CLI) and Python
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
python3 \
|
nodejs npm python3 \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Install Claude Code CLI — chat backend runtime
|
||||||
|
RUN npm install -g @anthropic-ai/claude-code@2.1.84
|
||||||
|
|
||||||
# Non-root user — fixed UID 10001 for sandbox hardening (#706)
|
# Non-root user — fixed UID 10001 for sandbox hardening (#706)
|
||||||
RUN useradd -m -u 10001 -s /bin/bash chat
|
RUN useradd -m -u 10001 -s /bin/bash chat
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -560,10 +560,168 @@ _launch_runner_docker() {
|
||||||
|
|
||||||
# _launch_runner_nomad ACTION_ID SECRETS_CSV MOUNTS_CSV
|
# _launch_runner_nomad ACTION_ID SECRETS_CSV MOUNTS_CSV
|
||||||
#
|
#
|
||||||
# Nomad backend stub — will be implemented in migration Step 5.
|
# Dispatches a vault-runner batch job via `nomad job dispatch`.
|
||||||
|
# Polls `nomad job status` until terminal state (completed/failed).
|
||||||
|
# Reads exit code from allocation and writes <action-id>.result.json.
|
||||||
|
#
|
||||||
|
# Usage: _launch_runner_nomad <action_id> <secrets_csv> <mounts_csv>
|
||||||
|
# Returns: exit code of the nomad job (0=success, non-zero=failure)
|
||||||
_launch_runner_nomad() {
|
_launch_runner_nomad() {
|
||||||
echo "nomad backend not yet implemented" >&2
|
local action_id="$1"
|
||||||
|
local secrets_csv="$2"
|
||||||
|
local mounts_csv="$3"
|
||||||
|
|
||||||
|
log "Dispatching vault-runner batch job via Nomad for action: ${action_id}"
|
||||||
|
|
||||||
|
# Dispatch the parameterized batch job
|
||||||
|
# The vault-runner job expects meta: action_id, secrets_csv
|
||||||
|
# Note: mounts_csv is not passed as meta (not declared in vault-runner.hcl)
|
||||||
|
local dispatch_output
|
||||||
|
dispatch_output=$(nomad job dispatch \
|
||||||
|
-detach \
|
||||||
|
-meta action_id="$action_id" \
|
||||||
|
-meta secrets_csv="$secrets_csv" \
|
||||||
|
vault-runner 2>&1) || {
|
||||||
|
log "ERROR: Failed to dispatch vault-runner job for ${action_id}"
|
||||||
|
log "Dispatch output: ${dispatch_output}"
|
||||||
|
write_result "$action_id" 1 "Nomad dispatch failed: ${dispatch_output}"
|
||||||
return 1
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Extract dispatched job ID from output (format: "vault-runner/dispatch-<timestamp>-<uuid>")
|
||||||
|
local dispatched_job_id
|
||||||
|
dispatched_job_id=$(echo "$dispatch_output" | grep -oP '(?<=Dispatched Job ID = ).+' || true)
|
||||||
|
|
||||||
|
if [ -z "$dispatched_job_id" ]; then
|
||||||
|
log "ERROR: Could not extract dispatched job ID from nomad output"
|
||||||
|
log "Dispatch output: ${dispatch_output}"
|
||||||
|
write_result "$action_id" 1 "Could not extract dispatched job ID from nomad output"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "Dispatched vault-runner with job ID: ${dispatched_job_id}"
|
||||||
|
|
||||||
|
# Poll job status until terminal state
|
||||||
|
# Batch jobs transition: running -> completed/failed
|
||||||
|
local max_wait=300 # 5 minutes max wait
|
||||||
|
local elapsed=0
|
||||||
|
local poll_interval=5
|
||||||
|
local alloc_id=""
|
||||||
|
|
||||||
|
log "Polling nomad job status for ${dispatched_job_id}..."
|
||||||
|
|
||||||
|
while [ "$elapsed" -lt "$max_wait" ]; do
|
||||||
|
# Get job status with JSON output for the dispatched child job
|
||||||
|
local job_status_json
|
||||||
|
job_status_json=$(nomad job status -json "$dispatched_job_id" 2>/dev/null) || {
|
||||||
|
log "ERROR: Failed to get job status for ${dispatched_job_id}"
|
||||||
|
write_result "$action_id" 1 "Failed to get job status for ${dispatched_job_id}"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check job status field (transitions to "dead" on completion)
|
||||||
|
local job_state
|
||||||
|
job_state=$(echo "$job_status_json" | jq -r '.Status // empty' 2>/dev/null) || job_state=""
|
||||||
|
|
||||||
|
# Check allocation state directly
|
||||||
|
alloc_id=$(echo "$job_status_json" | jq -r '.Allocations[0]?.ID // empty' 2>/dev/null) || alloc_id=""
|
||||||
|
|
||||||
|
if [ -n "$alloc_id" ]; then
|
||||||
|
local alloc_state
|
||||||
|
alloc_state=$(nomad alloc status -short "$alloc_id" 2>/dev/null || true)
|
||||||
|
|
||||||
|
case "$alloc_state" in
|
||||||
|
*completed*|*success*|*dead*)
|
||||||
|
log "Allocation ${alloc_id} reached terminal state: ${alloc_state}"
|
||||||
|
break
|
||||||
|
;;
|
||||||
|
*running*|*pending*|*starting*)
|
||||||
|
log "Allocation ${alloc_id} still running (state: ${alloc_state})..."
|
||||||
|
;;
|
||||||
|
*failed*|*crashed*)
|
||||||
|
log "Allocation ${alloc_id} failed (state: ${alloc_state})"
|
||||||
|
break
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Also check job-level state
|
||||||
|
case "$job_state" in
|
||||||
|
dead)
|
||||||
|
log "Job ${dispatched_job_id} reached terminal state: ${job_state}"
|
||||||
|
break
|
||||||
|
;;
|
||||||
|
failed)
|
||||||
|
log "Job ${dispatched_job_id} failed"
|
||||||
|
break
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
sleep "$poll_interval"
|
||||||
|
elapsed=$((elapsed + poll_interval))
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$elapsed" -ge "$max_wait" ]; then
|
||||||
|
log "ERROR: Timeout waiting for vault-runner job to complete"
|
||||||
|
write_result "$action_id" 1 "Timeout waiting for nomad job to complete"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Get final job status and exit code
|
||||||
|
local final_status_json
|
||||||
|
final_status_json=$(nomad job status -json "$dispatched_job_id" 2>/dev/null) || {
|
||||||
|
log "ERROR: Failed to get final job status"
|
||||||
|
write_result "$action_id" 1 "Failed to get final job status"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get allocation exit code
|
||||||
|
local exit_code=0
|
||||||
|
local logs=""
|
||||||
|
|
||||||
|
if [ -n "$alloc_id" ]; then
|
||||||
|
# Get allocation logs
|
||||||
|
logs=$(nomad alloc logs -short "$alloc_id" 2>/dev/null || true)
|
||||||
|
|
||||||
|
# Try to get exit code from alloc status JSON
|
||||||
|
# Nomad alloc status -json has .TaskStates["<task_name>"].Events[].ExitCode
|
||||||
|
local alloc_exit_code
|
||||||
|
alloc_exit_code=$(nomad alloc status -json "$alloc_id" 2>/dev/null | jq -r '.TaskStates["runner"].Events[-1].ExitCode // empty' 2>/dev/null) || alloc_exit_code=""
|
||||||
|
|
||||||
|
if [ -n "$alloc_exit_code" ] && [ "$alloc_exit_code" != "null" ]; then
|
||||||
|
exit_code="$alloc_exit_code"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# If we couldn't get exit code from alloc, check job state as fallback
|
||||||
|
# Note: "dead" = terminal state for batch jobs (includes successful completion)
|
||||||
|
# Only "failed" indicates actual failure
|
||||||
|
if [ "$exit_code" -eq 0 ]; then
|
||||||
|
local final_state
|
||||||
|
final_state=$(echo "$final_status_json" | jq -r '.Status // empty' 2>/dev/null) || final_state=""
|
||||||
|
|
||||||
|
case "$final_state" in
|
||||||
|
failed)
|
||||||
|
exit_code=1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Truncate logs if too long
|
||||||
|
if [ ${#logs} -gt 1000 ]; then
|
||||||
|
logs="${logs: -1000}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Write result file
|
||||||
|
write_result "$action_id" "$exit_code" "$logs"
|
||||||
|
|
||||||
|
if [ "$exit_code" -eq 0 ]; then
|
||||||
|
log "Vault-runner job completed successfully for action: ${action_id}"
|
||||||
|
else
|
||||||
|
log "Vault-runner job failed for action: ${action_id} (exit code: ${exit_code})"
|
||||||
|
fi
|
||||||
|
|
||||||
|
return "$exit_code"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Launch runner for the given action (backend-agnostic orchestrator)
|
# Launch runner for the given action (backend-agnostic orchestrator)
|
||||||
|
|
@ -1051,11 +1209,8 @@ main() {
|
||||||
|
|
||||||
# Validate backend selection at startup
|
# Validate backend selection at startup
|
||||||
case "$DISPATCHER_BACKEND" in
|
case "$DISPATCHER_BACKEND" in
|
||||||
docker) ;;
|
docker|nomad)
|
||||||
nomad)
|
log "Using ${DISPATCHER_BACKEND} backend for vault-runner dispatch"
|
||||||
log "ERROR: nomad backend not yet implemented"
|
|
||||||
echo "nomad backend not yet implemented" >&2
|
|
||||||
exit 1
|
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
log "ERROR: unknown DISPATCHER_BACKEND=${DISPATCHER_BACKEND}"
|
log "ERROR: unknown DISPATCHER_BACKEND=${DISPATCHER_BACKEND}"
|
||||||
|
|
|
||||||
|
|
@ -234,6 +234,13 @@ fi
|
||||||
rm -f "$_fetch_log"
|
rm -f "$_fetch_log"
|
||||||
done) &
|
done) &
|
||||||
|
|
||||||
|
# Nomad template renders Caddyfile to /local/Caddyfile via service discovery;
|
||||||
|
# copy it into the expected location if present (compose uses the mounted path).
|
||||||
|
if [ -f /local/Caddyfile ]; then
|
||||||
|
cp /local/Caddyfile /etc/caddy/Caddyfile
|
||||||
|
echo "edge: using Nomad-rendered Caddyfile from /local/Caddyfile" >&2
|
||||||
|
fi
|
||||||
|
|
||||||
# Caddy as main process — run in foreground via wait so background jobs survive
|
# Caddy as main process — run in foreground via wait so background jobs survive
|
||||||
# (exec replaces the shell, which can orphan backgrounded subshells)
|
# (exec replaces the shell, which can orphan backgrounded subshells)
|
||||||
caddy run --config /etc/caddy/Caddyfile --adapter caddyfile &
|
caddy run --config /etc/caddy/Caddyfile --adapter caddyfile &
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
<!-- last-reviewed: edf7a28bd3c85d4f72d28fd986fd2af3dcb885c1 -->
|
<!-- last-reviewed: b05a31197cc78aa28f3c3e6365e782032bfb25af -->
|
||||||
# Gardener Agent
|
# Gardener Agent
|
||||||
|
|
||||||
**Role**: Backlog grooming — detect duplicate issues, missing acceptance
|
**Role**: Backlog grooming — detect duplicate issues, missing acceptance
|
||||||
|
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
{"issue":915,"group":"lib/generators.sh","title":"remove no-op sed in generate_compose --build mode","reason":"sed replaces agents: with itself — no behavior change; single-line removal","ts":"2026-04-17T01:04:05Z"}
|
|
||||||
|
|
@ -1,37 +1,12 @@
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"action": "edit_body",
|
"action": "edit_body",
|
||||||
"issue": 947,
|
"issue": 915,
|
||||||
"body": "Flagged by AI reviewer in PR #945.\n\n## Problem\n\n`lib/init/nomad/wp-oauth-register.sh` line 46 computes REPO_ROOT with only two `../` levels:\n\n```bash\nREPO_ROOT=\"$(cd \"${SCRIPT_DIR}/../..\" && pwd)\"\n```\n\nBut the script lives at `lib/init/nomad/` — three levels deep — so `../../..` is required. Every sibling script in the same directory (`vault-engines.sh`, `vault-nomad-auth.sh`, `cluster-up.sh`, `systemd-vault.sh`) uses `../../..`.\n\nWith this bug, REPO_ROOT resolves to `lib/` (not the repo root). The subsequent `source \"${REPO_ROOT}/lib/hvault.sh\"` then looks for `lib/lib/hvault.sh` — a path that does not exist. The script fails at startup.\n\n## Fix\n\n```bash\nREPO_ROOT=\"$(cd \"${SCRIPT_DIR}/../../..\" && pwd)\"\n```\n\n*Auto-created from AI review*\n\n## Affected files\n- `lib/init/nomad/wp-oauth-register.sh` (line 46 — REPO_ROOT path depth)\n\n## Acceptance criteria\n- [ ] `REPO_ROOT` in `wp-oauth-register.sh` uses `../../..` (three levels up), matching all sibling scripts\n- [ ] `source \"${REPO_ROOT}/lib/hvault.sh\"` resolves correctly at runtime\n- [ ] `shellcheck` clean\n- [ ] CI green\n"
|
"body": "Flagged by AI reviewer in PR \\#911.\n\n## Problem\n\n`lib/generators.sh` line 660 contains a no-op `sed` invocation:\n```\nsed -i 's|^\\( agents:\\)|\\1|' \"$compose_file\"\n```\n\nThis replaces ` agents:` with itself — it does nothing. It is dead code left over from a prior iteration.\n\n## Fix\n\nRemove the no-op `sed` line at line 660 of `lib/generators.sh`.\n\n## Affected files\n- `lib/generators.sh` (line 660 — the no-op sed invocation in generate_compose --build mode)\n\n## Acceptance criteria\n- [ ] The no-op sed line is removed from `lib/generators.sh`\n- [ ] `shellcheck` clean on `lib/generators.sh`\n- [ ] CI green\n\n---\n*Auto-created from AI review*"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"action": "add_label",
|
"action": "add_label",
|
||||||
"issue": 947,
|
"issue": 915,
|
||||||
"label": "backlog"
|
"label": "backlog"
|
||||||
},
|
|
||||||
{
|
|
||||||
"action": "edit_body",
|
|
||||||
"issue": 950,
|
|
||||||
"body": "Flagged by AI reviewer in PR #949.\n\n## Problem\n\nAfter PR #949 the real run path in `_disinto_init_nomad` interleaves seed+deploy per service (seed-forgejo → deploy-forgejo → seed-woodpecker → deploy-woodpecker-…). However the dry-run preview block (`bin/disinto` ~lines 785–839) still displays the old batch pattern: all seeds listed first, then all deploys.\n\nBefore #949 both paths were consistent. Now dry-run output misrepresents what will actually execute, which can mislead operators planning or auditing a run.\n\n## Fix\nUpdate the dry-run block to emit one \"[dry-run] seed X → deploy X\" pair per service in canonical order, matching the real-run interleaved sequence.\n\n*Auto-created from AI review*\n\n## Affected files\n- `bin/disinto` (dry-run preview block, ~lines 785–839)\n\n## Acceptance criteria\n- [ ] `disinto init --dry-run` output shows one `[dry-run] seed X → deploy X` pair per service, in canonical order\n- [ ] Dry-run output matches the real-run execution order from `_disinto_init_nomad`\n- [ ] No behavior change to real run path\n- [ ] `shellcheck` clean\n- [ ] CI green\n"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"action": "add_label",
|
|
||||||
"issue": 950,
|
|
||||||
"label": "backlog"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"action": "remove_label",
|
|
||||||
"issue": 850,
|
|
||||||
"label": "blocked"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"action": "add_label",
|
|
||||||
"issue": 850,
|
|
||||||
"label": "backlog"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"action": "comment",
|
|
||||||
"issue": 850,
|
|
||||||
"body": "Gardener: removing blocked label — prior PRs (#872, #908) failed due to implementation issues (TEST_DIR unbound variable, compose early-return), not external dependencies. Fix path is fully documented in the issue body. Re-queueing as backlog for dev-agent pickup."
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
<!-- last-reviewed: edf7a28bd3c85d4f72d28fd986fd2af3dcb885c1 -->
|
<!-- last-reviewed: b05a31197cc78aa28f3c3e6365e782032bfb25af -->
|
||||||
# Shared Helpers (`lib/`)
|
# Shared Helpers (`lib/`)
|
||||||
|
|
||||||
All agents source `lib/env.sh` as their first action. Additional helpers are
|
All agents source `lib/env.sh` as their first action. Additional helpers are
|
||||||
|
|
@ -30,9 +30,9 @@ sourced as needed.
|
||||||
| `lib/git-creds.sh` | Shared git credential helper configuration. `configure_git_creds([HOME_DIR] [RUN_AS_CMD])` — writes a static credential helper script and configures git globally to use password-based HTTP auth (Forgejo 11.x rejects API tokens for `git push`, #361). **Retry on cold boot (#741)**: resolves bot username from `FORGE_TOKEN` with 5 retries (exponential backoff 1-5s); fails loudly and returns 1 if Forgejo is unreachable — never falls back to a wrong hardcoded default (exports `BOT_USER` on success). `repair_baked_cred_urls([--as RUN_AS_CMD] DIR ...)` — rewrites any git remote URLs that have credentials baked in to use clean URLs instead; uses `safe.directory` bypass for root-owned repos (#671). Requires `FORGE_PASS`, `FORGE_URL`, `FORGE_TOKEN`. | entrypoints (agents, edge) |
|
| `lib/git-creds.sh` | Shared git credential helper configuration. `configure_git_creds([HOME_DIR] [RUN_AS_CMD])` — writes a static credential helper script and configures git globally to use password-based HTTP auth (Forgejo 11.x rejects API tokens for `git push`, #361). **Retry on cold boot (#741)**: resolves bot username from `FORGE_TOKEN` with 5 retries (exponential backoff 1-5s); fails loudly and returns 1 if Forgejo is unreachable — never falls back to a wrong hardcoded default (exports `BOT_USER` on success). `repair_baked_cred_urls([--as RUN_AS_CMD] DIR ...)` — rewrites any git remote URLs that have credentials baked in to use clean URLs instead; uses `safe.directory` bypass for root-owned repos (#671). Requires `FORGE_PASS`, `FORGE_URL`, `FORGE_TOKEN`. | entrypoints (agents, edge) |
|
||||||
| `lib/ops-setup.sh` | `setup_ops_repo()` — creates ops repo on Forgejo if it doesn't exist, configures bot collaborators, clones/initializes ops repo locally, seeds directory structure (vault, knowledge, evidence, sprints). Evidence subdirectories seeded: engagement/, red-team/, holdout/, evolution/, user-test/. Also seeds sprints/ for architect output. Exports `_ACTUAL_OPS_SLUG`. `migrate_ops_repo(ops_root, [primary_branch])` — idempotent migration helper that seeds missing directories and .gitkeep files on existing ops repos (pre-#407 deployments). | bin/disinto (init) |
|
| `lib/ops-setup.sh` | `setup_ops_repo()` — creates ops repo on Forgejo if it doesn't exist, configures bot collaborators, clones/initializes ops repo locally, seeds directory structure (vault, knowledge, evidence, sprints). Evidence subdirectories seeded: engagement/, red-team/, holdout/, evolution/, user-test/. Also seeds sprints/ for architect output. Exports `_ACTUAL_OPS_SLUG`. `migrate_ops_repo(ops_root, [primary_branch])` — idempotent migration helper that seeds missing directories and .gitkeep files on existing ops repos (pre-#407 deployments). | bin/disinto (init) |
|
||||||
| `lib/ci-setup.sh` | `_install_cron_impl()` — installs crontab entries for bare-metal deployments (compose mode uses polling loop instead). `_create_forgejo_oauth_app()` — generic helper to create an OAuth2 app on Forgejo (shared by Woodpecker and chat). `_create_woodpecker_oauth_impl()` — creates Woodpecker OAuth2 app (thin wrapper). `_create_chat_oauth_impl()` — creates disinto-chat OAuth2 app, writes `CHAT_OAUTH_CLIENT_ID`/`CHAT_OAUTH_CLIENT_SECRET` to `.env` (#708). `_generate_woodpecker_token_impl()` — auto-generates WOODPECKER_TOKEN via OAuth2 flow. `_activate_woodpecker_repo_impl()` — activates repo in Woodpecker. All gated by `_load_ci_context()` which validates required env vars. | bin/disinto (init) |
|
| `lib/ci-setup.sh` | `_install_cron_impl()` — installs crontab entries for bare-metal deployments (compose mode uses polling loop instead). `_create_forgejo_oauth_app()` — generic helper to create an OAuth2 app on Forgejo (shared by Woodpecker and chat). `_create_woodpecker_oauth_impl()` — creates Woodpecker OAuth2 app (thin wrapper). `_create_chat_oauth_impl()` — creates disinto-chat OAuth2 app, writes `CHAT_OAUTH_CLIENT_ID`/`CHAT_OAUTH_CLIENT_SECRET` to `.env` (#708). `_generate_woodpecker_token_impl()` — auto-generates WOODPECKER_TOKEN via OAuth2 flow. `_activate_woodpecker_repo_impl()` — activates repo in Woodpecker. All gated by `_load_ci_context()` which validates required env vars. | bin/disinto (init) |
|
||||||
| `lib/generators.sh` | Template generation for `disinto init`: `generate_compose()` — docker-compose.yml (uses `codeberg.org/forgejo/forgejo:11.0` tag; adds `security_opt: [apparmor:unconfined]` to all services for rootless container compatibility; Forgejo includes a healthcheck so dependent services use `condition: service_healthy` — fixes cold-start races, #665; adds `chat` service block with isolated `chat-config` named volume and `CHAT_HISTORY_DIR` bind-mount for per-user NDJSON history persistence (#710); injects `FORWARD_AUTH_SECRET` for Caddy↔chat defense-in-depth auth (#709); cost-cap env vars `CHAT_MAX_REQUESTS_PER_HOUR`, `CHAT_MAX_REQUESTS_PER_DAY`, `CHAT_MAX_TOKENS_PER_DAY` (#711); subdomain fallback comment for `EDGE_TUNNEL_FQDN_*` vars (#713); all `depends_on` now use `condition: service_healthy/started` instead of bare service names; all services now include `restart: unless-stopped` including the edge service — #768; agents service now uses `image: ghcr.io/disinto/agents:${DISINTO_IMAGE_TAG:-latest}` instead of `build:` (#429); `WOODPECKER_PLUGINS_PRIVILEGED` env var added to woodpecker service (#779); agents-llama conditional block gated on `ENABLE_LLAMA_AGENT=1` (#769); `agents-llama-all` compose service (profile `agents-llama-all`, all 7 roles: review,dev,gardener,architect,planner,predictor,supervisor) added by #801; agents service gains volume mounts for `./projects`, `./.env`, `./state`), `generate_caddyfile()` — Caddyfile (routes: `/forge/*` → forgejo:3000, `/woodpecker/*` → woodpecker:8000, `/staging/*` → staging:80; `/chat/login` and `/chat/oauth/callback` bypass `forward_auth` so unauthenticated users can reach the OAuth flow; `/chat/*` gated by `forward_auth` on `chat:8080/chat/auth/verify` which stamps `X-Forwarded-User` (#709); root `/` redirects to `/forge/`), `generate_staging_index()` — staging index, `generate_deploy_pipelines()` — Woodpecker deployment pipeline configs. Requires `FACTORY_ROOT`, `PROJECT_NAME`, `PRIMARY_BRANCH`. | bin/disinto (init) |
|
| `lib/generators.sh` | Template generation for `disinto init`: `generate_compose()` — docker-compose.yml (uses `codeberg.org/forgejo/forgejo:11.0` tag; `CLAUDE_BIN_DIR` volume mount removed from agents/llama services — only `reproduce` and `edge` still use the host-mounted CLI (#992); adds `security_opt: [apparmor:unconfined]` to all services for rootless container compatibility; Forgejo includes a healthcheck so dependent services use `condition: service_healthy` — fixes cold-start races, #665; adds `chat` service block with isolated `chat-config` named volume and `CHAT_HISTORY_DIR` bind-mount for per-user NDJSON history persistence (#710); injects `FORWARD_AUTH_SECRET` for Caddy↔chat defense-in-depth auth (#709); cost-cap env vars `CHAT_MAX_REQUESTS_PER_HOUR`, `CHAT_MAX_REQUESTS_PER_DAY`, `CHAT_MAX_TOKENS_PER_DAY` (#711); subdomain fallback comment for `EDGE_TUNNEL_FQDN_*` vars (#713); all `depends_on` now use `condition: service_healthy/started` instead of bare service names; all services now include `restart: unless-stopped` including the edge service — #768; agents service now uses `image: ghcr.io/disinto/agents:${DISINTO_IMAGE_TAG:-latest}` instead of `build:` (#429); `WOODPECKER_PLUGINS_PRIVILEGED` env var added to woodpecker service (#779); agents-llama conditional block gated on `ENABLE_LLAMA_AGENT=1` (#769); `agents-llama-all` compose service (profile `agents-llama-all`, all 7 roles: review,dev,gardener,architect,planner,predictor,supervisor) added by #801; agents service gains volume mounts for `./projects`, `./.env`, `./state`), `generate_caddyfile()` — Caddyfile (routes: `/forge/*` → forgejo:3000, `/woodpecker/*` → woodpecker:8000, `/staging/*` → staging:80; `/chat/login` and `/chat/oauth/callback` bypass `forward_auth` so unauthenticated users can reach the OAuth flow; `/chat/*` gated by `forward_auth` on `chat:8080/chat/auth/verify` which stamps `X-Forwarded-User` (#709); root `/` redirects to `/forge/`), `generate_staging_index()` — staging index, `generate_deploy_pipelines()` — Woodpecker deployment pipeline configs. Requires `FACTORY_ROOT`, `PROJECT_NAME`, `PRIMARY_BRANCH`. | bin/disinto (init) |
|
||||||
| `lib/sprint-filer.sh` | Post-merge sub-issue filer for sprint PRs. Invoked by the `.woodpecker/ops-filer.yml` pipeline after a sprint PR merges to ops repo `main`. Parses `<!-- filer:begin --> ... <!-- filer:end -->` blocks from sprint PR bodies to extract sub-issue definitions, creates them on the project repo using `FORGE_FILER_TOKEN` (narrow-scope `filer-bot` identity with `issues:write` only), adds `in-progress` label to the parent vision issue, and handles vision lifecycle closure when all sub-issues are closed. Uses `filer_api_all()` for paginated fetches. Idempotent: uses `<!-- decomposed-from: #<vision>, sprint: <slug>, id: <id> -->` markers to skip already-filed issues. Requires `FORGE_FILER_TOKEN`, `FORGE_API`, `FORGE_API_BASE`, `FORGE_OPS_REPO`. | `.woodpecker/ops-filer.yml` (CI pipeline on ops repo) |
|
| `lib/sprint-filer.sh` | Post-merge sub-issue filer for sprint PRs. Invoked by the `.woodpecker/ops-filer.yml` pipeline after a sprint PR merges to ops repo `main`. Parses `<!-- filer:begin --> ... <!-- filer:end -->` blocks from sprint PR bodies to extract sub-issue definitions, creates them on the project repo using `FORGE_FILER_TOKEN` (narrow-scope `filer-bot` identity with `issues:write` only), adds `in-progress` label to the parent vision issue, and handles vision lifecycle closure when all sub-issues are closed. Uses `filer_api_all()` for paginated fetches. Idempotent: uses `<!-- decomposed-from: #<vision>, sprint: <slug>, id: <id> -->` markers to skip already-filed issues. Requires `FORGE_FILER_TOKEN`, `FORGE_API`, `FORGE_API_BASE`, `FORGE_OPS_REPO`. | `.woodpecker/ops-filer.yml` (CI pipeline on ops repo) |
|
||||||
| `lib/hire-agent.sh` | `disinto_hire_an_agent()` — user creation, `.profile` repo setup, formula copying, branch protection, and state marker creation for hiring a new agent. Requires `FORGE_URL`, `FORGE_TOKEN`, `FACTORY_ROOT`, `PROJECT_NAME`. Extracted from `bin/disinto`. | bin/disinto (hire) |
|
| `lib/hire-agent.sh` | `disinto_hire_an_agent()` — user creation, `.profile` repo setup, formula copying, branch protection, and state marker creation for hiring a new agent. Requires `FORGE_URL`, `FORGE_TOKEN`, `FACTORY_ROOT`, `PROJECT_NAME`. Extracted from `bin/disinto`. | bin/disinto (hire) |
|
||||||
| `lib/release.sh` | `disinto_release()` — vault TOML creation, branch setup on ops repo, PR creation, and auto-merge request for a versioned release. `_assert_release_globals()` validates required env vars. Requires `FORGE_URL`, `FORGE_TOKEN`, `FORGE_OPS_REPO`, `FACTORY_ROOT`, `PRIMARY_BRANCH`. Extracted from `bin/disinto`. | bin/disinto (release) |
|
| `lib/release.sh` | `disinto_release()` — vault TOML creation, branch setup on ops repo, PR creation, and auto-merge request for a versioned release. `_assert_release_globals()` validates required env vars. Requires `FORGE_URL`, `FORGE_TOKEN`, `FORGE_OPS_REPO`, `FACTORY_ROOT`, `PRIMARY_BRANCH`. Extracted from `bin/disinto`. | bin/disinto (release) |
|
||||||
| `lib/hvault.sh` | HashiCorp Vault helper module. `hvault_kv_get(PATH, [KEY])` — read KV v2 secret, optionally extract one key. `hvault_kv_put(PATH, KEY=VAL ...)` — write KV v2 secret. `hvault_kv_list(PATH)` — list keys at a KV path. `hvault_get_or_empty(PATH)` — GET /v1/PATH; 200→raw body, 404→empty, else structured error + return 1 (used by sync scripts to distinguish "absent, create" from hard failure without tripping errexit, #881). `hvault_ensure_kv_v2(MOUNT, [LOG_PREFIX])` — idempotent KV v2 mount assertion: enables mount if absent, fails loudly if present as wrong type/version. Extracted from all `vault-seed-*.sh` scripts to eliminate dup-detector violations. Respects `DRY_RUN=1`. `hvault_policy_apply(NAME, FILE)` — idempotent policy upsert. `hvault_jwt_login(ROLE, JWT)` — exchange JWT for short-lived token. `hvault_token_lookup()` — returns TTL/policies/accessor for current token. All functions use `VAULT_ADDR` + `VAULT_TOKEN` from env (fallback: `/etc/vault.d/root.token`), emit structured JSON errors to stderr on failure. Tests: `tests/lib-hvault.bats` (requires `vault server -dev`). | `tools/vault-apply-policies.sh`, `tools/vault-apply-roles.sh`, `lib/init/nomad/vault-nomad-auth.sh`, `tools/vault-seed-*.sh` |
|
| `lib/hvault.sh` | HashiCorp Vault helper module. `hvault_kv_get(PATH, [KEY])` — read KV v2 secret, optionally extract one key. `hvault_kv_put(PATH, KEY=VAL ...)` — write KV v2 secret. `hvault_kv_list(PATH)` — list keys at a KV path. `hvault_get_or_empty(PATH)` — GET /v1/PATH; 200→raw body, 404→empty, else structured error + return 1 (used by sync scripts to distinguish "absent, create" from hard failure without tripping errexit, #881). `hvault_ensure_kv_v2(MOUNT, [LOG_PREFIX])` — idempotent KV v2 mount assertion: enables mount if absent, fails loudly if present as wrong type/version. Extracted from all `vault-seed-*.sh` scripts to eliminate dup-detector violations. Respects `DRY_RUN=1`. `hvault_policy_apply(NAME, FILE)` — idempotent policy upsert. `hvault_jwt_login(ROLE, JWT)` — exchange JWT for short-lived token. `hvault_token_lookup()` — returns TTL/policies/accessor for current token. `_hvault_seed_key(PATH, KEY, [GENERATOR])` — seed one KV key if absent; reads existing data and merges to preserve sibling keys (KV v2 replaces atomically); returns 0=created, 1=unchanged, 2=API error (#992). All functions use `VAULT_ADDR` + `VAULT_TOKEN` from env (fallback: `/etc/vault.d/root.token`), emit structured JSON errors to stderr on failure. Tests: `tests/lib-hvault.bats` (requires `vault server -dev`). | `tools/vault-apply-policies.sh`, `tools/vault-apply-roles.sh`, `lib/init/nomad/vault-nomad-auth.sh`, `tools/vault-seed-*.sh` |
|
||||||
| `lib/init/nomad/` | Nomad+Vault installer scripts. `cluster-up.sh` — idempotent Step-0 orchestrator that runs all steps in order (installs packages, writes HCL, enables systemd units, unseals Vault); uses `poll_until_healthy()` helper for deduped readiness polling. `install.sh` — installs pinned Nomad+Vault apt packages. `vault-init.sh` — initializes Vault (unseal keys → `/etc/vault.d/`), creates dev-persisted unseal unit. `lib-systemd.sh` — shared systemd unit helpers. `systemd-nomad.sh`, `systemd-vault.sh` — write and enable service units. `vault-nomad-auth.sh` — Step-2 script that enables Vault's JWT auth at path `jwt-nomad`, writes the JWKS/algs config pointing at Nomad's workload-identity signer, delegates role sync to `tools/vault-apply-roles.sh`, installs `/etc/nomad.d/server.hcl`, and SIGHUPs `nomad.service` if the file changed (#881). `wp-oauth-register.sh` — S3.3 script that creates the Woodpecker OAuth2 app in Forgejo and stores `forgejo_client`/`forgejo_secret` in Vault KV v2 at `kv/disinto/shared/woodpecker`; idempotent (skips if app or secrets already present); called by `bin/disinto --with woodpecker`. `deploy.sh` — S4 dependency-ordered Nomad job deploy + health-wait; takes a list of jobspec basenames, submits each to Nomad and polls until healthy before proceeding to the next; supports `--dry-run` and per-job timeout overrides via `JOB_READY_TIMEOUT_<JOBNAME>`; invoked by `bin/disinto --with <svc>` and `cluster-up.sh`. Idempotent: each step checks current state before acting. Sourced and called by `cluster-up.sh`; not sourced by agents. | `bin/disinto init --backend=nomad` |
|
| `lib/init/nomad/` | Nomad+Vault installer scripts. `cluster-up.sh` — idempotent Step-0 orchestrator that runs all steps in order (installs packages, writes HCL, enables systemd units, unseals Vault); uses `poll_until_healthy()` helper for deduped readiness polling; `HOST_VOLUME_DIRS` array now includes `/srv/disinto/docker` (for staging file-server, S5.2, #989, #992). `install.sh` — installs pinned Nomad+Vault apt packages. `vault-init.sh` — initializes Vault (unseal keys → `/etc/vault.d/`), creates dev-persisted unseal unit. `lib-systemd.sh` — shared systemd unit helpers. `systemd-nomad.sh`, `systemd-vault.sh` — write and enable service units. `vault-nomad-auth.sh` — Step-2 script that enables Vault's JWT auth at path `jwt-nomad`, writes the JWKS/algs config pointing at Nomad's workload-identity signer, delegates role sync to `tools/vault-apply-roles.sh`, installs `/etc/nomad.d/server.hcl`, and SIGHUPs `nomad.service` if the file changed (#881). `wp-oauth-register.sh` — S3.3 script that creates the Woodpecker OAuth2 app in Forgejo and stores `forgejo_client`/`forgejo_secret` in Vault KV v2 at `kv/disinto/shared/woodpecker`; idempotent (skips if app or secrets already present); called by `bin/disinto --with woodpecker`. `deploy.sh` — S4 dependency-ordered Nomad job deploy + health-wait; takes a list of jobspec basenames, submits each to Nomad and polls until healthy before proceeding to the next; supports `--dry-run` and per-job timeout overrides via `JOB_READY_TIMEOUT_<JOBNAME>`; invoked by `bin/disinto --with <svc>` and `cluster-up.sh`; deploy order now covers staging, chat, edge (S5.5, #992). Idempotent: each step checks current state before acting. Sourced and called by `cluster-up.sh`; not sourced by agents. | `bin/disinto init --backend=nomad` |
|
||||||
|
|
|
||||||
|
|
@ -66,27 +66,6 @@ _get_primary_woodpecker_repo_id() {
|
||||||
echo "$max_id"
|
echo "$max_id"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Track service names to detect duplicates at generate-time.
|
|
||||||
# Associative arrays for O(1) lookup of seen services and their sources.
|
|
||||||
declare -A _seen_services
|
|
||||||
declare -A _service_sources
|
|
||||||
|
|
||||||
# Record a service name and source; return 1 if duplicate detected.
|
|
||||||
_record_service() {
|
|
||||||
local service_name="$1"
|
|
||||||
local source="$2"
|
|
||||||
if [ -n "${_seen_services[$service_name]:-}" ]; then
|
|
||||||
local original_source="${_service_sources[$service_name]}"
|
|
||||||
echo "ERROR: Duplicate service name '$service_name' detected —" >&2
|
|
||||||
echo " '$service_name' emitted twice — from $original_source and from $source" >&2
|
|
||||||
echo " Remove one of the conflicting activations to proceed." >&2
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
_seen_services[$service_name]=1
|
|
||||||
_service_sources[$service_name]="$source"
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# Parse project TOML for local-model agents and emit compose services.
|
# Parse project TOML for local-model agents and emit compose services.
|
||||||
# Writes service definitions to stdout; caller handles insertion into compose file.
|
# Writes service definitions to stdout; caller handles insertion into compose file.
|
||||||
_generate_local_model_services() {
|
_generate_local_model_services() {
|
||||||
|
|
@ -118,16 +97,6 @@ _generate_local_model_services() {
|
||||||
POLL_INTERVAL) poll_interval_val="$value" ;;
|
POLL_INTERVAL) poll_interval_val="$value" ;;
|
||||||
---)
|
---)
|
||||||
if [ -n "$service_name" ] && [ -n "$base_url" ]; then
|
if [ -n "$service_name" ] && [ -n "$base_url" ]; then
|
||||||
# Record service for duplicate detection using the full service name
|
|
||||||
local full_service_name="agents-${service_name}"
|
|
||||||
local toml_basename
|
|
||||||
toml_basename=$(basename "$toml")
|
|
||||||
if ! _record_service "$full_service_name" "[agents.$service_name] in projects/$toml_basename"; then
|
|
||||||
# Duplicate detected — clean up and abort
|
|
||||||
rm -f "$temp_file"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Per-agent FORGE_TOKEN / FORGE_PASS lookup (#834 Gap 3).
|
# Per-agent FORGE_TOKEN / FORGE_PASS lookup (#834 Gap 3).
|
||||||
# Two hired llama agents must not share the same Forgejo identity,
|
# Two hired llama agents must not share the same Forgejo identity,
|
||||||
# so we key the env-var lookup by forge_user (which hire-agent.sh
|
# so we key the env-var lookup by forge_user (which hire-agent.sh
|
||||||
|
|
@ -168,7 +137,6 @@ _generate_local_model_services() {
|
||||||
- project-repos-${service_name}:/home/agent/repos
|
- project-repos-${service_name}:/home/agent/repos
|
||||||
- \${CLAUDE_SHARED_DIR:-/var/lib/disinto/claude-shared}:\${CLAUDE_SHARED_DIR:-/var/lib/disinto/claude-shared}
|
- \${CLAUDE_SHARED_DIR:-/var/lib/disinto/claude-shared}:\${CLAUDE_SHARED_DIR:-/var/lib/disinto/claude-shared}
|
||||||
- \${CLAUDE_CONFIG_FILE:-\${HOME}/.claude.json}:/home/agent/.claude.json:ro
|
- \${CLAUDE_CONFIG_FILE:-\${HOME}/.claude.json}:/home/agent/.claude.json:ro
|
||||||
- \${CLAUDE_BIN_DIR}:/usr/local/bin/claude:ro
|
|
||||||
- \${AGENT_SSH_DIR:-\${HOME}/.ssh}:/home/agent/.ssh:ro
|
- \${AGENT_SSH_DIR:-\${HOME}/.ssh}:/home/agent/.ssh:ro
|
||||||
- ./projects:/home/agent/disinto/projects:ro
|
- ./projects:/home/agent/disinto/projects:ro
|
||||||
- ./.env:/home/agent/disinto/.env:ro
|
- ./.env:/home/agent/disinto/.env:ro
|
||||||
|
|
@ -313,28 +281,6 @@ _generate_compose_impl() {
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Initialize duplicate detection with base services defined in the template
|
|
||||||
_record_service "agents" "base compose template" || return 1
|
|
||||||
_record_service "forgejo" "base compose template" || return 1
|
|
||||||
_record_service "woodpecker" "base compose template" || return 1
|
|
||||||
_record_service "woodpecker-agent" "base compose template" || return 1
|
|
||||||
_record_service "runner" "base compose template" || return 1
|
|
||||||
_record_service "edge" "base compose template" || return 1
|
|
||||||
_record_service "staging" "base compose template" || return 1
|
|
||||||
_record_service "staging-deploy" "base compose template" || return 1
|
|
||||||
_record_service "chat" "base compose template" || return 1
|
|
||||||
|
|
||||||
# Check for legacy ENABLE_LLAMA_AGENT (now rejected at runtime, but check here)
|
|
||||||
# This ensures clear error message at generate-time, not at container startup
|
|
||||||
if [ "${ENABLE_LLAMA_AGENT:-0}" = "1" ]; then
|
|
||||||
if ! _record_service "agents-llama" "ENABLE_LLAMA_AGENT=1"; then
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
if ! _record_service "agents-llama-all" "ENABLE_LLAMA_AGENT=1"; then
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Extract primary woodpecker_repo_id from project TOML files
|
# Extract primary woodpecker_repo_id from project TOML files
|
||||||
local wp_repo_id
|
local wp_repo_id
|
||||||
wp_repo_id=$(_get_primary_woodpecker_repo_id)
|
wp_repo_id=$(_get_primary_woodpecker_repo_id)
|
||||||
|
|
@ -435,7 +381,6 @@ services:
|
||||||
- project-repos:/home/agent/repos
|
- project-repos:/home/agent/repos
|
||||||
- ${CLAUDE_SHARED_DIR:-/var/lib/disinto/claude-shared}:${CLAUDE_SHARED_DIR:-/var/lib/disinto/claude-shared}
|
- ${CLAUDE_SHARED_DIR:-/var/lib/disinto/claude-shared}:${CLAUDE_SHARED_DIR:-/var/lib/disinto/claude-shared}
|
||||||
- ${CLAUDE_CONFIG_FILE:-${HOME}/.claude.json}:/home/agent/.claude.json:ro
|
- ${CLAUDE_CONFIG_FILE:-${HOME}/.claude.json}:/home/agent/.claude.json:ro
|
||||||
- ${CLAUDE_BIN_DIR}:/usr/local/bin/claude:ro
|
|
||||||
- ${AGENT_SSH_DIR:-${HOME}/.ssh}:/home/agent/.ssh:ro
|
- ${AGENT_SSH_DIR:-${HOME}/.ssh}:/home/agent/.ssh:ro
|
||||||
- ${SOPS_AGE_DIR:-${HOME}/.config/sops/age}:/home/agent/.config/sops/age:ro
|
- ${SOPS_AGE_DIR:-${HOME}/.config/sops/age}:/home/agent/.config/sops/age:ro
|
||||||
- woodpecker-data:/woodpecker-data:ro
|
- woodpecker-data:/woodpecker-data:ro
|
||||||
|
|
@ -686,19 +631,16 @@ COMPOSEEOF
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Append local-model agent services if any are configured
|
# Append local-model agent services if any are configured
|
||||||
if ! _generate_local_model_services "$compose_file"; then
|
_generate_local_model_services "$compose_file"
|
||||||
echo "ERROR: Failed to generate local-model agent services. See errors above." >&2
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Resolve the Claude CLI binary path and persist as CLAUDE_BIN_DIR in .env.
|
# Resolve the Claude CLI binary path and persist as CLAUDE_BIN_DIR in .env.
|
||||||
# docker-compose.yml references ${CLAUDE_BIN_DIR} so the value must be set.
|
# Only used by reproduce and edge services which still use host-mounted CLI.
|
||||||
local claude_bin
|
local claude_bin
|
||||||
claude_bin="$(command -v claude 2>/dev/null || true)"
|
claude_bin="$(command -v claude 2>/dev/null || true)"
|
||||||
if [ -n "$claude_bin" ]; then
|
if [ -n "$claude_bin" ]; then
|
||||||
claude_bin="$(readlink -f "$claude_bin")"
|
claude_bin="$(readlink -f "$claude_bin")"
|
||||||
else
|
else
|
||||||
echo "Warning: claude CLI not found in PATH — set CLAUDE_BIN_DIR in .env manually" >&2
|
echo "Warning: claude CLI not found in PATH — reproduce/edge services will fail to start" >&2
|
||||||
claude_bin="/usr/local/bin/claude"
|
claude_bin="/usr/local/bin/claude"
|
||||||
fi
|
fi
|
||||||
# Persist CLAUDE_BIN_DIR into .env so docker-compose can resolve it.
|
# Persist CLAUDE_BIN_DIR into .env so docker-compose can resolve it.
|
||||||
|
|
@ -715,7 +657,6 @@ COMPOSEEOF
|
||||||
|
|
||||||
# In build mode, replace image: with build: for locally-built images
|
# In build mode, replace image: with build: for locally-built images
|
||||||
if [ "$use_build" = true ]; then
|
if [ "$use_build" = true ]; then
|
||||||
sed -i 's|^\( agents:\)|\1|' "$compose_file"
|
|
||||||
sed -i '/^ image: ghcr\.io\/disinto\/agents:/{s|image: ghcr\.io/disinto/agents:.*|build:\n context: .\n dockerfile: docker/agents/Dockerfile\n pull_policy: build|}' "$compose_file"
|
sed -i '/^ image: ghcr\.io\/disinto\/agents:/{s|image: ghcr\.io/disinto/agents:.*|build:\n context: .\n dockerfile: docker/agents/Dockerfile\n pull_policy: build|}' "$compose_file"
|
||||||
sed -i '/^ image: ghcr\.io\/disinto\/edge:/{s|image: ghcr\.io/disinto/edge:.*|build: ./docker/edge\n pull_policy: build|}' "$compose_file"
|
sed -i '/^ image: ghcr\.io\/disinto\/edge:/{s|image: ghcr\.io/disinto/edge:.*|build: ./docker/edge\n pull_policy: build|}' "$compose_file"
|
||||||
fi
|
fi
|
||||||
|
|
|
||||||
|
|
@ -405,3 +405,36 @@ hvault_token_lookup() {
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# _hvault_seed_key — Seed a single KV key if it doesn't exist.
|
||||||
|
# Reads existing data and merges to preserve sibling keys (KV v2 replaces
|
||||||
|
# .data atomically). Returns 0=created, 1=unchanged, 2=API error.
|
||||||
|
# Args:
|
||||||
|
# path: KV v2 logical path (e.g. "disinto/shared/chat")
|
||||||
|
# key: key name within the path (e.g. "chat_oauth_client_id")
|
||||||
|
# generator: shell command that outputs a random value (default: openssl rand -hex 32)
|
||||||
|
# Usage:
|
||||||
|
# _hvault_seed_key "disinto/shared/chat" "chat_oauth_client_id"
|
||||||
|
# rc=$? # 0=created, 1=unchanged
|
||||||
|
_hvault_seed_key() {
|
||||||
|
local path="$1" key="$2" generator="${3:-openssl rand -hex 32}"
|
||||||
|
local existing
|
||||||
|
existing=$(hvault_kv_get "$path" "$key" 2>/dev/null) || true
|
||||||
|
if [ -n "$existing" ]; then
|
||||||
|
return 1 # unchanged
|
||||||
|
fi
|
||||||
|
|
||||||
|
local value
|
||||||
|
value=$(eval "$generator")
|
||||||
|
|
||||||
|
# Read existing data to preserve sibling keys (KV v2 replaces atomically)
|
||||||
|
local kv_api="${VAULT_KV_MOUNT}/data/${path}"
|
||||||
|
local raw existing_data payload
|
||||||
|
raw="$(hvault_get_or_empty "$kv_api")" || return 2
|
||||||
|
existing_data="{}"
|
||||||
|
[ -n "$raw" ] && existing_data="$(printf '%s' "$raw" | jq '.data.data // {}')"
|
||||||
|
payload="$(printf '%s' "$existing_data" \
|
||||||
|
| jq --arg k "$key" --arg v "$value" '{data: (. + {($k): $v})}')"
|
||||||
|
_hvault_request POST "$kv_api" "$payload" >/dev/null
|
||||||
|
return 0 # created
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,7 @@ HOST_VOLUME_DIRS=(
|
||||||
"/srv/disinto/agent-data"
|
"/srv/disinto/agent-data"
|
||||||
"/srv/disinto/project-repos"
|
"/srv/disinto/project-repos"
|
||||||
"/srv/disinto/caddy-data"
|
"/srv/disinto/caddy-data"
|
||||||
|
"/srv/disinto/docker"
|
||||||
"/srv/disinto/chat-history"
|
"/srv/disinto/chat-history"
|
||||||
"/srv/disinto/ops-repo"
|
"/srv/disinto/ops-repo"
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
<!-- last-reviewed: edf7a28bd3c85d4f72d28fd986fd2af3dcb885c1 -->
|
<!-- last-reviewed: b05a31197cc78aa28f3c3e6365e782032bfb25af -->
|
||||||
# nomad/ — Agent Instructions
|
# nomad/ — Agent Instructions
|
||||||
|
|
||||||
Nomad + Vault HCL for the factory's single-node cluster. These files are
|
Nomad + Vault HCL for the factory's single-node cluster. These files are
|
||||||
the source of truth that `lib/init/nomad/cluster-up.sh` copies onto a
|
the source of truth that `lib/init/nomad/cluster-up.sh` copies onto a
|
||||||
factory box under `/etc/nomad.d/` and `/etc/vault.d/` at init time.
|
factory box under `/etc/nomad.d/` and `/etc/vault.d/` at init time.
|
||||||
|
|
||||||
This directory covers the **Nomad+Vault migration (Steps 0–4)** —
|
This directory covers the **Nomad+Vault migration (Steps 0–5)** —
|
||||||
see issues #821–#962 for the step breakdown.
|
see issues #821–#992 for the step breakdown.
|
||||||
|
|
||||||
## What lives here
|
## What lives here
|
||||||
|
|
||||||
|
|
@ -17,8 +17,11 @@ see issues #821–#962 for the step breakdown.
|
||||||
| `vault.hcl` | `/etc/vault.d/vault.hcl` | Vault storage, listener, UI, `disable_mlock` (S0.3) |
|
| `vault.hcl` | `/etc/vault.d/vault.hcl` | Vault storage, listener, UI, `disable_mlock` (S0.3) |
|
||||||
| `jobs/forgejo.hcl` | submitted via `lib/init/nomad/deploy.sh` | Forgejo job; reads creds from Vault via consul-template stanza (S2.4) |
|
| `jobs/forgejo.hcl` | submitted via `lib/init/nomad/deploy.sh` | Forgejo job; reads creds from Vault via consul-template stanza (S2.4) |
|
||||||
| `jobs/woodpecker-server.hcl` | submitted via `lib/init/nomad/deploy.sh` | Woodpecker CI server; host networking, Vault KV for `WOODPECKER_AGENT_SECRET` + Forgejo OAuth creds (S3.1) |
|
| `jobs/woodpecker-server.hcl` | submitted via `lib/init/nomad/deploy.sh` | Woodpecker CI server; host networking, Vault KV for `WOODPECKER_AGENT_SECRET` + Forgejo OAuth creds (S3.1) |
|
||||||
| `jobs/woodpecker-agent.hcl` | submitted via `lib/init/nomad/deploy.sh` | Woodpecker CI agent; host networking, `docker.sock` mount, Vault KV for `WOODPECKER_AGENT_SECRET` (S3.2) |
|
| `jobs/woodpecker-agent.hcl` | submitted via `lib/init/nomad/deploy.sh` | Woodpecker CI agent; host networking, `docker.sock` mount, Vault KV for `WOODPECKER_AGENT_SECRET`; `WOODPECKER_SERVER` uses `${attr.unique.network.ip-address}:9000` (Nomad interpolation) — port binds to LXC alloc IP, not localhost (S3.2, S3-fix-6, #964) |
|
||||||
| `jobs/agents.hcl` | submitted via `lib/init/nomad/deploy.sh` | All 7 agent roles (dev, review, gardener, planner, predictor, supervisor, architect) + llama variant; Vault-templated bot tokens via `service-agents` policy (S4.1, #955) |
|
| `jobs/agents.hcl` | submitted via `lib/init/nomad/deploy.sh` | All 7 agent roles (dev, review, gardener, planner, predictor, supervisor, architect) + llama variant; Vault-templated bot tokens via `service-agents` policy; `force_pull = false` — image is built locally by `bin/disinto --with agents`, no registry (S4.1, S4-fix-2, S4-fix-5, #955, #972, #978) |
|
||||||
|
| `jobs/staging.hcl` | submitted via `lib/init/nomad/deploy.sh` | Caddy file-server mounting `docker/` as `/srv/site:ro`; no Vault integration; **dynamic host port** (no static 80 — edge owns 80/443, collision fixed in S5-fix-7 #1018); edge discovers via Nomad service registration (S5.2, #989) |
|
||||||
|
| `jobs/chat.hcl` | submitted via `lib/init/nomad/deploy.sh` | Claude chat UI; custom `disinto/chat:local` image; sandbox hardening (cap_drop ALL, **tmpfs via mount block** not `tmpfs=` arg — S5-fix-5 #1012, pids_limit 128); Vault-templated OAuth secrets via `service-chat` policy (S5.2, #989) |
|
||||||
|
| `jobs/edge.hcl` | submitted via `lib/init/nomad/deploy.sh` | Caddy reverse proxy + dispatcher sidecar; routes /forge, /woodpecker, /staging, /chat; uses `disinto/edge:local` image built by `bin/disinto --with edge`; Vault-templated ops-repo creds via `service-dispatcher` policy (S5.1, #988) |
|
||||||
|
|
||||||
Nomad auto-merges every `*.hcl` under `-config=/etc/nomad.d/`, so the
|
Nomad auto-merges every `*.hcl` under `-config=/etc/nomad.d/`, so the
|
||||||
split between `server.hcl` and `client.hcl` is for readability, not
|
split between `server.hcl` and `client.hcl` is for readability, not
|
||||||
|
|
@ -33,8 +36,6 @@ convention, KV path summary, and JWT-auth role bindings (S2.1/S2.3).
|
||||||
|
|
||||||
## Not yet implemented
|
## Not yet implemented
|
||||||
|
|
||||||
- **Additional jobspecs** (caddy) — Woodpecker (S3.1-S3.2) and agents (S4.1) are now deployed;
|
|
||||||
caddy lands in a later step.
|
|
||||||
- **TLS, ACLs, gossip encryption** — deliberately absent for now; land
|
- **TLS, ACLs, gossip encryption** — deliberately absent for now; land
|
||||||
alongside multi-node support.
|
alongside multi-node support.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,12 @@ client {
|
||||||
read_only = false
|
read_only = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# staging static content (docker/ directory with images, HTML, etc.)
|
||||||
|
host_volume "site-content" {
|
||||||
|
path = "/srv/disinto/docker"
|
||||||
|
read_only = true
|
||||||
|
}
|
||||||
|
|
||||||
# disinto chat transcripts + attachments.
|
# disinto chat transcripts + attachments.
|
||||||
host_volume "chat-history" {
|
host_volume "chat-history" {
|
||||||
path = "/srv/disinto/chat-history"
|
path = "/srv/disinto/chat-history"
|
||||||
|
|
|
||||||
|
|
@ -84,7 +84,8 @@ job "agents" {
|
||||||
driver = "docker"
|
driver = "docker"
|
||||||
|
|
||||||
config {
|
config {
|
||||||
image = "disinto/agents:latest"
|
image = "disinto/agents:local"
|
||||||
|
force_pull = false
|
||||||
|
|
||||||
# apparmor=unconfined matches docker-compose — Claude Code needs
|
# apparmor=unconfined matches docker-compose — Claude Code needs
|
||||||
# ptrace for node.js inspector and /proc access.
|
# ptrace for node.js inspector and /proc access.
|
||||||
|
|
@ -151,37 +152,44 @@ FORGE_PASS={{ .Data.data.pass }}
|
||||||
FORGE_TOKEN=seed-me
|
FORGE_TOKEN=seed-me
|
||||||
FORGE_PASS=seed-me
|
FORGE_PASS=seed-me
|
||||||
{{- end }}
|
{{- end }}
|
||||||
{{- with secret "kv/data/disinto/bots/review" -}}
|
|
||||||
|
{{ with secret "kv/data/disinto/bots/review" -}}
|
||||||
FORGE_REVIEW_TOKEN={{ .Data.data.token }}
|
FORGE_REVIEW_TOKEN={{ .Data.data.token }}
|
||||||
{{- else -}}
|
{{- else -}}
|
||||||
FORGE_REVIEW_TOKEN=seed-me
|
FORGE_REVIEW_TOKEN=seed-me
|
||||||
{{- end }}
|
{{- end }}
|
||||||
{{- with secret "kv/data/disinto/bots/gardener" -}}
|
|
||||||
|
{{ with secret "kv/data/disinto/bots/gardener" -}}
|
||||||
FORGE_GARDENER_TOKEN={{ .Data.data.token }}
|
FORGE_GARDENER_TOKEN={{ .Data.data.token }}
|
||||||
{{- else -}}
|
{{- else -}}
|
||||||
FORGE_GARDENER_TOKEN=seed-me
|
FORGE_GARDENER_TOKEN=seed-me
|
||||||
{{- end }}
|
{{- end }}
|
||||||
{{- with secret "kv/data/disinto/bots/architect" -}}
|
|
||||||
|
{{ with secret "kv/data/disinto/bots/architect" -}}
|
||||||
FORGE_ARCHITECT_TOKEN={{ .Data.data.token }}
|
FORGE_ARCHITECT_TOKEN={{ .Data.data.token }}
|
||||||
{{- else -}}
|
{{- else -}}
|
||||||
FORGE_ARCHITECT_TOKEN=seed-me
|
FORGE_ARCHITECT_TOKEN=seed-me
|
||||||
{{- end }}
|
{{- end }}
|
||||||
{{- with secret "kv/data/disinto/bots/planner" -}}
|
|
||||||
|
{{ with secret "kv/data/disinto/bots/planner" -}}
|
||||||
FORGE_PLANNER_TOKEN={{ .Data.data.token }}
|
FORGE_PLANNER_TOKEN={{ .Data.data.token }}
|
||||||
{{- else -}}
|
{{- else -}}
|
||||||
FORGE_PLANNER_TOKEN=seed-me
|
FORGE_PLANNER_TOKEN=seed-me
|
||||||
{{- end }}
|
{{- end }}
|
||||||
{{- with secret "kv/data/disinto/bots/predictor" -}}
|
|
||||||
|
{{ with secret "kv/data/disinto/bots/predictor" -}}
|
||||||
FORGE_PREDICTOR_TOKEN={{ .Data.data.token }}
|
FORGE_PREDICTOR_TOKEN={{ .Data.data.token }}
|
||||||
{{- else -}}
|
{{- else -}}
|
||||||
FORGE_PREDICTOR_TOKEN=seed-me
|
FORGE_PREDICTOR_TOKEN=seed-me
|
||||||
{{- end }}
|
{{- end }}
|
||||||
{{- with secret "kv/data/disinto/bots/supervisor" -}}
|
|
||||||
|
{{ with secret "kv/data/disinto/bots/supervisor" -}}
|
||||||
FORGE_SUPERVISOR_TOKEN={{ .Data.data.token }}
|
FORGE_SUPERVISOR_TOKEN={{ .Data.data.token }}
|
||||||
{{- else -}}
|
{{- else -}}
|
||||||
FORGE_SUPERVISOR_TOKEN=seed-me
|
FORGE_SUPERVISOR_TOKEN=seed-me
|
||||||
{{- end }}
|
{{- end }}
|
||||||
{{- with secret "kv/data/disinto/bots/vault" -}}
|
|
||||||
|
{{ with secret "kv/data/disinto/bots/vault" -}}
|
||||||
FORGE_VAULT_TOKEN={{ .Data.data.token }}
|
FORGE_VAULT_TOKEN={{ .Data.data.token }}
|
||||||
{{- else -}}
|
{{- else -}}
|
||||||
FORGE_VAULT_TOKEN=seed-me
|
FORGE_VAULT_TOKEN=seed-me
|
||||||
|
|
|
||||||
157
nomad/jobs/chat.hcl
Normal file
157
nomad/jobs/chat.hcl
Normal file
|
|
@ -0,0 +1,157 @@
|
||||||
|
# =============================================================================
|
||||||
|
# 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 volume:
|
||||||
|
# - chat-history → /var/lib/chat/history (persists conversation history)
|
||||||
|
#
|
||||||
|
# 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
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── 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
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── 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_MAX_REQUESTS_PER_HOUR = "60"
|
||||||
|
CHAT_MAX_REQUESTS_PER_DAY = "1000"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
245
nomad/jobs/edge.hcl
Normal file
245
nomad/jobs/edge.hcl
Normal file
|
|
@ -0,0 +1,245 @@
|
||||||
|
# =============================================================================
|
||||||
|
# nomad/jobs/edge.hcl — Edge proxy (Caddy + dispatcher sidecar) (Nomad service job)
|
||||||
|
#
|
||||||
|
# Part of the Nomad+Vault migration (S5.1, issue #988). Caddy reverse proxy
|
||||||
|
# routes traffic to Forgejo, Woodpecker, staging, and chat services. The
|
||||||
|
# dispatcher sidecar polls disinto-ops for vault actions and dispatches them
|
||||||
|
# via Nomad batch jobs.
|
||||||
|
#
|
||||||
|
# Host_volume contract:
|
||||||
|
# This job mounts caddy-data from nomad/client.hcl. Path
|
||||||
|
# /srv/disinto/caddy-data is created by lib/init/nomad/cluster-up.sh before
|
||||||
|
# any job references it. Keep the `source = "caddy-data"` below in sync
|
||||||
|
# with the host_volume stanza in client.hcl.
|
||||||
|
#
|
||||||
|
# Build step (S5.1):
|
||||||
|
# docker/edge/Dockerfile is custom (adds bash, jq, curl, git, docker-cli,
|
||||||
|
# python3, openssh-client, autossh to caddy:latest). Build as
|
||||||
|
# disinto/edge:local using the same pattern as disinto/agents:local.
|
||||||
|
# Command: docker build -t disinto/edge:local -f docker/edge/Dockerfile docker/edge
|
||||||
|
#
|
||||||
|
# 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 edge` to `nomad job run` it.
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
job "edge" {
|
||||||
|
type = "service"
|
||||||
|
datacenters = ["dc1"]
|
||||||
|
|
||||||
|
group "edge" {
|
||||||
|
count = 1
|
||||||
|
|
||||||
|
# ── Vault workload identity for dispatcher (S5.1, issue #988) ──────────
|
||||||
|
# Service role for dispatcher task to fetch vault actions from KV v2.
|
||||||
|
# Role defined in vault/roles.yaml, policy in vault/policies/dispatcher.hcl.
|
||||||
|
vault {
|
||||||
|
role = "service-dispatcher"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Network ports (S5.1, issue #988) ──────────────────────────────────
|
||||||
|
# Caddy listens on :80 and :443. Expose both on the host.
|
||||||
|
network {
|
||||||
|
port "http" {
|
||||||
|
static = 80
|
||||||
|
to = 80
|
||||||
|
}
|
||||||
|
|
||||||
|
port "https" {
|
||||||
|
static = 443
|
||||||
|
to = 443
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Host-volume mounts (S5.1, issue #988) ─────────────────────────────
|
||||||
|
# caddy-data: ACME certificates, Caddy config state.
|
||||||
|
volume "caddy-data" {
|
||||||
|
type = "host"
|
||||||
|
source = "caddy-data"
|
||||||
|
read_only = false
|
||||||
|
}
|
||||||
|
|
||||||
|
# ops-repo: disinto-ops clone for vault actions polling.
|
||||||
|
volume "ops-repo" {
|
||||||
|
type = "host"
|
||||||
|
source = "ops-repo"
|
||||||
|
read_only = false
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Conservative restart policy ───────────────────────────────────────
|
||||||
|
# Caddy should be stable; dispatcher may restart on errors.
|
||||||
|
restart {
|
||||||
|
attempts = 3
|
||||||
|
interval = "5m"
|
||||||
|
delay = "15s"
|
||||||
|
mode = "delay"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Service registration ───────────────────────────────────────────────
|
||||||
|
# Caddy is an HTTP reverse proxy — health check on port 80.
|
||||||
|
service {
|
||||||
|
name = "edge"
|
||||||
|
port = "http"
|
||||||
|
provider = "nomad"
|
||||||
|
|
||||||
|
check {
|
||||||
|
type = "http"
|
||||||
|
path = "/"
|
||||||
|
interval = "10s"
|
||||||
|
timeout = "3s"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Caddy task (S5.1, issue #988) ─────────────────────────────────────
|
||||||
|
task "caddy" {
|
||||||
|
driver = "docker"
|
||||||
|
|
||||||
|
config {
|
||||||
|
# Use pre-built disinto/edge:local image (custom Dockerfile adds
|
||||||
|
# bash, jq, curl, git, docker-cli, python3, openssh-client, autossh).
|
||||||
|
image = "disinto/edge:local"
|
||||||
|
force_pull = false
|
||||||
|
ports = ["http", "https"]
|
||||||
|
|
||||||
|
# apparmor=unconfined matches docker-compose — needed for autossh
|
||||||
|
# in the entrypoint script.
|
||||||
|
security_opt = ["apparmor=unconfined"]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Mount caddy-data volume for ACME state and config directory.
|
||||||
|
# Caddyfile is mounted at /etc/caddy/Caddyfile by entrypoint-edge.sh.
|
||||||
|
volume_mount {
|
||||||
|
volume = "caddy-data"
|
||||||
|
destination = "/data"
|
||||||
|
read_only = false
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Caddyfile via Nomad service discovery (S5-fix-7, issue #1018) ────
|
||||||
|
# Renders staging upstream from Nomad service registration instead of
|
||||||
|
# hardcoded staging:80. Caddy picks up /local/Caddyfile via entrypoint.
|
||||||
|
template {
|
||||||
|
destination = "local/Caddyfile"
|
||||||
|
change_mode = "restart"
|
||||||
|
data = <<EOT
|
||||||
|
# Caddyfile — edge proxy configuration (Nomad-rendered)
|
||||||
|
# Staging upstream discovered via Nomad service registration.
|
||||||
|
|
||||||
|
:80 {
|
||||||
|
# Redirect root to Forgejo
|
||||||
|
handle / {
|
||||||
|
redir /forge/ 302
|
||||||
|
}
|
||||||
|
|
||||||
|
# Reverse proxy to Forgejo
|
||||||
|
handle /forge/* {
|
||||||
|
reverse_proxy forgejo:3000
|
||||||
|
}
|
||||||
|
|
||||||
|
# Reverse proxy to Woodpecker CI
|
||||||
|
handle /ci/* {
|
||||||
|
reverse_proxy woodpecker:8000
|
||||||
|
}
|
||||||
|
|
||||||
|
# Reverse proxy to staging — dynamic port via Nomad service discovery
|
||||||
|
handle /staging/* {
|
||||||
|
{{ range nomadService "staging" }} reverse_proxy {{ .Address }}:{{ .Port }}
|
||||||
|
{{ end }} }
|
||||||
|
|
||||||
|
# Chat service — reverse proxy to disinto-chat backend (#705)
|
||||||
|
# OAuth routes bypass forward_auth — unauthenticated users need these (#709)
|
||||||
|
handle /chat/login {
|
||||||
|
reverse_proxy chat:8080
|
||||||
|
}
|
||||||
|
handle /chat/oauth/callback {
|
||||||
|
reverse_proxy chat:8080
|
||||||
|
}
|
||||||
|
# Defense-in-depth: forward_auth stamps X-Forwarded-User from session (#709)
|
||||||
|
handle /chat/* {
|
||||||
|
forward_auth chat:8080 {
|
||||||
|
uri /chat/auth/verify
|
||||||
|
copy_headers X-Forwarded-User
|
||||||
|
header_up X-Forward-Auth-Secret {$FORWARD_AUTH_SECRET}
|
||||||
|
}
|
||||||
|
reverse_proxy chat:8080
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOT
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Non-secret env ───────────────────────────────────────────────────
|
||||||
|
env {
|
||||||
|
FORGE_URL = "http://forgejo:3000"
|
||||||
|
FORGE_REPO = "disinto-admin/disinto"
|
||||||
|
DISINTO_CONTAINER = "1"
|
||||||
|
PROJECT_NAME = "disinto"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Caddy needs CPU + memory headroom for reverse proxy work.
|
||||||
|
resources {
|
||||||
|
cpu = 200
|
||||||
|
memory = 256
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Dispatcher task (S5.1, issue #988) ────────────────────────────────
|
||||||
|
task "dispatcher" {
|
||||||
|
driver = "docker"
|
||||||
|
|
||||||
|
config {
|
||||||
|
# Use same disinto/agents:local image as other agents.
|
||||||
|
image = "disinto/agents:local"
|
||||||
|
force_pull = false
|
||||||
|
|
||||||
|
# apparmor=unconfined matches docker-compose.
|
||||||
|
security_opt = ["apparmor=unconfined"]
|
||||||
|
|
||||||
|
# Mount docker.sock via bind-volume (not host volume) for legacy
|
||||||
|
# docker backend compat. Nomad host volumes require named volumes
|
||||||
|
# from client.hcl; socket files cannot be host volumes.
|
||||||
|
volumes = ["/var/run/docker.sock:/var/run/docker.sock:ro"]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Mount ops-repo for vault actions polling.
|
||||||
|
volume_mount {
|
||||||
|
volume = "ops-repo"
|
||||||
|
destination = "/home/agent/repos/disinto-ops"
|
||||||
|
read_only = false
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Vault-templated secrets (S5.1, issue #988) ──────────────────────
|
||||||
|
# Renders FORGE_TOKEN from Vault KV v2 for ops repo access.
|
||||||
|
template {
|
||||||
|
destination = "secrets/dispatcher.env"
|
||||||
|
env = true
|
||||||
|
change_mode = "restart"
|
||||||
|
error_on_missing_key = false
|
||||||
|
data = <<EOT
|
||||||
|
{{- with secret "kv/data/disinto/bots/vault" -}}
|
||||||
|
FORGE_TOKEN={{ .Data.data.token }}
|
||||||
|
{{- else -}}
|
||||||
|
# WARNING: kv/disinto/bots/vault is empty — run tools/vault-seed-agents.sh
|
||||||
|
FORGE_TOKEN=seed-me
|
||||||
|
{{- end }}
|
||||||
|
EOT
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Non-secret env ───────────────────────────────────────────────────
|
||||||
|
env {
|
||||||
|
DISPATCHER_BACKEND = "nomad"
|
||||||
|
FORGE_URL = "http://forgejo:3000"
|
||||||
|
FORGE_REPO = "disinto-admin/disinto"
|
||||||
|
FORGE_OPS_REPO = "disinto-admin/disinto-ops"
|
||||||
|
PRIMARY_BRANCH = "main"
|
||||||
|
DISINTO_CONTAINER = "1"
|
||||||
|
OPS_REPO_ROOT = "/home/agent/repos/disinto-ops"
|
||||||
|
FORGE_ADMIN_USERS = "vault-bot,admin"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Dispatcher is lightweight — minimal CPU + memory.
|
||||||
|
resources {
|
||||||
|
cpu = 100
|
||||||
|
memory = 256
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
86
nomad/jobs/staging.hcl
Normal file
86
nomad/jobs/staging.hcl
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
# =============================================================================
|
||||||
|
# nomad/jobs/staging.hcl — Staging file server (Nomad service job)
|
||||||
|
#
|
||||||
|
# Part of the Nomad+Vault migration (S5.2, issue #989). Lightweight service job
|
||||||
|
# for the staging file server using Caddy as a static file server.
|
||||||
|
#
|
||||||
|
# Mount contract:
|
||||||
|
# This job mounts the `docker/` directory as `/srv/site` (read-only).
|
||||||
|
# The docker/ directory contains static content (images, HTML, etc.)
|
||||||
|
# served to staging environment users.
|
||||||
|
#
|
||||||
|
# Network:
|
||||||
|
# Dynamic host port — edge discovers via Nomad service registration.
|
||||||
|
# No static port to avoid collisions with edge (which owns 80/443).
|
||||||
|
#
|
||||||
|
# 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 staging` to `nomad job run` it.
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
job "staging" {
|
||||||
|
type = "service"
|
||||||
|
datacenters = ["dc1"]
|
||||||
|
|
||||||
|
group "staging" {
|
||||||
|
count = 1
|
||||||
|
|
||||||
|
# No Vault integration needed — no secrets required (static file server)
|
||||||
|
|
||||||
|
# Internal service — dynamic host port. Edge discovers via Nomad service.
|
||||||
|
network {
|
||||||
|
port "http" {
|
||||||
|
to = 80
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
volume "site-content" {
|
||||||
|
type = "host"
|
||||||
|
source = "site-content"
|
||||||
|
read_only = true
|
||||||
|
}
|
||||||
|
|
||||||
|
restart {
|
||||||
|
attempts = 3
|
||||||
|
interval = "5m"
|
||||||
|
delay = "15s"
|
||||||
|
mode = "delay"
|
||||||
|
}
|
||||||
|
|
||||||
|
service {
|
||||||
|
name = "staging"
|
||||||
|
port = "http"
|
||||||
|
provider = "nomad"
|
||||||
|
|
||||||
|
check {
|
||||||
|
type = "http"
|
||||||
|
path = "/"
|
||||||
|
interval = "10s"
|
||||||
|
timeout = "3s"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
task "staging" {
|
||||||
|
driver = "docker"
|
||||||
|
|
||||||
|
config {
|
||||||
|
image = "caddy:alpine"
|
||||||
|
ports = ["http"]
|
||||||
|
command = "caddy"
|
||||||
|
args = ["file-server", "--root", "/srv/site"]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Mount docker/ directory as /srv/site:ro (static content)
|
||||||
|
volume_mount {
|
||||||
|
volume = "site-content"
|
||||||
|
destination = "/srv/site"
|
||||||
|
read_only = true
|
||||||
|
}
|
||||||
|
|
||||||
|
resources {
|
||||||
|
cpu = 100
|
||||||
|
memory = 256
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
137
nomad/jobs/vault-runner.hcl
Normal file
137
nomad/jobs/vault-runner.hcl
Normal file
|
|
@ -0,0 +1,137 @@
|
||||||
|
# =============================================================================
|
||||||
|
# nomad/jobs/vault-runner.hcl — Parameterized batch job for vault action dispatch
|
||||||
|
#
|
||||||
|
# Part of the Nomad+Vault migration (S5.3, issue #990). Replaces the
|
||||||
|
# `docker run --rm vault-runner-${action_id}` pattern in dispatcher.sh with
|
||||||
|
# a Nomad-native parameterized batch job. Dispatched by the edge dispatcher
|
||||||
|
# (S5.4) via `nomad job dispatch`.
|
||||||
|
#
|
||||||
|
# Parameterized meta:
|
||||||
|
# action_id — vault action identifier (used by entrypoint-runner.sh)
|
||||||
|
# secrets_csv — comma-separated secret names (e.g. "GITHUB_TOKEN,DEPLOY_KEY")
|
||||||
|
#
|
||||||
|
# Vault integration (approach A — pre-defined templates):
|
||||||
|
# All 6 known runner secrets are rendered via template stanzas with
|
||||||
|
# error_on_missing_key = false. Secrets not granted by the dispatch's
|
||||||
|
# Vault policies render as empty strings. The dispatcher (S5.4) sets
|
||||||
|
# vault { policies = [...] } per-dispatch based on the action TOML's
|
||||||
|
# secrets=[...] list, scoping access to only the declared secrets.
|
||||||
|
#
|
||||||
|
# Cleanup: Nomad garbage-collects completed batch dispatches automatically.
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
job "vault-runner" {
|
||||||
|
type = "batch"
|
||||||
|
datacenters = ["dc1"]
|
||||||
|
|
||||||
|
parameterized {
|
||||||
|
meta_required = ["action_id", "secrets_csv"]
|
||||||
|
}
|
||||||
|
|
||||||
|
group "runner" {
|
||||||
|
count = 1
|
||||||
|
|
||||||
|
# ── Vault workload identity ──────────────────────────────────────────────
|
||||||
|
# Per-dispatch policies are composed by the dispatcher (S5.4) based on the
|
||||||
|
# action TOML's secrets=[...] list. Each policy grants read access to
|
||||||
|
# exactly one kv/data/disinto/runner/<NAME> path. Roles defined in
|
||||||
|
# vault/roles.yaml (runner-<NAME>), policies in vault/policies/.
|
||||||
|
vault {}
|
||||||
|
|
||||||
|
volume "ops-repo" {
|
||||||
|
type = "host"
|
||||||
|
source = "ops-repo"
|
||||||
|
read_only = true
|
||||||
|
}
|
||||||
|
|
||||||
|
# No restart for batch — fail fast, let the dispatcher handle retries.
|
||||||
|
restart {
|
||||||
|
attempts = 0
|
||||||
|
mode = "fail"
|
||||||
|
}
|
||||||
|
|
||||||
|
task "runner" {
|
||||||
|
driver = "docker"
|
||||||
|
|
||||||
|
config {
|
||||||
|
image = "disinto/agents:local"
|
||||||
|
force_pull = false
|
||||||
|
entrypoint = ["bash"]
|
||||||
|
args = [
|
||||||
|
"/home/agent/disinto/docker/runner/entrypoint-runner.sh",
|
||||||
|
"${NOMAD_META_action_id}",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
volume_mount {
|
||||||
|
volume = "ops-repo"
|
||||||
|
destination = "/home/agent/ops"
|
||||||
|
read_only = true
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Non-secret env ───────────────────────────────────────────────────────
|
||||||
|
env {
|
||||||
|
DISINTO_CONTAINER = "1"
|
||||||
|
FACTORY_ROOT = "/home/agent/disinto"
|
||||||
|
OPS_REPO_ROOT = "/home/agent/ops"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Vault-templated runner secrets (approach A) ────────────────────────
|
||||||
|
# Pre-defined templates for all 6 known runner secrets. Each renders
|
||||||
|
# from kv/data/disinto/runner/<NAME>. Secrets not granted by the
|
||||||
|
# dispatch's Vault policies produce empty env vars (harmless).
|
||||||
|
# error_on_missing_key = false prevents template-pending hangs when
|
||||||
|
# a secret path is absent or the policy doesn't grant access.
|
||||||
|
#
|
||||||
|
# Placeholder values kept < 16 chars to avoid secret-scan CI failures.
|
||||||
|
template {
|
||||||
|
destination = "secrets/runner.env"
|
||||||
|
env = true
|
||||||
|
error_on_missing_key = false
|
||||||
|
data = <<EOT
|
||||||
|
{{- with secret "kv/data/disinto/runner/GITHUB_TOKEN" -}}
|
||||||
|
GITHUB_TOKEN={{ .Data.data.value }}
|
||||||
|
{{- else -}}
|
||||||
|
GITHUB_TOKEN=
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{ with secret "kv/data/disinto/runner/CODEBERG_TOKEN" -}}
|
||||||
|
CODEBERG_TOKEN={{ .Data.data.value }}
|
||||||
|
{{- else -}}
|
||||||
|
CODEBERG_TOKEN=
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{ with secret "kv/data/disinto/runner/CLAWHUB_TOKEN" -}}
|
||||||
|
CLAWHUB_TOKEN={{ .Data.data.value }}
|
||||||
|
{{- else -}}
|
||||||
|
CLAWHUB_TOKEN=
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{ with secret "kv/data/disinto/runner/DEPLOY_KEY" -}}
|
||||||
|
DEPLOY_KEY={{ .Data.data.value }}
|
||||||
|
{{- else -}}
|
||||||
|
DEPLOY_KEY=
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{ with secret "kv/data/disinto/runner/NPM_TOKEN" -}}
|
||||||
|
NPM_TOKEN={{ .Data.data.value }}
|
||||||
|
{{- else -}}
|
||||||
|
NPM_TOKEN=
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{ with secret "kv/data/disinto/runner/DOCKER_HUB_TOKEN" -}}
|
||||||
|
DOCKER_HUB_TOKEN={{ .Data.data.value }}
|
||||||
|
{{- else -}}
|
||||||
|
DOCKER_HUB_TOKEN=
|
||||||
|
{{- end }}
|
||||||
|
EOT
|
||||||
|
}
|
||||||
|
|
||||||
|
# Formula execution headroom — matches agents.hcl baseline.
|
||||||
|
resources {
|
||||||
|
cpu = 500
|
||||||
|
memory = 1024
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
<!-- last-reviewed: edf7a28bd3c85d4f72d28fd986fd2af3dcb885c1 -->
|
<!-- last-reviewed: b05a31197cc78aa28f3c3e6365e782032bfb25af -->
|
||||||
# Planner Agent
|
# Planner Agent
|
||||||
|
|
||||||
**Role**: Strategic planning using a Prerequisite Tree (Theory of Constraints),
|
**Role**: Strategic planning using a Prerequisite Tree (Theory of Constraints),
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
<!-- last-reviewed: edf7a28bd3c85d4f72d28fd986fd2af3dcb885c1 -->
|
<!-- last-reviewed: b05a31197cc78aa28f3c3e6365e782032bfb25af -->
|
||||||
# Predictor Agent
|
# Predictor Agent
|
||||||
|
|
||||||
**Role**: Abstract adversary (the "goblin"). Runs a 2-step formula
|
**Role**: Abstract adversary (the "goblin"). Runs a 2-step formula
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
<!-- last-reviewed: edf7a28bd3c85d4f72d28fd986fd2af3dcb885c1 -->
|
<!-- last-reviewed: b05a31197cc78aa28f3c3e6365e782032bfb25af -->
|
||||||
# Review Agent
|
# Review Agent
|
||||||
|
|
||||||
**Role**: AI-powered PR review — post structured findings and formal
|
**Role**: AI-powered PR review — post structured findings and formal
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
<!-- last-reviewed: edf7a28bd3c85d4f72d28fd986fd2af3dcb885c1 -->
|
<!-- last-reviewed: b05a31197cc78aa28f3c3e6365e782032bfb25af -->
|
||||||
# Supervisor Agent
|
# Supervisor Agent
|
||||||
|
|
||||||
**Role**: Health monitoring and auto-remediation, executed as a formula-driven
|
**Role**: Health monitoring and auto-remediation, executed as a formula-driven
|
||||||
|
|
|
||||||
|
|
@ -215,7 +215,7 @@ setup_file() {
|
||||||
run "$DISINTO_BIN" init placeholder/repo --backend=nomad --with unknown-service --dry-run
|
run "$DISINTO_BIN" init placeholder/repo --backend=nomad --with unknown-service --dry-run
|
||||||
[ "$status" -ne 0 ]
|
[ "$status" -ne 0 ]
|
||||||
[[ "$output" == *"unknown service"* ]]
|
[[ "$output" == *"unknown service"* ]]
|
||||||
[[ "$output" == *"known: forgejo, woodpecker-server, woodpecker-agent, agents"* ]]
|
[[ "$output" == *"known: forgejo, woodpecker-server, woodpecker-agent, agents, staging, chat, edge"* ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
# S3.4: woodpecker auto-expansion and forgejo auto-inclusion
|
# S3.4: woodpecker auto-expansion and forgejo auto-inclusion
|
||||||
|
|
|
||||||
56
tests/smoke-init.sh
Executable file → Normal file
56
tests/smoke-init.sh
Executable file → Normal file
|
|
@ -70,10 +70,6 @@ pass "Mock Forgejo API v${api_version} (${retries}s)"
|
||||||
echo "=== 2/6 Setting up mock binaries ==="
|
echo "=== 2/6 Setting up mock binaries ==="
|
||||||
mkdir -p "$MOCK_BIN"
|
mkdir -p "$MOCK_BIN"
|
||||||
|
|
||||||
# ── 3-7. Main smoke tests ────────────────────────────────────────────────────
|
|
||||||
# Wrap sections 3-7 in a block so they can fail without preventing section 8
|
|
||||||
run_main_tests() {
|
|
||||||
|
|
||||||
# ── Mock: docker ──
|
# ── Mock: docker ──
|
||||||
# Intercepts docker exec calls that disinto init --bare makes to Forgejo CLI
|
# Intercepts docker exec calls that disinto init --bare makes to Forgejo CLI
|
||||||
cat > "$MOCK_BIN/docker" << 'DOCKERMOCK'
|
cat > "$MOCK_BIN/docker" << 'DOCKERMOCK'
|
||||||
|
|
@ -427,58 +423,6 @@ export CLAUDE_SHARED_DIR="$ORIG_CLAUDE_SHARED_DIR"
|
||||||
export CLAUDE_CONFIG_DIR="$ORIG_CLAUDE_CONFIG_DIR"
|
export CLAUDE_CONFIG_DIR="$ORIG_CLAUDE_CONFIG_DIR"
|
||||||
rm -rf /tmp/smoke-claude-shared /tmp/smoke-home-claude
|
rm -rf /tmp/smoke-claude-shared /tmp/smoke-home-claude
|
||||||
|
|
||||||
# ── End of sections 3-7 ─────────────────────────────────────────────────────
|
|
||||||
}
|
|
||||||
|
|
||||||
# Run main tests (sections 3-7) if mock forgejo is available
|
|
||||||
run_main_tests || true
|
|
||||||
|
|
||||||
# ── 8. Test duplicate service name detection ──────────────────────────────
|
|
||||||
# This test runs independently of sections 1-7 to ensure duplicate detection
|
|
||||||
# is tested even if earlier sections fail
|
|
||||||
echo "=== 8/8 Testing duplicate service detection ==="
|
|
||||||
|
|
||||||
# Clean up for duplicate test
|
|
||||||
rm -f "${FACTORY_ROOT}/docker-compose.yml"
|
|
||||||
rm -f "${FACTORY_ROOT}/projects/duplicate-test.toml"
|
|
||||||
|
|
||||||
# Create a TOML that would conflict with ENABLE_LLAMA_AGENT
|
|
||||||
cat > "${FACTORY_ROOT}/projects/duplicate-test.toml" <<'TOMLEOF'
|
|
||||||
name = "duplicate-test"
|
|
||||||
description = "Test project for duplicate service detection"
|
|
||||||
|
|
||||||
[ci]
|
|
||||||
woodpecker_repo_id = "999"
|
|
||||||
|
|
||||||
[agents.llama]
|
|
||||||
base_url = "http://localhost:8080"
|
|
||||||
model = "qwen:latest"
|
|
||||||
roles = ["dev"]
|
|
||||||
forge_user = "llama-bot"
|
|
||||||
TOMLEOF
|
|
||||||
|
|
||||||
# Run disinto init with ENABLE_LLAMA_AGENT=1
|
|
||||||
# This should fail because [agents.llama] conflicts with ENABLE_LLAMA_AGENT
|
|
||||||
export ENABLE_LLAMA_AGENT="1"
|
|
||||||
export FORGE_URL="http://localhost:3000"
|
|
||||||
export SMOKE_FORGE_URL="$FORGE_URL"
|
|
||||||
export FORGE_ADMIN_PASS="smoke-test-password-123"
|
|
||||||
export SKIP_PUSH=true
|
|
||||||
|
|
||||||
if bash "${FACTORY_ROOT}/bin/disinto" init \
|
|
||||||
"duplicate-test" \
|
|
||||||
--bare --yes \
|
|
||||||
--forge-url "$FORGE_URL" \
|
|
||||||
--repo-root "/tmp/smoke-test-repo" 2>&1 | grep -q "Duplicate service name 'agents-llama'"; then
|
|
||||||
pass "Duplicate service detection: correctly detected conflict between ENABLE_LLAMA_AGENT and [agents.llama]"
|
|
||||||
else
|
|
||||||
fail "Duplicate service detection: should have detected conflict between ENABLE_LLAMA_AGENT and [agents.llama]"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Clean up
|
|
||||||
rm -f "${FACTORY_ROOT}/projects/duplicate-test.toml"
|
|
||||||
unset ENABLE_LLAMA_AGENT
|
|
||||||
|
|
||||||
# ── Summary ──────────────────────────────────────────────────────────────────
|
# ── Summary ──────────────────────────────────────────────────────────────────
|
||||||
echo ""
|
echo ""
|
||||||
if [ "$FAILED" -ne 0 ]; then
|
if [ "$FAILED" -ne 0 ]; then
|
||||||
|
|
|
||||||
|
|
@ -1,265 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
# tests/test-duplicate-service-detection.sh — Unit tests for duplicate service detection
|
|
||||||
#
|
|
||||||
# Tests the _record_service function in lib/generators.sh to ensure:
|
|
||||||
# 1. Duplicate detection between ENABLE_LLAMA_AGENT and [agents.llama] TOML
|
|
||||||
# 2. No false positive when only ENABLE_LLAMA_AGENT is set
|
|
||||||
# 3. Duplicate detection between two TOML agents with same name
|
|
||||||
# 4. No false positive when agent names are different
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
FACTORY_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
||||||
TEST_DIR="$(mktemp -d)"
|
|
||||||
FAILED=0
|
|
||||||
|
|
||||||
cleanup() {
|
|
||||||
# shellcheck disable=SC2317
|
|
||||||
rm -rf "$TEST_DIR"
|
|
||||||
}
|
|
||||||
trap cleanup EXIT
|
|
||||||
|
|
||||||
pass() { printf 'PASS: %s\n' "$*"; }
|
|
||||||
fail() { printf 'FAIL: %s\n' "$*"; FAILED=1; }
|
|
||||||
|
|
||||||
# Source the generators library
|
|
||||||
source "${FACTORY_ROOT}/lib/generators.sh"
|
|
||||||
|
|
||||||
# Test 1: Duplicate between ENABLE_LLAMA_AGENT and [agents.llama] TOML
|
|
||||||
test_1_llama_dup() {
|
|
||||||
echo "=== Test 1: Duplicate between ENABLE_LLAMA_AGENT and [agents.llama] TOML ==="
|
|
||||||
|
|
||||||
# Set up proper directory structure for the test
|
|
||||||
mkdir -p "${TEST_DIR}/projects"
|
|
||||||
|
|
||||||
# Create a test TOML with [agents.llama] in projects directory
|
|
||||||
cat > "${TEST_DIR}/projects/test.toml" <<'TOMLEOF'
|
|
||||||
name = "test"
|
|
||||||
repo = "test/test"
|
|
||||||
|
|
||||||
[agents.llama]
|
|
||||||
base_url = "http://10.10.10.1:8081"
|
|
||||||
model = "unsloth/Qwen3.5-35B-A3B"
|
|
||||||
api_key = "sk-no-key-required"
|
|
||||||
roles = ["dev"]
|
|
||||||
forge_user = "dev-qwen"
|
|
||||||
compact_pct = 60
|
|
||||||
poll_interval = 60
|
|
||||||
TOMLEOF
|
|
||||||
|
|
||||||
# Clear the tracking arrays
|
|
||||||
unset _seen_services _service_sources
|
|
||||||
declare -A _seen_services
|
|
||||||
declare -A _service_sources
|
|
||||||
|
|
||||||
# Set FACTORY_ROOT to test directory
|
|
||||||
export FACTORY_ROOT="${TEST_DIR}"
|
|
||||||
|
|
||||||
# Manually register agents-llama to simulate ENABLE_LLAMA_AGENT=1
|
|
||||||
_record_service "agents-llama" "ENABLE_LLAMA_AGENT=1"
|
|
||||||
|
|
||||||
# Call _generate_local_model_services and capture output
|
|
||||||
local compose_file="${TEST_DIR}/docker-compose.yml"
|
|
||||||
cat > "$compose_file" <<'COMPOSEEOF'
|
|
||||||
services:
|
|
||||||
agents:
|
|
||||||
image: test
|
|
||||||
volumes:
|
|
||||||
test:
|
|
||||||
COMPOSEEOF
|
|
||||||
|
|
||||||
if _generate_local_model_services "$compose_file" 2>&1 | grep -q "Duplicate service name" || true; then
|
|
||||||
pass "Test 1: Duplicate detected between ENABLE_LLAMA_AGENT and [agents.llama]"
|
|
||||||
else
|
|
||||||
fail "Test 1: Expected duplicate detection for agents-llama"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Test 2: No duplicate when only ENABLE_LLAMA_AGENT is set
|
|
||||||
test_2_only_env_flag() {
|
|
||||||
echo "=== Test 2: No duplicate when only ENABLE_LLAMA_AGENT is set ==="
|
|
||||||
|
|
||||||
# Set up proper directory structure for the test
|
|
||||||
mkdir -p "${TEST_DIR}/projects"
|
|
||||||
|
|
||||||
# Create a TOML without [agents.llama]
|
|
||||||
cat > "${TEST_DIR}/projects/test2.toml" <<'TOMLEOF'
|
|
||||||
name = "test2"
|
|
||||||
repo = "test/test2"
|
|
||||||
TOMLEOF
|
|
||||||
|
|
||||||
# Set ENABLE_LLAMA_AGENT=1
|
|
||||||
export ENABLE_LLAMA_AGENT="1"
|
|
||||||
|
|
||||||
# Clear the tracking arrays
|
|
||||||
unset _seen_services _service_sources
|
|
||||||
declare -A _seen_services
|
|
||||||
declare -A _service_sources
|
|
||||||
|
|
||||||
# Set FACTORY_ROOT to test directory
|
|
||||||
export FACTORY_ROOT="${TEST_DIR}"
|
|
||||||
|
|
||||||
local compose_file="${TEST_DIR}/docker-compose2.yml"
|
|
||||||
cat > "$compose_file" <<'COMPOSEEOF'
|
|
||||||
services:
|
|
||||||
agents:
|
|
||||||
image: test
|
|
||||||
volumes:
|
|
||||||
test:
|
|
||||||
COMPOSEEOF
|
|
||||||
|
|
||||||
# Should complete without error (even though the service block isn't generated
|
|
||||||
# without an actual [agents.*] section, the important thing is no duplicate error)
|
|
||||||
if _generate_local_model_services "$compose_file" 2>&1 | grep -q "Duplicate service name"; then
|
|
||||||
fail "Test 2: False positive duplicate detection"
|
|
||||||
else
|
|
||||||
pass "Test 2: No false positive when only ENABLE_LLAMA_AGENT is set"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Test 3: Duplicate between two TOML agents with same name
|
|
||||||
test_3_toml_dup() {
|
|
||||||
echo "=== Test 3: Duplicate between two TOML agents with same name ==="
|
|
||||||
|
|
||||||
# Set up proper directory structure for the test
|
|
||||||
mkdir -p "${TEST_DIR}/projects"
|
|
||||||
|
|
||||||
# Create first TOML with [agents.llama]
|
|
||||||
cat > "${TEST_DIR}/projects/test3a.toml" <<'TOMLEOF'
|
|
||||||
name = "test3a"
|
|
||||||
repo = "test/test3a"
|
|
||||||
|
|
||||||
[agents.llama]
|
|
||||||
base_url = "http://10.10.10.1:8081"
|
|
||||||
model = "unsloth/Qwen3.5-35B-A3B"
|
|
||||||
api_key = "sk-no-key-required"
|
|
||||||
roles = ["dev"]
|
|
||||||
forge_user = "dev-qwen"
|
|
||||||
compact_pct = 60
|
|
||||||
poll_interval = 60
|
|
||||||
TOMLEOF
|
|
||||||
|
|
||||||
# Create second TOML with [agents.llama] (duplicate name)
|
|
||||||
cat > "${TEST_DIR}/projects/test3b.toml" <<'TOMLEOF'
|
|
||||||
name = "test3b"
|
|
||||||
repo = "test/test3b"
|
|
||||||
|
|
||||||
[agents.llama]
|
|
||||||
base_url = "http://10.10.10.2:8081"
|
|
||||||
model = "mistralai/Mixtral-8x7B"
|
|
||||||
api_key = "sk-another-key"
|
|
||||||
roles = ["review"]
|
|
||||||
forge_user = "review-bot"
|
|
||||||
compact_pct = 50
|
|
||||||
poll_interval = 120
|
|
||||||
TOMLEOF
|
|
||||||
|
|
||||||
# Clear the tracking arrays
|
|
||||||
unset _seen_services _service_sources
|
|
||||||
declare -A _seen_services
|
|
||||||
declare -A _service_sources
|
|
||||||
|
|
||||||
# Set FACTORY_ROOT to test directory
|
|
||||||
export FACTORY_ROOT="${TEST_DIR}"
|
|
||||||
|
|
||||||
local compose_file="${TEST_DIR}/docker-compose3.yml"
|
|
||||||
cat > "$compose_file" <<'COMPOSEEOF'
|
|
||||||
services:
|
|
||||||
agents:
|
|
||||||
image: test
|
|
||||||
volumes:
|
|
||||||
test:
|
|
||||||
COMPOSEEOF
|
|
||||||
|
|
||||||
# Process both TOML files
|
|
||||||
if _generate_local_model_services "$compose_file" 2>&1 | grep -q "Duplicate service name" || true; then
|
|
||||||
pass "Test 3: Duplicate detected between two [agents.llama] TOML entries"
|
|
||||||
else
|
|
||||||
fail "Test 3: Expected duplicate detection for agents-llama from two TOML files"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Test 4: No duplicate when agent names are different
|
|
||||||
test_4_different_names() {
|
|
||||||
echo "=== Test 4: No duplicate when agent names are different ==="
|
|
||||||
|
|
||||||
# Set up proper directory structure for the test
|
|
||||||
mkdir -p "${TEST_DIR}/projects"
|
|
||||||
|
|
||||||
# Create first TOML with [agents.llama]
|
|
||||||
cat > "${TEST_DIR}/projects/test4a.toml" <<'TOMLEOF'
|
|
||||||
name = "test4a"
|
|
||||||
repo = "test/test4a"
|
|
||||||
|
|
||||||
[agents.llama]
|
|
||||||
base_url = "http://10.10.10.1:8081"
|
|
||||||
model = "unsloth/Qwen3.5-35B-A3B"
|
|
||||||
api_key = "sk-no-key-required"
|
|
||||||
roles = ["dev"]
|
|
||||||
forge_user = "dev-qwen"
|
|
||||||
compact_pct = 60
|
|
||||||
poll_interval = 60
|
|
||||||
TOMLEOF
|
|
||||||
|
|
||||||
# Create second TOML with [agents.mixtral] (different name)
|
|
||||||
cat > "${TEST_DIR}/projects/test4b.toml" <<'TOMLEOF'
|
|
||||||
name = "test4b"
|
|
||||||
repo = "test/test4b"
|
|
||||||
|
|
||||||
[agents.mixtral]
|
|
||||||
base_url = "http://10.10.10.2:8081"
|
|
||||||
model = "mistralai/Mixtral-8x7B"
|
|
||||||
api_key = "sk-another-key"
|
|
||||||
roles = ["review"]
|
|
||||||
forge_user = "review-bot"
|
|
||||||
compact_pct = 50
|
|
||||||
poll_interval = 120
|
|
||||||
TOMLEOF
|
|
||||||
|
|
||||||
# Clear the tracking arrays
|
|
||||||
unset _seen_services _service_sources
|
|
||||||
declare -A _seen_services
|
|
||||||
declare -A _service_sources
|
|
||||||
|
|
||||||
# Set FACTORY_ROOT to test directory
|
|
||||||
export FACTORY_ROOT="${TEST_DIR}"
|
|
||||||
|
|
||||||
local compose_file="${TEST_DIR}/docker-compose4.yml"
|
|
||||||
cat > "$compose_file" <<'COMPOSEEOF'
|
|
||||||
services:
|
|
||||||
agents:
|
|
||||||
image: test
|
|
||||||
volumes:
|
|
||||||
test:
|
|
||||||
COMPOSEEOF
|
|
||||||
|
|
||||||
# Process both TOML files
|
|
||||||
if _generate_local_model_services "$compose_file" 2>&1 | grep -q "Duplicate service name"; then
|
|
||||||
fail "Test 4: False positive for different agent names"
|
|
||||||
else
|
|
||||||
pass "Test 4: No duplicate when agent names are different"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Run all tests
|
|
||||||
echo "Running duplicate service detection tests..."
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
test_1_llama_dup
|
|
||||||
echo ""
|
|
||||||
test_2_only_env_flag
|
|
||||||
echo ""
|
|
||||||
test_3_toml_dup
|
|
||||||
echo ""
|
|
||||||
test_4_different_names
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Summary
|
|
||||||
echo "=== Test Summary ==="
|
|
||||||
if [ "$FAILED" -eq 0 ]; then
|
|
||||||
echo "All tests passed!"
|
|
||||||
exit 0
|
|
||||||
else
|
|
||||||
echo "Some tests failed!"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
115
tools/vault-seed-chat.sh
Executable file
115
tools/vault-seed-chat.sh
Executable file
|
|
@ -0,0 +1,115 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# =============================================================================
|
||||||
|
# tools/vault-seed-chat.sh — Idempotent seed for kv/disinto/shared/chat
|
||||||
|
#
|
||||||
|
# Part of the Nomad+Vault migration (S5.2, issue #989). Populates the KV v2
|
||||||
|
# path that nomad/jobs/chat.hcl reads from, so a clean-install factory
|
||||||
|
# (no old-stack secrets to import) still has per-key values for
|
||||||
|
# CHAT_OAUTH_CLIENT_ID, CHAT_OAUTH_CLIENT_SECRET, and FORWARD_AUTH_SECRET.
|
||||||
|
#
|
||||||
|
# Companion to tools/vault-import.sh (S2.2) — when that import runs against
|
||||||
|
# a box with an existing stack, it overwrites these seeded values with the
|
||||||
|
# real ones. Order doesn't matter: whichever runs last wins, and both
|
||||||
|
# scripts are idempotent in the sense that re-running never rotates an
|
||||||
|
# existing non-empty key.
|
||||||
|
#
|
||||||
|
# Uses _hvault_seed_key (lib/hvault.sh) for each key — the helper reads
|
||||||
|
# existing data and merges to preserve sibling keys (KV v2 replaces .data
|
||||||
|
# atomically).
|
||||||
|
#
|
||||||
|
# Preconditions:
|
||||||
|
# - Vault reachable + unsealed at $VAULT_ADDR.
|
||||||
|
# - VAULT_TOKEN set (env) or /etc/vault.d/root.token readable.
|
||||||
|
# - The `kv/` mount is enabled as KV v2.
|
||||||
|
#
|
||||||
|
# Requires: VAULT_ADDR, VAULT_TOKEN, curl, jq, openssl
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# tools/vault-seed-chat.sh
|
||||||
|
# tools/vault-seed-chat.sh --dry-run
|
||||||
|
#
|
||||||
|
# Exit codes:
|
||||||
|
# 0 success (seed applied, or already applied)
|
||||||
|
# 1 precondition / API / mount-mismatch failure
|
||||||
|
# =============================================================================
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||||
|
|
||||||
|
# shellcheck source=../lib/hvault.sh
|
||||||
|
source "${REPO_ROOT}/lib/hvault.sh"
|
||||||
|
|
||||||
|
KV_MOUNT="kv"
|
||||||
|
KV_LOGICAL_PATH="disinto/shared/chat"
|
||||||
|
|
||||||
|
# Keys to seed — array-driven loop (structurally distinct from forgejo's
|
||||||
|
# sequential if-blocks and agents' role loop).
|
||||||
|
SEED_KEYS=(chat_oauth_client_id chat_oauth_client_secret forward_auth_secret)
|
||||||
|
|
||||||
|
LOG_TAG="[vault-seed-chat]"
|
||||||
|
log() { printf '%s %s\n' "$LOG_TAG" "$*"; }
|
||||||
|
die() { printf '%s ERROR: %s\n' "$LOG_TAG" "$*" >&2; exit 1; }
|
||||||
|
|
||||||
|
# ── Flag parsing — [[ ]] guard + case: shape distinct from forgejo
|
||||||
|
# (arity:value case), woodpecker (for-loop), agents (while/shift).
|
||||||
|
DRY_RUN=0
|
||||||
|
if [[ $# -gt 0 ]]; then
|
||||||
|
case "$1" in
|
||||||
|
--dry-run) DRY_RUN=1 ;;
|
||||||
|
-h|--help)
|
||||||
|
printf 'Usage: %s [--dry-run]\n\n' "$(basename "$0")"
|
||||||
|
printf 'Seed kv/disinto/shared/chat with random OAuth client\n'
|
||||||
|
printf 'credentials and forward auth secret if missing.\n'
|
||||||
|
printf 'Idempotent: existing non-empty values are preserved.\n\n'
|
||||||
|
printf ' --dry-run Show what would be seeded without writing.\n'
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*) die "invalid argument: ${1} (try --help)" ;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Preconditions — inline check-or-die (shape distinct from agents' array
|
||||||
|
# loop and forgejo's continuation-line style) ─────────────────────────────
|
||||||
|
command -v curl >/dev/null 2>&1 || die "curl not found"
|
||||||
|
command -v jq >/dev/null 2>&1 || die "jq not found"
|
||||||
|
command -v openssl >/dev/null 2>&1 || die "openssl not found"
|
||||||
|
[ -n "${VAULT_ADDR:-}" ] || die "VAULT_ADDR unset — export VAULT_ADDR=http://127.0.0.1:8200"
|
||||||
|
hvault_token_lookup >/dev/null || die "Vault auth probe failed — check VAULT_ADDR + VAULT_TOKEN"
|
||||||
|
|
||||||
|
# ── Step 1/2: ensure kv/ mount exists and is KV v2 ───────────────────────────
|
||||||
|
log "── Step 1/2: ensure ${KV_MOUNT}/ is KV v2 ──"
|
||||||
|
export DRY_RUN
|
||||||
|
hvault_ensure_kv_v2 "$KV_MOUNT" "${LOG_TAG}" \
|
||||||
|
|| die "KV mount check failed"
|
||||||
|
|
||||||
|
# ── Step 2/2: seed missing keys via _hvault_seed_key helper ──────────────────
|
||||||
|
log "── Step 2/2: seed ${KV_LOGICAL_PATH} ──"
|
||||||
|
|
||||||
|
generated=()
|
||||||
|
for key in "${SEED_KEYS[@]}"; do
|
||||||
|
if [ "$DRY_RUN" -eq 1 ]; then
|
||||||
|
# Check existence without writing
|
||||||
|
existing=$(hvault_kv_get "$KV_LOGICAL_PATH" "$key" 2>/dev/null) || true
|
||||||
|
if [ -z "$existing" ]; then
|
||||||
|
generated+=("$key")
|
||||||
|
log "[dry-run] ${key} would be generated"
|
||||||
|
else
|
||||||
|
log "[dry-run] ${key} unchanged"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
rc=0
|
||||||
|
_hvault_seed_key "$KV_LOGICAL_PATH" "$key" || rc=$?
|
||||||
|
case "$rc" in
|
||||||
|
0) generated+=("$key"); log "${key} generated" ;;
|
||||||
|
1) log "${key} unchanged" ;;
|
||||||
|
*) die "API error seeding ${key} (rc=${rc})" ;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "${#generated[@]}" -eq 0 ]; then
|
||||||
|
log "all keys present — no-op"
|
||||||
|
else
|
||||||
|
log "done — ${#generated[@]} key(s) seeded at kv/${KV_LOGICAL_PATH}"
|
||||||
|
fi
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
<!-- last-reviewed: edf7a28bd3c85d4f72d28fd986fd2af3dcb885c1 -->
|
<!-- last-reviewed: b05a31197cc78aa28f3c3e6365e782032bfb25af -->
|
||||||
# vault/policies/ — Agent Instructions
|
# vault/policies/ — Agent Instructions
|
||||||
|
|
||||||
HashiCorp Vault ACL policies for the disinto factory. One `.hcl` file per
|
HashiCorp Vault ACL policies for the disinto factory. One `.hcl` file per
|
||||||
|
|
@ -31,6 +31,8 @@ KV v2). Vault addresses KV v2 data at `kv/data/<path>` and metadata at
|
||||||
| `service-forgejo` | `kv/data/disinto/shared/forgejo/*` |
|
| `service-forgejo` | `kv/data/disinto/shared/forgejo/*` |
|
||||||
| `service-woodpecker` | `kv/data/disinto/shared/woodpecker/*` |
|
| `service-woodpecker` | `kv/data/disinto/shared/woodpecker/*` |
|
||||||
| `service-agents` | All 7 `kv/data/disinto/bots/<role>/*` namespaces + `kv/data/disinto/shared/forge/*`; composite policy for the `agents` Nomad job (S4.1) |
|
| `service-agents` | All 7 `kv/data/disinto/bots/<role>/*` namespaces + `kv/data/disinto/shared/forge/*`; composite policy for the `agents` Nomad job (S4.1) |
|
||||||
|
| `service-chat` | `kv/data/disinto/shared/chat/*`; read-only OAuth client config + forward-auth secret for the chat Nomad job (S5.2, #989) |
|
||||||
|
| `service-dispatcher` | `kv/data/disinto/runner/*` (list+read) + `kv/data/disinto/shared/ops-repo/*` (read); used by edge dispatcher sidecar (S5.1, #988) |
|
||||||
| `bot-<role>` (dev, review, gardener, architect, planner, predictor, supervisor, vault, dev-qwen) | `kv/data/disinto/bots/<role>/*` + `kv/data/disinto/shared/forge/*` |
|
| `bot-<role>` (dev, review, gardener, architect, planner, predictor, supervisor, vault, dev-qwen) | `kv/data/disinto/bots/<role>/*` + `kv/data/disinto/shared/forge/*` |
|
||||||
| `runner-<TOKEN>` (GITHUB\_TOKEN, CODEBERG\_TOKEN, CLAWHUB\_TOKEN, DEPLOY\_KEY, NPM\_TOKEN, DOCKER\_HUB\_TOKEN) | `kv/data/disinto/runner/<TOKEN>` (exactly one) |
|
| `runner-<TOKEN>` (GITHUB\_TOKEN, CODEBERG\_TOKEN, CLAWHUB\_TOKEN, DEPLOY\_KEY, NPM\_TOKEN, DOCKER\_HUB\_TOKEN) | `kv/data/disinto/runner/<TOKEN>` (exactly one) |
|
||||||
| `dispatcher` | `kv/data/disinto/runner/*` + `kv/data/disinto/shared/ops-repo/*` |
|
| `dispatcher` | `kv/data/disinto/runner/*` + `kv/data/disinto/shared/ops-repo/*` |
|
||||||
|
|
|
||||||
15
vault/policies/service-chat.hcl
Normal file
15
vault/policies/service-chat.hcl
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
# vault/policies/service-chat.hcl
|
||||||
|
#
|
||||||
|
# Read-only access to shared Chat secrets (OAuth client config, forward auth
|
||||||
|
# secret). Attached to the Chat Nomad job via workload identity (S5.2).
|
||||||
|
#
|
||||||
|
# Scope: kv/disinto/shared/chat — entries owned by the operator and
|
||||||
|
# shared between the chat service and edge proxy.
|
||||||
|
|
||||||
|
path "kv/data/disinto/shared/chat" {
|
||||||
|
capabilities = ["read"]
|
||||||
|
}
|
||||||
|
|
||||||
|
path "kv/metadata/disinto/shared/chat" {
|
||||||
|
capabilities = ["list", "read"]
|
||||||
|
}
|
||||||
29
vault/policies/service-dispatcher.hcl
Normal file
29
vault/policies/service-dispatcher.hcl
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
# vault/policies/service-dispatcher.hcl
|
||||||
|
#
|
||||||
|
# Edge dispatcher policy: needs to enumerate the runner secret namespace
|
||||||
|
# (to check secret presence before dispatching) and read the shared
|
||||||
|
# ops-repo credentials (token + clone URL) it uses to fetch action TOMLs.
|
||||||
|
#
|
||||||
|
# Scope:
|
||||||
|
# - kv/disinto/runner/* — read all per-secret values + list keys
|
||||||
|
# - kv/disinto/shared/ops-repo/* — read the ops-repo creds bundle
|
||||||
|
#
|
||||||
|
# The actual ephemeral runner container created per dispatch gets the
|
||||||
|
# narrow runner-<NAME> policies, NOT this one. This policy stays bound
|
||||||
|
# to the long-running dispatcher only.
|
||||||
|
|
||||||
|
path "kv/data/disinto/runner/*" {
|
||||||
|
capabilities = ["read"]
|
||||||
|
}
|
||||||
|
|
||||||
|
path "kv/metadata/disinto/runner/*" {
|
||||||
|
capabilities = ["list", "read"]
|
||||||
|
}
|
||||||
|
|
||||||
|
path "kv/data/disinto/shared/ops-repo" {
|
||||||
|
capabilities = ["read"]
|
||||||
|
}
|
||||||
|
|
||||||
|
path "kv/metadata/disinto/shared/ops-repo" {
|
||||||
|
capabilities = ["list", "read"]
|
||||||
|
}
|
||||||
|
|
@ -70,6 +70,13 @@ roles:
|
||||||
namespace: default
|
namespace: default
|
||||||
job_id: agents
|
job_id: agents
|
||||||
|
|
||||||
|
# ── Chat UI (nomad/jobs/chat.hcl — S5.2) ─────────────────────────────────
|
||||||
|
# Claude chat UI service with OAuth secrets. Uses vault/policies/service-chat.hcl.
|
||||||
|
- name: service-chat
|
||||||
|
policy: service-chat
|
||||||
|
namespace: default
|
||||||
|
job_id: chat
|
||||||
|
|
||||||
# ── Per-agent bots (nomad/jobs/bot-<role>.hcl — land in later steps) ───────
|
# ── Per-agent bots (nomad/jobs/bot-<role>.hcl — land in later steps) ───────
|
||||||
# job_id placeholders match the policy name 1:1 until each bot's jobspec
|
# job_id placeholders match the policy name 1:1 until each bot's jobspec
|
||||||
# lands. When a bot's jobspec is added under nomad/jobs/, update the
|
# lands. When a bot's jobspec is added under nomad/jobs/, update the
|
||||||
|
|
@ -121,10 +128,10 @@ roles:
|
||||||
job_id: bot-vault
|
job_id: bot-vault
|
||||||
|
|
||||||
# ── Edge dispatcher ────────────────────────────────────────────────────────
|
# ── Edge dispatcher ────────────────────────────────────────────────────────
|
||||||
- name: dispatcher
|
- name: service-dispatcher
|
||||||
policy: dispatcher
|
policy: service-dispatcher
|
||||||
namespace: default
|
namespace: default
|
||||||
job_id: dispatcher
|
job_id: edge
|
||||||
|
|
||||||
# ── Per-secret runner roles ────────────────────────────────────────────────
|
# ── Per-secret runner roles ────────────────────────────────────────────────
|
||||||
# vault-runner (Step 5) composes runner-<NAME> policies onto each
|
# vault-runner (Step 5) composes runner-<NAME> policies onto each
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue