Skip to content

Commit b4eb072

Browse files
feat(safety): owner allowlist + two-tier menu with clearer item names (#1)
Adds an ownership safety guard so scripts NEVER touch repositories outside a configured allowlist of owners (defaults to ["hyperpolymath"]; edit config/owners.config or set GIT_SCRIPTS_ALLOWED_OWNERS to add personal / family / additional org accounts). The guard is enforced in two parallel implementations that share the same config: - scripts/lib/ownership_guard.sh — sourced by every shell script that targets a single org or pushes to remotes; provides owner_allowed/repo_allowed/assert_owner_allowed and a host-agnostic owner extractor (works for GitHub, GitLab, Bitbucket, Gitea, self-hosted, SSH-style, etc.). - lib/script_manager/ownership_guard.ex — the Elixir equivalent; exposes allowed_owners/0, owner_allowed?/1, repo_allowed?/1, filter_allowed/1, filter_allowed_verbose/1 and assert_owner_allowed!/1. Wired into all the scripts/modules that can mutate or affect repos: shell: branch-protection-apply, wiki-audit, project-tabs-audit, audit_script (per-repo filter + uses derived owner for the Dependabot URL), update_repos (per-repo filter before push), standardize_readmes & md_to_adoc_converter (per-repo filter). elixir: PRProcessor.process_all/add_standard_comment (asserts org), GitSyncer.run (filters discovered repos before push), EstateDeployer.deploy_by_paths (filters before writing files), DependencyFixer.fix_lithoglyph/fix_rgtv (refuses to patch when enclosing repo is foreign-owned), RepoCleanup (warns the external cleanup scripts are NOT bound by the allowlist). Also rewrites the TUI menu as two tiers with clearer item names: [A] Audits & Reports — wiki, project metadata, contractiles, secrets/Dependabot, health dashboard, local-vs-remote sync verification [B] Repository Maintenance — update repos, global git sync, standardise READMEs, MD→AsciiDoc, clean unicode, cleanup ops, dep fixes [C] GitHub Operations — branch protection rulesets, mass PR processor, gh CLI helper [D] Estate-Wide Deployment — deploy estate standards, link toolchains, find media repos [E] External Tools — launch NQC, launch Invariant Path [F] Coming Soon — dependency updater, release manager The startup banner shows the active owner allowlist and the help and system-status screens both surface it so it's obvious at a glance. Note: rebuild the escript with `mix escript.build` to pick up the Elixir-side changes; the bash-side guard is active immediately. https://claude.ai/code/session_014ME3ph3UecQQAPQDKY2HPf Co-authored-by: Claude <noreply@anthropic.com>
1 parent a309b73 commit b4eb072

16 files changed

Lines changed: 883 additions & 286 deletions

config/owners.config

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Git Scripts — Owner Allowlist
2+
#
3+
# These scripts must NEVER touch repositories owned by anyone outside this
4+
# list. The guard refuses to operate (and exits non-zero) if a target repo's
5+
# GitHub owner is not present here.
6+
#
7+
# Add to this list:
8+
# - your own GitHub username
9+
# - any family members you maintain repos for (e.g. your son)
10+
# - any organisations you control
11+
#
12+
# The owner check is case-insensitive. One owner per line in the array.
13+
#
14+
# Override at runtime without editing this file by exporting:
15+
# GIT_SCRIPTS_ALLOWED_OWNERS="ownerA ownerB ownerC"
16+
# (space- or comma-separated).
17+
18+
ALLOWED_OWNERS=(
19+
"hyperpolymath"
20+
)
21+
22+
if [[ -n "${GIT_SCRIPTS_ALLOWED_OWNERS:-}" ]]; then
23+
# Replace the array entirely from the env var
24+
IFS=', ' read -r -a ALLOWED_OWNERS <<< "${GIT_SCRIPTS_ALLOWED_OWNERS}"
25+
fi
26+
27+
export ALLOWED_OWNERS

lib/script_manager/dependency_fixer.ex

Lines changed: 82 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -11,71 +11,115 @@ defmodule ScriptManager.DependencyFixer do
1111
def run do
1212
IO.puts("\n🔧 DEPENDENCY FIXER (Hardened Mode)")
1313
IO.puts("===================================")
14-
14+
1515
fix_lithoglyph()
1616
fix_rgtv()
17-
17+
1818
IO.puts("\n✅ Dependency fixing complete!")
1919
:ok
2020
end
2121

22+
# Walk up to the enclosing git working tree and check the owner allowlist.
23+
# Returns true if the directory has no enclosing repo (we can't tell, so allow
24+
# the explicit per-path edits to proceed inside our own filesystem layout).
25+
@spec safe_to_edit?(String.t()) :: boolean()
26+
defp safe_to_edit?(path) do
27+
case System.cmd("git", ["-C", path, "rev-parse", "--show-toplevel"], stderr_to_stdout: true) do
28+
{toplevel, 0} ->
29+
ScriptManager.OwnershipGuard.repo_allowed?(String.trim(toplevel))
30+
31+
_ ->
32+
# Not inside a git repo: nothing remote to push to, no owner to violate.
33+
true
34+
end
35+
end
36+
2237
@spec fix_lithoglyph() :: :ok
2338
defp fix_lithoglyph do
2439
path = "/var/mnt/eclipse/repos/nextgen-databases/lithoglyph/core-zig"
2540
IO.puts("Fixing Lithoglyph in #{path}...")
26-
41+
2742
try do
28-
if File.dir?(path) do
29-
build_zig = Path.join(path, "build.zig")
30-
if File.exists?(build_zig) do
31-
content = File.read!(build_zig)
32-
new_content = String.replace(content, "const crypto_tests = b.addTest(.{", "const _crypto_tests = b.addTest(.{")
33-
File.write!(build_zig, new_content)
34-
IO.puts(" ✓ build.zig patched")
35-
36-
IO.puts(" Running tests...")
37-
System.cmd("zig", ["build", "test"], cd: path, into: IO.stream(:stdio, :line))
38-
end
39-
else
40-
IO.puts(" ⚠ Lithoglyph directory not found")
43+
cond do
44+
not File.dir?(path) ->
45+
IO.puts(" ⚠ Lithoglyph directory not found")
46+
47+
not safe_to_edit?(path) ->
48+
IO.puts(" 🛡 Skipping: enclosing repo is outside the owner allowlist.")
49+
50+
true ->
51+
do_fix_lithoglyph(path)
4152
end
4253
rescue
4354
e -> IO.puts(" ❌ Failed to fix Lithoglyph: #{inspect(e)}")
4455
end
56+
57+
:ok
58+
end
59+
60+
@spec do_fix_lithoglyph(String.t()) :: :ok
61+
defp do_fix_lithoglyph(path) do
62+
build_zig = Path.join(path, "build.zig")
63+
64+
if File.exists?(build_zig) do
65+
content = File.read!(build_zig)
66+
new_content = String.replace(content, "const crypto_tests = b.addTest(.{", "const _crypto_tests = b.addTest(.{")
67+
File.write!(build_zig, new_content)
68+
IO.puts(" ✓ build.zig patched")
69+
70+
IO.puts(" Running tests...")
71+
System.cmd("zig", ["build", "test"], cd: path, into: IO.stream(:stdio, :line))
72+
end
73+
4574
:ok
4675
end
4776

4877
@spec fix_rgtv() :: :ok
4978
defp fix_rgtv do
5079
path = "/var/mnt/eclipse/repos/reasonably-good-token-vault/vault-core"
5180
IO.puts("Fixing RGTV in #{path}...")
52-
81+
5382
try do
54-
if File.dir?(path) do
55-
primes_rs = Path.join([path, "src", "primes.rs"])
56-
if File.exists?(primes_rs) do
57-
content = File.read!(primes_rs)
58-
new_content = String.replace(content, "use num_bigint::{BigUint, RandBigInt, ToBigUint};", "use num_bigint::{BigUint, ToBigUint};")
59-
File.write!(primes_rs, new_content)
60-
IO.puts(" ✓ src/primes.rs patched")
61-
end
62-
63-
crypto_rs = Path.join([path, "src", "crypto.rs"])
64-
if File.exists?(crypto_rs) do
65-
content = File.read!(crypto_rs)
66-
new_content = String.replace(content, "use ed448_goldilocks::EdwardsPoint::generator()", "use ed448_goldilocks::edwards::EdwardsPoint::generator()")
67-
File.write!(crypto_rs, new_content)
68-
IO.puts(" ✓ src/crypto.rs patched")
69-
end
70-
71-
IO.puts(" Running tests...")
72-
System.cmd("cargo", ["test", "--lib"], cd: path, into: IO.stream(:stdio, :line))
73-
else
74-
IO.puts(" ⚠ RGTV directory not found")
83+
cond do
84+
not File.dir?(path) ->
85+
IO.puts(" ⚠ RGTV directory not found")
86+
87+
not safe_to_edit?(path) ->
88+
IO.puts(" 🛡 Skipping: enclosing repo is outside the owner allowlist.")
89+
90+
true ->
91+
do_fix_rgtv(path)
7592
end
7693
rescue
7794
e -> IO.puts(" ❌ Failed to fix RGTV: #{inspect(e)}")
7895
end
96+
97+
:ok
98+
end
99+
100+
@spec do_fix_rgtv(String.t()) :: :ok
101+
defp do_fix_rgtv(path) do
102+
primes_rs = Path.join([path, "src", "primes.rs"])
103+
104+
if File.exists?(primes_rs) do
105+
content = File.read!(primes_rs)
106+
new_content = String.replace(content, "use num_bigint::{BigUint, RandBigInt, ToBigUint};", "use num_bigint::{BigUint, ToBigUint};")
107+
File.write!(primes_rs, new_content)
108+
IO.puts(" ✓ src/primes.rs patched")
109+
end
110+
111+
crypto_rs = Path.join([path, "src", "crypto.rs"])
112+
113+
if File.exists?(crypto_rs) do
114+
content = File.read!(crypto_rs)
115+
new_content = String.replace(content, "use ed448_goldilocks::EdwardsPoint::generator()", "use ed448_goldilocks::edwards::EdwardsPoint::generator()")
116+
File.write!(crypto_rs, new_content)
117+
IO.puts(" ✓ src/crypto.rs patched")
118+
end
119+
120+
IO.puts(" Running tests...")
121+
System.cmd("cargo", ["test", "--lib"], cd: path, into: IO.stream(:stdio, :line))
122+
79123
:ok
80124
end
81125
end

lib/script_manager/estate_deployer.ex

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ defmodule ScriptManager.EstateDeployer do
22
@moduledoc "Estate deployment logic generalized for all repositories"
33

44
alias ScriptManager.RepoHelper
5+
alias ScriptManager.OwnershipGuard
56

67
@contractile_types ["must", "trust", "dust", "lust", "adjust", "intend"]
78
@standards_dir "/var/mnt/eclipse/repos/standards"
@@ -65,9 +66,12 @@ defmodule ScriptManager.EstateDeployer do
6566
end
6667

6768
defp deploy_by_paths(repo_paths, phases) do
69+
# Ownership guard: refuse to deploy into repos outside the allowlist.
70+
repo_paths = OwnershipGuard.filter_allowed_verbose(repo_paths)
71+
6872
total = length(repo_paths)
6973
IO.puts("Processing #{total} repositories...")
70-
74+
7175
repo_paths
7276
|> Enum.with_index(1)
7377
|> Enum.each(fn {path, index} ->

lib/script_manager/git_syncer.ex

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ defmodule ScriptManager.GitSyncer do
55
"""
66

77
alias ScriptManager.RepoHelper
8+
alias ScriptManager.OwnershipGuard
89

910
@type sync_status :: String.t()
1011
@type merge_status :: String.t()
@@ -16,9 +17,12 @@ defmodule ScriptManager.GitSyncer do
1617
def run do
1718
IO.puts("\n🌐 GLOBAL GIT SYNC (Concurrent Strict Mode)")
1819
IO.puts("============================================")
19-
20-
all_repos = RepoHelper.find_all_repos()
21-
20+
21+
# Ownership guard: never push to repos outside the allowlist.
22+
all_repos =
23+
RepoHelper.find_all_repos()
24+
|> OwnershipGuard.filter_allowed_verbose()
25+
2226
header = "| Repository | Sync Status | Merge Status | Push Status |"
2327
separator = "| :--- | :--- | :--- | :--- |"
2428

0 commit comments

Comments
 (0)