feat: add draft data, gap analysis report, and workspace config
Some checks failed
CI / test (3.11) (push) Failing after 1m37s
CI / test (3.12) (push) Failing after 57s

This commit is contained in:
2026-04-06 18:47:15 +02:00
parent 4f310407b0
commit 2506b6325a
189 changed files with 62649 additions and 0 deletions

View File

@@ -0,0 +1,145 @@
"""Tests for act.crypto module."""
import pytest
from act.crypto import (
ACTKeyResolver,
KeyRegistry,
X509TrustStore,
b64url_sha256,
compute_sha256,
did_key_from_ed25519,
generate_ed25519_keypair,
generate_p256_keypair,
resolve_did_key,
sign,
verify,
)
from act.errors import ACTKeyResolutionError, ACTSignatureError
class TestEd25519:
def test_generate_keypair(self):
priv, pub = generate_ed25519_keypair()
assert priv is not None
assert pub is not None
def test_sign_verify(self):
priv, pub = generate_ed25519_keypair()
data = b"test data"
sig = sign(priv, data)
verify(pub, sig, data)
def test_verify_wrong_data(self):
priv, pub = generate_ed25519_keypair()
sig = sign(priv, b"correct data")
with pytest.raises(ACTSignatureError):
verify(pub, sig, b"wrong data")
def test_verify_wrong_key(self):
priv1, pub1 = generate_ed25519_keypair()
_, pub2 = generate_ed25519_keypair()
sig = sign(priv1, b"data")
with pytest.raises(ACTSignatureError):
verify(pub2, sig, b"data")
class TestP256:
def test_generate_keypair(self):
priv, pub = generate_p256_keypair()
assert priv is not None
assert pub is not None
def test_sign_verify(self):
priv, pub = generate_p256_keypair()
data = b"test data for p256"
sig = sign(priv, data)
assert len(sig) == 64 # r||s, 32 bytes each
verify(pub, sig, data)
def test_verify_wrong_data(self):
priv, pub = generate_p256_keypair()
sig = sign(priv, b"correct")
with pytest.raises(ACTSignatureError):
verify(pub, sig, b"wrong")
class TestSHA256:
def test_compute(self):
h = compute_sha256(b"hello")
assert len(h) == 32
def test_b64url(self):
result = b64url_sha256(b"hello world")
assert "=" not in result
assert isinstance(result, str)
class TestKeyRegistry:
def test_register_and_get(self):
reg = KeyRegistry()
_, pub = generate_ed25519_keypair()
reg.register("key-1", pub)
assert reg.get("key-1") is pub
assert "key-1" in reg
assert len(reg) == 1
def test_missing_key(self):
reg = KeyRegistry()
assert reg.get("missing") is None
assert "missing" not in reg
class TestDIDKey:
def test_ed25519_roundtrip(self):
_, pub = generate_ed25519_keypair()
did = did_key_from_ed25519(pub)
assert did.startswith("did:key:z6Mk")
resolved = resolve_did_key(did)
# Verify same key by signing/verifying
from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat
original_bytes = pub.public_bytes(Encoding.Raw, PublicFormat.Raw)
resolved_bytes = resolved.public_bytes(Encoding.Raw, PublicFormat.Raw)
assert original_bytes == resolved_bytes
def test_invalid_prefix(self):
with pytest.raises(ACTKeyResolutionError):
resolve_did_key("did:web:example.com")
def test_with_fragment(self):
_, pub = generate_ed25519_keypair()
did = did_key_from_ed25519(pub)
did_with_fragment = f"{did}#{did.split(':')[2]}"
resolved = resolve_did_key(did_with_fragment)
assert resolved is not None
class TestACTKeyResolver:
def test_tier1_resolution(self):
reg = KeyRegistry()
_, pub = generate_ed25519_keypair()
reg.register("my-key", pub)
resolver = ACTKeyResolver(registry=reg)
assert resolver.resolve("my-key") is pub
def test_tier3_did_key(self):
_, pub = generate_ed25519_keypair()
did = did_key_from_ed25519(pub)
resolver = ACTKeyResolver()
resolved = resolver.resolve(did)
assert resolved is not None
def test_unresolvable(self):
resolver = ACTKeyResolver()
with pytest.raises(ACTKeyResolutionError):
resolver.resolve("unknown-kid")
def test_did_web_resolver_callback(self):
_, pub = generate_ed25519_keypair()
def resolver_cb(did: str):
if did == "did:web:example.com":
return pub
return None
resolver = ACTKeyResolver(did_web_resolver=resolver_cb)
result = resolver.resolve("did:web:example.com")
assert result is pub

