Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
4cf984a
governance: mirror the maintainer-directed agent lane
xormania Sep 1, 2026
3a067a7
repo: register Serena for agent sessions
xormania Sep 1, 2026
5f024ba
Merge pull request #3 from minspec/governance/trusted-process-lane
xor-machine Sep 1, 2026
0356f2e
Merge pull request #4 from minspec/repo/agent-tooling
xor-machine Sep 1, 2026
1b0de0e
repo: real CI — the ci workflow on dev
xormania Sep 1, 2026
d864570
Merge pull request #5 from minspec/repo/ci
xor-machine Sep 1, 2026
472a851
repo: workflow and MCP hardening from the security audit
xormania Sep 1, 2026
91622e7
Merge pull request #7 from minspec/repo/hardening
xor-machine Sep 1, 2026
9099e81
repo: the term wall — names this organisation does not use, refused i…
xormania Sep 1, 2026
e6324b6
repo: pin the term wall's hit format and refusal classes
xormania Sep 1, 2026
69a006c
repo: term wall tests, from the contract — 15 real executions
Sep 1, 2026
ce33e5e
repo: the term wall to its contract — pattern from configuration, no …
Sep 1, 2026
a3142f3
repo: pin how CI hands the term wall its pattern
xormania Sep 1, 2026
6175b67
repo: bring the term wall onto the pinned contract
Sep 1, 2026
8411021
Merge pull request #8 from minspec/repo/term-wall
xor-machine Sep 1, 2026
5502456
repo: scan a pull request again when its text is edited
Sep 1, 2026
5836341
repo: pin the content surface to the object store
Sep 1, 2026
891440e
repo: tests for the content surface, from the contract
Sep 1, 2026
5c1a01e
repo: scan the content surface from the object store
Sep 1, 2026
2fc23a2
repo: retire the test that asserted a skipped binary blob
Sep 1, 2026
a1e76d0
repo: commit the planted fault in the self-test
Sep 2, 2026
594f43d
Merge pull request #10 from minspec/repo/wall-blobs
xor-machine Sep 2, 2026
3a5a52c
Merge branch 'dev' into repo/ci-edited
xor-machine Sep 2, 2026
a5d88bb
Merge pull request #9 from minspec/repo/ci-edited
xor-machine Sep 2, 2026
0c0d1aa
repo: master is the release branch
Sep 2, 2026
64cc8e7
Merge pull request #11 from minspec/repo/master-release
xor-machine Sep 2, 2026
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
101 changes: 101 additions & 0 deletions .github/actions/term-wall/CONTRACT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# The term wall — contract v1

Names this organisation does not use must not appear in any of its
repositories: not affirmed, not negated, not cited. The wall refuses
them, and the wall itself never carries them in any readable or
encoded form.

## Instruments

1. `.github/actions/term-wall/term-wall.sh` (run by the composite
action `.github/actions/term-wall`) — every repo's `ci` job runs it.
2. `ops/devlane/workflow/checks/term_wall.py` — the lane's local copy,
run by the commit-msg hook and by apply-push's guards.

## The pattern is configuration, never tree content

- The pattern is an extended, case-insensitive regular expression read
from the environment variable `TERM_WALL`. In CI the calling step
passes it: `env: TERM_WALL: ${{ vars.TERM_WALL }}` — a composite
action cannot read `vars` itself, so the action declares no default
and refuses when the step did not pass one. Locally the
Python check reads `TERM_WALL`, falling back to the gitignored file
`<root>/ops/bin/term-wall.conf` (one line: the pattern).
- No tracked file may contain the pattern, a piece of it, or any
encoding of the names (hex, base64, bracket tricks, escapes). The
self-test's planted fault comes from `vars.TERM_WALL_PLANT` (a string
the pattern matches), never from the tree.
- An unset or empty pattern is a refusal, exit 2, stdout empty, one
line on stderr: `pattern: expected TERM_WALL set; found empty;
needed the org variable (CI) or ops/bin/term-wall.conf (local)`.
The wall never passes vacuously.

## Surfaces (term-wall.sh)

1. tracked content — every `git ls-files` path, binaries skipped,
case-insensitive;
2. tracked paths;
3. the commit messages of the change — `pull_request`: `base..head`;
`push`: `before..head`, or only the head commit when `before` is
all zeros — read from git (fetching what the checkout lacks), never
from an API: the action needs no token and declares none;
4. the pull request title and body (from the event payload);
5. the branch name (`GITHUB_HEAD_REF` for a PR, `GITHUB_REF_NAME` for
a push).

Outside GitHub Actions (no `GITHUB_EVENT_PATH`), surfaces 1 and 2 run
against the current directory. The Python check covers surface 1 and
2 (`[--root DIR] [PATH ...]`), one message (`--message-file FILE`), a
range of commit messages (`--range BASE..HEAD`), or `--stdin`.

