writeback

package
v0.0.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 17, 2026 License: MIT Imports: 17 Imported by: 0

Documentation

Overview

Package writeback lands the work product of an isolated executor in the project's repository on the hub.

It is the receiving half of pkg/executor/gitwriteback, and the trust boundary between them is total. Everything it is handed — a branch name, a commit SHA, a bundle — was produced inside a sandbox running model-authored code. This package's job is to get that work into pkg/mergequeue, which is where local parallel work already goes, without letting a sandbox use the trip to write somewhere it was never given.

Quarantine before branch

Objects are fetched into refs/cloop/writeback/<branch>, never straight into refs/heads/<branch>, and the ref is only promoted after the commit range has passed inspection. The ordering is the point: a ref under refs/heads is a branch — it shows up in `git branch`, it can be checked out by a person or by the merge queue, and a checkout is what makes a .git/hooks entry execute. A ref that has not been vetted must never be reachable by any of that, so it spends its whole life somewhere `git checkout` will not find it, and a rejection deletes it.

Two layers of content checks, and why neither may be assumed

Every fetch here runs with fetch.fsckObjects and core.protectNTFS/protectHFS set, so git refuses malformed objects and — the part that matters — trees containing a path named .git in any of the spellings a case-insensitive filesystem would fold back to it.

Whether that layer fires on the bundle path depends on the git in $PATH, so it is not something this package may rely on. Git runs its fsck rules from index-pack, and which of index-pack or unpack-objects handles a bundle fetch changed under us. Measured, with a bundle carrying a tree entry named .git and both fsck settings on:

git 2.43, 2.45  fetch succeeds  — objects unpack loose, fsck never runs
git 2.47, 2.54  fetch fails     — hasDotgit, "fsck error in packed object"

The push path has always run through index-pack and so has always had it.

This is why executor.InspectWriteBack is not a second opinion: on any git that unpacks loose it is the only opinion, on the transport that needs one most — the one requiring no credential. It is also the layer that knows things fsck does not care about: that a symlink pointing outside the project root turns every later write through it into a write anywhere on the control plane, and that a submodule entry names a URL rather than content.

Because either layer may be the one that speaks, both classify a content refusal the same way — as ErrWriteBackRejected, never as the retryable ErrWriteBackUnavailable. A hostile write-back must not be reported as an outage on one git and a rejection on another. tests/security/writeback_bundle_test.go asserts the refusals over both transports for exactly this reason — the two paths do not have the same defences, so neither may be assumed to cover the other.

Both transports are inspected

A pushed branch goes through exactly the same vetting as a bundle. A sandbox holding a push credential can push a hook as easily as it can bundle one, and securing only the path that needs no credential would be securing the wrong one.

Index

Constants

View Source
const DefaultTimeout = 2 * time.Minute

DefaultTimeout bounds each git invocation when a Request names none.

View Source
const QuarantineRefPrefix = "refs/cloop/writeback/"

QuarantineRefPrefix is where an unvetted write-back lives between arriving and being accepted. Deliberately outside refs/heads: see the package comment.

Variables

This section is empty.

Functions

func Record

func Record(db Recorder, taskID int, taskTitle string, reported executor.WriteBackResult,
	applied Result, applyErr error) error

Record writes a write-back's outcome to the task record and the event journal.

It records failures as loudly as successes. A run whose work was lost looks exactly like a run that changed nothing — same transcript, same exit code, same green tick — so the case that most needs a row is the one where there is no branch to name.

Errors are returned for a caller that wants to log them and safe to ignore for one that does not: nothing here is load-bearing for the work itself, which is already on a branch by the time this runs.

Types

type Merger

type Merger interface {
	Submit(req mergequeue.Request) *mergequeue.Result
}

Merger is the subset of pkg/mergequeue.Queue this package needs.

It is an interface so a test can prove the handoff happens without starting a queue goroutine, and so the caller keeps ownership of the real queue's lifecycle — a package that lands one branch has no business deciding when the project's merge queue starts and stops.

type Recorder

type Recorder interface {
	LoadTask(id int) (*pm.Task, error)
	UpsertTask(t *pm.Task) error
	RecordEvent(row statedb.EventRow) error
}

Recorder is the subset of *statedb.DB this package needs.

An interface so a caller can pass a real database, a fake, or nothing at all. It is deliberately the two narrowest methods rather than the DB: a package that writes a task's branch name has no business being able to delete a plan.

type Request

type Request struct {
	// RepoDir is the project's git repository on the hub.
	RepoDir string
	// Reported is what the executor said the sandbox produced.
	Reported executor.WriteBackResult
	// Bundle is the git bundle received for mode bundle. Ignored for push.
	Bundle []byte
	// Remote is the name of the git remote the branch was pushed to, for mode
	// push. Empty means "origin".
	Remote string
	// TaskID and TaskTitle attribute the merge commit. They are the merge
	// queue's, not git's — nothing here trusts them.
	TaskID    int
	TaskTitle string
	// Queue receives the vetted branch. Nil means "land the branch and stop",
	// which is what a caller wants when the operator merges by hand.
	Queue Merger
	// MergeTimeout bounds the wait for the queue's verdict. 0 means ten
	// minutes: a merge may sit behind other tasks' merges.
	MergeTimeout time.Duration
	// Timeout bounds each git invocation. 0 means DefaultTimeout.
	Timeout time.Duration
	// Emit receives progress. May be nil.
	Emit func(string)
}

Request is one write-back to land.

type Result

type Result struct {
	// Branch and CommitSHA are the vetted ref, once it has been promoted.
	Branch    string
	CommitSHA string
	// Commits and FilesChanged describe the range that was accepted.
	Commits      int
	FilesChanged int
	// Merged and MergeSHA report the merge queue's outcome. Merged is false
	// when no queue was supplied or when the merge conflicted; MergeErr
	// carries the reason in the latter case.
	Merged   bool
	MergeSHA string
	MergeErr error
	// Skipped reports that there was nothing to land.
	Skipped bool
}

Result reports what landed.

func Apply

func Apply(ctx context.Context, req Request) (Result, error)

Apply lands a reported write-back in RepoDir and hands the branch to the merge queue.

A rejection is a *executor.WriteBackRejection wrapping executor.ErrWriteBackRejected, and it leaves nothing behind: the quarantine ref is deleted, no branch is created, and the merge queue is never told about it. A merge conflict is *not* an error — the branch landed, the merge did not, and Result.MergeErr says so — because losing the branch would throw away the work the conflict is about.

func (Result) Delivered

func (r Result) Delivered() bool

Delivered reports whether Apply produced a branch worth pointing at.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL