adds usage: scratchpad {ls|clear|help|make|write|size|find|lock|unlock} [ARGS]
authorThomas Walker Lynch <eknp9n@reasoningtechnology.com>
Wed, 29 Oct 2025 09:04:53 +0000 (09:04 +0000)
committerThomas Walker Lynch <eknp9n@reasoningtechnology.com>
Wed, 29 Oct 2025 09:04:53 +0000 (09:04 +0000)
  ls               List scratchpad in an indented tree with perms and owner (quiet if missing).
  clear            Remove all contents of scratchpad/ except top-level .gitignore.
  clear NAME       Remove scratchpad/NAME only.
  make [NAME]      Ensure scratchpad/ exists with .gitignore; with NAME, mkdir scratchpad/NAME.
  write SRC [DST]  Copy file/dir SRC into scratchpad (to DST if given; parents created).
  size             Print 'empty' if only .gitignore; else total bytes and item count.
  find [OPTS...]   Run system 'find' rooted at scratchpad/ with OPTS (omit literal 'scratchpad').
  lock PATH...     Attempt 'chattr +i' on given paths under scratchpad/ (no state kept).
  unlock PATH...   Attempt 'chattr -i' on given paths under scratchpad/.

Examples:
  scratchpad make
  scratchpad write ~/Downloads/test.tar.gz
  scratchpad find -type f -mtime +30 -print   # files older than 30 days
  scratchpad lock some/dir important.txt
  scratchpad unlock some/dir important.txt command

developer/cc/Rabbit_2017_to_WG.kmod.c
developer/tool/release
release/kmod/Rabbit_2017_count.ko
release/kmod/Rabbit_2017_to_WG.ko [new file with mode: 0644]
release/kmod/Rabbit_no-op.ko
tool_shared/bespoke/env
tool_shared/bespoke/scratchpad [new file with mode: 0755]
tool_shared/bespoke/version

index 17ab69e..8a29aa3 100644 (file)
@@ -12,7 +12,7 @@
 #include <net/sock.h>
 #include <net/inet_sock.h>
 
-#define RABBIT_UID 2017
+#define US_UID 2017
 #define DEV_NAME   "US"   /* WireGuard iface to force */
 
 static atomic64_t cnt_v4_local_out = ATOMIC64_INIT(0);
@@ -35,7 +35,7 @@ static int us_ifindex;  /* cached ifindex for DEV_NAME */
 static inline bool from_uid_2017(const struct nf_hook_state *st, struct sk_buff *skb){
   struct sock *sk = st->sk ? st->sk : skb_to_full_sk(skb);
   if (!sk) return false;
-  return __kuid_val(sock_i_uid(sk)) == RABBIT_UID;
+  return __kuid_val(sock_i_uid(sk)) == US_UID;
 }
 
 /* Bind the socket to DEV_NAME once we see its first packet */
@@ -85,7 +85,7 @@ static int __init rabbit_init(void){
 #else
   { int ret = nf_register_hooks(rabbit_ops, ARRAY_SIZE(rabbit_ops)); if (ret) return ret; }
 #endif
-  pr_info("rabbit_uid2017_bind: loaded; UID=%d bound to dev %s(ifindex=%d)\n", RABBIT_UID, DEV_NAME, us_ifindex);
+  pr_info("rabbit_uid2017_bind: loaded; UID=%d bound to dev %s(ifindex=%d)\n", US_UID, DEV_NAME, us_ifindex);
   return 0;
 }
 
index 0eba83f..63f0378 100755 (executable)
@@ -28,16 +28,13 @@ machine_executable_list="scratchpad/hello"
 shopt -s nullglob
 
 # zero or more machine executables
-for fglob in $machine_executable_list; do
-  # nesting the loop allows that $fglob is a file glob, and expands it
-  for f in $fglob; do
-    if [[ -x "$f" ]]; then
-      echo "+ install -m 0500 '$f' '$release_dir/machine/'"
-      install -m 0550 "$f" "$release_dir/machine/"
-    else
-      echo "(info) did not find '$f'"
-    fi
-  done
+for f in $machine_executable_list; do
+  if [[ -x "$f" ]]; then
+    echo "+ install -m 0500 '$f' '$release_dir/machine/'"
+    install -m 0550 "$f" "$release_dir/machine/"
+  else
+    echo "(info) did not find '$f'"
+  fi
 done
 
 # zero or more Kernel modules
index 6edb934..f998c53 100644 (file)
Binary files a/release/kmod/Rabbit_2017_count.ko and b/release/kmod/Rabbit_2017_count.ko differ
diff --git a/release/kmod/Rabbit_2017_to_WG.ko b/release/kmod/Rabbit_2017_to_WG.ko
new file mode 100644 (file)
index 0000000..c60a70f
Binary files /dev/null and b/release/kmod/Rabbit_2017_to_WG.ko differ
index 625d157..9565f97 100644 (file)
Binary files a/release/kmod/Rabbit_no-op.ko and b/release/kmod/Rabbit_no-op.ko differ
index 37ba2dc..0d47fca 100644 (file)
@@ -9,7 +9,7 @@ fi
 shopt -s nullglob
 
 # does not presume sharing or world permissions