View File

@@ -0,0 +1,103 @@
"""Tests for act.dag module."""
import time
import pytest
from act.dag import validate_dag
from act.errors import ACTCapabilityError, ACTDAGError
from act.ledger import ACTLedger
from act.token import ACTRecord, Capability, TaskClaim
def make_record(jti, par=None, exec_act="do.thing", exec_ts=None, cap=None):
"""Helper to create a minimal ACTRecord."""
return ACTRecord(
alg="EdDSA", kid="k", iss="a", sub="b", aud="b",
iat=1772064000, exp=1772064900,
jti=jti,
task=TaskClaim(purpose="t"),
cap=cap or [Capability(action="do.thing")],
exec_act=exec_act,
par=par or [],
exec_ts=exec_ts or 1772064100,
status="completed",
)
class TestDAGValidation:
def test_root_task(self):
ledger = ACTLedger()
r = make_record("root-1")
validate_dag(r, ledger)
def test_child_with_parent(self):
ledger = ACTLedger()
parent = make_record("parent-1", exec_ts=1772064050)
ledger.append(parent)
child = make_record("child-1", par=["parent-1"], exec_ts=1772064100)
validate_dag(child, ledger)
def test_fan_in(self):
ledger = ACTLedger()
p1 = make_record("p1", exec_ts=1772064050)
p2 = make_record("p2", exec_ts=1772064060)
ledger.append(p1)
ledger.append(p2)
child = make_record("child", par=["p1", "p2"], exec_ts=1772064100)
validate_dag(child, ledger)
def test_duplicate_jti(self):
ledger = ACTLedger()
r = make_record("dup-1")
ledger.append(r)
r2 = make_record("dup-1")
with pytest.raises(ACTDAGError, match="Duplicate"):
validate_dag(r2, ledger)
def test_missing_parent(self):
ledger = ACTLedger()
r = make_record("orphan", par=["nonexistent"])
with pytest.raises(ACTDAGError, match="not found"):
validate_dag(r, ledger)
def test_self_cycle(self):
ledger = ACTLedger()
r = make_record("cycle", par=["cycle"])
with pytest.raises(ACTDAGError, match="cycle"):
validate_dag(r, ledger)
def test_indirect_cycle(self):
ledger = ACTLedger()
# a -> b -> a would be a cycle
a = make_record("a", par=["b"], exec_ts=1772064100)
b = make_record("b", par=["a"], exec_ts=1772064100)
ledger.append(b)
# When validating a, following par leads to b,
# which has par=["a"] — cycle!
with pytest.raises(ACTDAGError, match="cycle"):
validate_dag(a, ledger)
def test_temporal_ordering_violation(self):
ledger = ACTLedger()
parent = make_record("parent", exec_ts=1772064200)
ledger.append(parent)
# Child's exec_ts is way before parent
child = make_record("child", par=["parent"], exec_ts=1772064100)
with pytest.raises(ACTDAGError, match="Temporal"):
validate_dag(child, ledger)
def test_temporal_within_tolerance(self):
ledger = ACTLedger()
parent = make_record("parent", exec_ts=1772064120)
ledger.append(parent)
# Child exec_ts is slightly before parent but within 30s tolerance
child = make_record("child", par=["parent"], exec_ts=1772064100)
validate_dag(child, ledger)
def test_bad_exec_act(self):
ledger = ACTLedger()
r = make_record("bad", exec_act="not.authorized",
cap=[Capability(action="do.thing")])
with pytest.raises(ACTCapabilityError):
validate_dag(r, ledger)

View File

