mirror of
https://github.com/Heretek-AI/heretek-rigs.git
synced 2026-11-07 10:55:57 -10:00
fix(sandbox): writable toolchain caches in the seat jail; single-seat refresh and write-scope docs
- seat-sandbox.sh: point XDG_CACHE_HOME and GOCACHE at the per-run scratch. The jail binds / read-only, so go build/test died on ~/.cache/go-build (live finding from the fleet-proofs runbook, Refs #91, Refs #12). Red-proof test reproduces the live error on the old wrapper. - Document the single-seat refresh and re-home routes that OpenRig 0.7.5 already ships (rig seat launch --fresh --stop, rig seat set-cwd), which the seat-heal/repair waves used live (Refs #11, Refs #78). - New generated docs/SEAT-WRITE-SCOPES.md (scripts/seat-scopes.py) with a drift test against fleet/fleet.yaml (Refs #78). Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01VjcGdhFQxjHWWV9xxvurqs
This commit is contained in:
8 files changed
+288
No files matched your search
@@ -26,6 +26,7 @@ Use `yaml.safe_dump` NEVER on `fleet/fleet.yaml` — it strips comments. Surgica
|
||||
| Seat render dry-run | `HERETEK_SECRETS=~/.config/heretek-rigs/secrets python3 scripts/render-seat.py --role <id> --rig <id> --pod <id> --dry-run` |
|
||||
| Seed then launch | `python3 scripts/seed-rig.py rigs/<rig>/rig.yaml [--allow-missing-secrets]` → `(cd rigs/<rig> && rig up rig.yaml --cwd <workspace> --yes)` |
|
||||
| Permission-only change on running seats | `python3 scripts/seed-rig.py rigs/<rig>/rig.yaml --refresh-policy-only` (pps hot-reloads config.json and updates sandbox.env path_write roots; no relaunch, #96). Charter/model/MCP changes still need a relaunch. |
|
||||
| Relaunch or re-home ONE seat | Re-seed, then `rig seat launch <seat> --fresh --stop --reason "<why>"` (blank conversation; queue/identity kept). Move a seat to a mission workspace first with `rig seat set-cwd <seat> --cwd <abs> --reason "<why>"`. `rig launch` never relaunches a running node (#11). Write-scope lookup: `docs/SEAT-WRITE-SCOPES.md` (`scripts/seat-scopes.py --write` after fleet.yaml changes). |
|
||||
| Talk to a seat | `rig send '<pod>-<member>@<rig>' "<message>"` / `rig capture` / `rig transcript --tail N` |
|
||||
| Queue work | `rig queue create --json --verify --destination <session> --body-file <file>` / `rig queue list --json [--destination S] [-a]` (copy qitem IDs only from `--json` output — never retype; bare queue output mixes JSON with a human trailer/advisory and MUST NOT be parsed; bare create output is advisory, not proof of persistence; bare `rig queue list`, bare `rig queue create` (no `--json --verify`), and bare `rig send` deny in-seat — always pass `--json` (`--json --verify` for create)) |
|
||||
| Tracker proposals | `make reconcile TRACKER=… [OUT=…]` / `make generate-tracker TRACKER=… OUT=…` — propose-only: NEVER auto-edit trackers or write queue state; `--out` MUST be a fresh non-tracker path (refuse `*dispatch-tracker.md`, `sprints/` locations, existing files; `O_EXCL` create) — violations fail closed (exit 2). |
|
||||
|
||||
@@ -409,3 +409,9 @@
|
||||
- Revert verified: `git status --short` shows only `M scripts/verify-rigs.sh`; re-ran verify → 30/0.
|
||||
- `make test` (untouched, proven): `Ran 257 tests ... OK`, exit 0; Makefile `test:` runs 11 suites.
|
||||
- `scripts/generate-rigs.py` NOT run (no catalog edits in this stage — nothing to re-bake).
|
||||
|
||||
## 2026-10-30 — #91/#12 go exec residual + #11/#78 single-seat routes (Claude follow-up)
|
||||
|
||||
- Live finding (fleet-proofs runbook S3): `go build`/`go test` were policy-allowed but died on `failed to initialize build cache at ~/.cache/go-build: read-only file system` because the jail binds `/` read-only. `scripts/seat-sandbox.sh` now sets `XDG_CACHE_HOME` and `GOCACHE` under the per-run scratch. Nothing persists between commands or crosses seats; each command rebuilds what it needs. Red-proof: `tests/test_seat_sandbox.py::test_go_test_runs_in_seat_scope` fails on the old wrapper with the exact live error and passes on the new one.
|
||||
- Remaining go limit (by design, not fixed): `go build` of a single main package writes its binary into the cwd, which is read-only in the jail, and `-o` is denied by the go command effect. Use `go test ./...` or `go build ./...` over several packages (outputs discarded) as the compile proof. `go mod init` cannot create `go.mod` from bash (file roots bind existing files only); write it with the `write` tool first.
|
||||
- #11/#78: OpenRig 0.7.5 (the pinned host toolchain) already ships `rig seat launch --fresh --stop`, `rig seat stop` and `rig seat set-cwd`, and `rig launch` help no longer promises relaunch. Seat-heal/seat-repair used the re-seed + fresh-launch route live on 24 seats. Documented in USER_GUIDE "Refreshing or re-homing one seat", AGENTS.md, and the generated `docs/SEAT-WRITE-SCOPES.md` (`scripts/seat-scopes.py`, drift-tested by `tests/test_seat_scopes.py`). `set-cwd` on a pi seat is not yet proven live.
|
||||
@@ -0,0 +1,80 @@
|
||||
# Seat write scopes
|
||||
|
||||
<!-- Generated by scripts/seat-scopes.py from fleet/fleet.yaml. Do not edit by hand. -->
|
||||
|
||||
Every seat's `path_write` globs are relative to the workspace it was launched
|
||||
in (`rig up ... --cwd <workspace>`), and `external_directory` is `deny`. A
|
||||
mission whose files sit outside these globs cannot be written by any seat:
|
||||
the seat will block honestly, not work around it (#78).
|
||||
|
||||
Place mission files so each slice lands under its owning seat's globs. A new
|
||||
top-level folder that no row below covers is writable by no seat.
|
||||
|
||||
To give a mission its own workspace without bouncing the fleet, move only
|
||||
the seats it needs (OpenRig 0.7.5+; see docs/USER_GUIDE.md, "Refreshing or
|
||||
re-homing one seat"):
|
||||
|
||||
```bash
|
||||
rig seat set-cwd <seat> --cwd /abs/path/to/mission-workspace --reason "<mission>"
|
||||
rig seat launch <seat> --fresh --stop --reason "<mission>: adopt workspace"
|
||||
```
|
||||
|
||||
Inside that workspace the same relative globs apply. Fresh launch starts a
|
||||
blank conversation; queue rows and seat identity are kept.
|
||||
|
||||
`*` in a glob also matches `/`, so `src/*` covers `src/a/b.c`. Rows marked
|
||||
read-only charter write reports only; they do not author code.
|
||||
|
||||
| Seat | Rig | Writable (relative to the workspace) |
|
||||
|---|---|---|
|
||||
| `data-analytics-metrics-eng` | cloud-sre-and-data-platform | `migrations/clickhouse/*`, `analytics/*` |
|
||||
| `data-cache-streaming-eng` | cloud-sre-and-data-platform | `schemas/events/*`, `src/cache/*`, `deploy/kafka/*` |
|
||||
| `platform-cloud-platform-eng` | cloud-sre-and-data-platform | `terraform/*`, `infra/*` |
|
||||
| `data-data-pipeline-eng` | cloud-sre-and-data-platform | `dbt/*`, `pipelines/*` |
|
||||
| `data-dba-specialist` | cloud-sre-and-data-platform | `migrations/*`, `docs/db/*` |
|
||||
| `platform-dependency-patcher` | cloud-sre-and-data-platform | `*package-lock.json`, `*pnpm-lock.yaml`, `package.json`, `Cargo.toml`, `Cargo.lock`, `pyproject.toml`, `go.mod`, `go.sum` |
|
||||
| `platform-k8s-orchestrator` | cloud-sre-and-data-platform | `deploy/*`, `k8s/*` |
|
||||
| `ml-mlops-serving-eng` | cloud-sre-and-data-platform | `deploy/mlops/*`, `tools/mlops/*` |
|
||||
| `sre-observability-eng` | cloud-sre-and-data-platform | `deploy/prometheus/*`, `deploy/grafana/*`, `deploy/otel/*` |
|
||||
| `sre-sre-incident-eng` | cloud-sre-and-data-platform | `docs/incidents/*`, `docs/runbooks/*` (read-only charter) |
|
||||
| `ml-vector-rag-spec` | cloud-sre-and-data-platform | `src/rag/*`, `tests/rag/*` |
|
||||
| `red-ai-red-teamer` | defensive-sec-and-red-team | `docs/security/ai-redteam/*` (read-only charter) |
|
||||
| `blue-appsec-engineer` | defensive-sec-and-red-team | `docs/security/advisories/*` (read-only charter) |
|
||||
| `red-binary-reverse-eng` | defensive-sec-and-red-team | `docs/protocols/*`, `docs/forensics/*` (read-only charter) |
|
||||
| `blue-cloud-sec-auditor` | defensive-sec-and-red-team | `docs/security/cloud/*` (read-only charter) |
|
||||
| `blue-compliance-auditor` | defensive-sec-and-red-team | `sbom/*`, `docs/compliance/*` (read-only charter) |
|
||||
| `red-firmware-sec-analyst` | defensive-sec-and-red-team | `docs/security/firmware/*` (read-only charter) |
|
||||
| `blue-threat-modeler` | defensive-sec-and-red-team | `docs/security/threat-models/*` (read-only charter) |
|
||||
| `exec-exec-director` | executive-and-architecture | `docs/executive/*` (read-only charter) |
|
||||
| `delivery-git-drift-coord` | executive-and-architecture | `docs/ops/*` (read-only charter) |
|
||||
| `exec-product-director` | executive-and-architecture | `docs/product/*`, `docs/brief/*` (read-only charter) |
|
||||
| `exec-system-architect` | executive-and-architecture | `docs/adr/*`, `docs/architecture/*` (read-only charter) |
|
||||
| `delivery-tech-docs-lead` | executive-and-architecture | `docs/*`, `docs/*.md` (read-only charter) |
|
||||
| `delivery-tpm-scrum-lead` | executive-and-architecture | `docs/sprints/*` (read-only charter) |
|
||||
| `design-a11y-specialist` | fullstack-and-web-studio | `tests/a11y/*`, `docs/a11y/*`, `docs/design-system/*` |
|
||||
| `design-brand-copy-spec` | fullstack-and-web-studio | `locales/*`, `docs/brand/*` |
|
||||
| `design-design-system-eng` | fullstack-and-web-studio | `packages/ui/*`, `tokens/*`, `src/components/ui/*`, `docs/design-system/*` |
|
||||
| `design-fe-perf-eng` | fullstack-and-web-studio | `*.config.js`, `*.config.ts`, `docs/perf/*` |
|
||||
| `build-go-backend-eng` | fullstack-and-web-studio | `cmd/*`, `internal/*`, `pkg/*`, `go.mod`, `go.sum` |
|
||||
| `build-java-kotlin-eng` | fullstack-and-web-studio | `src/main/*`, `src/test/*`, `build.gradle*`, `pom.xml` |
|
||||
| `design-mobile-web-eng` | fullstack-and-web-studio | `src/styles/*`, `src/components/*`, `public/*` |
|
||||
| `build-python-backend-spec` | fullstack-and-web-studio | `app/*`, `tests/*`, `pyproject.toml` |
|
||||
| `build-ts-node-fullstack` | fullstack-and-web-studio | `src/*`, `package.json`, `docs/architecture/*` |
|
||||
| `design-ui-ux-designer` | fullstack-and-web-studio | `src/components/*`, `src/assets/*`, `docs/design-system/*` |
|
||||
| `design-ux-research-analyst` | fullstack-and-web-studio | `docs/research/*`, `docs/design-system/*` (read-only charter) |
|
||||
| `engine-engine-systems-eng` | game-dev-and-sim-rig | `src/engine/*`, `tests/engine/*`, `build.sh`, `CMakePresets.json`, `CMakeLists.txt`, `build/*` |
|
||||
| `gameplay-game-ai-dev` | game-dev-and-sim-rig | `src/game/ai/*`, `tests/game/ai/*` |
|
||||
| `gameplay-game-systems-dev` | game-dev-and-sim-rig | `src/game/*`, `tests/game/*` |
|
||||
| `engine-graphics-shader-eng` | game-dev-and-sim-rig | `src/shaders/*`, `src/rendering/*` |
|
||||
| `engine-physics-sim-eng` | game-dev-and-sim-rig | `src/engine/physics/*`, `tests/engine/physics/*` |
|
||||
| `gameplay-tech-artist-pipeline` | game-dev-and-sim-rig | `assets/*`, `tools/asset-pipeline/*` |
|
||||
| `qa-api-contract-tester` | qa-and-verification-gauntlet | `tests/api/*`, `openapi/*` |
|
||||
| `qa-e2e-browser-qa` | qa-and-verification-gauntlet | `tests/e2e/*`, `playwright.config.*` |
|
||||
| `qa-load-stress-tester` | qa-and-verification-gauntlet | `tests/load/*`, `docs/perf/*` |
|
||||
| `qa-release-qa-officer` | qa-and-verification-gauntlet | `docs/releases/*` (read-only charter) |
|
||||
| `qa-sabotage-qa-tester` | qa-and-verification-gauntlet | `tests/chaos/*`, `docs/qa/*` |
|
||||
| `qa-static-analysis-lead` | qa-and-verification-gauntlet | `*.eslintrc.json`, `*eslint.config.js`, `rules/*`, `biome.json`, `ruff.toml` |
|
||||
| `mobile-android-mobile-spec` | systems-and-mobile-foundry | `app/src/*`, `build.gradle*`, `app/build.gradle*`, `docs/evidence/android-artemis/*` |
|
||||
| `systems-cpp-lowlevel-eng` | systems-and-mobile-foundry | `src/*`, `include/*`, `tests/*`, `CMakeLists.txt` |
|
||||
| `mobile-ios-mobile-spec` | systems-and-mobile-foundry | `ios/*`, `Sources/*`, `Tests/*`, `Package.swift` |
|
||||
| `systems-rust-systems-spec` | systems-and-mobile-foundry | `crates/*`, `src/*`, `tests/*`, `Cargo.toml`, `Cargo.lock` |
|
||||
@@ -244,6 +244,40 @@ Every seat is governed by the **Heretek Fleet Culture** (`CULTURE.md`) and `@got
|
||||
`[pi-permission-system] Denied by policy: 'path_write' for tool 'write' for path 'src/auth.ts' (rule '*')`
|
||||
- Instead of working around the restriction, the seat writes its advisory to `docs/security/advisories/` and escalates or hands off the code fix to its designated escalation target (`rig send <reports_to> "<message>"`) as documented in its `<agentDir>/AGENTS.md` `## Seat` block.
|
||||
|
||||
### Where Mission Files Must Live
|
||||
|
||||
Seats can only write under their own `path_write` globs, relative to the
|
||||
workspace they were launched in, and every seat denies paths outside that
|
||||
workspace. Check [`SEAT-WRITE-SCOPES.md`](SEAT-WRITE-SCOPES.md) (generated from
|
||||
`fleet/fleet.yaml`) before placing a mission, and put each slice under the
|
||||
globs of the seat that owns it. A scratch directory or a new top-level folder
|
||||
is writable by no seat (#78).
|
||||
|
||||
### Refreshing or Re-homing One Seat
|
||||
|
||||
`rig launch <rig> <node>` does not relaunch a running node (#11). Use the
|
||||
single-seat verbs instead; they touch only the named seat, and its queue rows,
|
||||
address and identity are kept.
|
||||
|
||||
```bash
|
||||
# Permission-only change: hot reload, no relaunch (#96)
|
||||
python3 scripts/seed-rig.py rigs/<rig>/rig.yaml --refresh-policy-only
|
||||
|
||||
# Charter, model, MCP or skill change: re-seed, then relaunch only the
|
||||
# affected seat. Check it is idle first (rig ps --nodes --json); a fresh
|
||||
# launch starts a blank conversation.
|
||||
python3 scripts/seed-rig.py rigs/<rig>/rig.yaml # or members/<role>.yaml --rig <rig> --pod <pod>
|
||||
rig seat launch <seat> --fresh --stop --reason "refresh: <change>"
|
||||
|
||||
# Give one seat a mission workspace (path_write globs then apply there)
|
||||
rig seat set-cwd <seat> --cwd /abs/path/to/mission-workspace --reason "<mission>"
|
||||
rig seat launch <seat> --fresh --stop --reason "<mission>: adopt workspace"
|
||||
```
|
||||
|
||||
Preview any launch with `--plan`. After a whole-fleet restart, resumed seats
|
||||
can fail their first model call (`encrypted_content was not issued to this
|
||||
caller`); the same `rig seat launch <seat> --fresh --stop` clears it.
|
||||
|
||||
### Snapshotting, Restoring, and Stopping a Rig
|
||||
|
||||
```bash
|
||||
|
||||
@@ -195,6 +195,11 @@ done
|
||||
scratch=$(mktemp -d "$jail_tmp/seatjail.$$.XXXXXX")
|
||||
# Cleaned on exit, so bwrap below must not be exec'd (exec drops the trap).
|
||||
trap 'jail_rm "$scratch"' EXIT
|
||||
# #91/#12: $HOME is read-only in the jail, so toolchains that default their
|
||||
# cache under it (go: ~/.cache/go-build, golangci-lint and others via
|
||||
# XDG_CACHE_HOME) fail before doing any work. Point them at the per-run
|
||||
# scratch: nothing persists between commands or crosses seats.
|
||||
mkdir -p "$scratch/cache/go-build"
|
||||
|
||||
# A write root may never resolve outside cwd (symlinked dir/file escape), and
|
||||
# never touch agent/VCS control paths (.git, .pi, .agents, AGENTS.md).
|
||||
@@ -304,6 +309,8 @@ bwrap \
|
||||
--ro-bind / / --dev /dev --proc /proc \
|
||||
"${broad[@]}" --ro-bind "$cwd" "$cwd" \
|
||||
--bind "$scratch" "$scratch" --setenv TMPDIR "$scratch" \
|
||||
--setenv XDG_CACHE_HOME "$scratch/cache" \
|
||||
--setenv GOCACHE "$scratch/cache/go-build" \
|
||||
"${binds[@]}" "${mask[@]}" --chdir "$cwd" \
|
||||
/bin/bash -c "$cmd" <&3 3<&- &
|
||||
child=$!
|
||||
|
||||
Executable
+92
@@ -0,0 +1,92 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Render the per-seat write-scope table (docs/SEAT-WRITE-SCOPES.md).
|
||||
|
||||
#78: every seat runs with `external_directory: deny` and cwd-relative
|
||||
`path_write` globs, so a mission's files must sit under the globs of the
|
||||
seats that will touch them. This table is the operator's lookup for that,
|
||||
generated from fleet/fleet.yaml so it cannot drift silently
|
||||
(tests/test_seat_scopes.py fails when the committed copy is stale).
|
||||
|
||||
Usage:
|
||||
python3 scripts/seat-scopes.py # print to stdout
|
||||
python3 scripts/seat-scopes.py --write # rewrite docs/SEAT-WRITE-SCOPES.md
|
||||
python3 scripts/seat-scopes.py --check # exit 1 if the committed copy is stale
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import yaml
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
FLEET = ROOT / "fleet/fleet.yaml"
|
||||
OUT = ROOT / "docs/SEAT-WRITE-SCOPES.md"
|
||||
|
||||
HEADER = """\
|
||||
# Seat write scopes
|
||||
|
||||
<!-- Generated by scripts/seat-scopes.py from fleet/fleet.yaml. Do not edit by hand. -->
|
||||
|
||||
Every seat's `path_write` globs are relative to the workspace it was launched
|
||||
in (`rig up ... --cwd <workspace>`), and `external_directory` is `deny`. A
|
||||
mission whose files sit outside these globs cannot be written by any seat:
|
||||
the seat will block honestly, not work around it (#78).
|
||||
|
||||
Place mission files so each slice lands under its owning seat's globs. A new
|
||||
top-level folder that no row below covers is writable by no seat.
|
||||
|
||||
To give a mission its own workspace without bouncing the fleet, move only
|
||||
the seats it needs (OpenRig 0.7.5+; see docs/USER_GUIDE.md, "Refreshing or
|
||||
re-homing one seat"):
|
||||
|
||||
```bash
|
||||
rig seat set-cwd <seat> --cwd /abs/path/to/mission-workspace --reason "<mission>"
|
||||
rig seat launch <seat> --fresh --stop --reason "<mission>: adopt workspace"
|
||||
```
|
||||
|
||||
Inside that workspace the same relative globs apply. Fresh launch starts a
|
||||
blank conversation; queue rows and seat identity are kept.
|
||||
|
||||
`*` in a glob also matches `/`, so `src/*` covers `src/a/b.c`. Rows marked
|
||||
read-only charter write reports only; they do not author code.
|
||||
|
||||
| Seat | Rig | Writable (relative to the workspace) |
|
||||
|---|---|---|
|
||||
"""
|
||||
|
||||
|
||||
def render() -> str:
|
||||
fleet = yaml.safe_load(FLEET.read_text())
|
||||
roles = fleet["roles"]
|
||||
rows = []
|
||||
for rid, role in sorted(roles.items(), key=lambda kv: (kv[1]["rig"], kv[0])):
|
||||
seat = role["session"].split("@", 1)[0]
|
||||
globs = role["permissions"]["path_write"]
|
||||
scope = ", ".join(f"`{g}`" for g in globs) or "_none_"
|
||||
if role.get("readonly_charter"):
|
||||
scope += " (read-only charter)"
|
||||
rows.append(f"| `{seat}` | {role['rig']} | {scope} |\n")
|
||||
return HEADER + "".join(rows)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
||||
mode = ap.add_mutually_exclusive_group()
|
||||
mode.add_argument("--write", action="store_true")
|
||||
mode.add_argument("--check", action="store_true")
|
||||
args = ap.parse_args()
|
||||
text = render()
|
||||
if args.write:
|
||||
OUT.write_text(text)
|
||||
elif args.check:
|
||||
if not OUT.exists() or OUT.read_text() != text:
|
||||
print(f"{OUT.relative_to(ROOT)} is stale: run scripts/seat-scopes.py --write", file=sys.stderr)
|
||||
return 1
|
||||
else:
|
||||
sys.stdout.write(text)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -105,6 +105,39 @@ class SeatSandboxEscapes(unittest.TestCase):
|
||||
proc = self.run_jailed('echo t > "$TMPDIR/x" && echo t > /tmp/y')
|
||||
self.assertEqual(proc.returncode, 0, proc.stderr)
|
||||
|
||||
def test_toolchain_caches_point_into_scratch(self):
|
||||
"""#91/#12: go (and anything on XDG_CACHE_HOME) needs a writable
|
||||
cache; it lives under the per-run scratch while $HOME stays
|
||||
read-only."""
|
||||
proc = self.run_jailed(
|
||||
'case "$GOCACHE" in "$TMPDIR"/*) ;; *) exit 3 ;; esac; '
|
||||
'case "$XDG_CACHE_HOME" in "$TMPDIR"/*) ;; *) exit 4 ;; esac; '
|
||||
'test -d "$GOCACHE" && echo t > "$XDG_CACHE_HOME/x"'
|
||||
)
|
||||
self.assertEqual(proc.returncode, 0, proc.stderr)
|
||||
self.run_jailed('mkdir -p "$HOME/.cache/go-build"')
|
||||
self.assertFalse((self.home / ".cache").exists())
|
||||
|
||||
def test_cache_does_not_survive_the_run(self):
|
||||
d = self._jail_scratch()
|
||||
proc = self.run_jailed(
|
||||
'echo t > "$GOCACHE/x"', HERETEK_JAIL_SCRATCH=str(d)
|
||||
)
|
||||
self.assertEqual(proc.returncode, 0, proc.stderr)
|
||||
self.assertEqual(list(d.iterdir()), [], "seatjail cache leaked")
|
||||
|
||||
@unittest.skipUnless(shutil.which("go"), "go toolchain not installed")
|
||||
def test_go_test_runs_in_seat_scope(self):
|
||||
"""#91 live finding: `go test ./...` failed on a read-only GOCACHE."""
|
||||
(self.ws / "src/go.mod").write_text("module example.com/w\n\ngo 1.21\n")
|
||||
(self.ws / "src/w_test.go").write_text(
|
||||
'package w\nimport "testing"\nfunc TestW(t *testing.T) {}\n'
|
||||
)
|
||||
# The test HOME sits under /tmp, which the jail replaces with a
|
||||
# writable tmpfs; a real seat's HOME is on the read-only host root.
|
||||
proc = self.run_jailed("cd src && go test ./...", HOME=str(REPO_ROOT))
|
||||
self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr)
|
||||
|
||||
# --- the per-command scratch dir is cleaned up ----------------------
|
||||
def _jail_scratch(self):
|
||||
d = Path(self.tmp.name) / "jailscratch"
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
#!/usr/bin/env python3
|
||||
"""#78: docs/SEAT-WRITE-SCOPES.md must match fleet/fleet.yaml path_write."""
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
import yaml
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parent.parent
|
||||
SCRIPT = REPO_ROOT / "scripts/seat-scopes.py"
|
||||
|
||||
|
||||
class SeatScopesDoc(unittest.TestCase):
|
||||
def test_committed_table_is_current(self):
|
||||
proc = subprocess.run(
|
||||
[sys.executable, str(SCRIPT), "--check"], capture_output=True, text=True
|
||||
)
|
||||
self.assertEqual(proc.returncode, 0, proc.stderr)
|
||||
|
||||
def test_every_seat_and_glob_listed(self):
|
||||
doc = (REPO_ROOT / "docs/SEAT-WRITE-SCOPES.md").read_text()
|
||||
roles = yaml.safe_load((REPO_ROOT / "fleet/fleet.yaml").read_text())["roles"]
|
||||
self.assertEqual(len(roles), 51)
|
||||
for role in roles.values():
|
||||
seat = role["session"].split("@", 1)[0]
|
||||
line = next((ln for ln in doc.splitlines() if ln.startswith(f"| `{seat}` |")), None)
|
||||
self.assertIsNotNone(line, seat)
|
||||
for glob in role["permissions"]["path_write"]:
|
||||
self.assertIn(f"`{glob}`", line, seat)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user