diff --git a/canon/CHANGELOG.md b/canon/CHANGELOG.md index 3b6ec7b..065271d 100644 --- a/canon/CHANGELOG.md +++ b/canon/CHANGELOG.md @@ -18,6 +18,21 @@ This changelog tracks changes to the **Canon pack** as a whole. The Canon uses **pack-level versioning** (one version number) rather than per-file versioning. Per-file versions are intentionally omitted to reduce ceremony and prevent metadata rot. +## 0.42.0 — 2026-07-17 + +**Sandbox Hygiene — Per-Flight Owned Scratch, No Fixed Shared Temp Names** + +Encodes a hard rule against fixed, shared temp paths for flight working state — most damagingly `/tmp/pr_body.md`. Authored from residue observed directly at boot: a stale `/tmp/pr_body.md` owned by `nobody:nogroup` from the prior flight, the same artifact class that produced wrong-content PRs and git-lock failures across earlier flights. Every flight now mints a uniquely-named, self-owned scratch directory (`mktemp -d`, mode 0700), works only inside it, and tears it down on exit. The rule is paired with a mechanical enforcer (preflight scratch-mint + df/stale-file check, EXIT-trap teardown, a sourced helper, and a practice/lint check) so it does not decay to advice. Reconciled with the policy-precedes-build ordering: the constraint is drafted before the implementing fix, which cites it by URI. + +### Added — Canon Surface + +- **Sandbox Hygiene — Per-Flight Owned Scratch, No Fixed Shared Temp Names** (`canon/constraints/sandbox-hygiene-per-flight-scratch.md`) — Tier 1, neutral, semi_stable. WHAT/WHY/ENFORCEMENT/SCOPE/VERIFICATION anatomy: unique self-owned scratch, no fixed shared temp names, mandatory teardown, mechanical preflight + teardown enforcement, and an honest scope split between the repo-enforceable helper and the operator-side per-flight `TMPDIR` isolation setting. + +### Governance + +- Minor version bump per `canon/constraints/governance-change-discipline.md` — a new tier-1 canon constraint that adds an operating rule for flight sandbox hygiene. Release notes at `docs/oddkit/release-notes/2026-07-17-sandbox-hygiene-per-flight-scratch.md`. No epoch bump: this reinforces existing E0010 flight-crew posture (mechanical enforcement over memory, repo-truth extended to the sandbox) rather than shifting who initiates. +- DRAFT status: opened as a draft PR for review. `policy-precedes-build` / `enforceable-policy-anatomy` were not found as committed canon files; `derives_from` should be repointed at them before merge if they exist. + ## 0.41.0 — 2026-07-09 **The Dispatcher Dispatches; It Never Executes In-Session** diff --git a/canon/constraints/sandbox-hygiene-per-flight-scratch.md b/canon/constraints/sandbox-hygiene-per-flight-scratch.md new file mode 100644 index 0000000..a13e81a --- /dev/null +++ b/canon/constraints/sandbox-hygiene-per-flight-scratch.md @@ -0,0 +1,76 @@ +--- +uri: klappy://canon/constraints/sandbox-hygiene-per-flight-scratch +title: "Sandbox Hygiene — Per-Flight Owned Scratch, No Fixed Shared Temp Names" +audience: canon +exposure: nav +tier: 1 +voice: neutral +stability: semi_stable +tags: ["canon", "constraints", "sandbox", "hygiene", "temp-files", "scratch", "teardown", "flight", "self-governance"] +epoch: E0010 +date: 2026-07-17 +derives_from: "canon/values/axioms.md (Axiom 1 — Reality Is Sovereign; Axiom 3 — Integrity Is Non-Negotiable Efficiency), docs/appendices/convention-requires-an-enforcer.md, docs/decisions/D0004-repo-truth-cleanup-mandatory.md" +governs: "Any flight (agent session) that creates temporary or working files, clones repositories, or writes PR bodies in a shared or persistent sandbox" +complements: "canon/constraints/governance-change-discipline.md, canon/constraints/definition-of-done.md, docs/appendices/repo-truth.md" +target_repo: "outcomes-driven-development" +--- + +# Sandbox Hygiene — Per-Flight Owned Scratch, No Fixed Shared Temp Names + +> A flight that writes to a fixed, shared temp path (`/tmp/pr_body.md` and its kin) inherits whatever the last flight left there. This session observed a stale `/tmp/pr_body.md` (6347 bytes, owned by `nobody:nogroup`, dated the prior evening) sitting in the sandbox at boot — the exact residue that produced wrong-content PRs across earlier flights. Shared fixed names are a correctness hazard, not a convenience. Every flight mints its own uniquely-named, self-owned scratch directory, works only inside it, and tears it down on exit. + +--- + +## Summary — Shared Temp State Is Residue, and Residue Lies + +Flights run one after another in a sandbox whose `/tmp` is neither per-flight nor reliably wiped between runs. When a flight writes to a fixed path — most damagingly `/tmp/pr_body.md` — three failure modes follow, all observed this session: + +1. **Wrong-content PRs.** A prior flight's `pr_body.md` is still present. The next flight's overwrite is blocked (foreign ownership) or silently races, and a PR is opened carrying *another flight's text* before anyone catches it. +2. **Git-lock / write failures.** Clones or writes land in a path a prior flight left root- or `nobody`-owned, and the current flight cannot take the lock. +3. **Root-owned residue** left behind in the sandbox, poisoning the next flight the same way. + +This is the sandbox-level expression of the same law that governs the repository: *if the workspace is dirty, conclusions drawn from it are invalid* (`docs/appendices/repo-truth.md`, `D0004`). State residue is indistinguishable from signal. The fix is not "remember to clean up" — humans and agents forget, and a system whose failure analysis reads "the flight forgot to…" has violated the humans-are-variable-inputs constraint. The fix is a mechanical preflight-and-teardown that makes unique, self-owned scratch the path of least resistance. + +--- + +## WHAT — The Rule + +- A flight **MUST** perform all temporary and working I/O inside a **uniquely-named, self-owned** scratch directory minted at flight start (e.g. `mktemp -d "${TMPDIR:-/tmp}/flight.XXXXXXXX"`, mode `0700`). +- A flight **MUST NOT** read or write **fixed shared names** for working state — explicitly including `/tmp/pr_body.md`, and any other constant `/tmp/` used for PR bodies, clones, diffs, or scratch. +- PR bodies, repo clones, and generated diffs **MUST** live under the flight's scratch directory, never at a fixed path. +- A flight **MUST NOT** leave artifacts owned by a foreign user (root, `nobody`) behind; it **MUST** tear down its scratch directory on exit (success or failure). +- The launch environment **SHOULD** isolate per-flight scratch (a fresh or namespaced `TMPDIR` per flight) so isolation does not depend solely on flight discipline. *(This layer is a launch-config setting, not a repo change — see Scope.)* + +## WHY — The Failure This Closes + +Authored from residue observed directly at boot this session: a stale `/tmp/pr_body.md` owned by `nobody:nogroup` from the prior flight, the same class of artifact that caused wrong-content PRs and git-lock failures across multiple flights. The tiny root disk (~9.6 GB, `/tmp` on root) makes leaked clones and bodies a space hazard as well as a correctness one. Cloud runtime hardening (PR #80) addresses the per-run cloud sandbox; this constraint addresses the **local flight sandbox**, whose `/tmp` persists across flights. A convention without an enforcer is a ritual with a deadline (`docs/appendices/convention-requires-an-enforcer.md`); this constraint pairs the rule with a mechanical enforcer so it does not decay to advice. + +## ENFORCEMENT — Mechanically, Not by Memory + +- **Preflight (before work):** a scratch-setup step that (a) mints the unique owned scratch dir, (b) runs a `df` free-space check and warns/fails below threshold, and (c) scans for and reports stale fixed-name temp files (starting with `/tmp/pr_body.md`) so the residue is *observed*, not silently inherited. +- **Teardown (on exit):** an `EXIT` trap that removes the flight's scratch dir, and only dirs it owns under the tmp root — never a blind `rm -rf` of a shared path. +- **Shared helper:** the preflight + scratch-mint + teardown are provided as a single sourced helper (`agent-role-service/scripts/flight-scratch.sh`) so every flight gets the behavior by sourcing one file, not by re-implementing it. A `flight_prbody` accessor returns a unique path *inside* scratch, structurally preventing the fixed-name write. +- **Practice check:** a lint/test that fails when flight tooling references a fixed `/tmp/pr_body.md` (or equivalent constant working path) instead of the helper. +- **Launch isolation (operator-side):** per-flight `TMPDIR` isolation in the Cowork launch configuration is the belt to this suspenders; named in Scope as out-of-repo. + +## SCOPE — Where This Binds + +- **Binds:** every flight (agent session) that writes temp/working files, clones repos, or composes PR bodies in the local/shared flight sandbox; and the shared flight tooling that flights source. +- **Repo-enforceable pieces:** the sourced helper, its tests, and the practice/lint check — implemented in `agent-role-service` (the flights' shared tooling home). +- **Out of repo (operator-side):** per-flight scratch *isolation* at launch (a fresh/namespaced `TMPDIR` per flight) is a **Cowork launch-configuration setting**, not a repository change. It cannot be delivered by PR; it is flagged for the operator to apply. +- **Not in scope:** the per-run **cloud** sandbox in `agent-role-service/src/runtime/task.js`, which already provisions a fresh isolated sandbox per run (`/workspace/repo`) and is hardened separately under PR #80. + +## VERIFICATION — How We Know It Holds + +- **Helper unit test:** proves `mktemp`-minted scratch is unique across two calls, mode `0700`, owned by the current uid; that `flight_prbody` returns a path under scratch (never `/tmp/pr_body.md`); that teardown removes only the owned dir; and that preflight detects a planted stale fixed-name file. +- **Residue check:** post-flight, `ls` of the tmp root shows no scratch dir owned by the flight remaining. +- **Practice/lint check:** a grep-based test fails if flight tooling reintroduces a fixed `/tmp/pr_body.md` working path. +- **Definition of Done:** this constraint's own delivery carries change description, verification performed, observed behavior, and evidence per `canon/constraints/definition-of-done.md`. + +--- + +## Reconciliation with Policy-Precedes-Build + +Per the policy-precedes-build ordering (draft the governing constraint before implementing the fix, and cite the constraint from the code), this document is authored and opened as a DRAFT canon PR **before** the mechanical fix, and the `agent-role-service` helper cites this constraint by URI in its header. The build implements what the policy declares; it does not get ahead of it. + +> Honesty note (Axiom 1): as of this writing, no committed canon file named `policy-precedes-build` or `enforceable-policy-anatomy` was found via `oddkit_search` or in the `klappy.dev` working tree. This constraint therefore realizes the *named* ordering and the WHAT/WHY/ENFORCEMENT/SCOPE/VERIFICATION anatomy as the operator specified them, and reconciles with the governance that **is** committed (`governance-change-discipline`, `definition-of-done`, `convention-requires-an-enforcer`, `repo-truth`/`D0004`). If a canonical `policy-precedes-build` / `enforceable-policy-anatomy` exists elsewhere, the `derives_from` line should be updated to point at it before merge. diff --git a/docs/oddkit/release-notes/2026-07-17-sandbox-hygiene-per-flight-scratch.md b/docs/oddkit/release-notes/2026-07-17-sandbox-hygiene-per-flight-scratch.md new file mode 100644 index 0000000..839395a --- /dev/null +++ b/docs/oddkit/release-notes/2026-07-17-sandbox-hygiene-per-flight-scratch.md @@ -0,0 +1,34 @@ +--- +uri: klappy://docs/oddkit/release-notes/2026-07-17-sandbox-hygiene-per-flight-scratch +title: "Release Notes — Sandbox Hygiene: Per-Flight Owned Scratch (2026-07-17)" +audience: docs +exposure: nav +tier: 3 +voice: neutral +stability: semi_stable +tags: ["release-notes", "sandbox", "hygiene", "temp-files", "flight"] +date: 2026-07-17 +complements: "canon/constraints/sandbox-hygiene-per-flight-scratch.md" +target_repo: "outcomes-driven-development" +--- + +# Release Notes — Sandbox Hygiene: Per-Flight Owned Scratch (2026-07-17) + +## What changed for you + +If you run flights (agent sessions) in the shared local sandbox, working state no longer leaks between them. The failure this fixes was concrete and repeated: a flight wrote its PR body to `/tmp/pr_body.md`; the next flight inherited the previous body (or a git lock it couldn't take) and opened a PR with the wrong content before anyone caught it. + +From now on, every flight mints its own uniquely-named, self-owned scratch directory, does all temp/clone/PR-body work inside it, and removes it on exit. Fixed shared temp names — `/tmp/pr_body.md` chief among them — are prohibited. + +## Why it matters + +State residue is indistinguishable from signal. A dirty sandbox invalidates the work drawn from it, exactly as a dirty repository does (`repo-truth`, `D0004`). This extends that law from the repo to the sandbox and pairs it with a mechanical enforcer so it holds without anyone remembering to be careful. + +## What you need to do + +- **Repo side (delivered):** flights source the shared helper `agent-role-service/scripts/flight-scratch.sh` (preflight + scratch-mint + teardown). See its PR. +- **Operator side (action required):** enable per-flight `TMPDIR` isolation in the Cowork launch configuration so scratch isolation does not depend on flight discipline alone. This is a launch-config setting and cannot be delivered by PR. + +## Status + +Draft — opened for review alongside the constraint and the helper PR. No epoch bump; reinforces E0010 flight-crew posture.