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:
2026-04-03 10:55:58 +02:00
parent ba044f6626
commit 884d2dc836
33 changed files with 416 additions and 481 deletions

View File

@@ -13,7 +13,6 @@ from ect import (
CreateOptions,
verify,
VerifyOptions,
POL_DECISION_APPROVED,
)
@@ -27,9 +26,7 @@ def test_create_roundtrip():
exp=now + 600,
jti="e4f5a6b7-c8d9-0123-ef01-234567890abc",
exec_act="review_spec",
par=[],
pol="spec_review_policy_v2",
pol_decision=POL_DECISION_APPROVED,
pred=[],
)
compact = create(payload, key, CreateOptions(key_id="agent-a-key-1"))
assert compact

View File

@@ -4,7 +4,7 @@ import time
import pytest
from ect import Payload, create, generate_key, CreateOptions, default_create_options, POL_DECISION_APPROVED
from ect import Payload, create, generate_key, CreateOptions, default_create_options
def test_default_create_options():
@@ -14,7 +14,7 @@ def test_default_create_options():
def test_create_errors():
key = generate_key()
p = Payload(iss="i", aud=["a"], iat=1, exp=2, jti="j", exec_act="e", par=[], pol="p", pol_decision=POL_DECISION_APPROVED)
p = Payload(iss="i", aud=["a"], iat=1, exp=2, jti="j", exec_act="e", pred=[])
with pytest.raises(ValueError, match="KeyID|required"):
create(p, key, CreateOptions(key_id=""))
with pytest.raises((ValueError, TypeError, AttributeError)):
@@ -26,7 +26,7 @@ def test_create_optional_pol():
now = int(time.time())
p = Payload(
iss="iss", aud=["a"], iat=now, exp=now + 3600,
jti="jti-nopol", exec_act="act", par=[],
jti="jti-nopol", exec_act="act", pred=[],
)
compact = create(p, key, CreateOptions(key_id="kid"))
assert compact
@@ -34,7 +34,7 @@ def test_create_optional_pol():
def test_create_validation_errors():
key = generate_key()
base = dict(iss="i", aud=["a"], iat=1, exp=2, jti="j", exec_act="e", par=[])
base = dict(iss="i", aud=["a"], iat=1, exp=2, jti="j", exec_act="e", pred=[])
with pytest.raises(ValueError, match="iss"):
create(Payload(**{**base, "iss": ""}), key, CreateOptions(key_id="k"))
with pytest.raises(ValueError, match="aud"):
@@ -43,16 +43,12 @@ def test_create_validation_errors():
create(Payload(**{**base, "jti": ""}), key, CreateOptions(key_id="k"))
with pytest.raises(ValueError, match="exec_act"):
create(Payload(**{**base, "exec_act": ""}), key, CreateOptions(key_id="k"))
with pytest.raises(ValueError, match="pol and pol_decision"):
create(Payload(**{**base, "pol": "p", "pol_decision": ""}), key, CreateOptions(key_id="k"))
with pytest.raises(ValueError, match="pol_decision"):
create(Payload(**{**base, "pol": "p", "pol_decision": "bad"}), key, CreateOptions(key_id="k"))
def test_create_ext_compensation_reason_requires_required():
key = generate_key()
p = Payload(
iss="i", aud=["a"], iat=1, exp=2, jti="j", exec_act="e", par=[],
iss="i", aud=["a"], iat=1, exp=2, jti="j", exec_act="e", pred=[],
ext={"compensation_reason": "rollback", "compensation_required": False},
)
with pytest.raises(ValueError, match="compensation_required"):
@@ -61,7 +57,7 @@ def test_create_ext_compensation_reason_requires_required():
def test_create_zero_expiry_uses_default():
key = generate_key()
p = Payload(iss="i", aud=["a"], iat=0, exp=0, jti="j", exec_act="e", par=[])
p = Payload(iss="i", aud=["a"], iat=0, exp=0, jti="j", exec_act="e", pred=[])
compact = create(p, key, CreateOptions(key_id="k", default_expiry_sec=300))
assert compact
# create() works on a copy; decode the token to verify defaults were applied
@@ -73,17 +69,17 @@ def test_create_zero_expiry_uses_default():
def test_create_validate_uuids_rejects_non_uuid_jti():
key = generate_key()
now = int(time.time())
p = Payload(iss="i", aud=["a"], iat=now, exp=now + 3600, jti="not-a-uuid", exec_act="e", par=[])
p = Payload(iss="i", aud=["a"], iat=now, exp=now + 3600, jti="not-a-uuid", exec_act="e", pred=[])
with pytest.raises(ValueError, match="jti must be UUID"):
create(p, key, CreateOptions(key_id="k", validate_uuids=True))
def test_create_max_par_length():
def test_create_max_pred_length():
key = generate_key()
now = int(time.time())
p = Payload(iss="i", aud=["a"], iat=now, exp=now + 3600, jti="550e8400-e29b-41d4-a716-446655440000", exec_act="e", par=["p1", "p2"])
with pytest.raises(ValueError, match="par exceeds max length"):
create(p, key, CreateOptions(key_id="k", max_par_length=1))
p = Payload(iss="i", aud=["a"], iat=now, exp=now + 3600, jti="550e8400-e29b-41d4-a716-446655440000", exec_act="e", pred=["p1", "p2"])
with pytest.raises(ValueError, match="pred exceeds max length"):
create(p, key, CreateOptions(key_id="k", max_pred_length=1))
def test_create_ext_size_rejected():
@@ -91,7 +87,7 @@ def test_create_ext_size_rejected():
key = generate_key()
now = int(time.time())
p = Payload(
iss="i", aud=["a"], iat=now, exp=now + 3600, jti="550e8400-e29b-41d4-a716-446655440000", exec_act="e", par=[],
iss="i", aud=["a"], iat=now, exp=now + 3600, jti="550e8400-e29b-41d4-a716-446655440000", exec_act="e", pred=[],
ext={"x": "y" * (EXT_MAX_SIZE - 5)},
)
with pytest.raises(ValueError, match="ext exceeds max size"):

