Project Candidate Git Publication v1
September 10, 2026 · View on GitHub
Audience: host integrators and compiler contributors. Status: implemented bounded profile; HOSTED GREEN under the v0.4.0 release baseline. Historical local, authoring-time, ignored, device/simulator, or separately provisioned evidence below retains its narrower scope; public promotion, registry publication and broader product completion remain separately gated.
This route writes actual canonical .spx blobs, trees and a commit into one
explicitly selected local Git repository, then publishes through one expected-old
branch-ref update. It does not update a checkout, index, raw Project source path,
managed Workspace ACTIVE, remote, or network service. Git-tree readers can
observe one complete commit; editors observing the original working tree see no
change. This is separate authority from candidate preparation, recovery capsules,
reports, test results, and the managed publication bridge.
Explicit host interface
CandidateGitProcessAuthority::open(executable, repository, max_commands, timeout_ms) admits a host-controlled bare SHA1 or SHA256 repository on Linux or
macOS. Both paths
must be absolute; the repository path must already be canonical. The explicitly
trusted Git executable may be a symlink, resolved to a held regular executable.
No executable, repository, branch, author, timestamp or message comes from the
candidate or recovery capsule. Windows and other process hosts fail closed.
Ordinary SHA1 repositories are supported for Git compatibility; they do not gain
a collision-resistance or collision-detection claim.
CandidateGitTarget::new(repository_identity, reference, expected_base_commit, project_prefix) binds the exact adapter identity, a bounded refs/heads/ name,
a 40-lowercase-hex SHA1 or 64-lowercase-hex SHA256 object ID, and an optional relative Project prefix.
The prefix cannot contain dot, parent, .git, empty, control or backslash
components. CandidateGitCommitMetadata::new(name, email, unix_seconds, message)
requires explicit author/committer identity and UTC time, and a bounded message
ending in one LF. No ambient Git identity, clock, signer or editor participates.
apply_candidate_git_publication(candidate, approved_candidate_digest, project_manifest, target, metadata, authority) is the publication API. Its
CandidateGitAuthority interface permits injected trusted hosts; the supplied
process adapter performs real Git object writes and ref mutation. Implementing
the interface is a host authority decision, not a claim that arbitrary providers
are trustworthy. The manifest is the exact absolute authenticated Project path.
A complete candidate and independently supplied exact approval digest are
required. Drafts and rejected attempts cannot publish.
The CLI is an explicit host operation:
semaprax project-candidate-git-publish <manifest> <capsule.json> <approved-candidate-digest> <host-policy.json>
The host policy file selects authority independently from the replayed recovery capsule. This does not grant source publication to any Image Agent Protocol profile. Invoking the CLI grants its selected local repository/ref operation; there is no automatic push or original-checkout update.
Replay and Git transaction
- Load all declared Project inputs with held identities, require the original candidate base and unchanged manifest/source inventory, and independently replay the complete ordered candidate history through ordinary admission.
- Authenticate the selected current branch against the exact host-selected base commit. Read and independently hash the commit, traversed trees, canonical manifest, and every original declared source blob, including unchanged sources. Git blob contents must equal the authenticated canonical original Project contents. Source paths must be ordinary blob entries, never symlinks or gitlinks. A candidate with no changed source is rejected.
- Preserve every unrelated tree entry, object identity, mode and name. Replace changed canonical source blobs only, preserving their regular-file modes. Build binary repository-format trees and one explicit commit with the selected original commit as its sole parent. Unrelated histories and signatures are not copied into the new commit. Git tree object names occupy 20 binary bytes for SHA1 and 32 for SHA256. Git object-format transition.
- Bound and render the complete receipt before writing objects. Recheck held
Project inputs, write content-addressed immutable objects, recheck inputs and
repository, then call
update-ref --no-derefonce with the exact old object ID. Git owns the atomic expected-old ref update; this implementation does not emulate it with loose-ref file writes. Git update-ref. - Recheck Project inputs and observe the ref after a successful pivot. Any
uncertain update or disagreement after publication returns
SPX-G267, with the prepared commit and ref. The caller must inspect the ref before retrying.
The receipt schema is semaprax.project-candidate-git-publication.v1; it binds
repository/ref, previous/published commit, result tree, approved candidate digest,
original/result Project revisions and changed paths. It explicitly says raw
working-tree files and managed ACTIVE were not changed and tests were not run.
Git commit content identity is not a signature or an approval service.
Process and filesystem boundary
The process adapter holds an exclusive permanent
.semaprax-git-publication.lock file in the repository. Dropping the adapter
explicitly unlocks its lease, including admission-error and unwind paths; the
lock file is never deleted. Close-on-exec remains set, but closing the owning
descriptor alone must not let a concurrent fork's pre-exec duplicate extend the
lease beyond the adapter's lifetime. This coordinates cooperating
publication hosts. Ordinary concurrent Git writers remain subject to Git's ref
CAS. The repository and executable are host-controlled inputs: neither this
lease nor repeated pathname checks protects against a malicious same-UID process
racing namespace/content mutations between checks. Such adversarial shared
storage is outside this adapter's authority model.
Admission accepts only a minimal bare config: core.bare=true, version 0
without extensions for ordinary SHA1, or version 1 with absent/explicit sha1
object format or explicit extensions.objectformat=sha256. Optional boolean
core.filemode and core.logallrefupdates remain accepted. Version 0 with an
object-format extension is rejected rather than guessing whether Git ignores it. Unknown/duplicate settings, includes, alternate object
stores, common-directory indirection, attached worktrees, shallow history and
grafts are rejected. Before each process call, held repository/config/executable
identities are checked, config bytes are compared, and a bounded recursive scan
rejects nested symlinks, special files and multiply linked regular files anywhere
in the repository, including refs, reflogs, packs and loose objects. Git executable
device, inode, size, mode and modification timestamp are held and rechecked. These checks do
not confer trust on an untrusted binary selected by a host.
Linux launches every process from the executable image and repository directory
held by the authority, using handle-relative working-directory selection and
descriptor execution rather than reopening either authenticated pathname.
macOS derives the launch path from the held executable, starts the child
suspended with a handle-relative working directory, attests its executable vnode
and working directory against the held objects, and resumes it only after
agreement. A namespace substitution that prevents exact Darwin vnode agreement
fails closed before resume. The child receives only its exact
standard-pipe inventory; every other descriptor is closed. It has an empty
inherited environment and explicit null global/system config, no replacement
objects, no lazy fetching, no terminal prompts and no optional locks. A macOS
safety entry pre-seeds CoreFoundation's user-text-encoding key with the process
UID and fixed encoding fields, preventing CoreFoundation's fallback from reading
the user-home encoding file and rewriting the child environment. A fixed
command-line protocol.allow=never forbids transport;
there are no network commands or request-selected command names. Hashing uses
--no-filters. Fixed core.hooksPath=/dev/null disables hooks, including
reference-transaction, which otherwise executes during ref operations.
Git hooks. Shells, aliases, signing, filters,
credential helpers and inherited SSH/secret variables are not used.
The syscall implementation is the root registry crate's only locally allowed
unsafe module. The root manifest denies unsafe code everywhere else, and
root_unsafe_quarantine source-locks that single exception and its private API.
This packaging-preserving quarantine is narrower than exposing a generic safe
process facade but is not a claim that arbitrary unsafe additions are accepted.
Only symbolic-ref, show-ref, raw cat-file, raw hash-object and
update-ref are invoked. Synchronous bounded pipe handling, a fresh process
group, one host-selected total adapter deadline (1–60,000 ms), and one command
budget (1–4,096) cover input, output, and ordinary child execution. Success
or ordinary failure is returned only after the leader is reaped and the owned
process group is quiescent. Timeout, output overflow, pipe failure, or any other
post-spawn failure selects a sticky error, kills the owned group, drains bounded
output while the command is active, closes the pipes, and settles it. Settlement
has a separate fixed 30-second fail-stop allowance after the operation deadline;
inability to prove settlement aborts rather than returning a recoverable error.
The operation deadline begins at adapter opening and includes time
spent by the caller before publication. No claim is made that a trusted executable
can be physically preempted inside an uninterruptible kernel operation.
Core limits include 64 MiB/object, 8 MiB/tree, 256 MiB total read bodies, 256 MiB staged bodies, 4,096 objects, 65,536 visited tree entries, 64 path components and 60 seconds of checked orchestration work. Adapter limits also include 64 KiB config/stderr, 512 MiB conservatively reserved total process I/O, and 65,536 filesystem entries/depth 64 per scan. Limits apply to this bounded route, not all Git repositories or total HIR memory.
Failure semantics and authored evidence
SPX-G263 rejects host/grammar/object-shape constraints; SPX-G264 reports core
capacity; SPX-G265 rejects stale source/ref/blob identity; SPX-G266 reports
pre-pivot host failure; SPX-G267 reports an uncertain or already-observed pivot.
Existing Project/candidate diagnostics remain authoritative for admission.
A rejected pre-pivot operation leaves the ref unchanged but may leave unreachable
objects and the permanent host lock file. Once an update was attempted, process
failure is conservatively uncertain even when Git returned nonzero. There is no
rollback that might overwrite a concurrent writer's ref.
tests/project_candidate/git_publication.rs exercises real bare-SHA256 Git
publication, unrelated entry/mode preservation, unchanged raw sources, disabled
ref hooks, stale-ref/original-blob rejection, unsafe-config rejection and nested
object-store symlink rejection. It also checks live-host contention, explicit
lease release, rejected-admission recovery, unchanged source/ref state and lock-file
retention and directory substitution. Private held-runner tests cover executable
substitution, expired authority deadlines, descriptor/environment isolation,
bounded stdout/stderr, and child-group settlement.
SEMAPRAX_TEST_GIT can select the trusted fixture binary; otherwise the
Unix fixture selects /usr/bin/git. On this current macOS head, the eight active
held-runner cases, seven real-Git cases, root quarantine contract, and four
integrated SHA1/SHA256 workflow cases pass locally; strict root Clippy also
passes. Linux descriptor-execution cases remain included in the v0.4.0 hosted-green regression corpus. The later exact-subject
graph-workflow bundle is local
Darwin evidence only. Neither result is current-head hosted or Windows evidence,
and the complete quality profile was not rerun for this tranche.
Additive legacy SHA1 compatibility
The old public constructors and CandidateGitRepository fields remain source
compatible. GitObjectFormat::{Sha1,Sha256} names the admitted algorithms;
repository.object_format() maps its existing sha256 flag, with false now
explicitly meaning SHA1. The process adapter also exposes object_format().
Target OID width is checked against the held host format before object reads.
Tree parsing, hashing, staged writes and CAS all use that same format. Existing
SHA256 success receipts retain their previous field set and exact serialization.
A small private streaming SHA1 implementation hashes Git's exact object framing
for compatibility; no dependency download or SHA1 collision-detection library is
introduced. It is never used for semantic revision identity or approval digests.
Original Project blobs still require exact byte equality against independently
admitted canonical sources. Every staged SHA1 object is reread before the ref
pivot and compared byte-for-byte with its prepared body, rejecting an existing
object that has the same SHA1 name but different bytes (SPX-G276). The usual
read/output/object/fuel bounds apply to these additional reads.
SHA1 receipts additionally contain sha256_object_content_binding: a SHA256
binding over the exact sequence of authenticated read objects followed by staged
objects. The first domain is semaprax.git-publication.read-objects.v1 plus NUL;
the second segment starts with semaprax.git-publication.staged-objects.v1 plus
NUL. Each object contributes its OID text length as big-endian u64, OID text,
Git's kind length header plus NUL, then exact body bytes. Traversal/staging order
is deterministic. Post-write rereads are excluded from the receipt binding. This
binds observed and prepared content, not a signature, a proof that the whole Git
history is collision-free, or authentication of unrelated untraversed objects.
The existing candidate/Project SHA256 identities remain separate bindings.
sha1_security explicitly reports
legacy_git_compatibility_no_collision_detection_or_collision_resistance_claim.
The adapter does not implement SHA1DC, promise rejection of every known collision
family, or promote SHA1 to a modern integrity primitive. A Git executable's own
collision defenses remain its responsibility. Host-controlled storage and the
previous cooperative-race limits still apply. SHA256 is the format for hosts that
require Git object naming with the stronger hash.
Regressions cover actual SHA1 bare publication and byte comparison,
format/width mismatch rejection, strict version/extension config admission,
standard SHA1 known answers (empty, abc, the 56-byte vector and one million
a bytes), split-update padding checks, and known Git empty blob/tree OIDs. No
compiler, tests, or Git adapter execution was run for this extension.