Skip to content

Commit 363f7cc

Browse files
authored
Merge pull request #1 from pboueke/carranca-ci
chore: add carranca CI config and PR review workflow
2 parents 152f580 + ab0458b commit 363f7cc

9 files changed

Lines changed: 471 additions & 2 deletions

File tree

.carranca.yml

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# .carranca.yml — carranca project configuration
2+
# See: https://github.com/pboueke/carranca
3+
4+
agents:
5+
- name: claude
6+
adapter: claude
7+
command: claude
8+
9+
- name: codex
10+
adapter: codex
11+
command: codex
12+
13+
- name: opencode
14+
adapter: opencode
15+
command: opencode
16+
17+
- name: reviewer
18+
adapter: stdin
19+
command: bash /workspace/_review.sh
20+
21+
# Container runtime settings
22+
runtime:
23+
engine: auto
24+
network: true
25+
# Fine-grained network policy (replaces boolean; requires yq):
26+
# network:
27+
# default: deny
28+
# allow:
29+
# - "*.anthropic.com:443"
30+
# - "registry.npmjs.org:443"
31+
# Extra container runtime flags for the agent container (e.g. --gpus all)
32+
# extra_flags: --gpus all
33+
# Extra container runtime flags for the logger container
34+
# logger_extra_flags:
35+
# seccomp_profile: default # "default" (carranca built-in), "unconfined", or absolute path
36+
# apparmor_profile: # AppArmor profile name (must be loaded); "unconfined" to disable
37+
# cap_drop_all: true # Drop all Linux capabilities (--cap-drop ALL)
38+
# read_only: true # Read-only root filesystem (--read-only + tmpfs)
39+
# Linux capabilities for the agent container (allowlist after cap_drop_all)
40+
# cap_add:
41+
# - SYS_PTRACE
42+
43+
# Persistent volumes for the agent container
44+
volumes:
45+
cache: true # Cache agent memory, config, session across runs
46+
# extra: # Custom volume mounts (host:container[:mode])
47+
# - ~/docs:/reference:ro
48+
49+
policy:
50+
docs_before_code: warn # warn, enforce, or off — git pre-commit hook
51+
tests_before_impl: warn # warn, enforce, or off — git pre-commit hook
52+
# max_duration: 3600 # Kill agent after N seconds (0 = no limit)
53+
# resource_limits: # Requires yq
54+
# memory: "2g"
55+
# cpus: "2.0"
56+
# pids: 256
57+
# filesystem: # Requires yq
58+
# enforce_watched_paths: false # Make watched_paths read-only
59+
60+
# observability:
61+
# independent_observer: false # Run execve/network monitoring in independent sidecar
62+
63+
# Environment variables forwarded into agent containers
64+
environment:
65+
passthrough:
66+
- OPENAI_API_KEY
67+
68+
watched_paths:
69+
- .env
70+
- secrets/
71+
- "*.key"

.carranca/Containerfile

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# Carranca agent container
2+
# Customize this file to install your agent CLI and project dependencies.
3+
# The shell wrapper is injected automatically — do not remove the last lines.
4+
#
5+
# Examples:
6+
# - Install Python: RUN apk add python3 py3-pip
7+
# - Install Node.js: RUN apk add nodejs npm
8+
# - Install Go: RUN apk add go
9+
10+
FROM alpine:3.21
11+
12+
# Install base tools
13+
RUN apk add --no-cache \
14+
bash \
15+
coreutils \
16+
curl \
17+
git \
18+
ca-certificates \
19+
iptables
20+
21+
RUN mkdir -p /home/carranca && chmod 0777 /home/carranca
22+
23+
# ──────────────────────────────────────────────
24+
# Add your agent and project dependencies below:
25+
# Node.js + npm (for project and agents)
26+
RUN apk add --no-cache bubblewrap nodejs npm python3
27+
28+
# OpenAI Codex CLI
29+
RUN npm install -g @openai/codex
30+
31+
# Claude Code CLI
32+
RUN npm install -g @anthropic-ai/claude-code
33+
34+
# OpenCode CLI
35+
RUN curl -fsSL https://opencode.ai/install | bash -s -- --no-modify-path && \
36+
mv /root/.opencode/bin/opencode /usr/local/bin/opencode && \
37+
chmod 0755 /usr/local/bin/opencode && \
38+
rm -rf /root/.opencode
39+
# ──────────────────────────────────────────────
40+
41+
42+
43+
# ──────────────────────────────────────────────
44+
# Carranca shell wrapper (do not remove)
45+
# ──────────────────────────────────────────────
46+
COPY lib/json.sh /usr/local/bin/lib/json.sh
47+
COPY shell-wrapper.sh /usr/local/bin/shell-wrapper.sh
48+
RUN chmod +x /usr/local/bin/shell-wrapper.sh
49+
WORKDIR /workspace
50+
ENTRYPOINT ["/usr/local/bin/shell-wrapper.sh"]

