Skip to content

Commit bde92d9

Browse files
committed
Conformance + reference operators: 201 Created for resource creation
POST /sessions and POST /sessions/{id}/messages both create new resources identifiable by `session_id` / `message_id`, which is the textbook 201 Created case per RFC 9110 §15.3.2. The conformance suite was previously asserting 200 — that was forcing every operator to be HTTP-incorrect to pass conformance. Pin to 201 instead. - tests/conformance/test_sessions.py + test_messages.py: status_code expectations bumped from 200 to 201 for the create endpoints. Lifecycle verbs (join, invite, leave, end, reopen) keep 200. - cli/src/server/protocol/sessions.ts (TypeScript reference operator): returns 201 for both create endpoints. - examples/local-operator/asp_operator/app.py (Python reference operator): adds `status_code=201` to the matching `@app.post` decorators. - cli/tests/sessions-routes.test.ts: assertions updated. All three reference operators (TypeScript example, Python example, in-tree robotnet-cli operator) now pass the updated 29/29 conformance suite. Idempotent-replay returns 201 on both calls (first creates, replay returns the original 201 response) — Stripe- style 201/200 split could be added later if useful but the simpler "201 every time" is sufficient.
1 parent e498087 commit bde92d9

5 files changed

Lines changed: 37 additions & 19 deletions

File tree

cli/src/server/protocol/sessions.ts

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -64,12 +64,18 @@ export function buildSessionRoutes(
6464
...(parsed.end_after_send === true ? { endAfterSend: true } : {}),
6565
});
6666

67-
return c.json({
68-
session_id: result.session.id,
69-
...(result.messageSequence !== undefined
70-
? { sequence: result.messageSequence }
71-
: {}),
72-
});
67+
// 201 Created per RFC 9110 §15.3.2: a new session resource is identified
68+
// by `session_id`. Lifecycle verbs (join, invite, leave, end, reopen)
69+
// mutate state without creating a top-level resource and stay 200.
70+
return c.json(
71+
{
72+
session_id: result.session.id,
73+
...(result.messageSequence !== undefined
74+
? { sequence: result.messageSequence }
75+
: {}),
76+
},
77+
201,
78+
);
7379
});
7480

7581
// ── GET / — list sessions for the calling agent ───────────────────────────
@@ -154,7 +160,12 @@ export function buildSessionRoutes(
154160
} catch (err) {
155161
return mapSessionError(c, err);
156162
}
157-
return c.json({ message_id: result.messageId, sequence: result.sequence });
163+
// 201 Created per RFC 9110 §15.3.2: a new message resource is identified
164+
// by `message_id`.
165+
return c.json(
166+
{ message_id: result.messageId, sequence: result.sequence },
167+
201,
168+
);
158169
});
159170

160171
// ── POST /:id/leave ───────────────────────────────────────────────────────

cli/tests/sessions-routes.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ describe("POST /sessions", () => {
7474
headers: agentHeaders(agent.token),
7575
body: JSON.stringify({}),
7676
});
77-
assert.equal(res.status, 200);
77+
assert.equal(res.status, 201);
7878
const body = (await json(res)) as { session_id: string };
7979
assert.ok(body.session_id.startsWith("sess_"));
8080
});
@@ -87,7 +87,7 @@ describe("POST /sessions", () => {
8787
headers: agentHeaders(alice.token),
8888
body: JSON.stringify({ invite: ["@bob.bot"] }),
8989
});
90-
assert.equal(res.status, 200);
90+
assert.equal(res.status, 201);
9191
const body = (await json(res)) as { session_id: string };
9292
const sess = s.sessionStore.get(body.session_id)!;
9393
const bob = sess.participants.find((p) => p.handle === "@bob.bot");
@@ -128,7 +128,7 @@ describe("POST /sessions", () => {
128128
headers: agentHeaders(alice.token),
129129
body: JSON.stringify({ invite: ["@bob.bot", "@ghost.bot"] }),
130130
});
131-
assert.equal(res.status, 200);
131+
assert.equal(res.status, 201);
132132
const body = (await json(res)) as { session_id: string };
133133
const sess = s.sessionStore.get(body.session_id)!;
134134
assert.equal(sess.participants.length, 2); // alice + bob
@@ -252,7 +252,7 @@ describe("POST /sessions/:id/messages", () => {
252252
headers: agentHeaders(alice.token),
253253
body: JSON.stringify({ content: "hello!" }),
254254
});
255-
assert.equal(res.status, 200);
255+
assert.equal(res.status, 201);
256256
const body = (await json(res)) as { message_id: string; sequence: number };
257257
assert.ok(body.message_id.startsWith("msg_"));
258258
assert.equal(typeof body.sequence, "number");

