FLUID-WP-0004 T01, T02, T05, T06. The pipeline is the only path from Candidate to Verified: no other code constructs a Verified value, and Publish takes one, so "AI-generated artifacts are untrusted until verified" is a property of the type signatures rather than a rule people are asked to remember. The policy gate is a pure function of the candidate, the governing intent and configured limits. It cannot consult a model or take an opinion as input, because a gate that can be argued with is not a gate. Two behaviours it enforces are worth naming: the tighter of the descriptor's own traffic ceiling and the gate's wins, so a descriptor can restrict itself but never widen; and a daimon cannot authorize its own promotion below FLUID-5, since generation authority is not promotion authority. Signatures cover the canonical document with the signature member removed, so a signed descriptor round-trips and a tampered one does not. The registry now refuses anything that does not verify, which is what makes the pipeline's signature mean something at the router. An unchecked pipeline stage is recorded as unchecked rather than omitted, so a pipeline with no security check cannot look identical to one that passed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014KmVxhJ35tCo7rE7UnLwWu Assistant: claude-code Assistant-Model: opus Assistant-Process: 1116572@bnt-lap001 Assistant-Session: 8ba9bb93-a72a-4883-b189-2499cce5c400
154 lines
5.1 KiB
Go
154 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
|
|
}
|