Skip to content

Commit f4f2f87

Browse files
author
PV
committed
Wave 29: WG specs directory + entity verification module
- specs/ directory: README, QSP-1 envelope, DID resolution, entity verification - Test vectors: Ed25519→X25519 (5), HKDF derivation, entity API - Entity verification module (entity.py): verify_entity(), verify_sender_entity() - 8 new tests (mock Corpo API), 240 total pass - WG principles: code-first, independent projects, shared interfaces, open membership
1 parent 52a7ce5 commit f4f2f87

10 files changed

Lines changed: 835 additions & 0 deletions

File tree

.company/waves/wave-029.md

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# Wave 29 — The WG Gets a Home
2+
Started: 2026-03-23T08:39:00Z
3+
Campaign: 6 (Waves 29+) — Standard or Product?
4+
5+
## 10 Questions (answered before execution)
6+
7+
1. **What changed since last wave?**
8+
- **CHAIRMAN UNBLOCKED ENTITY API.** Peter (@vessenes) posted Corpo staging entity at `api.corpo.llc/api/v1/entities/test-entity/verify` on APS#5. This is the P1 blocker from the last 5 waves — resolved by chairman action.
9+
- **haroldmalikfrimpong-ops CONFIRMED ENTITY API WORKING.** Already building `verify_agent_full(did)` — chains DID → AgentID certificate → Corpo entity verification. Moving faster than we can track.
10+
- **WG ENDORSED BY BOTH PARTNERS.** haroldmalikfrimpong-ops committed CA-issued identity, DID resolution, Python SDK, DID field support, framework integrations. qntm committed transport, QSP-1 spec, test vectors, echo bot, DID field. Waiting on aeoess.
11+
- **aeoess active but quiet on APS#5.** Last comment was Wave 27 timeframe. But committed relay/WebSocket tests (1122 tests, 302 suites). Building, not talking.
12+
- **232 tests pass.** Relay healthy. All green.
13+
14+
2. **Single biggest bottleneck?**
15+
- **The WG has no home.** I committed to creating a shared repo/directory for specs, test vectors, and DID resolution interface on A2A #1672. Both partners are waiting. Without a central location, the WG is just talk.
16+
17+
3. **Bottleneck category?**
18+
- Coordination infrastructure (code + specs)
19+
20+
4. **Evidence?**
21+
- Both partners committed to the WG on A2A #1672. haroldmalikfrimpong-ops is already building entity integration code. aeoess committed relay tests. They need a canonical place for shared specs, not scattered GitHub comments.
22+
23+
5. **Highest-impact action?**
24+
- Create the WG specs directory in corpollc/qntm with: QSP-1 spec, test vectors, WG README, entity verification interface. Then post links on A2A #1672.
25+
26+
6. **Customer conversation avoiding?**
27+
- The strategic direction question: standard vs product. Chairman's actions (entity API, @vessenes participating directly) strongly signal "standard" path. But no explicit ruling. I'll operate under "standard path" assumption and flag for confirmation.
28+
29+
7. **Manual work that teaches faster?**
30+
- Build the entity verification helper. Prove the DID → key → entity chain works in Python. Ship code, not specs.
31+
32+
8. **Pretending is progress?**
33+
- Creating a spec directory is necessary but not sufficient. The spec must be accurate and reflect what implementations actually do, not aspire to.
34+
35+
9. **Write down?**
36+
- WG spec structure, entity verification design, Campaign 6 goals.
37+
38+
10. **Escalation?**
39+
- **Strategic direction:** Chairman is acting on the standard/WG path (entity API, direct participation on APS#5, WG endorsement). Interpreting this as implicit approval for Campaign 6 as standard-track. Will confirm in next briefing.
40+
- **MCP marketplace:** 14th wave asking. Deprioritizing — the WG path may make this less relevant (framework maintainers integrate directly, not through marketplaces).
41+
42+
## Wave 29 Top 5 (force ranked)
43+
44+
1. **Create WG specs directory** — QSP-1 spec, test vectors, WG README with scope/membership/principles
45+
2. **Build entity verification module**`verify_entity(entity_id)` calling Corpo staging API
46+
3. **Post WG spec links on A2A #1672** — fulfill the commitment
47+
4. **Set Campaign 6 goals** (standard-track path)
48+
5. **Update state, KPIs, truth register, wave log**
49+
50+
## Execution Log

python-dist/src/qntm/entity.py

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
"""Entity verification via Corpo API.
2+
3+
Verifies that an agent's cryptographic identity is bound to a legal entity.
4+
Part of the Agent Identity Working Group interop surface.
5+
"""
6+
7+
from __future__ import annotations
8+
9+
import json
10+
import urllib.request
11+
import urllib.error
12+
from dataclasses import dataclass
13+
from typing import Optional
14+
15+
16+
CORPO_API_BASE = "https://api.corpo.llc/api/v1"
17+
18+
19+
@dataclass
20+
class EntityVerification:
21+
"""Result of an entity verification check."""
22+
23+
entity_id: str
24+
name: str
25+
status: str
26+
entity_type: str
27+
authority_ceiling: list[str]
28+
verified_at: str
29+
verified: bool
30+
31+
@property
32+
def is_active(self) -> bool:
33+
return self.status == "active"
34+
35+
36+
class EntityVerificationError(Exception):
37+
"""Raised when entity verification fails."""
38+
39+
pass
40+
41+
42+
def verify_entity(
43+
entity_id: str,
44+
*,
45+
api_base: str = CORPO_API_BASE,
46+
timeout: float = 10.0,
47+
) -> EntityVerification:
48+
"""Verify a legal entity via the Corpo API.
49+
50+
Args:
51+
entity_id: The entity identifier to verify.
52+
api_base: Base URL for the Corpo API (default: production).
53+
timeout: HTTP request timeout in seconds.
54+
55+
Returns:
56+
EntityVerification with the entity's status and metadata.
57+
58+
Raises:
59+
EntityVerificationError: If the entity is not found or the API fails.
60+
"""
61+
url = f"{api_base}/entities/{entity_id}/verify"
62+
63+
try:
64+
req = urllib.request.Request(url, method="GET")
65+
req.add_header("Accept", "application/json")
66+
with urllib.request.urlopen(req, timeout=timeout) as resp:
67+
data = json.loads(resp.read())
68+
except urllib.error.HTTPError as e:
69+
if e.code == 404:
70+
raise EntityVerificationError(
71+
f"Entity not found: {entity_id}"
72+
) from e
73+
if e.code == 410:
74+
raise EntityVerificationError(
75+
f"Entity dissolved: {entity_id}"
76+
) from e
77+
raise EntityVerificationError(
78+
f"API error {e.code}: {e.reason}"
79+
) from e
80+
except urllib.error.URLError as e:
81+
raise EntityVerificationError(
82+
f"Cannot reach Corpo API: {e.reason}"
83+
) from e
84+
85+
return EntityVerification(
86+
entity_id=data["entity_id"],
87+
name=data["name"],
88+
status=data["status"],
89+
entity_type=data["entity_type"],
90+
authority_ceiling=data.get("authority_ceiling", []),
91+
verified_at=data.get("verified_at", ""),
92+
verified=data["status"] == "active",
93+
)
94+
95+
96+
def verify_sender_entity(
97+
sender_key_id: bytes,
98+
did: Optional[str],
99+
entity_id: str,
100+
*,
101+
resolve_did_fn=None,
102+
api_base: str = CORPO_API_BASE,
103+
) -> tuple[bool, Optional[EntityVerification]]:
104+
"""Full verification chain: DID → key → sender match → entity.
105+
106+
Args:
107+
sender_key_id: 16-byte sender key ID from the QSP-1 envelope.
108+
did: DID URI from the envelope (optional).
109+
entity_id: Entity ID to verify against.
110+
resolve_did_fn: Callable(did_uri) → bytes(32) Ed25519 public key.
111+
If None, DID verification is skipped (entity-only check).
112+
api_base: Base URL for the Corpo API.
113+
114+
Returns:
115+
Tuple of (verified: bool, entity: EntityVerification or None).
116+
"""
117+
from .identity import key_id_from_public_key
118+
119+
# Step 1: If DID provided and resolver available, verify key matches sender
120+
if did and resolve_did_fn:
121+
try:
122+
resolved_key = resolve_did_fn(did)
123+
computed_kid = key_id_from_public_key(resolved_key)
124+
if computed_kid != sender_key_id:
125+
return False, None
126+
except Exception:
127+
return False, None
128+
129+
# Step 2: Verify entity
130+
try:
131+
entity = verify_entity(entity_id, api_base=api_base)
132+
return entity.is_active, entity
133+
except EntityVerificationError:
134+
return False, None

python-dist/tests/test_entity.py

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
"""Tests for entity verification module."""
2+
3+
import json
4+
import http.server
5+
import threading
6+
import pytest
7+
8+
from qntm.entity import (
9+
EntityVerification,
10+
EntityVerificationError,
11+
verify_entity,
12+
verify_sender_entity,
13+
)
14+
from qntm.identity import generate_identity, key_id_from_public_key
15+
16+
17+
# ── Mock Corpo API ──────────────────────────────────────────────────
18+
19+
TEST_ENTITY = {
20+
"entity_id": "test-entity",
21+
"name": "Test Verification DAO LLC",
22+
"status": "active",
23+
"entity_type": "wyoming_dao_llc",
24+
"authority_ceiling": ["hold_assets"],
25+
"verified_at": "2026-03-23T08:26:05Z",
26+
}
27+
28+
SUSPENDED_ENTITY = {
29+
"entity_id": "suspended-entity",
30+
"name": "Suspended Corp",
31+
"status": "suspended",
32+
"entity_type": "wyoming_dao_llc",
33+
"authority_ceiling": [],
34+
"verified_at": "2026-01-01T00:00:00Z",
35+
}
36+
37+
38+
class MockCorpoHandler(http.server.BaseHTTPRequestHandler):
39+
def do_GET(self):
40+
if "/entities/test-entity/" in self.path:
41+
self.send_response(200)
42+
self.send_header("Content-Type", "application/json")
43+
self.end_headers()
44+
self.wfile.write(json.dumps(TEST_ENTITY).encode())
45+
elif "/entities/suspended-entity/" in self.path:
46+
self.send_response(200)
47+
self.send_header("Content-Type", "application/json")
48+
self.end_headers()
49+
self.wfile.write(json.dumps(SUSPENDED_ENTITY).encode())
50+
elif "/entities/dissolved-entity/" in self.path:
51+
self.send_response(410)
52+
self.end_headers()
53+
else:
54+
self.send_response(404)
55+
self.end_headers()
56+
57+
def log_message(self, format, *args):
58+
pass # Suppress logs during tests
59+
60+
61+
@pytest.fixture(scope="module")
62+
def mock_api():
63+
"""Start a mock Corpo API server."""
64+
server = http.server.HTTPServer(("127.0.0.1", 0), MockCorpoHandler)
65+
port = server.server_address[1]
66+
thread = threading.Thread(target=server.serve_forever, daemon=True)
67+
thread.start()
68+
yield f"http://127.0.0.1:{port}/api/v1"
69+
server.shutdown()
70+
71+
72+
# ── Tests ───────────────────────────────────────────────────────────
73+
74+
75+
def test_verify_entity_active(mock_api):
76+
result = verify_entity("test-entity", api_base=mock_api)
77+
assert isinstance(result, EntityVerification)
78+
assert result.entity_id == "test-entity"
79+
assert result.name == "Test Verification DAO LLC"
80+
assert result.status == "active"
81+
assert result.is_active is True
82+
assert result.verified is True
83+
assert result.entity_type == "wyoming_dao_llc"
84+
assert "hold_assets" in result.authority_ceiling
85+
86+
87+
def test_verify_entity_suspended(mock_api):
88+
result = verify_entity("suspended-entity", api_base=mock_api)
89+
assert result.status == "suspended"
90+
assert result.is_active is False
91+
assert result.verified is False
92+
93+
94+
def test_verify_entity_not_found(mock_api):
95+
with pytest.raises(EntityVerificationError, match="not found"):
96+
verify_entity("nonexistent", api_base=mock_api)
97+
98+
99+
def test_verify_entity_dissolved(mock_api):
100+
with pytest.raises(EntityVerificationError, match="dissolved"):
101+
verify_entity("dissolved-entity", api_base=mock_api)
102+
103+
104+
def test_verify_sender_entity_full_chain(mock_api):
105+
"""Full chain: DID → key → sender match → entity."""
106+
identity = generate_identity()
107+
108+
def mock_resolve(did_uri):
109+
assert did_uri == "did:test:abc"
110+
return identity["publicKey"]
111+
112+
verified, entity = verify_sender_entity(
113+
sender_key_id=identity["keyID"],
114+
did="did:test:abc",
115+
entity_id="test-entity",
116+
resolve_did_fn=mock_resolve,
117+
api_base=mock_api,
118+
)
119+
assert verified is True
120+
assert entity is not None
121+
assert entity.entity_id == "test-entity"
122+
123+
124+
def test_verify_sender_entity_key_mismatch(mock_api):
125+
"""DID resolves to wrong key → rejected."""
126+
identity = generate_identity()
127+
other_identity = generate_identity()
128+
129+
def mock_resolve(did_uri):
130+
return other_identity["publicKey"] # Wrong key!
131+
132+
verified, entity = verify_sender_entity(
133+
sender_key_id=identity["keyID"],
134+
did="did:test:wrong",
135+
entity_id="test-entity",
136+
resolve_did_fn=mock_resolve,
137+
api_base=mock_api,
138+
)
139+
assert verified is False
140+
assert entity is None
141+
142+
143+
def test_verify_sender_entity_no_did(mock_api):
144+
"""No DID → entity-only verification."""
145+
identity = generate_identity()
146+
147+
verified, entity = verify_sender_entity(
148+
sender_key_id=identity["keyID"],
149+
did=None,
150+
entity_id="test-entity",
151+
api_base=mock_api,
152+
)
153+
assert verified is True
154+
assert entity is not None
155+
156+
157+
def test_verify_sender_entity_suspended(mock_api):
158+
"""Suspended entity → not verified."""
159+
identity = generate_identity()
160+
161+
verified, entity = verify_sender_entity(
162+
sender_key_id=identity["keyID"],
163+
did=None,
164+
entity_id="suspended-entity",
165+
api_base=mock_api,
166+
)
167+
assert verified is False
168+
assert entity is not None
169+
assert entity.status == "suspended"

0 commit comments

Comments
 (0)