Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 41 additions & 14 deletions README.md

Large diffs are not rendered by default.

37 changes: 31 additions & 6 deletions bootstrap.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,17 @@ running cycle.

## 1. Install

Run once, from the xcheck repo root, per audited project. The block below
copies the text-corpus template by default — swap `AUDIT-text.md` for
`AUDIT-code.md` in the second `cp` line when auditing a codebase.
Run once, from the xcheck repo root, per audited project. The one command
that does it is the installer, which also records provenance for
`xcheck upgrade` (README §5.2):

```bash
./install.sh --type code /path/to/project # or --type text for a text corpus
```

By hand, the block below copies the text-corpus template — swap
`AUDIT-text.md` for `AUDIT-code.md` in the second `cp` line when auditing a
codebase.

```bash
# from the xcheck repo root; TARGET = audited project root
Expand All @@ -30,7 +38,24 @@ audit state.
finding's frontmatter block are GENERATED MIRRORS of it (XCHECK.md §2). They
appear the moment there is state to render.

## 1a. From the Planner's AUDIT.md to state
## 1a. Operator profile — once per machine

`xcheck next`, and every launcher that wraps it, dispatches nothing until an
operator profile resolves. That file says which command each role runs and
inside what boundary. It lives outside every audited repository, so the
install above does not create it:

```bash
# from the xcheck repo root
./operator-profile.sh --trust trusted /path/to/operator.conf # or --trust untrusted
export XCHECK_OPERATOR_PROFILE=/path/to/operator.conf
```

README §5.3 explains the two trust levels and the other ways to name the
file; README §8 lists every key. The hand-pasted one-liners in §2 below are
`uncontained-direct` sessions: they never pass through the profile.

## 1b. From the Planner's AUDIT.md to state

`AUDIT.md` is prose a human reads and rules on. Loading it into the canonical
record is one explicit operator command, run once after the Planner session:
Expand Down Expand Up @@ -103,9 +128,9 @@ range, or stop condition and is not a charter; it must not start a session.

## 3. A typical cycle

1. Install (§1) into the target project.
1. Install (§1) into the target project, and write the operator profile if this machine has none (§1a).
2. Planner session produces `AUDIT.md` — norms catalog, dimensions, unit map, pass queue.
3. Human skims the pass queue and trims it: drop or reorder passes; cheap, no gate, just judgment. Then `xcheck migrate` (§1a) loads it into `audit/state.json` — after this point the queue is amended with `xcheck queue-pass`, not by editing `AUDIT.md`.
3. Human skims the pass queue and trims it: drop or reorder passes; cheap, no gate, just judgment. Then `xcheck migrate` (§1b) loads it into `audit/state.json` — after this point the queue is amended with `xcheck queue-pass`, not by editing `AUDIT.md`.
4. Auditor sessions work the queue one charter at a time, filing findings and a pass report per charter.
5. Triage: human sets every `reported` finding to `accepted`, `rejected`, or `deferred`.
6. Remediator sessions take batches of `accepted` findings through validate → census → plan → fix → self-check.
Expand Down
11 changes: 11 additions & 0 deletions ci/check-split.py
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,17 @@
"REPORT, which must never raise: it is called while building "
"a refusal, and a report that explodes turns an addressed "
"operator error into a traceback",

# The fresh-install route. The monolith had no state document, so it had no
# "before the state document exists" to decide in; the Planner of a new audit could
# not be dispatched by anything until this existed.
"pre_state_decision": "fresh install — the decision for a project with no "
"state.json yet: the Planner, or the operator's migrate gate, "
"and only on a tree proved to be a fresh install",
"_budget_stop": "fresh install — the budget gate over a dispatch, shared by both "
"decisions that can dispatch so a pre-state Planner is priced too",
"_status_tail": "fresh install — the quarantine, lock, sandbox and decision lines "
"`status` prints whether or not a state document exists",
}

# Modules that are NEW work, not split-out monolith code. Their names are exempt
Expand Down
6 changes: 6 additions & 0 deletions ci/tiers.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@
"test_admin_verbs",
# policy: what the operator owns, where it may live, and the trust classification
"test_conf_policy", "test_policy_location", "test_trust_level",
# and the way in to it: the operator profile nothing dispatches without, the AUDIT.md
# template an install picks, and a refusal whose commands run when typed
"test_operator_onboarding",
# the four shared-boundary gates: canonical state, transaction identity, the journal
# door and the dispatch preparation. AST-only and ~0.4s, and on the PR tier rather
# than the release tier on purpose — a gate that runs late is a gate that catches late
Expand All @@ -61,6 +64,9 @@
# the short mutation controls — the ones that plant a defect and read a red, without
# starting a container or building a venv
"test_mutations", "test_no_bypass", "test_consumer_paths",
# the one door through that boundary: a fresh install, with no state yet, routed to
# the Planner and the migrate gate — and every other stateless tree still refused
"test_fresh_install",
# the tier check itself: a guard that only the release tier ran would
# let a PR add an unassigned module and find out days later
"test_check_tiers",
Expand Down
70 changes: 66 additions & 4 deletions install.sh
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
#!/bin/sh
# Install the xcheck methodology into a target project: ./install.sh /path/to/project
# Install the xcheck methodology into a target project:
# ./install.sh [--type code|text] /path/to/project
#
# `--type` picks the template AUDIT.md starts from: `code` for a codebase, `text` for a
# text corpus. It has a default (`text`, what this script always installed) where the
# operator profile's trust level has none, because a wrong template is cheap to undo
# until the Planner runs: one `cp` over a file that holds nothing yet.
#
# This sets up the SUBJECT only. The operator profile — what each role runs, and inside
# what boundary — lives outside every audited repository, so `operator-profile.sh`
# writes it, once per machine; the closing lines below say so.
#
# Deliberately shell-only and small. This runs in target projects that may have no
# Python environment configured, and its simplicity is a feature — an installer that
Expand All @@ -17,7 +27,41 @@
set -e
cd "$(dirname "$0")"
SRC=$(pwd)
TARGET=${1:?usage: ./install.sh /path/to/project}

usage() {
echo "usage: ./install.sh [--type code|text] /path/to/project"
echo " --type code AUDIT.md starts from templates/AUDIT-code.md: a codebase"
echo " --type text AUDIT.md starts from templates/AUDIT-text.md: a text corpus (default)"
}
TYPE=
TARGET=
while [ $# -gt 0 ]; do
case "$1" in
--type) [ $# -ge 2 ] || { usage >&2; exit 2; }; TYPE=$2; shift 2 ;;
--type=*) TYPE=${1#--type=}; shift ;;
-h|--help) usage; exit 0 ;;
-*) echo "install: unknown option $1" >&2; usage >&2; exit 2 ;;
*) [ -z "$TARGET" ] || { usage >&2; exit 2; }; TARGET=$1; shift ;;
esac
done
[ -n "$TARGET" ] || { usage >&2; exit 2; }
# Checked before anything is written: a refused install leaves no `audit/` behind.
case "${TYPE:-text}" in
code|text) ;;
*) echo "install: --type $TYPE is not a project type (code or text)" >&2; exit 2 ;;
esac

# What the operator does next, printed on both ways out of this script. The profile step
# comes first because without it `xcheck next` and every launcher that wraps it refuse
# before a session starts, and nothing on the install path used to say so.
finish() {
echo " $AUDIT_NOTE"
echo "next: 1. once per machine, an operator profile, outside every audited repository."
echo " \`xcheck next\` and the launchers dispatch nothing until one exists:"
echo " $SRC/operator-profile.sh --trust trusted|untrusted /path/to/operator.conf"
echo " 2. plan the audit: README §6, Step 1."
}

mkdir -p "$TARGET/audit/findings" "$TARGET/audit/passes" "$TARGET/audit/plans" "$TARGET/audit/construals" "$TARGET/audit/templates"

# The shipped set — the ONE list, used for both copying and the manifest, so a file
Expand All @@ -27,8 +71,24 @@ SHIPPED="XCHECK.md templates/finding.md templates/class-finding.md templates/pas
cp XCHECK.md "$TARGET/audit/XCHECK.md"
cp templates/finding.md templates/class-finding.md templates/pass.md templates/plan.md templates/construal.md "$TARGET/audit/templates/"
# AUDIT.md and LEDGER.md are the OPERATOR's from the moment they exist: created once,
# never refreshed. `upgrade` will not touch them either.
[ -f "$TARGET/audit/AUDIT.md" ] || cp templates/AUDIT-text.md "$TARGET/audit/AUDIT.md" # use AUDIT-code.md for code projects
# never refreshed. `upgrade` will not touch them either, and neither does `--type`: an
# AUDIT.md that exists is reported on, never replaced — even one that still holds
# nothing but a template, for which the report names the `cp` that switches it.
if [ ! -f "$TARGET/audit/AUDIT.md" ]; then
cp "templates/AUDIT-${TYPE:-text}.md" "$TARGET/audit/AUDIT.md"
AUDIT_NOTE="audit/AUDIT.md from templates/AUDIT-${TYPE:-text}.md"
[ -n "$TYPE" ] || AUDIT_NOTE="$AUDIT_NOTE (the default; --type code for a codebase)"
else
AUDIT_NOTE="audit/AUDIT.md kept: it exists, and it is the operator's"
for t in code text; do
cmp -s "$TARGET/audit/AUDIT.md" "templates/AUDIT-$t.md" || continue
AUDIT_NOTE="audit/AUDIT.md kept: it is the unfilled $t template"
if [ -n "$TYPE" ] && [ "$TYPE" != "$t" ]; then
AUDIT_NOTE="$AUDIT_NOTE, and install.sh never replaces an AUDIT.md. To switch:
cp $SRC/templates/AUDIT-$TYPE.md $TARGET/audit/AUDIT.md"
fi
done
fi
[ -f "$TARGET/audit/LEDGER.md" ] || printf '| id | title | severity | status | next | updated |\n|---|---|---|---|---|---|\n' > "$TARGET/audit/LEDGER.md"

# ---- provenance -----------------------------------------------------------------
Expand All @@ -45,6 +105,7 @@ else
echo " audit/MANIFEST.sha256 were NOT written. The install is complete and"
echo " usable; \`xcheck upgrade\` will report the copy as unprovenanced and"
echo " refuse to refresh files it cannot prove it shipped."
finish
exit 0
fi

Expand Down Expand Up @@ -86,3 +147,4 @@ else
echo "install: xcheck $VERSION (schema $SCHEMA, source $SOURCE_REF) -> $TARGET/audit"
fi
echo " provenance: audit/.provenance.json, manifest: audit/MANIFEST.sha256 ($(wc -l < "$TARGET/audit/MANIFEST.sha256" | tr -d ' ') files)"
finish
185 changes: 185 additions & 0 deletions operator-profile.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
#!/bin/sh
# Write an xcheck operator profile, once per machine:
# ./operator-profile.sh --trust trusted|untrusted [--project DIR] [PATH]
#
# `install.sh` sets up the SUBJECT; this sets up the OPERATOR. They are two scripts
# because they write to two places that must never be the same: the profile says what
# each role runs and inside what boundary, so it lives OUTSIDE every audited repository
# (README §8), and `xcheck next` dispatches nothing until one resolves.
#
# It decides nothing for the operator. `trust_level` has no default in xcheck and none
# here: how much containment a repository needs is a fact about that repository. The
# location is named by the operator too, or by $XDG_CONFIG_HOME — like xcheck itself,
# this script never derives a path from the home directory.
#
# What it does: copies docs/operator.conf.example (byte-identical to the template the
# loader prints; `tests/test_conf_policy.py` holds that), sets `trust_level` — and for
# `untrusted` the `sandbox_profile=container` that classification makes mandatory —
# validates the result with xcheck's own loader, and only then links it into place. It
# never overwrites a profile.
set -e
SRC=$(cd "$(dirname "$0")" && pwd)
PY=${PYTHON:-python3}
ME=operator-profile

usage() {
cat <<EOF
usage: $0 --trust trusted|untrusted [--project DIR] [PATH]

--trust trusted code you or your organisation wrote and review
--trust untrusted anything else; also sets sandbox_profile=container (needs docker)
--project DIR an audited project; refuse a PATH inside it
PATH where the profile goes, outside every audited repository.
Optional when \$XDG_CONFIG_HOME is set: then it is
\$XDG_CONFIG_HOME/xcheck/operator.conf
EOF
}

TRUST=
PROJECT=
DEST=
while [ $# -gt 0 ]; do
case "$1" in
--trust) [ $# -ge 2 ] || { usage >&2; exit 2; }; TRUST=$2; shift 2 ;;
--trust=*) TRUST=${1#--trust=}; shift ;;
--project) [ $# -ge 2 ] || { usage >&2; exit 2; }; PROJECT=$2; shift 2 ;;
--project=*) PROJECT=${1#--project=}; shift ;;
-h|--help) usage; exit 0 ;;
-*) echo "$ME: unknown option $1" >&2; usage >&2; exit 2 ;;
*) [ -z "$DEST" ] || { echo "$ME: one PATH only" >&2; usage >&2; exit 2; }
DEST=$1; shift ;;
esac
done

case "$TRUST" in
trusted|untrusted) ;;
"")
cat >&2 <<EOF
$ME: --trust is required.
xcheck has no default for it, because how much containment a repository needs is a
fact about that repository, and only you know it:

--trust trusted code you or your organisation wrote and review. Sessions run in a
git worktree, which isolates the repository, not the machine.
--trust untrusted anything else. Sessions run in a container, so docker must work
on this machine.

SECURITY.md in the xcheck repository says what each boundary does and does not stop.
EOF
exit 2 ;;
*)
echo "$ME: --trust $TRUST is not a trust level (trusted or untrusted)" >&2
exit 2 ;;
esac

if [ -z "$DEST" ]; then
if [ -z "$XDG_CONFIG_HOME" ]; then
cat >&2 <<EOF
$ME: no PATH given, and \$XDG_CONFIG_HOME is not set. xcheck never derives a location
from your home directory, and neither does this script, so name one — any path outside
every audited repository:

$0 --trust $TRUST /path/to/operator.conf
EOF
exit 2
fi
DEST=$XDG_CONFIG_HOME/xcheck/operator.conf
fi

if [ -e "$DEST" ] || [ -L "$DEST" ]; then
echo "$ME: $DEST already exists, and this script never overwrites a profile." >&2
echo " Edit it by hand, or move it away and run this again." >&2
exit 1
fi
if [ -n "$PROJECT" ] && [ ! -d "$PROJECT" ]; then
echo "$ME: --project $PROJECT is not a directory" >&2
exit 2
fi
command -v "$PY" >/dev/null 2>&1 || {
echo "$ME: $PY not found. xcheck runs on Python 3; set PYTHON to the interpreter it uses." >&2
exit 2
}

DIR=$(dirname "$DEST")
mkdir -p "$DIR"
DIR=$(cd "$DIR" && pwd)
DEST=$DIR/$(basename "$DEST")

# Built and validated beside the destination, so the location check below judges the
# directory the profile will really live in, and the final link is one rename away.
TMP=$(mktemp "$DIR/.operator.conf.XXXXXX")
trap 'rm -f "$TMP"' EXIT

if [ "$TRUST" = untrusted ]; then
sed -e "s/^trust_level=\$/trust_level=$TRUST/" \
-e 's/^sandbox_profile=$/sandbox_profile=container/' \
"$SRC/docs/operator.conf.example" > "$TMP"
else
sed -e "s/^trust_level=\$/trust_level=$TRUST/" "$SRC/docs/operator.conf.example" > "$TMP"
fi
grep -q "^trust_level=$TRUST\$" "$TMP" || {
echo "$ME: docs/operator.conf.example has no empty trust_level= line to set; it no" >&2
echo " longer matches what this script expects, and nothing was written." >&2
exit 1
}

# xcheck's own loader, so a profile this script writes is one `xcheck next` accepts: the
# location gate, the unknown-key and project-key refusals, and the trust level. With no
# --project there is no subject to measure the location against; xcheck applies the
# same gate again at every dispatch, against the project it is dispatching for.
"$PY" -I - "$SRC" "$TMP" "$PROJECT" <<'EOF' || exit 1
import sys
from pathlib import Path
sys.path.insert(0, sys.argv[1])
from xcheck import policy
path, project = Path(sys.argv[2]), sys.argv[3]
if not project:
project = path.parent / ".no-project-named"
try:
level = policy.trust_level(policy.load(path, project))
except policy.PolicyError as e:
sys.exit(f"operator-profile: refused, nothing written.\n{e}")
if not level:
sys.exit("operator-profile: the loader read no trust_level; nothing written.")
EOF

# A hard link, not a move: it fails if DEST appeared since the check above, so a
# profile somebody wrote in the meantime is never replaced.
ln "$TMP" "$DEST" 2>/dev/null || {
echo "$ME: $DEST appeared while this ran; it was left alone and nothing was written." >&2
exit 1
}
rm -f "$TMP"

echo "$ME: wrote $DEST (trust_level=$TRUST)"
if [ "$TRUST" = untrusted ]; then
echo " sandbox_profile=container: every session runs in a container, so docker must"
echo " work on this machine."
fi
echo " Role commands, from the template. Edit the file to change them:"
grep '^[a-z]*_cmd=' "$DEST" | sed 's/^/ /'

XDG_DEST=
if [ -n "$XDG_CONFIG_HOME" ] && [ -d "$XDG_CONFIG_HOME" ]; then
XDG_DEST=$(cd "$XDG_CONFIG_HOME" && pwd)/xcheck/operator.conf
fi
if [ "$DEST" = "$XDG_DEST" ]; then
echo " xcheck finds it through \$XDG_CONFIG_HOME, so there is nothing to export."
else
echo " xcheck reads it only where it is told to. Run this, and add it to your"
echo " shell's startup file:"
case "${SHELL##*/}" in
fish) echo " set -gx XCHECK_OPERATOR_PROFILE \"$DEST\"" ;;
*) echo " export XCHECK_OPERATOR_PROFILE=\"$DEST\"" ;;
esac
fi
if [ -n "$XCHECK_OPERATOR_PROFILE" ] && [ "$XCHECK_OPERATOR_PROFILE" != "$DEST" ]; then
echo " note: XCHECK_OPERATOR_PROFILE is set to $XCHECK_OPERATOR_PROFILE in this shell,"
echo " and xcheck reads it first, so this profile is not in force until it points here."
fi
if [ -n "$PROJECT" ]; then
echo "next: $SRC/bin/xcheck --project $PROJECT status (its policy: line names the file)"
else
echo "next: from the audited project's root, \`xcheck status\` names the profile in force"
echo " on its policy: line; then plan with /xcheck-plan."
fi
Loading
Loading