View File

@@ -4,7 +4,7 @@ import time
import pytest
from ect import Payload, MemoryLedger, validate_dag, default_dag_config, POL_DECISION_APPROVED
from ect import Payload, MemoryLedger, validate_dag, default_dag_config
def test_validate_dag_root():
@@ -16,9 +16,7 @@ def test_validate_dag_root():
exp=0,
jti="jti-001",
exec_act="",
par=[],
pol="",
pol_decision=POL_DECISION_APPROVED,
pred=[],
wid="wf-1",
)
validate_dag(payload, store, default_dag_config())
@@ -33,9 +31,7 @@ def test_validate_dag_duplicate_jti():
exp=0,
jti="jti-001",
exec_act="a",
par=[],
pol="p",
pol_decision=POL_DECISION_APPROVED,
pred=[],
wid="wf-1",
)
store.append("dummy-jws", p)
@@ -46,16 +42,14 @@ def test_validate_dag_duplicate_jti():
exp=0,
jti="jti-001",
exec_act="",
par=[],
pol="",
pol_decision=POL_DECISION_APPROVED,
pred=[],
wid="wf-1",
)
with pytest.raises(ValueError, match="task ID.*already exists"):
validate_dag(payload, store, default_dag_config())
def test_validate_dag_parent_exists():
def test_validate_dag_pred_exists():
store = MemoryLedger()
now = int(time.time())
p = Payload(
@@ -65,9 +59,7 @@ def test_validate_dag_parent_exists():
exp=now + 600,
jti="jti-001",
exec_act="a",
par=[],
pol="p",
pol_decision=POL_DECISION_APPROVED,
pred=[],
wid="wf-1",
)
store.append("jws1", p)
@@ -78,15 +70,13 @@ def test_validate_dag_parent_exists():
exp=now + 600,
jti="jti-002",
exec_act="b",
par=["jti-001"],
pol="p",
pol_decision=POL_DECISION_APPROVED,
pred=["jti-001"],
wid="wf-1",
)
validate_dag(payload, store, default_dag_config())
def test_validate_dag_parent_not_found():
def test_validate_dag_pred_not_found():
store = MemoryLedger()
now = int(time.time())
payload = Payload(
@@ -96,26 +86,24 @@ def test_validate_dag_parent_not_found():
exp=now + 600,
jti="jti-002",
exec_act="",
par=["jti-missing"],
pol="",
pol_decision=POL_DECISION_APPROVED,
pred=["jti-missing"],
)
with pytest.raises(ValueError, match="parent task not found"):
with pytest.raises(ValueError, match="predecessor task not found"):
validate_dag(payload, store, default_dag_config())
def test_validate_dag_parent_policy_rejected_requires_compensation():
from ect import POL_DECISION_REJECTED
def test_validate_dag_pred_policy_rejected_requires_compensation():
store = MemoryLedger()
now = int(time.time())
p = Payload(
iss="x", aud=["y"], iat=now - 60, exp=now + 600,
jti="jti-rej", exec_act="a", par=[], pol="p", pol_decision=POL_DECISION_REJECTED, wid="wf-1",
jti="jti-rej", exec_act="a", pred=[], wid="wf-1",
ext={"pol": "p", "pol_decision": "rejected"},
)
store.append("jws1", p)
payload = Payload(
iss="", aud=[], iat=now, exp=now + 600,
jti="jti-child", exec_act="b", par=["jti-rej"], pol="p", pol_decision=POL_DECISION_APPROVED, wid="wf-1",
jti="jti-child", exec_act="b", pred=["jti-rej"], wid="wf-1",
)
with pytest.raises(ValueError, match="compensation"):
validate_dag(payload, store, default_dag_config())