@@ -0,0 +1,229 @@
"""Tests for act.delegation module."""
import time
import uuid
import pytest
from act.crypto import generate_ed25519_keypair, sign, verify, compute_sha256
from act.delegation import (
create_delegated_mandate,
verify_capability_subset,
verify_delegation_chain,
)
from act.errors import (
ACTDelegationError,
ACTPrivilegeEscalationError,
)
from act.token import (
ACTMandate,
Capability,
Delegation,
DelegationEntry,
TaskClaim,
_b64url_decode,
encode_jws,
)
@pytest.fixture
def parent_setup():
iss_priv, iss_pub = generate_ed25519_keypair()
mandate = ACTMandate(
alg="EdDSA", kid="iss-key",
iss="agent-a", sub="agent-b", aud="agent-b",
iat=1772064000, exp=1772064900,
jti="parent-jti-1",
task=TaskClaim(purpose="parent_task"),
cap=[
Capability(action="read.data", constraints={"max_records": 10}),
Capability(action="write.result"),
],
delegation=Delegation(depth=0, max_depth=3, chain=[]),
)
sig = sign(iss_priv, mandate.signing_input())
compact = encode_jws(mandate, sig)
return mandate, compact, iss_priv, iss_pub
class TestCreateDelegatedMandate:
def test_basic_delegation(self, parent_setup):
mandate, compact, priv, _ = parent_setup
delegated, _ = create_delegated_mandate(
parent_mandate=mandate, parent_compact=compact,
delegator_private_key=priv,
sub="agent-c", kid="key-b", iss="agent-a", aud="agent-c",
iat=1772064010, exp=1772064600,
jti="child-jti-1",
cap=[Capability(action="read.data", constraints={"max_records": 5})],
task=TaskClaim(purpose="child_task"),
)
assert delegated.delegation.depth == 1
assert len(delegated.delegation.chain) == 1
assert delegated.delegation.chain[0].delegator == "agent-a"
def test_depth_exceeded(self, parent_setup):
mandate, compact, priv, _ = parent_setup
# Set parent to max depth
mandate.delegation = Delegation(depth=3, max_depth=3, chain=[
DelegationEntry(delegator="x", jti="j", sig="s")
for _ in range(3)
])
with pytest.raises(ACTDelegationError, match="exceeds max_depth"):
create_delegated_mandate(
parent_mandate=mandate, parent_compact=compact,
delegator_private_key=priv,
sub="c", kid="k", iss="a", aud="c",
iat=1, exp=2, jti="j",
cap=[Capability(action="read.data", constraints={"max_records": 5})],
task=TaskClaim(purpose="t"),
)
def test_no_del_claim(self):
priv, _ = generate_ed25519_keypair()
mandate = ACTMandate(
alg="EdDSA", kid="k", iss="a", sub="b", aud="b",
iat=1, exp=2,
task=TaskClaim(purpose="t"),
cap=[Capability(action="x.y")],
delegation=None, # no del claim
)
with pytest.raises(ACTDelegationError, match="not permitted"):
create_delegated_mandate(
parent_mandate=mandate, parent_compact="compact",
delegator_private_key=priv,
sub="c", kid="k", iss="a", aud="c",
iat=1, exp=2, jti="j",
cap=[Capability(action="x.y")],
task=TaskClaim(purpose="t"),
)
def test_max_depth_reduction(self, parent_setup):
mandate, compact, priv, _ = parent_setup
delegated, _ = create_delegated_mandate(
parent_mandate=mandate, parent_compact=compact,
delegator_private_key=priv,
sub="c", kid="k", iss="a", aud="c",
iat=1, exp=2, jti="j",
cap=[Capability(action="read.data", constraints={"max_records": 5})],
task=TaskClaim(purpose="t"),
max_depth=2,
)
assert delegated.delegation.max_depth == 2
def test_max_depth_escalation(self, parent_setup):
mandate, compact, priv, _ = parent_setup
with pytest.raises(ACTDelegationError, match="exceeds parent max_depth"):
create_delegated_mandate(
parent_mandate=mandate, parent_compact=compact,
delegator_private_key=priv,
sub="c", kid="k", iss="a", aud="c",
iat=1, exp=2, jti="j",
cap=[Capability(action="read.data", constraints={"max_records": 5})],
task=TaskClaim(purpose="t"),
max_depth=10,
)
class TestCapabilitySubset:
def test_valid_subset(self):
parent = [Capability(action="read.data", constraints={"max_records": 10})]
child = [Capability(action="read.data", constraints={"max_records": 5})]
verify_capability_subset(parent, child)
def test_extra_action(self):
parent = [Capability(action="read.data")]
child = [Capability(action="delete.data")]
with pytest.raises(ACTPrivilegeEscalationError):
verify_capability_subset(parent, child)
def test_numeric_escalation(self):
parent = [Capability(action="read.data", constraints={"max_records": 10})]
child = [Capability(action="read.data", constraints={"max_records": 100})]
with pytest.raises(ACTPrivilegeEscalationError):
verify_capability_subset(parent, child)
def test_sensitivity_escalation(self):
parent = [Capability(action="read.data",
constraints={"data_sensitivity": "confidential"})]
child = [Capability(action="read.data",
constraints={"data_sensitivity": "internal"})]
with pytest.raises(ACTPrivilegeEscalationError):
verify_capability_subset(parent, child)
def test_sensitivity_more_restrictive(self):
parent = [Capability(action="read.data",
constraints={"data_sensitivity": "internal"})]
child = [Capability(action="read.data",
constraints={"data_sensitivity": "restricted"})]
verify_capability_subset(parent, child) # should pass
def test_missing_constraint(self):
parent = [Capability(action="read.data",
constraints={"max_records": 10, "scope": "local"})]
child = [Capability(action="read.data",
constraints={"max_records": 5})]
with pytest.raises(ACTPrivilegeEscalationError, match="missing"):
verify_capability_subset(parent, child)
def test_domain_specific_identical(self):
parent = [Capability(action="read.data",
constraints={"custom": "value_a"})]
child = [Capability(action="read.data",
constraints={"custom": "value_a"})]
verify_capability_subset(parent, child)
def test_domain_specific_different(self):
parent = [Capability(action="read.data",
constraints={"custom": "value_a"})]
child = [Capability(action="read.data",
constraints={"custom": "value_b"})]
with pytest.raises(ACTPrivilegeEscalationError, match="identical"):
verify_capability_subset(parent, child)
class TestVerifyDelegationChain:
def test_chain_sig_verification(self, parent_setup):
mandate, compact, priv, pub = parent_setup
delegated, _ = create_delegated_mandate(
parent_mandate=mandate, parent_compact=compact,
delegator_private_key=priv,
sub="c", kid="k", iss="agent-a", aud="c",
iat=1, exp=2, jti="j",
cap=[Capability(action="read.data", constraints={"max_records": 5})],
task=TaskClaim(purpose="t"),
)
# Verify the chain
def resolve_key(delegator_id):
return pub
def resolve_compact(jti):
if jti == "parent-jti-1":
return compact
return None
verify_delegation_chain(delegated, resolve_key, resolve_compact)
def test_no_delegation(self):
mandate = ACTMandate(
alg="EdDSA", kid="k", iss="a", sub="b", aud="b",
iat=1, exp=2,
task=TaskClaim(purpose="t"),
cap=[Capability(action="x.y")],
)
verify_delegation_chain(mandate, lambda x: None) # no-op
def test_depth_exceeds_max(self):
mandate = ACTMandate(
alg="EdDSA", kid="k", iss="a", sub="b", aud="b",
iat=1, exp=2,
task=TaskClaim(purpose="t"),
cap=[Capability(action="x.y")],
delegation=Delegation(depth=5, max_depth=3, chain=[
DelegationEntry(delegator="x", jti="j", sig="s")
for _ in range(5)
]),
)
with pytest.raises(ACTDelegationError, match="exceeds"):
verify_delegation_chain(mandate, lambda x: None)

