From 6758f10a7e98dd023f01f5b6cff373b5b0f0a63e Mon Sep 17 00:00:00 2001 From: Agent Date: Tue, 31 Mar 2026 18:10:14 +0000 Subject: [PATCH 1/3] fix: SECURITY: Replace eval usage with safer alternatives (#59) --- lib/env.sh | 17 +++++++++++++++-- lib/issue-lifecycle.sh | 24 ++++++++++++------------ lib/mirrors.sh | 10 +++++++++- 3 files changed, 36 insertions(+), 15 deletions(-) diff --git a/lib/env.sh b/lib/env.sh index 92eb676..48cd541 100755 --- a/lib/env.sh +++ b/lib/env.sh @@ -29,8 +29,21 @@ if [ -f "$FACTORY_ROOT/.env.enc" ] && command -v sops &>/dev/null; then set -a _saved_forge_url="${FORGE_URL:-}" _saved_forge_token="${FORGE_TOKEN:-}" - eval "$(sops -d --output-type dotenv "$FACTORY_ROOT/.env.enc" 2>/dev/null)" \ - || echo "Warning: failed to decrypt .env.enc — secrets not loaded" >&2 + # Use temp file + validate dotenv format before sourcing (avoids eval injection) + _tmpenv=$(mktemp) || { echo "Warning: failed to create temp file for .env.enc" >&2; } + if sops -d --output-type dotenv "$FACTORY_ROOT/.env.enc" > "$_tmpenv" 2>/dev/null; then + # Validate: each non-empty, non-comment line must match KEY=value pattern + if grep -qE '^[A-Za-z_][A-Za-z0-9_]*=' "$_tmpenv" 2>/dev/null && \ + ! grep -qE '^[^A-Za-z_]' "$_tmpenv" 2>/dev/null; then + # shellcheck source=/dev/null + source "$_tmpenv" + else + echo "Warning: .env.enc decryption output failed format validation" >&2 + fi + else + echo "Warning: failed to decrypt .env.enc — secrets not loaded" >&2 + fi + rm -f "$_tmpenv" set +a [ -n "$_saved_forge_url" ] && export FORGE_URL="$_saved_forge_url" [ -n "$_saved_forge_token" ] && export FORGE_TOKEN="$_saved_forge_token" diff --git a/lib/issue-lifecycle.sh b/lib/issue-lifecycle.sh index 19c422d..81586f9 100644 --- a/lib/issue-lifecycle.sh +++ b/lib/issue-lifecycle.sh @@ -45,16 +45,16 @@ _ilc_log() { # Label ID caching — lookup once per name, cache in globals. # Pattern follows ci-helpers.sh (ensure_blocked_label_id). # --------------------------------------------------------------------------- -_ILC_BACKLOG_ID="" -_ILC_IN_PROGRESS_ID="" -_ILC_BLOCKED_ID="" +declare -A _ILC_LABEL_IDS +_ILC_LABEL_IDS["backlog"]="" +_ILC_LABEL_IDS["in-progress"]="" +_ILC_LABEL_IDS["blocked"]="" -# _ilc_ensure_label_id VARNAME LABEL_NAME [COLOR] -# Generic: looks up label by name, creates if missing, caches in the named var. +# _ilc_ensure_label_id LABEL_NAME [COLOR] +# Looks up label by name, creates if missing, caches in associative array. _ilc_ensure_label_id() { - local varname="$1" name="$2" color="${3:-#e0e0e0}" - local current - eval "current=\"\${${varname}:-}\"" + local name="$1" color="${2:-#e0e0e0}" + local current="${_ILC_LABEL_IDS[$name]:-}" if [ -n "$current" ]; then printf '%s' "$current" return 0 @@ -71,14 +71,14 @@ _ilc_ensure_label_id() { | jq -r '.id // empty' 2>/dev/null || true) fi if [ -n "$label_id" ]; then - eval "${varname}=\"${label_id}\"" + _ILC_LABEL_IDS["$name"]="$label_id" fi printf '%s' "$label_id" } -_ilc_backlog_id() { _ilc_ensure_label_id _ILC_BACKLOG_ID "backlog" "#0075ca"; } -_ilc_in_progress_id() { _ilc_ensure_label_id _ILC_IN_PROGRESS_ID "in-progress" "#1d76db"; } -_ilc_blocked_id() { _ilc_ensure_label_id _ILC_BLOCKED_ID "blocked" "#e11d48"; } +_ilc_backlog_id() { _ilc_ensure_label_id "backlog" "#0075ca"; } +_ilc_in_progress_id() { _ilc_ensure_label_id "in-progress" "#1d76db"; } +_ilc_blocked_id() { _ilc_ensure_label_id "blocked" "#e11d48"; } # --------------------------------------------------------------------------- # issue_claim — assign issue to bot, add "in-progress" label, remove "backlog". diff --git a/lib/mirrors.sh b/lib/mirrors.sh index e6dfba1..1868dba 100644 --- a/lib/mirrors.sh +++ b/lib/mirrors.sh @@ -13,7 +13,15 @@ mirror_push() { local name url for name in $MIRROR_NAMES; do - url=$(eval "echo \"\$MIRROR_$(echo "$name" | tr '[:lower:]' '[:upper:]')\"") || true + # Convert name to uppercase env var name safely (only alphanumeric allowed) + local upper_name + upper_name=$(printf '%s' "$name" | tr '[:lower:]' '[:upper:]') + # Validate: only allow alphanumeric + underscore in var name + if [[ ! "$upper_name" =~ ^[A-Z_][A-Z0-9_]*$ ]]; then + continue + fi + # Use indirect expansion safely (no eval) + url="${!upper_name:-}" [ -z "$url" ] && continue # Ensure remote exists with correct URL From 92812ccc34193d316113bfa80a147f4499efcca2 Mon Sep 17 00:00:00 2001 From: dev-bot Date: Tue, 31 Mar 2026 18:17:38 +0000 Subject: [PATCH 2/3] docs: rewrite SKILL.md to focus on external project setup (#64) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Restructures SKILL.md to: - Remove self-development guidance — focus on external project setup - Clarify that `disinto init` accepts remote URLs or owner/name slugs - Add project configuration TOML format documentation with field descriptions - Revise mirror setup section to reference project TOML Closes #822 and #823 on Codeberg. --- _Upstream: codeberg johba/disinto PR #824_ Co-authored-by: johba Reviewed-on: http://forgejo:3000/johba/disinto/pulls/64 Reviewed-by: review-bot Co-authored-by: dev-bot Co-committed-by: dev-bot --- disinto-factory/SKILL.md | 83 ++++++++++++++++++++++++++++++++++------ 1 file changed, 71 insertions(+), 12 deletions(-) diff --git a/disinto-factory/SKILL.md b/disinto-factory/SKILL.md index 8e17508..8c6a672 100644 --- a/disinto-factory/SKILL.md +++ b/disinto-factory/SKILL.md @@ -9,6 +9,8 @@ You are helping the user set up and operate a **disinto autonomous code factory* of bash scripts and Claude CLI that automates the full development lifecycle: picking up issues, implementing via Claude, creating PRs, running CI, reviewing, merging, and mirroring. +This guide shows how to set up the factory to develop an **external project** (e.g., `johba/harb`). + ## First-time setup Walk the user through these steps interactively. Ask questions where marked with [ASK]. @@ -27,20 +29,34 @@ docker --version && git --version && jq --version && curl --version && tmux -V & Any missing tool — help the user install it before continuing. -### 2. Clone and init +### 2. Clone disinto and choose a target project +Clone the disinto factory itself: ```bash git clone https://codeberg.org/johba/disinto.git && cd disinto ``` -[ASK] What repo should the factory develop? Options: -- **Itself** (self-development): `bin/disinto init https://codeberg.org/johba/disinto --yes --repo-root $(pwd)` -- **Another project**: `bin/disinto init --yes` +[ASK] What repository should the factory develop? Provide the **remote repository URL** in one of these formats: +- Full URL: `https://github.com/johba/harb.git` or `https://codeberg.org/johba/harb.git` +- Short slug: `johba/harb` (uses local Forgejo as the primary remote) -Run the init and watch for: -- All bot users created (dev-bot, review-bot, etc.) -- `WOODPECKER_TOKEN` generated and saved -- Stack containers all started +The factory will clone from the remote URL (if provided) or from your local Forgejo, then mirror to the remote. + +Then initialize the factory for that project: +```bash +bin/disinto init johba/harb --yes +# or with full URL: +bin/disinto init https://github.com/johba/harb.git --yes +``` + +The `init` command will: +- Create all bot users (dev-bot, review-bot, etc.) on the local Forgejo +- Generate and save `WOODPECKER_TOKEN` +- Start the stack containers +- Clone the target repo into the agent workspace + +> **Note:** The `--repo-root` flag is optional and only needed if you want to customize +> where the cloned repo lives. By default, it goes under `/home/agent/repos/`. ### 3. Post-init verification @@ -70,7 +86,48 @@ docker exec disinto-agents-1 chown -R agent:agent /home/agent/repos docker exec -u agent disinto-agents-1 bash -c "source /home/agent/disinto/.env && git clone http://dev-bot:\${FORGE_TOKEN}@forgejo:3000//.git /home/agent/repos/" ``` -### 4. Mirrors (optional) +### 4. Create the project configuration file + +The factory uses a TOML file to configure how it manages your project. Create +`projects/.toml` based on the template format: + +```toml +# projects/harb.toml + +name = "harb" +repo = "johba/harb" +forge_url = "http://localhost:3000" +repo_root = "/home/agent/repos/harb" +primary_branch = "master" + +[ci] +woodpecker_repo_id = 0 +stale_minutes = 60 + +[services] +containers = ["ponder"] + +[monitoring] +check_prs = true +check_dev_agent = true +check_pipeline_stall = true + +# [mirrors] +# github = "git@github.com:johba/harb.git" +# codeberg = "git@codeberg.org:johba/harb.git" +``` + +**Key fields:** +- `name`: Project identifier (used for file names, logs, etc.) +- `repo`: The source repo in `owner/name` format +- `forge_url`: URL of your local Forgejo instance +- `repo_root`: Where the agent clones the repo +- `primary_branch`: Default branch name (e.g., `main` or `master`) +- `woodpecker_repo_id`: Set to `0` initially; auto-populated on first CI run +- `containers`: List of Docker containers the factory should manage +- `mirrors`: Optional external forge URLs for backup/sync + +### 5. Mirrors (optional) [ASK] Should the factory mirror to external forges? If yes, which? - GitHub: need repo URL and SSH key added to GitHub account @@ -88,7 +145,7 @@ ssh -T git@github.com 2>&1; ssh -T git@codeberg.org 2>&1 If SSH host keys are missing: `ssh-keyscan github.com codeberg.org >> ~/.ssh/known_hosts 2>/dev/null` -Edit `projects/.toml` to add mirrors: +Edit `projects/.toml` to uncomment and configure mirrors: ```toml [mirrors] github = "git@github.com:Org/repo.git" @@ -100,7 +157,7 @@ Test with a manual push: source .env && source lib/env.sh && export PROJECT_TOML=projects/.toml && source lib/load-project.sh && source lib/mirrors.sh && mirror_push ``` -### 5. Seed the backlog +### 6. Seed the backlog [ASK] What should the factory work on first? Brainstorm with the user. @@ -128,10 +185,12 @@ Use labels: - `blocked` — parked, not for the factory - No label — tracked but not for autonomous work -### 6. Watch it work +### 7. Watch it work The dev-agent polls every 5 minutes. Trigger manually to see it immediately: ```bash +source .env +export PROJECT_TOML=projects/.toml docker exec -u agent disinto-agents-1 bash -c "cd /home/agent/disinto && bash dev/dev-poll.sh projects/.toml" ``` From b64859a2a54006a3ab0cdf1e497c531e1b94e4d1 Mon Sep 17 00:00:00 2001 From: Agent Date: Tue, 31 Mar 2026 18:10:14 +0000 Subject: [PATCH 3/3] fix: SECURITY: Replace eval usage with safer alternatives (#59) --- lib/env.sh | 22 ++++++++++++++++++++-- lib/issue-lifecycle.sh | 24 ++++++++++++------------ lib/mirrors.sh | 11 ++++++++++- 3 files changed, 42 insertions(+), 15 deletions(-) diff --git a/lib/env.sh b/lib/env.sh index 92eb676..cfaa523 100755 --- a/lib/env.sh +++ b/lib/env.sh @@ -29,8 +29,26 @@ if [ -f "$FACTORY_ROOT/.env.enc" ] && command -v sops &>/dev/null; then set -a _saved_forge_url="${FORGE_URL:-}" _saved_forge_token="${FORGE_TOKEN:-}" - eval "$(sops -d --output-type dotenv "$FACTORY_ROOT/.env.enc" 2>/dev/null)" \ - || echo "Warning: failed to decrypt .env.enc — secrets not loaded" >&2 + # Use temp file + validate dotenv format before sourcing (avoids eval injection) + _tmpenv=$(mktemp) || { echo "Warning: failed to create temp file for .env.enc" >&2; exit 1; } + if sops -d --output-type dotenv "$FACTORY_ROOT/.env.enc" > "$_tmpenv" 2>/dev/null; then + # Validate: non-empty, non-comment lines must match KEY=value pattern + # Filter out blank lines and comments before validation + _validated=$(grep -E '^[A-Za-z_][A-Za-z0-9_]*=' "$_tmpenv" 2>/dev/null || true) + if [ -n "$_validated" ]; then + # Write validated content to a second temp file and source it + _validated_env=$(mktemp) + printf '%s\n' "$_validated" > "$_validated_env" + # shellcheck source=/dev/null + source "$_validated_env" + rm -f "$_validated_env" + else + echo "Warning: .env.enc decryption output failed format validation" >&2 + fi + else + echo "Warning: failed to decrypt .env.enc — secrets not loaded" >&2 + fi + rm -f "$_tmpenv" set +a [ -n "$_saved_forge_url" ] && export FORGE_URL="$_saved_forge_url" [ -n "$_saved_forge_token" ] && export FORGE_TOKEN="$_saved_forge_token" diff --git a/lib/issue-lifecycle.sh b/lib/issue-lifecycle.sh index 19c422d..81586f9 100644 --- a/lib/issue-lifecycle.sh +++ b/lib/issue-lifecycle.sh @@ -45,16 +45,16 @@ _ilc_log() { # Label ID caching — lookup once per name, cache in globals. # Pattern follows ci-helpers.sh (ensure_blocked_label_id). # --------------------------------------------------------------------------- -_ILC_BACKLOG_ID="" -_ILC_IN_PROGRESS_ID="" -_ILC_BLOCKED_ID="" +declare -A _ILC_LABEL_IDS +_ILC_LABEL_IDS["backlog"]="" +_ILC_LABEL_IDS["in-progress"]="" +_ILC_LABEL_IDS["blocked"]="" -# _ilc_ensure_label_id VARNAME LABEL_NAME [COLOR] -# Generic: looks up label by name, creates if missing, caches in the named var. +# _ilc_ensure_label_id LABEL_NAME [COLOR] +# Looks up label by name, creates if missing, caches in associative array. _ilc_ensure_label_id() { - local varname="$1" name="$2" color="${3:-#e0e0e0}" - local current - eval "current=\"\${${varname}:-}\"" + local name="$1" color="${2:-#e0e0e0}" + local current="${_ILC_LABEL_IDS[$name]:-}" if [ -n "$current" ]; then printf '%s' "$current" return 0 @@ -71,14 +71,14 @@ _ilc_ensure_label_id() { | jq -r '.id // empty' 2>/dev/null || true) fi if [ -n "$label_id" ]; then - eval "${varname}=\"${label_id}\"" + _ILC_LABEL_IDS["$name"]="$label_id" fi printf '%s' "$label_id" } -_ilc_backlog_id() { _ilc_ensure_label_id _ILC_BACKLOG_ID "backlog" "#0075ca"; } -_ilc_in_progress_id() { _ilc_ensure_label_id _ILC_IN_PROGRESS_ID "in-progress" "#1d76db"; } -_ilc_blocked_id() { _ilc_ensure_label_id _ILC_BLOCKED_ID "blocked" "#e11d48"; } +_ilc_backlog_id() { _ilc_ensure_label_id "backlog" "#0075ca"; } +_ilc_in_progress_id() { _ilc_ensure_label_id "in-progress" "#1d76db"; } +_ilc_blocked_id() { _ilc_ensure_label_id "blocked" "#e11d48"; } # --------------------------------------------------------------------------- # issue_claim — assign issue to bot, add "in-progress" label, remove "backlog". diff --git a/lib/mirrors.sh b/lib/mirrors.sh index e6dfba1..3ba561d 100644 --- a/lib/mirrors.sh +++ b/lib/mirrors.sh @@ -13,7 +13,16 @@ mirror_push() { local name url for name in $MIRROR_NAMES; do - url=$(eval "echo \"\$MIRROR_$(echo "$name" | tr '[:lower:]' '[:upper:]')\"") || true + # Convert name to uppercase env var name safely (only alphanumeric allowed) + local upper_name + upper_name=$(printf '%s' "$name" | tr '[:lower:]' '[:upper:]') + # Validate: only allow alphanumeric + underscore in var name + if [[ ! "$upper_name" =~ ^[A-Z_][A-Z0-9_]*$ ]]; then + continue + fi + # Use indirect expansion safely (no eval) — MIRROR_ prefix required + local varname="MIRROR_${upper_name}" + url="${!varname:-}" [ -z "$url" ] && continue # Ensure remote exists with correct URL