The Go gate serves synthetic receive-pack advertisements and validated single-branch pushes on
serve-git port 8004. It requires GIT_TRANSPORT_ENABLED, PUSH_GATE_ENABLED and a configured
GitHub App. Disabled requests refuse before authentication or upstream traffic. Port 8003 keeps
the read transport. /health and /ready remain listener probes.
Scratch PostgreSQL, fake Kubernetes Secrets, a fake GitHub App API and local bare repositories
prove branch creation, fast-forward, real smart-HTTP git push, replay, refusals and uncertainty
resolution. Workspace creation tests additionally prove creation-time attachment, replay,
readiness and release; a credential read from a fake workspace Secret publishes through the real
gate to a local bare repository. No live GitHub, Actor containment or production rollout is claimed.
Native workspace creation freezes the lowercase authorization reference from
pushgate.Reference before CreateSession. Only a current code-task attempt with the exact held
writer generation, repository and feature branch reserves one. Every other workspace with a
development environment (owner workspaces, default-branch and policy-denied tasks) freezes the same
reference with no reservation: kagent prepares a Session only if it holds exactly one read and one
push reference, so it needs the reference to exist, and nothing else. The reference carries no
bearer; no Secret or grant is published for it, and Service.Enroll is reachable only through a
reservation, so it grants no authority and the retrofit rule is unchanged. Any other workspace without an
environment freezes none. The durable workspace_push_reservations intent uses the
attempt as its idempotency key and the attempt's native creation request ID as its identity.
Retries and deletion replays carry the saved reference, even after release or configuration changes.
The native idempotent create checks the original request before returning or creating its Session.
The workspace enrollment reconciler freshly reads the native Session on every retry. It requires
READY, no operation and exactly one matching creation-time reference before calling
Service.Enroll, which freezes the active task/writer authority and runtime and publishes the
immutable Secret. No reference is added to an existing Session. A refused, abandoned, failed,
deleted or expired creation releases the reservation; release also revokes any push grant and
credential, with Secret deletion handled by the existing credential cleanup loop. The reservation
expires after the configured workspace create retry window if enrollment never completes.
A delete request revokes the reservation, grant and push credential in the transaction that records
the delete intent, and the gate refuses any workspace that has a delete op, in any state, so a
delayed or failing native delete cannot leave a window for a new push. A push already dispatched is
not recalled. A delete later rejected (for example because a turn is open) leaves the workspace
without push authority on purpose: it fails closed and the terminal reservation cannot reopen.
A workspace with no recorded development environment can never be attested, so enrollment ends at
once: it releases the reservation (and logs why) instead of retrying until the reservation expires.
Task creation refuses such a project first (project_environment_required).
The release sweep also settles a grant or push credential still planned or published behind an
already revoked reservation (a crash between Enroll and its fence), so the Secret is cleaned up.
Terminal reservations and their identities cannot reopen or change. No owner policy API is added,
and authentication never issues credentials.
A push bearer is a published git_push credential bound to a published grant and enrolled
repository. Read, MCP and another workspace's credentials supply no push authority. The stored
workspace, session, binding and project must agree on owner and repository; ended, archived,
superseded or deleted bindings refuse. Repository names compare without case; branches compare
with case.
Every grant freezes the branch, owner, native Session, writer generation, optional task attempt, policy version and a digest of the confirmed native runtime. Fresh GetSession must match the stored checkout, Agent and accepted development environment and show READY with no operation, a prepared revision, composition and current active generation/atespace/Actor name/UID. Runtime replacement refuses the old grant. Enrollment resumes a Session whose Actor kagent has parked, because the runtime association exists only while an Actor runs. A copied Session ID or client header supplies no authority.
Owner workspace writers require a held binding claim. Delegated writers additionally require the active current G8b task attempt, the same binding/session/workspace, the exact held branch claim and writer generation, the correct role and an open active parent when present. Authority is checked before processing and again after seed reads and remote-ref discovery, before committing dispatch. Short transactions serialize admission against writer release and credential/policy changes; no database transaction or row lock spans native, Secret or GitHub I/O.
Before reading a receive body, the gate commits a leased admission claim for its repository and
branch, then reserves min(body limit, disk budget) bytes from a process-wide scratch budget.
Separate pods share the branch claim; each pod has its own disk budget. Another live admission
for that branch or insufficient unreserved scratch returns HTTP 503 with the closed
admission_busy code and Retry-After: 1, before reading the body or creating any spool file.
Admission also blocks writer release/transfer. Claims and reservations remain held through
scratch cleanup and are released on every outcome. A crashed handler's claim expires after the
bounded request/dispatch/persistence window; stale handlers cannot reserve or dispatch a
publication, and their cleanup cannot release a successor's claim.
Before validation, the body reservation must expand to the full per-quarantine disk allowance.
That allowance covers the body, seed pack, repository files and temporary Git output in the same
process-wide budget. Validation is consequently serialized at the pod's disk budget; other
branches can spool concurrently before validation, but insufficient validation capacity returns
the same retryable admission_busy refusal. Capacity stays reserved through dispatch and cleanup.
The eight-branch regression checks real quarantine sizes against this reservation, refuses the
seven competing branches while validation is held, and proves they succeed after cleanup.
Policy requires a known default branch. That branch, every previously observed default and
protected patterns override the allowed patterns. Patterns match case sensitively, with *
covering nested branch names; malformed patterns refuse. An empty
allowlist permits other valid feature branches. SetPolicy is an internal compare-and-swap:
versions increase by one and former defaults cannot be removed. Existing grants remain subject
to the current policy. A remote default disagreement refuses publication until trusted metadata
and policy agree.
The project-metadata sweep (see projects) is what makes them agree. It
fills an empty projects.default_branch. A task workspace whose project default is still empty
when it is created does not freeze a "no push" decision: creation fails retryably ("waiting for
the project's default branch from GitHub") until the sweep fills the default, and the create's
retry window bounds the wait, after which the workspace operation is gave_up with that reason.
A workspace created with a known default is unaffected, and a reservation can only be made once
the default is known. On every successful refresh the sweep calls AdvanceDefaultBranch, which reads the
policy and moves the new default in through SetPolicy's compare-and-swap, keeping the owner's
patterns and retiring the old default into the previous defaults, so pushes to either are refused.
A project with no policy yet is left for enrollment, and a policy already naming the branch is not
changed, so a policy that lags the project row (a gate-off period, an older build) is repaired by
the next pass. A lost swap or a GitHub error leaves everything as it was and is retried by the next
pass. The policy advances before the project row, so a failure between the two is refused as a
disagreement until the next pass completes it.
Exactly one refs/heads/<granted-branch> update is allowed, creating a branch or fast-forwarding
its trusted current head. Deletion, tags, notes, multiple refs, rewind/divergence, protected or
default branches, missing objects and missing authority refuse. LFS routes and push certificates
are unsupported.
The Host is the configured push Service host (loopback is also permitted in development).
The Host, route and header policy is shared with the git read transport (server/internal/gitroute),
so both refuse the same routing headers: Forwarded, X-Forwarded-* and its underscore spelling,
X-Real-IP, Via, X-Envoy-*, X-Actor*, X-Session*, Upgrade, Proxy-Authorization and the
method/URL override headers, compared case-insensitively, before any authentication or upstream call.
Duplicate headers, control characters, gzip,
other HTTP encodings, contradictory framing and non-v0 Git protocols refuse. Neither client
headers nor paths are forwarded upstream. Responses return a server-generated 32-digit hex
correlation ID in x-mainloop-request-id. Refusal logs contain only that ID and closed code/stage
fields, plus standard log metadata. Unknown classifications become transport_unavailable or
unknown; request fields, packet content, credentials and exception text are omitted.
Git's exact complete four-byte 0000 receive-pack probe returns HTTP 200 with an empty
application/x-git-receive-pack-result, after route, framing, authentication, binding, policy,
runtime, writer-generation and unresolved-publication checks. It creates no publication, seeds no
objects, mints no App token and sends no upstream traffic. It holds normal admission while
checking at most five bytes in memory and creates no spool file. Empty, truncated, extra-byte and
shallow-only bodies remain invalid. The subsequent real POST passes independent validation.
Receive bodies spool privately on disk under /var/lib/mainloop/git, with mode 0700 and current
process ownership; tmpfs/ramfs and symlink roots refuse. Spool files are removed on every outcome.
Defaults are 256 MiB body, 256 KiB commands, 100,000 objects, 512 MiB reconstructed objects, 2 GiB
scratch disk and 64 MiB free reserve. Spooling, validation and dispatch each have a 60-second bound.
The configured body and command limits are used by the listener. The fixed configuration disk
budget is sized below the 3 GiB spool volume; configuration rejects a body limit above 2 GiB.
Each copy stops at min(body limit, disk budget) + 1 bytes, and checks remaining free space
before each body/seed allocation to preserve the 64 MiB reserve. Seed limits include existing
repository files. Before every Git subprocess, available space must cover its entire remaining
reserved disk allowance plus the 64 MiB headroom. A non-commit branch head
returns HTTP 403 with commit_required.
Strict pkt-lines require lowercase SHA-1 OIDs, one command and supported report-status
capabilities. Distinct nonzero shallow <oid> lines are accepted only before commands and are
forwarded unchanged. They never create trusted ancestry: validation seeds full history for the
target and default branches through the repository-scoped read port.
Before seeding, bounded zlib parsing checks PACK framing, object/expanded-size bounds, SHA-1
checksum and trailing bytes. Git then runs in a private bare quarantine with index-pack --strict
(and --fix-thin for incoming objects), fsck --strict, commit-type and ancestry checks. A measured
object inventory checks reconstructed count and size, including thin bases. prlimit caps each
Git subprocess at 1 GiB address space, 30 CPU seconds, 64 descriptors and the remaining file
budget. Hooks, protocols, inherited configuration, replacements and credential helpers are
disabled. Outputs and filesystem use are capped. Shallow grafts cannot excuse missing history.
GitHub read tokens are scoped to one repository with contents:read; metadata and publication
use independently scoped metadata:read and contents:write App tokens behind the shared GitHub
endpoint allowlist. Redirects are refused and replies are bounded. Tokens remain inside the
adapter. Raw receive replies/progress are never relayed or stored: only a locally synthesized
report and bounded receipt classification reach the client or ledger.
A validated command and immutable body/authority identity reserve a pending intent before
seed/CPU work while the earlier admission claim still holds the branch. The publication fence
retains unresolved durable outcomes after transient admission is released.
The workspace and server-generated request ID supply the operation's idempotency key; the ID is
also retained for correlation with refusal logs. A confirmed identical body and credential return
the recorded result without dispatching again. An explicit retry after rejection creates a new
intent and independently checks the current remote head before any publication. Pending, dispatching and unknown
publications block another push or probe, and a database trigger blocks writer release/transfer.
After an active handler has released admission, an unresolved publication continues to refuse
with publication_unresolved until the resolver establishes its outcome.
The audit ledger survives workspace/session deletion.
After measured validation and fresh authority/ref checks, the handler commits dispatching
before sending upstream bytes. An unpack report and exactly one matching ref report classify
confirmation or rejection. HTTP 200 alone proves neither. Cancellation after admission does not
cancel bounded dispatch/outcome persistence. Lost, malformed, reflected or incomplete replies,
timeouts and uncertain persistence retain unknown or an unresolved committed row. Immutable
identity cannot change and terminal states cannot reopen.
The serve-api reconciler claims expired pending/dispatching rows and unknown rows. It observes
the remote ref without a transaction and never resends the body. Abandoned pending intents are
rejected as not dispatched. For uncertain dispatches, the exact new head confirms remote
convergence; the unchanged old head rejects it. A third head, unavailable remote or incomplete
historical evidence retains the fence with backoff. Resolver evidence is distinguished from a
receive-pack receipt: remote equality proves convergence, not which request caused it.
External operator changes and runtime failure can occur after the last observation and cannot recall an already admitted push. Git's old-head compare-and-swap rejects a changed remote head. Native credential attachment, Actor egress containment, packaging, GitOps rollout and live use remain release gates.