From 4e6abf6c76ba642f5baf0b966838bdf6d43fd5c2 Mon Sep 17 00:00:00 2001 From: William Goeller Date: Sun, 23 Aug 2026 04:55:18 -0400 Subject: [PATCH] Provision isolated daemon session workspaces --- tooling/provision-session-workspace.sh | 185 ++++++++++++++++++++ tooling/test_provision_session_workspace.py | 160 +++++++++++++++++ 2 files changed, 345 insertions(+) create mode 100755 tooling/provision-session-workspace.sh create mode 100755 tooling/test_provision_session_workspace.py diff --git a/tooling/provision-session-workspace.sh b/tooling/provision-session-workspace.sh new file mode 100755 index 0000000000..9fb41f1417 --- /dev/null +++ b/tooling/provision-session-workspace.sh @@ -0,0 +1,185 @@ +#!/usr/bin/env bash +# Provision the direct repository that daemon-local tools derive for one session. +# +# The daemon never accepts an arbitrary workspace path from a session. Given a +# configured root `/srv/signalbox/workspace`, it looks only for the sibling path +# `/srv/signalbox/workspace.sessions/`. This helper performs the +# deployment-owned half of that contract without weakening the daemon's +# descriptor and repository-identity checks. +# +# Usage: provision-session-workspace.sh --configured-root +# --session-id --revision --remote +# [--seed-tree ] +set -euo pipefail + +configured_root="" +session_id="" +revision="" +remote="" +seed_tree="" + +usage() { + echo "usage: provision-session-workspace.sh --configured-root " \ + "--session-id --revision --remote " \ + "[--seed-tree ]" +} + +while [ "$#" -gt 0 ]; do + case "$1" in + --configured-root | --session-id | --revision | --remote | --seed-tree) + if [ "$#" -lt 2 ]; then + echo "provision-session-workspace: $1 needs a value" >&2 + exit 2 + fi + case "$1" in + --configured-root) configured_root=$2 ;; + --session-id) session_id=$2 ;; + --revision) revision=$2 ;; + --remote) remote=$2 ;; + --seed-tree) seed_tree=$2 ;; + esac + shift 2 + ;; + -h | --help) + usage + exit 0 + ;; + *) + echo "provision-session-workspace: unknown argument: $1" >&2 + usage >&2 + exit 2 + ;; + esac +done + +if [ -z "$configured_root" ] || [ -z "$session_id" ] || + [ -z "$revision" ] || [ -z "$remote" ]; then + echo "provision-session-workspace: all required arguments must be supplied" >&2 + usage >&2 + exit 2 +fi + +if [[ ! "$session_id" =~ ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$ ]]; then + echo "provision-session-workspace: session-id must be a canonical lowercase UUID" >&2 + exit 2 +fi + +if [[ ! "$revision" =~ ^[0-9a-f]{40}$ ]]; then + echo "provision-session-workspace: revision must be a lowercase 40-digit commit ID" >&2 + exit 2 +fi + +case "$configured_root" in +/*) ;; +*) + echo "provision-session-workspace: configured-root must be absolute" >&2 + exit 2 + ;; +esac + +if [ ! -d "$configured_root" ] || [ -L "$configured_root" ] || + [ ! -d "$configured_root/.git" ] || [ -L "$configured_root/.git" ]; then + echo "provision-session-workspace: configured-root must be a direct repository" >&2 + exit 1 +fi + +canonical_root=$(realpath -- "$configured_root") +repository_root=$(git -C "$configured_root" rev-parse --show-toplevel) +if [ "$repository_root" != "$canonical_root" ]; then + echo "provision-session-workspace: configured-root is not the repository root" >&2 + exit 1 +fi +if ! git -C "$configured_root" cat-file -e "$revision^{commit}"; then + echo "provision-session-workspace: revision is absent from configured-root" >&2 + exit 1 +fi +reachable_ref=$( + git -C "$configured_root" for-each-ref \ + --count=1 --format='%(refname)' --contains="$revision" +) +if [ -z "$reachable_ref" ]; then + echo "provision-session-workspace: revision is not retained by a repository ref" >&2 + exit 1 +fi + +if [ -n "$seed_tree" ]; then + case "$seed_tree" in + /*) ;; + *) + echo "provision-session-workspace: seed-tree must be absolute" >&2 + exit 2 + ;; + esac + if [ ! -d "$seed_tree" ] || [ -L "$seed_tree" ]; then + echo "provision-session-workspace: seed-tree must be a directory" >&2 + exit 1 + fi + if ! command -v rsync >/dev/null 2>&1; then + echo "provision-session-workspace: rsync is required with --seed-tree" >&2 + exit 1 + fi +fi + +configured_parent=$(dirname -- "$canonical_root") +configured_name=$(basename -- "$canonical_root") +derived_parent="$configured_parent/$configured_name.sessions" +target="$derived_parent/$session_id" + +if [ -e "$derived_parent" ] || [ -L "$derived_parent" ]; then + if [ ! -d "$derived_parent" ] || [ -L "$derived_parent" ]; then + echo "provision-session-workspace: derived parent is not a direct directory" >&2 + exit 1 + fi +else + mkdir -- "$derived_parent" +fi + +if [ -e "$target" ] || [ -L "$target" ]; then + if [ -d "$target" ] && [ ! -L "$target" ] && [ -d "$target/.git" ] && + [ ! -L "$target/.git" ] && + [ "$(git -C "$target" rev-parse --show-toplevel 2>/dev/null)" = "$target" ] && + [ "$(git -C "$target" remote get-url origin 2>/dev/null)" = "$remote" ]; then + printf '%s\n' "$target" + exit 0 + fi + echo "provision-session-workspace: target exists but does not match its deployment fence" >&2 + exit 1 +fi + +staging=$(mktemp -d "$derived_parent/.${session_id}.provisioning.XXXXXXXX") +cleanup() { + if [ -n "$staging" ] && [ -d "$staging" ]; then + rm -rf -- "$staging" + fi +} +trap cleanup EXIT HUP INT QUIT TERM + +# `--no-hardlinks` gives the session its own object store as well as its own +# worktree and administration directory. `--no-checkout` keeps the publication +# private until the exact commissioned revision has been selected. +rmdir -- "$staging" +git clone --local --no-hardlinks --no-checkout -- "$canonical_root" "$staging" +git -C "$staging" checkout --detach "$revision" +git -C "$staging" remote set-url origin "$remote" + +if [ -n "$seed_tree" ]; then + # Preserve work already present in a legacy session tree. Build products and + # dependency caches are deliberately regenerated under the new boundary. + rsync -a --delete \ + --exclude=/.git \ + --exclude=/.cache \ + --exclude=/.cargo/registry \ + --exclude=/node_modules \ + --exclude=/target \ + -- "$seed_tree/" "$staging/" +fi + +if [ ! -d "$staging/.git" ] || [ -L "$staging/.git" ]; then + echo "provision-session-workspace: clone did not produce a direct repository" >&2 + exit 1 +fi + +mv -T -- "$staging" "$target" +staging="" +trap - EXIT HUP INT QUIT TERM +printf '%s\n' "$target" diff --git a/tooling/test_provision_session_workspace.py b/tooling/test_provision_session_workspace.py new file mode 100755 index 0000000000..22ac0d58e3 --- /dev/null +++ b/tooling/test_provision_session_workspace.py @@ -0,0 +1,160 @@ +#!/usr/bin/env python3 +"""Contract tests for the local session-workspace deployment helper.""" + +from __future__ import annotations + +import os +import subprocess +import tempfile +import unittest +from pathlib import Path + + +SCRIPT = Path(__file__).with_name("provision-session-workspace.sh") +SESSION_ID = "018f6d4a-7b2c-7def-8123-456789abcdef" +REMOTE = "git@github.com:KeenWill/signalbox.git" + + +def run(*arguments: str, check: bool = True) -> subprocess.CompletedProcess[str]: + return subprocess.run( + [str(SCRIPT), *arguments], + check=check, + capture_output=True, + text=True, + ) + + +def git(repository: Path, *arguments: str) -> str: + completed = subprocess.run( + ["git", "-C", str(repository), *arguments], + check=True, + capture_output=True, + text=True, + env={ + **os.environ, + "GIT_AUTHOR_NAME": "Signalbox Test", + "GIT_AUTHOR_EMAIL": "signalbox@example.invalid", + "GIT_COMMITTER_NAME": "Signalbox Test", + "GIT_COMMITTER_EMAIL": "signalbox@example.invalid", + }, + ) + return completed.stdout.strip() + + +class ProvisionSessionWorkspaceTests(unittest.TestCase): + def setUp(self) -> None: + self.temporary = tempfile.TemporaryDirectory() + self.addCleanup(self.temporary.cleanup) + self.root = Path(self.temporary.name) / "workspace" + self.root.mkdir() + git(self.root, "init", "--initial-branch=main") + (self.root / "kept.txt").write_text("base\n", encoding="utf-8") + (self.root / "removed.txt").write_text("remove me\n", encoding="utf-8") + git(self.root, "add", "kept.txt", "removed.txt") + git(self.root, "commit", "-m", "fixture") + self.revision = git(self.root, "rev-parse", "HEAD") + self.target = Path(f"{self.root}.sessions") / SESSION_ID + + def provision( + self, *extra: str, check: bool = True + ) -> subprocess.CompletedProcess[str]: + return run( + "--configured-root", + str(self.root), + "--session-id", + SESSION_ID, + "--revision", + self.revision, + "--remote", + REMOTE, + *extra, + check=check, + ) + + def test_provisions_direct_repository_at_derived_path(self) -> None: + completed = self.provision() + + self.assertEqual(completed.stdout.strip(), str(self.target)) + self.assertTrue((self.target / ".git").is_dir()) + self.assertEqual(git(self.target, "rev-parse", "HEAD"), self.revision) + self.assertEqual(git(self.target, "remote", "get-url", "origin"), REMOTE) + + def test_seed_tree_preserves_work_without_legacy_git_boundary(self) -> None: + seed = Path(self.temporary.name) / "legacy" + subprocess.run( + ["cp", "-a", str(self.root), str(seed)], + check=True, + capture_output=True, + text=True, + ) + (seed / "kept.txt").write_text("changed\n", encoding="utf-8") + (seed / "removed.txt").unlink() + (seed / "new.txt").write_text("new\n", encoding="utf-8") + self.provision("--seed-tree", str(seed)) + + self.assertEqual((self.target / "kept.txt").read_text(), "changed\n") + self.assertFalse((self.target / "removed.txt").exists()) + self.assertEqual((self.target / "new.txt").read_text(), "new\n") + self.assertTrue((self.target / ".git").is_dir()) + + def test_existing_direct_repository_is_left_unchanged(self) -> None: + self.provision() + marker = self.target / "operator-work.txt" + marker.write_text("keep\n", encoding="utf-8") + + completed = self.provision() + + self.assertEqual(completed.stdout.strip(), str(self.target)) + self.assertEqual(marker.read_text(), "keep\n") + + def test_existing_repository_with_another_remote_is_refused(self) -> None: + self.provision() + git(self.target, "remote", "set-url", "origin", "ssh://example.invalid/other") + + completed = self.provision(check=False) + + self.assertEqual(completed.returncode, 1) + + def test_unreachable_commit_is_refused_before_parent_creation(self) -> None: + unreachable = git( + self.root, + "commit-tree", + f"{self.revision}^{{tree}}", + "-m", + "unreachable", + ) + + completed = run( + "--configured-root", + str(self.root), + "--session-id", + SESSION_ID, + "--revision", + unreachable, + "--remote", + REMOTE, + check=False, + ) + + self.assertEqual(completed.returncode, 1) + self.assertFalse(Path(f"{self.root}.sessions").exists()) + + def test_invalid_session_id_is_refused_before_parent_creation(self) -> None: + completed = run( + "--configured-root", + str(self.root), + "--session-id", + "not-a-session", + "--revision", + self.revision, + "--remote", + REMOTE, + check=False, + ) + + self.assertEqual(completed.returncode, 2) + self.assertFalse(Path(f"{self.root}.sessions").exists()) + + +if __name__ == "__main__": + unittest.main()