@yunuseon/kayra 0.11.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yunuseon/kayra",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
4
4
  "description": "A self-hosted app platform and the CLI that runs it: Traefik, Authelia, a registry, rootless BuildKit and a small deployer per server, set up with OpenTofu and Ansible.",
5
5
  "license": "MIT",
6
6
  "author": "Yunus Öztürk",
@@ -0,0 +1,84 @@
1
+ #!/usr/bin/env bash
2
+ # =============================================================================
3
+ # guard.sh — Claude Code's PreToolUse hook on Bash, in kayra and every fleet
4
+ # (.claude/settings.json): an AI agent may build, test and read freely, but a
5
+ # command that changes something real waits for the operator to approve it,
6
+ # in that moment.
7
+ #
8
+ # Reads the hook's JSON on stdin; prints a permissionDecision "ask" with the
9
+ # reason for:
10
+ # - kayra commands that change a server (up, provision, setup, deploy,
11
+ # apps, adopt, down, app purge|deploy|rollback, job) unless --dry-run or
12
+ # the server is a multipass one (its server.yml under the project)
13
+ # - ansible / ansible-playbook against an inventory, ansible-vault reading
14
+ # or writing a vault
15
+ # - merging a PR, API writes (gh api / tea api with PUT, PATCH, DELETE),
16
+ # deleting or editing a repository, publishing a release or a package
17
+ # - git push of main or a tag, a force push, deleting a remote branch
18
+ # - tofu / terraform apply, destroy, import, state changes
19
+ # - destructive commands over SSH (rm, docker rm/stop/prune/down, systemctl…)
20
+ # Anything else: no output, the normal permission flow.
21
+ #
22
+ # It reads the command's text, so it stops mistakes and instructions smuggled
23
+ # in through files or web pages, not a determined adversary: a script it does
24
+ # not read runs whatever it contains. Cases: ci/tests/guard/run.sh.
25
+ # =============================================================================
26
+ set -uo pipefail
27
+
28
+ command="$(jq -r '.tool_input.command // empty' 2>/dev/null)"
29
+ [ -n "$command" ] || exit 0
30
+ project="${CLAUDE_PROJECT_DIR:-$PWD}"
31
+
32
+ ask() {
33
+ jq -n --arg reason "$1" \
34
+ '{hookSpecificOutput: {hookEventName: "PreToolUse", permissionDecision: "ask", permissionDecisionReason: ("guard: " + $reason)}}'
35
+ exit 0
36
+ }
37
+ has() { grep -qE -- "$1" <<< "$command"; }
38
+
39
+ # ── kayra changing a server ─────────────────────────────────────────────────
40
+ kayra='(^|[;&|(` ]|/)(kayra|kayra\.js|main\.ts|main\.js)'
41
+ verbs='(up|provision|setup|deploy|apps|adopt|down|hosts|job|app (purge|deploy|rollback))'
42
+ if has "${kayra} +(--[a-z-]+ +)*${verbs}( |$)" && ! has '--dry-run'; then
43
+ # The server: the word after the verb (after the app subcommand for app …).
44
+ server="$(grep -oE -- "${kayra} +(--[a-z-]+ +)*${verbs} +[a-z0-9][a-z0-9-]*" <<< "$command" | head -1 | awk '{print $NF}')"
45
+ target=""
46
+ [ -z "$server" ] || [ ! -f "${project}/servers/${server}/server.yml" ] \
47
+ || target="$(sed -n 's/^target: *\([a-z]*\).*/\1/p' "${project}/servers/${server}/server.yml")"
48
+ [ "$target" = multipass ] || ask "kayra changes ${server:-a server}${target:+ (target ${target})}"
49
+ fi
50
+
51
+ # ── Ansible and the vaults ──────────────────────────────────────────────────
52
+ has '(^|[;&|( ])ansible-vault +(decrypt|view|edit|encrypt|encrypt_string|rekey|create)( |$)' \
53
+ && ask "ansible-vault opens or writes a vault"
54
+ has '(^|[;&|( ])ansible(-playbook)? .*(-i|--inventory)[ =]' && ask "ansible runs against an inventory"
55
+
56
+ # ── The forges ──────────────────────────────────────────────────────────────
57
+ has '(^|[;&|( ])(gh|tea) +(pr|pulls) +merge( |$)' && ask "a pull request is merged"
58
+ has '(^|[;&|( ])(gh|tea) +api .*(-X|--method)[ =]*(PUT|PATCH|DELETE)' && ask "a forge API write"
59
+ has '(^|[;&|( ])gh +repo +(delete|edit|rename|archive)( |$)' && ask "a repository is changed or deleted"
60
+ has '(^|[;&|( ])gh +release +(create|delete|edit|upload)( |$)' && ask "a release is published or changed"
61
+ has '(^|[;&|( ])npm +publish( |$)' && ! has 'npm +publish.*--dry-run' && ask "a package is published"
62
+
63
+ # ── git push of what others build on ────────────────────────────────────────
64
+ if has '(^|[;&|( ])git +push( |$)'; then
65
+ has 'git +push.*( |:)(main|master)( |$)' && ask "main is pushed"
66
+ has 'git +push.*( --tags| --follow-tags| refs/tags/| v[0-9])' && ask "a tag is pushed (a release)"
67
+ has 'git +push.*( -f( |$)| --force| \+)' && ask "a force push"
68
+ # A push that names no branch pushes the current one: main, if that is where we are.
69
+ case "$(git branch --show-current 2>/dev/null)" in
70
+ main|master) has 'git +push( +-[a-zA-Z-]+)*( +origin)?( *$|[;&|])' && ask "main is pushed (the current branch)" ;;
71
+ esac
72
+ has 'git +push.*( --delete| -d( |$)| :[^ ])' && ask "a remote branch or tag is deleted"
73
+ fi
74
+
75
+ # ── OpenTofu / Terraform ────────────────────────────────────────────────────
76
+ has '(^|[;&|( ])(tofu|terraform) +(-chdir=[^ ]+ +)?(apply|destroy|import|state +(rm|mv|push|replace-provider)|taint|untaint|force-unlock)( |$)' \
77
+ && ask "OpenTofu changes infrastructure or state"
78
+
79
+ # ── Destructive commands on a server ────────────────────────────────────────
80
+ if has '(^|[;&|( ])(kayra +ssh|ssh) '; then
81
+ has '(rm +-|docker +(volume|image|system|container|network) +(rm|prune)|docker +(rm|stop|kill|restart)( |$)|compose +(down|stop|rm|restart|kill)( |$)|systemctl +(stop|restart|disable|mask)|reboot|shutdown|apt(-get)? +(install|remove|purge)|ufw |iptables )' \
82
+ && ask "a destructive command over SSH"
83
+ fi
84
+ exit 0