v0.3.1: bearer-token reads; public skill + capabilities
Fixes an inconsistency an agent hit: it could POST/PATCH/DELETE with its token
but GET (readback) required a session and 302-redirected to the browser login.
- GET /api/artifacts and GET /api/artifacts/{id} now accept a bearer token OR
a session, owner-scoped (a token reads only its owner's artifacts). Agents
can read back what they posted with the same token.
- Unauthenticated API requests return a JSON 403, never a browser-login
redirect (resolve_request_user helper; dropped @login_required on the JSON
read views).
- GET /api/skill and GET /api/capabilities are now public (no user data), so
an agent can fetch the contract + posting instructions before it has a key.
- Docs (api.md, CLAUDE.md) updated; tests for bearer readback, owner-scoping,
and the JSON-403-not-redirect behavior. Suite: 132 passing; verified live.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
d30e2436ab
commit
2b901f7844
6 changed files with 91 additions and 32 deletions
|
|
@ -58,9 +58,10 @@ class ArtifactAPITestCase(APITestCase):
|
|||
|
||||
# -- capabilities -------------------------------------------------
|
||||
|
||||
def test_capabilities_requires_auth(self):
|
||||
def test_capabilities_is_public(self):
|
||||
# Capability/contract info only, no user data — public, no auth needed.
|
||||
response = self.client.get(reverse("capabilities-api"))
|
||||
self.assertEqual(response.status_code, 403)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_capabilities_with_token(self):
|
||||
response = self.client.get(reverse("capabilities-api"), **self.auth_headers(self.token))
|
||||
|
|
@ -85,9 +86,10 @@ class ArtifactAPITestCase(APITestCase):
|
|||
|
||||
# -- skill ----------------------------------------------------------
|
||||
|
||||
def test_skill_requires_auth(self):
|
||||
def test_skill_is_public(self):
|
||||
# Bootstrapping instructions, no secrets — public, no auth needed.
|
||||
response = self.client.get(reverse("skill-api"))
|
||||
self.assertEqual(response.status_code, 403)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_skill_returns_markdown_with_origin_filled_in(self):
|
||||
response = self.client.get(reverse("skill-api"), **self.auth_headers(self.token))
|
||||
|
|
@ -156,9 +158,10 @@ class ArtifactAPITestCase(APITestCase):
|
|||
|
||||
# -- list (GET) -------------------------------------------------------
|
||||
|
||||
def test_list_requires_session_login(self):
|
||||
def test_list_requires_auth(self):
|
||||
# No session and no token -> JSON 403, never a browser-login redirect.
|
||||
response = self.client.get(reverse("artifact-api"))
|
||||
self.assertEqual(response.status_code, 302) # redirected to login
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
def test_list_scoped_to_owner(self):
|
||||
self.create_artifact()
|
||||
|
|
@ -169,6 +172,15 @@ class ArtifactAPITestCase(APITestCase):
|
|||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual([a["title"] for a in response.data], ["Hello World"])
|
||||
|
||||
def test_list_readable_with_bearer_token(self):
|
||||
# Agents read back with the same token they post with; owner-scoped.
|
||||
self.create_artifact()
|
||||
self.create_artifact(token=self.other_token, title="Not mine")
|
||||
|
||||
response = self.client.get(reverse("artifact-api"), **self.auth_headers(self.token))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual([a["title"] for a in response.data], ["Hello World"])
|
||||
|
||||
def test_list_filter_by_label(self):
|
||||
self.create_artifact(tags=["urgent"])
|
||||
self.create_artifact(title="Second", tags=["later"])
|
||||
|
|
@ -220,6 +232,27 @@ class ArtifactAPITestCase(APITestCase):
|
|||
response = self.client.get(reverse("artifact-detail-api", args=[artifact_id]))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_detail_readable_with_bearer_token(self):
|
||||
# The api_url returned by POST is fetchable with the posting token.
|
||||
artifact_id = self.create_artifact().data["id"]
|
||||
|
||||
response = self.client.get(
|
||||
reverse("artifact-detail-api", args=[artifact_id]), **self.auth_headers(self.token)
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.data["id"], artifact_id)
|
||||
|
||||
# ...but only the owner's token; another user's token gets 404.
|
||||
response = self.client.get(
|
||||
reverse("artifact-detail-api", args=[artifact_id]), **self.auth_headers(self.other_token)
|
||||
)
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
def test_detail_requires_auth(self):
|
||||
artifact_id = self.create_artifact().data["id"]
|
||||
response = self.client.get(reverse("artifact-detail-api", args=[artifact_id]))
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
def test_patch_updates_title_and_tags(self):
|
||||
artifact_id = self.create_artifact().data["id"]
|
||||
response = self.client.patch(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue