#!/usr/bin/env python3
"""Anchored builder: provision-rig.sh (FROZEN) -> provision-rig-r2.sh.

Asserts the source sha256 first, then asserts every anchor matches EXACTLY ONCE
before replacing it. A drifted anchor fails the build rather than landing half
an edit. The source is READ, never written.

Correction being applied (doyle 23YE7ZXG, refined OCTAXWYQ):
  - staging goes through the supported maintainer verb `xtask debug-rollout
    --stage-dir`, not a hand placement in the release cache's layout;
  - the trust overlay goes through `xtask debug-pin --home`;
  - the signing seed is inherited by the SIGNING SUBPROCESS ONLY, never passed
    onward to apply/daemon/capture, and never appears in an argument;
  - the genuine apply/promotion STAYS. `xtask debug-mark-applied` exists and is
    NOT used: doyle ruled a supported command that writes Applied still does not
    demonstrate the transition this rig must exercise.
"""
import hashlib
import pathlib
import sys

HERE = pathlib.Path(__file__).resolve().parent
SRC = HERE / "provision-rig.sh"
DST = HERE / "provision-rig-r2.sh"
SRC_SHA = "7add2d4ad9be0419d2e18bcd1a6121d9ac19a02c401407d912331b9b4b450bae"

raw = SRC.read_bytes()
got = hashlib.sha256(raw).hexdigest()
if got != SRC_SHA:
    sys.exit(f"SOURCE_DRIFT: {SRC.name} is {got}, pin says {SRC_SHA}")
# The destination guard protects EVIDENCE. An unexecuted build product of THIS builder is not
# evidence, so a rebuild is allowed only when the existing destination is byte-identical to a
# build this builder itself recorded — anything else is somebody's attempt and is refused.
PRIOR_BUILDS = {"1f0eb59fcf17f7e4415e056376e57ab25ab8c90257f86222f77c6beed8cc902f"}
if DST.exists():
    have = hashlib.sha256(DST.read_bytes()).hexdigest()
    if have not in PRIOR_BUILDS:
        sys.exit(f"DESTINATION_EXISTS: {DST.name} is {have}, which this builder did not produce; it is some attempt's evidence and is not overwritten")
    print(f"REBUILD over this builder's own unexecuted output {have}")
text = raw.decode("utf-8")

EDITS = []


def edit(name, old, new):
    EDITS.append((name, old, new))


# ---- 1. the "never handles a signing secret" claim is no longer true --------
edit(
    "header-secret-claim",
    """#   - It never handles a signing secret. It consumes an ALREADY-SIGNED set and a PUBLIC key
#     hex. Where the secret lives, and how it is destroyed, is specified separately and is not
#     this file's business; nothing here may print, copy or archive one.""",
    """#   - It never hand-writes the applied record, and it does not use the maintainer verb that
#     would ('xtask debug-mark-applied', which records Applied for a hand-staged set). doyle
#     ruled OCTAXWYQ: a supported command that WRITES Applied still does not demonstrate the
#     transition this rig exists to exercise. The genuine apply/promotion stays.
#
# THE ONE CLAIM THIS REVISION WITHDRAWS. The predecessor said it never handles a signing
# secret, because it consumed an already-signed set. The supported staging verb SIGNS: xtask's
# debug_signing_identity() reads SPT_DEBUG_RELEASE_SEED from the environment. So the seed must
# be present in THIS script's environment, and that is stated here rather than hidden. What the
# script does with it is bounded and measurable:
#   - the seed is read ONCE at preflight into a shell variable and IMMEDIATELY unset from the
#     environment, so no later child — apply, daemon, capture — can inherit it (doyle OCTAXWYQ);
#   - it is exported for the SIGNING CALL ONLY, and unset again on the next line;
#   - it is NEVER an argument: an argument is visible in the process table to every other
#     process on the box. It is never printed, never written to evidence, never archived, and
#     the absence of the variable is ASSERTED before the apply rather than assumed.""",
)