## Outcomes, on the wire

| exit | meaning |
|---|---|
| 0 | clean; exactly one summary line on stdout |
| 1 | at least one hit, every hit printed on stdout in the pinned format below; a surface that could not be read (fetch failed, payload unreadable) is itself a hit — could-not-look is never a pass |
| 2 | refusal; stdout empty, one stderr line `<class>: expected …; found …; needed …` |

Refusal classes: `pattern` (unset or empty), `git work tree` (not
inside one), `message` (missing message file), `range` (unresolvable).

## Hit format, pinned

One stdout line per hit: `<surface>: <location>: <line with each
match replaced by [forbidden name]>`. The location never contains a
colon. The raw matched text never appears in any output.

| surface | location |
|---|---|
| `content` | `<path> line <n>` |
| `path` | `<path>` |
| `commit messages` | `<sha12> line <n>` |
| `pull request title` | `line <n>` |
| `pull request body` | `line <n>` |
| `branch name` | `<name>` |
| `event payload` | `<payload path>` — the hit when the payload cannot be read |
| `message` (`--message-file`) | `line <n>` |
| `range` (`--range`) | `<sha12> line <n>` |
| `stdin` | `line <n>` |

## Self-test (the `.github` repo's own ci)

With the calling step passing `env: TERM_WALL_PLANT: ${{ vars.TERM_WALL_PLANT }}` as the planted fault: a planted file fires
(exit 1, masked hit), a clean neighbour stays quiet (exit 0), and an
empty `TERM_WALL` refuses (exit 2).

## Tests

Tests execute the real instrument as a subprocess inside temporary git
repositories they create; nothing about the wall is mocked. They set
`TERM_WALL` explicitly to a test-only pattern (for example
`zz[q]orblat`) and plant matches of it, so no forbidden name exists
anywhere. They are deterministic and hermetic: no network, no sleeps,
no dependence on the caller's cwd, environment, or git identity
(configure user.name/user.email in each temp repo). Every test asserts
the exit code and the output shape. Push and pull-request events are
simulated with an event JSON file and the `GITHUB_*` variables.

## Content surface, pinned

The content surface is every blob the scanned commit's tree tracks, read from the object store, never from the working tree. Every blob is scanned bytewise; nothing tracked is unscannable. A symlink entry is scanned as the blob it is, its target path text, and is never followed. A blob that is not valid UTF-8 reports each hit with the location `<path> line <n>`, `n` counting newline-separated segments from 1, and the third field `[binary blob]` in place of the line. A blob the wall cannot read is a refusal of class `git work tree`: `git work tree: expected a readable blob at <path>; found <error>; needed the object`.
17 changes: 17 additions & 0 deletions .github/actions/term-wall/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
name: term wall
description: >-
Refuses names this organisation does not use — in tracked content, in
file paths, in the change's commit messages (read from git, never from
an API: no token needed or declared), in the pull request title and
body, and in the branch name. The pattern comes only from the
TERM_WALL environment variable the calling step passes from the
org-level Actions variable — a composite action cannot read vars
itself, so this action declares no default; an unset or empty
pattern is a refusal, never a pass. The wall never spells what it
refuses and masks every hit it prints.
runs:
using: composite
steps:
- name: term wall
shell: bash
run: bash "$GITHUB_ACTION_PATH/term-wall.sh"
210 changes: 210 additions & 0 deletions .github/actions/term-wall/term-wall.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
#!/usr/bin/env bash
# term-wall.sh — names this organisation does not use, refused everywhere.
#
# The pattern is configuration, never tree content: it is read only from
# the TERM_WALL environment variable (in CI the calling step passes it,
# `env: TERM_WALL: ${{ vars.TERM_WALL }}` — a composite action cannot
# read `vars` itself, so the action declares no default). An unset or
# empty pattern is a refusal, exit 2 — the wall never passes vacuously.
# Every hit it prints is masked, so the log does not carry what the tree
# may not.
#
# Surfaces, in order:
# 1. tracked file content — every blob the scanned commit's tree
# tracks, read from the object store, never from the working tree,
# and scanned bytewise (case-insensitive): nothing tracked is
# unscannable. A symlink entry is scanned as the blob it is — its
# target path text — and never followed. A blob that is not valid
# UTF-8 reports each hit with "[binary blob]" in place of the line.
# 2. tracked file paths
# 3. the commit messages of the change — pull_request: base..head;
# push: before..head, or only the head commit when before is all
# zeros — read from git, fetching what the checkout lacks; never
# from an API: the action needs no token and declares none
# 4. the pull request title and body (from the event payload)
# 5. the branch name (GITHUB_HEAD_REF for a PR, GITHUB_REF_NAME for a push)
#
# Exit 0 clean, one summary line on stdout. Exit 1 on any hit, every hit
# on stdout as "<surface>: <location>: <line with each match replaced by
# [forbidden name]>" — the location never contains a colon; a surface
# that could not be read (fetch failed, payload unreadable) is itself a
# hit — could-not-look is never a pass. Exit 2 refusal: stdout empty,
# one line on stderr shaped "class: expected …; found …; needed …".
#
# Outside GitHub Actions (no GITHUB_EVENT_PATH) only surfaces 1 and 2
# run, against the current directory.
set -uo pipefail

