From d76557542ab98f181cecc58205a2dbc3922f2d32 Mon Sep 17 00:00:00 2001 From: Pedro Boueke Date: Thu, 26 Mar 2026 10:49:37 -0300 Subject: [PATCH 1/2] chore: add carranca CI config and PR review workflow --- .carranca.yml | 71 +++++++++ .carranca/Containerfile | 47 ++++++ .carranca/lib/json.sh | 57 +++++++ .carranca/shell-wrapper.sh | 125 ++++++++++++++++ .carranca/skills/carranca/confiskill/SKILL.md | 18 +++ .github/workflows/pr-review.yml | 141 ++++++++++++++++++ CHANGELOG.md | 7 + README.md | 2 +- package.json | 2 +- 9 files changed, 468 insertions(+), 2 deletions(-) create mode 100644 .carranca.yml create mode 100644 .carranca/Containerfile create mode 100644 .carranca/lib/json.sh create mode 100644 .carranca/shell-wrapper.sh create mode 100644 .carranca/skills/carranca/confiskill/SKILL.md create mode 100644 .github/workflows/pr-review.yml diff --git a/.carranca.yml b/.carranca.yml new file mode 100644 index 0000000..dd54b41 --- /dev/null +++ b/.carranca.yml @@ -0,0 +1,71 @@ +# .carranca.yml — carranca project configuration +# See: https://github.com/pboueke/carranca + +agents: + - name: claude + adapter: claude + command: claude + + - name: codex + adapter: codex + command: codex + + - name: opencode + adapter: opencode + command: opencode + + - name: reviewer + adapter: stdin + command: bash /workspace/_review.sh + +# Container runtime settings +runtime: + engine: auto + network: true + # Fine-grained network policy (replaces boolean; requires yq): + # network: + # default: deny + # allow: + # - "*.anthropic.com:443" + # - "registry.npmjs.org:443" + # Extra container runtime flags for the agent container (e.g. --gpus all) + # extra_flags: --gpus all + # Extra container runtime flags for the logger container + # logger_extra_flags: + # seccomp_profile: default # "default" (carranca built-in), "unconfined", or absolute path + # apparmor_profile: # AppArmor profile name (must be loaded); "unconfined" to disable + # cap_drop_all: true # Drop all Linux capabilities (--cap-drop ALL) + # read_only: true # Read-only root filesystem (--read-only + tmpfs) + # Linux capabilities for the agent container (allowlist after cap_drop_all) + # cap_add: + # - SYS_PTRACE + +# Persistent volumes for the agent container +volumes: + cache: true # Cache agent memory, config, session across runs + # extra: # Custom volume mounts (host:container[:mode]) + # - ~/docs:/reference:ro + +policy: + docs_before_code: warn # warn, enforce, or off — git pre-commit hook + tests_before_impl: warn # warn, enforce, or off — git pre-commit hook + # max_duration: 3600 # Kill agent after N seconds (0 = no limit) + # resource_limits: # Requires yq + # memory: "2g" + # cpus: "2.0" + # pids: 256 + # filesystem: # Requires yq + # enforce_watched_paths: false # Make watched_paths read-only + +# observability: +# independent_observer: false # Run execve/network monitoring in independent sidecar + +# Environment variables forwarded into agent containers +environment: + passthrough: + - OPENAI_API_KEY + +watched_paths: + - .env + - secrets/ + - "*.key" diff --git a/.carranca/Containerfile b/.carranca/Containerfile new file mode 100644 index 0000000..60874ec --- /dev/null +++ b/.carranca/Containerfile @@ -0,0 +1,47 @@ +# Carranca agent container +# Customize this file to install your agent CLI and project dependencies. +# The shell wrapper is injected automatically — do not remove the last lines. +# +# Examples: +# - Install Python: RUN apk add python3 py3-pip +# - Install Node.js: RUN apk add nodejs npm +# - Install Go: RUN apk add go + +FROM alpine:3.21 + +# Install base tools +RUN apk add --no-cache \ + bash \ + coreutils \ + curl \ + git \ + ca-certificates \ + iptables + +RUN mkdir -p /home/carranca && chmod 0777 /home/carranca + +# ────────────────────────────────────────────── +# Add your agent and project dependencies below: +# Node.js + npm (for project and agents) +RUN apk add --no-cache bubblewrap nodejs npm python3 + +# OpenAI Codex CLI +RUN npm install -g @openai/codex + +# Claude Code CLI +RUN npm install -g @anthropic-ai/claude-code + +# OpenCode CLI +RUN npm install -g opencode-ai +# ────────────────────────────────────────────── + + + +# ────────────────────────────────────────────── +# Carranca shell wrapper (do not remove) +# ────────────────────────────────────────────── +COPY lib/json.sh /usr/local/bin/lib/json.sh +COPY shell-wrapper.sh /usr/local/bin/shell-wrapper.sh +RUN chmod +x /usr/local/bin/shell-wrapper.sh +WORKDIR /workspace +ENTRYPOINT ["/usr/local/bin/shell-wrapper.sh"] diff --git a/.carranca/lib/json.sh b/.carranca/lib/json.sh new file mode 100644 index 0000000..666e394 --- /dev/null +++ b/.carranca/lib/json.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env bash +# carranca/runtime/lib/json.sh — shared JSON utility functions +# Sourced by shell-wrapper.sh and any other runtime script that needs +# safe JSON string encoding. +# +# Provides: +# json_escape "$string" — RFC 8259 compliant string escaping +# json_validate_line "$line" — basic structural check (starts with {, ends with }) +# +# Implementation uses pure bash/sed for portability (no jq dependency). + +# Escape a string for safe embedding in a JSON value per RFC 8259. +# Handles: \ " newline carriage-return tab backspace form-feed +# and all remaining control characters U+0000–U+001F as \uXXXX. +json_escape() { + local input="$1" + # Phase 1: escape backslash first (must come before other escapes that + # introduce backslashes), then double-quote, then named controls. + local result + result="$(printf '%s' "$input" | sed \ + -e 's/\\/\\\\/g' \ + -e 's/"/\\"/g' \ + -e 's/\x08/\\b/g' \ + -e 's/\x0c/\\f/g' \ + -e 's/\t/\\t/g')" + + # Phase 2: handle newlines and carriage returns. + # sed operates line-by-line so we use bash parameter expansion instead. + result="${result//$'\n'/\\n}" + result="${result//$'\r'/\\r}" + + # Phase 3: escape remaining control characters U+0000–U+001F as \uXXXX. + # After the above, the only remaining controls are 0x00-0x07, 0x0e-0x1f + # (0x08=\b, 0x09=\t, 0x0a=\n, 0x0c=\f, 0x0d=\r already handled). + local i char_val hex out="" + for (( i=0; i<${#result}; i++ )); do + char_val="$(printf '%d' "'${result:i:1}" 2>/dev/null || echo 0)" + if (( char_val >= 0 && char_val <= 31 )); then + hex="$(printf '%04x' "$char_val")" + out+="\\u${hex}" + else + out+="${result:i:1}" + fi + done + + printf '%s' "$out" +} + +# Basic structural validation: a JSON line must start with { and end with }. +# Returns 0 (true) if valid, 1 (false) otherwise. +json_validate_line() { + local line="$1" + # Strip leading/trailing whitespace + local trimmed + trimmed="$(printf '%s' "$line" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')" + [[ "$trimmed" == "{"* && "$trimmed" == *"}" ]] +} diff --git a/.carranca/shell-wrapper.sh b/.carranca/shell-wrapper.sh new file mode 100644 index 0000000..2821d89 --- /dev/null +++ b/.carranca/shell-wrapper.sh @@ -0,0 +1,125 @@ +#!/usr/bin/env bash +# carranca shell-wrapper — wraps agent command execution and writes events to FIFO +# +# This script is the ENTRYPOINT of the agent container. It: +# 1. Waits for the FIFO to be ready (created by logger) +# 2. Starts a heartbeat background process (30s interval) +# 3. Writes agent_start event +# 4. Executes the agent command, capturing exit code +# 5. Writes agent_stop event +# 6. Exits immediately if the FIFO breaks (fail closed) +set -uo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +FIFO_PATH="/fifo/events" +SESSION_ID="${SESSION_ID:-unknown}" +AGENT_COMMAND="${AGENT_COMMAND:-bash}" + +# --- Helpers --- + +timestamp() { + date -u +%Y-%m-%dT%H:%M:%S.%3NZ +} + +ms_now() { + date +%s%3N 2>/dev/null || date +%s +} + +fail_closed() { + local message="$1" + echo "[carranca] $message — exiting (fail closed)" >&2 + kill 0 2>/dev/null + exit 1 +} + +fifo_is_healthy() { + [ -p "$FIFO_PATH" ] && [ -w "$FIFO_PATH" ] +} + +write_event() { + if ! fifo_is_healthy; then + fail_closed "FIFO is unavailable" + fi + + printf '%s\n' "$1" > "$FIFO_PATH" 2>/dev/null + local rc=$? + if [ "$rc" -ne 0 ]; then + fail_closed "FIFO write failed" + fi +} + +# shellcheck source=lib/json.sh +source "$SCRIPT_DIR/lib/json.sh" + +# --- Wait for FIFO --- + +WAIT_LIMIT=20 +WAIT_COUNT=0 +while [ ! -p "$FIFO_PATH" ]; do + WAIT_COUNT=$((WAIT_COUNT + 1)) + if [ "$WAIT_COUNT" -ge "$WAIT_LIMIT" ]; then + fail_closed "FIFO not found after ${WAIT_LIMIT}s" + fi + sleep 0.5 +done + +# --- Heartbeat --- + +_heartbeat_loop() { + while true; do + sleep 30 + printf '{"type":"heartbeat","source":"shell-wrapper","ts":"%s","session_id":"%s"}\n' "$(timestamp)" "$SESSION_ID" > "$FIFO_PATH" 2>/dev/null || exit 1 + done +} + +_heartbeat_loop & +HEARTBEAT_PID=$! + +_fifo_watchdog_loop() { + while true; do + sleep 1 + fifo_is_healthy || fail_closed "FIFO disappeared" + done +} + +_fifo_watchdog_loop & +WATCHDOG_PID=$! + +# --- Session start event --- + +write_event "{\"type\":\"session_event\",\"source\":\"shell-wrapper\",\"event\":\"agent_start\",\"ts\":\"$(timestamp)\",\"session_id\":\"$SESSION_ID\"}" + +# --- Policy hooks setup (4.3) --- + +if [ "${POLICY_HOOKS:-}" = "true" ] && [ -d "/carranca-hooks" ]; then + git config --global core.hooksPath /carranca-hooks 2>/dev/null || true +fi + +# --- Execute agent command --- +# We log the overall agent command as a shell_command event. +# The agent may run sub-commands internally — those are captured by +# inotifywait (file mutations) but not individually logged as shell_command +# events in MVP (that requires execve tracing, Phase 3). + +START_MS="$(ms_now)" +# AGENT_COMMAND is operator-authored (from .carranca.yml), not agent-controlled. +# eval is required to support shell syntax (pipes, &&, env vars, subshells). +# .carranca.yml is trusted operator input, hidden from the agent at runtime. +eval "$AGENT_COMMAND" +AGENT_EXIT=$? +END_MS="$(ms_now)" +DURATION=$((END_MS - START_MS)) + +ESCAPED_CMD="$(json_escape "$AGENT_COMMAND")" +ESCAPED_CWD="$(json_escape "$(pwd)")" +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\"}" + +# --- Session stop event --- + +write_event "{\"type\":\"session_event\",\"source\":\"shell-wrapper\",\"event\":\"agent_stop\",\"ts\":\"$(timestamp)\",\"session_id\":\"$SESSION_ID\",\"exit_code\":$AGENT_EXIT}" + +# Cleanup +kill $HEARTBEAT_PID 2>/dev/null || true +kill $WATCHDOG_PID 2>/dev/null || true +exit $AGENT_EXIT diff --git a/.carranca/skills/carranca/confiskill/SKILL.md b/.carranca/skills/carranca/confiskill/SKILL.md new file mode 100644 index 0000000..6f915d6 --- /dev/null +++ b/.carranca/skills/carranca/confiskill/SKILL.md @@ -0,0 +1,18 @@ +--- +name: carranca-confiskill +description: Guidance for proposing carranca runtime configuration updates for the current workspace +--- + +# Carranca Config Skill + +When asked to configure carranca for a repo: + +1. Review the workspace to identify the project stack, package managers, and development tooling needs. +2. Read both Carranca-managed skills and any user-provided skills before proposing changes. +3. Propose changes only to `.carranca.yml` and `.carranca/Containerfile`. +4. Preserve the shell-wrapper lines in the Containerfile. +5. Keep `.carranca.yml` on the `agents:` format and preserve valid existing agent entries unless the operator request says otherwise. +6. Use any explicit operator request in the prompt as a hard input when deciding what to change. +7. Prefer adding the minimum container dependencies needed for development inside the container. +8. Explain the rationale for each change clearly and briefly. +9. If no changes are needed, say so explicitly and output unchanged proposed files. diff --git a/.github/workflows/pr-review.yml b/.github/workflows/pr-review.yml new file mode 100644 index 0000000..9e76144 --- /dev/null +++ b/.github/workflows/pr-review.yml @@ -0,0 +1,141 @@ +name: PR Review + +on: + pull_request: + branches: [main] + +jobs: + review: + name: Carranca AI Review + runs-on: ubuntu-latest + if: github.event.pull_request.user.login == 'pboueke' + permissions: + contents: read + pull-requests: write + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Install carranca + run: | + git clone --depth 1 https://github.com/pboueke/carranca.git ~/.local/share/carranca + mkdir -p ~/.local/bin + ln -s ~/.local/share/carranca/cli/carranca ~/.local/bin/carranca + echo "$HOME/.local/bin" >> "$GITHUB_PATH" + + - name: Generate review prompt + run: | + BASE_REF="${{ github.event.pull_request.base.ref }}" + PR_NUM="${{ github.event.pull_request.number }}" + PR_BRANCH="${{ github.event.pull_request.head.ref }}" + PR_AUTHOR="${{ github.event.pull_request.user.login }}" + PR_TITLE="${{ github.event.pull_request.title }}" + + # Generate diff (truncate at ~100KB on a line boundary) + git diff "origin/${BASE_REF}...HEAD" > _review_diff_full.patch + DIFF_SIZE=$(wc -c < _review_diff_full.patch) + if [ "$DIFF_SIZE" -gt 102400 ]; then + head -c 102400 _review_diff_full.patch | head -n -1 > _review_diff.patch + TRUNCATED_NOTE="**WARNING: The diff was truncated from ${DIFF_SIZE} bytes to ~100KB. Your review is INCOMPLETE — flag this to the PR author.**" + else + cp _review_diff_full.patch _review_diff.patch + TRUNCATED_NOTE="" + fi + + cat > _review_prompt.md < _review.sh <<'SCRIPT_EOF' + #!/usr/bin/env bash + set -euo pipefail + + # Configure opencode with zai-coding-plan provider. + # Uses python3 json.dump for safe escaping of API key characters. + mkdir -p ~/.config/opencode + python3 -c " + import json, os + cfg = { + 'provider': {'zai-coding-plan': {'options': {'apiKey': os.environ['OPENAI_API_KEY']}}}, + 'model': 'zai-coding-plan/glm-4.6' + } + json.dump(cfg, open(os.path.expanduser('~/.config/opencode/opencode.json'), 'w')) + " + + opencode run 'Review this PR per the attached file' \ + -f /workspace/_review_prompt.md > /workspace/_review_output.md + SCRIPT_EOF + chmod +x _review.sh + + - name: Run carranca reviewer + env: + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + CARRANCA_CONTAINER_RUNTIME: docker + run: carranca run --agent reviewer --timeout 600 + + - name: Post review comment + if: always() + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + PR_NUM="${{ github.event.pull_request.number }}" + + if [ -s _review_output.md ]; then + # Wrap review in a collapsible section with header + { + echo "## Carranca AI Review" + echo "" + cat _review_output.md + echo "" + echo "---" + echo "*Automated review by [carranca](https://github.com/pboueke/carranca) sandbox*" + } > _review_comment.md + + gh pr comment "$PR_NUM" --body-file _review_comment.md + else + gh pr comment "$PR_NUM" --body "## Carranca AI Review + + :warning: The automated review did not produce output. The agent may have failed or timed out. + + See the [workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) for details. + + --- + *Automated review by [carranca](https://github.com/pboueke/carranca) sandbox*" + fi diff --git a/CHANGELOG.md b/CHANGELOG.md index 316ab3c..ebe1835 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## v1.0.4 - Carranca CI and container configuration + +- ci: add GitHub Actions PR review workflow using carranca-sandboxed AI reviewer +- chore: add claude, opencode, and reviewer agents to carranca configuration +- chore: add environment passthrough for OPENAI_API_KEY in carranca config +- chore: install claude-code, opencode-ai, and python3 in the carranca container + ## v1.0.3 - Remove CI badges from README - docs: remove GitHub Actions CI badges from the README while keeping version, test, and coverage badges diff --git a/README.md b/README.md index 88556c5..791b8f1 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ Data model library for [Canto](https://github.com/pboueke/canto), a private encrypted journaling app. [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) -![Version](https://img.shields.io/badge/version-1.0.3-green) +![Version](https://img.shields.io/badge/version-1.0.4-green) ![Tests](https://img.shields.io/badge/tests-156%2F156%20passed-brightgreen) ![Coverage](https://img.shields.io/badge/coverage-100%25-brightgreen) diff --git a/package.json b/package.json index 28e8d1e..9a6b9ca 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "canto-data", - "version": "1.0.3", + "version": "1.0.4", "description": "Canto journal data model — types, validation, versioning, and format utilities", "license": "MIT", "main": "dist/index.js", From ab0458ba5053556c756c905f1cc38ef4769197c3 Mon Sep 17 00:00:00 2001 From: Pedro Boueke Date: Thu, 26 Mar 2026 10:56:01 -0300 Subject: [PATCH 2/2] fix: install opencode via official installer instead of npm --- .carranca/Containerfile | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.carranca/Containerfile b/.carranca/Containerfile index 60874ec..1eb3124 100644 --- a/.carranca/Containerfile +++ b/.carranca/Containerfile @@ -32,7 +32,10 @@ RUN npm install -g @openai/codex RUN npm install -g @anthropic-ai/claude-code # OpenCode CLI -RUN npm install -g opencode-ai +RUN curl -fsSL https://opencode.ai/install | bash -s -- --no-modify-path && \ + mv /root/.opencode/bin/opencode /usr/local/bin/opencode && \ + chmod 0755 /usr/local/bin/opencode && \ + rm -rf /root/.opencode # ──────────────────────────────────────────────