# ---- 2. the pins: no pre-signed set exists on the supported route -----------
edit(
    "pins",
    """SET_FILE=''            # the already-signed SignedUpdateSet json (metadata_json + signature_hex)
SET_SHA=''             # its pinned sha256
SET_VERSION=''         # the version the signed metadata carries, stated here so a mismatch is
                       # a refusal rather than a discovery""",
    """# SET_FILE and SET_SHA ARE GONE. On the supported route no signed set pre-exists as a file to
# copy: 'xtask debug-rollout' MINTS and signs one from the artifact it is given. There is
# nothing to hash and nothing to place, so a pin for it would be a pin for a file this script
# never sees.
SET_VERSION=''         # --version for the staging verb. PINNED rather than left to the verb's
                       # own monotonic sequence, so the staged version is stated here and a
                       # mismatch downstream is a refusal rather than a discovery
XTASK_BIN=''           # the PREBUILT maintainer xtask executable that stages. A prebuilt binary
                       # and not 'cargo run -p xtask' on purpose: a cargo invocation inside
                       # provisioning would build into some tree's target/ and take a build-cache
                       # pool (AGENTS.md, releases#103) as a side effect of provisioning
XTASK_SHA=''           # its pinned sha256. These bytes decide what gets staged and signed, so
                       # they are pinned like every other subject in this run""",
)

edit(
    "pin-loop",
    """for p in RIG_ROOT SUBJECT_EXE SUBJECT_SHA SET_FILE SET_SHA SET_VERSION HOST_TRIPLE \\
         TRUST_KEY_ID TRUST_KEY_HEX FLEET_EXE WORKTREE_EXE; do""",
    """for p in RIG_ROOT SUBJECT_EXE SUBJECT_SHA SET_VERSION XTASK_BIN XTASK_SHA HOST_TRIPLE \\
         TRUST_KEY_ID TRUST_KEY_HEX FLEET_EXE WORKTREE_EXE; do""",
)

# ---- 3. preflight: the set-file checks become tool + seed checks ------------
edit(
    "preflight-setfile-exists",
    """[ -f "$SET_FILE" ]    || refuse "the signed set $SET_FILE does not exist\"""",
    """[ -f "$XTASK_BIN" ]   || refuse "the staging tool $XTASK_BIN does not exist\"""",
)

edit(
    "preflight-setfile-secret-grep",
    """grep -qi 'private\\|secret\\|seed' "$SET_FILE" && refuse "the signed set file mentions private key material; refusing to copy it anywhere\"""",
    """# THE SIGNING SEED: read once, then removed from the environment. Everything after this line
# runs WITHOUT it, and the staging call re-supplies it for its own subprocess only.
SIGN_SEED="${SPT_DEBUG_RELEASE_SEED:-}"
unset SPT_DEBUG_RELEASE_SEED
[ -n "$SIGN_SEED" ] || refuse "SPT_DEBUG_RELEASE_SEED is not in the environment. The supported staging verb signs, so provisioning cannot stage without it. It is an ENVIRONMENT input on purpose: a pin would put a secret in a file, and an argument would put it in the process table."
case "$SIGN_SEED" in
  *[!0-9a-fA-F]*) refuse "SPT_DEBUG_RELEASE_SEED is not hex. Its VALUE is not reported here, and no refusal in this script ever prints it." ;;
esac
[ "${#SIGN_SEED}" -eq 64 ] || refuse "SPT_DEBUG_RELEASE_SEED is ${#SIGN_SEED} characters; the seed xtask derives an identity from is 32 bytes of hex. The value is not reported."
say "SIGNING_SEED present in the environment, 64 hex characters, VALUE NOT REPORTED. It has been unset from this script's environment and is re-exported for the staging subprocess alone."
for a in "$@"; do
  [ "$a" = "$SIGN_SEED" ] && refuse "a secret value was passed as an argument to this script. An argument is readable from the process table by every other process on this box."
done""",
)

edit(
    "preflight-setfile-sha",
    """s=$(sha256sum "$SET_FILE" | cut -d' ' -f1)
[ "$s" = "$SET_SHA" ] || refuse "the signed set hashes to $s and the pin says $SET_SHA"
grep -q "$SET_VERSION" "$SET_FILE" || refuse "the signed set does not mention version $SET_VERSION — a version stated in a pin and absent from the bytes is a disagreement, not a detail\"""",
    """s=$(sha256sum "$XTASK_BIN" | cut -d' ' -f1)
[ "$s" = "$XTASK_SHA" ] || refuse "the staging tool hashes to $s and the pin says $XTASK_SHA: different bytes are a different tool, and this tool signs"
case "$SET_VERSION" in
  '' | *[!0-9]*) refuse "SET_VERSION is not a plain integer. The staging verb's --version is a u64 counter, and a version this script cannot state as a number it cannot pin." ;;
esac""",
)

