ADR-0053 Architecture decision record
A fetch inside the pod asks the node cache, and the cache asks the remote
ADR-0051 made the node cache the only thing that fetches from a remote, and left one consequence standing: stale stays stale from inside the pod until the interval or the next attach. The only ask a cache agent understood was a pod's birth — asked is the newest pod's creation time, and a Sandbox is Ready once its caches were fetched after its CR was created. Nothing after birth could ask: no CLI, no route out of the pod, no body primitive. The grill of 2026-09-13 took the concrete case — a human pushes a fix to master, the coder at request_changes must rebase onto it now, and the human in the User Container of the same pod wants the same commit — and found that a five-minute interval is not a freshness model, it is the absence of one. How a fetch inside a pod reaches the remote, and who may ask is one decision.
Decision
origin's fetch url is a command, not a path. The attach sets it to git's built-inext::transport, an absolute program on the runtime volume with the Repo's identity as its argument:ext::/opt/jr2/bin/jr2-upload-pack %S github.com/acme/appGit runs the program and bridges stdio, so the program owes no remote-helper protocol: it asks for a fetch, waits for the landing, then execs
git upload-pack /repos/<key>and gets out of the way. Git speaks its ordinary protocol and prints its ordinary output; refs update once, at the end. What the caller sees is a fetch on a slow handshake, never an early return and never a partial state.git fetch,git pull,git ls-remote origin, andgit fetch --dry-runall pass through it, so every fetch inside a pod is a fetch of the remote's now.git archive --remote=originis the one other read git asks a remote for, and the program serves it the same way — the same ask, thengit upload-archiveagainst the same cache — because the path url served it and a decision about fetching may not quietly take it away. Nothing else in git talks to a remote, sostatus,log, andrebasestay local. The push url is unchanged: the Binding's own spelling, the caller's own credential, never the Agent's (ADR-0005).The ask rides the Sandbox CR and reaches the agent through the pod. The program asks the Custodian on
localhost(POST /fetch, with the Sandbox token's Stand-in — a constant, because the User Container gets no env); the Custodian puts the token it holds in place of the Stand-in and asks the Orchestrator for THIS pod's Sandbox (POST /sandboxes/<name>/fetch— the program cannot name another, ADR-0059); the Orchestrator patches one annotation on the Sandbox CR, per Repo key, timestamp value — the Lease's shape, an annotation the Orchestrator writes. The operator copies it onto the pod as it copies everything else the pod needs from the CR, and the cache agent'saskedbecomes the later of the pod's creation and its ask for that key. Demand stays read off pods alone. The mark is a coalescer: however many fetches are in flight before one lands, the remote is fetched once, and a fetch that started before the ask does not satisfy it — the agent stampslastFetchedwith the attempt's start, which is what makes that true.The landing is reported on the Sandbox, standing and per key. The verdict the operator already computes on the way to
Ready— fresh, stale with git's error, or still cloning, per Repo, against the CR's creation — becomes a standing entry on the Sandbox status computed against the later of creation and the ask.Readyitself stays as it was: sticky, once per pod life. The Orchestrator's route waits on that entry — a Sandbox watch event, never a poll (ADR-0063) — then answers the Custodian, which answers the program. One wait, and it is the wait an attach already makes.The scope is the Sandbox token's scope, and no seat gains a credential. A Sandbox may ask for the caches it mounts and nothing else; the remote is reached by the cache agent with the Repo CR's
secretRef, the credential that cloned the cache. The pod holds none before and none after. The Menu is untouched — a fetch is not something the Agent says, it is something it does — and no Machine declares anything: the ability is ambient in every Workspace pod, the way Working tools are, and absent on the Instance Harness, which mounts no Repo.Freshness degrades, absence does not, and the caller is told. A remote fetch that fails or outruns the on-demand budget lets the program fall through to the objects the cache holds — ADR-0051's stance for attach — and writes one line to stderr, which git passes through verbatim:
warning: jr2: remote fetch failed (<git's error>); serving the cache as of <lastFetched>. The budget is the cache agent's one on-demand number; the program's wait is that plus watch slack, and nothing else owns a timeout.Every seat that holds the checkouts holds the program. The fetch url lives in the shared
default/.git/config, so a seat without the program has checkouts whosegit fetchdies. That is ADR-0005's own argument for/repos— half of one exception, not a second — applied once more: the pod's/opt/jr2volume is mounted read-only into the User Container beside/repos.ext::names the program by absolute path, so the seat needs no PATH and jr2 injects no env; the program is a static binary, thework-aclrule (ADR-0037), because it executes on a libc jr2 does not control. A human in the User Container gets the samegit fetchas the Agent, with no credential of their own; the humanexec'd into the Harness container already had it.Names.
fetchis what an Agent, a human, and a cache do;refreshis what the interval does and stays onrefreshInterval; an ask is the mark, the word the agent and the operator already use. The url carries the Repo's identity, never the cache key — a key is a derived directory name, never chosen by a human (ADR-0004), and not the name a human should read ingit remote -v.What stays. An attach still fetches before it runs, because a creation is an ask. The interval still runs, so a cold attach and
jr2 statusstay honest without anyone asking. There is no CLI verb: the human who wants the fetch is in the pod, wheregit fetchis the verb, and the mark makes ajr2verb additive if a caller outside the pod ever needs one.
Considered options
- The run asks — a body primitive before a Turn, or on
request_changesre-entry, the way attach asks on entry. Rejected as the only path: it covers what the author foresaw and nothing an Agent discovers mid-Turn. Not foreclosed: the mark is the substrate, and a body primitive that sets it is additive. - A Menu pick (
need_fresh, declared withdefineEvent, afetchingstate the pick transitions to). Rejected: a Turn boundary per fetch, an author opt-in per state, and a transition for something that moves the Machine nowhere. A fetch is not part of a Machine's Vocabulary. - A named Menu tool (
mcp__jr2__fetchbeside the Menu's events). Rejected: the model must learn a tool and remember to call it before the verb it already knows, and a human at a shell gets nothing from it. - Shorten the interval. Rejected: polling per node per Repo, and the latency is still the interval.
- The ask on the Repo CR. Rejected: every node holding the cache fetches, and the wait must then name a node.
- The cache agent reads Sandbox CRs. Rejected: two sources of demand where one exists, and a new RBAC grant for the DaemonSet, to save one copy the operator makes for every other field.
- A
jr2::remote helper found on PATH. Rejected: PATH is env, the User Container gets none (ADR-0005), and an owed image line whereext::costs nothing but a longer line ingit remote -v. - The human's credentialed fetch pushed into the cache. Rejected: the cache is read-only in the pod by design, the structural half of the gc invariant (ADR-0004). And moot: with this decision the human's own credential is not needed for a fetch at all.
- Fail the fetch when the remote fails. Rejected: attach chose availability (ADR-0051), and the Agent's
git fetchwould become less reliable than today, where it always succeeds against the cache.
Consequences
- ADR-0051 is rewritten in place: "stale stays stale from inside the pod" is retired; a stale attach is stale until the next fetch anyone inside the pod runs. ADR-0005 is rewritten in place: "
git fetch originreads the node cache" is now "asks the node cache", the User Container mounts/opt/jr2read-only beside/repos, and "nothing injected" narrows to the seat's process — no env, no command, no probe. ADR-0004's refresh section gains the in-pod ask. - The runtime volume carries a second static binary, and so a jr2 program is present in every seat of every Workspace pod, User Container included. It is the natural home for in-pod jr2 functionality that does not yet exist.
- A fetch inside a pod is a remote round trip through the node agent, seconds rather than milliseconds. Every git command that fetches pays it; none that does not.
- The Sandbox status grows a standing per-key Repo entry; the Custodian carries one loopback route for it; the Orchestrator gains a Sandbox-token route. No new credential, no new watch in the cache agent, no git knowledge in the operator beyond the timestamp comparison it already makes.
- The rewind gotcha vanishes: a human who fetched around the cache with their own credential advanced a remote-tracking ref the Agent's next cache fetch forced back. Nobody fetches around the cache now.
- The Harness
bashtool's own timeout still bounds a fetch the way it bounds any slow command.