Skip to content

Commit 590845b

Browse files
committed
refactor: isolate provenance HTTP routes
1 parent 7a4e1e0 commit 590845b

2 files changed

Lines changed: 207 additions & 147 deletions

File tree

src/agentnet/product_http.py

Lines changed: 9 additions & 147 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
EffectUncertaintyEvidence,
3737
)
3838
from agentnet.errors import AuthenticationError, AuthorizationError, ValidationError
39-
from agentnet.identity.actors import ActorKind, TrustedTransportContext, VerifiedActor
39+
from agentnet.identity.actors import TrustedTransportContext, VerifiedActor
4040
from agentnet.identity.credential_http import (
4141
ExpiredBodyAndContext,
4242
create_credential_routes,
@@ -54,12 +54,7 @@
5454
ReleasedArtifactBinding,
5555
TaskGrant,
5656
)
57-
from agentnet.provenance import (
58-
OriginKind,
59-
OriginRegistration,
60-
ProvenanceDerivation,
61-
ProvenanceObjectType,
62-
)
57+
from agentnet.provenance_http import create_provenance_routes
6358
from agentnet.rooms.governance import (
6459
RoomTransferSnapshot,
6560
SourceTransferProposal,
@@ -128,18 +123,6 @@ class TaskConflictAdjudicationBody(BaseModel):
128123
decision: TaskConflictAdjudication
129124

130125

131-
class ProvenanceOriginBody(BaseModel):
132-
model_config = ConfigDict(extra="forbid", strict=True)
133-
134-
registration: OriginRegistration
135-
136-
137-
class ProvenanceDerivationBody(BaseModel):
138-
model_config = ConfigDict(extra="forbid", strict=True)
139-
140-
derivation: ProvenanceDerivation
141-
142-
143126
class RoomCreateBody(BaseModel):
144127
model_config = ConfigDict(extra="forbid", strict=True)
145128

@@ -565,122 +548,6 @@ async def adjudicate_task_conflict(request: Request) -> Response:
565548
headers=RELATIONSHIP_RESPONSE_HEADERS,
566549
)
567550

