Read-only imports: consumer-declared read-only repo snapshots mounted into every spawn #261

Closed
opened 2026-08-02 23:58:36 +02:00 by dominik.polakovics · 1 comment

Parent

PRD: #260

What to build

The full read-only-imports feature as one slice, end-to-end: a repo's settings gain an Imports section declaring other lab repos its instances may read; every spawn of that repo (all run kinds, by construction) fetches each import's reference repo, materializes a snapshot of origin/<default> (no .git) into a per-run directory outside the worktree, and mounts it read-only at a host-identical path in the container runner (best-effort write-protected on the host runner). Any import fetch failure refuses the spawn before the claim, naming the target. The generated context file lists each import (name, absolute path, snapshotted commit) and marks it a read-only snapshot outside the working repo. /pull-base re-materializes every import in place and includes per-import changes in its injected digest. Deleting a repo is blocked while importers reference it, naming the importers. CONTEXT.md gains the read-only import term and an ADR records the decisions.

Grant semantics per the PRD: directional, consumer-declared, flat (no transitivity, mutual imports legal, self-import rejected at save), lab-repo targets only, always origin/<default>, no ref pinning, no tracker access (run token stays single-repo).

Acceptance criteria

  • Migration adds the imports relation keyed (importing repo, target repo), unique per pair, foreign-keyed to repos; migration tests cover it
  • Repo settings UI + API: add/remove imports against registered repos; self-import and unknown target rejected as 400s (service-level tests)
  • Deleting a repo with importers is refused with an error naming the importers (test)
  • A deep materializer module in the git layer: fetch target's reference repo, export origin/<default>'s tree (no .git) to a destination dir, return the snapshotted commit; idempotent in-place re-materialization — integration tests with real bare repos (fresh materialize, upstream change, upstream file deletion, fetch failure)
  • Spawn path materializes all imports (parallel fetches) into the per-run directory before any claim; a failure refuses the spawn with an actionable error naming the target, and no issue is parked (test)
  • Container runner mounts each import read-only at the host-identical path; host runner uses the same path with best-effort chmod a-w
  • Every spawn kind carries imports by construction — manual, AFK, scheduled, lander, fix, escalate — with no per-spawn toggle
  • Context file renders an imports section (name, path, commit, read-only-snapshot sentence); golden test
  • /pull-base refreshes every import in place and its digest reports per-import changes (tests alongside existing pull tests)
  • Import snapshots are removed with the run directory at teardown
  • CONTEXT.md defines read-only import with the avoid-list (dependency, submodule, link, mount, sibling, referenced repo); a new ADR records grant model, materialization, refusal, refresh, and delete-guard decisions

Blocked by

None - can start immediately

## Parent PRD: #260 ## What to build The full read-only-imports feature as one slice, end-to-end: a repo's settings gain an Imports section declaring other lab repos its instances may read; every spawn of that repo (all run kinds, by construction) fetches each import's reference repo, materializes a snapshot of `origin/<default>` (no `.git`) into a per-run directory outside the worktree, and mounts it read-only at a host-identical path in the container runner (best-effort write-protected on the host runner). Any import fetch failure refuses the spawn before the claim, naming the target. The generated context file lists each import (name, absolute path, snapshotted commit) and marks it a read-only snapshot outside the working repo. `/pull-base` re-materializes every import in place and includes per-import changes in its injected digest. Deleting a repo is blocked while importers reference it, naming the importers. CONTEXT.md gains the **read-only import** term and an ADR records the decisions. Grant semantics per the PRD: directional, consumer-declared, flat (no transitivity, mutual imports legal, self-import rejected at save), lab-repo targets only, always `origin/<default>`, no ref pinning, no tracker access (run token stays single-repo). ## Acceptance criteria - [ ] Migration adds the imports relation keyed (importing repo, target repo), unique per pair, foreign-keyed to repos; migration tests cover it - [ ] Repo settings UI + API: add/remove imports against registered repos; self-import and unknown target rejected as 400s (service-level tests) - [ ] Deleting a repo with importers is refused with an error naming the importers (test) - [ ] A deep materializer module in the git layer: fetch target's reference repo, export `origin/<default>`'s tree (no `.git`) to a destination dir, return the snapshotted commit; idempotent in-place re-materialization — integration tests with real bare repos (fresh materialize, upstream change, upstream file deletion, fetch failure) - [ ] Spawn path materializes all imports (parallel fetches) into the per-run directory before any claim; a failure refuses the spawn with an actionable error naming the target, and no issue is parked (test) - [ ] Container runner mounts each import read-only at the host-identical path; host runner uses the same path with best-effort `chmod a-w` - [ ] Every spawn kind carries imports by construction — manual, AFK, scheduled, lander, fix, escalate — with no per-spawn toggle - [ ] Context file renders an imports section (name, path, commit, read-only-snapshot sentence); golden test - [ ] `/pull-base` refreshes every import in place and its digest reports per-import changes (tests alongside existing pull tests) - [ ] Import snapshots are removed with the run directory at teardown - [ ] CONTEXT.md defines **read-only import** with the avoid-list (dependency, submodule, link, mount, sibling, referenced repo); a new ADR records grant model, materialization, refusal, refresh, and delete-guard decisions ## Blocked by None - can start immediately
Author
Owner

