Skip to content

Grant a task write access to a repository

Page status: release-ready Source state: shipped-source Applies to: Shepherd v0.2.0 Owner: @docs-system-owner (TBD) Validation: shepherd/packages/dialect/tests/test_lane_c_acceptance_gate.py

How-to guide. New to Shepherd? Start with the quickstart. For exact APIs, see the reference.

Job. Give a task read-write access to one bound repository and read-only access to another, so a violation is refused by the operating system — not caught by convention. This is the per-binding signature-grant surface that shipped in 0.2.0 (the mental model is in Permissions).

1. Declare the grants in the signature

Each parameter carries a grant. docs may be read but not written; backend is a writable root:

from shepherd import task, May, GitRepo, ReadOnly, ReadWrite

@task
def apply_documented_fix(
    docs:    May[GitRepo, ReadOnly],   # read-only: writes refused at the OS
    backend: May[GitRepo, ReadWrite],  # writable root
    issue:   str,
) -> None: ...

Reading the signature is reading the permission surface. shepherd task show apply_documented_fix renders it expanded (docs read-only / backend read-write).

2. Bind the repositories by name

Bound roots must be disjoint — Shepherd refuses overlapping or nested binds at bind time, so every managed path belongs to exactly one binding:

docs    = ws.bind(root="docs/",    name="docs")     # returns a GitRepo value
backend = ws.bind(root="backend/", name="backend")

3. Run on a jailed placement

Pass the bound repositories by name, and choose a jailed placement so the grants are enforced by the OS rather than merely recorded:

run = ws.run(
    apply_documented_fix,
    bindings={"docs": docs, "backend": backend},
    args={"issue": "#142"},
    placement="jail",          # writable roots compiled from the grants; Seatbelt/Landlock
)

A write to docs/ — or to any managed path not covered by a ReadWrite grant — is refused at the syscall, before the last undo point. On a jail-less host, placement="jail" fails closed rather than downgrading silently.

4. Inspect and settle

Review what the run produced per binding, then settle its retained output once:

cs = run.changeset(name="backend")   # a read-only view of one binding's delta
print(cs.changed_paths)
ws.select(run.output())              # keep it; or ws.release(...) / ws.discard(...)

Settlement is consume-once: after one of select / release / discard records its outcome, the others refuse for that output.

Scope (0.2.0)

Grants are whole-profile per binding (a bound repository is entirely writable or entirely read-only). Enforcement is exercised on macOS Seatbelt; Linux Landlock is container-gated. Sub-root / where(path=…) grants are not part of this cut.