feat: migrate refimpls from draft-00 to draft-01 claim names
- Rename `par` to `pred` (predecessor) in types, serialization, tests - Remove `pol`, `pol_decision` from core payload; move to `ect_ext` - Remove `sub` from payload (not part of ECT spec) - Update `typ` from `wimse-exec+jwt` to `exec+jwt` (accept both) - Rename MaxParLength to MaxPredLength everywhere - Update testdata, demos, READMEs with migration table - All Go tests pass, all 56 Python tests pass (90% coverage)
This commit is contained in:
@@ -8,7 +8,7 @@ from typing import TYPE_CHECKING
|
||||
if TYPE_CHECKING:
|
||||
from ect.types import Payload
|
||||
|
||||
from ect.validate import DEFAULT_MAX_PAR_LENGTH
|
||||
from ect.validate import DEFAULT_MAX_PRED_LENGTH
|
||||
|
||||
DEFAULT_CLOCK_SKEW_TOLERANCE = 30
|
||||
DEFAULT_MAX_ANCESTOR_LIMIT = 10000
|
||||
@@ -31,11 +31,11 @@ class DAGConfig:
|
||||
self,
|
||||
clock_skew_tolerance: int = DEFAULT_CLOCK_SKEW_TOLERANCE,
|
||||
max_ancestor_limit: int = DEFAULT_MAX_ANCESTOR_LIMIT,
|
||||
max_par_length: int = 0,
|
||||
max_pred_length: int = 0,
|
||||
):
|
||||
self.clock_skew_tolerance = clock_skew_tolerance or DEFAULT_CLOCK_SKEW_TOLERANCE
|
||||
self.max_ancestor_limit = max_ancestor_limit or DEFAULT_MAX_ANCESTOR_LIMIT
|
||||
self.max_par_length = max_par_length or 0
|
||||
self.max_pred_length = max_pred_length or 0
|
||||
|
||||
|
||||
def default_dag_config() -> DAGConfig:
|
||||
@@ -44,22 +44,22 @@ def default_dag_config() -> DAGConfig:
|
||||
|
||||
def _has_cycle(
|
||||
target_tid: str,
|
||||
parent_ids: list[str],
|
||||
pred_ids: list[str],
|
||||
store: ECTStore,
|
||||
visited: set[str],
|
||||
max_depth: int,
|
||||
) -> bool:
|
||||
if len(visited) >= max_depth:
|
||||
return True
|
||||
for parent_id in parent_ids:
|
||||
if parent_id == target_tid:
|
||||
for pred_id in pred_ids:
|
||||
if pred_id == target_tid:
|
||||
return True
|
||||
if parent_id in visited:
|
||||
if pred_id in visited:
|
||||
continue
|
||||
visited.add(parent_id)
|
||||
parent = store.get_by_tid(parent_id)
|
||||
if parent is not None:
|
||||
if _has_cycle(target_tid, parent.par, store, visited, max_depth):
|
||||
visited.add(pred_id)
|
||||
pred = store.get_by_tid(pred_id)
|
||||
if pred is not None:
|
||||
if _has_cycle(target_tid, pred.pred, store, visited, max_depth):
|
||||
return True
|
||||
return False
|
||||
|
||||
@@ -69,29 +69,28 @@ def validate_dag(
|
||||
store: ECTStore,
|
||||
cfg: DAGConfig,
|
||||
) -> None:
|
||||
"""Section 6.2: uniqueness (by jti), parent existence, temporal ordering, acyclicity, parent policy."""
|
||||
if cfg.max_par_length > 0 and len(payload.par) > cfg.max_par_length:
|
||||
raise ValueError("ect: par exceeds max length")
|
||||
"""Section 6.2: uniqueness (by jti), predecessor existence, temporal ordering, acyclicity, predecessor policy."""
|
||||
if cfg.max_pred_length > 0 and len(payload.pred) > cfg.max_pred_length:
|
||||
raise ValueError("ect: pred exceeds max length")
|
||||
if store.contains(payload.jti, payload.wid or ""):
|
||||
raise ValueError(f"ect: task ID (jti) already exists: {payload.jti}")
|
||||
from ect.types import POL_DECISION_REJECTED, POL_DECISION_PENDING_HUMAN_REVIEW
|
||||
|
||||
for parent_id in payload.par:
|
||||
parent = store.get_by_tid(parent_id)
|
||||
if parent is None:
|
||||
raise ValueError(f"ect: parent task not found: {parent_id}")
|
||||
if parent.iat >= payload.iat + cfg.clock_skew_tolerance:
|
||||
raise ValueError(f"ect: parent task not earlier than current: {parent_id}")
|
||||
for pred_id in payload.pred:
|
||||
pred = store.get_by_tid(pred_id)
|
||||
if pred is None:
|
||||
raise ValueError(f"ect: predecessor task not found: {pred_id}")
|
||||
if pred.iat >= payload.iat + cfg.clock_skew_tolerance:
|
||||
raise ValueError(f"ect: predecessor task not earlier than current: {pred_id}")
|
||||
|
||||
visited: set[str] = set()
|
||||
if _has_cycle(payload.jti, payload.par, store, visited, cfg.max_ancestor_limit):
|
||||
if _has_cycle(payload.jti, payload.pred, store, visited, cfg.max_ancestor_limit):
|
||||
raise ValueError("ect: circular dependency or depth limit exceeded")
|
||||
|
||||
# Parent policy decision: only when parent has policy claims per spec
|
||||
for parent_id in payload.par:
|
||||
parent = store.get_by_tid(parent_id)
|
||||
if parent and parent.has_policy_claims() and parent.pol_decision in (POL_DECISION_REJECTED, POL_DECISION_PENDING_HUMAN_REVIEW):
|
||||
# Predecessor policy decision: only when predecessor has policy claims in ext per -01
|
||||
for pred_id in payload.pred:
|
||||
pred = store.get_by_tid(pred_id)
|
||||
if pred and pred.has_policy_claims() and pred.pol_decision() in ("rejected", "pending_human_review"):
|
||||
if not payload.compensation_required():
|
||||
raise ValueError(
|
||||
"ect: parent has non-approved pol_decision; current ECT must be compensation/remediation or have ext.compensation_required true"
|
||||
"ect: predecessor has non-approved pol_decision; current ECT must be compensation/remediation or have ext.compensation_required true"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user