Workspace members
A workspace is shared with other admins through membership rows. Each member holds exactly one role, and that role decides what every workspace route will let them do. These routes read the roster and change it.
All routes require the configured API-key header and trusted principal headers (X-User-Subject, plus the optional role/unit/level claims) exactly as the other principal-bearing routes do; see Conversations and memory. A caller who is not a member of the workspace gets 404 on every route below, the same answer an unknown workspace id gets.
Workspace roles
owner, partner, and viewer are workspace roles — a person's standing inside one workspace. They are not the same thing as a partner, the integrating organisation that uploads documents through Kafka. The same admin can be an owner of one workspace and a viewer of another, and belonging to the integrating organisation does not itself grant a workspace role.
The routes enforce roles with one shared check, WorkspaceAccess.require(...) in src/aceh_rag/api/workspaces.py:327, which answers 403 with {"detail": "This action requires one of: <roles>"}. The permissions below are read from those calls, not from product intent.
| Capability | owner | partner | viewer | Code check |
|---|---|---|---|---|
| Read the workspace, its sources, documents, artifacts, and roster | Yes | Yes | Yes | _access() only, no require: GET sources workspaces.py:584, documents :922, artifacts :977, artifact content :1004, members :1094 |
| Upload documents, replace a document | Yes | Yes | No | access.require(OWNER, PARTNER): upload :850, replace :884 |
| Save the source set, select or unselect one document | Yes | Yes | No | access.require(OWNER, PARTNER): save :625, select :683, unselect :717 |
| Delete a workspace upload | Yes | Yes | No | access.require(OWNER, PARTNER): :756 |
| Delete an artifact | Any | Only one they created | No | :1063, plus the partner's own-artifact check at :1064 |
| Rename or delete the workspace | Yes | No | No | access.require(OWNER): :501, :528 |
| Invite a member, change a member's role | Yes | No | No | access.require(OWNER): :1122 |
| Remove another member | Yes | No | No | access.require(OWNER): :1169 |
| Leave the workspace (remove yourself) | Transfer ownership first | Yes | Yes | :1168 — self-removal skips the owner check; the last-owner guard at :1174 returns 422 |
| Transfer ownership | Yes | No | No | access.require(OWNER): :1208 |
A viewer may read and remove themselves. Other writes require owner or partner and answer 403 with This action requires one of: owner, partner when refused; administrative writes require owner and answer 403 with This action requires one of: owner. A partner may delete only artifacts they created. An owner must transfer ownership before leaving.
A workspace always has exactly one owner. The repository refuses a second owner (WorkspaceOwnerExistsError) and refuses to leave a workspace ownerless (LastWorkspaceOwnerError); both surface as 422 below.
Member object
GET /workspaces/{workspace_id}/members and the two routes that return a member share one shape:
| Field | Type | Meaning |
|---|---|---|
admin_id | string (UUID) | The member's admin subject, as supplied in X-User-Subject. |
role | string | owner, partner, or viewer. |
invited_by | string or null | Subject of the admin who added them; null when the row carries no recorded inviter. |
GET /workspaces/{workspace_id}/members
Returns every member, owner first, then by invitation order. Any member, including a viewer, may call it: a viewer can already see every answer and document in the workspace, so hiding the roster protects nothing.
{
"members": [
{"admin_id": "0f2b0000-0000-4000-8000-000000000001", "role": "owner", "invited_by": null},
{"admin_id": "0f2b0000-0000-4000-8000-000000000002", "role": "partner", "invited_by": "0f2b0000-0000-4000-8000-000000000001"}
]
}
| Status | Detail | When |
|---|---|---|
200 | Roster | The caller is a member. |
401 | {"detail": "Invalid or missing API key"} | The API-key header is absent or wrong. |
401 | {"detail": "Authenticated principal required"} | Principal enforcement is on and identity is absent. |
401 | {"detail": "Invalid authenticated principal"} | Principal enforcement is on and identity resolution fails, or the resolved principal subject is not a UUID, regardless of principal enforcement. |
404 | {"detail": "Workspace not found"} | Unknown workspace id, or the caller is not a member. The two are deliberately indistinguishable. |
422 | Validation array (detail) with msg: "Value error, workspace_id must be a valid UUID" | workspace_id is not a UUID. |
POST /workspaces/{workspace_id}/members
Invites an admin, or changes an existing member's role — the same call. Inviting somebody already present is a role change rather than an error, so a client never has to know which verb applies before the user acts. owner only.
{"admin_id": "0f2b0000-0000-4000-8000-000000000002", "role": "partner"}
extra fields are refused with 422. admin_id must be a UUID and role must be one of the three enum values.
| Status | Detail | When |
|---|---|---|
200 | Member object | The invite or role change was applied. |
401 | {"detail": "Invalid or missing API key"} | The API-key header is absent or wrong. |
401 | {"detail": "Authenticated principal required"} | Principal enforcement is on and identity is absent. |
401 | {"detail": "Invalid authenticated principal"} | Principal enforcement is on and identity resolution fails, or the resolved principal subject is not a UUID, regardless of principal enforcement. |
403 | {"detail": "This action requires one of: owner"} | The caller is a partner or viewer. Nothing changes. |
404 | {"detail": "Workspace not found"} | Unknown workspace id, or the caller is not a member. |
422 | Validation array | Non-UUID workspace_id or admin_id, missing admin_id or role, unknown role, or a forbidden extra field. |
422 | {"detail": "This workspace already has an owner. Transfer ownership instead of inviting a second one."} | The request tries to make somebody else an owner. Use PUT …/owner. |
422 | {"detail": "A workspace must always have an owner."} | The request demotes the only owner. |
Changing a role is not a way to hand over the workspace: the owner slot moves only through the transfer route.
DELETE /workspaces/{workspace_id}/members/{admin_id}
Removes a member. An owner removes anyone; any member may remove themselves, so a viewer who no longer wants the workspace in their list does not need the owner. Success returns 204 with no body.
| Status | Detail | When |
|---|---|---|
204 | Empty body | The member row was deleted. |
401 | {"detail": "Invalid or missing API key"} | The API-key header is absent or wrong. |
401 | {"detail": "Authenticated principal required"} | Principal enforcement is on and identity is absent. |
401 | {"detail": "Invalid authenticated principal"} | Principal enforcement is on and identity resolution fails, or the resolved principal subject is not a UUID, regardless of principal enforcement. |
403 | {"detail": "This action requires one of: owner"} | The caller is not the owner and admin_id is not the caller's own subject. Nothing changes. |
404 | {"detail": "Workspace not found"} | Unknown workspace id, or the caller is not a member. |
404 | {"detail": "Member not found"} | The caller is allowed to remove this id, but no such member exists here. Nothing changes. |
422 | Validation array | workspace_id or admin_id is not a UUID. |
422 | {"detail": "A workspace must always have an owner. Transfer ownership before leaving."} | The owner tries to remove themselves while they are the only owner. |
PUT /workspaces/{workspace_id}/owner
Hands the workspace to another admin, atomically. PUT on a singular sub-resource, because a workspace has exactly one owner and this call names who it is. owner only.
{"admin_id": "0f2b0000-0000-4000-8000-000000000002"}
The outgoing owner becomes a partner: they keep working in the workspace they were running and lose only administration. The target becomes owner; if the target was not a member, the row is created, so a transfer can also be the first invite. The caller keeps access as a partner. extra fields are refused with 422, and admin_id must be a UUID.
| Status | Detail | When |
|---|---|---|
200 | Member object | The new owner, with role: "owner". |
401 | {"detail": "Invalid or missing API key"} | The API-key header is absent or wrong. |
401 | {"detail": "Authenticated principal required"} | Principal enforcement is on and identity is absent. |
401 | {"detail": "Invalid authenticated principal"} | Principal enforcement is on and identity resolution fails, or the resolved principal subject is not a UUID, regardless of principal enforcement. |
403 | {"detail": "This action requires one of: owner"} | The caller is a partner or viewer. Nothing changes. |
404 | {"detail": "Workspace not found"} | Unknown workspace id, or the caller is not a member. |
422 | Validation array | Non-UUID workspace_id or admin_id, missing admin_id, or a forbidden extra field. |
Transferring to the current owner is a no-op that returns their row. Ownership is never cleared: transferring demotes the outgoing owner rather than leaving the workspace ownerless.