v0.21.2 — Worktree workspaces (#79); review follow-ups for v0.20.4–v0.21.1
Added
- A linked worktree works as the workspace or a
--dir/--dir-ro(#79,
from the #77 reproduction). Its.gitfile names a git directory under
the main repository, outside the worktree; git treats the missing target
as a hard error for every command, so the agent container died at the
entrypoint's firstgit config(exit 128) before the harness started.
The launcher now resolves the common directory through git and mounts it
alongside the worktree at its host path with the same writability —
validated by git's own linkage (a git directory whose worktree entry links
back to this checkout, or whosecore.worktreenames it), never by the
pointer alone: an unresolvable pointer, one that does not link back, a
symlinked.gitor a sensitive target refuses the launch with the fix
named (no--unsafe-diroverride for the last). The mounted common dir is
an ordinary root: git-trusted, kernel-granted, hooks masked (a bare-named
repo.gittoo), config pinned when opted in, judged by the nesting and
self-mount rules, baselined through the worktree without walking
objects/. Shared common dirs mount once, writable if any sharer is.
Tests: a daemon-freeworktreesuite (resolution, every refusal, list
wiring, the mask planner) and a live landlock section (status/log/commit
in-cage from a worktree, the main repository's hook masked).
Review follow-ups for v0.20.4 → v0.21.1 (external review of the batch; every
Critical and High below was confirmed against the code before fixing).
Security
tjor init/trust/policy/doctorstop on a core.worktree redirect
(v0.20.4 review, Critical).repo_root()ran the guarded resolver
inside command substitution, wheredie()exits only the subshell: the
redirect was printed and then ignored —init/trustwent on to say
"run inside a git repository",policyanddoctorfell through to the
user policy. Every$(repo_root)/$(policy_file)consumer now propagates
the refusal; the launcher test drives all four commands against a planted
redirect and asserts nothing was scaffolded.ext::/fd::transports onremote.*.url/pushurland
submodule.*.urlare findings (v0.21.1 review, Critical).
ext::<command>runs on every fetch, pull, push and clone; the value-based
rule mirrors the!-alias one, an https/ssh URL stays quiet. Added to the
documented set as well:sendemail.smtpServerCommand/sendmailCmd(and
per identity),sendemail.smtpServeras a program path,imap.tunnel,
protocol.allow/protocol.*.allow,trailer.*.command/cmd,
guitool.*.cmd,init.templateDir.- A truncated repository walk is never silent (v0.21.1 review,
Critical). The depth cap — now[landlock] git_check_depth, default
32, was a fixed 12 — and unreadable directories are recorded in every
snapshot: announced at launch, and a NEW spot after the baseline is a
finding. A repo planted below a spot already truncated at launch remains
the documented residual, named in the README. - An unparseable git config is a finding, never "no dangerous keys"
(v0.21.1 review, Critical).git config --listfailing records the file
as unreadable, reported at every check with git's error (fail closed, like
the hooks path). - A symlinked
.git,.git/hooksor.git/configrefuses the launch
(v0.21.0 review, High).mkdir -pand the bind mount both follow the
final component, so a link a previous session planted could have steered
the hooks mask (or the config pin) onto a chosen path under an
ordinary-looking launch line. The refusal names the link, its target, the
fix and the opt-out; the check also reports a hooks dir or.gitturned
symlink. --jsonsets the pending marker on findings (v0.21.1 review, High) —
the same rule as the human path; its output is escape-sanitized like the
terminal's, and credentials embedded in URL-shaped keys or values are
redacted in both.--ackis bound to the reviewed state (v0.21.1 review, High).
Findings print a token;tjor git-check --ack <token>accepts exactly
that state as the new baseline. A bare--ack, a wrong token or a stale
one (the state moved on) shows the findings and refuses.- Every value of a repeated key is compared (v0.21.1 review, Medium): a
value slipped between two legitimatesafe.directoryentries was
invisible to a last-value-wins dict. ssl_insecureis rejected synchronously (v0.20.4 review, Low): the
addon'sconfigureraisesOptionsErrorinstead of scheduling a
shutdown.
Changed
- Pending-marker writes are atomic (write, then rename) through one helper;
the repo-coverage, roots and re-baseline lookups moved from inline
heredocs intotjor_gitcheck.py(covers,roots,rebaseline);
discovery is one walk per snapshot instead of one per repository; the
hooks-maskfindprunes below.git.tjor git-check --jsonnow prints
an object (findings,token,incomplete) instead of a bare list. - One
gate_sensitive_pathfor the workspace and every--dir/--dir-ro
(three verbatim copies before);dir_is_sensitiveinitializes its own
roots (the ordering dependency is gone); onemask_coveredhelper for the
four mask blocks; the git masks live inplan_git_masks, driven directly
by the launcher test. GH_TOKENis unset whenever the broker does not cover the API host —
including a disabled broker — documented as intentional (README, ADR 0007)
and covered by the live broker test. New tests:doctor/init/trust/
policyredirect refusals; a worktree whose common dir lies outside every
root; every documented dangerous key has a unit case.