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
20 changes: 20 additions & 0 deletions boatstack/cmd/boatstack-helper/delegation_runtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,26 @@ func canReprojectDelegation(layout ports.ControllerLayout, invocation model.Invo
)
}

// revokedDelegationCanReachProgramPreflight permits no authority. It only lets
// an otherwise exact revoked request reach the read-only program-change probe
// that runs before delegation preparation. If that probe does not return the
// exact typed reconciliation suspension, ordinary delegation drift checks still
// reject the unadmitted candidate.
func revokedDelegationCanReachProgramPreflight(record delegation.Record, current delegation.Request) bool {
prior := record.Request
return record.Status == "revoked" &&
prior.RunID == current.RunID && prior.ProgramID == current.ProgramID &&
prior.EntryID == current.EntryID && prior.TargetID == current.TargetID &&
prior.ObjectiveID == current.ObjectiveID && prior.DeliveryID == current.DeliveryID &&
prior.RepositoryID == current.RepositoryID && prior.GitCommonID == current.GitCommonID &&
prior.InitialWorktreeID == current.InitialWorktreeID && prior.InitialRef == current.InitialRef &&
prior.BindingFingerprint == current.BindingFingerprint && prior.Description == current.Description &&
prior.HumanIdentityRole == current.HumanIdentityRole && prior.HumanIdentityProviderFingerprint == current.HumanIdentityProviderFingerprint &&
equalEntryInputFingerprints(prior.InputFingerprints, current.InputFingerprints) &&
sameStringSet(prior.EntryActivationAuthorities, current.EntryActivationAuthorities) &&
sameStringSet(prior.RequestedAuthorities, current.RequestedAuthorities)
}

