multica deployment
This commit is contained in:
1 parent
63c5f7eed7
commit
39e2b87941
26 files changed
+2746
-394
No files matched your search
@@ -0,0 +1,128 @@
|
||||
#!/bin/bash
|
||||
# kanban-gateway: SSH forced-command wrapper for n8n -> Hermes kanban.
|
||||
#
|
||||
# Install on the Mac as the user that owns ~/.hermes, e.g. ~/bin/kanban-gateway (chmod 755).
|
||||
# Wire it up in ~/.ssh/authorized_keys with a forced command (see the line in the chat).
|
||||
#
|
||||
# Verbs n8n may call (sent as the SSH command string):
|
||||
# create <base64 of path relative to the vault, e.g. raw/clippings/foo.md>
|
||||
# list [status] status in: triage todo ready running blocked review done archived
|
||||
# show <task id> id like t_ab12cd34
|
||||
#
|
||||
# Nothing from the SSH command is ever passed through a shell or eval; the path
|
||||
# travels base64-encoded so spaces/quotes/$()/newlines in filenames are inert.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# ---- EDIT THESE ------------------------------------------------------------
|
||||
HERMES_BIN="${HERMES_BIN:-$HOME/.local/bin/hermes}" # absolute path; non-interactive SSH has a minimal PATH
|
||||
VAULT_ROOT="${VAULT_ROOT:-$HOME/Documents/wiki-llm}" # absolute path to the vault root
|
||||
RAW_DIR="raw" # inbox folder inside the vault
|
||||
LIBRARIAN_PROFILE="${LIBRARIAN_PROFILE:-librarian}" # assignee profile name
|
||||
SKILL_NAME="${SKILL_NAME:-llm-wiki}" # skill installed on that profile ('' to skip pinning)
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
export PATH="/usr/bin:/bin:/usr/sbin:/sbin:/opt/homebrew/bin:/usr/local/bin:$HOME/.local/bin"
|
||||
|
||||
fail() { echo "kanban-gateway: $1" >&2; exit "${2:-2}"; }
|
||||
|
||||
cmd_line="${SSH_ORIGINAL_COMMAND:-}"
|
||||
[[ "$cmd_line" != *$'\n'* ]] || fail "newline in command"
|
||||
|
||||
# n8n's SSH node prefixes the command with "cd <dir> ; " (or "&&") when its Working Directory
|
||||
# field is set. The directory is never used or executed here; only that exact prefix shape is
|
||||
# stripped, and whatever remains must still be one of the allowed verbs below.
|
||||
cd_re='^cd[[:space:]]+("[^"]*"|'"'"'[^'"'"']*'"'"'|[^[:space:];&|`$()<>\\]+)[[:space:]]*(;|&&)[[:space:]]*'
|
||||
if [[ "$cmd_line" =~ $cd_re ]]; then
|
||||
cmd_line="${cmd_line#"${BASH_REMATCH[0]}"}"
|
||||
fi
|
||||
[[ -n "$cmd_line" ]] || fail "no command supplied"
|
||||
|
||||
# Split only on spaces; the verb and a single argument.
|
||||
verb="${cmd_line%% *}"
|
||||
arg=""
|
||||
if [[ "$cmd_line" == *" "* ]]; then arg="${cmd_line#* }"; fi
|
||||
|
||||
decode_b64() {
|
||||
# macOS older = -D, newer/GNU = -d
|
||||
printf '%s' "$1" | { base64 -d 2>/dev/null || base64 -D; }
|
||||
}
|
||||
|
||||
case "$verb" in
|
||||
create)
|
||||
[[ "$arg" =~ ^[A-Za-z0-9+/=]+$ ]] || fail "create expects base64 argument"
|
||||
rel="$(decode_b64 "$arg")" || fail "bad base64"
|
||||
[[ -n "$rel" ]] || fail "empty path"
|
||||
# Reject control characters (incl. newlines), absolute paths, and traversal.
|
||||
if printf '%s' "$rel" | LC_ALL=C grep -q '[[:cntrl:]]'; then fail "control characters in path"; fi
|
||||
[[ "$rel" != /* ]] || fail "absolute path rejected"
|
||||
case "/$rel/" in */../*) fail "path traversal rejected" ;; esac
|
||||
[[ "$rel" == "$RAW_DIR"/* ]] || fail "path must be under $RAW_DIR/"
|
||||
|
||||
vault_real="$(cd "$VAULT_ROOT" && pwd -P)" || fail "VAULT_ROOT not found"
|
||||
abs="$vault_real/$rel"
|
||||
[[ -f "$abs" ]] || fail "file not found: $rel" 3
|
||||
# Resolve symlinks in the directory part and confirm it is still inside raw/.
|
||||
dir_real="$(cd "$(dirname "$abs")" && pwd -P)" || fail "cannot resolve directory"
|
||||
case "$dir_real/" in
|
||||
"$vault_real/$RAW_DIR/"*) ;;
|
||||
*) fail "resolved path escapes $RAW_DIR/" ;;
|
||||
esac
|
||||
|
||||
# Folder-scoped rulesets live IN the vault as AGENTS.md files. Collect every AGENTS.md from the
|
||||
# vault root down to the file's folder (shallow -> deep) and name them on the card, so the
|
||||
# agent reads them deterministically instead of relying on lazy discovery.
|
||||
rules=()
|
||||
if [[ -f "$vault_real/AGENTS.md" && ! -L "$vault_real/AGENTS.md" ]]; then rules+=("AGENTS.md"); fi
|
||||
cur=""
|
||||
IFS='/' read -r -a parts <<< "$(dirname "$rel")"
|
||||
for part in "${parts[@]}"; do
|
||||
cur="${cur:+$cur/}$part"
|
||||
if [[ -f "$vault_real/$cur/AGENTS.md" && ! -L "$vault_real/$cur/AGENTS.md" ]]; then
|
||||
rules+=("$cur/AGENTS.md")
|
||||
fi
|
||||
done
|
||||
if (( ${#rules[@]} > 0 )); then
|
||||
rules_text="Rulesets for this file's folder. Read ALL of them before doing anything else, in this order (later ones are more specific and take precedence):"
|
||||
for r in "${rules[@]}"; do rules_text+=$'\n'"- $r"; done
|
||||
else
|
||||
rules_text="No AGENTS.md rulesets found for this folder; follow the llm-wiki skill defaults."
|
||||
fi
|
||||
|
||||
title="Catalog: $(basename "$rel")"
|
||||
body="Catalog this new raw file per the llm-wiki skill.
|
||||
Relative path: $rel
|
||||
Absolute path: $abs
|
||||
|
||||
$rules_text"
|
||||
|
||||
args=( kanban create "$title"
|
||||
--body "$body"
|
||||
--assignee "$LIBRARIAN_PROFILE"
|
||||
--workspace "dir:$vault_real"
|
||||
--idempotency-key "$rel"
|
||||
--json )
|
||||
if [[ -n "$SKILL_NAME" ]]; then args+=( --skill "$SKILL_NAME" ); fi
|
||||
exec "$HERMES_BIN" "${args[@]}"
|
||||
;;
|
||||
|
||||
list)
|
||||
if [[ -z "$arg" ]]; then
|
||||
exec "$HERMES_BIN" kanban list --json
|
||||
fi
|
||||
case "$arg" in
|
||||
triage|todo|ready|running|blocked|review|done|archived)
|
||||
exec "$HERMES_BIN" kanban list --status "$arg" --json ;;
|
||||
*) fail "invalid status" ;;
|
||||
esac
|
||||
;;
|
||||
|
||||
show)
|
||||
[[ "$arg" =~ ^t_[A-Za-z0-9]+$ ]] || fail "invalid task id"
|
||||
exec "$HERMES_BIN" kanban show "$arg" --json
|
||||
;;
|
||||
|
||||
*)
|
||||
fail "verb not allowed: $verb"
|
||||
;;
|
||||
esac
|
||||
Reference in new issue
Block a user