Skip to content

Commit d4a264a

Browse files
committed
refactor: isolate task grant HTTP routes
1 parent 81e2320 commit d4a264a

2 files changed

Lines changed: 116 additions & 62 deletions

File tree

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
"""Authenticated task-grant HTTP routes."""
2+
3+
from __future__ import annotations
4+
5+
from collections.abc import Awaitable, Callable
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.authorization.evidence import IssuanceAuthority, SignedAuthorityCommand
13+
from agentnet.core.app import CommunicationCore
14+
from agentnet.errors import AuthorizationError, ValidationError
15+
from agentnet.identity.actors import VerifiedActor
16+
from agentnet.protocol.models import TaskGrant
17+
18+
19+
BodyAndActor = Callable[
20+
[Request, CommunicationCore],
21+
Awaitable[tuple[bytes, VerifiedActor]],
22+
]
23+
AuthorityIssuer = Callable[..., IssuanceAuthority]
24+
25+
26+
class TaskGrantIssueBody(BaseModel):
27+
model_config = ConfigDict(extra="forbid")
28+
29+
grant: TaskGrant
30+
31+
32+
class TaskGrantAuthorityBody(BaseModel):
33+
model_config = ConfigDict(extra="forbid")
34+
35+
command: SignedAuthorityCommand
36+
37+
38+
def create_task_grant_routes(
39+
core: CommunicationCore,
40+
body_and_actor: BodyAndActor,
41+
issue_authority: AuthorityIssuer,
42+
) -> list[Route]:
43+
"""Mount only task-grant issue, read, and revocation routes."""
44+
45+
async def issue_task_grant(request: Request) -> Response:
46+
body, actor = await body_and_actor(request, core)
47+
parsed = TaskGrantIssueBody.model_validate_json(body)
48+
issued = core.issue_task_grant(actor=actor, grant=parsed.grant)
49+
return JSONResponse(
50+
{"grant": issued.model_dump(mode="json")},
51+
status_code=201,
52+
)
53+
54+
async def get_task_grant(request: Request) -> Response:
55+
_body, actor = await body_and_actor(request, core)
56+
grant_id = request.path_params["grant_id"]
57+
administrative = request.query_params.get("administrative", "false")
58+
if administrative not in {"true", "false"}:
59+
raise ValidationError("administrative must be true or false")
60+
action = (
61+
"authorization.task_grant.admin_read"
62+
if administrative == "true"
63+
else "authorization.task_grant.read"
64+
)
65+
resource, exact_request = core.grants.read_binding(grant_id)
66+
authority = issue_authority(
67+
core,
68+
actor=actor,
69+
action=action,
70+
resource=resource,
71+
request=exact_request,
72+
)
73+
grant = core.grants.get(
74+
grant_id,
75+
authority=authority,
76+
administrative=administrative == "true",
77+
)
78+
if grant is None:
79+
raise AuthorizationError("task grant is not visible")
80+
return JSONResponse({"grant": grant.model_dump(mode="json")})
81+
82+
async def revoke_task_grant(request: Request) -> Response:
83+
body, actor = await body_and_actor(request, core)
84+
parsed = TaskGrantAuthorityBody.model_validate_json(body)
85+
grant_id = request.path_params["grant_id"]
86+
if parsed.command.resource != f"task-grant:{grant_id}":
87+
raise AuthorizationError("task grant authority binding mismatch")
88+
authority = issue_authority(
89+
core,
90+
actor=actor,
91+
action=parsed.command.action,
92+
resource=parsed.command.resource,
93+
request={"request_digest": parsed.command.request_digest},
94+
)
95+
core.grants.revoke(
96+
grant_id,
97+
command=parsed.command,
98+
authority=authority,
99+
)
100+
return JSONResponse({"grant_id": grant_id, "revoked": True})
101+
102+
return [
103+
Route("/v1/task-grants", issue_task_grant, methods=["POST"]),
104+
Route("/v1/task-grants/{grant_id}", get_task_grant, methods=["GET"]),
105+
Route(
106+
"/v1/task-grants/{grant_id}/revoke",
107+
revoke_task_grant,
108+
methods=["POST"],
109+
),
110+
]
111+
112+
113+
__all__ = ["create_task_grant_routes"]

src/agentnet/product_http.py

Lines changed: 3 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from starlette.routing import Route
2121

