Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
* [#752](https://github.com/workos/workos-python/pull/752) fix(generated): regenerate from spec

**Features**
* **[agents](https://workos.com/docs/reference/agents)**:
* Added `revoked_at` to `AgentRegistration`
* Added endpoint `POST /agents/registrations/{id}/revoke`
2 changes: 1 addition & 1 deletion .last-synced-sha
Original file line number Diff line number Diff line change
@@ -1 +1 @@
d430b394483f6e00ff7f9e2ca8d4340ff8f7aa54
4ecccdfeab4244ffaf428c96b1615bf0440b4c0f
6 changes: 6 additions & 0 deletions .oagen-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,7 @@
"src/workos/common/models/flag_updated_context_actor_source.py",
"src/workos/common/models/flag_updated_context_previous_attribute.py",
"src/workos/common/models/flag_updated_context_previous_attribute_data.py",
"src/workos/common/models/flag_updated_context_previous_attribute_data_owner.py",
"src/workos/common/models/flag_updated_data.py",
"src/workos/common/models/flag_updated_data_owner.py",
"src/workos/common/models/generate_link_intent.py",
Expand Down Expand Up @@ -1302,6 +1303,7 @@
"tests/fixtures/flag_updated_context_actor.json",
"tests/fixtures/flag_updated_context_previous_attribute.json",
"tests/fixtures/flag_updated_context_previous_attribute_data.json",
"tests/fixtures/flag_updated_context_previous_attribute_data_owner.json",
"tests/fixtures/flag_updated_data.json",
"tests/fixtures/flag_updated_data_owner.json",
"tests/fixtures/generate_link.json",
Expand Down Expand Up @@ -1696,6 +1698,10 @@
"sdkMethod": "get_registration",
"service": "agents"
},
"POST /agents/registrations/{id}/revoke": {
"sdkMethod": "revoke_registration",
"service": "agents"
},
"GET /agents/instances": {
"sdkMethod": "list_instances",
"service": "agents"
Expand Down
4 changes: 2 additions & 2 deletions src/workos/admin_portal/_resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ def revoke_link(
external_user_id: str,
request_options: RequestOptions | None = None,
) -> None:
"""Revoke Portal Links
"""Revoke a Portal Link

Revokes every Portal Link generated for the given organization with the given `external_user_id`, and immediately ends any Admin Portal sessions that were started from those links. Use this when an admin is deprovisioned from your application: unopened links stop working, and their already-open portal sessions are ended. Only links that were generated with an `external_user_id` can be revoked, so pass one to `generate_link` for every admin you may later need to deprovision.

Expand Down Expand Up @@ -193,7 +193,7 @@ async def revoke_link(
external_user_id: str,
request_options: RequestOptions | None = None,
) -> None:
"""Revoke Portal Links
"""Revoke a Portal Link

Revokes every Portal Link generated for the given organization with the given `external_user_id`, and immediately ends any Admin Portal sessions that were started from those links. Use this when an admin is deprovisioned from your application: unopened links stop working, and their already-open portal sessions are ended. Only links that were generated with an `external_user_id` can be revoked, so pass one to `generate_link` for every admin you may later need to deprovision.

Expand Down
60 changes: 60 additions & 0 deletions src/workos/agents/_resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -493,6 +493,36 @@ def get_registration(
request_options=request_options,
)

def revoke_registration(
self,
id: str,
*,
request_options: RequestOptions | None = None,
) -> AgentRegistration:
"""Revoke an agent registration

Revoke an agent registration by ID. Revocation is permanent: the registration, its identity assertions, and every access token or API key issued from it stop working, and the agent must register again. Revoking an already revoked registration succeeds and returns the original `revoked_at`. The registration is scoped to the environment of the API key used to authenticate the request.

Args:
id: The unique ID of the agent registration.
request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override.

Returns:
AgentRegistration

Raises:
NotFoundError: If the resource is not found (404).
AuthenticationError: If the API key is invalid (401).
RateLimitExceededError: If rate limited (429).
ServerError: If the server returns a 5xx error.
"""
return self._client.request(
method="post",
path=("agents", "registrations", str(id), "revoke"),
model=AgentRegistration,
request_options=request_options,
)

def list_instances(
self,
*,
Expand Down Expand Up @@ -1175,6 +1205,36 @@ async def get_registration(
request_options=request_options,
)

async def revoke_registration(
self,
id: str,
*,
request_options: RequestOptions | None = None,
) -> AgentRegistration:
"""Revoke an agent registration

Revoke an agent registration by ID. Revocation is permanent: the registration, its identity assertions, and every access token or API key issued from it stop working, and the agent must register again. Revoking an already revoked registration succeeds and returns the original `revoked_at`. The registration is scoped to the environment of the API key used to authenticate the request.

Args:
id: The unique ID of the agent registration.
request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override.

Returns:
AgentRegistration

Raises:
NotFoundError: If the resource is not found (404).
AuthenticationError: If the API key is invalid (401).
RateLimitExceededError: If rate limited (429).
ServerError: If the server returns a 5xx error.
"""
return await self._client.request(
method="post",
path=("agents", "registrations", str(id), "revoke"),
model=AgentRegistration,
request_options=request_options,
)

async def list_instances(
self,
*,
Expand Down
1 change: 1 addition & 0 deletions src/workos/agents/models/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# This file is auto-generated by oagen. Do not edit.

from workos.common.models.list_metadata import ListMetadata as ListMetadata
from workos.common.models.pagination_order import PaginationOrder as PaginationOrder

from .agent_admin_link_claim_attempt_to_external_user_request import (
Expand Down
9 changes: 9 additions & 0 deletions src/workos/agents/models/agent_registration.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ class AgentRegistration:
"""Identifier of the organization the agent is registered to."""
status: AgentRegistrationStatus
"""The current verification status of the registration."""
revoked_at: datetime | None

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Existing registration constructors fail

Adding revoked_at without a default breaks existing AgentRegistration(...) calls. A caller supplying every previously required argument now gets TypeError because revoked_at is missing. Make the new field keyword-only with a default of None, or append it with that default, to preserve existing constructor calls.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/workos/agents/models/agent_registration.py
Line: 30

Comment:
**Existing registration constructors fail**

Adding `revoked_at` without a default breaks existing `AgentRegistration(...)` calls. A caller supplying every previously required argument now gets `TypeError` because `revoked_at` is missing. Make the new field keyword-only with a default of `None`, or append it with that default, to preserve existing constructor calls.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

"""The timestamp when the registration was revoked, or `null` if it has not been revoked."""
kind: AgentRegistrationKind
"""The kind of agent registration."""
claim: AgentRegistrationClaim | None
Expand All @@ -47,6 +49,9 @@ def from_dict(cls, data: dict[str, Any]) -> AgentRegistration:
),
organization_id=data["organization_id"],
status=AgentRegistrationStatus(data["status"]),
revoked_at=_parse_datetime(_v_revoked_at)
if (_v_revoked_at := data["revoked_at"]) is not None
else None,
kind=AgentRegistrationKind(data["kind"]),
claim=AgentRegistrationClaim.from_dict(cast(dict[str, Any], _v_claim))
if (_v_claim := data["claim"]) is not None
Expand All @@ -66,6 +71,10 @@ def to_dict(self) -> dict[str, Any]:
result["status"] = (
self.status.value if isinstance(self.status, Enum) else self.status
)
if self.revoked_at is not None:
result["revoked_at"] = _format_datetime(self.revoked_at)
else:
result["revoked_at"] = None
result["kind"] = self.kind.value if isinstance(self.kind, Enum) else self.kind
if self.claim is not None:
result["claim"] = self.claim.to_dict()
Expand Down
1 change: 0 additions & 1 deletion src/workos/authorization/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
from workos.common.models.authorization_permission import (
AuthorizationPermission as AuthorizationPermission,
)
from workos.common.models.list_metadata import ListMetadata as ListMetadata
from workos.common.models.slim_role import SlimRole as SlimRole

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Existing authorization imports fail

Removing ListMetadata breaks existing imports from workos.authorization.models. It also removes the name from workos.authorization, which forwards these exports. Applications using either import now fail with ImportError. Keep the existing export alongside the new export in agents.

Suggested change
from workos.common.models.slim_role import SlimRole as SlimRole
from workos.common.models.list_metadata import ListMetadata as ListMetadata
from workos.common.models.slim_role import SlimRole as SlimRole
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/workos/authorization/models/__init__.py
Line: 9

Comment:
**Existing authorization imports fail**

Removing `ListMetadata` breaks existing imports from `workos.authorization.models`. It also removes the name from `workos.authorization`, which forwards these exports. Applications using either import now fail with `ImportError`. Keep the existing export alongside the new export in `agents`.

```suggestion
from workos.common.models.list_metadata import ListMetadata as ListMetadata
from workos.common.models.slim_role import SlimRole as SlimRole
```

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.


from .assign_role import AssignRole as AssignRole
Expand Down
3 changes: 3 additions & 0 deletions src/workos/common/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -470,6 +470,9 @@
from .models import (
FlagUpdatedContextPreviousAttributeData as FlagUpdatedContextPreviousAttributeData,
)
from .models import (
FlagUpdatedContextPreviousAttributeDataOwner as FlagUpdatedContextPreviousAttributeDataOwner,
)
from .models import FlagUpdatedData as FlagUpdatedData
from .models import FlagUpdatedDataOwner as FlagUpdatedDataOwner
from .models import GenerateLinkIntent as GenerateLinkIntent
Expand Down
3 changes: 3 additions & 0 deletions src/workos/common/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -676,6 +676,9 @@
from .flag_updated_context_previous_attribute_data import (
FlagUpdatedContextPreviousAttributeData as FlagUpdatedContextPreviousAttributeData,
)
from .flag_updated_context_previous_attribute_data_owner import (
FlagUpdatedContextPreviousAttributeDataOwner as FlagUpdatedContextPreviousAttributeDataOwner,
)
from .flag_updated_data import FlagUpdatedData as FlagUpdatedData
from .flag_updated_data_owner import FlagUpdatedDataOwner as FlagUpdatedDataOwner
from .generate_link_intent import GenerateLinkIntent as GenerateLinkIntent
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,14 @@
from __future__ import annotations

from dataclasses import dataclass
from typing import Any
from typing import Any, cast

from workos._types import _raise_deserialize_error

from .flag_updated_context_previous_attribute_data_owner import (
FlagUpdatedContextPreviousAttributeDataOwner,
)


@dataclass(slots=True)
class FlagUpdatedContextPreviousAttributeData:
Expand All @@ -18,6 +22,8 @@ class FlagUpdatedContextPreviousAttributeData:
"""The previous description of the flag."""
tags: list[str] | None = None
"""The previous tags of the flag."""
owner: FlagUpdatedContextPreviousAttributeDataOwner | None = None
"""The previous owner of the flag, or `null` if it had none."""
enabled: bool | None = None
"""Whether the flag was previously enabled."""
default_value: bool | None = None
Expand All @@ -31,6 +37,11 @@ def from_dict(cls, data: dict[str, Any]) -> FlagUpdatedContextPreviousAttributeD
name=data.get("name"),
description=data.get("description"),
tags=data.get("tags"),
owner=FlagUpdatedContextPreviousAttributeDataOwner.from_dict(
cast(dict[str, Any], _v_owner)
)
if (_v_owner := data.get("owner")) is not None
else None,
enabled=data.get("enabled"),
default_value=data.get("default_value"),
)
Expand All @@ -48,6 +59,10 @@ def to_dict(self) -> dict[str, Any]:
result["description"] = None
if self.tags is not None:
result["tags"] = self.tags
if self.owner is not None:
result["owner"] = self.owner.to_dict()
else:
result["owner"] = None
Comment on lines +62 to +65

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Unchanged owners become null

Serializing previous flag attributes now adds "owner": null even when the input omitted owner. These attributes describe fields that changed, so an update that only changed enabled should not gain a previous-owner entry. Callers that serialize parsed events receive a value the event never supplied. Preserve the difference between an omitted owner and an explicit null, and test a round trip where owner is absent.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/workos/common/models/flag_updated_context_previous_attribute_data.py
Line: 62-65

Comment:
**Unchanged owners become null**

Serializing previous flag attributes now adds `"owner": null` even when the input omitted `owner`. These attributes describe fields that changed, so an update that only changed `enabled` should not gain a previous-owner entry. Callers that serialize parsed events receive a value the event never supplied. Preserve the difference between an omitted `owner` and an explicit null, and test a round trip where `owner` is absent.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

if self.enabled is not None:
result["enabled"] = self.enabled
if self.default_value is not None:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# This file is auto-generated by oagen. Do not edit.

from typing import TypeAlias

from .feature_flag_owner import FeatureFlagOwner

FlagUpdatedContextPreviousAttributeDataOwner: TypeAlias = FeatureFlagOwner
1 change: 1 addition & 0 deletions tests/fixtures/agent_registration.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
},
"organization_id": "org_01EHQMYV6MBK39QC5PZXHY59C3",
"status": "verified",
"revoked_at": null,
"kind": "service_auth",
"claim": {
"id": "agent_reg_claim_01EHWNCE74X7JSDV0X3SZ3KJNY",
Expand Down
5 changes: 5 additions & 0 deletions tests/fixtures/flag_updated.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@
"beta",
"release"
],
"owner": {
"email": "jane@example.com",
"first_name": "Jane",
"last_name": "Doe"
},
"enabled": true,
"default_value": false
}
Expand Down
5 changes: 5 additions & 0 deletions tests/fixtures/flag_updated_context.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@
"beta",
"release"
],
"owner": {
"email": "jane@example.com",
"first_name": "Jane",
"last_name": "Doe"
},
"enabled": true,
"default_value": false
}
Expand Down
5 changes: 5 additions & 0 deletions tests/fixtures/flag_updated_context_previous_attribute.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@
"beta",
"release"
],
"owner": {
"email": "jane@example.com",
"first_name": "Jane",
"last_name": "Doe"
},
"enabled": true,
"default_value": false
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@
"beta",
"release"
],
"owner": {
"email": "jane@example.com",
"first_name": "Jane",
"last_name": "Doe"
},
"enabled": true,
"default_value": false
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"email": "jane@example.com",
"first_name": "Jane",
"last_name": "Doe"
}
23 changes: 23 additions & 0 deletions tests/test_agents.py
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,18 @@ def test_get_registration(self, workos, httpx_mock):
assert request.method == "GET"
assert request.url.path.endswith("/agents/registrations/test_id")

