mirror
mirror
¶
Host-side git gate (mirror) management and upstream comparison.
The git gate is a bare git repository stored on the host. Its role varies with how the caller configures it:
- Upstream set, gatekeeping mode — the gate is a mirror of upstream and is the only repository the container can access, enforcing human review before changes reach upstream.
- Upstream set, online mode — the gate mirrors upstream and serves as a read-only clone accelerator (faster than cloning over the network).
- No upstream —
sync()initialises the gate as a remoteless bare repo that the container can still push to. Nothing is fetched because there is no remote; subsequent syncs are no-ops.
GitGate is the main service class — wraps git CLI operations for
syncing, comparing, and querying the gate.
All constructor parameters are plain values (strings, paths) — no
terok-specific types like ProjectConfig.
Ref model¶
The gate keeps three ref namespaces, and the split is what makes sync safe:
refs/heads/*— the container-facing view. Sync on its own only ever creates branches or fast-forwards them; every other change (delete, non-fast-forward move) is returned as a pending op and applied only viaapply_pending_opsafter the operator confirmed it. Agent work pushed to the gate but not yet upstream therefore survives any number of syncs.refs/terok/upstream/*— a private snapshot of upstream's heads, force-updated and pruned freely on every fetch. It is the "what did upstream have last time" memory that classifies each branch: a gate head equal to its snapshot entry has no gate-local work (a destructive op on it is lossless), anything else diverged locally (lossy).refs/terok/backup/<branch>/<stamp>-<sha12>— the old tip of every destructively changed branch, written before the change so nothing ever becomes unreachable. Expired per the retention policy byprune_backups.refs/terok/attic/<branch>— the last upstream tip a branch had before it went pending (deleted upstream, or rewritten while the gate head stayed behind). The pruning, force-updating snapshot forgets that tip immediately, but it is exactly what proves a pending op lossless — so the attic keeps it until the branch is resolved (op applied, or gate and upstream agree again). First writer wins: across repeated upstream rewrites the attic still names the last tip the gate was actually in sync with.
The whole refs/terok namespace is hidden (transfer.hideRefs), so
containers can neither see nor overwrite the snapshot, attic, or backups —
a container that could push the snapshot could forge "lossless".
Value types returned by GitGate methods:
GateSyncResult— full sync outcome: applied ops, pending destructive ops, kept gate-only branches (upstream_urlisNonefor remoteless gates)AppliedOp/PendingOp— one branch-level ref change, performed or awaiting confirmationApplyPendingResult— outcome of applying confirmed pending opsBackupRef— one parsed backup refCommitInfo— single commit metadata (hash, date, author, message)GateStalenessInfo— frozen comparison of gate HEAD vs upstream HEAD
logger = logging.getLogger(__name__)
module-attribute
¶
OpKind = Literal['create', 'fast_forward', 'force_update', 'delete']
module-attribute
¶
Branch-level ref change kinds. create/fast_forward are the only
kinds sync applies on its own; force_update/delete only ever appear
as pending ops.
PendingReason = Literal['upstream_rewrite', 'upstream_delete', 'unknown_provenance']
module-attribute
¶
Why a destructive op is proposed. unknown_provenance marks branches
found during migration from a pre-snapshot mirror gate — upstream doesn't
have them now, but whether it ever did is unknowable, so they are
surfaced once and otherwise left alone.
GateAuthNotConfigured(scope)
¶
Bases: RuntimeError
Raised when a scope has no vault key and personal-SSH fallback is not opted in.
Callers (the gate-sync CLI dispatch) turn this into a two-door
remediation hint:
- generate a terok-managed key with
terok ssh-init <project>and register it upstream, or - opt in to the user's own
~/.sshkeys with--use-personal-ssh(orssh.use_personal: truein the project YAML).
Source code in src/terok_sandbox/gate/mirror.py
scope = scope
instance-attribute
¶
AppliedOp
¶
PendingOp
¶
Bases: TypedDict
A destructive branch change awaiting operator confirmation.
lossless is the heart of the safety story: True means the gate
tip equals what upstream last advertised for this branch — no agent
commits would be discarded. gate_only_commits quantifies the lossy
case (None when provenance is unknown and the count would be
meaningless). gate_sha doubles as the compare-and-swap guard in
apply_pending_ops:
an op is refused if the branch moved since it was proposed.
GateSyncResult
¶
Bases: TypedDict
Result of a gate sync operation.
upstream_url is None when the gate is initialised without a
remote — a local-only mirror that the container can push to but that
never fetches external commits.
pending ops are proposals, not failures — a sync that fetched
cleanly and applied its safe ops reports success: True regardless
of how much destructive work awaits confirmation. notes carries
non-fatal observations (moved tags, expired backups, foreign refs).
The clone-cache refresh is best-effort: cache_error carries the
failure description when the refresh was attempted and failed, so
callers can report it instead of silently claiming a clean sync.
cache_refreshed stays False both on failure and when no
cache is configured; cache_error distinguishes the two.
path
instance-attribute
¶
upstream_url
instance-attribute
¶
created
instance-attribute
¶
migrated
instance-attribute
¶
success
instance-attribute
¶
errors
instance-attribute
¶
notes
instance-attribute
¶
applied
instance-attribute
¶
pending
instance-attribute
¶
gate_only_branches
instance-attribute
¶
cache_refreshed
instance-attribute
¶
cache_error
instance-attribute
¶
ApplyPendingResult
¶
Bases: TypedDict
Result of applying operator-confirmed pending ops.
Ops whose branch moved between proposal and apply land in errors
and leave the branch untouched — the rest are still applied, so a
single race never voids a whole confirmation. backups maps each
changed branch to the backup ref holding its previous tip (empty when
backups are disabled).
BackupRef
¶
CommitInfo
¶
GateStalenessInfo(branch, gate_head, upstream_head, is_stale, commits_behind, commits_ahead, last_checked, error)
dataclass
¶
GitGate(*, scope, gate_path, upstream_url=None, default_branch=None, use_personal_ssh=False, validate_gate_fn=None, clone_cache_base=None, backups_enabled=True, backup_retention_days=30)
¶
Repository + Gateway for a host-side git gate mirror.
Manages the bare git mirror that containers clone from. Provides operations for initial creation, incremental sync from upstream, selective branch fetching, and staleness detection.
Constructor takes plain parameters — no terok-specific types.
Initialise with plain parameters.
Parameters¶
scope:
Credential scope for this gate's owner. Used to locate the
per-scope vault SSH-agent socket.
gate_path:
Path to the bare git mirror on the host.
upstream_url:
Git upstream URL to sync from.
default_branch:
Branch name used for staleness comparisons.
use_personal_ssh:
When True, skip the vault socket entirely and let git fall
through to the user's ~/.ssh keys / loaded agent. Default
False — "terok never touches your real keys" is the advertised
property. Opt in per-invocation (--use-personal-ssh) or
per-project (ssh.use_personal: true in project YAML).
validate_gate_fn:
Optional callback (scope) -> None that validates no other
scope uses the same gate with a different upstream. Injected by
the orchestration layer; omitted for standalone use.
clone_cache_base:
Base directory for non-bare clone caches. When set,
sync refreshes a working-tree cache at
clone_cache_base / scope after updating the bare mirror.
The cache accelerates task startup by enabling a host-side
file copy instead of a full git clone.
backups_enabled:
When True (default), every destructive branch change applied
via apply_pending_ops
first saves the old tip under refs/terok/backup/. Opt out
per project when the reflog alone is protection enough.
backup_retention_days:
Backups older than this are expired by
prune_backups
(which a successful sync runs automatically). 0 keeps
backups forever.
Source code in src/terok_sandbox/gate/mirror.py
cache_path
property
¶
Clone cache directory for this scope, or None if caching is disabled.
close()
¶
Stop the ephemeral signer this gate started, if any.
Idempotent. Long-lived processes (the TUI) should call this explicitly so the signer thread and temp socket don't outlive the gate's last use.
Source code in src/terok_sandbox/gate/mirror.py
__del__()
¶
sync(branches=None, force_reinit=False)
¶
Sync the host-side git gate from upstream, destroying nothing.
With an upstream configured, clones (or fetches) from it using the project's SSH setup. Without one, initialises a bare repo in place and returns a no-op sync — the gate then serves as a local-only remote that the container can push to, giving the agent somewhere to stage commits even when there is nothing external to mirror.
A remoteless gate that already exists is a proper no-op: nothing re-initialises, and the returned op lists are empty.
Only safe branch changes are applied here — creates and
fast-forwards. Deletes and non-fast-forward moves come back as
pending proposals for
apply_pending_ops;
branches that exist only on the gate (agent work not yet upstream)
are listed in gate_only_branches and never touched.
branches restricts the sync to the named branches (the auto-sync allowlist); the default full sync covers everything upstream has.
force_reinit recreates the whole local footprint — the bare
mirror and the clone cache — so a hopeless state can always be
recovered with one flag. Deletion failures propagate: rebuilding
over stale or partial data would silently defeat the point of a
from-scratch recovery. This is the one path that still discards
gate-local work, which is exactly why it hides behind an explicit
operator flag and never runs implicitly.
Source code in src/terok_sandbox/gate/mirror.py
436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 | |
pending_ops()
¶
Recompute the pending destructive ops without touching the network.
Compares the current gate heads against the snapshot and attic —
state the last sync left behind — so TUI badges and confirmation
dialogs can refresh cheaply (no fetch, no SSH signer). The one
thing this cannot see is the one-shot unknown_provenance batch
a migration sync reports.
Source code in src/terok_sandbox/gate/mirror.py
apply_pending_ops(ops)
¶
Apply operator-confirmed destructive ops, backing up every old tip.
Each op is guarded by compare-and-swap on the gate_sha it was
proposed against: a branch that moved since (an agent pushed) fails
that op only and stays untouched — confirmations never apply to
state the operator didn't see. Unless backups are disabled, the
old tip is first saved under refs/terok/backup/ so even an
approved mistake is one update-ref away from recovery.
Finishes with the same HEAD-alignment and clone-cache refresh a sync performs — deleting the branch HEAD points at is precisely when healing matters.
Source code in src/terok_sandbox/gate/mirror.py
list_backups()
¶
Return all backup refs, newest first, parsed from their names.
Source code in src/terok_sandbox/gate/mirror.py
prune_backups(older_than_days=None)
¶
Delete backup refs older than the retention window; return them.
Age is the ref-name timestamp — when the backup was taken, the
only clock that matters for "have I had a chance to notice". With
retention 0 (or a nonexistent gate) nothing is ever expired.
Source code in src/terok_sandbox/gate/mirror.py
compare_vs_upstream(branch=None)
¶
Compare gate HEAD vs upstream HEAD for a branch.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
branch
|
str | None
|
Branch to compare (default: configured default_branch) |
None
|
Returns:
| Type | Description |
|---|---|
GateStalenessInfo
|
GateStalenessInfo with comparison results |
Source code in src/terok_sandbox/gate/mirror.py
1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 | |
last_commit()
¶
Get information about the last commit on the configured branch.
Returns None if the gate doesn't exist or is not accessible.
Source code in src/terok_sandbox/gate/mirror.py
is_ssh_url(url)
¶
Return True for SSH-scheme git URLs.
Accepts the two forms git itself accepts:
ssh://[user@]host[:port]/path— explicit URL scheme.[user@]host:path— scp-style shorthand. The user part is optional (git@github.com:foo.git,deploy@host:repo.git, baregithub.com:foo.git).
Shared with terok-main: both the gate's env builder and callers that branch on "does this project use SSH?" (e.g. deploy-key prompts, gate-sync fallback hints) must agree on one definition.