Documentation
¶
Overview ¶
Package verify boots a proposed manifest and repairs it when it fails.
This is what makes generated configuration trustworthy. Detection is imperfect and always will be: rules reach a useful majority of repositories and a model reaches further, but neither can be relied on to be right the first time. The developer does zero work not because detection is perfect, but because recovery from imperfect detection is automated.
The loop is: propose, validate, boot, probe. A failure at any stage is fed back with the evidence — which service, which stage, and what it actually printed — and a new proposal is attempted. Bounded, because a loop that tries forever is a loop that burns an afternoon producing nothing; after the last attempt the partial manifest and the final failure are handed to the human, who is usually four lines from a working file.
The airlock ¶
A Patcher is untrusted. It reads repository content — READMEs, CI configs, dependency manifests, and now the error output of code from that repository — which is precisely the material an attacker can influence, and it produces something devbay is about to execute.
So every proposal crosses one checkpoint, in this package, before anything runs it:
- It must parse and pass the full validator. A shell string where an argv array belongs fails structurally, before any rule runs.
- `egress:` is stripped from every proposal, always. If content from a repository could widen the network policy, an injected instruction could widen the network policy, and the sandbox would be arguing with itself.
- A proposal that fails validation is not retried blindly: the validation error is itself fed back, because a patcher that produced invalid YAML can usually fix it when told what was wrong.
Index ¶
Constants ¶
const AwaitingApproval = "a command in this manifest needs a human's approval before it can run"
AwaitingApproval marks a result that stopped for a human decision. The manifest may be perfectly good; it simply has not been agreed to.
const NoPatcher = "no patcher is configured, so the failure was not repaired"
NoPatcher marks a result that stopped because nothing could repair it. It is recorded on the result rather than returned as an error: the loop ran, and the answer -- with the failure and the container's logs -- is the useful part. Returning an error here made callers discard exactly that.
Variables ¶
This section is empty.
Functions ¶
func StripEgress ¶
StripEgress is the exported form, for callers admitting a manifest by hand.
Types ¶
type Attempt ¶
type Attempt struct {
N int `json:"n"`
Manifest []byte `json:"-"`
Failure *Failure `json:"failure,omitempty"`
Took time.Duration `json:"took"`
}
Attempt records one pass through the loop.
type Booter ¶
Booter brings a manifest up and reports whether it works.
Returning a *Failure rather than a bare error is the point: a patcher given "boot failed" can do nothing useful, and a patcher given the service, the stage and the container's own output usually can.
type BooterFunc ¶
BooterFunc adapts a function to Booter.
type Failure ¶
type Failure struct {
Stage Stage `json:"stage"`
// Service is the service that failed, when the failure belongs to one.
Service string `json:"service,omitempty"`
// Message is the error as devbay saw it.
Message string `json:"message"`
// Logs is the container's own output, which is usually where the real
// cause is: a missing environment variable, a refused connection, a
// migration that has not run.
Logs string `json:"logs,omitempty"`
}
Failure describes why an attempt did not work, in the terms a patcher needs.
type Loop ¶
type Loop struct {
// Boot brings a candidate up. Required.
Boot Booter
// Patch proposes a revision. When nil the loop validates and boots once,
// which is the deterministic-only path.
Patch Patcher
// MaxAttempts bounds the work. Zero means three.
MaxAttempts int
// Log receives progress.
Log func(format string, args ...any)
}
Loop repairs a proposed manifest until it boots.
type Patcher ¶
type Patcher interface {
// Patch returns revised YAML, given the current file and why it failed.
Patch(ctx context.Context, current []byte, f Failure) ([]byte, error)
}
Patcher proposes a revised manifest.
Implementations are untrusted by construction, including the ones that call a model. Nothing here trusts the result: see the package comment.
type PatcherFunc ¶
PatcherFunc adapts a function to Patcher.
type Result ¶
type Result struct {
// Manifest is the last proposal that was tried. On success it is the one
// that worked; on failure it is the best available starting point, which
// is far more useful to a human than nothing.
Manifest []byte `json:"-"`
// Parsed is the validated form of Manifest, when it validated.
Parsed *manifest.Manifest `json:"-"`
Attempts []Attempt `json:"attempts"`
OK bool `json:"ok"`
// Note explains why the loop stopped when that is not obvious from the
// last failure, e.g. that no patcher was available.
Note string `json:"note,omitempty"`
}
Result is the outcome of the loop.
func (*Result) LastFailure ¶
LastFailure returns the failure that ended the loop, if any.
type Stage ¶
type Stage string
Stage names where an attempt failed.
const ( StageParse Stage = "parse" StageValidate Stage = "validate" StageBoot Stage = "boot" StagePatch Stage = "patch" // StageApproval means the candidate is well-formed but runs a command no // human has agreed to. Distinct from the others because it is the one // failure a patcher must not try to fix: rewriting the command to get past // the gate is precisely what the gate is there to stop. StageApproval Stage = "approval" )