Skip to content

Commit 5d829e0

Browse files
committed
fix(v2): snake_case attributes + alpha versioning before publish
Two pre-publish corrections to commit 6ec56f3: 1. Reshape camelCase dataclass attributes to snake_case per PEP 8. Add to_canonical_dict() methods that emit camelCase JSON for cross-impl byte-parity with TypeScript SDK. The canonical hash path now goes through to_canonical_dict; Python user-facing attributes are Pythonic. Byte-parity tests still pass — verified all 15 cross-impl scenarios produce byte-identical canonical JSON to the TS fixture. 2. Version bump 2.4.0 -> 2.4.0a0 (PEP 440 alpha). Symmetric with TS SDK 2.6.0-alpha.0 alpha-tag. Paper review window may shape-shift these primitives; alpha avoids forcing major- version ceremony for every adjustment. Default pip install still resolves to 2.3.0 stable; --pre opts into 2.4.0a0.
1 parent 6ec56f3 commit 5d829e0

7 files changed

Lines changed: 99 additions & 87 deletions

File tree

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
pip install agent-passport-system
1111
```
1212

13-
> **Current PyPI version**: `2.4.0` (default install). Cross-language parity with `agent-passport-system` npm v2.6.0-alpha.0 covers identity, delegation, governance, data source registration, training attribution, per-period attribution settlement, mutual authentication, and the evidentiary type safety primitives (claim/evidence registry, claim verifier, contestation cascade). Wave 1 accountability primitives are still TypeScript-only this iteration.
13+
> **Current stable**: `2.3.0` (default `pip install`). **Pre-release**: `2.4.0a0` (`pip install --pre agent-passport-system==2.4.0a0`). The 2.4.0a0 alpha adds the evidentiary type safety primitives (claim/evidence registry, claim verifier with forbidden-substitution detection, contestation cascade) in symmetry with TypeScript SDK npm 2.6.0-alpha.0. Cross-impl byte-parity verified against TS-generated canonical JSON fixtures. Paper review window may shape-shift these primitives; alpha versioning avoids forcing major-version ceremony for every adjustment. Wave 1 accountability primitives are still TypeScript-only this iteration.
1414
1515

1616
## Quick Start
@@ -110,7 +110,7 @@ This Python SDK implements all 8 Agent Passport Protocol layers:
110110
7. **Integration Wiring** — Cross-layer bridges (commerce+intent, coordination+agora)
111111
8. **Agentic Commerce** — 4-gate checkout, human approval, spend limits
112112

113-
Strict subset of the [TypeScript SDK](https://www.npmjs.com/package/agent-passport-system) at npm v2.6.0-alpha.0. The four evidentiary type safety primitives (claim/evidence registry, claim verifier with forbidden-substitution detection, contestation cascade, GroundsClass extension) ship in `agent_passport.v2` from Python SDK 2.4.0 onward, with cross-impl byte-parity verified against TS-generated fixtures. Wave 1 accountability primitives (ActionReceipt, AuthorityBoundaryReceipt, CustodyReceipt, ContestabilityReceipt, APSBundle) ship in the TypeScript SDK only this iteration; full Python port deferred. The cascade primitive uses a minimal Python ContestabilityReceipt that widens when Wave 1 ports. Cross-language signature verification continues to work for the primitives Python does ship. Also available via the [MCP server](https://mcp.aeoess.com/sse).
113+
Strict subset of the [TypeScript SDK](https://www.npmjs.com/package/agent-passport-system) at npm v2.6.0-alpha.0. The four evidentiary type safety primitives (claim/evidence registry, claim verifier with forbidden-substitution detection, contestation cascade, GroundsClass extension) ship in `agent_passport.v2` from Python SDK 2.4.0a0 (alpha pre-release) onward, with cross-impl byte-parity verified against TS-generated fixtures. Wave 1 accountability primitives (ActionReceipt, AuthorityBoundaryReceipt, CustodyReceipt, ContestabilityReceipt, APSBundle) ship in the TypeScript SDK only this iteration; full Python port deferred. The cascade primitive uses a minimal Python ContestabilityReceipt that widens when Wave 1 ports. Cross-language signature verification continues to work for the primitives Python does ship. Also available via the [MCP server](https://mcp.aeoess.com/sse).
114114

115115
## Links
116116

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "agent-passport-system"
7-
version = "2.4.0"
7+
version = "2.4.0a0"
88
description = "Python SDK for the Agent Passport System. Identity, delegation, governance, data source registration, training attribution, per-period attribution settlement, mutual authentication, evidentiary type safety (claim/evidence registry, claim verifier with forbidden-substitution detection, contestation cascade). Cross-language parity with agent-passport-system npm v2.6.0-alpha.0 verified by byte-identical canonical JSON fixtures. Wave 1 accountability primitives in JS SDK; Python port deferred. Product intelligence lives in the private gateway."
99
readme = "README.md"
1010
license = "Apache-2.0"

src/agent_passport/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
Docs: https://aeoess.com/llms-full.txt
2929
"""
3030

31-
__version__ = "2.4.0"
31+
__version__ = "2.4.0a0"
3232

3333
# Crypto
3434
from .crypto import generate_key_pair, sign, verify, public_key_from_private
@@ -285,7 +285,7 @@
285285
from .canonical import canonicalize_jcs
286286

287287

288-
# Evidentiary Type Safety primitives (SDK v2.4.0)
288+
# Evidentiary Type Safety primitives (SDK v2.4.0a0 alpha pre-release)
289289
# Ports of the four TypeScript SDK 2.6.0-alpha.0 primitives:
290290
# claim-evidence-types, claim-verifier, downstream-taint, GroundsClass.
291291
# ContestabilityReceipt fully ports when Wave 1 accountability ports;

src/agent_passport/v2/claim_verifier.py

Lines changed: 47 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,10 @@
4747
rest of this Python SDK does not carry.
4848
4949
Pre-decided in the port spec: option 1. Documented here for future
50-
maintainers. The to_dict() helper drops None-valued fields so the
51-
canonical JSON form is byte-identical to what TypeScript produces.
50+
maintainers. Python attribute names are snake_case per PEP 8; the
51+
to_canonical_dict() method emits camelCase JSON keys for cross-impl
52+
byte-parity with TypeScript. The canonical-hash path goes through
53+
to_canonical_dict, not the dataclass attributes directly.
5254
"""
5355

5456
from dataclasses import dataclass, field
@@ -133,61 +135,59 @@ class ClaimVerificationResult:
133135
134136
The `status` field is the discriminator. Per-variant fields are
135137
Optional and only populated when the discriminator selects them.
136-
to_dict() drops None-valued fields so the canonical JSON shape
137-
matches what the TypeScript discriminated union produces.
138-
139-
Field naming follows TS exactly (camelCase, not snake_case) for
140-
cross-impl JSON byte-parity. Python callers reading these via
141-
attribute access still get camelCase identifiers; this is the
142-
deliberate trade-off for wire compatibility.
138+
Python attribute names are snake_case per PEP 8; the
139+
to_canonical_dict() method emits camelCase JSON keys for cross-impl
140+
byte-parity with the TypeScript SDK. None-valued fields drop from
141+
the canonical-dict output so each variant's JSON shape matches
142+
TS exactly.
143143
"""
144144

145145
status: ClaimVerificationStatus
146-
claimType: ClaimType
146+
claim_type: ClaimType
147147
# 'valid' fields
148-
satisfiedBy: Optional[List[RecordType]] = None
148+
satisfied_by: Optional[List[RecordType]] = None
149149
# 'missing_evidence' fields
150150
missing: Optional[List[RecordType]] = None
151151
provided: Optional[List[RecordType]] = None
152152
# 'forbidden_substitution' fields
153-
offendingRecord: Optional[RecordType] = None
153+
offending_record: Optional[RecordType] = None
154154
reason: Optional[str] = None
155155
# 'bundle_requires_inclusion_proof' fields
156-
bundleRecord: Optional[APSBundle] = None
156+
bundle_record: Optional[APSBundle] = None
157157
# 'contested' fields
158-
contestedRecordId: Optional[str] = None
159-
contestationId: Optional[str] = None
160-
contestationStatus: Optional[ContestStatus] = None
158+
contested_record_id: Optional[str] = None
159+
contestation_id: Optional[str] = None
160+
contestation_status: Optional[ContestStatus] = None
161161

162-
def to_dict(self) -> dict:
163-
"""Serialize as a dict matching the TS discriminated-union shape.
162+
def to_canonical_dict(self) -> dict:
163+
"""Emit camelCase JSON dict for cross-impl byte-parity with TS.
164164
165-
None-valued fields drop. Enums collapse to their string values.
166-
Lists of enums collapse element-wise. Use this output as the
167-
input to canonicalize() for cross-impl byte comparison.
165+
The canonical hash path uses this output, not the dataclass
166+
attributes. None-valued fields drop. Enums collapse to their
167+
string values. Lists of enums collapse element-wise.
168168
"""
169169
out: dict = {
170170
"status": self.status,
171-
"claimType": _enum_value(self.claimType),
171+
"claimType": _enum_value(self.claim_type),
172172
}
173-
if self.satisfiedBy is not None:
174-
out["satisfiedBy"] = [_enum_value(r) for r in self.satisfiedBy]
173+
if self.satisfied_by is not None:
174+
out["satisfiedBy"] = [_enum_value(r) for r in self.satisfied_by]
175175
if self.missing is not None:
176176
out["missing"] = [_enum_value(r) for r in self.missing]
177177
if self.provided is not None:
178178
out["provided"] = [_enum_value(r) for r in self.provided]
179-
if self.offendingRecord is not None:
180-
out["offendingRecord"] = _enum_value(self.offendingRecord)
179+
if self.offending_record is not None:
180+
out["offendingRecord"] = _enum_value(self.offending_record)
181181
if self.reason is not None:
182182
out["reason"] = self.reason
183-
if self.bundleRecord is not None:
184-
out["bundleRecord"] = self.bundleRecord
185-
if self.contestedRecordId is not None:
186-
out["contestedRecordId"] = self.contestedRecordId
187-
if self.contestationId is not None:
188-
out["contestationId"] = self.contestationId
189-
if self.contestationStatus is not None:
190-
out["contestationStatus"] = self.contestationStatus
183+
if self.bundle_record is not None:
184+
out["bundleRecord"] = self.bundle_record
185+
if self.contested_record_id is not None:
186+
out["contestedRecordId"] = self.contested_record_id
187+
if self.contestation_id is not None:
188+
out["contestationId"] = self.contestation_id
189+
if self.contestation_status is not None:
190+
out["contestationStatus"] = self.contestation_status
191191
return out
192192

193193

@@ -213,7 +213,7 @@ def verify_evidence_claim(input: ClaimVerificationInput) -> ClaimVerificationRes
213213
if profile is None:
214214
return ClaimVerificationResult(
215215
status="unsupported_claim_type",
216-
claimType=claim_type,
216+
claim_type=claim_type,
217217
)
218218

219219
# 2. profile_not_populated — registry entry is a stub.
@@ -222,7 +222,7 @@ def verify_evidence_claim(input: ClaimVerificationInput) -> ClaimVerificationRes
222222
if not has_required and not has_forbidden:
223223
return ClaimVerificationResult(
224224
status="profile_not_populated",
225-
claimType=claim_type,
225+
claim_type=claim_type,
226226
)
227227

228228
# 3. bundle_requires_inclusion_proof — APSBundle slipped in for a
@@ -232,8 +232,8 @@ def verify_evidence_claim(input: ClaimVerificationInput) -> ClaimVerificationRes
232232
if entry.record_type == RecordType.APSBundle:
233233
return ClaimVerificationResult(
234234
status="bundle_requires_inclusion_proof",
235-
claimType=claim_type,
236-
bundleRecord=entry.record,
235+
claim_type=claim_type,
236+
bundle_record=entry.record,
237237
)
238238

239239
# 4. forbidden_substitution — first match wins, in evidence order.
@@ -242,8 +242,8 @@ def verify_evidence_claim(input: ClaimVerificationInput) -> ClaimVerificationRes
242242
if reason is not None:
243243
return ClaimVerificationResult(
244244
status="forbidden_substitution",
245-
claimType=claim_type,
246-
offendingRecord=entry.record_type,
245+
claim_type=claim_type,
246+
offending_record=entry.record_type,
247247
reason=reason,
248248
)
249249

@@ -254,7 +254,7 @@ def verify_evidence_claim(input: ClaimVerificationInput) -> ClaimVerificationRes
254254
if missing:
255255
return ClaimVerificationResult(
256256
status="missing_evidence",
257-
claimType=claim_type,
257+
claim_type=claim_type,
258258
missing=missing,
259259
provided=provided_types,
260260
)
@@ -269,15 +269,15 @@ def verify_evidence_claim(input: ClaimVerificationInput) -> ClaimVerificationRes
269269
if lookup is not None and lookup.status in BLOCKING_CONTEST_STATUSES:
270270
return ClaimVerificationResult(
271271
status="contested",
272-
claimType=claim_type,
273-
contestedRecordId=entry.receipt_id,
274-
contestationId=lookup.contestation_id,
275-
contestationStatus=lookup.status,
272+
claim_type=claim_type,
273+
contested_record_id=entry.receipt_id,
274+
contestation_id=lookup.contestation_id,
275+
contestation_status=lookup.status,
276276
)
277277

278278
# 7. valid.
279279
return ClaimVerificationResult(
280280
status="valid",
281-
claimType=claim_type,
282-
satisfiedBy=list(profile.required),
281+
claim_type=claim_type,
282+
satisfied_by=list(profile.required),
283283
)

src/agent_passport/v2/downstream_taint.py

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,23 +107,50 @@ def is_contestation_tainting(c: ContestabilityReceipt) -> bool:
107107

108108
@dataclass
109109
class TaintedRecord:
110-
"""One receipt downstream of a tainting contestation."""
110+
"""One receipt downstream of a tainting contestation.
111+
112+
Python attribute names are snake_case per PEP 8; to_canonical_dict
113+
emits camelCase JSON keys for cross-impl byte-parity with TS.
114+
"""
111115

112116
receipt_id: str
113117
record_type: RecordType
114118
taint_reason: str
115119
# 1 = direct reference to the contested action_id. 2+ = transitive.
116120
taint_depth: int
117121

122+
def to_canonical_dict(self) -> dict:
123+
"""Emit camelCase JSON dict for cross-impl byte-parity with TS."""
124+
return {
125+
"receiptId": self.receipt_id,
126+
"recordType": self.record_type.value
127+
if isinstance(self.record_type, RecordType)
128+
else self.record_type,
129+
"taintReason": self.taint_reason,
130+
"taintDepth": self.taint_depth,
131+
}
132+
118133

119134
@dataclass
120135
class TaintedSet:
121-
"""Result of computing the cascade closure."""
136+
"""Result of computing the cascade closure.
137+
138+
Python attribute names are snake_case per PEP 8; to_canonical_dict
139+
emits camelCase JSON keys for cross-impl byte-parity with TS.
140+
"""
122141

123142
root_action_id: str
124143
root_contestation_id: str
125144
tainted: List[TaintedRecord] = field(default_factory=list)
126145

146+
def to_canonical_dict(self) -> dict:
147+
"""Emit camelCase JSON dict for cross-impl byte-parity with TS."""
148+
return {
149+
"rootActionId": self.root_action_id,
150+
"rootContestationId": self.root_contestation_id,
151+
"tainted": [t.to_canonical_dict() for t in self.tainted],
152+
}
153+
127154

128155
@dataclass
129156
class TaintCandidate:

tests/v2/test_claim_verifier.py

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,8 @@ def test_binding_commitment_with_only_action_receipt_returns_forbidden_substitut
5757
)
5858
)
5959
assert result.status == "forbidden_substitution"
60-
assert result.claimType == ClaimType.BINDING_COMMITMENT
61-
assert result.offendingRecord == RecordType.ActionReceipt
60+
assert result.claim_type == ClaimType.BINDING_COMMITMENT
61+
assert result.offending_record == RecordType.ActionReceipt
6262
assert (
6363
result.reason
6464
== "Action receipts prove execution or communication, not binding commitment."
@@ -76,7 +76,7 @@ def test_binding_commitment_with_promotion_and_provisional_returns_valid():
7676
)
7777
)
7878
assert result.status == "valid"
79-
assert result.satisfiedBy == [
79+
assert result.satisfied_by == [
8080
RecordType.PromotionEvent,
8181
RecordType.ProvisionalStatement,
8282
]
@@ -94,7 +94,7 @@ def test_authority_to_execute_with_authority_boundary_returns_valid():
9494
)
9595
)
9696
assert result.status == "valid"
97-
assert result.satisfiedBy == [RecordType.AuthorityBoundaryReceipt]
97+
assert result.satisfied_by == [RecordType.AuthorityBoundaryReceipt]
9898

9999

100100
def test_authority_to_execute_with_no_evidence_returns_missing_evidence():
@@ -112,7 +112,7 @@ def test_batch_attested_with_aps_bundle_returns_valid():
112112
)
113113
)
114114
assert result.status == "valid"
115-
assert result.satisfiedBy == [RecordType.APSBundle]
115+
assert result.satisfied_by == [RecordType.APSBundle]
116116

117117

118118
def test_binding_commitment_with_aps_bundle_returns_bundle_requires_inclusion_proof():
@@ -124,7 +124,7 @@ def test_binding_commitment_with_aps_bundle_returns_bundle_requires_inclusion_pr
124124
)
125125
)
126126
assert result.status == "bundle_requires_inclusion_proof"
127-
assert result.bundleRecord == bundle_rec
127+
assert result.bundle_record == bundle_rec
128128

129129

130130
def test_evidence_custody_held_with_action_receipt_forbidden():
@@ -135,7 +135,7 @@ def test_evidence_custody_held_with_action_receipt_forbidden():
135135
)
136136
)
137137
assert result.status == "forbidden_substitution"
138-
assert result.offendingRecord == RecordType.ActionReceipt
138+
assert result.offending_record == RecordType.ActionReceipt
139139
assert "held the evidence" in result.reason.lower()
140140

141141

@@ -147,7 +147,7 @@ def test_evidence_custody_held_with_custody_receipt_valid():
147147
)
148148
)
149149
assert result.status == "valid"
150-
assert result.satisfiedBy == [RecordType.CustodyReceipt]
150+
assert result.satisfied_by == [RecordType.CustodyReceipt]
151151

152152

153153
def test_effect_safety_attested_with_full_chain_returns_profile_not_populated():
@@ -167,7 +167,7 @@ def test_effect_safety_attested_with_full_chain_returns_profile_not_populated():
167167
)
168168
)
169169
assert result.status == "profile_not_populated"
170-
assert result.claimType == ClaimType.EFFECT_SAFETY_ATTESTED
170+
assert result.claim_type == ClaimType.EFFECT_SAFETY_ATTESTED
171171

172172

173173
def test_unsupported_claim_type():
@@ -221,9 +221,9 @@ def resolver(record_id):
221221
)
222222
)
223223
assert result.status == "contested"
224-
assert result.contestedRecordId == "auth_001"
225-
assert result.contestationId == "contest_xyz"
226-
assert result.contestationStatus == "filed"
224+
assert result.contested_record_id == "auth_001"
225+
assert result.contestation_id == "contest_xyz"
226+
assert result.contestation_status == "filed"
227227

228228

229229
def test_resolver_rejected_returns_valid():
@@ -264,7 +264,7 @@ def resolver(record_id):
264264
)
265265
)
266266
assert result.status == "contested"
267-
assert result.contestationStatus == "upheld"
267+
assert result.contestation_status == "upheld"
268268

269269

270270
# ── Cross-impl byte-parity (load TS fixtures, assert byte-identical output) ──
@@ -309,7 +309,7 @@ def test_verifier_byte_parity_with_ts(fixture):
309309
evidence=evidence,
310310
)
311311
result = verify_evidence_claim(input)
312-
result_dict = result.to_dict()
312+
result_dict = result.to_canonical_dict()
313313
result_dict.pop("bundleRecord", None)
314314
canonical = canonicalize(result_dict)
315315
digest = hashlib.sha256(canonical.encode("utf-8")).hexdigest()

0 commit comments

Comments
 (0)