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
19 changes: 19 additions & 0 deletions .github/tests/test_detached_supervision.py
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,11 @@ def test_detached_installation_and_engaged_guard_use_the_same_kernel(self) -> No
),
)

self.helper_json(
"apply", "--repo", self.repo, "--transition", "goal.configure",
*self.goal_flags(), "--human", "contract",
"--param", "goal_kind=approved-plan", "--param", "delivery_id=bootstrap",
)
self.helper_json(
"apply", "--repo", self.repo, "--transition", "engagement.begin",
*self.goal_flags(), "--repository-authority",
Expand Down Expand Up @@ -233,6 +238,12 @@ def test_authority_free_frontier_does_not_block_authorized_plan_creation(self) -
"init", "--repo", self.repo, *goal, *flow, "--human", "contract",
"--param", f"config_path={config}",
)
self.helper_json(
"apply", "--repo", self.repo, "--transition", "goal.configure",
*goal, *flow, "--human", "contract",
"--param", "goal_kind=open-or-updated-pr",
"--param", "delivery_id=codex-driver-authority-triggers",
)
self.helper_json(
"apply", "--repo", self.repo, "--transition", "engagement.begin",
*goal, *flow, "--repository-authority",
Expand Down Expand Up @@ -355,6 +366,14 @@ def test_one_delivery_context_rematerializes_repository_authority_after_initiali
for field in ('"admission"', '"receipt"', '"snapshot"', '"target_fingerprint"', '"recovery"'):
self.assertIn(field, initialized_process.stdout)

configured = self.helper_json(
"apply", "--repo", self.repo, "--transition", "goal.configure",
*goal, *flow, *actor,
"--param", "goal_kind=open-or-updated-pr",
"--param", "delivery_id=preserve-repository-authority-context",
)
self.assertEqual(configured["receipt"]["transition_id"], "goal.configure")

engagement = self.helper_json(
"next", "--repo", self.repo, *goal, *flow, *actor,
"--repository-authority",
Expand Down
6 changes: 6 additions & 0 deletions .github/tests/test_repository_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -452,6 +452,12 @@ def test_offline_installer_initializes_updates_and_guards_through_kernel(self) -
"--goal-id", "bootstrap", "--goal-kind", "approved-plan",
"--delivery", "bootstrap",
)
self.run_command(
launcher, "apply", "--repo", repository,
"--transition", "goal.configure", *goal,
"--human", "contract", "--param", "goal_kind=approved-plan",
"--param", "delivery_id=bootstrap", env=env,
)
self.run_command(
launcher, "apply", "--repo", repository,
"--transition", "engagement.begin", *goal,
Expand Down
6 changes: 1 addition & 5 deletions boatstack/cmd/boatstack-helper/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -200,11 +200,7 @@ func classifyCommand(command string) (surfaces.Operation, catalog.TransitionID,
return surfaces.OperationGuard, "", nil, nil
}
if transition, ok := aliases[command]; ok {
defaults := map[string]string{}
if command == "init" {
defaults["goal-kind"], defaults["delivery"], defaults["goal-id"] = string(model.GoalApprovedPlan), "bootstrap", "bootstrap"
}
return surfaces.OperationApply, transition, defaults, nil
return surfaces.OperationApply, transition, nil, nil
}
return "", "", nil, fmt.Errorf("unknown command %q", command)
}
Expand Down
3 changes: 3 additions & 0 deletions boatstack/control/control.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ type AuthorityClass = catalog.AuthorityClass
type FacetCondition = catalog.FacetCondition
type SelectionClass = catalog.SelectionClass
type GoalContract = catalog.GoalContract
type GoalScope = catalog.GoalScope
type EffectID = catalog.EffectID
type Prescription = catalog.Prescription
type ParameterSpec = catalog.ParameterSpec
Expand Down Expand Up @@ -56,6 +57,8 @@ const (
SelectionExplicitOnly = catalog.SelectionExplicitOnly
SelectionObservedExternal = catalog.SelectionObservedExternal

GoalScopeOptionalPreserve = catalog.GoalScopeOptionalPreserve

GoalApprovedPlan = model.GoalApprovedPlan
GoalVerified = model.GoalVerified
GoalOpenPR = model.GoalOpenPR
Expand Down
36 changes: 19 additions & 17 deletions boatstack/core/transitions.json
Original file line number Diff line number Diff line change
Expand Up @@ -699,7 +699,7 @@
"privacy_classification": "metadata-only",
"telemetry_classification": "transition-receipt",
"cost_class": "declared-neutral",
"policy": {},
"policy": {"goal_scope": "optional-preserve"},
"priority": 15,
"allows_identity_rebind": true
},
Expand Down Expand Up @@ -869,7 +869,7 @@
"privacy_classification": "metadata-only",
"telemetry_classification": "transition-receipt",
"cost_class": "declared-neutral",
"policy": {},
"policy": {"goal_scope": "optional-preserve"},
"priority": 12,
"allows_identity_rebind": true
},
Expand Down Expand Up @@ -1211,7 +1211,7 @@
"privacy_classification": "metadata-only",
"telemetry_classification": "transition-receipt",
"cost_class": "declared-neutral",
"policy": {},
"policy": {"goal_scope": "optional-preserve"},
"priority": 20
},
{
Expand Down Expand Up @@ -1392,7 +1392,7 @@
"privacy_classification": "metadata-only",
"telemetry_classification": "transition-receipt",
"cost_class": "declared-neutral",
"policy": {},
"policy": {"goal_scope": "optional-preserve"},
"priority": 25
},
{
Expand Down Expand Up @@ -1548,7 +1548,7 @@
"privacy_classification": "metadata-only",
"telemetry_classification": "transition-receipt",
"cost_class": "declared-neutral",
"policy": {},
"policy": {"goal_scope": "optional-preserve"},
"priority": 4
},
{
Expand Down Expand Up @@ -1720,7 +1720,7 @@
"privacy_classification": "metadata-only",
"telemetry_classification": "transition-receipt",
"cost_class": "declared-neutral",
"policy": {},
"policy": {"goal_scope": "optional-preserve"},
"priority": 22
},
{
Expand Down Expand Up @@ -1925,7 +1925,7 @@
"privacy_classification": "metadata-only",
"telemetry_classification": "transition-receipt",
"cost_class": "declared-neutral",
"policy": {},
"policy": {"goal_scope": "optional-preserve"},
"priority": 60
},
{
Expand Down Expand Up @@ -2067,7 +2067,7 @@
"privacy_classification": "metadata-only",
"telemetry_classification": "transition-receipt",
"cost_class": "declared-neutral",
"policy": {},
"policy": {"goal_scope": "optional-preserve"},
"priority": 3
},
{
Expand Down Expand Up @@ -2261,7 +2261,7 @@
"privacy_classification": "metadata-only",
"telemetry_classification": "transition-receipt",
"cost_class": "declared-neutral",
"policy": {},
"policy": {"goal_scope": "optional-preserve"},
"priority": 11
},
{
Expand Down Expand Up @@ -2417,7 +2417,8 @@
{
"facet": "goal",
"statuses": [
"known"
"known",
"absent"
]
},
{
Expand Down Expand Up @@ -2470,7 +2471,7 @@
"privacy_classification": "metadata-only",
"telemetry_classification": "transition-receipt",
"cost_class": "declared-neutral",
"policy": {},
"policy": {"goal_scope": "optional-preserve"},
"priority": 65
},
{
Expand Down Expand Up @@ -2541,7 +2542,7 @@
{"facet": "recovery", "statuses": ["known"], "values": ["none"]},
{"facet": "transaction", "statuses": ["known"], "values": ["none"]},
{"facet": "terminal", "statuses": ["known"], "values": ["nonterminal", "stale", "established"]},
{"facet": "goal", "statuses": ["known"]},
{"facet": "goal", "statuses": ["known", "absent"]},
{"facet": "configuration", "statuses": ["known"], "values": ["verified"]}
],
"admission_predicate": "predicate:exact-admission:installation.reconcile-update",
Expand All @@ -2567,7 +2568,7 @@
"privacy_classification": "metadata-only",
"telemetry_classification": "transition-receipt",
"cost_class": "declared-neutral",
"policy": {"reconciles_program": true},
"policy": {"reconciles_program": true, "goal_scope": "optional-preserve"},
"priority": 1
},
{
Expand Down Expand Up @@ -2750,7 +2751,8 @@
"telemetry_classification": "transition-receipt",
"cost_class": "declared-neutral",
"policy": {
"reconciles_program": true
"reconciles_program": true,
"goal_scope": "optional-preserve"
},
"priority": 1
},
Expand Down Expand Up @@ -3038,7 +3040,7 @@
"privacy_classification": "metadata-only",
"telemetry_classification": "transition-receipt",
"cost_class": "declared-neutral",
"policy": {},
"policy": {"goal_scope": "optional-preserve"},
"priority": 2
},
{
Expand Down Expand Up @@ -3182,7 +3184,7 @@
"privacy_classification": "metadata-only",
"telemetry_classification": "transition-receipt",
"cost_class": "declared-neutral",
"policy": {},
"policy": {"goal_scope": "optional-preserve"},
"priority": 3
},
{
Expand Down Expand Up @@ -3321,7 +3323,7 @@
"privacy_classification": "metadata-only",
"telemetry_classification": "transition-receipt",
"cost_class": "declared-neutral",
"policy": {},
"policy": {"goal_scope": "optional-preserve"},
"priority": 5
},
{
Expand Down
22 changes: 19 additions & 3 deletions boatstack/internal/effects/artifacts.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,9 +89,13 @@ type publicationPreview struct {

func prepareArtifacts(layout ports.ControllerLayout, admission protocol.Admission, transition catalog.Transition, state *durable.State) ([]ports.ResourceMutation, error) {
var mutations []ports.ResourceMutation
deliveryID, err := safeSegment(admission.Goal.DeliveryID, "delivery identity")
if err != nil {
return nil, err
var deliveryID string
if transitionUsesDeliveryArtifacts(transition.ID) {
var err error
deliveryID, err = safeSegment(admission.Goal.DeliveryID, "delivery identity")
if err != nil {
return nil, err
}
}
artifactRoot := filepath.Join(layout.RepositoryRoot, ".boatstack")
switch transition.ID {
Expand Down Expand Up @@ -291,6 +295,18 @@ func prepareArtifacts(layout ports.ControllerLayout, admission protocol.Admissio
return mutations, nil
}

func transitionUsesDeliveryArtifacts(id catalog.TransitionID) bool {
switch id {
case "plan.create", "plan.amend", "plan.validate", "plan.approve", "plan.approve-amendment",
"evidence.approval.revoke", "gate.build.record", "gate.test.record", "gate.review.record",
"gate.change.record", "gate.journey.record", "evidence.visual.attach", "publication.preview",
"publication.execute", "publication.correct":
return true
default:
return false
}
}

func loadPublicationPreview(path string) (publicationPreview, error) {
raw, err := os.ReadFile(path)
if err != nil {
Expand Down
26 changes: 25 additions & 1 deletion boatstack/internal/effects/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,7 @@ func TestExternalConfigurationAuthorityTransfersAcrossAttachAndDetach(t *testing
{Name: "source_revision", Value: "external-config-fixture"}, {Name: "runtime_path", Value: executable}, {Name: "runtime_sha256", Value: digestBytes(runtimeRaw)},
{Name: "config_path", Value: initialPath}, {Name: "config_sha256", Value: configFingerprint(t, initialConfig)},
})
apply("goal.configure", human, false, protocol.Parameters{{Name: "goal_kind", Value: string(goal.Kind)}, {Name: "delivery_id", Value: goal.DeliveryID}})
apply("repository.attach", human, false, protocol.Parameters{{Name: "topology", Value: "detached"}, {Name: "config_authority", Value: "external"}})
resolver, err := plant.NewResolver(externalRoot)
if err != nil {
Expand Down Expand Up @@ -273,6 +274,9 @@ func TestProgramDriftRequiresAtomicInstallationReconciliation(t *testing.T) {
if initialized.Receipt == nil || initialized.Receipt.ProgramFingerprint != oldProgram.Fingerprint() {
t.Fatalf("initial receipt did not freeze old program: %#v", initialized.Receipt)
}
if initialized.Snapshot == nil || initialized.Snapshot.Goal.Status != model.FactAbsent || initialized.Receipt.GoalStatus != model.FactAbsent || initialized.Receipt.GoalID != "" {
t.Fatalf("installation initialization invented product intent: %#v", initialized)
}

newProgram := testProgram()
newKernel, err := boatstack.NewKernel(externalRoot, newProgram)
Expand Down Expand Up @@ -342,6 +346,9 @@ func TestProgramDriftRequiresAtomicInstallationReconciliation(t *testing.T) {
reconciled.Snapshot.Program.Value != model.ProgramCurrent || reconciled.Snapshot.Phase.Value != model.PhaseObserved {
t.Fatalf("reconciliation did not establish exact program identity: %#v", reconciled)
}
if reconciled.Snapshot.Goal.Status != model.FactAbsent || reconciled.Receipt.GoalStatus != model.FactAbsent || reconciled.Receipt.GoalID != "" {
t.Fatalf("reconcile-update invented product intent: %#v", reconciled)
}
afterSuccess, err := os.ReadFile(layout.StatePath)
if err != nil {
t.Fatal(err)
Expand All @@ -356,6 +363,20 @@ func TestProgramDriftRequiresAtomicInstallationReconciliation(t *testing.T) {
if !bytes.Equal(afterSuccess, afterReplay) {
t.Fatal("rejected repeated reconciliation mutated durable state")
}
updated, err := newKernel.Handle(ctx, surfaces.Request{
SchemaVersion: surfaces.SchemaVersion, Operation: surfaces.OperationApply, Repository: repository, Host: "cli", CorrelationID: "program-current-update",
FlowID: "flow-program-drift", Goal: model.Goal{ID: "ignored-command-goal", Kind: model.GoalOpenPR, DeliveryID: "ignored"},
TransitionID: "installation.update", Authority: human,
Parameters: protocol.Parameters{
{Name: "source_revision", Value: "program-current"}, {Name: "runtime_path", Value: executable}, {Name: "runtime_sha256", Value: digestBytes(runtimeRaw)},
},
})
if err != nil {
t.Fatalf("current-program update after reconciliation: %v", err)
}
if updated.Snapshot == nil || updated.Snapshot.Goal.Status != model.FactAbsent || updated.Receipt == nil || updated.Receipt.GoalStatus != model.FactAbsent || updated.Receipt.GoalID != "" {
t.Fatalf("reconcile to update composition invented product intent: %#v", updated)
}
}

func TestReferenceExtensionUsesKernelAdmissionVerificationAndReceiptPath(t *testing.T) {
Expand Down Expand Up @@ -420,6 +441,7 @@ func TestReferenceExtensionUsesKernelAdmissionVerificationAndReceiptPath(t *test
{Name: "source_revision", Value: "extension-fixture"}, {Name: "runtime_path", Value: executable}, {Name: "runtime_sha256", Value: digestBytes(runtimeRaw)},
{Name: "config_path", Value: configPath}, {Name: "config_sha256", Value: configFingerprint(t, configRaw)},
})
apply("goal.configure", authority(catalog.AuthorityHuman), protocol.Parameters{{Name: "goal_kind", Value: string(goal.Kind)}, {Name: "delivery_id", Value: goal.DeliveryID}})
apply("engagement.begin", authority(catalog.AuthorityRepository), nil)
planPath := filepath.Join(t.TempDir(), "plan.md")
planRaw := []byte("# Extension plan\n")
Expand Down Expand Up @@ -547,6 +569,7 @@ func TestConcreteWorkflowPreservesConfigurationProofAndGoalTerminals(t *testing.
{Name: "source_revision", Value: "integration-revision"}, {Name: "runtime_path", Value: executable}, {Name: "runtime_sha256", Value: digestBytes(runtimeRaw)},
{Name: "config_path", Value: configPath}, {Name: "config_sha256", Value: configFingerprint(t, configRaw)},
})
apply(approvedGoal, "goal.configure", authority(catalog.AuthorityHuman), protocol.Parameters{{Name: "goal_kind", Value: string(approvedGoal.Kind)}, {Name: "delivery_id", Value: approvedGoal.DeliveryID}})
apply(approvedGoal, "engagement.begin", authority(catalog.AuthorityRepository), nil)

updatedConfigPath := filepath.Join(t.TempDir(), "project-v2-updated.json")
Expand Down Expand Up @@ -677,6 +700,7 @@ func TestWorkspaceCutTransfersAuthorityToExactDestinationWorktree(t *testing.T)
{Name: "source_revision", Value: "integration-revision"}, {Name: "runtime_path", Value: executable}, {Name: "runtime_sha256", Value: digestBytes(runtimeRaw)},
{Name: "config_path", Value: configSource}, {Name: "config_sha256", Value: configFingerprint(t, configRaw)},
})
apply(sourceInvocation, "goal.configure", human, protocol.Parameters{{Name: "goal_kind", Value: string(goal.Kind)}, {Name: "delivery_id", Value: goal.DeliveryID}})
run(t, repository, "git", "add", ".boatstack/project.json")
run(t, repository, "git", "commit", "-q", "-m", "install V2 configuration")
repositoryAuthority := func(path string) protocol.AuthorityBundle {
Expand Down Expand Up @@ -768,7 +792,7 @@ func TestWorkspaceCutTransfersAuthorityToExactDestinationWorktree(t *testing.T)
if err != nil {
t.Fatal(err)
}
if strings.Count(strings.TrimSpace(string(eventBytes)), "\n")+1 != 7 {
if strings.Count(strings.TrimSpace(string(eventBytes)), "\n")+1 != 8 {
t.Fatalf("shared flow telemetry lost a cross-worktree transition: %s", eventBytes)
}
}
Expand Down
3 changes: 3 additions & 0 deletions boatstack/internal/effects/receipts.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,8 @@ type processEvent struct {
Sequence uint64 `json:"sequence"`
Timestamp time.Time `json:"timestamp"`
GoalID string `json:"goal_id"`
GoalScope string `json:"goal_scope,omitempty"`
GoalStatus string `json:"goal_status,omitempty"`
TransitionID string `json:"transition_id"`
ProgramFingerprint string `json:"program_fingerprint"`
SourceFingerprint string `json:"source_fingerprint"`
Expand Down Expand Up @@ -181,6 +183,7 @@ func (s *ReceiptStore) Append(ctx context.Context, receipt protocol.TransitionRe
}
event := processEvent{
SchemaVersion: 1, FlowID: receipt.FlowID, Sequence: receipt.Sequence, Timestamp: s.clock.Now().UTC(), GoalID: receipt.GoalID,
GoalScope: string(receipt.GoalScope), GoalStatus: string(receipt.GoalStatus),
TransitionID: string(receipt.TransitionID), ProgramFingerprint: receipt.ProgramFingerprint, SourceFingerprint: receipt.SourceFingerprint, TargetFingerprint: receipt.TargetFingerprint,
Outcome: string(receipt.Outcome), DurationNanoseconds: receipt.DurationNanoseconds,
AuthorityClasses: append([]string(nil), receipt.AuthorityClasses...), Recovery: string(receipt.Recovery), Terminal: string(receipt.Terminal), FailureClass: receipt.FailureClass,
Expand Down
3 changes: 2 additions & 1 deletion boatstack/internal/effects/recovery_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,8 @@ func TestRestartRecoveryRollsBackExactPriorBytesAndArchivesJournal(t *testing.T)
if err != nil {
t.Fatal(err)
}
if result.Target.Phase.Value != model.PhaseDormant || result.Target.Recovery.Value != model.RecoveryNone || result.Receipt.ID == "" {
if result.Target.Phase.Value != model.PhaseDormant || result.Target.Recovery.Value != model.RecoveryNone || result.Target.Goal.Status != model.FactAbsent ||
result.Receipt.ID == "" || result.Receipt.GoalStatus != model.FactAbsent || result.Receipt.GoalID != "" {
t.Fatalf("rollback target=%#v receipt=%q", result.Target, result.Receipt.ID)
}
layout, _, _ := resolver.ResolveLayout(ctx, restartedInvocation)
Expand Down
Loading