Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
303 changes: 283 additions & 20 deletions boatstack/core/transitions.json

Large diffs are not rendered by default.

59 changes: 52 additions & 7 deletions boatstack/delivery/control.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@ type ParameterSpec = catalog.ParameterSpec
type InterruptionContract = catalog.InterruptionContract
type PolicyContract = catalog.PolicyContract
type Reversibility = catalog.Reversibility
type StateEffect = catalog.StateEffect
type StateEffectKind = catalog.StateEffectKind
type StateAssignment = catalog.StateAssignment
type StatePrecondition = catalog.StatePrecondition
type StateValueReference = catalog.StateValueReference
type StateFacet = model.StateFacet
type ObjectiveKind = model.ObjectiveKind
type ProtocolPhase = model.ProtocolPhase
type FactStatus = model.FactStatus
Expand Down Expand Up @@ -87,11 +93,19 @@ const (
PhaseUnresolved = model.PhaseUnresolved
PhaseAbandoned = model.PhaseAbandoned

FactKnown = model.FactKnown
FactAbsent = model.FactAbsent
FactUnknown = model.FactUnknown
FactStale = model.FactStale
FactConflicting = model.FactConflicting
FactKnown = model.FactKnown

StateEffectAssignments = catalog.StateEffectAssignments
StateEffectNative = catalog.StateEffectNative

StateFacetInstallation = model.StateFacetInstallation
StateFacetProgram = model.StateFacetProgram
StateFacetControl = model.StateFacetControl
StateFacetProduct = model.StateFacetProduct
FactAbsent = model.FactAbsent
FactUnknown = model.FactUnknown
FactStale = model.FactStale
FactConflicting = model.FactConflicting

Reversible = catalog.Reversible
Compensatable = catalog.Compensatable
Expand All @@ -118,7 +132,7 @@ const (
FacetObjective = model.FacetObjective
)

const ProgramSchemaVersion = 3
const ProgramSchemaVersion = 4

func KernelEffectCapabilities(transition Transition) []Capability {
return catalog.KernelEffectCapabilities(transition)
Expand Down Expand Up @@ -583,7 +597,10 @@ func compileSupervisoryProgram(runtime ProgramRuntimeManifest, compatibility, do
for _, capability := range transition.RequiredCapabilities {
capabilities = append(capabilities, general.Capability(capability))
}
facets := append([]string(nil), transition.OwnedResources...)
facets := make([]string, 0, len(transition.OwnedFacets)+1)
for _, facet := range transition.OwnedFacets {
facets = append(facets, string(facet))
}
mutation := general.PreserveObjective
if transition.Policy.BindsRequestedObjective {
mutation = general.BindObjectiveMutation
Expand Down Expand Up @@ -691,6 +708,9 @@ func validateProgramRuntime(manifest ProgramRuntimeManifest) error {
}
if manifest.RuntimeMode == ProgramRuntimeProtocol {
for _, condition := range transition.TargetConditions {
if stateEffectTargetsFacet(transition.StateEffect, condition.Facet) {
continue
}
if !strings.HasPrefix(string(condition.Facet), manifest.ID+".") {
return fmt.Errorf("protocol ProgramRuntime transition %q targets non-owned fact %q", transition.ID, condition.Facet)
}
Expand Down Expand Up @@ -720,6 +740,16 @@ func stringSet(values []string) map[string]bool {
return result
}

func stateEffectTargetsFacet(effect StateEffect, target FacetName) bool {
for _, assignment := range effect.Assignments {
facet, ok := catalog.DeclaredStateResolverFacet(assignment.Facet)
if ok && facet == target {
return true
}
}
return false
}

func transitionSet(values []TransitionID) map[TransitionID]bool {
result := make(map[TransitionID]bool, len(values))
for _, value := range values {
Expand Down Expand Up @@ -836,6 +866,9 @@ func validateExtension(manifest ExtensionManifest, seen, reserved map[string]boo
}
seenTransitions[transition.ID] = true
for _, condition := range transition.TargetConditions {
if stateEffectTargetsFacet(transition.StateEffect, condition.Facet) {
continue
}
if !strings.HasPrefix(string(condition.Facet), manifest.ID+".") {
return fmt.Errorf("extension transition %q targets non-owned fact %q", transition.ID, condition.Facet)
}
Expand Down Expand Up @@ -1037,6 +1070,18 @@ func cloneTransition(value Transition) Transition {
value.DeclaredCapabilities = append([]Capability(nil), value.DeclaredCapabilities...)
value.RequiredEvidence = append([]string(nil), value.RequiredEvidence...)
value.OwnedResources = append([]string(nil), value.OwnedResources...)
value.OwnedFacets = append([]model.StateFacet(nil), value.OwnedFacets...)
value.StateEffect.Preconditions = append([]catalog.StatePrecondition(nil), value.StateEffect.Preconditions...)
for index := range value.StateEffect.Preconditions {
value.StateEffect.Preconditions[index].Values = append([]string(nil), value.StateEffect.Preconditions[index].Values...)
}
value.StateEffect.Assignments = append([]catalog.StateAssignment(nil), value.StateEffect.Assignments...)
for index := range value.StateEffect.Assignments {
if literal := value.StateEffect.Assignments[index].Value; literal != nil {
copy := *literal
value.StateEffect.Assignments[index].Value = &copy
}
}
value.LocalEffects = append([]catalog.EffectID(nil), value.LocalEffects...)
value.ExternalEffects = append([]catalog.EffectID(nil), value.ExternalEffects...)
value.Parameters = append([]catalog.ParameterSpec(nil), value.Parameters...)
Expand Down
2 changes: 2 additions & 0 deletions boatstack/delivery/control_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,8 @@ func TestProgramFingerprintBindsCompositionAndPolicyInputs(t *testing.T) {
recoveryTransition.SelectionClass = delivery.SelectionExtensionRecovery
recoveryTransition.SourcePhases = []delivery.ProtocolPhase{delivery.PhaseRecovery}
recoveryTransition.TargetPhases = []delivery.ProtocolPhase{delivery.PhaseActive}
activePhase := string(delivery.PhaseActive)
recoveryTransition.StateEffect.Assignments = []delivery.StateAssignment{{Facet: "phase", Value: &activePhase}}
recoveryTransition.Effect = "boatstack.release-note.recover-effect"
recoveryTransition.LocalEffects = []delivery.EffectID{recoveryTransition.Effect}
recoveryTransition.Verifier = "boatstack.release-note.recover-verifier"
Expand Down
2 changes: 1 addition & 1 deletion boatstack/delivery/extension.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import (
"time"
)

const ExtensionProtocolVersion = 2
const ExtensionProtocolVersion = 3

type ExtensionOperation string

Expand Down
21 changes: 21 additions & 0 deletions boatstack/delivery/program_manifest.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ type ProgramTransition struct {
RequiredCapabilities []Capability `json:"required_capabilities"`
RequiredEvidence []string `json:"required_evidence"`
OwnedResources []string `json:"owned_resources,omitempty"`
OwnedFacets []StateFacet `json:"owned_facets"`
StateEffect StateEffect `json:"state_effect"`
Comment thread
bigboateng marked this conversation as resolved.
Effect EffectID `json:"effect,omitempty"`
LocalEffects []EffectID `json:"local_effects,omitempty"`
ExternalEffects []EffectID `json:"external_effects,omitempty"`
Expand Down Expand Up @@ -337,6 +339,7 @@ func (value ProgramTransition) runtimeTransition() Transition {
RequiredIdentity: value.RequiredIdentity, Authority: value.Authority, AuthorityAll: value.AuthorityAll,
RequiredCapabilities: value.RequiredCapabilities,
RequiredEvidence: value.RequiredEvidence, OwnedResources: value.OwnedResources, Effect: value.Effect,
OwnedFacets: value.OwnedFacets, StateEffect: value.StateEffect,
LocalEffects: value.LocalEffects, ExternalEffects: value.ExternalEffects, Idempotent: value.Idempotent,
Parameters: value.Parameters, Prescription: value.Prescription, SourcePredicate: value.SourcePredicate,
SourceConditions: value.SourceConditions, AdmissionPredicate: value.AdmissionPredicate,
Expand Down Expand Up @@ -469,6 +472,24 @@ func normalizeProgramTransition(value Transition) (Transition, error) {
if err != nil {
return Transition{}, err
}
value.OwnedFacets, err = uniqueSorted(value.OwnedFacets, func(v StateFacet) string { return string(v) })
if err != nil {
return Transition{}, err
}
for index := range value.StateEffect.Preconditions {
value.StateEffect.Preconditions[index].Values, err = uniqueSorted(value.StateEffect.Preconditions[index].Values, func(v string) string { return v })
if err != nil {
return Transition{}, err
}
}
value.StateEffect.Preconditions, err = uniqueSorted(value.StateEffect.Preconditions, func(v StatePrecondition) string { return v.Facet })
if err != nil {
return Transition{}, err
}
value.StateEffect.Assignments, err = uniqueSorted(value.StateEffect.Assignments, func(v StateAssignment) string { return v.Facet })
if err != nil {
return Transition{}, err
}
value.LocalEffects, err = uniqueSorted(value.LocalEffects, func(v EffectID) string { return string(v) })
if err != nil {
return Transition{}, err
Expand Down
71 changes: 68 additions & 3 deletions boatstack/delivery/program_manifest_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ func TestProgramManifestCanonicalFingerprintContract(t *testing.T) {
},
"target-phase": func(value *delivery.ProgramManifest) {
value.Transitions[0].TargetPhases = []delivery.ProtocolPhase{delivery.PhaseFrontier}
phase := string(delivery.PhaseFrontier)
value.Transitions[0].StateEffect.Assignments[0].Value = &phase
},
"authority": func(value *delivery.ProgramManifest) {
value.Transitions[0].Authority = []delivery.AuthorityClass{delivery.AuthorityHuman}
Expand All @@ -83,12 +85,23 @@ func TestProgramManifestCanonicalFingerprintContract(t *testing.T) {
value.Transitions[0].Verifier = "alternate.verifier"
},
"postcondition": func(value *delivery.ProgramManifest) {
value.Transitions[0].TargetConditions[0].Values = []string{"alternate"}
value.Transitions[0].TargetConditions[0].Values = []string{"published"}
published := "published"
value.Transitions[0].StateEffect.Assignments[1].Value = &published
},
"recovery": func(value *delivery.ProgramManifest) {
value.Transitions[0].Interruption.Recovery = "alternate.recover"
},
"priority": func(value *delivery.ProgramManifest) { value.Transitions[0].Priority++ },
"owned-facets": func(value *delivery.ProgramManifest) {
value.Transitions[0].OwnedFacets = []delivery.StateFacet{delivery.StateFacetControl}
value.Transitions[0].StateEffect.Assignments = value.Transitions[0].StateEffect.Assignments[:1]
value.Transitions[0].TargetConditions = []delivery.FacetCondition{delivery.KnownCondition(delivery.FacetProgram, "current")}
},
"state-effect": func(value *delivery.ProgramManifest) {
nonterminal := "nonterminal"
value.Transitions[0].StateEffect.Assignments = append(value.Transitions[0].StateEffect.Assignments, delivery.StateAssignment{Facet: "terminal", Value: &nonterminal})
},
}
for name, mutate := range mutations {
t.Run(name, func(t *testing.T) {
Expand All @@ -108,6 +121,37 @@ func TestProgramManifestCanonicalFingerprintContract(t *testing.T) {
}
}

func TestProgramManifestCanonicalizesStateEffectSets(t *testing.T) {
fixture := func() delivery.ProgramManifest {
manifest := programFixture()
transition := &manifest.Transitions[0]
transition.SourcePhases = []delivery.ProtocolPhase{delivery.PhaseActive, delivery.PhaseObserved}
transition.SourceConditions = append(transition.SourceConditions, delivery.KnownCondition(delivery.FacetTerminal, "nonterminal"))
transition.TargetConditions = append(transition.TargetConditions, delivery.KnownCondition(delivery.FacetTerminal, "established"))
phase, terminal, deliveryState := string(delivery.PhaseTerminal), "established", "terminal"
transition.StateEffect = delivery.StateEffect{
Kind: delivery.StateEffectAssignments,
Preconditions: []delivery.StatePrecondition{
{Facet: "phase", Values: []string{string(delivery.PhaseActive), string(delivery.PhaseObserved)}},
{Facet: "terminal", Values: []string{"nonterminal"}},
},
Assignments: []delivery.StateAssignment{{Facet: "phase", Value: &phase}, {Facet: "terminal", Value: &terminal}, {Facet: "delivery", Value: &deliveryState}},
}
return manifest
}

oneManifest := fixture()
one := loadManifest(t, oneManifest)
twoManifest := fixture()
reverse(twoManifest.Transitions[0].StateEffect.Preconditions)
reverse(twoManifest.Transitions[0].StateEffect.Preconditions[1].Values)
reverse(twoManifest.Transitions[0].StateEffect.Assignments)
two := loadManifest(t, twoManifest)
if one.Fingerprint() != two.Fingerprint() {
t.Fatalf("state-effect set ordering changed executable identity: %s != %s", one.Fingerprint(), two.Fingerprint())
}
}

func TestProgramManifestNamespaceAndCompatibilityBoundary(t *testing.T) {
// control-law: only-compatible-validated-programs-reach-the-runtime-registry
first := programFixture()
Expand Down Expand Up @@ -152,6 +196,22 @@ func TestProgramManifestNamespaceAndCompatibilityBoundary(t *testing.T) {
{"under-declared-kernel-effect", func(value *delivery.ProgramManifest) {
value.Capabilities.CapabilitySurface = []delivery.Capability{delivery.CapabilityRepositoryWrite}
}, runtimeFixture(), delivery.ProgramInvalid},
{"host-native-state-handler", func(value *delivery.ProgramManifest) {
value.Transitions[0].StateEffect = delivery.StateEffect{Kind: delivery.StateEffectNative, NativeHandler: "abandon-delivery"}
}, runtimeFixture(), delivery.ProgramInvalid},
{"product-only-owned-facets", func(value *delivery.ProgramManifest) {
value.Transitions[0].OwnedFacets = []delivery.StateFacet{delivery.StateFacetProduct}
}, runtimeFixture(), delivery.ProgramInvalid},
{"unclosed-verified-configuration", func(value *delivery.ProgramManifest) {
verified := "verified"
value.Transitions[0].SourceConditions = append(value.Transitions[0].SourceConditions, delivery.KnownCondition(delivery.FacetConfiguration, "unsupported"))
value.Transitions[0].TargetConditions = append(value.Transitions[0].TargetConditions, delivery.KnownCondition(delivery.FacetConfiguration, verified))
value.Transitions[0].StateEffect.Assignments = []delivery.StateAssignment{{Facet: "configuration", Value: &verified}}
}, runtimeFixture(), delivery.ProgramInvalid},
{"optional-assignment-parameter", func(value *delivery.ProgramManifest) {
value.Transitions[0].Parameters = append(value.Transitions[0].Parameters, delivery.ParameterSpec{Name: "optional_state"})
value.Transitions[0].StateEffect.Assignments = []delivery.StateAssignment{{Facet: "phase", ValueFrom: delivery.StateValueReference{Parameter: "optional_state"}}}
}, runtimeFixture(), delivery.ProgramInvalid},
{"duplicate-condition", func(value *delivery.ProgramManifest) {
value.Transitions[0].SourceConditions = append(value.Transitions[0].SourceConditions, value.Transitions[0].SourceConditions[0])
}, runtimeFixture(), delivery.ProgramInvalid},
Expand Down Expand Up @@ -234,18 +294,21 @@ func TestValidatedProgramIsTheKernelRegistry(t *testing.T) {
}

func programFixture() delivery.ProgramManifest {
activePhase := string(delivery.PhaseActive)
recovery := delivery.ProgramTransition{
ID: "recover", Version: 1, SelectionClass: delivery.SelectionProgramRecovery, Class: delivery.EventRecovery,
SourcePhases: []delivery.ProtocolPhase{delivery.PhaseRecovery}, TargetPhases: []delivery.ProtocolPhase{delivery.PhaseActive},
RequiredIdentity: []string{"repository-id"}, Authority: []delivery.AuthorityClass{delivery.AuthorityRepository}, RequiredCapabilities: []delivery.Capability{delivery.CapabilityRepositoryWrite}, RequiredEvidence: []string{"snapshot"},
OwnedResources: []string{"program.state"}, Effect: "program.recover", LocalEffects: []delivery.EffectID{"program.recover"}, Idempotent: true,
OwnedResources: []string{"program.state"}, OwnedFacets: []delivery.StateFacet{delivery.StateFacetControl}, StateEffect: delivery.StateEffect{Kind: delivery.StateEffectAssignments, Assignments: []delivery.StateAssignment{{Facet: "phase", Value: &activePhase}}},
Effect: "program.recover", LocalEffects: []delivery.EffectID{"program.recover"}, Idempotent: true,
Prescription: delivery.Prescription{Operation: "recover", ExpectedPostcondition: "active"}, SourcePredicate: "recovery-required",
SourceConditions: []delivery.FacetCondition{delivery.KnownCondition(delivery.FacetRecovery, "required")}, AdmissionPredicate: "exact-admission",
SourceConditions: []delivery.FacetCondition{delivery.KnownCondition(delivery.FacetRecovery, "required"), delivery.KnownCondition(delivery.FacetProgram, "current")}, AdmissionPredicate: "exact-admission",
TargetPredicate: "active", TargetConditions: []delivery.FacetCondition{delivery.KnownCondition(delivery.FacetProgram, "current")}, Verifier: "program.current",
Interruption: interruption("recover"), Reversibility: delivery.Reversible, TerminalEffect: "none",
PrivacyClassification: "metadata-only", TelemetryClassification: "transition-receipt", CostClass: "local", Policy: delivery.PolicyContract{ObjectiveScope: delivery.ObjectiveScopeOptionalPreserve}, Priority: 1,
}
advance := recovery
terminalPhase, terminalDelivery := string(delivery.PhaseTerminal), "terminal"
advance.ID = "advance"
advance.SelectionClass = delivery.SelectionProgramProgress
advance.Class = delivery.EventOwnedLocal
Expand All @@ -260,6 +323,8 @@ func programFixture() delivery.ProgramManifest {
advance.SourceConditions = []delivery.FacetCondition{delivery.KnownCondition(delivery.FacetProgram, "current"), delivery.KnownCondition(delivery.FacetDelivery, "active")}
advance.TargetPredicate = "terminal"
advance.TargetConditions = []delivery.FacetCondition{delivery.KnownCondition(delivery.FacetDelivery, "terminal")}
advance.OwnedFacets = []delivery.StateFacet{delivery.StateFacetControl, delivery.StateFacetProduct}
advance.StateEffect.Assignments = []delivery.StateAssignment{{Facet: "phase", Value: &terminalPhase}, {Facet: "delivery", Value: &terminalDelivery}}
advance.Verifier = "program.terminal"
advance.Policy.ObjectiveScope = delivery.ObjectiveScopeBoundExact
return delivery.ProgramManifest{
Expand Down
2 changes: 1 addition & 1 deletion boatstack/delivery/program_runtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import (
"fmt"
)

const ProgramRuntimeProtocolVersion = 2
const ProgramRuntimeProtocolVersion = 3

type ProgramRuntimeMode string

Expand Down
1 change: 1 addition & 0 deletions boatstack/extension/releasenote/releasenote.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ func (Extension) ExtensionManifest(context.Context) (delivery.ExtensionManifest,
ObjectiveKinds: []model.ObjectiveKind{model.ObjectiveOpenPR, model.ObjectiveMerged}, RequiredIdentity: []string{"repository-id", "git-common-id", "worktree-id", "controller-id", "topology", "host", "correlation-id"},
Authority: []catalog.AuthorityClass{catalog.AuthorityRepository}, RequiredEvidence: []string{"snapshot-fingerprint", "objective", "facet:" + FactID},
OwnedResources: []string{Resource}, Effect: Effect, LocalEffects: []catalog.EffectID{Effect}, Idempotent: true,
OwnedFacets: []model.StateFacet{model.StateFacetControl}, StateEffect: catalog.StateEffect{Kind: catalog.StateEffectAssignments},
Prescription: catalog.Prescription{Operation: Transition, ExpectedPostcondition: "release-note evidence is verified"},
SourcePredicate: "reference-release-note-missing", AdmissionPredicate: "exact-extension-admission", TargetPredicate: "reference-release-note-verified", Verifier: Verifier,
SourceConditions: []catalog.FacetCondition{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
request = json.load(sys.stdin)
operation = request["operation"]
response = {
"protocol_version": 2,
"protocol_version": 3,
"operation": operation,
"extension_id": request["extension_id"],
"extension_version": request["extension_version"],
Expand All @@ -17,7 +17,7 @@
response["manifest"] = {
"id": "fixture.echo",
"version": "1.0.0",
"protocol_version": 2,
"protocol_version": 3,
"settings_schema": {"type": "object"},
"facts": ["fixture.echo.present"],
"capabilities": ["command.execute"],
Expand Down
Loading