.carranca/lib/json.sh

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
#!/usr/bin/env bash
2+
# carranca/runtime/lib/json.sh — shared JSON utility functions
3+
# Sourced by shell-wrapper.sh and any other runtime script that needs
4+
# safe JSON string encoding.
5+
#
6+
# Provides:
7+
# json_escape "$string" — RFC 8259 compliant string escaping
8+
# json_validate_line "$line" — basic structural check (starts with {, ends with })
9+
#
10+
# Implementation uses pure bash/sed for portability (no jq dependency).
11+
12+
# Escape a string for safe embedding in a JSON value per RFC 8259.
13+
# Handles: \ " newline carriage-return tab backspace form-feed
14+
# and all remaining control characters U+0000–U+001F as \uXXXX.
15+
json_escape() {
16+
local input="$1"
17+
# Phase 1: escape backslash first (must come before other escapes that
18+
# introduce backslashes), then double-quote, then named controls.
19+
local result
20+
result="$(printf '%s' "$input" | sed \
21+
-e 's/\\/\\\\/g' \
22+
-e 's/"/\\"/g' \
23+
-e 's/\x08/\\b/g' \
24+
-e 's/\x0c/\\f/g' \
25+
-e 's/\t/\\t/g')"
26+
27+
# Phase 2: handle newlines and carriage returns.
28+
# sed operates line-by-line so we use bash parameter expansion instead.
29+
result="${result//$'\n'/\\n}"
30+
result="${result//$'\r'/\\r}"
31+
32+
# Phase 3: escape remaining control characters U+0000–U+001F as \uXXXX.
33+
# After the above, the only remaining controls are 0x00-0x07, 0x0e-0x1f
34+
# (0x08=\b, 0x09=\t, 0x0a=\n, 0x0c=\f, 0x0d=\r already handled).
35+
local i char_val hex out=""
36+
for (( i=0; i<${#result}; i++ )); do
37+
char_val="$(printf '%d' "'${result:i:1}" 2>/dev/null || echo 0)"
38+
if (( char_val >= 0 && char_val <= 31 )); then
39+
hex="$(printf '%04x' "$char_val")"
40+
out+="\\u${hex}"
41+
else
42+
out+="${result:i:1}"
43+
fi
44+
done
45+
46+
printf '%s' "$out"
47+
}
48+
49+
# Basic structural validation: a JSON line must start with { and end with }.
50+
# Returns 0 (true) if valid, 1 (false) otherwise.
51+
json_validate_line() {
52+
local line="$1"
53+
# Strip leading/trailing whitespace
54+
local trimmed
55+
trimmed="$(printf '%s' "$line" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')"
56+
[[ "$trimmed" == "{"* && "$trimmed" == *"}" ]]
57+
}

.carranca/shell-wrapper.sh

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
#!/usr/bin/env bash
2+
# carranca shell-wrapper — wraps agent command execution and writes events to FIFO
3+
#
4+
# This script is the ENTRYPOINT of the agent container. It:
5+
# 1. Waits for the FIFO to be ready (created by logger)
6+
# 2. Starts a heartbeat background process (30s interval)
7+
# 3. Writes agent_start event
8+
# 4. Executes the agent command, capturing exit code
9+
# 5. Writes agent_stop event
10+
# 6. Exits immediately if the FIFO breaks (fail closed)
11+
set -uo pipefail
12+
13+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
14+
15+
FIFO_PATH="/fifo/events"
16+
SESSION_ID="${SESSION_ID:-unknown}"
17+
AGENT_COMMAND="${AGENT_COMMAND:-bash}"
18+
19+
# --- Helpers ---
20+
21+
timestamp() {
22+
date -u +%Y-%m-%dT%H:%M:%S.%3NZ
23+
}
24+
25+
ms_now() {
26+
date +%s%3N 2>/dev/null || date +%s
27+
}
28+
29+
fail_closed() {
30+
local message="$1"
31+
echo "[carranca] $message — exiting (fail closed)" >&2
32+
kill 0 2>/dev/null
33+
exit 1
34+
}
35+
36+
fifo_is_healthy() {
37+
[ -p "$FIFO_PATH" ] && [ -w "$FIFO_PATH" ]
38+
}
39+
40+
write_event() {
41+
if ! fifo_is_healthy; then
42+
fail_closed "FIFO is unavailable"
43+
fi
44+
45+
printf '%s\n' "$1" > "$FIFO_PATH" 2>/dev/null
46+
local rc=$?
47+
if [ "$rc" -ne 0 ]; then
48+
fail_closed "FIFO write failed"
49+
fi
50+
}
51+
52+
# shellcheck source=lib/json.sh
53+
source "$SCRIPT_DIR/lib/json.sh"
54+
55+
# --- Wait for FIFO ---
56+
57+
WAIT_LIMIT=20
58+
WAIT_COUNT=0
59+
while [ ! -p "$FIFO_PATH" ]; do
60+
WAIT_COUNT=$((WAIT_COUNT + 1))
61+
if [ "$WAIT_COUNT" -ge "$WAIT_LIMIT" ]; then
62+
fail_closed "FIFO not found after ${WAIT_LIMIT}s"
63+
fi
64+
sleep 0.5
65+
done
66+
67+
# --- Heartbeat ---
68+
69+
_heartbeat_loop() {
70+
while true; do
71+
sleep 30
72+
printf '{"type":"heartbeat","source":"shell-wrapper","ts":"%s","session_id":"%s"}\n' "$(timestamp)" "$SESSION_ID" > "$FIFO_PATH" 2>/dev/null || exit 1
73+
done
74+
}
75+
76+
_heartbeat_loop &
77+
HEARTBEAT_PID=$!
78+
79+
_fifo_watchdog_loop() {
80+
while true; do
81+
sleep 1
82+
fifo_is_healthy || fail_closed "FIFO disappeared"
83+
done
84+
}
85+
86+
_fifo_watchdog_loop &
87+
WATCHDOG_PID=$!
88+
89+
# --- Session start event ---
90+
91+
write_event "{\"type\":\"session_event\",\"source\":\"shell-wrapper\",\"event\":\"agent_start\",\"ts\":\"$(timestamp)\",\"session_id\":\"$SESSION_ID\"}"
92+
93+
# --- Policy hooks setup (4.3) ---
94+
95+
if [ "${POLICY_HOOKS:-}" = "true" ] && [ -d "/carranca-hooks" ]; then
96+
git config --global core.hooksPath /carranca-hooks 2>/dev/null || true
97+
fi
98+
99+
# --- Execute agent command ---
100+
# We log the overall agent command as a shell_command event.
101+
# The agent may run sub-commands internally — those are captured by
102+
# inotifywait (file mutations) but not individually logged as shell_command
103+
# events in MVP (that requires execve tracing, Phase 3).
104+
105+
START_MS="$(ms_now)"
106+
# AGENT_COMMAND is operator-authored (from .carranca.yml), not agent-controlled.
107+
# eval is required to support shell syntax (pipes, &&, env vars, subshells).
108+
# .carranca.yml is trusted operator input, hidden from the agent at runtime.
109+
eval "$AGENT_COMMAND"
110+
AGENT_EXIT=$?
111+
END_MS="$(ms_now)"
112+
DURATION=$((END_MS - START_MS))
113+
114+
ESCAPED_CMD="$(json_escape "$AGENT_COMMAND")"
115+
ESCAPED_CWD="$(json_escape "$(pwd)")"
116+
write_event "{\"type\":\"shell_command\",\"source\":\"shell-wrapper\",\"ts\":\"$(timestamp)\",\"session_id\":\"$SESSION_ID\",\"command\":\"$ESCAPED_CMD\",\"exit_code\":$AGENT_EXIT,\"duration_ms\":$DURATION,\"cwd\":\"$ESCAPED_CWD\"}"
117+
118+
# --- Session stop event ---
119+
120+
write_event "{\"type\":\"session_event\",\"source\":\"shell-wrapper\",\"event\":\"agent_stop\",\"ts\":\"$(timestamp)\",\"session_id\":\"$SESSION_ID\",\"exit_code\":$AGENT_EXIT}"
121+
122+
# Cleanup
123+
kill $HEARTBEAT_PID 2>/dev/null || true
124+
kill $WATCHDOG_PID 2>/dev/null || true
125+
exit $AGENT_EXIT
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
name: carranca-confiskill
3+
description: Guidance for proposing carranca runtime configuration updates for the current workspace
4+
---
5+
6+
# Carranca Config Skill
7+
8+
When asked to configure carranca for a repo:
9+
10+
1. Review the workspace to identify the project stack, package managers, and development tooling needs.
11+
2. Read both Carranca-managed skills and any user-provided skills before proposing changes.
12+
3. Propose changes only to `.carranca.yml` and `.carranca/Containerfile`.
13+
4. Preserve the shell-wrapper lines in the Containerfile.
14+
5. Keep `.carranca.yml` on the `agents:` format and preserve valid existing agent entries unless the operator request says otherwise.
15+
6. Use any explicit operator request in the prompt as a hard input when deciding what to change.
16+
7. Prefer adding the minimum container dependencies needed for development inside the container.
17+
8. Explain the rationale for each change clearly and briefly.
18+
9. If no changes are needed, say so explicitly and output unchanged proposed files.

0 commit comments

Comments
 (0)