View File

@@ -0,0 +1,84 @@
"""Tests for act.ledger module."""
import pytest
from act.errors import ACTLedgerImmutabilityError
from act.ledger import ACTLedger
from act.token import ACTRecord, Capability, TaskClaim
def make_record(jti, wid=None):
return ACTRecord(
alg="EdDSA", kid="k", iss="a", sub="b", aud="b",
iat=1772064000, exp=1772064900,
jti=jti, wid=wid,
task=TaskClaim(purpose="t"),
cap=[Capability(action="do.thing")],
exec_act="do.thing", par=[], exec_ts=1772064100,
status="completed",
)
class TestACTLedger:
def test_append_and_get(self):
ledger = ACTLedger()
r = make_record("jti-1")
seq = ledger.append(r)
assert seq == 0
assert ledger.get("jti-1") is r
def test_sequential_ordering(self):
ledger = ACTLedger()
for i in range(5):
seq = ledger.append(make_record(f"jti-{i}"))
assert seq == i
def test_duplicate_rejected(self):
ledger = ACTLedger()
ledger.append(make_record("jti-1"))
with pytest.raises(ACTLedgerImmutabilityError):
ledger.append(make_record("jti-1"))
def test_get_missing(self):
ledger = ACTLedger()
assert ledger.get("missing") is None
def test_list_all(self):
ledger = ACTLedger()
ledger.append(make_record("a"))
ledger.append(make_record("b"))
records = ledger.list()
assert len(records) == 2
def test_list_by_wid(self):
ledger = ACTLedger()
ledger.append(make_record("a", wid="w1"))
ledger.append(make_record("b", wid="w2"))
ledger.append(make_record("c", wid="w1"))
assert len(ledger.list("w1")) == 2
assert len(ledger.list("w2")) == 1
assert len(ledger.list("w3")) == 0
def test_verify_integrity_empty(self):
ledger = ACTLedger()
assert ledger.verify_integrity() is True
def test_verify_integrity_with_records(self):
ledger = ACTLedger()
for i in range(10):
ledger.append(make_record(f"jti-{i}"))
assert ledger.verify_integrity() is True
def test_verify_integrity_tampered(self):
ledger = ACTLedger()
ledger.append(make_record("jti-1"))
ledger.append(make_record("jti-2"))
# Tamper with chain hash
ledger._chain_hashes[0] = b"\x00" * 32
assert ledger.verify_integrity() is False
def test_len(self):
ledger = ACTLedger()
assert len(ledger) == 0
ledger.append(make_record("a"))
assert len(ledger) == 1