func admittedDelegationReprojection(prior, current delegation.Request, configurationAdmits, installationAdmits func() (bool, error)) (bool, error) {
configurationChanged := prior.ControlBundleFingerprint != current.ControlBundleFingerprint || prior.HumanIdentityRole != current.HumanIdentityRole || prior.HumanIdentityProviderFingerprint != current.HumanIdentityProviderFingerprint
if prior.ProgramFingerprint == current.ProgramFingerprint && configurationChanged {
Expand Down
6 changes: 3 additions & 3 deletions boatstack/cmd/boatstack-helper/flow_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ type flowCommandOptions struct {

func runFlowCommand(arguments []string) error {
if len(arguments) == 0 {
return fmt.Errorf("usage: boatstack flow <compile|check|authorize|revoke|run|work|input> [flags]")
return fmt.Errorf("usage: boatstack flow <compile|check|authorize|revoke|run|work|work-package|input> [flags]")
}
action := arguments[0]
if action == "authorize" {
Expand All @@ -54,8 +54,8 @@ func runFlowCommand(arguments []string) error {
if action == "input" {
return runFlowInput(arguments[1:])
}
if action == "planning-package" {
return runFlowPlanningPackage(arguments[1:])
if action == "work-package" {
return runFlowWorkPackage(arguments[1:])
}
flags := flag.NewFlagSet("flow "+action, flag.ContinueOnError)
flags.SetOutput(os.Stderr)
Expand Down
7 changes: 5 additions & 2 deletions boatstack/cmd/boatstack-helper/flow_runtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -306,9 +306,12 @@ func bindFlowEntry(ctx context.Context, options commandOptions) (commandOptions,
return commandOptions{}, reprojectErr
}
if !reprojected {
return commandOptions{}, fmt.Errorf("DELEGATION_DRIFT: current Flow context does not match the authorized request (bundle %s, authorized %s)", delegationRequest.ControlBundleFingerprint, bound.ControlBundleFingerprint)
if !revokedDelegationCanReachProgramPreflight(record, delegationRequest) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Keep revoked candidates read-only until program preflight

Invariant: an unadmitted program with revoked delegation must produce no durable mutations before program-change preflight. With a revoked exact run, change the Flow fingerprint, then invoke a targeted repository transition requiring host input. This exception permits the candidate through; bindFlowEntry subsequently calls materializeFlowInvocation, whose SaveRequest persists an input request before preflightFlowAuthorizationProgramChange runs. Previously this path stopped at DELEGATION_DRIFT. The observable impact is kernel-owned request state created or superseded by an unadmitted program under revoked authority. A regression test should invoke that sequence, assert the program-change suspension, and assert the input-request store remains byte-for-byte unchanged.

Confidence: 0.96

return commandOptions{}, fmt.Errorf("DELEGATION_DRIFT: current Flow context does not match the authorized request (bundle %s, authorized %s)", delegationRequest.ControlBundleFingerprint, bound.ControlBundleFingerprint)
}
} else {
options.delegationReprojection = true
}
options.delegationReprojection = true
} else {
delegationRequest = bound
}
Expand Down
52 changes: 52 additions & 0 deletions boatstack/cmd/boatstack-helper/flow_runtime_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3308,6 +3308,58 @@ func TestExplicitAuthorizationCanReplaceRevokedPreReconciliationRequest(t *testi
}
}

func TestRevokedDelegationMayOnlyReachProgramPreflightWithExactStructuralContext(t *testing.T) {
// control-law: a revoked product delegation carries no authority while an
// exact request may still expose the installation reconciliation boundary.
prior := delegation.Request{
RunID: "run-example", ProgramID: "product-delivery", ProgramFingerprint: strings.Repeat("a", 64), ControlBundleFingerprint: strings.Repeat("b", 64),
EntryID: "run", TargetID: "published-pr", ObjectiveID: "objective", DeliveryID: "delivery", InputFingerprints: []delegation.InputFingerprint{{ID: "plan", Fingerprint: strings.Repeat("1", 64)}},
RepositoryID: "repository", GitCommonID: "common", InitialWorktreeID: "worktree", InitialRef: "refs/heads/feature",
BindingFingerprint: strings.Repeat("c", 64), HumanIdentityRole: "developer", HumanIdentityProviderFingerprint: strings.Repeat("f", 64),
EntryActivationAuthorities: []string{"human"}, RequestedAuthorities: []string{"autonomy"}, Description: "Run product delivery",
}
record := delegation.Record{Request: prior, Status: "revoked"}
candidate := prior
candidate.ProgramFingerprint = strings.Repeat("d", 64)
candidate.ControlBundleFingerprint = strings.Repeat("e", 64)
if !revokedDelegationCanReachProgramPreflight(record, candidate) {
t.Fatal("exact revoked program candidate could not reach program preflight")
}
active := record
active.Status = "active"
if revokedDelegationCanReachProgramPreflight(active, candidate) {
t.Fatal("active authority crossed an unadmitted program boundary")
}
tests := []struct {
name string
mutate func(*delegation.Request)
}{
{name: "entry", mutate: func(request *delegation.Request) { request.EntryID = "other" }},
{name: "objective", mutate: func(request *delegation.Request) { request.ObjectiveID = "other" }},
{name: "input", mutate: func(request *delegation.Request) { request.InputFingerprints[0].Fingerprint = strings.Repeat("2", 64) }},
{name: "repository", mutate: func(request *delegation.Request) { request.RepositoryID = "other" }},
{name: "worktree", mutate: func(request *delegation.Request) { request.InitialWorktreeID = "other" }},
{name: "ref", mutate: func(request *delegation.Request) { request.InitialRef = "refs/heads/other" }},
{name: "binding", mutate: func(request *delegation.Request) { request.BindingFingerprint = strings.Repeat("9", 64) }},
{name: "identity", mutate: func(request *delegation.Request) { request.HumanIdentityRole = "other" }},
{name: "activation authority", mutate: func(request *delegation.Request) { request.EntryActivationAuthorities = []string{"repository-policy"} }},
{name: "delegated authority", mutate: func(request *delegation.Request) { request.RequestedAuthorities = []string{"external-provider"} }},
{name: "description", mutate: func(request *delegation.Request) { request.Description = "other" }},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
changed := candidate
changed.InputFingerprints = append([]delegation.InputFingerprint(nil), candidate.InputFingerprints...)
changed.EntryActivationAuthorities = append([]string(nil), candidate.EntryActivationAuthorities...)
changed.RequestedAuthorities = append([]string(nil), candidate.RequestedAuthorities...)
test.mutate(&changed)
if revokedDelegationCanReachProgramPreflight(record, changed) {
t.Fatal("structural delegation drift reached program preflight")
}
})
}
}

func TestDelegationReprojectionRejectsUnadmittedContextChanges(t *testing.T) {
// control-law: ordinary input or objective drift cannot be relabeled as an
// installation or configuration-identity reprojection.
Expand Down
69 changes: 0 additions & 69 deletions boatstack/cmd/boatstack-helper/planning_package_command_test.go

This file was deleted.

25 changes: 18 additions & 7 deletions boatstack/cmd/boatstack-helper/product_delivery_flow_e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import (

"github.com/operatorstack/boatstack/boatstack/controlprogram"
softwareflow "github.com/operatorstack/boatstack/boatstack/flow/softwaredelivery"
"github.com/operatorstack/boatstack/boatstack/flow/softwaredelivery/planningpackage"
"github.com/operatorstack/boatstack/boatstack/flow/softwaredelivery/workpackage"
"github.com/operatorstack/boatstack/boatstack/internal/buildinfo"
boatstackruntime "github.com/operatorstack/boatstack/boatstack/internal/runtime"
"github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/catalog"
Expand Down Expand Up @@ -314,7 +314,7 @@ func TestExactProductDeliveryFlowReachesPublishedPRWithFakeProvider(t *testing.T
continue
}
if final.Receipt == nil || final.Prescription == nil {
t.Fatalf("unexpected non-terminal continuation response at step %d: %#v", step, final)
t.Fatalf("unexpected non-terminal continuation response at step %d: decision=%#v work=%#v response=%#v", step, final.Decision, final.Work, final)
}
if err := advanceContinuation(&continuation, final); err != nil {
t.Fatal(err)
Expand All @@ -332,7 +332,7 @@ func TestExactProductDeliveryFlowReachesPublishedPRWithFakeProvider(t *testing.T
}
wantTrace := []string{
"installation.initialize", "objective.bind", "engagement.begin",
"planning.package.admit", "planning.package.approve", "planning.package.promote", "plan.activate",
"work.package.admit", "work.package.approve", "planning.package.promote", "plan.activate",
"workspace.cut", "workspace.activate",
"gate.build.record", "gate.test.record", "gate.review.record",
"publication.preview", "publication.execute", "publication.observe",
Expand All @@ -352,19 +352,30 @@ func TestExactProductDeliveryFlowReachesPublishedPRWithFakeProvider(t *testing.T
t.Fatalf("trace transition %d = %s, want %s", index+1, receipts[index].TransitionID, transitionID)
}
}
packageRoot := filepath.Join(repository, ".boatstack", "planning-packages", "todo")
packageRoot := filepath.Join(repository, ".boatstack", "work-packages", "todo")
packages, err := os.ReadDir(packageRoot)
if err != nil || len(packages) != 1 || !packages[0].IsDir() {
t.Fatalf("planning package inventory = %#v, err=%v", packages, err)
}
currentProgram, err := loadCurrentPlanningProgram(context.Background(), repository)
writeFixture(t, repository, ".boatstack/flows/unrelated.flow.ir.json", []byte("must not be selected\n"))
currentProgram, err := loadCurrentWorkProgram(context.Background(), repository, "product-delivery")
if err != nil {
t.Fatalf("load current planning program: %v", err)
}
packageVerification := planningpackage.Verify(repository, "todo", packages[0].Name(), &currentProgram)
if packageVerification.Integrity != planningpackage.Valid || packageVerification.Contract != planningpackage.Valid || packageVerification.Approval != planningpackage.Valid || packageVerification.CurrentProgram != planningpackage.Match {
packageVerification := workpackage.Verify(repository, "todo", packages[0].Name(), &currentProgram)
if packageVerification.Integrity != workpackage.Valid || packageVerification.Contract != workpackage.Valid || packageVerification.Approval != workpackage.Valid || packageVerification.CurrentProgram != workpackage.Match {
t.Fatalf("portable planning package verification = %#v", packageVerification)
}
verificationRaw, err := captureStdout(t, func() error {
return runFlowWorkPackage([]string{"verify", "--repo", repository, "--delivery", "todo", "--package", packages[0].Name(), "--require-approval", "--require-current-program", "--format", "json"})
})
if err != nil {
t.Fatalf("multi-Flow package-bound verification: %v", err)
}
var commandVerification workpackage.Result
if err := json.Unmarshal(verificationRaw, &commandVerification); err != nil || commandVerification.ProgramID != "product-delivery" || commandVerification.CurrentProgram != workpackage.Match {
t.Fatalf("multi-Flow package-bound result = %#v, decode=%v", commandVerification, err)
}
last := receipts[len(receipts)-1]
if last.TransitionID != "publication.observe" || final.Snapshot.Publication.Value != model.PublicationOpen {
t.Fatalf("final publication observation = receipt=%s state=%s", last.TransitionID, final.Snapshot.Publication.Value)
Expand Down
Loading
Loading