# ---- 4. the layout the VERBS create, not the rig ----------------------------
edit(
    "rig-layout",
    """mkdir -p "$RIG_ROOT/bin" "$RIG_ROOT/home/identity" "$RIG_ROOT/home/releases/artifacts" "$RIG_ROOT/evidence" \\
  || fail "could not create the rig layout\"""",
    """# ONLY what this script itself owns. identity/ and releases/artifacts/ are NOT pre-created:
# the supported verbs create their own (debug-pin does create_dir_all on identity, and
# stage_update_set on releases/artifacts), so leaving them absent keeps 'the verb made this'
# observable instead of pre-staging a shape that would look the same either way.
mkdir -p "$RIG_ROOT/bin" "$RIG_ROOT/home" "$RIG_ROOT/evidence" \\
  || fail "could not create the rig layout\"""",
)

# ---- 5. step 3: the trust overlay through its own supported verb ------------
edit(
    "trust-root",
    """# ---- 3. the home's trust root: ONE key ADDED, nothing revoked ----------------
# A `revoked` entry would void a builtin key id and that IS a weakening. This file adds.
printf '{"keys":{"%s":"%s"}}\\n' "$TRUST_KEY_ID" "$TRUST_KEY_HEX" > "$H/identity/release-keys.json" \\
  || fail "could not write the rig trust root"
say "TRUST_ROOT $H/identity/release-keys.json adds key_id=$TRUST_KEY_ID and revokes nothing (builtin keys keep their standing)"
rollout_state TRUST_WRITTEN "$TRUST_KEY_ID\"""",
    """# ---- 3. the home's trust root, written by the SUPPORTED VERB -----------------
# 'xtask debug-pin --home' is the documented maintainer route (docs/DEBUG-ROLLOUT.md, one-time
# lab setup). It ADDS the key to identity/release-keys.json, leaves any existing keys alone,
# writes an empty revoked list only when none exists, and sets the home's channel to debug.
# A revoked entry would void a builtin key id and that IS a weakening; the verb adds.
# The predecessor hand-wrote this file. It no longer does: a second convention for a file the
# product's own tooling already writes is exactly the drift doyle rejected.
TRUST_FILE="$H/identity/release-keys.json"
[ -e "$TRUST_FILE" ] && fail "a trust root already exists at $TRUST_FILE inside a root this script just created exclusively; it is not overwritten"
STAGE_DEADLINE=$(( $(date +%s) + STAGE_BUDGET_S ))
bounded debug_pin "$STAGE_DEADLINE" "$EVID/debug-pin.out" "$EVID/debug-pin.err" -- \\
  env SPT_HOME="$H" "$XTASK_BIN" debug-pin --home "$H" --key-id "$TRUST_KEY_ID" --public-key "$TRUST_KEY_HEX"
prc=$?
say "DEBUG_PIN native_exit=$prc (preserved verbatim in evidence/debug-pin.out and debug-pin.err)"
[ "$prc" -eq 0 ] || fail "debug-pin did not return zero (exit $prc). Nothing is hand-written in its place."
[ -f "$TRUST_FILE" ] || fail "debug-pin returned zero and $TRUST_FILE does not exist: the verb's exit is not the state of the evidence"
grep -q "$TRUST_KEY_ID" "$TRUST_FILE" || fail "the trust root does not name key_id=$TRUST_KEY_ID"
cp -p "$TRUST_FILE" "$EVID/release-keys.json"
say "TRUST_ROOT $TRUST_FILE names key_id=$TRUST_KEY_ID, written by debug-pin, and revokes nothing (builtin keys keep their standing)"
rollout_state TRUST_WRITTEN "$TRUST_KEY_ID\"""",
)