View File

@@ -0,0 +1,103 @@
"""Tests for act.lifecycle module."""
import time
import uuid
import pytest
from act.crypto import generate_ed25519_keypair, sign
from act.errors import ACTCapabilityError, ACTPhaseError
from act.lifecycle import transition_to_record
from act.token import (
ACTMandate,
ACTRecord,
Capability,
Delegation,
ErrorClaim,
TaskClaim,
decode_jws,
encode_jws,
)
from act.crypto import verify
@pytest.fixture
def keys():
iss_priv, iss_pub = generate_ed25519_keypair()
sub_priv, sub_pub = generate_ed25519_keypair()
return iss_priv, iss_pub, sub_priv, sub_pub
@pytest.fixture
def mandate(keys):
iss_priv, _, _, _ = keys
m = ACTMandate(
alg="EdDSA", kid="iss-key",
iss="agent-a", sub="agent-b", aud="agent-b",
iat=1772064000, exp=1772064900,
jti=str(uuid.uuid4()),
task=TaskClaim(purpose="test"),
cap=[Capability(action="read.data"), Capability(action="write.result")],
delegation=Delegation(depth=0, max_depth=2, chain=[]),
)
return m
class TestTransitionToRecord:
def test_basic_transition(self, mandate, keys):
_, _, sub_priv, sub_pub = keys
record, compact = transition_to_record(
mandate, sub_kid="sub-key", sub_private_key=sub_priv,
exec_act="read.data", par=[], status="completed",
)
assert isinstance(record, ACTRecord)
assert record.exec_act == "read.data"
assert record.kid == "sub-key"
assert record.iss == mandate.iss # preserved
# Verify signature
_, _, sig, si = decode_jws(compact)
verify(sub_pub, sig, si)
def test_with_hashes(self, mandate, keys):
_, _, sub_priv, _ = keys
record, _ = transition_to_record(
mandate, sub_kid="k", sub_private_key=sub_priv,
exec_act="write.result", par=[], status="completed",
inp_hash="abc", out_hash="def",
)
assert record.inp_hash == "abc"
assert record.out_hash == "def"
def test_with_error(self, mandate, keys):
_, _, sub_priv, _ = keys
record, _ = transition_to_record(
mandate, sub_kid="k", sub_private_key=sub_priv,
exec_act="read.data", par=[], status="failed",
err=ErrorClaim(code="timeout", detail="request timed out"),
)
assert record.status == "failed"
assert record.err is not None
assert record.err.code == "timeout"
def test_rejects_bad_exec_act(self, mandate, keys):
_, _, sub_priv, _ = keys
with pytest.raises(ACTCapabilityError):
transition_to_record(
mandate, sub_kid="k", sub_private_key=sub_priv,
exec_act="delete.everything", par=[],
)
def test_preserves_phase1_claims(self, mandate, keys):
_, _, sub_priv, _ = keys
record, _ = transition_to_record(
mandate, sub_kid="k", sub_private_key=sub_priv,
exec_act="read.data", par=[], status="completed",
)
assert record.iss == mandate.iss
assert record.sub == mandate.sub
assert record.aud == mandate.aud
assert record.iat == mandate.iat
assert record.exp == mandate.exp
assert record.jti == mandate.jti
assert record.task == mandate.task
assert record.cap == mandate.cap

View File

