From f656994b0280f23784c83312d426bfe4db47bd5e Mon Sep 17 00:00:00 2001 From: Thomas Walker Lynch Date: Sat, 20 Sep 2025 14:17:22 +0000 Subject: [PATCH] approaching run of test_0 --- developer/machine/x86_64/man_in_grey_apply | Bin 17456 -> 17232 bytes developer/source/Man_In_Grey.py | 6 +- developer/source/executor_inner.py | 18 +- developer/tool/compile | 23 + developer/tool/release | 96 ++-- env_tester | 25 +- release/python3/Man_In_Grey.py | 23 +- release/python3/Planner.py | 515 ++++++++++++++++++ release/python3/executor_inner.py | 18 +- release/x86_64/man_in_grey_apply | Bin 17456 -> 17232 bytes tester/git_holder | 0 tester/test_0/run | 92 ++++ .../stage_test_0/DNS/unbound.conf.py | 0 .../{ => test_0}/stage_test_0/unbound_conf.py | 0 .../stage_test_0/web/site_conf.py | 0 .../{ => test_0}/stage_test_0_out/.githolder | 0 tester/tool/env | 28 + tool/bless | 35 ++ tool/bless_release | 47 -- tool/unbless | 30 + tool_shared/bespoke/env | 1 + 21 files changed, 837 insertions(+), 120 deletions(-) create mode 100644 release/python3/Planner.py delete mode 100644 tester/git_holder create mode 100755 tester/test_0/run rename tester/{ => test_0}/stage_test_0/DNS/unbound.conf.py (100%) rename tester/{ => test_0}/stage_test_0/unbound_conf.py (100%) rename tester/{ => test_0}/stage_test_0/web/site_conf.py (100%) rename tester/{ => test_0}/stage_test_0_out/.githolder (100%) create mode 100644 tester/tool/env create mode 100755 tool/bless delete mode 100644 tool/bless_release create mode 100755 tool/unbless diff --git a/developer/machine/x86_64/man_in_grey_apply b/developer/machine/x86_64/man_in_grey_apply index 09bd80da16b6cc9fb12018a4f2bb0f7f296b0267..ee431abde30d5d8d74318972d771c04085608d04 100755 GIT binary patch literal 17232 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 Path|None: return cand if cand.is_file() else None def _apply_via_gasket(cbor_bytes: bytes ,apply_cmd: Path ,args)-> int: - cmd = [str(apply_cmd)] + cmd = [ + str(apply_cmd) + ,"--plan" ,"-" + ] if args.phase_2_print: cmd.append("--phase-2-print") if args.phase_2_then_stop: cmd.append("--phase-2-then-stop") - # fine-grained gates (optional pass-through if gasket proxies them) if args.phase_2_wellformed_then_stop: cmd.append("--phase-2-wellformed-then-stop") if args.phase_2_sanity1_then_stop: cmd.append("--phase-2-sanity1-then-stop") if args.phase_2_validity_then_stop: cmd.append("--phase-2-validity-then-stop") diff --git a/developer/source/executor_inner.py b/developer/source/executor_inner.py index 3c2ea1a..433b705 100644 --- a/developer/source/executor_inner.py +++ b/developer/source/executor_inner.py @@ -379,6 +379,7 @@ def run_executor_inner( # --- main stays a thin arg wrapper ------------------------------------------ def main(argv: list[str]|None=None)-> int: + ap = argparse.ArgumentParser( prog="executor_inner.py" ,description="Man_In_Gray inner executor (decode → validate → apply)" @@ -391,13 +392,26 @@ def main(argv: list[str]|None=None)-> int: ap.add_argument("--phase-2-validity-then-stop" ,action="store_true" ,help="stop after validity checks") ap.add_argument("--phase-2-sanity2-then-stop" ,action="store_true" ,help="stop after sanity-2 checks") + ap.add_argument("--plan" ,default="" ,help="path to CBOR plan file or '-' for stdin") + ap.add_argument("--plan-fd" ,type=int ,default=-1 ,help=argparse.SUPPRESS) + args = ap.parse_args(argv) # load plan try: - data = Path(args.plan).read_bytes() + if args.plan_fd >= 0: + import os as _os + data = _os.read(args.plan_fd ,1<<30) + elif args.plan == "-": + import sys as _sys + data = _sys.stdin.buffer.read() + elif args.plan: + data = Path(args.plan).read_bytes() + else: + print("error: either --plan or --plan-fd is required" ,file=sys.stderr) + return 2 except Exception as e: - print(f"error: failed to read plan file: {e}" ,file=sys.stderr) + print(f"error: failed to read plan: {e}" ,file=sys.stderr) return 2 try: diff --git a/developer/tool/compile b/developer/tool/compile index e69de29..155a81c 100755 --- a/developer/tool/compile +++ b/developer/tool/compile @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +# developer/tool/compile — build gasket into developer/machine/ (no sudo) +set -euo pipefail +SELF_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)" +REPO_HOME="$(CDPATH= cd -- "$SELF_DIR/../.." && pwd)" + +raw="$(uname -m | tr '[:upper:]' '[:lower:]')" +case "$raw" in + amd64|x64) arch="x86_64" ;; x86_64) arch="x86_64" ;; + i386|i486|i586|i686) arch="i686" ;; + arm64|aarch64) arch="aarch64" ;; + armv7l) arch="armv7l" ;; armv6l) arch="armv6l" ;; + riscv64) arch="riscv64" ;; ppc64le|powerpc64le) arch="ppc64le" ;; + s390x) arch="s390x" ;; *) arch="$raw" ;; +esac + +SRC="${REPO_HOME}/developer/source/Man_In_Grey_apply.c" +OUT_DIR="${REPO_HOME}/developer/machine/${arch}" +OUT="${OUT_DIR}/man_in_grey_apply" + +mkdir -p "$OUT_DIR" +cc -O2 -Wall -Wextra -Wl,-z,relro -Wl,-z,now -fstack-protector-strong -o "$OUT" "$SRC" +echo "built: $OUT" diff --git a/developer/tool/release b/developer/tool/release index e066da4..d3ab278 100755 --- a/developer/tool/release +++ b/developer/tool/release @@ -2,71 +2,59 @@ # developer/tool/release — stage current build into ../release (no privilege changes) set -euo pipefail -# Resolve repo root from this script’s location: $REPO_HOME/developer/tool/release -SELF_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)" -REPO_HOME="$(CDPATH= cd -- "$SELF_DIR/../.." && pwd)" -REL_DIR="${REPO_HOME}/release" - -# Normalize arch (matches Man_In_Grey.py/Man_In_Grey wrapper) -raw="$(uname -m | tr '[:upper:]' '[:lower:]')" -case "$raw" in - amd64|x64) arch="x86_64" ;; - x86_64) arch="x86_64" ;; - i386|i486|i586|i686) arch="i686" ;; - arm64|aarch64) arch="aarch64" ;; - armv7l) arch="armv7l" ;; - armv6l) arch="armv6l" ;; - riscv64) arch="riscv64" ;; - ppc64le|powerpc64le) arch="ppc64le" ;; - s390x) arch="s390x" ;; - *) arch="$raw" ;; +script_afp=$(realpath "${BASH_SOURCE[0]}") +REPO_HOME="$(cd "$(dirname "$script_afp")/../.." && pwd -P)" + +# --- arch normalize (same mapping as orchestrator/wrapper) --- +arch_raw=$(uname -m | tr '[:upper:]' '[:lower:]') +case "$arch_raw" in + amd64|x64) arch="x86_64" ;; + x86_64) arch="x86_64" ;; + i386|i486|i586|i686) arch="i686" ;; + arm64|aarch64) arch="aarch64" ;; + armv7l) arch="armv7l" ;; + armv6l) arch="armv6l" ;; + riscv64) arch="riscv64" ;; + ppc64le|powerpc64le) arch="ppc64le" ;; + s390x) arch="s390x" ;; + *) arch="$arch_raw" ;; esac -# Locations -BUILD_DIR="${REPO_HOME}/developer/build/${arch}" +# --- inputs/outputs --- SRC_DIR="${REPO_HOME}/developer/source" +MACHINE_DIR="${REPO_HOME}/developer/machine/${arch}" -DEST_ARCH_DIR="${REL_DIR}/${arch}" -DEST_PY_DIR="${REL_DIR}/python3" -DEST_SH_DIR="${REL_DIR}/shell" +REL_DIR="${REPO_HOME}/release" +REL_ARCH="${REL_DIR}/${arch}" +REL_PY="${REL_DIR}/python3" +REL_SH="${REL_DIR}/shell" -# Inputs -GASKET_SRC="${BUILD_DIR}/man_in_grey_apply" +GASKET_SRC="${MACHINE_DIR}/man_in_grey_apply" PY_ORCH_SRC="${SRC_DIR}/Man_In_Grey.py" PY_INNER_SRC="${SRC_DIR}/executor_inner.py" PY_PLANNER_SRC="${SRC_DIR}/Planner.py" -WRAP_SRC="${SRC_DIR}/Man_In_Grey" # shell wrapper - -# Sanity -[[ -f "$PY_ORCH_SRC" ]] || { echo "error: missing $PY_ORCH_SRC" >&2; exit 2; } -[[ -f "$PY_INNER_SRC" ]] || { echo "error: missing $PY_INNER_SRC" >&2; exit 2; } -[[ -f "$PY_PLANNER_SRC" ]] || { echo "error: missing $PY_PLANNER_SRC" >&2; exit 2; } -[[ -f "$WRAP_SRC" ]] || { echo "error: missing $WRAP_SRC (shell wrapper)" >&2; exit 2; } +WRAP_SRC="${SRC_DIR}/Man_In_Grey" # shell entrypoint -# Gasket is optional for unprivileged testing; warn if not present -if [[ ! -x "$GASKET_SRC" ]]; then - echo "warn: gasket not found for arch ${arch}: $GASKET_SRC" - echo " (unprivileged apply will fall back to python inner)" -fi +# --- sanity checks --- +[[ -x "$GASKET_SRC" ]] || { echo "error: missing gasket for ${arch}: $GASKET_SRC (run: compile)"; exit 2; } +[[ -f "$PY_ORCH_SRC" ]] || { echo "error: missing $PY_ORCH_SRC"; exit 2; } +[[ -f "$PY_INNER_SRC" ]] || { echo "error: missing $PY_INNER_SRC"; exit 2; } +[[ -f "$PY_PLANNER_SRC" ]] || { echo "error: missing $PY_PLANNER_SRC"; exit 2; } +[[ -f "$WRAP_SRC" ]] || { echo "error: missing $WRAP_SRC (shell wrapper)"; exit 2; } -# Create dest dirs -mkdir -p "$DEST_ARCH_DIR" "$DEST_PY_DIR" "$DEST_SH_DIR" +# --- create destinations --- +mkdir -p "$REL_ARCH" "$REL_PY" "$REL_SH" -# Stage Python bits -install -m 0755 "$PY_ORCH_SRC" "$DEST_PY_DIR/Man_In_Grey.py" -install -m 0755 "$PY_INNER_SRC" "$DEST_PY_DIR/executor_inner.py" -install -m 0644 "$PY_PLANNER_SRC" "$DEST_PY_DIR/Planner.py" +# --- stage artifacts (no ownership or setuid flips here) --- +install -m 0755 "$GASKET_SRC" "${REL_ARCH}/man_in_grey_apply" -# Stage wrapper -install -m 0755 "$WRAP_SRC" "$DEST_SH_DIR/Man_In_Grey" +install -m 0755 "$PY_ORCH_SRC" "${REL_PY}/Man_In_Grey.py" +install -m 0755 "$PY_INNER_SRC" "${REL_PY}/executor_inner.py" +install -m 0644 "$PY_PLANNER_SRC" "${REL_PY}/Planner.py" -# Stage gasket (no setuid/owner changes here) -if [[ -x "$GASKET_SRC" ]]; then - install -m 0755 "$GASKET_SRC" "$DEST_ARCH_DIR/man_in_grey_apply" -fi +install -m 0755 "$WRAP_SRC" "${REL_SH}/Man_In_Grey" -echo "release staged to: $REL_DIR" -echo " arch : $arch" -echo " py : $(realpath "$DEST_PY_DIR")" -echo " shell: $(realpath "$DEST_SH_DIR")" -[[ -x "$GASKET_SRC" ]] && echo " gasket: $(realpath "$DEST_ARCH_DIR/man_in_grey_apply")" +echo "released to: ${REL_DIR}" +echo " arch : ${REL_ARCH}/man_in_grey_apply" +echo " py : ${REL_PY}/" +echo " shell: ${REL_SH}/Man_In_Grey" diff --git a/env_tester b/env_tester index 45439c1..af74b18 100644 --- a/env_tester +++ b/env_tester @@ -1,17 +1,34 @@ #!/usr/bin/env bash +# toolsmith-owned tester environment file + script_afp=$(realpath "${BASH_SOURCE[0]}") if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then echo "$script_afp:: This script must be sourced, not executed." exit 1 fi -export ROLE=tester +# shared, project-wide source tool_shared/bespoke/env +export ROLE=tester +export ENV=$ROLE + +# tester-local tool dir first (if any) +if [[ -d "$REPO_HOME/$ROLE/tool" ]]; then + PATH="$REPO_HOME/$ROLE/tool:$PATH" +fi + +# shared Python (from toolsmith-provided venv) if [[ ":$PATH:" != *":$PYTHON_HOME/bin:"* ]]; then - export PATH="$PYTHON_HOME/bin:$PATH" + PATH="$PYTHON_HOME/bin:$PATH" fi +export PATH -cd $ROLE -export ENV=$ROLE +export RELEASE="$REPO_HOME/release" + +cd "$ROLE" + +# pull in tester customizations (optional file) +[[ -f tool/env ]] && source tool/env "$@" +echo "in environment: $ENV" diff --git a/release/python3/Man_In_Grey.py b/release/python3/Man_In_Grey.py index a7000d1..e10314b 100755 --- a/release/python3/Man_In_Grey.py +++ b/release/python3/Man_In_Grey.py @@ -202,10 +202,12 @@ def _find_inner_py(repo_root: Path)-> Path|None: return cand if cand.is_file() else None def _apply_via_gasket(cbor_bytes: bytes ,apply_cmd: Path ,args)-> int: - cmd = [str(apply_cmd)] + cmd = [ + str(apply_cmd) + ,"--plan" ,"-" + ] if args.phase_2_print: cmd.append("--phase-2-print") if args.phase_2_then_stop: cmd.append("--phase-2-then-stop") - # fine-grained gates (optional pass-through if gasket proxies them) if args.phase_2_wellformed_then_stop: cmd.append("--phase-2-wellformed-then-stop") if args.phase_2_sanity1_then_stop: cmd.append("--phase-2-sanity1-then-stop") if args.phase_2_validity_then_stop: cmd.append("--phase-2-validity-then-stop") @@ -319,14 +321,17 @@ def main(argv: list[str]|None=None)-> int: print(f"error: CBOR encode failed: {e}" ,file=sys.stderr) return 2 - # Prefer gasket; else fall back to Python inner + # Always use the gasket under release//man_in_grey_apply (or explicit --apply-cmd) apply_cmd = Path(args.apply_cmd).resolve() if args.apply_cmd else (_find_apply_cmd(repo_root) or None) - if apply_cmd: - try: - return _apply_via_gasket(cbor_bytes ,apply_cmd ,args) - except Exception as e: - print(f"error: apply-cmd failed: {e}" ,file=sys.stderr) - return 2 + if not apply_cmd: + print("error: gasket not found; build/release first (release//man_in_grey_apply)", file=sys.stderr) + return 2 + + try: + return _apply_via_gasket(cbor_bytes ,apply_cmd ,args) + except Exception as e: + print(f"error: apply-cmd failed: {e}" ,file=sys.stderr) + return 2 inner_py = Path(args.inner_py).resolve() if args.inner_py else (_find_inner_py(repo_root) or None) if inner_py: diff --git a/release/python3/Planner.py b/release/python3/Planner.py new file mode 100644 index 0000000..94d8226 --- /dev/null +++ b/release/python3/Planner.py @@ -0,0 +1,515 @@ +#!/usr/bin/env -S python3 -B +""" +Planner.py — plan builder for staged configuration (UNPRIVILEGED). + +Given: runner-side provenance (PlanProvenance) and optional defaults (WriteFileMeta). +Does: expose Planner whose command methods (copy/displace/delete) build Command entries, + resolving arguments with precedence: kwarg > per-call WriteFileMeta > planner default + (and for filename, fallback to provenance-derived basename). On any argument error, + the Command is returned with errors and NOT appended to the Journal. +Returns: Journal (model only; dict in/out) via planner.journal(). +""" + +from __future__ import annotations + +# no bytecode anywhere (works under sudo/root shells too) +import sys ,os +sys.dont_write_bytecode = True +os.environ.setdefault("PYTHONDONTWRITEBYTECODE" ,"1") + +from pathlib import Path +import getpass + +# ===== Utilities ===== + +def norm_perm(value: int|str)-> tuple[int ,str]|None: + "Given int or 3/4-char octal string (optionally 0o-prefixed). Does validate/normalize. Returns (int ,'%04o') or None." + if isinstance(value ,int): + if 0 <= value <= 0o7777: + return value ,f"{value:04o}" + return None + if isinstance(value ,str): + s = value.strip().lower() + if s.startswith("0o"): + try: + v = int(s ,8) + return v ,f"{v:04o}" + except Exception: + return None + if len(s) in (3 ,4) and all(ch in "01234567" for ch in s): + try: + v = int(s ,8) + return v ,f"{v:04o}" + except Exception: + return None + return None + +def is_abs_dpath(dpath_str: str|None)-> bool: + "Given path string. Does quick abs dir check. Returns bool." + return isinstance(dpath_str ,str) and dpath_str.startswith("/") and "\x00" not in dpath_str + +def norm_abs_dpath_str(value: str|Path|None)-> str|None: + "Given str/Path/None. Does normalize absolute dir path string. Returns str or None." + if value is None: return None + s = value.as_posix() if isinstance(value ,Path) else str(value) + return s if is_abs_dpath(s) else None + +def norm_dpath_str(value: str|Path|None)-> str|None: + "Given str/Path/None. Does minimal sanitize; allows relative. Returns str or None." + if value is None: return None + s = value.as_posix() if isinstance(value ,Path) else str(value) + if not s or "\x00" in s: return None + return s + +def norm_fname_or_none(value: str|None)-> str|None: + "Given candidate filename or None. Does validate bare filename. Returns str or None." + if value is None: return None + s = str(value) + if not s: return None + if "/" in s or s in ("." ,"..") or "\x00" in s: return None + return s + +def norm_nonempty_owner(value: str|None)-> str|None: + "Given owner string or None. Does minimally validate (non-empty). Returns str or None." + if value is None: return None + s = str(value).strip() + return s if s else None + +def parse_mode(value: int|str|None)-> tuple[int|None ,str|None]: + "Given int/str/None. Does normalize via norm_perm. Returns (int ,'%04o') or (None ,None)." + if value is None: return None ,None + r = norm_perm(value) + return r if r is not None else (None ,None) + +def norm_content_bytes(value: bytes|str|None)-> bytes|None: + "Given bytes/str/None. Does normalize to UTF-8 bytes or None. Returns bytes|None." + if value is None: return None + if isinstance(value ,bytes): return value + return value.encode("utf-8") + +# ===== Wire-ready model types (no CBOR here) ===== + +class Command: + """ + Command — a single planned operation. + + Given name_str ('copy'|'displace'|'delete'), optional arg_dict, optional errors_list. + Does hold op name, own a fresh arg_dict, collect per-entry errors. + Returns dictionary via as_dictionary(). + """ + __slots__ = ( + "name_str" + ,"arg_dict" + ,"errors_list" + ) + + def __init__(self ,name_str: str ,arg_dict: dict|None=None ,errors_list: list[str]|None=None)-> None: + self.name_str = name_str + self.arg_dict = dict(arg_dict) if arg_dict is not None else {} + self.errors_list = list(errors_list) if errors_list is not None else [] + + def add_error(self ,msg_str: str)-> None: + self.errors_list.append(msg_str) + + def as_dictionary(self)-> dict: + return { + "op": self.name_str + ,"arg_dict": dict(self.arg_dict) + ,"errors_list": list(self.errors_list) + } + + def print(self ,* ,index: int|None=None ,file=None)-> None: + """ + Given: optional index for numbering and optional file-like (defaults to stdout). + Does: print a compact, human-readable one-line summary of this command; prints any errors indented below. + Returns: None. + """ + if file is None: + import sys as _sys + file = _sys.stdout + + op = self.name_str + ad = self.arg_dict or {} + + # Compose destination path for display (normalize to collapse '..') + d = ad.get("write_file_dpath_str") or "" + f = ad.get("write_file_fname") or "" + try: + from pathlib import Path as _Path + if d and f and "/" not in f: + dst = (_Path(d)/f).resolve().as_posix() + else: + dst = "?" + except Exception: + dst = "?" + + prefix = f"{index:02d}. " if index is not None else "" + + if op == "copy": + mode = ad.get("mode_int") + owner = ad.get("owner_name") + size = len(ad.get("content_bytes") or b"") + line = f"{prefix}copy -> {dst} mode {mode:04o} owner {owner} bytes {size}" + elif op == "displace": + line = f"{prefix}displace -> {dst}" + elif op == "delete": + line = f"{prefix}delete -> {dst}" + else: + line = f"{prefix}?op? -> {dst}" + + print(line ,file=file) + + for err in self.errors_list: + print(f" ! {err}" ,file=file) + +class Journal: + """ + Journal — ordered list of Command plus provenance metadata (model only; no CBOR). + + Given optional plan_dict in wire shape (for reconstruction). + Does manage meta, append commands, expose entries, and pack to dict. + Returns dict via as_dictionary(). + """ + __slots__ = ( + "meta_dict" + ,"command_list" + ) + + def __init__(self ,plan_dict: dict|None=None)-> None: + self.meta_dict = {} + self.command_list = [] + if plan_dict is not None: + self._init_from_dict(plan_dict) + + def _init_from_dict(self ,plan_dict: dict)-> None: + if not isinstance(plan_dict ,dict): + raise ValueError("plan_dict must be a dict") + meta = dict(plan_dict.get("meta_dict") or {}) + entries = plan_dict.get("entries_list") or [] + self.meta_dict.update(meta) + for e in entries: + if not isinstance(e ,dict): + continue + op = e.get("op") or "?" + args = e.get("arg_dict") or {} + errs = e.get("errors_list") or [] + self.command_list.append(Command(name_str=op ,arg_dict=dict(args) ,errors_list=list(errs))) + + def set_meta(self ,**kv)-> None: + self.meta_dict.update(kv) + + def append(self ,cmd: Command)-> None: + self.command_list.append(cmd) + + def entries_list(self)-> list[dict]: + return [c.as_dictionary() for c in self.command_list] + + def as_dictionary(self)-> dict: + return { + "version_int": 1 + ,"meta_dict": dict(self.meta_dict) + ,"entries_list": self.entries_list() + } + + def print(self ,* ,index_start: int=1 ,file=None)-> None: + """ + Given: optional starting index and optional file-like (defaults to stdout). + Does: print each Command on a single line via Command.print(), numbered. + Returns: None. + """ + if file is None: + import sys as _sys + file = _sys.stdout + + if not self.command_list: + print("(plan is empty)" ,file=file) + return + + for i ,cmd in enumerate(self.command_list ,start=index_start): + cmd.print(index=i ,file=file) + +# ===== Runner-provided provenance ===== + +class PlanProvenance: + """ + Runner-provided, read-only provenance for a single config script. + """ + __slots__ = ( + "stage_root_dpath" + ,"config_abs_fpath" + ,"config_rel_fpath" + ,"read_dir_dpath" + ,"read_fname" + ,"process_user" + ,"cwd_dpath" + ) + + def __init__(self ,* ,stage_root: Path ,config_path: Path): + self.stage_root_dpath = stage_root.resolve() + self.config_abs_fpath = config_path.resolve() + try: + self.config_rel_fpath = self.config_abs_fpath.relative_to(self.stage_root_dpath) + except Exception: + self.config_rel_fpath = Path(self.config_abs_fpath.name) + + self.read_dir_dpath = self.config_abs_fpath.parent + + name = self.config_abs_fpath.name + if name.endswith(".stage.py"): + self.read_fname = name[:-len(".stage.py")] + elif name.endswith(".py"): + self.read_fname = name[:-3] + else: + self.read_fname = name + + self.process_user = getpass.getuser() + self.cwd_dpath = Path.cwd().resolve() + + def print(self ,* ,file=None)-> None: + if file is None: + import sys as _sys + file = _sys.stdout + print(f"Stage root: {self.stage_root_dpath}" ,file=file) + print(f"Config (rel): {self.config_rel_fpath.as_posix()}" ,file=file) + print(f"Config (abs): {self.config_abs_fpath}" ,file=file) + print(f"Read dir: {self.read_dir_dpath}" ,file=file) + print(f"Read fname: {self.read_fname}" ,file=file) + print(f"Process user: {self.process_user}" ,file=file) + +# ===== Admin-facing defaults carrier ===== + +class WriteFileMeta: + """ + WriteFileMeta — per-call or planner-default write-file attributes. + + Given dpath (str/Path, may be relative) ,fname (bare name or None) ,owner (str) + ,mode (int|'0644') ,content (bytes|str|None). + Does normalize into fields (may remain None if absent/invalid). + Returns object suitable for providing defaults to Planner methods. + """ + __slots__ = ( + "dpath_str" + ,"fname" + ,"owner_name_str" + ,"mode_int" + ,"mode_octal_str" + ,"content_bytes" + ) + + def __init__(self + ,* + ,dpath="/" + ,fname=None + ,owner="root" + ,mode=0o444 + ,content=None + ): + self.dpath_str = norm_dpath_str(dpath) + self.fname = norm_fname_or_none(fname) + self.owner_name_str = norm_nonempty_owner(owner) + self.mode_int ,self.mode_octal_str = parse_mode(mode) + self.content_bytes = norm_content_bytes(content) + + def print(self ,* ,label: str|None=None ,file=None)-> None: + if file is None: + import sys as _sys + file = _sys.stdout + dpath = self.dpath_str or "?" + fname = self.fname or "?" + owner = self.owner_name_str or "?" + mode_str = f"{self.mode_int:04o}" if isinstance(self.mode_int ,int) else (self.mode_octal_str or "?") + size = len(self.content_bytes) if isinstance(self.content_bytes ,(bytes ,bytearray)) else 0 + prefix = (label + ": ") if label else "" + print(f"{prefix}dpath={dpath} fname={fname} owner={owner} mode={mode_str} bytes={size}" ,file=file) + +# ===== Planner ===== + +class Planner: + """ + Planner — constructs a Journal of Commands from config scripts. + + Given provenance (PlanProvenance) and optional default WriteFileMeta. + Does resolve command parameters by precedence: kwarg > per-call WriteFileMeta > planner default, + with a final filename fallback to provenance basename if still missing. + On any argument error, returns the Command with errors and DOES NOT append it to Journal. + Returns live Journal via journal(). + """ + __slots__ = ( + "_prov" + ,"_defaults" + ,"_journal" + ) + + def __init__(self ,provenance: PlanProvenance ,defaults: WriteFileMeta|None=None)-> None: + self._prov = provenance + self._defaults = defaults if defaults is not None else WriteFileMeta( + dpath="/" + ,fname=provenance.read_fname + ,owner="root" + ,mode=0o444 + ,content=None + ) + self._journal = Journal() + self._journal.set_meta( + stage_root_dpath_str=str(self._prov.stage_root_dpath) + ,config_rel_fpath_str=self._prov.config_rel_fpath.as_posix() + ) + + # --- provenance/defaults/journal access --- + + def set_provenance(self ,prov: PlanProvenance)-> None: + self._prov = prov + + def set_defaults(self ,defaults: WriteFileMeta)-> None: + self._defaults = defaults + + def defaults(self)-> WriteFileMeta: + return self._defaults + + def journal(self)-> Journal: + return self._journal + + # --- resolution helpers --- + + def _pick(self ,kw ,meta_attr ,default_attr): + "Pick first non-None among kw ,meta_attr ,default_attr." + return kw if kw is not None else (meta_attr if meta_attr is not None else default_attr) + + def _resolve_write_file(self ,wfm ,dpath ,fname)-> tuple[str|None ,str|None]: + # normalize explicit kwargs + dpath_str = norm_dpath_str(dpath) if dpath is not None else None + fname_str = norm_fname_or_none(fname) if fname is not None else None + + # precedence: kwarg > per-call meta > planner default + dpath_val = self._pick(dpath_str ,(wfm.dpath_str if wfm else None) ,self._defaults.dpath_str) + fname_val = self._pick(fname_str ,(wfm.fname if wfm else None) ,self._defaults.fname) + + # final fallback for filename: derive from config name + if fname_val is None: + fname_val = self._prov.read_fname + + # anchor/normalize dpath + if dpath_val is not None: + p = Path(dpath_val) + if not p.is_absolute(): + p = (self._prov.cwd_dpath/p) + dpath_val = p.resolve().as_posix() + + return dpath_val ,fname_val + + def _resolve_owner_mode_content(self + ,wfm: WriteFileMeta|None + ,owner: str|None + ,mode: int|str|None + ,content: bytes|str|None + )-> tuple[str|None ,tuple[int|None ,str|None] ,bytes|None]: + owner_norm = norm_nonempty_owner(owner) if owner is not None else None + mode_norm = parse_mode(mode) if mode is not None else (None ,None) + content_b = norm_content_bytes(content) if content is not None else None + + owner_v = self._pick(owner_norm ,(wfm.owner_name_str if wfm else None) ,self._defaults.owner_name_str) + mode_v = (mode_norm if mode_norm != (None ,None) else + ((wfm.mode_int ,wfm.mode_octal_str) if wfm else (self._defaults.mode_int ,self._defaults.mode_octal_str))) + content_v = self._pick(content_b ,(wfm.content_bytes if wfm else None) ,self._defaults.content_bytes) + return owner_v ,mode_v ,content_v + + # --- printing --- + + def print(self ,* ,show_journal: bool=True ,file=None)-> None: + if file is None: + import sys as _sys + file = _sys.stdout + + print("== Provenance ==" ,file=file) + self._prov.print(file=file) + + print("\n== Defaults ==" ,file=file) + self._defaults.print(label="defaults" ,file=file) + + if show_journal: + entries = getattr(self._journal ,"command_list" ,[]) + n_total = len(entries) + n_copy = sum(1 for c in entries if getattr(c ,"name_str" ,None) == "copy") + n_disp = sum(1 for c in entries if getattr(c ,"name_str" ,None) == "displace") + n_del = sum(1 for c in entries if getattr(c ,"name_str" ,None) == "delete") + + print("\n== Journal ==" ,file=file) + print(f"entries: {n_total} copy:{n_copy} displace:{n_disp} delete:{n_del}" ,file=file) + if n_total: + self._journal.print(index_start=1 ,file=file) + else: + print("(plan is empty)" ,file=file) + + # --- Command builders (first arg may be WriteFileMeta) --- + + def copy(self + ,wfm: WriteFileMeta|None=None + ,* + ,write_file_dpath: str|Path|None=None + ,write_file_fname: str|None=None + ,owner: str|None=None + ,mode: int|str|None=None + ,content: bytes|str|None=None + )-> Command: + cmd = Command("copy") + dpath ,fname = self._resolve_write_file(wfm ,write_file_dpath ,write_file_fname) + owner_v ,(mode_int ,mode_oct) ,content_b = self._resolve_owner_mode_content(wfm ,owner ,mode ,content) + + # well-formed checks + if not is_abs_dpath(dpath): cmd.add_error("write_file_dpath must be absolute") + if norm_fname_or_none(fname) is None: cmd.add_error("write_file_fname must be a bare filename") + if not owner_v: cmd.add_error("owner must be non-empty") + if (mode_int ,mode_oct) == (None ,None): + cmd.add_error("mode must be int <= 0o7777 or 3/4-digit octal string") + if content_b is None: + cmd.add_error("content is required for copy() (bytes or str)") + + cmd.arg_dict.update({ + "write_file_dpath_str": dpath + ,"write_file_fname": fname + ,"owner_name": owner_v + ,"mode_int": mode_int + ,"mode_octal_str": mode_oct + ,"content_bytes": content_b + ,"provenance_config_rel_fpath_str": self._prov.config_rel_fpath.as_posix() + }) + + if not cmd.errors_list: + self._journal.append(cmd) + return cmd + + def displace(self + ,wfm: WriteFileMeta|None=None + ,* + ,write_file_dpath: str|Path|None=None + ,write_file_fname: str|None=None + )-> Command: + cmd = Command("displace") + dpath ,fname = self._resolve_write_file(wfm ,write_file_dpath ,write_file_fname) + if not is_abs_dpath(dpath): cmd.add_error("write_file_dpath must be absolute") + if norm_fname_or_none(fname) is None: cmd.add_error("write_file_fname must be a bare filename") + cmd.arg_dict.update({ + "write_file_dpath_str": dpath + ,"write_file_fname": fname + }) + if not cmd.errors_list: + self._journal.append(cmd) + return cmd + + def delete(self + ,wfm: WriteFileMeta|None=None + ,* + ,write_file_dpath: str|Path|None=None + ,write_file_fname: str|None=None + )-> Command: + cmd = Command("delete") + dpath ,fname = self._resolve_write_file(wfm ,write_file_dpath ,write_file_fname) + if not is_abs_dpath(dpath): cmd.add_error("write_file_dpath must be absolute") + if norm_fname_or_none(fname) is None: cmd.add_error("write_file_fname must be a bare filename") + cmd.arg_dict.update({ + "write_file_dpath_str": dpath + ,"write_file_fname": fname + }) + if not cmd.errors_list: + self._journal.append(cmd) + return cmd diff --git a/release/python3/executor_inner.py b/release/python3/executor_inner.py index 3c2ea1a..433b705 100755 --- a/release/python3/executor_inner.py +++ b/release/python3/executor_inner.py @@ -379,6 +379,7 @@ def run_executor_inner( # --- main stays a thin arg wrapper ------------------------------------------ def main(argv: list[str]|None=None)-> int: + ap = argparse.ArgumentParser( prog="executor_inner.py" ,description="Man_In_Gray inner executor (decode → validate → apply)" @@ -391,13 +392,26 @@ def main(argv: list[str]|None=None)-> int: ap.add_argument("--phase-2-validity-then-stop" ,action="store_true" ,help="stop after validity checks") ap.add_argument("--phase-2-sanity2-then-stop" ,action="store_true" ,help="stop after sanity-2 checks") + ap.add_argument("--plan" ,default="" ,help="path to CBOR plan file or '-' for stdin") + ap.add_argument("--plan-fd" ,type=int ,default=-1 ,help=argparse.SUPPRESS) + args = ap.parse_args(argv) # load plan try: - data = Path(args.plan).read_bytes() + if args.plan_fd >= 0: + import os as _os + data = _os.read(args.plan_fd ,1<<30) + elif args.plan == "-": + import sys as _sys + data = _sys.stdin.buffer.read() + elif args.plan: + data = Path(args.plan).read_bytes() + else: + print("error: either --plan or --plan-fd is required" ,file=sys.stderr) + return 2 except Exception as e: - print(f"error: failed to read plan file: {e}" ,file=sys.stderr) + print(f"error: failed to read plan: {e}" ,file=sys.stderr) return 2 try: diff --git a/release/x86_64/man_in_grey_apply b/release/x86_64/man_in_grey_apply index 09bd80da16b6cc9fb12018a4f2bb0f7f296b0267..ee431abde30d5d8d74318972d771c04085608d04 100755 GIT binary patch literal 17232 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 exit 2; } + +# ensure tester won’t hit privileged gasket refusal: +ARCH_RAW=$(uname -m | tr '[:upper:]' '[:lower:]') +case "$ARCH_RAW" in + amd64|x64) ARCH="x86_64" ;; + x86_64) ARCH="x86_64" ;; + i386|i486|i586|i686) ARCH="i686" ;; + arm64|aarch64) ARCH="aarch64" ;; + armv7l) ARCH="armv7l" ;; + armv6l) ARCH="armv6l" ;; + riscv64) ARCH="riscv64" ;; + ppc64le|powerpc64le) ARCH="ppc64le" ;; + s390x) ARCH="s390x" ;; + *) ARCH="$ARCH_RAW" ;; +esac +GASKET="$REPO_HOME/release/$ARCH/man_in_grey_apply" + +if [[ -x "$GASKET" && -u "$GASKET" ]]; then + echo "⚠️ Gasket is blessed (setuid-root) but tester is sudo-less." + echo " Run: sudo ./tool/unbless" + exit 1 +fi + +# fresh output dir +rm -rf -- "$OUT" +mkdir -p "$OUT" + +echo "▶️ Running Man_In_Grey on tester/$STAGE → $OUT" +# Run planner → CBOR → apply (unprivileged). Default filter will be emitted in CWD if missing. +( cd "$TESTER_DIR" && \ + "$ENTRY" \ + --stage "$STAGE" \ + --phase-1-print \ + --phase-2-print ) + +echo "✅ Apply finished. Verifying…" + +fail=0 + +# expected artifacts (from your sample stage) +chk() { + local path="$1" desc="$2" + if [[ -f "$path" ]]; then + echo " ✓ $desc ($path)" + else + echo " ✗ $desc missing ($path)"; fail=1 + fi +} + +# files to expect +chk "$OUT/unbound_conf" "DNS base file" +chk "$OUT/net/unbound.conf" "DNS net override" +chk "$OUT/web/nginx.conf" "web nginx.conf" + +# content spot checks (adjust to your test content) +if [[ -f "$OUT/net/unbound.conf" ]]; then + grep -q 'verbosity: 1' "$OUT/net/unbound.conf" \ + && echo " ✓ verbosity content OK" \ + || { echo " ✗ expected 'verbosity: 1' in net/unbound.conf"; fail=1; } +fi + +if [[ -f "$OUT/web/nginx.conf" ]]; then + grep -q 'listen 8080' "$OUT/web/nginx.conf" \ + && echo " ✓ nginx listen OK" \ + || { echo " ✗ expected 'listen 8080' in web/nginx.conf"; fail=1; } +fi + +# mode spot check (0444 example; chmod prints in octal differently across distros, use stat) +if [[ -f "$OUT/unbound_conf" ]]; then + mode=$(stat -c '%a' "$OUT/unbound_conf" 2>/dev/null || stat -f '%Lp' "$OUT/unbound_conf") + [[ "$mode" == "444" ]] \ + && echo " ✓ mode unbound_conf is 0444" \ + || { echo " ⚠︎ mode unbound_conf is $mode (expected 444)"; :; } +fi + +[[ $fail -eq 0 ]] && echo "🎉 test_0 PASS" || { echo "❌ test_0 FAIL"; exit 1; } diff --git a/tester/stage_test_0/DNS/unbound.conf.py b/tester/test_0/stage_test_0/DNS/unbound.conf.py similarity index 100% rename from tester/stage_test_0/DNS/unbound.conf.py rename to tester/test_0/stage_test_0/DNS/unbound.conf.py diff --git a/tester/stage_test_0/unbound_conf.py b/tester/test_0/stage_test_0/unbound_conf.py similarity index 100% rename from tester/stage_test_0/unbound_conf.py rename to tester/test_0/stage_test_0/unbound_conf.py diff --git a/tester/stage_test_0/web/site_conf.py b/tester/test_0/stage_test_0/web/site_conf.py similarity index 100% rename from tester/stage_test_0/web/site_conf.py rename to tester/test_0/stage_test_0/web/site_conf.py diff --git a/tester/stage_test_0_out/.githolder b/tester/test_0/stage_test_0_out/.githolder similarity index 100% rename from tester/stage_test_0_out/.githolder rename to tester/test_0/stage_test_0_out/.githolder diff --git a/tester/tool/env b/tester/tool/env new file mode 100644 index 0000000..de541e8 --- /dev/null +++ b/tester/tool/env @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +# tester-authored custom environment (optional) +script_afp=$(realpath "${BASH_SOURCE[0]}") +if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then + echo "$script_afp:: This script must be sourced, not executed." + exit 1 +fi + +# Normalize machine arch to our release dir names +_arch_raw=$(uname -m | tr '[:upper:]' '[:lower:]') +case "$_arch_raw" in + amd64|x64) _arch="x86_64" ;; + x86_64) _arch="x86_64" ;; + i386|i486|i586|i686) _arch="i686" ;; + arm64|aarch64) _arch="aarch64" ;; + armv7l) _arch="armv7l" ;; + armv6l) _arch="armv6l" ;; + riscv64) _arch="riscv64" ;; + ppc64le|powerpc64le) _arch="ppc64le" ;; + s390x) _arch="s390x" ;; + *) _arch="$_arch_raw" ;; +esac +export ARCH="$_arch" + +# Handy convenience paths (optional) +export REL_SHELL="$RELEASE/shell" +export REL_PY="$RELEASE/python3" +export REL_ARCH="$RELEASE/$ARCH" diff --git a/tool/bless b/tool/bless new file mode 100755 index 0000000..333efd2 --- /dev/null +++ b/tool/bless @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +set -euo pipefail +script_afp=$(realpath "${BASH_SOURCE[0]}") +REPO_HOME="$(cd "$(dirname "$script_afp")/.." && pwd -P)" + +if [[ $EUID -ne 0 ]]; then + echo "must be run as root (sudo)"; exit 1 +fi + +arch_raw=$(uname -m | tr '[:upper:]' '[:lower:]') +case "$arch_raw" in + amd64|x64) arch="x86_64" ;; + x86_64) arch="x86_64" ;; + i386|i486|i586|i686) arch="i686" ;; + arm64|aarch64) arch="aarch64" ;; + armv7l) arch="armv7l" ;; + armv6l) arch="armv6l" ;; + riscv64) arch="riscv64" ;; + ppc64le|powerpc64le) arch="ppc64le" ;; + s390x) arch="s390x" ;; + *) arch="$arch_raw" ;; +esac + +GASKET="${REPO_HOME}/release/${arch}/man_in_grey_apply" +if [[ ! -x "$GASKET" ]]; then + echo "gasket missing: $GASKET (run developer/tool/release first)"; exit 2 +fi + +chown root:root "$GASKET" +chmod 4755 "$GASKET" + +echo "blessed: $GASKET" +ls -l "$GASKET" +echo "flags:" +"$GASKET" --print-flags || true diff --git a/tool/bless_release b/tool/bless_release deleted file mode 100644 index a6f387e..0000000 --- a/tool/bless_release +++ /dev/null @@ -1,47 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -# bless_release — elevate released artifacts so privileged apply is possible -# usage: bless_release [arch] -# default arch = normalized uname -m → {x86_64,i686,aarch64,armv7l,armv6l,riscv64,ppc64le,s390x} - -if [[ "${EUID:-$(id -u)}" -ne 0 ]]; then - echo "error: bless_release must run as root" >&2 - exit 2 -fi - -SELF_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)" -REPO_HOME="$(CDPATH= cd -- "$SELF_DIR/../.." && pwd)" -REL_DIR="${REPO_HOME}/release" - -# normalize arch -raw="$(uname -m | tr '[:upper:]' '[:lower:]')" -case "$raw" in - amd64|x64) arch="x86_64" ;; - x86_64) arch="x86_64" ;; - i386|i486|i586|i686) arch="i686" ;; - arm64|aarch64) arch="aarch64" ;; - armv7l) arch="armv7l" ;; - armv6l) arch="armv6l" ;; - riscv64) arch="riscv64" ;; - ppc64le|powerpc64le) arch="ppc64le" ;; - s390x) arch="s390x" ;; - *) arch="$raw" ;; -esac -[[ $# -ge 1 ]] && arch="$1" - -GASKET="${REL_DIR}/${arch}/man_in_grey_apply" -INNER_PY="${REL_DIR}/python3/executor_inner.py" - -# sanity checks -[[ -x "$GASKET" ]] || { echo "error: gasket not found/executable: $GASKET" >&2; exit 2; } -[[ -f "$INNER_PY" ]] || { echo "error: inner executor missing: $INNER_PY" >&2; exit 2; } - -# set ownership/mode -chown root:root "$GASKET" "$INNER_PY" -chmod 4755 "$GASKET" # setuid root -chmod 0755 "$INNER_PY" # root-owned, not setuid - -echo "blessed:" -echo " gasket: $GASKET (root:root, 4755)" -echo " inner : $INNER_PY (root:root, 0755)" diff --git a/tool/unbless b/tool/unbless new file mode 100755 index 0000000..74ee239 --- /dev/null +++ b/tool/unbless @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +set -euo pipefail +SELF_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)" +REPO_HOME="$(CDPATH= cd -- "$SELF_DIR/.." && pwd)" + +if [[ $EUID -ne 0 ]]; then + echo "must be run as root (sudo)"; exit 1 +fi + +raw="$(uname -m | tr '[:upper:]' '[:lower:]')" +case "$raw" in + amd64|x64) arch="x86_64" ;; x86_64) arch="x86_64" ;; + i386|i486|i586|i686) arch="i686" ;; + arm64|aarch64) arch="aarch64" ;; + armv7l) arch="armv7l" ;; armv6l) arch="armv6l" ;; + riscv64) arch="riscv64" ;; ppc64le|powerpc64le) arch="ppc64le" ;; + s390x) arch="s390x" ;; *) arch="$raw" ;; +esac + +GASKET="${REPO_HOME}/release/${arch}/man_in_grey_apply" +[[ -e "$GASKET" ]] || { echo "not found: $GASKET"; exit 2; } + +# default target owner to the user who owns REPO_HOME +OWNER="$(stat -c '%U' "$REPO_HOME")" +GROUP="$(stat -c '%G' "$REPO_HOME")" + +chmod 0755 "$GASKET" +chown "$OWNER:$GROUP" "$GASKET" +echo "unblessed: $GASKET" +ls -l "$GASKET" diff --git a/tool_shared/bespoke/env b/tool_shared/bespoke/env index eb93ff3..88f8564 100644 --- a/tool_shared/bespoke/env +++ b/tool_shared/bespoke/env @@ -7,6 +7,7 @@ fi # without this bash takes non-matching globs literally shopt -s nullglob +umask 022 # -------------------------------------------------------------------------------- # project definition -- 2.20.1