examples/local-operator/asp_operator/app.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,10 @@ def auth_handle(request: Request) -> str:
8181

8282
# ---- Sessions ------------------------------------------------------
8383

84-
@app.post("/sessions")
84+
# 201 Created per RFC 9110 §15.3.2: a new session resource is identified
85+
# by `session_id`. Lifecycle verbs (join, invite, leave, end, reopen)
86+
# mutate state without creating a top-level resource and stay 200.
87+
@app.post("/sessions", status_code=201)
8588
async def post_sessions(body: CreateSessionBody, request: Request):
8689
creator = auth_handle(request)
8790
if body.end_after_send and body.initial_message is None:
@@ -130,7 +133,9 @@ async def post_invite(session_id: str, body: InviteBody, request: Request):
130133
raise HTTPException(status_code=404, detail="not found")
131134
return {"invited": invited}
132135

133-
@app.post("/sessions/{session_id}/messages")
136+
# 201 Created per RFC 9110 §15.3.2: a new message resource is identified
137+
# by `message_id`.
138+
@app.post("/sessions/{session_id}/messages", status_code=201)
134139
async def post_message(session_id: str, body: SendMessageBody, request: Request):
135140
sender = auth_handle(request)
136141
try:

tests/conformance/test_messages.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ async def _join_session(alice, bob) -> str:
2222
async def test_send_message_returns_message_id_and_sequence(alice, bob):
2323
sid = await _join_session(alice, bob)
2424
resp = await alice.send_message(sid, "hello")
25-
assert resp.status_code == 200
25+
assert resp.status_code == 201
2626
body = resp.json()
2727
assert body["message_id"].startswith("msg_")
2828
assert isinstance(body["sequence"], int)
@@ -75,8 +75,10 @@ async def test_idempotency_key_dedupes_retries(alice, bob):
7575
r1 = await alice.send_message(sid, "duplicate", idempotency_key=key)
7676
r2 = await alice.send_message(sid, "duplicate", idempotency_key=key)
7777

78-
assert r1.status_code == 200
79-
assert r2.status_code == 200
78+
# Both calls return 201 (idempotent replay returns the original 201, not 200) —
79+
# spec accepts either, but the local operator currently returns 201 on both.
80+
assert r1.status_code == 201
81+
assert r2.status_code == 201
8082
assert r1.json()["message_id"] == r2.json()["message_id"]
8183
assert r1.json()["sequence"] == r2.json()["sequence"]
8284

@@ -95,7 +97,7 @@ async def test_multipart_content_is_supported(alice, bob):
9597
{"type": "data", "data": {"action": "review_complete", "doc_id": "abc"}},
9698
]
9799
resp = await alice.send_message(sid, parts)
98-
assert resp.status_code == 200
100+
assert resp.status_code == 201
99101

100102
event = await bob.expect_event("session.message", session_id=sid)
101103
received = event["payload"]["content"]

tests/conformance/test_sessions.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
async def test_create_session_returns_session_id(alice, bob):
1616
"""POST /sessions with one invitee returns a session_id (Appendix C.1)."""
1717
resp = await alice.create_session(invite=[bob.handle])
18-
assert resp.status_code == 200
18+
assert resp.status_code == 201
1919
body = resp.json()
2020
assert "session_id" in body
2121
assert body["session_id"].startswith("sess_")
@@ -24,7 +24,7 @@ async def test_create_session_returns_session_id(alice, bob):
2424
async def test_create_session_with_zero_invitees(alice):
2525
"""A session with zero invitees is permitted (Whitepaper §6.3)."""
2626
resp = await alice.create_session()
27-
assert resp.status_code == 200
27+
assert resp.status_code == 201
2828
assert "session_id" in resp.json()
2929

3030

0 commit comments

Comments
 (0)