@@ -17,22 +17,26 @@ import (
1717 softwareflow "github.com/operatorstack/boatstack/boatstack/flow/softwaredelivery"
1818 boatstackruntime "github.com/operatorstack/boatstack/boatstack/internal/runtime"
1919 "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/effects"
20+ "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/humanidentity"
2021 "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/plant"
2122 "github.com/operatorstack/boatstack/boatstack/invocation"
2223)
2324
24- const declarativeRunSchemaRevision = 3
25+ const declarativeRunSchemaRevision = 4
2526
2627type declarativeTransitionReceipt struct {
27- ID string `json:"id"`
28- TransitionID string `json:"transition_id"`
29- InvocationFingerprint string `json:"invocation_fingerprint"`
30- PriorStateRevision uint64 `json:"prior_state_revision"`
31- ResultStateRevision uint64 `json:"result_state_revision"`
32- PriorReceiptFingerprint string `json:"prior_receipt_fingerprint,omitempty"`
33- Parameters []invocation.ResolvedParameter `json:"parameters"`
34- HumanActor string `json:"human_actor,omitempty"`
35- Fingerprint string `json:"fingerprint"`
28+ ID string `json:"id"`
29+ TransitionID string `json:"transition_id"`
30+ InvocationFingerprint string `json:"invocation_fingerprint"`
31+ PriorStateRevision uint64 `json:"prior_state_revision"`
32+ ResultStateRevision uint64 `json:"result_state_revision"`
33+ PriorReceiptFingerprint string `json:"prior_receipt_fingerprint,omitempty"`
34+ Parameters []invocation.ResolvedParameter `json:"parameters"`
35+ ControlBundleFingerprint string `json:"control_bundle_fingerprint"`
36+ AuthorityContextFingerprint string `json:"authority_context_fingerprint,omitempty"`
37+ AuthorityFingerprint string `json:"authority_fingerprint,omitempty"`
38+ HumanActor string `json:"human_actor,omitempty"`
39+ Fingerprint string `json:"fingerprint"`
3640}
3741
3842type declarativeRunState struct {
@@ -50,12 +54,15 @@ type declarativeRunState struct {
5054}
5155
5256type declarativeRuntimeContext struct {
53- compiled controlprogram.Compiled
54- entry controlprogram.Entry
55- state declarativeRunState
56- statePath string
57- store invocation.Store
58- executionScopeFingerprint string
57+ compiled controlprogram.Compiled
58+ entry controlprogram.Entry
59+ state declarativeRunState
60+ statePath string
61+ store invocation.Store
62+ controlBundle boatstackruntime.ControlBundleSnapshot
63+ executionScopeFingerprint string
64+ authorityContextFingerprint string
65+ humanIdentity * humanidentity.Presentation
5966}
6067
6168func tryRunDeclarativeFlow (ctx context.Context , options commandOptions ) (bool , error ) {
@@ -173,6 +180,15 @@ func runDeclarativeFlow(ctx context.Context, compiled controlprogram.Compiled, o
173180 if ! ok {
174181 return fmt .Errorf ("FLOW_RUNTIME_INVALID: declarative transition %q has no executable operator" , transition .ID )
175182 }
183+ requiresHumanAuthority := declarativeRequiresHumanAuthority (transition , operator )
184+ if requiresHumanAuthority || transitionUsesHostInput (transition ) {
185+ presentation , identityErr := humanIdentityPresentationForRepositoryBound (ctx , repository , options .host , "declarative-suspension" , runtimeContext .controlBundle , nil )
186+ if identityErr != nil {
187+ return identityErr
188+ }
189+ runtimeContext .authorityContextFingerprint = presentation .ProviderFingerprint
190+ runtimeContext .humanIdentity = & presentation
191+ }
176192 result , materializationContext , err := materializeDeclarativeInvocation (runtimeContext , transition , operator )
177193 if err != nil {
178194 return err
@@ -190,17 +206,23 @@ func runDeclarativeFlow(ctx context.Context, compiled controlprogram.Compiled, o
190206 return encodeDeclarativeResult (map [string ]any {
191207 "kind" : "suspended" , "code" : result .Request .Code , "run_id" : runtimeContext .state .RunID ,
192208 "program_fingerprint" : compiled .Fingerprint , "entry_id" : entry .ID , "target_id" : entry .Target ,
193- "transition_id" : transition .ID , "request" : result .Request ,
209+ "transition_id" : transition .ID , "request" : result .Request , "human_identity" : runtimeContext . humanIdentity ,
194210 }, options .format )
195211 }
196212 if result .Ready == nil {
197213 return fmt .Errorf ("FLOW_INVOCATION_INCOMPLETE: declarative materialization produced no evidence" )
198214 }
199- if err := requireDeclarativeAuthority (transition , operator , options .humanActor ); err != nil {
215+ authorityFingerprint := ""
216+ if requiresHumanAuthority {
217+ authorityFingerprint = declarativeAuthorityFingerprint (* result .Ready , transition , operator , runtimeContext .authorityContextFingerprint )
218+ }
219+ if err := requireDeclarativeAuthority (transition , operator , options .humanActor , options .authorityFingerprint , authorityFingerprint ); err != nil {
200220 return encodeDeclarativeResult (map [string ]any {
201221 "kind" : "blocked" , "code" : "AUTHORITY_REQUIRED" , "detail" : err .Error (),
202222 "run_id" : runtimeContext .state .RunID , "program_fingerprint" : compiled .Fingerprint ,
203223 "entry_id" : entry .ID , "target_id" : entry .Target , "transition_id" : transition .ID ,
224+ "authority_fingerprint" : authorityFingerprint , "requested_authorities" : declarativeAuthorities (transition , operator ),
225+ "human_identity" : runtimeContext .humanIdentity ,
204226 }, options .format )
205227 }
206228
@@ -211,6 +233,15 @@ func runDeclarativeFlow(ctx context.Context, compiled controlprogram.Compiled, o
211233 if err != nil {
212234 return err
213235 }
236+ if runtimeContext .humanIdentity != nil {
237+ current , identityErr := humanIdentityPresentationForRepositoryBound (ctx , repository , options .host , "declarative-commit" , runtimeContext .controlBundle , nil )
238+ if identityErr != nil {
239+ return identityErr
240+ }
241+ if current .ProviderFingerprint != runtimeContext .authorityContextFingerprint {
242+ return fmt .Errorf ("HUMAN_IDENTITY_DRIFT: verified identity provider changed before declarative state commit" )
243+ }
244+ }
214245 fresh , err := invocation .Materialize (operator .Parameters , transition .Parameters , materializationContext , nil )
215246 if err != nil {
216247 return err
@@ -230,7 +261,10 @@ func runDeclarativeFlow(ctx context.Context, compiled controlprogram.Compiled, o
230261 receipt := declarativeTransitionReceipt {
231262 TransitionID : transition .ID , InvocationFingerprint : fresh .Ready .InvocationFingerprint ,
232263 PriorStateRevision : priorRevision , ResultStateRevision : candidate .StateRevision ,
233- Parameters : append ([]invocation.ResolvedParameter (nil ), fresh .Ready .Parameters ... ), HumanActor : strings .TrimSpace (options .humanActor ),
264+ Parameters : append ([]invocation.ResolvedParameter (nil ), fresh .Ready .Parameters ... ),
265+ ControlBundleFingerprint : runtimeContext .controlBundle .Fingerprint ,
266+ AuthorityContextFingerprint : runtimeContext .authorityContextFingerprint , AuthorityFingerprint : authorityFingerprint ,
267+ HumanActor : strings .TrimSpace (options .humanActor ),
234268 }
235269 if previous := lastDeclarativeReceipt (candidate ); previous != nil {
236270 receipt .PriorReceiptFingerprint = previous .Fingerprint
@@ -255,8 +289,14 @@ func runDeclarativeFlow(ctx context.Context, compiled controlprogram.Compiled, o
255289 }, options .format )
256290}
257291
258- func requireDeclarativeAuthority (transition controlprogram.Transition , operator controlprogram.Operator , humanActor string ) error {
259- providedHuman := strings .TrimSpace (humanActor ) != ""
292+ func requireDeclarativeAuthority (transition controlprogram.Transition , operator controlprogram.Operator , humanActor , providedFingerprint , expectedFingerprint string ) error {
293+ actor := strings .TrimSpace (humanActor )
294+ providedHuman := actor != ""
295+ if providedHuman {
296+ if err := humanidentity .ValidateActor (actor ); err != nil {
297+ return err
298+ }
299+ }
260300 if len (operator .Authority .AnyOf ) != 0 && ! providedHuman {
261301 return fmt .Errorf ("operator %q requires one of %s" , operator .ID , strings .Join (operator .Authority .AnyOf , ", " ))
262302 }
@@ -266,10 +306,41 @@ func requireDeclarativeAuthority(transition controlprogram.Transition, operator
266306 if containsString (transition .Requires .Authorities , "human" ) && ! providedHuman {
267307 return fmt .Errorf ("transition %q requires human authority" , transition .ID )
268308 }
309+ if declarativeRequiresHumanAuthority (transition , operator ) && (len (expectedFingerprint ) != 64 || providedFingerprint != expectedFingerprint ) {
310+ return fmt .Errorf ("exact current authority fingerprint is required for transition %q" , transition .ID )
311+ }
269312 return nil
270313}
271314
315+ func declarativeRequiresHumanAuthority (transition controlprogram.Transition , operator controlprogram.Operator ) bool {
316+ return containsString (operator .Authority .AnyOf , "human" ) || containsString (operator .Authority .AllOf , "human" ) || containsString (transition .Requires .Authorities , "human" )
317+ }
318+
319+ func declarativeAuthorities (transition controlprogram.Transition , operator controlprogram.Operator ) []string {
320+ values := append (append (append ([]string (nil ), operator .Authority .AnyOf ... ), operator .Authority .AllOf ... ), transition .Requires .Authorities ... )
321+ sort .Strings (values )
322+ result := values [:0 ]
323+ for _ , value := range values {
324+ if len (result ) == 0 || result [len (result )- 1 ] != value {
325+ result = append (result , value )
326+ }
327+ }
328+ return result
329+ }
330+
331+ func declarativeAuthorityFingerprint (evidence invocation.Evidence , transition controlprogram.Transition , operator controlprogram.Operator , authorityContextFingerprint string ) string {
332+ return digestDeclarative (struct {
333+ InvocationFingerprint string `json:"invocation_fingerprint"`
334+ AuthorityContextFingerprint string `json:"authority_context_fingerprint"`
335+ Authorities []string `json:"authorities"`
336+ }{evidence .InvocationFingerprint , authorityContextFingerprint , declarativeAuthorities (transition , operator )})
337+ }
338+
272339func loadDeclarativeRuntimeContext (ctx context.Context , repository string , compiled controlprogram.Compiled , entry controlprogram.Entry , options commandOptions ) (declarativeRuntimeContext , error ) {
340+ controlBundle , err := buildRepositoryControlBundle (ctx , repository )
341+ if err != nil {
342+ return declarativeRuntimeContext {}, err
343+ }
273344 resolver , err := plant .NewResolver ("" )
274345 if err != nil {
275346 return declarativeRuntimeContext {}, err
@@ -325,7 +396,10 @@ func loadDeclarativeRuntimeContext(ctx context.Context, repository string, compi
325396 receipt := state .Receipts [index ]
326397 fingerprint := receipt .Fingerprint
327398 receipt .Fingerprint = ""
328- if fingerprint == "" || fingerprint != digestDeclarative (receipt ) || receipt .PriorReceiptFingerprint != priorReceiptFingerprint || receipt .PriorStateRevision != expectedRevision || receipt .ResultStateRevision != expectedRevision + 1 {
399+ invalidAuthority := (receipt .AuthorityContextFingerprint != "" && len (receipt .AuthorityContextFingerprint ) != 64 ) ||
400+ (receipt .HumanActor != "" && (len (receipt .AuthorityContextFingerprint ) != 64 || len (receipt .AuthorityFingerprint ) != 64 )) ||
401+ (receipt .HumanActor == "" && receipt .AuthorityFingerprint != "" )
402+ if fingerprint == "" || fingerprint != digestDeclarative (receipt ) || len (receipt .ControlBundleFingerprint ) != 64 || invalidAuthority || receipt .PriorReceiptFingerprint != priorReceiptFingerprint || receipt .PriorStateRevision != expectedRevision || receipt .ResultStateRevision != expectedRevision + 1 {
329403 return declarativeRuntimeContext {}, fmt .Errorf ("FLOW_CONTEXT_MISMATCH: declarative transition receipt is invalid" )
330404 }
331405 expectedRevision = receipt .ResultStateRevision
@@ -342,7 +416,7 @@ func loadDeclarativeRuntimeContext(ctx context.Context, repository string, compi
342416 }
343417 return declarativeRuntimeContext {
344418 compiled : compiled , entry : entry , state : state , statePath : statePath ,
345- store : invocation.Store {Root : layout .FlowRoot , Writer : effects .NewRuntimeStore ()}, executionScopeFingerprint : scope ,
419+ store : invocation.Store {Root : layout .FlowRoot , Writer : effects .NewRuntimeStore ()}, controlBundle : controlBundle , executionScopeFingerprint : scope ,
346420 }, nil
347421}
348422
@@ -359,15 +433,17 @@ func materializeDeclarativeInvocation(runtimeContext declarativeRuntimeContext,
359433 stateValues [facet ] = invocation.Value {Type : controlprogram.ValueTypeDefinition {Kind : "string" }, Canonical : value , Provenance : "state" , ProducerFingerprint : digestDeclarative (map [string ]string {"facet" : facet , "value" : value })}
360434 }
361435 contextFingerprint := digestDeclarative (struct {
362- RunID string `json:"run_id"`
363- Program string `json:"program"`
364- Entry string `json:"entry"`
365- Target string `json:"target"`
366- Transition string `json:"transition"`
367- StateRevision uint64 `json:"state_revision"`
368- Inputs map [string ]string `json:"inputs"`
369- Facts map [string ]string `json:"facts"`
370- }{runtimeContext .state .RunID , runtimeContext .compiled .Fingerprint , runtimeContext .entry .ID , runtimeContext .entry .Target , transition .ID , runtimeContext .state .StateRevision , runtimeContext .state .EntryInputs , runtimeContext .state .Facts })
436+ RunID string `json:"run_id"`
437+ Program string `json:"program"`
438+ Entry string `json:"entry"`
439+ Target string `json:"target"`
440+ Transition string `json:"transition"`
441+ StateRevision uint64 `json:"state_revision"`
442+ ControlBundleFingerprint string `json:"control_bundle_fingerprint"`
443+ AuthorityContextFingerprint string `json:"authority_context_fingerprint,omitempty"`
444+ Inputs map [string ]string `json:"inputs"`
445+ Facts map [string ]string `json:"facts"`
446+ }{runtimeContext .state .RunID , runtimeContext .compiled .Fingerprint , runtimeContext .entry .ID , runtimeContext .entry .Target , transition .ID , runtimeContext .state .StateRevision , runtimeContext .controlBundle .Fingerprint , runtimeContext .authorityContextFingerprint , runtimeContext .state .EntryInputs , runtimeContext .state .Facts })
371447 receipts , err := runtimeContext .store .LoadReceipts (runtimeContext .state .RunID , transition .ID )
372448 if err != nil {
373449 return invocation.Result {}, invocation.Context {}, err
@@ -376,8 +452,8 @@ func materializeDeclarativeInvocation(runtimeContext declarativeRuntimeContext,
376452 RunID : runtimeContext .state .RunID , ProgramFingerprint : runtimeContext .compiled .Fingerprint ,
377453 ExecutionProgramFingerprint : runtimeContext .compiled .Fingerprint ,
378454 EntryID : runtimeContext .entry .ID , TargetID : runtimeContext .entry .Target , TransitionID : transition .ID ,
379- StateRevision : runtimeContext .state .StateRevision , ContextFingerprint : contextFingerprint ,
380- ExecutionScopeFingerprint : runtimeContext .executionScopeFingerprint , EntryInputs : entryInputs ,
455+ StateRevision : runtimeContext .state .StateRevision , ContextFingerprint : contextFingerprint , ControlBundleFingerprint : runtimeContext . controlBundle . Fingerprint ,
456+ AuthorityContextFingerprint : runtimeContext . authorityContextFingerprint , ExecutionScopeFingerprint : runtimeContext .executionScopeFingerprint , EntryInputs : entryInputs ,
381457 State : stateValues , Receipts : map [string ]invocation.Value {}, WorkOutputs : map [string ]invocation.Value {}, InputReceipts : receipts ,
382458 }
383459 result , err := invocation .Materialize (operator .Parameters , transition .Parameters , materializationContext , nil )
0 commit comments