2222
from agentnet.authorization.evidence import IssuanceAuthority, SignedAuthorityCommand
23+
from agentnet.authorization.grant_http import create_task_grant_routes
2324
from agentnet.authorization.grants import GrantUse
2425
from agentnet.approval import IndependentApprovalReceipt
2526
from agentnet.automation_http import create_automation_routes
@@ -47,7 +48,7 @@
4748
RelationshipPolicyException,
4849
TaskConflictAdjudication,
4950
)
50-
from agentnet.protocol.models import Classification, Relationship, TaskGrant
51+
from agentnet.protocol.models import Classification, Relationship
5152
from agentnet.provenance_http import create_provenance_routes
5253
from agentnet.rooms.http import create_room_routes
5354
from agentnet.security.signatures import canonical_digest, canonical_json
@@ -95,12 +96,6 @@ class RelationshipPolicyExceptionActivationBody(BaseModel):
9596
expected_lifecycle_revision: int = Field(ge=1)
9697

9798

98-
class TaskGrantIssueBody(BaseModel):
99-
model_config = ConfigDict(extra="forbid")
100-
101-
grant: TaskGrant
102-
103-
10499
class AuthorityCommandBody(BaseModel):
105100
model_config = ConfigDict(extra="forbid")
106101

@@ -468,56 +463,6 @@ async def adjudicate_task_conflict(request: Request) -> Response:
468463
headers=RELATIONSHIP_RESPONSE_HEADERS,
469464
)
470465

471-
async def issue_task_grant(request: Request) -> Response:
472-
body, actor = await body_and_actor(request, core)
473-
parsed = TaskGrantIssueBody.model_validate_json(body)
474-
issued = core.issue_task_grant(actor=actor, grant=parsed.grant)
475-
return JSONResponse({"grant": issued.model_dump(mode="json")}, status_code=201)
476-
477-
async def get_task_grant(request: Request) -> Response:
478-
_body, actor = await body_and_actor(request, core)
479-
grant_id = request.path_params["grant_id"]
480-
administrative = request.query_params.get("administrative", "false")
481-
if administrative not in {"true", "false"}:
482-
raise ValidationError("administrative must be true or false")
483-
action = (
484-
"authorization.task_grant.admin_read"
485-
if administrative == "true"
486-
else "authorization.task_grant.read"
487-
)
488-
resource, exact_request = core.grants.read_binding(grant_id)
489-
authority = _authority(
490-
core,
491-
actor=actor,
492-
action=action,
493-
resource=resource,
494-
request=exact_request,
495-
)
496-
grant = core.grants.get(
497-
grant_id,
498-
authority=authority,
499-
administrative=administrative == "true",
500-
)
501-
if grant is None:
502-
raise AuthorizationError("task grant is not visible")
503-
return JSONResponse({"grant": grant.model_dump(mode="json")})
504-
505-
async def revoke_task_grant(request: Request) -> Response:
506-
body, actor = await body_and_actor(request, core)
507-
parsed = AuthorityCommandBody.model_validate_json(body)
508-
grant_id = request.path_params["grant_id"]
509-
if parsed.command.resource != f"task-grant:{grant_id}":
510-
raise AuthorizationError("task grant authority binding mismatch")
511-
authority = _authority(
512-
core,
513-
actor=actor,
514-
action=parsed.command.action,
515-
resource=parsed.command.resource,
516-
request={"request_digest": parsed.command.request_digest},
517-
)
518-
core.grants.revoke(grant_id, command=parsed.command, authority=authority)
519-
return JSONResponse({"grant_id": grant_id, "revoked": True})
520-
521466
async def reserve_artifact(request: Request) -> Response:
522467
core.artifacts.require_enabled()
523468
body, actor = await body_and_actor(request, core)
@@ -1041,11 +986,7 @@ async def replay_version_events(request: Request) -> Response:
1041986
RELATIONSHIP_RESPONSE_HEADERS,
1042987
)
1043988
)
1044-
routes += [
1045-
Route("/v1/task-grants", issue_task_grant, methods=["POST"]),
1046-
Route("/v1/task-grants/{grant_id}", get_task_grant, methods=["GET"]),
1047-
Route("/v1/task-grants/{grant_id}/revoke", revoke_task_grant, methods=["POST"]),
1048-
]
989+
routes.extend(create_task_grant_routes(core, body_and_actor, _authority))
1049990
routes.extend(create_room_routes(core, body_and_actor, _decode_b64))
1050991
routes += [
1051992
Route("/v1/artifacts/reservations", reserve_artifact, methods=["POST"]),

0 commit comments

Comments
 (0)