# ---- 6. step 4: staging through the supported verb --------------------------
edit(
    "staging",
    """# ---- 4. STAGING: the placement a fetch would have made ----------------------
# THE PRODUCT HAS NO LOCAL-STAGE VERB. `spt update` offers Apply, Fetch (GitHub origin only)
# and Adapters — so a rig-signed set cannot be staged through a verb, and this script places
# the bytes in the cache layout instead: release.json (the signed record) and
# artifacts/<triple>.bin (relcache.rs:35-50, :266-283).
#
# THIS IS A PLACEMENT, NOT A TRUST DECISION. Nothing here is verified by this script and
# nothing here asserts that it is valid: the product re-verifies these exact bytes twice, at
# apply and again at the set gate, under the home's own policy. What it DOES couple to is the
# cache's on-disk layout, and that coupling is stated rather than hidden: if those constants
# move, this step must move with them.
cp -p "$SET_FILE" "$H/releases/release.json" || fail "could not place the signed set"
cp -p "$RIG_EXE" "$H/releases/artifacts/$HOST_TRIPLE.bin" || fail "could not place the host artifact"
rm -f "$H/releases/artifact.bin" "$H/releases/platform.json" 2>/dev/null
say "STAGED release.json + artifacts/$HOST_TRIPLE.bin — bytes the product verifies for itself; this script asserts nothing about them"
rollout_state STAGED "$SET_VERSION\"""",
    """# ---- 4. STAGING, THROUGH THE SUPPORTED MAINTAINER VERB ----------------------
# THE CORRECTION (doyle 23YE7ZXG). The predecessor claimed the product has no local-stage verb
# and placed release.json + artifacts/<triple>.bin into the release cache's layout itself. That
# claim was WRONG and the placement is gone. 'spt update' — the PUBLIC surface — indeed has no
# local-stage subcommand, but the MAINTAINER surface does: 'xtask debug-rollout --stage-dir'
# (docs/DEBUG-ROLLOUT.md), which signs a set over the artifact it is given, verifies it, and
# stages it through ReleaseCache::stage_update_set. That function writes exactly what this
# script used to write by hand — the signed record, artifacts/<platform>.bin, and the removal
# of a stale single-artifact stamp — atomically, and it is the product's own code.
#
# So there is no cache-layout coupling in this script any more. It does not know the file names.
#
# WHAT THIS CALL DOES THAT THE PLACEMENT DID NOT: it SIGNS. The seed is exported for this one
# subprocess and unset on the next line; it is never an argument, and --key-id names the key
# only. Everything after this line runs without the seed in its environment, and that absence
# is asserted rather than assumed.
#
# --artifact pins the host triple to the RIG's copy, so the signed digest is the copy's digest.
# --state keeps the verb's debug sequence inside the rig: its default is a repo target/ path,
# and provisioning has no business writing into a build tree.
# --build-current is deliberately NOT passed: it would run cargo and take a build-cache pool.
STAGE_DEADLINE=$(( $(date +%s) + STAGE_BUDGET_S ))
export SPT_DEBUG_RELEASE_SEED="$SIGN_SEED"
bounded debug_rollout "$STAGE_DEADLINE" "$EVID/stage.out" "$EVID/stage.err" -- \\
  env SPT_HOME="$H" "$XTASK_BIN" debug-rollout \\
      --stage-dir "$H/releases" \\
      --state "$RIG_ROOT/debug-rollout-state.json" \\
      --version "$SET_VERSION" \\
      --key-id "$TRUST_KEY_ID" \\
      --artifact "$HOST_TRIPLE=$RIG_EXE"
src=$?
unset SPT_DEBUG_RELEASE_SEED
unset SIGN_SEED
say "DEBUG_ROLLOUT native_exit=$src (preserved verbatim in evidence/stage.out and stage.err)"
rollout_state STAGE_RETURNED "$src"
[ "$src" -eq 0 ] || fail "the staging verb did not return zero (exit $src). Nothing is placed by hand in its place: a hand placement is the convention this revision exists to remove."
# THE EXIT IS NOT THE STATE OF THE EVIDENCE. The staged set is observed, by the verb's own
# stdout and on disk, and the version it reports is compared with the pin.
grep -q "DEBUG_ROLLOUT_STAGED version=$SET_VERSION" "$EVID/stage.out" \\
  || fail "the staging verb returned zero and its output does not report version=$SET_VERSION: [$(tr -d '\\r' < "$EVID/stage.out" | tail -3 | tr '\\n' ' ')]"
[ -d "$H/releases" ] || fail "the staging verb returned zero and $H/releases does not exist"
( cd "$H/releases" && find . -type f -print0 | xargs -0 sha256sum ) > "$EVID/staged-files.sha256" 2>/dev/null \\
  || fail "could not record what was staged"
[ -s "$EVID/staged-files.sha256" ] || fail "nothing was staged under $H/releases: an empty listing is not a staged set"
say "STAGED version=$SET_VERSION by debug-rollout; every staged file is hashed in evidence/staged-files.sha256. This script named no cache file and asserts nothing about the bytes' validity: the product re-verifies them at apply and again at the set gate, under this home's own policy."
rollout_state STAGED "$SET_VERSION\"""",
)

