| 1 | package runtimepolicy |
| 2 | |
| 3 | import "sync" |
| 4 | |
| 5 | // Engine decides whether an individual action is allowed. It owns no task |
| 6 | // acceptance state and does not judge when a model should finish. |
| 7 | type Engine struct { |
| 8 | mu sync.Mutex |
| 9 | guards []Guard |
| 10 | constraints Constraints |
| 11 | } |
| 12 | |
| 13 | func NewEngine(constraints Constraints, extra ...Guard) *Engine { |
| 14 | guards := []Guard{PlanGuard{}, ConstraintGuard{Constraints: constraints}, MutationDependencyGuard{}, OpaqueWriterGuard{}} |
| 15 | return &Engine{guards: append(guards, extra...), constraints: constraints} |
| 16 | } |
| 17 | |
| 18 | func (e *Engine) Constraints() Constraints { |
| 19 | if e == nil { |
| 20 | return Constraints{} |
| 21 | } |
| 22 | return e.constraints |
| 23 | } |
| 24 | |
| 25 | // BeforeTool returns a decision before any permission prompt or execution. |
| 26 | // Serializing custom guards preserves their existing concurrency contract. |
| 27 | func (e *Engine) BeforeTool(ctx CallContext) GuardDecision { |
| 28 | if e == nil { |
| 29 | return GuardDecision{Action: GuardAbstain} |
| 30 | } |
| 31 | e.mu.Lock() |
| 32 | defer e.mu.Unlock() |
| 33 | var decisions []GuardDecision |
| 34 | for _, guard := range e.guards { |
| 35 | decisions = append(decisions, guard.BeforeTool(ctx)) |
| 36 | } |
| 37 | return MergeDecisions(decisions...) |
| 38 | } |
| 39 |