#!/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//` 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// # 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 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). if [ -f "$skill_dir/setup.sh" ]; then echo "caller skill '$name': running setup.sh for @$NAME" SECRETS_JSON="${SECRETS_JSON:-{}}" 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