568-
async def register_provenance_origin(request: Request) -> Response:
569-
body, actor = await body_and_actor(request, core)
570-
parsed = ProvenanceOriginBody.model_validate_json(body, strict=True)
571-
registration = parsed.registration
572-
if registration.domain_id != actor.domain_id:
573-
raise AuthorizationError("provenance origin crossed the authenticated domain")
574-
if registration.origin.kind is not OriginKind.HUMAN_INPUT:
575-
raise AuthorizationError("non-human provenance origins require a composed server service")
576-
if (
577-
actor.kind is not ActorKind.VERIFIED_HUMAN_HARNESS
578-
or registration.origin.principal_id != actor.principal_id
579-
or registration.origin.harness_id != actor.harness_id
580-
):
581-
raise AuthorizationError("human provenance origin is not the authenticated human harness")
582-
resource = f"provenance:{registration.object_type.value}:{registration.object_id}"
583-
core._require(
584-
actor=actor,
585-
action="provenance.origin.register",
586-
resource=resource,
587-
classification=registration.classification,
588-
context={
589-
"registration_digest": canonical_digest(
590-
registration.model_dump(mode="json")
591-
)
592-
},
593-
)
594-
record = core.provenance.register_origin(registration)
595-
return JSONResponse(
596-
{"provenance": record.model_dump(mode="json")},
597-
status_code=201,
598-
headers=RELATIONSHIP_RESPONSE_HEADERS,
599-
)
600-
601-
async def derive_provenance(request: Request) -> Response:
602-
body, actor = await body_and_actor(request, core)
603-
parsed = ProvenanceDerivationBody.model_validate_json(body, strict=True)
604-
derivation = parsed.derivation
605-
if derivation.domain_id != actor.domain_id:
606-
raise AuthorizationError("derived provenance crossed the authenticated domain")
607-
if actor.harness_id is None or any(
608-
step.executor_harness_id != actor.harness_id
609-
for step in derivation.transformations
610-
):
611-
raise AuthorizationError(
612-
"provenance transformation executor is not the authenticated harness"
613-
)
614-
resource = f"provenance:{derivation.object_type.value}:{derivation.object_id}"
615-
core._require(
616-
actor=actor,
617-
action="provenance.derive",
618-
resource=resource,
619-
classification=derivation.classification,
620-
context={
621-
"derivation_digest": canonical_digest(
622-
derivation.model_dump(mode="json")
623-
)
624-
},
625-
)
626-
record = core.provenance.derive(derivation)
627-
return JSONResponse(
628-
{"provenance": record.model_dump(mode="json")},
629-
status_code=201,
630-
headers=RELATIONSHIP_RESPONSE_HEADERS,
631-
)
632-
633-
async def provenance_versions(request: Request) -> Response:
634-
_body, actor = await body_and_actor(request, core)
635-
try:
636-
object_type = ProvenanceObjectType(request.path_params["object_type"])
637-
except ValueError as exc:
638-
raise ValidationError("provenance object type is invalid") from exc
639-
object_id = request.path_params["object_id"]
640-
resource = f"provenance:{object_type.value}:{object_id}"
641-
core._require(
642-
actor=actor,
643-
action="provenance.read",
644-
resource=resource,
645-
context={"object_type": object_type.value, "object_id": object_id},
646-
)
647-
records = core.provenance.versions(object_type=object_type, object_id=object_id)
648-
return JSONResponse(
649-
{"versions": [record.model_dump(mode="json") for record in records]},
650-
headers=RELATIONSHIP_RESPONSE_HEADERS,
651-
)
652-
653-
async def provenance_version(request: Request) -> Response:
654-
_body, actor = await body_and_actor(request, core)
655-
try:
656-
object_type = ProvenanceObjectType(request.path_params["object_type"])
657-
except ValueError as exc:
658-
raise ValidationError("provenance object type is invalid") from exc
659-
raw_version = request.path_params["version"]
660-
if not raw_version.isascii() or not raw_version.isdigit() or int(raw_version) < 1:
661-
raise ValidationError("provenance version is invalid")
662-
object_id = request.path_params["object_id"]
663-
resource = f"provenance:{object_type.value}:{object_id}"
664-
core._require(
665-
actor=actor,
666-
action="provenance.read",
667-
resource=resource,
668-
context={
669-
"object_type": object_type.value,
670-
"object_id": object_id,
671-
"version": int(raw_version),
672-
},
673-
)
674-
record = core.provenance.get_version(
675-
object_type=object_type,
676-
object_id=object_id,
677-
version=int(raw_version),
678-
)
679-
return JSONResponse(
680-
{"provenance": record.model_dump(mode="json")},
681-
headers=RELATIONSHIP_RESPONSE_HEADERS,
682-
)
683-
684551
async def issue_task_grant(request: Request) -> Response:
685552
body, actor = await body_and_actor(request, core)
686553
parsed = TaskGrantIssueBody.model_validate_json(body)
@@ -1461,19 +1328,14 @@ async def replay_version_events(request: Request) -> Response:
14611328
RELATIONSHIP_RESPONSE_HEADERS,
14621329
)
14631330
)
1331+
routes.extend(
1332+
create_provenance_routes(
1333+
core,
1334+
body_and_actor,
1335+
RELATIONSHIP_RESPONSE_HEADERS,
1336+
)
1337+
)
14641338
routes += [
1465-
Route("/v1/provenance/origins", register_provenance_origin, methods=["POST"]),
1466-
Route("/v1/provenance/derivations", derive_provenance, methods=["POST"]),
1467-
Route(
1468-
"/v1/provenance/{object_type}/{object_id}",
1469-
provenance_versions,
1470-
methods=["GET"],
1471-
),
1472-
Route(
1473-
"/v1/provenance/{object_type}/{object_id}/{version}",
1474-
provenance_version,
1475-
methods=["GET"],
1476-
),
14771339
Route("/v1/task-grants", issue_task_grant, methods=["POST"]),
14781340
Route("/v1/task-grants/{grant_id}", get_task_grant, methods=["GET"]),
14791341
Route("/v1/task-grants/{grant_id}/revoke", revoke_task_grant, methods=["POST"]),

src/agentnet/provenance_http.py

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
"""Authenticated provenance registration and read HTTP routes."""
2+
3+
from __future__ import annotations
4+
5+
from collections.abc import Awaitable, Callable, Mapping
6+
7+
from pydantic import BaseModel, ConfigDict
8+
from starlette.requests import Request
9+
from starlette.responses import JSONResponse, Response
10+
from starlette.routing import Route
11+
12+
from agentnet.core.app import CommunicationCore
13+
from agentnet.errors import AuthorizationError, ValidationError
14+
from agentnet.identity.actors import ActorKind, VerifiedActor
15+
from agentnet.provenance import (
16+
OriginKind,
17+
OriginRegistration,
18+
ProvenanceDerivation,
19+
ProvenanceObjectType,
20+
)
21+
from agentnet.security.signatures import canonical_digest
22+
23+
24+
BodyAndActor = Callable[
25+
[Request, CommunicationCore],
26+
Awaitable[tuple[bytes, VerifiedActor]],
27+
]
28+
29+
30+
class ProvenanceOriginBody(BaseModel):
31+
model_config = ConfigDict(extra="forbid", strict=True)
32+
33+
registration: OriginRegistration
34+
35+
36+
class ProvenanceDerivationBody(BaseModel):
37+
model_config = ConfigDict(extra="forbid", strict=True)
38+
39+
derivation: ProvenanceDerivation
40+
41+
42+
def create_provenance_routes(
43+
core: CommunicationCore,
44+
body_and_actor: BodyAndActor,
45+
response_headers: Mapping[str, str],
46+
) -> list[Route]:
47+
"""Mount only provenance registration, derivation, and read routes."""
48+
49+
async def register_provenance_origin(request: Request) -> Response:
50+
body, actor = await body_and_actor(request, core)
51+
parsed = ProvenanceOriginBody.model_validate_json(body, strict=True)
52+
registration = parsed.registration
53+
if registration.domain_id != actor.domain_id:
54+
raise AuthorizationError(
55+
"provenance origin crossed the authenticated domain"
56+
)
57+
if registration.origin.kind is not OriginKind.HUMAN_INPUT:
58+
raise AuthorizationError(
59+
"non-human provenance origins require a composed server service"
60+
)
61+
if (
62+
actor.kind is not ActorKind.VERIFIED_HUMAN_HARNESS
63+
or registration.origin.principal_id != actor.principal_id
64+
or registration.origin.harness_id != actor.harness_id
65+
):
66+
raise AuthorizationError(
67+
"human provenance origin is not the authenticated human harness"
68+
)
69+
resource = (
70+
f"provenance:{registration.object_type.value}:{registration.object_id}"
71+
)
72+
core._require(
73+
actor=actor,
74+
action="provenance.origin.register",
75+
resource=resource,
76+
classification=registration.classification,
77+
context={
78+
"registration_digest": canonical_digest(
79+
registration.model_dump(mode="json")
80+
)
81+
},
82+
)
83+
record = core.provenance.register_origin(registration)
84+
return JSONResponse(
85+
{"provenance": record.model_dump(mode="json")},
86+
status_code=201,
87+
headers=response_headers,
88+
)
89+
90+
async def derive_provenance(request: Request) -> Response:
91+
body, actor = await body_and_actor(request, core)
92+
parsed = ProvenanceDerivationBody.model_validate_json(body, strict=True)
93+
derivation = parsed.derivation
94+
if derivation.domain_id != actor.domain_id:
95+
raise AuthorizationError(
96+
"derived provenance crossed the authenticated domain"
97+
)
98+
if actor.harness_id is None or any(
99+
step.executor_harness_id != actor.harness_id
100+
for step in derivation.transformations
101+
):
102+
raise AuthorizationError(
103+
"provenance transformation executor is not the authenticated harness"
104+
)
105+
resource = f"provenance:{derivation.object_type.value}:{derivation.object_id}"
106+
core._require(
107+
actor=actor,
108+
action="provenance.derive",
109+
resource=resource,
110+
classification=derivation.classification,
111+
context={
112+
"derivation_digest": canonical_digest(
113+
derivation.model_dump(mode="json")
114+
)
115+
},
116+
)
117+
record = core.provenance.derive(derivation)
118+
return JSONResponse(
119+
{"provenance": record.model_dump(mode="json")},
120+
status_code=201,
121+
headers=response_headers,
122+
)
123+
124+
async def provenance_versions(request: Request) -> Response:
125+
_body, actor = await body_and_actor(request, core)
126+
try:
127+
object_type = ProvenanceObjectType(request.path_params["object_type"])
128+
except ValueError as exc:
129+
raise ValidationError("provenance object type is invalid") from exc
130+
object_id = request.path_params["object_id"]
131+
resource = f"provenance:{object_type.value}:{object_id}"
132+
core._require(
133+
actor=actor,
134+
action="provenance.read",
135+
resource=resource,
136+
context={"object_type": object_type.value, "object_id": object_id},
137+
)
138+
records = core.provenance.versions(
139+
object_type=object_type,
140+
object_id=object_id,
141+
)
142+
return JSONResponse(
143+
{"versions": [record.model_dump(mode="json") for record in records]},
144+
headers=response_headers,
145+
)
146+
147+
async def provenance_version(request: Request) -> Response:
148+
_body, actor = await body_and_actor(request, core)
149+
try:
150+
object_type = ProvenanceObjectType(request.path_params["object_type"])
151+
except ValueError as exc:
152+
raise ValidationError("provenance object type is invalid") from exc
153+
raw_version = request.path_params["version"]
154+
if (
155+
not raw_version.isascii()
156+
or not raw_version.isdigit()
157+
or int(raw_version) < 1
158+
):
159+
raise ValidationError("provenance version is invalid")
160+
object_id = request.path_params["object_id"]
161+
resource = f"provenance:{object_type.value}:{object_id}"
162+
core._require(
163+
actor=actor,
164+
action="provenance.read",
165+
resource=resource,
166+
context={
167+
"object_type": object_type.value,
168+
"object_id": object_id,
169+
"version": int(raw_version),
170+
},
171+
)
172+
record = core.provenance.get_version(
173+
object_type=object_type,
174+
object_id=object_id,
175+
version=int(raw_version),
176+
)
177+
return JSONResponse(
178+
{"provenance": record.model_dump(mode="json")},
179+
headers=response_headers,
180+
)
181+
182+
return [
183+
Route("/v1/provenance/origins", register_provenance_origin, methods=["POST"]),
184+
Route("/v1/provenance/derivations", derive_provenance, methods=["POST"]),
185+
Route(
186+
"/v1/provenance/{object_type}/{object_id}",
187+
provenance_versions,
188+
methods=["GET"],
189+
),
190+
Route(
191+
"/v1/provenance/{object_type}/{object_id}/{version}",
192+
provenance_version,
193+
methods=["GET"],
194+
),
195+
]
196+
197+
198+
__all__ = ["create_provenance_routes"]

0 commit comments

Comments
 (0)