Files
ietf-wimse-ect/refimpl/go-lang/ect/dag.go
Christian Nennemann bbf557e54b Restructure refimpl into go-lang and python subdirectories
Move Go reference implementation to refimpl/go-lang/ and add new
Python reference implementation in refimpl/python/. Update build.sh
with renamed draft and simplified tool paths.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 23:11:55 +01:00

115 lines
3.7 KiB
Go

package ect
import (
"errors"
"fmt"
)
// DefaultClockSkewTolerance is the recommended clock skew between agents (Section 6.2).
const DefaultClockSkewTolerance = 30 // seconds
// DefaultMaxAncestorLimit is the recommended max ancestor traversal for cycle detection (Section 6.3).
const DefaultMaxAncestorLimit = 10000
// ECTStore provides lookup of ECTs by task ID (and optionally workflow ID) for DAG validation.
// Implemented by ledger or in-memory cache of verified parent ECTs.
type ECTStore interface {
// GetByTid returns the payload for the given task ID, or nil if not found.
GetByTid(tid string) *Payload
// Contains returns true if (tid, wid) already exists. wid may be empty for global scope.
Contains(tid, wid string) bool
}
// DAGConfig holds parameters for DAG validation.
type DAGConfig struct {
ClockSkewTolerance int // seconds; recommended 30
MaxAncestorLimit int // recommended 10000
MaxParLength int // max par length (0 = no limit; recommended 100)
}
// DefaultDAGConfig returns recommended defaults.
func DefaultDAGConfig() DAGConfig {
return DAGConfig{
ClockSkewTolerance: DefaultClockSkewTolerance,
MaxAncestorLimit: DefaultMaxAncestorLimit,
MaxParLength: DefaultMaxParLength,
}
}
// ValidateDAG runs Section 6.2 validation rules: uniqueness, parent existence,
// temporal ordering, acyclicity, parent policy decision.
func ValidateDAG(ect *Payload, store ECTStore, cfg DAGConfig) error {
if store == nil {
return errors.New("ect: ECTStore required for DAG validation")
}
if cfg.ClockSkewTolerance <= 0 {
cfg.ClockSkewTolerance = DefaultClockSkewTolerance
}
if cfg.MaxAncestorLimit <= 0 {
cfg.MaxAncestorLimit = DefaultMaxAncestorLimit
}
if cfg.MaxParLength > 0 && len(ect.Par) > cfg.MaxParLength {
return ErrParLength
}
// 1. Task ID Uniqueness (task id = jti per spec)
if store.Contains(ect.Jti, ect.Wid) {
return fmt.Errorf("ect: task ID (jti) already exists: %s", ect.Jti)
}
// 2. Parent Existence and 3. Temporal Ordering
for _, parentID := range ect.Par {
parent := store.GetByTid(parentID)
if parent == nil {
return fmt.Errorf("ect: parent task not found: %s", parentID)
}
// parent.iat < child.iat + clock_skew_tolerance => parent.iat - ect.iat <= clock_skew_tolerance
if parent.Iat >= ect.Iat+int64(cfg.ClockSkewTolerance) {
return fmt.Errorf("ect: parent task not earlier than current: %s", parentID)
}
}
// 4. Acyclicity (and depth limit)
visited := make(map[string]struct{})
if hasCycle(ect.Jti, ect.Par, store, visited, cfg.MaxAncestorLimit) {
return errors.New("ect: circular dependency or depth limit exceeded")
}
// 5. Parent Policy Decision (only when parent has policy claims per spec)
for _, parentID := range ect.Par {
parent := store.GetByTid(parentID)
if parent != nil && parent.HasPolicyClaims() &&
(parent.PolDecision == PolDecisionRejected || parent.PolDecision == PolDecisionPendingHumanReview) {
if !ect.CompensationRequired() {
return errors.New("ect: parent has non-approved pol_decision; current ECT must be compensation/remediation or have ext.compensation_required true")
}
}
}
return nil
}
// hasCycle returns true if following par from the given parent IDs leads back to targetTid
// or if traversal exceeds maxDepth. visited is mutated.
func hasCycle(targetTid string, parentIDs []string, store ECTStore, visited map[string]struct{}, maxDepth int) bool {
if len(visited) >= maxDepth {
return true
}
for _, parentID := range parentIDs {
if parentID == targetTid {
return true
}
if _, ok := visited[parentID]; ok {
continue
}
visited[parentID] = struct{}{}
parent := store.GetByTid(parentID)
if parent != nil {
if hasCycle(targetTid, parent.Par, store, visited, maxDepth) {
return true
}
}
}
return false
}