refuse() { printf '%s\n' "$1" >&2; exit 2; }

pat=${TERM_WALL:-}
[[ -n $pat ]] || refuse 'pattern: expected TERM_WALL set; found empty; needed the org variable (CI) or ops/bin/term-wall.conf (local)'

# A pattern grep or sed cannot use would make every scan silently vacuous
# and could leak raw text past the mask — refuse it up front.
printf '' | grep -i -E -- "$pat" >/dev/null 2>&1
[[ $? -le 1 ]] || refuse 'pattern: expected TERM_WALL to be an extended regular expression grep accepts; found one it rejects; needed a working pattern in the org variable (CI) or ops/bin/term-wall.conf (local)'
printf '' | sed -E "s/($pat)/[forbidden name]/Ig" >/dev/null 2>&1 \
|| refuse 'pattern: expected TERM_WALL usable in a sed substitution; found one sed rejects (an unescaped "/"?); needed a working pattern in the org variable (CI) or ops/bin/term-wall.conf (local)'

git rev-parse --is-inside-work-tree >/dev/null 2>&1 \
|| refuse 'git work tree: expected to run inside a git work tree; found none; needed a checkout (CI) or a repository directory (local)'

rc=0
mask() { sed -E "s/($pat)/[forbidden name]/Ig"; }

emit() { # emit <surface> <location> <masked line> — one hit. Called only
# from the main shell, never from a pipeline stage, so the exit
# code it sets survives.
printf '%s: %s: %s\n' "$1" "$2" "$3"
rc=1
}

scan() { # scan <surface> <location prefix> <text> — text as an argument,
# never a pipe: a hit must set rc in this shell, and a pipeline's
# stages run in subshells. Each hit's location is
# "<prefix>line <n>".
local surface=$1 prefix=$2 text=$3 found line
[[ -n $text ]] || return 0
found=$(printf '%s\n' "$text" | grep -i -n -E -- "$pat" 2>/dev/null | mask || true)
[[ -n $found ]] || return 0
while IFS= read -r line; do
emit "$surface" "${prefix}line ${line%%:*}" "${line#*:}"
done <<< "$found"
}

scan_name() { # scan_name <surface> <value> — for surfaces whose location
# is the (masked) value itself: a path, a branch name.
local surface=$1 value=$2 masked
[[ -n $value ]] || return 0
printf '%s\n' "$value" | grep -i -E -- "$pat" >/dev/null 2>&1 || return 0
masked=$(printf '%s\n' "$value" | mask)
emit "$surface" "$masked" "$masked"
}