View File

@@ -4,12 +4,12 @@ import time
import pytest
from ect import Payload, MemoryLedger, ErrTaskIDExists, POL_DECISION_APPROVED
from ect import Payload, MemoryLedger, ErrTaskIDExists
def test_ledger_append_and_get():
m = MemoryLedger()
p = Payload(iss="i", aud=["a"], iat=1, exp=2, jti="j1", exec_act="act", par=[])
p = Payload(iss="i", aud=["a"], iat=1, exp=2, jti="j1", exec_act="act", pred=[])
seq = m.append("jws1", p)
assert seq == 1
assert m.get_by_tid("j1").jti == "j1"
@@ -17,7 +17,7 @@ def test_ledger_append_and_get():
def test_ledger_err_task_id_exists():
m = MemoryLedger()
p = Payload(iss="i", aud=["a"], iat=1, exp=2, jti="j-dup", exec_act="e", par=[])
p = Payload(iss="i", aud=["a"], iat=1, exp=2, jti="j-dup", exec_act="e", pred=[])
m.append("jws1", p)
with pytest.raises(ErrTaskIDExists):
m.append("jws2", p)
@@ -25,7 +25,7 @@ def test_ledger_err_task_id_exists():
def test_ledger_contains_wid():
m = MemoryLedger()
p = Payload(iss="i", aud=["a"], iat=1, exp=2, jti="j1", exec_act="e", par=[], wid="wf1")
p = Payload(iss="i", aud=["a"], iat=1, exp=2, jti="j1", exec_act="e", pred=[], wid="wf1")
m.append("jws", p)
assert m.contains("j1", "") is True
assert m.contains("j1", "wf1") is True

View File