@@ -0,0 +1,244 @@
"""Tests for act.token module."""
import json
import time
import uuid
import pytest
from act.token import (
ACTMandate,
ACTRecord,
Capability,
Delegation,
DelegationEntry,
ErrorClaim,
Oversight,
TaskClaim,
_b64url_decode,
_b64url_encode,
decode_jws,
encode_jws,
parse_token,
validate_action_name,
)
from act.errors import ACTPhaseError, ACTValidationError
@pytest.fixture
def base_time():
return 1772064000
@pytest.fixture
def mandate(base_time):
return ACTMandate(
alg="EdDSA",
kid="test-key",
iss="agent-a",
sub="agent-b",
aud="agent-b",
iat=base_time,
exp=base_time + 900,
jti=str(uuid.uuid4()),
task=TaskClaim(purpose="test_task"),
cap=[Capability(action="read.data")],
)
class TestBase64url:
def test_roundtrip(self):
data = b"hello world"
assert _b64url_decode(_b64url_encode(data)) == data
def test_no_padding(self):
encoded = _b64url_encode(b"test")
assert "=" not in encoded
class TestActionNameValidation:
def test_valid_simple(self):
validate_action_name("read")
def test_valid_dotted(self):
validate_action_name("read.data")
def test_valid_with_hyphens(self):
validate_action_name("read-write.data_item")
def test_invalid_starts_with_digit(self):
with pytest.raises(ACTValidationError):
validate_action_name("1read")
def test_invalid_empty(self):
with pytest.raises(ACTValidationError):
validate_action_name("")
def test_invalid_double_dot(self):
with pytest.raises(ACTValidationError):
validate_action_name("read..data")
class TestTaskClaim:
def test_roundtrip(self):
t = TaskClaim(purpose="test", data_sensitivity="restricted")
d = t.to_dict()
t2 = TaskClaim.from_dict(d)
assert t == t2
def test_missing_purpose(self):
with pytest.raises(ACTValidationError):
TaskClaim.from_dict({})
class TestCapability:
def test_roundtrip(self):
c = Capability(action="read.data", constraints={"max": 10})
d = c.to_dict()
c2 = Capability.from_dict(d)
assert c == c2
def test_validates_action(self):
with pytest.raises(ACTValidationError):
Capability(action="")
class TestDelegation:
def test_roundtrip(self):
d = Delegation(
depth=1,
max_depth=3,
chain=[DelegationEntry(delegator="a", jti="j1", sig="sig1")],
)
as_dict = d.to_dict()
d2 = Delegation.from_dict(as_dict)
assert d.depth == d2.depth
assert len(d2.chain) == 1
class TestACTMandate:
def test_validate_success(self, mandate):
mandate.validate()
def test_validate_missing_iss(self, base_time):
m = ACTMandate(
alg="EdDSA", kid="k", iss="", sub="b", aud="b",
iat=base_time, exp=base_time + 900,
task=TaskClaim(purpose="t"), cap=[Capability(action="x.y")],
)
with pytest.raises(ACTValidationError, match="iss"):
m.validate()
def test_validate_forbidden_alg(self, base_time):
m = ACTMandate(
alg="HS256", kid="k", iss="a", sub="b", aud="b",
iat=base_time, exp=base_time + 900,
task=TaskClaim(purpose="t"), cap=[Capability(action="x.y")],
)
with pytest.raises(ACTValidationError):
m.validate()
def test_validate_alg_none(self, base_time):
m = ACTMandate(
alg="none", kid="k", iss="a", sub="b", aud="b",
iat=base_time, exp=base_time + 900,
task=TaskClaim(purpose="t"), cap=[Capability(action="x.y")],
)
with pytest.raises(ACTValidationError):
m.validate()
def test_to_claims_includes_optional(self, base_time):
m = ACTMandate(
alg="EdDSA", kid="k", iss="a", sub="b", aud="b",
iat=base_time, exp=base_time + 900,
task=TaskClaim(purpose="t"), cap=[Capability(action="x.y")],
wid="w-1",
oversight=Oversight(requires_approval_for=["x.y"]),
)
claims = m.to_claims()
assert claims["wid"] == "w-1"
assert "oversight" in claims
def test_is_phase2(self, mandate):
assert mandate.is_phase2() is False
def test_from_claims_rejects_phase2(self):
with pytest.raises(ACTPhaseError):
ACTMandate.from_claims(
{"alg": "EdDSA", "typ": "act+jwt", "kid": "k"},
{"exec_act": "x", "iss": "a", "sub": "b", "aud": "b",
"iat": 1, "exp": 2, "jti": "j",
"task": {"purpose": "t"}, "cap": [{"action": "x"}]},
)
class TestACTRecord:
def test_from_mandate(self, mandate):
r = ACTRecord.from_mandate(
mandate, kid="sub-key", exec_act="read.data",
par=[], status="completed",
)
assert r.iss == mandate.iss
assert r.exec_act == "read.data"
assert r.kid == "sub-key"
def test_validate_bad_status(self, mandate):
r = ACTRecord.from_mandate(
mandate, kid="k", exec_act="read.data",
par=[], exec_ts=mandate.iat + 100, status="invalid",
)
with pytest.raises(ACTValidationError, match="status"):
r.validate()
def test_is_phase2(self, mandate):
r = ACTRecord.from_mandate(
mandate, kid="k", exec_act="read.data",
par=[], status="completed",
)
assert r.is_phase2() is True
def test_from_claims_rejects_phase1(self):
with pytest.raises(ACTPhaseError):
ACTRecord.from_claims(
{"alg": "EdDSA", "typ": "act+jwt", "kid": "k"},
{"iss": "a", "sub": "b", "aud": "b",
"iat": 1, "exp": 2, "jti": "j",
"task": {"purpose": "t"}, "cap": [{"action": "x"}]},
)
class TestJWSSerialization:
def test_decode_invalid_parts(self):
with pytest.raises(ACTValidationError):
decode_jws("only.two")
def test_decode_invalid_header(self):
with pytest.raises(ACTValidationError):
decode_jws("!!!.cGF5bG9hZA.c2ln")
def test_decode_wrong_typ(self):
header = _b64url_encode(json.dumps({"alg": "EdDSA", "typ": "jwt", "kid": "k"}).encode())
payload = _b64url_encode(json.dumps({"iss": "a"}).encode())
sig = _b64url_encode(b"sig")
with pytest.raises(ACTValidationError, match="typ"):
decode_jws(f"{header}.{payload}.{sig}")
def test_parse_token_phase1(self, mandate):
from act.crypto import generate_ed25519_keypair, sign
priv, pub = generate_ed25519_keypair()
sig = sign(priv, mandate.signing_input())
compact = encode_jws(mandate, sig)
parsed = parse_token(compact)
assert isinstance(parsed, ACTMandate)
def test_parse_token_phase2(self, mandate):
from act.crypto import generate_ed25519_keypair, sign
priv, pub = generate_ed25519_keypair()
record = ACTRecord.from_mandate(
mandate, kid="k", exec_act="read.data",
par=[], status="completed",
)
sig = sign(priv, record.signing_input())
compact = encode_jws(record, sig)
parsed = parse_token(compact)
assert isinstance(parsed, ACTRecord)