# ---- 7. the seed's absence is ASSERTED before the apply --------------------
edit(
    "apply-seed-absent",
    """APPLY_DEADLINE=$(( $(date +%s) + APPLY_BUDGET_S ))""",
    """# doyle OCTAXWYQ: the seed reaches the SIGNING subprocess and nothing else. Measured here,
# not claimed: if it is still in the environment, the apply does not run.
[ -z "${SPT_DEBUG_RELEASE_SEED:-}" ] || fail "the signing seed is still in the environment at the apply. It must not be inherited by apply, daemon or capture processes."
[ -z "${SIGN_SEED:-}" ] || fail "the signing seed is still held in a shell variable at the apply"
say "SEED_SCOPE verified: SPT_DEBUG_RELEASE_SEED is absent from this script's environment before the apply, so no process from here on can inherit it"
APPLY_DEADLINE=$(( $(date +%s) + APPLY_BUDGET_S ))""",
)

edit(
    "stage-budget-pin",
    """APPLY_BUDGET_S=300     # the apply/promotion lifecycle's bound""",
    """STAGE_BUDGET_S=120     # the two maintainer verbs' bound. No cargo runs inside them (XTASK_BIN
                       # is prebuilt), so this is a tool invocation, not a build
APPLY_BUDGET_S=300     # the apply/promotion lifecycle's bound""",
)

# ---- 8. the residue paragraph's withdrawn claim ----------------------------
edit(
    "residue-secret-claim",
    """say "  explicitly in the manifest so its destruction is a measured act rather than an implication,"
say "  and the signing SECRET was never an input to this script: its disposal is specified apart.\"""",
    """say "  explicitly in the manifest so its destruction is a measured act rather than an implication."
say "  THE SIGNING SEED was an ENVIRONMENT input to this run, for the staging subprocess alone. It"
say "  is in no file this script wrote, in no argument, and in no line of this transcript; it was"
say "  unset before the apply and that absence was asserted, not assumed. Its disposal in the"
say "  environment it came from is specified apart and is not this script's act.\"""",
)

# ---- 9. a stale carried-forward sentence: HOST_TRIPLE is no longer a filename
edit(
    "host-triple-comment",
    """HOST_TRIPLE=''         # e.g. x86_64-pc-windows-msvc — the artifacts/<triple>.bin name""",
    """HOST_TRIPLE=''         # e.g. x86_64-pc-windows-msvc — the TARGET the staging verb is given as
                       # --artifact <triple>=<path>, and the triple the set gate looks its own
                       # platform up by. It is no longer a filename this script spells: the
                       # product's own staging chooses the on-disk name""",
)

# ---- 10. FLEET_EXE: doyle's configured exclusion, not a measurement --------
edit(
    "fleet-exe",
    """FLEET_EXE=''           # the fleet installation's path, named so it can be refused explicitly""",
    """# A CONFIGURED EXCLUSION, resolved by doyle 23YE7ZXG — and explicitly NOT a fresh canonical
# measurement of this box's install location. It is here so the fleet binary can be refused BY
# NAME; if the real install moves, this pin is stale and the refusal it powers is weaker, so it
# is re-resolved rather than trusted on age.
FLEET_EXE='C:/Users/decid/AppData/Local/spt-core/bin/spt.exe'""",
)

for name, old, new in EDITS:
    n = text.count(old)
    if n != 1:
        sys.exit(f"ANCHOR_{'ABSENT' if n == 0 else 'AMBIGUOUS'}: {name} matched {n} times")
    text = text.replace(old, new, 1)

DST.write_text(text, encoding="utf-8", newline="\n")
print(f"BUILT {DST.name} sha256={hashlib.sha256(DST.read_bytes()).hexdigest()}")
print(f"anchors replaced: {len(EDITS)}")