This was generated by AI during triage.

Agent Brief

Category: enhancement
Summary: Read-only imports, end-to-end — a repo declares other lab repos whose code its instances may read; every spawn materializes a read-only snapshot of each import outside the worktree, refuses before the claim on fetch failure, and /pull-base refreshes the snapshots.

PRD #260 is the authoritative decision record (grant semantics, materialization, refresh, out-of-scope list). This brief is the working contract; where they seem to differ, ask before diverging.

Current behavior:
An instance sees exactly one repo. The containerized runner (ADR-0052) deliberately closed the hole through which a host-run agent could read other repos' worktrees and bares, so agents code against sibling repos from memory — guessing signatures and endpoints. No "imports" concept exists anywhere today: no store relation, no API field, no settings section, no domain term. The container runner currently mounts exactly one thing read-only (the agent-tools image, an image-type mount); every bind mount is read-write — there is no read-only bind machinery yet. This feature introduces the first.

Desired behavior:

  • A repo's settings gain an Imports section declaring other lab repos (by ID, registered repos only) whose code its instances may read. Directional and consumer-declared; flat (no transitivity); mutual imports legal; self-import rejected at save; unknown target rejected. Both rejections are 400s from the repo-settings service.
  • At every spawn — manual, AFK, scheduled, lander, fix, escalate, by construction via the single launch path (spawn-parity pin in ADR-0062 "Schedules") — lab fetches each import target's reference repo and materializes a snapshot of origin/<default>'s tree (no .git) into the per-run directory outside the worktree. Fetches may run in parallel to keep spawn latency flat.
  • Any import fetch failure refuses the spawn before the claim, with an actionable error naming the failing target, and no issue is parked. Precedent: the dev-image gate already refuses before worktree creation — for an AFK spec the worktree IS the claim, so import materialization must sit above it.
  • The container runner mounts each snapshot read-only at a host-identical path; the host runner uses the same path with best-effort write-protection (chmod a-w).
  • The generated context file gains an imports section: one line per import (name, absolute path, snapshotted commit) plus a sentence marking it a read-only snapshot outside the working repo.
  • /pull-base re-materializes every import in place and reports per-import changes in its injected digest. No other refresh trigger; /clear untouched.
  • Deleting a repo is refused while other repos import it, with the importers named in the error.
  • Import snapshots are removed with the run directory at teardown (they live inside it, so no separate cleanup path should be needed — verify).
  • CONTEXT.md gains the read-only import term with its avoid-list (dependency, submodule, link, mount, sibling, referenced repo); a new ADR records the grant model, materialization, refusal, refresh, and delete-guard decisions.

Key interfaces:

  • Store: a new imports relation keyed (importing repo, target repo), unique per pair, foreign-keyed to repos. New migration in both sqlite and postgres dialects — a migrations parity test enforces the pair exists.
  • Repo-settings service/API: add/remove imports; self-import and unknown-target validation; the delete guard extends the existing delete-refusal precedent (live-instances guard) with an importers check that names the importers.
  • Web UI: a new Imports section in repo settings, following the existing section-registry pattern (each section is a component with a co-located test).
  • Git layer: a new deep materializer module exposing one operation — fetch the target's reference repo, export origin/<default>'s tree (no .git) to a destination directory, return the snapshotted commit — idempotent for in-place re-materialization (upstream file deletions must disappear from the snapshot). All git ops stay on the reference repo, never a checkout. Follow the git layer's existing real-bare-repo integration-test prior art.
  • Spawn path: materialization of all imports into the per-run directory, ordered before any claim; failure produces the actionable named-target refusal.
  • Container runner mount inventory: one read-only bind per import at the host-identical path (first :ro bind in the inventory). Dev images already treat lab's mount points as reserved per the pin in ADR-0053 "Per-repo dev images", so no container-contract renegotiation.
  • Context-file seeder: imports section in the rendered context file, pinned by golden tests (byte-identity precedent from the skills-index work, ADR-0035).
  • Pull service: /pull-base refresh of each import plus per-import digest lines.