def test_revoke_registration(self, workos, httpx_mock):
httpx_mock.add_response(
json=load_fixture("agent_registration.json"),
)
result = workos.agents.revoke_registration("test_id")
assert isinstance(result, AgentRegistration)
assert result.id == "agent_reg_01EHWNCE74X7JSDV0X3SZ3KJNY"
assert result.organization_id == "org_01EHQMYV6MBK39QC5PZXHY59C3"
Comment on lines +205 to +212

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Revoked timestamps go untested

Both new revoke tests return an unrevoked registration fixture, and neither checks revoked_at. The added model tests also use only null timestamps, so the new timestamp parsing and formatting paths are never exercised. Return a non-null timestamp in the revoke tests and check both the parsed value and its serialized form.

Prompt To Fix With AI
This is a comment left during a code review.
Path: tests/test_agents.py
Line: 205-212

Comment:
**Revoked timestamps go untested**

Both new revoke tests return an unrevoked registration fixture, and neither checks `revoked_at`. The added model tests also use only null timestamps, so the new timestamp parsing and formatting paths are never exercised. Return a non-null timestamp in the revoke tests and check both the parsed value and its serialized form.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

request = httpx_mock.get_request()
assert request.method == "POST"
assert request.url.path.endswith("/agents/registrations/test_id/revoke")

def test_list_instances(self, workos, httpx_mock):
httpx_mock.add_response(
json=load_fixture("list_agent_instance.json"),
Expand Down Expand Up @@ -546,6 +558,17 @@ async def test_get_registration(self, async_workos, httpx_mock):
assert request.method == "GET"
assert request.url.path.endswith("/agents/registrations/test_id")

@pytest.mark.asyncio
async def test_revoke_registration(self, async_workos, httpx_mock):
httpx_mock.add_response(json=load_fixture("agent_registration.json"))
result = await async_workos.agents.revoke_registration("test_id")
assert isinstance(result, AgentRegistration)
assert result.id == "agent_reg_01EHWNCE74X7JSDV0X3SZ3KJNY"
assert result.organization_id == "org_01EHQMYV6MBK39QC5PZXHY59C3"
request = httpx_mock.get_request()
assert request.method == "POST"
assert request.url.path.endswith("/agents/registrations/test_id/revoke")

@pytest.mark.asyncio
async def test_list_instances(self, async_workos, httpx_mock):
httpx_mock.add_response(json=load_fixture("list_agent_instance.json"))
Expand Down
Loading
Loading