View File

@@ -0,0 +1,35 @@
"""Tests for act.vectors module — Appendix B test vectors."""
import pytest
from act.vectors import generate_vectors, validate_vectors
class TestVectorGeneration:
def test_generates_15_vectors(self):
vectors, ctx = generate_vectors()
assert len(vectors) == 15
def test_vector_ids(self):
vectors, _ = generate_vectors()
ids = [v.id for v in vectors]
expected = [f"B.{i}" for i in range(1, 16)]
assert ids == expected
def test_valid_vectors_have_compact(self):
vectors, _ = generate_vectors()
for v in vectors:
if v.valid and v.id != "B.7":
assert v.compact, f"{v.id} should have compact"
def test_invalid_vectors_have_exception(self):
vectors, _ = generate_vectors()
for v in vectors:
if not v.valid:
assert v.expected_exception is not None, \
f"{v.id} should have expected_exception"
class TestVectorValidation:
def test_all_vectors_pass(self):
assert validate_vectors() is True

View File

@@ -0,0 +1,191 @@
"""Tests for act.verify module."""
import time
import uuid
import pytest
from act.crypto import (
ACTKeyResolver,
KeyRegistry,
generate_ed25519_keypair,
sign,
)
from act.errors import (
ACTAudienceMismatchError,
ACTCapabilityError,
ACTExpiredError,
ACTPhaseError,
ACTSignatureError,
ACTValidationError,
)
from act.ledger import ACTLedger
from act.lifecycle import transition_to_record
from act.token import (
ACTMandate,
ACTRecord,
Capability,
Delegation,
TaskClaim,
encode_jws,
)
from act.verify import ACTVerifier
@pytest.fixture
def setup():
iss_priv, iss_pub = generate_ed25519_keypair()
sub_priv, sub_pub = generate_ed25519_keypair()
registry = KeyRegistry()
registry.register("iss-key", iss_pub)
registry.register("sub-key", sub_pub)
resolver = ACTKeyResolver(registry=registry)
base_time = 1772064000
return {
"iss_priv": iss_priv, "iss_pub": iss_pub,
"sub_priv": sub_priv, "sub_pub": sub_pub,
"registry": registry, "resolver": resolver,
"base_time": base_time,
}
def make_mandate(setup, **overrides):
bt = setup["base_time"]
defaults = dict(
alg="EdDSA", kid="iss-key",
iss="agent-issuer", sub="agent-subject",
aud="agent-subject",
iat=bt, exp=bt + 900,
jti=str(uuid.uuid4()),
task=TaskClaim(purpose="test"),
cap=[Capability(action="read.data")],
)
defaults.update(overrides)
return ACTMandate(**defaults)
def sign_mandate(mandate, priv_key):
sig = sign(priv_key, mandate.signing_input())
return encode_jws(mandate, sig)
class TestVerifyMandate:
def test_valid_mandate(self, setup):
verifier = ACTVerifier(
setup["resolver"],
verifier_id="agent-subject",
trusted_issuers={"agent-issuer"},
)
mandate = make_mandate(setup)
compact = sign_mandate(mandate, setup["iss_priv"])
result = verifier.verify_mandate(compact, now=setup["base_time"] + 100)
assert result.iss == "agent-issuer"
def test_expired(self, setup):
verifier = ACTVerifier(setup["resolver"], verifier_id="agent-subject")
mandate = make_mandate(setup)
compact = sign_mandate(mandate, setup["iss_priv"])
with pytest.raises(ACTExpiredError):
verifier.verify_mandate(compact, now=setup["base_time"] + 2000)
def test_wrong_audience(self, setup):
verifier = ACTVerifier(
setup["resolver"], verifier_id="other-agent",
trusted_issuers={"agent-issuer"},
)
mandate = make_mandate(setup)
compact = sign_mandate(mandate, setup["iss_priv"])
with pytest.raises(ACTAudienceMismatchError):
verifier.verify_mandate(
compact, now=setup["base_time"] + 100, check_sub=False,
)
def test_untrusted_issuer(self, setup):
verifier = ACTVerifier(
setup["resolver"], verifier_id="agent-subject",
trusted_issuers={"trusted-only"},
)
mandate = make_mandate(setup)
compact = sign_mandate(mandate, setup["iss_priv"])
with pytest.raises(ACTValidationError, match="not trusted"):
verifier.verify_mandate(compact, now=setup["base_time"] + 100)
def test_signature_failure(self, setup):
verifier = ACTVerifier(setup["resolver"], verifier_id="agent-subject")
mandate = make_mandate(setup)
compact = sign_mandate(mandate, setup["iss_priv"])
# Tamper with signature
parts = compact.split(".")
parts[2] = parts[2][:-4] + "XXXX"
tampered = ".".join(parts)
with pytest.raises(ACTSignatureError):
verifier.verify_mandate(tampered, now=setup["base_time"] + 100)
def test_phase2_as_mandate(self, setup):
verifier = ACTVerifier(setup["resolver"])
mandate = make_mandate(setup)
record, compact = transition_to_record(
mandate, sub_kid="sub-key", sub_private_key=setup["sub_priv"],
exec_act="read.data", par=[], status="completed",
exec_ts=setup["base_time"] + 100,
)
with pytest.raises(ACTPhaseError):
verifier.verify_mandate(compact, now=setup["base_time"] + 100)
def test_future_iat(self, setup):
verifier = ACTVerifier(setup["resolver"], verifier_id="agent-subject")
bt = setup["base_time"]
mandate = make_mandate(setup, iat=bt + 1000, exp=bt + 2000)
compact = sign_mandate(mandate, setup["iss_priv"])
with pytest.raises(ACTValidationError, match="future"):
verifier.verify_mandate(compact, now=bt)
class TestVerifyRecord:
def test_valid_record(self, setup):
verifier = ACTVerifier(
setup["resolver"],
verifier_id="agent-subject",
trusted_issuers={"agent-issuer"},
)
mandate = make_mandate(setup)
record, compact = transition_to_record(
mandate, sub_kid="sub-key", sub_private_key=setup["sub_priv"],
exec_act="read.data", par=[],
exec_ts=setup["base_time"] + 100, status="completed",
)
result = verifier.verify_record(
compact, now=setup["base_time"] + 200, check_aud=False,
)
assert result.exec_act == "read.data"
def test_wrong_signer(self, setup):
verifier = ACTVerifier(setup["resolver"])
mandate = make_mandate(setup)
record = ACTRecord.from_mandate(
mandate, kid="sub-key", exec_act="read.data",
par=[], exec_ts=setup["base_time"] + 100, status="completed",
)
# Sign with iss key instead of sub key
sig = sign(setup["iss_priv"], record.signing_input())
compact = encode_jws(record, sig)
with pytest.raises(ACTSignatureError):
verifier.verify_record(compact, now=setup["base_time"] + 200)
def test_with_dag_validation(self, setup):
verifier = ACTVerifier(
setup["resolver"], verifier_id="agent-subject",
trusted_issuers={"agent-issuer"},
)
ledger = ACTLedger()
mandate = make_mandate(setup)
record, compact = transition_to_record(
mandate, sub_kid="sub-key", sub_private_key=setup["sub_priv"],
exec_act="read.data", par=[],
exec_ts=setup["base_time"] + 100, status="completed",
)
result = verifier.verify_record(
compact, store=ledger,
now=setup["base_time"] + 200, check_aud=False,
)
assert result.status == "completed"