Skip to content

Commit 81e2320

Browse files
committed
refactor: isolate room HTTP routes
1 parent 590845b commit 81e2320

2 files changed

Lines changed: 371 additions & 305 deletions

File tree

src/agentnet/product_http.py

Lines changed: 5 additions & 305 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010

1111
import base64
1212
import binascii
13-
import hashlib
1413
from collections.abc import Awaitable, Callable
1514
from datetime import datetime
1615
from typing import Any
@@ -48,18 +47,9 @@
4847
RelationshipPolicyException,
4948
TaskConflictAdjudication,
5049
)
51-
from agentnet.protocol.models import (
52-
Classification,
53-
Relationship,
54-
ReleasedArtifactBinding,
55-
TaskGrant,
56-
)
50+
from agentnet.protocol.models import Classification, Relationship, TaskGrant
5751
from agentnet.provenance_http import create_provenance_routes
58-
from agentnet.rooms.governance import (
59-
RoomTransferSnapshot,
60-
SourceTransferProposal,
61-
TargetTransferAcceptance,
62-
)
52+
from agentnet.rooms.http import create_room_routes
6353
from agentnet.security.signatures import canonical_digest, canonical_json
6454

6555

@@ -123,76 +113,6 @@ class TaskConflictAdjudicationBody(BaseModel):
123113
decision: TaskConflictAdjudication
124114

125115

126-
class RoomCreateBody(BaseModel):
127-
model_config = ConfigDict(extra="forbid", strict=True)
128-
129-
collaboration_scope_id: str = Field(min_length=1, max_length=256)
130-
classification: Classification = Classification.C1_INTERNAL
131-
persistent: bool = True
132-
expires_at: datetime | None = None
133-
policy: dict[str, Any] | None = None
134-
135-
136-
class MeetingCreateBody(BaseModel):
137-
model_config = ConfigDict(extra="forbid", strict=True)
138-
139-
collaboration_scope_id: str = Field(min_length=1, max_length=256)
140-
classification: Classification = Classification.C1_INTERNAL
141-
expires_at: datetime
142-
policy: dict[str, Any] | None = None
143-
144-
145-
class RoomMemberBody(BaseModel):
146-
model_config = ConfigDict(extra="forbid", strict=True)
147-
148-
collaboration_scope_id: str = Field(min_length=1, max_length=256)
149-
harness_id: str = Field(min_length=1, max_length=256)
150-
role: str = Field(default="member", pattern=r"^(member|guest|moderator)$")
151-
mls_key_package_b64: str | None = Field(default=None, max_length=1_000_000)
152-
153-
154-
class RoomMemberRemoveBody(BaseModel):
155-
model_config = ConfigDict(extra="forbid", strict=True)
156-
157-
collaboration_scope_id: str = Field(min_length=1, max_length=256)
158-
harness_id: str = Field(min_length=1, max_length=256)
159-
160-
161-
class RoomSendBody(BaseModel):
162-
model_config = ConfigDict(extra="forbid", strict=True)
163-
164-
collaboration_scope_id: str = Field(min_length=1, max_length=256)
165-
recipients: tuple[str, ...] = Field(min_length=1, max_length=1000)
166-
payload: dict[str, Any]
167-
idempotency_key: str = Field(min_length=16, max_length=256)
168-
classification: Classification = Classification.C1_INTERNAL
169-
released_artifacts: tuple[ReleasedArtifactBinding, ...] = ()
170-
expected_control_sequence: int = Field(ge=1)
171-
conversation_id: str | None = None
172-
173-
174-
class RoomDescribeBody(BaseModel):
175-
model_config = ConfigDict(extra="forbid", strict=True)
176-
177-
collaboration_scope_id: str = Field(min_length=1, max_length=256)
178-
179-
180-
class TransferProposalBody(BaseModel):
181-
model_config = ConfigDict(extra="forbid")
182-
183-
proposal: SourceTransferProposal
184-
snapshot: RoomTransferSnapshot
185-
signature: str = Field(min_length=1, max_length=2048)
186-
additional_signatures: dict[str, str] = Field(default_factory=dict)
187-
188-
189-
class TransferAcceptanceBody(BaseModel):
190-
model_config = ConfigDict(extra="forbid")
191-
192-
acceptance: TargetTransferAcceptance
193-
signature: str = Field(min_length=1, max_length=2048)
194-
195-
196116
class ArtifactReserveBody(BaseModel):
197117
model_config = ConfigDict(extra="forbid")
198118

@@ -598,220 +518,6 @@ async def revoke_task_grant(request: Request) -> Response:
598518
core.grants.revoke(grant_id, command=parsed.command, authority=authority)
599519
return JSONResponse({"grant_id": grant_id, "revoked": True})
600520

