Files
gstack/bin/gstack-harness-ledger.sh
T
Garry Tanandcapy-ai[bot] 44f19d0106 v1.91.290.0 feat(harness): gstack registers its skills in whatever agent harness it is installed into (GSTA-135) (#3526)
* wip: gstack-harness registrar (GSTA-135)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* wip: harness registration in setup, uninstall, tests

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* wip: harness hosts experimental, troubleshooting codes, kit

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* docs: agent harness registration (GSTA-135)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix: harness hosts run in place from the checkout in --status

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* test: bun-free ledger reader

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* docs: CHANGELOG for harness registration (Unreleased)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(harness): keep edits beside themselves, never follow symlinks, cut only gstack's own context block

Review blockers on #3526: uninstall deleted the edit backups under the state root;
unregister followed a symlinked folder below the destination; a lone or edited
context marker made the cut eat user lines; a re-register adding --context left an
unrecorded block. Also: ledger written before the files, --refresh-registered no
longer re-runs harness hosts through setup (gstack-harness refresh keeps their mode),
bash goes through runBashSync, and setup tests follow openclaw/hermes becoming installs.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(harness): a ledger with no destination row touches nothing (no hang)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* docs: harness registration in README, install and hosts (document-release audit)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

---------

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
2026-10-28 04:33:53 +00:00

90 lines
4.4 KiB
Bash

#!/usr/bin/env bash
# gstack-harness-ledger.sh — sourced; removes what gstack-harness registered,
# from its ledger, without bun (bin/gstack-uninstall runs it before it removes
# the checkout and the state root). Ledger format: lib/harness-register.ts.
# Callers set GSTACK_STATE_ROOT first (bin/gstack-state-root.sh).
#
# gstack_harness_unregister_ledger <ledger> remove one registration
# gstack_harness_unregister_all remove every registration under
# $GSTACK_STATE_ROOT/harness
#
# A file whose bytes still match the ledger is removed; an edited one is renamed
# in place to <file>.edited-<stamp>, a name no harness loads, so it survives the
# uninstall of the state root and the checkout. Only paths below the recorded
# destination, reached through no symlink, are touched. A context block is cut
# only when its file holds exactly one whole-line begin/end marker pair whose
# lines hash to the ledger's block hash; otherwise the file is left as it is
# (the file is removed only when nothing else is left in it). Folders gstack
# created are removed only when empty. A QM pack is never touched here: detaching it from
# the org is `gstack-harness unregister qm --remote`.
_gstack_harness_sha() {
if command -v sha256sum >/dev/null 2>&1; then sha256sum ${1:+"$1"} | cut -d' ' -f1
else shasum -a 256 ${1:+"$1"} | cut -d' ' -f1; fi
}
# True when $1 is below $2 and no path from $2 down to $1 is a symlink.
_gstack_harness_owned() {
local p="$1" dest="$2"
[ -n "$dest" ] || return 1
case "$p" in "$dest"/*) ;; *) return 1 ;; esac
while [ "$p" != "$dest" ]; do [ -L "$p" ] && return 1; p="$(dirname "$p")"; done
[ ! -L "$dest" ]
}
_gstack_harness_cut_block() {
local file="$1" want="$2" span s e tmp
local begin='<!-- gstack:begin harness-routing -->' end='<!-- gstack:end harness-routing -->'
{ [ -f "$file" ] && [ ! -L "$file" ]; } || return 0
span="$(awk -v B="$begin" -v E="$end" '$0 == B { nb++; s = NR } $0 == E { ne++; e = NR } END { if (nb == 1 && ne == 1 && e > s) print s, e; else if (nb || ne) print "bad" }' "$file")"
[ -n "$span" ] || return 0
s="${span% *}"; e="${span#* }"
if [ "$span" = bad ] || [ "$(awk -v s="$s" -v e="$e" 'NR >= s && NR <= e' "$file" | _gstack_harness_sha)" != "$want" ]; then
echo " left $file as it is: its gstack block was edited; delete the lines from $begin to $end yourself"
return 0
fi
tmp="$file.gstack-cut.$$"
awk -v s="$s" -v e="$e" 'NR < s || NR > e' "$file" > "$tmp" || { rm -f "$tmp"; return 1; }
if [ -z "$(tr -d '[:space:]' < "$tmp")" ]; then rm -f "$file"; else cat "$tmp" > "$file"; fi
rm -f "$tmp"
}
gstack_harness_unregister_ledger() {
local ledger="$1" stamp kind a b target="" dest="" pack=""
: "${GSTACK_STATE_ROOT:?gstack-harness-ledger: GSTACK_STATE_ROOT is not set}"
[ -f "$ledger" ] || return 0
stamp="$(date -u +%Y-%m-%dT%H-%M-%SZ)"
while IFS=$'\t' read -r kind a b; do
case "$kind" in
meta) case "$a" in target) target="$b" ;; dest) dest="$b" ;; qm_pack) pack="$b" ;; esac ;;
file)
{ [ -f "$b" ] && _gstack_harness_owned "$b" "$dest"; } || continue
if [ "$(_gstack_harness_sha "$b")" = "$a" ]; then rm -f "$b"
else mv "$b" "$b.edited-$stamp" && echo " kept your edit: $b.edited-$stamp"; fi ;;
block) _gstack_harness_cut_block "$b" "$a" ;;
esac
done < "$ledger"
while IFS=$'\t' read -r kind a b; do
[ "$kind" = dir ] && [ -d "$b" ] && [ ! -L "$b" ] || continue
case "$b" in "$dest"/*) _gstack_harness_owned "$b" "$dest" || continue ;; esac
rmdir "$b" 2>/dev/null
done < "$ledger"
case "$target" in
openclaw|hermes)
if command -v gstack_install_registry_remove >/dev/null 2>&1; then gstack_install_registry_remove "$target" "$dest" || true; fi ;;
qm) [ -z "$pack" ] || echo " QM: dropped the local record of pack $pack; the pack stays in your QM org until an org admin archives it (DELETE $dest/v1/admin/skill-packs/$pack)" ;;
esac
rm -f "$ledger"
}
gstack_harness_unregister_all() {
local root="${GSTACK_STATE_ROOT:?gstack-harness-ledger: GSTACK_STATE_ROOT is not set}" l
for l in "$root"/harness/*.ledger; do
[ -f "$l" ] || continue
gstack_harness_unregister_ledger "$l" && echo "harness: $(basename "$l" .ledger)"
done
}
_gstack_harness_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
[ -f "$_gstack_harness_dir/gstack-install-registry.sh" ] && . "$_gstack_harness_dir/gstack-install-registry.sh"