diff --git a/CHANGELOG.md b/CHANGELOG.md index 96ec111..33b760e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## [v0.6.3] - 2026-05-21 + +### Features + +- `repo clone [DIR]` — structured clone command, wraps `gh repo clone` (inherits your `gh auth` for private repos). Optional `--depth N` for shallow clones. +- `repo workspace [-r REF] [--depth N]` — the higher-level "give me a sandbox" command for agents. Creates a fresh `/tmp/fledge-gh---XXXXXX/` dir, clones the repo there (optionally shallow + at a specific ref), and prints the absolute path on stdout. The `-r REF` is wired through to git's `--branch` so shallow + ref work together. +- `repo workspace-clean ` — `rm -rf`s a workspace dir. Refuses to remove anything that doesn't live under `/tmp/fledge-gh-`, so a bad agent path can't wipe arbitrary files. + +These give agents driven from non-interactive bridges (Discord/Telegram) a structured path to clone a repo for edit work, instead of falling back to `shell-exec git clone ...` which is auto-denied by the Tier 1.5 safety gate in merlin's `--yes` mode. + ## [v0.6.2] - 2026-05-21 ### Fixes diff --git a/bin/fledge-github-repo b/bin/fledge-github-repo index 5d261a5..c876562 100755 --- a/bin/fledge-github-repo +++ b/bin/fledge-github-repo @@ -14,6 +14,8 @@ REPO="" REF="" PATH_ARG="" JSON=false +CLONE_DIR="" +DEPTH="" if [ $# -gt 0 ] && [ "${1:0:1}" != "-" ]; then case "$1" in @@ -38,6 +40,36 @@ if [ $# -gt 0 ] && [ "${1:0:1}" != "-" ]; then shift fi ;; + clone) + ACTION="clone" + shift + # Two positionals: OWNER/NAME and an optional target DIR. + # `gh repo clone` accepts both; we pass them through verbatim. + if [ $# -gt 0 ] && [ "${1:0:1}" != "-" ] && [[ "$1" == */* ]]; then + REPO="$1" + shift + fi + if [ $# -gt 0 ] && [ "${1:0:1}" != "-" ]; then + CLONE_DIR="$1" + shift + fi + ;; + workspace) + ACTION="workspace" + shift + if [ $# -gt 0 ] && [ "${1:0:1}" != "-" ] && [[ "$1" == */* ]]; then + REPO="$1" + shift + fi + ;; + workspace-clean) + ACTION="workspace-clean" + shift + if [ $# -gt 0 ] && [ "${1:0:1}" != "-" ]; then + CLONE_DIR="$1" + shift + fi + ;; esac fi @@ -45,33 +77,51 @@ while [ $# -gt 0 ]; do case "$1" in -R|--repo) REPO="${2:-}"; shift 2;; -r|--ref) REF="${2:-}"; shift 2;; + --depth) DEPTH="${2:-}"; shift 2;; --json) JSON=true; shift;; -h|--help) cat <<'EOF' fledge github repo — read repo metadata and file contents via the gh CLI USAGE: - fledge github repo [view] [OWNER/NAME] View repo metadata (default) - fledge github repo file Read a file OR list a directory + fledge github repo [view] [OWNER/NAME] View repo metadata (default) + fledge github repo file Read a file OR list a directory + fledge github repo clone [DIR] Clone a repo via `gh repo clone` + fledge github repo workspace Clone into a fresh /tmp dir, print path + fledge github repo workspace-clean rm -rf a workspace dir (safety-checked) OPTIONS: -R, --repo Target repo (default: current working repo). - view also accepts OWNER/NAME as a positional. - -r, --ref Branch / tag / SHA (file only; default: repo default) + view + clone + workspace also accept it as a positional. + -r, --ref Branch / tag / SHA (file: lookup; workspace: checkout) + --depth Shallow-clone depth (clone + workspace) --json Output JSON The `file` action returns the file's decoded contents when PATH is a file, or a tab-separated listing (nametypesize) when PATH is a directory — same command, agent can browse and read with one tool. +`clone` wraps `gh repo clone OWNER/NAME [DIR]` directly. + +`workspace` is the higher-level "give me a sandbox" command for +agents: creates a fresh `/tmp/fledge-gh---XXXXXX/` +directory, clones the repo there (optionally shallow + at a specific +ref), and prints the absolute path on stdout. Cleanup via +`workspace-clean ` which `rm -rf`s after sanity-checking the +path is under `/tmp/fledge-gh-`. + EXAMPLES: fledge github repo fledge github repo view CorvidLabs/merlin fledge github repo view -R CorvidLabs/merlin --json fledge github repo file CHANGELOG.md fledge github repo file Sources -R CorvidLabs/corvid-verify - fledge github repo file README.md -R CorvidLabs/fledge-plugin-github fledge github repo file Cargo.toml -r v0.3.0 + fledge github repo clone CorvidLabs/corvid-verify + fledge github repo clone CorvidLabs/corvid-verify /tmp/cv --depth 1 + fledge github repo workspace CorvidLabs/corvid-verify + fledge github repo workspace CorvidLabs/corvid-verify -r feat/jwks-endpoint --depth 1 + fledge github repo workspace-clean /tmp/fledge-gh-CorvidLabs-corvid-verify-abc123 EOF exit 0 ;; @@ -87,6 +137,110 @@ if ! command -v gh >/dev/null 2>&1; then exit 127 fi +if [ "$ACTION" = "clone" ]; then + if [ -z "$REPO" ]; then + echo "fledge github repo clone: missing OWNER/NAME. Try: fledge github repo clone CorvidLabs/merlin" >&2 + exit 64 + fi + ARGS=("$REPO") + [ -n "$CLONE_DIR" ] && ARGS+=("$CLONE_DIR") + if [ -n "$DEPTH" ]; then + # `gh repo clone` forwards anything after `--` to git, so depth + # has to land there. Position matters: ARGS first, then `--`, + # then git-specific options. + ARGS+=(-- --depth "$DEPTH") + fi + gh repo clone "${ARGS[@]}" + exit 0 +fi + +if [ "$ACTION" = "workspace" ]; then + if [ -z "$REPO" ]; then + echo "fledge github repo workspace: missing OWNER/NAME. Try: fledge github repo workspace CorvidLabs/merlin" >&2 + exit 64 + fi + + # Sanitize OWNER/NAME for use in a path: replace anything that isn't + # alnum/-/./_ with `_`. That keeps the resulting dir name human- + # readable (so the agent's logs make sense) and avoids the very real + # risk of an attacker-controlled repo name escaping the /tmp prefix + # via something cute like `../../etc/passwd`. + OWNER_NAME_SAFE="$(printf '%s' "$REPO" | tr '/' '-' | tr -c 'A-Za-z0-9._-' '_')" + + # The plain repo name (post-slash) becomes the inner clone dir, so + # the agent can `cd $WORKSPACE` and end up at the repo root, matching + # everyone's existing intuition about `git clone owner/repo`. + NAME_ONLY="${REPO#*/}" + NAME_ONLY_SAFE="$(printf '%s' "$NAME_ONLY" | tr -c 'A-Za-z0-9._-' '_')" + + # mktemp -d gives us a unique parent. The clone target is one level + # deeper so cleanup of the parent removes both the dir and any junk + # gh / git leave behind. + PARENT="$(mktemp -d "/tmp/fledge-gh-${OWNER_NAME_SAFE}-XXXXXX")" + TARGET="${PARENT}/${NAME_ONLY_SAFE}" + + # Build the gh repo clone arg list. Everything after `--` is passed + # to git itself. We put --branch / --depth there together so a + # shallow clone targets the requested ref directly — without that, + # `--depth 1` + post-clone checkout fails because the shallow fetch + # only contains the default branch's tip. + CLONE_ARGS=("$REPO" "$TARGET") + GIT_ARGS=() + [ -n "$REF" ] && GIT_ARGS+=(--branch "$REF") + [ -n "$DEPTH" ] && GIT_ARGS+=(--depth "$DEPTH") + if [ "${#GIT_ARGS[@]}" -gt 0 ]; then + CLONE_ARGS+=(-- "${GIT_ARGS[@]}") + fi + + # Clone with stderr passed through so the user/agent sees progress + # and any auth errors. Stdout is reserved for the final path so a + # JSON-parsing caller doesn't get progress noise mixed in. + if ! gh repo clone "${CLONE_ARGS[@]}" >&2; then + # Best-effort cleanup of the empty parent; failure is fine. + rmdir "$PARENT" 2>/dev/null || true + echo "fledge github repo workspace: gh repo clone failed" >&2 + exit 1 + fi + + if [ "$JSON" = "true" ]; then + printf '{"path":"%s","repo":"%s","ref":"%s"}\n' "$TARGET" "$REPO" "${REF:-}" + else + printf '%s\n' "$TARGET" + fi + exit 0 +fi + +if [ "$ACTION" = "workspace-clean" ]; then + if [ -z "$CLONE_DIR" ]; then + echo "fledge github repo workspace-clean: missing DIR. Try: fledge github repo workspace-clean /tmp/fledge-gh-XXXX" >&2 + exit 64 + fi + # Safety: only delete dirs that look like fledge-gh workspaces. A + # bug in the agent's reasoning shouldn't be able to wipe arbitrary + # paths via this tool. The prefix check is intentionally strict — + # symlinks and `..` segments fail it cleanly. + case "$CLONE_DIR" in + /tmp/fledge-gh-*) ;; + *) + echo "fledge github repo workspace-clean: refusing to remove '$CLONE_DIR' (must start with /tmp/fledge-gh-)" >&2 + exit 64 + ;; + esac + if [ ! -d "$CLONE_DIR" ]; then + echo "fledge github repo workspace-clean: no such directory: $CLONE_DIR" >&2 + exit 1 + fi + # If the caller passed the inner clone path, we still want to remove + # the parent so we don't leave an empty fledge-gh-* shell behind. + # Walk up only when CLONE_DIR's parent itself matches our prefix. + PARENT_OF="$(dirname "$CLONE_DIR")" + case "$PARENT_OF" in + /tmp/fledge-gh-*) rm -rf -- "$PARENT_OF";; + *) rm -rf -- "$CLONE_DIR";; + esac + exit 0 +fi + if [ "$ACTION" = "view" ]; then # ${ARRAY[@]+...} expansion: with `set -u`, "${EMPTY[@]}" errors on # bash < 4.4. Use the +-form so the expansion only happens when the diff --git a/plugin.toml b/plugin.toml index 773e359..a0bc7d9 100644 --- a/plugin.toml +++ b/plugin.toml @@ -1,14 +1,14 @@ [plugin] name = "fledge-plugin-github" -version = "0.6.2" +version = "0.6.3" description = "GitHub commands for fledge — list/view/create/comment/review/merge PRs, list/view/create/comment/close issues, read repo files, view CI checks, and poll for daemon events via the gh CLI" author = "0xLeif" license = "MIT" [[commands]] name = "github" -description = "GitHub commands via the gh CLI. All subcommands accept -R OWNER/NAME to target any repo (the agent's working dir is otherwise used). Subcommands: prs (list/view [--diff --comments]/create/comment -b BODY/review --approve|--request-changes|--comment-review -b BODY/merge /close /reopen ), issues (list/view [--comments]/create/comment -b BODY/close /reopen ), repo (view / file [--ref REF]), checks (CI status), poll (daemon event polling)." +description = "GitHub commands via the gh CLI. All subcommands accept -R OWNER/NAME to target any repo (the agent's working dir is otherwise used). Subcommands: prs (list/view [--diff --comments]/create/comment -b BODY/review --approve|--request-changes|--comment-review -b BODY/merge /close /reopen ), issues (list/view [--comments]/create/comment -b BODY/close /reopen ), repo (view OWNER/NAME / file [--ref REF] (handles files and dirs) / clone OWNER/NAME [DIR] / workspace OWNER/NAME [--ref REF] [--depth N] (clones into /tmp/fledge-gh-* and prints the path — preferred for agents needing a local edit sandbox) / workspace-clean DIR), checks (CI status), poll (daemon event polling)." binary = "bin/fledge-github" args = [ - { name = "args", type = "string", required = true, description = "Subcommand and options. Pass -R OWNER/NAME to target any repo without changing directory. Examples: 'prs view 5 -R CorvidLabs/corvid-verify --diff', 'prs comment 208 -b \"LGTM\"', 'issues create -R CorvidLabs/merlin -t \"Bug\" -b \"...\"', 'repo file CHANGELOG.md -R CorvidLabs/merlin'" }, + { name = "args", type = "string", required = true, description = "Subcommand and options. Pass -R OWNER/NAME for cross-repo lookups. For agent edit sandboxes use 'repo workspace OWNER/NAME [-r BRANCH] [--depth 1]' — it returns a /tmp path; clean up with 'repo workspace-clean '. Examples: 'prs view 5 -R CorvidLabs/corvid-verify --diff', 'repo file Sources -R CorvidLabs/corvid-verify --ref feat/x', 'repo workspace CorvidLabs/corvid-verify -r feat/jwks-endpoint --depth 1'" }, ]