@@ -2,62 +2,63 @@
import pytest
from ect import Payload, POL_DECISION_APPROVED
from ect.types import valid_pol_decision
def test_valid_pol_decision():
assert valid_pol_decision("approved") is True
assert valid_pol_decision("rejected") is True
assert valid_pol_decision("pending_human_review") is True
assert valid_pol_decision("invalid") is False
from ect import Payload
def test_payload_contains_audience():
p = Payload(iss="", aud=["a", "b"], iat=0, exp=0, jti="", exec_act="", par=[])
p = Payload(iss="", aud=["a", "b"], iat=0, exp=0, jti="", exec_act="", pred=[])
assert p.contains_audience("a") is True
assert p.contains_audience("c") is False
def test_payload_compensation_required():
p = Payload(iss="", aud=[], iat=0, exp=0, jti="", exec_act="", par=[])
p = Payload(iss="", aud=[], iat=0, exp=0, jti="", exec_act="", pred=[])
assert p.compensation_required() is False
p.ext = {"compensation_required": True}
assert p.compensation_required() is True
def test_payload_has_policy_claims():
p = Payload(iss="", aud=[], iat=0, exp=0, jti="", exec_act="", par=[], pol="p", pol_decision=POL_DECISION_APPROVED)
p = Payload(iss="", aud=[], iat=0, exp=0, jti="", exec_act="", pred=[],
ext={"pol": "p", "pol_decision": "approved"})
assert p.has_policy_claims() is True
p.pol = ""
p.ext = {"pol_decision": "approved"}
assert p.has_policy_claims() is False
p.ext = None
assert p.has_policy_claims() is False
def test_payload_pol_decision():
p = Payload(iss="", aud=[], iat=0, exp=0, jti="", exec_act="", pred=[],
ext={"pol_decision": "rejected"})
assert p.pol_decision() == "rejected"
p.ext = None
assert p.pol_decision() == ""
def test_payload_to_claims_optional():
p = Payload(iss="i", aud=["a"], iat=1, exp=2, jti="j", exec_act="e", par=[], wid="wf")
p = Payload(iss="i", aud=["a"], iat=1, exp=2, jti="j", exec_act="e", pred=[], wid="wf")
claims = p.to_claims()
assert claims["wid"] == "wf"
assert "ext" not in claims or not claims.get("ext")
assert "ect_ext" not in claims or not claims.get("ect_ext")
def test_payload_from_claims_aud_string():
claims = {"iss": "i", "aud": "single", "iat": 1, "exp": 2, "jti": "j", "exec_act": "e", "par": []}
claims = {"iss": "i", "aud": "single", "iat": 1, "exp": 2, "jti": "j", "exec_act": "e", "pred": []}
p = Payload.from_claims(claims)
assert p.aud == ["single"]
def test_payload_to_claims_all_optional():
p = Payload(
iss="i", aud=["a"], iat=1, exp=2, jti="j", exec_act="e", par=[],
sub="s", wid="w", pol="p", pol_decision="approved", pol_enforcer="e",
pol_timestamp=1, inp_hash="h", out_hash="o", inp_classification="c",
iss="i", aud=["a"], iat=1, exp=2, jti="j", exec_act="e", pred=[],
wid="w", inp_hash="h", out_hash="o", inp_classification="c",
ext={"pol": "p", "pol_decision": "approved"},
)
claims = p.to_claims()
assert claims["sub"] == "s"
assert claims["wid"] == "w"
assert claims["pol"] == "p"
assert claims["pol_enforcer"] == "e"
assert claims["pol_timestamp"] == 1
assert claims["inp_hash"] == "h"
assert claims["out_hash"] == "o"
assert claims["inp_classification"] == "c"
assert claims["ect_ext"]["pol"] == "p"
assert claims["ect_ext"]["pol_decision"] == "approved"

View File

