Withhold uncommitted decision effects and pin accepted layer canon

Assistant: codex
Assistant-Model: gpt-6.1-sol
Assistant-Session: 01a126e1-76f8-7643-8cf3-e93336539ade
This commit is contained in:
tegwick 2026-10-11 02:21:40 +02:00
parent f5fad7c285
commit 22185e94ec
12 changed files with 269 additions and 65 deletions

View file

@ -42,15 +42,9 @@ type BatchRecorder interface {
AppendBatch([]api.DecisionEnvelope) error
}
// UncommittedNoter counts restrictions released after their record failed to
// commit (FLEX-DEC-2026-018).
type UncommittedNoter interface {
NoteReleasedUncommitted(api.DecisionEffect)
}
// ErrRecordNotCommitted means a permissive decision was withheld because its
// record did not commit. The service answers 503, and the consumer's declared
// stance applies (FLEX-DEC-2026-018).
// ErrRecordNotCommitted means a decision was withheld because its required
// record did not commit (GH-DEC-2026-024). The service answers 503; this is
// unavailability, never a substitute authorization verdict.
var ErrRecordNotCommitted = errors.New("decision withheld: record not committed")
// ListAllowedRequest describes a deterministic list_allowed call.
@ -134,8 +128,7 @@ func (e *Engine) decide(ctx context.Context, request api.CheckRequest) (api.Deci
}
// Check evaluates one subject/action/resource request. The decision is
// released only after its record commits, except that a restriction is never
// withheld (FLEX-DEC-2026-018).
// released only after its required record commits (GH-DEC-2026-024).
func (e *Engine) Check(ctx context.Context, request api.CheckRequest) (api.DecisionEnvelope, error) {
decision, err := e.decide(ctx, request)
if err != nil {
@ -441,34 +434,22 @@ func callerProvenance(ctx context.Context) *api.CallerProvenance {
return out
}
// recordDecisions commits decisions and applies FLEX-DEC-2026-018 when the
// commit fails: restrictions are released and counted, and any permissive
// decision withholds the call. In a batch that withholds the batch's
// restrictions too; no live consumer calls batch_check (user-engine loops
// /v1/check), and the residual is stated in the ruling.
// recordDecisions releases no effect until its required record commits.
// GH-DEC-2026-024 refuses the former restrictive-effect failure exception.
// Failed decisions must not enter Explain's history as if they were released.
func (e *Engine) recordDecisions(decisions []api.DecisionEnvelope) error {
e.mu.Lock()
for _, decision := range decisions {
e.history[decision.ID] = decision
}
e.mu.RLock()
log := e.log
e.mu.Unlock()
if log == nil || len(decisions) == 0 {
return nil
}
err := appendAll(log, decisions)
if err == nil {
return nil
}
for _, decision := range decisions {
if !decision.Effect.Restricts() {
e.mu.RUnlock()
if log != nil && len(decisions) != 0 {
if err := appendAll(log, decisions); err != nil {
return fmt.Errorf("%w: %v", ErrRecordNotCommitted, err)
}
}
if noter, ok := log.(UncommittedNoter); ok {
for _, decision := range decisions {
noter.NoteReleasedUncommitted(decision.Effect)
}
e.mu.Lock()
defer e.mu.Unlock()
for _, decision := range decisions {
e.history[decision.ID] = decision
}
return nil
}