-umask 0027
+umask 0077
 
 # --------------------------------------------------------------------------------
 # project definition
diff --git a/tool_shared/bespoke/scratchpad b/tool_shared/bespoke/scratchpad
new file mode 100755 (executable)
index 0000000..f14f140
--- /dev/null
@@ -0,0 +1,225 @@
+#!/usr/bin/env -S python3 -B
+# -*- mode: python; coding: utf-8; python-indent-offset: 2; indent-tabs-mode: nil -*-
+
+import os, sys, shutil, stat, pwd, grp, subprocess
+
+HELP = """usage: scratchpad {ls|clear|help|make|write|size|find|lock|unlock} [ARGS]
+  ls               List scratchpad in an indented tree with perms and owner (quiet if missing).
+  clear            Remove all contents of scratchpad/ except top-level .gitignore.
+  clear NAME       Remove scratchpad/NAME only.
+  make [NAME]      Ensure scratchpad/ exists with .gitignore; with NAME, mkdir scratchpad/NAME.
+  write SRC [DST]  Copy file/dir SRC into scratchpad (to DST if given; parents created).
+  size             Print 'empty' if only .gitignore; else total bytes and item count.
+  find [OPTS...]   Run system 'find' rooted at scratchpad/ with OPTS (omit literal 'scratchpad').
+  lock PATH...     Attempt 'chattr +i' on given paths under scratchpad/ (no state kept).
+  unlock PATH...   Attempt 'chattr -i' on given paths under scratchpad/.
+
+Examples:
+  scratchpad make
+  scratchpad write ~/Downloads/test.tar.gz
+  scratchpad find -type f -mtime +30 -print   # files older than 30 days
+  scratchpad lock some/dir important.txt
+  scratchpad unlock some/dir important.txt
+"""
+
+CWD = os.getcwd()
+SP = os.path.join(CWD, "scratchpad")
+GITIGNORE = os.path.join(SP, ".gitignore")
+
+def have_sp() -> bool:
+  return os.path.isdir(SP)
+
+def ensure_sp():
+  os.makedirs(SP, exist_ok=True)
+  ensure_gitignore()
+
+def ensure_gitignore():
+  os.makedirs(SP, exist_ok=True)
+  if not os.path.isfile(GITIGNORE):
+    with open(GITIGNORE, "w", encoding="utf-8") as f:
+      f.write("*\n!.gitignore\n")
+
+def filemode(mode: int) -> str:
+  try:
+    return stat.filemode(mode)
+  except Exception:
+    return oct(mode & 0o777)
+
+def owner_group(st) -> str:
+  try:
+    return f"{pwd.getpwuid(st.st_uid).pw_name}:{grp.getgrgid(st.st_gid).gr_name}"
+  except Exception:
+    return f"{st.st_uid}:{st.st_gid}"
+
+def rel_depth(base: str, root: str) -> int:
+  rel = os.path.relpath(base, root)
+  return 0 if rel == "." else rel.count(os.sep) + 1
+
+def ls_tree(root: str) -> None:
+  if not have_sp():
+    return
+  print("scratchpad/")
+
+  def walk(path: str, indent: str, is_root: bool) -> None:
+    try:
+      it = list(os.scandir(path))
+    except FileNotFoundError:
+      return
+
+    dirs  = [e for e in it if e.is_dir(follow_symlinks=False)]
+    files = [e for e in it if not e.is_dir(follow_symlinks=False)]
+    dirs.sort(key=lambda e: e.name)
+    files.sort(key=lambda e: e.name)
+
+    if is_root:
+      # 1) root-level hidden files first
+      for f in (e for e in files if e.name.startswith(".")):
+        st = os.lstat(f.path)
+        print(f"{filemode(st.st_mode)}  {owner_group(st)}  {indent}{f.name}")
+      # 2) then directories (and recurse so children sit under the parent)
+      for d in dirs:
+        st = os.lstat(d.path)
+        print(f"{filemode(st.st_mode)}  {owner_group(st)}  {indent}{d.name}/")
+        walk(d.path, indent + '  ', False)
+      # 3) then non-hidden files
+      for f in (e for e in files if not e.name.startswith(".")):
+        st = os.lstat(f.path)
+        print(f"{filemode(st.st_mode)}  {owner_group(st)}  {indent}{f.name}")
+    else:
+      # subdirs: keep previous order (dirs first, then files; dotfiles naturally sort first)
+      for d in dirs:
+        st = os.lstat(d.path)
+        print(f"{filemode(st.st_mode)}  {owner_group(st)}  {indent}{d.name}/")
+        walk(d.path, indent + '  ', False)
+      for f in files:
+        st = os.lstat(f.path)
+        print(f"{filemode(st.st_mode)}  {owner_group(st)}  {indent}{f.name}")
+
+  walk(root, "  ", True)
+
+
+def clear_all() -> None:
+  if not have_sp():
+    return
+  for name in os.listdir(SP):
+    p = os.path.join(SP, name)
+    if name == ".gitignore" and os.path.isfile(p):
+      continue  # preserve only top-level .gitignore
+    if os.path.isdir(p) and not os.path.islink(p):
+      shutil.rmtree(p, ignore_errors=True)
+    else:
+      try: os.unlink(p)
+      except FileNotFoundError: pass
+
+def clear_subdir(sub: str) -> None:
+  if not have_sp():
+    return
+  target = os.path.normpath(os.path.join(SP, sub))
+  try:
+    if os.path.commonpath([SP]) != os.path.commonpath([SP, target]):
+      return
+  except Exception:
+    return
+  if os.path.isdir(target) and not os.path.islink(target):
+    shutil.rmtree(target, ignore_errors=True)
+
+def cmd_make(args):
+  ensure_sp()
+  if args:
+    os.makedirs(os.path.join(SP, args[0]), exist_ok=True)
+
+def cmd_write(args):
+  if len(args) < 1:
+    print(HELP); return
+  if not have_sp():
+    ensure_sp()
+  src = args[0]
+  dst = args[1] if len(args) >= 2 else (os.path.basename(src.rstrip(os.sep)) or "untitled")
+  dst_path = os.path.normpath(os.path.join(SP, dst))
+  try:
+    if os.path.commonpath([SP]) != os.path.commonpath([SP, dst_path]):
+      print("refusing to write outside scratchpad", file=sys.stderr); return
+  except Exception:
+    print("invalid destination", file=sys.stderr); return
+  os.makedirs(os.path.dirname(dst_path), exist_ok=True)
+  if os.path.isdir(src):
+    if os.path.exists(dst_path):
+      shutil.rmtree(dst_path, ignore_errors=True)
+    shutil.copytree(src, dst_path, dirs_exist_ok=False)
+  else:
+    shutil.copy2(src, dst_path)
+
+def cmd_size():
+  if not have_sp():
+    return
+  names = os.listdir(SP)
+  if [n for n in names if n != ".gitignore"] == []:
+    print("empty"); return
+  total = 0; count = 0
+  for base, dirs, files in os.walk(SP):
+    for fn in files:
+      if fn == ".gitignore": 
+        continue
+      p = os.path.join(base, fn)
+      try:
+        total += os.path.getsize(p); count += 1
+      except OSError:
+        pass
+  print(f"bytes={total} items={count}")
+
+def cmd_find(args):
+  if not have_sp():
+    return
+  try:
+    subprocess.run(["find", SP] + args, check=False)
+  except FileNotFoundError:
+    print("find not available", file=sys.stderr)
+
+def cmd_chattr(flag: str, paths):
+  if not have_sp() or not paths:
+    return
+  try:
+    subprocess.run(["chattr", "-V"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=False)
+  except FileNotFoundError:
+    print("chattr not available; lock/unlock skipped", file=sys.stderr); return
+  for rel in paths:
+    target = os.path.normpath(os.path.join(SP, rel))
+    try:
+      if os.path.commonpath([SP]) != os.path.commonpath([SP, target]):
+        continue
+    except Exception:
+      continue
+    try:
+      subprocess.run(["chattr", flag, target], check=False)
+    except Exception:
+      pass
+
+def CLI():
+  if len(sys.argv) < 2:
+    print(HELP); return
+  cmd, *args = sys.argv[1:]
+  if cmd == "ls":
+    if have_sp(): ls_tree(SP)
+    else: return
+  elif cmd == "clear":
+    if len(args) >= 1: clear_subdir(args[0])
+    else: clear_all()
+  elif cmd == "help":
+    print(HELP)
+  elif cmd == "make":
+    cmd_make(args)
+  elif cmd == "write":
+    cmd_write(args)
+  elif cmd == "size":
+    cmd_size()
+  elif cmd == "find":
+    cmd_find(args)
+  elif cmd == "lock":
+    cmd_chattr("+i", args)
+  elif cmd == "unlock":
+    cmd_chattr("-i", args)
+  else:
+    print(HELP)
+
+if __name__ == "__main__":
+  CLI()
index 9d91a98..829308d 100755 (executable)
@@ -1,5 +1,5 @@
 #!/bin/env bash
 script_afp=$(realpath "${BASH_SOURCE[0]}")
 
-echo "Harmony v0.1 2025-05-19"
+echo "Rabbit v0.1 2025-10-20"