@@ -13,7 +13,6 @@ from ect import (
verify,
VerifyOptions,
default_verify_options,
POL_DECISION_APPROVED,
)
@@ -22,7 +21,7 @@ def test_parse():
now = int(time.time())
p = Payload(
iss="iss", aud=["a"], iat=now, exp=now + 3600,
jti="jti-parse", exec_act="act", par=[], pol="p", pol_decision=POL_DECISION_APPROVED,
jti="jti-parse", exec_act="act", pred=[],
)
compact = create(p, key, CreateOptions(key_id="kid"))
parsed = parse(compact)
@@ -41,7 +40,7 @@ def test_verify_expired():
now = int(time.time())
p = Payload(
iss="iss", aud=["v"], iat=now - 3600, exp=now - 60,
jti="jti-exp", exec_act="act", par=[], pol="p", pol_decision=POL_DECISION_APPROVED,
jti="jti-exp", exec_act="act", pred=[],
)
compact = create(p, key, CreateOptions(key_id="kid"))
resolver = lambda kid: key.public_key() if kid == "kid" else None
@@ -54,7 +53,7 @@ def test_verify_replay():
now = int(time.time())
p = Payload(
iss="iss", aud=["v"], iat=now, exp=now + 3600,
jti="jti-replay", exec_act="act", par=[], pol="p", pol_decision=POL_DECISION_APPROVED,
jti="jti-replay", exec_act="act", pred=[],
)
compact = create(p, key, CreateOptions(key_id="kid"))
resolver = lambda kid: key.public_key() if kid == "kid" else None
@@ -76,7 +75,7 @@ def test_verify_audience_mismatch():
now = int(time.time())
p = Payload(
iss="iss", aud=["other"], iat=now, exp=now + 3600,
jti="jti-a", exec_act="act", par=[], pol="p", pol_decision=POL_DECISION_APPROVED,
jti="jti-a", exec_act="act", pred=[],
)
compact = create(p, key, CreateOptions(key_id="kid"))
resolver = lambda kid: key.public_key() if kid == "kid" else None
@@ -89,7 +88,7 @@ def test_verify_wit_subject_mismatch():
now = int(time.time())
p = Payload(
iss="wrong-iss", aud=["v"], iat=now, exp=now + 3600,
jti="jti-w", exec_act="act", par=[], pol="p", pol_decision=POL_DECISION_APPROVED,
jti="jti-w", exec_act="act", pred=[],
)
compact = create(p, key, CreateOptions(key_id="kid"))
resolver = lambda kid: key.public_key() if kid == "kid" else None
@@ -104,7 +103,7 @@ def test_verify_iat_too_old():
now = int(time.time())
p = Payload(
iss="iss", aud=["v"], iat=now - 2000, exp=now + 3600,
jti="jti-old", exec_act="act", par=[], pol="p", pol_decision=POL_DECISION_APPROVED,
jti="jti-old", exec_act="act", pred=[],
)
compact = create(p, key, CreateOptions(key_id="kid"))
resolver = lambda kid: key.public_key() if kid == "kid" else None
@@ -119,7 +118,7 @@ def test_verify_unknown_key():
now = int(time.time())
p = Payload(
iss="iss", aud=["v"], iat=now, exp=now + 3600,
jti="jti-k", exec_act="act", par=[], pol="p", pol_decision=POL_DECISION_APPROVED,
jti="jti-k", exec_act="act", pred=[],
)
compact = create(p, key, CreateOptions(key_id="kid"))
resolver = lambda kid: None # unknown key
@@ -132,7 +131,7 @@ def test_verify_resolve_key_required():
now = int(time.time())
p = Payload(
iss="iss", aud=["v"], iat=now, exp=now + 3600,
jti="jti-r", exec_act="act", par=[], pol="p", pol_decision=POL_DECISION_APPROVED,
jti="jti-r", exec_act="act", pred=[],
)
compact = create(p, key, CreateOptions(key_id="kid"))
with pytest.raises(ValueError, match="ResolveKey"):
@@ -146,7 +145,7 @@ def test_verify_with_dag():
now = int(time.time())
root = Payload(
iss="iss", aud=["v"], iat=now, exp=now + 3600,
jti="jti-root", exec_act="act", par=[], pol="p", pol_decision=POL_DECISION_APPROVED,
jti="jti-root", exec_act="act", pred=[],
)
compact_root = create(root, key, CreateOptions(key_id="kid"))
resolver = lambda kid: key.public_key() if kid == "kid" else None
@@ -155,7 +154,7 @@ def test_verify_with_dag():
ledger.append(compact_root, parsed.payload)
child = Payload(
iss="iss", aud=["v"], iat=now + 1, exp=now + 3600,
jti="jti-child", exec_act="act2", par=["jti-root"], pol="p", pol_decision=POL_DECISION_APPROVED,
jti="jti-child", exec_act="act2", pred=["jti-root"],
)
compact_child = create(child, key, CreateOptions(key_id="kid"))
parsed2 = verify(compact_child, opts)
@@ -166,7 +165,7 @@ def test_on_verify_attempt_callback():
"""Observability: on_verify_attempt is called with jti and error (or None)."""
key = generate_key()
now = int(time.time())
p = Payload(iss="i", aud=["v"], iat=now, exp=now + 3600, jti="jti-obs", exec_act="a", par=[])
p = Payload(iss="i", aud=["v"], iat=now, exp=now + 3600, jti="jti-obs", exec_act="a", pred=[])
compact = create(p, key, CreateOptions(key_id="kid"))
resolver = lambda k: key.public_key() if k == "kid" else None
seen = []
@@ -183,7 +182,7 @@ def test_on_verify_attempt_callback():
def test_on_verify_attempt_called_on_failure():
key = generate_key()
now = int(time.time())
p = Payload(iss="i", aud=["v"], iat=now, exp=now - 1, jti="jti-fail", exec_act="a", par=[])
p = Payload(iss="i", aud=["v"], iat=now, exp=now - 1, jti="jti-fail", exec_act="a", pred=[])
compact = create(p, key, CreateOptions(key_id="kid"))
resolver = lambda k: key.public_key() if k == "kid" else None
seen = []
@@ -193,5 +192,3 @@ def test_on_verify_attempt_called_on_failure():
assert len(seen) == 1
assert seen[0][0] == "jti-fail"
assert seen[0][1] is not None