Files
agents/.gitea/workflows/scripts/install-caller-skills.sh
Felix FaerberandClaude Opus 4.8 e0ef954454 agents: fix ${SECRETS_JSON:-{}} brace bug that silently skipped caller skills
`${VAR:-{}}` appends a stray '}' when VAR is set (bash brace-matching), so the
JSON handed to a caller skill's setup.sh was corrupted and its jq failed with
"Unmatched '}'" — install-caller-skills.sh then caught the non-zero exit and
skipped the skill. Default SECRETS_JSON in two safe steps and pass it as a plain
var. This is why node1-ssh never installed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 18:32:21 +03:00

71 lines
4.1 KiB
Bash

#!/usr/bin/env bash
# Install CALLER-PROVIDED opencode skills — the framework's skill-plugin hook.
#
# The reusable workflow ships a few built-in skills (gitea-api, gitea-admin). A consuming repo can
# add its OWN, repo-specific skills (e.g. a homelab "ssh into the deploy host" skill) without any
# change to this framework: it commits them under `.gitea/agent-skills/<name>/` in its own repo.
# This step discovers them in the checked-out caller workspace and installs the ones allowed for the
# running agent. That keeps deploy-target / infra specifics in the repo they belong to, not here.
#
# Layout the framework expects, per skill, in the CALLER repo:
# .gitea/agent-skills/<name>/
# SKILL.md (required) — the opencode Skill doc; copied verbatim into the skill registry.
# skill.json (required) — {"agents":["senior","lead"]} — which agents may load this skill.
# setup.sh (optional) — runtime setup (e.g. write an SSH alias). Runs ONLY when this agent is
# allowed the skill. Receives $SECRETS_JSON (all inherited secrets, as JSON) and must
# extract what it needs via jq; it must no-op cleanly if its secrets aren't set.
#
# Required env (provided by the workflow step): NAME WORKSPACE SECRETS_JSON
# (SECRETS_JSON = toJSON(secrets); passed so a caller's setup.sh can read repo-specific secrets
# whose names this framework cannot know in advance.)
#
# IMPORTANT — caller-skill secrets read from SECRETS_JSON MUST be single-line. The runner masks a
# secret's value in logs by exact match, but toJSON(secrets) escapes newlines to '\n', so a MULTILINE
# secret (e.g. a raw PEM key) no longer matches the mask and would print in cleartext in the step's
# "expression evaluated to …" log line. Store multiline values base64-encoded (single-line) and
# decode them inside setup.sh. Single-line values mask correctly.
set -eu
# Safe default for SECRETS_JSON (see note at the setup.sh call below re: the ${x:-{}} brace bug).
SJ="${SECRETS_JSON:-}"; [ -n "$SJ" ] || SJ='{}'
DIR="${WORKSPACE:-$GITHUB_WORKSPACE}/.gitea/agent-skills"
CFG="$HOME/.config/opencode/opencode.json"
[ -d "$DIR" ] || { echo "no caller skills (.gitea/agent-skills/ absent) — nothing to install"; exit 0; }
allow='{}' # skills to flip to "allow" in permission.skill for THIS agent
for skill_dir in "$DIR"/*/; do
[ -d "$skill_dir" ] || continue
name=$(basename "$skill_dir")
md="$skill_dir/SKILL.md"; meta="$skill_dir/skill.json"
if [ ! -f "$md" ] || [ ! -f "$meta" ]; then
echo "caller skill '$name': missing SKILL.md or skill.json — skipping"; continue
fi
# Is this agent allowed the skill?
if ! jq -e --arg n "$NAME" '(.agents // []) | index($n)' "$meta" >/dev/null 2>&1; then
echo "caller skill '$name': not allowed for @$NAME — skipping"; continue
fi
# Install the doc.
dest="$HOME/.config/opencode/skills/$name"
mkdir -p "$dest" && chmod 700 "$dest"
cp "$md" "$dest/SKILL.md"
chmod -R o=rX "$dest"
# Optional runtime setup, with all inherited secrets available as JSON (never printed here).
# NOTE: pass SECRETS_JSON via a plain variable — do NOT inline ${SECRETS_JSON:-{}} here or in
# setup.sh: bash brace-matching appends a stray '}' when the var is set, corrupting the JSON so
# the skill's `jq` fails ("Unmatched '}'") and the skill is silently skipped.
if [ -f "$skill_dir/setup.sh" ]; then
echo "caller skill '$name': running setup.sh for @$NAME"
SECRETS_JSON="$SJ" NAME="$NAME" WORKSPACE="${WORKSPACE:-$GITHUB_WORKSPACE}" \
bash "$skill_dir/setup.sh" || { echo "caller skill '$name': setup.sh failed — skipping this skill"; continue; }
fi
allow=$(jq -nc --argjson a "$allow" --arg n "$name" '$a + {($n):"allow"}')
echo "caller skill '$name': installed + allowed for @$NAME"
done
# Merge the allowed caller skills into the permission allow-list opencode already wrote.
if [ "$allow" != '{}' ] && [ -f "$CFG" ]; then
tmp=$(mktemp)
jq --argjson add "$allow" '.permission.skill = ((.permission.skill // {}) + $add)' "$CFG" > "$tmp" && mv "$tmp" "$CFG"
echo "permission.skill updated with caller skills: $(jq -c '.permission.skill' "$CFG")"
fi