ADR citation precision (the PRD's shorthand is slightly off — cite the pins, not imagined titles): ADR-0052 is "Containerized runner" (closed the hole); ADR-0053 is "Per-repo dev images" (contains the pin reserving read-only mounts for future imports); ADR-0062 is "Schedules" (contains the spawn-parity pin). There is no standalone mount-contract or spawn-parity ADR.

Acceptance criteria:

  • Migration adds the imports relation (importing repo, target repo), unique per pair, foreign-keyed to repos, in both dialects; migration tests cover it
  • Repo settings UI + API add/remove imports against registered repos; self-import and unknown target rejected as 400s, with service-level tests
  • Deleting a repo with importers is refused with an error naming the importers (test)
  • Materializer integration tests with real bare repos: fresh materialize, re-materialize after upstream change, upstream file deletion removed from snapshot, fetch failure surfaced; returns the snapshotted commit
  • Spawn materializes all imports (parallel fetches) into the per-run directory before any claim; a failing fetch refuses the spawn with an error naming the target, and no issue is parked (test)
  • Container runner mounts each import read-only at the host-identical path; host runner uses the same path with best-effort chmod a-w
  • Every spawn kind carries imports by construction — no per-spawn toggle anywhere
  • Context file renders the imports section (name, path, commit, read-only-snapshot sentence); golden test
  • /pull-base refreshes every import in place and its digest reports per-import changes, tested alongside the existing pull tests
  • Import snapshots vanish with the run directory at teardown
  • CONTEXT.md defines read-only import with the avoid-list; a new ADR records grant model, materialization, refusal, refresh, and delete-guard decisions

Out of scope:

  • Tracker access to imported repos — the run token stays scoped to the run's own repo; labctl never reads an import's issues or PRs
  • Git history in the snapshot — no .git, no log/blame
  • Ref pinning, arbitrary git URLs, transitive closure, per-spawn toggles, target-side grants or handshakes
  • Shared or cached materializations across runs — one copy per run
  • Any change to /clear / New conversation
> *This was generated by AI during triage.* ## Agent Brief **Category:** enhancement **Summary:** Read-only imports, end-to-end — a repo declares other lab repos whose code its instances may read; every spawn materializes a read-only snapshot of each import outside the worktree, refuses before the claim on fetch failure, and `/pull-base` refreshes the snapshots. PRD #260 is the authoritative decision record (grant semantics, materialization, refresh, out-of-scope list). This brief is the working contract; where they seem to differ, ask before diverging. **Current behavior:** An instance sees exactly one repo. The containerized runner (ADR-0052) deliberately closed the hole through which a host-run agent could read other repos' worktrees and bares, so agents code against sibling repos from memory — guessing signatures and endpoints. No "imports" concept exists anywhere today: no store relation, no API field, no settings section, no domain term. The container runner currently mounts exactly one thing read-only (the agent-tools image, an image-type mount); every bind mount is read-write — there is no read-only *bind* machinery yet. This feature introduces the first. **Desired behavior:** - A repo's settings gain an **Imports** section declaring other lab repos (by ID, registered repos only) whose code its instances may read. Directional and consumer-declared; flat (no transitivity); mutual imports legal; self-import rejected at save; unknown target rejected. Both rejections are 400s from the repo-settings service. - At every spawn — manual, AFK, scheduled, lander, fix, escalate, by construction via the single launch path (spawn-parity pin in ADR-0062 "Schedules") — lab fetches each import target's reference repo and materializes a snapshot of `origin/<default>`'s tree (no `.git`) into the per-run directory outside the worktree. Fetches may run in parallel to keep spawn latency flat. - Any import fetch failure refuses the spawn **before the claim**, with an actionable error naming the failing target, and no issue is parked. Precedent: the dev-image gate already refuses before worktree creation — for an AFK spec the worktree IS the claim, so import materialization must sit above it. - The container runner mounts each snapshot read-only at a host-identical path; the host runner uses the same path with best-effort write-protection (`chmod a-w`). - The generated context file gains an imports section: one line per import (name, absolute path, snapshotted commit) plus a sentence marking it a read-only snapshot outside the working repo. - `/pull-base` re-materializes every import in place and reports per-import changes in its injected digest. No other refresh trigger; `/clear` untouched. - Deleting a repo is refused while other repos import it, with the importers named in the error. - Import snapshots are removed with the run directory at teardown (they live inside it, so no separate cleanup path should be needed — verify). - CONTEXT.md gains the **read-only import** term with its avoid-list (dependency, submodule, link, mount, sibling, referenced repo); a new ADR records the grant model, materialization, refusal, refresh, and delete-guard decisions. **Key interfaces:** - **Store:** a new imports relation keyed (importing repo, target repo), unique per pair, foreign-keyed to repos. New migration in *both* sqlite and postgres dialects — a migrations parity test enforces the pair exists. - **Repo-settings service/API:** add/remove imports; self-import and unknown-target validation; the delete guard extends the existing delete-refusal precedent (live-instances guard) with an importers check that names the importers. - **Web UI:** a new Imports section in repo settings, following the existing section-registry pattern (each section is a component with a co-located test). - **Git layer:** a new deep materializer module exposing one operation — fetch the target's reference repo, export `origin/<default>`'s tree (no `.git`) to a destination directory, return the snapshotted commit — idempotent for in-place re-materialization (upstream file deletions must disappear from the snapshot). All git ops stay on the reference repo, never a checkout. Follow the git layer's existing real-bare-repo integration-test prior art. - **Spawn path:** materialization of all imports into the per-run directory, ordered before any claim; failure produces the actionable named-target refusal. - **Container runner mount inventory:** one read-only bind per import at the host-identical path (first `:ro` bind in the inventory). Dev images already treat lab's mount points as reserved per the pin in ADR-0053 "Per-repo dev images", so no container-contract renegotiation. - **Context-file seeder:** imports section in the rendered context file, pinned by golden tests (byte-identity precedent from the skills-index work, ADR-0035). - **Pull service:** `/pull-base` refresh of each import plus per-import digest lines. **ADR citation precision** (the PRD's shorthand is slightly off — cite the pins, not imagined titles): ADR-0052 is "Containerized runner" (closed the hole); ADR-0053 is "Per-repo dev images" (contains the pin reserving read-only mounts for future imports); ADR-0062 is "Schedules" (contains the spawn-parity pin). There is no standalone mount-contract or spawn-parity ADR. **Acceptance criteria:** - [ ] Migration adds the imports relation (importing repo, target repo), unique per pair, foreign-keyed to repos, in both dialects; migration tests cover it - [ ] Repo settings UI + API add/remove imports against registered repos; self-import and unknown target rejected as 400s, with service-level tests - [ ] Deleting a repo with importers is refused with an error naming the importers (test) - [ ] Materializer integration tests with real bare repos: fresh materialize, re-materialize after upstream change, upstream file deletion removed from snapshot, fetch failure surfaced; returns the snapshotted commit - [ ] Spawn materializes all imports (parallel fetches) into the per-run directory before any claim; a failing fetch refuses the spawn with an error naming the target, and no issue is parked (test) - [ ] Container runner mounts each import read-only at the host-identical path; host runner uses the same path with best-effort `chmod a-w` - [ ] Every spawn kind carries imports by construction — no per-spawn toggle anywhere - [ ] Context file renders the imports section (name, path, commit, read-only-snapshot sentence); golden test - [ ] `/pull-base` refreshes every import in place and its digest reports per-import changes, tested alongside the existing pull tests - [ ] Import snapshots vanish with the run directory at teardown - [ ] CONTEXT.md defines **read-only import** with the avoid-list; a new ADR records grant model, materialization, refusal, refresh, and delete-guard decisions **Out of scope:** - Tracker access to imported repos — the run token stays scoped to the run's own repo; `labctl` never reads an import's issues or PRs - Git history in the snapshot — no `.git`, no log/blame - Ref pinning, arbitrary git URLs, transitive closure, per-spawn toggles, target-side grants or handshakes - Shared or cached materializations across runs — one copy per run - Any change to `/clear` / New conversation
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Cloonar/coding-lab#261
No description provided.