# 1. tracked content — every blob of the scanned commit's tree, from the
# object store, bytewise (grep -a, never -I). A symlink entry (mode
# 120000) is a blob holding its target path and is never followed; a
# gitlink (type commit) is not a blob and has no content here. A blob
# the wall cannot read is a refusal — could-not-look is never a pass.
if git rev-parse -q --verify 'HEAD^{commit}' >/dev/null 2>&1; then
git ls-tree -r HEAD >/dev/null 2>&1 \
|| refuse 'git work tree: expected a readable tree at HEAD; found git ls-tree cannot read it; needed the object'
while IFS= read -r -d '' entry; do
meta=${entry%%$'\t'*}
path=${entry#*$'\t'}
read -r _mode type oid <<< "$meta"
[[ $type == blob ]] || continue
if ! err=$(git cat-file -e "$oid" 2>&1); then
err=$(printf '%s' "${err:-git cat-file cannot read $oid}" | tr '\n' ' ')
refuse "$(printf 'git work tree: expected a readable blob at %s; found %s; needed the object' "$path" "$err" | mask)"
fi
if git cat-file blob "$oid" 2>/dev/null | iconv -f UTF-8 -t UTF-8 >/dev/null 2>&1; then
# NUL is valid UTF-8 (U+0000) but a shell variable cannot hold
# it — drop it before masking, never after.
found=$(git cat-file blob "$oid" 2>/dev/null | grep -a -i -n -E -- "$pat" | tr -d '\000' | mask || true)
[[ -n $found ]] || continue
while IFS= read -r line; do
emit "content" "$path line ${line%%:*}" "${line#*:}"
done <<< "$found"
else
# Not valid UTF-8: never echo its bytes — only line numbers
# (newline-separated segments, from 1) and "[binary blob]".
found=$(git cat-file blob "$oid" 2>/dev/null | grep -a -i -n -E -- "$pat" | cut -d: -f1 || true)
[[ -n $found ]] || continue
while IFS= read -r n; do
emit "content" "$path line $n" "[binary blob]"
done <<< "$found"
fi
done < <(git ls-tree -r -z HEAD 2>/dev/null)
fi

# 2. tracked paths
paths=$(git ls-files 2>/dev/null | grep -i -E -- "$pat" 2>/dev/null | mask || true)
if [[ -n $paths ]]; then
while IFS= read -r line; do
emit "path" "$line" "$line"
done <<< "$paths"
fi

# 3-5. the change itself, when running under Actions
if [[ -n ${GITHUB_EVENT_PATH:-} && -f ${GITHUB_EVENT_PATH:-} ]]; then
event=${GITHUB_EVENT_NAME:-}
payload_ok=1
jq empty "$GITHUB_EVENT_PATH" >/dev/null 2>&1 || payload_ok=0
if [[ $payload_ok -eq 0 ]]; then
emit "event payload" "$GITHUB_EVENT_PATH" "could not read the event payload (could-not-look is never a pass)"
fi
base="" head="" want_messages=0
case $event in
pull_request|pull_request_target)
if [[ $payload_ok -eq 1 ]]; then
base=$(jq -r '.pull_request.base.sha // empty' "$GITHUB_EVENT_PATH" 2>/dev/null) || base=""
head=$(jq -r '.pull_request.head.sha // empty' "$GITHUB_EVENT_PATH" 2>/dev/null) || head=""
scan "pull request title" "" "$(jq -r '.pull_request.title // ""' "$GITHUB_EVENT_PATH" 2>/dev/null)"
scan "pull request body" "" "$(jq -r '.pull_request.body // ""' "$GITHUB_EVENT_PATH" 2>/dev/null)"
want_messages=1
fi
scan_name "branch name" "${GITHUB_HEAD_REF:-}"
;;
push)
if [[ $payload_ok -eq 1 ]]; then
base=$(jq -r '.before // empty' "$GITHUB_EVENT_PATH" 2>/dev/null) || base=""
head=$(jq -r '.after // empty' "$GITHUB_EVENT_PATH" 2>/dev/null) || head=""
want_messages=1
fi
scan_name "branch name" "${GITHUB_REF_NAME:-}"
;;
esac
if [[ $want_messages -eq 1 ]]; then
only_head=0
[[ $event == push && ( -z $base || $base =~ ^0+$ ) ]] && only_head=1
if [[ -z $head || ( $only_head -eq 0 && -z $base ) ]]; then
emit "commit messages" "$event" "could not resolve the change's commits from the event payload (could-not-look is never a pass)"
else
refs=("$head"); [[ $only_head -eq 0 ]] && refs=("$base" "$head")
# Fetch what the checkout lacks; a shallow clone would walk a
# truncated history without erroring, so unshallow it.
shallow=$(git rev-parse --is-shallow-repository 2>/dev/null) || shallow=false
missing=0
for ref in "${refs[@]}"; do
git cat-file -e "$ref^{commit}" 2>/dev/null || missing=1
done
fetch_failed=0
if [[ $shallow == true || $missing -eq 1 ]]; then
fetch_opts=(--no-tags --quiet)
[[ $shallow == true ]] && fetch_opts+=(--unshallow)
if ! fetch_err=$(git fetch "${fetch_opts[@]}" origin "${refs[@]}" 2>&1); then
emit "commit messages" "${refs[*]}" "could not fetch the change from origin — $(printf '%s' "$fetch_err" | mask | tr '\n' ' ' | head -c 300) (could-not-look is never a pass)"
fetch_failed=1
fi
fi
if [[ $fetch_failed -eq 0 ]]; then
if [[ $only_head -eq 1 ]]; then
range=$head
shas=$(git rev-list -n 1 "$head" 2>/dev/null); list_rc=$?
else
range="$base..$head"
shas=$(git rev-list "$base..$head" 2>/dev/null); list_rc=$?
fi
if [[ $list_rc -eq 0 ]]; then
while IFS= read -r sha; do
[[ -n $sha ]] || continue
if msg=$(git log -1 --format=%B "$sha" 2>/dev/null); then
scan "commit messages" "${sha:0:12} " "$msg"
else
emit "commit messages" "${sha:0:12}" "could not read the commit message from git (could-not-look is never a pass)"
fi
done <<< "$shas"
else
emit "commit messages" "$range" "could not read the commit messages from git (could-not-look is never a pass)"
fi
fi
fi
fi
fi

if [[ $rc -eq 0 ]]; then
echo "term wall: clean — $(git ls-files 2>/dev/null | wc -l) tracked files, their paths, and the change's messages, title, body and branch name carry no forbidden name"
fi
exit "$rc"
Loading
Loading