155 lines
5.1 KiB
Go
155 lines
5.1 KiB
Go
|
|
// Package publish implements the revision publication pipeline and the trust
|
||
|
|
// transitions it enforces.
|
||
|
|
//
|
||
|
|
// ArchitectureBlueprint.md section 47 assigns different trust levels to
|
||
|
|
// different things: AI interpretation is advisory, generated code is an
|
||
|
|
// untrusted candidate, deterministic tests are verification evidence, and only
|
||
|
|
// a signed revision is a deployable artifact. Section 52 asks that this
|
||
|
|
// distinction stay visible in code and data models rather than living in
|
||
|
|
// reviewers' heads.
|
||
|
|
//
|
||
|
|
// The types here make it visible. A candidate cannot be published, because the
|
||
|
|
// publish path takes a Verified value, and the only way to obtain one is to
|
||
|
|
// pass verification. That is a weaker guarantee than a proof, but it means the
|
||
|
|
// unsafe path has to be written deliberately rather than reached by accident.
|
||
|
|
package publish
|
||
|
|
|
||
|
|
import (
|
||
|
|
"errors"
|
||
|
|
"fmt"
|
||
|
|
|
||
|
|
"github.com/tegwick/fluid-core/internal/contract"
|
||
|
|
"github.com/tegwick/fluid-core/internal/signing"
|
||
|
|
)
|
||
|
|
|
||
|
|
// Candidate is an unverified revision descriptor.
|
||
|
|
//
|
||
|
|
// Whatever produced it — a human, a Builder, an LLM — it carries no authority.
|
||
|
|
// The field is unexported so that a Candidate can only be made through
|
||
|
|
// NewCandidate, and cannot be forged into a Verified value by struct literal
|
||
|
|
// from another package.
|
||
|
|
type Candidate struct {
|
||
|
|
descriptor contract.Revision
|
||
|
|
origin contract.Actor
|
||
|
|
}
|
||
|
|
|
||
|
|
// NewCandidate wraps a descriptor as an untrusted candidate.
|
||
|
|
func NewCandidate(d contract.Revision, origin contract.Actor) Candidate {
|
||
|
|
return Candidate{descriptor: d, origin: origin}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Descriptor returns a copy of the candidate's descriptor for inspection.
|
||
|
|
//
|
||
|
|
// It is a copy on purpose: verification decides about a specific byte sequence,
|
||
|
|
// and handing out a mutable reference would let a caller change the artifact
|
||
|
|
// after it was judged.
|
||
|
|
func (c Candidate) Descriptor() contract.Revision { return c.descriptor }
|
||
|
|
|
||
|
|
// Origin reports who or what produced the candidate.
|
||
|
|
func (c Candidate) Origin() contract.Actor { return c.origin }
|
||
|
|
|
||
|
|
// ID reports the candidate's revision id.
|
||
|
|
func (c Candidate) ID() contract.RevisionID { return c.descriptor.ID }
|
||
|
|
|
||
|
|
// Verified is a candidate that has passed every deterministic gate and been
|
||
|
|
// signed. Only a Verified value may be published and routed.
|
||
|
|
type Verified struct {
|
||
|
|
descriptor contract.Revision
|
||
|
|
origin contract.Actor
|
||
|
|
report Report
|
||
|
|
}
|
||
|
|
|
||
|
|
// Descriptor returns the verified descriptor, signature included.
|
||
|
|
func (v Verified) Descriptor() contract.Revision { return v.descriptor }
|
||
|
|
|
||
|
|
// Origin reports who or what produced the underlying candidate.
|
||
|
|
func (v Verified) Origin() contract.Actor { return v.origin }
|
||
|
|
|
||
|
|
// Report returns the evidence that justified verification.
|
||
|
|
func (v Verified) Report() Report { return v.report }
|
||
|
|
|
||
|
|
// ID reports the revision id.
|
||
|
|
func (v Verified) ID() contract.RevisionID { return v.descriptor.ID }
|
||
|
|
|
||
|
|
// Stage names a step of the publication pipeline (Blueprint section 35).
|
||
|
|
type Stage string
|
||
|
|
|
||
|
|
const (
|
||
|
|
StageSource Stage = "SOURCE"
|
||
|
|
StageBuild Stage = "BUILD"
|
||
|
|
StageContractCheck Stage = "CONTRACT_CHECK"
|
||
|
|
StageTest Stage = "TEST"
|
||
|
|
StageSecurityCheck Stage = "SECURITY_CHECK"
|
||
|
|
StagePolicyCheck Stage = "POLICY_CHECK"
|
||
|
|
StageSign Stage = "SIGN"
|
||
|
|
StagePublish Stage = "PUBLISH"
|
||
|
|
)
|
||
|
|
|
||
|
|
// Stages is the pipeline in order.
|
||
|
|
var Stages = []Stage{
|
||
|
|
StageSource, StageBuild, StageContractCheck, StageTest,
|
||
|
|
StageSecurityCheck, StagePolicyCheck, StageSign, StagePublish,
|
||
|
|
}
|
||
|
|
|
||
|
|
// StageResult records what happened at one stage.
|
||
|
|
type StageResult struct {
|
||
|
|
Stage Stage `json:"stage"`
|
||
|
|
Passed bool `json:"passed"`
|
||
|
|
Detail string `json:"detail,omitempty"`
|
||
|
|
Evidence []string `json:"evidence,omitempty"`
|
||
|
|
}
|
||
|
|
|
||
|
|
// Report is the accumulated evidence for a publication attempt.
|
||
|
|
//
|
||
|
|
// It is retained whether or not the attempt succeeded. Failed candidates are
|
||
|
|
// normal (Blueprint invariant 14) and the record of why one was rejected is
|
||
|
|
// exactly the evidence a later hypothesis needs.
|
||
|
|
type Report struct {
|
||
|
|
Revision contract.RevisionID `json:"revision"`
|
||
|
|
Stages []StageResult `json:"stages"`
|
||
|
|
}
|
||
|
|
|
||
|
|
// Passed reports whether every recorded stage passed.
|
||
|
|
func (r Report) Passed() bool {
|
||
|
|
for _, s := range r.Stages {
|
||
|
|
if !s.Passed {
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return len(r.Stages) > 0
|
||
|
|
}
|
||
|
|
|
||
|
|
// FirstFailure returns the stage that stopped the pipeline.
|
||
|
|
func (r Report) FirstFailure() (StageResult, bool) {
|
||
|
|
for _, s := range r.Stages {
|
||
|
|
if !s.Passed {
|
||
|
|
return s, true
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return StageResult{}, false
|
||
|
|
}
|
||
|
|
|
||
|
|
// ErrRejected reports a candidate that failed a gate.
|
||
|
|
type ErrRejected struct {
|
||
|
|
Revision contract.RevisionID
|
||
|
|
Failure StageResult
|
||
|
|
}
|
||
|
|
|
||
|
|
func (e *ErrRejected) Error() string {
|
||
|
|
return fmt.Sprintf("revision %s rejected at %s: %s", e.Revision, e.Failure.Stage, e.Failure.Detail)
|
||
|
|
}
|
||
|
|
|
||
|
|
// ErrNotVerified reports an attempt to publish something unverified.
|
||
|
|
var ErrNotVerified = errors.New("revision has not passed verification")
|
||
|
|
|
||
|
|
// signedDescriptor attaches a signature to a descriptor.
|
||
|
|
func signedDescriptor(d contract.Revision, sig signing.Signature) contract.Revision {
|
||
|
|
out := d
|
||
|
|
out.Signature = &contract.RevisionSignature{
|
||
|
|
Algorithm: contract.RevisionSignatureAlgorithm(sig.Algorithm),
|
||
|
|
KeyID: sig.KeyID,
|
||
|
|
Value: sig.Value,
|
||
|
|
}
|
||
|
|
return out
|
||
|
|
}
|