601-
async def create_room(request: Request) -> Response:
602-
body, actor = await body_and_actor(request, core)
603-
parsed = RoomCreateBody.model_validate_json(body)
604-
core._require(
605-
actor=actor,
606-
action="room.create",
607-
resource="room:new",
608-
classification=parsed.classification,
609-
context={
610-
"classification": parsed.classification.value,
611-
"persistent": parsed.persistent,
612-
"expires_at": parsed.expires_at.isoformat() if parsed.expires_at else None,
613-
"policy_digest": canonical_digest(parsed.policy or {}),
614-
},
615-
)
616-
result = core.rooms.create(
617-
actor=actor,
618-
collaboration_scope_id=parsed.collaboration_scope_id,
619-
classification=parsed.classification,
620-
persistent=parsed.persistent,
621-
expires_at=parsed.expires_at,
622-
policy=parsed.policy,
623-
)
624-
return JSONResponse(result, status_code=201)
625-
626-
async def create_meeting(request: Request) -> Response:
627-
body, actor = await body_and_actor(request, core)
628-
parsed = MeetingCreateBody.model_validate_json(body)
629-
core._require(
630-
actor=actor,
631-
action="room.create",
632-
resource="room:new",
633-
classification=parsed.classification,
634-
context={
635-
"classification": parsed.classification.value,
636-
"persistent": False,
637-
"expires_at": parsed.expires_at.isoformat(),
638-
"policy_digest": canonical_digest(parsed.policy or {}),
639-
},
640-
)
641-
result = core.rooms.create(
642-
actor=actor,
643-
collaboration_scope_id=parsed.collaboration_scope_id,
644-
classification=parsed.classification,
645-
persistent=False,
646-
expires_at=parsed.expires_at,
647-
policy=parsed.policy,
648-
)
649-
return JSONResponse(result, status_code=201)
650-
651-
async def add_room_member(request: Request) -> Response:
652-
body, actor = await body_and_actor(request, core)
653-
parsed = RoomMemberBody.model_validate_json(body)
654-
room_id = request.path_params["room_id"]
655-
mls_key_package = (
656-
_decode_b64(parsed.mls_key_package_b64, field="mls_key_package_b64")
657-
if parsed.mls_key_package_b64 is not None
658-
else None
659-
)
660-
core._require(
661-
actor=actor,
662-
action="room.action",
663-
resource=room_id,
664-
context={
665-
"operation": "member.add",
666-
"harness_id": parsed.harness_id,
667-
"role": parsed.role,
668-
"mls_key_package_digest": (
669-
hashlib.sha256(mls_key_package).hexdigest()
670-
if mls_key_package is not None
671-
else None
672-
),
673-
},
674-
)
675-
result = core.rooms.add_member(
676-
actor=actor,
677-
collaboration_scope_id=parsed.collaboration_scope_id,
678-
room_id=room_id,
679-
harness_id=parsed.harness_id,
680-
role=parsed.role,
681-
mls_key_package=mls_key_package,
682-
)
683-
return JSONResponse(result, status_code=201)
684-
685-
async def remove_room_member(request: Request) -> Response:
686-
body, actor = await body_and_actor(request, core)
687-
parsed = RoomMemberRemoveBody.model_validate_json(body)
688-
room_id = request.path_params["room_id"]
689-
core._require(
690-
actor=actor,
691-
action="room.action",
692-
resource=room_id,
693-
context={"operation": "member.remove", "harness_id": parsed.harness_id},
694-
)
695-
return JSONResponse(
696-
core.rooms.remove_member(
697-
actor=actor,
698-
collaboration_scope_id=parsed.collaboration_scope_id,
699-
room_id=room_id,
700-
harness_id=parsed.harness_id,
701-
)
702-
)
703-
704-
async def describe_room(request: Request) -> Response:
705-
body, actor = await body_and_actor(request, core)
706-
parsed = RoomDescribeBody.model_validate_json(body)
707-
room_id = request.path_params["room_id"]
708-
core._require(actor=actor, action="room.read", resource=room_id)
709-
return JSONResponse(
710-
core.rooms.describe(
711-
actor=actor,
712-
collaboration_scope_id=parsed.collaboration_scope_id,
713-
room_id=room_id,
714-
)
715-
)
716-
717-
async def send_room_message(request: Request) -> Response:
718-
body, actor = await body_and_actor(request, core)
719-
parsed = RoomSendBody.model_validate_json(body)
720-
room_id = request.path_params["room_id"]
721-
core._require(
722-
actor=actor,
723-
action="room.action",
724-
resource=room_id,
725-
classification=parsed.classification,
726-
context={
727-
"operation": "message.send",
728-
"recipient_harness_ids": sorted(parsed.recipients),
729-
"payload_digest": canonical_digest(parsed.payload),
730-
"expected_control_sequence": parsed.expected_control_sequence,
731-
},
732-
)
733-
result = core.send_message(
734-
actor=actor,
735-
collaboration_scope_id=parsed.collaboration_scope_id,
736-
recipients=parsed.recipients,
737-
payload=parsed.payload,
738-
idempotency_key=parsed.idempotency_key,
739-
classification=parsed.classification,
740-
released_artifacts=parsed.released_artifacts,
741-
conversation_id=parsed.conversation_id,
742-
room_id=room_id,
743-
expected_room_control_sequence=parsed.expected_control_sequence,
744-
)
745-
return JSONResponse(result, status_code=202)
746-
747-
async def propose_room_transfer(request: Request) -> Response:
748-
body, actor = await body_and_actor(request, core)
749-
parsed = TransferProposalBody.model_validate_json(body)
750-
room_id = request.path_params["room_id"]
751-
if parsed.proposal.room_id != room_id or parsed.snapshot.room_id != room_id:
752-
raise AuthorizationError("room transfer path binding mismatch")
753-
core.outage.require_privileged()
754-
core._require(
755-
actor=actor,
756-
action="room.transfer.propose",
757-
resource=room_id,
758-
context={"proposal_digest": parsed.proposal.digest, "snapshot_digest": parsed.snapshot.digest},
759-
)
760-
return JSONResponse(
761-
core.room_governance.propose_transfer(
762-
actor=actor,
763-
proposal=parsed.proposal,
764-
snapshot=parsed.snapshot,
765-
signature=parsed.signature,
766-
additional_signatures=parsed.additional_signatures,
767-
),
768-
status_code=202,
769-
)
770-
771-
async def accept_room_transfer(request: Request) -> Response:
772-
body, actor = await body_and_actor(request, core)
773-
parsed = TransferAcceptanceBody.model_validate_json(body)
774-
transfer_id = request.path_params["transfer_id"]
775-
if parsed.acceptance.transfer_id != transfer_id:
776-
raise AuthorizationError("room transfer path binding mismatch")
777-
core.outage.require_privileged()
778-
core._require(
779-
actor=actor,
780-
action="room.transfer.accept",
781-
resource=f"room-transfer:{transfer_id}",
782-
context={"acceptance_digest": parsed.acceptance.digest},
783-
)
784-
return JSONResponse(
785-
core.room_governance.accept_target(
786-
actor=actor,
787-
acceptance=parsed.acceptance,
788-
signature=parsed.signature,
789-
)
790-
)
791-
792-
async def commit_room_transfer(request: Request) -> Response:
793-
_body, actor = await body_and_actor(request, core)
794-
transfer_id = request.path_params["transfer_id"]
795-
with core.store.transaction(immediate=False) as connection:
796-
transfer = connection.execute(
797-
"SELECT target_domain_id,target_credential_id,state FROM room_transfers WHERE transfer_id=?",
798-
(transfer_id,),
799-
).fetchone()
800-
if (
801-
transfer is None
802-
or transfer["state"] != "target_accepted"
803-
or transfer["target_domain_id"] != actor.domain_id
804-
or transfer["target_credential_id"] != actor.credential_id
805-
):
806-
raise AuthorizationError("room transfer is not visible")
807-
core.outage.require_privileged()
808-
core._require(
809-
actor=actor,
810-
action="room.transfer.commit",
811-
resource=f"room-transfer:{transfer_id}",
812-
)
813-
return JSONResponse(core.room_governance.commit(transfer_id))
814-
815521
async def reserve_artifact(request: Request) -> Response:
816522
core.artifacts.require_enabled()
817523
body, actor = await body_and_actor(request, core)
@@ -1339,15 +1045,9 @@ async def replay_version_events(request: Request) -> Response:
13391045
Route("/v1/task-grants", issue_task_grant, methods=["POST"]),
13401046
Route("/v1/task-grants/{grant_id}", get_task_grant, methods=["GET"]),
13411047
Route("/v1/task-grants/{grant_id}/revoke", revoke_task_grant, methods=["POST"]),
1342-
Route("/v1/rooms", create_room, methods=["POST"]),
1343-
Route("/v1/meetings", create_meeting, methods=["POST"]),
1344-
Route("/v1/rooms/{room_id}", describe_room, methods=["POST"]),
1345-
Route("/v1/rooms/{room_id}/members", add_room_member, methods=["POST"]),
1346-
Route("/v1/rooms/{room_id}/members/remove", remove_room_member, methods=["POST"]),
1347-
Route("/v1/rooms/{room_id}/messages", send_room_message, methods=["POST"]),
1348-
Route("/v1/rooms/{room_id}/transfers", propose_room_transfer, methods=["POST"]),
1349-
Route("/v1/room-transfers/{transfer_id}/accept", accept_room_transfer, methods=["POST"]),
1350-
Route("/v1/room-transfers/{transfer_id}/commit", commit_room_transfer, methods=["POST"]),
1048+
]
1049+
routes.extend(create_room_routes(core, body_and_actor, _decode_b64))
1050+
routes += [
13511051
Route("/v1/artifacts/reservations", reserve_artifact, methods=["POST"]),
13521052
Route("/v1/artifacts/reservations/{reservation_id}/bytes", upload_artifact, methods=["POST"]),
13531053
Route("/v1/artifacts/reservations/{reservation_id}/abort", abort_artifact_reservation, methods=["POST"]),

0 commit comments

Comments
 (0)